From fc5cbb108a515d0eabb5494981488fab6cc7f95e Mon Sep 17 00:00:00 2001 From: Rahma Ashraf Date: Wed, 18 Feb 2026 04:51:03 +0200 Subject: [PATCH 1/6] feat(SCRUM-92) implement data & domain layer --- .../api/track_order_remote_source_impl.dart | 21 ++++++++++ .../datasource/track_order_remote_source.dart | 5 +++ .../data/models/track_order_model.dart | 25 ++++++++++++ .../data/repos/track_order_imp.dart | 14 +++++++ .../entities/order_location_entity.dart | 20 ++++++++++ .../domain/repos/track_order_repo.dart | 5 +++ .../domain/usecases/track_order_usecase.dart | 12 ++++++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 24 ++++++++++++ pubspec.yaml | 39 +++++++++---------- .../flutter/generated_plugin_registrant.cc | 3 ++ windows/flutter/generated_plugins.cmake | 1 + 12 files changed, 150 insertions(+), 21 deletions(-) create mode 100644 lib/features/track_order/api/track_order_remote_source_impl.dart create mode 100644 lib/features/track_order/data/datasource/track_order_remote_source.dart create mode 100644 lib/features/track_order/data/models/track_order_model.dart create mode 100644 lib/features/track_order/data/repos/track_order_imp.dart create mode 100644 lib/features/track_order/domain/entities/order_location_entity.dart create mode 100644 lib/features/track_order/domain/repos/track_order_repo.dart create mode 100644 lib/features/track_order/domain/usecases/track_order_usecase.dart diff --git a/lib/features/track_order/api/track_order_remote_source_impl.dart b/lib/features/track_order/api/track_order_remote_source_impl.dart new file mode 100644 index 0000000..f4255eb --- /dev/null +++ b/lib/features/track_order/api/track_order_remote_source_impl.dart @@ -0,0 +1,21 @@ +import 'package:tracking_app/features/track_order/data/datasource/track_order_remote_source.dart'; +import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; + +class TrackOrderRemoteDataSourceImpl implements TrackOrderRemoteDataSource { + final FirebaseFirestore firestore; + + TrackOrderRemoteDataSourceImpl(this.firestore); + + @override + Stream trackOrder(String orderId) { + return firestore + .collection('u8sj29sk2sff') + .doc(orderId) + .snapshots() + .map((snapshot) { + final data = snapshot.data(); + return OrderModel.fromFirestore(data!); + }); + } +} diff --git a/lib/features/track_order/data/datasource/track_order_remote_source.dart b/lib/features/track_order/data/datasource/track_order_remote_source.dart new file mode 100644 index 0000000..805c2d1 --- /dev/null +++ b/lib/features/track_order/data/datasource/track_order_remote_source.dart @@ -0,0 +1,5 @@ +import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; + +abstract class TrackOrderRemoteDataSource { + Stream trackOrder(String orderId); +} diff --git a/lib/features/track_order/data/models/track_order_model.dart b/lib/features/track_order/data/models/track_order_model.dart new file mode 100644 index 0000000..9f15144 --- /dev/null +++ b/lib/features/track_order/data/models/track_order_model.dart @@ -0,0 +1,25 @@ +import 'package:tracking_app/features/track_order/domain/entities/order_location_entity.dart'; + +class OrderModel extends Order { + OrderModel({ + required super.id, + required super.driverId, + required super.userId, + required super.status, + required super.totalPrice, + required super.address, + required super.name, + }); + + factory OrderModel.fromFirestore(Map json) { + return OrderModel( + id: json['id'] ?? '', + driverId: json['driverId'] ?? '', + userId: json['userId'] ?? '', + status: json['status'] ?? '', + totalPrice: json['totalPrice'] ?? '', + address: json['userAddress']?['address'] ?? '', + name: json['userAddress']?['name'] ?? '', + ); + } +} diff --git a/lib/features/track_order/data/repos/track_order_imp.dart b/lib/features/track_order/data/repos/track_order_imp.dart new file mode 100644 index 0000000..add9d9c --- /dev/null +++ b/lib/features/track_order/data/repos/track_order_imp.dart @@ -0,0 +1,14 @@ +import 'package:tracking_app/features/track_order/data/datasource/track_order_remote_source.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_location_entity.dart'; +import 'package:tracking_app/features/track_order/domain/repos/track_order_repo.dart'; + +class TrackOrderRepoImpl implements TrackOrderRepo { + final TrackOrderRemoteDataSource remoteDataSource; + + TrackOrderRepoImpl(this.remoteDataSource); + + @override + Stream trackOrder(String orderId) { + return remoteDataSource.trackOrder(orderId); + } +} diff --git a/lib/features/track_order/domain/entities/order_location_entity.dart b/lib/features/track_order/domain/entities/order_location_entity.dart new file mode 100644 index 0000000..c323d76 --- /dev/null +++ b/lib/features/track_order/domain/entities/order_location_entity.dart @@ -0,0 +1,20 @@ +class Order { + final String id; + final String userId; + final String status; + + final String? driverId; + final String? totalPrice; + final String? address; + final String? name; + + Order({ + required this.id, + required this.userId, + required this.status, + this.driverId, + this.totalPrice, + this.address, + this.name, + }); +} diff --git a/lib/features/track_order/domain/repos/track_order_repo.dart b/lib/features/track_order/domain/repos/track_order_repo.dart new file mode 100644 index 0000000..0f674f9 --- /dev/null +++ b/lib/features/track_order/domain/repos/track_order_repo.dart @@ -0,0 +1,5 @@ +import 'package:tracking_app/features/track_order/domain/entities/order_location_entity.dart'; + +abstract class TrackOrderRepo { + Stream trackOrder(String orderId); +} diff --git a/lib/features/track_order/domain/usecases/track_order_usecase.dart b/lib/features/track_order/domain/usecases/track_order_usecase.dart new file mode 100644 index 0000000..443a46c --- /dev/null +++ b/lib/features/track_order/domain/usecases/track_order_usecase.dart @@ -0,0 +1,12 @@ +import 'package:tracking_app/features/track_order/domain/entities/order_location_entity.dart'; +import 'package:tracking_app/features/track_order/domain/repos/track_order_repo.dart'; + +class TrackOrderUseCase { + final TrackOrderRepo repository; + + TrackOrderUseCase(this.repository); + + Stream call(String orderId) { + return repository.trackOrder(orderId); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cac8596..e884426 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import cloud_firestore import file_selector_macos import firebase_core import firebase_crashlytics @@ -15,6 +16,7 @@ import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 779a2a3..15f4f0e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -169,6 +169,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + cloud_firestore: + dependency: "direct dev" + description: + name: cloud_firestore + sha256: "54484b2fc49f41b46f35b60a54b12351181eeaad22c0e3def276a81e17ae7c9b" + url: "https://pub.dev" + source: hosted + version: "6.1.2" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: dfaa8b2c0d0a824af289d4159816a5c78417feec264c2194081d645687195158 + url: "https://pub.dev" + source: hosted + version: "7.0.6" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: "35d01f502b3b701d700470d32a8f82704dac8341a66e86c074900cde5bab343d" + url: "https://pub.dev" + source: hosted + version: "5.1.2" code_builder: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bb7cff1..1e23b9c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,57 +1,56 @@ name: tracking_app description: "A new Flutter project." -publish_to: 'none' +publish_to: "none" version: 1.0.0+1 environment: sdk: ">=3.8.1 <4.0.0" dependencies: - flutter: - sdk: flutter bloc: ^9.2.0 cupertino_icons: ^1.0.8 dio: ^5.9.1 easy_localization: ^3.0.8 - equatable: ^2.0.8 + equatable: ^2.0.8 + firebase_core: ^4.4.0 + firebase_crashlytics: ^5.0.7 + firebase_messaging: ^16.1.1 + flutter: + sdk: flutter flutter_bloc: ^9.1.1 + flutter_local_notifications: ^20.0.0 flutter_otp_text_field: ^1.5.1+1 flutter_svg: ^2.2.3 + geolocator: ^10.1.0 get_it: ^9.2.0 go_router: ^13.2.0 + google_maps_flutter: ^2.14.0 + image_picker: ^1.2.1 injectable: 2.7.0 intl: ^0.20.2 json_annotation: ^4.9.0 + lottie: ^3.3.2 pretty_dio_logger: ^1.4.0 provider: ^6.1.5+1 retrofit: ^4.4.1 shared_preferences: ^2.2.2 shimmer: ^3.0.0 skeletonizer: ^2.1.2 - image_picker: ^1.2.1 - google_maps_flutter: ^2.14.0 - geolocator: ^10.1.0 - firebase_core: ^4.4.0 - lottie: ^3.3.2 url_launcher: ^6.1.10 - firebase_messaging: ^16.1.1 - flutter_local_notifications: ^20.0.0 - firebase_crashlytics: ^5.0.7 dev_dependencies: bloc_test: ^10.0.0 build_runner: ^2.4.13 - flutter_lints: ^6.0.0 + cloud_firestore: ^6.1.2 + flutter_lints: ^6.0.0 + flutter_test: + sdk: flutter injectable_generator: ^2.4.1 json_serializable: ^6.8.0 mockito: ^5.4.4 - retrofit_generator: 7.0.8 - network_image_mock: ^2.1.1 mocktail: ^1.0.3 - - flutter_test: - sdk: flutter - + network_image_mock: ^2.1.1 + retrofit_generator: 7.0.8 flutter: uses-material-design: true @@ -60,8 +59,6 @@ flutter: - assets/translations/ - assets/data/ - assets/images/ - - # fonts: # - family: Schyler # fonts: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index b762e91..8e904a1 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + CloudFirestorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); FirebaseCorePluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b5e0031..8d3f745 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + cloud_firestore file_selector_windows firebase_core geolocator_windows From 3d3a1c8cdee16e24b11d1734514a79d03825b5cd Mon Sep 17 00:00:00 2001 From: Rahma Ashraf Date: Tue, 24 Feb 2026 15:54:24 +0200 Subject: [PATCH 2/6] feat(SCRUM-92)modified data & domain layers to use base response --- lib/app/config/di/di.config.dart | 28 ++++++++++++ lib/app/core/router/route_names.dart | 1 + .../api/track_order_remote_source_impl.dart | 37 +++++++++++++--- .../datasource/track_order_remote_source.dart | 7 ++- .../track_order/data/models/driver_model.dart | 22 ++++++++++ .../data/models/track_order_model.dart | 40 ++++++++--------- .../data/repos/track_order_imp.dart | 14 ------ .../data/repos/track_order_repo_imp.dart | 43 +++++++++++++++++++ .../domain/entities/driver_entity.dart | 11 +++++ ...location_entity.dart => order_entity.dart} | 4 +- .../track_order/domain/repos/track_data.dart | 8 ++++ .../domain/repos/track_order_repo.dart | 7 ++- .../domain/usecases/track_order_usecase.dart | 10 +++-- 13 files changed, 183 insertions(+), 49 deletions(-) create mode 100644 lib/features/track_order/data/models/driver_model.dart delete mode 100644 lib/features/track_order/data/repos/track_order_imp.dart create mode 100644 lib/features/track_order/data/repos/track_order_repo_imp.dart create mode 100644 lib/features/track_order/domain/entities/driver_entity.dart rename lib/features/track_order/domain/entities/{order_location_entity.dart => order_entity.dart} (89%) create mode 100644 lib/features/track_order/domain/repos/track_data.dart diff --git a/lib/app/config/di/di.config.dart b/lib/app/config/di/di.config.dart index 98a5274..2291e86 100644 --- a/lib/app/config/di/di.config.dart +++ b/lib/app/config/di/di.config.dart @@ -8,6 +8,7 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:cloud_firestore/cloud_firestore.dart' as _i974; import 'package:dio/dio.dart' as _i361; import 'package:get_it/get_it.dart' as _i174; import 'package:injectable/injectable.dart' as _i526; @@ -48,6 +49,20 @@ import '../../../features/auth/presentation/reset_password/manager/reset_passwor as _i378; import '../../../features/auth/presentation/verify_reset/manger/cubit/verify_reset_cubit.dart' as _i466; +import '../../../features/track_order/api/track_order_remote_source_impl.dart' + as _i1007; +import '../../../features/track_order/data/datasource/track_order_remote_source.dart' + as _i511; +import '../../../features/track_order/data/repos/track_order_repo_imp.dart' + as _i40; +import '../../../features/track_order/domain/repos/track_order_repo.dart' + as _i1042; +import '../../../features/track_order/domain/usecases/driver_usecase.dart' + as _i866; +import '../../../features/track_order/domain/usecases/track_order_usecase.dart' + as _i810; +import '../../../features/track_order/presentation/manager/cubit/track_order_cubit.dart' + as _i364; import '../../core/api_manger/api_client.dart' as _i890; import '../auth_storage/auth_storage.dart' as _i603; import '../network/network_module.dart' as _i200; @@ -68,8 +83,21 @@ extension GetItInjectableX on _i174.GetIt { gh.lazySingleton<_i603.AuthStorage>(() => _i603.AuthStorage()); gh.lazySingleton<_i783.CountryLocalDataSource>( () => _i783.CountryLocalDataSourceImpl()); + gh.factory<_i511.TrackOrderRemoteDataSource>(() => + _i1007.TrackOrderRemoteDataSourceImpl(gh<_i974.FirebaseFirestore>())); gh.lazySingleton<_i361.Dio>( () => networkModule.dio(gh<_i603.AuthStorage>())); + gh.factory<_i1042.TrackOrderRepo>( + () => _i40.TrackOrderRepoImpl(gh<_i511.TrackOrderRemoteDataSource>())); + gh.factory<_i866.TrackDriverUseCase>( + () => _i866.TrackDriverUseCase(gh<_i1042.TrackOrderRepo>())); + gh.factory<_i810.TrackOrderUseCase>( + () => _i810.TrackOrderUseCase(gh<_i1042.TrackOrderRepo>())); + gh.factory<_i364.TrackOrderCubit>(() => _i364.TrackOrderCubit( + gh<_i974.FirebaseFirestore>(), + gh<_i810.TrackOrderUseCase>(), + gh<_i866.TrackDriverUseCase>(), + )); gh.lazySingleton<_i890.ApiClient>( () => networkModule.authApiClient(gh<_i361.Dio>())); gh.factory<_i708.AuthRemoteDataSource>( diff --git a/lib/app/core/router/route_names.dart b/lib/app/core/router/route_names.dart index 118b723..a9b7c8a 100644 --- a/lib/app/core/router/route_names.dart +++ b/lib/app/core/router/route_names.dart @@ -10,4 +10,5 @@ abstract class RouteNames { static const changePassword = '/changePassword'; static const applyScreen = '/applyScreen'; static const onboarding = '/onboarding'; + static const trackOrder = '/trackOrder'; } diff --git a/lib/features/track_order/api/track_order_remote_source_impl.dart b/lib/features/track_order/api/track_order_remote_source_impl.dart index f4255eb..2442316 100644 --- a/lib/features/track_order/api/track_order_remote_source_impl.dart +++ b/lib/features/track_order/api/track_order_remote_source_impl.dart @@ -1,21 +1,46 @@ +import 'package:injectable/injectable.dart'; import 'package:tracking_app/features/track_order/data/datasource/track_order_remote_source.dart'; +import 'package:tracking_app/features/track_order/data/models/driver_model.dart'; import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; +@Injectable(as: TrackOrderRemoteDataSource) class TrackOrderRemoteDataSourceImpl implements TrackOrderRemoteDataSource { final FirebaseFirestore firestore; TrackOrderRemoteDataSourceImpl(this.firestore); + @override + Stream trackOrder(String orderId) { + return firestore.collection('orders').doc(orderId).snapshots().map(( + snapshot, + ) { + final data = snapshot.data(); + if (data == null) { + throw Exception("Order not found"); + } + return TrackOrderModel.fromFirestore(snapshot.id, data); + }); + } + + @override + Stream trackDriver(String driverId) { + return firestore.collection('drivers').doc(driverId).snapshots().map(( + snapshot, + ) { + final data = snapshot.data(); + if (data == null) throw Exception("Driver not found"); + return DriverModel.fromFirestore(snapshot.id, data); + }); + } @override - Stream trackOrder(String orderId) { + Future updateOrderStatus(String orderId, String status) { return firestore - .collection('u8sj29sk2sff') + .collection('orders') .doc(orderId) - .snapshots() - .map((snapshot) { - final data = snapshot.data(); - return OrderModel.fromFirestore(data!); + .update({'status': status}) + .then((_) { + return firestore.collection('orders').doc(orderId).get(); }); } } diff --git a/lib/features/track_order/data/datasource/track_order_remote_source.dart b/lib/features/track_order/data/datasource/track_order_remote_source.dart index 805c2d1..dce4814 100644 --- a/lib/features/track_order/data/datasource/track_order_remote_source.dart +++ b/lib/features/track_order/data/datasource/track_order_remote_source.dart @@ -1,5 +1,10 @@ +import 'package:tracking_app/features/track_order/data/models/driver_model.dart'; import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; abstract class TrackOrderRemoteDataSource { - Stream trackOrder(String orderId); + Stream trackOrder(String orderId); + Stream trackDriver(String driverId); + Future updateOrderStatus(String orderId, String status); } + diff --git a/lib/features/track_order/data/models/driver_model.dart b/lib/features/track_order/data/models/driver_model.dart new file mode 100644 index 0000000..d567538 --- /dev/null +++ b/lib/features/track_order/data/models/driver_model.dart @@ -0,0 +1,22 @@ +class DriverModel { + final String id; + final double lat; + final double lng; + + DriverModel({ + required this.id, + required this.lat, + required this.lng, + }); + + + factory DriverModel.fromFirestore(String id, Map data) { + return DriverModel( + + id: id, + lat: (data['lat'] as num).toDouble(), + lng: (data['lng'] as num).toDouble(), + ); + } + +} diff --git a/lib/features/track_order/data/models/track_order_model.dart b/lib/features/track_order/data/models/track_order_model.dart index 9f15144..4de0d22 100644 --- a/lib/features/track_order/data/models/track_order_model.dart +++ b/lib/features/track_order/data/models/track_order_model.dart @@ -1,25 +1,25 @@ -import 'package:tracking_app/features/track_order/domain/entities/order_location_entity.dart'; +class TrackOrderModel { + final String driverId; + final String id; + final String status; + final String totalPrice; + final String userId; -class OrderModel extends Order { - OrderModel({ - required super.id, - required super.driverId, - required super.userId, - required super.status, - required super.totalPrice, - required super.address, - required super.name, + TrackOrderModel({ + required this.driverId, + required this.id, + required this.status, + required this.totalPrice, + required this.userId, }); - factory OrderModel.fromFirestore(Map json) { - return OrderModel( - id: json['id'] ?? '', - driverId: json['driverId'] ?? '', - userId: json['userId'] ?? '', - status: json['status'] ?? '', - totalPrice: json['totalPrice'] ?? '', - address: json['userAddress']?['address'] ?? '', - name: json['userAddress']?['name'] ?? '', + factory TrackOrderModel.fromFirestore(String id, Map data) { + return TrackOrderModel( + id: id, + driverId: data['driverId'] ?? '', + status: data['status'] ?? '', + totalPrice: data['totalPrice'] ?? '', + userId: data['userId'] ?? '', ); } -} +} \ No newline at end of file diff --git a/lib/features/track_order/data/repos/track_order_imp.dart b/lib/features/track_order/data/repos/track_order_imp.dart deleted file mode 100644 index add9d9c..0000000 --- a/lib/features/track_order/data/repos/track_order_imp.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:tracking_app/features/track_order/data/datasource/track_order_remote_source.dart'; -import 'package:tracking_app/features/track_order/domain/entities/order_location_entity.dart'; -import 'package:tracking_app/features/track_order/domain/repos/track_order_repo.dart'; - -class TrackOrderRepoImpl implements TrackOrderRepo { - final TrackOrderRemoteDataSource remoteDataSource; - - TrackOrderRepoImpl(this.remoteDataSource); - - @override - Stream trackOrder(String orderId) { - return remoteDataSource.trackOrder(orderId); - } -} diff --git a/lib/features/track_order/data/repos/track_order_repo_imp.dart b/lib/features/track_order/data/repos/track_order_repo_imp.dart new file mode 100644 index 0000000..d2ad6e1 --- /dev/null +++ b/lib/features/track_order/data/repos/track_order_repo_imp.dart @@ -0,0 +1,43 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/features/track_order/data/datasource/track_order_remote_source.dart'; +import 'package:tracking_app/features/track_order/data/models/driver_model.dart'; +import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; +import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; +import 'package:tracking_app/features/track_order/domain/repos/track_order_repo.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; + +@Injectable(as: TrackOrderRepo) +class TrackOrderRepoImpl implements TrackOrderRepo { + final TrackOrderRemoteDataSource remoteDataSource; + + TrackOrderRepoImpl(this.remoteDataSource); + @override + Stream trackOrder(String orderId) { + return remoteDataSource.trackOrder(orderId).map((model) { + return OrderEntity( + id: model.id, + userId: model.userId, + status: model.status, + driverId: model.driverId, + totalPrice: model.totalPrice, + ); + }); + } + + @override + Stream trackOrderWithDriver(String orderId) { + return remoteDataSource.trackDriver(orderId).map((model) { + return DriverEntity( + id: model.id, + lat: model.lat, + lng: model.lng, + ); + }); + } + + @override + Future updateOrderStatus(String orderId, String status) { + return remoteDataSource.updateOrderStatus(orderId, status); + } +} diff --git a/lib/features/track_order/domain/entities/driver_entity.dart b/lib/features/track_order/domain/entities/driver_entity.dart new file mode 100644 index 0000000..79fe007 --- /dev/null +++ b/lib/features/track_order/domain/entities/driver_entity.dart @@ -0,0 +1,11 @@ +class DriverEntity { + final String id; + final double lat; + final double lng; + + DriverEntity({ + required this.id, + required this.lat, + required this.lng, + }); +} \ No newline at end of file diff --git a/lib/features/track_order/domain/entities/order_location_entity.dart b/lib/features/track_order/domain/entities/order_entity.dart similarity index 89% rename from lib/features/track_order/domain/entities/order_location_entity.dart rename to lib/features/track_order/domain/entities/order_entity.dart index c323d76..7707b23 100644 --- a/lib/features/track_order/domain/entities/order_location_entity.dart +++ b/lib/features/track_order/domain/entities/order_entity.dart @@ -1,4 +1,4 @@ -class Order { +class OrderEntity { final String id; final String userId; final String status; @@ -8,7 +8,7 @@ class Order { final String? address; final String? name; - Order({ + OrderEntity({ required this.id, required this.userId, required this.status, diff --git a/lib/features/track_order/domain/repos/track_data.dart b/lib/features/track_order/domain/repos/track_data.dart new file mode 100644 index 0000000..b47fdaa --- /dev/null +++ b/lib/features/track_order/domain/repos/track_data.dart @@ -0,0 +1,8 @@ +import 'package:tracking_app/features/track_order/data/models/driver_model.dart'; +import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; + +class TrackingData { + final TrackOrderModel order; + final DriverModel driver; + TrackingData({required this.order, required this.driver}); +} diff --git a/lib/features/track_order/domain/repos/track_order_repo.dart b/lib/features/track_order/domain/repos/track_order_repo.dart index 0f674f9..063acf1 100644 --- a/lib/features/track_order/domain/repos/track_order_repo.dart +++ b/lib/features/track_order/domain/repos/track_order_repo.dart @@ -1,5 +1,8 @@ -import 'package:tracking_app/features/track_order/domain/entities/order_location_entity.dart'; +import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; abstract class TrackOrderRepo { - Stream trackOrder(String orderId); + Stream trackOrder(String orderId); + Stream trackOrderWithDriver(String orderId); + Future updateOrderStatus(String orderId, String status); } diff --git a/lib/features/track_order/domain/usecases/track_order_usecase.dart b/lib/features/track_order/domain/usecases/track_order_usecase.dart index 443a46c..5d27e21 100644 --- a/lib/features/track_order/domain/usecases/track_order_usecase.dart +++ b/lib/features/track_order/domain/usecases/track_order_usecase.dart @@ -1,12 +1,14 @@ -import 'package:tracking_app/features/track_order/domain/entities/order_location_entity.dart'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; import 'package:tracking_app/features/track_order/domain/repos/track_order_repo.dart'; +@injectable class TrackOrderUseCase { final TrackOrderRepo repository; TrackOrderUseCase(this.repository); - Stream call(String orderId) { - return repository.trackOrder(orderId); - } + ApiResult> call(orderId) => + repository.trackOrder(orderId); } From b0db2d8d33cc6bf863b44cc198f873c5f026b721 Mon Sep 17 00:00:00 2001 From: Rahma Ashraf Date: Tue, 24 Feb 2026 15:55:36 +0200 Subject: [PATCH 3/6] feat(SCRUM-92):implemented presentation layer --- lib/app/core/router/app_router.dart | 12 ++- .../presentation/pages/home_page_test.dart | 16 +++- .../presentation/pages/profile_page.dart | 11 ++- .../api/track_order_remote_source_impl.dart | 74 +++++++++++------- .../datasource/track_order_remote_source.dart | 8 +- .../data/repos/track_order_repo_imp.dart | 58 ++++++++++---- .../domain/repos/track_order_repo.dart | 5 +- .../domain/usecases/driver_usecase.dart | 12 +++ .../manager/cubit/track_order_cubit.dart | 73 ++++++++++++++++++ .../manager/cubit/track_order_state.dart | 32 ++++++++ .../presentation/pages/track_order_page.dart | 77 +++++++++++++++++++ .../presentation/widgets/driver_section.dart | 30 ++++++++ .../presentation/widgets/order_section.dart | 29 +++++++ 13 files changed, 386 insertions(+), 51 deletions(-) create mode 100644 lib/features/track_order/domain/usecases/driver_usecase.dart create mode 100644 lib/features/track_order/presentation/manager/cubit/track_order_cubit.dart create mode 100644 lib/features/track_order/presentation/manager/cubit/track_order_state.dart create mode 100644 lib/features/track_order/presentation/pages/track_order_page.dart create mode 100644 lib/features/track_order/presentation/widgets/driver_section.dart create mode 100644 lib/features/track_order/presentation/widgets/order_section.dart diff --git a/lib/app/core/router/app_router.dart b/lib/app/core/router/app_router.dart index a56daf9..72dadf8 100644 --- a/lib/app/core/router/app_router.dart +++ b/lib/app/core/router/app_router.dart @@ -15,7 +15,8 @@ import 'package:tracking_app/features/auth/presentation/reset_password/pages/res import 'package:tracking_app/features/auth/presentation/verify_reset/manger/cubit/verify_reset_cubit.dart'; import 'package:tracking_app/features/auth/presentation/verify_reset/pages/verify_reset_page.dart'; import 'package:tracking_app/features/profile/presentation/pages/profile_page.dart'; - +import 'package:tracking_app/features/track_order/presentation/manager/cubit/track_order_cubit.dart'; +import 'package:tracking_app/features/track_order/presentation/pages/track_order_page.dart'; final GoRouter appRouter = GoRouter( initialLocation: RouteNames.onboarding, @@ -75,7 +76,16 @@ final GoRouter appRouter = GoRouter( path: RouteNames.profile, builder: (context, state) => const ProfilePage(), ), + + GoRoute( + path: RouteNames.trackOrder, + builder: (context, state) => BlocProvider( + create: (_) => getIt(param1: state.extra as String), + child: TrackOrderPage(orderId: '123'), + ), + ), ], + redirect: (context, state) async { final token = await getIt().getToken(); final rememberMe = await getIt().getRememberMe(); diff --git a/lib/features/app_sections/presentation/pages/home_page_test.dart b/lib/features/app_sections/presentation/pages/home_page_test.dart index 52b8e91..e33cefb 100644 --- a/lib/features/app_sections/presentation/pages/home_page_test.dart +++ b/lib/features/app_sections/presentation/pages/home_page_test.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tracking_app/app/core/router/route_names.dart'; import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; class HomePageTest extends StatelessWidget { @@ -6,6 +8,18 @@ class HomePageTest extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold(backgroundColor: AppColors.green); + return Scaffold( + backgroundColor: AppColors.green, + body: Column( + children: [ + ElevatedButton( + onPressed: () { + context.go(RouteNames.trackOrder); + }, + child: const Text("Track Order"), + ), + ], + ), + ); } } diff --git a/lib/features/profile/presentation/pages/profile_page.dart b/lib/features/profile/presentation/pages/profile_page.dart index 6c970df..6f8bf25 100644 --- a/lib/features/profile/presentation/pages/profile_page.dart +++ b/lib/features/profile/presentation/pages/profile_page.dart @@ -6,6 +6,15 @@ class ProfilePage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold(body: Center(child: const Text("Welcome to Profile Page"))); + return Scaffold( + body: Center( + child: ElevatedButton( + onPressed: () { + Navigator.pushNamed(context, RouteNames.trackOrder); + }, + child: const Text("Track Order"), + ), + ), + ); } } diff --git a/lib/features/track_order/api/track_order_remote_source_impl.dart b/lib/features/track_order/api/track_order_remote_source_impl.dart index 2442316..3095d69 100644 --- a/lib/features/track_order/api/track_order_remote_source_impl.dart +++ b/lib/features/track_order/api/track_order_remote_source_impl.dart @@ -1,4 +1,5 @@ import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/track_order/data/datasource/track_order_remote_source.dart'; import 'package:tracking_app/features/track_order/data/models/driver_model.dart'; import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; @@ -10,37 +11,58 @@ class TrackOrderRemoteDataSourceImpl implements TrackOrderRemoteDataSource { TrackOrderRemoteDataSourceImpl(this.firestore); @override - Stream trackOrder(String orderId) { - return firestore.collection('orders').doc(orderId).snapshots().map(( - snapshot, - ) { - final data = snapshot.data(); - if (data == null) { - throw Exception("Order not found"); - } - return TrackOrderModel.fromFirestore(snapshot.id, data); - }); + ApiResult> trackOrder(String orderId) { + try { + final stream = firestore + .collection('orders') + .doc(orderId) + .snapshots() + .map((snapshot) { + final data = snapshot.data(); + if (data == null) { + throw Exception("Order not found"); + } + return TrackOrderModel.fromFirestore(snapshot.id, data); + }); + return SuccessApiResult>(data: stream); + } catch (e) { + return ErrorApiResult>(error: e.toString()); + } + ; } @override - Stream trackDriver(String driverId) { - return firestore.collection('drivers').doc(driverId).snapshots().map(( - snapshot, - ) { - final data = snapshot.data(); - if (data == null) throw Exception("Driver not found"); - return DriverModel.fromFirestore(snapshot.id, data); - }); + ApiResult> trackDriver(String driverId) { + try { + final stream = firestore + .collection('drivers') + .doc(driverId) + .snapshots() + .map((snapshot) { + final data = snapshot.data(); + if (!snapshot.exists || data == null) + throw Exception("Driver not found"); + return DriverModel.fromFirestore(snapshot.id, data); + }); + return SuccessApiResult>(data: stream); + } catch (e) { + return ErrorApiResult>(error: e.toString()); + } } @override - Future updateOrderStatus(String orderId, String status) { - return firestore - .collection('orders') - .doc(orderId) - .update({'status': status}) - .then((_) { - return firestore.collection('orders').doc(orderId).get(); - }); + Future>> updateOrderStatus( + String orderId, + String status, + ) async { + try { + await firestore.collection('orders').doc(orderId).update({ + 'status': status, + }); + + return await firestore.collection('orders').doc(orderId).get(); + } catch (e) { + rethrow; // Let upper layer handle it + } } } diff --git a/lib/features/track_order/data/datasource/track_order_remote_source.dart b/lib/features/track_order/data/datasource/track_order_remote_source.dart index dce4814..65276e0 100644 --- a/lib/features/track_order/data/datasource/track_order_remote_source.dart +++ b/lib/features/track_order/data/datasource/track_order_remote_source.dart @@ -1,10 +1,10 @@ +import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/track_order/data/models/driver_model.dart'; import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; abstract class TrackOrderRemoteDataSource { - Stream trackOrder(String orderId); - Stream trackDriver(String driverId); - Future updateOrderStatus(String orderId, String status); + ApiResult> trackOrder(String orderId); + ApiResult> trackDriver(String driverId); + Future>> updateOrderStatus(String orderId, String status); } - diff --git a/lib/features/track_order/data/repos/track_order_repo_imp.dart b/lib/features/track_order/data/repos/track_order_repo_imp.dart index d2ad6e1..2142a41 100644 --- a/lib/features/track_order/data/repos/track_order_repo_imp.dart +++ b/lib/features/track_order/data/repos/track_order_repo_imp.dart @@ -1,4 +1,5 @@ import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/track_order/data/datasource/track_order_remote_source.dart'; import 'package:tracking_app/features/track_order/data/models/driver_model.dart'; import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; @@ -12,28 +13,53 @@ class TrackOrderRepoImpl implements TrackOrderRepo { final TrackOrderRemoteDataSource remoteDataSource; TrackOrderRepoImpl(this.remoteDataSource); + @override - Stream trackOrder(String orderId) { - return remoteDataSource.trackOrder(orderId).map((model) { - return OrderEntity( - id: model.id, - userId: model.userId, - status: model.status, - driverId: model.driverId, - totalPrice: model.totalPrice, + ApiResult> trackOrder(String orderId) { + final result = remoteDataSource.trackOrder(orderId); + + if (result is SuccessApiResult>) { + final entityStream = result.data.map( + (model) => OrderEntity( + id: model.id, + userId: model.userId, + status: model.status, + driverId: model.driverId, + totalPrice: model.totalPrice, + ), ); - }); + + return SuccessApiResult(data: entityStream); + } + + if (result is ErrorApiResult>) { + return ErrorApiResult(error: result.error); + } + + throw Exception("Unhandled ApiResult type"); } @override - Stream trackOrderWithDriver(String orderId) { - return remoteDataSource.trackDriver(orderId).map((model) { - return DriverEntity( - id: model.id, - lat: model.lat, - lng: model.lng, + ApiResult> trackOrderWithDriver(String driverId) { + final result = remoteDataSource.trackDriver(driverId); + + if (result is SuccessApiResult>) { + final entityStream = result.data.map( + (model) => DriverEntity( + id: model.id, + lat: model.lat, + lng: model.lng, + ), ); - }); + + return SuccessApiResult(data: entityStream); + } + + if (result is ErrorApiResult>) { + return ErrorApiResult(error: result.error); + } + + throw Exception("Unhandled ApiResult type"); } @override diff --git a/lib/features/track_order/domain/repos/track_order_repo.dart b/lib/features/track_order/domain/repos/track_order_repo.dart index 063acf1..84fae72 100644 --- a/lib/features/track_order/domain/repos/track_order_repo.dart +++ b/lib/features/track_order/domain/repos/track_order_repo.dart @@ -1,8 +1,9 @@ +import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; abstract class TrackOrderRepo { - Stream trackOrder(String orderId); - Stream trackOrderWithDriver(String orderId); + ApiResult> trackOrder(String orderId); + ApiResult> trackOrderWithDriver(String orderId); Future updateOrderStatus(String orderId, String status); } diff --git a/lib/features/track_order/domain/usecases/driver_usecase.dart b/lib/features/track_order/domain/usecases/driver_usecase.dart new file mode 100644 index 0000000..d1215fa --- /dev/null +++ b/lib/features/track_order/domain/usecases/driver_usecase.dart @@ -0,0 +1,12 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; +import 'package:tracking_app/features/track_order/domain/repos/track_order_repo.dart'; + +@injectable +class TrackDriverUseCase { + final TrackOrderRepo repository; + TrackDriverUseCase(this.repository); + ApiResult> call(String orderId) => + repository.trackOrderWithDriver(orderId); +} diff --git a/lib/features/track_order/presentation/manager/cubit/track_order_cubit.dart b/lib/features/track_order/presentation/manager/cubit/track_order_cubit.dart new file mode 100644 index 0000000..1b15de0 --- /dev/null +++ b/lib/features/track_order/presentation/manager/cubit/track_order_cubit.dart @@ -0,0 +1,73 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; +import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; +import 'package:tracking_app/features/track_order/domain/usecases/track_order_usecase.dart'; +import 'package:tracking_app/features/track_order/domain/usecases/driver_usecase.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; + +part 'track_order_state.dart'; + +@injectable +class TrackOrderCubit extends Cubit { + final FirebaseFirestore firestore; + + final TrackOrderUseCase trackOrderUseCase; + final TrackDriverUseCase driverUseCase; + + StreamSubscription? _orderSubscription; + StreamSubscription? _driverSubscription; + + TrackOrderCubit(this.firestore, this.trackOrderUseCase, this.driverUseCase) + : super(const TrackOrderState()); + + void trackOrder(String orderId) { + emit(state.copyWith(isLoading: true, error: null)); + + _orderSubscription?.cancel(); + _driverSubscription?.cancel(); + + /// -------- ORDER -------- + final orderResult = trackOrderUseCase(orderId); + + if (orderResult is SuccessApiResult>) { + _orderSubscription = orderResult.data.listen( + (order) { + emit(state.copyWith(order: order, isLoading: false, error: null)); + }, + onError: (error) { + emit(state.copyWith(error: error.toString(), isLoading: false)); + }, + ); + } else if (orderResult is ErrorApiResult>) { + emit(state.copyWith(error: orderResult.error, isLoading: false)); + } + + /// -------- DRIVER -------- + final driverResult = driverUseCase(orderId); + + if (driverResult is SuccessApiResult>) { + _driverSubscription = driverResult.data.listen( + (driver) { + emit(state.copyWith(driver: driver, error: null)); + }, + onError: (error) { + emit(state.copyWith(error: error.toString(), isLoading: false)); + }, + ); + } else if (driverResult is ErrorApiResult>) { + emit(state.copyWith(error: driverResult.error, isLoading: false)); + } + } + + @override + Future close() async { + await _orderSubscription?.cancel(); + await _driverSubscription?.cancel(); + return super.close(); + } +} diff --git a/lib/features/track_order/presentation/manager/cubit/track_order_state.dart b/lib/features/track_order/presentation/manager/cubit/track_order_state.dart new file mode 100644 index 0000000..dd64de8 --- /dev/null +++ b/lib/features/track_order/presentation/manager/cubit/track_order_state.dart @@ -0,0 +1,32 @@ +part of 'track_order_cubit.dart'; + +class TrackOrderState extends Equatable { + final OrderEntity? order; + final DriverEntity? driver; + final bool isLoading; + final String? error; + + const TrackOrderState({ + this.order, + this.driver, + this.isLoading = false, + this.error, + }); + + TrackOrderState copyWith({ + OrderEntity? order, + DriverEntity? driver, + bool? isLoading, + String? error, + }) { + return TrackOrderState( + order: order ?? this.order, + driver: driver ?? this.driver, + isLoading: isLoading ?? this.isLoading, + error: error, + ); + } + + @override + List get props => [order, driver, isLoading, error]; +} diff --git a/lib/features/track_order/presentation/pages/track_order_page.dart b/lib/features/track_order/presentation/pages/track_order_page.dart new file mode 100644 index 0000000..8034eef --- /dev/null +++ b/lib/features/track_order/presentation/pages/track_order_page.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tracking_app/features/track_order/presentation/manager/cubit/track_order_cubit.dart'; +import 'package:tracking_app/features/track_order/presentation/widgets/driver_section.dart'; +import 'package:tracking_app/features/track_order/presentation/widgets/order_section.dart'; + +class TrackOrderPage extends StatefulWidget { + final String orderId; + + const TrackOrderPage({ + super.key, + required this.orderId, + }); + + @override + State createState() => _TrackOrderPageState(); +} + +class _TrackOrderPageState extends State { + @override + void initState() { + super.initState(); + context.read().trackOrder(widget.orderId); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Track Order'), + ), + body: BlocBuilder( + builder: (context, state) { + if (state.isLoading && state.order == null) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (state.error != null) { + return Center( + child: Text( + state.error!, + style: const TextStyle(color: Colors.red), + ), + ); + } + + return RefreshIndicator( + onRefresh: () async { + context + .read() + .trackOrder(widget.orderId); + }, + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + if (state.order != null) + OrderSection(order: state.order!), + + const SizedBox(height: 20), + + if (state.driver != null) + DriverSection(driver: state.driver!), + + if (state.driver == null) + const Center( + child: Text("Waiting for driver assignment..."), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/features/track_order/presentation/widgets/driver_section.dart b/lib/features/track_order/presentation/widgets/driver_section.dart new file mode 100644 index 0000000..caf4650 --- /dev/null +++ b/lib/features/track_order/presentation/widgets/driver_section.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class DriverSection extends StatelessWidget { + final dynamic driver; + + const DriverSection({required this.driver}); + + @override + Widget build(BuildContext context) { + return Card( + color: Colors.blue.shade50, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Driver Information", + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Text("Driver ID: ${driver.id}"), + const SizedBox(height: 8), + Text("Phone: ${driver.phone ?? 'N/A'}"), + ], + ), + ), + ); + } +} diff --git a/lib/features/track_order/presentation/widgets/order_section.dart b/lib/features/track_order/presentation/widgets/order_section.dart new file mode 100644 index 0000000..e29688a --- /dev/null +++ b/lib/features/track_order/presentation/widgets/order_section.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class OrderSection extends StatelessWidget { + final dynamic order; + + const OrderSection({required this.order}); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Order ID: ${order.id}", + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Text("Status: ${order.status}"), + const SizedBox(height: 8), + Text("Total: ${order.totalPrice} EGP"), + ], + ), + ), + ); + } +} From 9eaecac56ec65553d3fbc637f82e7989987e6bcd Mon Sep 17 00:00:00 2001 From: Rahma Ashraf Date: Wed, 25 Feb 2026 05:34:21 +0200 Subject: [PATCH 4/6] feat(SCRUM-92)modify presentation layer --- lib/app/config/di/di.config.dart | 9 +- lib/app/config/di/di.dart | 2 +- lib/app/core/network/firebase_module.dart | 12 ++ lib/app/core/router/app_router.dart | 4 +- .../api/track_order_remote_source_impl.dart | 3 +- .../data/repos/track_order_repo_imp.dart | 41 +++--- .../track_order/domain/repos/track_data.dart | 14 +-- .../domain/repos/track_order_repo.dart | 4 +- .../domain/usecases/track_order_usecase.dart | 2 +- .../manager/cubit/track_order_cubit.dart | 82 +++++++----- .../manager/cubit/track_order_state.dart | 13 +- .../presentation/pages/track_order_page.dart | 117 ++++++++++++------ .../presentation/widgets/driver_section.dart | 54 ++++---- .../presentation/widgets/order_section.dart | 52 ++++---- lib/main.dart | 2 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 24 ++++ pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 20 files changed, 279 insertions(+), 163 deletions(-) create mode 100644 lib/app/core/network/firebase_module.dart diff --git a/lib/app/config/di/di.config.dart b/lib/app/config/di/di.config.dart index 2291e86..e735250 100644 --- a/lib/app/config/di/di.config.dart +++ b/lib/app/config/di/di.config.dart @@ -10,6 +10,7 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:cloud_firestore/cloud_firestore.dart' as _i974; import 'package:dio/dio.dart' as _i361; +import 'package:firebase_auth/firebase_auth.dart' as _i59; import 'package:get_it/get_it.dart' as _i174; import 'package:injectable/injectable.dart' as _i526; @@ -64,6 +65,7 @@ import '../../../features/track_order/domain/usecases/track_order_usecase.dart' import '../../../features/track_order/presentation/manager/cubit/track_order_cubit.dart' as _i364; import '../../core/api_manger/api_client.dart' as _i890; +import '../../core/network/firebase_module.dart' as _i236; import '../auth_storage/auth_storage.dart' as _i603; import '../network/network_module.dart' as _i200; @@ -78,9 +80,12 @@ extension GetItInjectableX on _i174.GetIt { environment, environmentFilter, ); + final firebaseModule = _$FirebaseModule(); final networkModule = _$NetworkModule(); gh.factory<_i959.AppSectionCubit>(() => _i959.AppSectionCubit()); gh.lazySingleton<_i603.AuthStorage>(() => _i603.AuthStorage()); + gh.lazySingleton<_i974.FirebaseFirestore>(() => firebaseModule.firestore); + gh.lazySingleton<_i59.FirebaseAuth>(() => firebaseModule.auth); gh.lazySingleton<_i783.CountryLocalDataSource>( () => _i783.CountryLocalDataSourceImpl()); gh.factory<_i511.TrackOrderRemoteDataSource>(() => @@ -94,9 +99,9 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i810.TrackOrderUseCase>( () => _i810.TrackOrderUseCase(gh<_i1042.TrackOrderRepo>())); gh.factory<_i364.TrackOrderCubit>(() => _i364.TrackOrderCubit( - gh<_i974.FirebaseFirestore>(), gh<_i810.TrackOrderUseCase>(), gh<_i866.TrackDriverUseCase>(), + gh<_i603.AuthStorage>(), )); gh.lazySingleton<_i890.ApiClient>( () => networkModule.authApiClient(gh<_i361.Dio>())); @@ -156,4 +161,6 @@ extension GetItInjectableX on _i174.GetIt { } } +class _$FirebaseModule extends _i236.FirebaseModule {} + class _$NetworkModule extends _i200.NetworkModule {} diff --git a/lib/app/config/di/di.dart b/lib/app/config/di/di.dart index b2094df..70978fa 100644 --- a/lib/app/config/di/di.dart +++ b/lib/app/config/di/di.dart @@ -9,4 +9,4 @@ final getIt = GetIt.instance; preferRelativeImports: true, // default asExtension: true, // default ) -void configureDependencies() => getIt.init(); +Future configureDependencies() async => getIt.init(); diff --git a/lib/app/core/network/firebase_module.dart b/lib/app/core/network/firebase_module.dart new file mode 100644 index 0000000..e16b370 --- /dev/null +++ b/lib/app/core/network/firebase_module.dart @@ -0,0 +1,12 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:injectable/injectable.dart'; +import 'package:firebase_auth/firebase_auth.dart'; + +@module +abstract class FirebaseModule { + @lazySingleton + FirebaseFirestore get firestore => FirebaseFirestore.instance; + + @lazySingleton + FirebaseAuth get auth => FirebaseAuth.instance; +} diff --git a/lib/app/core/router/app_router.dart b/lib/app/core/router/app_router.dart index 72dadf8..bdd2db1 100644 --- a/lib/app/core/router/app_router.dart +++ b/lib/app/core/router/app_router.dart @@ -80,8 +80,8 @@ final GoRouter appRouter = GoRouter( GoRoute( path: RouteNames.trackOrder, builder: (context, state) => BlocProvider( - create: (_) => getIt(param1: state.extra as String), - child: TrackOrderPage(orderId: '123'), + create: (_) => getIt(), + child: TrackOrderPage(), ), ), ], diff --git a/lib/features/track_order/api/track_order_remote_source_impl.dart b/lib/features/track_order/api/track_order_remote_source_impl.dart index 3095d69..a05ade7 100644 --- a/lib/features/track_order/api/track_order_remote_source_impl.dart +++ b/lib/features/track_order/api/track_order_remote_source_impl.dart @@ -40,8 +40,7 @@ class TrackOrderRemoteDataSourceImpl implements TrackOrderRemoteDataSource { .snapshots() .map((snapshot) { final data = snapshot.data(); - if (!snapshot.exists || data == null) - throw Exception("Driver not found"); + if (data == null) throw Exception("Driver not found"); return DriverModel.fromFirestore(snapshot.id, data); }); return SuccessApiResult>(data: stream); diff --git a/lib/features/track_order/data/repos/track_order_repo_imp.dart b/lib/features/track_order/data/repos/track_order_repo_imp.dart index 2142a41..10b3f32 100644 --- a/lib/features/track_order/data/repos/track_order_repo_imp.dart +++ b/lib/features/track_order/data/repos/track_order_repo_imp.dart @@ -6,7 +6,6 @@ import 'package:tracking_app/features/track_order/data/models/track_order_model. import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; import 'package:tracking_app/features/track_order/domain/repos/track_order_repo.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; @Injectable(as: TrackOrderRepo) class TrackOrderRepoImpl implements TrackOrderRepo { @@ -15,25 +14,31 @@ class TrackOrderRepoImpl implements TrackOrderRepo { TrackOrderRepoImpl(this.remoteDataSource); @override - ApiResult> trackOrder(String orderId) { - final result = remoteDataSource.trackOrder(orderId); + ApiResult>> trackOrder(String userId) { + final result = remoteDataSource.trackOrder(userId); - if (result is SuccessApiResult>) { - final entityStream = result.data.map( - (model) => OrderEntity( - id: model.id, - userId: model.userId, - status: model.status, - driverId: model.driverId, - totalPrice: model.totalPrice, - ), + if (result is SuccessApiResult>>) { + final successResult = result as SuccessApiResult>>; + final entityStream = successResult.data.map( + (models) => models + .map( + (model) => OrderEntity( + id: model.id, + userId: model.userId, + status: model.status, + driverId: model.driverId, + totalPrice: model.totalPrice, + ), + ) + .toList(), ); return SuccessApiResult(data: entityStream); } - if (result is ErrorApiResult>) { - return ErrorApiResult(error: result.error); + if (result is ErrorApiResult>>) { + final errorResult = result as ErrorApiResult>>; + return ErrorApiResult(error: errorResult.error); } throw Exception("Unhandled ApiResult type"); @@ -44,7 +49,8 @@ class TrackOrderRepoImpl implements TrackOrderRepo { final result = remoteDataSource.trackDriver(driverId); if (result is SuccessApiResult>) { - final entityStream = result.data.map( + final successResult = result as SuccessApiResult>; + final entityStream = successResult.data.map( (model) => DriverEntity( id: model.id, lat: model.lat, @@ -56,7 +62,8 @@ class TrackOrderRepoImpl implements TrackOrderRepo { } if (result is ErrorApiResult>) { - return ErrorApiResult(error: result.error); + final errorResult = result as ErrorApiResult>; + return ErrorApiResult(error: errorResult.error); } throw Exception("Unhandled ApiResult type"); @@ -66,4 +73,4 @@ class TrackOrderRepoImpl implements TrackOrderRepo { Future updateOrderStatus(String orderId, String status) { return remoteDataSource.updateOrderStatus(orderId, status); } -} +} \ No newline at end of file diff --git a/lib/features/track_order/domain/repos/track_data.dart b/lib/features/track_order/domain/repos/track_data.dart index b47fdaa..71ada16 100644 --- a/lib/features/track_order/domain/repos/track_data.dart +++ b/lib/features/track_order/domain/repos/track_data.dart @@ -1,8 +1,8 @@ -import 'package:tracking_app/features/track_order/data/models/driver_model.dart'; -import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; +// import 'package:tracking_app/features/track_order/data/models/driver_model.dart'; +// import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; -class TrackingData { - final TrackOrderModel order; - final DriverModel driver; - TrackingData({required this.order, required this.driver}); -} +// class TrackingData { +// final TrackOrderModel order; +// final DriverModel driver; +// TrackingData({required this.order, required this.driver}); +// } diff --git a/lib/features/track_order/domain/repos/track_order_repo.dart b/lib/features/track_order/domain/repos/track_order_repo.dart index 84fae72..592cbbe 100644 --- a/lib/features/track_order/domain/repos/track_order_repo.dart +++ b/lib/features/track_order/domain/repos/track_order_repo.dart @@ -1,9 +1,9 @@ +import 'package:injectable/injectable.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; - abstract class TrackOrderRepo { - ApiResult> trackOrder(String orderId); + ApiResult>> trackOrder(String orderId); ApiResult> trackOrderWithDriver(String orderId); Future updateOrderStatus(String orderId, String status); } diff --git a/lib/features/track_order/domain/usecases/track_order_usecase.dart b/lib/features/track_order/domain/usecases/track_order_usecase.dart index 5d27e21..1b1f9d1 100644 --- a/lib/features/track_order/domain/usecases/track_order_usecase.dart +++ b/lib/features/track_order/domain/usecases/track_order_usecase.dart @@ -9,6 +9,6 @@ class TrackOrderUseCase { TrackOrderUseCase(this.repository); - ApiResult> call(orderId) => + ApiResult>> call(orderId) => repository.trackOrder(orderId); } diff --git a/lib/features/track_order/presentation/manager/cubit/track_order_cubit.dart b/lib/features/track_order/presentation/manager/cubit/track_order_cubit.dart index 1b15de0..128fba3 100644 --- a/lib/features/track_order/presentation/manager/cubit/track_order_cubit.dart +++ b/lib/features/track_order/presentation/manager/cubit/track_order_cubit.dart @@ -1,73 +1,87 @@ import 'dart:async'; - import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:injectable/injectable.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; import 'package:tracking_app/features/track_order/domain/usecases/track_order_usecase.dart'; import 'package:tracking_app/features/track_order/domain/usecases/driver_usecase.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:tracking_app/features/track_order/domain/repos/track_order_repo.dart'; part 'track_order_state.dart'; @injectable class TrackOrderCubit extends Cubit { - final FirebaseFirestore firestore; - - final TrackOrderUseCase trackOrderUseCase; + final TrackOrderUseCase trackOrderUseCase; final TrackDriverUseCase driverUseCase; + final AuthStorage authStorage; - StreamSubscription? _orderSubscription; + StreamSubscription>? _ordersSubscription; StreamSubscription? _driverSubscription; - TrackOrderCubit(this.firestore, this.trackOrderUseCase, this.driverUseCase) - : super(const TrackOrderState()); + TrackOrderCubit( + this.trackOrderUseCase, + this.driverUseCase, + this.authStorage, + ) : super(const TrackOrderState()); - void trackOrder(String orderId) { + Future loadUserOrders() async { emit(state.copyWith(isLoading: true, error: null)); - _orderSubscription?.cancel(); - _driverSubscription?.cancel(); + final userId = await authStorage.getToken(); + + if (userId == null) { + emit(state.copyWith( + isLoading: false, + error: "User not logged in", + )); + return; + } - /// -------- ORDER -------- - final orderResult = trackOrderUseCase(orderId); + final result = trackOrderUseCase(userId); - if (orderResult is SuccessApiResult>) { - _orderSubscription = orderResult.data.listen( - (order) { - emit(state.copyWith(order: order, isLoading: false, error: null)); + if (result is SuccessApiResult>>) { + _ordersSubscription = result.data.listen( + (orders) { + emit(state.copyWith( + orders: orders, + isLoading: false, + error: null, + )); }, onError: (error) { - emit(state.copyWith(error: error.toString(), isLoading: false)); + emit(state.copyWith( + isLoading: false, + error: error.toString(), + )); }, ); - } else if (orderResult is ErrorApiResult>) { - emit(state.copyWith(error: orderResult.error, isLoading: false)); + } else if (result is ErrorApiResult>>) { + emit(state.copyWith( + isLoading: false, + error: result.error, + )); } + } - /// -------- DRIVER -------- - final driverResult = driverUseCase(orderId); + void trackDriver(String driverId) { + final result = driverUseCase(driverId); - if (driverResult is SuccessApiResult>) { - _driverSubscription = driverResult.data.listen( - (driver) { - emit(state.copyWith(driver: driver, error: null)); - }, - onError: (error) { - emit(state.copyWith(error: error.toString(), isLoading: false)); - }, + if (result is SuccessApiResult>) { + _driverSubscription = result.data.listen( + (driver) => emit(state.copyWith(driver: driver)), + onError: (error) => + emit(state.copyWith(error: error.toString())), ); - } else if (driverResult is ErrorApiResult>) { - emit(state.copyWith(error: driverResult.error, isLoading: false)); } } @override Future close() async { - await _orderSubscription?.cancel(); + await _ordersSubscription?.cancel(); await _driverSubscription?.cancel(); return super.close(); } -} +} \ No newline at end of file diff --git a/lib/features/track_order/presentation/manager/cubit/track_order_state.dart b/lib/features/track_order/presentation/manager/cubit/track_order_state.dart index dd64de8..c706bd7 100644 --- a/lib/features/track_order/presentation/manager/cubit/track_order_state.dart +++ b/lib/features/track_order/presentation/manager/cubit/track_order_state.dart @@ -1,26 +1,25 @@ part of 'track_order_cubit.dart'; - class TrackOrderState extends Equatable { - final OrderEntity? order; + final List orders; final DriverEntity? driver; final bool isLoading; final String? error; const TrackOrderState({ - this.order, + this.orders = const [], this.driver, this.isLoading = false, this.error, }); TrackOrderState copyWith({ - OrderEntity? order, + List? orders, DriverEntity? driver, bool? isLoading, String? error, }) { return TrackOrderState( - order: order ?? this.order, + orders: orders ?? this.orders, driver: driver ?? this.driver, isLoading: isLoading ?? this.isLoading, error: error, @@ -28,5 +27,5 @@ class TrackOrderState extends Equatable { } @override - List get props => [order, driver, isLoading, error]; -} + List get props => [orders, driver, isLoading, error]; +} \ No newline at end of file diff --git a/lib/features/track_order/presentation/pages/track_order_page.dart b/lib/features/track_order/presentation/pages/track_order_page.dart index 8034eef..289dffe 100644 --- a/lib/features/track_order/presentation/pages/track_order_page.dart +++ b/lib/features/track_order/presentation/pages/track_order_page.dart @@ -1,16 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:tracking_app/features/track_order/presentation/manager/cubit/track_order_cubit.dart'; -import 'package:tracking_app/features/track_order/presentation/widgets/driver_section.dart'; -import 'package:tracking_app/features/track_order/presentation/widgets/order_section.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tracking_app/features/track_order/presentation/manager/cubit/track_order_cubit.dart'; class TrackOrderPage extends StatefulWidget { - final String orderId; - - const TrackOrderPage({ - super.key, - required this.orderId, - }); + const TrackOrderPage({super.key}); @override State createState() => _TrackOrderPageState(); @@ -20,21 +16,19 @@ class _TrackOrderPageState extends State { @override void initState() { super.initState(); - context.read().trackOrder(widget.orderId); + context.read().loadUserOrders(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Track Order'), + title: const Text('Track Orders'), ), body: BlocBuilder( builder: (context, state) { - if (state.isLoading && state.order == null) { - return const Center( - child: CircularProgressIndicator(), - ); + if (state.isLoading) { + return const Center(child: CircularProgressIndicator()); } if (state.error != null) { @@ -46,32 +40,85 @@ class _TrackOrderPageState extends State { ); } - return RefreshIndicator( - onRefresh: () async { - context - .read() - .trackOrder(widget.orderId); - }, - child: ListView( - padding: const EdgeInsets.all(16), - children: [ - if (state.order != null) - OrderSection(order: state.order!), - - const SizedBox(height: 20), + if (state.orders.isEmpty) { + return const Center( + child: Text('No orders found'), + ); + } - if (state.driver != null) - DriverSection(driver: state.driver!), + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: state.orders.length, + itemBuilder: (context, index) { + final order = state.orders[index]; - if (state.driver == null) - const Center( - child: Text("Waiting for driver assignment..."), + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: ListTile( + title: Text('Order ID: ${order.id}'), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Status: ${order.status}'), + Text('Total: \$${order.totalPrice ?? '-'}'), + ], ), - ], - ), + trailing: const Icon(Icons.arrow_forward_ios), + onTap: () { + if (order.driverId != null && + order.driverId!.isNotEmpty) { + context + .read() + .trackDriver(order.driverId!); + + _showDriverBottomSheet(context); + } + }, + ), + ); + }, ); }, ), ); } -} + + void _showDriverBottomSheet(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (_) { + return BlocBuilder( + builder: (context, state) { + if (state.driver == null) { + return const Padding( + padding: EdgeInsets.all(16), + child: Text('Driver not assigned yet'), + ); + } + + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Driver Info', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Text('Driver ID: ${state.driver!.id}'), + Text('Latitude: ${state.driver!.lat}'), + Text('Longitude: ${state.driver!.lng}'), + ], + ), + ); + }, + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/features/track_order/presentation/widgets/driver_section.dart b/lib/features/track_order/presentation/widgets/driver_section.dart index caf4650..7e639dd 100644 --- a/lib/features/track_order/presentation/widgets/driver_section.dart +++ b/lib/features/track_order/presentation/widgets/driver_section.dart @@ -1,30 +1,30 @@ -import 'package:flutter/material.dart'; +// import 'package:flutter/material.dart'; -class DriverSection extends StatelessWidget { - final dynamic driver; +// class DriverSection extends StatelessWidget { +// final dynamic driver; - const DriverSection({required this.driver}); +// const DriverSection({required this.driver}); - @override - Widget build(BuildContext context) { - return Card( - color: Colors.blue.shade50, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Driver Information", - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - Text("Driver ID: ${driver.id}"), - const SizedBox(height: 8), - Text("Phone: ${driver.phone ?? 'N/A'}"), - ], - ), - ), - ); - } -} +// @override +// Widget build(BuildContext context) { +// return Card( +// color: Colors.blue.shade50, +// child: Padding( +// padding: const EdgeInsets.all(16), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Text( +// "Driver Information", +// style: Theme.of(context).textTheme.titleMedium, +// ), +// const SizedBox(height: 8), +// Text("Driver ID: ${driver.id}"), +// const SizedBox(height: 8), +// Text("Phone: ${driver.phone ?? 'N/A'}"), +// ], +// ), +// ), +// ); +// } +// } diff --git a/lib/features/track_order/presentation/widgets/order_section.dart b/lib/features/track_order/presentation/widgets/order_section.dart index e29688a..8b55c57 100644 --- a/lib/features/track_order/presentation/widgets/order_section.dart +++ b/lib/features/track_order/presentation/widgets/order_section.dart @@ -1,29 +1,29 @@ -import 'package:flutter/material.dart'; +// import 'package:flutter/material.dart'; -class OrderSection extends StatelessWidget { - final dynamic order; +// class OrderSection extends StatelessWidget { +// final dynamic order; - const OrderSection({required this.order}); +// const OrderSection({required this.order}); - @override - Widget build(BuildContext context) { - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Order ID: ${order.id}", - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - Text("Status: ${order.status}"), - const SizedBox(height: 8), - Text("Total: ${order.totalPrice} EGP"), - ], - ), - ), - ); - } -} +// @override +// Widget build(BuildContext context) { +// return Card( +// child: Padding( +// padding: const EdgeInsets.all(16), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Text( +// "Order ID: ${order.id}", +// style: Theme.of(context).textTheme.titleMedium, +// ), +// const SizedBox(height: 8), +// Text("Status: ${order.status}"), +// const SizedBox(height: 8), +// Text("Total: ${order.totalPrice} EGP"), +// ], +// ), +// ), +// ); +// } +// } diff --git a/lib/main.dart b/lib/main.dart index 5281b8d..be1f06a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,7 +11,7 @@ import 'package:tracking_app/firebase_options.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); - configureDependencies(); + await configureDependencies(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); FirebaseMessaging.onBackgroundMessage( CloudMessaging.firebaseMessagingBackgroundHandler, diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index e884426..a0ed465 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import cloud_firestore import file_selector_macos +import firebase_auth import firebase_core import firebase_crashlytics import firebase_messaging @@ -18,6 +19,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 15f4f0e..7951f0d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -377,6 +377,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+5" + firebase_auth: + dependency: "direct dev" + description: + name: firebase_auth + sha256: b20d1540460814c5984474c1e9dd833bdbcff6ecd8d6ad86cc9da8cfd581c172 + url: "https://pub.dev" + source: hosted + version: "6.1.4" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: fd0225320b6bbc92460c86352d16b60aea15f9ef88292774cca97b0522ea9f72 + url: "https://pub.dev" + source: hosted + version: "8.1.6" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: be7dccb263b89fbda2a564de9d8193118196e8481ffb937222a025cdfdf82c40 + url: "https://pub.dev" + source: hosted + version: "6.1.2" firebase_core: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 1e23b9c..5406561 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,7 @@ dev_dependencies: bloc_test: ^10.0.0 build_runner: ^2.4.13 cloud_firestore: ^6.1.2 + firebase_auth: ^6.1.4 flutter_lints: ^6.0.0 flutter_test: sdk: flutter diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8e904a1..7b36576 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -17,6 +18,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FirebaseAuthPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); GeolocatorWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 8d3f745..2a1542b 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST cloud_firestore file_selector_windows + firebase_auth firebase_core geolocator_windows url_launcher_windows From 3c3bde1cfd8eb68f522859a56276e3e0c7d24cdc Mon Sep 17 00:00:00 2001 From: Rahma Ashraf Date: Fri, 27 Feb 2026 15:41:40 +0200 Subject: [PATCH 5/6] feat(SCRUM-92)refactor domain & data --- android/app/build.gradle.kts | 2 +- android/build.gradle.kts | 2 +- .../api/track_order_remote_source_impl.dart | 22 +++--- .../datasource/track_order_remote_source.dart | 2 +- .../data/models/track_order_model.dart | 37 ++++++++-- .../data/repos/track_order_repo_imp.dart | 70 ++++++++----------- .../domain/repos/track_order_repo.dart | 4 +- .../domain/usecases/track_order_usecase.dart | 4 +- .../manager/cubit/track_order_cubit.dart | 68 ++++++++++-------- lib/firebase_options.dart | 3 +- pubspec.lock | 8 +-- pubspec.yaml | 2 + 12 files changed, 127 insertions(+), 97 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 04ed454..b70e6a0 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -50,4 +50,4 @@ dependencies { flutter { source = "../.." -} +} \ No newline at end of file diff --git a/android/build.gradle.kts b/android/build.gradle.kts index dbee657..77b9add 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -21,4 +21,4 @@ subprojects { tasks.register("clean") { delete(rootProject.layout.buildDirectory) -} +} \ No newline at end of file diff --git a/lib/features/track_order/api/track_order_remote_source_impl.dart b/lib/features/track_order/api/track_order_remote_source_impl.dart index a05ade7..f5ff6ce 100644 --- a/lib/features/track_order/api/track_order_remote_source_impl.dart +++ b/lib/features/track_order/api/track_order_remote_source_impl.dart @@ -11,24 +11,26 @@ class TrackOrderRemoteDataSourceImpl implements TrackOrderRemoteDataSource { TrackOrderRemoteDataSourceImpl(this.firestore); @override - ApiResult> trackOrder(String orderId) { + ApiResult>> trackOrder(String userId) { try { final stream = firestore .collection('orders') - .doc(orderId) + .where( + Filter.or( + Filter('userAddress.user_id', isEqualTo: userId), + Filter('driver_id', isEqualTo: userId), + ), + ) .snapshots() .map((snapshot) { - final data = snapshot.data(); - if (data == null) { - throw Exception("Order not found"); - } - return TrackOrderModel.fromFirestore(snapshot.id, data); + return snapshot.docs + .map((doc) => TrackOrderModel.fromFirestore(doc.id, doc.data())) + .toList(); }); - return SuccessApiResult>(data: stream); + return SuccessApiResult>>(data: stream); } catch (e) { - return ErrorApiResult>(error: e.toString()); + return ErrorApiResult>>(error: e.toString()); } - ; } @override diff --git a/lib/features/track_order/data/datasource/track_order_remote_source.dart b/lib/features/track_order/data/datasource/track_order_remote_source.dart index 65276e0..ba75257 100644 --- a/lib/features/track_order/data/datasource/track_order_remote_source.dart +++ b/lib/features/track_order/data/datasource/track_order_remote_source.dart @@ -4,7 +4,7 @@ import 'package:tracking_app/features/track_order/data/models/track_order_model. import 'package:cloud_firestore/cloud_firestore.dart'; abstract class TrackOrderRemoteDataSource { - ApiResult> trackOrder(String orderId); + ApiResult>> trackOrder(String userId); ApiResult> trackDriver(String driverId); Future>> updateOrderStatus(String orderId, String status); } diff --git a/lib/features/track_order/data/models/track_order_model.dart b/lib/features/track_order/data/models/track_order_model.dart index 4de0d22..0fbfb37 100644 --- a/lib/features/track_order/data/models/track_order_model.dart +++ b/lib/features/track_order/data/models/track_order_model.dart @@ -13,13 +13,38 @@ class TrackOrderModel { required this.userId, }); - factory TrackOrderModel.fromFirestore(String id, Map data) { + factory TrackOrderModel.fromFirestore(String id, Map data) { + String safeString(dynamic value) { + if (value == null) return ''; + if (value is String) return value; + return value.toString(); + } + + dynamic userAddress = data['userAddress']; + String parsedUserId = ''; + if (userAddress is Map) { + parsedUserId = safeString(userAddress['user_id']); + } else { + parsedUserId = safeString(data['userId']); + } + + dynamic orderDt = data['oder_dt']; + String parsedStatus = ''; + String parsedTotal = ''; + if (orderDt is Map) { + parsedStatus = safeString(orderDt['status']); + parsedTotal = safeString(orderDt['totalPrice']); + } else { + parsedStatus = safeString(data['status']); + parsedTotal = safeString(data['totalPrice']); + } + return TrackOrderModel( id: id, - driverId: data['driverId'] ?? '', - status: data['status'] ?? '', - totalPrice: data['totalPrice'] ?? '', - userId: data['userId'] ?? '', + driverId: safeString(data['driver_id'] ?? data['driverId']), + status: parsedStatus, + totalPrice: parsedTotal, + userId: parsedUserId, ); } -} \ No newline at end of file +} diff --git a/lib/features/track_order/data/repos/track_order_repo_imp.dart b/lib/features/track_order/data/repos/track_order_repo_imp.dart index 10b3f32..21141c4 100644 --- a/lib/features/track_order/data/repos/track_order_repo_imp.dart +++ b/lib/features/track_order/data/repos/track_order_repo_imp.dart @@ -17,56 +17,44 @@ class TrackOrderRepoImpl implements TrackOrderRepo { ApiResult>> trackOrder(String userId) { final result = remoteDataSource.trackOrder(userId); - if (result is SuccessApiResult>>) { - final successResult = result as SuccessApiResult>>; - final entityStream = successResult.data.map( - (models) => models - .map( - (model) => OrderEntity( - id: model.id, - userId: model.userId, - status: model.status, - driverId: model.driverId, - totalPrice: model.totalPrice, - ), - ) - .toList(), - ); - - return SuccessApiResult(data: entityStream); - } - - if (result is ErrorApiResult>>) { - final errorResult = result as ErrorApiResult>>; - return ErrorApiResult(error: errorResult.error); - } + return switch (result) { + SuccessApiResult() => SuccessApiResult( + data: (result.data as Stream>).map( + (models) => models + .map( + (model) => OrderEntity( + id: model.id, + userId: model.userId, + status: model.status, + driverId: model.driverId, + totalPrice: model.totalPrice, + ), + ) + .toList(), + ), + ), - throw Exception("Unhandled ApiResult type"); + ErrorApiResult() => ErrorApiResult(error: result.error), + }; } @override ApiResult> trackOrderWithDriver(String driverId) { final result = remoteDataSource.trackDriver(driverId); - if (result is SuccessApiResult>) { - final successResult = result as SuccessApiResult>; - final entityStream = successResult.data.map( - (model) => DriverEntity( - id: model.id, - lat: model.lat, - lng: model.lng, + return switch (result) { + SuccessApiResult() => SuccessApiResult( + data: (result.data as Stream).map( + (model) => DriverEntity( + id: model.id, + lat: model.lat, + lng: model.lng, + ), + ), ), - ); - - return SuccessApiResult(data: entityStream); - } - - if (result is ErrorApiResult>) { - final errorResult = result as ErrorApiResult>; - return ErrorApiResult(error: errorResult.error); - } - throw Exception("Unhandled ApiResult type"); + ErrorApiResult() => ErrorApiResult(error: result.error), + }; } @override diff --git a/lib/features/track_order/domain/repos/track_order_repo.dart b/lib/features/track_order/domain/repos/track_order_repo.dart index 592cbbe..4189859 100644 --- a/lib/features/track_order/domain/repos/track_order_repo.dart +++ b/lib/features/track_order/domain/repos/track_order_repo.dart @@ -3,7 +3,7 @@ import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; abstract class TrackOrderRepo { - ApiResult>> trackOrder(String orderId); - ApiResult> trackOrderWithDriver(String orderId); + ApiResult>> trackOrder(String userId); + ApiResult> trackOrderWithDriver(String driverId); Future updateOrderStatus(String orderId, String status); } diff --git a/lib/features/track_order/domain/usecases/track_order_usecase.dart b/lib/features/track_order/domain/usecases/track_order_usecase.dart index 1b1f9d1..9326760 100644 --- a/lib/features/track_order/domain/usecases/track_order_usecase.dart +++ b/lib/features/track_order/domain/usecases/track_order_usecase.dart @@ -9,6 +9,6 @@ class TrackOrderUseCase { TrackOrderUseCase(this.repository); - ApiResult>> call(orderId) => - repository.trackOrder(orderId); + ApiResult>> call(String userId) => + repository.trackOrder(userId); } diff --git a/lib/features/track_order/presentation/manager/cubit/track_order_cubit.dart b/lib/features/track_order/presentation/manager/cubit/track_order_cubit.dart index 128fba3..11b78d9 100644 --- a/lib/features/track_order/presentation/manager/cubit/track_order_cubit.dart +++ b/lib/features/track_order/presentation/manager/cubit/track_order_cubit.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:injectable/injectable.dart'; @@ -14,55 +15,67 @@ part 'track_order_state.dart'; @injectable class TrackOrderCubit extends Cubit { - final TrackOrderUseCase trackOrderUseCase; + final TrackOrderUseCase trackOrderUseCase; final TrackDriverUseCase driverUseCase; final AuthStorage authStorage; StreamSubscription>? _ordersSubscription; StreamSubscription? _driverSubscription; - TrackOrderCubit( - this.trackOrderUseCase, - this.driverUseCase, - this.authStorage, - ) : super(const TrackOrderState()); + TrackOrderCubit(this.trackOrderUseCase, this.driverUseCase, this.authStorage) + : super(const TrackOrderState()); Future loadUserOrders() async { emit(state.copyWith(isLoading: true, error: null)); - final userId = await authStorage.getToken(); + final token = await authStorage.getToken(); + print('DEBUG: loadUserOrders called with string length: ${token?.length}'); - if (userId == null) { - emit(state.copyWith( - isLoading: false, - error: "User not logged in", - )); + if (token == null) { + emit(state.copyWith(isLoading: false, error: "User not logged in")); return; } + String userId; + try { + final parts = token.split('.'); + if (parts.length != 3) throw Exception('Invalid token'); + String payload = parts[1]; + payload = payload.replaceAll('-', '+').replaceAll('_', '/'); + switch (payload.length % 4) { + case 0: break; + case 2: payload += '=='; break; + case 3: payload += '='; break; + default: throw Exception('Illegal base64url string!'); + } + final decoded = utf8.decode(base64Decode(payload)); + final Map data = jsonDecode(decoded); + userId = data['userId'] ?? data['id'] ?? data['user'] ?? data['driver'] ?? token; + print('DEBUG: Decoded ID from payload: $userId'); + } catch (e) { + print('DEBUG: Token decode error: $e'); + userId = token; + } + final result = trackOrderUseCase(userId); if (result is SuccessApiResult>>) { + print('DEBUG: Successfully subscribed to track orders stream'); _ordersSubscription = result.data.listen( (orders) { - emit(state.copyWith( - orders: orders, - isLoading: false, - error: null, - )); + print( + 'DEBUG: Stream emitted new orders list. Count: ${orders.length}', + ); + emit(state.copyWith(orders: orders, isLoading: false, error: null)); }, onError: (error) { - emit(state.copyWith( - isLoading: false, - error: error.toString(), - )); + print('DEBUG: Stream error: $error'); + emit(state.copyWith(isLoading: false, error: error.toString())); }, ); } else if (result is ErrorApiResult>>) { - emit(state.copyWith( - isLoading: false, - error: result.error, - )); + print('DEBUG: ApiResult Error: ${result.error}'); + emit(state.copyWith(isLoading: false, error: result.error)); } } @@ -72,8 +85,7 @@ class TrackOrderCubit extends Cubit { if (result is SuccessApiResult>) { _driverSubscription = result.data.listen( (driver) => emit(state.copyWith(driver: driver)), - onError: (error) => - emit(state.copyWith(error: error.toString())), + onError: (error) => emit(state.copyWith(error: error.toString())), ); } } @@ -84,4 +96,4 @@ class TrackOrderCubit extends Cubit { await _driverSubscription?.cancel(); return super.close(); } -} \ No newline at end of file +} diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index f4c5a20..bf2f980 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -71,4 +71,5 @@ class DefaultFirebaseOptions { authDomain: 'elevate-flower-app.firebaseapp.com', storageBucket: 'elevate-flower-app.firebasestorage.app', ); -} + +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 7951f0d..4d8c251 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -498,10 +498,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "76cd20bcfa72fabe50ea27eeaf165527f446f55d3033021462084b87805b4cac" + sha256: "2b50e938a275e1ad77352d6a25e25770f4130baa61eaf02de7a9a884680954ad" url: "https://pub.dev" source: hosted - version: "20.0.0" + version: "20.1.0" flutter_local_notifications_linux: dependency: transitive description: @@ -522,10 +522,10 @@ packages: dependency: transitive description: name: flutter_local_notifications_windows - sha256: "7ddd964fa85b6a23e96956c5b63ef55cdb9e5947b71b95712204db42ad46da61" + sha256: e97a1a3016512437d9c0b12fae7d1491c3c7b9aa7f03a69b974308840656b02a url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" flutter_localizations: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 5406561..4a7cd05 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,7 +43,9 @@ dev_dependencies: build_runner: ^2.4.13 cloud_firestore: ^6.1.2 firebase_auth: ^6.1.4 + firebase_messaging: ^16.1.1 flutter_lints: ^6.0.0 + flutter_local_notifications: ^20.1.0 flutter_test: sdk: flutter injectable_generator: ^2.4.1 From 69ca13942b49d547fdc45e2d02c032e5f2b39324 Mon Sep 17 00:00:00 2001 From: Rahma Ashraf Date: Fri, 27 Feb 2026 16:56:55 +0200 Subject: [PATCH 6/6] feat(SCRUM-92)unit test for track order feature --- .../track_order/domain/repos/track_data.dart | 8 - .../track_order_remote_source_impl_test.dart | 164 ++++++++++++++++++ .../data/models/driver_model_test.dart | 44 +++++ .../data/models/track_order_model_test.dart | 73 ++++++++ .../data/repos/track_order_repo_imp_test.dart | 102 +++++++++++ .../domain/entities/driver_entity_test.dart | 32 ++++ .../domain/entities/order_entity_test.dart | 60 +++++++ .../domain/usecases/driver_usecase_test.dart | 49 ++++++ .../usecases/track_order_usecase_test.dart | 48 +++++ .../manager/cubit/track_order_cubit_test.dart | 123 +++++++++++++ 10 files changed, 695 insertions(+), 8 deletions(-) delete mode 100644 lib/features/track_order/domain/repos/track_data.dart create mode 100644 test/features/track_order/api/track_order_remote_source_impl_test.dart create mode 100644 test/features/track_order/data/models/driver_model_test.dart create mode 100644 test/features/track_order/data/models/track_order_model_test.dart create mode 100644 test/features/track_order/data/repos/track_order_repo_imp_test.dart create mode 100644 test/features/track_order/domain/entities/driver_entity_test.dart create mode 100644 test/features/track_order/domain/entities/order_entity_test.dart create mode 100644 test/features/track_order/domain/usecases/driver_usecase_test.dart create mode 100644 test/features/track_order/domain/usecases/track_order_usecase_test.dart create mode 100644 test/features/track_order/presentation/manager/cubit/track_order_cubit_test.dart diff --git a/lib/features/track_order/domain/repos/track_data.dart b/lib/features/track_order/domain/repos/track_data.dart deleted file mode 100644 index 71ada16..0000000 --- a/lib/features/track_order/domain/repos/track_data.dart +++ /dev/null @@ -1,8 +0,0 @@ -// import 'package:tracking_app/features/track_order/data/models/driver_model.dart'; -// import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; - -// class TrackingData { -// final TrackOrderModel order; -// final DriverModel driver; -// TrackingData({required this.order, required this.driver}); -// } diff --git a/test/features/track_order/api/track_order_remote_source_impl_test.dart b/test/features/track_order/api/track_order_remote_source_impl_test.dart new file mode 100644 index 0000000..85109ea --- /dev/null +++ b/test/features/track_order/api/track_order_remote_source_impl_test.dart @@ -0,0 +1,164 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/track_order/api/track_order_remote_source_impl.dart'; +import 'package:tracking_app/features/track_order/data/datasource/track_order_remote_source.dart'; +import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; +import 'package:tracking_app/features/track_order/data/models/driver_model.dart'; + + +/// ---------------- MOCKS ---------------- + +class MockFirebaseFirestore extends Mock implements FirebaseFirestore {} +class MockCollectionReference extends Mock + implements CollectionReference> {} + +class MockQuery extends Mock + implements Query> {} + +class MockQuerySnapshot extends Mock + implements QuerySnapshot> {} + +class MockQueryDocumentSnapshot extends Mock + implements QueryDocumentSnapshot> {} + +class MockDocumentReference extends Mock + implements DocumentReference> {} + +class MockDocumentSnapshot extends Mock + implements DocumentSnapshot> {} + +/// ---------------------------------------- + +void main() { + late MockFirebaseFirestore mockFirestore; + late TrackOrderRemoteDataSourceImpl dataSource; + + setUp(() { + mockFirestore = MockFirebaseFirestore(); + dataSource = TrackOrderRemoteDataSourceImpl(mockFirestore); + }); + + group('trackOrder', () { + test('returns SuccessApiResult with mapped models', () async { + final mockCollection = MockCollectionReference(); + final mockQuery = MockQuery(); + final mockSnapshot = MockQuerySnapshot(); + final mockDoc = MockQueryDocumentSnapshot(); + + when(() => mockFirestore.collection('orders')) + .thenReturn(mockCollection); + + when(() => mockCollection.where(any())) + .thenReturn(mockQuery); + + when(() => mockQuery.snapshots()) + .thenAnswer((_) => Stream.value(mockSnapshot)); + + when(() => mockSnapshot.docs) + .thenReturn([mockDoc]); + + when(() => mockDoc.id).thenReturn('1'); + + when(() => mockDoc.data()).thenReturn({ + 'status': 'delivered', + 'driver_id': 'd1', + 'total_price': 100, + 'userAddress': {'user_id': 'u1'} + }); + + final result = dataSource.trackOrder('u1'); + + expect(result, isA()); + + final stream = (result as SuccessApiResult).data; + + final list = await stream.first; + + expect(list, isA>()); + expect(list.length, 1); + expect(list.first.id, '1'); + }); + + test('returns ErrorApiResult when firestore throws', () { + when(() => mockFirestore.collection('orders')) + .thenThrow(Exception('Firestore error')); + + final result = dataSource.trackOrder('u1'); + + expect(result, isA()); + }); + }); + + group('trackDriver', () { + test('returns SuccessApiResult with driver model', () async { + final mockCollection = MockCollectionReference(); + final mockDocRef = MockDocumentReference(); + final mockSnapshot = MockDocumentSnapshot(); + + when(() => mockFirestore.collection('drivers')) + .thenReturn(mockCollection); + + when(() => mockCollection.doc('d1')) + .thenReturn(mockDocRef); + + when(() => mockDocRef.snapshots()) + .thenAnswer((_) => Stream.value(mockSnapshot)); + + when(() => mockSnapshot.id).thenReturn('d1'); + + when(() => mockSnapshot.data()).thenReturn({ + 'lat': 30.0, + 'lng': 31.0, + }); + + final result = dataSource.trackDriver('d1'); + + expect(result, isA()); + + final stream = (result as SuccessApiResult).data; + final driver = await stream.first; + + expect(driver, isA()); + expect(driver.id, 'd1'); + }); + + test('returns ErrorApiResult if firestore throws', () { + when(() => mockFirestore.collection('drivers')) + .thenThrow(Exception('Error')); + + final result = dataSource.trackDriver('d1'); + + expect(result, isA()); + }); + }); + + group('updateOrderStatus', () { + test('updates order and returns document snapshot', () async { + final mockCollection = MockCollectionReference(); + final mockDocRef = MockDocumentReference(); + final mockSnapshot = MockDocumentSnapshot(); + + when(() => mockFirestore.collection('orders')) + .thenReturn(mockCollection); + + when(() => mockCollection.doc('1')) + .thenReturn(mockDocRef); + + when(() => mockDocRef.update(any())) + .thenAnswer((_) async {}); + + when(() => mockDocRef.get()) + .thenAnswer((_) async => mockSnapshot); + + final result = + await dataSource.updateOrderStatus('1', 'delivered'); + + expect(result, mockSnapshot); + + verify(() => mockDocRef.update({'status': 'delivered'})) + .called(1); + }); + }); +} \ No newline at end of file diff --git a/test/features/track_order/data/models/driver_model_test.dart b/test/features/track_order/data/models/driver_model_test.dart new file mode 100644 index 0000000..24f9457 --- /dev/null +++ b/test/features/track_order/data/models/driver_model_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/track_order/data/models/driver_model.dart'; + +void main() { + group('DriverModel.fromFirestore', () { + + test('creates DriverModel correctly from map', () { + final data = { + 'lat': 30.5, + 'lng': 31.2, + }; + + final model = DriverModel.fromFirestore('driver1', data); + + expect(model.id, 'driver1'); + expect(model.lat, 30.5); + expect(model.lng, 31.2); + }); + + test('converts int to double', () { + final data = { + 'lat': 30, + 'lng': 31, + }; + + final model = DriverModel.fromFirestore('driver2', data); + + expect(model.lat, 30.0); + expect(model.lng, 31.0); + }); + + test('throws error if lat is missing', () { + final data = { + 'lng': 31, + }; + + expect( + () => DriverModel.fromFirestore('driver3', data), + throwsA(isA()), + ); + }); + + }); +} \ No newline at end of file diff --git a/test/features/track_order/data/models/track_order_model_test.dart b/test/features/track_order/data/models/track_order_model_test.dart new file mode 100644 index 0000000..fac3d47 --- /dev/null +++ b/test/features/track_order/data/models/track_order_model_test.dart @@ -0,0 +1,73 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; + +void main() { + group('TrackOrderModel.fromFirestore', () { + + test('parses flat structure correctly', () { + final data = { + 'driver_id': 'driver1', + 'status': 'on_the_way', + 'totalPrice': '200', + 'userId': 'user1', + }; + + final model = TrackOrderModel.fromFirestore('order1', data); + + expect(model.id, 'order1'); + expect(model.driverId, 'driver1'); + expect(model.status, 'on_the_way'); + expect(model.totalPrice, '200'); + expect(model.userId, 'user1'); + }); + + test('parses nested structure correctly', () { + final data = { + 'driverId': 'driver2', + 'userAddress': { + 'user_id': 'user2', + }, + 'oder_dt': { + 'status': 'delivered', + 'totalPrice': 350, + } + }; + + final model = TrackOrderModel.fromFirestore('order2', data); + + expect(model.id, 'order2'); + expect(model.driverId, 'driver2'); + expect(model.status, 'delivered'); + expect(model.totalPrice, '350'); // int converted to string + expect(model.userId, 'user2'); + }); + + test('handles null values safely', () { + final data = { + 'driver_id': null, + 'status': null, + 'totalPrice': null, + 'userId': null, + }; + + final model = TrackOrderModel.fromFirestore('order3', data); + + expect(model.driverId, ''); + expect(model.status, ''); + expect(model.totalPrice, ''); + expect(model.userId, ''); + }); + + test('handles missing nested maps', () { + final data = {}; + + final model = TrackOrderModel.fromFirestore('order4', data); + + expect(model.driverId, ''); + expect(model.status, ''); + expect(model.totalPrice, ''); + expect(model.userId, ''); + }); + + }); +} \ No newline at end of file diff --git a/test/features/track_order/data/repos/track_order_repo_imp_test.dart b/test/features/track_order/data/repos/track_order_repo_imp_test.dart new file mode 100644 index 0000000..9dd1779 --- /dev/null +++ b/test/features/track_order/data/repos/track_order_repo_imp_test.dart @@ -0,0 +1,102 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/track_order/data/models/driver_model.dart'; +import 'package:tracking_app/features/track_order/data/models/track_order_model.dart'; +import 'package:tracking_app/features/track_order/data/repos/track_order_repo_imp.dart'; +import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; +import 'package:tracking_app/features/track_order/data/datasource/track_order_remote_source.dart'; + +import '../../api/track_order_remote_source_impl_test.dart'; + +class MockRemoteDataSource extends Mock implements TrackOrderRemoteDataSource {} + +void main() { + late MockRemoteDataSource mockRemote; + late TrackOrderRepoImpl repo; + + setUp(() { + mockRemote = MockRemoteDataSource(); + repo = TrackOrderRepoImpl(mockRemote); + }); + + group('trackOrder', () { + test('returns SuccessApiResult with mapped OrderEntity', () async { + final model = TrackOrderModel( + id: 'o1', + userId: 'u1', + driverId: 'd1', + status: 'delivered', + totalPrice: '100', + ); + + when(() => mockRemote.trackOrder('u1')).thenReturn( + SuccessApiResult(data: Stream.value([model])), + ); + + final result = repo.trackOrder('u1'); + + expect(result, isA>>>()); + + final list = await (result as SuccessApiResult).data.first; + + expect(list.length, 1); + expect(list.first.id, 'o1'); + expect(list.first.userId, 'u1'); + }); + + test('returns ErrorApiResult if remote fails', () { + when(() => mockRemote.trackOrder('u1')).thenReturn( + ErrorApiResult(error: 'Network Error'), + ); + + final result = repo.trackOrder('u1'); + + expect(result, isA()); + expect((result as ErrorApiResult).error, 'Network Error'); + }); + }); + + group('trackOrderWithDriver', () { + test('returns SuccessApiResult with mapped DriverEntity', () async { + final model = DriverModel(id: 'd1', lat: 10.0, lng: 20.0); + + when(() => mockRemote.trackDriver('d1')).thenReturn( + SuccessApiResult(data: Stream.value(model)), + ); + + final result = repo.trackOrderWithDriver('d1'); + + expect(result, isA>>()); + + final driver = await (result as SuccessApiResult).data.first; + + expect(driver.id, 'd1'); + expect(driver.lat, 10.0); + expect(driver.lng, 20.0); + }); + + test('returns ErrorApiResult if remote fails', () { + when(() => mockRemote.trackDriver('d1')).thenReturn( + ErrorApiResult(error: 'Driver not found'), + ); + + final result = repo.trackOrderWithDriver('d1'); + + expect(result, isA()); + expect((result as ErrorApiResult).error, 'Driver not found'); + }); + }); + + group('updateOrderStatus', () { + test('calls remoteDataSource.updateOrderStatus', () async { + when(() => mockRemote.updateOrderStatus('o1', 'delivered')) + .thenAnswer((_) async =>MockDocumentSnapshot()); + + await repo.updateOrderStatus('o1', 'delivered'); + + verify(() => mockRemote.updateOrderStatus('o1', 'delivered')).called(1); + }); + }); +} \ No newline at end of file diff --git a/test/features/track_order/domain/entities/driver_entity_test.dart b/test/features/track_order/domain/entities/driver_entity_test.dart new file mode 100644 index 0000000..a290327 --- /dev/null +++ b/test/features/track_order/domain/entities/driver_entity_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; + +void main() { + group('DriverEntity', () { + test('should create a DriverEntity with correct values', () { + // Arrange + const id = 'driver1'; + const lat = 10.5; + const lng = 20.3; + + // Act + final driver = DriverEntity(id: id, lat: lat, lng: lng); + + // Assert + expect(driver.id, id); + expect(driver.lat, lat); + expect(driver.lng, lng); + }); + + test('should be immutable', () { + final driver = DriverEntity(id: 'd1', lat: 0.0, lng: 0.0); + + // Attempting to modify fields should fail + // (Since fields are final, Dart will throw a compile-time error) + // So just check that fields are final by reading them + expect(driver.id, 'd1'); + expect(driver.lat, 0.0); + expect(driver.lng, 0.0); + }); + }); +} \ No newline at end of file diff --git a/test/features/track_order/domain/entities/order_entity_test.dart b/test/features/track_order/domain/entities/order_entity_test.dart new file mode 100644 index 0000000..fbcf853 --- /dev/null +++ b/test/features/track_order/domain/entities/order_entity_test.dart @@ -0,0 +1,60 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; + +void main() { + group('OrderEntity', () { + test('should create an OrderEntity with all fields', () { + // Arrange + const id = 'o1'; + const userId = 'u1'; + const status = 'delivered'; + const driverId = 'd1'; + const totalPrice = '100'; + const address = '123 Street'; + const name = 'John Doe'; + + // Act + final order = OrderEntity( + id: id, + userId: userId, + status: status, + driverId: driverId, + totalPrice: totalPrice, + address: address, + name: name, + ); + + // Assert + expect(order.id, id); + expect(order.userId, userId); + expect(order.status, status); + expect(order.driverId, driverId); + expect(order.totalPrice, totalPrice); + expect(order.address, address); + expect(order.name, name); + }); + + test('should create an OrderEntity with only required fields', () { + // Arrange + const id = 'o2'; + const userId = 'u2'; + const status = 'pending'; + + // Act + final order = OrderEntity( + id: id, + userId: userId, + status: status, + ); + + // Assert + expect(order.id, id); + expect(order.userId, userId); + expect(order.status, status); + expect(order.driverId, isNull); + expect(order.totalPrice, isNull); + expect(order.address, isNull); + expect(order.name, isNull); + }); + }); +} \ No newline at end of file diff --git a/test/features/track_order/domain/usecases/driver_usecase_test.dart b/test/features/track_order/domain/usecases/driver_usecase_test.dart new file mode 100644 index 0000000..d066c2d --- /dev/null +++ b/test/features/track_order/domain/usecases/driver_usecase_test.dart @@ -0,0 +1,49 @@ +// test/features/track_order/domain/usecases/track_driver_usecase_test.dart + +import 'dart:async'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; +import 'package:tracking_app/features/track_order/domain/usecases/driver_usecase.dart'; +import 'package:tracking_app/features/track_order/domain/repos/track_order_repo.dart'; + +class MockTrackOrderRepo extends Mock implements TrackOrderRepo {} + +void main() { + late MockTrackOrderRepo mockRepo; + late TrackDriverUseCase useCase; + + setUp(() { + mockRepo = MockTrackOrderRepo(); + useCase = TrackDriverUseCase(mockRepo); + }); + + group('TrackDriverUseCase', () { + final driver = DriverEntity(id: 'd1', lat: 10.0, lng: 20.0); + + test('returns SuccessApiResult with driver stream', () async { + when(() => mockRepo.trackOrderWithDriver('d1')) + .thenReturn(SuccessApiResult(data: Stream.value(driver))); + + final result = useCase.call('d1'); + + expect(result, isA>>()); + + final d = await (result as SuccessApiResult).data.first; + expect(d.id, 'd1'); + expect(d.lat, 10.0); + expect(d.lng, 20.0); + }); + + test('returns ErrorApiResult when repository fails', () { + when(() => mockRepo.trackOrderWithDriver('d1')) + .thenReturn(ErrorApiResult(error: 'Driver not found')); + + final result = useCase.call('d1'); + + expect(result, isA()); + expect((result as ErrorApiResult).error, 'Driver not found'); + }); + }); +} \ No newline at end of file diff --git a/test/features/track_order/domain/usecases/track_order_usecase_test.dart b/test/features/track_order/domain/usecases/track_order_usecase_test.dart new file mode 100644 index 0000000..0ad6044 --- /dev/null +++ b/test/features/track_order/domain/usecases/track_order_usecase_test.dart @@ -0,0 +1,48 @@ +// test/features/track_order/domain/usecases/track_order_usecase_test.dart + +import 'dart:async'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; +import 'package:tracking_app/features/track_order/domain/usecases/track_order_usecase.dart'; +import 'package:tracking_app/features/track_order/domain/repos/track_order_repo.dart'; + +class MockTrackOrderRepo extends Mock implements TrackOrderRepo {} + +void main() { + late MockTrackOrderRepo mockRepo; + late TrackOrderUseCase useCase; + + setUp(() { + mockRepo = MockTrackOrderRepo(); + useCase = TrackOrderUseCase(mockRepo); + }); + + group('TrackOrderUseCase', () { + final orders = [OrderEntity(id: 'o1', userId: 'u1', status: 'delivered')]; + + test('returns SuccessApiResult with orders stream', () async { + when(() => mockRepo.trackOrder('u1')) + .thenReturn(SuccessApiResult(data: Stream.value(orders))); + + final result = useCase.call('u1'); + + expect(result, isA>>>()); + + final list = await (result as SuccessApiResult).data.first; + expect(list.length, 1); + expect(list.first.id, 'o1'); + }); + + test('returns ErrorApiResult when repository fails', () { + when(() => mockRepo.trackOrder('u1')) + .thenReturn(ErrorApiResult(error: 'Network Error')); + + final result = useCase.call('u1'); + + expect(result, isA()); + expect((result as ErrorApiResult).error, 'Network Error'); + }); + }); +} \ No newline at end of file diff --git a/test/features/track_order/presentation/manager/cubit/track_order_cubit_test.dart b/test/features/track_order/presentation/manager/cubit/track_order_cubit_test.dart new file mode 100644 index 0000000..e81e2c8 --- /dev/null +++ b/test/features/track_order/presentation/manager/cubit/track_order_cubit_test.dart @@ -0,0 +1,123 @@ +import 'dart:async'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/features/track_order/domain/entities/order_entity.dart'; +import 'package:tracking_app/features/track_order/domain/entities/driver_entity.dart'; +import 'package:tracking_app/features/track_order/domain/usecases/track_order_usecase.dart'; +import 'package:tracking_app/features/track_order/domain/usecases/driver_usecase.dart'; +import 'package:tracking_app/features/track_order/presentation/manager/cubit/track_order_cubit.dart'; + +class MockTrackOrderUseCase extends Mock implements TrackOrderUseCase {} +class MockTrackDriverUseCase extends Mock implements TrackDriverUseCase {} +class MockAuthStorage extends Mock implements AuthStorage {} + +void main() { + late MockTrackOrderUseCase mockTrackOrderUseCase; + late MockTrackDriverUseCase mockTrackDriverUseCase; + late MockAuthStorage mockAuthStorage; + late TrackOrderCubit cubit; + + setUp(() { + mockTrackOrderUseCase = MockTrackOrderUseCase(); + mockTrackDriverUseCase = MockTrackDriverUseCase(); + mockAuthStorage = MockAuthStorage(); + + cubit = TrackOrderCubit( + mockTrackOrderUseCase, + mockTrackDriverUseCase, + mockAuthStorage, + ); + }); + + tearDown(() async { + await cubit.close(); + }); + + group('loadUserOrders', () { + final order = OrderEntity(id: 'o1', userId: 'u1', status: 'delivered'); + final ordersStream = Stream.value([order]); + + test('emits error if token is null', () async { + when(() => mockAuthStorage.getToken()).thenAnswer((_) async => null); + + await cubit.loadUserOrders(); + + expect(cubit.state.isLoading, false); + expect(cubit.state.error, 'User not logged in'); + expect(cubit.state.orders, []); + }); + + test('emits orders when SuccessApiResult is returned', () async { + when(() => mockAuthStorage.getToken()).thenAnswer((_) async => 'dummy.token.value'); + when(() => mockTrackOrderUseCase.call(any())) + .thenReturn(SuccessApiResult(data: ordersStream)); + + await cubit.loadUserOrders(); + + final emittedOrders = await cubit.stream.first; + expect(emittedOrders.orders.length, 1); + expect(emittedOrders.orders.first.id, 'o1'); + }); + + test('emits error when ErrorApiResult is returned', () async { + when(() => mockAuthStorage.getToken()).thenAnswer((_) async => 'dummy.token.value'); + when(() => mockTrackOrderUseCase.call(any())) + .thenReturn(ErrorApiResult(error: 'Network Error')); + + await cubit.loadUserOrders(); + + expect(cubit.state.isLoading, false); + expect(cubit.state.error, 'Network Error'); + expect(cubit.state.orders, []); + }); + }); + + group('trackDriver', () { + final driver = DriverEntity(id: 'd1', lat: 10.0, lng: 20.0); + final driverStream = Stream.value(driver); + + test('emits driver when SuccessApiResult is returned', () async { + when(() => mockTrackDriverUseCase.call('d1')) + .thenReturn(SuccessApiResult(data: driverStream)); + + cubit.trackDriver('d1'); + + final emittedState = await cubit.stream.first; + expect(emittedState.driver, isNotNull); + expect(emittedState.driver!.id, 'd1'); + expect(emittedState.driver!.lat, 10.0); + expect(emittedState.driver!.lng, 20.0); + }); + + test('emits error if stream has error', () async { + final errorStream = Stream.error('Driver not found'); + + when(() => mockTrackDriverUseCase.call('d1')) + .thenReturn(SuccessApiResult(data: errorStream)); + + cubit.trackDriver('d1'); + + final emittedState = await cubit.stream.first; + expect(emittedState.error, 'Driver not found'); + }); + }); + + test('close cancels subscriptions', () async { + final orderStream = Stream.value([OrderEntity(id: 'o1', userId: 'u1', status: 'delivered')]); + final driverStream = Stream.value(DriverEntity(id: 'd1', lat: 10, lng: 20)); + + when(() => mockAuthStorage.getToken()).thenAnswer((_) async => 'token'); + when(() => mockTrackOrderUseCase.call(any())) + .thenReturn(SuccessApiResult(data: orderStream)); + when(() => mockTrackDriverUseCase.call(any())) + .thenReturn(SuccessApiResult(data: driverStream)); + + await cubit.loadUserOrders(); + cubit.trackDriver('d1'); + + await cubit.close(); + expect(cubit.isClosed, true); + }); +} \ No newline at end of file