236 lines
6.6 KiB
Dart
236 lines
6.6 KiB
Dart
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<Object?> get props => [];
|
|
}
|
|
|
|
/// بدء الاستماع للعروض (بعد الاتصال بالـWebSocket).
|
|
class OffersStarted extends OffersEvent {
|
|
const OffersStarted();
|
|
}
|
|
|
|
/// عرض رحلة وارد (من WebSocket).
|
|
class OfferReceived extends OffersEvent {
|
|
final TripOfferModel offer;
|
|
const OfferReceived(this.offer);
|
|
@override
|
|
List<Object?> get props => [offer.tripId];
|
|
}
|
|
|
|
/// العرض تم قبوله من سائق آخر.
|
|
class OfferTaken extends OffersEvent {
|
|
final String tripId;
|
|
const OfferTaken(this.tripId);
|
|
@override
|
|
List<Object?> get props => [tripId];
|
|
}
|
|
|
|
/// العرض انتهت مهلته (من المؤقت).
|
|
class OfferExpired extends OffersEvent {
|
|
final String tripId;
|
|
const OfferExpired(this.tripId);
|
|
@override
|
|
List<Object?> get props => [tripId];
|
|
}
|
|
|
|
/// قبول العرض.
|
|
class OfferAccepted extends OffersEvent {
|
|
final String tripId;
|
|
const OfferAccepted(this.tripId);
|
|
@override
|
|
List<Object?> get props => [tripId];
|
|
}
|
|
|
|
/// رفض العرض يدوياً.
|
|
class OfferRejected extends OffersEvent {
|
|
final String tripId;
|
|
const OfferRejected(this.tripId);
|
|
@override
|
|
List<Object?> get props => [tripId];
|
|
}
|
|
|
|
/// مسح كل العروض (عند بدء رحلة نشطة).
|
|
class OffersClear extends OffersEvent {
|
|
const OffersClear();
|
|
}
|
|
|
|
// ─── حالة ───
|
|
|
|
class OffersState extends Equatable {
|
|
final List<TripOfferModel> 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<TripOfferModel>? 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<Object?> get props => [offers, acceptedTripId, accepting, error];
|
|
}
|
|
|
|
// ─── Bloc ───
|
|
|
|
/// OffersBloc — Bloc الثاني للسائق (docs/03 §14).
|
|
///
|
|
/// يستقبل عروض من WebSocket ويدير القبول/الرفض والمؤقت.
|
|
/// العروض تُعرض كـstack: الأحدث أولاً.
|
|
class OffersBloc extends Bloc<OffersEvent, OffersState> {
|
|
final OffersRepository _offersRepo;
|
|
final DriverSocketService _socket;
|
|
final DriverTripEventMerger _merger;
|
|
|
|
/// مؤقتات المهلة لكل عرض ( tripId → Timer ).
|
|
final Map<String, Timer> _expiryTimers = {};
|
|
|
|
OffersBloc({
|
|
required OffersRepository offersRepo,
|
|
required DriverSocketService socket,
|
|
DriverTripEventMerger? merger,
|
|
}) : _offersRepo = offersRepo,
|
|
_socket = socket,
|
|
_merger = merger ?? DriverTripEventMerger(),
|
|
super(const OffersState()) {
|
|
on<OffersStarted>(_onStarted);
|
|
on<OfferReceived>(_onOfferReceived);
|
|
on<OfferTaken>(_onOfferTaken);
|
|
on<OfferExpired>(_onOfferExpired);
|
|
on<OfferAccepted>(_onOfferAccepted);
|
|
on<OfferRejected>(_onOfferRejected);
|
|
on<OffersClear>(_onClear);
|
|
|
|
// استمع لتحديثات Merger.
|
|
_merger.offerStream.listen((offer) {
|
|
add(OfferReceived(offer));
|
|
});
|
|
|
|
_merger.offerTakenStream.listen((tripId) {
|
|
add(OfferTaken(tripId));
|
|
});
|
|
}
|
|
|
|
Future<void> _onStarted(OffersStarted event, Emitter<OffersState> emit) async {
|
|
_socket.connect();
|
|
}
|
|
|
|
void _onOfferReceived(OfferReceived event, Emitter<OffersState> 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<OffersState> 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<OffersState> emit) {
|
|
final updated = state.offers.where((o) => o.tripId != event.tripId).toList();
|
|
_cancelExpiryTimer(event.tripId);
|
|
emit(state.copyWith(offers: updated));
|
|
}
|
|
|
|
Future<void> _onOfferAccepted(OfferAccepted event, Emitter<OffersState> 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<OffersState> 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<OffersState> 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<void> close() {
|
|
_cancelAllTimers();
|
|
_merger.dispose();
|
|
return super.close();
|
|
}
|
|
}
|