Files
tripz-llc/apps/driver/lib/features/trip/bloc/driver_trip_bloc.dart
T

286 lines
8.0 KiB
Dart

import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../data/driver_trip_repository.dart';
import '../data/driver_socket_service.dart';
import '../data/trip_event_merger.dart';
import 'package:driver/features/trip/data/models/driver_trip_model.dart';
// ─── أحداث ───
sealed class DriverTripEvent extends Equatable {
const DriverTripEvent();
@override
List<Object?> get props => [];
}
/// فحص رحلة نشطة عند بدء التشغيل.
class DriverTripCheckActive extends DriverTripEvent {
const DriverTripCheckActive();
}
/// تم قبول رحلة (من OffersBloc).
class DriverTripAccepted extends DriverTripEvent {
final DriverTripModel trip;
const DriverTripAccepted(this.trip);
@override
List<Object?> get props => [trip.id];
}
/// تحديث حالة من WebSocket أو FCM.
class DriverTripUpdateReceived extends DriverTripEvent {
final String tripId;
final String status;
final Map<String, dynamic> extra;
const DriverTripUpdateReceived({
required this.tripId,
required this.status,
this.extra = const {},
});
@override
List<Object?> get props => [tripId, status];
}
/// تغيير الحالة يدوياً (من زر السائق).
class DriverTripStatusChanged extends DriverTripEvent {
final String status;
const DriverTripStatusChanged(this.status);
@override
List<Object?> get props => [status];
}
/// إلغاء الرحلة من السائق.
class DriverTripCancelRequested extends DriverTripEvent {
const DriverTripCancelRequested();
}
/// مسح الرحلة من الذاكرة.
class DriverTripClear extends DriverTripEvent {
const DriverTripClear();
}
// ─── حالة ───
class DriverTripState extends Equatable {
final DriverTripModel? trip;
final bool loading;
final String? error;
final bool cancelled;
const DriverTripState({
this.trip,
this.loading = false,
this.error,
this.cancelled = false,
});
DriverTripState copyWith({
DriverTripModel? trip,
bool? loading,
String? error,
bool? cancelled,
bool clearError = false,
}) {
return DriverTripState(
trip: trip ?? this.trip,
loading: loading ?? this.loading,
error: clearError ? null : (error ?? this.error),
cancelled: cancelled ?? this.cancelled,
);
}
/// هل هناك رحلة نشطة؟
bool get hasActiveTrip {
if (trip == null) return false;
return !['completed', 'paid', 'cancelled', 'expired']
.contains(trip!.status);
}
/// الحالة التالية المتوقعة للسائق.
String? get nextStatus {
if (trip == null) return null;
switch (trip!.status) {
case 'assigned':
return 'driver_arriving';
case 'driver_arriving':
return 'driver_arrived';
case 'driver_arrived':
return 'in_progress';
case 'in_progress':
return 'completed';
default:
return null;
}
}
/// هل يمكن للسائق إلغاء الرحلة حالياً؟
bool get canCancel {
if (trip == null) return false;
return ['assigned', 'driver_arriving'].contains(trip!.status);
}
@override
List<Object?> get props => [trip, loading, error, cancelled];
}
// ─── Bloc ───
/// DriverTripBloc — يدير دورة حياة الرحلة من وجهة نظر السائق.
///
/// الحالات: assigned → driver_arriving → driver_arrived → in_progress → completed
class DriverTripBloc extends Bloc<DriverTripEvent, DriverTripState> {
final DriverTripRepository _tripRepo;
final DriverSocketService _socket;
final DriverTripEventMerger _merger;
Timer? _reconciliationTimer;
DriverTripBloc({
required DriverTripRepository tripRepo,
required DriverSocketService socket,
DriverTripEventMerger? merger,
}) : _tripRepo = tripRepo,
_socket = socket,
_merger = merger ?? DriverTripEventMerger(),
super(const DriverTripState()) {
on<DriverTripCheckActive>(_onCheckActive);
on<DriverTripAccepted>(_onAccepted);
on<DriverTripUpdateReceived>(_onUpdateReceived);
on<DriverTripStatusChanged>(_onStatusChanged);
on<DriverTripCancelRequested>(_onCancel);
on<DriverTripClear>(_onClear);
// استمع لتحديثات Merger.
_merger.updateStream.listen((update) {
add(DriverTripUpdateReceived(
tripId: update.tripId,
status: update.status,
extra: update.extra,
));
});
}
Future<void> _onCheckActive(
DriverTripCheckActive event, Emitter<DriverTripState> emit) async {
emit(state.copyWith(loading: true));
try {
final trip = await _tripRepo.getTrip('me'); // backend يُعيد الرحلة النشطة للسائق
if (trip != null && trip.status != 'completed' && trip.status != 'paid') {
emit(state.copyWith(trip: trip, loading: false));
_joinTripRoom(trip.id);
_startReconciliation(trip.id);
} else {
emit(state.copyWith(loading: false));
}
} catch (_) {
emit(state.copyWith(loading: false));
}
}
void _onAccepted(DriverTripAccepted event, Emitter<DriverTripState> emit) {
emit(state.copyWith(trip: event.trip));
_joinTripRoom(event.trip.id);
_startReconciliation(event.trip.id);
}
void _onUpdateReceived(
DriverTripUpdateReceived event, Emitter<DriverTripState> emit) {
final currentTrip = state.trip;
if (currentTrip == null || currentTrip.id != event.tripId) return;
// تصفية صامتة: تجاهل الحالات القديمة.
if (isTerminalStatus(event.status)) {
emit(state.copyWith(
trip: currentTrip.copyWith(status: event.status),
));
_stopReconciliation();
_socket.leaveTrip(event.tripId);
return;
}
emit(state.copyWith(
trip: currentTrip.copyWith(status: event.status),
clearError: true,
));
}
Future<void> _onStatusChanged(
DriverTripStatusChanged event, Emitter<DriverTripState> emit) async {
if (state.trip == null) return;
emit(state.copyWith(loading: true, clearError: true));
final trip = await _tripRepo.updateStatus(state.trip!.id, event.status);
if (trip == null) {
emit(state.copyWith(loading: false, error: 'Failed to update status'));
return;
}
emit(state.copyWith(trip: trip, loading: false));
if (isTerminalStatus(trip.status)) {
_stopReconciliation();
_socket.leaveTrip(trip.id);
}
}
Future<void> _onCancel(
DriverTripCancelRequested event, Emitter<DriverTripState> emit) async {
if (state.trip == null) return;
emit(state.copyWith(loading: true, clearError: true));
final trip = await _tripRepo.cancelTrip(state.trip!.id);
if (trip == null) {
emit(state.copyWith(loading: false, error: 'Failed to cancel trip'));
return;
}
emit(state.copyWith(trip: trip, cancelled: true, loading: false));
_stopReconciliation();
_socket.leaveTrip(trip.id);
}
void _onClear(DriverTripClear event, Emitter<DriverTripState> emit) {
_stopReconciliation();
emit(const DriverTripState());
}
// ─── مساعدات ───
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(DriverTripUpdateReceived(
tripId: trip.id,
status: trip.status,
));
}
} catch (_) {}
});
}
void _stopReconciliation() {
_reconciliationTimer?.cancel();
_reconciliationTimer = null;
}
@override
Future<void> close() {
_stopReconciliation();
_merger.dispose();
return super.close();
}
}
/// هل هذه الحالة نهائية؟
bool isTerminalStatus(String status) {
return status == 'cancelled' || status == 'expired' || status == 'paid' || status == 'completed';
}