fix to prod
This commit is contained in:
@@ -10,12 +10,14 @@ class CardBindingResultScreen extends StatefulWidget {
|
||||
final CardBindingStatus status;
|
||||
final String title;
|
||||
final String cardImagePath;
|
||||
final String redirectPath; // ← новое поле
|
||||
|
||||
const CardBindingResultScreen({
|
||||
super.key,
|
||||
required this.status,
|
||||
required this.title,
|
||||
required this.cardImagePath,
|
||||
this.redirectPath = '/home/payment-methods', // сохраняем текущее поведение по умолчанию
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -32,7 +34,7 @@ class _CardBindingResultScreenState extends State<CardBindingResultScreen> {
|
||||
_autoPopTimer = Timer(const Duration(seconds: 5), () {
|
||||
if (mounted) {
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
context.go("/home/payment-methods?refreshId=$timestamp");
|
||||
context.go("${widget.redirectPath}?refreshId=$timestamp");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -128,7 +130,7 @@ class _CardBindingResultScreenState extends State<CardBindingResultScreen> {
|
||||
text: "Попробовать снова",
|
||||
onTap: () {
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
context.go("/home/payment-methods?refreshId=$timestamp");
|
||||
context.go("${widget.redirectPath}?refreshId=$timestamp");
|
||||
},
|
||||
enabled: true,
|
||||
showArrows: true,
|
||||
@@ -142,7 +144,7 @@ class _CardBindingResultScreenState extends State<CardBindingResultScreen> {
|
||||
onPressed: ()
|
||||
{
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
context.go("/home/payment-methods?refreshId=$timestamp");
|
||||
context.go("${widget.redirectPath}?refreshId=$timestamp");
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
|
||||
376
lib/presentation/screens/fine_list_screen.dart
Normal file
376
lib/presentation/screens/fine_list_screen.dart
Normal file
@@ -0,0 +1,376 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../di/service_locator.dart';
|
||||
import '../../domain/entities/fine.dart';
|
||||
import '../components/custom_app_bar.dart';
|
||||
import '../components/gradient_button.dart';
|
||||
import '../event/fine_list_event.dart';
|
||||
import '../state/fine_list_state.dart';
|
||||
import '../viewmodel/fine_list_bloc.dart';
|
||||
import 'fine_payment_screen.dart';
|
||||
|
||||
class FineListScreen extends StatelessWidget {
|
||||
const FineListScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<FineListBloc>()..add(FetchFinesRequested()),
|
||||
child: const FineListView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FineListView extends StatelessWidget {
|
||||
const FineListView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomAppBar(title: 'Штрафы'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: BlocBuilder<FineListBloc, FineListState>(
|
||||
builder: (context, state) {
|
||||
if (state.status == FineListStatus.loading && state.fines.isEmpty) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status == FineListStatus.failure) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'Ошибка загрузки',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
state.errorMessage ?? '',
|
||||
style: TextStyle(color: Colors.white.withOpacity(0.6), fontSize: 14),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<FineListBloc>().add(FetchFinesRequested());
|
||||
},
|
||||
child: const Text('Повторить'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status == FineListStatus.empty) {
|
||||
return const _EmptyState();
|
||||
}
|
||||
|
||||
final groupedFines = _groupByMonth(state.fines);
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<FineListBloc>().add(RefreshFinesRequested());
|
||||
},
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
itemCount: groupedFines.length,
|
||||
itemBuilder: (context, index) {
|
||||
final monthKey = groupedFines.keys.elementAt(index);
|
||||
final fines = groupedFines[monthKey]!;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Text(
|
||||
_getMonthTitle(monthKey),
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
...fines.map((fine) => _FineCard(fine: fine)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, List<Fine>> _groupByMonth(List<Fine> fines) {
|
||||
final Map<String, List<Fine>> grouped = {};
|
||||
|
||||
for (var fine in fines) {
|
||||
final localDate = fine.createdAtDateTime.toLocal();
|
||||
final monthKey = '${localDate.year}-${localDate.month.toString().padLeft(2, '0')}';
|
||||
|
||||
grouped.putIfAbsent(monthKey, () => []);
|
||||
grouped[monthKey]!.add(fine);
|
||||
}
|
||||
|
||||
final sortedKeys = grouped.keys.toList()..sort((a, b) => b.compareTo(a));
|
||||
|
||||
return Map.fromEntries(
|
||||
sortedKeys.map((key) => MapEntry(key, grouped[key]!)),
|
||||
);
|
||||
}
|
||||
|
||||
String _getMonthTitle(String monthKey) {
|
||||
final parts = monthKey.split('-');
|
||||
final year = int.parse(parts[0]);
|
||||
final month = int.parse(parts[1]);
|
||||
|
||||
const months = [
|
||||
'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
|
||||
'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'
|
||||
];
|
||||
|
||||
return '${months[month - 1]} $year';
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyState extends StatelessWidget {
|
||||
const _EmptyState();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/history.png',
|
||||
width: 380,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
width: 280,
|
||||
height: 280,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.warning_amber_outlined,
|
||||
size: 120,
|
||||
color: Colors.white38,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
const Text(
|
||||
'У вас пока нет штрафов.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
GradientButton(
|
||||
text: 'Прокатиться',
|
||||
showArrows: true,
|
||||
height: 56,
|
||||
width: double.infinity,
|
||||
fontSize: 16,
|
||||
onTap: () {
|
||||
context.go('/home');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 🔹 КАРТОЧКА ШТРАФА — полностью как _OrderCard, но с Fine
|
||||
class _FineCard extends StatelessWidget {
|
||||
final Fine fine;
|
||||
|
||||
const _FineCard({required this.fine});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final date = _formatDateFromFineDate(fine.createdAtDateTime); // преобразуем "16-06-2025" → "16 июня, 15:48"
|
||||
final orderId = '${fine.orderId}';
|
||||
final price = fine.amountPrint;
|
||||
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.push('/home/fine-payment/${fine.id}'); },
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.fromLTRB(4, 4, 16, 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0A0F2E).withOpacity(0.7),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.asset(
|
||||
'assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png',
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Icon(
|
||||
Icons.warning_amber,
|
||||
color: Colors.white,
|
||||
size: 48,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
date,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.qr_code_2,
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
orderId,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Столбец 3: Статус + цена
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 100,
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
fine.isPaid ? 'ОПЛАЧЕН' : 'НЕ ОПЛАЧЕН',
|
||||
style: TextStyle(
|
||||
color: fine.isPaid
|
||||
? Colors.greenAccent
|
||||
: Colors.redAccent,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
price,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDateFromFineDate(DateTime dateTime) {
|
||||
final now = DateTime.now();
|
||||
final localDate = dateTime.toLocal();
|
||||
|
||||
final isToday = localDate.year == now.year &&
|
||||
localDate.month == now.month &&
|
||||
localDate.day == now.day;
|
||||
|
||||
final yesterday = now.subtract(const Duration(days: 1));
|
||||
final isYesterday = localDate.year == yesterday.year &&
|
||||
localDate.month == yesterday.month &&
|
||||
localDate.day == yesterday.day;
|
||||
|
||||
final time = '${localDate.hour.toString().padLeft(2, '0')}:${localDate.minute.toString().padLeft(2, '0')}';
|
||||
|
||||
if (isToday) {
|
||||
return 'Сегодня, $time';
|
||||
} else if (isYesterday) {
|
||||
return 'Вчера, $time';
|
||||
} else {
|
||||
const months = [
|
||||
'января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
|
||||
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'
|
||||
];
|
||||
return '${localDate.day} ${months[localDate.month - 1]}, $time';
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDateFull(DateTime date) {
|
||||
const months = [
|
||||
'января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
|
||||
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'
|
||||
];
|
||||
return '${date.day} ${months[date.month - 1]}';
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../data/network/api_service.dart';
|
||||
import '../../di/service_locator.dart';
|
||||
import '../../domain/entities/payment_card.dart';
|
||||
import '../../domain/usecase/add_payment_card_usecase.dart';
|
||||
@@ -34,13 +35,20 @@ class FinePaymentScreen extends StatelessWidget {
|
||||
getFineDetailsUsecase: getIt(),
|
||||
payFineUsecase: getIt(),
|
||||
)..add(FetchFineDetails(fineId)),
|
||||
child: const _FinePaymentScreenContent(),
|
||||
child: _FinePaymentScreenContent(fineId: fineId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FinePaymentScreenContent extends StatelessWidget {
|
||||
const _FinePaymentScreenContent();
|
||||
final int fineId;
|
||||
|
||||
const _FinePaymentScreenContent({required this.fineId});
|
||||
|
||||
String _resolveImageUrl(String path) {
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) return path;
|
||||
return 'https://sharing-api.sparkit.by/$path';
|
||||
}
|
||||
|
||||
void _openFullScreenImage(BuildContext context, List<String> photos, int initialIndex) {
|
||||
Navigator.of(context).push(
|
||||
@@ -88,24 +96,30 @@ class _FinePaymentScreenContent extends StatelessWidget {
|
||||
listenWhen: (previous, current) => previous.status != current.status,
|
||||
listener: (context, state) {
|
||||
if (state.status == FinePaymentStatus.success && state.paymentCompleted) {
|
||||
context.go('/home');
|
||||
context.go('/home/fine-payment/$fineId/result/success');
|
||||
} else if (state.status == FinePaymentStatus.failure) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
state.errorMessage ?? 'Произошла ошибка при оплате штрафа',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
if (state.fine != null) {
|
||||
// штраф уже был загружен — значит ошибка произошла при попытке оплаты
|
||||
context.go('/home/fine-payment/$fineId/result/failed');
|
||||
} else {
|
||||
// штраф не удалось загрузить — остаёмся на месте, показываем снекбар
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
state.errorMessage ?? 'Произошла ошибка при оплате штрафа',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
backgroundColor: Colors.redAccent.withOpacity(0.9),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
margin: const EdgeInsets.all(20),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
backgroundColor: Colors.redAccent.withOpacity(0.9),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
margin: const EdgeInsets.all(20),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
@@ -122,6 +136,8 @@ class _FinePaymentScreenContent extends StatelessWidget {
|
||||
|
||||
if (fine == null) return const SizedBox.shrink();
|
||||
|
||||
final bool isPaid = fine.isPaid;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -136,7 +152,7 @@ class _FinePaymentScreenContent extends StatelessWidget {
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final photo = fine.photoUrls[index];
|
||||
final isNetwork = photo.startsWith('http');
|
||||
final imageUrl = _resolveImageUrl(photo);
|
||||
return GestureDetector(
|
||||
onTap: () => _openFullScreenImage(
|
||||
context,
|
||||
@@ -145,18 +161,35 @@ class _FinePaymentScreenContent extends StatelessWidget {
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: isNetwork
|
||||
? Image.network(
|
||||
photo,
|
||||
width: 110,
|
||||
height: 110,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Image.asset(
|
||||
photo,
|
||||
child: Image.network(
|
||||
imageUrl,
|
||||
width: 110,
|
||||
height: 110,
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return SizedBox(
|
||||
width: 110,
|
||||
height: 110,
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Icon(Icons.error, color: Colors.white, size: 48);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -166,17 +199,17 @@ class _FinePaymentScreenContent extends StatelessWidget {
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
_buildInfoLabel('Самокат:'),
|
||||
_buildInfoLabel('Номер заказа:'),
|
||||
_buildValueRow(
|
||||
'qr_icon.png',
|
||||
fine.scooterNumber,
|
||||
'${fine.orderId}',
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
_buildInfoLabel('Дата:'),
|
||||
_buildInfoLabel('Дата и время:'),
|
||||
Text(
|
||||
fine.date,
|
||||
_formatDateTime(fine.createdAtDateTime),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
@@ -244,111 +277,112 @@ class _FinePaymentScreenContent extends StatelessWidget {
|
||||
|
||||
const SizedBox(height: 28),
|
||||
|
||||
(state.useBalance || state.selectedCard != null)
|
||||
? PaymentOption(
|
||||
title: state.useBalance ? 'Баланс' : "",
|
||||
subtitle: state.useBalance
|
||||
? '${state.userBalance.toStringAsFixed(2)} BYN'
|
||||
: '${state.selectedCard!.cardLastNumber}',
|
||||
isSelected: true,
|
||||
onTap: () async {
|
||||
final result = await showModalBottomSheet<dynamic>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (innerContext) => BlocProvider(
|
||||
create: (context) => PaymentMethodSheetBloc(
|
||||
getIt<GetPaymentCardsUsecase>(),
|
||||
getIt<SetMainPaymentCardUsecase>(),
|
||||
getIt<GetProfileUseCase>(),
|
||||
getIt<AddPaymentCardUsecase>(),
|
||||
)..add(PaymentMethodSheetStarted()),
|
||||
child: PaymentMethodSheet(
|
||||
initialSelectedCard: state.useBalance ? null : state.selectedCard,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null && context.mounted) {
|
||||
if (result is PaymentCard) {
|
||||
context.read<FinePaymentBloc>().add(FinePaymentCardChanged(result));
|
||||
} else if (result == 'balance') {
|
||||
context.read<FinePaymentBloc>().add(SelectFineBalancePressed());
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
: 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: () async {
|
||||
final result = await showModalBottomSheet<dynamic>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (innerContext) => BlocProvider(
|
||||
create: (context) => PaymentMethodSheetBloc(
|
||||
getIt<GetPaymentCardsUsecase>(),
|
||||
getIt<SetMainPaymentCardUsecase>(),
|
||||
getIt<GetProfileUseCase>(),
|
||||
getIt<AddPaymentCardUsecase>(),
|
||||
)..add(PaymentMethodSheetStarted()),
|
||||
child: const PaymentMethodSheet(),
|
||||
if (!isPaid)
|
||||
(state.useBalance || state.selectedCard != null)
|
||||
? PaymentOption(
|
||||
title: state.useBalance ? 'Баланс' : "",
|
||||
subtitle: state.useBalance
|
||||
? '${state.userBalance.toStringAsFixed(2)} BYN'
|
||||
: '${state.selectedCard!.cardLastNumber}',
|
||||
isSelected: true,
|
||||
onTap: () async {
|
||||
final result = await showModalBottomSheet<dynamic>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (innerContext) => BlocProvider(
|
||||
create: (context) => PaymentMethodSheetBloc(
|
||||
getIt<GetPaymentCardsUsecase>(),
|
||||
getIt<SetMainPaymentCardUsecase>(),
|
||||
getIt<GetProfileUseCase>(),
|
||||
getIt<AddPaymentCardUsecase>(),
|
||||
)..add(PaymentMethodSheetStarted()),
|
||||
child: PaymentMethodSheet(
|
||||
initialSelectedCard: state.useBalance ? null : state.selectedCard,
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null && context.mounted) {
|
||||
if (result is PaymentCard) {
|
||||
context.read<FinePaymentBloc>().add(FinePaymentCardChanged(result));
|
||||
} else if (result == 'balance') {
|
||||
context.read<FinePaymentBloc>().add(SelectFineBalancePressed());
|
||||
}
|
||||
if (result != null && context.mounted) {
|
||||
if (result is PaymentCard) {
|
||||
context.read<FinePaymentBloc>().add(FinePaymentCardChanged(result));
|
||||
} else if (result == 'balance') {
|
||||
context.read<FinePaymentBloc>().add(SelectFineBalancePressed());
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
: Container(
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'Способ оплаты',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final result = await showModalBottomSheet<dynamic>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (innerContext) => BlocProvider(
|
||||
create: (context) => PaymentMethodSheetBloc(
|
||||
getIt<GetPaymentCardsUsecase>(),
|
||||
getIt<SetMainPaymentCardUsecase>(),
|
||||
getIt<GetProfileUseCase>(),
|
||||
getIt<AddPaymentCardUsecase>(),
|
||||
)..add(PaymentMethodSheetStarted()),
|
||||
child: const PaymentMethodSheet(),
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null && context.mounted) {
|
||||
if (result is PaymentCard) {
|
||||
context.read<FinePaymentBloc>().add(FinePaymentCardChanged(result));
|
||||
} else if (result == 'balance') {
|
||||
context.read<FinePaymentBloc>().add(SelectFineBalancePressed());
|
||||
}
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'Способ оплаты',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
);
|
||||
@@ -361,6 +395,11 @@ class _FinePaymentScreenContent extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: BlocBuilder<FinePaymentBloc, FinePaymentState>(
|
||||
builder: (context, state) {
|
||||
final fine = state.fine;
|
||||
if (fine == null || fine.isPaid) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final isButtonDisabled = state.status == FinePaymentStatus.loading ||
|
||||
(!state.useBalance && state.selectedCard == null);
|
||||
|
||||
@@ -418,4 +457,32 @@ class _FinePaymentScreenContent extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDateTime(DateTime dateTime) {
|
||||
final now = DateTime.now();
|
||||
final localDate = dateTime.toLocal();
|
||||
|
||||
final isToday = localDate.year == now.year &&
|
||||
localDate.month == now.month &&
|
||||
localDate.day == now.day;
|
||||
|
||||
final yesterday = now.subtract(const Duration(days: 1));
|
||||
final isYesterday = localDate.year == yesterday.year &&
|
||||
localDate.month == yesterday.month &&
|
||||
localDate.day == yesterday.day;
|
||||
|
||||
final time = '${localDate.hour.toString().padLeft(2, '0')}:${localDate.minute.toString().padLeft(2, '0')}';
|
||||
|
||||
if (isToday) {
|
||||
return 'Сегодня, $time';
|
||||
} else if (isYesterday) {
|
||||
return 'Вчера, $time';
|
||||
} else {
|
||||
const months = [
|
||||
'января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
|
||||
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'
|
||||
];
|
||||
return '${localDate.day} ${months[localDate.month - 1]}, $time';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ class _EmptyState extends StatelessWidget {
|
||||
const SizedBox(height: 40),
|
||||
|
||||
const Text(
|
||||
'У вас пока нет завершенных поездок.',
|
||||
'У вас пока нет завершенных поездок',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
@@ -232,7 +232,7 @@ class _OrderCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final date = _formatDate(order.startAt ?? order.finishAt ?? DateTime.now());
|
||||
final scooterNumber = order.scooter?.title ?? '№${order.scooterId}';
|
||||
final price = '${order.totalPrice?.toStringAsFixed(2)} BYN' ?? '${order.totalPrice?.toStringAsFixed(2) ?? '0.00'} BYN';
|
||||
final price = '${order.totalPrice?.toStringAsFixed(2)} руб.' ?? '${order.totalPrice?.toStringAsFixed(2) ?? '0.00'} BYN';
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
|
||||
@@ -22,41 +22,6 @@ class PhoneScreen extends StatefulWidget {
|
||||
class _PhoneScreenState extends State<PhoneScreen> {
|
||||
final TextEditingController _phoneController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_showTestModeDialog();
|
||||
});
|
||||
}
|
||||
|
||||
void _showTestModeDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text(
|
||||
'Внимание!',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
content: const Text('Приложение находится в тестовом режиме!'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text(
|
||||
'Продолжить',
|
||||
style: TextStyle(color: Colors.blueAccent),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_phoneController.dispose();
|
||||
|
||||
@@ -26,21 +26,21 @@ class SupportScreen extends StatelessWidget {
|
||||
LinkRow(
|
||||
icon: 'assets/icons/telegram.png',
|
||||
title: 'Telegram',
|
||||
onTap: () => openLink('https://t.me/...'),
|
||||
onTap: () => openLink('https://t.me/BeHappySupportt'),
|
||||
),
|
||||
const Divider(height: 1, color: Colors.white24),
|
||||
const SizedBox(height: 12),
|
||||
LinkRow(
|
||||
icon: 'assets/icons/whatsapp.png',
|
||||
title: 'WhatsApp',
|
||||
onTap: () => openLink('https://wa.me/...'),
|
||||
onTap: () => openLink('https://wa.me/375257110909'),
|
||||
),
|
||||
const Divider(height: 1, color: Colors.white24),
|
||||
const SizedBox(height: 12),
|
||||
LinkRow(
|
||||
icon: 'assets/icons/viber.png',
|
||||
title: 'Viber',
|
||||
onTap: () => openLink('viber://chat?number=...'),
|
||||
onTap: () => openLink('viber://chat?number=+375257110909'),
|
||||
),
|
||||
const Divider(height: 1, color: Colors.white24),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Reference in New Issue
Block a user