fix to prod
This commit is contained in:
@@ -36,8 +36,8 @@ import '../models/subscriptions_response.dart';
|
||||
import '../models/user_check_response_dto.dart';
|
||||
|
||||
class ApiService {
|
||||
static const String baseUrl = "https://sharing-api.sparkit.by/api/v1";
|
||||
static const String fileBaseUrl = "https://sharing-api.sparkit.by";
|
||||
static const String baseUrl = "https://api.behappybel.by/api/v1";
|
||||
static const String fileBaseUrl = "https://api.behappybel.by/";
|
||||
|
||||
final SecurityService _securityService;
|
||||
final Dio _dio;
|
||||
@@ -935,11 +935,30 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /scooterorder/fines/client
|
||||
Future<List<Fine>> getClientFines() async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
"$baseUrl/scooterorder/fines/client",
|
||||
options: await _getAuthOptions(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 && response.data is Map) {
|
||||
final List<dynamic> dataList = response.data['data'] ?? [];
|
||||
return dataList.map((json) => Fine.fromJson(json)).toList();
|
||||
}
|
||||
return [];
|
||||
} on DioException catch (e) {
|
||||
_handleDioError(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /scooterorder/{id}/fine
|
||||
Future<Fine?> getFineDetails(int orderId) async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
"$baseUrl/scooterorder/$orderId/fine",
|
||||
"$baseUrl/scooterorder/fines/client/$orderId",
|
||||
options: await _getAuthOptions(),
|
||||
);
|
||||
|
||||
@@ -953,7 +972,8 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
/// PUT /scooterorder/{id}/fine
|
||||
/// PUT /scooterorder/fines/client/{id}
|
||||
/// {id} — это id штрафа (Fine.id), а не id заказа (Fine.orderId)
|
||||
Future<void> payFine({
|
||||
required int orderId,
|
||||
int? cardId,
|
||||
@@ -961,10 +981,10 @@ class ApiService {
|
||||
}) async {
|
||||
try {
|
||||
await _dio.put(
|
||||
"$baseUrl/scooterorder/$orderId/fine",
|
||||
"$baseUrl/scooterorder/fines/client/$orderId",
|
||||
data: {
|
||||
if (cardId != null) 'cardId': cardId,
|
||||
'isBalance': isBalance,
|
||||
"cardId": cardId ?? 0,
|
||||
"isBalance": isBalance,
|
||||
},
|
||||
options: await _getAuthOptions(),
|
||||
);
|
||||
|
||||
@@ -449,6 +449,24 @@ class ScooterRepositoryImpl extends ScooterRepository {
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<List<Fine>>> getClientFines() async {
|
||||
late final Result<List<Fine>> result;
|
||||
try {
|
||||
final fines = await _apiService.getClientFines();
|
||||
result = Success(fines);
|
||||
} on AuthException catch (e) {
|
||||
result = Failure(AuthFailure(e.attemptsLeft));
|
||||
} on UnauthorizedException catch (e) {
|
||||
result = Failure(AuthFailure(0, message: e.toString()));
|
||||
} on AuthBlockException catch (e) {
|
||||
result = Failure(AuthBlockFailure(e.toString()));
|
||||
} catch (e) {
|
||||
result = Failure(UnknownFailure("Неизвестная ошибка"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> payFine({
|
||||
required int orderId,
|
||||
|
||||
@@ -94,14 +94,18 @@ import '../domain/service/device_info_service.dart';
|
||||
import '../domain/usecase/activate_subscription_usecase.dart';
|
||||
import '../domain/usecase/apply_promo_code_usecase.dart';
|
||||
import '../domain/usecase/get_client_subscriptions_usecase.dart';
|
||||
import '../domain/usecase/get_fine_details_usecase.dart';
|
||||
import '../domain/usecase/get_fines_usecase.dart';
|
||||
import '../domain/usecase/get_news_by_id_usecase.dart';
|
||||
import '../domain/usecase/get_notifications_usecase.dart';
|
||||
import '../domain/usecase/get_scooter_by_title_usecase.dart';
|
||||
import '../domain/usecase/get_scooter_order_history_usecase.dart';
|
||||
import '../domain/usecase/pay_fine_usecase.dart';
|
||||
import '../domain/usecase/remove_payment_card_usecase.dart';
|
||||
import '../domain/usecase/set_main_payment_card_usecase.dart';
|
||||
import '../presentation/viewmodel/auth_bloc.dart';
|
||||
import '../presentation/viewmodel/edit_profile_bloc.dart';
|
||||
import '../presentation/viewmodel/fine_list_bloc.dart';
|
||||
import '../presentation/viewmodel/map_bloc.dart';
|
||||
import '../presentation/viewmodel/news_bloc.dart';
|
||||
import '../presentation/viewmodel/notifications_bloc.dart';
|
||||
@@ -391,6 +395,8 @@ Future<void> setupDependencies() async {
|
||||
() => OrderHistoryBloc(getIt<GetScooterOrderHistoryUsecase>()),
|
||||
);
|
||||
|
||||
getIt.registerFactory<FineListBloc>(() => FineListBloc(getIt()));
|
||||
|
||||
getIt.registerFactory<ScooterCodeBloc>(
|
||||
() => ScooterCodeBloc(
|
||||
getScooterByTitleUsecase: getIt<GetScooterByTitleUsecase>(),
|
||||
@@ -409,6 +415,12 @@ Future<void> setupDependencies() async {
|
||||
ApplyPromoCodeUsecase(getIt<PromoCodeRepository>()),
|
||||
);
|
||||
|
||||
getIt.registerFactory<GetFinesUsecase>(() => GetFinesUsecase(getIt()));
|
||||
|
||||
getIt.registerFactory<GetFineDetailsUsecase>(() => GetFineDetailsUsecase(getIt()));
|
||||
|
||||
getIt.registerFactory<PayFineUsecase>(() => PayFineUsecase(getIt()));
|
||||
|
||||
// Bloc (factory, т.к. экран создаёт новый экземпляр)
|
||||
getIt.registerFactory<PromoCodeBloc>(
|
||||
() => PromoCodeBloc(getIt<ApplyPromoCodeUsecase>()),
|
||||
|
||||
@@ -2,8 +2,9 @@ import 'package:equatable/equatable.dart';
|
||||
|
||||
class Fine extends Equatable {
|
||||
final int id;
|
||||
final String scooterNumber;
|
||||
final String date;
|
||||
final int orderId; // ← добавлено
|
||||
final String scooterNumber; // можно оставить для совместимости, но не использовать в UI
|
||||
final DateTime createdAtDateTime; // ← новое поле
|
||||
final String description;
|
||||
final double amount;
|
||||
final String amountPrint;
|
||||
@@ -12,8 +13,9 @@ class Fine extends Equatable {
|
||||
|
||||
const Fine({
|
||||
required this.id,
|
||||
required this.orderId,
|
||||
required this.scooterNumber,
|
||||
required this.date,
|
||||
required this.createdAtDateTime,
|
||||
required this.description,
|
||||
required this.amount,
|
||||
required this.amountPrint,
|
||||
@@ -28,22 +30,15 @@ class Fine extends Equatable {
|
||||
.where((path) => path.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
// Форматирование даты из createdAt (2026-08-07T09:15:26.565Z -> 07-08-2026)
|
||||
String formattedDate = '';
|
||||
if (json['createdAt'] != null) {
|
||||
final parsedDate = DateTime.tryParse(json['createdAt'].toString());
|
||||
if (parsedDate != null) {
|
||||
final day = parsedDate.day.toString().padLeft(2, '0');
|
||||
final month = parsedDate.month.toString().padLeft(2, '0');
|
||||
final year = parsedDate.year;
|
||||
formattedDate = '$day-$month-$year';
|
||||
}
|
||||
}
|
||||
// Парсим createdAt как DateTime
|
||||
final createdAtDateTime = DateTime.tryParse(json['createdAt'].toString()) ??
|
||||
DateTime.now(); // fallback
|
||||
|
||||
return Fine(
|
||||
id: json['id'] as int? ?? 0,
|
||||
orderId: json['orderId'] as int? ?? 0,
|
||||
scooterNumber: scooterNumber ?? '',
|
||||
date: formattedDate,
|
||||
createdAtDateTime: createdAtDateTime,
|
||||
description: json['description'] as String? ?? '',
|
||||
amount: (json['value'] as num?)?.toDouble() ?? 0.0,
|
||||
amountPrint: json['valuePrint'] as String? ?? '0,00 BYN',
|
||||
@@ -52,21 +47,12 @@ class Fine extends Equatable {
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'description': description,
|
||||
'value': amount,
|
||||
'valuePrint': amountPrint,
|
||||
'isPaid': isPaid,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
orderId,
|
||||
scooterNumber,
|
||||
date,
|
||||
createdAtDateTime,
|
||||
description,
|
||||
amount,
|
||||
amountPrint,
|
||||
|
||||
@@ -62,4 +62,6 @@ abstract class ScooterRepository {
|
||||
required bool isBalance,
|
||||
});
|
||||
|
||||
Future<Result<List<Fine>>> getClientFines();
|
||||
|
||||
}
|
||||
|
||||
13
lib/domain/usecase/get_fines_usecase.dart
Normal file
13
lib/domain/usecase/get_fines_usecase.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import '../../core/result.dart';
|
||||
import '../entities/fine.dart';
|
||||
import '../repositories/scooter_repository.dart';
|
||||
|
||||
class GetFinesUsecase {
|
||||
final ScooterRepository repository;
|
||||
|
||||
GetFinesUsecase(this.repository);
|
||||
|
||||
Future<Result<List<Fine>>> call() {
|
||||
return repository.getClientFines();
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,11 @@ class _FullScreenImageViewerState extends State<FullScreenImageViewer> {
|
||||
_pageController = PageController(initialPage: widget.initialIndex);
|
||||
}
|
||||
|
||||
String _resolveImageUrl(String path) {
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) return path;
|
||||
return 'https://sharing-api.sparkit.by/$path';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -35,12 +40,17 @@ class _FullScreenImageViewerState extends State<FullScreenImageViewer> {
|
||||
controller: _pageController,
|
||||
itemCount: widget.photoUrls.length,
|
||||
itemBuilder: (context, index) {
|
||||
final isNetwork = widget.photoUrls[index].startsWith('http');
|
||||
final photo = widget.photoUrls[index];
|
||||
final imageUrl = _resolveImageUrl(photo);
|
||||
return InteractiveViewer(
|
||||
child: Center(
|
||||
child: isNetwork
|
||||
? Image.network(widget.photoUrls[index])
|
||||
: Image.asset(widget.photoUrls[index]),
|
||||
child: Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Icon(Icons.error, color: Colors.white, size: 64);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -73,6 +73,14 @@ class SideMenu extends StatelessWidget {
|
||||
context.go('/home/profile');
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/fine.png',
|
||||
title: 'Штрафы',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go('/home/fine-list');
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/list_star_icon.png',
|
||||
title: 'История поездок',
|
||||
|
||||
5
lib/presentation/event/fine_list_event.dart
Normal file
5
lib/presentation/event/fine_list_event.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
abstract class FineListEvent {}
|
||||
|
||||
class FetchFinesRequested extends FineListEvent {}
|
||||
|
||||
class RefreshFinesRequested extends FineListEvent {}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:be_happy/presentation/screens/card_binding_result_screen.dart';
|
||||
import 'package:be_happy/presentation/screens/company_details.dart';
|
||||
import 'package:be_happy/presentation/screens/lease_agreement_screen.dart';
|
||||
import 'package:be_happy/presentation/screens/payment_rules_screen.dart';
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
@@ -79,6 +78,9 @@ import '../event/subscription_details_event.dart';
|
||||
import '../event/tariff_sheet_event.dart';
|
||||
import '../event/top_up_event.dart';
|
||||
import '../screens/add_card_screen.dart'; // ← новый импорт
|
||||
import '../screens/company_details_screen.dart';
|
||||
import '../screens/fine_list_screen.dart';
|
||||
import '../screens/fine_payment_screen.dart';
|
||||
import '../screens/license_agreement_screen.dart';
|
||||
import '../screens/notifications_screen.dart';
|
||||
import '../screens/order_history_screen.dart';
|
||||
@@ -545,6 +547,41 @@ class AppRouter {
|
||||
builder: (context, state) => const OrderHistoryScreen(),
|
||||
routes: []
|
||||
),
|
||||
GoRoute(
|
||||
path: 'fine-list',
|
||||
builder: (context, state) => const FineListScreen(),
|
||||
routes: []
|
||||
),
|
||||
GoRoute(
|
||||
path: 'fine-payment/:fineId',
|
||||
builder: (context, state) {
|
||||
final fineId = int.parse(state.pathParameters['fineId']!);
|
||||
return FinePaymentScreen(fineId: fineId);
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'result/:status',
|
||||
builder: (context, state) {
|
||||
final modeParam = state.pathParameters['status'];
|
||||
|
||||
CardBindingStatus status = CardBindingStatus.success;
|
||||
String title = 'Штраф успешно\nоплачен!';
|
||||
|
||||
if (modeParam == 'failed') {
|
||||
status = CardBindingStatus.failed;
|
||||
title = 'Извините, что-то\nпошло не так';
|
||||
}
|
||||
|
||||
return CardBindingResultScreen(
|
||||
status: status,
|
||||
title: title,
|
||||
cardImagePath: 'assets/icons/card_result_screen.png',
|
||||
redirectPath: '/home/fine-list',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: 'notifications',
|
||||
builder: (context, state) => const NotificationsScreen(),
|
||||
|
||||
@@ -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),
|
||||
|
||||
31
lib/presentation/state/fine_list_state.dart
Normal file
31
lib/presentation/state/fine_list_state.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../domain/entities/fine.dart';
|
||||
|
||||
enum FineListStatus { loading, success, failure, empty }
|
||||
|
||||
class FineListState extends Equatable {
|
||||
final FineListStatus status;
|
||||
final List<Fine> fines;
|
||||
final String? errorMessage;
|
||||
|
||||
const FineListState({
|
||||
this.status = FineListStatus.loading,
|
||||
this.fines = const [],
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
FineListState copyWith({
|
||||
FineListStatus? status,
|
||||
List<Fine>? fines,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return FineListState(
|
||||
status: status ?? this.status,
|
||||
fines: fines ?? this.fines,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, fines, errorMessage];
|
||||
}
|
||||
73
lib/presentation/viewmodel/fine_list_bloc.dart
Normal file
73
lib/presentation/viewmodel/fine_list_bloc.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../core/result.dart'; // Убедитесь, что импортирован!
|
||||
import '../../domain/entities/fine.dart';
|
||||
import '../../domain/usecase/get_fines_usecase.dart';
|
||||
import '../event/fine_list_event.dart';
|
||||
import '../state/fine_list_state.dart';
|
||||
|
||||
class FineListBloc extends Bloc<FineListEvent, FineListState> {
|
||||
final GetFinesUsecase getFinesUsecase;
|
||||
|
||||
FineListBloc(this.getFinesUsecase) : super(const FineListState()) {
|
||||
on<FetchFinesRequested>(_onFetchFines);
|
||||
on<RefreshFinesRequested>(_onRefreshFines);
|
||||
}
|
||||
|
||||
Future<void> _onFetchFines(
|
||||
FetchFinesRequested event,
|
||||
Emitter<FineListState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: FineListStatus.loading));
|
||||
try {
|
||||
final result = await getFinesUsecase(); // Возвращает Result<List<Fine>>
|
||||
|
||||
// Распаковываем Result
|
||||
if (result is Success<List<Fine>>) {
|
||||
final fines = result.data ?? []; // List<Fine>
|
||||
emit(state.copyWith(
|
||||
status: fines.isEmpty ? FineListStatus.empty : FineListStatus.success,
|
||||
fines: fines,
|
||||
));
|
||||
} else if (result is Failure<List<Fine>>) {
|
||||
emit(state.copyWith(
|
||||
status: FineListStatus.failure,
|
||||
errorMessage: result.failure.toString(), // или конкретное сообщение
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
status: FineListStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onRefreshFines(
|
||||
RefreshFinesRequested event,
|
||||
Emitter<FineListState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: FineListStatus.loading));
|
||||
try {
|
||||
final result = await getFinesUsecase(); // Возвращает Result<List<Fine>>
|
||||
|
||||
// Распаковываем Result
|
||||
if (result is Success<List<Fine>>) {
|
||||
final fines = result.data ?? []; // List<Fine>
|
||||
emit(state.copyWith(
|
||||
status: fines.isEmpty ? FineListStatus.empty : FineListStatus.success,
|
||||
fines: fines,
|
||||
));
|
||||
} else if (result is Failure<List<Fine>>) {
|
||||
emit(state.copyWith(
|
||||
status: FineListStatus.failure,
|
||||
errorMessage: result.failure.toString(), // или конкретное сообщение
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
status: FineListStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../core/result.dart';import '../../domain/entities/fine.dart';
|
||||
|
||||
import '../../domain/usecase/get_fine_details_usecase.dart';
|
||||
import '../../domain/usecase/pay_fine_usecase.dart';
|
||||
import '../event/fine_payment_event.dart';
|
||||
@@ -19,13 +21,37 @@ class FinePaymentBloc extends Bloc<FinePaymentEvent, FinePaymentState> {
|
||||
}
|
||||
|
||||
Future<void> _onFetchFineDetails(
|
||||
FetchFineDetails event,
|
||||
Emitter<FinePaymentState> emit,
|
||||
) async {
|
||||
FetchFineDetails event,
|
||||
Emitter<FinePaymentState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: FinePaymentStatus.loading));
|
||||
try {
|
||||
final fine = await getFineDetailsUsecase(event.fineId);
|
||||
// emit(state.copyWith(status: FinePaymentStatus.initial, fine: fine));
|
||||
final result = await getFineDetailsUsecase(event.fineId); // Возвращает Result<Fine>
|
||||
|
||||
// Распаковываем Result
|
||||
if (result is Success<Fine>) {
|
||||
final fine = result.data; // Fine?
|
||||
if (fine != null) {
|
||||
emit(state.copyWith(
|
||||
status: FinePaymentStatus.initial,
|
||||
fine: fine,
|
||||
));
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FinePaymentStatus.failure,
|
||||
errorMessage: "Не удалось получить данные о штрафе",
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (result is Failure<Fine>) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FinePaymentStatus.failure,
|
||||
errorMessage: result.failure.toString(), // или конкретное сообщение
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
|
||||
Reference in New Issue
Block a user