import 'dart:async'; import 'package:be_happy/core/result.dart'; 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_all_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'; import 'package:be_happy/presentation/viewmodel/splash_bloc.dart'; 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'; import '../event/map_event.dart'; import '../event/spalsh_event.dart'; import '../state/map_state.dart'; import 'package:maps_toolkit/maps_toolkit.dart' as mt; class MapBloc extends Bloc { final GetAvailableScootersUsecase getScootersUsecase; final GetClientOrdersUsecase getClientOrdersUsecase; final GetAllZonesUsecase getAllZonesUsecase; final GetMapSettingsUsecase getMapSettingsUsecase; final GetNotificationsStreamUseCase getNotificationsStreamUseCase; final GetProfileUseCase getProfileUseCase; final CheckUserUseCase checkUserUseCase; final LogoutUseCase logoutUseCase; final SplashBloc splashBloc; StreamSubscription? _notificationSubscription; Timer? _ordersTimer; Future>? _zonesFuture; MapBloc( this.getAllZonesUsecase, this.getScootersUsecase, this.getClientOrdersUsecase, this.getMapSettingsUsecase, this.getNotificationsStreamUseCase, this.getProfileUseCase, this.checkUserUseCase, this.logoutUseCase, this.splashBloc, ) : super(ScooterState(isGeomarksShowed: true)) { on(_onFetchScooters); on(_onUpdateMap); on(_onUpdateUserLocation); on(_onNotificationReceived); on(_onFetchProfileData); on(_onCheckUser); on(_onLogoutPressed); on(_onFocusOnScooter); on(_onClearMapPlacemarks); on(_onClearMapFocus); on(_onFetchActiveOrders); } Future> _getZones() async { _zonesFuture ??= getAllZonesUsecase(); try { return await _zonesFuture!; } catch (e) { // Не кешируем неудачную попытку — иначе сбой первого запроса // (сеть/авторизация) навсегда "выключает" зоны до перезапуска // приложения. Следующий FetchScooters/UpdateMap попробует заново. _zonesFuture = null; rethrow; } } List _filterZones(List zones, MapSettings settings) { List filteredZones = []; if (settings.all_zones) { if (settings.parking_zones) { filteredZones.addAll(zones.where((el) => el.type == "Finish")); } if (settings.restricted_parking_zones) { filteredZones.addAll(zones.where((el) => el.type == "Drive")); } if (settings.restricted_driving_zones) { filteredZones.addAll(zones.where((el) => el.type == "NotDrive")); } } return filteredZones; } void startNotificationStream() { _notificationSubscription?.cancel(); _notificationSubscription = getNotificationsStreamUseCase().listen( (notification) { add(NotificationReceived(notification)); }, onError: (error) { print(" SSE BLOC ERROR: $error"); _handleReconnect(); }, onDone: () { print(" SSE Stream closed by server (onDone)"); _handleReconnect(); }, cancelOnError: true, ); } void _handleReconnect() { if (isClosed) return; Future.delayed(const Duration(seconds: 5), () { if (!isClosed) { print(" Attempting to reconnect to SSE..."); startNotificationStream(); } }); } void stopNotificationStream() { _notificationSubscription?.cancel(); _notificationSubscription = null; } Future _onFetchScooters( FetchScooters event, Emitter emit, ) async { emit(state.copyWith(status: ScooterStatus.loading)); try { final results = await Future.wait([ getScootersUsecase(event.areaScooters, 0, 100), _getZones(), getMapSettingsUsecase(), getClientOrdersUsecase(), ]); final scooters = results[0] as List; final zones = results[1] as List; final settings = results[2] as MapSettings; final orders = results[3]; final filteredZones = _filterZones(zones, settings); print( " FETCH: filteredZones.length = ${filteredZones.length}, all_zones=${settings.all_zones}", ); emit( state.copyWith( status: ScooterStatus.success, scooters: scooters, // reservedScooters: reservedScooters, zones: filteredZones, area: event.area, areaScooters: event.areaScooters, isGeomarksShowed: settings.all_placemarks, ), ); } catch (e) { print("FETCH ERROR: $e"); emit( state.copyWith( status: ScooterStatus.failure, errorMessage: e.toString(), ), ); } } Future _onUpdateMap(UpdateMap event, Emitter emit) async { emit(state.copyWith(status: ScooterStatus.loading)); try { final results = await Future.wait([ getScootersUsecase(state.areaScooters, 0, 100), _getZones(), getMapSettingsUsecase(), ]); final scooters = results[0] as List; final zones = results[1] as List; final settings = results[2] as MapSettings; final filteredZones = _filterZones(zones, settings); print( " UPDATE MAP: filteredZones.length = ${filteredZones.length}, all_zones=${settings.all_zones}", ); emit( state.copyWith( status: ScooterStatus.success, scooters: scooters, zones: filteredZones, isGeomarksShowed: settings.all_placemarks, ), ); } catch (e) { print("UPDATE ERROR: $e"); emit( state.copyWith( status: ScooterStatus.failure, errorMessage: e.toString(), ), ); } } FutureOr _onUpdateUserLocation( UpdateUserLocation event, Emitter emit, ) { print("USER LOCATION UPDATED EVENT start"); state.zones.forEach((z) { if (checkUserInZone(Point(event.latitude, event.longitude), z.points)) { print("USER IN ZONE - ${z.type}"); } }); } FutureOr _onNotificationReceived( NotificationReceived event, Emitter emit, ) { print("NOTIFICATION RECEIVED: ${event.notification.content}"); emit(state.copyWith(lastNotification: event.notification)); } bool checkUserInZone(Point userPos, List zonePoints) { List polygon = zonePoints .map((p) => mt.LatLng(p.latitude, p.longitude)) .toList(); mt.LatLng userLatLng = mt.LatLng(userPos.latitude, userPos.longitude); return mt.PolygonUtil.containsLocation(userLatLng, polygon, true); } FutureOr _onFetchProfileData( FetchProfileData event, Emitter emit, ) async { try { final profile = await getProfileUseCase(); emit( state.copyWith(phoneNumber: profile.phone, balance: profile.balance), ); } catch (e) { emit( state.copyWith( status: ScooterStatus.failure, errorMessage: "FetchProfileData for SideMenu: ${e.toString()}", ), ); } } FutureOr _onLogoutPressed( LogoutPressed event, Emitter emit, ) async { await logoutUseCase(); stopNotificationStream(); _zonesFuture = null; splashBloc.add(AuthLogoutRequested()); } FutureOr _onCheckUser( CheckUser event, Emitter emit, ) async { try { final flags = await checkUserUseCase(); print("flags: $flags"); if (flags == null) { return; } print("check user success"); emit(state.copyWith(flags: flags)); } catch (e) { emit( state.copyWith( status: ScooterStatus.failure, errorMessage: "CheckUser: ${e.toString()}", ), ); } } 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, )); } 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(); } }