diff --git a/lib/data/service/app_setting_service.dart b/lib/data/service/app_setting_service.dart index 372b6df..5787971 100644 --- a/lib/data/service/app_setting_service.dart +++ b/lib/data/service/app_setting_service.dart @@ -10,6 +10,6 @@ class AppSettingsService { } bool getMapSettingsFlag(String key) { - return sharedPreferences.getBool(key) ?? false; + return sharedPreferences.getBool(key) ?? true; } } diff --git a/lib/domain/entities/map_settings.dart b/lib/domain/entities/map_settings.dart index 0dcc908..741a1fd 100644 --- a/lib/domain/entities/map_settings.dart +++ b/lib/domain/entities/map_settings.dart @@ -6,10 +6,10 @@ class MapSettings { final bool restricted_driving_zones; MapSettings({ - required this.all_placemarks, - required this.all_zones, - required this.parking_zones, - required this.restricted_parking_zones, - required this.restricted_driving_zones, + this.all_placemarks = true, + this.all_zones = true, + this.parking_zones = true, + this.restricted_parking_zones = true, + this.restricted_driving_zones = true, }); } diff --git a/lib/presentation/components/sheet/map_settings_sheet.dart b/lib/presentation/components/sheet/map_settings_sheet.dart index 649e493..6711326 100644 --- a/lib/presentation/components/sheet/map_settings_sheet.dart +++ b/lib/presentation/components/sheet/map_settings_sheet.dart @@ -73,7 +73,6 @@ class MapSettingsSheet extends StatelessWidget { TextButton( onPressed: () { context.read().add(ApllyButtonClick()); - context.read().add(UpdateMap()); Navigator.of(context).pop(); }, child: const Text( diff --git a/lib/presentation/components/sheet/reserved_ride_sheet.dart b/lib/presentation/components/sheet/reserved_ride_sheet.dart index 8a7748a..6bf37ab 100644 --- a/lib/presentation/components/sheet/reserved_ride_sheet.dart +++ b/lib/presentation/components/sheet/reserved_ride_sheet.dart @@ -3,8 +3,10 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../di/service_locator.dart'; +import '../../event/map_event.dart'; import '../../event/reserved_ride_event.dart'; import '../../state/reserved_ride_state.dart'; +import '../../viewmodel/map_bloc.dart'; import '../../viewmodel/reserved_ride_bloc.dart'; import '../dialog/cancel_booking_dialog.dart'; import '../gradient_button.dart'; @@ -125,7 +127,6 @@ class _ReservedRideSheetState extends State { const SizedBox(height: 20), - // ТАЙМЕР + ИНФО О САМОКАТЕ (КОМПАКТНЫЙ) Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( @@ -227,6 +228,7 @@ class _ReservedRideSheetState extends State { listener: (context, state) { if (state.rideStarted) { Navigator.pop(context); + context.read().add(FetchActiveOrders()); showModalBottomSheet( context: context, isScrollControlled: true, diff --git a/lib/presentation/components/sheet/tariff_sheet.dart b/lib/presentation/components/sheet/tariff_sheet.dart index 4c83e03..a7f567e 100644 --- a/lib/presentation/components/sheet/tariff_sheet.dart +++ b/lib/presentation/components/sheet/tariff_sheet.dart @@ -15,9 +15,11 @@ import '../../../domain/usecase/add_payment_card_usecase.dart'; import '../../../domain/usecase/get_payment_cards_usecase.dart'; import '../../../domain/usecase/get_profile_usecase.dart'; import '../../../domain/usecase/set_main_payment_card_usecase.dart'; +import '../../event/map_event.dart'; import '../../event/payment_method_sheet_event.dart'; import '../../event/tariff_sheet_event.dart'; import '../../state/tariff_sheet_state.dart'; +import '../../viewmodel/map_bloc.dart'; import '../../viewmodel/tariff_sheet_bloc.dart'; import '../gradient_button.dart'; import '../scooter/mini_battery_indicator.dart'; @@ -62,6 +64,14 @@ class _TariffSheetState extends State { } return BlocConsumer( listener: (context, state) { + if (state.status == TariffSheetStatus.success && + state.tariffs.isNotEmpty && + _selectedTariffIndex == null) { + setState(() { + _selectedTariffIndex = 0; + }); + } + if (state.status == TariffSheetStatus.failure && state.errorMessage != null) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( @@ -82,6 +92,8 @@ class _TariffSheetState extends State { } }, builder: (context, state) { + final selectedIndex = _selectedTariffIndex ?? (state.tariffs.isNotEmpty ? 0 : null); + if (state.status == TariffSheetStatus.loading) { return Align( alignment: Alignment.bottomCenter, @@ -283,7 +295,7 @@ class _TariffSheetState extends State { 'Далее ${tariff.drivePrice.toStringAsFixed(2)} ${tariff.currency}/мин.', 'Минута на паузе ${tariff.pausePrice.toStringAsFixed(2)} ${tariff.currency}', ], - isSelected: _selectedTariffIndex == index, + isSelected: selectedIndex == index, isInsurance: state.isInsurance, tariff: tariff, onTap: () { @@ -412,19 +424,20 @@ class _TariffSheetState extends State { height: 56, width: double.infinity, fontSize: 16, - enabled: _selectedTariffIndex != null && (state.selectedCard != null || state.useBalance), - onTap: (_selectedTariffIndex != null && (state.selectedCard != null || state.useBalance)) + enabled: selectedIndex != null && (state.selectedCard != null || state.useBalance), + onTap: (selectedIndex != null && (state.selectedCard != null || state.useBalance)) ? () { context.read().add( BookScooterPressed( widget.scooter.id, - state.tariffs[_selectedTariffIndex!].id, - state.tariffs[_selectedTariffIndex!].subscriptionId, + state.tariffs[selectedIndex].id, + state.tariffs[selectedIndex].subscriptionId, state.useBalance ? null : state.selectedCard?.id, state.useBalance, state.isInsurance ) ); + context.read().add(FetchActiveOrders()); context.pushReplacement('/home/current-rides-sheet'); } : null, diff --git a/lib/presentation/event/map_event.dart b/lib/presentation/event/map_event.dart index 1897ae6..1be16b4 100644 --- a/lib/presentation/event/map_event.dart +++ b/lib/presentation/event/map_event.dart @@ -38,6 +38,8 @@ class FocusOnScooter extends ScooterEvent { class ClearMapPlacemarks extends ScooterEvent {} class ClearMapFocus extends ScooterEvent {} +class FetchActiveOrders extends ScooterEvent {} + diff --git a/lib/presentation/screens/map_screen.dart b/lib/presentation/screens/map_screen.dart index 3341b44..37085b0 100644 --- a/lib/presentation/screens/map_screen.dart +++ b/lib/presentation/screens/map_screen.dart @@ -71,10 +71,10 @@ class _MapScreenState extends State { BlocConsumer( listenWhen: (previous, current) { return current.lastNotification != - previous.lastNotification || + previous.lastNotification || current.flags != previous.flags || - previous.selectedScooterForFocus?.id - != current.selectedScooterForFocus?.id; + previous.selectedScooterForFocus?.id != + current.selectedScooterForFocus?.id; }, listener: (context, state) { @@ -176,7 +176,8 @@ class _MapScreenState extends State { final reservedScooters = _buildScooterPlacemarks( scooters: state.reservedScooters ?? [], - iconAsset: 'assets/icons/scooter_reserved_placemark_fill.png', + iconAsset: + 'assets/icons/scooter_reserved_placemark_fill.png', isClickable: false, ); @@ -193,10 +194,10 @@ class _MapScreenState extends State { }, onCameraPositionChanged: (cameraPosition, reason, finished) { - if (finished) { - _fetchScooters(); - } - }, + if (finished) { + _fetchScooters(); + } + }, mapObjects: [ ...zonePolygons, ...reservedScooters, @@ -210,7 +211,7 @@ class _MapScreenState extends State { final clusteredPlacemarks = cluster.placemarks; final filtered = state.scooters.where((scooter) { return clusteredPlacemarks.any( - (pm) => pm.mapId.value == scooter.id.toString(), + (pm) => pm.mapId.value == scooter.id.toString(), ); }).toList(); _onMarkerTap(filtered); @@ -241,7 +242,7 @@ class _MapScreenState extends State { BlocBuilder( buildWhen: (previous, current) => - previous.status != current.status, + previous.status != current.status, builder: (context, state) { if (state.status == ScooterStatus.loading) { return const Positioned( @@ -368,6 +369,7 @@ class _MapScreenState extends State { } }); } + Future _moveCameraToPoint( double lat, double lon, { @@ -401,17 +403,13 @@ class _MapScreenState extends State { } if (flags.hasUnpaidOrder) { _showExistingNotification( - child: UnpaidOrderNotificationCard( - onClose: () => BotToast.cleanAll(), - ), + child: UnpaidOrderNotificationCard(onClose: () => BotToast.cleanAll()), ); return; } if (flags.hasFine) { _showExistingNotification( - child: FineNotificationCard( - onClose: () => BotToast.cleanAll(), - ), + child: FineNotificationCard(onClose: () => BotToast.cleanAll()), ); return; } @@ -567,7 +565,8 @@ class _MapScreenState extends State { }) { return scooters.map((scooter) { return PlacemarkMapObject( - mapId: MapObjectId('${isClickable ? "" : "reserved_"}${scooter.id}'), // уникальный ID для карты + mapId: MapObjectId('${isClickable ? "" : "reserved_"}${scooter.id}'), + // уникальный ID для карты point: Point(latitude: scooter.longitude, longitude: scooter.latitude), icon: PlacemarkIcon.single( PlacemarkIconStyle( @@ -595,7 +594,9 @@ class _MapScreenState extends State { for (var polygonData in zone.geometry) { if (polygonData.isEmpty) continue; - var outerPoints = polygonData[0].map((p) => Point(latitude: p.latitude, longitude: p.longitude)).toList(); + var outerPoints = polygonData[0] + .map((p) => Point(latitude: p.latitude, longitude: p.longitude)) + .toList(); var cleanPoints = []; for (var p in outerPoints) { @@ -619,12 +620,14 @@ class _MapScreenState extends State { PolygonMapObject( mapId: const MapObjectId('global_inverse_mask'), polygon: Polygon( - outerRing: const LinearRing(points: [ - Point(latitude: 85, longitude: -179.9), - Point(latitude: 85, longitude: 179.9), - Point(latitude: -85, longitude: 179.9), - Point(latitude: -85, longitude: -179.9), - ]), + outerRing: const LinearRing( + points: [ + Point(latitude: 85, longitude: -179.9), + Point(latitude: 85, longitude: 179.9), + Point(latitude: -85, longitude: 179.9), + Point(latitude: -85, longitude: -179.9), + ], + ), innerRings: driveOuterRings, // Вырезаем только зоны Drive ), strokeWidth: 0, @@ -641,13 +644,17 @@ class _MapScreenState extends State { // Стилизация в зависимости от типа зоны if (zone.type == "Drive") { borderColor = const Color(0xFF5ECD4C); // Зеленый бордюр - fillColor = Colors.transparent; // Прозрачная внутри маски + fillColor = Colors.transparent; // Прозрачная внутри маски } else if (zone.type == "NotDrive") { borderColor = const Color(0xFFEF4444); // Красный бордюр - fillColor = const Color(0xFFEF4444).withOpacity(0.3); // Красная полупрозрачная заливка для наглядности внутри Drive + fillColor = const Color(0xFFEF4444).withOpacity( + 0.3, + ); // Красная полупрозрачная заливка для наглядности внутри Drive } else { borderColor = const Color(0xFFA78BFA); // Фиолетовый бордюр (Парковка) - fillColor = const Color(0xFFA78BFA).withOpacity(0.25); // Фиолетовая заливка + fillColor = const Color( + 0xFFA78BFA, + ).withOpacity(0.25); // Фиолетовая заливка } int polyIndex = 0; @@ -655,7 +662,9 @@ class _MapScreenState extends State { if (polygonData.isEmpty) continue; // Строим внешнее кольцо текущего полигона - var outerPoints = polygonData[0].map((p) => Point(latitude: p.latitude, longitude: p.longitude)).toList(); + var outerPoints = polygonData[0] + .map((p) => Point(latitude: p.latitude, longitude: p.longitude)) + .toList(); if (outerPoints.first != outerPoints.last) { outerPoints.add(outerPoints.first); } @@ -664,7 +673,9 @@ class _MapScreenState extends State { // Строим внутренние кольца (дыры), если они есть у этого полигона List innerRings = []; for (int i = 1; i < polygonData.length; i++) { - var innerPoints = polygonData[i].map((p) => Point(latitude: p.latitude, longitude: p.longitude)).toList(); + var innerPoints = polygonData[i] + .map((p) => Point(latitude: p.latitude, longitude: p.longitude)) + .toList(); if (innerPoints.isNotEmpty) { if (innerPoints.first != innerPoints.last) { innerPoints.add(innerPoints.first); @@ -677,10 +688,7 @@ class _MapScreenState extends State { objects.add( PolygonMapObject( mapId: MapObjectId('zone_poly_${zone.type}_${zone.id}_$polyIndex'), - polygon: Polygon( - outerRing: outerRing, - innerRings: innerRings, - ), + polygon: Polygon(outerRing: outerRing, innerRings: innerRings), strokeColor: borderColor, strokeWidth: 2.0, fillColor: fillColor, @@ -707,7 +715,7 @@ class _MapScreenState extends State { onPressed: () { context.read().add(FetchProfileData()); Scaffold.of(innerContext).openDrawer(); - }, + }, ), ), ), @@ -721,10 +729,21 @@ class _MapScreenState extends State { onPressed: () => context.push("/home/notifications"), ), const SizedBox(height: 12), - _RoundButton( - imagePath: 'assets/icons/scooter_placemark.png', - onPressed: () => context.push("/home/current-rides-sheet"), - ) + BlocBuilder( + buildWhen: (previous, current) => + previous.activeRidesCount != current.activeRidesCount, + builder: (context, state) { + return _RoundButton( + imagePath: 'assets/icons/scooter_placemark.png', + count: state.activeRidesCount ?? 0, + onPressed: () { + context.push("/home/current-rides-sheet").then((_) { + context.read().add(FetchActiveOrders()); + }); + }, + ); + }, + ), ], ), ), @@ -765,9 +784,7 @@ class _MapScreenState extends State { } if (flags.hasFine) { _showExistingNotification( - child: FineNotificationCard( - onClose: () => BotToast.cleanAll(), - ), + child: FineNotificationCard(onClose: () => BotToast.cleanAll()), ); return; } @@ -870,32 +887,63 @@ class _RoundIconButton extends StatelessWidget { class _RoundButton extends StatelessWidget { final String imagePath; final VoidCallback onPressed; + final int count; const _RoundButton({ required this.imagePath, required this.onPressed, + this.count = 0, }); @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, + return GestureDetector( + onTap: onPressed, + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.darkBlue, + borderRadius: BorderRadius.circular(12), + ), + child: Center( + child: Image.asset( + imagePath, + width: 20, + height: 20, + fit: BoxFit.contain, + color: Colors.white, + ), + ), ), - ), + if (count > 0) + Positioned( + top: -4, + right: -4, + child: Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: Colors.redAccent, + shape: BoxShape.circle, + ), + constraints: const BoxConstraints(minWidth: 18, minHeight: 18), + child: Center( + child: Text( + '$count', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ), + ), + ], ), ); } @@ -929,10 +977,12 @@ void _showExistingNotification({required Widget child}) { duration: null, // не исчезает, пока пользователь не закроет toastBuilder: (_) { return Container( - margin: const EdgeInsets.only(top: 120), // тот же отступ, что в listener + margin: const EdgeInsets.only(top: 120), + // тот же отступ, что в listener child: Material( color: Colors.transparent, - child: child, // PaymentNotificationCard / UnpaidOrderNotificationCard / FineNotificationCard + child: + child, // PaymentNotificationCard / UnpaidOrderNotificationCard / FineNotificationCard ), ); }, diff --git a/lib/presentation/screens/support_screen.dart b/lib/presentation/screens/support_screen.dart index 967d790..495b59a 100644 --- a/lib/presentation/screens/support_screen.dart +++ b/lib/presentation/screens/support_screen.dart @@ -30,13 +30,13 @@ class SupportScreen extends StatelessWidget { ), const Divider(height: 1, color: Colors.white24), const SizedBox(height: 12), - LinkRow( + /*LinkRow( icon: 'assets/icons/whatsapp.png', title: 'WhatsApp', onTap: () => openLink('https://wa.me/375257110909'), ), const Divider(height: 1, color: Colors.white24), - const SizedBox(height: 12), + const SizedBox(height: 12),*/ LinkRow( icon: 'assets/icons/viber.png', title: 'Viber', diff --git a/lib/presentation/state/map_settings_modal_state.dart b/lib/presentation/state/map_settings_modal_state.dart index 853ef76..825e637 100644 --- a/lib/presentation/state/map_settings_modal_state.dart +++ b/lib/presentation/state/map_settings_modal_state.dart @@ -6,11 +6,11 @@ class MapSettingsModalState { final bool isRestrictedParkingZoneActive; MapSettingsModalState({ - required this.isAllGeomarksActive, - required this.isAllGeozonesActive, - required this.isRestrictedDrivingZoneActive, - required this.isParkingZoneActive, - required this.isRestrictedParkingZoneActive, + this.isAllGeomarksActive = true, + this.isAllGeozonesActive = true, + this.isRestrictedDrivingZoneActive = true, + this.isParkingZoneActive = true, + this.isRestrictedParkingZoneActive = true, }); MapSettingsModalState copyWith({ diff --git a/lib/presentation/state/map_state.dart b/lib/presentation/state/map_state.dart index 2a2f015..03c29d4 100644 --- a/lib/presentation/state/map_state.dart +++ b/lib/presentation/state/map_state.dart @@ -22,6 +22,7 @@ class ScooterState { final double balance; final UserCheckFlags flags; final ClientNotification? lastNotification; + final int activeRidesCount; ScooterState({ this.scooters = const [], @@ -38,6 +39,7 @@ class ScooterState { this.balance = 999, this.flags = const UserCheckFlags(hasFine: false, hasUnpaidOrder: false, hasCard: false), this.lastNotification, + this.activeRidesCount = 0, }); ScooterState copyWith({ @@ -55,6 +57,7 @@ class ScooterState { double? balance, UserCheckFlags? flags, ClientNotification? lastNotification, + int? activeRidesCount, }) { return ScooterState( scooters: scooters ?? this.scooters, @@ -71,6 +74,7 @@ class ScooterState { balance: balance ?? this.balance, flags: flags ?? this.flags, lastNotification: lastNotification ?? this.lastNotification, + activeRidesCount: activeRidesCount ?? this.activeRidesCount, ); } } diff --git a/lib/presentation/state/tariff_sheet_state.dart b/lib/presentation/state/tariff_sheet_state.dart index 34acce2..1d61b03 100644 --- a/lib/presentation/state/tariff_sheet_state.dart +++ b/lib/presentation/state/tariff_sheet_state.dart @@ -17,7 +17,7 @@ class TariffSheetState { this.status = TariffSheetStatus.initial, this.tariffs = const [], this.selectedCard, - this.isInsurance = false, + this.isInsurance = true, this.errorMessage, this.userBalance = 0, this.useBalance = false, diff --git a/lib/presentation/viewmodel/map_bloc.dart b/lib/presentation/viewmodel/map_bloc.dart index 5cbb60b..1d24077 100644 --- a/lib/presentation/viewmodel/map_bloc.dart +++ b/lib/presentation/viewmodel/map_bloc.dart @@ -34,6 +34,7 @@ class MapBloc extends Bloc { final LogoutUseCase logoutUseCase; final SplashBloc splashBloc; StreamSubscription? _notificationSubscription; + Timer? _ordersTimer; Future>? _zonesFuture; @@ -58,6 +59,7 @@ class MapBloc extends Bloc { on(_onFocusOnScooter); on(_onClearMapPlacemarks); on(_onClearMapFocus); + on(_onFetchActiveOrders); } Future> _getZones() async { @@ -344,4 +346,50 @@ class MapBloc extends Bloc { selectedScooterForFocus: null, )); } + + void _startOrdersTimer() { + _ordersTimer?.cancel(); + // Опрашиваем раз в 15 секунд, если есть активные поездки + _ordersTimer = Timer.periodic(const Duration(seconds: 15), (_) { + add(FetchActiveOrders()); + }); + } + + void _stopOrdersTimer() { + _ordersTimer?.cancel(); + _ordersTimer = null; + } + + Future _onFetchActiveOrders( + FetchActiveOrders event, + Emitter emit, + ) async { + try { + final result = await getClientOrdersUsecase(); + if (result is Success>) { + final activeOrders = result.data ?? []; + final count = activeOrders.length; + + // Управляем таймером в зависимости от наличия активных поездок + if (count > 0 && (_ordersTimer == null || !_ordersTimer!.isActive)) { + _startOrdersTimer(); + } else if (count == 0) { + _stopOrdersTimer(); + } + + emit(state.copyWith( + activeRidesCount: count, + reservedScooters: activeOrders.map((e) => e.scooter).toList(), + )); + } + } catch (e) { + print("Error fetching active orders: $e"); + } + } + + @override + Future close() { + _stopOrdersTimer(); + return super.close(); + } } \ No newline at end of file diff --git a/lib/presentation/viewmodel/map_settings_modal_bloc.dart b/lib/presentation/viewmodel/map_settings_modal_bloc.dart index fa70d36..b408cee 100644 --- a/lib/presentation/viewmodel/map_settings_modal_bloc.dart +++ b/lib/presentation/viewmodel/map_settings_modal_bloc.dart @@ -15,13 +15,7 @@ class MapSettingsModalBloc extends Bloc(_onAllGeozonesToggled); on(_onAllGeomarksToggled); on(_onParkingZonesToggled); diff --git a/pubspec.yaml b/pubspec.yaml index 0f9859b..1311745 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: be_happy description: "" publish_to: 'none' -version: 1.0.3+1 +version: 1.0.4+1 environment: sdk: ^3.8.1