import 'dart:async'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../data/offers_repository.dart'; import 'package:driver/features/home/data/models/trip_offer_model.dart'; import '../../trip/data/driver_socket_service.dart'; import '../../trip/data/trip_event_merger.dart'; // ─── أحداث ─── sealed class OffersEvent extends Equatable { const OffersEvent(); @override List get props => []; } /// بدء الاستماع للعروض (بعد الاتصال بالـWebSocket). class OffersStarted extends OffersEvent { const OffersStarted(); } /// عرض رحلة وارد (من WebSocket). class OfferReceived extends OffersEvent { final TripOfferModel offer; const OfferReceived(this.offer); @override List get props => [offer.tripId]; } /// العرض تم قبوله من سائق آخر. class OfferTaken extends OffersEvent { final String tripId; const OfferTaken(this.tripId); @override List get props => [tripId]; } /// العرض انتهت مهلته (من المؤقت). class OfferExpired extends OffersEvent { final String tripId; const OfferExpired(this.tripId); @override List get props => [tripId]; } /// قبول العرض. class OfferAccepted extends OffersEvent { final String tripId; const OfferAccepted(this.tripId); @override List get props => [tripId]; } /// رفض العرض يدوياً. class OfferRejected extends OffersEvent { final String tripId; const OfferRejected(this.tripId); @override List get props => [tripId]; } /// مسح كل العروض (عند بدء رحلة نشطة). class OffersClear extends OffersEvent { const OffersClear(); } // ─── حالة ─── class OffersState extends Equatable { final List offers; final String? acceptedTripId; final bool accepting; final String? error; const OffersState({ this.offers = const [], this.acceptedTripId, this.accepting = false, this.error, }); OffersState copyWith({ List? offers, String? acceptedTripId, bool? accepting, String? error, bool clearError = false, bool clearAccepted = false, }) { return OffersState( offers: offers ?? this.offers, acceptedTripId: clearAccepted ? null : (acceptedTripId ?? this.acceptedTripId), accepting: accepting ?? this.accepting, error: clearError ? null : (error ?? this.error), ); } /// هل هناك عرض واحد على الأقل؟ bool get hasOffers => offers.isNotEmpty; @override List get props => [offers, acceptedTripId, accepting, error]; } // ─── Bloc ─── /// OffersBloc — Bloc الثاني للسائق (docs/03 §14). /// /// يستقبل عروض من WebSocket ويدير القبول/الرفض والمؤقت. /// العروض تُعرض كـstack: الأحدث أولاً. class OffersBloc extends Bloc { final OffersRepository _offersRepo; final DriverSocketService _socket; final DriverTripEventMerger _merger; /// مؤقتات المهلة لكل عرض ( tripId → Timer ). final Map _expiryTimers = {}; OffersBloc({ required OffersRepository offersRepo, required DriverSocketService socket, DriverTripEventMerger? merger, }) : _offersRepo = offersRepo, _socket = socket, _merger = merger ?? DriverTripEventMerger(), super(const OffersState()) { on(_onStarted); on(_onOfferReceived); on(_onOfferTaken); on(_onOfferExpired); on(_onOfferAccepted); on(_onOfferRejected); on(_onClear); // استمع لتحديثات Merger. _merger.offerStream.listen((offer) { add(OfferReceived(offer)); }); _merger.offerTakenStream.listen((tripId) { add(OfferTaken(tripId)); }); } Future _onStarted(OffersStarted event, Emitter emit) async { _socket.connect(); } void _onOfferReceived(OfferReceived event, Emitter emit) { // لا تضف عروضاً مكررة. final exists = state.offers.any((o) => o.tripId == event.offer.tripId); if (exists) return; final updated = [event.offer, ...state.offers]; emit(state.copyWith(offers: updated, clearError: true)); // ابدأ مؤقت المهلة. _startExpiryTimer(event.offer); } void _onOfferTaken(OfferTaken event, Emitter emit) { final updated = state.offers.where((o) => o.tripId != event.tripId).toList(); _cancelExpiryTimer(event.tripId); emit(state.copyWith(offers: updated)); } void _onOfferExpired(OfferExpired event, Emitter emit) { final updated = state.offers.where((o) => o.tripId != event.tripId).toList(); _cancelExpiryTimer(event.tripId); emit(state.copyWith(offers: updated)); } Future _onOfferAccepted(OfferAccepted event, Emitter emit) async { emit(state.copyWith(accepting: true, clearError: true)); final trip = await _offersRepo.acceptTrip(event.tripId); if (trip == null) { emit(state.copyWith(accepting: false, error: 'Failed to accept trip')); return; } // أزل كل العروض المتبقية (تم اختيار سائق). _cancelAllTimers(); emit(state.copyWith( offers: [], accepting: false, acceptedTripId: event.tripId, )); } void _onOfferRejected(OfferRejected event, Emitter emit) { final updated = state.offers.where((o) => o.tripId != event.tripId).toList(); _cancelExpiryTimer(event.tripId); emit(state.copyWith(offers: updated)); } void _onClear(OffersClear event, Emitter emit) { _cancelAllTimers(); emit(const OffersState()); } // ─── مساعدات المؤقتات ─── void _startExpiryTimer(TripOfferModel offer) { final remaining = offer.expiresAt.difference(DateTime.now()); if (remaining.isNegative) { add(OfferExpired(offer.tripId)); return; } _expiryTimers[offer.tripId]?.cancel(); _expiryTimers[offer.tripId] = Timer(remaining, () { add(OfferExpired(offer.tripId)); }); } void _cancelExpiryTimer(String tripId) { _expiryTimers[tripId]?.cancel(); _expiryTimers.remove(tripId); } void _cancelAllTimers() { for (final timer in _expiryTimers.values) { timer.cancel(); } _expiryTimers.clear(); } @override Future close() { _cancelAllTimers(); _merger.dispose(); return super.close(); } }