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/app/config/di/di.config.dart b/lib/app/config/di/di.config.dart index 98a5274..e735250 100644 --- a/lib/app/config/di/di.config.dart +++ b/lib/app/config/di/di.config.dart @@ -8,7 +8,9 @@ // 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:firebase_auth/firebase_auth.dart' as _i59; import 'package:get_it/get_it.dart' as _i174; import 'package:injectable/injectable.dart' as _i526; @@ -48,7 +50,22 @@ 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 '../../core/network/firebase_module.dart' as _i236; import '../auth_storage/auth_storage.dart' as _i603; import '../network/network_module.dart' as _i200; @@ -63,13 +80,29 @@ 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>(() => + _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<_i810.TrackOrderUseCase>(), + gh<_i866.TrackDriverUseCase>(), + gh<_i603.AuthStorage>(), + )); gh.lazySingleton<_i890.ApiClient>( () => networkModule.authApiClient(gh<_i361.Dio>())); gh.factory<_i708.AuthRemoteDataSource>( @@ -128,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 a56daf9..bdd2db1 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(), + child: TrackOrderPage(), + ), + ), ], + redirect: (context, state) async { final token = await getIt().getToken(); final rememberMe = await getIt().getRememberMe(); 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/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 new file mode 100644 index 0000000..f5ff6ce --- /dev/null +++ b/lib/features/track_order/api/track_order_remote_source_impl.dart @@ -0,0 +1,69 @@ +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'; +import 'package:cloud_firestore/cloud_firestore.dart'; + +@Injectable(as: TrackOrderRemoteDataSource) +class TrackOrderRemoteDataSourceImpl implements TrackOrderRemoteDataSource { + final FirebaseFirestore firestore; + + TrackOrderRemoteDataSourceImpl(this.firestore); + @override + ApiResult>> trackOrder(String userId) { + try { + final stream = firestore + .collection('orders') + .where( + Filter.or( + Filter('userAddress.user_id', isEqualTo: userId), + Filter('driver_id', isEqualTo: userId), + ), + ) + .snapshots() + .map((snapshot) { + return snapshot.docs + .map((doc) => TrackOrderModel.fromFirestore(doc.id, doc.data())) + .toList(); + }); + return SuccessApiResult>>(data: stream); + } catch (e) { + return ErrorApiResult>>(error: e.toString()); + } + } + + @override + ApiResult> trackDriver(String driverId) { + try { + final stream = 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); + }); + return SuccessApiResult>(data: stream); + } catch (e) { + return ErrorApiResult>(error: e.toString()); + } + } + + @override + 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 new file mode 100644 index 0000000..ba75257 --- /dev/null +++ b/lib/features/track_order/data/datasource/track_order_remote_source.dart @@ -0,0 +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 { + ApiResult>> trackOrder(String userId); + ApiResult> 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 new file mode 100644 index 0000000..0fbfb37 --- /dev/null +++ b/lib/features/track_order/data/models/track_order_model.dart @@ -0,0 +1,50 @@ +class TrackOrderModel { + final String driverId; + final String id; + final String status; + final String totalPrice; + final String userId; + + TrackOrderModel({ + required this.driverId, + required this.id, + required this.status, + required this.totalPrice, + required this.userId, + }); + + 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: safeString(data['driver_id'] ?? data['driverId']), + status: parsedStatus, + totalPrice: parsedTotal, + userId: parsedUserId, + ); + } +} 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..21141c4 --- /dev/null +++ b/lib/features/track_order/data/repos/track_order_repo_imp.dart @@ -0,0 +1,64 @@ +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'; +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'; + +@Injectable(as: TrackOrderRepo) +class TrackOrderRepoImpl implements TrackOrderRepo { + final TrackOrderRemoteDataSource remoteDataSource; + + TrackOrderRepoImpl(this.remoteDataSource); + + @override + ApiResult>> trackOrder(String userId) { + final result = remoteDataSource.trackOrder(userId); + + 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(), + ), + ), + + ErrorApiResult() => ErrorApiResult(error: result.error), + }; + } + + @override + ApiResult> trackOrderWithDriver(String driverId) { + final result = remoteDataSource.trackDriver(driverId); + + return switch (result) { + SuccessApiResult() => SuccessApiResult( + data: (result.data as Stream).map( + (model) => DriverEntity( + id: model.id, + lat: model.lat, + lng: model.lng, + ), + ), + ), + + ErrorApiResult() => ErrorApiResult(error: result.error), + }; + } + + @override + 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/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_entity.dart b/lib/features/track_order/domain/entities/order_entity.dart new file mode 100644 index 0000000..7707b23 --- /dev/null +++ b/lib/features/track_order/domain/entities/order_entity.dart @@ -0,0 +1,20 @@ +class OrderEntity { + final String id; + final String userId; + final String status; + + final String? driverId; + final String? totalPrice; + final String? address; + final String? name; + + OrderEntity({ + 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..4189859 --- /dev/null +++ b/lib/features/track_order/domain/repos/track_order_repo.dart @@ -0,0 +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 userId); + ApiResult> trackOrderWithDriver(String driverId); + 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/domain/usecases/track_order_usecase.dart b/lib/features/track_order/domain/usecases/track_order_usecase.dart new file mode 100644 index 0000000..9326760 --- /dev/null +++ b/lib/features/track_order/domain/usecases/track_order_usecase.dart @@ -0,0 +1,14 @@ +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); + + 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 new file mode 100644 index 0000000..11b78d9 --- /dev/null +++ b/lib/features/track_order/presentation/manager/cubit/track_order_cubit.dart @@ -0,0 +1,99 @@ +import 'dart:async'; +import 'dart:convert'; +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:tracking_app/features/track_order/domain/repos/track_order_repo.dart'; + +part 'track_order_state.dart'; + +@injectable +class TrackOrderCubit extends Cubit { + final TrackOrderUseCase trackOrderUseCase; + final TrackDriverUseCase driverUseCase; + final AuthStorage authStorage; + + StreamSubscription>? _ordersSubscription; + StreamSubscription? _driverSubscription; + + TrackOrderCubit(this.trackOrderUseCase, this.driverUseCase, this.authStorage) + : super(const TrackOrderState()); + + Future loadUserOrders() async { + emit(state.copyWith(isLoading: true, error: null)); + + final token = await authStorage.getToken(); + print('DEBUG: loadUserOrders called with string length: ${token?.length}'); + + 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) { + print( + 'DEBUG: Stream emitted new orders list. Count: ${orders.length}', + ); + emit(state.copyWith(orders: orders, isLoading: false, error: null)); + }, + onError: (error) { + print('DEBUG: Stream error: $error'); + emit(state.copyWith(isLoading: false, error: error.toString())); + }, + ); + } else if (result is ErrorApiResult>>) { + print('DEBUG: ApiResult Error: ${result.error}'); + emit(state.copyWith(isLoading: false, error: result.error)); + } + } + + void trackDriver(String driverId) { + final result = driverUseCase(driverId); + + if (result is SuccessApiResult>) { + _driverSubscription = result.data.listen( + (driver) => emit(state.copyWith(driver: driver)), + onError: (error) => emit(state.copyWith(error: error.toString())), + ); + } + } + + @override + Future close() async { + await _ordersSubscription?.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..c706bd7 --- /dev/null +++ b/lib/features/track_order/presentation/manager/cubit/track_order_state.dart @@ -0,0 +1,31 @@ +part of 'track_order_cubit.dart'; +class TrackOrderState extends Equatable { + final List orders; + final DriverEntity? driver; + final bool isLoading; + final String? error; + + const TrackOrderState({ + this.orders = const [], + this.driver, + this.isLoading = false, + this.error, + }); + + TrackOrderState copyWith({ + List? orders, + DriverEntity? driver, + bool? isLoading, + String? error, + }) { + return TrackOrderState( + orders: orders ?? this.orders, + driver: driver ?? this.driver, + isLoading: isLoading ?? this.isLoading, + error: error, + ); + } + + @override + 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 new file mode 100644 index 0000000..289dffe --- /dev/null +++ b/lib/features/track_order/presentation/pages/track_order_page.dart @@ -0,0 +1,124 @@ +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: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 { + const TrackOrderPage({super.key}); + + @override + State createState() => _TrackOrderPageState(); +} + +class _TrackOrderPageState extends State { + @override + void initState() { + super.initState(); + context.read().loadUserOrders(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Track Orders'), + ), + body: BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (state.error != null) { + return Center( + child: Text( + state.error!, + style: const TextStyle(color: Colors.red), + ), + ); + } + + if (state.orders.isEmpty) { + return const Center( + child: Text('No orders found'), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: state.orders.length, + itemBuilder: (context, index) { + final order = state.orders[index]; + + 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 new file mode 100644 index 0000000..7e639dd --- /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..8b55c57 --- /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"), +// ], +// ), +// ), +// ); +// } +// } 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/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 cac8596..a0ed465 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,7 +5,9 @@ import FlutterMacOS import Foundation +import cloud_firestore import file_selector_macos +import firebase_auth import firebase_core import firebase_crashlytics import firebase_messaging @@ -15,7 +17,9 @@ 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")) + 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 779a2a3..4d8c251 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: @@ -353,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: @@ -450,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: @@ -474,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 bb7cff1..4a7cd05 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,57 +1,59 @@ 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 + 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 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 +62,6 @@ flutter: - assets/translations/ - assets/data/ - assets/images/ - - # fonts: # - family: Schyler # fonts: 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 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index b762e91..7b36576 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,14 +6,20 @@ #include "generated_plugin_registrant.h" +#include #include +#include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + CloudFirestorePluginCApiRegisterWithRegistrar( + 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 b5e0031..2a1542b 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,7 +3,9 @@ # list(APPEND FLUTTER_PLUGIN_LIST + cloud_firestore file_selector_windows + firebase_auth firebase_core geolocator_windows url_launcher_windows