This commit is contained in:
2026-07-13 01:38:13 +03:00
parent 9eda119527
commit 98c5a7a1d1
54 changed files with 860 additions and 454 deletions

View File

@@ -4,22 +4,81 @@ import 'scooter_info_item.dart';
class ScooterInfoSection extends StatelessWidget {
final double distance;
final String location;
const ScooterInfoSection({super.key, required this.distance, required this.location});
final int batteryLevel;
const ScooterInfoSection({
super.key,
required this.distance,
required this.location,
required this.batteryLevel,
});
@override
Widget build(BuildContext context) {
// 1. Расчет оставшегося расстояния (макс. 62 км)
final double remainingKm = (62 * batteryLevel) / 100;
// Красиво форматируем: если число целое — убираем точку (например, 31 км вместо 31.0 км)
final String kmText = remainingKm % 1 == 0
? '${remainingKm.toInt()} км'
: '${remainingKm.toStringAsFixed(1)} км';
// 2. Расчет оставшегося времени (макс. 2.5 часа = 150 минут)
final int totalMinutes = ((150 * batteryLevel) / 100).round();
final int hours = totalMinutes ~/ 60;
final int minutes = totalMinutes % 60;
// Формируем строку времени в зависимости от получившихся значений
String timeText = '';
if (hours > 0 && minutes > 0) {
timeText = '$hours ч $minutes мин';
} else if (hours > 0) {
timeText = '$hours ч';
} else {
timeText = '$minutes мин';
}
final int walkingMinutes = (distance / 80).round().clamp(1, 999);
final int walkHours = walkingMinutes ~/ 60;
final int walkMins = walkingMinutes % 60;
String walkingText = '';
if (walkHours > 0 && walkMins > 0) {
walkingText = '$walkHours ч $walkMins мин';
} else if (walkHours > 0) {
walkingText = '$walkHours ч';
} else {
walkingText = '$walkingMinutes мин';
}
return Column(
children: [
ScooterInfoItem(icon: 'assets/icons/bolt.png', text: '47 км или 4 часа 17 минут'),
ScooterInfoItem(icon: 'assets/icons/speed.png', text: 'max = 25 км/ч'),
ScooterInfoItem(icon: 'assets/icons/location.png', text: location),
// Динамическая строка с зарядом
ScooterInfoItem(
icon: 'assets/icons/bolt.png',
text: '$kmText или $timeText',
),
ScooterInfoItem(
icon: 'assets/icons/speed.png',
text: 'max = 25 км/ч',
),
ScooterInfoItem(
icon: 'assets/icons/location.png',
text: location,
),
Row(
children: [
ScooterInfoItem(icon: 'assets/icons/person.png', text: distance.toInt() >= 1000
? '${((distance.toInt()) / 1000).toStringAsFixed(1)} km'
: '${distance.toInt()} m',),
SizedBox(width: 16),
ScooterInfoItem(icon: 'assets/icons/time.png', text: '1 минута'),
ScooterInfoItem(
icon: 'assets/icons/person.png',
text: distance.toInt() >= 1000
? '${((distance.toInt()) / 1000).toStringAsFixed(1)} km'
: '${distance.toInt()} m',
),
const SizedBox(width: 16),
ScooterInfoItem(
icon: 'assets/icons/time.png',
text: walkingText,
),
],
),
],

View File

@@ -233,7 +233,7 @@ class _ScooterCard extends StatelessWidget {
GestureDetector(
onTap: () {
context.go('/home/scooter/${scooter.id}', extra: address);
context.go('/home/scooter/${scooter.id}', extra: [address, scooter.distance]);
},
child: Container(
height: 42,
@@ -245,7 +245,7 @@ class _ScooterCard extends StatelessWidget {
width: double.infinity,
fontSize: 12,
onTap: () {
context.go('/home/scooter/${scooter.id}', extra: address);
context.go('/home/scooter/${scooter.id}', extra: [address, scooter.distance]);
},
),
),

View File

@@ -218,6 +218,9 @@ class _RideCardState extends State<_RideCard> {
} else {
displayTime = _elapsedTime;
}
final isExpired = isReserved && (_reservationTime - _elapsedTime).isNegative;
final timeString = _formatDuration(displayTime);
final statusText = widget.order.status == 'Booking' ? 'Забронирован' : "Активный";
final statusColor = widget.order.status == 'Booking' ? Color(0xFFFFCC00) : Color(0xFF8bffaa);
@@ -347,7 +350,8 @@ class _RideCardState extends State<_RideCard> {
height: 32,
width: 100,
fontSize: 11,
onTap: () {
enabled: !isExpired,
onTap: !isExpired ? () {
if (isReserved) {
Navigator.pop(context);
showModalBottomSheet(
@@ -374,7 +378,7 @@ class _RideCardState extends State<_RideCard> {
),
);
}
},
} : null,
),
),
],

View File

@@ -174,7 +174,7 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
if (widget.showBalance) ...[
PaymentOption(
title: 'Баланс',
subtitle: '${state.balance.toStringAsFixed(2)} BYN',
subtitle: '${state.balance} баллов',
isSelected: _selectedPaymentMethod == -1,
onTap: () {
setState(() {
@@ -192,7 +192,7 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: PaymentOption(
title: card.isMain ? "Основная" : "",
title: card.isMain ? "Основная" : "Карта",
subtitle: '${card.cardLastNumber}',
isSelected: _selectedPaymentMethod == index,
onTap: () async {

View File

@@ -12,6 +12,7 @@ import '../../../domain/entities/payment_card.dart';
import '../../../domain/entities/scooter.dart';
import '../../../domain/entities/tariff.dart';
import '../../../domain/usecase/get_payment_cards_usecase.dart';
import '../../../domain/usecase/get_profile_usecase.dart';
import '../../../domain/usecase/set_main_payment_card_usecase.dart';
import '../../event/payment_method_sheet_event.dart';
import '../../event/tariff_sheet_event.dart';
@@ -40,6 +41,24 @@ class _TariffSheetState extends State<TariffSheet> {
@override
Widget build(BuildContext context) {
final double remainingKm = (62 * widget.scooter.batteryLevel) / 100;
final String kmText = remainingKm % 1 == 0
? '${remainingKm.toInt()} км'
: '${remainingKm.toStringAsFixed(1)} км';
final int totalMinutes = ((150 * widget.scooter.batteryLevel) / 100).round();
final int hours = totalMinutes ~/ 60;
final int minutes = totalMinutes % 60;
String timeText = '';
if (hours > 0 && minutes > 0) {
timeText = '$hours ч $minutes мин';
} else if (hours > 0) {
timeText = '$hours ч';
} else {
timeText = '$minutes мин';
}
return BlocConsumer<TariffSheetBloc, TariffSheetState>(
listener: (context, state) {
if (state.status == TariffSheetStatus.failure && state.errorMessage != null) {
@@ -135,7 +154,6 @@ class _TariffSheetState extends State<TariffSheet> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 🔹 HEADER
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
@@ -227,7 +245,7 @@ class _TariffSheetState extends State<TariffSheet> {
const SizedBox(width: 12),
Expanded(
child: Text(
'Заряда хватит на 4 часа 17 минут\nили 47 км',
'Заряда хватит на $kmText или $timeText',
style: const TextStyle(
color: Colors.white,
fontSize: 13,
@@ -303,6 +321,7 @@ class _TariffSheetState extends State<TariffSheet> {
create: (context) => PaymentMethodSheetBloc(
getIt<GetPaymentCardsUsecase>(),
getIt<SetMainPaymentCardUsecase>(),
getIt<GetProfileUseCase>(),
)..add(PaymentMethodSheetStarted()),
child: PaymentMethodSheet(
initialSelectedCard: state.useBalance ? null : state.selectedCard,
@@ -547,7 +566,7 @@ class _TariffCard extends StatelessWidget {
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => TariffInfoSheet(
builder: (_) => TariffInfoSheet(
tariff: tariff,
isInsurance: isInsurance,
onChanged: (val) {

View File

@@ -63,59 +63,81 @@ class SideMenu extends StatelessWidget {
child: SingleChildScrollView(
child: Column(
children: [
// Пункты меню
Column(
children: [
_MenuItem(
icon: 'assets/icons/person_icon.png',
title: 'Профиль',
onTap: () => {
context.go('/home/profile'),
onTap: () {
Navigator.pop(context);
context.go('/home/profile');
},
),
_MenuItem(
icon: 'assets/icons/list_star_icon.png',
title: 'История поездок',
onTap: () => context.go("/home/order-history"),
onTap: () {
Navigator.pop(context);
context.go("/home/order-history");
},
),
const Divider(color: Colors.white30, height: 1),
_MenuItem(
icon: 'assets/icons/creditcard_icon.png',
title: 'Способ оплаты',
onTap: () => context.go("/home/payment-methods"),
onTap: () {
Navigator.pop(context);
context.go("/home/payment-methods");
},
),
_MenuItem(
icon: 'assets/icons/promo_icon.png',
title: 'Промокоды',
onTap: () => context.go("/home/promo"),
onTap: () {
Navigator.pop(context);
context.go("/home/promo");
},
),
_MenuItem(
icon: 'assets/icons/plans_icon.png',
title: 'Абонементы',
onTap: () => context.push("/home/subscriptions"),
onTap: () {
Navigator.pop(context);
context.push("/home/subscriptions");
},
),
const Divider(color: Colors.white30, height: 1),
_MenuItem(
icon: 'assets/icons/magazine_icon.png',
title: 'Правила пользования самокатом',
onTap: () => context.go("/home/rules")
onTap: () {
Navigator.pop(context);
context.go("/home/rules");
}
),
_MenuItem(
icon: 'assets/icons/headphones_icon.png',
title: 'Техподдержка',
onTap: () => {
context.go('/home/support'),
onTap: () {
Navigator.pop(context);
context.go('/home/support');
},
),
_MenuItem(
icon: 'assets/icons/doc_icon.png',
title: 'Документы',
onTap: () => context.go("/home/documents"),
onTap: () {
Navigator.pop(context);
context.go("/home/documents");
},
),
_MenuItem(
icon: 'assets/icons/news_icon.png',
title: 'Новости',
onTap: () => context.go("/home/news"),
onTap: () {
Navigator.pop(context);
context.go("/home/news");
},
),
const Divider(color: Colors.white30, height: 1),
_MenuItem(