fix
This commit is contained in:
3
devtools_options.yaml
Normal file
3
devtools_options.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../domain/entities/client_notification.dart';
|
||||
|
||||
class ClientNotificationDto {
|
||||
@@ -43,12 +42,19 @@ class ClientNotificationDto {
|
||||
clientId: clientId,
|
||||
type: _parseType(type),
|
||||
category: _parseCategory(category),
|
||||
createdAt: DateTime.parse(createdAt),
|
||||
canceledAt: canceledAt != null ? DateTime.parse(canceledAt!) : null,
|
||||
readAt: readAt != null ? DateTime.parse(readAt!) : null,
|
||||
createdAt: _parseUtcDate(createdAt),
|
||||
canceledAt: canceledAt != null ? _parseUtcDate(canceledAt!) : null,
|
||||
readAt: readAt != null ? _parseUtcDate(readAt!) : null,
|
||||
);
|
||||
}
|
||||
|
||||
DateTime _parseUtcDate(String dateString) {
|
||||
if (dateString.endsWith('Z') || dateString.contains('+') || dateString.contains('-')) {
|
||||
return DateTime.parse(dateString).toLocal();
|
||||
}
|
||||
return DateTime.parse('${dateString}Z').toLocal();
|
||||
}
|
||||
|
||||
NotificationType _parseType(String type) {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'info':
|
||||
|
||||
@@ -147,11 +147,12 @@ class ApiService {
|
||||
}
|
||||
|
||||
dynamic balanceRaw = profileData["balance"];
|
||||
int? balance;
|
||||
if (balanceRaw is int) {
|
||||
balance = balanceRaw;
|
||||
double? balance;
|
||||
|
||||
if (balanceRaw is num) {
|
||||
balance = balanceRaw.toDouble();
|
||||
} else if (balanceRaw is String) {
|
||||
balance = int.tryParse(balanceRaw);
|
||||
balance = double.tryParse(balanceRaw);
|
||||
}
|
||||
|
||||
return UserProfile(
|
||||
@@ -238,11 +239,11 @@ class ApiService {
|
||||
}
|
||||
|
||||
dynamic balanceRaw = data["balance"];
|
||||
int? balance;
|
||||
if (balanceRaw is int) {
|
||||
double? balance;
|
||||
if (balanceRaw is double) {
|
||||
balance = balanceRaw;
|
||||
} else if (balanceRaw is String) {
|
||||
balance = int.tryParse(balanceRaw);
|
||||
balance = double.tryParse(balanceRaw);
|
||||
}
|
||||
|
||||
return UserProfile(
|
||||
@@ -420,7 +421,7 @@ class ApiService {
|
||||
int page = 1,
|
||||
int pageSize = 500,
|
||||
}) async {
|
||||
const path = "$baseUrl/zone/available";
|
||||
const path = "$baseUrl/zone/available2";
|
||||
|
||||
final queryParams = {
|
||||
'readAll': 'true',
|
||||
@@ -708,17 +709,13 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ScooterOrder?> cancelRide(int orderId) async {
|
||||
Future<void> cancelRide(int orderId) async {
|
||||
try {
|
||||
final response = await _dio.put(
|
||||
"$baseUrl/scooterorder/$orderId/cancel",
|
||||
options: await _getAuthOptions(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
return ScooterOrder.fromJson(response.data);
|
||||
}
|
||||
return null;
|
||||
} on DioException catch (e) {
|
||||
_handleDioError(e);
|
||||
return null;
|
||||
@@ -759,7 +756,7 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ScooterOrder?> finishRide({
|
||||
Future<void> finishRide({
|
||||
required int orderId,
|
||||
required List<int> filesId,
|
||||
}) async {
|
||||
@@ -770,10 +767,6 @@ class ApiService {
|
||||
options: await _getAuthOptions(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
return ScooterOrder.fromJson(response.data);
|
||||
}
|
||||
return null;
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 400) {
|
||||
final data = e.response?.data;
|
||||
@@ -1013,10 +1006,10 @@ class ApiService {
|
||||
if (event.data != null && event.data!.isNotEmpty) {
|
||||
try {
|
||||
final Map<String, dynamic> jsonData = jsonDecode(event.data!);
|
||||
print("✅ [SSE] Parsed JSON: $jsonData");
|
||||
print("[SSE] Parsed JSON: $jsonData");
|
||||
controller.add(jsonData);
|
||||
} catch (e) {
|
||||
print("❌ [SSE] JSON Parse Error: $e | Raw: ${event.data}");
|
||||
print("[SSE] JSON Parse Error: $e | Raw: ${event.data}");
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1034,17 +1027,19 @@ class ApiService {
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getNotifications() async {
|
||||
final url = Uri.parse('$baseUrl/notification/client');
|
||||
Future<Map<String, dynamic>> getNotifications({
|
||||
int page = 1,
|
||||
int pageSize = 10,
|
||||
}) async {
|
||||
final url = Uri.parse('$baseUrl/notification/client').replace(
|
||||
queryParameters: {
|
||||
'page': page.toString(),
|
||||
'pageSize': pageSize.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
final accessToken = await _securityService.getAccessToken();
|
||||
if (accessToken == null) {
|
||||
print("APISERVICE Error: Access token is null.");
|
||||
throw UnauthorizedException();
|
||||
}
|
||||
|
||||
print("GET NOTIFICATIONS REQUEST:");
|
||||
print("URL: $url");
|
||||
if (accessToken == null) throw UnauthorizedException();
|
||||
|
||||
final response = await http.get(
|
||||
url,
|
||||
@@ -1054,34 +1049,17 @@ class ApiService {
|
||||
},
|
||||
);
|
||||
|
||||
print("GET NOTIFICATIONS RESPONSE:");
|
||||
print("STATUS: ${response.statusCode}");
|
||||
print("BODY: ${response.body}");
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(utf8.decode(response.bodyBytes));
|
||||
|
||||
// ✅ Проверяем, является ли ответ массивом или объектом с data[]
|
||||
if (data is List) {
|
||||
return data.cast<Map<String, dynamic>>();
|
||||
} else if (data is Map<String, dynamic>) {
|
||||
final list = data['data'];
|
||||
if (list is List) {
|
||||
return list.cast<Map<String, dynamic>>();
|
||||
} else {
|
||||
throw Exception(
|
||||
'Expected a List under "data" but got ${list.runtimeType}',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw Exception('Expected a List or Map but got ${data.runtimeType}');
|
||||
if (data is Map<String, dynamic>) {
|
||||
return data;
|
||||
}
|
||||
throw Exception('Expected Map with data and pagination');
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else if (response.statusCode == 403) {
|
||||
throw AuthBlockException();
|
||||
}
|
||||
|
||||
throw Exception('Ошибка сервера: ${response.statusCode}');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:be_happy/domain/entities/client_notification.dart';
|
||||
|
||||
import '../../data/network/api_service.dart';
|
||||
import '../../domain/entities/pagination_meta.dart';
|
||||
import '../../domain/repositories/notification_repository.dart';
|
||||
import '../../domain/usecase/get_notifications_usecase.dart';
|
||||
import '../models/client_notification_dto.dart';
|
||||
|
||||
class NotificationRepositoryImpl implements NotificationRepository {
|
||||
@@ -35,19 +37,27 @@ class NotificationRepositoryImpl implements NotificationRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ClientNotification>> getNotifications() async {
|
||||
Future<NotificationsResult> getNotifications({
|
||||
int page = 1,
|
||||
int pageSize = 10,
|
||||
}) async {
|
||||
try {
|
||||
final List<Map<String, dynamic>> data = await _apiService.getNotifications();
|
||||
final notifications = data.map((json) {
|
||||
final dto = ClientNotificationDto.fromJson(json);
|
||||
return dto.toEntity();
|
||||
}).toList();
|
||||
final response = await _apiService.getNotifications(
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
);
|
||||
|
||||
// dev.log('NotificationRepository: Загружено ${notifications.length} уведомлений');
|
||||
final notifications = (response['data'] as List)
|
||||
.map((json) => ClientNotificationDto.fromJson(json).toEntity())
|
||||
.toList();
|
||||
|
||||
return notifications;
|
||||
final pagination = PaginationMeta.fromJson(response['pagination']);
|
||||
|
||||
return NotificationsResult(
|
||||
notifications: notifications,
|
||||
pagination: pagination,
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
// dev.log('NotificationRepository: Ошибка: $e', stackTrace: stackTrace);
|
||||
throw Exception('Не удалось загрузить уведомления: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,15 +192,11 @@ class ScooterRepositoryImpl extends ScooterRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<ScooterOrder>> cancelRide(int orderId) async {
|
||||
late final Result<ScooterOrder> result;
|
||||
Future<Result<void>> cancelRide(int orderId) async {
|
||||
late final Result<void> result;
|
||||
try {
|
||||
final order = await _apiService.cancelRide(orderId);
|
||||
if (order != null) {
|
||||
result = Success(order);
|
||||
} else {
|
||||
result = Failure(UnknownFailure("Неизвестная ошибка"));
|
||||
}
|
||||
await _apiService.cancelRide(orderId);
|
||||
result = Success(null);
|
||||
} on AuthException catch (e) {
|
||||
result = Failure(AuthFailure(e.attemptsLeft));
|
||||
} catch (e) {
|
||||
@@ -246,18 +242,11 @@ class ScooterRepositoryImpl extends ScooterRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<ScooterOrder>> finishRide(int orderId, List<int> files) async {
|
||||
Future<Result<void>> finishRide(int orderId, List<int> files) async {
|
||||
late final Result<ScooterOrder> result;
|
||||
try {
|
||||
final order = await _apiService.finishRide(
|
||||
orderId: orderId,
|
||||
filesId: files,
|
||||
);
|
||||
if (order != null) {
|
||||
result = Success(order);
|
||||
} else {
|
||||
result = Failure(UnknownFailure("Неизвестная ошибка"));
|
||||
}
|
||||
await _apiService.finishRide(orderId: orderId, filesId: files);
|
||||
result = Success(null);
|
||||
} on AuthException catch (e) {
|
||||
result = Failure(AuthFailure(e.attemptsLeft));
|
||||
} on WrongZoneException catch (e) {
|
||||
@@ -274,7 +263,6 @@ class ScooterRepositoryImpl extends ScooterRepository {
|
||||
try {
|
||||
await _apiService.payRide(orderId);
|
||||
result = Success(null);
|
||||
|
||||
} on AuthException catch (e) {
|
||||
result = Failure(AuthFailure(e.attemptsLeft));
|
||||
} catch (e) {
|
||||
@@ -345,7 +333,7 @@ class ScooterRepositoryImpl extends ScooterRepository {
|
||||
cardId: cardId,
|
||||
isBalance: isBalance,
|
||||
);
|
||||
result = Success(null);
|
||||
result = Success(null);
|
||||
} on AuthException catch (e) {
|
||||
result = Failure(AuthFailure(e.attemptsLeft));
|
||||
} catch (e) {
|
||||
|
||||
@@ -305,9 +305,9 @@ Future<void> setupDependencies() async {
|
||||
|
||||
getIt.registerFactory<PhoneAuthBloc>(() => PhoneAuthBloc(getIt()));
|
||||
|
||||
getIt.registerFactory<VerifyCodeBloc>(() => VerifyCodeBloc(getIt(), getIt()));
|
||||
getIt.registerFactory<VerifyCodeBloc>(() => VerifyCodeBloc(getIt(), getIt(), getIt()));
|
||||
|
||||
getIt.registerFactory<ProfileBloc>(() => ProfileBloc(getIt(), getIt(), getIt()));
|
||||
getIt.registerLazySingleton<ProfileBloc>(() => ProfileBloc(getIt(), getIt(), getIt()));
|
||||
|
||||
getIt.registerFactory<EditProfileBloc>(
|
||||
() => EditProfileBloc(getIt(), getIt()),
|
||||
@@ -337,7 +337,7 @@ Future<void> setupDependencies() async {
|
||||
|
||||
|
||||
getIt.registerFactory<PaymentMethodSheetBloc>(
|
||||
() => PaymentMethodSheetBloc(getIt(), getIt()),
|
||||
() => PaymentMethodSheetBloc(getIt(), getIt(), getIt()),
|
||||
);
|
||||
|
||||
getIt.registerFactory<CurrentRidesBloc>(() => CurrentRidesBloc(getIt()));
|
||||
|
||||
@@ -32,7 +32,7 @@ class ClientNotification {
|
||||
required this.createdAt,
|
||||
this.canceledAt,
|
||||
this.readAt,
|
||||
});
|
||||
}) ;
|
||||
|
||||
ClientNotification copyWith({
|
||||
int? id,
|
||||
|
||||
28
lib/domain/entities/pagination_meta.dart
Normal file
28
lib/domain/entities/pagination_meta.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
class PaginationMeta {
|
||||
final int total;
|
||||
final int lastPage;
|
||||
final int currentPage;
|
||||
final int totalPerPage;
|
||||
final int? prevPage;
|
||||
final int? nextPage;
|
||||
|
||||
PaginationMeta({
|
||||
required this.total,
|
||||
required this.lastPage,
|
||||
required this.currentPage,
|
||||
required this.totalPerPage,
|
||||
this.prevPage,
|
||||
this.nextPage,
|
||||
});
|
||||
|
||||
factory PaginationMeta.fromJson(Map<String, dynamic> json) {
|
||||
return PaginationMeta(
|
||||
total: json['total'] as int,
|
||||
lastPage: json['lastPage'] as int,
|
||||
currentPage: json['currentPage'] as int,
|
||||
totalPerPage: json['totalPerPage'] as int,
|
||||
prevPage: json['prevPage'] as int?,
|
||||
nextPage: json['nextPage'] as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,9 @@ class ScooterOrder {
|
||||
final String? cancelDescription;
|
||||
final double mileage; //эту
|
||||
final double avgSpeed; //эту и снизу еще 4 есть
|
||||
final String payTotalPricePrint;
|
||||
final String unpayTotalPricePrint;
|
||||
final String totalOrderPricePrint;
|
||||
|
||||
ScooterOrder({
|
||||
required this.id,
|
||||
@@ -57,11 +60,16 @@ class ScooterOrder {
|
||||
this.expiresAt,
|
||||
this.cancelAt,
|
||||
this.cancelDescription,
|
||||
required this.mileage, //эту
|
||||
required this.avgSpeed, //эту и снизу еще 2 есть
|
||||
required this.mileage,
|
||||
required this.avgSpeed,
|
||||
required this.payTotalPricePrint, // оплачено
|
||||
required this.unpayTotalPricePrint, //неоплачено
|
||||
required this.totalOrderPricePrint, //общая
|
||||
});
|
||||
|
||||
factory ScooterOrder.fromJson(Map<String, dynamic> json) {
|
||||
final paymentInfo = json['paymentInfo'] as Map<String, dynamic>?;
|
||||
|
||||
return ScooterOrder(
|
||||
id: json['id'] ?? 0,
|
||||
scooterId: json['scooterId'] ?? 0,
|
||||
@@ -101,8 +109,11 @@ class ScooterOrder {
|
||||
? DateTime.parse(json['cancelAt'])
|
||||
: null,
|
||||
cancelDescription: json['cancelDescription'],
|
||||
mileage: (json['mileage'] as num?)?.toDouble() ?? 0.0, //эту
|
||||
avgSpeed: (json['avgSpeed'] as num?)?.toDouble() ?? 0.0, //эту
|
||||
mileage: (json['mileage'] as num?)?.toDouble() ?? 0.0,
|
||||
avgSpeed: (json['avgSpeed'] as num?)?.toDouble() ?? 0.0,
|
||||
payTotalPricePrint: paymentInfo?['payTotalPricePrint'] ?? '',
|
||||
unpayTotalPricePrint: paymentInfo?['unpayTotalPricePrint'] ?? '',
|
||||
totalOrderPricePrint: paymentInfo?['totalOrderPricePrint'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ class UserProfile {
|
||||
String birthDate;
|
||||
String phone;
|
||||
String email;
|
||||
int? balance;
|
||||
double? balance;
|
||||
int? avatarId;
|
||||
String? avatarUrl;
|
||||
|
||||
@@ -21,7 +21,7 @@ class UserProfile {
|
||||
String? name,
|
||||
String? birthDate,
|
||||
String? email,
|
||||
int? balance,
|
||||
double? balance,
|
||||
int? avatarId,
|
||||
String? avatarUrl,
|
||||
}) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:be_happy/domain/entities/point.dart';
|
||||
|
||||
class Zone {
|
||||
@@ -9,7 +8,10 @@ class Zone {
|
||||
final String type;
|
||||
final bool isActive;
|
||||
final String shapeType;
|
||||
final List<Point> points;
|
||||
// Трехмерная структура для поддержки MultiPolygon и дыр (holes):
|
||||
// geometry[индекс_полигона][индекс_кольца][индекс_точки]
|
||||
// индекс_кольца == 0 — внешняя граница, > 0 — внутренние вырезы (дыры)
|
||||
final List<List<List<Point>>> geometry;
|
||||
final String speedLimit;
|
||||
|
||||
Zone({
|
||||
@@ -19,38 +21,84 @@ class Zone {
|
||||
required this.type,
|
||||
required this.isActive,
|
||||
required this.shapeType,
|
||||
required this.points,
|
||||
required this.geometry,
|
||||
required this.speedLimit,
|
||||
});
|
||||
|
||||
// Геттер для обратной совместимости (возвращает внешнее кольцо первого полигона)
|
||||
List<Point> get points {
|
||||
if (geometry.isNotEmpty && geometry[0].isNotEmpty) {
|
||||
return geometry[0][0];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
factory Zone.fromJson(Map<String, dynamic> json) {
|
||||
final zoneCoordinates = json['coordinates'] as Map<String, dynamic>? ?? {};
|
||||
final String coordsString = zoneCoordinates['coordinates'] ?? '[]';
|
||||
final String shapeType = zoneCoordinates['type'] ?? 'Polygon';
|
||||
|
||||
List<Point> points = [];
|
||||
List<List<List<Point>>> parsedGeometry = [];
|
||||
|
||||
try {
|
||||
final dynamic decoded = jsonDecode(coordsString);
|
||||
|
||||
if (decoded is List && decoded.isNotEmpty) {
|
||||
List<dynamic> targetList = [];
|
||||
|
||||
if (shapeType == 'Polygon') {
|
||||
// У полигона структура [[[lat, lon], ...]] -> уходим на 1 уровень вглубь
|
||||
targetList = decoded[0] as List<dynamic>;
|
||||
} else {
|
||||
// У LineString структура [[lat, lon], ...] -> используем как есть
|
||||
targetList = decoded;
|
||||
}
|
||||
// Структура: [[[lon, lat], [lon, lat], ...], [hole1], ...]
|
||||
List<List<Point>> polygonRings = [];
|
||||
for (var ring in decoded) {
|
||||
if (ring is List) {
|
||||
List<Point> ringPoints = [];
|
||||
for (var coord in ring) {
|
||||
if (coord is List && coord.length >= 2) {
|
||||
// В GeoJSON сначала идет Longitude, затем Latitude
|
||||
ringPoints.add(Point(
|
||||
(coord[1] as num).toDouble(),
|
||||
(coord[0] as num).toDouble(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if (ringPoints.isNotEmpty) polygonRings.add(ringPoints);
|
||||
}
|
||||
}
|
||||
if (polygonRings.isNotEmpty) parsedGeometry.add(polygonRings);
|
||||
|
||||
points = targetList.map((item) {
|
||||
final List<dynamic> coords = item as List<dynamic>;
|
||||
return Point(
|
||||
(coords[1] as num).toDouble(),
|
||||
(coords[0] as num).toDouble(),
|
||||
);
|
||||
}).toList();
|
||||
} else if (shapeType == 'MultiPolygon') {
|
||||
// Структура: [[[[lon, lat], ...]], [[[lon, lat], ...]]]
|
||||
for (var poly in decoded) {
|
||||
if (poly is List) {
|
||||
List<List<Point>> polygonRings = [];
|
||||
for (var ring in poly) {
|
||||
if (ring is List) {
|
||||
List<Point> ringPoints = [];
|
||||
for (var coord in ring) {
|
||||
if (coord is List && coord.length >= 2) {
|
||||
ringPoints.add(Point(
|
||||
(coord[1] as num).toDouble(),
|
||||
(coord[0] as num).toDouble(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if (ringPoints.isNotEmpty) polygonRings.add(ringPoints);
|
||||
}
|
||||
}
|
||||
if (polygonRings.isNotEmpty) parsedGeometry.add(polygonRings);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback для LineString или плоских списков
|
||||
List<Point> ringPoints = [];
|
||||
for (var coord in decoded) {
|
||||
if (coord is List && coord.length >= 2) {
|
||||
ringPoints.add(Point(
|
||||
(coord[1] as num).toDouble(),
|
||||
(coord[0] as num).toDouble(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if (ringPoints.isNotEmpty) parsedGeometry.add([ringPoints]);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print("PARSE ERROR for Zone ID ${json['id']}: $e");
|
||||
@@ -64,11 +112,12 @@ class Zone {
|
||||
isActive: json['isActive'] ?? false,
|
||||
speedLimit: json['speedLimit'] ?? '',
|
||||
shapeType: shapeType,
|
||||
points: points,
|
||||
geometry: parsedGeometry,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Zone{id: $id, title: $title, type: $type, points: $points}';
|
||||
return 'Zone{id: $id, title: $title, type: $type, polygonsCount: ${geometry.length}}';
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:be_happy/domain/entities/client_notification.dart';
|
||||
|
||||
import '../usecase/get_notifications_usecase.dart';
|
||||
|
||||
abstract class NotificationRepository {
|
||||
/// Устанавливает постоянное SSE-соединение и возвращает поток уведомлений
|
||||
Stream<ClientNotification> getNotificationsStream();
|
||||
@@ -11,5 +13,7 @@ abstract class NotificationRepository {
|
||||
void closeStream();
|
||||
|
||||
/// получить список уведомлений
|
||||
Future<List<ClientNotification>> getNotifications();
|
||||
}
|
||||
Future<NotificationsResult> getNotifications({
|
||||
int page = 1,
|
||||
int pageSize = 10,
|
||||
});}
|
||||
|
||||
@@ -27,10 +27,10 @@ abstract class ScooterRepository {
|
||||
required bool isInsurance,
|
||||
});
|
||||
Future<Result<ScooterOrder>> startRide(int orderId);
|
||||
Future<Result<ScooterOrder>> cancelRide(int orderId);
|
||||
Future<Result<void>> cancelRide(int orderId);
|
||||
Future<Result<ScooterOrder>> pauseRide(int orderId);
|
||||
Future<Result<ScooterOrder>> resumeRide(int orderId);
|
||||
Future<Result<ScooterOrder>> finishRide(int orderId, List<int> files);
|
||||
Future<Result<void>> finishRide(int orderId, List<int> files);
|
||||
Future<Result<void>> payRide(int orderId);
|
||||
Future<Result<List<ScooterOrder>>> getClientOrders();
|
||||
Future<Result<List<int>>> uploadScooterPhotos(List<File> images);
|
||||
|
||||
@@ -7,7 +7,7 @@ class CancelRideUsecase {
|
||||
|
||||
CancelRideUsecase(this.repository);
|
||||
|
||||
Future<Result<ScooterOrder>> call(int orderId) {
|
||||
Future<Result<void>> call(int orderId) {
|
||||
return repository.cancelRide(orderId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ class FinishRideUsecase {
|
||||
|
||||
FinishRideUsecase(this.repository);
|
||||
|
||||
Future<Result<ScooterOrder>> call(int orderId, List<int> files) {
|
||||
Future<Result<void>> call(int orderId, List<int> files) {
|
||||
return repository.finishRide(orderId, files);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import '../entities/client_notification.dart';
|
||||
import '../entities/pagination_meta.dart';
|
||||
import '../repositories/notification_repository.dart';
|
||||
|
||||
class GetNotificationsUsecase {
|
||||
@@ -6,7 +7,17 @@ class GetNotificationsUsecase {
|
||||
|
||||
GetNotificationsUsecase(this.repository);
|
||||
|
||||
Future<List<ClientNotification>> call() {
|
||||
return repository.getNotifications();
|
||||
Future<NotificationsResult> call({int page = 1, int pageSize = 10}) {
|
||||
return repository.getNotifications(page: page, pageSize: pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationsResult {
|
||||
final List<ClientNotification> notifications;
|
||||
final PaginationMeta pagination;
|
||||
|
||||
NotificationsResult({
|
||||
required this.notifications,
|
||||
required this.pagination,
|
||||
});
|
||||
}
|
||||
@@ -57,6 +57,7 @@ class MyApp extends StatelessWidget {
|
||||
BlocProvider(create: (context) => getIt<VerifyCodeBloc>()),
|
||||
BlocProvider(create: (context) => getIt<PinBloc>()),
|
||||
BlocProvider(create: (context) => getIt<MapBloc>()),
|
||||
BlocProvider(create: (context) => getIt<ProfileBloc>()),
|
||||
],
|
||||
child: BlocListener<SplashBloc, SplashState>(
|
||||
listener: (context, state) {
|
||||
|
||||
@@ -4,22 +4,81 @@ import 'scooter_info_item.dart';
|
||||
class ScooterInfoSection extends StatelessWidget {
|
||||
final double distance;
|
||||
final String location;
|
||||
const ScooterInfoSection({super.key, required this.distance, required this.location});
|
||||
final int batteryLevel;
|
||||
|
||||
const ScooterInfoSection({
|
||||
super.key,
|
||||
required this.distance,
|
||||
required this.location,
|
||||
required this.batteryLevel,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 1. Расчет оставшегося расстояния (макс. 62 км)
|
||||
final double remainingKm = (62 * batteryLevel) / 100;
|
||||
|
||||
// Красиво форматируем: если число целое — убираем точку (например, 31 км вместо 31.0 км)
|
||||
final String kmText = remainingKm % 1 == 0
|
||||
? '${remainingKm.toInt()} км'
|
||||
: '${remainingKm.toStringAsFixed(1)} км';
|
||||
|
||||
// 2. Расчет оставшегося времени (макс. 2.5 часа = 150 минут)
|
||||
final int totalMinutes = ((150 * batteryLevel) / 100).round();
|
||||
final int hours = totalMinutes ~/ 60;
|
||||
final int minutes = totalMinutes % 60;
|
||||
|
||||
// Формируем строку времени в зависимости от получившихся значений
|
||||
String timeText = '';
|
||||
if (hours > 0 && minutes > 0) {
|
||||
timeText = '$hours ч $minutes мин';
|
||||
} else if (hours > 0) {
|
||||
timeText = '$hours ч';
|
||||
} else {
|
||||
timeText = '$minutes мин';
|
||||
}
|
||||
|
||||
final int walkingMinutes = (distance / 80).round().clamp(1, 999);
|
||||
final int walkHours = walkingMinutes ~/ 60;
|
||||
final int walkMins = walkingMinutes % 60;
|
||||
|
||||
String walkingText = '';
|
||||
if (walkHours > 0 && walkMins > 0) {
|
||||
walkingText = '$walkHours ч $walkMins мин';
|
||||
} else if (walkHours > 0) {
|
||||
walkingText = '$walkHours ч';
|
||||
} else {
|
||||
walkingText = '$walkingMinutes мин';
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
ScooterInfoItem(icon: 'assets/icons/bolt.png', text: '47 км или 4 часа 17 минут'),
|
||||
ScooterInfoItem(icon: 'assets/icons/speed.png', text: 'max = 25 км/ч'),
|
||||
ScooterInfoItem(icon: 'assets/icons/location.png', text: location),
|
||||
// Динамическая строка с зарядом
|
||||
ScooterInfoItem(
|
||||
icon: 'assets/icons/bolt.png',
|
||||
text: '$kmText или $timeText',
|
||||
),
|
||||
ScooterInfoItem(
|
||||
icon: 'assets/icons/speed.png',
|
||||
text: 'max = 25 км/ч',
|
||||
),
|
||||
ScooterInfoItem(
|
||||
icon: 'assets/icons/location.png',
|
||||
text: location,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ScooterInfoItem(icon: 'assets/icons/person.png', text: distance.toInt() >= 1000
|
||||
? '${((distance.toInt()) / 1000).toStringAsFixed(1)} km'
|
||||
: '${distance.toInt()} m',),
|
||||
SizedBox(width: 16),
|
||||
ScooterInfoItem(icon: 'assets/icons/time.png', text: '1 минута'),
|
||||
ScooterInfoItem(
|
||||
icon: 'assets/icons/person.png',
|
||||
text: distance.toInt() >= 1000
|
||||
? '${((distance.toInt()) / 1000).toStringAsFixed(1)} km'
|
||||
: '${distance.toInt()} m',
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
ScooterInfoItem(
|
||||
icon: 'assets/icons/time.png',
|
||||
text: walkingText,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -233,7 +233,7 @@ class _ScooterCard extends StatelessWidget {
|
||||
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.go('/home/scooter/${scooter.id}', extra: address);
|
||||
context.go('/home/scooter/${scooter.id}', extra: [address, scooter.distance]);
|
||||
},
|
||||
child: Container(
|
||||
height: 42,
|
||||
@@ -245,7 +245,7 @@ class _ScooterCard extends StatelessWidget {
|
||||
width: double.infinity,
|
||||
fontSize: 12,
|
||||
onTap: () {
|
||||
context.go('/home/scooter/${scooter.id}', extra: address);
|
||||
context.go('/home/scooter/${scooter.id}', extra: [address, scooter.distance]);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -218,6 +218,9 @@ class _RideCardState extends State<_RideCard> {
|
||||
} else {
|
||||
displayTime = _elapsedTime;
|
||||
}
|
||||
|
||||
final isExpired = isReserved && (_reservationTime - _elapsedTime).isNegative;
|
||||
|
||||
final timeString = _formatDuration(displayTime);
|
||||
final statusText = widget.order.status == 'Booking' ? 'Забронирован' : "Активный";
|
||||
final statusColor = widget.order.status == 'Booking' ? Color(0xFFFFCC00) : Color(0xFF8bffaa);
|
||||
@@ -347,7 +350,8 @@ class _RideCardState extends State<_RideCard> {
|
||||
height: 32,
|
||||
width: 100,
|
||||
fontSize: 11,
|
||||
onTap: () {
|
||||
enabled: !isExpired,
|
||||
onTap: !isExpired ? () {
|
||||
if (isReserved) {
|
||||
Navigator.pop(context);
|
||||
showModalBottomSheet(
|
||||
@@ -374,7 +378,7 @@ class _RideCardState extends State<_RideCard> {
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
} : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -174,7 +174,7 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
|
||||
if (widget.showBalance) ...[
|
||||
PaymentOption(
|
||||
title: 'Баланс',
|
||||
subtitle: '${state.balance.toStringAsFixed(2)} BYN',
|
||||
subtitle: '${state.balance} баллов',
|
||||
isSelected: _selectedPaymentMethod == -1,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
@@ -192,7 +192,7 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: PaymentOption(
|
||||
title: card.isMain ? "Основная" : "",
|
||||
title: card.isMain ? "Основная" : "Карта",
|
||||
subtitle: '${card.cardLastNumber}',
|
||||
isSelected: _selectedPaymentMethod == index,
|
||||
onTap: () async {
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../../../domain/entities/payment_card.dart';
|
||||
import '../../../domain/entities/scooter.dart';
|
||||
import '../../../domain/entities/tariff.dart';
|
||||
import '../../../domain/usecase/get_payment_cards_usecase.dart';
|
||||
import '../../../domain/usecase/get_profile_usecase.dart';
|
||||
import '../../../domain/usecase/set_main_payment_card_usecase.dart';
|
||||
import '../../event/payment_method_sheet_event.dart';
|
||||
import '../../event/tariff_sheet_event.dart';
|
||||
@@ -40,6 +41,24 @@ class _TariffSheetState extends State<TariffSheet> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double remainingKm = (62 * widget.scooter.batteryLevel) / 100;
|
||||
|
||||
final String kmText = remainingKm % 1 == 0
|
||||
? '${remainingKm.toInt()} км'
|
||||
: '${remainingKm.toStringAsFixed(1)} км';
|
||||
|
||||
final int totalMinutes = ((150 * widget.scooter.batteryLevel) / 100).round();
|
||||
final int hours = totalMinutes ~/ 60;
|
||||
final int minutes = totalMinutes % 60;
|
||||
|
||||
String timeText = '';
|
||||
if (hours > 0 && minutes > 0) {
|
||||
timeText = '$hours ч $minutes мин';
|
||||
} else if (hours > 0) {
|
||||
timeText = '$hours ч';
|
||||
} else {
|
||||
timeText = '$minutes мин';
|
||||
}
|
||||
return BlocConsumer<TariffSheetBloc, TariffSheetState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == TariffSheetStatus.failure && state.errorMessage != null) {
|
||||
@@ -135,7 +154,6 @@ class _TariffSheetState extends State<TariffSheet> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 🔹 HEADER
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
@@ -227,7 +245,7 @@ class _TariffSheetState extends State<TariffSheet> {
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Заряда хватит на 4 часа 17 минут\nили 47 км',
|
||||
'Заряда хватит на $kmText или $timeText',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
@@ -303,6 +321,7 @@ class _TariffSheetState extends State<TariffSheet> {
|
||||
create: (context) => PaymentMethodSheetBloc(
|
||||
getIt<GetPaymentCardsUsecase>(),
|
||||
getIt<SetMainPaymentCardUsecase>(),
|
||||
getIt<GetProfileUseCase>(),
|
||||
)..add(PaymentMethodSheetStarted()),
|
||||
child: PaymentMethodSheet(
|
||||
initialSelectedCard: state.useBalance ? null : state.selectedCard,
|
||||
@@ -547,7 +566,7 @@ class _TariffCard extends StatelessWidget {
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => TariffInfoSheet(
|
||||
builder: (_) => TariffInfoSheet(
|
||||
tariff: tariff,
|
||||
isInsurance: isInsurance,
|
||||
onChanged: (val) {
|
||||
|
||||
@@ -63,59 +63,81 @@ class SideMenu extends StatelessWidget {
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// Пункты меню
|
||||
Column(
|
||||
children: [
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/person_icon.png',
|
||||
title: 'Профиль',
|
||||
onTap: () => {
|
||||
context.go('/home/profile'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go('/home/profile');
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/list_star_icon.png',
|
||||
title: 'История поездок',
|
||||
onTap: () => context.go("/home/order-history"),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go("/home/order-history");
|
||||
},
|
||||
),
|
||||
const Divider(color: Colors.white30, height: 1),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/creditcard_icon.png',
|
||||
title: 'Способ оплаты',
|
||||
onTap: () => context.go("/home/payment-methods"),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go("/home/payment-methods");
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/promo_icon.png',
|
||||
title: 'Промокоды',
|
||||
onTap: () => context.go("/home/promo"),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go("/home/promo");
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/plans_icon.png',
|
||||
title: 'Абонементы',
|
||||
onTap: () => context.push("/home/subscriptions"),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.push("/home/subscriptions");
|
||||
},
|
||||
),
|
||||
const Divider(color: Colors.white30, height: 1),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/magazine_icon.png',
|
||||
title: 'Правила пользования самокатом',
|
||||
onTap: () => context.go("/home/rules")
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go("/home/rules");
|
||||
}
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/headphones_icon.png',
|
||||
title: 'Техподдержка',
|
||||
onTap: () => {
|
||||
context.go('/home/support'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go('/home/support');
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/doc_icon.png',
|
||||
title: 'Документы',
|
||||
onTap: () => context.go("/home/documents"),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go("/home/documents");
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/news_icon.png',
|
||||
title: 'Новости',
|
||||
onTap: () => context.go("/home/news"),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go("/home/news");
|
||||
},
|
||||
),
|
||||
const Divider(color: Colors.white30, height: 1),
|
||||
_MenuItem(
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
sealed class NotificationsEvent {}
|
||||
|
||||
class NotificationsFetchRequested extends NotificationsEvent {}
|
||||
class NotificationsFetchRequested extends NotificationsEvent {
|
||||
final int page;
|
||||
NotificationsFetchRequested({this.page = 1});
|
||||
}
|
||||
|
||||
class NotificationsLoadMoreRequested extends NotificationsEvent {}
|
||||
@@ -13,3 +13,5 @@ class AuthCheckRequested extends SplashEvent {}
|
||||
class AuthStarted extends SplashEvent {}
|
||||
|
||||
class PinVerificationSuccess extends SplashEvent {}
|
||||
|
||||
class AuthLogoutRequested extends SplashEvent {}
|
||||
|
||||
@@ -260,8 +260,11 @@ class AppRouter {
|
||||
type: MaterialType.transparency,
|
||||
child: BlocProvider(
|
||||
create: (context) =>
|
||||
PaymentMethodSheetBloc(getIt<GetPaymentCardsUsecase>(),
|
||||
getIt<SetMainPaymentCardUsecase>()),
|
||||
PaymentMethodSheetBloc(
|
||||
getIt<GetPaymentCardsUsecase>(),
|
||||
getIt<SetMainPaymentCardUsecase>(),
|
||||
getIt<GetProfileUseCase>(),
|
||||
),
|
||||
child: PaymentMethodSheet(),
|
||||
),
|
||||
),
|
||||
@@ -323,10 +326,16 @@ class AppRouter {
|
||||
GoRoute(
|
||||
path: 'scooter/:id',
|
||||
builder: (context, state) {
|
||||
String address = state.extra as String;
|
||||
List<dynamic> extras = [];
|
||||
if (state.extra != null) {
|
||||
extras = state.extra as List<dynamic>;
|
||||
}
|
||||
String address = extras[0];
|
||||
double distance = extras[1];
|
||||
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<ScooterDetailBloc>(),
|
||||
child: ScooterDetailScreen(address: address),
|
||||
child: ScooterDetailScreen(address: address, distance: distance),
|
||||
);
|
||||
}
|
||||
),
|
||||
@@ -444,19 +453,21 @@ class AppRouter {
|
||||
),
|
||||
GoRoute(
|
||||
path: 'payment-methods',
|
||||
builder: (context, state) =>
|
||||
BlocProvider(
|
||||
create: (context) =>
|
||||
PaymentMethodsBloc(
|
||||
getIt<GetPaymentCardsUsecase>(),
|
||||
getIt<RemovePaymentCardUsecase>(),
|
||||
getIt<SetMainPaymentCardUsecase>(),
|
||||
getIt<GetProfileUseCase>(),
|
||||
getIt<AddPaymentCardUsecase>(),
|
||||
)
|
||||
..add(PaymentMethodsStarted()),
|
||||
child: PaymentMethodsScreen(),
|
||||
),
|
||||
builder: (context, state) {
|
||||
final refreshId = state.uri.queryParameters['refreshId'];
|
||||
|
||||
return BlocProvider(
|
||||
key: refreshId != null ? ValueKey(refreshId) : null,
|
||||
create: (context) => PaymentMethodsBloc(
|
||||
getIt<GetPaymentCardsUsecase>(),
|
||||
getIt<RemovePaymentCardUsecase>(),
|
||||
getIt<SetMainPaymentCardUsecase>(),
|
||||
getIt<GetProfileUseCase>(),
|
||||
getIt<AddPaymentCardUsecase>(),
|
||||
)..add(PaymentMethodsStarted()),
|
||||
child: const PaymentMethodsScreen(),
|
||||
);
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'add-card',
|
||||
@@ -480,6 +491,28 @@ class AppRouter {
|
||||
..add(LoadTopUpData()),
|
||||
child: TopUpScreen(),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'result/:status',
|
||||
builder: (context, state) {
|
||||
final modeParam = state.pathParameters['status'];
|
||||
|
||||
CardBindingStatus status = CardBindingStatus.success;
|
||||
String title = 'Успешно!';
|
||||
|
||||
if (modeParam == 'failed') {
|
||||
status = CardBindingStatus.failed;
|
||||
title = 'Извините, что-то\nпошло не так';
|
||||
}
|
||||
|
||||
return CardBindingResultScreen(
|
||||
status: status,
|
||||
title: title,
|
||||
cardImagePath: 'assets/icons/card_result_screen.png',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -524,45 +557,59 @@ class AppRouter {
|
||||
print(splashBloc.state);
|
||||
print(state.uri.toString());
|
||||
|
||||
if (authState is AuthInitial && currentLocation != '/splash') {
|
||||
print("splash");
|
||||
return '/splash';
|
||||
if (authState is AuthInitial) {
|
||||
if (currentLocation != '/splash') return '/splash';
|
||||
return null;
|
||||
}
|
||||
|
||||
if (authState is AuthFirstLaunch &&
|
||||
currentLocation != '/login' &&
|
||||
currentLocation != '/privacy-policy' &&
|
||||
currentLocation != '/license-agreement') {
|
||||
print("login");
|
||||
return '/login';
|
||||
// 2. Первый запуск или пользователь не авторизован
|
||||
if (authState is AuthFirstLaunch || authState is AuthUnauthenticated) {
|
||||
final isAllowed = currentLocation == '/login' ||
|
||||
currentLocation.startsWith('/verify') ||
|
||||
currentLocation == '/privacy-policy' ||
|
||||
currentLocation == '/license-agreement';
|
||||
|
||||
if (!isAllowed) return '/login';
|
||||
return null;
|
||||
}
|
||||
|
||||
if (authState is AuthUnauthenticated &&
|
||||
currentLocation != '/login' &&
|
||||
currentLocation != '/privacy-policy' &&
|
||||
currentLocation != '/license-agreement') {
|
||||
print("login2");
|
||||
return '/login';
|
||||
// 3. Авторизация в процессе (переход от ввода телефона/SMS к PIN-коду)
|
||||
if (authState is AuthInProgress) {
|
||||
final isAllowed = currentLocation == '/login' ||
|
||||
currentLocation.startsWith('/verify') ||
|
||||
currentLocation == '/pin' || // ← Теперь роутер пустит сюда!
|
||||
currentLocation == '/privacy-policy' ||
|
||||
currentLocation == '/license-agreement';
|
||||
|
||||
if (!isAllowed) return '/login';
|
||||
return null;
|
||||
}
|
||||
|
||||
// 4. Пользователь авторизован, но PIN еще не проверен
|
||||
if (authState is AuthAuthenticated) {
|
||||
final isComingFromStart =
|
||||
currentLocation == '/splash' ||
|
||||
currentLocation == '/login' ||
|
||||
currentLocation == '/phone';
|
||||
final isComingFromStart = currentLocation == '/splash' ||
|
||||
currentLocation == '/login' ||
|
||||
currentLocation.startsWith('/verify') ||
|
||||
currentLocation == '/phone';
|
||||
|
||||
if (isComingFromStart) {
|
||||
print("redirecting to pin check");
|
||||
return '/pin';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 5. Пользователь полностью авторизован и PIN успешно верифицирован
|
||||
if (authState is AuthPinVerified) {
|
||||
if (currentLocation == '/splash' ||
|
||||
final isComingFromAuth = currentLocation == '/splash' ||
|
||||
currentLocation == '/login' ||
|
||||
currentLocation == '/pin') {
|
||||
currentLocation.startsWith('/verify') ||
|
||||
currentLocation == '/pin';
|
||||
|
||||
if (isComingFromAuth) {
|
||||
return '/home';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -31,7 +31,8 @@ class _CardBindingResultScreenState extends State<CardBindingResultScreen> {
|
||||
if (widget.status == CardBindingStatus.success) {
|
||||
_autoPopTimer = Timer(const Duration(seconds: 5), () {
|
||||
if (mounted) {
|
||||
context.go('/home/payment-methods');
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
context.go("/home/payment-methods?refreshId=$timestamp");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -125,7 +126,10 @@ class _CardBindingResultScreenState extends State<CardBindingResultScreen> {
|
||||
|
||||
GradientButton(
|
||||
text: "Попробовать снова",
|
||||
onTap: () {},
|
||||
onTap: () {
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
context.go("/home/payment-methods?refreshId=$timestamp");
|
||||
},
|
||||
enabled: true,
|
||||
showArrows: true,
|
||||
),
|
||||
@@ -135,7 +139,11 @@ class _CardBindingResultScreenState extends State<CardBindingResultScreen> {
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: TextButton(
|
||||
onPressed: () => context.go("/home/payment-methods"),
|
||||
onPressed: ()
|
||||
{
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
context.go("/home/payment-methods?refreshId=$timestamp");
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../di/service_locator.dart';
|
||||
import '../../domain/entities/user_profile.dart';
|
||||
import '../components/custom_app_bar.dart';
|
||||
import '../components/gradient_button.dart';
|
||||
@@ -106,7 +107,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
}
|
||||
|
||||
if (state.isSuccess) {
|
||||
context.read<ProfileBloc>().add(ProfileUpdated());
|
||||
getIt<ProfileBloc>().add(ProfileUpdated());
|
||||
context.pop();
|
||||
}
|
||||
if (state.error != null) {
|
||||
|
||||
@@ -4,24 +4,9 @@ import 'dart:ui' as ui;
|
||||
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:be_happy/domain/entities/client_notification.dart';
|
||||
import 'package:be_happy/domain/usecase/book_scooter_usecase.dart';
|
||||
import 'package:be_happy/domain/usecase/get_client_orders_usecase.dart';
|
||||
import 'package:be_happy/domain/usecase/get_map_settings_usecase.dart';
|
||||
import 'package:be_happy/domain/usecase/get_payment_cards_usecase.dart';
|
||||
import 'package:be_happy/domain/usecase/get_pedestrian_routes_usecase.dart';
|
||||
import 'package:be_happy/domain/usecase/get_scooter_usecase.dart';
|
||||
import 'package:be_happy/domain/usecase/save_map_settings_usecase.dart';
|
||||
import 'package:be_happy/presentation/components/fine_notification_card.dart';
|
||||
import 'package:be_happy/presentation/components/map_icon_painter/clusterized_icon_painter.dart';
|
||||
import 'package:be_happy/presentation/components/payment_notification_card.dart';
|
||||
import 'package:be_happy/presentation/components/sheet/current_rides_sheet.dart';
|
||||
import 'package:be_happy/presentation/components/sheet/map_settings_sheet.dart';
|
||||
import 'package:be_happy/presentation/components/sheet/tariff_sheet.dart';
|
||||
import 'package:be_happy/presentation/event/current_rides_event.dart';
|
||||
import 'package:be_happy/presentation/event/map_settings_modal_event.dart';
|
||||
import 'package:be_happy/presentation/viewmodel/current_rides_bloc.dart';
|
||||
import 'package:be_happy/presentation/viewmodel/map_settings_modal_bloc.dart';
|
||||
import 'package:be_happy/presentation/viewmodel/tariff_sheet_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
@@ -32,18 +17,12 @@ import '../../core/app_colors.dart';
|
||||
import '../../di/service_locator.dart';
|
||||
import '../../domain/entities/scooter.dart';
|
||||
import '../../domain/entities/zone.dart';
|
||||
import '../../domain/usecase/get_address_by_point_usecase.dart';
|
||||
import '../../domain/usecase/get_available_tariffs_usecase.dart';
|
||||
import '../../domain/usecase/get_notifications_stream_usecase.dart';
|
||||
import '../components/notification_toast.dart';
|
||||
import '../components/sheet/scooter_bottom_sheet.dart';
|
||||
import '../components/side_menu.dart';
|
||||
import '../components/unpaid_order_notification_card.dart';
|
||||
import '../event/map_event.dart';
|
||||
import '../event/scooter_detail_modal_event.dart';
|
||||
import '../state/map_state.dart';
|
||||
import '../viewmodel/map_bloc.dart';
|
||||
import '../viewmodel/scooter_detail_modal_bloc.dart';
|
||||
|
||||
class MapScreen extends StatefulWidget {
|
||||
const MapScreen({super.key});
|
||||
@@ -609,28 +588,33 @@ class _MapScreenState extends State<MapScreen> {
|
||||
if (zones == null || zones.isEmpty) return [];
|
||||
|
||||
List<MapObject> objects = [];
|
||||
|
||||
List<LinearRing> allZoneHoles = [];
|
||||
List<LinearRing> driveOuterRings = [];
|
||||
|
||||
for (var zone in zones) {
|
||||
var points = zone.points.map((p) => Point(latitude: p.latitude, longitude: p.longitude)).toList();
|
||||
if (zone.type == "Drive") {
|
||||
for (var polygonData in zone.geometry) {
|
||||
if (polygonData.isEmpty) continue;
|
||||
|
||||
var cleanPoints = <Point>[];
|
||||
for (var p in points) {
|
||||
if (cleanPoints.isEmpty || cleanPoints.last != p) {
|
||||
cleanPoints.add(p);
|
||||
var outerPoints = polygonData[0].map((p) => Point(latitude: p.latitude, longitude: p.longitude)).toList();
|
||||
|
||||
var cleanPoints = <Point>[];
|
||||
for (var p in outerPoints) {
|
||||
if (cleanPoints.isEmpty || cleanPoints.last != p) {
|
||||
cleanPoints.add(p);
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanPoints.length > 2) {
|
||||
if (cleanPoints.first != cleanPoints.last) {
|
||||
cleanPoints.add(cleanPoints.first);
|
||||
}
|
||||
driveOuterRings.add(LinearRing(points: cleanPoints));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanPoints.length > 2) {
|
||||
if (cleanPoints.first != cleanPoints.last) {
|
||||
cleanPoints.add(cleanPoints.first);
|
||||
}
|
||||
|
||||
allZoneHoles.add(LinearRing(points: cleanPoints));
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем глобальную маску (все, что НЕ Drive — затеняется красным)
|
||||
objects.add(
|
||||
PolygonMapObject(
|
||||
mapId: const MapObjectId('global_inverse_mask'),
|
||||
@@ -641,7 +625,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
Point(latitude: -85, longitude: 179.9),
|
||||
Point(latitude: -85, longitude: -179.9),
|
||||
]),
|
||||
innerRings: allZoneHoles,
|
||||
innerRings: driveOuterRings, // Вырезаем только зоны Drive
|
||||
),
|
||||
strokeWidth: 0,
|
||||
fillColor: Colors.red.withOpacity(0.15),
|
||||
@@ -649,27 +633,63 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
);
|
||||
|
||||
// 2. Отрисовываем каждую зону (включая MultiPolygon и вложенные NotDrive/Finish)
|
||||
for (var zone in zones) {
|
||||
Color borderColor;
|
||||
Color fillColor;
|
||||
|
||||
// Стилизация в зависимости от типа зоны
|
||||
if (zone.type == "Drive") {
|
||||
borderColor = const Color(0xFF5ECD4C);
|
||||
borderColor = const Color(0xFF5ECD4C); // Зеленый бордюр
|
||||
fillColor = Colors.transparent; // Прозрачная внутри маски
|
||||
} else if (zone.type == "NotDrive") {
|
||||
borderColor = const Color(0xFFEF4444);
|
||||
borderColor = const Color(0xFFEF4444); // Красный бордюр
|
||||
fillColor = const Color(0xFFEF4444).withOpacity(0.3); // Красная полупрозрачная заливка для наглядности внутри Drive
|
||||
} else {
|
||||
borderColor = const Color(0xFFA78BFA);
|
||||
borderColor = const Color(0xFFA78BFA); // Фиолетовый бордюр (Парковка)
|
||||
fillColor = const Color(0xFFA78BFA).withOpacity(0.25); // Фиолетовая заливка
|
||||
}
|
||||
|
||||
objects.add(
|
||||
PolylineMapObject(
|
||||
mapId: MapObjectId('zone_contour_${zone.id}'),
|
||||
polyline: Polyline(
|
||||
points: zone.points.map((p) => Point(latitude: p.latitude, longitude: p.longitude)).toList(),
|
||||
int polyIndex = 0;
|
||||
for (var polygonData in zone.geometry) {
|
||||
if (polygonData.isEmpty) continue;
|
||||
|
||||
// Строим внешнее кольцо текущего полигона
|
||||
var outerPoints = polygonData[0].map((p) => Point(latitude: p.latitude, longitude: p.longitude)).toList();
|
||||
if (outerPoints.first != outerPoints.last) {
|
||||
outerPoints.add(outerPoints.first);
|
||||
}
|
||||
LinearRing outerRing = LinearRing(points: outerPoints);
|
||||
|
||||
// Строим внутренние кольца (дыры), если они есть у этого полигона
|
||||
List<LinearRing> innerRings = [];
|
||||
for (int i = 1; i < polygonData.length; i++) {
|
||||
var innerPoints = polygonData[i].map((p) => Point(latitude: p.latitude, longitude: p.longitude)).toList();
|
||||
if (innerPoints.isNotEmpty) {
|
||||
if (innerPoints.first != innerPoints.last) {
|
||||
innerPoints.add(innerPoints.first);
|
||||
}
|
||||
innerRings.add(LinearRing(points: innerPoints));
|
||||
}
|
||||
}
|
||||
|
||||
// Рендерим полноценный PolygonMapObject (он поддерживает и заливку, и обводку)
|
||||
objects.add(
|
||||
PolygonMapObject(
|
||||
mapId: MapObjectId('zone_poly_${zone.type}_${zone.id}_$polyIndex'),
|
||||
polygon: Polygon(
|
||||
outerRing: outerRing,
|
||||
innerRings: innerRings,
|
||||
),
|
||||
strokeColor: borderColor,
|
||||
strokeWidth: 2.0,
|
||||
fillColor: fillColor,
|
||||
// Поднимаем зоны ограничений и парковок выше маски и контуров Drive
|
||||
zIndex: zone.type == "Drive" ? 1 : 2,
|
||||
),
|
||||
strokeColor: borderColor,
|
||||
strokeWidth: 2.0,
|
||||
zIndex: 1,
|
||||
),
|
||||
);
|
||||
);
|
||||
polyIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return objects;
|
||||
@@ -684,7 +704,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
child: Builder(
|
||||
builder: (innerContext) => _RoundIconButton(
|
||||
icon: Icons.menu,
|
||||
onPressed: () => {Scaffold.of(innerContext).openDrawer()},
|
||||
onPressed: () {
|
||||
context.read<MapBloc>().add(FetchProfileData());
|
||||
Scaffold.of(innerContext).openDrawer();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -23,7 +23,10 @@ class NotificationsScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _NotificationsScreenContent();
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<NotificationsBloc>()..add(NotificationsFetchRequested(page: 1)),
|
||||
child: _NotificationsScreenContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,21 +39,31 @@ class _NotificationsScreenContent extends StatefulWidget {
|
||||
|
||||
class _NotificationsScreenContentState extends State<_NotificationsScreenContent> {
|
||||
NotificationFilter _filter = NotificationFilter.all;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _setFilter(NotificationFilter filter) {
|
||||
setState(() {
|
||||
_filter = filter;
|
||||
});
|
||||
setState(() => _filter = filter);
|
||||
_scrollController.jumpTo(0);
|
||||
context.read<NotificationsBloc>().add(NotificationsFetchRequested(page: 1));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<NotificationsBloc>()..add(NotificationsFetchRequested()),
|
||||
child: NotificationsView(
|
||||
filter: _filter,
|
||||
onFilterChanged: _setFilter,
|
||||
),
|
||||
return NotificationsView(
|
||||
filter: _filter,
|
||||
onFilterChanged: _setFilter,
|
||||
scrollController: _scrollController,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -58,11 +71,13 @@ class _NotificationsScreenContentState extends State<_NotificationsScreenContent
|
||||
class NotificationsView extends StatelessWidget {
|
||||
final NotificationFilter filter;
|
||||
final ValueChanged<NotificationFilter> onFilterChanged;
|
||||
final ScrollController? scrollController;
|
||||
|
||||
const NotificationsView({
|
||||
super.key,
|
||||
this.filter = NotificationFilter.all,
|
||||
required this.onFilterChanged,
|
||||
this.scrollController,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -119,7 +134,7 @@ class NotificationsView extends StatelessWidget {
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<NotificationsBloc>().add(NotificationsFetchRequested());
|
||||
context.read<NotificationsBloc>().add(NotificationsFetchRequested(page: 1));
|
||||
},
|
||||
child: const Text('Повторить'),
|
||||
),
|
||||
@@ -138,9 +153,6 @@ class NotificationsView extends StatelessWidget {
|
||||
return n.category == NotificationCategory.payment;
|
||||
case NotificationFilter.order:
|
||||
return n.category == NotificationCategory.scooter;
|
||||
// || n.category == NotificationCategory.adminInfo
|
||||
// || n.category == NotificationCategory.companyInfo;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
@@ -152,14 +164,39 @@ class NotificationsView extends StatelessWidget {
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<NotificationsBloc>().add(NotificationsFetchRequested());
|
||||
context.read<NotificationsBloc>().add(NotificationsFetchRequested(page: 1));
|
||||
},
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
itemCount: filtered.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _NotificationCard(notification: filtered[index]);
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (scrollInfo) {
|
||||
if (scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent - 50 &&
|
||||
scrollInfo.metrics.maxScrollExtent > 0 &&
|
||||
state.hasMore &&
|
||||
!state.isLoadingMore &&
|
||||
state.status == NotificationsStatus.success) {
|
||||
context.read<NotificationsBloc>().add(NotificationsLoadMoreRequested());
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: ListView.builder(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
itemCount: filtered.length + (state.hasMore && filtered.length >= 10 ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == filtered.length && state.hasMore && filtered.length >= 10) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return _NotificationCard(notification: filtered[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -308,16 +345,23 @@ class _NotificationCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
final localDate = date.toLocal();
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(date);
|
||||
|
||||
if (difference.inDays == 0) {
|
||||
return 'Сегодня, ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
|
||||
} else if (difference.inDays == 1) {
|
||||
return 'Вчера, ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
|
||||
} else {
|
||||
return '${date.day.toString().padLeft(2, '0')}.${date.month.toString().padLeft(2, '0')}.${date.year}';
|
||||
if (localDate.year == now.year &&
|
||||
localDate.month == now.month &&
|
||||
localDate.day == now.day) {
|
||||
return 'Сегодня, ${localDate.hour.toString().padLeft(2, '0')}:${localDate.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
final yesterday = now.subtract(const Duration(days: 1));
|
||||
if (localDate.year == yesterday.year &&
|
||||
localDate.month == yesterday.month &&
|
||||
localDate.day == yesterday.day) {
|
||||
return 'Вчера, ${localDate.hour.toString().padLeft(2, '0')}:${localDate.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
return '${localDate.day.toString().padLeft(2, '0')}.${localDate.month.toString().padLeft(2, '0')}.${localDate.year}';
|
||||
}
|
||||
|
||||
String _getTypeLabel(NotificationType type) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import '../../core/app_colors.dart';
|
||||
import '../../di/service_locator.dart';
|
||||
import '../../domain/entities/payment_card.dart';
|
||||
import '../../domain/usecase/get_payment_cards_usecase.dart';
|
||||
import '../../domain/usecase/get_profile_usecase.dart';
|
||||
import '../../domain/usecase/set_main_payment_card_usecase.dart';
|
||||
import '../components/custom_app_bar.dart';
|
||||
import '../components/gradient_button.dart';
|
||||
@@ -134,11 +135,6 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
_buildInfoLabel('Начислено баллов:'),
|
||||
_buildValueRow('points_icon.png', '2 балла'),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -149,7 +145,7 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
|
||||
_buildInfoLabel('Стоимость поездки:'),
|
||||
_buildValueRow(
|
||||
'money_icon.png',
|
||||
'17,17 BYN',
|
||||
order?.totalOrderPricePrint ?? '- BYN',
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -162,7 +158,7 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
|
||||
_buildInfoLabel('Оплачено:'),
|
||||
_buildValueRow(
|
||||
'money_icon.png',
|
||||
'10,00 BYN',
|
||||
order?.payTotalPricePrint ?? '0,00 BYN',
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -193,9 +189,9 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
|
||||
width: 40,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
'7,17 BYN',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
order?.unpayTotalPricePrint ?? '0,00 BYN',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -227,6 +223,7 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
|
||||
create: (context) => PaymentMethodSheetBloc(
|
||||
getIt<GetPaymentCardsUsecase>(),
|
||||
getIt<SetMainPaymentCardUsecase>(),
|
||||
getIt<GetProfileUseCase>(),
|
||||
)..add(PaymentMethodSheetStarted()),
|
||||
child: PaymentMethodSheet(
|
||||
initialSelectedCard: state.useBalance ? null : state.selectedCard,
|
||||
|
||||
@@ -80,7 +80,7 @@ class PaymentMethodsScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBalanceCard(BuildContext context, int balance) {
|
||||
Widget _buildBalanceCard(BuildContext context, double balance) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
|
||||
@@ -77,7 +77,7 @@ class _PhoneLoginScreenState extends State<PhoneLoginScreen> {
|
||||
return BlocConsumer<VerifyCodeBloc, VerifyCodeState>(
|
||||
listener: (context, state) {
|
||||
if (state.isSuccess) {
|
||||
context.go("/pin");
|
||||
|
||||
} else if (state.error != null) {
|
||||
setState(() { _isFirstTry = false; });
|
||||
codeController.clear();
|
||||
|
||||
@@ -13,7 +13,8 @@ import '../viewmodel/scooter_detail_bloc.dart';
|
||||
|
||||
class ScooterDetailScreen extends StatelessWidget {
|
||||
final String address;
|
||||
const ScooterDetailScreen({super.key, required this.address});
|
||||
final double distance;
|
||||
const ScooterDetailScreen({super.key, required this.address, required this.distance});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -84,7 +85,7 @@ class ScooterDetailScreen extends StatelessWidget {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: ScooterInfoSection(distance: scooter?.distance ?? 0, location: address)),
|
||||
Expanded(child: ScooterInfoSection(distance: distance, location: address, batteryLevel: scooter?.batteryLevel ?? 0,)),
|
||||
const SizedBox(width: 20),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../core/app_colors.dart';
|
||||
import '../../di/service_locator.dart';
|
||||
import '../../domain/entities/payment_card.dart';
|
||||
import '../../domain/usecase/get_payment_cards_usecase.dart';
|
||||
import '../../domain/usecase/get_profile_usecase.dart';
|
||||
import '../../domain/usecase/set_main_payment_card_usecase.dart';
|
||||
import '../components/app_checkbox.dart';
|
||||
import '../components/custom_app_bar.dart';
|
||||
@@ -170,6 +171,7 @@ class SubscriptionDetailsScreen extends StatelessWidget {
|
||||
create: (context) => PaymentMethodSheetBloc(
|
||||
getIt<GetPaymentCardsUsecase>(),
|
||||
getIt<SetMainPaymentCardUsecase>(),
|
||||
getIt<GetProfileUseCase>(),
|
||||
)..add(PaymentMethodSheetStarted()),
|
||||
child: const PaymentMethodSheet(showBalance: false),
|
||||
),
|
||||
|
||||
@@ -1,84 +1,108 @@
|
||||
import 'package:be_happy/presentation/viewmodel/payment_methods_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../di/service_locator.dart';
|
||||
import '../../domain/entities/payment_card.dart';
|
||||
import '../../domain/usecase/get_payment_cards_usecase.dart';
|
||||
import '../../domain/usecase/get_profile_usecase.dart';
|
||||
import '../../domain/usecase/set_main_payment_card_usecase.dart';
|
||||
import '../components/app_checkbox.dart';
|
||||
import '../components/custom_app_bar.dart'; // ✅ Уже есть
|
||||
import '../components/custom_app_bar.dart';
|
||||
import '../components/gradient_button.dart';
|
||||
import '../components/payment_option.dart';
|
||||
import '../components/sheet/payment_method_sheet.dart';
|
||||
import '../event/payment_method_sheet_event.dart';
|
||||
import '../event/payment_methods_event.dart';
|
||||
import '../event/top_up_event.dart';
|
||||
import '../state/top_up_state.dart';
|
||||
import '../viewmodel/payment_method_sheet_bloc.dart';
|
||||
import '../viewmodel/top_up_bloc.dart';
|
||||
|
||||
class TopUpScreen extends StatelessWidget {
|
||||
const TopUpScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: BlocListener<TopUpBloc, TopUpState>(
|
||||
listener: (context, state) {
|
||||
if (state is TopUpPaymentSuccess) {
|
||||
context.go('/home/payment-methods/top-up/result/success');
|
||||
} else if (state is TopUpPaymentFailure) {
|
||||
context.go('/home/payment-methods/top-up/result/failed');
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const CustomAppBar(title: 'Пополнение баланса'),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
BlocBuilder<TopUpBloc, TopUpState>(
|
||||
builder: (context, state) {
|
||||
if (state.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
if (state is TopUpLoading) {
|
||||
return const Expanded(
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTariffList(state, context),
|
||||
const SizedBox(height: 30),
|
||||
_buildPriceInfo(state),
|
||||
const SizedBox(height: 40),
|
||||
const Text(
|
||||
'Способ оплаты',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
_buildCardSelector(state, context),
|
||||
const SizedBox(height: 20),
|
||||
_buildAgreement(state, context),
|
||||
const Spacer(),
|
||||
_buildPayButton(state, () {
|
||||
context.read<TopUpBloc>().add(TopUpButtonPressed());
|
||||
}),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (state is TopUpLoaded) {
|
||||
if (state.isProcessingPayment) {
|
||||
return const Expanded(
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTariffList(state, context),
|
||||
const SizedBox(height: 30),
|
||||
_buildPriceInfo(state),
|
||||
const SizedBox(height: 40),
|
||||
const Text(
|
||||
'Способ оплаты',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
_buildCardSelector(state, context),
|
||||
const SizedBox(height: 20),
|
||||
_buildAgreement(state, context),
|
||||
const Spacer(),
|
||||
_buildPayButton(state, () {
|
||||
context.read<TopUpBloc>().add(TopUpButtonPressed());
|
||||
}),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTariffList(TopUpState state, BuildContext context) {
|
||||
Widget _buildTariffList(TopUpLoaded state, BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 120,
|
||||
child: ListView.separated(
|
||||
@@ -89,14 +113,11 @@ class TopUpScreen extends StatelessWidget {
|
||||
final tariff = state.certificates[index];
|
||||
final isSelected = state.selectedTariff == tariff;
|
||||
return GestureDetector(
|
||||
onTap: () =>
|
||||
context.read<TopUpBloc>().add(SelectCertificate(tariff)),
|
||||
onTap: () => context.read<TopUpBloc>().add(SelectCertificate(tariff)),
|
||||
child: Container(
|
||||
width: 140,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? const Color(0xFF80FFC1)
|
||||
: Colors.white.withOpacity(0.1),
|
||||
color: isSelected ? const Color(0xFF80FFC1) : Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.white24),
|
||||
),
|
||||
@@ -127,7 +148,7 @@ class TopUpScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPriceInfo(TopUpState state) {
|
||||
Widget _buildPriceInfo(TopUpLoaded state) {
|
||||
if (state.selectedTariff == null) return const SizedBox.shrink();
|
||||
return Center(
|
||||
child: Column(
|
||||
@@ -157,7 +178,7 @@ class TopUpScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCardSelector(TopUpState state, BuildContext context) {
|
||||
Widget _buildCardSelector(TopUpLoaded state, BuildContext context) {
|
||||
return state.selectedCard != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
@@ -170,16 +191,14 @@ class TopUpScreen extends StatelessWidget {
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (innerContext) =>
|
||||
BlocProvider(
|
||||
create: (context) =>
|
||||
PaymentMethodSheetBloc(
|
||||
getIt<GetPaymentCardsUsecase>(),
|
||||
getIt<SetMainPaymentCardUsecase>(),
|
||||
)
|
||||
..add(PaymentMethodSheetStarted()),
|
||||
child: const PaymentMethodSheet(showBalance: false,),
|
||||
),
|
||||
builder: (innerContext) => BlocProvider(
|
||||
create: (context) => PaymentMethodSheetBloc(
|
||||
getIt<GetPaymentCardsUsecase>(),
|
||||
getIt<SetMainPaymentCardUsecase>(),
|
||||
getIt<GetProfileUseCase>(),
|
||||
)..add(PaymentMethodSheetStarted()),
|
||||
child: const PaymentMethodSheet(showBalance: false),
|
||||
),
|
||||
);
|
||||
|
||||
if (selectedCard != null) {
|
||||
@@ -210,7 +229,6 @@ class TopUpScreen extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
||||
const Text(
|
||||
'Способ оплаты',
|
||||
style: TextStyle(
|
||||
@@ -220,21 +238,9 @@ class TopUpScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
),
|
||||
Icon(Icons.arrow_forward_ios, size: 12, color: Colors.white.withOpacity(0.6)),
|
||||
Icon(Icons.arrow_forward_ios, size: 12, color: Colors.white.withOpacity(0.4)),
|
||||
Icon(Icons.arrow_forward_ios, size: 12, color: Colors.white.withOpacity(0.2)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -244,20 +250,19 @@ class TopUpScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAgreement(TopUpState state, BuildContext context) {
|
||||
Widget _buildAgreement(TopUpLoaded state, BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
AppCheckbox(
|
||||
value: state.isAgreed,
|
||||
onChanged: (v) =>
|
||||
context.read<TopUpBloc>().add(ToggleAgreement(v ?? false)),
|
||||
onChanged: (v) => context.read<TopUpBloc>().add(ToggleAgreement(v ?? false)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Flexible(
|
||||
const Flexible(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
text: 'Я принимаю условия покупки бонусного пакета, при котором денежные средства не подлежат возврату... ',
|
||||
style: const TextStyle(
|
||||
text: 'Я принимаю условия покупки бонусного пакека, при котором денежные средства не подлежат возврату... ',
|
||||
style: TextStyle(
|
||||
color: AppColors.white70,
|
||||
fontSize: 12,
|
||||
),
|
||||
@@ -268,7 +273,7 @@ class TopUpScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPayButton(TopUpState state, VoidCallback onTap) {
|
||||
Widget _buildPayButton(TopUpLoaded state, VoidCallback onTap) {
|
||||
return GradientButton(
|
||||
text: 'Оплатить',
|
||||
onTap: state.isAgreed ? onTap : null,
|
||||
@@ -279,5 +284,4 @@ class TopUpScreen extends StatelessWidget {
|
||||
fontSize: 16,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,7 +19,7 @@ class ScooterState {
|
||||
final String? address;
|
||||
final String? errorMessage;
|
||||
final String phoneNumber;
|
||||
final int balance;
|
||||
final double balance;
|
||||
final UserCheckFlags flags;
|
||||
final ClientNotification? lastNotification;
|
||||
|
||||
@@ -52,7 +52,7 @@ class ScooterState {
|
||||
String? address,
|
||||
String? errorMessage,
|
||||
String? phoneNumber,
|
||||
int? balance,
|
||||
double? balance,
|
||||
UserCheckFlags? flags,
|
||||
ClientNotification? lastNotification,
|
||||
}) {
|
||||
|
||||
@@ -1,27 +1,43 @@
|
||||
import '../../domain/entities/client_notification.dart';
|
||||
|
||||
enum NotificationsStatus { initial, loading, success, failure }
|
||||
enum NotificationsStatus { initial, loading, loadingMore, success, failure }
|
||||
|
||||
class NotificationsState {
|
||||
final NotificationsStatus status;
|
||||
final List<ClientNotification> notifications;
|
||||
final String? errorMessage;
|
||||
final int currentPage;
|
||||
final int totalPages;
|
||||
final bool hasMore;
|
||||
final bool isLoadingMore;
|
||||
|
||||
const NotificationsState({
|
||||
this.status = NotificationsStatus.initial,
|
||||
this.notifications = const [],
|
||||
this.errorMessage,
|
||||
this.currentPage = 1,
|
||||
this.totalPages = 1,
|
||||
this.hasMore = true,
|
||||
this.isLoadingMore = false,
|
||||
});
|
||||
|
||||
NotificationsState copyWith({
|
||||
NotificationsStatus? status,
|
||||
List<ClientNotification>? notifications,
|
||||
String? errorMessage,
|
||||
int? currentPage,
|
||||
int? totalPages,
|
||||
bool? hasMore,
|
||||
bool? isLoadingMore,
|
||||
}) {
|
||||
return NotificationsState(
|
||||
status: status ?? this.status,
|
||||
notifications: notifications ?? this.notifications,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
totalPages: totalPages ?? this.totalPages,
|
||||
hasMore: hasMore ?? this.hasMore,
|
||||
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ class PaymentConfirmState {
|
||||
final bool paymentCompleted;
|
||||
final ScooterOrder? order;
|
||||
final bool useBalance;
|
||||
final int userBalance;
|
||||
final double userBalance;
|
||||
|
||||
const PaymentConfirmState({
|
||||
this.status = PaymentConfirmStatus.initial,
|
||||
@@ -30,7 +30,7 @@ class PaymentConfirmState {
|
||||
bool? paymentCompleted,
|
||||
ScooterOrder? order,
|
||||
bool? useBalance,
|
||||
int? userBalance,
|
||||
double? userBalance,
|
||||
}) {
|
||||
return PaymentConfirmState(
|
||||
status: status ?? this.status,
|
||||
|
||||
@@ -11,7 +11,7 @@ class PaymentMethodSheetState {
|
||||
PaymentMethodSheetState({
|
||||
this.status = PaymentMethodSheetStatus.initial,
|
||||
this.cards = const [],
|
||||
this.balance = 0.0,
|
||||
this.balance = 0,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ enum PaymentMethodsStatus { initial, loading, success, failure }
|
||||
class PaymentMethodsState {
|
||||
final PaymentMethodsStatus status;
|
||||
final List<PaymentCard> cards;
|
||||
final int balance;
|
||||
final double balance;
|
||||
final String? errorMessage;
|
||||
final bool isDeleting;
|
||||
final bool isSettingMain;
|
||||
@@ -22,7 +22,7 @@ class PaymentMethodsState {
|
||||
PaymentMethodsState copyWith({
|
||||
PaymentMethodsStatus? status,
|
||||
List<PaymentCard>? cards,
|
||||
int? balance,
|
||||
double? balance,
|
||||
String? errorMessage,
|
||||
bool? isDeleting,
|
||||
bool? isSettingMain,
|
||||
|
||||
@@ -10,7 +10,7 @@ class TariffSheetState {
|
||||
final String? errorMessage;
|
||||
final PaymentCard? selectedCard;
|
||||
final bool isInsurance;
|
||||
final int userBalance;
|
||||
final double userBalance;
|
||||
final bool useBalance ;
|
||||
|
||||
TariffSheetState({
|
||||
@@ -29,7 +29,7 @@ class TariffSheetState {
|
||||
PaymentCard? selectedCard,
|
||||
String? errorMessage,
|
||||
bool? isInsurance,
|
||||
int? userBalance,
|
||||
double? userBalance,
|
||||
bool? useBalance,
|
||||
|
||||
}) => TariffSheetState(
|
||||
|
||||
@@ -1,40 +1,55 @@
|
||||
import 'package:be_happy/domain/entities/certificate.dart';
|
||||
import 'package:be_happy/domain/entities/payment_card.dart';
|
||||
|
||||
import '../../domain/entities/top_up_tariff.dart';
|
||||
sealed class TopUpState {
|
||||
const TopUpState();
|
||||
}
|
||||
|
||||
class TopUpState {
|
||||
class TopUpLoading extends TopUpState {
|
||||
const TopUpLoading();
|
||||
}
|
||||
|
||||
class TopUpLoaded extends TopUpState {
|
||||
final List<Certificate> certificates;
|
||||
final List<PaymentCard> cards;
|
||||
final Certificate? selectedTariff;
|
||||
final PaymentCard? selectedCard;
|
||||
final bool isLoading;
|
||||
final bool isAgreed;
|
||||
final bool isProcessingPayment;
|
||||
|
||||
TopUpState({
|
||||
const TopUpLoaded({
|
||||
this.certificates = const [],
|
||||
this.cards = const [],
|
||||
this.selectedTariff,
|
||||
this.selectedCard,
|
||||
this.isLoading = false,
|
||||
this.isAgreed = false,
|
||||
this.isProcessingPayment = false,
|
||||
});
|
||||
|
||||
TopUpState copyWith({
|
||||
TopUpLoaded copyWith({
|
||||
List<Certificate>? certificates,
|
||||
List<PaymentCard>? cards,
|
||||
Certificate? selectedTariff,
|
||||
PaymentCard? selectedCard,
|
||||
bool? isLoading,
|
||||
bool? isAgreed,
|
||||
bool? isProcessingPayment,
|
||||
}) {
|
||||
return TopUpState(
|
||||
return TopUpLoaded(
|
||||
certificates: certificates ?? this.certificates,
|
||||
cards: cards ?? this.cards,
|
||||
selectedTariff: selectedTariff ?? this.selectedTariff,
|
||||
selectedCard: selectedCard ?? this.selectedCard,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
isAgreed: isAgreed ?? this.isAgreed,
|
||||
isProcessingPayment: isProcessingPayment ?? this.isProcessingPayment,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TopUpPaymentSuccess extends TopUpState {
|
||||
const TopUpPaymentSuccess();
|
||||
}
|
||||
|
||||
class TopUpPaymentFailure extends TopUpState {
|
||||
final String errorMessage;
|
||||
const TopUpPaymentFailure(this.errorMessage);
|
||||
}
|
||||
@@ -8,7 +8,11 @@ class PhoneAuthBloc extends Bloc<PhoneAuthEvent, PhoneAuthState> {
|
||||
|
||||
PhoneAuthBloc(this._loginUseCase) : super(PhoneAuthState.initial()) {
|
||||
on<PhoneChanged>((event, emit) {
|
||||
emit(state.copyWith(phone: event.phone));
|
||||
emit(state.copyWith(
|
||||
phone: event.phone,
|
||||
isSuccess: false,
|
||||
error: null,
|
||||
));
|
||||
});
|
||||
|
||||
on<IsAdultChanged>((event, emit) {
|
||||
|
||||
@@ -267,7 +267,7 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
|
||||
) async {
|
||||
await logoutUseCase();
|
||||
stopNotificationStream();
|
||||
splashBloc.add(AuthCheckRequested());
|
||||
splashBloc.add(AuthLogoutRequested());
|
||||
}
|
||||
|
||||
FutureOr<void> _onCheckUser(
|
||||
|
||||
@@ -9,26 +9,53 @@ class NotificationsBloc extends Bloc<NotificationsEvent, NotificationsState> {
|
||||
|
||||
NotificationsBloc(this._getNotificationsUsecase) : super(const NotificationsState()) {
|
||||
on<NotificationsFetchRequested>(_onFetchRequested);
|
||||
on<NotificationsLoadMoreRequested>(_onLoadMoreRequested);
|
||||
}
|
||||
|
||||
Future<void> _onFetchRequested(
|
||||
NotificationsFetchRequested event,
|
||||
Emitter<NotificationsState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: NotificationsStatus.loading));
|
||||
if (event.page == 1) {
|
||||
emit(state.copyWith(status: NotificationsStatus.loading));
|
||||
}
|
||||
|
||||
try {
|
||||
final notifications = await _getNotificationsUsecase();
|
||||
final result = await _getNotificationsUsecase(
|
||||
page: event.page,
|
||||
pageSize: 100,
|
||||
);
|
||||
|
||||
final newNotifications = event.page == 1
|
||||
? result.notifications
|
||||
: [...state.notifications, ...result.notifications];
|
||||
|
||||
emit(state.copyWith(
|
||||
status: NotificationsStatus.success,
|
||||
notifications: notifications,
|
||||
notifications: newNotifications,
|
||||
currentPage: result.pagination.currentPage,
|
||||
totalPages: result.pagination.lastPage,
|
||||
hasMore: result.pagination.currentPage < result.pagination.lastPage,
|
||||
isLoadingMore: false,
|
||||
));
|
||||
} catch (e) {
|
||||
dev.log('NotificationsBloc: Ошибка загрузки: $e');
|
||||
emit(state.copyWith(
|
||||
status: NotificationsStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
status: event.page == 1 ? NotificationsStatus.failure : NotificationsStatus.success,
|
||||
errorMessage: event.page == 1 ? e.toString() : null,
|
||||
isLoadingMore: false,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLoadMoreRequested(
|
||||
NotificationsLoadMoreRequested event,
|
||||
Emitter<NotificationsState> emit,
|
||||
) async {
|
||||
if (!state.hasMore || state.isLoadingMore) return;
|
||||
|
||||
emit(state.copyWith(isLoadingMore: true));
|
||||
final nextPage = state.currentPage + 1;
|
||||
add(NotificationsFetchRequested(page: nextPage));
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,17 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../core/result.dart';
|
||||
import '../../domain/entities/payment_card.dart';
|
||||
import '../../domain/usecase/get_payment_cards_usecase.dart';
|
||||
import '../../domain/usecase/get_profile_usecase.dart';
|
||||
import '../../domain/usecase/set_main_payment_card_usecase.dart';
|
||||
import '../event/payment_method_sheet_event.dart';
|
||||
import '../state/payment_method_sheet_state.dart';
|
||||
|
||||
class PaymentMethodSheetBloc extends Bloc<PaymentMethodSheetEvent, PaymentMethodSheetState> {
|
||||
final GetPaymentCardsUsecase _getPaymentCardsUsecase;
|
||||
final GetProfileUseCase _getProfileUseCase;
|
||||
final SetMainPaymentCardUsecase _setMainPaymentCardUsecase;
|
||||
|
||||
PaymentMethodSheetBloc(this._getPaymentCardsUsecase, this._setMainPaymentCardUsecase)
|
||||
PaymentMethodSheetBloc(this._getPaymentCardsUsecase, this._setMainPaymentCardUsecase, this._getProfileUseCase)
|
||||
: super(PaymentMethodSheetState(status: PaymentMethodSheetStatus.initial)) {
|
||||
on<PaymentMethodSheetStarted>(_onStarted);
|
||||
}
|
||||
@@ -24,10 +26,13 @@ class PaymentMethodSheetBloc extends Bloc<PaymentMethodSheetEvent, PaymentMethod
|
||||
|
||||
try {
|
||||
final result = await _getPaymentCardsUsecase();
|
||||
final profile = await _getProfileUseCase();
|
||||
|
||||
|
||||
if (result is Success<List<PaymentCard>>) {
|
||||
emit(state.copyWith(
|
||||
status: PaymentMethodSheetStatus.success,
|
||||
balance: profile.balance,
|
||||
cards: result.data ?? [],
|
||||
));
|
||||
} else {
|
||||
|
||||
@@ -46,6 +46,8 @@ class PaymentMethodsBloc extends Bloc<PaymentMethodsEvent, PaymentMethodsState>
|
||||
|
||||
|
||||
if (result is Success<List<PaymentCard>>) {
|
||||
|
||||
print("BALANCE: ${profile.balance} BYN");
|
||||
emit(state.copyWith(
|
||||
status: PaymentMethodsStatus.success,
|
||||
cards: result.data ?? [],
|
||||
|
||||
@@ -48,7 +48,7 @@ class ReservedRideBloc extends Bloc<ReservedRideEvent, ReservedRideState> {
|
||||
|
||||
final result = await _cancelRideUsecase(event.orderId);
|
||||
|
||||
if (result is Success<ScooterOrder>) {
|
||||
if (result is Success) {
|
||||
emit(state.copyWith(
|
||||
status: ReservedRideStatus.success,
|
||||
rideCancelled: true,
|
||||
|
||||
@@ -40,7 +40,6 @@ class SendPhotoBloc extends Bloc<SendPhotoEvent, SendPhotoState> {
|
||||
|
||||
switch (result) {
|
||||
case Success<List<int>>():
|
||||
// ✅ Защита: если data null или пустой — не вызываем finish
|
||||
if (result.data == null || result.data!.isEmpty) {
|
||||
emit(state.copyWith(
|
||||
status: SendPhotoStatus.failure,
|
||||
@@ -49,20 +48,19 @@ class SendPhotoBloc extends Bloc<SendPhotoEvent, SendPhotoState> {
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Передаём два параметра в UseCase
|
||||
final finishResult = await _finishRideUsecase(
|
||||
event.orderId,
|
||||
result.data! // ✅ ! т.к. проверили выше
|
||||
result.data!
|
||||
);
|
||||
|
||||
switch (finishResult) {
|
||||
case Success<ScooterOrder>():
|
||||
case Success():
|
||||
emit(state.copyWith(
|
||||
status: SendPhotoStatus.success,
|
||||
recievedPhotoIds: result.data,
|
||||
));
|
||||
case Failure():
|
||||
print("❌ FINISH ERROR: ${finishResult.failure.message}");
|
||||
print("FINISH ERROR: ${finishResult.failure.message}");
|
||||
emit(state.copyWith(
|
||||
status: SendPhotoStatus.failure,
|
||||
errorMessage: 'Ошибка завершения поездки',
|
||||
|
||||
@@ -16,6 +16,9 @@ class SplashBloc extends Bloc<SplashEvent, SplashState> {
|
||||
on<PinVerificationSuccess>((event, emit) {
|
||||
emit(AuthPinVerified());
|
||||
});
|
||||
on<AuthLogoutRequested>((event, emit) {
|
||||
emit(AuthUnauthenticated());
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onAuthCheckRequested(
|
||||
|
||||
@@ -18,9 +18,10 @@ class TopUpBloc extends Bloc<TopUpEvent, TopUpState> {
|
||||
required this.getCertificatesUsecase,
|
||||
required this.purchaseCertificateUsecase,
|
||||
required this.getUserCards,
|
||||
}) : super(TopUpState(isLoading: true)) {
|
||||
}) : super(const TopUpLoading()) {
|
||||
|
||||
on<LoadTopUpData>((event, emit) async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
emit(const TopUpLoading());
|
||||
|
||||
final results = await Future.wait([
|
||||
getCertificatesUsecase(),
|
||||
@@ -33,64 +34,68 @@ class TopUpBloc extends Bloc<TopUpEvent, TopUpState> {
|
||||
switch ((certResult, cardResult)) {
|
||||
case (Success(data: final certs), Success(data: final cards)):
|
||||
emit(
|
||||
state.copyWith(
|
||||
certificates: certs,
|
||||
cards: cards,
|
||||
selectedTariff: certs!.length > 1
|
||||
TopUpLoaded(
|
||||
certificates: certs ?? [],
|
||||
cards: cards ?? [],
|
||||
selectedTariff: certs != null && certs.length > 1
|
||||
? certs[1]
|
||||
: (certs.isNotEmpty ? certs.first : null),
|
||||
|
||||
selectedCard: cards!.isEmpty
|
||||
: (certs != null && certs.isNotEmpty ? certs.first : null),
|
||||
selectedCard: cards == null || cards.isEmpty
|
||||
? null
|
||||
: cards.firstWhere(
|
||||
(c) => c.isMain,
|
||||
orElse: () => cards.first,
|
||||
),
|
||||
isLoading: false,
|
||||
(c) => c.isMain,
|
||||
orElse: () => cards.first,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
case (
|
||||
Success<List<Certificate>>(data: null),
|
||||
Failure<List<PaymentCard>>(),
|
||||
):
|
||||
// TODO: Handle this case.
|
||||
throw UnimplementedError();
|
||||
case (
|
||||
Success<List<Certificate>>(data: [...]),
|
||||
Failure<List<PaymentCard>>(),
|
||||
):
|
||||
// TODO: Handle this case.
|
||||
throw UnimplementedError();
|
||||
case (Failure<List<Certificate>>(), _):
|
||||
// TODO: Handle this case.
|
||||
throw UnimplementedError();
|
||||
default:
|
||||
emit(const TopUpPaymentFailure('Не удалось загрузить данные'));
|
||||
}
|
||||
});
|
||||
on<SelectCertificate>(
|
||||
(event, emit) => emit(state.copyWith(selectedTariff: event.certificate)),
|
||||
);
|
||||
on<SelectCard>(
|
||||
(event, emit) => emit(state.copyWith(selectedCard: event.card)),
|
||||
);
|
||||
on<ToggleAgreement>(
|
||||
(event, emit) => emit(state.copyWith(isAgreed: event.value)),
|
||||
);
|
||||
|
||||
on<SelectCertificate>((event, emit) {
|
||||
if (state is TopUpLoaded) {
|
||||
final currentState = state as TopUpLoaded;
|
||||
emit(currentState.copyWith(selectedTariff: event.certificate));
|
||||
}
|
||||
});
|
||||
|
||||
on<SelectCard>((event, emit) {
|
||||
if (state is TopUpLoaded) {
|
||||
final currentState = state as TopUpLoaded;
|
||||
emit(currentState.copyWith(selectedCard: event.card));
|
||||
}
|
||||
});
|
||||
|
||||
on<ToggleAgreement>((event, emit) {
|
||||
if (state is TopUpLoaded) {
|
||||
final currentState = state as TopUpLoaded;
|
||||
emit(currentState.copyWith(isAgreed: event.value));
|
||||
}
|
||||
});
|
||||
|
||||
on<TopUpButtonPressed>((event, emit) async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
if (state is TopUpLoaded) {
|
||||
final currentState = state as TopUpLoaded;
|
||||
|
||||
if (state.isAgreed) {
|
||||
final result = await purchaseCertificateUsecase(
|
||||
certificateId: state.selectedTariff!.id,
|
||||
cardId:state.selectedCard!.id,
|
||||
);
|
||||
if (currentState.isAgreed &&
|
||||
currentState.selectedTariff != null &&
|
||||
currentState.selectedCard != null) {
|
||||
|
||||
if (result is Success) {
|
||||
emit(state.copyWith(isLoading: false));
|
||||
emit(currentState.copyWith(isProcessingPayment: true));
|
||||
|
||||
final result = await purchaseCertificateUsecase(
|
||||
certificateId: currentState.selectedTariff!.id,
|
||||
cardId: currentState.selectedCard!.id,
|
||||
);
|
||||
|
||||
if (result is Success) {
|
||||
emit(const TopUpPaymentSuccess());
|
||||
} else {
|
||||
emit(const TopUpPaymentFailure('Ошибка при оплате'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@ import 'dart:async';
|
||||
|
||||
import 'package:be_happy/core/failures.dart';
|
||||
import 'package:be_happy/core/result.dart';
|
||||
import 'package:be_happy/presentation/viewmodel/splash_bloc.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../domain/usecase/verify_code_usecase.dart';
|
||||
import '../event/spalsh_event.dart';
|
||||
import '../event/verify_code_event.dart';
|
||||
import '../state/verify_code_state.dart';
|
||||
|
||||
@@ -21,11 +23,11 @@ import '../state/verify_code_state.dart';
|
||||
|
||||
class VerifyCodeBloc extends Bloc<VerifyCodeEvent, VerifyCodeState> {
|
||||
final VerifyCodeUseCase _verifyCodeUseCase;
|
||||
final LoginUseCase _loginUseCase; // 🔹 Добавляем поле для юзкейса логина
|
||||
final LoginUseCase _loginUseCase;
|
||||
final SplashBloc _splashBloc;
|
||||
Timer? _timer;
|
||||
|
||||
// 🔹 Обновляем конструктор
|
||||
VerifyCodeBloc(this._verifyCodeUseCase, this._loginUseCase)
|
||||
VerifyCodeBloc(this._verifyCodeUseCase, this._loginUseCase, this._splashBloc)
|
||||
: super(VerifyCodeState.initial()) {
|
||||
|
||||
on<VerifyCodeStarted>((event, emit) {
|
||||
@@ -96,12 +98,14 @@ class VerifyCodeBloc extends Bloc<VerifyCodeEvent, VerifyCodeState> {
|
||||
|
||||
emit(state.copyWith(isSubmitting: true, error: null));
|
||||
|
||||
// 🔹 Здесь автоматически подставится обновленный из стейта tempToken
|
||||
final result = await _verifyCodeUseCase.execute(state.code, state.tempToken);
|
||||
|
||||
final newState = switch (result) {
|
||||
Success() => (() {
|
||||
_timer?.cancel();
|
||||
|
||||
_splashBloc.add(AuthCheckRequested());
|
||||
|
||||
return state.copyWith(
|
||||
isSubmitting: false,
|
||||
isSuccess: true,
|
||||
|
||||
Reference in New Issue
Block a user