# Conflicts:
#	lib/presentation/navigation/app_router.dart
#	pubspec.yaml
This commit is contained in:
2026-08-11 00:14:00 +03:00
parent c9b8d143d1
commit 182be1049b
351 changed files with 24566 additions and 22898 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,433 +1,433 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../di/service_locator.dart';
import '../../../domain/entities/scooter_order.dart';
import '../../event/current_rides_event.dart';
import '../../state/current_rides_state.dart';
import '../../viewmodel/current_rides_bloc.dart';
import '../gradient_button.dart';
import 'reserved_ride_sheet.dart';
import 'active_ride_sheet.dart';
class CurrentRidesSheet extends StatefulWidget {
const CurrentRidesSheet({super.key});
@override
State<CurrentRidesSheet> createState() => _CurrentRidesSheetState();
}
class _CurrentRidesSheetState extends State<CurrentRidesSheet> {
late final CurrentRidesBloc _bloc;
@override
Widget build(BuildContext context) {
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: 450,
padding: const EdgeInsets.only(top: 20, bottom: 10),
decoration: BoxDecoration(
color: const Color(0x00000032).withOpacity(0.6),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(30),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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,
),
),
],
),
),
const SizedBox(height: 20),
Expanded(
child: BlocBuilder<CurrentRidesBloc, CurrentRidesState>(
builder: (context, state) {
if (state.status == CurrentRidesStatus.loading) {
return const Center(
child: CircularProgressIndicator(color: Colors.white),
);
}
if (state.status == CurrentRidesStatus.failure) {
return Center(
child: Text(
state.errorMessage ?? 'Ошибка загрузки',
style: const TextStyle(color: Colors.white),
),
);
}
if (state.orders.isEmpty) {
return const Center(
child: Text(
'Нет активных поездок',
style: TextStyle(color: Colors.white70),
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: state.orders.map((order) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _RideCard(order: order),
);
}).toList(),
),
);
},
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Container(
height: 56,
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: () {
Navigator.pop(context);
},
borderRadius: BorderRadius.circular(24),
child: const Center(
child: Text(
'Взять ещё самокат',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
),
const SizedBox(height: 16),
],
),
),
),
),
);
}
}
class _RideCard extends StatefulWidget {
final ScooterOrder order;
const _RideCard({required this.order});
@override
State<_RideCard> createState() => _RideCardState();
}
class _RideCardState extends State<_RideCard> {
late Timer _timer;
late Duration _elapsedTime;
late Duration _reservationTime;
late DateTime _startTime;
@override
void initState() {
super.initState();
_startTime = widget.order.startAt ?? widget.order.createdAt;
_elapsedTime = DateTime.now().difference(_startTime);
_reservationTime = const Duration(minutes: 5);
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (mounted) {
setState(() {
_elapsedTime = DateTime.now().difference(_startTime);
});
}
});
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isReserved =
widget.order.status == 'Booking' || //Drive, Finish
widget.order.status == 'holding';
Duration displayTime;
if (isReserved) {
displayTime = _reservationTime - _elapsedTime;
if (displayTime.isNegative) {
displayTime = Duration.zero;
}
} 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);
final scooterNumber =
widget.order.scooter.number ?? widget.order.scooterId.toString();
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
SizedBox(
width: 70,
child: Image.asset(
'assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png',
height: 70,
fit: BoxFit.contain,
),
),
const SizedBox(width: 12),
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: statusColor,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Text(
statusText,
style: TextStyle(
color: statusColor,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 4),
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,
),
),
],
)
],
)
],
),
),
const SizedBox(width: 12),
Expanded(
flex: 1,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
timeString,
style: TextStyle(
color: statusColor,
fontSize: 20,
fontWeight: FontWeight.bold,
fontFeatures: const [FontFeature.tabularFigures()],
fontFamily: 'DigitalNumbers',
),
),
],
),
const SizedBox(height: 8),
SizedBox(
height: 32,
child: GradientButton(
text: 'Подробнее',
showArrows: false,
height: 32,
width: 100,
fontSize: 11,
enabled: !isExpired,
onTap: !isExpired ? () {
if (isReserved) {
Navigator.pop(context);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => ReservedRideSheet(
orderId: widget.order.id,
scooterNumber: scooterNumber,
initialReservationTime:
_reservationTime - _elapsedTime,
),
);
} else {
Navigator.pop(context);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => ActiveRideSheet(
orderId: widget.order.id,
scooterNumber: scooterNumber,
initialElapsedTime: _elapsedTime,
),
);
}
} : null,
),
),
],
),
),
],
),
);
}
String _getStatusText(String status) {
switch (status.toLowerCase()) {
case 'reserved':
case 'holding':
return 'Забронирован';
case 'active':
case 'in_progress':
return 'Активно';
case 'completed':
return 'Завершено';
default:
return status;
}
}
Color _getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'reserved':
case 'holding':
return const Color(0xFFFFB800);
case 'active':
case 'in_progress':
return const Color(0xFF66E3C4);
default:
return Colors.white;
}
}
String _getLocationText() {
/*if (widget.order.scooter != null && widget.order.scooter!.address != null) {
return widget.order.scooter!.address!;
}*/
return 'Московский 33';
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final minutes = twoDigits(duration.inMinutes.remainder(60));
final seconds = twoDigits(duration.inSeconds.remainder(60));
return '$minutes:$seconds';
}
}
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../di/service_locator.dart';
import '../../../domain/entities/scooter_order.dart';
import '../../event/current_rides_event.dart';
import '../../state/current_rides_state.dart';
import '../../viewmodel/current_rides_bloc.dart';
import '../gradient_button.dart';
import 'reserved_ride_sheet.dart';
import 'active_ride_sheet.dart';
class CurrentRidesSheet extends StatefulWidget {
const CurrentRidesSheet({super.key});
@override
State<CurrentRidesSheet> createState() => _CurrentRidesSheetState();
}
class _CurrentRidesSheetState extends State<CurrentRidesSheet> {
late final CurrentRidesBloc _bloc;
@override
Widget build(BuildContext context) {
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: 450,
padding: const EdgeInsets.only(top: 20, bottom: 10),
decoration: BoxDecoration(
color: const Color(0x00000032).withOpacity(0.6),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(30),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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,
),
),
],
),
),
const SizedBox(height: 20),
Expanded(
child: BlocBuilder<CurrentRidesBloc, CurrentRidesState>(
builder: (context, state) {
if (state.status == CurrentRidesStatus.loading) {
return const Center(
child: CircularProgressIndicator(color: Colors.white),
);
}
if (state.status == CurrentRidesStatus.failure) {
return Center(
child: Text(
state.errorMessage ?? 'Ошибка загрузки',
style: const TextStyle(color: Colors.white),
),
);
}
if (state.orders.isEmpty) {
return const Center(
child: Text(
'Нет активных поездок',
style: TextStyle(color: Colors.white70),
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: state.orders.map((order) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _RideCard(order: order),
);
}).toList(),
),
);
},
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Container(
height: 56,
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: () {
Navigator.pop(context);
},
borderRadius: BorderRadius.circular(24),
child: const Center(
child: Text(
'Взять ещё самокат',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
),
const SizedBox(height: 16),
],
),
),
),
),
);
}
}
class _RideCard extends StatefulWidget {
final ScooterOrder order;
const _RideCard({required this.order});
@override
State<_RideCard> createState() => _RideCardState();
}
class _RideCardState extends State<_RideCard> {
late Timer _timer;
late Duration _elapsedTime;
late Duration _reservationTime;
late DateTime _startTime;
@override
void initState() {
super.initState();
_startTime = widget.order.startAt ?? widget.order.createdAt;
_elapsedTime = DateTime.now().difference(_startTime);
_reservationTime = const Duration(minutes: 5);
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (mounted) {
setState(() {
_elapsedTime = DateTime.now().difference(_startTime);
});
}
});
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isReserved =
widget.order.status == 'Booking' || //Drive, Finish
widget.order.status == 'holding';
Duration displayTime;
if (isReserved) {
displayTime = _reservationTime - _elapsedTime;
if (displayTime.isNegative) {
displayTime = Duration.zero;
}
} 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);
final scooterNumber =
widget.order.scooter.number ?? widget.order.scooterId.toString();
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
SizedBox(
width: 70,
child: Image.asset(
'assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png',
height: 70,
fit: BoxFit.contain,
),
),
const SizedBox(width: 12),
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: statusColor,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Text(
statusText,
style: TextStyle(
color: statusColor,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 4),
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,
),
),
],
)
],
)
],
),
),
const SizedBox(width: 12),
Expanded(
flex: 1,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
timeString,
style: TextStyle(
color: statusColor,
fontSize: 20,
fontWeight: FontWeight.bold,
fontFeatures: const [FontFeature.tabularFigures()],
fontFamily: 'DigitalNumbers',
),
),
],
),
const SizedBox(height: 8),
SizedBox(
height: 32,
child: GradientButton(
text: 'Подробнее',
showArrows: false,
height: 32,
width: 100,
fontSize: 11,
enabled: !isExpired,
onTap: !isExpired ? () {
if (isReserved) {
Navigator.pop(context);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => ReservedRideSheet(
orderId: widget.order.id,
scooterNumber: scooterNumber,
initialReservationTime:
_reservationTime - _elapsedTime,
),
);
} else {
Navigator.pop(context);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => ActiveRideSheet(
orderId: widget.order.id,
scooterNumber: scooterNumber,
initialElapsedTime: _elapsedTime,
),
);
}
} : null,
),
),
],
),
),
],
),
);
}
String _getStatusText(String status) {
switch (status.toLowerCase()) {
case 'reserved':
case 'holding':
return 'Забронирован';
case 'active':
case 'in_progress':
return 'Активно';
case 'completed':
return 'Завершено';
default:
return status;
}
}
Color _getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'reserved':
case 'holding':
return const Color(0xFFFFB800);
case 'active':
case 'in_progress':
return const Color(0xFF66E3C4);
default:
return Colors.white;
}
}
String _getLocationText() {
/*if (widget.order.scooter != null && widget.order.scooter!.address != null) {
return widget.order.scooter!.address!;
}*/
return 'Московский 33';
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final minutes = twoDigits(duration.inMinutes.remainder(60));
final seconds = twoDigits(duration.inSeconds.remainder(60));
return '$minutes:$seconds';
}
}

