Files
tripz-llc/apps/driver-archive-cubit/lib/features/trip/bloc/driver_trip_bloc.dart
T
Hamza-AyedandClaude Opus 5 9909d9b4c1 chore: أرشفة تطبيقات ولوحات تريبز التي سيحلّ محلها كود سيرو
استكمال قرار 2026-07-27 (كوميت fff76c9 أرشف الباك إند):
  apps/rider          → apps/rider-archive-cubit
  apps/driver         → apps/driver-archive-cubit
  dashboards/admin-web    → dashboards/admin-web-archive
  dashboards/service-web  → dashboards/service-web-archive

إعادة تسمية بلا تعديل محتوى. rider/driver كانا إعادة بناء Cubit مبنية على
عقد NestJS المؤرشف. admin-web و service-web نموذجان بملف HTML واحد (164 و
128 سطراً) — يحلّ محلهما siro_admin و siro_service المكتملان.

يُحمل من المؤرشف ولا يُعاد اكتشافه: عقد map-saas المثبَّت بالتجربة في
apps/rider-archive-cubit/lib/core/api/ (x-api-key ترويسة، reverse مصفوفة،
route في points) ودرس مسار FCM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:12:04 +03:00

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';
}