fix functional bugs
This commit is contained in:
@@ -1,141 +0,0 @@
|
||||
import 'dart:ui';
|
||||
import 'package:be_happy/presentation/event/map_event.dart';
|
||||
import 'package:be_happy/presentation/event/map_settings_modal_event.dart';
|
||||
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 {
|
||||
final VoidCallback? onClose;
|
||||
|
||||
const MapSettingsSheet({super.key, this.onClose});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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,
|
||||
color: const Color(0xFF86EFAC),
|
||||
isActive: state.isAllGeozonesActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(AllGeozonesToggled(val)),
|
||||
),
|
||||
_SettingItemData(
|
||||
label: 'Парковка',
|
||||
icon: Icons.home_outlined,
|
||||
color: const Color(0xFFA78BFA),
|
||||
isActive: state.isParkingZoneActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(ParkingZonesToggled(val)),
|
||||
),
|
||||
_SettingItemData(
|
||||
label: 'Парковка запрещена',
|
||||
icon: Icons.block_outlined,
|
||||
color: const Color(0xFFF59E0B),
|
||||
isActive: state.isRestrictedParkingZoneActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(RestrictedParkingZonesToggled(val)),
|
||||
),
|
||||
_SettingItemData(
|
||||
label: 'Запрещено кататься',
|
||||
icon: Icons.warning_amber_outlined,
|
||||
color: const Color(0xFFEF4444),
|
||||
isActive: state.isRestrictedDrivingZoneActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(RestrictedDrivingZonesToggled(val)),
|
||||
),
|
||||
];
|
||||
|
||||
return 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(
|
||||
height: 365,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF000032).withOpacity(0.88),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Параметры карты',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.read<MapSettingsModalBloc>().add(ApllyButtonClick());
|
||||
context.read<MapBloc>().add(UpdateMap());
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text(
|
||||
'Готово',
|
||||
style: TextStyle(color: Color(0xFF66E3C4), fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return ListTile(
|
||||
leading: Icon(item.icon, color: item.color),
|
||||
title: Text(item.label, style: const TextStyle(color: Colors.white)),
|
||||
trailing: Switch.adaptive(
|
||||
value: item.isActive,
|
||||
onChanged: item.onChanged,
|
||||
activeTrackColor: const Color(0xFF66E3C4),
|
||||
inactiveThumbColor: Colors.white,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательный класс для описания строк
|
||||
class _SettingItemData {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final bool isActive;
|
||||
final ValueChanged<bool> onChanged;
|
||||
|
||||
_SettingItemData({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.isActive,
|
||||
required this.onChanged,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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 'Активно';
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import 'package:be_happy/presentation/components/gradient_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../domain/entities/subscription.dart';
|
||||
import '../event/subscription_list_event.dart';
|
||||
|
||||
class SubscriptionCard extends StatelessWidget {
|
||||
final Subscription subscription;
|
||||
final bool isActive;
|
||||
final DateTime? expiredAt;
|
||||
final VoidCallback? onRefresh;
|
||||
|
||||
const SubscriptionCard({super.key, required this.subscription, required this.isActive});
|
||||
const SubscriptionCard({super.key, required this.subscription, required this.isActive, this.expiredAt, this.onRefresh});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -15,9 +20,9 @@ class SubscriptionCard extends StatelessWidget {
|
||||
? subscription.options.reduce((a, b) => a.price < b.price ? a : b)
|
||||
: null;
|
||||
|
||||
final maxDaysOption = subscription.options.isNotEmpty
|
||||
/*final maxDaysOption = subscription.options.isNotEmpty
|
||||
? subscription.options.reduce((a, b) => a.days > b.days ? a : b)
|
||||
: null;
|
||||
: null;*/
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
@@ -43,7 +48,7 @@ class SubscriptionCard extends StatelessWidget {
|
||||
padding: EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(12), // Опционально: скругление углов
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
"АКТИВНА",
|
||||
@@ -64,12 +69,19 @@ class SubscriptionCard extends StatelessWidget {
|
||||
subscription.shortDescription,
|
||||
style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (maxDaysOption != null) ...[
|
||||
if (isActive && expiredAt != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"Период действия: до ${maxDaysOption.days} дней",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final day = expiredAt!.day.toString().padLeft(2, '0');
|
||||
final month = expiredAt!.month.toString().padLeft(2, '0');
|
||||
final year = expiredAt!.year;
|
||||
|
||||
return Text(
|
||||
"Период действия: до $day.$month.$year",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
@@ -83,15 +95,17 @@ class SubscriptionCard extends StatelessWidget {
|
||||
)
|
||||
else
|
||||
const SizedBox.shrink(),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.push("/home/subscriptions/${subscription.id}"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF80FFD1),
|
||||
foregroundColor: Colors.black,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
),
|
||||
child: const Text("Подробнее", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
GradientButton(
|
||||
onTap: () async {
|
||||
final isSubscribed = await context.push<bool>("/home/subscriptions/${subscription.id}");
|
||||
if (isSubscribed == true && onRefresh != null) {
|
||||
onRefresh!();
|
||||
}
|
||||
},
|
||||
text: "Подробнее",
|
||||
enabled: true,
|
||||
width: 120,
|
||||
height: 40,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user