diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index e337cda..ae88da7 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -44,7 +44,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.sparkit.be_happy" + applicationId = "com.sparkit.behappy" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = 26 diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4..c647d81 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b7..2bb4769 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d4391..e8a2188 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d..21bfa5e 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372e..ab31fc9 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/assets/fonts/DigitalNumbers-Regular.ttf b/assets/fonts/DigitalNumbers-Regular.ttf new file mode 100644 index 0000000..d6017ca Binary files /dev/null and b/assets/fonts/DigitalNumbers-Regular.ttf differ diff --git a/assets/icons/qr_icon_order.png b/assets/icons/qr_icon_order.png new file mode 100644 index 0000000..00e4334 Binary files /dev/null and b/assets/icons/qr_icon_order.png differ diff --git a/assets/icons/scooter_reserved_placemark_fill.png b/assets/icons/scooter_reserved_placemark_fill.png new file mode 100644 index 0000000..92c6074 Binary files /dev/null and b/assets/icons/scooter_reserved_placemark_fill.png differ diff --git a/assets/icons/timer.png b/assets/icons/timer.png new file mode 100644 index 0000000..7efb8ca Binary files /dev/null and b/assets/icons/timer.png differ diff --git a/assets/news_def.png b/assets/news_def.png new file mode 100644 index 0000000..2102c55 Binary files /dev/null and b/assets/news_def.png differ diff --git a/assets/splash_logo.png b/assets/splash_logo.png new file mode 100644 index 0000000..61ed566 Binary files /dev/null and b/assets/splash_logo.png differ diff --git a/assets/splash_map.png b/assets/splash_map.png new file mode 100644 index 0000000..77840e2 Binary files /dev/null and b/assets/splash_map.png differ diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 5756aab..0cc5a6a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -559,7 +559,7 @@ isa = XCBuildConfiguration; buildSettings = { 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_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -616,7 +616,7 @@ isa = XCBuildConfiguration; buildSettings = { 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_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d36b1fa..d0d98aa 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" : "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" - } -} +{"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"}} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index dc9ada4..1dd276b 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 7353c41..34458af 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 797d452..3711926 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index 6ed2d93..d625354 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 4cd7b00..fe907cf 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index fe73094..d31bb50 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 321773c..61706cb 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 797d452..3711926 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 502f463..4a0b2de 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index 0ec3034..6c2fb29 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..36d18eb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..e1ac9e4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..3bbefb0 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..f437fd4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index 0ec3034..6c2fb29 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index e9f5fea..54e93a6 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..c647d81 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..21bfa5e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index 84ac32a..87b2bc7 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 8953cba..4ef52a2 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 0467bf1..7a28ee6 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/lib.rar b/lib.rar deleted file mode 100644 index 02c830e..0000000 Binary files a/lib.rar and /dev/null differ diff --git a/lib/data/models/promo_code_response_dto.dart b/lib/data/models/promo_code_response_dto.dart new file mode 100644 index 0000000..24ba6b1 --- /dev/null +++ b/lib/data/models/promo_code_response_dto.dart @@ -0,0 +1,25 @@ +import '../../domain/entities/promo_code_result.dart'; + +class PromoCodeResponseDto { + final bool success; + final double balance; + + PromoCodeResponseDto({ + required this.success, + required this.balance, + }); + + factory PromoCodeResponseDto.fromJson(Map json) { + return PromoCodeResponseDto( + success: json['success'] as bool, + balance: (json['balance'] as num).toDouble(), + ); + } + + PromoCodeResult toEntity() { + return PromoCodeResult( + success: success, + balance: balance, + ); + } +} \ No newline at end of file diff --git a/lib/data/network/api_service.dart b/lib/data/network/api_service.dart index f67e822..970ed04 100644 --- a/lib/data/network/api_service.dart +++ b/lib/data/network/api_service.dart @@ -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/flutter_client_sse.dart'; +import '../../domain/entities/client_subscription.dart'; import '../../domain/entities/point.dart'; import '../../domain/entities/user_profile.dart'; import '../../domain/entities/payment_card.dart'; @@ -137,7 +138,8 @@ class ApiService { if (avatarId != null && profileData["avatar"] != null) { final String? avatarPath = profileData["avatar"]["path"]; if (avatarPath != null && avatarPath.isNotEmpty) { - avatarUrl = Uri.parse(fileBaseUrl).resolve(avatarPath).toString(); } + avatarUrl = Uri.parse(fileBaseUrl).resolve(avatarPath).toString(); + } } dynamic balanceRaw = profileData["balance"]; @@ -394,19 +396,16 @@ class ApiService { final url = "$baseUrl/scooter/$title/code"; try { - final response = await _dio.get( - url, - options: await _getAuthOptions(), - ); + final response = await _dio.get(url, options: await _getAuthOptions()); if (response.statusCode == 200 || response.statusCode == 201) { return Scooter.fromJson(response.data); } - + if (response.statusCode == 404) { throw ScooterNotFoundException(message: "Самокат не найден"); } - + return null; } on DioException catch (e) { if (e.response?.statusCode == 401) throw UnauthorizedException(); @@ -463,7 +462,7 @@ class ApiService { } } - Future> getClientSubscriptions() async { + Future> getClientSubscriptions() async { const url = "$baseUrl/scootersubscription/client"; try { @@ -474,14 +473,7 @@ class ApiService { final List items = responseData['data'] ?? []; return items.map((item) { - final Map subscriptionMap = - Map.from(item['subscription'] ?? {}); - - if (item['expiredAt'] != null) { - subscriptionMap['activeTo'] = item['expiredAt']; - } - - return Subscription.fromJson(subscriptionMap); + return ClientSubscription.fromJson(item); }).toList(); } return []; @@ -547,7 +539,7 @@ class ApiService { } } - Future addPaymentCard({ + Future addPaymentCard({ required String cardNumber, required String cardHolder, required int expirationMonth, @@ -571,8 +563,9 @@ class ApiService { ); if (response.statusCode == 200 || response.statusCode == 201) { - return response.data['id'] as int; + return; } + throw AuthException('Непредвиденный статус: ${response.statusCode}', 0); } on DioException catch (e) { final data = e.response?.data; @@ -674,7 +667,9 @@ class ApiService { final firstError = data['message'][0]['message'].toString(); 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(); 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(); if (firstError.contains("Wrong start zone")) { - throw WrongZoneException(message: "Некорректная зона для завершения поездки."); + throw WrongZoneException( + message: "Некорректная зона для завершения поездки.", + ); } } @@ -803,7 +802,7 @@ class ApiService { } } - Future payRide(int orderId) async { + Future payRide(int orderId) async { try { final response = await _dio.put( "$baseUrl/scooterorder/$orderId/pay", @@ -811,12 +810,12 @@ class ApiService { ); if (response.statusCode == 200 || response.statusCode == 201) { - return ScooterOrder.fromJson(response.data); + // return ScooterOrder.fromJson(response.data); + return; } - return null; } on DioException catch (e) { _handleDioError(e); - return null; + return; } } @@ -878,7 +877,7 @@ class ApiService { } } - Future payScooterOrderWithPhotos({ + Future payScooterOrderWithPhotos({ required int orderId, required int? cardId, required bool isBalance, @@ -891,12 +890,10 @@ class ApiService { ); if (response.statusCode == 200 || response.statusCode == 201) { - return ScooterOrder.fromJson(response.data); + return; } - return null; } on DioException catch (e) { _handleDioError(e); - return null; } } @@ -918,7 +915,6 @@ class ApiService { } } - Future> getScooterOrderRouteHistory({required int id}) async { try { final response = await _dio.get( @@ -930,13 +926,19 @@ class ApiService { final String routeString = response.data['route'] ?? '[]'; final List routeList = json.decode(routeString); - return routeList.map((item) => Point( - (item[1] as num).toDouble(), - (item[0] as num).toDouble(), - )).toList(); + return routeList + .map( + (item) => Point( + (item[1] as num).toDouble(), + (item[0] as num).toDouble(), + ), + ) + .toList(); } - throw RouteHistoryNotFoundException(message: "История маршрута не найдена"); + throw RouteHistoryNotFoundException( + message: "История маршрута не найдена", + ); } on DioException catch (e) { if (e.response?.statusCode == 401) throw UnauthorizedException(); if (e.response?.statusCode == 403) throw AuthBlockException(); @@ -1038,6 +1040,57 @@ class ApiService { return controller.stream; } + Future>> 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>(); + } else if (data is Map) { + final list = data['data']; + if (list is List) { + return list.cast>(); + } 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> getCertificates() async { try { final response = await _dio.get( @@ -1097,4 +1150,31 @@ class ApiService { } return null; } + + Future> applyPromoCode(String code) async { + final token = await _securityService.getAccessToken(); + if (token == null) throw Exception('No access token'); + + final response = await _dio.post( + '$baseUrl/promocode/apply', + data: {'code': code}, + options: Options( + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + ), + ); + + if (response.statusCode == 201 || response.statusCode == 200) { + return response.data; + } else if (response.statusCode == 404) { + throw PromoCodeNotFoundException(); + } else { + throw Exception('API Error: ${response.statusCode}'); + } + } } + +// ✅ Добавь исключение (можно в отдельный файл) +class PromoCodeNotFoundException implements Exception {} diff --git a/lib/data/repositories/notification_repository_impl.dart b/lib/data/repositories/notification_repository_impl.dart index e75d066..f7f0cdf 100644 --- a/lib/data/repositories/notification_repository_impl.dart +++ b/lib/data/repositories/notification_repository_impl.dart @@ -33,4 +33,22 @@ class NotificationRepositoryImpl implements NotificationRepository { void closeStream() { // соединение закрывается автоматически при отписке от stream } + + @override + Future> getNotifications() async { + try { + final List> 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'); + } + } } diff --git a/lib/data/repositories/payment_repository_impl.dart b/lib/data/repositories/payment_repository_impl.dart index cd53cf0..9b2ac5c 100644 --- a/lib/data/repositories/payment_repository_impl.dart +++ b/lib/data/repositories/payment_repository_impl.dart @@ -40,7 +40,7 @@ class PaymentRepositoryImpl implements PaymentRepository { required String cvv, }) async { try { - final cardId = await apiService.addPaymentCard( + await apiService.addPaymentCard( cardNumber: cardNumber, cardHolder: cardHolder, expirationMonth: int.parse(expiryMonth), @@ -48,8 +48,7 @@ class PaymentRepositoryImpl implements PaymentRepository { cvv: cvv, ); - // Сохраняем полный номер карты локально - await securityService.saveCardFullNumber(cardId, cardNumber); + // await securityService.saveCardFullNumber(cardId, cardNumber); return Success(null); } on AuthException catch (e) { diff --git a/lib/data/repositories/promo_code_repository_impl.dart b/lib/data/repositories/promo_code_repository_impl.dart new file mode 100644 index 0000000..8f6a0c5 --- /dev/null +++ b/lib/data/repositories/promo_code_repository_impl.dart @@ -0,0 +1,23 @@ +import '../../domain/entities/promo_code_result.dart'; +import '../../domain/repositories/promo_code_repository.dart'; +import '../models/promo_code_response_dto.dart'; +import '../network/api_service.dart'; + +class PromoCodeRepositoryImpl implements PromoCodeRepository { + final ApiService apiService; + + PromoCodeRepositoryImpl(this.apiService); + + @override + Future applyPromoCode(String code) async { + try { + final data = await apiService.applyPromoCode(code); + final dto = PromoCodeResponseDto.fromJson(data); + return dto.toEntity(); + } on PromoCodeNotFoundException { + throw Exception('Промокод не найден'); + } catch (e) { + throw Exception('Ошибка активации промокода: $e'); + } + } +} \ No newline at end of file diff --git a/lib/data/repositories/scooter_repository_impl.dart b/lib/data/repositories/scooter_repository_impl.dart index 2ba98e3..e39e751 100644 --- a/lib/data/repositories/scooter_repository_impl.dart +++ b/lib/data/repositories/scooter_repository_impl.dart @@ -10,6 +10,7 @@ import 'package:be_happy/domain/repositories/scooter_repository.dart'; import '../../core/failures.dart'; import '../../core/result.dart'; import '../../domain/entities/active_scooter_order.dart'; +import '../../domain/entities/client_subscription.dart'; import '../../domain/entities/scooter.dart'; import '../../domain/entities/tariff.dart'; import '../../domain/entities/subscription.dart'; @@ -127,8 +128,8 @@ class ScooterRepositoryImpl extends ScooterRepository { } @override - Future>> getClientSubscriptions() async { - late final Result> result; + Future>> getClientSubscriptions() async { + late final Result> result; try { final subscriptions = await _apiService.getClientSubscriptions(); result = Success(subscriptions); @@ -268,15 +269,12 @@ class ScooterRepositoryImpl extends ScooterRepository { } @override - Future> payRide(int orderId) async { - late final Result result; + Future> payRide(int orderId) async { + late final Result result; try { - final order = await _apiService.payRide(orderId); - if (order != null) { - result = Success(order); - } else { - result = Failure(UnknownFailure("Неизвестная ошибка")); - } + await _apiService.payRide(orderId); + result = Success(null); + } on AuthException catch (e) { result = Failure(AuthFailure(e.attemptsLeft)); } catch (e) { @@ -335,23 +333,19 @@ class ScooterRepositoryImpl extends ScooterRepository { } @override - Future> payScooterOrderWithPhotos({ + Future> payScooterOrderWithPhotos({ required int orderId, required int? cardId, required bool isBalance, }) async { - late final Result result; + late final Result result; try { final order = await _apiService.payScooterOrderWithPhotos( orderId: orderId, cardId: cardId, isBalance: isBalance, ); - if (order != null) { - result = Success(order); - } else { - result = Failure(UnknownFailure("Неизвестная ошибка")); - } + result = Success(null); } on AuthException catch (e) { result = Failure(AuthFailure(e.attemptsLeft)); } catch (e) { diff --git a/lib/di/service_locator.dart b/lib/di/service_locator.dart index 602cd0f..1198bff 100644 --- a/lib/di/service_locator.dart +++ b/lib/di/service_locator.dart @@ -85,13 +85,17 @@ import '../data/repositories/auth_repository_impl.dart'; import '../data/repositories/news_repository_impl.dart'; import '../data/repositories/pin_repository_impl.dart'; import '../data/repositories/profile_repository_impl.dart'; +import '../data/repositories/promo_code_repository_impl.dart'; import '../data/service/news_api_service.dart'; import '../domain/repositories/auth_repository.dart'; import '../domain/repositories/news_repository.dart'; +import '../domain/repositories/promo_code_repository.dart'; import '../domain/service/device_info_service.dart'; import '../domain/usecase/activate_subscription_usecase.dart'; +import '../domain/usecase/apply_promo_code_usecase.dart'; import '../domain/usecase/get_client_subscriptions_usecase.dart'; import '../domain/usecase/get_news_by_id_usecase.dart'; +import '../domain/usecase/get_notifications_usecase.dart'; import '../domain/usecase/get_scooter_by_title_usecase.dart'; import '../domain/usecase/get_scooter_order_history_usecase.dart'; import '../domain/usecase/remove_payment_card_usecase.dart'; @@ -100,7 +104,9 @@ import '../presentation/viewmodel/auth_bloc.dart'; import '../presentation/viewmodel/edit_profile_bloc.dart'; import '../presentation/viewmodel/map_bloc.dart'; import '../presentation/viewmodel/news_bloc.dart'; +import '../presentation/viewmodel/notifications_bloc.dart'; import '../presentation/viewmodel/order_history_bloc.dart'; +import '../presentation/viewmodel/promo_code_bloc.dart'; import '../presentation/viewmodel/scooter_detail_modal_bloc.dart'; import '../presentation/viewmodel/subscription_list_bloc.dart'; import '../presentation/viewmodel/verify_code_bloc.dart'; @@ -110,7 +116,7 @@ final getIt = GetIt.instance; Future setupDependencies() async { final sharedPreferences = await SharedPreferences.getInstance(); 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()); // HTTP getIt.registerSingleton(http.Client()); @@ -286,6 +292,13 @@ Future setupDependencies() async { getIt.registerSingleton( GetScooterByTitleUsecase(getIt()), ); + getIt.registerSingleton( + GetNotificationsUsecase(getIt()), + ); + getIt.registerFactory( + () => NotificationsBloc(getIt()), + ); + // Blocs getIt.registerLazySingleton(() => SplashBloc(getIt())); @@ -310,6 +323,7 @@ Future setupDependencies() async { getIt(), getIt(), getIt(), + getIt(), ), ); @@ -373,4 +387,20 @@ Future setupDependencies() async { getIt.registerFactory( () => ScooterCodeBloc(getScooterByTitleUsecase: getIt()), ); + + + // Repository + getIt.registerSingleton( + PromoCodeRepositoryImpl(getIt()), + ); + +// UseCase + getIt.registerSingleton( + ApplyPromoCodeUsecase(getIt()), + ); + +// Bloc (factory, т.к. экран создаёт новый экземпляр) + getIt.registerFactory( + () => PromoCodeBloc(getIt()), + ); } diff --git a/lib/domain/entities/client_subscription.dart b/lib/domain/entities/client_subscription.dart new file mode 100644 index 0000000..f68a51c --- /dev/null +++ b/lib/domain/entities/client_subscription.dart @@ -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 json) { + return ClientSubscription( + id: json['id'] ?? 0, + subscriptionId: json['subscriptionId'] ?? 0, + subscription: Subscription.fromJson(json['subscription'] as Map), + expiredAt: json['expiredAt'] != null ? DateTime.parse(json['expiredAt']) : null, + ); + } +} \ No newline at end of file diff --git a/lib/domain/entities/promo_code_result.dart b/lib/domain/entities/promo_code_result.dart new file mode 100644 index 0000000..7256168 --- /dev/null +++ b/lib/domain/entities/promo_code_result.dart @@ -0,0 +1,9 @@ +class PromoCodeResult { + final bool success; + final double balance; + + PromoCodeResult({ + required this.success, + required this.balance, + }); +} \ No newline at end of file diff --git a/lib/domain/entities/scooter.dart b/lib/domain/entities/scooter.dart index 3422e00..4168e57 100644 --- a/lib/domain/entities/scooter.dart +++ b/lib/domain/entities/scooter.dart @@ -43,8 +43,9 @@ class Scooter { ); } + @override String toString() { - return 'Scooter{id: $id, title: $title}'; + return 'Scooter{id: $id, title: $title, status: $status, latitude: $latitude, longitude: $longitude, batteryLevel: $batteryLevel, isOnline: $isOnline}'; } } diff --git a/lib/domain/entities/scooter_order.dart b/lib/domain/entities/scooter_order.dart index 85b62c0..9d815ca 100644 --- a/lib/domain/entities/scooter_order.dart +++ b/lib/domain/entities/scooter_order.dart @@ -3,7 +3,7 @@ import 'scooter.dart'; class ScooterOrder { final int id; final int scooterId; - final Scooter? scooter; + final Scooter scooter; final int? planId; final ScooterPlan? plan; final int clientId; @@ -33,7 +33,7 @@ class ScooterOrder { ScooterOrder({ required this.id, required this.scooterId, - this.scooter, + required this.scooter, this.planId, this.plan, required this.clientId, @@ -65,7 +65,7 @@ class ScooterOrder { return ScooterOrder( id: json['id'] ?? 0, scooterId: json['scooterId'] ?? 0, - scooter: json['scooter'] != null ? Scooter.fromJson(json['scooter']) : null, + scooter: Scooter.fromJson(json['scooter']), planId: json['planId'], plan: json['plan'] != null ? ScooterPlan.fromJson(json['plan']) : null, clientId: json['clientId'] ?? 0, diff --git a/lib/domain/entities/subscription.dart b/lib/domain/entities/subscription.dart index 4f6ddba..9f7ec7f 100644 --- a/lib/domain/entities/subscription.dart +++ b/lib/domain/entities/subscription.dart @@ -6,8 +6,10 @@ class Subscription { final String title; final String shortDescription; final String fullDescription; + final String fullDescriptionJson; final int planId; final bool isActive; + final bool isCurrent; final String currency; final DateTime? activeFrom; final DateTime? activeTo; @@ -21,6 +23,7 @@ class Subscription { required this.title, required this.shortDescription, required this.fullDescription, + required this.fullDescriptionJson, required this.planId, required this.isActive, required this.currency, @@ -28,6 +31,7 @@ class Subscription { this.activeTo, required this.createdAt, required this.updatedAt, + required this.isCurrent, required this.options, }); @@ -41,6 +45,7 @@ class Subscription { title: json['title'] ?? '', shortDescription: json['shortDescription'] ?? '', fullDescription: json['fullDescription'] ?? '', + fullDescriptionJson: json['fullDescriptionJson'] ?? '', planId: json['planId'] ?? 0, isActive: json['isActive'] ?? false, currency: currencyData['currency'] ?? 'BYN', @@ -48,6 +53,7 @@ class Subscription { activeTo: json['activeTo'] != null ? DateTime.parse(json['activeTo']) : null, createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : 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)).toList(), ); } diff --git a/lib/domain/repositories/notification_repository.dart b/lib/domain/repositories/notification_repository.dart index 9f3b044..7d58dac 100644 --- a/lib/domain/repositories/notification_repository.dart +++ b/lib/domain/repositories/notification_repository.dart @@ -9,4 +9,7 @@ abstract class NotificationRepository { /// Закрывает SSE-соединение void closeStream(); + + /// получить список уведомлений + Future> getNotifications(); } diff --git a/lib/domain/repositories/promo_code_repository.dart b/lib/domain/repositories/promo_code_repository.dart new file mode 100644 index 0000000..467ed16 --- /dev/null +++ b/lib/domain/repositories/promo_code_repository.dart @@ -0,0 +1,5 @@ +import '../entities/promo_code_result.dart'; + +abstract class PromoCodeRepository { + Future applyPromoCode(String code); +} \ No newline at end of file diff --git a/lib/domain/repositories/scooter_repository.dart b/lib/domain/repositories/scooter_repository.dart index 239a008..1e2be47 100644 --- a/lib/domain/repositories/scooter_repository.dart +++ b/lib/domain/repositories/scooter_repository.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:be_happy/domain/entities/active_scooter_order.dart'; import '../../core/result.dart'; +import '../entities/client_subscription.dart'; import '../entities/point.dart'; import '../entities/scooter.dart'; import '../entities/subscription.dart'; @@ -16,7 +17,7 @@ abstract class ScooterRepository { Future>> getAvailableTariffs(int scooterId); Future>> getAvailableSubscriptions(); Future> getSubscriptionById(int id); - Future>> getClientSubscriptions(); + Future>> getClientSubscriptions(); Future> bookScooter({ required int scooterId, required int planId, @@ -30,13 +31,13 @@ abstract class ScooterRepository { Future> pauseRide(int orderId); Future> resumeRide(int orderId); Future> finishRide(int orderId, List files); - Future> payRide(int orderId); + Future> payRide(int orderId); Future>> getClientOrders(); Future>> uploadScooterPhotos(List images); Future> updateScooterOrderData({ required int orderId, }); - Future> payScooterOrderWithPhotos({ + Future> payScooterOrderWithPhotos({ required int orderId, required int? cardId, required bool isBalance, diff --git a/lib/domain/usecase/apply_promo_code_usecase.dart b/lib/domain/usecase/apply_promo_code_usecase.dart new file mode 100644 index 0000000..683f075 --- /dev/null +++ b/lib/domain/usecase/apply_promo_code_usecase.dart @@ -0,0 +1,12 @@ +import '../entities/promo_code_result.dart'; +import '../repositories/promo_code_repository.dart'; + +class ApplyPromoCodeUsecase { + final PromoCodeRepository repository; + + ApplyPromoCodeUsecase(this.repository); + + Future call(String code) { + return repository.applyPromoCode(code); + } +} \ No newline at end of file diff --git a/lib/domain/usecase/get_client_subscriptions_usecase.dart b/lib/domain/usecase/get_client_subscriptions_usecase.dart index 0155332..d1b0be3 100644 --- a/lib/domain/usecase/get_client_subscriptions_usecase.dart +++ b/lib/domain/usecase/get_client_subscriptions_usecase.dart @@ -1,6 +1,7 @@ import 'package:be_happy/core/result.dart'; import 'package:be_happy/domain/entities/scooter_order.dart'; +import '../entities/client_subscription.dart'; import '../repositories/scooter_repository.dart'; @@ -14,7 +15,7 @@ class GetClientSubscriptionsUsecase { GetClientSubscriptionsUsecase(this.repository); - Future>> call() { + Future>> call() { return repository.getClientSubscriptions(); } } diff --git a/lib/domain/usecase/get_notifications_usecase.dart b/lib/domain/usecase/get_notifications_usecase.dart new file mode 100644 index 0000000..f67f238 --- /dev/null +++ b/lib/domain/usecase/get_notifications_usecase.dart @@ -0,0 +1,12 @@ +import '../entities/client_notification.dart'; +import '../repositories/notification_repository.dart'; + +class GetNotificationsUsecase { + final NotificationRepository repository; + + GetNotificationsUsecase(this.repository); + + Future> call() { + return repository.getNotifications(); + } +} \ No newline at end of file diff --git a/lib/domain/usecase/pay_ride_usecase.dart b/lib/domain/usecase/pay_ride_usecase.dart index a75b5b3..034cefe 100644 --- a/lib/domain/usecase/pay_ride_usecase.dart +++ b/lib/domain/usecase/pay_ride_usecase.dart @@ -7,7 +7,7 @@ class PayRideUsecase { PayRideUsecase(this.repository); - Future> call(int orderId, int? cardId, + Future> call(int orderId, int? cardId, bool isBalance) { return repository.payScooterOrderWithPhotos(orderId: orderId, cardId: cardId, isBalance: isBalance); } diff --git a/lib/domain/usecase/pay_scooter_order_with_photos_usecase.dart b/lib/domain/usecase/pay_scooter_order_with_photos_usecase.dart index 8d51b42..a35c40b 100644 --- a/lib/domain/usecase/pay_scooter_order_with_photos_usecase.dart +++ b/lib/domain/usecase/pay_scooter_order_with_photos_usecase.dart @@ -7,7 +7,7 @@ class PayScooterOrderWithPhotosUsecase { PayScooterOrderWithPhotosUsecase(this.repository); - Future> call({ + Future> call({ required int orderId, required int cardId, required bool isBalance, diff --git a/lib/main.dart b/lib/main.dart index 7795097..5dc56a8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,9 +23,21 @@ import 'di/service_locator.dart'; void main() async { 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(); + runApp(const MyApp()); } @@ -62,4 +74,4 @@ class MyApp extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/presentation/components/content_parser.dart b/lib/presentation/components/content_parser.dart new file mode 100644 index 0000000..71e4e21 --- /dev/null +++ b/lib/presentation/components/content_parser.dart @@ -0,0 +1,244 @@ +import 'package:flutter/material.dart'; +import 'package:html/parser.dart' as html_parser; +import 'package:html/dom.dart' as dom; +import 'dart:convert'; + +/// Парсер контента новостей (поддерживает HTML и JSON-формат) +class ContentParser { + + /// Парсит текст новости: сначала проверяет textJson, потом text + static List parseContent({String? textJson, String? text}) { + if (textJson != null && textJson.isNotEmpty) { + return _parseJsonText(textJson); + } else if (text != null && text.isNotEmpty) { + return _parseHtmlText(text); + } + return const []; + } + + // 🔹 HTML-парсинг + static List _parseHtmlText(String htmlText) { + final parsedHtml = html_parser.parse(htmlText); + final List elements = parsedHtml.body?.nodes ?? []; + return _parseHtmlElements(elements); + } + + static List _parseHtmlElements(List nodes) { + List widgets = []; + + for (final node in nodes) { + if (node is dom.Element) { + final text = node.text.trim(); + if (text.isEmpty) continue; + + switch (node.localName) { + case 'h1': + case 'h2': + case 'h3': + widgets.add(_buildHeader(text)); + break; + case 'p': + widgets.add(_buildParagraph(text)); + break; + case 'ul': + widgets.add(_buildUnorderedList(node.nodes)); + break; + default: + widgets.add(_buildDefaultText(text)); + } + } else if (node is dom.Text) { + final text = node.text.trim(); + if (text.isNotEmpty) { + widgets.add(_buildDefaultText(text)); + } + } + } + + return widgets; + } + + // 🔹 JSON-парсинг + static List _parseJsonText(String jsonString) { + try { + final dynamic data = jsonDecode(jsonString); + return _parseJsonNode(data); + } catch (e) { + return [ + const Text( + 'Ошибка отображения текста новости', + style: TextStyle(color: Colors.red, fontSize: 14), + ), + ]; + } + } + + static List _parseJsonNode(dynamic node) { + List widgets = []; + + if (node is List) { + for (final item in node) { + widgets.addAll(_parseJsonNode(item)); + } + } else if (node is Map) { + final type = node['type'] as String?; + final content = node['content']; + + switch (type) { + case 'div': + if (content is List) { + widgets.addAll(_parseJsonNode(content)); + } + break; + case 'h1': + case 'h2': + case 'h3': + final text = _extractJsonText(content); + if (text != null && text.isNotEmpty) { + widgets.add(_buildHeader(text)); + } + break; + case 'p': + final text = _extractJsonText(content); + if (text != null && text.isNotEmpty) { + widgets.add(_buildParagraph(text)); + } + break; + case 'ul': + if (content is List) { + widgets.add(_buildJsonUnorderedList(content)); + } + break; + case 'li': + final text = _extractJsonText(content); + if (text != null && text.isNotEmpty) { + widgets.add(_buildListItem(text)); + } + break; + default: + final text = _extractJsonText(content); + if (text != null && text.isNotEmpty) { + widgets.add(_buildDefaultText(text)); + } + } + } + + return widgets; + } + + // 🔹 Вспомогательные методы извлечения текста из JSON + static String? _extractJsonText(dynamic content) { + if (content == null) return null; + if (content is List) { + return content.map((e) => e?.toString() ?? '').where((s) => s.isNotEmpty).join(' '); + } + return content.toString(); + } + + // 🔹 Виджеты для отображения (принимают НЕ nullable String) + static Widget _buildHeader(String text) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text( + text, // ✅ text — не nullable + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ); + } + + static Widget _buildParagraph(String text) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + text, + style: const TextStyle( + color: Colors.white70, + fontSize: 14, + height: 1.5, + ), + ), + ); + } + + static Widget _buildDefaultText(String text) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + text, + style: const TextStyle( + color: Colors.white70, + fontSize: 14, + ), + ), + ); + } + + static Widget _buildUnorderedList(List children) { + final items = []; + for (final child in children) { + if (child is dom.Element && child.localName == 'li') { + final text = child.text.trim(); + if (text.isNotEmpty) { + items.add(_buildListItem(text)); + } + } + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: items, + ), + ); + } + + static Widget _buildJsonUnorderedList(List content) { + final items = []; + for (final item in content) { + if (item is Map && item['type'] == 'li') { + final text = _extractJsonText(item['content']); + if (text != null && text.isNotEmpty) { + items.add(_buildListItem(text)); + } + } + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: items, + ), + ); + } + + static Widget _buildListItem(String text) { + return Padding( + padding: const EdgeInsets.only(left: 16, top: 4, bottom: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('• ', + style: TextStyle( + color: Colors.white70, + fontSize: 14, + )), + Expanded( + child: Text( + text, + style: const TextStyle( + color: Colors.white70, + fontSize: 14, + height: 1.5, + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/presentation/components/dialog/cancel_booking_dialog.dart b/lib/presentation/components/dialog/cancel_booking_dialog.dart index d980296..c7a7047 100644 --- a/lib/presentation/components/dialog/cancel_booking_dialog.dart +++ b/lib/presentation/components/dialog/cancel_booking_dialog.dart @@ -3,11 +3,8 @@ import 'package:flutter/material.dart'; class CancelBookingDialog extends StatelessWidget { const CancelBookingDialog({super.key}); - @override Widget build(BuildContext context) { - bool result = false; - return Dialog( backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.symmetric(horizontal: 40), @@ -42,6 +39,7 @@ class CancelBookingDialog extends StatelessWidget { ), const SizedBox(height: 24), + // 🔹 Кнопка "Отменить" — ПЕРВАЯ, зелёная (градиент), возвращает true Container( width: double.infinity, height: 52, @@ -52,10 +50,7 @@ class CancelBookingDialog extends StatelessWidget { ), ), child: ElevatedButton( - onPressed: () => { - result = false, - Navigator.pop(context, result) - }, + onPressed: () => Navigator.pop(context, true), // ✅ true = отменить style: ElevatedButton.styleFrom( backgroundColor: Colors.transparent, shadowColor: Colors.transparent, @@ -64,9 +59,9 @@ class CancelBookingDialog extends StatelessWidget { ), ), child: const Text( - "Оставить", + "Отменить", style: TextStyle( - color: Color(0xFF1D273A), + color: Color(0xFF1D273A), // тёмный текст на светлом градиенте fontSize: 16, fontWeight: FontWeight.bold, ), @@ -74,14 +69,13 @@ class CancelBookingDialog extends StatelessWidget { ), ), const SizedBox(height: 12), + + // 🔹 Кнопка "Оставить" — ВТОРАЯ, тёмная, возвращает false SizedBox( width: double.infinity, height: 52, child: ElevatedButton( - onPressed: () { - result = true; - Navigator.pop(context, result); - }, + onPressed: () => Navigator.pop(context, false), // ❌ false = оставить style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF0D1024), shape: RoundedRectangleBorder( @@ -89,7 +83,7 @@ class CancelBookingDialog extends StatelessWidget { ), ), child: const Text( - "Отменить", + "Оставить", style: TextStyle(color: Colors.white, fontSize: 16), ), ), @@ -99,4 +93,4 @@ class CancelBookingDialog extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/presentation/components/dialog/finish_ride_confirmation_dialog.dart b/lib/presentation/components/dialog/finish_ride_confirmation_dialog.dart new file mode 100644 index 0000000..8850254 --- /dev/null +++ b/lib/presentation/components/dialog/finish_ride_confirmation_dialog.dart @@ -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), + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/presentation/components/map_settings_sheet.dart b/lib/presentation/components/map_settings_sheet.dart deleted file mode 100644 index c04c4dc..0000000 --- a/lib/presentation/components/map_settings_sheet.dart +++ /dev/null @@ -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( - 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().add(AllGeomarksToggled(val)), - ), - _SettingItemData( - label: 'Геозоны', - icon: Icons.gps_fixed_outlined, - color: const Color(0xFF86EFAC), - isActive: state.isAllGeozonesActive, - onChanged: (val) => context.read().add(AllGeozonesToggled(val)), - ), - _SettingItemData( - label: 'Парковка', - icon: Icons.home_outlined, - color: const Color(0xFFA78BFA), - isActive: state.isParkingZoneActive, - onChanged: (val) => context.read().add(ParkingZonesToggled(val)), - ), - _SettingItemData( - label: 'Парковка запрещена', - icon: Icons.block_outlined, - color: const Color(0xFFF59E0B), - isActive: state.isRestrictedParkingZoneActive, - onChanged: (val) => context.read().add(RestrictedParkingZonesToggled(val)), - ), - _SettingItemData( - label: 'Запрещено кататься', - icon: Icons.warning_amber_outlined, - color: const Color(0xFFEF4444), - isActive: state.isRestrictedDrivingZoneActive, - onChanged: (val) => context.read().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().add(ApllyButtonClick()); - context.read().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 onChanged; - - _SettingItemData({ - required this.label, - required this.icon, - required this.color, - required this.isActive, - required this.onChanged, - }); -} diff --git a/lib/presentation/components/sheet/active_ride_sheet.dart b/lib/presentation/components/sheet/active_ride_sheet.dart index 416317e..2be73e1 100644 --- a/lib/presentation/components/sheet/active_ride_sheet.dart +++ b/lib/presentation/components/sheet/active_ride_sheet.dart @@ -1,13 +1,17 @@ import 'dart:async'; import 'dart:ui'; +import 'package:be_happy/domain/entities/scooter.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import '../../../di/service_locator.dart'; import '../../event/active_ride_event.dart'; +import '../../event/map_event.dart'; import '../../state/active_ride_state.dart'; import '../../viewmodel/active_ride_bloc.dart'; +import '../../viewmodel/map_bloc.dart'; +import '../dialog/finish_ride_confirmation_dialog.dart'; import '../notification_toast.dart'; class ActiveRideSheet extends StatefulWidget { @@ -55,7 +59,7 @@ class _ActiveRideSheetState extends State { return BlocProvider.value( value: _bloc, child: BlocConsumer( - listenWhen: (previous, current) => previous.inZone != current.inZone, + listenWhen: (previous, current) => previous.inZone != current.inZone || previous.status != current.status, listener: (context, state) { if (!state.inZone) { BotToast.showCustomNotification( @@ -70,6 +74,15 @@ class _ActiveRideSheetState extends State { }, ); } + + if (state.status == ActiveRideStatus.success && state.order != null) { + final scooter = state.order!.scooter; + context.read().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) { // Логика отображения загрузки и ошибок остается прежней @@ -202,8 +215,8 @@ class _ActiveRideSheetState extends State { color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold, - fontFeatures: [FontFeature.tabularFigures()], - fontFamily: 'Digital Numbers', + fontFeatures: const [FontFeature.tabularFigures()], + fontFamily: 'DigitalNumbers', ), ), const SizedBox(height: 4), @@ -290,13 +303,23 @@ class _ActiveRideSheetState extends State { child: InkWell( onTap: state.status == ActiveRideStatus.loading ? null - : () { - _bloc.add(FinishRide(widget.orderId)); - Navigator.pop(context); - context.go("/home/order-photos/${widget.orderId}"); - }, + : () async { + // 🔹 Показываем диалог подтверждения + final result = await showDialog( + context: context, + builder: (context) => const FinishRideConfirmationDialog(), + ); + + // 🔹 Если пользователь подтвердил — завершаем и переходим + if (result == true) { + _bloc.add(FinishRide(widget.orderId)); + Navigator.pop(context); // закрываем ActiveRideSheet + context.go("/home/order-photos/${widget.orderId}"); + } + // 🔹 Если отменил — ничего не делаем, диалог уже закрылся + }, borderRadius: BorderRadius.circular(16), - child: Column( + child: Column( // ✅ Вернули Column с иконкой и текстом mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( diff --git a/lib/presentation/components/sheet/current_rides_sheet.dart b/lib/presentation/components/sheet/current_rides_sheet.dart index d0bd767..5305cea 100644 --- a/lib/presentation/components/sheet/current_rides_sheet.dart +++ b/lib/presentation/components/sheet/current_rides_sheet.dart @@ -219,11 +219,11 @@ class _RideCardState extends State<_RideCard> { displayTime = _elapsedTime; } final timeString = _formatDuration(displayTime); - final statusText = _getStatusText(widget.order.status); - final statusColor = _getStatusColor(widget.order.status); + final statusText = widget.order.status == 'Booking' ? 'Забронирован' : "Активный"; + final statusColor = widget.order.status == 'Booking' ? Color(0xFFFFCC00) : Color(0xFF8bffaa); final scooterNumber = - widget.order.scooter?.number ?? widget.order.scooterId.toString(); + widget.order.scooter.number ?? widget.order.scooterId.toString(); return Container( padding: const EdgeInsets.all(16), @@ -261,7 +261,7 @@ class _RideCardState extends State<_RideCard> { Text( statusText, style: TextStyle( - color: Colors.white, + color: statusColor, fontSize: 14, fontWeight: FontWeight.w600, ), @@ -269,22 +269,51 @@ class _RideCardState extends State<_RideCard> { ], ), const SizedBox(height: 4), - Text( - scooterNumber, - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 4), - Text( - _getLocationText(), - style: TextStyle( - color: Colors.white.withOpacity(0.6), - fontSize: 13, - ), + Row( + children: [ + Image.asset("assets/icons/qr_icon_order.png"), + const SizedBox(width: 6), + Text( + scooterNumber, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], ), + + if (isReserved) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + Text( + "Тариф", + style: TextStyle( + color: Color(0xFF8bffaa), + 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, style: TextStyle( color: statusColor, - fontSize: 26, + fontSize: 20, fontWeight: FontWeight.bold, fontFeatures: const [FontFeature.tabularFigures()], - fontFamily: 'Digital Numbers', + fontFamily: 'DigitalNumbers', ), ), ], @@ -360,7 +389,7 @@ class _RideCardState extends State<_RideCard> { switch (status.toLowerCase()) { case 'reserved': case 'holding': - return 'Забронировано'; + return 'Забронирован'; case 'active': case 'in_progress': return 'Активно'; diff --git a/lib/presentation/components/sheet/map_settings_sheet.dart b/lib/presentation/components/sheet/map_settings_sheet.dart index 07b8a2e..27b2f5c 100644 --- a/lib/presentation/components/sheet/map_settings_sheet.dart +++ b/lib/presentation/components/sheet/map_settings_sheet.dart @@ -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: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 { @@ -19,13 +17,6 @@ class MapSettingsSheet extends StatelessWidget { return BlocBuilder( 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().add(AllGeomarksToggled(val)), - ), _SettingItemData( label: 'Геозоны', icon: Icons.gps_fixed_outlined, @@ -41,9 +32,9 @@ class MapSettingsSheet extends StatelessWidget { onChanged: (val) => context.read().add(ParkingZonesToggled(val)), ), _SettingItemData( - label: 'Разрешено кататься', + label: 'Парковка запрещена', icon: Icons.block_outlined, - color: const Color(0xFF5ECD4C), + color: const Color(0xFFF59E0B), isActive: state.isRestrictedParkingZoneActive, onChanged: (val) => context.read().add(RestrictedParkingZonesToggled(val)), ), diff --git a/lib/presentation/components/sheet/payment_method_sheet.dart b/lib/presentation/components/sheet/payment_method_sheet.dart index 4eb5648..f30365d 100644 --- a/lib/presentation/components/sheet/payment_method_sheet.dart +++ b/lib/presentation/components/sheet/payment_method_sheet.dart @@ -12,11 +12,13 @@ import '../../state/payment_method_sheet_state.dart'; import '../../viewmodel/payment_method_sheet_bloc.dart'; class PaymentMethodSheet extends StatefulWidget { - final PaymentCard? initialSelectedCard; // Добавляем это поле + final PaymentCard? initialSelectedCard; + final bool showBalance; const PaymentMethodSheet({ super.key, - this.initialSelectedCard, // Инициализируем в конструкторе + this.initialSelectedCard, + this.showBalance = true, }); @override @@ -92,7 +94,7 @@ class _PaymentMethodSheetState extends State { _selectedPaymentMethod = initialIndex != -1 ? initialIndex : -1; } else { final mainCardIndex = state.cards.indexWhere((card) => card.isMain); - _selectedPaymentMethod = mainCardIndex != -1 ? mainCardIndex : -1; + _selectedPaymentMethod = mainCardIndex != -1 ? mainCardIndex : (widget.showBalance ? -1 : 0); } } @@ -169,19 +171,20 @@ class _PaymentMethodSheetState extends State { padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( children: [ - PaymentOption( - title: 'Баланс', - subtitle: '${state.balance.toStringAsFixed(2)} BYN', - isSelected: _selectedPaymentMethod == -1, - onTap: () { - setState(() { - _selectedPaymentMethod = -1; - }); - Navigator.pop(context, 'balance'); - }, - ), - - const SizedBox(height: 12), + if (widget.showBalance) ...[ + PaymentOption( + title: 'Баланс', + subtitle: '${state.balance.toStringAsFixed(2)} BYN', + isSelected: _selectedPaymentMethod == -1, + onTap: () { + setState(() { + _selectedPaymentMethod = -1; + }); + Navigator.pop(context, 'balance'); + }, + ), + const SizedBox(height: 12), + ], ...state.cards.asMap().entries.map((entry) { final index = entry.key; diff --git a/lib/presentation/components/sheet/reserved_ride_sheet.dart b/lib/presentation/components/sheet/reserved_ride_sheet.dart index d122c0b..06c9d2f 100644 --- a/lib/presentation/components/sheet/reserved_ride_sheet.dart +++ b/lib/presentation/components/sheet/reserved_ride_sheet.dart @@ -14,7 +14,7 @@ class ReservedRideSheet extends StatefulWidget { final String scooterNumber; final int orderId; final Duration initialReservationTime; - + const ReservedRideSheet({ super.key, required this.scooterNumber, @@ -60,264 +60,286 @@ class _ReservedRideSheetState extends State { Widget build(BuildContext context) { return BlocProvider.value( value: _bloc, - child: 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( - padding: const EdgeInsets.only(top: 20, bottom: 10), - decoration: BoxDecoration( - color: const Color(0xFF000032).withOpacity(0.5), - borderRadius: const BorderRadius.vertical(top: Radius.circular(30)), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // HEADER - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Row( - children: [ - GestureDetector( - onTap: () => Navigator.pop(context), - child: Row( - children: [ - Icon( - Icons.arrow_back_ios_sharp, - color: const Color(0x99FFFFFF), - size: 20, - ), - Icon( - Icons.arrow_back_ios_sharp, - color: const Color(0x66FFFFFF), - size: 20, - ), - Icon( - Icons.arrow_back_ios_sharp, - color: const Color(0x22FFFFFF), - size: 20, - ), - ], - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Бесплатное бронирование', - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), + child: Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + body: 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( + padding: const EdgeInsets.only(top: 20, bottom: 10), + decoration: BoxDecoration( + color: const Color(0xFF000032).withOpacity(0.5), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(30), ), - - const SizedBox(height: 20), - - // ТАЙМЕР + ИНФО О САМОКАТЕ (КОМПАКТНЫЙ) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Row( - children: [ - // Таймер - Expanded( - flex: 2, - child: Text( - _formatDuration(_reservationTime), - style: const TextStyle( - color: Colors.white, - fontSize: 32, - fontWeight: FontWeight.bold, - fontFeatures: [FontFeature.tabularFigures()], - fontFamily: 'Digital Numbers', - ), - ), - ), - // Иконка и информация (ВЫСОКИЙ БЛОК) - Expanded( - flex: 3, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // HEADER + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + GestureDetector( + onTap: () => Navigator.pop(context), child: Row( children: [ - // Иконка самоката (ВЫШЕ) - SizedBox( - width: 44, - height: 56, - child: Image.asset( - 'assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png', - fit: BoxFit.contain, - ), + Icon( + Icons.arrow_back_ios_sharp, + color: const Color(0x99FFFFFF), + size: 20, ), - const SizedBox(width: 12), - // Инфо - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 8, - height: 8, - decoration: const BoxDecoration( - color: Color(0xFFFFB800), - shape: BoxShape.circle, - ), - ), - const SizedBox(width: 6), - const Text( - 'Забронирован', - style: TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - overflow: TextOverflow.ellipsis, - ), - ], - ), - const SizedBox(height: 8), - Text( - '№${widget.scooterNumber}', - style: const TextStyle( - color: Colors.white, - fontSize: 15, - fontWeight: FontWeight.w600, - ), - ), - ], - ), + Icon( + Icons.arrow_back_ios_sharp, + color: const Color(0x66FFFFFF), + size: 20, + ), + Icon( + Icons.arrow_back_ios_sharp, + color: const Color(0x22FFFFFF), + size: 20, ), ], ), ), - ), - ], - ), - ), - - const SizedBox(height: 16), - - // КНОПКА "НАЧАТЬ ПОЕЗДКУ" - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: BlocListener( - listener: (context, state) { - if (state.rideStarted) { - Navigator.pop(context); - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => ActiveRideSheet( - scooterNumber: widget.scooterNumber, - initialElapsedTime: Duration.zero, - orderId: widget.orderId, + const SizedBox(width: 12), + Expanded( + child: Text( + 'Бесплатное бронирование', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + overflow: TextOverflow.ellipsis, ), - ); - } else if (state.status == ReservedRideStatus.failure) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(state.errorMessage ?? 'Ошибка')), - ); - } - }, - child: GradientButton( - text: 'Начать поездку', - showArrows: true, - height: 48, - width: double.infinity, - fontSize: 15, - onTap: () { - _bloc.add(StartRide(widget.orderId)); - }, + ), + ], ), ), - ), - const SizedBox(height: 12), + const SizedBox(height: 20), - // КНОПКА "ОТМЕНИТЬ БРОНИРОВАНИЕ" - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: BlocListener( - listener: (context, state) { - if (state.rideCancelled) { - Navigator.pop(context); - } else if (state.status == ReservedRideStatus.failure) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(state.errorMessage ?? 'Ошибка')), - ); - } - }, - child: Container( - height: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - border: Border.all( - color: Colors.white.withOpacity(0.4), - width: 1, + // ТАЙМЕР + ИНФО О САМОКАТЕ (КОМПАКТНЫЙ) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + // Таймер + Expanded( + flex: 2, + child: Text( + _formatDuration(_reservationTime), + style: const TextStyle( + color: Colors.white, + fontSize: 32, + fontWeight: FontWeight.bold, + fontFeatures: [FontFeature.tabularFigures()], + fontFamily: 'Digital Numbers', + ), + ), ), - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () async { - final result = await showDialog( - context: context, - builder: (context) => const CancelBookingDialog(), - ); - if (result != null && result) { - _bloc.add(CancelRide(widget.orderId)); - } - }, - borderRadius: BorderRadius.circular(24), - child: BlocBuilder( - builder: (context, state) { - if (state.status == ReservedRideStatus.loading) { - return const Center( - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, - ), - ), - ); - } - return const Center( - child: Text( - 'Отменить бронирование', - style: TextStyle( - color: Colors.white, - fontSize: 15, - fontWeight: FontWeight.w600, + // Иконка и информация (ВЫСОКИЙ БЛОК) + Expanded( + flex: 3, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 16, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + // Иконка самоката (ВЫШЕ) + SizedBox( + width: 44, + height: 56, + child: Image.asset( + 'assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png', + fit: BoxFit.contain, ), ), + const SizedBox(width: 12), + // Инфо + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: Color(0xFFFFB800), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 6), + const Text( + 'Забронирован', + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + const SizedBox(height: 8), + Text( + '№${widget.scooterNumber}', + style: const TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 16), + + // КНОПКА "НАЧАТЬ ПОЕЗДКУ" + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: BlocListener( + listener: (context, state) { + if (state.rideStarted) { + Navigator.pop(context); + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => ActiveRideSheet( + scooterNumber: widget.scooterNumber, + initialElapsedTime: Duration.zero, + orderId: widget.orderId, + ), + ); + } else if (state.status == + ReservedRideStatus.failure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage ?? 'Ошибка'), + ), + ); + } + }, + child: GradientButton( + text: 'Начать поездку', + showArrows: true, + height: 48, + width: double.infinity, + fontSize: 15, + onTap: () { + _bloc.add(StartRide(widget.orderId)); + }, + ), + ), + ), + + const SizedBox(height: 12), + + // КНОПКА "ОТМЕНИТЬ БРОНИРОВАНИЕ" + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: BlocListener( + listener: (context, state) { + if (state.rideCancelled) { + Navigator.pop(context); + } else if (state.status == + ReservedRideStatus.failure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage ?? 'Ошибка'), + ), + ); + } + }, + child: Container( + height: 48, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: Colors.white.withOpacity(0.4), + width: 1, + ), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () async { + final result = await showDialog( + context: context, + builder: (context) => + const CancelBookingDialog(), ); + if (result != null && result) { + _bloc.add(CancelRide(widget.orderId)); + } }, + borderRadius: BorderRadius.circular(24), + child: + BlocBuilder< + ReservedRideBloc, + ReservedRideState + >( + builder: (context, state) { + if (state.status == + ReservedRideStatus.loading) { + return const Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ), + ); + } + return const Center( + child: Text( + 'Отменить бронирование', + style: TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ); + }, + ), ), ), ), ), ), - ), - const SizedBox(height: 20), - ], + const SizedBox(height: 20), + ], + ), ), ), ), diff --git a/lib/presentation/components/subscription_card.dart b/lib/presentation/components/subscription_card.dart index 1c086ed..a7a26ec 100644 --- a/lib/presentation/components/subscription_card.dart +++ b/lib/presentation/components/subscription_card.dart @@ -1,13 +1,18 @@ +import 'package:be_happy/presentation/components/gradient_button.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import '../../domain/entities/subscription.dart'; +import '../event/subscription_list_event.dart'; class SubscriptionCard extends StatelessWidget { final Subscription subscription; 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 Widget build(BuildContext context) { @@ -15,9 +20,9 @@ class SubscriptionCard extends StatelessWidget { ? subscription.options.reduce((a, b) => a.price < b.price ? a : b) : null; - final maxDaysOption = subscription.options.isNotEmpty + /*final maxDaysOption = subscription.options.isNotEmpty ? subscription.options.reduce((a, b) => a.days > b.days ? a : b) - : null; + : null;*/ return Container( margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), @@ -43,7 +48,7 @@ class SubscriptionCard extends StatelessWidget { padding: EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.white.withOpacity(0.3), - borderRadius: BorderRadius.circular(12), // Опционально: скругление углов + borderRadius: BorderRadius.circular(12), ), child: Text( "АКТИВНА", @@ -64,12 +69,20 @@ class SubscriptionCard extends StatelessWidget { subscription.shortDescription, style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 14), ), - const SizedBox(height: 16), - if (maxDaysOption != null) ...[ + Spacer(), + if (isActive && expiredAt != null) ...[ const SizedBox(height: 16), - Text( - "Период действия: до ${maxDaysOption.days} дней", - style: const TextStyle(color: Colors.white, fontSize: 14), + Builder( + builder: (context) { + final day = expiredAt!.day.toString().padLeft(2, '0'); + 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), + ); + }, ), ], const SizedBox(height: 20), @@ -83,15 +96,17 @@ class SubscriptionCard extends StatelessWidget { ) else const SizedBox.shrink(), - ElevatedButton( - onPressed: () => context.push("/home/subscriptions/${subscription.id}"), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF80FFD1), - foregroundColor: Colors.black, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - ), - child: const Text("Подробнее", style: TextStyle(fontWeight: FontWeight.bold)), + GradientButton( + onTap: () async { + final isSubscribed = await context.push("/home/subscriptions/${subscription.id}"); + if (isSubscribed == true && onRefresh != null) { + onRefresh!(); + } + }, + text: "Подробнее", + enabled: true, + width: 120, + height: 40, ), ], ), diff --git a/lib/presentation/event/map_event.dart b/lib/presentation/event/map_event.dart index 1d4c21e..16886ef 100644 --- a/lib/presentation/event/map_event.dart +++ b/lib/presentation/event/map_event.dart @@ -30,4 +30,14 @@ class NotificationReceived extends ScooterEvent { NotificationReceived(this.notification); } +class FocusOnScooter extends ScooterEvent { + final Scooter scooter; + FocusOnScooter(this.scooter); +} + +class ClearMapPlacemarks extends ScooterEvent {} +class ClearMapFocus extends ScooterEvent {} + + + diff --git a/lib/presentation/event/notifications_event.dart b/lib/presentation/event/notifications_event.dart new file mode 100644 index 0000000..b4eb4b1 --- /dev/null +++ b/lib/presentation/event/notifications_event.dart @@ -0,0 +1,3 @@ +sealed class NotificationsEvent {} + +class NotificationsFetchRequested extends NotificationsEvent {} \ No newline at end of file diff --git a/lib/presentation/event/promo_code_event.dart b/lib/presentation/event/promo_code_event.dart new file mode 100644 index 0000000..65c8852 --- /dev/null +++ b/lib/presentation/event/promo_code_event.dart @@ -0,0 +1,8 @@ +sealed class PromoCodeEvent {} + +class PromoCodeApplyRequested extends PromoCodeEvent { + final String code; + PromoCodeApplyRequested(this.code); +} + +class PromoCodeReset extends PromoCodeEvent {} \ No newline at end of file diff --git a/lib/presentation/navigation/app_router.dart b/lib/presentation/navigation/app_router.dart index e1ed39c..7b994bc 100644 --- a/lib/presentation/navigation/app_router.dart +++ b/lib/presentation/navigation/app_router.dart @@ -58,9 +58,9 @@ import '../../domain/usecase/remove_payment_card_usecase.dart'; import '../../domain/usecase/save_map_settings_usecase.dart'; import '../../domain/usecase/set_main_payment_card_usecase.dart'; import '../../domain/usecase/verify_pin_usecase.dart'; -import '../components/map_settings_sheet.dart'; import '../components/scooter_bottom_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/reserved_ride_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 '../screens/add_card_screen.dart'; // ← новый импорт import '../screens/license_agreement_screen.dart'; +import '../screens/notifications_screen.dart'; import '../screens/order_history_screen.dart'; import '../screens/payment_methods_screen.dart'; import '../screens/phone_login_screen.dart'; @@ -371,6 +372,7 @@ class AppRouter { SubscriptionDetailsBloc( getIt(), getIt(), + getIt(), ) ..add( LoadDetailsEvent( @@ -456,6 +458,10 @@ class AppRouter { builder: (context, state) => const OrderHistoryScreen(), routes: [] ), + GoRoute( + path: 'notifications', + builder: (context, state) => const NotificationsScreen(), + ), ], ), ], diff --git a/lib/presentation/screens/add_card_screen.dart b/lib/presentation/screens/add_card_screen.dart index e6f55f7..0b4044e 100644 --- a/lib/presentation/screens/add_card_screen.dart +++ b/lib/presentation/screens/add_card_screen.dart @@ -18,12 +18,16 @@ class AddCardScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: false, body: BlocListener( - listenWhen: (previous, current) => - previous.status != current.status && - current.status == AddCardStatus.success, + listenWhen: (previous, current) { + print( + 'Смена статуса: ${previous.status} -> ${current.status} ${current.errorMessage}'); + return previous.status != current.status && + current.status == AddCardStatus.success; + }, listener: (context, state) { - context.pop(); + context.pop(true); }, child: Container( decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg), @@ -35,6 +39,7 @@ class AddCardScreen extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( children: [ + const SizedBox(height: 16), const CustomAppBar(title: 'Добавление карты'), const SizedBox(height: 24), @@ -148,10 +153,7 @@ class AddCardScreen extends StatelessWidget { child: InkWell( onTap: state.isFormValid ? () => { - context.read().add( - AddCardSubmitted()), - context.read()..add(PaymentMethodsStarted()), - context.pop() + context.read().add(AddCardSubmitted()), } : null, borderRadius: BorderRadius.circular( diff --git a/lib/presentation/screens/documents_screen.dart b/lib/presentation/screens/documents_screen.dart index 49f5e8a..9e71c22 100644 --- a/lib/presentation/screens/documents_screen.dart +++ b/lib/presentation/screens/documents_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../core/app_colors.dart'; import '../components/custom_app_bar.dart'; @@ -17,12 +18,10 @@ class DocumentsScreen extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( children: [ - // ✅ Используем общий AppBar const SizedBox(height: 16), CustomAppBar(title: 'Документы'), const SizedBox(height: 32), - // Список ссылок LinkRow( icon: 'assets/icons/doc.png', title: 'Договор аренды', @@ -33,14 +32,14 @@ class DocumentsScreen extends StatelessWidget { LinkRow( icon: 'assets/icons/doc.png', title: 'Политика конфиденциальности', - onTap: () => openLink('https://...'), + onTap: () => context.push('/privacy-policy') ), const Divider(height: 1, color: Colors.white24), const SizedBox(height: 12), LinkRow( icon: 'assets/icons/doc.png', title: 'Правила вождения', - onTap: () => openLink('https://...'), + onTap: () => openLink('https://behappybel.by/#rule'), ), const Divider(height: 1, color: Colors.white24), const SizedBox(height: 12), diff --git a/lib/presentation/screens/license_agreement_screen.dart b/lib/presentation/screens/license_agreement_screen.dart index c8d3599..526c58b 100644 --- a/lib/presentation/screens/license_agreement_screen.dart +++ b/lib/presentation/screens/license_agreement_screen.dart @@ -14,7 +14,7 @@ class LicenseAgreementScreen extends StatelessWidget { child: SafeArea( child: Column( children: [ - // 🔹 APPBAR С КНОПКОЙ НАЗАД + const SizedBox(height: 16), const Padding( padding: EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: ' '), diff --git a/lib/presentation/screens/map_screen.dart b/lib/presentation/screens/map_screen.dart index f7d35b2..a6956ac 100644 --- a/lib/presentation/screens/map_screen.dart +++ b/lib/presentation/screens/map_screen.dart @@ -93,7 +93,9 @@ class _MapScreenState extends State { listenWhen: (previous, current) { return current.lastNotification != previous.lastNotification || - current.flags != previous.flags; + current.flags != previous.flags || + previous.selectedScooterForFocus?.id + != current.selectedScooterForFocus?.id; }, listener: (context, state) { @@ -164,14 +166,39 @@ class _MapScreenState extends State { ); } } + + if (state.selectedScooterForFocus != null) { + final targetScooter = state.selectedScooterForFocus!; + + print("RESERVED SCOOTER: $targetScooter"); + + _moveCameraToPoint( + targetScooter.longitude, + targetScooter.latitude, + zoom: 17, + ); + + context.read().add(ClearMapFocus()); + } }, - buildWhen: (previous, current) => - previous.scooters != current.scooters || - previous.zones != current.zones, + buildWhen: (previous, current) { + return previous.scooters != current.scooters || + previous.reservedScooters != current.reservedScooters || + previous.zones != current.zones || + previous.status != current.status; + }, + builder: (context, state) { - final scooters = _buildScooterPlacemarks( - state.scooters, - state.address ?? "Unknown address", + final freeScooters = _buildScooterPlacemarks( + scooters: state.scooters, + 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); @@ -193,9 +220,10 @@ class _MapScreenState extends State { }, mapObjects: [ ...zonePolygons, + ...reservedScooters, ClusterizedPlacemarkCollection( mapId: const MapObjectId('scooters_cluster'), - placemarks: scooters, + placemarks: freeScooters, radius: 30, minZoom: 15, consumeTapEvents: true, @@ -232,7 +260,6 @@ class _MapScreenState extends State { }, ), - // Индикатор загрузки (отдельный строитель для статуса) BlocBuilder( buildWhen: (previous, current) => previous.status != current.status, @@ -378,70 +405,42 @@ class _MapScreenState extends State { } void _onMarkerTap(List scooters) async { + context.read().add(CheckUser()); + final flags = context.read().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( "/home/scooter-sheet", extra: {'scooters': scooters, 'currentLocation': _currentPosition}, ); - - /*final scoot = await showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - isDismissible: true, - builder: (context) { - return BlocProvider( - create: (context) => - ScooterDetailModalBloc( - getIt(), - getIt(), - getIt(), - )..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( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - isDismissible: true, - builder: (context) => BlocProvider( - create: (context) => TariffSheetBloc( - getIt(), - getIt(), - getIt(), - ), - child: TariffSheet(scooter: scoot), - ), - ); - }); - } - } - - if (isBooking ?? false) { - showModalBottomSheet( - context: context, - builder: (context) => BlocProvider( - create: (context) => - CurrentRidesBloc(getIt()) - ..add(LoadClientOrders(1)), - child: CurrentRidesSheet(clientId: 1), - ), - ); - }*/ } void _onMapSettingsTap() { @@ -582,26 +581,26 @@ class _MapScreenState extends State { await ClusterIconPainter.initImage('assets/icons/scooter_placemark.png'); } - List _buildScooterPlacemarks( - List scooters, - String address, - ) { + List _buildScooterPlacemarks({ + required List scooters, + required String iconAsset, + required bool isClickable, + }) { return scooters.map((scooter) { return PlacemarkMapObject( - mapId: MapObjectId('${scooter.id}'), + mapId: MapObjectId('${isClickable ? "" : "reserved_"}${scooter.id}'), // уникальный ID для карты point: Point(latitude: scooter.longitude, longitude: scooter.latitude), icon: PlacemarkIcon.single( PlacemarkIconStyle( - image: BitmapDescriptor.fromAssetImage( - 'assets/icons/scooter_placemark_fill.png', - ), + image: BitmapDescriptor.fromAssetImage(iconAsset), scale: 0.2, ), ), opacity: 1.0, - onTap: (object, point) async => { - _onMarkerTap([scooter]), - }, + consumeTapEvents: isClickable, + onTap: isClickable + ? (object, point) async => _onMarkerTap([scooter]) + : null, ); }).toList(); } @@ -696,13 +695,13 @@ class _MapScreenState extends State { children: [ _RoundIconButton( icon: Icons.notifications_sharp, - onPressed: _onNotificationTap, + onPressed: () => context.push("/home/notifications"), ), const SizedBox(height: 12), - _RoundIconButton( - icon: Icons.directions_run, + _RoundButton( + imagePath: 'assets/icons/scooter_placemark.png', onPressed: () => context.push("/home/current-rides-sheet"), - ), + ) ], ), ), @@ -717,7 +716,42 @@ class _MapScreenState extends State { right: 0, child: Center( child: GestureDetector( - onTap: () => context.push("/home/qr-info"), + onTap: () { + final flags = context.read().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( width: 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 { final IconData icon; 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 + ), + ); + }, + ); +} diff --git a/lib/presentation/screens/news_detail_screen.dart b/lib/presentation/screens/news_detail_screen.dart index cdc853c..c118259 100644 --- a/lib/presentation/screens/news_detail_screen.dart +++ b/lib/presentation/screens/news_detail_screen.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:html/parser.dart' as html_parser; -import 'package:html/dom.dart' as dom; -import 'dart:convert'; import 'package:be_happy/di/service_locator.dart'; import '../../core/app_colors.dart'; import '../components/custom_app_bar.dart'; +import '../components/content_parser.dart'; import '../../domain/usecase/get_news_by_id_usecase.dart'; class NewsDetailScreen extends StatefulWidget { @@ -61,7 +59,7 @@ class _NewsDetailScreenState extends State { child: SafeArea( child: Column( children: [ - // 🔹 Заголовок в AppBar + const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: widget.title), @@ -126,258 +124,17 @@ class _NewsDetailScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - - // Текст новости: сначала проверяем textJson, потом text - if (news.textJson != null) - ..._parseJsonText(news.textJson) - else if (news.text != null) - ..._parseHtmlText(news.text), + // ✅ Используем вынесенный парсер + ...ContentParser.parseContent( + textJson: news.textJson, + text: news.text, + ), ], ), ); } - List _parseHtmlText(String htmlText) { - final parsedHtml = html_parser.parse(htmlText); - final List elements = parsedHtml.body?.nodes ?? []; - - return _parseHtmlElements(elements); - } - - List _parseHtmlElements(List nodes) { - List widgets = []; - - for (final node in nodes) { - if (node is dom.Element) { - switch (node.localName) { - case 'h1': - case 'h2': - case 'h3': - widgets.add( - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text( - node.text, - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ), - ); - break; - case 'p': - widgets.add( - Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Text( - node.text, - style: const TextStyle( - color: Colors.white70, - fontSize: 14, - height: 1.5, - ), - ), - ), - ); - break; - case 'ul': - widgets.add( - Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: node.children - .where((element) => element is dom.Element && element.localName == 'li') - .map((dom.Element li) => Padding( - padding: const EdgeInsets.only(left: 16, top: 4, bottom: 4), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('• ', - style: TextStyle( - color: Colors.white70, - fontSize: 14, - )), - Expanded( - child: Text( - li.text, - style: const TextStyle( - color: Colors.white70, - fontSize: 14, - height: 1.5, - ), - ), - ), - ], - ), - )) - .toList(), - ), - ), - ); - break; - default: - widgets.add( - Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Text( - node.text, - style: const TextStyle( - color: Colors.white70, - fontSize: 14, - ), - ), - ), - ); - } - } else if (node is dom.Text) { - widgets.add( - Text( - node.text, - style: const TextStyle( - color: Colors.white70, - fontSize: 14, - ), - ), - ); - } - } - - return widgets; - } - - // 🔹 Парсинг JSON-текста (новый метод) - List _parseJsonText(String jsonString) { - try { - final dynamic data = jsonDecode(jsonString); - return _parseJsonNode(data); - } catch (e) { - return [ - const Text( - 'Ошибка отображения текста новости', - style: TextStyle(color: Colors.red, fontSize: 14), - ), - ]; - } - } - - List _parseJsonNode(dynamic node) { - List widgets = []; - - if (node is List) { - // Если корень — массив, парсим каждый элемент - for (final item in node) { - widgets.addAll(_parseJsonNode(item)); - } - } else if (node is Map) { - final type = node['type'] as String?; - final content = node['content']; - - switch (type) { - case 'div': - if (content is List) { - widgets.addAll(_parseJsonNode(content)); - } - break; - case 'h2': - widgets.add( - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text( - (content is List) - ? content.join(' ') - : content?.toString() ?? '', - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ), - ); - break; - case 'p': - widgets.add( - Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Text( - (content is List) - ? content.join(' ') - : content?.toString() ?? '', - style: const TextStyle( - color: Colors.white70, - fontSize: 14, - height: 1.5, - ), - ), - ), - ); - break; - case 'ul': - if (content is List) { - widgets.add( - Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: content - .where((item) => item is Map) - .map((item) => item as Map) - .where((item) => item['type'] == 'li') - .map((li) => Padding( - padding: const EdgeInsets.only(left: 16, top: 4, bottom: 4), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('• ', - style: TextStyle( - color: Colors.white70, - fontSize: 14, - )), - Expanded( - child: Text( - (li['content'] is List) - ? li['content'].join(' ') - : li['content']?.toString() ?? '', - style: const TextStyle( - color: Colors.white70, - fontSize: 14, - height: 1.5, - ), - ), - ), - ], - ), - )) - .toList(), - ), - ), - ); - } - break; - default: - // Если тип неизвестен, просто выводим текст - widgets.add( - Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Text( - (content is List) - ? content.join(' ') - : content?.toString() ?? type ?? 'unknown', - style: const TextStyle( - color: Colors.white70, - fontSize: 14, - ), - ), - ), - ); - } - } - - return widgets; - } + // 🔹 Удалены старые методы: _parseHtmlText, _parseHtmlElements, _parseJsonText, _parseJsonNode String _formatDate(DateTime? date) { if (date == null) return ''; diff --git a/lib/presentation/screens/news_screen.dart b/lib/presentation/screens/news_screen.dart index 965f7c8..8555a5f 100644 --- a/lib/presentation/screens/news_screen.dart +++ b/lib/presentation/screens/news_screen.dart @@ -4,6 +4,7 @@ import 'dart:developer' as dev; import '../../core/app_colors.dart'; import '../../di/service_locator.dart'; import '../components/custom_app_bar.dart'; +import '../components/gradient_button.dart'; import '../event/news_event.dart'; import '../state/news_state.dart'; import '../viewmodel/news_bloc.dart'; @@ -14,11 +15,8 @@ class NewsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - dev.log('🔍 NewsScreen: Создание экрана новостей'); - return BlocProvider( create: (context) { - dev.log('🔍 NewsScreen: Создание NewsBloc'); return getIt()..add(const NewsFetchRequested()); }, child: const NewsView(), @@ -31,7 +29,6 @@ class NewsView extends StatelessWidget { @override Widget build(BuildContext context) { - dev.log('🔍 NewsView: Построение UI'); return Scaffold( body: Container( @@ -48,7 +45,6 @@ class NewsView extends StatelessWidget { Expanded( child: BlocBuilder( builder: (context, state) { - dev.log('🔍 NewsView: Состояние ${state.status}, новостей: ${state.news.length}'); if (state.status == NewsStatus.initial || state.status == NewsStatus.loading) { return const Center( @@ -84,7 +80,6 @@ class NewsView extends StatelessWidget { const SizedBox(height: 24), ElevatedButton( onPressed: () { - dev.log('🔍 NewsView: Повторная загрузка'); context.read().add(const NewsFetchRequested()); }, style: ElevatedButton.styleFrom( @@ -173,7 +168,7 @@ class _NewsCard extends StatelessWidget { margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: const Color(0xFF141530), + color: const Color(0xFF0A0F2E).withOpacity(0.7), borderRadius: BorderRadius.circular(16), ), child: Column( @@ -196,6 +191,18 @@ class _NewsCard extends StatelessWidget { ), ), 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( news.previewText, style: const TextStyle( @@ -205,51 +212,28 @@ class _NewsCard extends StatelessWidget { ), ), const SizedBox(height: 16), - SizedBox( - height: 40, - child: OutlinedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => NewsDetailScreen( - newsId: news.id, - title: news.title, + + Align( + alignment: Alignment.centerRight, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 150), + child: GradientButton( + text: 'Подробнее', + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NewsDetailScreen( + newsId: news.id, + title: news.title, + ), ), - ), - ); - }, - style: OutlinedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), - 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), - ), - ], + ); + }, + showArrows: true, + fontSize: 14, + height: 40, + width: double.infinity, ), ), ), diff --git a/lib/presentation/screens/notifications_screen.dart b/lib/presentation/screens/notifications_screen.dart new file mode 100644 index 0000000..d3777d0 --- /dev/null +++ b/lib/presentation/screens/notifications_screen.dart @@ -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()..add(NotificationsFetchRequested()), + child: NotificationsView( + filter: _filter, + onFilterChanged: _setFilter, + ), + ); + } +} + +class NotificationsView extends StatelessWidget { + final NotificationFilter filter; + final ValueChanged 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( + 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().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().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 'Самокат'; + } + } +} \ No newline at end of file diff --git a/lib/presentation/screens/order_history_detail_screen.dart b/lib/presentation/screens/order_history_detail_screen.dart index e39d1fb..66f2e06 100644 --- a/lib/presentation/screens/order_history_detail_screen.dart +++ b/lib/presentation/screens/order_history_detail_screen.dart @@ -32,7 +32,7 @@ class OrderHistoryDetailScreen extends StatelessWidget { child: SafeArea( child: Column( children: [ - // 🔹 HEADER + const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: 'Поездка $date'), diff --git a/lib/presentation/screens/order_history_screen.dart b/lib/presentation/screens/order_history_screen.dart index 612b3f1..4257a5c 100644 --- a/lib/presentation/screens/order_history_screen.dart +++ b/lib/presentation/screens/order_history_screen.dart @@ -32,6 +32,7 @@ class OrderHistoryView extends StatelessWidget { child: SafeArea( child: Column( children: [ + const SizedBox(height: 16), const Padding( padding: EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: 'История поездок'), diff --git a/lib/presentation/screens/payment_confirm_screen.dart b/lib/presentation/screens/payment_confirm_screen.dart index 8b05cba..b8aafa1 100644 --- a/lib/presentation/screens/payment_confirm_screen.dart +++ b/lib/presentation/screens/payment_confirm_screen.dart @@ -9,9 +9,11 @@ import '../components/custom_app_bar.dart'; import '../components/gradient_button.dart'; import '../components/payment_option.dart'; import '../components/sheet/payment_method_sheet.dart'; +import '../event/map_event.dart'; import '../event/payment_confirm_event.dart'; import '../event/payment_method_sheet_event.dart'; import '../state/payment_confirm_state.dart'; +import '../viewmodel/map_bloc.dart'; import '../viewmodel/payment_confirm_bloc.dart'; import '../viewmodel/payment_method_sheet_bloc.dart'; @@ -62,6 +64,7 @@ class _PaymentConfirmScreenContent extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox(height: 16), const Padding( padding: EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: 'Завершение поездки'), @@ -77,6 +80,7 @@ class _PaymentConfirmScreenContent extends StatelessWidget { listener: (context, state) { if (state.status == PaymentConfirmStatus.success && state.paymentCompleted) { + context.read().add(ClearMapPlacemarks()); context.go('/home'); } else if (state.status == PaymentConfirmStatus.failure) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); diff --git a/lib/presentation/screens/payment_methods_screen.dart b/lib/presentation/screens/payment_methods_screen.dart index 0e2f13d..f206781 100644 --- a/lib/presentation/screens/payment_methods_screen.dart +++ b/lib/presentation/screens/payment_methods_screen.dart @@ -17,42 +17,63 @@ class PaymentMethodsScreen extends StatelessWidget { body: Container( decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg), child: SafeArea( - child: Column( - children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 20), - child: CustomAppBar(title: 'Способы оплаты'), - ), - const SizedBox(height: 24), - Expanded( - child: BlocConsumer( - listener: (context, state) { - if (state.status == PaymentMethodsStatus.failure) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(state.errorMessage ?? 'Ошибка')), - ); - } - }, - builder: (context, state) { - if (state.status == PaymentMethodsStatus.loading && state.cards.isEmpty) { - return const Center(child: CircularProgressIndicator(color: Color(0xFF00D4AA))); - } + child: BlocConsumer( + listener: (context, state) { + if (state.status == PaymentMethodsStatus.failure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.errorMessage ?? 'Ошибка')), + ); + } + }, + builder: (context, state) { + final isNetworkProcessing = state.status == PaymentMethodsStatus.loading || + (state.isDeleting ?? false) || + (state.isSettingMain ?? false); - return SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildBalanceCard(context, state.balance), - const SizedBox(height: 20), - _buildCardsList(context, state), - ], + 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), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildBalanceCard(context, state.balance), + const SizedBox(height: 20), + _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( color: Colors.transparent, child: InkWell( - onTap: () => context.go('/home/payment-methods/add-card'), + onTap: () async { + final isCardAdded = await context.push('/home/payment-methods/add-card'); + + if (isCardAdded == true && context.mounted) { + context.read().add(PaymentMethodsStarted()); + } + }, borderRadius: BorderRadius.circular(24), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16), diff --git a/lib/presentation/screens/privacy_policy_screen.dart b/lib/presentation/screens/privacy_policy_screen.dart index c4c6430..18838e0 100644 --- a/lib/presentation/screens/privacy_policy_screen.dart +++ b/lib/presentation/screens/privacy_policy_screen.dart @@ -14,6 +14,7 @@ class PrivacyPolicyScreen extends StatelessWidget { child: SafeArea( child: Column( children: [ + const SizedBox(height: 16), const Padding( padding: EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: ''), diff --git a/lib/presentation/screens/profile_screen.dart b/lib/presentation/screens/profile_screen.dart index c1ce386..916b3fd 100644 --- a/lib/presentation/screens/profile_screen.dart +++ b/lib/presentation/screens/profile_screen.dart @@ -50,7 +50,9 @@ class _ProfileScreenState extends State { Widget build(BuildContext context) { return Scaffold( body: Container( - decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg), + decoration: const BoxDecoration( + gradient: AppColors.phoneScreenBg, + ), child: SafeArea( child: BlocBuilder( builder: (context, state) { @@ -70,56 +72,82 @@ class _ProfileScreenState extends State { } final profile = state.profile!; - return SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - children: [ - const SizedBox(height: 16), - CustomAppBar(title: 'Профиль'), - const SizedBox(height: 32), - Stack( - alignment: Alignment.topRight, - children: [ - CircleAvatar( - radius: 60, - backgroundColor: AppColors.checkboxFill, - backgroundImage: (profile.avatarUrl != null && profile.avatarUrl!.isNotEmpty) - ? NetworkImage("${profile.avatarUrl!}?v=${DateTime.now().minute}") - : null, - 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, - child: Container( - margin: const EdgeInsets.only(top: 0, right: 0), - child: Image.asset( - 'assets/icons/edit.png', - width: 24, - height: 24, + return LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + const SizedBox(height: 16), + CustomAppBar(title: 'Профиль'), + const SizedBox(height: 32), + + Stack( + alignment: Alignment.topRight, + children: [ + CircleAvatar( + radius: 60, + backgroundColor: AppColors.checkboxFill, + backgroundImage: + (profile.avatarUrl != null && + profile.avatarUrl!.isNotEmpty) + ? NetworkImage( + "${profile.avatarUrl!}?v=${DateTime.now().minute}", + ) + : null, + 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, + child: Container( + child: Image.asset( + 'assets/icons/edit.png', + width: 24, + height: 24, + ), + ), + ), + ], ), - ), + + const SizedBox(height: 32), + + _ProfileInfoBlock( + profile: profile, + onEditTap: () => context.go("/home/profile/edit"), + ), + + // const SizedBox(height: 24), + // _SettingsBlock( + // notificationsEnabled: notificationsEnabled, + // onNotificationsChanged: (v) => + // setState(() => notificationsEnabled = v), + // ), + ], ), - ], + ), ), - const SizedBox(height: 32), - _ProfileInfoBlock( - profile: profile, - onEditTap: () => context.go("/home/profile/edit"), - ), - const SizedBox(height: 24), - _SettingsBlock( - notificationsEnabled: notificationsEnabled, - onNotificationsChanged: (v) => - setState(() => notificationsEnabled = v), - ), - const SizedBox(height: 24), - ], - ), + ); + }, ); }, ), diff --git a/lib/presentation/screens/promo_code_screen.dart b/lib/presentation/screens/promo_code_screen.dart index d9a510b..7cccae9 100644 --- a/lib/presentation/screens/promo_code_screen.dart +++ b/lib/presentation/screens/promo_code_screen.dart @@ -1,40 +1,45 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import '../../core/app_colors.dart'; +import '../../di/service_locator.dart'; +import '../../domain/usecase/apply_promo_code_usecase.dart'; import '../components/custom_app_bar.dart'; +import '../event/promo_code_event.dart'; +import '../state/promo_code_state.dart'; +import '../viewmodel/promo_code_bloc.dart'; -class PromoCodeScreen extends StatefulWidget { +class PromoCodeScreen extends StatelessWidget { const PromoCodeScreen({super.key}); @override - State createState() => _PromoCodeScreenState(); + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => PromoCodeBloc(getIt()), + child: const _PromoCodeScreenContent(), + ); + } } -class _PromoCodeScreenState extends State { +class _PromoCodeScreenContent extends StatefulWidget { + const _PromoCodeScreenContent(); + + @override + State<_PromoCodeScreenContent> createState() => _PromoCodeScreenContentState(); +} + +class _PromoCodeScreenContentState extends State<_PromoCodeScreenContent> { final TextEditingController promoController = TextEditingController(); - bool isError = false; - void _activatePromo() { - if (promoController.text == 'G17N160') { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Промокод активирован!')), - ); - } else { - setState(() { - isError = true; - }); - } - } - - void _retry() { - setState(() { - isError = false; - promoController.clear(); - }); + @override + void dispose() { + promoController.dispose(); + super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: false, body: Container( decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg), child: SafeArea( @@ -45,109 +50,122 @@ class _PromoCodeScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( children: [ + const SizedBox(height: 16), CustomAppBar(title: 'Промокоды'), const SizedBox(height: 32), - Container( - padding: const EdgeInsets.all(25), - decoration: BoxDecoration( - color: Color(0xFF141530), - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'У вас есть промокод?', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.w600, + // 🔹 LISTENER: Только сайд-эффекты (SnackBar, навигация) + BlocListener( + listener: (context, state) { + if (state.status == PromoCodeStatus.success) { + promoController.clear(); + Future.delayed(const Duration(milliseconds: 800), () { + if (mounted) Navigator.pop(context); + }); + } + }, + child: BlocBuilder( + builder: (context, state) { + final bool hasError = state.status == PromoCodeStatus.failure; + final bool isLoading = state.status == PromoCodeStatus.loading; + + return Container( + padding: const EdgeInsets.all(25), + decoration: BoxDecoration( + color: const Color(0xFF141530), + borderRadius: BorderRadius.circular(16), ), - ), - const SizedBox(height:24), - const Text( - 'Введите промокод и получите скидку на поездку', - style: TextStyle( - color: AppColors.white70, - fontSize: 16, - ), - ), - const SizedBox(height: 20), - TextField( - controller: promoController, - style: TextStyle( - color: isError ? Colors.red : Colors.white, - ), - decoration: InputDecoration( - hintText: 'Введите промокод', - hintStyle: const TextStyle(color: AppColors.white70), - filled: true, - fillColor: Colors.white.withOpacity(0.1), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), - borderSide: BorderSide(color: AppColors.smsDigit, width: 1.5), - ), - ), - ), - const SizedBox(height: 20), - Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () => Navigator.pop(context), - style: OutlinedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), - side: BorderSide(color: AppColors.white70.withOpacity(0.4)), - padding: const EdgeInsets.symmetric(vertical: 12), - ), - child: const Text( - 'Отмена', - style: TextStyle(color: AppColors.white70), - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'У вас есть промокод?', + style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600), ), - ), - const SizedBox(width: 22), - Expanded( - child: ElevatedButton( - onPressed: _activatePromo, - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), - backgroundColor: AppColors.activeButtonGradient.colors[0], - padding: const EdgeInsets.symmetric(vertical: 12), - ), - child: const Text( - 'Активировать', - style: TextStyle(color: AppColors.activeButtonText), - ), + const SizedBox(height: 24), + const Text( + 'Введите промокод и получите скидку на поездку', + style: TextStyle(color: AppColors.white70, fontSize: 16), ), - ), - ], - ), - ], + const SizedBox(height: 20), + + TextField( + controller: promoController, + enabled: !isLoading, + style: TextStyle(color: hasError ? Colors.red : Colors.white), + decoration: InputDecoration( + hintText: 'Введите промокод', + hintStyle: const TextStyle(color: AppColors.white70), + filled: true, + fillColor: Colors.white.withOpacity(0.1), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: BorderSide( + color: hasError ? Colors.red : AppColors.smsDigit, + width: 1.5, + ), + ), + // ✅ Явно убираем ошибку при успехе + errorText: hasError ? 'Неверный промокод' : null, + ), + onSubmitted: (_) => _submit(context), + ), + const SizedBox(height: 20), + + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: isLoading ? null : () => Navigator.pop(context), + style: OutlinedButton.styleFrom( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + side: BorderSide(color: AppColors.white70.withOpacity(0.4)), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: const Text('Отмена', style: TextStyle(color: AppColors.white70)), + ), + ), + const SizedBox(width: 22), + Expanded( + child: ElevatedButton( + onPressed: isLoading ? null : () => _submit(context), + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + backgroundColor: AppColors.activeButtonGradient.colors[0], + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(color: AppColors.activeButtonText, strokeWidth: 2), + ) + : const Text('Активировать', style: TextStyle(color: AppColors.activeButtonText)), + ), + ), + ], + ), + ], + ), + ); + }, ), ), - const Spacer(), ], ), ), ), - - - Image.asset('assets/promo_bottom.png', + Image.asset( + 'assets/promo_bottom.png', width: double.infinity, fit: BoxFit.contain, alignment: Alignment.center, @@ -159,4 +177,10 @@ class _PromoCodeScreenState extends State { ), ); } + + void _submit(BuildContext context) { + final code = promoController.text.trim(); + if (code.isEmpty) return; + context.read().add(PromoCodeApplyRequested(code)); + } } \ No newline at end of file diff --git a/lib/presentation/screens/qr_scan_info_screen.dart b/lib/presentation/screens/qr_scan_info_screen.dart index 972c7d3..21868a6 100644 --- a/lib/presentation/screens/qr_scan_info_screen.dart +++ b/lib/presentation/screens/qr_scan_info_screen.dart @@ -17,6 +17,7 @@ class QRScanInfoScreen extends StatelessWidget { child: SafeArea( child: Column( children: [ + const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: "Сканирование QR-кода"), diff --git a/lib/presentation/screens/qr_scan_screen.dart b/lib/presentation/screens/qr_scan_screen.dart index 6130a8a..11ae6fa 100644 --- a/lib/presentation/screens/qr_scan_screen.dart +++ b/lib/presentation/screens/qr_scan_screen.dart @@ -6,6 +6,7 @@ import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:go_router/go_router.dart'; import 'package:be_happy/di/service_locator.dart'; import 'package:be_happy/domain/usecase/get_scooter_by_title_usecase.dart'; +import '../components/custom_app_bar.dart'; import '../components/gradient_button.dart'; class QrScanScreen extends StatefulWidget { @@ -149,15 +150,17 @@ class _QrScanScreenState extends State { ), ), + // ✅ ИЗМЕНЕНО: прижимаем аппбар к левому краю SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 40), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, // ✅ Выравнивание по левому краю children: [ + const SizedBox(height: 16), + CustomAppBar(title: "Сканирование QR-кода"), const SizedBox(height: 60), const Text( 'Наведите рамку на QR-код — номер будет распознан автоматически', - textAlign: TextAlign.center, + textAlign: TextAlign.center, style: TextStyle( color: Colors.white, fontSize: 16, @@ -170,7 +173,6 @@ class _QrScanScreenState extends State { ], ), ), - ), SafeArea( child: Align( diff --git a/lib/presentation/screens/scooter_code_input_screen.dart b/lib/presentation/screens/scooter_code_input_screen.dart index 6fddc3f..083b059 100644 --- a/lib/presentation/screens/scooter_code_input_screen.dart +++ b/lib/presentation/screens/scooter_code_input_screen.dart @@ -58,6 +58,7 @@ class _ScooterCodeInputScreenState extends State { child: SafeArea( child: Column( children: [ + const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: "Ввод QR-кода"), @@ -86,7 +87,7 @@ class _ScooterCodeInputScreenState extends State { TextField( controller: controller, - keyboardType: TextInputType.number, + keyboardType: TextInputType.text, maxLength: 7, textAlign: TextAlign.left, style: const TextStyle(color: Colors.white, fontSize: 18), diff --git a/lib/presentation/screens/scooter_detail_screen.dart b/lib/presentation/screens/scooter_detail_screen.dart index 8ed64b8..096e93d 100644 --- a/lib/presentation/screens/scooter_detail_screen.dart +++ b/lib/presentation/screens/scooter_detail_screen.dart @@ -70,6 +70,7 @@ class ScooterDetailScreen extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( children: [ + const SizedBox(height: 16), CustomAppBar( title: scooter?.title != null ? 'Самокат ${scooter!.title}' : 'Самокат', ), @@ -101,6 +102,7 @@ class ScooterDetailScreen extends StatelessWidget { context.pushReplacement('/home/tarif-sheet', extra: scooter); }, ), + const SizedBox(height: 30) ], ), ), diff --git a/lib/presentation/screens/send_photo_screen.dart b/lib/presentation/screens/send_photo_screen.dart index af080eb..041ff6c 100644 --- a/lib/presentation/screens/send_photo_screen.dart +++ b/lib/presentation/screens/send_photo_screen.dart @@ -123,6 +123,11 @@ class _SendPhotoViewState extends State { child: SafeArea( child: Column( children: [ + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomAppBar(title: "Отправить фото"), + ), Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 100), child: Text( diff --git a/lib/presentation/screens/splash_screen.dart b/lib/presentation/screens/splash_screen.dart index b4805b4..695b783 100644 --- a/lib/presentation/screens/splash_screen.dart +++ b/lib/presentation/screens/splash_screen.dart @@ -1,13 +1,8 @@ - +import 'dart:math' as math; import 'package:be_happy/presentation/event/spalsh_event.dart'; import 'package:be_happy/presentation/viewmodel/splash_bloc.dart'; import 'package:flutter/material.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 { const SplashScreen({super.key}); @@ -19,35 +14,61 @@ class SplashScreen extends StatefulWidget { class _SplashScreenState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; - late final Animation _revealAnimation; - static const double logoSize = 300; + // Фаза 1: Заполнение цветом слева направо (0.0 -> 0.5) + late final Animation _fillProgress; + + // Фаза 2: Укатывание вправо (0.6 -> 1.0) + late final Animation _rollTranslation; + late final Animation _rollRotation; + + // Уменьшенный размер логотипа по вашему запросу + static const double logoSize = 130; @override void initState() { super.initState(); - // контроллер анимации _controller = AnimationController( vsync: this, - duration: const Duration(milliseconds: 2500), + duration: const Duration(milliseconds: 3000), ); - // анимация движения "затемняющего" прямоугольника - _revealAnimation = Tween(begin: 0.0, end: 1.0).animate( - CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + _fillProgress = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.5, curve: Curves.easeInOut), + ), ); - // запускаем анимацию - _controller.forward().then((_) async { - // небольшая пауза после анимации - await Future.delayed(const Duration(milliseconds: 800)); - if (!mounted) { - return; - } - context.read().add(AuthCheckRequested()); + _rollTranslation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.6, 1.0, curve: Curves.easeInCubic), + ), + ); + + _rollRotation = Tween(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().add(AuthCheckRequested()); + }); }); - } @override @@ -56,69 +77,102 @@ class _SplashScreenState extends State super.dispose(); } - @override @override Widget build(BuildContext context) { + final double screenWidth = MediaQuery.of(context).size.width; + final double endTranslation = screenWidth / 2 + logoSize; + return Scaffold( - backgroundColor: const Color(0xFF3A3A3A), - body: Center( - child: AnimatedBuilder( - animation: _controller, - builder: (context, _) { - final double offset = _revealAnimation.value * (logoSize * 1.2); - - return Stack( - alignment: Alignment.center, - children: [ - // Цветной логотип (на заднем плане) - Image.asset( - 'assets/logo_color.png', - width: logoSize, - height: logoSize, - fit: BoxFit.contain, + body: Stack( + children: [ + Positioned.fill( + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF293A69), + Color(0xFF202741), + ], ), + ), + ), + ), - // Прямоугольник, который "уезжает" вправо, открывая логотип - 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), - ), + // 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( + animation: _controller, + builder: (context, child) { + final double translationX = _rollTranslation.value * endTranslation; + + return Transform( + transform: Matrix4.translationValues(translationX, 0, 0) + ..rotateZ(_rollRotation.value), + alignment: Alignment.center, + // Используем ShaderMask для эффекта заполнения/проявления + child: ShaderMask( + shaderCallback: (bounds) { + 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, + height: logoSize, + fit: BoxFit.contain, ), ), - ), - - // Обводка логотипа (поверх) - Image.asset( - 'assets/logo_outline.png', - width: logoSize * 1.01, - height: logoSize * 1.01, - fit: BoxFit.contain, - ), - ], - ); - }, - ), - ), - - bottomNavigationBar: Padding( - padding: const EdgeInsets.only(bottom: 24), - child: Text( - 'Версия приложения 1.0', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white.withOpacity(0.8), - fontSize: 12, + ); + }, + ), ), - ), + + // 5. Версия приложения + Positioned( + left: 0, + right: 0, + bottom: 24, + child: Text( + 'Версия приложения 1.0', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white.withOpacity(0.6), + fontSize: 12, + fontWeight: FontWeight.w300, + letterSpacing: 0.5, + ), + ), + ), + ], ), ); } - -} +} \ No newline at end of file diff --git a/lib/presentation/screens/subscription_details_screen.dart b/lib/presentation/screens/subscription_details_screen.dart index 6efe23e..4481c3b 100644 --- a/lib/presentation/screens/subscription_details_screen.dart +++ b/lib/presentation/screens/subscription_details_screen.dart @@ -2,9 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import '../../core/app_colors.dart'; import '../components/app_checkbox.dart'; +import '../components/custom_app_bar.dart'; import '../components/gradient_button.dart'; import '../components/period_selector.dart'; +import '../components/content_parser.dart'; // ✅ Парсер контента import '../event/subscription_details_event.dart'; import '../state/susbcription_details_state.dart'; import '../viewmodel/susbcription_details_bloc.dart'; @@ -17,93 +20,123 @@ class SubscriptionDetailsScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFF1A2355), - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios, color: Colors.white), - onPressed: () => context.pop(), - ), - title: BlocBuilder( - builder: (context, state) { - if (state is DetailsContentState) { - return Text(state.subscription.title); - } - return const Text("Загрузка..."); - }, + body: Container( + decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg), + child: SafeArea( + child: Column( + children: [ + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: BlocConsumer< + SubscriptionDetailsBloc, + SubscriptionDetailsState + >( + listenWhen: (previous, current) => + current is DetailsContentState && current.isSuccess, + listener: (context, state) { + if (state is DetailsContentState && state.isSuccess) { + context.pop(true); + } + }, + builder: (context, state) { + String title = "Загрузка..."; + if (state is DetailsContentState) { + title = state.subscription.title; + } + return CustomAppBar(title: title); + }, + ), + ), + const SizedBox(height: 20), + + // 🔹 Контент + Expanded( + child: Stack( + children: [ + // Волна снизу + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Opacity( + opacity: 0.5, + child: Image.asset('assets/wave.png'), + ), + ), + BlocBuilder< + SubscriptionDetailsBloc, + SubscriptionDetailsState + >( + builder: (context, state) { + if (state is DetailsLoading) { + return const Center( + child: CircularProgressIndicator( + color: Color(0xFF80FFD1), + ), + ); + } + if (state is DetailsError) { + return Center( + child: Text( + state.message, + style: const TextStyle(color: Colors.white), + ), + ); + } + if (state is DetailsContentState) { + return _buildContent(context, state); + } + return const SizedBox(); + }, + ), + ], + ), + ), + ], + ), ), ), - body: Stack( - children: [ - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Opacity( - opacity: 0.5, - child: Image.asset('assets/wave.png'), - ), - ), - BlocBuilder( - builder: (context, state) { - if (state is DetailsLoading) { - return const Center( - child: CircularProgressIndicator(color: Color(0xFF80FFD1)), - ); - } - if (state is DetailsError) { - return Center( - child: Text( - state.message, - style: const TextStyle(color: Colors.white), - ), - ); - } - if (state is DetailsContentState) { - return _buildContent(context, state); - } - return const SizedBox(); - }, - ), - ], - ) - ); - - } Widget _buildContent(BuildContext context, DetailsContentState state) { + final bool isAvailableForPurchase = state.subscription.isActive; + return SingleChildScrollView( - padding: const EdgeInsets.all(20.0), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - state.subscription.fullDescription, - style: const TextStyle( - color: Colors.white, - fontSize: 15, - height: 1.5, + // 🔹 Обёртка для растягивания контента на всю ширину + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, // ✅ Растягивает детей на всю ширину + mainAxisSize: MainAxisSize.min, // ✅ Не занимает лишнее место по вертикали + children: ContentParser.parseContent( + textJson: state.subscription.fullDescriptionJson, + text: state.subscription.fullDescription, ), ), - const SizedBox(height: 30), - _ActionCard(state: state), - - const SizedBox(height: 30), - - GradientButton( - text: 'Активировать', - onTap: () => context.read().add( - ActivateSubscriptionPressed(), + if (isAvailableForPurchase) ...[ + const SizedBox(height: 30), + _ActionCard(state: state), + const SizedBox(height: 30), + GradientButton( + text: state.isAlreadyPurchased ? 'Продлить' : 'Активировать', + onTap: () { + context.read().add( + ActivateSubscriptionPressed(), + ); + }, + enabled: state.isAgreed, + width: double.infinity, + height: 56, + fontSize: 16, + showArrows: true, ), - width: double.infinity, - height: 56, - fontSize: 16, - showArrows: true, - ), + ], + const SizedBox(height: 20), ], ), ); @@ -125,9 +158,6 @@ class _ActionCard extends StatelessWidget { state.selectedPeriod, ); - context.read().add( - SelectPeriodEvent(state.subscription.options[selectedIndex])); - return Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( @@ -205,7 +235,7 @@ class _PriceRow extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset("assets/icons/money_icon.png", width: 72, height: 72), - SizedBox(width: 15), + const SizedBox(width: 15), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -224,5 +254,4 @@ class _PriceRow extends StatelessWidget { ), ); } -} - +} \ No newline at end of file diff --git a/lib/presentation/screens/subscription_list_screen.dart b/lib/presentation/screens/subscription_list_screen.dart index c176b95..d290a96 100644 --- a/lib/presentation/screens/subscription_list_screen.dart +++ b/lib/presentation/screens/subscription_list_screen.dart @@ -1,3 +1,4 @@ +import 'package:be_happy/presentation/event/subscription_list_event.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -21,6 +22,7 @@ class SubscriptionsListScreen extends StatelessWidget { child: SafeArea( child: Column( children: [ + const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: 'Абонементы'), @@ -43,17 +45,31 @@ class SubscriptionsListScreen extends StatelessWidget { if (state is SubscriptionsLoading) { return const Center(child: CircularProgressIndicator()); } 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( padding: const EdgeInsets.only(top: 20), itemCount: state.subscriptions.length, itemBuilder: (context, index) { final subscription = state.subscriptions[index]; - final bool isActive = activeIds.contains(subscription.id); - return SubscriptionCard( - subscription: subscription, - isActive: isActive, + final bool isPurchased = subscription.isCurrent; + + final clientSub = clientSubsMap[subscription.id]; + final DateTime? expirationDate = isPurchased ? clientSub?.expiredAt : null; + + return SizedBox( + height: 230, // + child: SubscriptionCard( + subscription: subscription, + isActive: isPurchased, + expiredAt: expirationDate, + onRefresh: () { + context.read().add(LoadSubscriptionsEvent()); + }, + ), ); }, ); diff --git a/lib/presentation/screens/top_up_screen.dart b/lib/presentation/screens/top_up_screen.dart index 8caba4d..6e772c6 100644 --- a/lib/presentation/screens/top_up_screen.dart +++ b/lib/presentation/screens/top_up_screen.dart @@ -21,53 +21,55 @@ class TopUpScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFF1A234E), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - children: [ - const SizedBox(height: 16), - const CustomAppBar(title: 'Пополнение баланса'), - const SizedBox(height: 20), + body: Container( + decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + const SizedBox(height: 16), + const CustomAppBar(title: 'Пополнение баланса'), + const SizedBox(height: 20), - BlocBuilder( - builder: (context, state) { - if (state.isLoading) { - return const Center(child: CircularProgressIndicator()); - } + BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return const Center(child: CircularProgressIndicator()); + } - return Expanded( - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildTariffList(state, context), - const SizedBox(height: 30), - _buildPriceInfo(state), - const SizedBox(height: 40), - const Text( - 'Способ оплаты', - style: TextStyle(color: Colors.white70), - ), - const SizedBox(height: 15), - _buildCardSelector(state, context), - const SizedBox(height: 20), - _buildAgreement(state, context), - const Spacer(), - _buildPayButton(state), - const SizedBox(height: 30), - ], - ), - ], - ), - ); - }, - ), - ], + return Expanded( + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTariffList(state, context), + const SizedBox(height: 30), + _buildPriceInfo(state), + const SizedBox(height: 40), + const Text( + 'Способ оплаты', + style: TextStyle(color: Colors.white70), + ), + const SizedBox(height: 15), + _buildCardSelector(state, context), + const SizedBox(height: 20), + _buildAgreement(state, context), + const Spacer(), + _buildPayButton(state), + const SizedBox(height: 30), + ], + ), + ], + ), + ); + }, + ), + ], + ), ), - ), + ) ), ); } @@ -168,7 +170,7 @@ class TopUpScreen extends StatelessWidget { create: (context) => PaymentMethodSheetBloc(getIt()) ..add(PaymentMethodSheetStarted()), - child: const PaymentMethodSheet(), + child: const PaymentMethodSheet(showBalance: false,), ), ); diff --git a/lib/presentation/state/active_ride_state.dart b/lib/presentation/state/active_ride_state.dart index b04c81a..3f87931 100644 --- a/lib/presentation/state/active_ride_state.dart +++ b/lib/presentation/state/active_ride_state.dart @@ -12,6 +12,8 @@ class ActiveRideState { final double cost; final bool isPaused; final bool inZone; + final double latitude; + final double longitude; const ActiveRideState({ this.status = ActiveRideStatus.initial, @@ -20,6 +22,8 @@ class ActiveRideState { this.elapsedTime = Duration.zero, this.speed = 0.0, this.distance = 0.0, + this.latitude = 0.0, + this.longitude = 0.0, this.cost = 0.0, this.isPaused = false, this.inZone = true, @@ -33,6 +37,8 @@ class ActiveRideState { double? speed, double? distance, double? cost, + double? longitude, + double? latitude, bool? isPaused, bool? inZone, }) { @@ -44,6 +50,8 @@ class ActiveRideState { speed: speed ?? this.speed, distance: distance ?? this.distance, cost: cost ?? this.cost, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, isPaused: isPaused ?? this.isPaused, inZone: inZone ?? this.inZone, ); diff --git a/lib/presentation/state/map_state.dart b/lib/presentation/state/map_state.dart index 3c411a4..1c2a72e 100644 --- a/lib/presentation/state/map_state.dart +++ b/lib/presentation/state/map_state.dart @@ -9,6 +9,8 @@ enum ScooterStatus { initial, loading, success, failure } class ScooterState { final List scooters; + final List reservedScooters; + final Scooter? selectedScooterForFocus; final List zones; final List area; final List areaScooters; @@ -23,6 +25,8 @@ class ScooterState { ScooterState({ this.scooters = const [], + this.reservedScooters = const [], + this.selectedScooterForFocus, this.zones = const [], this.area = const [], this.areaScooters = const [], @@ -38,6 +42,8 @@ class ScooterState { ScooterState copyWith({ List? scooters, + List? reservedScooters, + Scooter? selectedScooterForFocus, List? zones, List? area, List? areaScooters, @@ -52,6 +58,8 @@ class ScooterState { }) { return ScooterState( scooters: scooters ?? this.scooters, + reservedScooters: reservedScooters ?? this.reservedScooters, + selectedScooterForFocus: selectedScooterForFocus ?? this.selectedScooterForFocus, zones: zones ?? this.zones, area: area ?? this.area, areaScooters: areaScooters ?? this.areaScooters, diff --git a/lib/presentation/state/notifications_state.dart b/lib/presentation/state/notifications_state.dart new file mode 100644 index 0000000..ea5728f --- /dev/null +++ b/lib/presentation/state/notifications_state.dart @@ -0,0 +1,27 @@ +import '../../domain/entities/client_notification.dart'; + +enum NotificationsStatus { initial, loading, success, failure } + +class NotificationsState { + final NotificationsStatus status; + final List notifications; + final String? errorMessage; + + const NotificationsState({ + this.status = NotificationsStatus.initial, + this.notifications = const [], + this.errorMessage, + }); + + NotificationsState copyWith({ + NotificationsStatus? status, + List? notifications, + String? errorMessage, + }) { + return NotificationsState( + status: status ?? this.status, + notifications: notifications ?? this.notifications, + errorMessage: errorMessage ?? this.errorMessage, + ); + } +} \ No newline at end of file diff --git a/lib/presentation/state/promo_code_state.dart b/lib/presentation/state/promo_code_state.dart new file mode 100644 index 0000000..f8418f6 --- /dev/null +++ b/lib/presentation/state/promo_code_state.dart @@ -0,0 +1,25 @@ +enum PromoCodeStatus { initial, loading, success, failure } + +class PromoCodeState { + final PromoCodeStatus status; + final double? newBalance; + final String? errorMessage; + + const PromoCodeState({ + this.status = PromoCodeStatus.initial, + this.newBalance, + this.errorMessage, + }); + + PromoCodeState copyWith({ + PromoCodeStatus? status, + double? newBalance, + String? errorMessage, + }) { + return PromoCodeState( + status: status ?? this.status, + newBalance: newBalance ?? this.newBalance, + errorMessage: errorMessage ?? this.errorMessage, + ); + } +} \ No newline at end of file diff --git a/lib/presentation/state/subscription_list_state.dart b/lib/presentation/state/subscription_list_state.dart index 24596a1..1db5045 100644 --- a/lib/presentation/state/subscription_list_state.dart +++ b/lib/presentation/state/subscription_list_state.dart @@ -1,3 +1,5 @@ +import 'package:be_happy/domain/entities/client_subscription.dart'; + import '../../domain/entities/subscription.dart'; abstract class SubscriptionState {} @@ -6,7 +8,7 @@ class SubscriptionsLoading extends SubscriptionState {} class SubscriptionsLoaded extends SubscriptionState { final List subscriptions; - final List activeSubscriptions; + final List activeSubscriptions; SubscriptionsLoaded({ required this.subscriptions, diff --git a/lib/presentation/state/susbcription_details_state.dart b/lib/presentation/state/susbcription_details_state.dart index 1bc7d58..7ce8636 100644 --- a/lib/presentation/state/susbcription_details_state.dart +++ b/lib/presentation/state/susbcription_details_state.dart @@ -15,21 +15,29 @@ class DetailsContentState extends SubscriptionDetailsState { final Subscription subscription; final SubscriptionPeriod selectedPeriod; final bool isAgreed; + final bool isAlreadyPurchased; // ✅ Куплена ли эта подписка сейчас + final bool isSuccess; // ✅ Сигнал для навигатора назад DetailsContentState({ required this.subscription, required this.selectedPeriod, this.isAgreed = false, + this.isAlreadyPurchased = false, + this.isSuccess = false, }); DetailsContentState copyWith({ SubscriptionPeriod? selectedPeriod, bool? isAgreed, + bool? isAlreadyPurchased, + bool? isSuccess, }) { return DetailsContentState( subscription: this.subscription, selectedPeriod: selectedPeriod ?? this.selectedPeriod, isAgreed: isAgreed ?? this.isAgreed, + isAlreadyPurchased: isAlreadyPurchased ?? this.isAlreadyPurchased, + isSuccess: isSuccess ?? this.isSuccess, ); } } \ No newline at end of file diff --git a/lib/presentation/viewmodel/active_ride_bloc.dart b/lib/presentation/viewmodel/active_ride_bloc.dart index 661e95d..b525bc0 100644 --- a/lib/presentation/viewmodel/active_ride_bloc.dart +++ b/lib/presentation/viewmodel/active_ride_bloc.dart @@ -76,6 +76,8 @@ class ActiveRideBloc extends Bloc { cost: orderData?.price ?? 0.0, isPaused: isPaused, inZone: orderData?.zone, + latitude: orderData?.latitude, + longitude: orderData?.longitude, )); _syncTimer?.cancel(); @@ -196,6 +198,8 @@ class ActiveRideBloc extends Bloc { cost: orderData?.price ?? state.cost, isPaused: isPaused, inZone: orderData?.zone, + latitude: orderData?.latitude, + longitude: orderData?.longitude, )); } print("CURRENT STATE $state"); diff --git a/lib/presentation/viewmodel/map_bloc.dart b/lib/presentation/viewmodel/map_bloc.dart index d06169a..72268d0 100644 --- a/lib/presentation/viewmodel/map_bloc.dart +++ b/lib/presentation/viewmodel/map_bloc.dart @@ -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/usecase/check_user_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_notifications_stream_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/scooter.dart'; +import '../../domain/entities/scooter_order.dart'; import '../../domain/entities/zone.dart'; import '../../domain/usecase/get_available_scooters_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 { final GetAvailableScootersUsecase getScootersUsecase; + final GetClientOrdersUsecase getClientOrdersUsecase; final GetAvailableZonesUsecase getAvailableZonesUsecase; final GetMapSettingsUsecase getMapSettingsUsecase; final GetNotificationsStreamUseCase getNotificationsStreamUseCase; @@ -35,6 +38,7 @@ class MapBloc extends Bloc { MapBloc( this.getAvailableZonesUsecase, this.getScootersUsecase, + this.getClientOrdersUsecase, this.getMapSettingsUsecase, this.getNotificationsStreamUseCase, this.getProfileUseCase, @@ -49,6 +53,9 @@ class MapBloc extends Bloc { on(_onFetchProfileData); on(_onCheckUser); on(_onLogoutPressed); + on(_onFocusOnScooter); + on(_onClearMapPlacemarks); + on(_onClearMapFocus); } void startNotificationStream() { @@ -97,11 +104,15 @@ class MapBloc extends Bloc { getScootersUsecase(event.areaScooters, 0, 100), getAvailableZonesUsecase(event.area, 0, 100), getMapSettingsUsecase(), + getClientOrdersUsecase(), ]); final scooters = results[0] as List; final zones = results[1] as List; final settings = results[2] as MapSettings; + final orders = results[3]; + + zones.forEach(print); @@ -127,6 +138,7 @@ class MapBloc extends Bloc { state.copyWith( status: ScooterStatus.success, scooters: scooters, + // reservedScooters: reservedScooters, zones: filteredZones, area: event.area, areaScooters: event.areaScooters, @@ -236,7 +248,9 @@ class MapBloc extends Bloc { try { final profile = await getProfileUseCase(); - emit(state.copyWith(phoneNumber: profile.phone, balance: profile.balance)); + emit( + state.copyWith(phoneNumber: profile.phone, balance: profile.balance), + ); } catch (e) { emit( state.copyWith( @@ -280,4 +294,52 @@ class MapBloc extends Bloc { ); } } + + FutureOr _onFocusOnScooter(FocusOnScooter event, Emitter emit) { + final updatedReserved = List.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 _onClearMapPlacemarks(ClearMapPlacemarks event, Emitter emit) async { + try{ + final orders = await getClientOrdersUsecase(); + + List updatedReservedScooters = []; + + if (orders is Success>) { + 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 _onClearMapFocus(ClearMapFocus event, Emitter emit) { + emit(state.copyWith( + selectedScooterForFocus: null, + )); + } } + + diff --git a/lib/presentation/viewmodel/notifications_bloc.dart b/lib/presentation/viewmodel/notifications_bloc.dart new file mode 100644 index 0000000..1bcec78 --- /dev/null +++ b/lib/presentation/viewmodel/notifications_bloc.dart @@ -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 { + final GetNotificationsUsecase _getNotificationsUsecase; + + NotificationsBloc(this._getNotificationsUsecase) : super(const NotificationsState()) { + on(_onFetchRequested); + } + + Future _onFetchRequested( + NotificationsFetchRequested event, + Emitter 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(), + )); + } + } +} \ No newline at end of file diff --git a/lib/presentation/viewmodel/order_history_bloc.dart b/lib/presentation/viewmodel/order_history_bloc.dart index 5956994..fb7e752 100644 --- a/lib/presentation/viewmodel/order_history_bloc.dart +++ b/lib/presentation/viewmodel/order_history_bloc.dart @@ -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/core/result.dart'; -// 🔹 EVENTS abstract class OrderHistoryEvent {} class OrderHistoryFetchRequested extends OrderHistoryEvent { @@ -13,7 +12,6 @@ class OrderHistoryFetchRequested extends OrderHistoryEvent { class OrderHistoryRefreshRequested extends OrderHistoryEvent {} -// 🔹 STATES enum OrderHistoryStatus { initial, loading, success, failure, empty } class OrderHistoryState { @@ -21,14 +19,14 @@ class OrderHistoryState { final List orders; final String? errorMessage; final int currentPage; - final bool hasMore; + final bool hasMore; // можно оставить, но всегда будет false OrderHistoryState({ required this.status, this.orders = const [], this.errorMessage, this.currentPage = 1, - this.hasMore = true, + this.hasMore = false, // ✅ По умолчанию — нет больше данных }); OrderHistoryState copyWith({ @@ -51,7 +49,8 @@ class OrderHistoryState { class OrderHistoryBloc extends Bloc { final GetScooterOrderHistoryUsecase _usecase; - OrderHistoryBloc(this._usecase) : super(OrderHistoryState(status: OrderHistoryStatus.initial)) { + OrderHistoryBloc(this._usecase) + : super(OrderHistoryState(status: OrderHistoryStatus.initial)) { on(_onFetchRequested); on(_onRefreshRequested); } @@ -64,9 +63,11 @@ class OrderHistoryBloc extends Bloc { 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>) { final orders = result.data; @@ -84,12 +85,10 @@ class OrderHistoryBloc extends Bloc { status: OrderHistoryStatus.success, orders: newOrders, currentPage: event.page, - hasMore: orders.length == 20, + hasMore: false, )); } - } - // ✅ Явно указываем тип Failure - else if (result is Failure>) { + } else if (result is Failure>) { emit(state.copyWith( status: OrderHistoryStatus.failure, errorMessage: result.failure.message ?? 'Неизвестная ошибка', diff --git a/lib/presentation/viewmodel/payment_confirm_bloc.dart b/lib/presentation/viewmodel/payment_confirm_bloc.dart index 826de8e..4ce9f0f 100644 --- a/lib/presentation/viewmodel/payment_confirm_bloc.dart +++ b/lib/presentation/viewmodel/payment_confirm_bloc.dart @@ -89,12 +89,11 @@ class PaymentConfirmBloc event.isBalance, ); - if (result is Success) { + if (result is Success) { emit( state.copyWith( status: PaymentConfirmStatus.success, paymentCompleted: true, - ), ); } else if (result is Failure) { diff --git a/lib/presentation/viewmodel/payment_methods_bloc.dart b/lib/presentation/viewmodel/payment_methods_bloc.dart index 48bca02..7ac3819 100644 --- a/lib/presentation/viewmodel/payment_methods_bloc.dart +++ b/lib/presentation/viewmodel/payment_methods_bloc.dart @@ -70,13 +70,16 @@ class PaymentMethodsBloc extends Bloc PaymentMethodsDeleteCard event, Emitter emit, ) async { - emit(state.copyWith(isDeleting: true)); + emit(state.copyWith( + status: PaymentMethodsStatus.loading, + isDeleting: true, + )); try { final result = await _removePaymentCardUsecase(event.cardId); if (result is Success) { - emit(state.copyWith(isDeleting: false)); + emit(state.copyWith(isDeleting: false, status: PaymentMethodsStatus.success)); add(PaymentMethodsStarted()); } else if (result is Failure) { String errorMessage = 'Не удалось удалить карту'; diff --git a/lib/presentation/viewmodel/promo_code_bloc.dart b/lib/presentation/viewmodel/promo_code_bloc.dart new file mode 100644 index 0000000..50e7e47 --- /dev/null +++ b/lib/presentation/viewmodel/promo_code_bloc.dart @@ -0,0 +1,37 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../domain/usecase/apply_promo_code_usecase.dart'; +import '../event/promo_code_event.dart'; +import '../state/promo_code_state.dart'; + +class PromoCodeBloc extends Bloc { + final ApplyPromoCodeUsecase _applyPromoCodeUsecase; + + PromoCodeBloc(this._applyPromoCodeUsecase) : super(const PromoCodeState()) { + on(_onApplyRequested); + on(_onReset); + } + + Future _onApplyRequested( + PromoCodeApplyRequested event, + Emitter emit, + ) async { + emit(state.copyWith(status: PromoCodeStatus.loading)); + + try { + final result = await _applyPromoCodeUsecase(event.code); + emit(state.copyWith( + status: PromoCodeStatus.success, + newBalance: result.balance, + )); + } catch (e) { + emit(state.copyWith( + status: PromoCodeStatus.failure, + errorMessage: e.toString(), + )); + } + } + + void _onReset(PromoCodeReset event, Emitter emit) { + emit(const PromoCodeState()); + } +} \ No newline at end of file diff --git a/lib/presentation/viewmodel/subscription_list_bloc.dart b/lib/presentation/viewmodel/subscription_list_bloc.dart index eee7195..78a0086 100644 --- a/lib/presentation/viewmodel/subscription_list_bloc.dart +++ b/lib/presentation/viewmodel/subscription_list_bloc.dart @@ -1,4 +1,5 @@ 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/usecase/get_available_subscriptions_usecase.dart'; import 'package:be_happy/domain/usecase/get_client_subscriptions_usecase.dart'; @@ -11,6 +12,7 @@ class SubscriptionListBloc extends Bloc { final GetAvailableSubscriptionsUsecase getAvailableSubscriptionsUsecase; final GetClientSubscriptionsUsecase getClientSubscriptionsUsecase; + SubscriptionListBloc({ required this.getAvailableSubscriptionsUsecase, required this.getClientSubscriptionsUsecase, @@ -26,10 +28,25 @@ class SubscriptionListBloc extends Bloc { final allResult = results[0]; final activeResult = results[1] ; - if (allResult is Success> && activeResult is Success>) { + if (allResult is Success> && activeResult is Success>) { + final availableSubs = allResult.data ?? []; + final clientSubs = activeResult.data ?? []; + + final Map 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( - subscriptions: allResult.data ?? [], - activeSubscriptions: activeResult.data ?? [], + subscriptions: combinedSubsMap.values.toList(), + activeSubscriptions: clientSubs, )); } else { emit(SubscriptionsError("Не удалось загрузить данные из API")); diff --git a/lib/presentation/viewmodel/susbcription_details_bloc.dart b/lib/presentation/viewmodel/susbcription_details_bloc.dart index 68f2265..24d4208 100644 --- a/lib/presentation/viewmodel/susbcription_details_bloc.dart +++ b/lib/presentation/viewmodel/susbcription_details_bloc.dart @@ -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: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 '../state/susbcription_details_state.dart'; -class SubscriptionDetailsBloc extends Bloc { +class SubscriptionDetailsBloc + extends Bloc { final GetSubscriptionByIdUsecase getSubscriptionByIdUsecase; final ActivateSubscriptionUsecase activateSubscriptionUsecase; + final GetClientSubscriptionsUsecase getClientSubscriptionsUsecase; - SubscriptionDetailsBloc(this.getSubscriptionByIdUsecase, - this.activateSubscriptionUsecase) : super(DetailsLoading()) { + SubscriptionDetailsBloc( + this.getSubscriptionByIdUsecase, + this.activateSubscriptionUsecase, + this.getClientSubscriptionsUsecase, + ) : super(DetailsLoading()) { on((event, emit) async { emit(DetailsLoading()); try { - final result = await getSubscriptionByIdUsecase(event.subscriptionId); - switch (result) { + final clientSubsResult = await getClientSubscriptionsUsecase(); + bool isPurchased = false; - case Success(): - final sub = result.data; - - if (sub == null) return; - - emit(DetailsContentState( - subscription: sub, - selectedPeriod: sub.options.first, - )); - case Failure(): - emit(DetailsError("Ошибка при запросе данных")); + switch (clientSubsResult) { + case Success>(): + isPurchased = clientSubsResult.data?.any( + (element) => element.subscriptionId == event.subscriptionId, + ) ?? false; + break; + case Failure>(): + isPurchased = false; + break; } + switch (result) { + case Success(): + final sub = result.data; + if (sub == null) return; + emit( + DetailsContentState( + subscription: sub, + selectedPeriod: sub.options.first, + isAlreadyPurchased: isPurchased, + ), + ); + break; + case Failure(): + emit(DetailsError("Ошибка при запросе данных")); + break; + } } catch (e) { emit(DetailsError("Не удалось загрузить данные")); } @@ -43,7 +64,9 @@ class SubscriptionDetailsBloc extends Bloc((event, emit) { if (state is DetailsContentState) { - emit((state as DetailsContentState).copyWith(selectedPeriod: event.period)); + emit( + (state as DetailsContentState).copyWith(selectedPeriod: event.period), + ); } }); @@ -53,14 +76,23 @@ class SubscriptionDetailsBloc extends Bloc((event, emit) { - switch(state) { + on((event, emit) async { + switch (state) { 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; default: break; } }); } -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index 5baa7da..2543c04 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: "direct main" description: name: async - sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.13.1" + version: "2.13.0" bloc: dependency: transitive description: @@ -125,10 +125,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.9" + version: "1.0.8" device_info_plus: dependency: "direct main" description: @@ -205,10 +205,10 @@ packages: dependency: transitive description: name: file_selector_macos - sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" url: "https://pub.dev" source: hosted - version: "0.9.5" + version: "0.9.4+4" file_selector_platform_interface: dependency: transitive description: @@ -274,10 +274,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" + sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476 url: "https://pub.dev" source: hosted - version: "2.0.34" + version: "2.0.31" flutter_secure_storage: dependency: "direct main" description: @@ -436,18 +436,18 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac" + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.1" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f + sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e" url: "https://pub.dev" source: hosted - version: "0.8.13+17" + version: "0.8.13+1" image_picker_for_web: dependency: transitive description: @@ -460,10 +460,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad" + sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e url: "https://pub.dev" source: hosted - version: "0.8.13+3" + version: "0.8.13" image_picker_linux: dependency: transitive description: @@ -476,10 +476,10 @@ packages: dependency: transitive description: name: image_picker_macos - sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 url: "https://pub.dev" source: hosted - version: "0.2.2+1" + version: "0.2.2" image_picker_platform_interface: dependency: transitive description: @@ -504,22 +504,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" - jni: - dependency: transitive - description: - name: jni - sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f - url: "https://pub.dev" - source: hosted - version: "1.0.0" - jni_flutter: - dependency: transitive - description: - name: jni_flutter - sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" - url: "https://pub.dev" - source: hosted - version: "1.0.1" js: dependency: transitive description: @@ -532,34 +516,34 @@ packages: dependency: transitive description: name: json_annotation - sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.11.0" + version: "4.9.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "11.0.2" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.10" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.1" lints: dependency: transitive description: @@ -604,10 +588,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" mime: dependency: "direct main" description: @@ -632,14 +616,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - package_config: - dependency: transitive - description: - name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" - source: hosted - version: "2.2.0" path: dependency: "direct main" description: @@ -660,18 +636,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.2.19" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.4.2" path_provider_linux: dependency: transitive description: @@ -740,26 +716,26 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.5.5" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e url: "https://pub.dev" source: hosted - version: "2.4.23" + version: "2.4.13" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: @@ -772,10 +748,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.1" shared_preferences_web: dependency: transitive description: @@ -801,10 +777,10 @@ packages: dependency: transitive description: name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.2" + version: "1.10.1" stack_trace: dependency: transitive description: @@ -841,10 +817,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.4" typed_data: dependency: transitive description: @@ -865,18 +841,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" + sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e" url: "https://pub.dev" source: hosted - version: "6.3.29" + version: "6.3.20" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 url: "https://pub.dev" source: hosted - version: "6.3.6" + version: "6.3.4" url_launcher_linux: dependency: transitive description: @@ -889,10 +865,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f url: "https://pub.dev" source: hosted - version: "3.2.5" + version: "3.2.3" url_launcher_platform_interface: dependency: transitive description: @@ -929,18 +905,18 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.1.4" vm_service: dependency: transitive description: name: vm_service - sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "15.2.0" + version: "15.0.0" web: dependency: transitive description: @@ -998,5 +974,5 @@ packages: source: hosted version: "4.2.1" sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.6" + dart: ">=3.8.1 <4.0.0" + flutter: ">=3.32.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4589644..afa186d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: be_happy description: "" publish_to: 'none' -version: 1.0.0+11 +version: 1.0.0+12 environment: sdk: ^3.8.1 @@ -54,6 +54,11 @@ flutter: - assets/ - assets/icons/ + fonts: + - family: "DigitalNumbers" + fonts: + - asset: assets/fonts/DigitalNumbers-Regular.ttf + # fonts: # - family: Schyler