View File

@@ -1,132 +1,132 @@
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 '../../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.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,
});
}
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 '../../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.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,
});
}

View File

@@ -1,202 +1,202 @@
import 'dart:ui';
import 'package:be_happy/presentation/event/tariff_sheet_event.dart';
import 'package:be_happy/presentation/viewmodel/tariff_sheet_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:be_happy/presentation/components/payment_option.dart';
import '../../../domain/entities/payment_card.dart';
import '../../event/payment_method_sheet_event.dart';
import '../../state/payment_method_sheet_state.dart';
import '../../viewmodel/payment_method_sheet_bloc.dart';
class PaymentMethodSheet extends StatefulWidget {
final PaymentCard? initialSelectedCard;
final bool showBalance;
const PaymentMethodSheet({
super.key,
this.initialSelectedCard,
this.showBalance = true,
});
@override
State<PaymentMethodSheet> createState() => _PaymentMethodSheetState();
}
class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
int? _selectedPaymentMethod = -2;
int? _processingCardId; // <-- Переменная для отслеживания отправленного запроса
@override
void initState() {
super.initState();
context.read<PaymentMethodSheetBloc>().add(PaymentMethodSheetStarted());
}
@override
Widget build(BuildContext context) {
return BlocConsumer<PaymentMethodSheetBloc, PaymentMethodSheetState>(
listener: (context, state) {
if (state.status == PaymentMethodSheetStatus.success && _processingCardId != null) {
final updatedCard = state.cards.cast<PaymentCard?>().firstWhere(
(c) => c?.id == _processingCardId,
orElse: () => state.cards.cast<PaymentCard?>().firstWhere((c) => c?.isMain == true, orElse: () => null),
);
_processingCardId = null;
Navigator.pop(context, updatedCard);
}
},
builder: (context, state) {
if (state.status == PaymentMethodSheetStatus.success && _selectedPaymentMethod == -2) {
if (widget.initialSelectedCard != null) {
final initialIndex = state.cards.indexWhere(
(card) => card.cardLastNumber == widget.initialSelectedCard!.cardLastNumber
);
_selectedPaymentMethod = initialIndex != -1 ? initialIndex : -1;
} else {
final mainCardIndex = state.cards.indexWhere((card) => card.isMain);
_selectedPaymentMethod = mainCardIndex != -1 ? mainCardIndex : (widget.showBalance ? -1 : 0);
}
}
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: 450,
padding: const EdgeInsets.only(top: 20, bottom: 10),
decoration: BoxDecoration(
color: const Color(0x00000032).withOpacity(0.6),
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 20),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
if (widget.showBalance) ...[
PaymentOption(
title: 'Баланс',
subtitle: '${state.balance} баллов',
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;
final card = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: PaymentOption(
title: card.isMain ? "Основная" : "Карта",
subtitle: '${card.cardLastNumber}',
isSelected: _selectedPaymentMethod == index,
onTap: () async {
setState(() {
_selectedPaymentMethod = index;
_processingCardId = card.id;
});
// Отправляем событие в Блок
context.read<PaymentMethodSheetBloc>().add(
SelectPaymentCardRequested(card),
);
},
),
);
}).toList(),
Container(
height: 56,
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: () {
context.read<PaymentMethodSheetBloc>().add(AddPaymentCardRequested());
},
borderRadius: BorderRadius.circular(24),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.add,
color: const Color(0xFF66E3C4),
size: 20,
),
const SizedBox(width: 8),
const Text(
'Добавить платежную карту',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
),
const SizedBox(height: 16),
],
),
),
),
],
),
),
),
),
);
},
);
}
String _getCardType(String lastNumber) {
if (lastNumber.isEmpty) return 'Card';
final firstDigit = lastNumber[0];
switch (firstDigit) {
case '4':
return 'Visa';
case '5':
return 'Mastercard';
case '9':
return 'BelCard';
default:
return 'Card';
}
}
}
import 'dart:ui';
import 'package:be_happy/presentation/event/tariff_sheet_event.dart';
import 'package:be_happy/presentation/viewmodel/tariff_sheet_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:be_happy/presentation/components/payment_option.dart';
import '../../../domain/entities/payment_card.dart';
import '../../event/payment_method_sheet_event.dart';
import '../../state/payment_method_sheet_state.dart';
import '../../viewmodel/payment_method_sheet_bloc.dart';
class PaymentMethodSheet extends StatefulWidget {
final PaymentCard? initialSelectedCard;
final bool showBalance;
const PaymentMethodSheet({
super.key,
this.initialSelectedCard,
this.showBalance = true,
});
@override
State<PaymentMethodSheet> createState() => _PaymentMethodSheetState();
}
class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
int? _selectedPaymentMethod = -2;
int? _processingCardId; // <-- Переменная для отслеживания отправленного запроса
@override
void initState() {
super.initState();
context.read<PaymentMethodSheetBloc>().add(PaymentMethodSheetStarted());
}
@override
Widget build(BuildContext context) {
return BlocConsumer<PaymentMethodSheetBloc, PaymentMethodSheetState>(
listener: (context, state) {
if (state.status == PaymentMethodSheetStatus.success && _processingCardId != null) {
final updatedCard = state.cards.cast<PaymentCard?>().firstWhere(
(c) => c?.id == _processingCardId,
orElse: () => state.cards.cast<PaymentCard?>().firstWhere((c) => c?.isMain == true, orElse: () => null),
);
_processingCardId = null;
Navigator.pop(context, updatedCard);
}
},
builder: (context, state) {
if (state.status == PaymentMethodSheetStatus.success && _selectedPaymentMethod == -2) {
if (widget.initialSelectedCard != null) {
final initialIndex = state.cards.indexWhere(
(card) => card.cardLastNumber == widget.initialSelectedCard!.cardLastNumber
);
_selectedPaymentMethod = initialIndex != -1 ? initialIndex : -1;
} else {
final mainCardIndex = state.cards.indexWhere((card) => card.isMain);
_selectedPaymentMethod = mainCardIndex != -1 ? mainCardIndex : (widget.showBalance ? -1 : 0);
}
}
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: 450,
padding: const EdgeInsets.only(top: 20, bottom: 10),
decoration: BoxDecoration(
color: const Color(0x00000032).withOpacity(0.6),
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 20),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
if (widget.showBalance) ...[
PaymentOption(
title: 'Баланс',
subtitle: '${state.balance} баллов',
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;
final card = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: PaymentOption(
title: card.isMain ? "Основная" : "Карта",
subtitle: '${card.cardLastNumber}',
isSelected: _selectedPaymentMethod == index,
onTap: () async {
setState(() {
_selectedPaymentMethod = index;
_processingCardId = card.id;
});
// Отправляем событие в Блок
context.read<PaymentMethodSheetBloc>().add(
SelectPaymentCardRequested(card),
);
},
),
);
}).toList(),
Container(
height: 56,
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: () {
context.read<PaymentMethodSheetBloc>().add(AddPaymentCardRequested());
},
borderRadius: BorderRadius.circular(24),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.add,
color: const Color(0xFF66E3C4),
size: 20,
),
const SizedBox(width: 8),
const Text(
'Добавить платежную карту',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
),
const SizedBox(height: 16),
],
),
),
),
],
),
),
),
),
);
},
);
}
String _getCardType(String lastNumber) {
if (lastNumber.isEmpty) return 'Card';
final firstDigit = lastNumber[0];
switch (firstDigit) {
case '4':
return 'Visa';
case '5':
return 'Mastercard';
case '9':
return 'BelCard';
default:
return 'Card';
}
}
}

