286 lines
8.0 KiB
Dart
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';
|
|
}
|