fix to prod
This commit is contained in:
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.yandex.runtime.API_KEY"
|
android:name="com.yandex.runtime.API_KEY"
|
||||||
android:value="a0ef1404-2650-4f28-9891-c965ecc09174" />
|
android:value="45fe500d-b549-4444-9e4b-3b2acd78ee2c" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import io.flutter.embedding.android.FlutterActivity
|
|||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
override protected fun onCreate(savedInstanceState: Bundle?) {
|
override protected fun onCreate(savedInstanceState: Bundle?) {
|
||||||
MapKitFactory.setApiKey("a0ef1404-2650-4f28-9891-c965ecc09174")
|
MapKitFactory.setApiKey("45fe500d-b549-4444-9e4b-3b2acd78ee2c")
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
assets/icons/fine.png
Normal file
BIN
assets/icons/fine.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -1,6 +1,6 @@
|
|||||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
#include "Generated.xcconfig"
|
#include "Generated.xcconfig"
|
||||||
#include? "Secrets.xcconfig"
|
#include? "Secrets.xcconfig"
|
||||||
YANDEX_MAPKIT_API_KEY=a0ef1404-2650-4f28-9891-c965ecc09174
|
YANDEX_MAPKIT_API_KEY=45fe500d-b549-4444-9e4b-3b2acd78ee2c
|
||||||
// Переопределение Pods/ML Kit: arm64 для симулятора на Apple Silicon (M1/M4)
|
// Переопределение Pods/ML Kit: arm64 для симулятора на Apple Silicon (M1/M4)
|
||||||
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
|
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"
|
||||||
#include "Generated.xcconfig"
|
#include "Generated.xcconfig"
|
||||||
#include? "Secrets.xcconfig"
|
#include? "Secrets.xcconfig"
|
||||||
YANDEX_MAPKIT_API_KEY=a0ef1404-2650-4f28-9891-c965ecc09174
|
YANDEX_MAPKIT_API_KEY=45fe500d-b549-4444-9e4b-3b2acd78ee2c
|
||||||
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
|
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
#include "Generated.xcconfig"
|
#include "Generated.xcconfig"
|
||||||
#include? "Secrets.xcconfig"
|
#include? "Secrets.xcconfig"
|
||||||
YANDEX_MAPKIT_API_KEY=a0ef1404-2650-4f28-9891-c965ecc09174
|
YANDEX_MAPKIT_API_KEY=45fe500d-b549-4444-9e4b-3b2acd78ee2c
|
||||||
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
|
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ import '../models/subscriptions_response.dart';
|
|||||||
import '../models/user_check_response_dto.dart';
|
import '../models/user_check_response_dto.dart';
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
static const String baseUrl = "https://sharing-api.sparkit.by/api/v1";
|
static const String baseUrl = "https://api.behappybel.by/api/v1";
|
||||||
static const String fileBaseUrl = "https://sharing-api.sparkit.by";
|
static const String fileBaseUrl = "https://api.behappybel.by/";
|
||||||
|
|
||||||
final SecurityService _securityService;
|
final SecurityService _securityService;
|
||||||
final Dio _dio;
|
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
|
/// GET /scooterorder/{id}/fine
|
||||||
Future<Fine?> getFineDetails(int orderId) async {
|
Future<Fine?> getFineDetails(int orderId) async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.get(
|
final response = await _dio.get(
|
||||||
"$baseUrl/scooterorder/$orderId/fine",
|
"$baseUrl/scooterorder/fines/client/$orderId",
|
||||||
options: await _getAuthOptions(),
|
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({
|
Future<void> payFine({
|
||||||
required int orderId,
|
required int orderId,
|
||||||
int? cardId,
|
int? cardId,
|
||||||
@@ -961,10 +981,10 @@ class ApiService {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
await _dio.put(
|
await _dio.put(
|
||||||
"$baseUrl/scooterorder/$orderId/fine",
|
"$baseUrl/scooterorder/fines/client/$orderId",
|
||||||
data: {
|
data: {
|
||||||
if (cardId != null) 'cardId': cardId,
|
"cardId": cardId ?? 0,
|
||||||
'isBalance': isBalance,
|
"isBalance": isBalance,
|
||||||
},
|
},
|
||||||
options: await _getAuthOptions(),
|
options: await _getAuthOptions(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -449,6 +449,24 @@ class ScooterRepositoryImpl extends ScooterRepository {
|
|||||||
return result;
|
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
|
@override
|
||||||
Future<Result<void>> payFine({
|
Future<Result<void>> payFine({
|
||||||
required int orderId,
|
required int orderId,
|
||||||
|
|||||||
@@ -94,14 +94,18 @@ import '../domain/service/device_info_service.dart';
|
|||||||
import '../domain/usecase/activate_subscription_usecase.dart';
|
import '../domain/usecase/activate_subscription_usecase.dart';
|
||||||
import '../domain/usecase/apply_promo_code_usecase.dart';
|
import '../domain/usecase/apply_promo_code_usecase.dart';
|
||||||
import '../domain/usecase/get_client_subscriptions_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_news_by_id_usecase.dart';
|
||||||
import '../domain/usecase/get_notifications_usecase.dart';
|
import '../domain/usecase/get_notifications_usecase.dart';
|
||||||
import '../domain/usecase/get_scooter_by_title_usecase.dart';
|
import '../domain/usecase/get_scooter_by_title_usecase.dart';
|
||||||
import '../domain/usecase/get_scooter_order_history_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/remove_payment_card_usecase.dart';
|
||||||
import '../domain/usecase/set_main_payment_card_usecase.dart';
|
import '../domain/usecase/set_main_payment_card_usecase.dart';
|
||||||
import '../presentation/viewmodel/auth_bloc.dart';
|
import '../presentation/viewmodel/auth_bloc.dart';
|
||||||
import '../presentation/viewmodel/edit_profile_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/map_bloc.dart';
|
||||||
import '../presentation/viewmodel/news_bloc.dart';
|
import '../presentation/viewmodel/news_bloc.dart';
|
||||||
import '../presentation/viewmodel/notifications_bloc.dart';
|
import '../presentation/viewmodel/notifications_bloc.dart';
|
||||||
@@ -391,6 +395,8 @@ Future<void> setupDependencies() async {
|
|||||||
() => OrderHistoryBloc(getIt<GetScooterOrderHistoryUsecase>()),
|
() => OrderHistoryBloc(getIt<GetScooterOrderHistoryUsecase>()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
getIt.registerFactory<FineListBloc>(() => FineListBloc(getIt()));
|
||||||
|
|
||||||
getIt.registerFactory<ScooterCodeBloc>(
|
getIt.registerFactory<ScooterCodeBloc>(
|
||||||
() => ScooterCodeBloc(
|
() => ScooterCodeBloc(
|
||||||
getScooterByTitleUsecase: getIt<GetScooterByTitleUsecase>(),
|
getScooterByTitleUsecase: getIt<GetScooterByTitleUsecase>(),
|
||||||
@@ -409,6 +415,12 @@ Future<void> setupDependencies() async {
|
|||||||
ApplyPromoCodeUsecase(getIt<PromoCodeRepository>()),
|
ApplyPromoCodeUsecase(getIt<PromoCodeRepository>()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
getIt.registerFactory<GetFinesUsecase>(() => GetFinesUsecase(getIt()));
|
||||||
|
|
||||||
|
getIt.registerFactory<GetFineDetailsUsecase>(() => GetFineDetailsUsecase(getIt()));
|
||||||
|
|
||||||
|
getIt.registerFactory<PayFineUsecase>(() => PayFineUsecase(getIt()));
|
||||||
|
|
||||||
// Bloc (factory, т.к. экран создаёт новый экземпляр)
|
// Bloc (factory, т.к. экран создаёт новый экземпляр)
|
||||||
getIt.registerFactory<PromoCodeBloc>(
|
getIt.registerFactory<PromoCodeBloc>(
|
||||||
() => PromoCodeBloc(getIt<ApplyPromoCodeUsecase>()),
|
() => PromoCodeBloc(getIt<ApplyPromoCodeUsecase>()),
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import 'package:equatable/equatable.dart';
|
|||||||
|
|
||||||
class Fine extends Equatable {
|
class Fine extends Equatable {
|
||||||
final int id;
|
final int id;
|
||||||
final String scooterNumber;
|
final int orderId; // ← добавлено
|
||||||
final String date;
|
final String scooterNumber; // можно оставить для совместимости, но не использовать в UI
|
||||||
|
final DateTime createdAtDateTime; // ← новое поле
|
||||||
final String description;
|
final String description;
|
||||||
final double amount;
|
final double amount;
|
||||||
final String amountPrint;
|
final String amountPrint;
|
||||||
@@ -12,8 +13,9 @@ class Fine extends Equatable {
|
|||||||
|
|
||||||
const Fine({
|
const Fine({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
required this.orderId,
|
||||||
required this.scooterNumber,
|
required this.scooterNumber,
|
||||||
required this.date,
|
required this.createdAtDateTime,
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.amount,
|
required this.amount,
|
||||||
required this.amountPrint,
|
required this.amountPrint,
|
||||||
@@ -28,22 +30,15 @@ class Fine extends Equatable {
|
|||||||
.where((path) => path.isNotEmpty)
|
.where((path) => path.isNotEmpty)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// Форматирование даты из createdAt (2026-08-07T09:15:26.565Z -> 07-08-2026)
|
// Парсим createdAt как DateTime
|
||||||
String formattedDate = '';
|
final createdAtDateTime = DateTime.tryParse(json['createdAt'].toString()) ??
|
||||||
if (json['createdAt'] != null) {
|
DateTime.now(); // fallback
|
||||||
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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Fine(
|
return Fine(
|
||||||
id: json['id'] as int? ?? 0,
|
id: json['id'] as int? ?? 0,
|
||||||
|
orderId: json['orderId'] as int? ?? 0,
|
||||||
scooterNumber: scooterNumber ?? '',
|
scooterNumber: scooterNumber ?? '',
|
||||||
date: formattedDate,
|
createdAtDateTime: createdAtDateTime,
|
||||||
description: json['description'] as String? ?? '',
|
description: json['description'] as String? ?? '',
|
||||||
amount: (json['value'] as num?)?.toDouble() ?? 0.0,
|
amount: (json['value'] as num?)?.toDouble() ?? 0.0,
|
||||||
amountPrint: json['valuePrint'] as String? ?? '0,00 BYN',
|
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
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
id,
|
id,
|
||||||
|
orderId,
|
||||||
scooterNumber,
|
scooterNumber,
|
||||||
date,
|
createdAtDateTime,
|
||||||
description,
|
description,
|
||||||
amount,
|
amount,
|
||||||
amountPrint,
|
amountPrint,
|
||||||
|
|||||||
@@ -62,4 +62,6 @@ abstract class ScooterRepository {
|
|||||||
required bool isBalance,
|
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);
|
_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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -35,12 +40,17 @@ class _FullScreenImageViewerState extends State<FullScreenImageViewer> {
|
|||||||
controller: _pageController,
|
controller: _pageController,
|
||||||
itemCount: widget.photoUrls.length,
|
itemCount: widget.photoUrls.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final isNetwork = widget.photoUrls[index].startsWith('http');
|
final photo = widget.photoUrls[index];
|
||||||
|
final imageUrl = _resolveImageUrl(photo);
|
||||||
return InteractiveViewer(
|
return InteractiveViewer(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: isNetwork
|
child: Image.network(
|
||||||
? Image.network(widget.photoUrls[index])
|
imageUrl,
|
||||||
: Image.asset(widget.photoUrls[index]),
|
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');
|
context.go('/home/profile');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
_MenuItem(
|
||||||
|
icon: 'assets/icons/fine.png',
|
||||||
|
title: 'Штрафы',
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
context.go('/home/fine-list');
|
||||||
|
},
|
||||||
|
),
|
||||||
_MenuItem(
|
_MenuItem(
|
||||||
icon: 'assets/icons/list_star_icon.png',
|
icon: 'assets/icons/list_star_icon.png',
|
||||||
title: 'История поездок',
|
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 'dart:async';
|
||||||
|
|
||||||
import 'package:be_happy/presentation/screens/card_binding_result_screen.dart';
|
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/lease_agreement_screen.dart';
|
||||||
import 'package:be_happy/presentation/screens/payment_rules_screen.dart';
|
import 'package:be_happy/presentation/screens/payment_rules_screen.dart';
|
||||||
import 'package:bot_toast/bot_toast.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/tariff_sheet_event.dart';
|
||||||
import '../event/top_up_event.dart';
|
import '../event/top_up_event.dart';
|
||||||
import '../screens/add_card_screen.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/license_agreement_screen.dart';
|
||||||
import '../screens/notifications_screen.dart';
|
import '../screens/notifications_screen.dart';
|
||||||
import '../screens/order_history_screen.dart';
|
import '../screens/order_history_screen.dart';
|
||||||
@@ -545,6 +547,41 @@ class AppRouter {
|
|||||||
builder: (context, state) => const OrderHistoryScreen(),
|
builder: (context, state) => const OrderHistoryScreen(),
|
||||||
routes: []
|
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(
|
GoRoute(
|
||||||
path: 'notifications',
|
path: 'notifications',
|
||||||
builder: (context, state) => const NotificationsScreen(),
|
builder: (context, state) => const NotificationsScreen(),
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ class CardBindingResultScreen extends StatefulWidget {
|
|||||||
final CardBindingStatus status;
|
final CardBindingStatus status;
|
||||||
final String title;
|
final String title;
|
||||||
final String cardImagePath;
|
final String cardImagePath;
|
||||||
|
final String redirectPath; // ← новое поле
|
||||||
|
|
||||||
const CardBindingResultScreen({
|
const CardBindingResultScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.status,
|
required this.status,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.cardImagePath,
|
required this.cardImagePath,
|
||||||
|
this.redirectPath = '/home/payment-methods', // сохраняем текущее поведение по умолчанию
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -32,7 +34,7 @@ class _CardBindingResultScreenState extends State<CardBindingResultScreen> {
|
|||||||
_autoPopTimer = Timer(const Duration(seconds: 5), () {
|
_autoPopTimer = Timer(const Duration(seconds: 5), () {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
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: "Попробовать снова",
|
text: "Попробовать снова",
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
context.go("/home/payment-methods?refreshId=$timestamp");
|
context.go("${widget.redirectPath}?refreshId=$timestamp");
|
||||||
},
|
},
|
||||||
enabled: true,
|
enabled: true,
|
||||||
showArrows: true,
|
showArrows: true,
|
||||||
@@ -142,7 +144,7 @@ class _CardBindingResultScreenState extends State<CardBindingResultScreen> {
|
|||||||
onPressed: ()
|
onPressed: ()
|
||||||
{
|
{
|
||||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
context.go("/home/payment-methods?refreshId=$timestamp");
|
context.go("${widget.redirectPath}?refreshId=$timestamp");
|
||||||
},
|
},
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
shape: RoundedRectangleBorder(
|
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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../core/app_colors.dart';
|
import '../../core/app_colors.dart';
|
||||||
|
import '../../data/network/api_service.dart';
|
||||||
import '../../di/service_locator.dart';
|
import '../../di/service_locator.dart';
|
||||||
import '../../domain/entities/payment_card.dart';
|
import '../../domain/entities/payment_card.dart';
|
||||||
import '../../domain/usecase/add_payment_card_usecase.dart';
|
import '../../domain/usecase/add_payment_card_usecase.dart';
|
||||||
@@ -34,13 +35,20 @@ class FinePaymentScreen extends StatelessWidget {
|
|||||||
getFineDetailsUsecase: getIt(),
|
getFineDetailsUsecase: getIt(),
|
||||||
payFineUsecase: getIt(),
|
payFineUsecase: getIt(),
|
||||||
)..add(FetchFineDetails(fineId)),
|
)..add(FetchFineDetails(fineId)),
|
||||||
child: const _FinePaymentScreenContent(),
|
child: _FinePaymentScreenContent(fineId: fineId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FinePaymentScreenContent extends StatelessWidget {
|
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) {
|
void _openFullScreenImage(BuildContext context, List<String> photos, int initialIndex) {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
@@ -88,24 +96,30 @@ class _FinePaymentScreenContent extends StatelessWidget {
|
|||||||
listenWhen: (previous, current) => previous.status != current.status,
|
listenWhen: (previous, current) => previous.status != current.status,
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.status == FinePaymentStatus.success && state.paymentCompleted) {
|
if (state.status == FinePaymentStatus.success && state.paymentCompleted) {
|
||||||
context.go('/home');
|
context.go('/home/fine-payment/$fineId/result/success');
|
||||||
} else if (state.status == FinePaymentStatus.failure) {
|
} else if (state.status == FinePaymentStatus.failure) {
|
||||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
if (state.fine != null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
// штраф уже был загружен — значит ошибка произошла при попытке оплаты
|
||||||
SnackBar(
|
context.go('/home/fine-payment/$fineId/result/failed');
|
||||||
content: Text(
|
} else {
|
||||||
state.errorMessage ?? 'Произошла ошибка при оплате штрафа',
|
// штраф не удалось загрузить — остаёмся на месте, показываем снекбар
|
||||||
style: const TextStyle(color: Colors.white),
|
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) {
|
builder: (context, state) {
|
||||||
@@ -122,6 +136,8 @@ class _FinePaymentScreenContent extends StatelessWidget {
|
|||||||
|
|
||||||
if (fine == null) return const SizedBox.shrink();
|
if (fine == null) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
final bool isPaid = fine.isPaid;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -136,7 +152,7 @@ class _FinePaymentScreenContent extends StatelessWidget {
|
|||||||
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final photo = fine.photoUrls[index];
|
final photo = fine.photoUrls[index];
|
||||||
final isNetwork = photo.startsWith('http');
|
final imageUrl = _resolveImageUrl(photo);
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => _openFullScreenImage(
|
onTap: () => _openFullScreenImage(
|
||||||
context,
|
context,
|
||||||
@@ -145,18 +161,35 @@ class _FinePaymentScreenContent extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: isNetwork
|
child: Image.network(
|
||||||
? Image.network(
|
imageUrl,
|
||||||
photo,
|
|
||||||
width: 110,
|
|
||||||
height: 110,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
)
|
|
||||||
: Image.asset(
|
|
||||||
photo,
|
|
||||||
width: 110,
|
width: 110,
|
||||||
height: 110,
|
height: 110,
|
||||||
fit: BoxFit.cover,
|
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),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
_buildInfoLabel('Самокат:'),
|
_buildInfoLabel('Номер заказа:'),
|
||||||
_buildValueRow(
|
_buildValueRow(
|
||||||
'qr_icon.png',
|
'qr_icon.png',
|
||||||
fine.scooterNumber,
|
'${fine.orderId}',
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
_buildInfoLabel('Дата:'),
|
_buildInfoLabel('Дата и время:'),
|
||||||
Text(
|
Text(
|
||||||
fine.date,
|
_formatDateTime(fine.createdAtDateTime),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
@@ -244,111 +277,112 @@ class _FinePaymentScreenContent extends StatelessWidget {
|
|||||||
|
|
||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
|
|
||||||
(state.useBalance || state.selectedCard != null)
|
if (!isPaid)
|
||||||
? PaymentOption(
|
(state.useBalance || state.selectedCard != null)
|
||||||
title: state.useBalance ? 'Баланс' : "",
|
? PaymentOption(
|
||||||
subtitle: state.useBalance
|
title: state.useBalance ? 'Баланс' : "",
|
||||||
? '${state.userBalance.toStringAsFixed(2)} BYN'
|
subtitle: state.useBalance
|
||||||
: '${state.selectedCard!.cardLastNumber}',
|
? '${state.userBalance.toStringAsFixed(2)} BYN'
|
||||||
isSelected: true,
|
: '${state.selectedCard!.cardLastNumber}',
|
||||||
onTap: () async {
|
isSelected: true,
|
||||||
final result = await showModalBottomSheet<dynamic>(
|
onTap: () async {
|
||||||
context: context,
|
final result = await showModalBottomSheet<dynamic>(
|
||||||
isScrollControlled: true,
|
context: context,
|
||||||
backgroundColor: Colors.transparent,
|
isScrollControlled: true,
|
||||||
builder: (innerContext) => BlocProvider(
|
backgroundColor: Colors.transparent,
|
||||||
create: (context) => PaymentMethodSheetBloc(
|
builder: (innerContext) => BlocProvider(
|
||||||
getIt<GetPaymentCardsUsecase>(),
|
create: (context) => PaymentMethodSheetBloc(
|
||||||
getIt<SetMainPaymentCardUsecase>(),
|
getIt<GetPaymentCardsUsecase>(),
|
||||||
getIt<GetProfileUseCase>(),
|
getIt<SetMainPaymentCardUsecase>(),
|
||||||
getIt<AddPaymentCardUsecase>(),
|
getIt<GetProfileUseCase>(),
|
||||||
)..add(PaymentMethodSheetStarted()),
|
getIt<AddPaymentCardUsecase>(),
|
||||||
child: PaymentMethodSheet(
|
)..add(PaymentMethodSheetStarted()),
|
||||||
initialSelectedCard: state.useBalance ? null : state.selectedCard,
|
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 (result != null && context.mounted) {
|
if (result != null && context.mounted) {
|
||||||
if (result is PaymentCard) {
|
if (result is PaymentCard) {
|
||||||
context.read<FinePaymentBloc>().add(FinePaymentCardChanged(result));
|
context.read<FinePaymentBloc>().add(FinePaymentCardChanged(result));
|
||||||
} else if (result == 'balance') {
|
} else if (result == 'balance') {
|
||||||
context.read<FinePaymentBloc>().add(SelectFineBalancePressed());
|
context.read<FinePaymentBloc>().add(SelectFineBalancePressed());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
height: 56,
|
||||||
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
child: Center(
|
border: Border.all(
|
||||||
child: Row(
|
color: Colors.white.withOpacity(0.4),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
width: 1,
|
||||||
children: [
|
),
|
||||||
const Text(
|
),
|
||||||
'Способ оплаты',
|
child: Material(
|
||||||
style: TextStyle(
|
color: Colors.transparent,
|
||||||
color: Colors.white,
|
child: InkWell(
|
||||||
fontSize: 16,
|
onTap: () async {
|
||||||
fontWeight: FontWeight.w600,
|
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),
|
||||||
const SizedBox(width: 8),
|
Icon(
|
||||||
Icon(
|
Icons.arrow_forward_ios,
|
||||||
Icons.arrow_forward_ios,
|
size: 12,
|
||||||
size: 12,
|
color: Colors.white.withOpacity(0.6),
|
||||||
color: Colors.white.withOpacity(0.6),
|
),
|
||||||
),
|
Icon(
|
||||||
Icon(
|
Icons.arrow_forward_ios,
|
||||||
Icons.arrow_forward_ios,
|
size: 12,
|
||||||
size: 12,
|
color: Colors.white.withOpacity(0.4),
|
||||||
color: Colors.white.withOpacity(0.4),
|
),
|
||||||
),
|
Icon(
|
||||||
Icon(
|
Icons.arrow_forward_ios,
|
||||||
Icons.arrow_forward_ios,
|
size: 12,
|
||||||
size: 12,
|
color: Colors.white.withOpacity(0.2),
|
||||||
color: Colors.white.withOpacity(0.2),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -361,6 +395,11 @@ class _FinePaymentScreenContent extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: BlocBuilder<FinePaymentBloc, FinePaymentState>(
|
child: BlocBuilder<FinePaymentBloc, FinePaymentState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
final fine = state.fine;
|
||||||
|
if (fine == null || fine.isPaid) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
final isButtonDisabled = state.status == FinePaymentStatus.loading ||
|
final isButtonDisabled = state.status == FinePaymentStatus.loading ||
|
||||||
(!state.useBalance && state.selectedCard == null);
|
(!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 SizedBox(height: 40),
|
||||||
|
|
||||||
const Text(
|
const Text(
|
||||||
'У вас пока нет завершенных поездок.',
|
'У вас пока нет завершенных поездок',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -232,7 +232,7 @@ class _OrderCard extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final date = _formatDate(order.startAt ?? order.finishAt ?? DateTime.now());
|
final date = _formatDate(order.startAt ?? order.finishAt ?? DateTime.now());
|
||||||
final scooterNumber = order.scooter?.title ?? '№${order.scooterId}';
|
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(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|||||||
@@ -22,41 +22,6 @@ class PhoneScreen extends StatefulWidget {
|
|||||||
class _PhoneScreenState extends State<PhoneScreen> {
|
class _PhoneScreenState extends State<PhoneScreen> {
|
||||||
final TextEditingController _phoneController = TextEditingController();
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_phoneController.dispose();
|
_phoneController.dispose();
|
||||||
|
|||||||
@@ -26,21 +26,21 @@ class SupportScreen extends StatelessWidget {
|
|||||||
LinkRow(
|
LinkRow(
|
||||||
icon: 'assets/icons/telegram.png',
|
icon: 'assets/icons/telegram.png',
|
||||||
title: 'Telegram',
|
title: 'Telegram',
|
||||||
onTap: () => openLink('https://t.me/...'),
|
onTap: () => openLink('https://t.me/BeHappySupportt'),
|
||||||
),
|
),
|
||||||
const Divider(height: 1, color: Colors.white24),
|
const Divider(height: 1, color: Colors.white24),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
LinkRow(
|
LinkRow(
|
||||||
icon: 'assets/icons/whatsapp.png',
|
icon: 'assets/icons/whatsapp.png',
|
||||||
title: 'WhatsApp',
|
title: 'WhatsApp',
|
||||||
onTap: () => openLink('https://wa.me/...'),
|
onTap: () => openLink('https://wa.me/375257110909'),
|
||||||
),
|
),
|
||||||
const Divider(height: 1, color: Colors.white24),
|
const Divider(height: 1, color: Colors.white24),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
LinkRow(
|
LinkRow(
|
||||||
icon: 'assets/icons/viber.png',
|
icon: 'assets/icons/viber.png',
|
||||||
title: 'Viber',
|
title: 'Viber',
|
||||||
onTap: () => openLink('viber://chat?number=...'),
|
onTap: () => openLink('viber://chat?number=+375257110909'),
|
||||||
),
|
),
|
||||||
const Divider(height: 1, color: Colors.white24),
|
const Divider(height: 1, color: Colors.white24),
|
||||||
const SizedBox(height: 12),
|
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 '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/get_fine_details_usecase.dart';
|
||||||
import '../../domain/usecase/pay_fine_usecase.dart';
|
import '../../domain/usecase/pay_fine_usecase.dart';
|
||||||
import '../event/fine_payment_event.dart';
|
import '../event/fine_payment_event.dart';
|
||||||
@@ -19,13 +21,37 @@ class FinePaymentBloc extends Bloc<FinePaymentEvent, FinePaymentState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onFetchFineDetails(
|
Future<void> _onFetchFineDetails(
|
||||||
FetchFineDetails event,
|
FetchFineDetails event,
|
||||||
Emitter<FinePaymentState> emit,
|
Emitter<FinePaymentState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(status: FinePaymentStatus.loading));
|
emit(state.copyWith(status: FinePaymentStatus.loading));
|
||||||
try {
|
try {
|
||||||
final fine = await getFineDetailsUsecase(event.fineId);
|
final result = await getFineDetailsUsecase(event.fineId); // Возвращает Result<Fine>
|
||||||
// emit(state.copyWith(status: FinePaymentStatus.initial, fine: 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) {
|
} catch (e) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: be_happy
|
|||||||
description: ""
|
description: ""
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.0.0+21
|
version: 1.0.0+22
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.8.1
|
sdk: ^3.8.1
|
||||||
|
|||||||
Reference in New Issue
Block a user