Files
be_happy_public/lib/presentation/viewmodel/map_bloc.dart
2026-08-29 00:00:09 +03:00

347 lines
10 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<ScooterEvent, ScooterState> {
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<ClientNotification>? _notificationSubscription;
Future<List<Zone>>? _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<FetchScooters>(_onFetchScooters);
on<UpdateMap>(_onUpdateMap);
on<UpdateUserLocation>(_onUpdateUserLocation);
on<NotificationReceived>(_onNotificationReceived);
on<FetchProfileData>(_onFetchProfileData);
on<CheckUser>(_onCheckUser);
on<LogoutPressed>(_onLogoutPressed);
on<FocusOnScooter>(_onFocusOnScooter);
on<ClearMapPlacemarks>(_onClearMapPlacemarks);
on<ClearMapFocus>(_onClearMapFocus);
}
Future<List<Zone>> _getZones() async {
_zonesFuture ??= getAllZonesUsecase();
try {
return await _zonesFuture!;
} catch (e) {
// Не кешируем неудачную попытку — иначе сбой первого запроса
// (сеть/авторизация) навсегда "выключает" зоны до перезапуска
// приложения. Следующий FetchScooters/UpdateMap попробует заново.
_zonesFuture = null;
rethrow;
}
}
List<Zone> _filterZones(List<Zone> zones, MapSettings settings) {
List<Zone> 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<void> _onFetchScooters(
FetchScooters event,
Emitter<ScooterState> 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<Scooter>;
final zones = results[1] as List<Zone>;
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<void> _onUpdateMap(UpdateMap event, Emitter<ScooterState> 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<Scooter>;
final zones = results[1] as List<Zone>;
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<void> _onUpdateUserLocation(
UpdateUserLocation event,
Emitter<ScooterState> 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<void> _onNotificationReceived(
NotificationReceived event,
Emitter<ScooterState> emit,
) {
print("NOTIFICATION RECEIVED: ${event.notification.content}");
emit(state.copyWith(lastNotification: event.notification));
}
bool checkUserInZone(Point userPos, List<Point> zonePoints) {
List<mt.LatLng> 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<void> _onFetchProfileData(
FetchProfileData event,
Emitter<ScooterState> 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<void> _onLogoutPressed(
LogoutPressed event,
Emitter<ScooterState> emit,
) async {
await logoutUseCase();
stopNotificationStream();
_zonesFuture = null;
splashBloc.add(AuthLogoutRequested());
}
FutureOr<void> _onCheckUser(
CheckUser event,
Emitter<ScooterState> 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<void> _onFocusOnScooter(FocusOnScooter event, Emitter<ScooterState> emit) {
final updatedReserved = List<Scooter>.from(state.reservedScooters ?? []);
if (!updatedReserved.any((s) => s.id == event.scooter.id)) {
updatedReserved.add(event.scooter);
}
emit(state.copyWith(
selectedScooterForFocus: event.scooter,
reservedScooters: updatedReserved,
));
}
FutureOr<void> _onClearMapPlacemarks(ClearMapPlacemarks event, Emitter<ScooterState> emit) async {
try{
final orders = await getClientOrdersUsecase();
List<Scooter> updatedReservedScooters = [];
if (orders is Success<List<ScooterOrder>>) {
print("FETCH: orders.data.length = ${orders.data?.length}");
updatedReservedScooters = orders.data?.map((order) {
return order.scooter;
}).toList() ?? [];
}
emit(state.copyWith(
reservedScooters: updatedReservedScooters,
));
} catch (e) {
print("Error in _onClearMapPlacemarks: $e");
emit(state.copyWith(
status: ScooterStatus.failure,
errorMessage: e.toString(),
));
}
}
FutureOr<void> _onClearMapFocus(ClearMapFocus event, Emitter<ScooterState> emit) {
emit(state.copyWith(
selectedScooterForFocus: null,
));
}
}