fix functional bugs

This commit is contained in:
2026-05-29 11:40:55 +03:00
parent 591265a7fc
commit 134ffdde60
50 changed files with 1086 additions and 771 deletions

View File

@@ -1,13 +1,16 @@
import 'dart:async';
import 'dart:ui';
import 'package:be_happy/domain/entities/scooter.dart';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../../di/service_locator.dart';
import '../../event/active_ride_event.dart';
import '../../event/map_event.dart';
import '../../state/active_ride_state.dart';
import '../../viewmodel/active_ride_bloc.dart';
import '../../viewmodel/map_bloc.dart';
import '../dialog/finish_ride_confirmation_dialog.dart';
import '../notification_toast.dart';
@@ -56,7 +59,7 @@ class _ActiveRideSheetState extends State<ActiveRideSheet> {
return BlocProvider.value(
value: _bloc,
child: BlocConsumer<ActiveRideBloc, ActiveRideState>(
listenWhen: (previous, current) => previous.inZone != current.inZone,
listenWhen: (previous, current) => previous.inZone != current.inZone || previous.status != current.status,
listener: (context, state) {
if (!state.inZone) {
BotToast.showCustomNotification(
@@ -71,6 +74,15 @@ class _ActiveRideSheetState extends State<ActiveRideSheet> {
},
);
}
if (state.status == ActiveRideStatus.success && state.order != null) {
final scooter = state.order!.scooter;
context.read<MapBloc>().add(FocusOnScooter(Scooter(id: scooter.id,
title: scooter.title, status: scooter.status,
latitude: state.longitude, longitude: state.latitude,
batteryLevel: scooter.batteryLevel, isOnline: scooter.isOnline,
maxSpeed: scooter.maxSpeed, number: scooter.number)));
}
},
builder: (context, state) {
// Логика отображения загрузки и ошибок остается прежней
@@ -203,8 +215,8 @@ class _ActiveRideSheetState extends State<ActiveRideSheet> {
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
fontFeatures: [FontFeature.tabularFigures()],
fontFamily: 'Digital Numbers',
fontFeatures: const [FontFeature.tabularFigures()],
fontFamily: 'DigitalNumbers',
),
),
const SizedBox(height: 4),
@@ -306,10 +318,27 @@ class _ActiveRideSheetState extends State<ActiveRideSheet> {
}
// 🔹 Если отменил — ничего не делаем, диалог уже закрылся
},
borderRadius: BorderRadius.circular(16),
child: Column( // ✅ Вернули Column с иконкой и текстом
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.stop,
color: Colors.white,
size: 24,
),
const SizedBox(height: 4),
const Text(
'ЗАВЕРШИТЬ',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
),

View File

@@ -219,11 +219,11 @@ class _RideCardState extends State<_RideCard> {
displayTime = _elapsedTime;
}
final timeString = _formatDuration(displayTime);
final statusText = _getStatusText(widget.order.status);
final statusColor = _getStatusColor(widget.order.status);
final statusText = widget.order.status == 'Booking' ? 'Забронирован' : "Активный";
final statusColor = widget.order.status == 'Booking' ? Color(0xFFFFCC00) : Color(0xFF8bffaa);
final scooterNumber =
widget.order.scooter?.number ?? widget.order.scooterId.toString();
widget.order.scooter.number ?? widget.order.scooterId.toString();
return Container(
padding: const EdgeInsets.all(16),
@@ -261,7 +261,7 @@ class _RideCardState extends State<_RideCard> {
Text(
statusText,
style: TextStyle(
color: Colors.white,
color: statusColor,
fontSize: 14,
fontWeight: FontWeight.w600,
),
@@ -269,22 +269,51 @@ class _RideCardState extends State<_RideCard> {
],
),
const SizedBox(height: 4),
Text(
scooterNumber,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
_getLocationText(),
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 13,
),
Row(
children: [
Image.asset("assets/icons/qr_icon_order.png"),
const SizedBox(width: 6),
Text(
scooterNumber,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
if (isReserved)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Text(
"Тариф",
style: TextStyle(
color: Color(0xFF8bffaa),
fontSize: 11,
),
),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset("assets/icons/timer.png", width: 14,),
const SizedBox(width: 4),
Text(
widget.order.plan?.title ?? "Название тарифа",
style: TextStyle(
color: Color(0xFF8bffaa),
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
)
],
)
],
),
),
@@ -301,10 +330,10 @@ class _RideCardState extends State<_RideCard> {
timeString,
style: TextStyle(
color: statusColor,
fontSize: 26,
fontSize: 20,
fontWeight: FontWeight.bold,
fontFeatures: const [FontFeature.tabularFigures()],
fontFamily: 'Digital Numbers',
fontFamily: 'DigitalNumbers',
),
),
],
@@ -360,7 +389,7 @@ class _RideCardState extends State<_RideCard> {
switch (status.toLowerCase()) {
case 'reserved':
case 'holding':
return 'Забронировано';
return 'Забронирован';
case 'active':
case 'in_progress':
return 'Активно';

View File

@@ -5,8 +5,6 @@ import 'package:be_happy/presentation/state/map_settings_modal_state.dart';
import 'package:be_happy/presentation/viewmodel/map_settings_modal_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../viewmodel/map_bloc.dart';
class MapSettingsSheet extends StatelessWidget {
@@ -19,13 +17,6 @@ class MapSettingsSheet extends StatelessWidget {
return BlocBuilder<MapSettingsModalBloc, MapSettingsModalState>(
builder: (context, state) {
final List<_SettingItemData> items = [
_SettingItemData(
label: 'Геоточки',
icon: Icons.location_on_outlined,
color: const Color(0xFF66E3C4),
isActive: state.isAllGeomarksActive,
onChanged: (val) => context.read<MapSettingsModalBloc>().add(AllGeomarksToggled(val)),
),
_SettingItemData(
label: 'Геозоны',
icon: Icons.gps_fixed_outlined,
@@ -41,9 +32,9 @@ class MapSettingsSheet extends StatelessWidget {
onChanged: (val) => context.read<MapSettingsModalBloc>().add(ParkingZonesToggled(val)),
),
_SettingItemData(
label: 'Разрешено кататься',
label: 'Парковка запрещена',
icon: Icons.block_outlined,
color: const Color(0xFF5ECD4C),
color: const Color(0xFFF59E0B),
isActive: state.isRestrictedParkingZoneActive,
onChanged: (val) => context.read<MapSettingsModalBloc>().add(RestrictedParkingZonesToggled(val)),
),

View File

@@ -12,11 +12,13 @@ import '../../state/payment_method_sheet_state.dart';
import '../../viewmodel/payment_method_sheet_bloc.dart';
class PaymentMethodSheet extends StatefulWidget {
final PaymentCard? initialSelectedCard; // Добавляем это поле
final PaymentCard? initialSelectedCard;
final bool showBalance;
const PaymentMethodSheet({
super.key,
this.initialSelectedCard, // Инициализируем в конструкторе
this.initialSelectedCard,
this.showBalance = true,
});
@override
@@ -92,7 +94,7 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
_selectedPaymentMethod = initialIndex != -1 ? initialIndex : -1;
} else {
final mainCardIndex = state.cards.indexWhere((card) => card.isMain);
_selectedPaymentMethod = mainCardIndex != -1 ? mainCardIndex : -1;
_selectedPaymentMethod = mainCardIndex != -1 ? mainCardIndex : (widget.showBalance ? -1 : 0);
}
}
@@ -169,19 +171,20 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
PaymentOption(
title: 'Баланс',
subtitle: '${state.balance.toStringAsFixed(2)} BYN',
isSelected: _selectedPaymentMethod == -1,
onTap: () {
setState(() {
_selectedPaymentMethod = -1;
});
Navigator.pop(context, 'balance');
},
),
const SizedBox(height: 12),
if (widget.showBalance) ...[
PaymentOption(
title: 'Баланс',
subtitle: '${state.balance.toStringAsFixed(2)} BYN',
isSelected: _selectedPaymentMethod == -1,
onTap: () {
setState(() {
_selectedPaymentMethod = -1;
});
Navigator.pop(context, 'balance');
},
),
const SizedBox(height: 12),
],
...state.cards.asMap().entries.map((entry) {
final index = entry.key;

View File

@@ -14,7 +14,7 @@ class ReservedRideSheet extends StatefulWidget {
final String scooterNumber;
final int orderId;
final Duration initialReservationTime;
const ReservedRideSheet({
super.key,
required this.scooterNumber,
@@ -60,264 +60,286 @@ class _ReservedRideSheetState extends State<ReservedRideSheet> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _bloc,
child: Align(
alignment: Alignment.bottomCenter,
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
padding: const EdgeInsets.only(top: 20, bottom: 10),
decoration: BoxDecoration(
color: const Color(0xFF000032).withOpacity(0.5),
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// HEADER
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Row(
children: [
Icon(
Icons.arrow_back_ios_sharp,
color: const Color(0x99FFFFFF),
size: 20,
),
Icon(
Icons.arrow_back_ios_sharp,
color: const Color(0x66FFFFFF),
size: 20,
),
Icon(
Icons.arrow_back_ios_sharp,
color: const Color(0x22FFFFFF),
size: 20,
),
],
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
'Бесплатное бронирование',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
child: Scaffold(
backgroundColor: Colors.transparent,
resizeToAvoidBottomInset: false,
body: Align(
alignment: Alignment.bottomCenter,
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
padding: const EdgeInsets.only(top: 20, bottom: 10),
decoration: BoxDecoration(
color: const Color(0xFF000032).withOpacity(0.5),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(30),
),
const SizedBox(height: 20),
// ТАЙМЕР + ИНФО О САМОКАТЕ (КОМПАКТНЫЙ)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
// Таймер
Expanded(
flex: 2,
child: Text(
_formatDuration(_reservationTime),
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
fontFeatures: [FontFeature.tabularFigures()],
fontFamily: 'Digital Numbers',
),
),
),
// Иконка и информация (ВЫСОКИЙ БЛОК)
Expanded(
flex: 3,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// HEADER
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Row(
children: [
// Иконка самоката (ВЫШЕ)
SizedBox(
width: 44,
height: 56,
child: Image.asset(
'assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png',
fit: BoxFit.contain,
),
Icon(
Icons.arrow_back_ios_sharp,
color: const Color(0x99FFFFFF),
size: 20,
),
const SizedBox(width: 12),
// Инфо
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Color(0xFFFFB800),
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
const Text(
'Забронирован',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
],
),
const SizedBox(height: 8),
Text(
'${widget.scooterNumber}',
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
],
),
Icon(
Icons.arrow_back_ios_sharp,
color: const Color(0x66FFFFFF),
size: 20,
),
Icon(
Icons.arrow_back_ios_sharp,
color: const Color(0x22FFFFFF),
size: 20,
),
],
),
),
),
],
),
),
const SizedBox(height: 16),
// КНОПКА "НАЧАТЬ ПОЕЗДКУ"
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: BlocListener<ReservedRideBloc, ReservedRideState>(
listener: (context, state) {
if (state.rideStarted) {
Navigator.pop(context);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => ActiveRideSheet(
scooterNumber: widget.scooterNumber,
initialElapsedTime: Duration.zero,
orderId: widget.orderId,
const SizedBox(width: 12),
Expanded(
child: Text(
'Бесплатное бронирование',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
);
} else if (state.status == ReservedRideStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage ?? 'Ошибка')),
);
}
},
child: GradientButton(
text: 'Начать поездку',
showArrows: true,
height: 48,
width: double.infinity,
fontSize: 15,
onTap: () {
_bloc.add(StartRide(widget.orderId));
},
),
],
),
),
),
const SizedBox(height: 12),
const SizedBox(height: 20),
// КНОПКА "ОТМЕНИТЬ БРОНИРОВАНИЕ"
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: BlocListener<ReservedRideBloc, ReservedRideState>(
listener: (context, state) {
if (state.rideCancelled) {
Navigator.pop(context);
} else if (state.status == ReservedRideStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage ?? 'Ошибка')),
);
}
},
child: Container(
height: 48,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Colors.white.withOpacity(0.4),
width: 1,
// ТАЙМЕР + ИНФО О САМОКАТЕ (КОМПАКТНЫЙ)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
// Таймер
Expanded(
flex: 2,
child: Text(
_formatDuration(_reservationTime),
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
fontFeatures: [FontFeature.tabularFigures()],
fontFamily: 'Digital Numbers',
),
),
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () async {
final result = await showDialog<bool>(
context: context,
builder: (context) => const CancelBookingDialog(),
);
if (result != null && result) {
_bloc.add(CancelRide(widget.orderId));
}
},
borderRadius: BorderRadius.circular(24),
child: BlocBuilder<ReservedRideBloc, ReservedRideState>(
builder: (context, state) {
if (state.status == ReservedRideStatus.loading) {
return const Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
),
);
}
return const Center(
child: Text(
'Отменить бронирование',
style: TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w600,
// Иконка и информация (ВЫСОКИЙ БЛОК)
Expanded(
flex: 3,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 16,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
// Иконка самоката (ВЫШЕ)
SizedBox(
width: 44,
height: 56,
child: Image.asset(
'assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png',
fit: BoxFit.contain,
),
),
const SizedBox(width: 12),
// Инфо
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Color(0xFFFFB800),
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
const Text(
'Забронирован',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
],
),
const SizedBox(height: 8),
Text(
'${widget.scooterNumber}',
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
),
),
],
),
),
const SizedBox(height: 16),
// КНОПКА "НАЧАТЬ ПОЕЗДКУ"
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: BlocListener<ReservedRideBloc, ReservedRideState>(
listener: (context, state) {
if (state.rideStarted) {
Navigator.pop(context);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => ActiveRideSheet(
scooterNumber: widget.scooterNumber,
initialElapsedTime: Duration.zero,
orderId: widget.orderId,
),
);
} else if (state.status ==
ReservedRideStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage ?? 'Ошибка'),
),
);
}
},
child: GradientButton(
text: 'Начать поездку',
showArrows: true,
height: 48,
width: double.infinity,
fontSize: 15,
onTap: () {
_bloc.add(StartRide(widget.orderId));
},
),
),
),
const SizedBox(height: 12),
// КНОПКА "ОТМЕНИТЬ БРОНИРОВАНИЕ"
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: BlocListener<ReservedRideBloc, ReservedRideState>(
listener: (context, state) {
if (state.rideCancelled) {
Navigator.pop(context);
} else if (state.status ==
ReservedRideStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage ?? 'Ошибка'),
),
);
}
},
child: Container(
height: 48,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Colors.white.withOpacity(0.4),
width: 1,
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () async {
final result = await showDialog<bool>(
context: context,
builder: (context) =>
const CancelBookingDialog(),
);
if (result != null && result) {
_bloc.add(CancelRide(widget.orderId));
}
},
borderRadius: BorderRadius.circular(24),
child:
BlocBuilder<
ReservedRideBloc,
ReservedRideState
>(
builder: (context, state) {
if (state.status ==
ReservedRideStatus.loading) {
return const Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
),
);
}
return const Center(
child: Text(
'Отменить бронирование',
style: TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
);
},
),
),
),
),
),
),
),
const SizedBox(height: 20),
],
const SizedBox(height: 20),
],
),
),
),
),