View File

@@ -1,357 +1,357 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../di/service_locator.dart';
import '../../event/reserved_ride_event.dart';
import '../../state/reserved_ride_state.dart';
import '../../viewmodel/reserved_ride_bloc.dart';
import '../dialog/cancel_booking_dialog.dart';
import '../gradient_button.dart';
import 'active_ride_sheet.dart';
class ReservedRideSheet extends StatefulWidget {
final String scooterNumber;
final int orderId;
final Duration initialReservationTime;
const ReservedRideSheet({
super.key,
required this.scooterNumber,
required this.orderId,
this.initialReservationTime = const Duration(minutes: 3, seconds: 17),
});
@override
State<ReservedRideSheet> createState() => _ReservedRideSheetState();
}
class _ReservedRideSheetState extends State<ReservedRideSheet> {
late final ReservedRideBloc _bloc;
late Duration _reservationTime;
late Timer _timer;
@override
void initState() {
super.initState();
_bloc = getIt<ReservedRideBloc>();
_reservationTime = widget.initialReservationTime;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (mounted) {
setState(() {
_reservationTime = _reservationTime - const Duration(seconds: 1);
if (_reservationTime.isNegative) {
_reservationTime = Duration.zero;
timer.cancel();
}
});
}
});
}
@override
void dispose() {
_timer.cancel();
_bloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _bloc,
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),
),
),
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,
),
),
],
),
),
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: 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),
],
),
),
),
),
),
),
);
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final minutes = twoDigits(duration.inMinutes.remainder(60));
final seconds = twoDigits(duration.inSeconds.remainder(60));
return '$minutes:$seconds';
}
}
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../di/service_locator.dart';
import '../../event/reserved_ride_event.dart';
import '../../state/reserved_ride_state.dart';
import '../../viewmodel/reserved_ride_bloc.dart';
import '../dialog/cancel_booking_dialog.dart';
import '../gradient_button.dart';
import 'active_ride_sheet.dart';
class ReservedRideSheet extends StatefulWidget {
final String scooterNumber;
final int orderId;
final Duration initialReservationTime;
const ReservedRideSheet({
super.key,
required this.scooterNumber,
required this.orderId,
this.initialReservationTime = const Duration(minutes: 3, seconds: 17),
});
@override
State<ReservedRideSheet> createState() => _ReservedRideSheetState();
}
class _ReservedRideSheetState extends State<ReservedRideSheet> {
late final ReservedRideBloc _bloc;
late Duration _reservationTime;
late Timer _timer;
@override
void initState() {
super.initState();
_bloc = getIt<ReservedRideBloc>();
_reservationTime = widget.initialReservationTime;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (mounted) {
setState(() {
_reservationTime = _reservationTime - const Duration(seconds: 1);
if (_reservationTime.isNegative) {
_reservationTime = Duration.zero;
timer.cancel();
}
});
}
});
}
@override
void dispose() {
_timer.cancel();
_bloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _bloc,
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),
),
),
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,
),
),
],
),
),
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: 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),
],
),
),
),
),
),
),
);
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final minutes = twoDigits(duration.inMinutes.remainder(60));
final seconds = twoDigits(duration.inSeconds.remainder(60));
return '$minutes:$seconds';
}
}

