fix to prod

This commit is contained in:
2026-08-14 13:47:11 +03:00
parent f53fea576e
commit 80a95ece5b
27 changed files with 872 additions and 221 deletions

View File

@@ -26,7 +26,7 @@
<meta-data
android:name="com.yandex.runtime.API_KEY"
android:value="a0ef1404-2650-4f28-9891-c965ecc09174" />
android:value="45fe500d-b549-4444-9e4b-3b2acd78ee2c" />
<activity
android:name=".MainActivity"

View File

@@ -6,7 +6,7 @@ import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity() {
override protected fun onCreate(savedInstanceState: Bundle?) {
MapKitFactory.setApiKey("a0ef1404-2650-4f28-9891-c965ecc09174")
MapKitFactory.setApiKey("45fe500d-b549-4444-9e4b-3b2acd78ee2c")
super.onCreate(savedInstanceState)
}
}

BIN
assets/icons/fine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,6 +1,6 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.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)
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386

View File

@@ -1,5 +1,5 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"
#include "Generated.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

View File

@@ -1,5 +1,5 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.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

View File

@@ -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(),
);

View File

@@ -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,

View File

@@ -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>()),

View File

@@ -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,

View File

@@ -62,4 +62,6 @@ abstract class ScooterRepository {
required bool isBalance,
});
Future<Result<List<Fine>>> getClientFines();
}

View 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();
}
}

View File

@@ -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);
},
),
),
);
},

View File

@@ -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: 'История поездок',

View File

@@ -0,0 +1,5 @@
abstract class FineListEvent {}
class FetchFinesRequested extends FineListEvent {}
class RefreshFinesRequested extends FineListEvent {}

View File

@@ -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(),

View File

@@ -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(

View 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]}';
}
}

View File

@@ -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,8 +96,13 @@ 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) {
if (state.fine != null) {
// штраф уже был загружен — значит ошибка произошла при попытке оплаты
context.go('/home/fine-payment/$fineId/result/failed');
} else {
// штраф не удалось загрузить — остаёмся на месте, показываем снекбар
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
@@ -107,6 +120,7 @@ class _FinePaymentScreenContent extends StatelessWidget {
),
);
}
}
},
builder: (context, state) {
final fine = state.fine;
@@ -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,
child: Image.network(
imageUrl,
width: 110,
height: 110,
fit: BoxFit.cover,
)
: Image.asset(
photo,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return SizedBox(
width: 110,
height: 110,
fit: BoxFit.cover,
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,6 +277,7 @@ class _FinePaymentScreenContent extends StatelessWidget {
const SizedBox(height: 28),
if (!isPaid)
(state.useBalance || state.selectedCard != null)
? PaymentOption(
title: state.useBalance ? 'Баланс' : "",
@@ -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';
}
}
}

View File

@@ -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: () {

View File

@@ -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();

View File

@@ -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),

View 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];
}

View 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(),
));
}
}
}

View File

@@ -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';
@@ -24,8 +26,32 @@ class FinePaymentBloc extends Bloc<FinePaymentEvent, FinePaymentState> {
) 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(

View File

@@ -2,7 +2,7 @@ name: be_happy
description: ""
publish_to: 'none'
version: 1.0.0+21
version: 1.0.0+22
environment:
sdk: ^3.8.1