feat: implement core architecture, design system, localization, and trip features for both driver and rider apps.

This commit is contained in:
Hamza-Ayed
2026-07-19 16:21:58 +03:00
parent 959e2fa758
commit c4dfeb4c6f
207 changed files with 14017 additions and 5567 deletions
@@ -0,0 +1,235 @@
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();
}
}
@@ -0,0 +1,41 @@
import 'package:driver/core/api/api_client.dart';
import 'package:driver/features/trip/data/models/driver_trip_model.dart';
/// مستودع عروض ورحلات السائق — يُستخدم من OffersBloc وDriverTripBloc.
class OffersRepository {
final ApiClient _api;
OffersRepository(this._api);
/// `GET /trips/available` — جلب الرحلات المتاحة.
Future<List<DriverTripModel>> getAvailableTrips() async {
try {
final r = await _api.dio.get('/trips/available');
final items = (r.data['items'] ?? r.data) as List;
return items
.map((j) => DriverTripModel.fromJson(j as Map<String, dynamic>))
.toList();
} catch (_) {
return [];
}
}
/// `POST /trips/:id/accept` — قبول عرض الرحلة.
Future<DriverTripModel?> acceptTrip(String tripId) async {
try {
final r = await _api.dio.post('/trips/$tripId/accept');
return DriverTripModel.fromJson(r.data as Map<String, dynamic>);
} catch (_) {
return null;
}
}
/// `POST /trips/:id/cancel` — إلغاء رحلة مقبولة.
Future<DriverTripModel?> cancelTrip(String tripId) async {
try {
final r = await _api.dio.post('/trips/$tripId/cancel');
return DriverTripModel.fromJson(r.data as Map<String, dynamic>);
} catch (_) {
return null;
}
}
}