import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/api/api_exception.dart'; import '../../../core/location/location_service.dart'; import '../../../core/realtime/realtime_service.dart'; import '../data/models/geo_point.dart'; import '../data/models/trip.dart'; import '../data/trip_repository.dart'; import 'duty_state.dart'; /// دوام السائق: الاتصال ← استقبال العروض ← القبول ← إدارة الرحلة. class DutyCubit extends Cubit { DutyCubit({ required TripRepository trips, required LocationService location, required RealtimeService realtime, }) : _trips = trips, _location = location, _realtime = realtime, super(const DutyState()); final TripRepository _trips; final LocationService _location; final RealtimeService _realtime; StreamSubscription? _events; Timer? _offerTimer; @override Future close() { _events?.cancel(); _offerTimer?.cancel(); _location.stop(); return super.close(); } Future init() async { _events = _realtime.events.listen(_onRealtime); await _realtime.connect(); unawaited(_loadCredit()); unawaited(_resumeActiveTrip()); } Future _loadCredit() async { try { final credit = await _trips.credit(); if (isClosed) return; emit(state.copyWith( creditBalance: credit.balance, creditBlocked: credit.blocked, )); } on ApiException { // الرصيد معلومة مساعدة لا حاجز إقلاع. } } Future _resumeActiveTrip() async { try { final live = (await _trips.mine()).where((t) => t.status.isLive); if (isClosed || live.isEmpty) return; _attachTrip(live.first); } on ApiException { // لا رحلة جارية. } } // ── الاتصال ──────────────────────────────────────────────────────────── Future toggleOnline() async { final goingOnline = !state.isOnline; emit(state.copyWith(busy: true, error: DutyError.none)); try { await _trips.setOnline(goingOnline); if (isClosed) return; if (goingOnline) { // الموقع يُرفع فور الاتصال: سائقٌ متصل بلا موقع لا يصله عمل. _location.listen(_onPosition); emit(state.copyWith(phase: DutyPhase.waiting, busy: false)); } else { await _location.stop(); emit(state.copyWith( phase: DutyPhase.offline, busy: false, clearOffer: true, )); } } on ApiException { if (!isClosed) { emit(state.copyWith(busy: false, error: DutyError.goOnlineFailed)); } } } void _onPosition(dynamic position) { final lat = position.latitude as double; final lng = position.longitude as double; final heading = position.heading as double?; emit(state.copyWith(myLocation: GeoPoint(lat, lng))); // مساران عمداً: REST يغذّي المطابقة في Redis، والسوكت يغذّي خريطة // الراكب الحيّة. سقوط أحدهما لا يُعمي الآخر. unawaited(_trips .pushLocation(GeoPoint(lat, lng), heading: heading) .catchError((_) {})); _realtime.sendDriverLocation(lat, lng, heading: heading); } // ── العروض ───────────────────────────────────────────────────────────── void _showOffer(Trip offer) { if (state.phase == DutyPhase.active) return; // مشغول برحلة _offerTimer?.cancel(); emit(state.copyWith( offer: offer, phase: DutyPhase.offered, error: DutyError.none, )); // العرض لا يبقى معلّقاً إلى الأبد: الخادم قد يعطيه لغيره بلا إشعارنا. _offerTimer = Timer(const Duration(seconds: 25), dismissOffer); } void dismissOffer() { _offerTimer?.cancel(); if (isClosed) return; emit(state.copyWith( clearOffer: true, phase: state.trip != null ? DutyPhase.active : DutyPhase.waiting, )); } Future acceptOffer() async { final offer = state.offer; if (offer == null) return; _offerTimer?.cancel(); emit(state.copyWith(busy: true, error: DutyError.none)); try { await _trips.accept(offer.id); _attachTrip(await _trips.get(offer.id)); } on ApiException catch (e) { if (isClosed) return; // القبول ذرّي: أول سائق يفوز، والباقي يُرفض. هذا ليس عطلاً — يُعرض // «سبقك سائق آخر» لا رسالة خطأ عامة. emit(state.copyWith( busy: false, clearOffer: true, phase: DutyPhase.waiting, error: e.isForbidden || e.statusCode == 409 ? DutyError.offerTaken : DutyError.acceptFailed, )); } } // ── الرحلة ───────────────────────────────────────────────────────────── /// الانتقال التالي في آلة الحالات — الشاشة لا تقرّره (docs/38 §4). TripStatus? get nextStatus => switch (state.trip?.status) { TripStatus.assigned => TripStatus.driverArriving, TripStatus.driverArriving => TripStatus.driverArrived, TripStatus.driverArrived => TripStatus.inProgress, TripStatus.inProgress => TripStatus.completed, _ => null, }; Future advanceTrip() async { final trip = state.trip; final next = nextStatus; if (trip == null || next == null) return; emit(state.copyWith(busy: true, error: DutyError.none)); try { await _trips.updateStatus(trip.id, next); _attachTrip(await _trips.get(trip.id)); // العمولة تُخصم من الرصيد التشغيلي عند الإنهاء — يُحدَّث فوراً. if (next == TripStatus.completed) unawaited(_loadCredit()); } on ApiException { if (!isClosed) { emit(state.copyWith(busy: false, error: DutyError.statusFailed)); } } } void _attachTrip(Trip trip) { _realtime.joinTrip(trip.id); if (trip.status.isOver) { emit(state.copyWith( clearTrip: true, clearOffer: true, busy: false, phase: state.isOnline ? DutyPhase.waiting : DutyPhase.offline, )); return; } emit(state.copyWith( trip: trip, clearOffer: true, busy: false, phase: DutyPhase.active, )); } void _onRealtime(RealtimeEvent event) { switch (event.name) { case RealtimeEvent.tripOffer: _showOffer(Trip.fromJson(event.data)); case RealtimeEvent.tripOfferTaken: final id = event.data['id'] ?? event.data['tripId']; if (id == state.offer?.id) { dismissOffer(); emit(state.copyWith(error: DutyError.offerTaken)); } case RealtimeEvent.tripUpdate: final id = event.data['id'] ?? event.data['tripId']; if (id != null && id == state.trip?.id) { _attachTrip(Trip.fromJson(event.data)); } } } }