Files
tripz-llc/apps/driver/lib/features/offers/bloc/offers_bloc.dart
T

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();
}
}