Compare commits
3 Commits
main
...
fix/bug-fi
| Author | SHA1 | Date | |
|---|---|---|---|
| 134ffdde60 | |||
| 591265a7fc | |||
| c996d0847f |
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 26 KiB |
BIN
assets/fonts/DigitalNumbers-Regular.ttf
Normal file
BIN
assets/icons/qr_icon_order.png
Normal file
|
After Width: | Height: | Size: 336 B |
BIN
assets/icons/scooter_reserved_placemark_fill.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/icons/timer.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
assets/news_def.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
assets/splash_logo.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
assets/splash_map.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
@@ -427,7 +427,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
@@ -484,7 +484,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
|||||||
@@ -1,122 +1 @@
|
|||||||
{
|
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-20x20@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-20x20@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-29x29@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-29x29@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-29x29@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-40x40@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-40x40@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "60x60",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-60x60@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "60x60",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-60x60@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-20x20@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-20x20@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-29x29@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-29x29@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-40x40@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-40x40@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "76x76",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-76x76@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "76x76",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-76x76@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "83.5x83.5",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "1024x1024",
|
|
||||||
"idiom" : "ios-marketing",
|
|
||||||
"filename" : "Icon-App-1024x1024@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"version" : 1,
|
|
||||||
"author" : "xcode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 351 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 21 KiB |
@@ -20,6 +20,7 @@ import 'package:path/path.dart';
|
|||||||
import 'package:flutter_client_sse/constants/sse_request_type_enum.dart';
|
import 'package:flutter_client_sse/constants/sse_request_type_enum.dart';
|
||||||
import 'package:flutter_client_sse/flutter_client_sse.dart';
|
import 'package:flutter_client_sse/flutter_client_sse.dart';
|
||||||
|
|
||||||
|
import '../../domain/entities/client_subscription.dart';
|
||||||
import '../../domain/entities/point.dart';
|
import '../../domain/entities/point.dart';
|
||||||
import '../../domain/entities/user_profile.dart';
|
import '../../domain/entities/user_profile.dart';
|
||||||
import '../../domain/entities/payment_card.dart';
|
import '../../domain/entities/payment_card.dart';
|
||||||
@@ -137,7 +138,8 @@ class ApiService {
|
|||||||
if (avatarId != null && profileData["avatar"] != null) {
|
if (avatarId != null && profileData["avatar"] != null) {
|
||||||
final String? avatarPath = profileData["avatar"]["path"];
|
final String? avatarPath = profileData["avatar"]["path"];
|
||||||
if (avatarPath != null && avatarPath.isNotEmpty) {
|
if (avatarPath != null && avatarPath.isNotEmpty) {
|
||||||
avatarUrl = Uri.parse(fileBaseUrl).resolve(avatarPath).toString(); }
|
avatarUrl = Uri.parse(fileBaseUrl).resolve(avatarPath).toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamic balanceRaw = profileData["balance"];
|
dynamic balanceRaw = profileData["balance"];
|
||||||
@@ -394,10 +396,7 @@ class ApiService {
|
|||||||
final url = "$baseUrl/scooter/$title/code";
|
final url = "$baseUrl/scooter/$title/code";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await _dio.get(
|
final response = await _dio.get(url, options: await _getAuthOptions());
|
||||||
url,
|
|
||||||
options: await _getAuthOptions(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
return Scooter.fromJson(response.data);
|
return Scooter.fromJson(response.data);
|
||||||
@@ -463,7 +462,7 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Subscription>> getClientSubscriptions() async {
|
Future<List<ClientSubscription>> getClientSubscriptions() async {
|
||||||
const url = "$baseUrl/scootersubscription/client";
|
const url = "$baseUrl/scootersubscription/client";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -474,14 +473,7 @@ class ApiService {
|
|||||||
final List<dynamic> items = responseData['data'] ?? [];
|
final List<dynamic> items = responseData['data'] ?? [];
|
||||||
|
|
||||||
return items.map((item) {
|
return items.map((item) {
|
||||||
final Map<String, dynamic> subscriptionMap =
|
return ClientSubscription.fromJson(item);
|
||||||
Map<String, dynamic>.from(item['subscription'] ?? {});
|
|
||||||
|
|
||||||
if (item['expiredAt'] != null) {
|
|
||||||
subscriptionMap['activeTo'] = item['expiredAt'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return Subscription.fromJson(subscriptionMap);
|
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
@@ -547,7 +539,7 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> addPaymentCard({
|
Future<void> addPaymentCard({
|
||||||
required String cardNumber,
|
required String cardNumber,
|
||||||
required String cardHolder,
|
required String cardHolder,
|
||||||
required int expirationMonth,
|
required int expirationMonth,
|
||||||
@@ -571,8 +563,9 @@ class ApiService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
return response.data['id'] as int;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw AuthException('Непредвиденный статус: ${response.statusCode}', 0);
|
throw AuthException('Непредвиденный статус: ${response.statusCode}', 0);
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
final data = e.response?.data;
|
final data = e.response?.data;
|
||||||
@@ -674,7 +667,9 @@ class ApiService {
|
|||||||
final firstError = data['message'][0]['message'].toString();
|
final firstError = data['message'][0]['message'].toString();
|
||||||
|
|
||||||
if (firstError.contains("Wrong start zone")) {
|
if (firstError.contains("Wrong start zone")) {
|
||||||
throw WrongZoneException(message: "Некорректная зона для начала поездки.");
|
throw WrongZoneException(
|
||||||
|
message: "Некорректная зона для начала поездки.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,7 +700,9 @@ class ApiService {
|
|||||||
final firstError = data['message'][0]['message'].toString();
|
final firstError = data['message'][0]['message'].toString();
|
||||||
|
|
||||||
if (firstError.contains("Wrong start zone")) {
|
if (firstError.contains("Wrong start zone")) {
|
||||||
throw WrongZoneException(message: "Некорректная зона для начала поездки.");
|
throw WrongZoneException(
|
||||||
|
message: "Некорректная зона для начала поездки.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -791,7 +788,9 @@ class ApiService {
|
|||||||
final firstError = data['message'][0]['message'].toString();
|
final firstError = data['message'][0]['message'].toString();
|
||||||
|
|
||||||
if (firstError.contains("Wrong start zone")) {
|
if (firstError.contains("Wrong start zone")) {
|
||||||
throw WrongZoneException(message: "Некорректная зона для завершения поездки.");
|
throw WrongZoneException(
|
||||||
|
message: "Некорректная зона для завершения поездки.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,7 +802,7 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ScooterOrder?> payRide(int orderId) async {
|
Future<void> payRide(int orderId) async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.put(
|
final response = await _dio.put(
|
||||||
"$baseUrl/scooterorder/$orderId/pay",
|
"$baseUrl/scooterorder/$orderId/pay",
|
||||||
@@ -811,12 +810,12 @@ class ApiService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
return ScooterOrder.fromJson(response.data);
|
// return ScooterOrder.fromJson(response.data);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
_handleDioError(e);
|
_handleDioError(e);
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,7 +877,7 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ScooterOrder?> payScooterOrderWithPhotos({
|
Future<void> payScooterOrderWithPhotos({
|
||||||
required int orderId,
|
required int orderId,
|
||||||
required int? cardId,
|
required int? cardId,
|
||||||
required bool isBalance,
|
required bool isBalance,
|
||||||
@@ -891,12 +890,10 @@ class ApiService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
return ScooterOrder.fromJson(response.data);
|
return;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
_handleDioError(e);
|
_handleDioError(e);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -918,7 +915,6 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<List<Point>> getScooterOrderRouteHistory({required int id}) async {
|
Future<List<Point>> getScooterOrderRouteHistory({required int id}) async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.get(
|
final response = await _dio.get(
|
||||||
@@ -930,13 +926,19 @@ class ApiService {
|
|||||||
final String routeString = response.data['route'] ?? '[]';
|
final String routeString = response.data['route'] ?? '[]';
|
||||||
final List<dynamic> routeList = json.decode(routeString);
|
final List<dynamic> routeList = json.decode(routeString);
|
||||||
|
|
||||||
return routeList.map((item) => Point(
|
return routeList
|
||||||
|
.map(
|
||||||
|
(item) => Point(
|
||||||
(item[1] as num).toDouble(),
|
(item[1] as num).toDouble(),
|
||||||
(item[0] as num).toDouble(),
|
(item[0] as num).toDouble(),
|
||||||
)).toList();
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
throw RouteHistoryNotFoundException(message: "История маршрута не найдена");
|
throw RouteHistoryNotFoundException(
|
||||||
|
message: "История маршрута не найдена",
|
||||||
|
);
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
if (e.response?.statusCode == 401) throw UnauthorizedException();
|
if (e.response?.statusCode == 401) throw UnauthorizedException();
|
||||||
if (e.response?.statusCode == 403) throw AuthBlockException();
|
if (e.response?.statusCode == 403) throw AuthBlockException();
|
||||||
@@ -1038,6 +1040,57 @@ class ApiService {
|
|||||||
return controller.stream;
|
return controller.stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<Map<String, dynamic>>> getNotifications() async {
|
||||||
|
final url = Uri.parse('$baseUrl/notification/client');
|
||||||
|
|
||||||
|
final accessToken = await _securityService.getAccessToken();
|
||||||
|
if (accessToken == null) {
|
||||||
|
print("APISERVICE Error: Access token is null.");
|
||||||
|
throw UnauthorizedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
print("GET NOTIFICATIONS REQUEST:");
|
||||||
|
print("URL: $url");
|
||||||
|
|
||||||
|
final response = await http.get(
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer $accessToken",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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}');
|
||||||
|
}
|
||||||
|
} else if (response.statusCode == 401) {
|
||||||
|
throw UnauthorizedException();
|
||||||
|
} else if (response.statusCode == 403) {
|
||||||
|
throw AuthBlockException();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception('Ошибка сервера: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<Certificate>> getCertificates() async {
|
Future<List<Certificate>> getCertificates() async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.get(
|
final response = await _dio.get(
|
||||||
|
|||||||
@@ -33,4 +33,22 @@ class NotificationRepositoryImpl implements NotificationRepository {
|
|||||||
void closeStream() {
|
void closeStream() {
|
||||||
// соединение закрывается автоматически при отписке от stream
|
// соединение закрывается автоматически при отписке от stream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<ClientNotification>> getNotifications() 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();
|
||||||
|
|
||||||
|
// dev.log('NotificationRepository: Загружено ${notifications.length} уведомлений');
|
||||||
|
|
||||||
|
return notifications;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
// dev.log('NotificationRepository: Ошибка: $e', stackTrace: stackTrace);
|
||||||
|
throw Exception('Не удалось загрузить уведомления: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class PaymentRepositoryImpl implements PaymentRepository {
|
|||||||
required String cvv,
|
required String cvv,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final cardId = await apiService.addPaymentCard(
|
await apiService.addPaymentCard(
|
||||||
cardNumber: cardNumber,
|
cardNumber: cardNumber,
|
||||||
cardHolder: cardHolder,
|
cardHolder: cardHolder,
|
||||||
expirationMonth: int.parse(expiryMonth),
|
expirationMonth: int.parse(expiryMonth),
|
||||||
@@ -48,8 +48,7 @@ class PaymentRepositoryImpl implements PaymentRepository {
|
|||||||
cvv: cvv,
|
cvv: cvv,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Сохраняем полный номер карты локально
|
// await securityService.saveCardFullNumber(cardId, cardNumber);
|
||||||
await securityService.saveCardFullNumber(cardId, cardNumber);
|
|
||||||
|
|
||||||
return Success(null);
|
return Success(null);
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:be_happy/domain/repositories/scooter_repository.dart';
|
|||||||
import '../../core/failures.dart';
|
import '../../core/failures.dart';
|
||||||
import '../../core/result.dart';
|
import '../../core/result.dart';
|
||||||
import '../../domain/entities/active_scooter_order.dart';
|
import '../../domain/entities/active_scooter_order.dart';
|
||||||
|
import '../../domain/entities/client_subscription.dart';
|
||||||
import '../../domain/entities/scooter.dart';
|
import '../../domain/entities/scooter.dart';
|
||||||
import '../../domain/entities/tariff.dart';
|
import '../../domain/entities/tariff.dart';
|
||||||
import '../../domain/entities/subscription.dart';
|
import '../../domain/entities/subscription.dart';
|
||||||
@@ -127,8 +128,8 @@ class ScooterRepositoryImpl extends ScooterRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<List<Subscription>>> getClientSubscriptions() async {
|
Future<Result<List<ClientSubscription>>> getClientSubscriptions() async {
|
||||||
late final Result<List<Subscription>> result;
|
late final Result<List<ClientSubscription>> result;
|
||||||
try {
|
try {
|
||||||
final subscriptions = await _apiService.getClientSubscriptions();
|
final subscriptions = await _apiService.getClientSubscriptions();
|
||||||
result = Success(subscriptions);
|
result = Success(subscriptions);
|
||||||
@@ -268,15 +269,12 @@ class ScooterRepositoryImpl extends ScooterRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<ScooterOrder>> payRide(int orderId) async {
|
Future<Result<void>> payRide(int orderId) async {
|
||||||
late final Result<ScooterOrder> result;
|
late final Result<void> result;
|
||||||
try {
|
try {
|
||||||
final order = await _apiService.payRide(orderId);
|
await _apiService.payRide(orderId);
|
||||||
if (order != null) {
|
result = Success(null);
|
||||||
result = Success(order);
|
|
||||||
} else {
|
|
||||||
result = Failure(UnknownFailure("Неизвестная ошибка"));
|
|
||||||
}
|
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
result = Failure(AuthFailure(e.attemptsLeft));
|
result = Failure(AuthFailure(e.attemptsLeft));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -335,23 +333,19 @@ class ScooterRepositoryImpl extends ScooterRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<ScooterOrder>> payScooterOrderWithPhotos({
|
Future<Result<void>> payScooterOrderWithPhotos({
|
||||||
required int orderId,
|
required int orderId,
|
||||||
required int? cardId,
|
required int? cardId,
|
||||||
required bool isBalance,
|
required bool isBalance,
|
||||||
}) async {
|
}) async {
|
||||||
late final Result<ScooterOrder> result;
|
late final Result<void> result;
|
||||||
try {
|
try {
|
||||||
final order = await _apiService.payScooterOrderWithPhotos(
|
final order = await _apiService.payScooterOrderWithPhotos(
|
||||||
orderId: orderId,
|
orderId: orderId,
|
||||||
cardId: cardId,
|
cardId: cardId,
|
||||||
isBalance: isBalance,
|
isBalance: isBalance,
|
||||||
);
|
);
|
||||||
if (order != null) {
|
result = Success(null);
|
||||||
result = Success(order);
|
|
||||||
} else {
|
|
||||||
result = Failure(UnknownFailure("Неизвестная ошибка"));
|
|
||||||
}
|
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
result = Failure(AuthFailure(e.attemptsLeft));
|
result = Failure(AuthFailure(e.attemptsLeft));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ import '../domain/service/device_info_service.dart';
|
|||||||
import '../domain/usecase/activate_subscription_usecase.dart';
|
import '../domain/usecase/activate_subscription_usecase.dart';
|
||||||
import '../domain/usecase/get_client_subscriptions_usecase.dart';
|
import '../domain/usecase/get_client_subscriptions_usecase.dart';
|
||||||
import '../domain/usecase/get_news_by_id_usecase.dart';
|
import '../domain/usecase/get_news_by_id_usecase.dart';
|
||||||
|
import '../domain/usecase/get_notifications_usecase.dart';
|
||||||
import '../domain/usecase/get_scooter_by_title_usecase.dart';
|
import '../domain/usecase/get_scooter_by_title_usecase.dart';
|
||||||
import '../domain/usecase/get_scooter_order_history_usecase.dart';
|
import '../domain/usecase/get_scooter_order_history_usecase.dart';
|
||||||
import '../domain/usecase/remove_payment_card_usecase.dart';
|
import '../domain/usecase/remove_payment_card_usecase.dart';
|
||||||
@@ -100,6 +101,7 @@ import '../presentation/viewmodel/auth_bloc.dart';
|
|||||||
import '../presentation/viewmodel/edit_profile_bloc.dart';
|
import '../presentation/viewmodel/edit_profile_bloc.dart';
|
||||||
import '../presentation/viewmodel/map_bloc.dart';
|
import '../presentation/viewmodel/map_bloc.dart';
|
||||||
import '../presentation/viewmodel/news_bloc.dart';
|
import '../presentation/viewmodel/news_bloc.dart';
|
||||||
|
import '../presentation/viewmodel/notifications_bloc.dart';
|
||||||
import '../presentation/viewmodel/order_history_bloc.dart';
|
import '../presentation/viewmodel/order_history_bloc.dart';
|
||||||
import '../presentation/viewmodel/scooter_detail_modal_bloc.dart';
|
import '../presentation/viewmodel/scooter_detail_modal_bloc.dart';
|
||||||
import '../presentation/viewmodel/subscription_list_bloc.dart';
|
import '../presentation/viewmodel/subscription_list_bloc.dart';
|
||||||
@@ -110,7 +112,7 @@ final getIt = GetIt.instance;
|
|||||||
Future<void> setupDependencies() async {
|
Future<void> setupDependencies() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await SharedPreferences.getInstance();
|
||||||
final dio = Dio();
|
final dio = Dio();
|
||||||
dio.interceptors.add(LogInterceptor(responseBody: true, requestBody: true));
|
dio.interceptors.add(LogInterceptor(/*requestHeader: false, responseHeader:false, */responseBody: true, requestBody: true));
|
||||||
dio.interceptors.add(AuthInterceptor());
|
dio.interceptors.add(AuthInterceptor());
|
||||||
// HTTP
|
// HTTP
|
||||||
getIt.registerSingleton<http.Client>(http.Client());
|
getIt.registerSingleton<http.Client>(http.Client());
|
||||||
@@ -286,6 +288,13 @@ Future<void> setupDependencies() async {
|
|||||||
getIt.registerSingleton<GetScooterByTitleUsecase>(
|
getIt.registerSingleton<GetScooterByTitleUsecase>(
|
||||||
GetScooterByTitleUsecase(getIt()),
|
GetScooterByTitleUsecase(getIt()),
|
||||||
);
|
);
|
||||||
|
getIt.registerSingleton<GetNotificationsUsecase>(
|
||||||
|
GetNotificationsUsecase(getIt<NotificationRepository>()),
|
||||||
|
);
|
||||||
|
getIt.registerFactory<NotificationsBloc>(
|
||||||
|
() => NotificationsBloc(getIt<GetNotificationsUsecase>()),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
// Blocs
|
// Blocs
|
||||||
getIt.registerLazySingleton<SplashBloc>(() => SplashBloc(getIt()));
|
getIt.registerLazySingleton<SplashBloc>(() => SplashBloc(getIt()));
|
||||||
@@ -310,6 +319,7 @@ Future<void> setupDependencies() async {
|
|||||||
getIt(),
|
getIt(),
|
||||||
getIt(),
|
getIt(),
|
||||||
getIt(),
|
getIt(),
|
||||||
|
getIt(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
24
lib/domain/entities/client_subscription.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:be_happy/domain/entities/subscription.dart';
|
||||||
|
|
||||||
|
class ClientSubscription {
|
||||||
|
final int id;
|
||||||
|
final int subscriptionId;
|
||||||
|
final Subscription subscription;
|
||||||
|
final DateTime? expiredAt;
|
||||||
|
|
||||||
|
ClientSubscription({
|
||||||
|
required this.id,
|
||||||
|
required this.subscriptionId,
|
||||||
|
required this.subscription,
|
||||||
|
this.expiredAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ClientSubscription.fromJson(Map<String, dynamic> json) {
|
||||||
|
return ClientSubscription(
|
||||||
|
id: json['id'] ?? 0,
|
||||||
|
subscriptionId: json['subscriptionId'] ?? 0,
|
||||||
|
subscription: Subscription.fromJson(json['subscription'] as Map<String, dynamic>),
|
||||||
|
expiredAt: json['expiredAt'] != null ? DateTime.parse(json['expiredAt']) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,8 +43,9 @@ class Scooter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'Scooter{id: $id, title: $title}';
|
return 'Scooter{id: $id, title: $title, status: $status, latitude: $latitude, longitude: $longitude, batteryLevel: $batteryLevel, isOnline: $isOnline}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'scooter.dart';
|
|||||||
class ScooterOrder {
|
class ScooterOrder {
|
||||||
final int id;
|
final int id;
|
||||||
final int scooterId;
|
final int scooterId;
|
||||||
final Scooter? scooter;
|
final Scooter scooter;
|
||||||
final int? planId;
|
final int? planId;
|
||||||
final ScooterPlan? plan;
|
final ScooterPlan? plan;
|
||||||
final int clientId;
|
final int clientId;
|
||||||
@@ -33,7 +33,7 @@ class ScooterOrder {
|
|||||||
ScooterOrder({
|
ScooterOrder({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.scooterId,
|
required this.scooterId,
|
||||||
this.scooter,
|
required this.scooter,
|
||||||
this.planId,
|
this.planId,
|
||||||
this.plan,
|
this.plan,
|
||||||
required this.clientId,
|
required this.clientId,
|
||||||
@@ -65,7 +65,7 @@ class ScooterOrder {
|
|||||||
return ScooterOrder(
|
return ScooterOrder(
|
||||||
id: json['id'] ?? 0,
|
id: json['id'] ?? 0,
|
||||||
scooterId: json['scooterId'] ?? 0,
|
scooterId: json['scooterId'] ?? 0,
|
||||||
scooter: json['scooter'] != null ? Scooter.fromJson(json['scooter']) : null,
|
scooter: Scooter.fromJson(json['scooter']),
|
||||||
planId: json['planId'],
|
planId: json['planId'],
|
||||||
plan: json['plan'] != null ? ScooterPlan.fromJson(json['plan']) : null,
|
plan: json['plan'] != null ? ScooterPlan.fromJson(json['plan']) : null,
|
||||||
clientId: json['clientId'] ?? 0,
|
clientId: json['clientId'] ?? 0,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class Subscription {
|
|||||||
final String fullDescription;
|
final String fullDescription;
|
||||||
final int planId;
|
final int planId;
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
|
final bool isCurrent;
|
||||||
final String currency;
|
final String currency;
|
||||||
final DateTime? activeFrom;
|
final DateTime? activeFrom;
|
||||||
final DateTime? activeTo;
|
final DateTime? activeTo;
|
||||||
@@ -28,6 +29,7 @@ class Subscription {
|
|||||||
this.activeTo,
|
this.activeTo,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
|
required this.isCurrent,
|
||||||
required this.options,
|
required this.options,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@ class Subscription {
|
|||||||
activeTo: json['activeTo'] != null ? DateTime.parse(json['activeTo']) : null,
|
activeTo: json['activeTo'] != null ? DateTime.parse(json['activeTo']) : null,
|
||||||
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : DateTime.now(),
|
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : DateTime.now(),
|
||||||
updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt']) : DateTime.now(),
|
updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt']) : DateTime.now(),
|
||||||
|
isCurrent: json['isCurrent'] ?? false,
|
||||||
options: optionsData.map((e) => SubscriptionPeriod.fromJson(e as Map<String, dynamic>)).toList(),
|
options: optionsData.map((e) => SubscriptionPeriod.fromJson(e as Map<String, dynamic>)).toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,7 @@ abstract class NotificationRepository {
|
|||||||
|
|
||||||
/// Закрывает SSE-соединение
|
/// Закрывает SSE-соединение
|
||||||
void closeStream();
|
void closeStream();
|
||||||
|
|
||||||
|
/// получить список уведомлений
|
||||||
|
Future<List<ClientNotification>> getNotifications();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
|||||||
import 'package:be_happy/domain/entities/active_scooter_order.dart';
|
import 'package:be_happy/domain/entities/active_scooter_order.dart';
|
||||||
|
|
||||||
import '../../core/result.dart';
|
import '../../core/result.dart';
|
||||||
|
import '../entities/client_subscription.dart';
|
||||||
import '../entities/point.dart';
|
import '../entities/point.dart';
|
||||||
import '../entities/scooter.dart';
|
import '../entities/scooter.dart';
|
||||||
import '../entities/subscription.dart';
|
import '../entities/subscription.dart';
|
||||||
@@ -16,7 +17,7 @@ abstract class ScooterRepository {
|
|||||||
Future<Result<List<Tariff>>> getAvailableTariffs(int scooterId);
|
Future<Result<List<Tariff>>> getAvailableTariffs(int scooterId);
|
||||||
Future<Result<List<Subscription>>> getAvailableSubscriptions();
|
Future<Result<List<Subscription>>> getAvailableSubscriptions();
|
||||||
Future<Result<Subscription>> getSubscriptionById(int id);
|
Future<Result<Subscription>> getSubscriptionById(int id);
|
||||||
Future<Result<List<Subscription>>> getClientSubscriptions();
|
Future<Result<List<ClientSubscription>>> getClientSubscriptions();
|
||||||
Future<Result<ScooterOrder>> bookScooter({
|
Future<Result<ScooterOrder>> bookScooter({
|
||||||
required int scooterId,
|
required int scooterId,
|
||||||
required int planId,
|
required int planId,
|
||||||
@@ -30,13 +31,13 @@ abstract class ScooterRepository {
|
|||||||
Future<Result<ScooterOrder>> pauseRide(int orderId);
|
Future<Result<ScooterOrder>> pauseRide(int orderId);
|
||||||
Future<Result<ScooterOrder>> resumeRide(int orderId);
|
Future<Result<ScooterOrder>> resumeRide(int orderId);
|
||||||
Future<Result<ScooterOrder>> finishRide(int orderId, List<int> files);
|
Future<Result<ScooterOrder>> finishRide(int orderId, List<int> files);
|
||||||
Future<Result<ScooterOrder>> payRide(int orderId);
|
Future<Result<void>> payRide(int orderId);
|
||||||
Future<Result<List<ScooterOrder>>> getClientOrders();
|
Future<Result<List<ScooterOrder>>> getClientOrders();
|
||||||
Future<Result<List<int>>> uploadScooterPhotos(List<File> images);
|
Future<Result<List<int>>> uploadScooterPhotos(List<File> images);
|
||||||
Future<Result<ActiveScooterOrder>> updateScooterOrderData({
|
Future<Result<ActiveScooterOrder>> updateScooterOrderData({
|
||||||
required int orderId,
|
required int orderId,
|
||||||
});
|
});
|
||||||
Future<Result<ScooterOrder>> payScooterOrderWithPhotos({
|
Future<Result<void>> payScooterOrderWithPhotos({
|
||||||
required int orderId,
|
required int orderId,
|
||||||
required int? cardId,
|
required int? cardId,
|
||||||
required bool isBalance,
|
required bool isBalance,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:be_happy/core/result.dart';
|
import 'package:be_happy/core/result.dart';
|
||||||
import 'package:be_happy/domain/entities/scooter_order.dart';
|
import 'package:be_happy/domain/entities/scooter_order.dart';
|
||||||
|
|
||||||
|
import '../entities/client_subscription.dart';
|
||||||
import '../repositories/scooter_repository.dart';
|
import '../repositories/scooter_repository.dart';
|
||||||
|
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ class GetClientSubscriptionsUsecase {
|
|||||||
|
|
||||||
GetClientSubscriptionsUsecase(this.repository);
|
GetClientSubscriptionsUsecase(this.repository);
|
||||||
|
|
||||||
Future<Result<List<Subscription>>> call() {
|
Future<Result<List<ClientSubscription>>> call() {
|
||||||
return repository.getClientSubscriptions();
|
return repository.getClientSubscriptions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
lib/domain/usecase/get_notifications_usecase.dart
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import '../entities/client_notification.dart';
|
||||||
|
import '../repositories/notification_repository.dart';
|
||||||
|
|
||||||
|
class GetNotificationsUsecase {
|
||||||
|
final NotificationRepository repository;
|
||||||
|
|
||||||
|
GetNotificationsUsecase(this.repository);
|
||||||
|
|
||||||
|
Future<List<ClientNotification>> call() {
|
||||||
|
return repository.getNotifications();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ class PayRideUsecase {
|
|||||||
|
|
||||||
PayRideUsecase(this.repository);
|
PayRideUsecase(this.repository);
|
||||||
|
|
||||||
Future<Result<ScooterOrder>> call(int orderId, int? cardId,
|
Future<Result<void>> call(int orderId, int? cardId,
|
||||||
bool isBalance) {
|
bool isBalance) {
|
||||||
return repository.payScooterOrderWithPhotos(orderId: orderId, cardId: cardId, isBalance: isBalance);
|
return repository.payScooterOrderWithPhotos(orderId: orderId, cardId: cardId, isBalance: isBalance);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class PayScooterOrderWithPhotosUsecase {
|
|||||||
|
|
||||||
PayScooterOrderWithPhotosUsecase(this.repository);
|
PayScooterOrderWithPhotosUsecase(this.repository);
|
||||||
|
|
||||||
Future<Result<ScooterOrder>> call({
|
Future<Result<void>> call({
|
||||||
required int orderId,
|
required int orderId,
|
||||||
required int cardId,
|
required int cardId,
|
||||||
required bool isBalance,
|
required bool isBalance,
|
||||||
|
|||||||
@@ -23,9 +23,21 @@ import 'di/service_locator.dart';
|
|||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
SystemChrome.setEnabledSystemUIMode(
|
||||||
|
SystemUiMode.manual,
|
||||||
|
overlays: [SystemUiOverlay.top],
|
||||||
|
);
|
||||||
|
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
|
const SystemUiOverlayStyle(
|
||||||
|
statusBarColor: Colors.transparent,
|
||||||
|
statusBarIconBrightness: Brightness.light,
|
||||||
|
statusBarBrightness: Brightness.dark,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
await setupDependencies();
|
await setupDependencies();
|
||||||
|
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ import 'package:flutter/material.dart';
|
|||||||
class CancelBookingDialog extends StatelessWidget {
|
class CancelBookingDialog extends StatelessWidget {
|
||||||
const CancelBookingDialog({super.key});
|
const CancelBookingDialog({super.key});
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
bool result = false;
|
|
||||||
|
|
||||||
return Dialog(
|
return Dialog(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
insetPadding: const EdgeInsets.symmetric(horizontal: 40),
|
insetPadding: const EdgeInsets.symmetric(horizontal: 40),
|
||||||
@@ -42,6 +39,7 @@ class CancelBookingDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// 🔹 Кнопка "Отменить" — ПЕРВАЯ, зелёная (градиент), возвращает true
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 52,
|
height: 52,
|
||||||
@@ -52,10 +50,7 @@ class CancelBookingDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () => {
|
onPressed: () => Navigator.pop(context, true), // ✅ true = отменить
|
||||||
result = false,
|
|
||||||
Navigator.pop(context, result)
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
shadowColor: Colors.transparent,
|
shadowColor: Colors.transparent,
|
||||||
@@ -64,9 +59,9 @@ class CancelBookingDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"Оставить",
|
"Отменить",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFF1D273A),
|
color: Color(0xFF1D273A), // тёмный текст на светлом градиенте
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -74,14 +69,13 @@ class CancelBookingDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// 🔹 Кнопка "Оставить" — ВТОРАЯ, тёмная, возвращает false
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 52,
|
height: 52,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () => Navigator.pop(context, false), // ❌ false = оставить
|
||||||
result = true;
|
|
||||||
Navigator.pop(context, result);
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF0D1024),
|
backgroundColor: const Color(0xFF0D1024),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -89,7 +83,7 @@ class CancelBookingDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"Отменить",
|
"Оставить",
|
||||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class FinishRideConfirmationDialog extends StatelessWidget {
|
||||||
|
const FinishRideConfirmationDialog({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
insetPadding: const EdgeInsets.symmetric(horizontal: 40),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF2E3253).withOpacity(0.95),
|
||||||
|
borderRadius: BorderRadius.circular(28),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"Завершить поездку?",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
height: 1.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text(
|
||||||
|
"Вы действительно хотите завершить поездку?",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 14,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// 🔹 Кнопка "Завершить" — ПЕРВАЯ, зелёная (градиент), возвращает true
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 52,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(26),
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [Color(0xFF8EFEB5), Color(0xFF86FEF1)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true), // ✅ true = завершить
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(26),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
"Завершить",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFF1D273A), // тёмный текст на светлом градиенте
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// 🔹 Кнопка "Отмена" — ВТОРАЯ, тёмная, возвращает false
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 52,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false), // ❌ false = отмена
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF0D1024),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(26),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
"Отмена",
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import 'dart:ui';
|
|
||||||
import 'package:be_happy/presentation/event/map_event.dart';
|
|
||||||
import 'package:be_happy/presentation/event/map_settings_modal_event.dart';
|
|
||||||
import 'package:be_happy/presentation/state/map_settings_modal_state.dart';
|
|
||||||
import 'package:be_happy/presentation/viewmodel/map_settings_modal_bloc.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
|
|
||||||
import '../viewmodel/map_bloc.dart';
|
|
||||||
|
|
||||||
class MapSettingsSheet extends StatelessWidget {
|
|
||||||
final VoidCallback? onClose;
|
|
||||||
|
|
||||||
const MapSettingsSheet({super.key, this.onClose});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocBuilder<MapSettingsModalBloc, MapSettingsModalState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
final List<_SettingItemData> items = [
|
|
||||||
_SettingItemData(
|
|
||||||
label: 'Геоточки',
|
|
||||||
icon: Icons.location_on_outlined,
|
|
||||||
color: const Color(0xFF66E3C4),
|
|
||||||
isActive: state.isAllGeomarksActive,
|
|
||||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(AllGeomarksToggled(val)),
|
|
||||||
),
|
|
||||||
_SettingItemData(
|
|
||||||
label: 'Геозоны',
|
|
||||||
icon: Icons.gps_fixed_outlined,
|
|
||||||
color: const Color(0xFF86EFAC),
|
|
||||||
isActive: state.isAllGeozonesActive,
|
|
||||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(AllGeozonesToggled(val)),
|
|
||||||
),
|
|
||||||
_SettingItemData(
|
|
||||||
label: 'Парковка',
|
|
||||||
icon: Icons.home_outlined,
|
|
||||||
color: const Color(0xFFA78BFA),
|
|
||||||
isActive: state.isParkingZoneActive,
|
|
||||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(ParkingZonesToggled(val)),
|
|
||||||
),
|
|
||||||
_SettingItemData(
|
|
||||||
label: 'Парковка запрещена',
|
|
||||||
icon: Icons.block_outlined,
|
|
||||||
color: const Color(0xFFF59E0B),
|
|
||||||
isActive: state.isRestrictedParkingZoneActive,
|
|
||||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(RestrictedParkingZonesToggled(val)),
|
|
||||||
),
|
|
||||||
_SettingItemData(
|
|
||||||
label: 'Запрещено кататься',
|
|
||||||
icon: Icons.warning_amber_outlined,
|
|
||||||
color: const Color(0xFFEF4444),
|
|
||||||
isActive: state.isRestrictedDrivingZoneActive,
|
|
||||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(RestrictedDrivingZonesToggled(val)),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
|
||||||
child: BackdropFilter(
|
|
||||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
|
||||||
child: Container(
|
|
||||||
height: 365,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFF000032).withOpacity(0.88),
|
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'Параметры карты',
|
|
||||||
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.read<MapSettingsModalBloc>().add(ApllyButtonClick());
|
|
||||||
context.read<MapBloc>().add(UpdateMap());
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
'Готово',
|
|
||||||
style: TextStyle(color: Color(0xFF66E3C4), fontSize: 16, fontWeight: FontWeight.w600),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: ListView.builder(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
|
||||||
itemCount: items.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final item = items[index];
|
|
||||||
return ListTile(
|
|
||||||
leading: Icon(item.icon, color: item.color),
|
|
||||||
title: Text(item.label, style: const TextStyle(color: Colors.white)),
|
|
||||||
trailing: Switch.adaptive(
|
|
||||||
value: item.isActive,
|
|
||||||
onChanged: item.onChanged,
|
|
||||||
activeTrackColor: const Color(0xFF66E3C4),
|
|
||||||
inactiveThumbColor: Colors.white,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Вспомогательный класс для описания строк
|
|
||||||
class _SettingItemData {
|
|
||||||
final String label;
|
|
||||||
final IconData icon;
|
|
||||||
final Color color;
|
|
||||||
final bool isActive;
|
|
||||||
final ValueChanged<bool> onChanged;
|
|
||||||
|
|
||||||
_SettingItemData({
|
|
||||||
required this.label,
|
|
||||||
required this.icon,
|
|
||||||
required this.color,
|
|
||||||
required this.isActive,
|
|
||||||
required this.onChanged,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
import 'package:be_happy/domain/entities/scooter.dart';
|
||||||
import 'package:bot_toast/bot_toast.dart';
|
import 'package:bot_toast/bot_toast.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../../di/service_locator.dart';
|
import '../../../di/service_locator.dart';
|
||||||
import '../../event/active_ride_event.dart';
|
import '../../event/active_ride_event.dart';
|
||||||
|
import '../../event/map_event.dart';
|
||||||
import '../../state/active_ride_state.dart';
|
import '../../state/active_ride_state.dart';
|
||||||
import '../../viewmodel/active_ride_bloc.dart';
|
import '../../viewmodel/active_ride_bloc.dart';
|
||||||
|
import '../../viewmodel/map_bloc.dart';
|
||||||
|
import '../dialog/finish_ride_confirmation_dialog.dart';
|
||||||
import '../notification_toast.dart';
|
import '../notification_toast.dart';
|
||||||
|
|
||||||
class ActiveRideSheet extends StatefulWidget {
|
class ActiveRideSheet extends StatefulWidget {
|
||||||
@@ -55,7 +59,7 @@ class _ActiveRideSheetState extends State<ActiveRideSheet> {
|
|||||||
return BlocProvider.value(
|
return BlocProvider.value(
|
||||||
value: _bloc,
|
value: _bloc,
|
||||||
child: BlocConsumer<ActiveRideBloc, ActiveRideState>(
|
child: BlocConsumer<ActiveRideBloc, ActiveRideState>(
|
||||||
listenWhen: (previous, current) => previous.inZone != current.inZone,
|
listenWhen: (previous, current) => previous.inZone != current.inZone || previous.status != current.status,
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (!state.inZone) {
|
if (!state.inZone) {
|
||||||
BotToast.showCustomNotification(
|
BotToast.showCustomNotification(
|
||||||
@@ -70,6 +74,15 @@ class _ActiveRideSheetState extends State<ActiveRideSheet> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.status == ActiveRideStatus.success && state.order != null) {
|
||||||
|
final scooter = state.order!.scooter;
|
||||||
|
context.read<MapBloc>().add(FocusOnScooter(Scooter(id: scooter.id,
|
||||||
|
title: scooter.title, status: scooter.status,
|
||||||
|
latitude: state.longitude, longitude: state.latitude,
|
||||||
|
batteryLevel: scooter.batteryLevel, isOnline: scooter.isOnline,
|
||||||
|
maxSpeed: scooter.maxSpeed, number: scooter.number)));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
// Логика отображения загрузки и ошибок остается прежней
|
// Логика отображения загрузки и ошибок остается прежней
|
||||||
@@ -202,8 +215,8 @@ class _ActiveRideSheetState extends State<ActiveRideSheet> {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontFeatures: [FontFeature.tabularFigures()],
|
fontFeatures: const [FontFeature.tabularFigures()],
|
||||||
fontFamily: 'Digital Numbers',
|
fontFamily: 'DigitalNumbers',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
@@ -290,13 +303,23 @@ class _ActiveRideSheetState extends State<ActiveRideSheet> {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: state.status == ActiveRideStatus.loading
|
onTap: state.status == ActiveRideStatus.loading
|
||||||
? null
|
? null
|
||||||
: () {
|
: () async {
|
||||||
|
// 🔹 Показываем диалог подтверждения
|
||||||
|
final result = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const FinishRideConfirmationDialog(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔹 Если пользователь подтвердил — завершаем и переходим
|
||||||
|
if (result == true) {
|
||||||
_bloc.add(FinishRide(widget.orderId));
|
_bloc.add(FinishRide(widget.orderId));
|
||||||
Navigator.pop(context);
|
Navigator.pop(context); // закрываем ActiveRideSheet
|
||||||
context.go("/home/order-photos/${widget.orderId}");
|
context.go("/home/order-photos/${widget.orderId}");
|
||||||
|
}
|
||||||
|
// 🔹 Если отменил — ничего не делаем, диалог уже закрылся
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: Column(
|
child: Column( // ✅ Вернули Column с иконкой и текстом
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
const Icon(
|
||||||
|
|||||||
@@ -219,11 +219,11 @@ class _RideCardState extends State<_RideCard> {
|
|||||||
displayTime = _elapsedTime;
|
displayTime = _elapsedTime;
|
||||||
}
|
}
|
||||||
final timeString = _formatDuration(displayTime);
|
final timeString = _formatDuration(displayTime);
|
||||||
final statusText = _getStatusText(widget.order.status);
|
final statusText = widget.order.status == 'Booking' ? 'Забронирован' : "Активный";
|
||||||
final statusColor = _getStatusColor(widget.order.status);
|
final statusColor = widget.order.status == 'Booking' ? Color(0xFFFFCC00) : Color(0xFF8bffaa);
|
||||||
|
|
||||||
final scooterNumber =
|
final scooterNumber =
|
||||||
widget.order.scooter?.number ?? widget.order.scooterId.toString();
|
widget.order.scooter.number ?? widget.order.scooterId.toString();
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@@ -261,7 +261,7 @@ class _RideCardState extends State<_RideCard> {
|
|||||||
Text(
|
Text(
|
||||||
statusText,
|
statusText,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: statusColor,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
@@ -269,6 +269,10 @@ class _RideCardState extends State<_RideCard> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Image.asset("assets/icons/qr_icon_order.png"),
|
||||||
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
scooterNumber,
|
scooterNumber,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@@ -277,14 +281,39 @@ class _RideCardState extends State<_RideCard> {
|
|||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
if (isReserved)
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
_getLocationText(),
|
"Тариф",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withOpacity(0.6),
|
color: Color(0xFF8bffaa),
|
||||||
fontSize: 13,
|
fontSize: 11,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Image.asset("assets/icons/timer.png", width: 14,),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
widget.order.plan?.title ?? "Название тарифа",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFF8bffaa),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -301,10 +330,10 @@ class _RideCardState extends State<_RideCard> {
|
|||||||
timeString,
|
timeString,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: statusColor,
|
color: statusColor,
|
||||||
fontSize: 26,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontFeatures: const [FontFeature.tabularFigures()],
|
fontFeatures: const [FontFeature.tabularFigures()],
|
||||||
fontFamily: 'Digital Numbers',
|
fontFamily: 'DigitalNumbers',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -360,7 +389,7 @@ class _RideCardState extends State<_RideCard> {
|
|||||||
switch (status.toLowerCase()) {
|
switch (status.toLowerCase()) {
|
||||||
case 'reserved':
|
case 'reserved':
|
||||||
case 'holding':
|
case 'holding':
|
||||||
return 'Забронировано';
|
return 'Забронирован';
|
||||||
case 'active':
|
case 'active':
|
||||||
case 'in_progress':
|
case 'in_progress':
|
||||||
return 'Активно';
|
return 'Активно';
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import 'package:be_happy/presentation/state/map_settings_modal_state.dart';
|
|||||||
import 'package:be_happy/presentation/viewmodel/map_settings_modal_bloc.dart';
|
import 'package:be_happy/presentation/viewmodel/map_settings_modal_bloc.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
|
|
||||||
import '../../viewmodel/map_bloc.dart';
|
import '../../viewmodel/map_bloc.dart';
|
||||||
|
|
||||||
class MapSettingsSheet extends StatelessWidget {
|
class MapSettingsSheet extends StatelessWidget {
|
||||||
@@ -19,13 +17,6 @@ class MapSettingsSheet extends StatelessWidget {
|
|||||||
return BlocBuilder<MapSettingsModalBloc, MapSettingsModalState>(
|
return BlocBuilder<MapSettingsModalBloc, MapSettingsModalState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final List<_SettingItemData> items = [
|
final List<_SettingItemData> items = [
|
||||||
_SettingItemData(
|
|
||||||
label: 'Геоточки',
|
|
||||||
icon: Icons.location_on_outlined,
|
|
||||||
color: const Color(0xFF66E3C4),
|
|
||||||
isActive: state.isAllGeomarksActive,
|
|
||||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(AllGeomarksToggled(val)),
|
|
||||||
),
|
|
||||||
_SettingItemData(
|
_SettingItemData(
|
||||||
label: 'Геозоны',
|
label: 'Геозоны',
|
||||||
icon: Icons.gps_fixed_outlined,
|
icon: Icons.gps_fixed_outlined,
|
||||||
@@ -41,9 +32,9 @@ class MapSettingsSheet extends StatelessWidget {
|
|||||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(ParkingZonesToggled(val)),
|
onChanged: (val) => context.read<MapSettingsModalBloc>().add(ParkingZonesToggled(val)),
|
||||||
),
|
),
|
||||||
_SettingItemData(
|
_SettingItemData(
|
||||||
label: 'Разрешено кататься',
|
label: 'Парковка запрещена',
|
||||||
icon: Icons.block_outlined,
|
icon: Icons.block_outlined,
|
||||||
color: const Color(0xFF5ECD4C),
|
color: const Color(0xFFF59E0B),
|
||||||
isActive: state.isRestrictedParkingZoneActive,
|
isActive: state.isRestrictedParkingZoneActive,
|
||||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(RestrictedParkingZonesToggled(val)),
|
onChanged: (val) => context.read<MapSettingsModalBloc>().add(RestrictedParkingZonesToggled(val)),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ import '../../state/payment_method_sheet_state.dart';
|
|||||||
import '../../viewmodel/payment_method_sheet_bloc.dart';
|
import '../../viewmodel/payment_method_sheet_bloc.dart';
|
||||||
|
|
||||||
class PaymentMethodSheet extends StatefulWidget {
|
class PaymentMethodSheet extends StatefulWidget {
|
||||||
final PaymentCard? initialSelectedCard; // Добавляем это поле
|
final PaymentCard? initialSelectedCard;
|
||||||
|
final bool showBalance;
|
||||||
|
|
||||||
const PaymentMethodSheet({
|
const PaymentMethodSheet({
|
||||||
super.key,
|
super.key,
|
||||||
this.initialSelectedCard, // Инициализируем в конструкторе
|
this.initialSelectedCard,
|
||||||
|
this.showBalance = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -92,7 +94,7 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
|
|||||||
_selectedPaymentMethod = initialIndex != -1 ? initialIndex : -1;
|
_selectedPaymentMethod = initialIndex != -1 ? initialIndex : -1;
|
||||||
} else {
|
} else {
|
||||||
final mainCardIndex = state.cards.indexWhere((card) => card.isMain);
|
final mainCardIndex = state.cards.indexWhere((card) => card.isMain);
|
||||||
_selectedPaymentMethod = mainCardIndex != -1 ? mainCardIndex : -1;
|
_selectedPaymentMethod = mainCardIndex != -1 ? mainCardIndex : (widget.showBalance ? -1 : 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +171,7 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
if (widget.showBalance) ...[
|
||||||
PaymentOption(
|
PaymentOption(
|
||||||
title: 'Баланс',
|
title: 'Баланс',
|
||||||
subtitle: '${state.balance.toStringAsFixed(2)} BYN',
|
subtitle: '${state.balance.toStringAsFixed(2)} BYN',
|
||||||
@@ -180,8 +183,8 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
|
|||||||
Navigator.pop(context, 'balance');
|
Navigator.pop(context, 'balance');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
|
||||||
...state.cards.asMap().entries.map((entry) {
|
...state.cards.asMap().entries.map((entry) {
|
||||||
final index = entry.key;
|
final index = entry.key;
|
||||||
|
|||||||
@@ -60,7 +60,10 @@ class _ReservedRideSheetState extends State<ReservedRideSheet> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider.value(
|
return BlocProvider.value(
|
||||||
value: _bloc,
|
value: _bloc,
|
||||||
child: Align(
|
child: Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
|
body: Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||||
@@ -70,7 +73,9 @@ class _ReservedRideSheetState extends State<ReservedRideSheet> {
|
|||||||
padding: const EdgeInsets.only(top: 20, bottom: 10),
|
padding: const EdgeInsets.only(top: 20, bottom: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF000032).withOpacity(0.5),
|
color: const Color(0xFF000032).withOpacity(0.5),
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
borderRadius: const BorderRadius.vertical(
|
||||||
|
top: Radius.circular(30),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -143,7 +148,10 @@ class _ReservedRideSheetState extends State<ReservedRideSheet> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
flex: 3,
|
flex: 3,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withOpacity(0.1),
|
color: Colors.white.withOpacity(0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@@ -163,7 +171,8 @@ class _ReservedRideSheetState extends State<ReservedRideSheet> {
|
|||||||
// Инфо
|
// Инфо
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
@@ -228,9 +237,12 @@ class _ReservedRideSheetState extends State<ReservedRideSheet> {
|
|||||||
orderId: widget.orderId,
|
orderId: widget.orderId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (state.status == ReservedRideStatus.failure) {
|
} else if (state.status ==
|
||||||
|
ReservedRideStatus.failure) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(state.errorMessage ?? 'Ошибка')),
|
SnackBar(
|
||||||
|
content: Text(state.errorMessage ?? 'Ошибка'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -256,9 +268,12 @@ class _ReservedRideSheetState extends State<ReservedRideSheet> {
|
|||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.rideCancelled) {
|
if (state.rideCancelled) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
} else if (state.status == ReservedRideStatus.failure) {
|
} else if (state.status ==
|
||||||
|
ReservedRideStatus.failure) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(state.errorMessage ?? 'Ошибка')),
|
SnackBar(
|
||||||
|
content: Text(state.errorMessage ?? 'Ошибка'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -277,16 +292,22 @@ class _ReservedRideSheetState extends State<ReservedRideSheet> {
|
|||||||
onTap: () async {
|
onTap: () async {
|
||||||
final result = await showDialog<bool>(
|
final result = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => const CancelBookingDialog(),
|
builder: (context) =>
|
||||||
|
const CancelBookingDialog(),
|
||||||
);
|
);
|
||||||
if (result != null && result) {
|
if (result != null && result) {
|
||||||
_bloc.add(CancelRide(widget.orderId));
|
_bloc.add(CancelRide(widget.orderId));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
child: BlocBuilder<ReservedRideBloc, ReservedRideState>(
|
child:
|
||||||
|
BlocBuilder<
|
||||||
|
ReservedRideBloc,
|
||||||
|
ReservedRideState
|
||||||
|
>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.status == ReservedRideStatus.loading) {
|
if (state.status ==
|
||||||
|
ReservedRideStatus.loading) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 20,
|
width: 20,
|
||||||
@@ -323,6 +344,7 @@ class _ReservedRideSheetState extends State<ReservedRideSheet> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
|
import 'package:be_happy/presentation/components/gradient_button.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../domain/entities/subscription.dart';
|
import '../../domain/entities/subscription.dart';
|
||||||
|
import '../event/subscription_list_event.dart';
|
||||||
|
|
||||||
class SubscriptionCard extends StatelessWidget {
|
class SubscriptionCard extends StatelessWidget {
|
||||||
final Subscription subscription;
|
final Subscription subscription;
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
|
final DateTime? expiredAt;
|
||||||
|
final VoidCallback? onRefresh;
|
||||||
|
|
||||||
const SubscriptionCard({super.key, required this.subscription, required this.isActive});
|
const SubscriptionCard({super.key, required this.subscription, required this.isActive, this.expiredAt, this.onRefresh});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -15,9 +20,9 @@ class SubscriptionCard extends StatelessWidget {
|
|||||||
? subscription.options.reduce((a, b) => a.price < b.price ? a : b)
|
? subscription.options.reduce((a, b) => a.price < b.price ? a : b)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
final maxDaysOption = subscription.options.isNotEmpty
|
/*final maxDaysOption = subscription.options.isNotEmpty
|
||||||
? subscription.options.reduce((a, b) => a.days > b.days ? a : b)
|
? subscription.options.reduce((a, b) => a.days > b.days ? a : b)
|
||||||
: null;
|
: null;*/
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
@@ -43,7 +48,7 @@ class SubscriptionCard extends StatelessWidget {
|
|||||||
padding: EdgeInsets.all(4),
|
padding: EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withOpacity(0.3),
|
color: Colors.white.withOpacity(0.3),
|
||||||
borderRadius: BorderRadius.circular(12), // Опционально: скругление углов
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
"АКТИВНА",
|
"АКТИВНА",
|
||||||
@@ -64,12 +69,19 @@ class SubscriptionCard extends StatelessWidget {
|
|||||||
subscription.shortDescription,
|
subscription.shortDescription,
|
||||||
style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 14),
|
style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 14),
|
||||||
),
|
),
|
||||||
|
if (isActive && expiredAt != null) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (maxDaysOption != null) ...[
|
Builder(
|
||||||
const SizedBox(height: 16),
|
builder: (context) {
|
||||||
Text(
|
final day = expiredAt!.day.toString().padLeft(2, '0');
|
||||||
"Период действия: до ${maxDaysOption.days} дней",
|
final month = expiredAt!.month.toString().padLeft(2, '0');
|
||||||
|
final year = expiredAt!.year;
|
||||||
|
|
||||||
|
return Text(
|
||||||
|
"Период действия: до $day.$month.$year",
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
@@ -83,15 +95,17 @@ class SubscriptionCard extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
else
|
else
|
||||||
const SizedBox.shrink(),
|
const SizedBox.shrink(),
|
||||||
ElevatedButton(
|
GradientButton(
|
||||||
onPressed: () => context.push("/home/subscriptions/${subscription.id}"),
|
onTap: () async {
|
||||||
style: ElevatedButton.styleFrom(
|
final isSubscribed = await context.push<bool>("/home/subscriptions/${subscription.id}");
|
||||||
backgroundColor: const Color(0xFF80FFD1),
|
if (isSubscribed == true && onRefresh != null) {
|
||||||
foregroundColor: Colors.black,
|
onRefresh!();
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
}
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
},
|
||||||
),
|
text: "Подробнее",
|
||||||
child: const Text("Подробнее", style: TextStyle(fontWeight: FontWeight.bold)),
|
enabled: true,
|
||||||
|
width: 120,
|
||||||
|
height: 40,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -30,4 +30,14 @@ class NotificationReceived extends ScooterEvent {
|
|||||||
NotificationReceived(this.notification);
|
NotificationReceived(this.notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FocusOnScooter extends ScooterEvent {
|
||||||
|
final Scooter scooter;
|
||||||
|
FocusOnScooter(this.scooter);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClearMapPlacemarks extends ScooterEvent {}
|
||||||
|
class ClearMapFocus extends ScooterEvent {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
3
lib/presentation/event/notifications_event.dart
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
sealed class NotificationsEvent {}
|
||||||
|
|
||||||
|
class NotificationsFetchRequested extends NotificationsEvent {}
|
||||||
@@ -58,9 +58,9 @@ import '../../domain/usecase/remove_payment_card_usecase.dart';
|
|||||||
import '../../domain/usecase/save_map_settings_usecase.dart';
|
import '../../domain/usecase/save_map_settings_usecase.dart';
|
||||||
import '../../domain/usecase/set_main_payment_card_usecase.dart';
|
import '../../domain/usecase/set_main_payment_card_usecase.dart';
|
||||||
import '../../domain/usecase/verify_pin_usecase.dart';
|
import '../../domain/usecase/verify_pin_usecase.dart';
|
||||||
import '../components/map_settings_sheet.dart';
|
|
||||||
import '../components/scooter_bottom_sheet.dart';
|
import '../components/scooter_bottom_sheet.dart';
|
||||||
import '../components/sheet/current_rides_sheet.dart';
|
import '../components/sheet/current_rides_sheet.dart';
|
||||||
|
import '../components/sheet/map_settings_sheet.dart';
|
||||||
import '../components/sheet/payment_method_sheet.dart';
|
import '../components/sheet/payment_method_sheet.dart';
|
||||||
import '../components/sheet/reserved_ride_sheet.dart';
|
import '../components/sheet/reserved_ride_sheet.dart';
|
||||||
import '../components/sheet/tariff_sheet.dart';
|
import '../components/sheet/tariff_sheet.dart';
|
||||||
@@ -75,6 +75,7 @@ import '../event/tariff_sheet_event.dart';
|
|||||||
import '../event/top_up_event.dart';
|
import '../event/top_up_event.dart';
|
||||||
import '../screens/add_card_screen.dart'; // ← новый импорт
|
import '../screens/add_card_screen.dart'; // ← новый импорт
|
||||||
import '../screens/license_agreement_screen.dart';
|
import '../screens/license_agreement_screen.dart';
|
||||||
|
import '../screens/notifications_screen.dart';
|
||||||
import '../screens/order_history_screen.dart';
|
import '../screens/order_history_screen.dart';
|
||||||
import '../screens/payment_methods_screen.dart';
|
import '../screens/payment_methods_screen.dart';
|
||||||
import '../screens/phone_login_screen.dart';
|
import '../screens/phone_login_screen.dart';
|
||||||
@@ -371,6 +372,7 @@ class AppRouter {
|
|||||||
SubscriptionDetailsBloc(
|
SubscriptionDetailsBloc(
|
||||||
getIt<GetSubscriptionByIdUsecase>(),
|
getIt<GetSubscriptionByIdUsecase>(),
|
||||||
getIt<ActivateSubscriptionUsecase>(),
|
getIt<ActivateSubscriptionUsecase>(),
|
||||||
|
getIt<GetClientSubscriptionsUsecase>(),
|
||||||
)
|
)
|
||||||
..add(
|
..add(
|
||||||
LoadDetailsEvent(
|
LoadDetailsEvent(
|
||||||
@@ -456,6 +458,10 @@ class AppRouter {
|
|||||||
builder: (context, state) => const OrderHistoryScreen(),
|
builder: (context, state) => const OrderHistoryScreen(),
|
||||||
routes: []
|
routes: []
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'notifications',
|
||||||
|
builder: (context, state) => const NotificationsScreen(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -19,11 +19,14 @@ class AddCardScreen extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: BlocListener<AddCardBloc, AddCardState>(
|
body: BlocListener<AddCardBloc, AddCardState>(
|
||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) {
|
||||||
previous.status != current.status &&
|
print(
|
||||||
current.status == AddCardStatus.success,
|
'Смена статуса: ${previous.status} -> ${current.status} ${current.errorMessage}');
|
||||||
|
return previous.status != current.status &&
|
||||||
|
current.status == AddCardStatus.success;
|
||||||
|
},
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
context.pop();
|
context.pop(true);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
||||||
@@ -35,6 +38,7 @@ class AddCardScreen extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
const CustomAppBar(title: 'Добавление карты'),
|
const CustomAppBar(title: 'Добавление карты'),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
@@ -148,10 +152,7 @@ class AddCardScreen extends StatelessWidget {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: state.isFormValid
|
onTap: state.isFormValid
|
||||||
? () => {
|
? () => {
|
||||||
context.read<AddCardBloc>().add(
|
context.read<AddCardBloc>().add(AddCardSubmitted()),
|
||||||
AddCardSubmitted()),
|
|
||||||
context.read<PaymentMethodsBloc>()..add(PaymentMethodsStarted()),
|
|
||||||
context.pop()
|
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import '../../core/app_colors.dart';
|
import '../../core/app_colors.dart';
|
||||||
import '../components/custom_app_bar.dart';
|
import '../components/custom_app_bar.dart';
|
||||||
@@ -17,12 +18,10 @@ class DocumentsScreen extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// ✅ Используем общий AppBar
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
CustomAppBar(title: 'Документы'),
|
CustomAppBar(title: 'Документы'),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
// Список ссылок
|
|
||||||
LinkRow(
|
LinkRow(
|
||||||
icon: 'assets/icons/doc.png',
|
icon: 'assets/icons/doc.png',
|
||||||
title: 'Договор аренды',
|
title: 'Договор аренды',
|
||||||
@@ -33,14 +32,14 @@ class DocumentsScreen extends StatelessWidget {
|
|||||||
LinkRow(
|
LinkRow(
|
||||||
icon: 'assets/icons/doc.png',
|
icon: 'assets/icons/doc.png',
|
||||||
title: 'Политика конфиденциальности',
|
title: 'Политика конфиденциальности',
|
||||||
onTap: () => openLink('https://...'),
|
onTap: () => context.push('/privacy-policy')
|
||||||
),
|
),
|
||||||
const Divider(height: 1, color: Colors.white24),
|
const Divider(height: 1, color: Colors.white24),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
LinkRow(
|
LinkRow(
|
||||||
icon: 'assets/icons/doc.png',
|
icon: 'assets/icons/doc.png',
|
||||||
title: 'Правила вождения',
|
title: 'Правила вождения',
|
||||||
onTap: () => openLink('https://...'),
|
onTap: () => openLink('https://behappybel.by/#rule'),
|
||||||
),
|
),
|
||||||
const Divider(height: 1, color: Colors.white24),
|
const Divider(height: 1, color: Colors.white24),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class LicenseAgreementScreen extends StatelessWidget {
|
|||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// 🔹 APPBAR С КНОПКОЙ НАЗАД
|
const SizedBox(height: 16),
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: CustomAppBar(title: ' '),
|
child: CustomAppBar(title: ' '),
|
||||||
|
|||||||
@@ -93,7 +93,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
listenWhen: (previous, current) {
|
listenWhen: (previous, current) {
|
||||||
return current.lastNotification !=
|
return current.lastNotification !=
|
||||||
previous.lastNotification ||
|
previous.lastNotification ||
|
||||||
current.flags != previous.flags;
|
current.flags != previous.flags ||
|
||||||
|
previous.selectedScooterForFocus?.id
|
||||||
|
!= current.selectedScooterForFocus?.id;
|
||||||
},
|
},
|
||||||
|
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
@@ -164,14 +166,39 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.selectedScooterForFocus != null) {
|
||||||
|
final targetScooter = state.selectedScooterForFocus!;
|
||||||
|
|
||||||
|
print("RESERVED SCOOTER: $targetScooter");
|
||||||
|
|
||||||
|
_moveCameraToPoint(
|
||||||
|
targetScooter.longitude,
|
||||||
|
targetScooter.latitude,
|
||||||
|
zoom: 17,
|
||||||
|
);
|
||||||
|
|
||||||
|
context.read<MapBloc>().add(ClearMapFocus());
|
||||||
|
}
|
||||||
},
|
},
|
||||||
buildWhen: (previous, current) =>
|
buildWhen: (previous, current) {
|
||||||
previous.scooters != current.scooters ||
|
return previous.scooters != current.scooters ||
|
||||||
previous.zones != current.zones,
|
previous.reservedScooters != current.reservedScooters ||
|
||||||
|
previous.zones != current.zones ||
|
||||||
|
previous.status != current.status;
|
||||||
|
},
|
||||||
|
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final scooters = _buildScooterPlacemarks(
|
final freeScooters = _buildScooterPlacemarks(
|
||||||
state.scooters,
|
scooters: state.scooters,
|
||||||
state.address ?? "Unknown address",
|
iconAsset: 'assets/icons/scooter_placemark_fill.png',
|
||||||
|
isClickable: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final reservedScooters = _buildScooterPlacemarks(
|
||||||
|
scooters: state.reservedScooters ?? [],
|
||||||
|
iconAsset: 'assets/icons/scooter_reserved_placemark_fill.png',
|
||||||
|
isClickable: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
final zonePolygons = _buildZonePolygons(state.zones);
|
final zonePolygons = _buildZonePolygons(state.zones);
|
||||||
@@ -193,9 +220,10 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
},
|
},
|
||||||
mapObjects: [
|
mapObjects: [
|
||||||
...zonePolygons,
|
...zonePolygons,
|
||||||
|
...reservedScooters,
|
||||||
ClusterizedPlacemarkCollection(
|
ClusterizedPlacemarkCollection(
|
||||||
mapId: const MapObjectId('scooters_cluster'),
|
mapId: const MapObjectId('scooters_cluster'),
|
||||||
placemarks: scooters,
|
placemarks: freeScooters,
|
||||||
radius: 30,
|
radius: 30,
|
||||||
minZoom: 15,
|
minZoom: 15,
|
||||||
consumeTapEvents: true,
|
consumeTapEvents: true,
|
||||||
@@ -232,7 +260,6 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
// Индикатор загрузки (отдельный строитель для статуса)
|
|
||||||
BlocBuilder<MapBloc, ScooterState>(
|
BlocBuilder<MapBloc, ScooterState>(
|
||||||
buildWhen: (previous, current) =>
|
buildWhen: (previous, current) =>
|
||||||
previous.status != current.status,
|
previous.status != current.status,
|
||||||
@@ -378,70 +405,42 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onMarkerTap(List<Scooter> scooters) async {
|
void _onMarkerTap(List<Scooter> scooters) async {
|
||||||
|
context.read<MapBloc>().add(CheckUser());
|
||||||
|
final flags = context.read<MapBloc>().state.flags;
|
||||||
|
|
||||||
|
if (!flags.hasCard) {
|
||||||
|
_showExistingNotification(
|
||||||
|
child: PaymentNotificationCard(
|
||||||
|
onBindCard: () {
|
||||||
|
BotToast.cleanAll();
|
||||||
|
context.push("/home/payment-methods");
|
||||||
|
},
|
||||||
|
onClose: () => BotToast.cleanAll(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (flags.hasUnpaidOrder) {
|
||||||
|
_showExistingNotification(
|
||||||
|
child: UnpaidOrderNotificationCard(
|
||||||
|
onClose: () => BotToast.cleanAll(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (flags.hasFine) {
|
||||||
|
_showExistingNotification(
|
||||||
|
child: FineNotificationCard(
|
||||||
|
onClose: () => BotToast.cleanAll(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
context.push(
|
context.push(
|
||||||
"/home/scooter-sheet",
|
"/home/scooter-sheet",
|
||||||
extra: {'scooters': scooters, 'currentLocation': _currentPosition},
|
extra: {'scooters': scooters, 'currentLocation': _currentPosition},
|
||||||
);
|
);
|
||||||
|
|
||||||
/*final scoot = await showModalBottomSheet<Scooter>(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
isDismissible: true,
|
|
||||||
builder: (context) {
|
|
||||||
return BlocProvider(
|
|
||||||
create: (context) =>
|
|
||||||
ScooterDetailModalBloc(
|
|
||||||
getIt<GetAddressByPointUsecase>(),
|
|
||||||
getIt<GetScooterUsecase>(),
|
|
||||||
getIt<GetPedestrianRoutesUsecase>(),
|
|
||||||
)..add(
|
|
||||||
ScooterDetailModalStarted(
|
|
||||||
scooters,
|
|
||||||
_currentPosition!.latitude,
|
|
||||||
_currentPosition!.longitude,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: ScooterBottomSheet(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);*/
|
|
||||||
/*bool? isBooking = false;
|
|
||||||
if (scoot != null) {
|
|
||||||
final result = await context.push('/home/scooter/${scoot.id}');
|
|
||||||
|
|
||||||
if (result == true) {
|
|
||||||
// Даем небольшую задержку, чтобы навигация завершилась корректно
|
|
||||||
await Future.delayed(Duration(milliseconds: 300), () async {
|
|
||||||
isBooking = await showModalBottomSheet<bool>(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
isDismissible: true,
|
|
||||||
builder: (context) => BlocProvider(
|
|
||||||
create: (context) => TariffSheetBloc(
|
|
||||||
getIt<GetAvailableTariffsUsecase>(),
|
|
||||||
getIt<GetPaymentCardsUsecase>(),
|
|
||||||
getIt<BookScooterUsecase>(),
|
|
||||||
),
|
|
||||||
child: TariffSheet(scooter: scoot),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBooking ?? false) {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => BlocProvider(
|
|
||||||
create: (context) =>
|
|
||||||
CurrentRidesBloc(getIt<GetClientOrdersUsecase>())
|
|
||||||
..add(LoadClientOrders(1)),
|
|
||||||
child: CurrentRidesSheet(clientId: 1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onMapSettingsTap() {
|
void _onMapSettingsTap() {
|
||||||
@@ -582,26 +581,26 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
await ClusterIconPainter.initImage('assets/icons/scooter_placemark.png');
|
await ClusterIconPainter.initImage('assets/icons/scooter_placemark.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
List<PlacemarkMapObject> _buildScooterPlacemarks(
|
List<PlacemarkMapObject> _buildScooterPlacemarks({
|
||||||
List<Scooter> scooters,
|
required List<Scooter> scooters,
|
||||||
String address,
|
required String iconAsset,
|
||||||
) {
|
required bool isClickable,
|
||||||
|
}) {
|
||||||
return scooters.map((scooter) {
|
return scooters.map((scooter) {
|
||||||
return PlacemarkMapObject(
|
return PlacemarkMapObject(
|
||||||
mapId: MapObjectId('${scooter.id}'),
|
mapId: MapObjectId('${isClickable ? "" : "reserved_"}${scooter.id}'), // уникальный ID для карты
|
||||||
point: Point(latitude: scooter.longitude, longitude: scooter.latitude),
|
point: Point(latitude: scooter.longitude, longitude: scooter.latitude),
|
||||||
icon: PlacemarkIcon.single(
|
icon: PlacemarkIcon.single(
|
||||||
PlacemarkIconStyle(
|
PlacemarkIconStyle(
|
||||||
image: BitmapDescriptor.fromAssetImage(
|
image: BitmapDescriptor.fromAssetImage(iconAsset),
|
||||||
'assets/icons/scooter_placemark_fill.png',
|
|
||||||
),
|
|
||||||
scale: 0.2,
|
scale: 0.2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
opacity: 1.0,
|
opacity: 1.0,
|
||||||
onTap: (object, point) async => {
|
consumeTapEvents: isClickable,
|
||||||
_onMarkerTap([scooter]),
|
onTap: isClickable
|
||||||
},
|
? (object, point) async => _onMarkerTap([scooter])
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
@@ -696,13 +695,13 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
children: [
|
children: [
|
||||||
_RoundIconButton(
|
_RoundIconButton(
|
||||||
icon: Icons.notifications_sharp,
|
icon: Icons.notifications_sharp,
|
||||||
onPressed: _onNotificationTap,
|
onPressed: () => context.push("/home/notifications"),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_RoundIconButton(
|
_RoundButton(
|
||||||
icon: Icons.directions_run,
|
imagePath: 'assets/icons/scooter_placemark.png',
|
||||||
onPressed: () => context.push("/home/current-rides-sheet"),
|
onPressed: () => context.push("/home/current-rides-sheet"),
|
||||||
),
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -717,7 +716,42 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
right: 0,
|
right: 0,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => context.push("/home/qr-info"),
|
onTap: () {
|
||||||
|
final flags = context.read<MapBloc>().state.flags;
|
||||||
|
|
||||||
|
// 🔹 Проверка флагов — показываем те же карточки, что и в listener
|
||||||
|
if (!flags.hasCard) {
|
||||||
|
_showExistingNotification(
|
||||||
|
child: PaymentNotificationCard(
|
||||||
|
onBindCard: () {
|
||||||
|
BotToast.cleanAll();
|
||||||
|
context.push("/home/payment-methods");
|
||||||
|
},
|
||||||
|
onClose: () => BotToast.cleanAll(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (flags.hasUnpaidOrder) {
|
||||||
|
_showExistingNotification(
|
||||||
|
child: UnpaidOrderNotificationCard(
|
||||||
|
onClose: () => BotToast.cleanAll(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (flags.hasFine) {
|
||||||
|
_showExistingNotification(
|
||||||
|
child: FineNotificationCard(
|
||||||
|
onClose: () => BotToast.cleanAll(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Все проверки пройдены — переход на сканирование
|
||||||
|
context.push("/home/qr-info");
|
||||||
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 64,
|
width: 64,
|
||||||
height: 64,
|
height: 64,
|
||||||
@@ -810,6 +844,40 @@ class _RoundIconButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _RoundButton extends StatelessWidget {
|
||||||
|
final String imagePath;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
|
const _RoundButton({
|
||||||
|
required this.imagePath,
|
||||||
|
required this.onPressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.darkBlue,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: onPressed,
|
||||||
|
child: Center(
|
||||||
|
child: Image.asset(
|
||||||
|
imagePath,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _CircleIconButton extends StatelessWidget {
|
class _CircleIconButton extends StatelessWidget {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final VoidCallback onPressed;
|
final VoidCallback onPressed;
|
||||||
@@ -832,3 +900,18 @@ class _CircleIconButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showExistingNotification({required Widget child}) {
|
||||||
|
BotToast.showCustomNotification(
|
||||||
|
duration: null, // не исчезает, пока пользователь не закроет
|
||||||
|
toastBuilder: (_) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(top: 120), // тот же отступ, что в listener
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: child, // PaymentNotificationCard / UnpaidOrderNotificationCard / FineNotificationCard
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
|||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// 🔹 Заголовок в AppBar
|
const SizedBox(height: 16),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: CustomAppBar(title: widget.title),
|
child: CustomAppBar(title: widget.title),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:developer' as dev;
|
|||||||
import '../../core/app_colors.dart';
|
import '../../core/app_colors.dart';
|
||||||
import '../../di/service_locator.dart';
|
import '../../di/service_locator.dart';
|
||||||
import '../components/custom_app_bar.dart';
|
import '../components/custom_app_bar.dart';
|
||||||
|
import '../components/gradient_button.dart';
|
||||||
import '../event/news_event.dart';
|
import '../event/news_event.dart';
|
||||||
import '../state/news_state.dart';
|
import '../state/news_state.dart';
|
||||||
import '../viewmodel/news_bloc.dart';
|
import '../viewmodel/news_bloc.dart';
|
||||||
@@ -14,11 +15,8 @@ class NewsScreen extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
dev.log('🔍 NewsScreen: Создание экрана новостей');
|
|
||||||
|
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) {
|
create: (context) {
|
||||||
dev.log('🔍 NewsScreen: Создание NewsBloc');
|
|
||||||
return getIt<NewsBloc>()..add(const NewsFetchRequested());
|
return getIt<NewsBloc>()..add(const NewsFetchRequested());
|
||||||
},
|
},
|
||||||
child: const NewsView(),
|
child: const NewsView(),
|
||||||
@@ -31,7 +29,6 @@ class NewsView extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
dev.log('🔍 NewsView: Построение UI');
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Container(
|
body: Container(
|
||||||
@@ -48,7 +45,6 @@ class NewsView extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: BlocBuilder<NewsBloc, NewsState>(
|
child: BlocBuilder<NewsBloc, NewsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
dev.log('🔍 NewsView: Состояние ${state.status}, новостей: ${state.news.length}');
|
|
||||||
|
|
||||||
if (state.status == NewsStatus.initial || state.status == NewsStatus.loading) {
|
if (state.status == NewsStatus.initial || state.status == NewsStatus.loading) {
|
||||||
return const Center(
|
return const Center(
|
||||||
@@ -84,7 +80,6 @@ class NewsView extends StatelessWidget {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
dev.log('🔍 NewsView: Повторная загрузка');
|
|
||||||
context.read<NewsBloc>().add(const NewsFetchRequested());
|
context.read<NewsBloc>().add(const NewsFetchRequested());
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
@@ -173,7 +168,7 @@ class _NewsCard extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.only(bottom: 16),
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF141530),
|
color: const Color(0xFF0A0F2E).withOpacity(0.7),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -196,6 +191,18 @@ class _NewsCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage('assets/news_def.png'),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
news.previewText,
|
news.previewText,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@@ -205,10 +212,14 @@ class _NewsCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
SizedBox(
|
|
||||||
height: 40,
|
Align(
|
||||||
child: OutlinedButton(
|
alignment: Alignment.centerRight,
|
||||||
onPressed: () {
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 150),
|
||||||
|
child: GradientButton(
|
||||||
|
text: 'Подробнее',
|
||||||
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -219,37 +230,10 @@ class _NewsCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
style: OutlinedButton.styleFrom(
|
showArrows: true,
|
||||||
shape: RoundedRectangleBorder(
|
fontSize: 14,
|
||||||
borderRadius: BorderRadius.circular(24),
|
height: 40,
|
||||||
),
|
width: double.infinity,
|
||||||
side: BorderSide(color: AppColors.smsDigit.withOpacity(0.3)),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'Подробнее',
|
|
||||||
style: TextStyle(color: AppColors.smsDigit),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward_ios_sharp,
|
|
||||||
size: 12,
|
|
||||||
color: AppColors.smsDigit,
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward_ios_sharp,
|
|
||||||
size: 12,
|
|
||||||
color: AppColors.smsDigit.withOpacity(0.6),
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward_ios_sharp,
|
|
||||||
size: 12,
|
|
||||||
color: AppColors.smsDigit.withOpacity(0.3),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
350
lib/presentation/screens/notifications_screen.dart
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'dart:developer' as dev;
|
||||||
|
|
||||||
|
import '../../core/app_colors.dart';
|
||||||
|
import '../../di/service_locator.dart';
|
||||||
|
import '../../domain/entities/client_notification.dart';
|
||||||
|
import '../components/custom_app_bar.dart';
|
||||||
|
import '../event/notifications_event.dart';
|
||||||
|
import '../state/notifications_state.dart';
|
||||||
|
import '../viewmodel/notifications_bloc.dart';
|
||||||
|
|
||||||
|
enum NotificationFilter {
|
||||||
|
all,
|
||||||
|
auth,
|
||||||
|
payment,
|
||||||
|
order,
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationsScreen extends StatelessWidget {
|
||||||
|
const NotificationsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return _NotificationsScreenContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NotificationsScreenContent extends StatefulWidget {
|
||||||
|
const _NotificationsScreenContent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_NotificationsScreenContent> createState() => _NotificationsScreenContentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NotificationsScreenContentState extends State<_NotificationsScreenContent> {
|
||||||
|
NotificationFilter _filter = NotificationFilter.all;
|
||||||
|
|
||||||
|
void _setFilter(NotificationFilter filter) {
|
||||||
|
setState(() {
|
||||||
|
_filter = filter;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => getIt<NotificationsBloc>()..add(NotificationsFetchRequested()),
|
||||||
|
child: NotificationsView(
|
||||||
|
filter: _filter,
|
||||||
|
onFilterChanged: _setFilter,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationsView extends StatelessWidget {
|
||||||
|
final NotificationFilter filter;
|
||||||
|
final ValueChanged<NotificationFilter> onFilterChanged;
|
||||||
|
|
||||||
|
const NotificationsView({
|
||||||
|
super.key,
|
||||||
|
this.filter = NotificationFilter.all,
|
||||||
|
required this.onFilterChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@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),
|
||||||
|
|
||||||
|
_buildFilterBar(),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
Expanded(
|
||||||
|
child: BlocBuilder<NotificationsBloc, NotificationsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.status == NotificationsStatus.loading) {
|
||||||
|
return const Center(child: CircularProgressIndicator(color: Colors.white));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status == NotificationsStatus.failure) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Ошибка загрузки уведомлений',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||||
|
child: Text(
|
||||||
|
state.errorMessage ?? '',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.6),
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.read<NotificationsBloc>().add(NotificationsFetchRequested());
|
||||||
|
},
|
||||||
|
child: const Text('Повторить'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final filtered = state.notifications.where((n) {
|
||||||
|
switch (filter) {
|
||||||
|
case NotificationFilter.all:
|
||||||
|
return true;
|
||||||
|
case NotificationFilter.auth:
|
||||||
|
return n.category == NotificationCategory.auth;
|
||||||
|
case NotificationFilter.payment:
|
||||||
|
return n.category == NotificationCategory.payment;
|
||||||
|
case NotificationFilter.order:
|
||||||
|
return n.category == NotificationCategory.scooter;
|
||||||
|
// || n.category == NotificationCategory.adminInfo
|
||||||
|
// || n.category == NotificationCategory.companyInfo;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
if (filtered.isEmpty) {
|
||||||
|
return const _EmptyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
context.read<NotificationsBloc>().add(NotificationsFetchRequested());
|
||||||
|
},
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
itemCount: filtered.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return _NotificationCard(notification: filtered[index]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFilterBar() {
|
||||||
|
final items = [
|
||||||
|
{'label': 'Все', 'value': NotificationFilter.all},
|
||||||
|
{'label': 'Авторизация', 'value': NotificationFilter.auth},
|
||||||
|
{'label': 'Оплата', 'value': NotificationFilter.payment},
|
||||||
|
{'label': 'Поездка', 'value': NotificationFilter.order},
|
||||||
|
];
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
height: 40,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: items.map((item) {
|
||||||
|
final isActive = item['value'] == filter;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 12),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => onFilterChanged(item['value'] as NotificationFilter),
|
||||||
|
child: Container(
|
||||||
|
height: 32,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: isActive ? AppColors.activeButtonGradient : null,
|
||||||
|
color: isActive ? null : Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(
|
||||||
|
color: isActive
|
||||||
|
? Colors.transparent
|
||||||
|
: Colors.white.withOpacity(0.4),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
item['label'] as String,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isActive
|
||||||
|
? AppColors.activeButtonText
|
||||||
|
: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EmptyState extends StatelessWidget {
|
||||||
|
const _EmptyState();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/notification_empty.png',
|
||||||
|
width: 280,
|
||||||
|
height: 280,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return const Icon(
|
||||||
|
Icons.notifications_none_outlined,
|
||||||
|
size: 120,
|
||||||
|
color: Colors.white38,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 40),
|
||||||
|
child: Text(
|
||||||
|
'У вас пока нет уведомлений.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 16,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NotificationCard extends StatelessWidget {
|
||||||
|
final ClientNotification notification;
|
||||||
|
|
||||||
|
const _NotificationCard({required this.notification});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final date = _formatDate(notification.createdAt);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF141530).withOpacity(0.7),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
date,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.6),
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
notification.content,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDate(DateTime date) {
|
||||||
|
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}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getTypeLabel(NotificationType type) {
|
||||||
|
switch (type) {
|
||||||
|
case NotificationType.info:
|
||||||
|
return 'Информация';
|
||||||
|
case NotificationType.attention:
|
||||||
|
return 'Внимание';
|
||||||
|
case NotificationType.warning:
|
||||||
|
return 'Предупреждение';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getCategoryLabel(NotificationCategory category) {
|
||||||
|
switch (category) {
|
||||||
|
case NotificationCategory.auth:
|
||||||
|
return 'Авторизация';
|
||||||
|
case NotificationCategory.zone:
|
||||||
|
return 'Зоны';
|
||||||
|
case NotificationCategory.payment:
|
||||||
|
return 'Оплата';
|
||||||
|
case NotificationCategory.companyInfo:
|
||||||
|
return 'Акции';
|
||||||
|
case NotificationCategory.adminInfo:
|
||||||
|
return 'Админ';
|
||||||
|
case NotificationCategory.scooter:
|
||||||
|
return 'Самокат';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ class OrderHistoryDetailScreen extends StatelessWidget {
|
|||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// 🔹 HEADER
|
const SizedBox(height: 16),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: CustomAppBar(title: 'Поездка $date'),
|
child: CustomAppBar(title: 'Поездка $date'),
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class OrderHistoryView extends StatelessWidget {
|
|||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: CustomAppBar(title: 'История поездок'),
|
child: CustomAppBar(title: 'История поездок'),
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import '../components/custom_app_bar.dart';
|
|||||||
import '../components/gradient_button.dart';
|
import '../components/gradient_button.dart';
|
||||||
import '../components/payment_option.dart';
|
import '../components/payment_option.dart';
|
||||||
import '../components/sheet/payment_method_sheet.dart';
|
import '../components/sheet/payment_method_sheet.dart';
|
||||||
|
import '../event/map_event.dart';
|
||||||
import '../event/payment_confirm_event.dart';
|
import '../event/payment_confirm_event.dart';
|
||||||
import '../event/payment_method_sheet_event.dart';
|
import '../event/payment_method_sheet_event.dart';
|
||||||
import '../state/payment_confirm_state.dart';
|
import '../state/payment_confirm_state.dart';
|
||||||
|
import '../viewmodel/map_bloc.dart';
|
||||||
import '../viewmodel/payment_confirm_bloc.dart';
|
import '../viewmodel/payment_confirm_bloc.dart';
|
||||||
import '../viewmodel/payment_method_sheet_bloc.dart';
|
import '../viewmodel/payment_method_sheet_bloc.dart';
|
||||||
|
|
||||||
@@ -62,6 +64,7 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: CustomAppBar(title: 'Завершение поездки'),
|
child: CustomAppBar(title: 'Завершение поездки'),
|
||||||
@@ -77,6 +80,7 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
|
|||||||
|
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.status == PaymentConfirmStatus.success && state.paymentCompleted) {
|
if (state.status == PaymentConfirmStatus.success && state.paymentCompleted) {
|
||||||
|
context.read<MapBloc>().add(ClearMapPlacemarks());
|
||||||
context.go('/home');
|
context.go('/home');
|
||||||
} else if (state.status == PaymentConfirmStatus.failure) {
|
} else if (state.status == PaymentConfirmStatus.failure) {
|
||||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||||
|
|||||||
@@ -17,14 +17,6 @@ class PaymentMethodsScreen extends StatelessWidget {
|
|||||||
body: Container(
|
body: Container(
|
||||||
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
child: CustomAppBar(title: 'Способы оплаты'),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
Expanded(
|
|
||||||
child: BlocConsumer<PaymentMethodsBloc, PaymentMethodsState>(
|
child: BlocConsumer<PaymentMethodsBloc, PaymentMethodsState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.status == PaymentMethodsStatus.failure) {
|
if (state.status == PaymentMethodsStatus.failure) {
|
||||||
@@ -34,11 +26,26 @@ class PaymentMethodsScreen extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.status == PaymentMethodsStatus.loading && state.cards.isEmpty) {
|
final isNetworkProcessing = state.status == PaymentMethodsStatus.loading ||
|
||||||
return const Center(child: CircularProgressIndicator(color: Color(0xFF00D4AA)));
|
(state.isDeleting ?? false) ||
|
||||||
}
|
(state.isSettingMain ?? false);
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: CustomAppBar(title: 'Способы оплаты'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Expanded(
|
||||||
|
child: state.cards.isEmpty && state.status == PaymentMethodsStatus.loading
|
||||||
|
? const Center(
|
||||||
|
child: CircularProgressIndicator(color: Color(0xFF00D4AA)),
|
||||||
|
)
|
||||||
|
: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
@@ -48,12 +55,26 @@ class PaymentMethodsScreen extends StatelessWidget {
|
|||||||
_buildCardsList(context, state),
|
_buildCardsList(context, state),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
if (isNetworkProcessing && state.cards.isNotEmpty)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withOpacity(0.4),
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Color(0xFF00D4AA),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -162,7 +183,13 @@ class PaymentMethodsScreen extends StatelessWidget {
|
|||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => context.go('/home/payment-methods/add-card'),
|
onTap: () async {
|
||||||
|
final isCardAdded = await context.push<bool>('/home/payment-methods/add-card');
|
||||||
|
|
||||||
|
if (isCardAdded == true && context.mounted) {
|
||||||
|
context.read<PaymentMethodsBloc>().add(PaymentMethodsStarted());
|
||||||
|
}
|
||||||
|
},
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
child: const Padding(
|
child: const Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class PrivacyPolicyScreen extends StatelessWidget {
|
|||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: CustomAppBar(title: ''),
|
child: CustomAppBar(title: ''),
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Container(
|
body: Container(
|
||||||
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
decoration: const BoxDecoration(
|
||||||
|
gradient: AppColors.phoneScreenBg,
|
||||||
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: BlocBuilder<ProfileBloc, ProfileState>(
|
child: BlocBuilder<ProfileBloc, ProfileState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
@@ -70,33 +72,53 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final profile = state.profile!;
|
final profile = state.profile!;
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
CustomAppBar(title: 'Профиль'),
|
CustomAppBar(title: 'Профиль'),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
Stack(
|
Stack(
|
||||||
alignment: Alignment.topRight,
|
alignment: Alignment.topRight,
|
||||||
children: [
|
children: [
|
||||||
|
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 60,
|
radius: 60,
|
||||||
backgroundColor: AppColors.checkboxFill,
|
backgroundColor: AppColors.checkboxFill,
|
||||||
backgroundImage: (profile.avatarUrl != null && profile.avatarUrl!.isNotEmpty)
|
backgroundImage:
|
||||||
? NetworkImage("${profile.avatarUrl!}?v=${DateTime.now().minute}")
|
(profile.avatarUrl != null &&
|
||||||
: null,
|
profile.avatarUrl!.isNotEmpty)
|
||||||
child: (profile.avatarUrl == null || profile.avatarUrl!.isEmpty)
|
? NetworkImage(
|
||||||
? Text(
|
"${profile.avatarUrl!}?v=${DateTime.now().minute}",
|
||||||
profile.name.isNotEmpty ? profile.name[0].toUpperCase() : '',
|
|
||||||
style: const TextStyle(fontSize: 50, color: AppColors.darkBlue),
|
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
), GestureDetector(
|
child:
|
||||||
|
(profile.avatarUrl == null ||
|
||||||
|
profile.avatarUrl!.isEmpty)
|
||||||
|
? Text(
|
||||||
|
profile.name.isNotEmpty
|
||||||
|
? profile.name[0].toUpperCase()
|
||||||
|
: '',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 50,
|
||||||
|
color: AppColors.darkBlue,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
|
||||||
|
GestureDetector(
|
||||||
onTap: _pickImage,
|
onTap: _pickImage,
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.only(top: 0, right: 0),
|
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/icons/edit.png',
|
'assets/icons/edit.png',
|
||||||
width: 24,
|
width: 24,
|
||||||
@@ -106,20 +128,26 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
_ProfileInfoBlock(
|
_ProfileInfoBlock(
|
||||||
profile: profile,
|
profile: profile,
|
||||||
onEditTap: () => context.go("/home/profile/edit"),
|
onEditTap: () => context.go("/home/profile/edit"),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
|
||||||
_SettingsBlock(
|
// const SizedBox(height: 24),
|
||||||
notificationsEnabled: notificationsEnabled,
|
// _SettingsBlock(
|
||||||
onNotificationsChanged: (v) =>
|
// notificationsEnabled: notificationsEnabled,
|
||||||
setState(() => notificationsEnabled = v),
|
// onNotificationsChanged: (v) =>
|
||||||
),
|
// setState(() => notificationsEnabled = v),
|
||||||
const SizedBox(height: 24),
|
// ),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class _PromoCodeScreenState extends State<PromoCodeScreen> {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
CustomAppBar(title: 'Промокоды'),
|
CustomAppBar(title: 'Промокоды'),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class QRScanInfoScreen extends StatelessWidget {
|
|||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: CustomAppBar(title: "Сканирование QR-кода"),
|
child: CustomAppBar(title: "Сканирование QR-кода"),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:mobile_scanner/mobile_scanner.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:be_happy/di/service_locator.dart';
|
import 'package:be_happy/di/service_locator.dart';
|
||||||
import 'package:be_happy/domain/usecase/get_scooter_by_title_usecase.dart';
|
import 'package:be_happy/domain/usecase/get_scooter_by_title_usecase.dart';
|
||||||
|
import '../components/custom_app_bar.dart';
|
||||||
import '../components/gradient_button.dart';
|
import '../components/gradient_button.dart';
|
||||||
|
|
||||||
class QrScanScreen extends StatefulWidget {
|
class QrScanScreen extends StatefulWidget {
|
||||||
@@ -149,11 +150,13 @@ class _QrScanScreenState extends State<QrScanScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// ✅ ИЗМЕНЕНО: прижимаем аппбар к левому краю
|
||||||
SafeArea(
|
SafeArea(
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, // ✅ Выравнивание по левому краю
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
CustomAppBar(title: "Сканирование QR-кода"),
|
||||||
const SizedBox(height: 60),
|
const SizedBox(height: 60),
|
||||||
const Text(
|
const Text(
|
||||||
'Наведите рамку на QR-код — номер будет распознан автоматически',
|
'Наведите рамку на QR-код — номер будет распознан автоматически',
|
||||||
@@ -170,7 +173,6 @@ class _QrScanScreenState extends State<QrScanScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
SafeArea(
|
SafeArea(
|
||||||
child: Align(
|
child: Align(
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class _ScooterCodeInputScreenState extends State<ScooterCodeInputScreen> {
|
|||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: CustomAppBar(title: "Ввод QR-кода"),
|
child: CustomAppBar(title: "Ввод QR-кода"),
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ class ScooterDetailScreen extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
CustomAppBar(
|
CustomAppBar(
|
||||||
title: scooter?.title != null ? 'Самокат ${scooter!.title}' : 'Самокат',
|
title: scooter?.title != null ? 'Самокат ${scooter!.title}' : 'Самокат',
|
||||||
),
|
),
|
||||||
@@ -101,6 +102,7 @@ class ScooterDetailScreen extends StatelessWidget {
|
|||||||
context.pushReplacement('/home/tarif-sheet', extra: scooter);
|
context.pushReplacement('/home/tarif-sheet', extra: scooter);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 30)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -123,6 +123,11 @@ class _SendPhotoViewState extends State<SendPhotoView> {
|
|||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: CustomAppBar(title: "Отправить фото"),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 100),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 100),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
import 'package:be_happy/presentation/event/spalsh_event.dart';
|
import 'package:be_happy/presentation/event/spalsh_event.dart';
|
||||||
import 'package:be_happy/presentation/viewmodel/splash_bloc.dart';
|
import 'package:be_happy/presentation/viewmodel/splash_bloc.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
|
||||||
// Подключи сюда свои реальные экраны:
|
|
||||||
import 'phone_screen.dart';
|
|
||||||
import 'pin_login_screen.dart';
|
|
||||||
|
|
||||||
class SplashScreen extends StatefulWidget {
|
class SplashScreen extends StatefulWidget {
|
||||||
const SplashScreen({super.key});
|
const SplashScreen({super.key});
|
||||||
@@ -19,35 +14,61 @@ class SplashScreen extends StatefulWidget {
|
|||||||
class _SplashScreenState extends State<SplashScreen>
|
class _SplashScreenState extends State<SplashScreen>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
late final AnimationController _controller;
|
late final AnimationController _controller;
|
||||||
late final Animation<double> _revealAnimation;
|
|
||||||
|
|
||||||
static const double logoSize = 300;
|
// Фаза 1: Заполнение цветом слева направо (0.0 -> 0.5)
|
||||||
|
late final Animation<double> _fillProgress;
|
||||||
|
|
||||||
|
// Фаза 2: Укатывание вправо (0.6 -> 1.0)
|
||||||
|
late final Animation<double> _rollTranslation;
|
||||||
|
late final Animation<double> _rollRotation;
|
||||||
|
|
||||||
|
// Уменьшенный размер логотипа по вашему запросу
|
||||||
|
static const double logoSize = 130;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
// контроллер анимации
|
|
||||||
_controller = AnimationController(
|
_controller = AnimationController(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
duration: const Duration(milliseconds: 2500),
|
duration: const Duration(milliseconds: 3000),
|
||||||
);
|
);
|
||||||
|
|
||||||
// анимация движения "затемняющего" прямоугольника
|
_fillProgress = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||||
_revealAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
CurvedAnimation(
|
||||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
parent: _controller,
|
||||||
|
curve: const Interval(0.0, 0.5, curve: Curves.easeInOut),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// запускаем анимацию
|
_rollTranslation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||||
_controller.forward().then((_) async {
|
CurvedAnimation(
|
||||||
// небольшая пауза после анимации
|
parent: _controller,
|
||||||
await Future.delayed(const Duration(milliseconds: 800));
|
curve: const Interval(0.6, 1.0, curve: Curves.easeInCubic),
|
||||||
if (!mounted) {
|
),
|
||||||
return;
|
);
|
||||||
}
|
|
||||||
|
_rollRotation = Tween<double>(begin: 0.0, end: 2 * math.pi).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: const Interval(0.6, 1.0, curve: Curves.easeIn),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Добавляем задержку перед стартом, чтобы пользователь успел увидеть экран
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
// Ждем 500мс после того, как первый кадр отрисовался
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
// Запускаем анимацию
|
||||||
|
_controller.forward().then((_) {
|
||||||
|
if (!mounted) return;
|
||||||
context.read<SplashBloc>().add(AuthCheckRequested());
|
context.read<SplashBloc>().add(AuthCheckRequested());
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -56,69 +77,102 @@ class _SplashScreenState extends State<SplashScreen>
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final double screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
final double endTranslation = screenWidth / 2 + logoSize;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF3A3A3A),
|
body: Stack(
|
||||||
body: Center(
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Color(0xFF293A69),
|
||||||
|
Color(0xFF202741),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 2. Волна
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/wave.png',
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/splash_map.png',
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Center(
|
||||||
child: AnimatedBuilder(
|
child: AnimatedBuilder(
|
||||||
animation: _controller,
|
animation: _controller,
|
||||||
builder: (context, _) {
|
builder: (context, child) {
|
||||||
final double offset = _revealAnimation.value * (logoSize * 1.2);
|
final double translationX = _rollTranslation.value * endTranslation;
|
||||||
|
|
||||||
return Stack(
|
return Transform(
|
||||||
|
transform: Matrix4.translationValues(translationX, 0, 0)
|
||||||
|
..rotateZ(_rollRotation.value),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
// Используем ShaderMask для эффекта заполнения/проявления
|
||||||
// Цветной логотип (на заднем плане)
|
child: ShaderMask(
|
||||||
Image.asset(
|
shaderCallback: (bounds) {
|
||||||
'assets/logo_color.png',
|
return LinearGradient(
|
||||||
|
begin: Alignment.centerLeft,
|
||||||
|
end: Alignment.centerRight,
|
||||||
|
// Используем _fillProgress для сдвига жесткой границы градиента
|
||||||
|
colors: const [Colors.white, Colors.transparent],
|
||||||
|
stops: [_fillProgress.value, _fillProgress.value],
|
||||||
|
).createShader(bounds);
|
||||||
|
},
|
||||||
|
blendMode: BlendMode.dstIn, // Оставляет только ту часть логотипа, где градиент белый
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/splash_logo.png',
|
||||||
width: logoSize,
|
width: logoSize,
|
||||||
height: logoSize,
|
height: logoSize,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
|
|
||||||
// Прямоугольник, который "уезжает" вправо, открывая логотип
|
|
||||||
ClipRect(
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: FractionallySizedBox(
|
|
||||||
widthFactor: 1,
|
|
||||||
child: Container(
|
|
||||||
width: logoSize,
|
|
||||||
height: logoSize,
|
|
||||||
color: const Color(0xFF3A3A3A),
|
|
||||||
transform: Matrix4.translationValues(offset, 0, 0),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Обводка логотипа (поверх)
|
|
||||||
Image.asset(
|
|
||||||
'assets/logo_outline.png',
|
|
||||||
width: logoSize * 1.01,
|
|
||||||
height: logoSize * 1.01,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
bottomNavigationBar: Padding(
|
// 5. Версия приложения
|
||||||
padding: const EdgeInsets.only(bottom: 24),
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 24,
|
||||||
child: Text(
|
child: Text(
|
||||||
'Версия приложения 1.0',
|
'Версия приложения 1.0',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withOpacity(0.8),
|
color: Colors.white.withOpacity(0.6),
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w300,
|
||||||
|
letterSpacing: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../../core/app_colors.dart';
|
||||||
import '../components/app_checkbox.dart';
|
import '../components/app_checkbox.dart';
|
||||||
|
import '../components/custom_app_bar.dart'; // ✅ Добавь импорт
|
||||||
import '../components/gradient_button.dart';
|
import '../components/gradient_button.dart';
|
||||||
import '../components/period_selector.dart';
|
import '../components/period_selector.dart';
|
||||||
import '../event/subscription_details_event.dart';
|
import '../event/subscription_details_event.dart';
|
||||||
@@ -17,25 +19,42 @@ class SubscriptionDetailsScreen extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF1A2355),
|
body: Container(
|
||||||
appBar: AppBar(
|
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
||||||
backgroundColor: Colors.transparent,
|
child: SafeArea(
|
||||||
elevation: 0,
|
child: Column(
|
||||||
leading: IconButton(
|
children: [
|
||||||
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
|
const SizedBox(height: 16),
|
||||||
onPressed: () => context.pop(),
|
Padding(
|
||||||
),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
title: BlocBuilder<SubscriptionDetailsBloc, SubscriptionDetailsState>(
|
child:
|
||||||
builder: (context, state) {
|
BlocConsumer<
|
||||||
if (state is DetailsContentState) {
|
SubscriptionDetailsBloc,
|
||||||
return Text(state.subscription.title);
|
SubscriptionDetailsState
|
||||||
|
>(
|
||||||
|
listenWhen: (previous, current) =>
|
||||||
|
current is DetailsContentState && current.isSuccess,
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state is DetailsContentState && state.isSuccess) {
|
||||||
|
context.pop(true);
|
||||||
}
|
}
|
||||||
return const Text("Загрузка...");
|
},
|
||||||
|
builder: (context, state) {
|
||||||
|
String title = "Загрузка...";
|
||||||
|
if (state is DetailsContentState) {
|
||||||
|
title = state.subscription.title;
|
||||||
|
}
|
||||||
|
return CustomAppBar(title: title);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Stack(
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// 🔹 Контент
|
||||||
|
Expanded(
|
||||||
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
|
// Волна снизу
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -45,11 +64,16 @@ class SubscriptionDetailsScreen extends StatelessWidget {
|
|||||||
child: Image.asset('assets/wave.png'),
|
child: Image.asset('assets/wave.png'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BlocBuilder<SubscriptionDetailsBloc, SubscriptionDetailsState>(
|
BlocBuilder<
|
||||||
|
SubscriptionDetailsBloc,
|
||||||
|
SubscriptionDetailsState
|
||||||
|
>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state is DetailsLoading) {
|
if (state is DetailsLoading) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator(color: Color(0xFF80FFD1)),
|
child: CircularProgressIndicator(
|
||||||
|
color: Color(0xFF80FFD1),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (state is DetailsError) {
|
if (state is DetailsError) {
|
||||||
@@ -67,16 +91,19 @@ class SubscriptionDetailsScreen extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context, DetailsContentState state) {
|
Widget _buildContent(BuildContext context, DetailsContentState state) {
|
||||||
|
final bool isAvailableForPurchase = state.subscription.isActive;
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(20.0),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -88,6 +115,8 @@ class SubscriptionDetailsScreen extends StatelessWidget {
|
|||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
if (isAvailableForPurchase) ...[
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
_ActionCard(state: state),
|
_ActionCard(state: state),
|
||||||
@@ -95,16 +124,21 @@ class SubscriptionDetailsScreen extends StatelessWidget {
|
|||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
GradientButton(
|
GradientButton(
|
||||||
text: 'Активировать',
|
text: state.isAlreadyPurchased ? 'Продлить' : 'Активировать',
|
||||||
onTap: () => context.read<SubscriptionDetailsBloc>().add(
|
onTap: () {
|
||||||
|
context.read<SubscriptionDetailsBloc>().add(
|
||||||
ActivateSubscriptionPressed(),
|
ActivateSubscriptionPressed(),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
|
enabled: state.isAgreed,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 56,
|
height: 56,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
showArrows: true,
|
showArrows: true,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -125,9 +159,6 @@ class _ActionCard extends StatelessWidget {
|
|||||||
state.selectedPeriod,
|
state.selectedPeriod,
|
||||||
);
|
);
|
||||||
|
|
||||||
context.read<SubscriptionDetailsBloc>().add(
|
|
||||||
SelectPeriodEvent(state.subscription.options[selectedIndex]));
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -205,7 +236,7 @@ class _PriceRow extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Image.asset("assets/icons/money_icon.png", width: 72, height: 72),
|
Image.asset("assets/icons/money_icon.png", width: 72, height: 72),
|
||||||
SizedBox(width: 15),
|
const SizedBox(width: 15),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -225,4 +256,3 @@ class _PriceRow extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:be_happy/presentation/event/subscription_list_event.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -21,6 +22,7 @@ class SubscriptionsListScreen extends StatelessWidget {
|
|||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: CustomAppBar(title: 'Абонементы'),
|
child: CustomAppBar(title: 'Абонементы'),
|
||||||
@@ -43,17 +45,26 @@ class SubscriptionsListScreen extends StatelessWidget {
|
|||||||
if (state is SubscriptionsLoading) {
|
if (state is SubscriptionsLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
} else if (state is SubscriptionsLoaded) {
|
} else if (state is SubscriptionsLoaded) {
|
||||||
final activeIds = state.activeSubscriptions.map((e) => e.id).toSet();
|
final clientSubsMap = {
|
||||||
|
for (var sub in state.activeSubscriptions) sub.subscriptionId: sub
|
||||||
|
};
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.only(top: 20),
|
padding: const EdgeInsets.only(top: 20),
|
||||||
itemCount: state.subscriptions.length,
|
itemCount: state.subscriptions.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final subscription = state.subscriptions[index];
|
final subscription = state.subscriptions[index];
|
||||||
final bool isActive = activeIds.contains(subscription.id);
|
|
||||||
|
final bool isPurchased = subscription.isCurrent;
|
||||||
|
|
||||||
|
final clientSub = clientSubsMap[subscription.id];
|
||||||
|
final DateTime? expirationDate = isPurchased ? clientSub?.expiredAt : null;
|
||||||
|
|
||||||
return SubscriptionCard(
|
return SubscriptionCard(
|
||||||
subscription: subscription,
|
subscription: subscription,
|
||||||
isActive: isActive,
|
isActive: isPurchased,
|
||||||
|
expiredAt: expirationDate,
|
||||||
|
onRefresh: () { context.read<SubscriptionListBloc>().add(LoadSubscriptionsEvent());},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ class TopUpScreen extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF1A234E),
|
body: Container(
|
||||||
body: SafeArea(
|
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
||||||
|
child: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -68,6 +69,7 @@ class TopUpScreen extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -168,7 +170,7 @@ class TopUpScreen extends StatelessWidget {
|
|||||||
create: (context) =>
|
create: (context) =>
|
||||||
PaymentMethodSheetBloc(getIt<GetPaymentCardsUsecase>())
|
PaymentMethodSheetBloc(getIt<GetPaymentCardsUsecase>())
|
||||||
..add(PaymentMethodSheetStarted()),
|
..add(PaymentMethodSheetStarted()),
|
||||||
child: const PaymentMethodSheet(),
|
child: const PaymentMethodSheet(showBalance: false,),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class ActiveRideState {
|
|||||||
final double cost;
|
final double cost;
|
||||||
final bool isPaused;
|
final bool isPaused;
|
||||||
final bool inZone;
|
final bool inZone;
|
||||||
|
final double latitude;
|
||||||
|
final double longitude;
|
||||||
|
|
||||||
const ActiveRideState({
|
const ActiveRideState({
|
||||||
this.status = ActiveRideStatus.initial,
|
this.status = ActiveRideStatus.initial,
|
||||||
@@ -20,6 +22,8 @@ class ActiveRideState {
|
|||||||
this.elapsedTime = Duration.zero,
|
this.elapsedTime = Duration.zero,
|
||||||
this.speed = 0.0,
|
this.speed = 0.0,
|
||||||
this.distance = 0.0,
|
this.distance = 0.0,
|
||||||
|
this.latitude = 0.0,
|
||||||
|
this.longitude = 0.0,
|
||||||
this.cost = 0.0,
|
this.cost = 0.0,
|
||||||
this.isPaused = false,
|
this.isPaused = false,
|
||||||
this.inZone = true,
|
this.inZone = true,
|
||||||
@@ -33,6 +37,8 @@ class ActiveRideState {
|
|||||||
double? speed,
|
double? speed,
|
||||||
double? distance,
|
double? distance,
|
||||||
double? cost,
|
double? cost,
|
||||||
|
double? longitude,
|
||||||
|
double? latitude,
|
||||||
bool? isPaused,
|
bool? isPaused,
|
||||||
bool? inZone,
|
bool? inZone,
|
||||||
}) {
|
}) {
|
||||||
@@ -44,6 +50,8 @@ class ActiveRideState {
|
|||||||
speed: speed ?? this.speed,
|
speed: speed ?? this.speed,
|
||||||
distance: distance ?? this.distance,
|
distance: distance ?? this.distance,
|
||||||
cost: cost ?? this.cost,
|
cost: cost ?? this.cost,
|
||||||
|
latitude: latitude ?? this.latitude,
|
||||||
|
longitude: longitude ?? this.longitude,
|
||||||
isPaused: isPaused ?? this.isPaused,
|
isPaused: isPaused ?? this.isPaused,
|
||||||
inZone: inZone ?? this.inZone,
|
inZone: inZone ?? this.inZone,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ enum ScooterStatus { initial, loading, success, failure }
|
|||||||
|
|
||||||
class ScooterState {
|
class ScooterState {
|
||||||
final List<Scooter> scooters;
|
final List<Scooter> scooters;
|
||||||
|
final List<Scooter> reservedScooters;
|
||||||
|
final Scooter? selectedScooterForFocus;
|
||||||
final List<Zone> zones;
|
final List<Zone> zones;
|
||||||
final List<double> area;
|
final List<double> area;
|
||||||
final List<double> areaScooters;
|
final List<double> areaScooters;
|
||||||
@@ -23,6 +25,8 @@ class ScooterState {
|
|||||||
|
|
||||||
ScooterState({
|
ScooterState({
|
||||||
this.scooters = const [],
|
this.scooters = const [],
|
||||||
|
this.reservedScooters = const [],
|
||||||
|
this.selectedScooterForFocus,
|
||||||
this.zones = const [],
|
this.zones = const [],
|
||||||
this.area = const [],
|
this.area = const [],
|
||||||
this.areaScooters = const [],
|
this.areaScooters = const [],
|
||||||
@@ -38,6 +42,8 @@ class ScooterState {
|
|||||||
|
|
||||||
ScooterState copyWith({
|
ScooterState copyWith({
|
||||||
List<Scooter>? scooters,
|
List<Scooter>? scooters,
|
||||||
|
List<Scooter>? reservedScooters,
|
||||||
|
Scooter? selectedScooterForFocus,
|
||||||
List<Zone>? zones,
|
List<Zone>? zones,
|
||||||
List<double>? area,
|
List<double>? area,
|
||||||
List<double>? areaScooters,
|
List<double>? areaScooters,
|
||||||
@@ -52,6 +58,8 @@ class ScooterState {
|
|||||||
}) {
|
}) {
|
||||||
return ScooterState(
|
return ScooterState(
|
||||||
scooters: scooters ?? this.scooters,
|
scooters: scooters ?? this.scooters,
|
||||||
|
reservedScooters: reservedScooters ?? this.reservedScooters,
|
||||||
|
selectedScooterForFocus: selectedScooterForFocus ?? this.selectedScooterForFocus,
|
||||||
zones: zones ?? this.zones,
|
zones: zones ?? this.zones,
|
||||||
area: area ?? this.area,
|
area: area ?? this.area,
|
||||||
areaScooters: areaScooters ?? this.areaScooters,
|
areaScooters: areaScooters ?? this.areaScooters,
|
||||||
|
|||||||
27
lib/presentation/state/notifications_state.dart
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import '../../domain/entities/client_notification.dart';
|
||||||
|
|
||||||
|
enum NotificationsStatus { initial, loading, success, failure }
|
||||||
|
|
||||||
|
class NotificationsState {
|
||||||
|
final NotificationsStatus status;
|
||||||
|
final List<ClientNotification> notifications;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
const NotificationsState({
|
||||||
|
this.status = NotificationsStatus.initial,
|
||||||
|
this.notifications = const [],
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
NotificationsState copyWith({
|
||||||
|
NotificationsStatus? status,
|
||||||
|
List<ClientNotification>? notifications,
|
||||||
|
String? errorMessage,
|
||||||
|
}) {
|
||||||
|
return NotificationsState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
notifications: notifications ?? this.notifications,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:be_happy/domain/entities/client_subscription.dart';
|
||||||
|
|
||||||
import '../../domain/entities/subscription.dart';
|
import '../../domain/entities/subscription.dart';
|
||||||
|
|
||||||
abstract class SubscriptionState {}
|
abstract class SubscriptionState {}
|
||||||
@@ -6,7 +8,7 @@ class SubscriptionsLoading extends SubscriptionState {}
|
|||||||
|
|
||||||
class SubscriptionsLoaded extends SubscriptionState {
|
class SubscriptionsLoaded extends SubscriptionState {
|
||||||
final List<Subscription> subscriptions;
|
final List<Subscription> subscriptions;
|
||||||
final List<Subscription> activeSubscriptions;
|
final List<ClientSubscription> activeSubscriptions;
|
||||||
|
|
||||||
SubscriptionsLoaded({
|
SubscriptionsLoaded({
|
||||||
required this.subscriptions,
|
required this.subscriptions,
|
||||||
|
|||||||
@@ -15,21 +15,29 @@ class DetailsContentState extends SubscriptionDetailsState {
|
|||||||
final Subscription subscription;
|
final Subscription subscription;
|
||||||
final SubscriptionPeriod selectedPeriod;
|
final SubscriptionPeriod selectedPeriod;
|
||||||
final bool isAgreed;
|
final bool isAgreed;
|
||||||
|
final bool isAlreadyPurchased; // ✅ Куплена ли эта подписка сейчас
|
||||||
|
final bool isSuccess; // ✅ Сигнал для навигатора назад
|
||||||
|
|
||||||
DetailsContentState({
|
DetailsContentState({
|
||||||
required this.subscription,
|
required this.subscription,
|
||||||
required this.selectedPeriod,
|
required this.selectedPeriod,
|
||||||
this.isAgreed = false,
|
this.isAgreed = false,
|
||||||
|
this.isAlreadyPurchased = false,
|
||||||
|
this.isSuccess = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
DetailsContentState copyWith({
|
DetailsContentState copyWith({
|
||||||
SubscriptionPeriod? selectedPeriod,
|
SubscriptionPeriod? selectedPeriod,
|
||||||
bool? isAgreed,
|
bool? isAgreed,
|
||||||
|
bool? isAlreadyPurchased,
|
||||||
|
bool? isSuccess,
|
||||||
}) {
|
}) {
|
||||||
return DetailsContentState(
|
return DetailsContentState(
|
||||||
subscription: this.subscription,
|
subscription: this.subscription,
|
||||||
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
|
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
|
||||||
isAgreed: isAgreed ?? this.isAgreed,
|
isAgreed: isAgreed ?? this.isAgreed,
|
||||||
|
isAlreadyPurchased: isAlreadyPurchased ?? this.isAlreadyPurchased,
|
||||||
|
isSuccess: isSuccess ?? this.isSuccess,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,6 +76,8 @@ class ActiveRideBloc extends Bloc<ActiveRideEvent, ActiveRideState> {
|
|||||||
cost: orderData?.price ?? 0.0,
|
cost: orderData?.price ?? 0.0,
|
||||||
isPaused: isPaused,
|
isPaused: isPaused,
|
||||||
inZone: orderData?.zone,
|
inZone: orderData?.zone,
|
||||||
|
latitude: orderData?.latitude,
|
||||||
|
longitude: orderData?.longitude,
|
||||||
));
|
));
|
||||||
|
|
||||||
_syncTimer?.cancel();
|
_syncTimer?.cancel();
|
||||||
@@ -196,6 +198,8 @@ class ActiveRideBloc extends Bloc<ActiveRideEvent, ActiveRideState> {
|
|||||||
cost: orderData?.price ?? state.cost,
|
cost: orderData?.price ?? state.cost,
|
||||||
isPaused: isPaused,
|
isPaused: isPaused,
|
||||||
inZone: orderData?.zone,
|
inZone: orderData?.zone,
|
||||||
|
latitude: orderData?.latitude,
|
||||||
|
longitude: orderData?.longitude,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
print("CURRENT STATE $state");
|
print("CURRENT STATE $state");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:be_happy/domain/entities/client_notification.dart';
|
|||||||
import 'package:be_happy/domain/entities/map_settings.dart';
|
import 'package:be_happy/domain/entities/map_settings.dart';
|
||||||
import 'package:be_happy/domain/usecase/check_user_usecase.dart';
|
import 'package:be_happy/domain/usecase/check_user_usecase.dart';
|
||||||
import 'package:be_happy/domain/usecase/get_available_zones_usecase.dart';
|
import 'package:be_happy/domain/usecase/get_available_zones_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_map_settings_usecase.dart';
|
||||||
import 'package:be_happy/domain/usecase/get_notifications_stream_usecase.dart';
|
import 'package:be_happy/domain/usecase/get_notifications_stream_usecase.dart';
|
||||||
import 'package:be_happy/domain/usecase/logout_usecase.dart';
|
import 'package:be_happy/domain/usecase/logout_usecase.dart';
|
||||||
@@ -13,6 +14,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
|
|
||||||
import '../../domain/entities/point.dart';
|
import '../../domain/entities/point.dart';
|
||||||
import '../../domain/entities/scooter.dart';
|
import '../../domain/entities/scooter.dart';
|
||||||
|
import '../../domain/entities/scooter_order.dart';
|
||||||
import '../../domain/entities/zone.dart';
|
import '../../domain/entities/zone.dart';
|
||||||
import '../../domain/usecase/get_available_scooters_usecase.dart';
|
import '../../domain/usecase/get_available_scooters_usecase.dart';
|
||||||
import '../../domain/usecase/get_profile_usecase.dart';
|
import '../../domain/usecase/get_profile_usecase.dart';
|
||||||
@@ -23,6 +25,7 @@ import 'package:maps_toolkit/maps_toolkit.dart' as mt;
|
|||||||
|
|
||||||
class MapBloc extends Bloc<ScooterEvent, ScooterState> {
|
class MapBloc extends Bloc<ScooterEvent, ScooterState> {
|
||||||
final GetAvailableScootersUsecase getScootersUsecase;
|
final GetAvailableScootersUsecase getScootersUsecase;
|
||||||
|
final GetClientOrdersUsecase getClientOrdersUsecase;
|
||||||
final GetAvailableZonesUsecase getAvailableZonesUsecase;
|
final GetAvailableZonesUsecase getAvailableZonesUsecase;
|
||||||
final GetMapSettingsUsecase getMapSettingsUsecase;
|
final GetMapSettingsUsecase getMapSettingsUsecase;
|
||||||
final GetNotificationsStreamUseCase getNotificationsStreamUseCase;
|
final GetNotificationsStreamUseCase getNotificationsStreamUseCase;
|
||||||
@@ -35,6 +38,7 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
|
|||||||
MapBloc(
|
MapBloc(
|
||||||
this.getAvailableZonesUsecase,
|
this.getAvailableZonesUsecase,
|
||||||
this.getScootersUsecase,
|
this.getScootersUsecase,
|
||||||
|
this.getClientOrdersUsecase,
|
||||||
this.getMapSettingsUsecase,
|
this.getMapSettingsUsecase,
|
||||||
this.getNotificationsStreamUseCase,
|
this.getNotificationsStreamUseCase,
|
||||||
this.getProfileUseCase,
|
this.getProfileUseCase,
|
||||||
@@ -49,6 +53,9 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
|
|||||||
on<FetchProfileData>(_onFetchProfileData);
|
on<FetchProfileData>(_onFetchProfileData);
|
||||||
on<CheckUser>(_onCheckUser);
|
on<CheckUser>(_onCheckUser);
|
||||||
on<LogoutPressed>(_onLogoutPressed);
|
on<LogoutPressed>(_onLogoutPressed);
|
||||||
|
on<FocusOnScooter>(_onFocusOnScooter);
|
||||||
|
on<ClearMapPlacemarks>(_onClearMapPlacemarks);
|
||||||
|
on<ClearMapFocus>(_onClearMapFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
void startNotificationStream() {
|
void startNotificationStream() {
|
||||||
@@ -97,11 +104,15 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
|
|||||||
getScootersUsecase(event.areaScooters, 0, 100),
|
getScootersUsecase(event.areaScooters, 0, 100),
|
||||||
getAvailableZonesUsecase(event.area, 0, 100),
|
getAvailableZonesUsecase(event.area, 0, 100),
|
||||||
getMapSettingsUsecase(),
|
getMapSettingsUsecase(),
|
||||||
|
getClientOrdersUsecase(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
final scooters = results[0] as List<Scooter>;
|
final scooters = results[0] as List<Scooter>;
|
||||||
final zones = results[1] as List<Zone>;
|
final zones = results[1] as List<Zone>;
|
||||||
final settings = results[2] as MapSettings;
|
final settings = results[2] as MapSettings;
|
||||||
|
final orders = results[3];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
zones.forEach(print);
|
zones.forEach(print);
|
||||||
|
|
||||||
@@ -127,6 +138,7 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
|
|||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: ScooterStatus.success,
|
status: ScooterStatus.success,
|
||||||
scooters: scooters,
|
scooters: scooters,
|
||||||
|
// reservedScooters: reservedScooters,
|
||||||
zones: filteredZones,
|
zones: filteredZones,
|
||||||
area: event.area,
|
area: event.area,
|
||||||
areaScooters: event.areaScooters,
|
areaScooters: event.areaScooters,
|
||||||
@@ -236,7 +248,9 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
|
|||||||
try {
|
try {
|
||||||
final profile = await getProfileUseCase();
|
final profile = await getProfileUseCase();
|
||||||
|
|
||||||
emit(state.copyWith(phoneNumber: profile.phone, balance: profile.balance));
|
emit(
|
||||||
|
state.copyWith(phoneNumber: profile.phone, balance: profile.balance),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@@ -280,4 +294,52 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FutureOr<void> _onFocusOnScooter(FocusOnScooter event, Emitter<ScooterState> emit) {
|
||||||
|
final updatedReserved = List<Scooter>.from(state.reservedScooters ?? []);
|
||||||
|
if (!updatedReserved.any((s) => s.id == event.scooter.id)) {
|
||||||
|
updatedReserved.add(event.scooter);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(state.copyWith(
|
||||||
|
selectedScooterForFocus: event.scooter,
|
||||||
|
reservedScooters: updatedReserved,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
FutureOr<void> _onClearMapPlacemarks(ClearMapPlacemarks event, Emitter<ScooterState> emit) async {
|
||||||
|
try{
|
||||||
|
final orders = await getClientOrdersUsecase();
|
||||||
|
|
||||||
|
List<Scooter> updatedReservedScooters = [];
|
||||||
|
|
||||||
|
if (orders is Success<List<ScooterOrder>>) {
|
||||||
|
print("FETCH: orders.data.length = ${orders.data?.length}");
|
||||||
|
|
||||||
|
updatedReservedScooters = orders.data?.map((order) {
|
||||||
|
return order.scooter;
|
||||||
|
}).toList() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(state.copyWith(
|
||||||
|
reservedScooters: updatedReservedScooters,
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
print("Error in _onClearMapPlacemarks: $e");
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: ScooterStatus.failure,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FutureOr<void> _onClearMapFocus(ClearMapFocus event, Emitter<ScooterState> emit) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
selectedScooterForFocus: null,
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
34
lib/presentation/viewmodel/notifications_bloc.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'dart:developer' as dev;
|
||||||
|
import '../../domain/usecase/get_notifications_usecase.dart';
|
||||||
|
import '../event/notifications_event.dart';
|
||||||
|
import '../state/notifications_state.dart';
|
||||||
|
|
||||||
|
class NotificationsBloc extends Bloc<NotificationsEvent, NotificationsState> {
|
||||||
|
final GetNotificationsUsecase _getNotificationsUsecase;
|
||||||
|
|
||||||
|
NotificationsBloc(this._getNotificationsUsecase) : super(const NotificationsState()) {
|
||||||
|
on<NotificationsFetchRequested>(_onFetchRequested);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onFetchRequested(
|
||||||
|
NotificationsFetchRequested event,
|
||||||
|
Emitter<NotificationsState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: NotificationsStatus.loading));
|
||||||
|
|
||||||
|
try {
|
||||||
|
final notifications = await _getNotificationsUsecase();
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: NotificationsStatus.success,
|
||||||
|
notifications: notifications,
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
dev.log('NotificationsBloc: Ошибка загрузки: $e');
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: NotificationsStatus.failure,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ import 'package:be_happy/domain/entities/scooter_order.dart';
|
|||||||
import 'package:be_happy/domain/usecase/get_scooter_order_history_usecase.dart';
|
import 'package:be_happy/domain/usecase/get_scooter_order_history_usecase.dart';
|
||||||
import 'package:be_happy/core/result.dart';
|
import 'package:be_happy/core/result.dart';
|
||||||
|
|
||||||
// 🔹 EVENTS
|
|
||||||
abstract class OrderHistoryEvent {}
|
abstract class OrderHistoryEvent {}
|
||||||
|
|
||||||
class OrderHistoryFetchRequested extends OrderHistoryEvent {
|
class OrderHistoryFetchRequested extends OrderHistoryEvent {
|
||||||
@@ -13,7 +12,6 @@ class OrderHistoryFetchRequested extends OrderHistoryEvent {
|
|||||||
|
|
||||||
class OrderHistoryRefreshRequested extends OrderHistoryEvent {}
|
class OrderHistoryRefreshRequested extends OrderHistoryEvent {}
|
||||||
|
|
||||||
// 🔹 STATES
|
|
||||||
enum OrderHistoryStatus { initial, loading, success, failure, empty }
|
enum OrderHistoryStatus { initial, loading, success, failure, empty }
|
||||||
|
|
||||||
class OrderHistoryState {
|
class OrderHistoryState {
|
||||||
@@ -21,14 +19,14 @@ class OrderHistoryState {
|
|||||||
final List<ScooterOrder> orders;
|
final List<ScooterOrder> orders;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
final int currentPage;
|
final int currentPage;
|
||||||
final bool hasMore;
|
final bool hasMore; // можно оставить, но всегда будет false
|
||||||
|
|
||||||
OrderHistoryState({
|
OrderHistoryState({
|
||||||
required this.status,
|
required this.status,
|
||||||
this.orders = const [],
|
this.orders = const [],
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
this.currentPage = 1,
|
this.currentPage = 1,
|
||||||
this.hasMore = true,
|
this.hasMore = false, // ✅ По умолчанию — нет больше данных
|
||||||
});
|
});
|
||||||
|
|
||||||
OrderHistoryState copyWith({
|
OrderHistoryState copyWith({
|
||||||
@@ -51,7 +49,8 @@ class OrderHistoryState {
|
|||||||
class OrderHistoryBloc extends Bloc<OrderHistoryEvent, OrderHistoryState> {
|
class OrderHistoryBloc extends Bloc<OrderHistoryEvent, OrderHistoryState> {
|
||||||
final GetScooterOrderHistoryUsecase _usecase;
|
final GetScooterOrderHistoryUsecase _usecase;
|
||||||
|
|
||||||
OrderHistoryBloc(this._usecase) : super(OrderHistoryState(status: OrderHistoryStatus.initial)) {
|
OrderHistoryBloc(this._usecase)
|
||||||
|
: super(OrderHistoryState(status: OrderHistoryStatus.initial)) {
|
||||||
on<OrderHistoryFetchRequested>(_onFetchRequested);
|
on<OrderHistoryFetchRequested>(_onFetchRequested);
|
||||||
on<OrderHistoryRefreshRequested>(_onRefreshRequested);
|
on<OrderHistoryRefreshRequested>(_onRefreshRequested);
|
||||||
}
|
}
|
||||||
@@ -64,9 +63,11 @@ class OrderHistoryBloc extends Bloc<OrderHistoryEvent, OrderHistoryState> {
|
|||||||
emit(state.copyWith(status: OrderHistoryStatus.loading));
|
emit(state.copyWith(status: OrderHistoryStatus.loading));
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = await _usecase.call(page: event.page);
|
final result = await _usecase.call(
|
||||||
|
page: event.page,
|
||||||
|
pageSize: 1000,
|
||||||
|
);
|
||||||
|
|
||||||
// ✅ Явная проверка с правильным типом
|
|
||||||
if (result is Success<List<ScooterOrder>>) {
|
if (result is Success<List<ScooterOrder>>) {
|
||||||
final orders = result.data;
|
final orders = result.data;
|
||||||
|
|
||||||
@@ -84,12 +85,10 @@ class OrderHistoryBloc extends Bloc<OrderHistoryEvent, OrderHistoryState> {
|
|||||||
status: OrderHistoryStatus.success,
|
status: OrderHistoryStatus.success,
|
||||||
orders: newOrders,
|
orders: newOrders,
|
||||||
currentPage: event.page,
|
currentPage: event.page,
|
||||||
hasMore: orders.length == 20,
|
hasMore: false,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
} else if (result is Failure<List<ScooterOrder>>) {
|
||||||
// ✅ Явно указываем тип Failure
|
|
||||||
else if (result is Failure<List<ScooterOrder>>) {
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: OrderHistoryStatus.failure,
|
status: OrderHistoryStatus.failure,
|
||||||
errorMessage: result.failure.message ?? 'Неизвестная ошибка',
|
errorMessage: result.failure.message ?? 'Неизвестная ошибка',
|
||||||
|
|||||||
@@ -89,12 +89,11 @@ class PaymentConfirmBloc
|
|||||||
event.isBalance,
|
event.isBalance,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result is Success<ScooterOrder>) {
|
if (result is Success<void>) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: PaymentConfirmStatus.success,
|
status: PaymentConfirmStatus.success,
|
||||||
paymentCompleted: true,
|
paymentCompleted: true,
|
||||||
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (result is Failure) {
|
} else if (result is Failure) {
|
||||||
|
|||||||
@@ -70,13 +70,16 @@ class PaymentMethodsBloc extends Bloc<PaymentMethodsEvent, PaymentMethodsState>
|
|||||||
PaymentMethodsDeleteCard event,
|
PaymentMethodsDeleteCard event,
|
||||||
Emitter<PaymentMethodsState> emit,
|
Emitter<PaymentMethodsState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(isDeleting: true));
|
emit(state.copyWith(
|
||||||
|
status: PaymentMethodsStatus.loading,
|
||||||
|
isDeleting: true,
|
||||||
|
));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final result = await _removePaymentCardUsecase(event.cardId);
|
final result = await _removePaymentCardUsecase(event.cardId);
|
||||||
|
|
||||||
if (result is Success) {
|
if (result is Success) {
|
||||||
emit(state.copyWith(isDeleting: false));
|
emit(state.copyWith(isDeleting: false, status: PaymentMethodsStatus.success));
|
||||||
add(PaymentMethodsStarted());
|
add(PaymentMethodsStarted());
|
||||||
} else if (result is Failure) {
|
} else if (result is Failure) {
|
||||||
String errorMessage = 'Не удалось удалить карту';
|
String errorMessage = 'Не удалось удалить карту';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:be_happy/core/result.dart';
|
import 'package:be_happy/core/result.dart';
|
||||||
|
import 'package:be_happy/domain/entities/client_subscription.dart';
|
||||||
import 'package:be_happy/domain/entities/subscription.dart';
|
import 'package:be_happy/domain/entities/subscription.dart';
|
||||||
import 'package:be_happy/domain/usecase/get_available_subscriptions_usecase.dart';
|
import 'package:be_happy/domain/usecase/get_available_subscriptions_usecase.dart';
|
||||||
import 'package:be_happy/domain/usecase/get_client_subscriptions_usecase.dart';
|
import 'package:be_happy/domain/usecase/get_client_subscriptions_usecase.dart';
|
||||||
@@ -11,6 +12,7 @@ class SubscriptionListBloc extends Bloc<SubscriptionEvent, SubscriptionState> {
|
|||||||
final GetAvailableSubscriptionsUsecase getAvailableSubscriptionsUsecase;
|
final GetAvailableSubscriptionsUsecase getAvailableSubscriptionsUsecase;
|
||||||
final GetClientSubscriptionsUsecase getClientSubscriptionsUsecase;
|
final GetClientSubscriptionsUsecase getClientSubscriptionsUsecase;
|
||||||
|
|
||||||
|
|
||||||
SubscriptionListBloc({
|
SubscriptionListBloc({
|
||||||
required this.getAvailableSubscriptionsUsecase,
|
required this.getAvailableSubscriptionsUsecase,
|
||||||
required this.getClientSubscriptionsUsecase,
|
required this.getClientSubscriptionsUsecase,
|
||||||
@@ -26,10 +28,25 @@ class SubscriptionListBloc extends Bloc<SubscriptionEvent, SubscriptionState> {
|
|||||||
final allResult = results[0];
|
final allResult = results[0];
|
||||||
final activeResult = results[1] ;
|
final activeResult = results[1] ;
|
||||||
|
|
||||||
if (allResult is Success<List<Subscription>> && activeResult is Success<List<Subscription>>) {
|
if (allResult is Success<List<Subscription>> && activeResult is Success<List<ClientSubscription>>) {
|
||||||
|
final availableSubs = allResult.data ?? [];
|
||||||
|
final clientSubs = activeResult.data ?? [];
|
||||||
|
|
||||||
|
final Map<int, Subscription> combinedSubsMap = {};
|
||||||
|
|
||||||
|
for (var clientSub in clientSubs) {
|
||||||
|
if (clientSub.subscription != null) {
|
||||||
|
combinedSubsMap[clientSub.subscriptionId] = clientSub.subscription;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var sub in availableSubs) {
|
||||||
|
combinedSubsMap[sub.id] = sub;
|
||||||
|
}
|
||||||
|
|
||||||
emit(SubscriptionsLoaded(
|
emit(SubscriptionsLoaded(
|
||||||
subscriptions: allResult.data ?? [],
|
subscriptions: combinedSubsMap.values.toList(),
|
||||||
activeSubscriptions: activeResult.data ?? [],
|
activeSubscriptions: clientSubs,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
emit(SubscriptionsError("Не удалось загрузить данные из API"));
|
emit(SubscriptionsError("Не удалось загрузить данные из API"));
|
||||||
|
|||||||
@@ -4,38 +4,59 @@ import 'package:be_happy/domain/usecase/activate_subscription_usecase.dart';
|
|||||||
import 'package:be_happy/domain/usecase/get_subscription_by_id_usecase.dart';
|
import 'package:be_happy/domain/usecase/get_subscription_by_id_usecase.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import '../../domain/entities/client_subscription.dart';
|
||||||
|
import '../../domain/usecase/get_client_subscriptions_usecase.dart';
|
||||||
import '../event/subscription_details_event.dart';
|
import '../event/subscription_details_event.dart';
|
||||||
import '../state/susbcription_details_state.dart';
|
import '../state/susbcription_details_state.dart';
|
||||||
|
|
||||||
class SubscriptionDetailsBloc extends Bloc<SubscriptionDetailsEvent, SubscriptionDetailsState> {
|
class SubscriptionDetailsBloc
|
||||||
|
extends Bloc<SubscriptionDetailsEvent, SubscriptionDetailsState> {
|
||||||
final GetSubscriptionByIdUsecase getSubscriptionByIdUsecase;
|
final GetSubscriptionByIdUsecase getSubscriptionByIdUsecase;
|
||||||
final ActivateSubscriptionUsecase activateSubscriptionUsecase;
|
final ActivateSubscriptionUsecase activateSubscriptionUsecase;
|
||||||
|
final GetClientSubscriptionsUsecase getClientSubscriptionsUsecase;
|
||||||
|
|
||||||
SubscriptionDetailsBloc(this.getSubscriptionByIdUsecase,
|
SubscriptionDetailsBloc(
|
||||||
this.activateSubscriptionUsecase) : super(DetailsLoading()) {
|
this.getSubscriptionByIdUsecase,
|
||||||
|
this.activateSubscriptionUsecase,
|
||||||
|
this.getClientSubscriptionsUsecase,
|
||||||
|
) : super(DetailsLoading()) {
|
||||||
on<LoadDetailsEvent>((event, emit) async {
|
on<LoadDetailsEvent>((event, emit) async {
|
||||||
emit(DetailsLoading());
|
emit(DetailsLoading());
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final result = await getSubscriptionByIdUsecase(event.subscriptionId);
|
final result = await getSubscriptionByIdUsecase(event.subscriptionId);
|
||||||
|
|
||||||
switch (result) {
|
final clientSubsResult = await getClientSubscriptionsUsecase();
|
||||||
|
bool isPurchased = false;
|
||||||
|
|
||||||
case Success<Subscription>():
|
switch (clientSubsResult) {
|
||||||
final sub = result.data;
|
case Success<List<ClientSubscription>>():
|
||||||
|
isPurchased = clientSubsResult.data?.any(
|
||||||
if (sub == null) return;
|
(element) => element.subscriptionId == event.subscriptionId,
|
||||||
|
) ?? false;
|
||||||
emit(DetailsContentState(
|
break;
|
||||||
subscription: sub,
|
|
||||||
selectedPeriod: sub.options.first,
|
|
||||||
));
|
|
||||||
case Failure<Subscription>():
|
|
||||||
emit(DetailsError("Ошибка при запросе данных"));
|
|
||||||
|
|
||||||
|
case Failure<List<ClientSubscription>>():
|
||||||
|
isPurchased = false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (result) {
|
||||||
|
case Success<Subscription>():
|
||||||
|
final sub = result.data;
|
||||||
|
if (sub == null) return;
|
||||||
|
|
||||||
|
emit(
|
||||||
|
DetailsContentState(
|
||||||
|
subscription: sub,
|
||||||
|
selectedPeriod: sub.options.first,
|
||||||
|
isAlreadyPurchased: isPurchased,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case Failure<Subscription>():
|
||||||
|
emit(DetailsError("Ошибка при запросе данных"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(DetailsError("Не удалось загрузить данные"));
|
emit(DetailsError("Не удалось загрузить данные"));
|
||||||
}
|
}
|
||||||
@@ -43,7 +64,9 @@ class SubscriptionDetailsBloc extends Bloc<SubscriptionDetailsEvent, Subscriptio
|
|||||||
|
|
||||||
on<SelectPeriodEvent>((event, emit) {
|
on<SelectPeriodEvent>((event, emit) {
|
||||||
if (state is DetailsContentState) {
|
if (state is DetailsContentState) {
|
||||||
emit((state as DetailsContentState).copyWith(selectedPeriod: event.period));
|
emit(
|
||||||
|
(state as DetailsContentState).copyWith(selectedPeriod: event.period),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,10 +76,19 @@ class SubscriptionDetailsBloc extends Bloc<SubscriptionDetailsEvent, Subscriptio
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
on<ActivateSubscriptionPressed>((event, emit) {
|
on<ActivateSubscriptionPressed>((event, emit) async {
|
||||||
switch(state) {
|
switch (state) {
|
||||||
case DetailsContentState contentState:
|
case DetailsContentState contentState:
|
||||||
activateSubscriptionUsecase(contentState.selectedPeriod.id);
|
if (!contentState.isAgreed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await activateSubscriptionUsecase(contentState.selectedPeriod.id);
|
||||||
|
|
||||||
|
if (result is Success) {
|
||||||
|
emit(contentState.copyWith(isSuccess: true));
|
||||||
|
} else {
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|||||||