some fix
# Conflicts: # lib/presentation/navigation/app_router.dart # pubspec.yaml
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user