View File

@@ -1,290 +1,287 @@
import 'dart:ui';
import 'package:be_happy/presentation/components/scooter/mini_battery_indicator.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../../domain/entities/scooter.dart';
import '../../state/scooter_detail_modal_state.dart';
import '../../viewmodel/scooter_detail_modal_bloc.dart';
import '../gradient_button.dart';
class ScooterData {
final String distance;
final String number;
final double batteryPercent;
ScooterData({
required this.distance,
required this.number,
required this.batteryPercent,
});
}
class ScooterBottomSheet extends StatefulWidget {
const ScooterBottomSheet({super.key});
@override
State<ScooterBottomSheet> createState() => _ScooterBottomSheetState();
}
class _ScooterBottomSheetState extends State<ScooterBottomSheet> {
final PageController _pageController = PageController(viewportFraction: 0.5);
double _currentPage = 0;
_ScooterBottomSheetState();
@override
void initState() {
super.initState();
_pageController.addListener(() {
setState(() {
_currentPage = _pageController.page ?? 0;
});
});
}
@override
Widget build(BuildContext context) {
return BlocBuilder<ScooterDetailModalBloc, ScooterDetailModalState>(
builder: (context, state) {
if (state.status == ScooterDetailModalStatus.loading) {
return const Center(
child: CircularProgressIndicator(color: Colors.white),
);
}
if (state.status == ScooterDetailModalStatus.success) {
return Dismissible(
key: const Key('scooter-modal'),
direction: DismissDirection.down, // Закрытие только вниз
onDismissed: (_) => context.pop(),
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(
height: 320,
padding: const EdgeInsets.only(top: 20, bottom: 10),
decoration: BoxDecoration(
color: const Color(0xFF000032).withOpacity(0.88),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(30),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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(
state.address ?? "Unknown address",
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(height: 20),
// PageView с динамическим списком
SizedBox(
height: 220,
child: PageView.builder(
controller: _pageController,
padEnds: false,
// Оставляем false, чтобы первый элемент прилипал к левому краю
itemCount: state.scooters!.length,
itemBuilder: (context, index) {
final scooter = state.scooters![index];
final diff = (_currentPage - index).abs();
final isActive = diff < 0.5;
return AnimatedContainer(
duration: const Duration(milliseconds: 250),
// 2. Добавляем левый отступ ТОЛЬКО для первого элемента,
// чтобы он совпадал с заголовком
margin: EdgeInsets.only(
left: index == 0 ? 10 : 0,
right: 10,
),
child: Align(
alignment: Alignment.bottomCenter,
child: Transform.scale(
scale: isActive ? 1.0 : 0.9,
child: _ScooterCard(
scooter: scooter,
isActive: isActive,
address: state.address ?? "Unknown address",
),
),
),
);
},
),
),
],
),
),
),
),
),
);
}
return Center(child: Text("Error"));
},
);
}
}
class _ScooterCard extends StatelessWidget {
final Scooter scooter;
final String address;
final bool isActive;
const _ScooterCard({required this.scooter, required this.isActive, required this.address});
@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
children: [
Container(
width: 220,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
gradient: LinearGradient(
colors: [
Colors.white.withOpacity(isActive ? 0.35 : 0.25),
Colors.white.withOpacity(isActive ? 0.25 : 0.18),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
border: Border.all(color: Colors.white.withOpacity(0.4), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(
Icons.location_on_outlined,
color: Colors.white,
size: 18,
),
const SizedBox(width: 6),
Text(
(scooter.distance?.toInt() ?? 0) >= 1000
? '${((scooter.distance?.toInt() ?? 0) / 1000).toStringAsFixed(1)} km'
: '${scooter.distance?.toInt() ?? 0} m',
style: const TextStyle(color: Colors.white, fontSize: 14),
),
],
),
const SizedBox(height: 12),
Row(
children: [
const Icon(
Icons.qr_code_scanner_outlined,
color: Colors.white,
size: 18,
),
const SizedBox(width: 6),
Text(
scooter.number,
style: const TextStyle(color: Colors.white, fontSize: 16),
),
],
),
const SizedBox(height: 20),
Row(
children: [
MiniBatteryIndicator(percent: scooter.batteryLevel),
const SizedBox(width: 8),
Transform.translate(
offset: const Offset(-40, 0),
child: Text(
'${(scooter.batteryLevel)}%',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
],
),
const Spacer(),
GradientButton(
text: "Подробнеe",
showArrows: true,
height: 32,
width: double.infinity,
fontSize: 12,
onTap: () {
Navigator.pop(context, [scooter, address]);
},
),
],
),
),
Positioned(
right: isActive ? -30 : -5,
top: isActive ? -10 : 15,
child: SizedBox(
height: isActive ? 190 : 160,
child: Image.asset(
"assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png",
fit: BoxFit.contain,
),
),
),
],
);
}
}
import 'dart:ui';
import 'package:be_happy/presentation/components/scooter/mini_battery_indicator.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../../domain/entities/scooter.dart';
import '../../state/scooter_detail_modal_state.dart';
import '../../viewmodel/scooter_detail_modal_bloc.dart';
import '../gradient_button.dart';
class ScooterData {
final String distance;
final String number;
final double batteryPercent;
ScooterData({
required this.distance,
required this.number,
required this.batteryPercent,
});
}
class ScooterBottomSheet extends StatefulWidget {
const ScooterBottomSheet({super.key});
@override
State<ScooterBottomSheet> createState() => _ScooterBottomSheetState();
}
class _ScooterBottomSheetState extends State<ScooterBottomSheet> {
final PageController _pageController = PageController(viewportFraction: 0.5);
double _currentPage = 0;
_ScooterBottomSheetState();
@override
void initState() {
super.initState();
_pageController.addListener(() {
setState(() {
_currentPage = _pageController.page ?? 0;
});
});
}
@override
Widget build(BuildContext context) {
return BlocBuilder<ScooterDetailModalBloc, ScooterDetailModalState>(
builder: (context, state) {
if (state.status == ScooterDetailModalStatus.loading) {
return const Center(
child: CircularProgressIndicator(color: Colors.white),
);
}
if (state.status == ScooterDetailModalStatus.success) {
return Dismissible(
key: const Key('scooter-modal'),
direction: DismissDirection.down,
onDismissed: (_) => context.pop(),
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(
height: 320,
padding: const EdgeInsets.only(top: 20, bottom: 10),
decoration: BoxDecoration(
color: const Color(0xFF000032).withOpacity(0.88),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(30),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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(
state.address ?? "Unknown address",
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(height: 20),
SizedBox(
height: 220,
child: PageView.builder(
controller: _pageController,
padEnds: false,
itemCount: state.scooters!.length,
itemBuilder: (context, index) {
final scooter = state.scooters![index];
final diff = (_currentPage - index).abs();
final isActive = diff < 0.5;
return AnimatedContainer(
duration: const Duration(milliseconds: 250),
// 2. Добавляем левый отступ ТОЛЬКО для первого элемента,
// чтобы он совпадал с заголовком
margin: EdgeInsets.only(
left: index == 0 ? 10 : 0,
right: 10,
),
child: Align(
alignment: Alignment.bottomCenter,
child: Transform.scale(
scale: isActive ? 1.0 : 0.9,
child: _ScooterCard(
scooter: scooter,
isActive: isActive,
address: state.address ?? "Unknown address",
),
),
),
);
},
),
),
],
),
),
),
),
),
);
}
return Center(child: Text("Error"));
},
);
}
}
class _ScooterCard extends StatelessWidget {
final Scooter scooter;
final String address;
final bool isActive;
const _ScooterCard({required this.scooter, required this.isActive, required this.address});
@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
children: [
Container(
width: 220,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
gradient: LinearGradient(
colors: [
Colors.white.withOpacity(isActive ? 0.35 : 0.25),
Colors.white.withOpacity(isActive ? 0.25 : 0.18),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
border: Border.all(color: Colors.white.withOpacity(0.4), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(
Icons.location_on_outlined,
color: Colors.white,
size: 18,
),
const SizedBox(width: 6),
Text(
(scooter.distance?.toInt() ?? 0) >= 1000
? '${((scooter.distance?.toInt() ?? 0) / 1000).toStringAsFixed(1)} km'
: '${scooter.distance?.toInt() ?? 0} m',
style: const TextStyle(color: Colors.white, fontSize: 14),
),
],
),
const SizedBox(height: 12),
Row(
children: [
const Icon(
Icons.qr_code_scanner_outlined,
color: Colors.white,
size: 18,
),
const SizedBox(width: 6),
Text(
scooter.number,
style: const TextStyle(color: Colors.white, fontSize: 16),
),
],
),
const SizedBox(height: 20),
Row(
children: [
MiniBatteryIndicator(percent: scooter.batteryLevel),
const SizedBox(width: 8),
Transform.translate(
offset: const Offset(-40, 0),
child: Text(
'${(scooter.batteryLevel)}%',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
],
),
const Spacer(),
GradientButton(
text: "Подробнеe",
showArrows: true,
height: 32,
width: double.infinity,
fontSize: 12,
onTap: () {
Navigator.pop(context, [scooter, address]);
},
),
],
),
),
Positioned(
right: isActive ? -30 : -5,
top: isActive ? -10 : 15,
child: SizedBox(
height: isActive ? 190 : 160,
child: Image.asset(
"assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png",
fit: BoxFit.contain,
),
),
),
],
);
}
}

