Files
tripz-llc/apps/rider/lib/features/trip/bloc/trip_bloc.dart
T

269 lines
8.3 KiB
Dart

import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../data/models/trip_model.dart';
import '../data/trip_repository.dart';
import '../data/trip_socket_service.dart';
import '../data/trip_event_merger.dart';
import 'trip_event.dart';
import 'trip_state.dart';
/// TripBloc — Bloc الوحيد للراكب (docs/03 §14).
///
/// يتعامل مع Lifecycle كامل: إنشاء → تتبّع → إتمام → تقييم.
/// المصادر الثلاث: أحداث المستخدم، WebSocket، FCM — تمر عبر TripEventMerger.
class TripBloc extends Bloc<TripEvent, TripState> {
final TripRepository _tripRepo;
final TripSocketService _socket;
final TripEventMerger _merger;
Timer? _reconciliationTimer;
TripBloc({
required TripRepository tripRepo,
required TripSocketService socket,
TripEventMerger? merger,
}) : _tripRepo = tripRepo,
_socket = socket,
_merger = merger ?? TripEventMerger(),
super(const TripState()) {
on<TripCheckActive>(_onCheckActive);
on<TripRequested>(_onRequested);
on<TripUpdateReceived>(_onUpdateReceived);
on<TripDriverLocation>(_onDriverLocation);
on<TripCancelRequested>(_onCancel);
on<TripRetry>(_onRetry);
on<TripRate>(_onRate);
on<TripClear>(_onClear);
// استمع لتحديثات TripEventMerger.
_merger.updateStream.listen((update) {
add(TripUpdateReceived(
tripId: update.tripId,
status: update.status,
driverId: update.driverId,
extra: update.extra,
));
});
_merger.locationStream.listen((loc) {
add(TripDriverLocation(
tripId: loc.tripId,
lat: loc.lat,
lng: loc.lng,
heading: loc.heading,
speed: loc.speed,
));
});
// اتصل بالـWebSocket.
_socket.connect();
}
/// فحص رحلة نشطة عند بدء التشغيل أو العودة من الخلفية.
Future<void> _onCheckActive(TripCheckActive event, Emitter<TripState> emit) async {
try {
final trip = await _tripRepo.getActiveTrip();
if (trip != null) {
emit(state.copyWith(
requestStatus: _statusFromTrip(trip.status),
trip: trip,
));
_joinTripRoom(trip.id);
_startReconciliation(trip.id);
}
} catch (_) {
// خطأ صامت — سنحاول مرة أخرى عند إعادة الاتصال.
}
}
/// طلب رحلة جديدة.
Future<void> _onRequested(TripRequested event, Emitter<TripState> emit) async {
emit(state.copyWith(requestStatus: TripRequestStatus.loading, clearError: true));
try {
final trip = await _tripRepo.createTrip(
pickupLat: event.pickupLat,
pickupLng: event.pickupLng,
pickupAddress: event.pickupAddress,
dropoffLat: event.dropoffLat,
dropoffLng: event.dropoffLng,
dropoffAddress: event.dropoffAddress,
vehicleClass: event.vehicleClass,
paymentMethod: event.paymentMethod,
couponCode: event.couponCode,
);
emit(state.copyWith(
requestStatus: TripRequestStatus.searching,
trip: trip,
));
_joinTripRoom(trip.id);
_startReconciliation(trip.id);
} catch (e) {
emit(state.copyWith(
requestStatus: TripRequestStatus.error,
error: e.toString(),
));
}
}
/// تحديث حالة الرحلة — من WebSocket أو FCM عبر TripEventMerger.
void _onUpdateReceived(TripUpdateReceived event, Emitter<TripState> emit) {
final currentTrip = state.trip;
if (currentTrip == null || currentTrip.id != event.tripId) return;
// التصفية الصامتة: تجاهل الأحداث القديمة.
final currentOrder = statusOrder(currentTrip.status);
final newOrder = statusOrder(event.status);
if (newOrder >= 0 && newOrder <= currentOrder) return;
// تحديث النموذج محلياً.
final updatedTrip = TripModel(
id: currentTrip.id,
status: event.status,
clientId: currentTrip.clientId,
pickupLat: currentTrip.pickupLat,
pickupLng: currentTrip.pickupLng,
pickupAddress: currentTrip.pickupAddress,
dropoffLat: currentTrip.dropoffLat,
dropoffLng: currentTrip.dropoffLng,
dropoffAddress: currentTrip.dropoffAddress,
vehicleClass: currentTrip.vehicleClass,
fare: (event.extra['fare'] as num?) ?? currentTrip.fare,
currency: event.extra['currency'] as String? ?? currentTrip.currency,
paymentMethod: currentTrip.paymentMethod,
driverId: event.driverId ?? currentTrip.driverId,
createdAt: currentTrip.createdAt,
updatedAt: DateTime.now(),
);
final newStatus = _statusFromTrip(event.status);
// استخراج معلومات السائق من extra.
final vehicle = event.extra['vehicle'] as Map<String, dynamic>?;
emit(state.copyWith(
requestStatus: newStatus,
trip: updatedTrip,
driverName: event.extra['driverName'] as String?,
vehicleMake: vehicle?['make'] as String?,
vehicleModel: vehicle?['model'] as String?,
vehiclePlate: vehicle?['plate'] as String?,
vehicleColor: vehicle?['color'] as String?,
));
// حالة نهائية: أوقف مؤقت الم reconciliation.
if (isTerminalStatus(event.status)) {
_stopReconciliation();
_socket.leaveTrip(event.tripId);
}
}
/// موقع السائق الحي.
void _onDriverLocation(TripDriverLocation event, Emitter<TripState> emit) {
if (state.trip?.id != event.tripId) return;
emit(state.copyWith(
driverLat: event.lat,
driverLng: event.lng,
driverHeading: event.heading,
));
}
/// إلغاء الرحلة.
Future<void> _onCancel(TripCancelRequested event, Emitter<TripState> emit) async {
if (state.trip == null) return;
try {
final trip = await _tripRepo.cancelTrip(state.trip!.id);
_stopReconciliation();
_socket.leaveTrip(state.trip!.id);
emit(state.copyWith(
requestStatus: TripRequestStatus.completed,
trip: trip,
));
} catch (e) {
emit(state.copyWith(error: e.toString()));
}
}
/// إعادة المحاولة بعد خطأ.
void _onRetry(TripRetry event, Emitter<TripState> emit) {
emit(state.copyWith(requestStatus: TripRequestStatus.idle, clearError: true));
}
/// تقييم الرحلة.
Future<void> _onRate(TripRate event, Emitter<TripState> emit) async {
if (state.trip == null) return;
try {
await _tripRepo.rateTrip(state.trip!.id, stars: event.stars, comment: event.comment);
emit(state.copyWith(rated: true));
} catch (e) {
emit(state.copyWith(error: e.toString()));
}
}
/// مسح الرحلة من الذاكرة.
void _onClear(TripClear event, Emitter<TripState> emit) {
_stopReconciliation();
emit(const TripState());
}
// --- مساعدات ---
void _joinTripRoom(String tripId) {
_socket.joinTrip(tripId);
}
void _startReconciliation(String tripId) {
_stopReconciliation();
_reconciliationTimer = Timer.periodic(const Duration(seconds: 25), (_) async {
try {
final trip = await _tripRepo.getTrip(tripId);
if (trip != null) {
add(TripUpdateReceived(
tripId: trip.id,
status: trip.status,
driverId: trip.driverId,
extra: {
if (trip.fare != null) 'fare': trip.fare,
if (trip.currency != null) 'currency': trip.currency,
},
));
}
} catch (_) {
// خطأ صامت — سيحاول مرة أخرى.
}
});
}
void _stopReconciliation() {
_reconciliationTimer?.cancel();
_reconciliationTimer = null;
}
TripRequestStatus _statusFromTrip(String status) {
switch (status) {
case 'searching':
return TripRequestStatus.searching;
case 'assigned':
case 'driver_arriving':
case 'driver_arrived':
case 'in_progress':
return TripRequestStatus.active;
case 'completed':
case 'paid':
return TripRequestStatus.completed;
case 'cancelled':
case 'no_drivers':
case 'expired':
return TripRequestStatus.completed;
default:
return TripRequestStatus.searching;
}
}
@override
Future<void> close() {
_stopReconciliation();
_merger.dispose();
_socket.disconnect();
return super.close();
}
}