View File

@@ -1,186 +1,186 @@
import 'dart:ui';
import 'package:be_happy/domain/entities/tariff.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
class TariffInfoSheet extends StatefulWidget {
final Tariff tariff;
final bool isInsurance;
final Function(bool) onChanged;
const TariffInfoSheet({super.key, required this.tariff, required this.isInsurance, required this.onChanged});
@override
State<TariffInfoSheet> createState() => _TariffInfoSheetState();
}
class _TariffInfoSheetState extends State<TariffInfoSheet> {
late bool _isInsuranceEnabled;
@override
void initState() {
super.initState();
_isInsuranceEnabled = widget.isInsurance;
}
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
initialChildSize: 0.6,
minChildSize: 0.4,
maxChildSize: 0.9,
expand: false,
builder: (context, scrollController) {
return ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
decoration: BoxDecoration(
color: const Color(0xFF000032).withOpacity(0.9),
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
),
child: Column(
children: [
// Полоска сверху (Handle)
const SizedBox(height: 12),
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 12),
// Контент со скроллом
Expanded(
child: ListView(
controller: scrollController,
padding: const EdgeInsets.symmetric(horizontal: 20),
children: [
// Заголовок
Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.timer_outlined, color: Color(0xFF66E3C4), size: 18),
const SizedBox(width: 8),
Text(
widget.tariff.title,
style: const TextStyle(
color: Color(0xFF66E3C4),
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(height: 30),
// Таблица цен
_buildPriceRow('Старт поездки', '${widget.tariff.startPrice} BYN'),
_buildPriceRow('Последующая минута', '${widget.tariff.drivePrice} BYN'),
_buildPriceRow('Пауза', '${widget.tariff.pausePrice} BYN/мин'),
_buildPriceRow('КЕШБЭК', '${widget.tariff.cashback * 100}%', isAccent: true),
const Divider(color: Colors.white24, height: 40),
Row(
children: [
const Text(
'Страховка',
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(width: 8),
Image.asset('assets/icons/info_icon.png', width: 18, height: 18),
const Spacer(),
Text(
'${widget.tariff.insurance} BYN',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(width: 12),
Transform.scale(
scale: 0.8,
child: CupertinoSwitch(
value: _isInsuranceEnabled,
activeColor: const Color(0xFF66E3C4),
onChanged: (val) {
setState(() => _isInsuranceEnabled = val);
widget.onChanged(val);
},
),
),
],
),
const Divider(color: Colors.white24, height: 40),
_buildInfoBullet('Оплата страховки осуществляется только по банковской карте отдельным платежом'),
_buildInfoBullet('В режиме паузы время тарифа приостанавливается'),
_buildInfoBullet('При старте заказа будет заблокирована сумма в размере 7 рублей для проверки платежеспособности. Сумма разблокируется по факту списания средств за поездку.'),
const SizedBox(height: 30),
],
),
),
],
),
),
),
);
},
);
}
Widget _buildPriceRow(String label, String value, {bool isAccent = false}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 15),
),
Text(
value,
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: isAccent ? FontWeight.bold : FontWeight.w500,
),
),
],
),
);
}
Widget _buildInfoBullet(String text) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(top: 6),
child: Icon(Icons.circle, size: 4, color: Colors.white),
),
const SizedBox(width: 12),
Expanded(
child: Text(
text,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 13,
height: 1.5,
),
),
),
],
),
);
}
import 'dart:ui';
import 'package:be_happy/domain/entities/tariff.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
class TariffInfoSheet extends StatefulWidget {
final Tariff tariff;
final bool isInsurance;
final Function(bool) onChanged;
const TariffInfoSheet({super.key, required this.tariff, required this.isInsurance, required this.onChanged});
@override
State<TariffInfoSheet> createState() => _TariffInfoSheetState();
}
class _TariffInfoSheetState extends State<TariffInfoSheet> {
late bool _isInsuranceEnabled;
@override
void initState() {
super.initState();
_isInsuranceEnabled = widget.isInsurance;
}
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
initialChildSize: 0.6,
minChildSize: 0.4,
maxChildSize: 0.9,
expand: false,
builder: (context, scrollController) {
return ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
decoration: BoxDecoration(
color: const Color(0xFF000032).withOpacity(0.9),
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
),
child: Column(
children: [
// Полоска сверху (Handle)
const SizedBox(height: 12),
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 12),
// Контент со скроллом
Expanded(
child: ListView(
controller: scrollController,
padding: const EdgeInsets.symmetric(horizontal: 20),
children: [
// Заголовок
Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.timer_outlined, color: Color(0xFF66E3C4), size: 18),
const SizedBox(width: 8),
Text(
widget.tariff.title,
style: const TextStyle(
color: Color(0xFF66E3C4),
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(height: 30),
// Таблица цен
_buildPriceRow('Старт поездки', '${widget.tariff.startPrice} BYN'),
_buildPriceRow('Последующая минута', '${widget.tariff.drivePrice} BYN'),
_buildPriceRow('Пауза', '${widget.tariff.pausePrice} BYN/мин'),
_buildPriceRow('КЕШБЭК', '${widget.tariff.cashback * 100}%', isAccent: true),
const Divider(color: Colors.white24, height: 40),
Row(
children: [
const Text(
'Страховка',
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(width: 8),
Image.asset('assets/icons/info_icon.png', width: 18, height: 18),
const Spacer(),
Text(
'${widget.tariff.insurance} BYN',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(width: 12),
Transform.scale(
scale: 0.8,
child: CupertinoSwitch(
value: _isInsuranceEnabled,
activeColor: const Color(0xFF66E3C4),
onChanged: (val) {
setState(() => _isInsuranceEnabled = val);
widget.onChanged(val);
},
),
),
],
),
const Divider(color: Colors.white24, height: 40),
_buildInfoBullet('Оплата страховки осуществляется только по банковской карте отдельным платежом'),
_buildInfoBullet('В режиме паузы время тарифа приостанавливается'),
_buildInfoBullet('При старте заказа будет заблокирована сумма в размере 7 рублей для проверки платежеспособности. Сумма разблокируется по факту списания средств за поездку.'),
const SizedBox(height: 30),
],
),
),
],
),
),
),
);
},
);
}
Widget _buildPriceRow(String label, String value, {bool isAccent = false}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 15),
),
Text(
value,
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: isAccent ? FontWeight.bold : FontWeight.w500,
),
),
],
),
);
}
Widget _buildInfoBullet(String text) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(top: 6),
child: Icon(Icons.circle, size: 4, color: Colors.white),
),
const SizedBox(width: 12),
Expanded(
child: Text(
text,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 13,
height: 1.5,
),
),
),
],
),
);
}
}

File diff suppressed because it is too large Load Diff