diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2cc440e..eb848a5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,7 @@ + + + get _prefs async => await SharedPreferences.getInstance(); + Future saveOrderId(String orderId) async { + final prefs = await _prefs; + await prefs.setString(_orderIdKey, orderId); + } + + Future getOrderId() async { + final prefs = await _prefs; + return prefs.getString(_orderIdKey); + } + + Future clearOrderId() async { + final prefs = await _prefs; + await prefs.remove(_orderIdKey); + } + Future saveToken(String token) async { final prefs = await _prefs; await prefs.setString(_tokenKey, token); @@ -54,5 +70,6 @@ class AuthStorage { await clearToken(); await clearUser(); await setRememberMe(false); + await clearOrderId(); } } diff --git a/lib/app/config/di/di.config.dart b/lib/app/config/di/di.config.dart index ac67fd2..cc849cb 100644 --- a/lib/app/config/di/di.config.dart +++ b/lib/app/config/di/di.config.dart @@ -1,3 +1,4 @@ +// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND // ************************************************************************** @@ -8,6 +9,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; @@ -51,6 +53,41 @@ 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/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart' + as _i860; +import '../../../features/driver_orders_details/data/datasource/order_details_remote_datasource.dart' + as _i114; +import '../../../features/driver_orders_details/data/repos/order_details_repo_impl.dart' + as _i55; +import '../../../features/driver_orders_details/domain/repos/order_details_repo.dart' + as _i313; +import '../../../features/driver_orders_details/domain/usecases/get_order_details_usecase.dart' + as _i1045; +import '../../../features/driver_orders_details/presentation/manager/order_details_cubit.dart' + as _i375; +import '../../../features/home/api/driverOrderDataS_imp.dart' as _i495; +import '../../../features/home/data/datascourse/driverOrderDatascource.dart' + as _i743; +import '../../../features/home/data/repo/driverOrderRepo_impl.dart' as _i1020; +import '../../../features/home/domain/repo/driverOrderRepo.dart' as _i499; +import '../../../features/home/domain/usecase/getdriverOrderUsecase.dart' + as _i858; +import '../../../features/home/domain/usecase/upload_driver_fire_data_use_case.dart' + as _i329; +import '../../../features/home/domain/usecase/upload_order_fire_data_use_case.dart' + as _i233; +import '../../../features/home/presentation/manger/driverorderCubit.dart' + as _i573; +import '../../../features/my_orders/api/datasource/my_orders_remote_data_source_imp.dart' + as _i583; +import '../../../features/my_orders/data/datasource/my_orders_remote_data_source.dart' + as _i466; +import '../../../features/my_orders/data/repo/my_orders_repo_imp.dart' as _i754; +import '../../../features/my_orders/domain/repo/my_orders_repo.dart' as _i919; +import '../../../features/my_orders/domain/usecases/get_order_use_case.dart' + as _i335; +import '../../../features/my_orders/presentation/manager/my_orders_cubit.dart' + as _i156; import '../../../features/profile/api/profile_lacal_datasource_imp.dart' as _i495; import '../../../features/profile/api/profile_remote_datasource_imp.dart' @@ -71,6 +108,7 @@ import '../../../features/profile/presentation/managers/profile_cubit.dart' as _i603; import '../../core/api_manger/api_client.dart' as _i890; import '../auth_storage/auth_storage.dart' as _i603; +import '../network/firebase_module.dart' as _i383; import '../network/network_module.dart' as _i200; extension GetItInjectableX on _i174.GetIt { @@ -80,21 +118,60 @@ extension GetItInjectableX on _i174.GetIt { _i526.EnvironmentFilter? environmentFilter, }) { final gh = _i526.GetItHelper(this, 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<_i783.CountryLocalDataSource>( () => _i783.CountryLocalDataSourceImpl(), ); + gh.lazySingleton<_i974.FirebaseFirestore>( + () => networkModule.firestore, + instanceName: 'firestore', + ); gh.lazySingleton<_i361.Dio>( () => networkModule.dio(gh<_i603.AuthStorage>()), ); + gh.factory<_i329.UploadDriverFireDataUseCase>( + () => _i329.UploadDriverFireDataUseCase( + gh<_i974.FirebaseFirestore>(instanceName: 'firestore'), + ), + ); + gh.factory<_i233.UploadOrderFireDataUseCase>( + () => _i233.UploadOrderFireDataUseCase( + gh<_i974.FirebaseFirestore>(instanceName: 'firestore'), + ), + ); gh.lazySingleton<_i697.ProfileLocalDataSource>( () => _i495.ProfileLocalDataSourceImpl(gh<_i603.AuthStorage>()), ); + gh.factory<_i114.OrderDetailsRemoteDatasource>( + () => _i860.OrderDetailsRemoteDatasourceImpl( + firestore: gh<_i974.FirebaseFirestore>(), + ), + ); gh.lazySingleton<_i890.ApiClient>( () => networkModule.authApiClient(gh<_i361.Dio>()), ); + gh.factory<_i466.MyOrdersRemoteDataSource>( + () => _i583.MyOrdersRemoteDataSourceImp(gh<_i890.ApiClient>()), + ); + gh.factory<_i313.OrderDetailsRepo>( + () => _i55.OrderDetailsRepoImpl(gh<_i114.OrderDetailsRemoteDatasource>()), + ); + gh.factory<_i919.MyOrdersRepo>( + () => _i754.MyOrdersRepoImpl(gh<_i466.MyOrdersRemoteDataSource>()), + ); + gh.factory<_i335.GetOrderUseCase>( + () => _i335.GetOrderUseCase(gh<_i919.MyOrdersRepo>()), + ); + gh.factory<_i1045.GetOrderDetailsUsecase>( + () => _i1045.GetOrderDetailsUsecase(repo: gh<_i313.OrderDetailsRepo>()), + ); + gh.factory<_i743.DriverOrderDataSource>( + () => _i495.DriverOrderDataSourceImpl(gh<_i890.ApiClient>()), + ); gh.factory<_i943.ProfileRemoteDatasource>( () => _i899.ProfileRemoteDatasourceImp(gh<_i890.ApiClient>()), ); @@ -104,6 +181,9 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i712.AuthRepo>( () => _i566.AuthRepoImpl(gh<_i708.AuthRemoteDataSource>()), ); + gh.factory<_i375.OrderDetailsCubit>( + () => _i375.OrderDetailsCubit(gh<_i1045.GetOrderDetailsUsecase>()), + ); gh.factory<_i991.ChangePasswordUsecase>( () => _i991.ChangePasswordUsecase(gh<_i712.AuthRepo>()), ); @@ -123,10 +203,19 @@ extension GetItInjectableX on _i174.GetIt { email, ), ); + gh.factory<_i156.MyOrdersCubit>( + () => _i156.MyOrdersCubit( + gh<_i335.GetOrderUseCase>(), + gh<_i603.AuthStorage>(), + ), + ); gh.factoryParam<_i378.ResetPasswordCubit, String, dynamic>( (email, _) => _i378.ResetPasswordCubit(email, gh<_i294.ResetPasswordUsecase>()), ); + gh.factory<_i499.DriverOrderRepo>( + () => _i1020.DriverOrderRepositoryImpl(gh<_i743.DriverOrderDataSource>()), + ); gh.factory<_i863.ProfileRepo>( () => _i1048.ProfileRepoImpl( gh<_i943.ProfileRemoteDatasource>(), @@ -160,6 +249,9 @@ extension GetItInjectableX on _i174.GetIt { gh<_i603.AuthStorage>(), ), ); + gh.factory<_i858.GetDriverOrdersUseCase>( + () => _i858.GetDriverOrdersUseCase(gh<_i499.DriverOrderRepo>()), + ); gh.factory<_i377.ApplyCubit>( () => _i377.ApplyCubit( gh<_i940.GetCountriesUseCase>(), @@ -183,6 +275,15 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i810.LoginCubit>( () => _i810.LoginCubit(gh<_i75.LoginUseCase>(), gh<_i603.AuthStorage>()), ); + gh.factory<_i573.DriverOrderCubit>( + () => _i573.DriverOrderCubit( + gh<_i858.GetDriverOrdersUseCase>(), + gh<_i603.AuthStorage>(), + gh<_i329.UploadDriverFireDataUseCase>(), + gh<_i233.UploadOrderFireDataUseCase>(), + gh<_i499.DriverOrderRepo>(), + ), + ); gh.factory<_i603.ProfileCubit>( () => _i603.ProfileCubit( gh<_i221.EditProfileUseCase>(), @@ -195,4 +296,6 @@ extension GetItInjectableX on _i174.GetIt { } } +class _$FirebaseModule extends _i383.FirebaseModule {} + class _$NetworkModule extends _i200.NetworkModule {} diff --git a/lib/app/config/network/firebase_module.dart b/lib/app/config/network/firebase_module.dart new file mode 100644 index 0000000..e7ebf30 --- /dev/null +++ b/lib/app/config/network/firebase_module.dart @@ -0,0 +1,8 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:injectable/injectable.dart'; + +@module +abstract class FirebaseModule { + @lazySingleton + FirebaseFirestore get firestore => FirebaseFirestore.instance; +} diff --git a/lib/app/config/network/network_module.dart b/lib/app/config/network/network_module.dart index fa0d692..c976283 100644 --- a/lib/app/config/network/network_module.dart +++ b/lib/app/config/network/network_module.dart @@ -2,6 +2,7 @@ import 'package:dio/dio.dart'; import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; import 'package:tracking_app/app/config/network/interceptor.dart'; import 'package:injectable/injectable.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:pretty_dio_logger/pretty_dio_logger.dart'; import '../../core/api_manger/api_client.dart'; @@ -37,4 +38,8 @@ abstract class NetworkModule { @lazySingleton ApiClient authApiClient(Dio dio) => ApiClient(dio); + + @lazySingleton + @Named('firestore') + FirebaseFirestore get firestore => FirebaseFirestore.instance; } diff --git a/lib/app/core/api_manger/api_client.dart b/lib/app/core/api_manger/api_client.dart index 154ddb5..3952e0e 100644 --- a/lib/app/core/api_manger/api_client.dart +++ b/lib/app/core/api_manger/api_client.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:dio/dio.dart'; -import 'package:retrofit/dio.dart'; import 'package:tracking_app/app/core/values/app_endpoint_strings.dart'; import 'package:tracking_app/features/auth/data/model/response/change_password_dto.dart'; import 'package:tracking_app/features/auth/data/model/request/LoginRequest.dart'; @@ -13,12 +12,14 @@ import 'package:tracking_app/features/auth/data/models/request/verifyreset_reque import 'package:tracking_app/features/auth/data/models/response/forgetpassword_response.dart'; import 'package:tracking_app/features/auth/data/models/response/resetpassword_response.dart'; import 'package:tracking_app/features/auth/data/models/response/verifyreset_response.dart'; +import 'package:tracking_app/features/my_orders/data/models/response/my_order_response.dart'; import '../../../features/auth/data/models/response/apply_response_model.dart'; import '../../../features/auth/data/models/response/vehicles_response_model.dart'; import 'package:tracking_app/app/core/values/api_constants.dart'; import 'package:tracking_app/features/profile/data/models/requests/edit_profile_request.dart'; import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; import '../../../features/auth/data/models/response/logout_response_dto/logout_response_dto.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; part 'api_client.g.dart'; @RestApi(baseUrl: AppEndpointString.baseUrl) @@ -74,4 +75,16 @@ abstract class ApiClient { Future> getProfile({ @Header(ApiConstants.authorization) required String token, }); + + @GET(AppEndpointString.mydriverOrders) + Future> getAllOrders({ + @Header("Authorization") required String token, + @Query("limit") int? limit, + @Query("page") int? page, + }); + + @GET(AppEndpointString.mydriverOrders) + Future> getPendingOrders( + @Header("Authorization") String token, + ); } diff --git a/lib/app/core/api_manger/api_client.g.dart b/lib/app/core/api_manger/api_client.g.dart index d8ad524..6debbf5 100644 --- a/lib/app/core/api_manger/api_client.g.dart +++ b/lib/app/core/api_manger/api_client.g.dart @@ -2,14 +2,16 @@ part of 'api_client.dart'; +// dart format off + // ************************************************************************** // RetrofitGenerator // ************************************************************************** -// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter,avoid_unused_constructor_parameters,unreachable_from_main class _ApiClient implements ApiClient { - _ApiClient(this._dio, {this.baseUrl}) { + _ApiClient(this._dio, {this.baseUrl, this.errorLogger}) { baseUrl ??= 'https://flower.elevateegy.com/api/v1/'; } @@ -17,27 +19,34 @@ class _ApiClient implements ApiClient { String? baseUrl; + final ParseErrorLogger? errorLogger; + @override Future> logout(String token) async { - const _extra = {}; + final _extra = {}; final queryParameters = {}; final _headers = {r'Authorization': token}; _headers.removeWhere((k, v) => v == null); - final Map? _data = null; - final _result = await _dio.fetch>( - _setStreamType>( - Options(method: 'GET', headers: _headers, extra: _extra) - .compose( - _dio.options, - 'drivers/logout', - queryParameters: queryParameters, - data: _data, - ) - .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), - ), + const Map? _data = null; + final _options = _setStreamType>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'drivers/logout', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), ); - final value = LogoutResponseDto.fromJson(_result.data!); - final httpResponse = HttpResponse(value, _result); + final _result = await _dio.fetch>(_options); + late LogoutResponseDto _value; + try { + _value = LogoutResponseDto.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); return httpResponse; } @@ -45,25 +54,30 @@ class _ApiClient implements ApiClient { Future> forgetPassword( ForgetPasswordRequest request, ) async { - const _extra = {}; + final _extra = {}; final queryParameters = {}; final _headers = {}; final _data = {}; _data.addAll(request.toJson()); - final _result = await _dio.fetch>( - _setStreamType>( - Options(method: 'POST', headers: _headers, extra: _extra) - .compose( - _dio.options, - 'drivers/forgotPassword', - queryParameters: queryParameters, - data: _data, - ) - .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), - ), + final _options = _setStreamType>( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'drivers/forgotPassword', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), ); - final value = ForgetpasswordResponse.fromJson(_result.data!); - final httpResponse = HttpResponse(value, _result); + final _result = await _dio.fetch>(_options); + late ForgetpasswordResponse _value; + try { + _value = ForgetpasswordResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); return httpResponse; } @@ -71,25 +85,30 @@ class _ApiClient implements ApiClient { Future> resetPassword( ResetPasswordRequest request, ) async { - const _extra = {}; + final _extra = {}; final queryParameters = {}; final _headers = {}; final _data = {}; _data.addAll(request.toJson()); - final _result = await _dio.fetch>( - _setStreamType>( - Options(method: 'PUT', headers: _headers, extra: _extra) - .compose( - _dio.options, - 'drivers/resetPassword', - queryParameters: queryParameters, - data: _data, - ) - .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), - ), + final _options = _setStreamType>( + Options(method: 'PUT', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'drivers/resetPassword', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), ); - final value = ResetpasswordResponse.fromJson(_result.data!); - final httpResponse = HttpResponse(value, _result); + final _result = await _dio.fetch>(_options); + late ResetpasswordResponse _value; + try { + _value = ResetpasswordResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); return httpResponse; } @@ -97,25 +116,30 @@ class _ApiClient implements ApiClient { Future> verifyResetCode( VerifyResetRequest request, ) async { - const _extra = {}; + final _extra = {}; final queryParameters = {}; final _headers = {}; final _data = {}; _data.addAll(request.toJson()); - final _result = await _dio.fetch>( - _setStreamType>( - Options(method: 'POST', headers: _headers, extra: _extra) - .compose( - _dio.options, - 'drivers/verifyResetCode', - queryParameters: queryParameters, - data: _data, - ) - .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), - ), + final _options = _setStreamType>( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'drivers/verifyResetCode', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), ); - final value = VerifyresetResponse.fromJson(_result.data!); - final httpResponse = HttpResponse(value, _result); + final _result = await _dio.fetch>(_options); + late VerifyresetResponse _value; + try { + _value = VerifyresetResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); return httpResponse; } @@ -124,100 +148,120 @@ class _ApiClient implements ApiClient { required String token, required Map body, }) async { - const _extra = {}; + final _extra = {}; final queryParameters = {}; final _headers = {r'Authorization': token}; _headers.removeWhere((k, v) => v == null); final _data = {}; _data.addAll(body); - final _result = await _dio.fetch>( - _setStreamType>( - Options(method: 'PATCH', headers: _headers, extra: _extra) - .compose( - _dio.options, - 'drivers/change-password', - queryParameters: queryParameters, - data: _data, - ) - .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), - ), + final _options = _setStreamType>( + Options(method: 'PATCH', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'drivers/change-password', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), ); - final value = ChangePasswordDto.fromJson(_result.data!); - final httpResponse = HttpResponse(value, _result); + final _result = await _dio.fetch>(_options); + late ChangePasswordDto _value; + try { + _value = ChangePasswordDto.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); return httpResponse; } @override Future login(LoginRequest request) async { - const _extra = {}; + final _extra = {}; final queryParameters = {}; final _headers = {}; final _data = {}; _data.addAll(request.toJson()); - final _result = await _dio.fetch>( - _setStreamType( - Options(method: 'POST', headers: _headers, extra: _extra) - .compose( - _dio.options, - 'drivers/signin', - queryParameters: queryParameters, - data: _data, - ) - .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), - ), + final _options = _setStreamType( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'drivers/signin', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), ); - final value = LoginResponse.fromJson(_result.data!); - return value; + final _result = await _dio.fetch>(_options); + late LoginResponse _value; + try { + _value = LoginResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, _result); + rethrow; + } + return _value; } @override Future> getAllVehicle() async { - const _extra = {}; + final _extra = {}; final queryParameters = {}; final _headers = {}; - final Map? _data = null; - final _result = await _dio.fetch>( - _setStreamType>( - Options(method: 'GET', headers: _headers, extra: _extra) - .compose( - _dio.options, - 'vehicles', - queryParameters: queryParameters, - data: _data, - ) - .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), - ), + const Map? _data = null; + final _options = _setStreamType>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'vehicles', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), ); - final value = VehiclesResponse.fromJson(_result.data!); - final httpResponse = HttpResponse(value, _result); + final _result = await _dio.fetch>(_options); + late VehiclesResponse _value; + try { + _value = VehiclesResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); return httpResponse; } @override Future> apply(FormData formData) async { - const _extra = {}; + final _extra = {}; final queryParameters = {}; final _headers = {}; final _data = formData; - final _result = await _dio.fetch>( - _setStreamType>( - Options( - method: 'POST', - headers: _headers, - extra: _extra, - contentType: 'multipart/form-data', - ) - .compose( - _dio.options, - 'drivers/apply', - queryParameters: queryParameters, - data: _data, - ) - .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), - ), + final _options = _setStreamType>( + Options( + method: 'POST', + headers: _headers, + extra: _extra, + contentType: 'multipart/form-data', + ) + .compose( + _dio.options, + 'drivers/apply', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), ); - final value = ApplyResponseModel.fromJson(_result.data!); - final httpResponse = HttpResponse(value, _result); + final _result = await _dio.fetch>(_options); + late ApplyResponseModel _value; + try { + _value = ApplyResponseModel.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); return httpResponse; } @@ -226,26 +270,31 @@ class _ApiClient implements ApiClient { required String token, required EditProfileRequest request, }) async { - const _extra = {}; + final _extra = {}; final queryParameters = {}; final _headers = {r'Authorization': token}; _headers.removeWhere((k, v) => v == null); final _data = {}; _data.addAll(request.toJson()); - final _result = await _dio.fetch>( - _setStreamType>( - Options(method: 'PUT', headers: _headers, extra: _extra) - .compose( - _dio.options, - 'drivers/editProfile', - queryParameters: queryParameters, - data: _data, - ) - .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), - ), + final _options = _setStreamType>( + Options(method: 'PUT', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'drivers/editProfile', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), ); - final value = EditProfileResponse.fromJson(_result.data!); - final httpResponse = HttpResponse(value, _result); + final _result = await _dio.fetch>(_options); + late EditProfileResponse _value; + try { + _value = EditProfileResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); return httpResponse; } @@ -254,7 +303,7 @@ class _ApiClient implements ApiClient { required String token, required File photo, }) async { - const _extra = {}; + final _extra = {}; final queryParameters = {}; final _headers = {r'Authorization': token}; _headers.removeWhere((k, v) => v == null); @@ -268,25 +317,30 @@ class _ApiClient implements ApiClient { ), ), ); - final _result = await _dio.fetch>( - _setStreamType>( - Options( - method: 'PUT', - headers: _headers, - extra: _extra, - contentType: 'multipart/form-data', - ) - .compose( - _dio.options, - 'drivers/upload-photo', - queryParameters: queryParameters, - data: _data, - ) - .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), - ), + final _options = _setStreamType>( + Options( + method: 'PUT', + headers: _headers, + extra: _extra, + contentType: 'multipart/form-data', + ) + .compose( + _dio.options, + 'drivers/upload-photo', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), ); - final value = EditProfileResponse.fromJson(_result.data!); - final httpResponse = HttpResponse(value, _result); + final _result = await _dio.fetch>(_options); + late EditProfileResponse _value; + try { + _value = EditProfileResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); return httpResponse; } @@ -294,25 +348,93 @@ class _ApiClient implements ApiClient { Future> getProfile({ required String token, }) async { - const _extra = {}; + final _extra = {}; final queryParameters = {}; final _headers = {r'Authorization': token}; _headers.removeWhere((k, v) => v == null); - final Map? _data = null; - final _result = await _dio.fetch>( - _setStreamType>( - Options(method: 'GET', headers: _headers, extra: _extra) - .compose( - _dio.options, - 'drivers/profile-data', - queryParameters: queryParameters, - data: _data, - ) - .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), - ), + const Map? _data = null; + final _options = _setStreamType>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'drivers/profile-data', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late EditProfileResponse _value; + try { + _value = EditProfileResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + + @override + Future> getAllOrders({ + required String token, + int? limit, + int? page, + }) async { + final _extra = {}; + final queryParameters = {r'limit': limit, r'page': page}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {r'Authorization': token}; + _headers.removeWhere((k, v) => v == null); + const Map? _data = null; + final _options = _setStreamType>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'orders/pending-orders', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), ); - final value = EditProfileResponse.fromJson(_result.data!); - final httpResponse = HttpResponse(value, _result); + final _result = await _dio.fetch>(_options); + late MyOrderResponse _value; + try { + _value = MyOrderResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + + @override + Future> getPendingOrders(String token) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {r'Authorization': token}; + _headers.removeWhere((k, v) => v == null); + const Map? _data = null; + final _options = _setStreamType>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'orders/pending-orders', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late OrderResponse _value; + try { + _value = OrderResponse.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options, _result); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); return httpResponse; } @@ -343,3 +465,5 @@ class _ApiClient implements ApiClient { return Uri.parse(dioBaseUrl).resolveUri(url).toString(); } } + +// dart format on diff --git a/lib/app/core/app_constants.dart b/lib/app/core/app_constants.dart index e185f38..06be015 100644 --- a/lib/app/core/app_constants.dart +++ b/lib/app/core/app_constants.dart @@ -37,4 +37,5 @@ class AppConstants { static const String english = 'English'; static const String arabic = 'Arabic'; static const String logoutFailed = 'Logout failed'; + static const String floweryRider = 'Flowery Rider'; } diff --git a/lib/app/core/router/app_router.dart b/lib/app/core/router/app_router.dart index e28bae9..4f7f329 100644 --- a/lib/app/core/router/app_router.dart +++ b/lib/app/core/router/app_router.dart @@ -1,17 +1,16 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; import 'package:tracking_app/app/config/di/di.dart'; import 'package:tracking_app/app/core/router/route_names.dart'; -import 'package:tracking_app/features/auth/presentation/logout/manager/logout_cubit.dart'; import 'package:tracking_app/features/Onboarding/presentation/pages/onboardingScreen.dart'; import 'package:tracking_app/features/app_sections/presentation/pages/app_sections.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart'; import 'package:tracking_app/features/profile/data/models/driver_model.dart'; import 'package:tracking_app/features/profile/presentation/pages/edit_driver_profile_page.dart'; import 'package:tracking_app/features/profile/presentation/pages/edit_vehicle_page.dart'; import 'package:tracking_app/features/profile/presentation/pages/profile_page.dart'; - -import '../../config/di/di.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; +import 'package:tracking_app/features/my_orders/presentation/pages/order_details_page.dart'; import 'package:tracking_app/features/auth/presentation/apply/view/apply_view.dart'; import 'package:tracking_app/features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit.dart'; import 'package:tracking_app/features/auth/presentation/forget_pass/pages/forget_pass_page.dart'; @@ -98,5 +97,18 @@ final GoRouter appRouter = GoRouter( return EditVehiclePage(driver: driver); }, ), + + GoRoute( + path: RouteNames.ordersDetailsPage, + builder: (context, state) => DriversOrdersDetailsPage(), + ), + + GoRoute( + path: RouteNames.orderDetails, + builder: (context, state) { + final order = state.extra as OrderEntity; + return OrderDetailsPage(order: order); + }, + ), ], ); diff --git a/lib/app/core/router/route_names.dart b/lib/app/core/router/route_names.dart index 93702d0..c435505 100644 --- a/lib/app/core/router/route_names.dart +++ b/lib/app/core/router/route_names.dart @@ -13,4 +13,7 @@ abstract class RouteNames { static const editDriverProfile = "/editDriverProfile"; static const editVehicle = "/editVehicle"; static const getProfle = "/profile-data"; + static const ordersDetailsPage = "/ordersDetails"; + static const myOrders = "/myOrders"; + static const orderDetails = "/orderDetails"; } diff --git a/lib/app/core/ui_helper/color/colors.dart b/lib/app/core/ui_helper/color/colors.dart index 394c8a3..bcdc243 100644 --- a/lib/app/core/ui_helper/color/colors.dart +++ b/lib/app/core/ui_helper/color/colors.dart @@ -17,4 +17,5 @@ abstract final class AppColors { static const Color white = Color(0xFFFFFFFF); static const Color purple = Color(0xFF441AB0); static const Color white70 = Color(0xFFA6A6A6); + static const Color lightPink = Color(0xFFF9ECF0); } diff --git a/lib/app/core/values/app_endpoint_strings.dart b/lib/app/core/values/app_endpoint_strings.dart index f8a65e4..eb418ac 100644 --- a/lib/app/core/values/app_endpoint_strings.dart +++ b/lib/app/core/values/app_endpoint_strings.dart @@ -9,8 +9,7 @@ class AppEndpointString { static const String profileData = 'auth/profile-data'; static const String updateRole = 'auth/update-role'; static const String cashOrder = 'orders'; - static const String orders = 'orders'; - static const String checkout = '$orders/checkout'; + static const String addresses = 'addresses'; static const String signup = '/auth/signup'; static const String allCategories = 'categories'; @@ -32,4 +31,7 @@ class AppEndpointString { static const String getProfile = "drivers/profile-data"; static const String login = "drivers/signin"; static const String logout = 'drivers/logout'; + static const String driverOrders = 'orders/driver-orders'; + + static const String mydriverOrders = 'orders/pending-orders'; } diff --git a/lib/app/core/values/paths.dart b/lib/app/core/values/paths.dart index d1ceac7..26ff989 100644 --- a/lib/app/core/values/paths.dart +++ b/lib/app/core/values/paths.dart @@ -4,4 +4,7 @@ class AppPaths { static const String aboutUs = 'about_app'; static const String terms = 'terms_and_conditions'; static const String onboardingImage = 'assets/images/Clip path group.png'; + static const String whatsappImage = 'assets/images/whatsapp.png'; + static const String flowerLogo = 'assets/images/flower_logo.png'; + static const String mediaUrl = 'https://flower.elevateegy.com/uploads/'; } diff --git a/lib/features/app_sections/presentation/widgets/app_section_view.dart b/lib/features/app_sections/presentation/widgets/app_section_view.dart index bce7b04..0e18e2e 100644 --- a/lib/features/app_sections/presentation/widgets/app_section_view.dart +++ b/lib/features/app_sections/presentation/widgets/app_section_view.dart @@ -7,6 +7,8 @@ import 'package:tracking_app/features/app_sections/presentation/manager/app_sect import 'package:tracking_app/features/app_sections/presentation/pages/home_page_test.dart'; import 'package:tracking_app/features/app_sections/presentation/pages/orders_page_test.dart'; import 'package:tracking_app/features/app_sections/presentation/pages/profile_page_test.dart'; +import 'package:tracking_app/features/my_orders/presentation/pages/my_orders_page.dart'; +import 'package:tracking_app/features/home/presentation/pages/driverOrderScreen.dart'; import 'package:tracking_app/features/profile/presentation/pages/profile_page.dart'; import 'package:tracking_app/generated/locale_keys.g.dart'; @@ -25,10 +27,10 @@ class _AppSectionsViewState extends State { Widget bodyWidget; switch (state.selectedIndex) { case 0: - bodyWidget = const HomePageTest(); + bodyWidget = const DriverOrderScreen(); break; case 1: - bodyWidget = const OrdersPageTest(); + bodyWidget = const MyOrdersPage(); break; case 2: bodyWidget = const ProfilePage(); diff --git a/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart b/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart new file mode 100644 index 0000000..f893869 --- /dev/null +++ b/lib/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart @@ -0,0 +1,32 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; + +@Injectable(as: OrderDetailsRemoteDatasource) +class OrderDetailsRemoteDatasourceImpl implements OrderDetailsRemoteDatasource { + final FirebaseFirestore _firestore; + OrderDetailsRemoteDatasourceImpl({required FirebaseFirestore firestore}) + : _firestore = firestore; + + @override + ApiResult> getOrderStream(String orderId) { + try { + final stream = _firestore + .collection('orders') + .doc(orderId) + .snapshots() + .where((snapshot) => snapshot.exists && snapshot.data() != null) + .map((snapshot) { + return OrderDto.fromJson( + snapshot.data() as Map, + snapshot.id, + ); + }); + return SuccessApiResult>(data: stream); + } catch (e) { + return ErrorApiResult>(error: e.toString()); + } + } +} diff --git a/lib/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart b/lib/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart new file mode 100644 index 0000000..49bbd41 --- /dev/null +++ b/lib/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart @@ -0,0 +1,6 @@ +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; + +abstract class OrderDetailsRemoteDatasource { + ApiResult> getOrderStream(String orderId); +} diff --git a/lib/features/driver_orders_details/data/mapper/order_dto_mapper.dart b/lib/features/driver_orders_details/data/mapper/order_dto_mapper.dart new file mode 100644 index 0000000..ab50afe --- /dev/null +++ b/lib/features/driver_orders_details/data/mapper/order_dto_mapper.dart @@ -0,0 +1,51 @@ +import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; + +extension OrderDtoMapper on OrderDto { + OrderModel toOrderModel() { + return OrderModel( + driverId: driverId, + orderId: orderId, + userAddress: userAddress.toUserAddressModel(), + userId: userId, + orderDetails: orderDetails.toOrderDetailsModel(), + ); + } +} + +extension OrderDetailsDtoMapper on OrderDetailsDto { + OrderDetailsModel toOrderDetailsModel() { + return OrderDetailsModel( + items: items.map((i) => i.toOrderItemModel()).toList(), + status: status, + totalPrice: totalPrice, + pickupAddress: pickupAddress.toPickedAddressModel(), + orderId: orderId, + userAddress: userAddress, + ); + } +} + +extension OrderItemDtoMapper on OrderItemDto { + OrderItemModel toOrderItemModel() { + return OrderItemModel( + productId: productId, + title: title, + image: image, + quantity: quantity, + price: price, + ); + } +} + +extension PickedAddressDtoMapper on PickedAddressDto { + PickedAddressModel toPickedAddressModel() { + return PickedAddressModel(name: name, address: address); + } +} + +extension UserAddressDtoMapper on UserAddressDto { + UserAddressModel toUserAddressModel() { + return UserAddressModel(name: name, address: address, userId: userId); + } +} diff --git a/lib/features/driver_orders_details/data/models/orders_dto.dart b/lib/features/driver_orders_details/data/models/orders_dto.dart new file mode 100644 index 0000000..0b14faf --- /dev/null +++ b/lib/features/driver_orders_details/data/models/orders_dto.dart @@ -0,0 +1,154 @@ +class OrderDto { + final String orderId; + final String driverId; + final String userId; + final OrderDetailsDto orderDetails; + final UserAddressDto userAddress; + + OrderDto({ + required this.orderId, + required this.driverId, + required this.userId, + required this.orderDetails, + required this.userAddress, + }); + + factory OrderDto.fromJson(Map json, String id) { + return OrderDto( + orderId: id, + driverId: json['driver_id'] ?? '', + userId: json['user_id'] ?? '', + orderDetails: OrderDetailsDto.fromJson(json['oder_dt'] ?? {}), + userAddress: UserAddressDto.fromJson(json['userAddress'] ?? {}), + ); + } + + Map toJson() { + return { + 'driver_id': driverId, + 'user_id': userId, + 'oder_dt': (orderDetails).toJson(), + 'userAddress': (userAddress).toJson(), + }; + } +} + +class OrderDetailsDto { + final List items; + final String status; + final double totalPrice; + final PickedAddressDto pickupAddress; + final String orderId; + final String userAddress; + + OrderDetailsDto({ + required this.items, + required this.status, + required this.totalPrice, + required this.pickupAddress, + required this.orderId, + required this.userAddress, + }); + + factory OrderDetailsDto.fromJson(Map json) { + return OrderDetailsDto( + status: json['status'] ?? '', + totalPrice: (json['totalPrice'] ?? 0).toDouble(), + pickupAddress: PickedAddressDto.fromJson(json['pickupAddress'] ?? {}), + items: (json['items'] as List? ?? []) + .map((i) => OrderItemDto.fromJson(i)) + .toList(), + orderId: json['orderId'] ?? '', + userAddress: json['userAddress'] ?? '', + ); + } + + Map toJson() { + return { + 'status': status, + 'totalPrice': totalPrice, + 'pickupAddress': (pickupAddress).toJson(), + 'items': items.map((i) => (i).toJson()).toList(), + 'orderId': orderId, + 'userAddress': userAddress, + }; + } +} + +class OrderItemDto { + final String productId; + final String title; + final String image; + final int quantity; + final double price; + + OrderItemDto({ + required this.productId, + required this.title, + required this.image, + required this.quantity, + required this.price, + }); + + factory OrderItemDto.fromJson(Map json) { + return OrderItemDto( + productId: json['productId'] ?? '', + title: json['title'] ?? '', + image: json['image'] ?? '', + quantity: json['quantity'] ?? 0, + price: (json['price'] ?? 0).toDouble(), + ); + } + + Map toJson() { + return { + 'productId': productId, + 'title': title, + 'image': image, + 'quantity': quantity, + 'price': price, + }; + } +} + +class PickedAddressDto { + final String name; + final String address; + + PickedAddressDto({required this.name, required this.address}); + + factory PickedAddressDto.fromJson(Map json) { + return PickedAddressDto( + name: json['name'] ?? '', + address: json['address'] ?? '', + ); + } + + Map toJson() { + return {'name': name, 'address': address}; + } +} + +class UserAddressDto { + final String name; + final String address; + final String userId; + + UserAddressDto({ + required this.name, + required this.address, + required this.userId, + }); + + factory UserAddressDto.fromJson(Map json) { + return UserAddressDto( + name: json['name'] ?? '', + address: json['adress'] ?? '', + userId: json['user_id'] ?? '', + ); + } + + Map toJson() { + return {'name': name, 'adress': address, 'user_id': userId}; + } +} diff --git a/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart b/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart new file mode 100644 index 0000000..37251f2 --- /dev/null +++ b/lib/features/driver_orders_details/data/repos/order_details_repo_impl.dart @@ -0,0 +1,27 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart'; +import 'package:tracking_app/features/driver_orders_details/data/mapper/order_dto_mapper.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; + +@Injectable(as: OrderDetailsRepo) +class OrderDetailsRepoImpl implements OrderDetailsRepo { + final OrderDetailsRemoteDatasource _remoteDataSource; + OrderDetailsRepoImpl(this._remoteDataSource); + + @override + ApiResult> getOrderDetails(String orderId) { + final result = _remoteDataSource.getOrderStream(orderId); + + switch (result) { + case SuccessApiResult>(): + return SuccessApiResult>( + data: result.data.map((dto) => dto.toOrderModel()), + ); + case ErrorApiResult>(): + return ErrorApiResult>(error: result.error); + } + } +} diff --git a/lib/features/driver_orders_details/domain/models/orders_model.dart b/lib/features/driver_orders_details/domain/models/orders_model.dart new file mode 100644 index 0000000..9e96435 --- /dev/null +++ b/lib/features/driver_orders_details/domain/models/orders_model.dart @@ -0,0 +1,68 @@ +class OrderModel { + final String orderId; + final String driverId; + final String userId; + final OrderDetailsModel orderDetails; + final UserAddressModel userAddress; + + OrderModel({ + required this.orderId, + required this.driverId, + required this.userId, + required this.orderDetails, + required this.userAddress, + }); +} + +class OrderDetailsModel { + final List items; + final String status; + final double totalPrice; + final PickedAddressModel pickupAddress; + final String orderId; + final String userAddress; + + OrderDetailsModel({ + required this.items, + required this.status, + required this.totalPrice, + required this.pickupAddress, + required this.orderId, + required this.userAddress, + }); +} + +class OrderItemModel { + final String productId; + final String title; + final String image; + final int quantity; + final double price; + + OrderItemModel({ + required this.productId, + required this.title, + required this.image, + required this.quantity, + required this.price, + }); +} + +class PickedAddressModel { + final String name; + final String address; + + PickedAddressModel({required this.name, required this.address}); +} + +class UserAddressModel { + final String userId; + final String name; + final String address; + + UserAddressModel({ + required this.name, + required this.address, + required this.userId, + }); +} diff --git a/lib/features/driver_orders_details/domain/repos/order_details_repo.dart b/lib/features/driver_orders_details/domain/repos/order_details_repo.dart new file mode 100644 index 0000000..942beaa --- /dev/null +++ b/lib/features/driver_orders_details/domain/repos/order_details_repo.dart @@ -0,0 +1,6 @@ +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; + +abstract class OrderDetailsRepo { + ApiResult> getOrderDetails(String orderId); +} diff --git a/lib/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart b/lib/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart new file mode 100644 index 0000000..e3253c1 --- /dev/null +++ b/lib/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart @@ -0,0 +1,13 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; + +@injectable +class GetOrderDetailsUsecase { + OrderDetailsRepo _repo; + GetOrderDetailsUsecase({required OrderDetailsRepo repo}) : _repo = repo; + + ApiResult> call(String orderId) => + _repo.getOrderDetails(orderId); +} diff --git a/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart b/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart new file mode 100644 index 0000000..224458f --- /dev/null +++ b/lib/features/driver_orders_details/presentation/manager/order_details_cubit.dart @@ -0,0 +1,58 @@ +import 'dart:async'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; +import '../../domain/usecases/get_order_details_usecase.dart'; +import 'order_details_states.dart'; + +@injectable +class OrderDetailsCubit extends Cubit { + final GetOrderDetailsUsecase _getOrderDetailsUsecase; + StreamSubscription? _subscription; + final _authStorage = getIt(); + + OrderDetailsCubit(this._getOrderDetailsUsecase) : super(OrderDetailsStates()); + + void getOrderDetails() async { + emit(state.copyWith(data: Resource.loading())); + _subscription?.cancel(); + + try { + final orderId = await _authStorage.getOrderId(); + if (orderId == null || orderId.isEmpty) { + emit(state.copyWith(data: Resource.error('Order ID not found'))); + return; + } + final result = _getOrderDetailsUsecase.call(orderId); + + if (result is SuccessApiResult>) { + _subscription = result.data.listen( + (order) { + emit(state.copyWith(data: Resource.success(order))); + }, + onError: (error) { + emit(state.copyWith(data: Resource.error(error.toString()))); + }, + ); + } else if (result is ErrorApiResult>) { + emit(state.copyWith(data: Resource.error(result.error))); + } + } catch (e) { + emit( + state.copyWith( + data: Resource.error("Error retrieving order details: $e"), + ), + ); + } + } + + @override + Future close() { + _subscription?.cancel(); + return super.close(); + } +} diff --git a/lib/features/driver_orders_details/presentation/manager/order_details_states.dart b/lib/features/driver_orders_details/presentation/manager/order_details_states.dart new file mode 100644 index 0000000..267a1ca --- /dev/null +++ b/lib/features/driver_orders_details/presentation/manager/order_details_states.dart @@ -0,0 +1,11 @@ +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; + +class OrderDetailsStates { + final Resource? data; + const OrderDetailsStates({this.data}); + + OrderDetailsStates copyWith({Resource? data}) { + return OrderDetailsStates(data: data ?? this.data); + } +} diff --git a/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart b/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart new file mode 100644 index 0000000..aa8ba57 --- /dev/null +++ b/lib/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart @@ -0,0 +1,165 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/app/core/values/paths.dart'; +import 'package:tracking_app/app/core/widgets/custom_button.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_cubit.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_states.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/address_card.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/bottom_row_section.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/order_item.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/order_status.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/section_title.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; + +class DriversOrdersDetailsPage extends StatelessWidget { + const DriversOrdersDetailsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: AppColors.blackColor), + onPressed: () => context.pop(), + ), + title: Text( + LocaleKeys.orderDetails.tr(), + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 20, + color: AppColors.blackColor, + ), + ), + ), + body: BlocProvider( + create: (context) => getIt()..getOrderDetails(), + child: BlocBuilder( + builder: (context, state) { + if (state.data?.status == Status.loading) { + return const Center(child: CircularProgressIndicator()); + } else if (state.data?.status == Status.error) { + return Center(child: Text(state.data!.error.toString())); + } else if (state.data?.status == Status.success) { + final order = state.data!.data; + final status = OrderStatus.fromString(order?.orderDetails.status); + + int currentStep = status.step; + return SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: List.generate(5, (index) { + return Expanded( + child: Container( + height: 4, + margin: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: index < currentStep + ? AppColors.green + : AppColors.lightGrey, + borderRadius: BorderRadius.circular(2), + ), + ), + ); + }), + ), + const SizedBox(height: 20), + + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.lightPink, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${LocaleKeys.status.tr()}${order?.orderDetails.status}', + style: TextStyle( + color: AppColors.green, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 4), + Text( + '${LocaleKeys.orderId.tr()}${order?.orderId}', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 4), + Text( + 'Wed, 03 Sep 2024, 11:00 AM', + style: TextStyle( + color: AppColors.grey, + fontSize: 14, + ), + ), + ], + ), + ), + const SizedBox(height: 24), + + SectionTitle(title: LocaleKeys.pickupAddress.tr()), + AddressCard( + title: order?.orderDetails.pickupAddress.name ?? '', + address: order?.orderDetails.pickupAddress.address ?? '', + imagePath: AppPaths.flowerLogo, + ), + const SizedBox(height: 16), + SectionTitle(title: LocaleKeys.userAddress.tr()), + + AddressCard( + title: order?.userAddress.name ?? '', + address: order?.userAddress.address ?? '', + imagePath: AppPaths.flowerLogo, + ), + const SizedBox(height: 24), + + SectionTitle(title: LocaleKeys.orderDetails.tr()), + OrderItems(), + const SizedBox(height: 16), + + BottomRowSection( + label: LocaleKeys.total.tr(), + value: + '${LocaleKeys.egp.tr()} ${order?.orderDetails.totalPrice.toStringAsFixed(2)}', + ), + BottomRowSection( + label: LocaleKeys.payment_method.tr(), + value: LocaleKeys.cash_on_delivery.tr(), + ), + + const SizedBox(height: 32), + + SizedBox( + width: double.infinity, + height: 55, + child: CustomButton( + isEnabled: true, + onPressed: () {}, + isLoading: false, + text: status.buttonTextKey.tr(), + ), + ), + ], + ), + ); + } + return const SizedBox.shrink(); + }, + ), + ), + ); + } +} diff --git a/lib/features/driver_orders_details/presentation/widgets/address_card.dart b/lib/features/driver_orders_details/presentation/widgets/address_card.dart new file mode 100644 index 0000000..6b211c2 --- /dev/null +++ b/lib/features/driver_orders_details/presentation/widgets/address_card.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/app/core/values/paths.dart'; + +class AddressCard extends StatelessWidget { + final String title; + final String address; + final String imagePath; + + const AddressCard({ + super.key, + required this.title, + required this.address, + required this.imagePath, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: AppColors.lightGrey), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + CircleAvatar(backgroundImage: AssetImage(imagePath), radius: 25), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of( + context, + ).textTheme.labelSmall!.copyWith(fontWeight: FontWeight.w400), + ), + Row( + children: [ + Icon( + Icons.location_on_outlined, + size: 16, + color: AppColors.blackColor, + ), + Flexible( + child: Text( + address, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: Theme.of(context).textTheme.labelSmall!.copyWith( + fontWeight: FontWeight.w400, + color: AppColors.blackColor, + ), + ), + ), + ], + ), + ], + ), + ), + IconButton( + onPressed: () {}, + icon: Icon(Icons.phone_outlined, color: AppColors.pink, size: 20), + ), + + IconButton( + onPressed: () {}, + icon: ImageIcon( + AssetImage(AppPaths.whatsappImage), + color: AppColors.pink, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/driver_orders_details/presentation/widgets/bottom_row_section.dart b/lib/features/driver_orders_details/presentation/widgets/bottom_row_section.dart new file mode 100644 index 0000000..481983d --- /dev/null +++ b/lib/features/driver_orders_details/presentation/widgets/bottom_row_section.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; + +class BottomRowSection extends StatelessWidget { + final String label; + final String value; + const BottomRowSection({super.key, required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: AppColors.lightGrey), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: Theme.of(context).textTheme.labelMedium!.copyWith( + fontWeight: FontWeight.w500, + fontSize: 16, + ), + ), + Text( + value, + style: Theme.of( + context, + ).textTheme.labelSmall!.copyWith(fontWeight: FontWeight.w500), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/driver_orders_details/presentation/widgets/order_item.dart b/lib/features/driver_orders_details/presentation/widgets/order_item.dart new file mode 100644 index 0000000..1d6cebc --- /dev/null +++ b/lib/features/driver_orders_details/presentation/widgets/order_item.dart @@ -0,0 +1,85 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/app/core/values/paths.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_cubit.dart'; + +class OrderItems extends StatelessWidget { + const OrderItems({super.key}); + + @override + Widget build(BuildContext context) { + final order = BlocProvider.of(context).state.data!.data; + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: order?.orderDetails.items.length, + itemBuilder: (context, index) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: AppColors.lightGrey), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(50), + child: CachedNetworkImage( + imageUrl: + "${AppPaths.mediaUrl}${order!.orderDetails.items[index].image}", + placeholder: (context, url) => Shimmer( + gradient: LinearGradient( + colors: [ + AppColors.lightGrey, + AppColors.white, + AppColors.lightGrey, + ], + ), + child: const CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => Icon(Icons.error), + width: 55, + height: 55, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + order.orderDetails.items[index].title, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.labelSmall!.copyWith( + fontWeight: FontWeight.w400, + ), + ), + Text( + 'EGP ${order.orderDetails.items[index].price.toStringAsFixed(2)}', + style: Theme.of(context).textTheme.labelSmall!.copyWith( + fontWeight: FontWeight.w500, + color: AppColors.blackColor, + ), + ), + ], + ), + ), + Text( + 'X${order.orderDetails.items[index].quantity}', + style: Theme.of(context).textTheme.labelSmall!.copyWith( + fontWeight: FontWeight.w500, + color: AppColors.pink, + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/features/driver_orders_details/presentation/widgets/order_status.dart b/lib/features/driver_orders_details/presentation/widgets/order_status.dart new file mode 100644 index 0000000..4c88788 --- /dev/null +++ b/lib/features/driver_orders_details/presentation/widgets/order_status.dart @@ -0,0 +1,83 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; + +enum OrderStatus { + accepted, + pickup, + outForDelivery, + arrived, + delivered, + unknown; + + static OrderStatus fromString(String? status) { + switch (status?.toLowerCase()) { + case 'accepted': + return OrderStatus.accepted; + case 'pickup': + return OrderStatus.pickup; + case 'out_for_delivery': + return OrderStatus.outForDelivery; + case 'arrived': + return OrderStatus.arrived; + case 'delivered': + return OrderStatus.delivered; + default: + debugPrint('Unknown order status: $status'); + return OrderStatus.unknown; + } + } +} + +extension OrderStatusX on OrderStatus { + int get step { + switch (this) { + case OrderStatus.accepted: + return 1; + case OrderStatus.pickup: + return 2; + case OrderStatus.outForDelivery: + return 3; + case OrderStatus.arrived: + return 4; + case OrderStatus.delivered: + return 5; + case OrderStatus.unknown: + return 1; + } + } + + String get buttonTextKey { + switch (this) { + case OrderStatus.accepted: + return LocaleKeys.arrivedAtPickupPoint.tr(); + case OrderStatus.pickup: + return LocaleKeys.startDelivery.tr(); + case OrderStatus.outForDelivery: + return LocaleKeys.arriverAtDestination.tr(); + case OrderStatus.arrived: + return LocaleKeys.confirmDelivery.tr(); + case OrderStatus.delivered: + return LocaleKeys.orderCompleted.tr(); + case OrderStatus.unknown: + return LocaleKeys.arrivedAtPickupPoint; + } + } + + String get statusTextKey { + switch (this) { + case OrderStatus.accepted: + return LocaleKeys.accepted.tr(); + case OrderStatus.pickup: + return LocaleKeys.pickedUp.tr(); + case OrderStatus.outForDelivery: + return LocaleKeys.outForDelivery.tr(); + case OrderStatus.arrived: + return LocaleKeys.arrived.tr(); + case OrderStatus.delivered: + return LocaleKeys.delivered.tr(); + case OrderStatus.unknown: + return ''; + } + } +} diff --git a/lib/features/driver_orders_details/presentation/widgets/section_title.dart b/lib/features/driver_orders_details/presentation/widgets/section_title.dart new file mode 100644 index 0000000..8055f29 --- /dev/null +++ b/lib/features/driver_orders_details/presentation/widgets/section_title.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; + +class SectionTitle extends StatelessWidget { + final String title; + const SectionTitle({super.key, required this.title}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Text( + title, + style: Theme.of( + context, + ).textTheme.bodyMedium!.copyWith(color: AppColors.blackColor), + ), + ); + } +} diff --git a/lib/features/home/api/driverOrderDataS_imp.dart b/lib/features/home/api/driverOrderDataS_imp.dart new file mode 100644 index 0000000..58a9510 --- /dev/null +++ b/lib/features/home/api/driverOrderDataS_imp.dart @@ -0,0 +1,24 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/api_manger/api_client.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/app/core/network/safe_api_call.dart'; +import 'package:tracking_app/features/home/data/datascourse/driverOrderDatascource.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; + +@Injectable(as: DriverOrderDataSource) +class DriverOrderDataSourceImpl implements DriverOrderDataSource { + final ApiClient _apiClient; + + DriverOrderDataSourceImpl(this._apiClient); + + @override + Future> getPendingOrders(String token) { + return safeApiCall(call: () => _apiClient.getPendingOrders(token)); + } + + @override + Future> getProfile(String token) { + return safeApiCall(call: () => _apiClient.getProfile(token: token)); + } +} diff --git a/lib/features/home/data/datascourse/driverOrderDatascource.dart b/lib/features/home/data/datascourse/driverOrderDatascource.dart new file mode 100644 index 0000000..b0c7709 --- /dev/null +++ b/lib/features/home/data/datascourse/driverOrderDatascource.dart @@ -0,0 +1,8 @@ +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; + +abstract class DriverOrderDataSource { + Future> getPendingOrders(String token); + Future> getProfile(String token); +} diff --git a/lib/features/home/data/model/response/orderRespons.dart b/lib/features/home/data/model/response/orderRespons.dart new file mode 100644 index 0000000..0b96f51 --- /dev/null +++ b/lib/features/home/data/model/response/orderRespons.dart @@ -0,0 +1,277 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'orderRespons.g.dart'; + +@JsonSerializable() +class OrderResponse { + @JsonKey(name: "message") + final String? message; + @JsonKey(name: "metadata") + final Metadata? metadata; + @JsonKey(name: "orders") + final List? orders; + + OrderResponse({this.message, this.metadata, this.orders}); + + factory OrderResponse.fromJson(Map json) => + _$OrderResponseFromJson(json); + + Map toJson() => _$OrderResponseToJson(this); + + OrderResponse copyWith({ + String? message, + Metadata? metadata, + List? orders, + }) { + return OrderResponse( + message: message ?? this.message, + metadata: metadata ?? this.metadata, + orders: orders ?? this.orders, + ); + } +} + +@JsonSerializable() +class Metadata { + @JsonKey(name: "currentPage") + final int? currentPage; + @JsonKey(name: "totalPages") + final int? totalPages; + @JsonKey(name: "totalItems") + final int? totalItems; + @JsonKey(name: "limit") + final int? limit; + + Metadata({this.currentPage, this.totalPages, this.totalItems, this.limit}); + + factory Metadata.fromJson(Map json) => + _$MetadataFromJson(json); + + Map toJson() => _$MetadataToJson(this); +} + +@JsonSerializable() +class Order { + @JsonKey(name: "_id") + final String? id; + @JsonKey(name: "user") + final User? user; + @JsonKey(name: "orderItems") + final List? orderItems; + @JsonKey(name: "totalPrice") + final int? totalPrice; + @JsonKey(name: "paymentType") + final String? paymentType; + @JsonKey(name: "isPaid") + final bool? isPaid; + @JsonKey(name: "isDelivered") + final bool? isDelivered; + @JsonKey(name: "state") + final String? state; + @JsonKey(name: "createdAt") + final DateTime? createdAt; + @JsonKey(name: "updatedAt") + final DateTime? updatedAt; + @JsonKey(name: "orderNumber") + final String? orderNumber; + @JsonKey(name: "__v") + final int? v; + @JsonKey(name: "store") + final Store? store; + @JsonKey(name: "shippingAddress") + final ShippingAddress? shippingAddress; + @JsonKey(name: "paidAt") + final DateTime? paidAt; + + Order({ + this.id, + this.user, + this.orderItems, + this.totalPrice, + this.paymentType, + this.isPaid, + this.isDelivered, + this.state, + this.createdAt, + this.updatedAt, + this.orderNumber, + this.v, + this.store, + this.shippingAddress, + this.paidAt, + }); + + factory Order.fromJson(Map json) => _$OrderFromJson(json); + + Map toJson() => _$OrderToJson(this); +} + +@JsonSerializable() +class OrderItem { + @JsonKey(name: "product") + final Product? product; + @JsonKey(name: "price") + final int? price; + @JsonKey(name: "quantity") + final int? quantity; + @JsonKey(name: "_id") + final String? id; + + OrderItem({this.product, this.price, this.quantity, this.id}); + + factory OrderItem.fromJson(Map json) => + _$OrderItemFromJson(json); + + Map toJson() => _$OrderItemToJson(this); +} + +@JsonSerializable() +class Product { + @JsonKey(name: "_id") + final String? id; + @JsonKey(name: "title") + final String? title; + @JsonKey(name: "slug") + final String? slug; + @JsonKey(name: "description") + final String? description; + @JsonKey(name: "imgCover") + final String? imgCover; + @JsonKey(name: "images") + final List? images; + @JsonKey(name: "price") + final int? price; + @JsonKey(name: "priceAfterDiscount") + final int? priceAfterDiscount; + @JsonKey(name: "quantity") + final int? quantity; + @JsonKey(name: "category") + final String? category; + @JsonKey(name: "occasion") + final String? occasion; + @JsonKey(name: "createdAt") + final DateTime? createdAt; + @JsonKey(name: "updatedAt") + final DateTime? updatedAt; + @JsonKey(name: "__v") + final int? v; + @JsonKey(name: "sold") + final int? sold; + @JsonKey(name: "isSuperAdmin") + final bool? isSuperAdmin; + @JsonKey(name: "rateAvg") + final int? rateAvg; + @JsonKey(name: "rateCount") + final int? rateCount; + + Product({ + this.id, + this.title, + this.slug, + this.description, + this.imgCover, + this.images, + this.price, + this.priceAfterDiscount, + this.quantity, + this.category, + this.occasion, + this.createdAt, + this.updatedAt, + this.v, + this.sold, + this.isSuperAdmin, + this.rateAvg, + this.rateCount, + }); + + factory Product.fromJson(Map json) => + _$ProductFromJson(json); + + Map toJson() => _$ProductToJson(this); +} + +@JsonSerializable() +class ShippingAddress { + @JsonKey(name: "street") + final String? street; + @JsonKey(name: "city") + final String? city; + @JsonKey(name: "phone") + final String? phone; + @JsonKey(name: "lat") + final String? lat; + @JsonKey(name: "long") + final String? long; + + ShippingAddress({this.street, this.city, this.phone, this.lat, this.long}); + + factory ShippingAddress.fromJson(Map json) => + _$ShippingAddressFromJson(json); + + Map toJson() => _$ShippingAddressToJson(this); +} + +@JsonSerializable() +class Store { + @JsonKey(name: "name") + final String? name; + @JsonKey(name: "image") + final String? image; + @JsonKey(name: "address") + final String? address; + @JsonKey(name: "phoneNumber") + final String? phoneNumber; + @JsonKey(name: "latLong") + final String? latLong; + + Store({this.name, this.image, this.address, this.phoneNumber, this.latLong}); + + factory Store.fromJson(Map json) => _$StoreFromJson(json); + + Map toJson() => _$StoreToJson(this); +} + +@JsonSerializable() +class User { + @JsonKey(name: "_id") + final String? id; + @JsonKey(name: "firstName") + final String? firstName; + @JsonKey(name: "lastName") + final String? lastName; + @JsonKey(name: "email") + final String? email; + @JsonKey(name: "gender") + final String? gender; + @JsonKey(name: "phone") + final String? phone; + @JsonKey(name: "photo") + final String? photo; + @JsonKey(name: "passwordChangedAt") + final DateTime? passwordChangedAt; + @JsonKey(name: "passwordResetCode") + final String? passwordResetCode; + @JsonKey(name: "passwordResetExpires") + final DateTime? passwordResetExpires; + @JsonKey(name: "resetCodeVerified") + final bool? resetCodeVerified; + + User({ + this.id, + this.firstName, + this.lastName, + this.email, + this.gender, + this.phone, + this.photo, + this.passwordChangedAt, + this.passwordResetCode, + this.passwordResetExpires, + this.resetCodeVerified, + }); + + factory User.fromJson(Map json) => _$UserFromJson(json); + + Map toJson() => _$UserToJson(this); +} diff --git a/lib/features/home/data/repo/driverOrderRepo_impl.dart b/lib/features/home/data/repo/driverOrderRepo_impl.dart new file mode 100644 index 0000000..51cad99 --- /dev/null +++ b/lib/features/home/data/repo/driverOrderRepo_impl.dart @@ -0,0 +1,23 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/data/datascourse/driverOrderDatascource.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; +import 'package:tracking_app/features/home/domain/repo/driverOrderRepo.dart'; + +@Injectable(as: DriverOrderRepo) +class DriverOrderRepositoryImpl implements DriverOrderRepo { + final DriverOrderDataSource _dataSource; + + DriverOrderRepositoryImpl(this._dataSource); + + @override + Future> getPendingOrders(String token) { + return _dataSource.getPendingOrders(token); + } + + @override + Future> getProfile(String token) { + return _dataSource.getProfile(token); + } +} diff --git a/lib/features/home/domain/repo/driverOrderRepo.dart b/lib/features/home/domain/repo/driverOrderRepo.dart new file mode 100644 index 0000000..5fad3ee --- /dev/null +++ b/lib/features/home/domain/repo/driverOrderRepo.dart @@ -0,0 +1,8 @@ +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/profile/data/models/responses/edit_profile_response.dart'; + +abstract class DriverOrderRepo { + Future> getPendingOrders(String token); + Future> getProfile(String token); +} diff --git a/lib/features/home/domain/usecase/getdriverOrderUsecase.dart b/lib/features/home/domain/usecase/getdriverOrderUsecase.dart new file mode 100644 index 0000000..a138cb1 --- /dev/null +++ b/lib/features/home/domain/usecase/getdriverOrderUsecase.dart @@ -0,0 +1,15 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/home/domain/repo/driverOrderRepo.dart'; + +@injectable +class GetDriverOrdersUseCase { + final DriverOrderRepo _repository; + + GetDriverOrdersUseCase(this._repository); + + Future> call(String token) { + return _repository.getPendingOrders(token); + } +} diff --git a/lib/features/home/domain/usecase/upload_driver_fire_data_use_case.dart b/lib/features/home/domain/usecase/upload_driver_fire_data_use_case.dart new file mode 100644 index 0000000..89926b5 --- /dev/null +++ b/lib/features/home/domain/usecase/upload_driver_fire_data_use_case.dart @@ -0,0 +1,26 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/features/profile/data/models/driver_model.dart'; + +@injectable +class UploadDriverFireDataUseCase { + final FirebaseFirestore _firestore; + + UploadDriverFireDataUseCase(@Named('firestore') this._firestore); + + Future call( + DriverModel driver, { + required double lat, + required double lng, + String? deviceToken, + }) async { + final driverCollection = _firestore.collection('drivers'); + await driverCollection.doc(driver.Id).set({ + 'id': driver.Id, + 'name': '${driver.firstName} ${driver.lastName}', + 'phone': driver.phone, + 'currentLocation': {'lat': lat, 'lng': lng}, + 'deviceToken': deviceToken, + }, SetOptions(merge: true)); + } +} diff --git a/lib/features/home/domain/usecase/upload_order_fire_data_use_case.dart b/lib/features/home/domain/usecase/upload_order_fire_data_use_case.dart new file mode 100644 index 0000000..c35f894 --- /dev/null +++ b/lib/features/home/domain/usecase/upload_order_fire_data_use_case.dart @@ -0,0 +1,55 @@ +import 'package:cloud_firestore/cloud_firestore.dart' hide Order; +import 'package:injectable/injectable.dart' hide Order; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; + +@injectable +class UploadOrderFireDataUseCase { + final FirebaseFirestore _firestore; + + UploadOrderFireDataUseCase(@Named('firestore') this._firestore); + + Future call({required Order order, required String driverId}) async { + final orderCollection = _firestore.collection('orders'); + + final data = { + 'driver_id': driverId, + 'oder_dt': { + 'items': + order.orderItems + ?.map( + (e) => { + 'productId': e.product?.id, + 'title': e.product?.title, + 'quantity': e.quantity, + 'price': e.product?.price, + 'image': e.product?.imgCover, + }, + ) + .toList() ?? + [], + 'orderId': order.id, + 'pickupAddress': { + 'address': order.store?.address ?? '', + 'name': order.store?.name ?? '', + }, + 'status': order.state ?? 'pending', + 'totalPrice': order.totalPrice ?? 0, + 'userAddress': + '${order.shippingAddress?.street ?? ''}, ${order.shippingAddress?.city ?? ''}', + }, + 'userAddress': { + 'adress': + '${order.shippingAddress?.street ?? ''}, ${order.shippingAddress?.city ?? ''}', + 'name': '${order.user?.firstName ?? ''} ${order.user?.lastName ?? ''}', + 'user_id': order.user?.id ?? '', + }, + 'user_id': order.user?.id ?? '', + }; + + if (order.id != null) { + await orderCollection.doc(order.id).set(data, SetOptions(merge: true)); + } else { + await orderCollection.add(data); + } + } +} diff --git a/lib/features/home/presentation/manger/driverorderCubit.dart b/lib/features/home/presentation/manger/driverorderCubit.dart new file mode 100644 index 0000000..01f5170 --- /dev/null +++ b/lib/features/home/presentation/manger/driverorderCubit.dart @@ -0,0 +1,147 @@ +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart' hide Order; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/domain/usecase/getdriverOrderUsecase.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderIntent.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderStates.dart'; +import 'package:tracking_app/features/home/domain/repo/driverOrderRepo.dart'; +import 'package:tracking_app/features/home/domain/usecase/upload_driver_fire_data_use_case.dart'; +import 'package:tracking_app/features/home/domain/usecase/upload_order_fire_data_use_case.dart'; + +@injectable +class DriverOrderCubit extends Cubit { + final GetDriverOrdersUseCase _getDriverOrdersUseCase; + final AuthStorage _authStorage; + final UploadDriverFireDataUseCase _uploadDriverFireDataUseCase; + final UploadOrderFireDataUseCase _uploadOrderFireDataUseCase; + final DriverOrderRepo _driverOrderRepository; + + DriverOrderCubit( + this._getDriverOrdersUseCase, + this._authStorage, + this._uploadDriverFireDataUseCase, + this._uploadOrderFireDataUseCase, + this._driverOrderRepository, + ) : super(DriverOrderState()); + + void onIntent(DriverOrderIntent intent) { + switch (intent) { + case GetPendingOrders(): + _getPendingOrders(); + case RemoveOrder(order: final order): + _removeOrder(order); + case AcceptOrder(order: final order): + _acceptOrder(order); + } + } + + void _removeOrder(Order order) { + final currentResource = state.orderResource; + if (currentResource.status == Status.success && + currentResource.data != null) { + final currentOrders = currentResource.data!.orders!; + final updatedOrders = currentOrders + .where((element) => element != order) + .toList(); + emit( + state.copyWith( + orderResource: Resource.success( + currentResource.data!.copyWith(orders: updatedOrders), + ), + ), + ); + } + } + + Future _acceptOrder(Order order) async { + final token = await _authStorage.getToken(); + if (token == null) return; + + final result = await _driverOrderRepository.getProfile(token); + + if (result is SuccessApiResult) { + final profile = (result as SuccessApiResult).data; + if (profile.driver != null) { + try { + final position = await _determinePosition(); + if (position == null) { + if (kDebugMode) + print("Location permission denied or service disabled."); + return; + } + + final deviceToken = await FirebaseMessaging.instance.getToken(); + await _uploadDriverFireDataUseCase( + profile.driver!, + lat: position.latitude, + lng: position.longitude, + deviceToken: deviceToken, + ); + + await _uploadOrderFireDataUseCase( + order: order, + driverId: profile.driver?.Id ?? '', + ); + + if (order.id != null) { + await _authStorage.saveOrderId(order.id!); + } + } catch (e) { + if (kDebugMode) { + print("Firestore/Location Error: $e"); + } + } + } + } + } + + Future _determinePosition() async { + bool serviceEnabled; + LocationPermission permission; + + serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + return null; + } + + permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) { + return null; + } + } + + if (permission == LocationPermission.deniedForever) { + return null; + } + + return await Geolocator.getCurrentPosition(); + } + + Future _getPendingOrders() async { + emit(state.copyWith(orderResource: Resource.loading())); + final token = await _authStorage.getToken(); + if (token == null) { + emit( + state.copyWith(orderResource: Resource.error("User not authenticated")), + ); + return; + } + final result = await _getDriverOrdersUseCase(token); + return switch (result) { + SuccessApiResult(data: final orderResponse) => emit( + state.copyWith(orderResource: Resource.success(orderResponse)), + ), + ErrorApiResult(error: final error) => emit( + state.copyWith(orderResource: Resource.error(error)), + ), + }; + } +} diff --git a/lib/features/home/presentation/manger/driverorderIntent.dart b/lib/features/home/presentation/manger/driverorderIntent.dart new file mode 100644 index 0000000..9f88440 --- /dev/null +++ b/lib/features/home/presentation/manger/driverorderIntent.dart @@ -0,0 +1,15 @@ +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; + +sealed class DriverOrderIntent {} + +class GetPendingOrders extends DriverOrderIntent {} + +class RemoveOrder extends DriverOrderIntent { + final Order order; + RemoveOrder(this.order); +} + +class AcceptOrder extends DriverOrderIntent { + final Order order; + AcceptOrder(this.order); +} diff --git a/lib/features/home/presentation/manger/driverorderStates.dart b/lib/features/home/presentation/manger/driverorderStates.dart new file mode 100644 index 0000000..c93079f --- /dev/null +++ b/lib/features/home/presentation/manger/driverorderStates.dart @@ -0,0 +1,13 @@ +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; + +class DriverOrderState { + final Resource orderResource; + + DriverOrderState({Resource? orderResource}) + : orderResource = orderResource ?? Resource.initial(); + + DriverOrderState copyWith({Resource? orderResource}) { + return DriverOrderState(orderResource: orderResource ?? this.orderResource); + } +} diff --git a/lib/features/home/presentation/pages/driverOrderScreen.dart b/lib/features/home/presentation/pages/driverOrderScreen.dart new file mode 100644 index 0000000..41b2d8e --- /dev/null +++ b/lib/features/home/presentation/pages/driverOrderScreen.dart @@ -0,0 +1,31 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderCubit.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderIntent.dart'; +import 'package:tracking_app/features/home/presentation/widgets/driverScreenBody.dart'; + +class DriverOrderScreen extends StatelessWidget { + const DriverOrderScreen({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + getIt()..onIntent(GetPendingOrders()), + child: Scaffold( + appBar: AppBar( + title: Text( + LocaleKeys.floweryRider.tr(), + style: const TextStyle(color: AppColors.pink), + ), + ), + body: const DriverOrderBody(), + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/driverOrderButton.dart b/lib/features/home/presentation/widgets/driverOrderButton.dart new file mode 100644 index 0000000..6759d98 --- /dev/null +++ b/lib/features/home/presentation/widgets/driverOrderButton.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +class DriverOrderButton extends StatelessWidget { + final String text; + final VoidCallback onTap; + final bool isPrimary; + + const DriverOrderButton({ + super.key, + required this.text, + required this.onTap, + required this.isPrimary, + }); + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + final height = MediaQuery.of(context).size.height; + return InkWell( + onTap: onTap, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width * 0.06, + vertical: height * 0.012, + ), + decoration: BoxDecoration( + color: isPrimary ? const Color(0xFFE91E63) : Colors.white, + borderRadius: BorderRadius.circular(24), + border: isPrimary ? null : Border.all(color: const Color(0xFFE91E63)), + ), + child: Text( + text, + style: TextStyle( + color: isPrimary ? Colors.white : const Color(0xFFE91E63), + fontSize: width * 0.035, + fontWeight: FontWeight.w500, + ), + ), + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/driverOrderInfoCard.dart b/lib/features/home/presentation/widgets/driverOrderInfoCard.dart new file mode 100644 index 0000000..c8b668a --- /dev/null +++ b/lib/features/home/presentation/widgets/driverOrderInfoCard.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; + +class DriverOrderInfoCard extends StatelessWidget { + final String? image; + final String title; + final String subtitle; + final bool isStore; + + const DriverOrderInfoCard({ + super.key, + required this.image, + required this.title, + required this.subtitle, + required this.isStore, + }); + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + final height = MediaQuery.of(context).size.height; + return Container( + padding: EdgeInsets.all(width * 0.03), + decoration: BoxDecoration( + color: const Color(0xFFF9F9F9), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFEEEEEE)), + ), + child: Row( + children: [ + Container( + width: width * 0.12, + height: width * 0.12, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isStore ? const Color(0xFFE91E63) : Colors.grey[300], + image: image != null + ? DecorationImage( + image: NetworkImage(image!), + fit: BoxFit.cover, + ) + : null, + ), + child: image == null + ? Icon( + isStore ? Icons.store : Icons.person, + color: Colors.white, + ) + : null, + ), + SizedBox(width: width * 0.03), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: width * 0.035, + fontWeight: FontWeight.w500, + color: const Color(0xFF2D2D2D), + ), + ), + SizedBox(height: height * 0.005), + Row( + children: [ + Icon( + Icons.location_on_outlined, + size: width * 0.035, + color: Colors.black54, + ), + SizedBox(width: width * 0.01), + Expanded( + child: Text( + subtitle, + style: TextStyle( + fontSize: width * 0.03, + color: Colors.black54, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/driverOrderItem.dart b/lib/features/home/presentation/widgets/driverOrderItem.dart new file mode 100644 index 0000000..271950d --- /dev/null +++ b/lib/features/home/presentation/widgets/driverOrderItem.dart @@ -0,0 +1,104 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/home/presentation/widgets/driverOrderButton.dart'; +import 'package:tracking_app/features/home/presentation/widgets/driverOrderInfoCard.dart'; +import 'package:tracking_app/features/home/presentation/widgets/driverOrderSectionLabel.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; + +class DriverOrderItem extends StatelessWidget { + final Order order; + final VoidCallback onAccept; + final VoidCallback onReject; + + const DriverOrderItem({ + super.key, + required this.order, + required this.onAccept, + required this.onReject, + }); + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + final height = MediaQuery.of(context).size.height; + return Container( + margin: EdgeInsets.symmetric( + horizontal: width * 0.04, + vertical: height * 0.01, + ), + padding: EdgeInsets.all(width * 0.04), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.driverOrderTitle.tr(), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF2D2D2D), + ), + ), + SizedBox(height: height * 0.02), + DriverOrderSectionLabel(LocaleKeys.pickupAddress.tr()), + SizedBox(height: height * 0.01), + DriverOrderInfoCard( + image: order.store?.image, + title: order.store?.name ?? LocaleKeys.unknownStore.tr(), + subtitle: order.store?.address ?? LocaleKeys.noAddress.tr(), + isStore: true, + ), + SizedBox(height: height * 0.02), + DriverOrderSectionLabel(LocaleKeys.userAddress.tr()), + SizedBox(height: height * 0.01), + DriverOrderInfoCard( + image: order.user?.photo != null + ? "https://flower.elevateegy.com/uploads/${order.user!.photo!}" + : null, + title: + "${order.user?.firstName ?? ''} ${order.user?.lastName ?? ''}", + subtitle: + order.shippingAddress?.street ?? LocaleKeys.noAddress.tr(), + isStore: false, + ), + SizedBox(height: height * 0.03), + Row( + children: [ + Text( + "${order.totalPrice ?? 0} ${LocaleKeys.egp.tr()}", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF2D2D2D), + ), + ), + const Spacer(), + DriverOrderButton( + text: LocaleKeys.reject.tr(), + onTap: onReject, + isPrimary: false, + ), + SizedBox(width: width * 0.02), + DriverOrderButton( + text: LocaleKeys.accept.tr(), + onTap: onAccept, + isPrimary: true, + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/driverOrderSectionLabel.dart b/lib/features/home/presentation/widgets/driverOrderSectionLabel.dart new file mode 100644 index 0000000..f15fb59 --- /dev/null +++ b/lib/features/home/presentation/widgets/driverOrderSectionLabel.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class DriverOrderSectionLabel extends StatelessWidget { + final String text; + const DriverOrderSectionLabel(this.text, {super.key}); + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + return Text( + text, + style: TextStyle(fontSize: width * 0.035, color: Colors.grey), + ); + } +} diff --git a/lib/features/home/presentation/widgets/driverScreenBody.dart b/lib/features/home/presentation/widgets/driverScreenBody.dart new file mode 100644 index 0000000..1bfefb6 --- /dev/null +++ b/lib/features/home/presentation/widgets/driverScreenBody.dart @@ -0,0 +1,80 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/app/core/router/route_names.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderCubit.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderIntent.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderStates.dart'; +import 'package:tracking_app/features/home/presentation/widgets/driverOrderItem.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; + +class DriverOrderBody extends StatefulWidget { + const DriverOrderBody({super.key}); + + @override + State createState() => _DriverOrderBodyState(); +} + +class _DriverOrderBodyState extends State { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final resource = state.orderResource; + + if (resource.status == Status.loading) { + return const Center(child: CircularProgressIndicator()); + } + + if (resource.status == Status.error) { + return Center( + child: Text( + resource.error ?? LocaleKeys.unknownError.tr(), + style: const TextStyle(color: Colors.red), + ), + ); + } + + if (resource.status == Status.success) { + final orders = resource.data?.orders ?? []; + if (orders.isEmpty) { + return Center(child: Text(LocaleKeys.noPendingOrders.tr())); + } + return RefreshIndicator( + onRefresh: () async { + context.read().onIntent(GetPendingOrders()); + }, + child: ListView.builder( + itemCount: orders.length, + itemBuilder: (context, index) { + return DriverOrderItem( + order: orders[index], + onAccept: () async { + final order = orders[index]; + await getIt().saveOrderId(order.id.toString()); + debugPrint('<<<< Saved Order ID: ${order.id}'); + context.read().onIntent( + AcceptOrder(orders[index]), + ); + context.push(RouteNames.ordersDetailsPage); + }, + onReject: () { + context.read().onIntent( + RemoveOrder(orders[index]), + ); + }, + ); + }, + ), + ); + } + + return const SizedBox.shrink(); + }, + ); + } +} diff --git a/lib/features/my_orders/api/datasource/my_orders_remote_data_source_imp.dart b/lib/features/my_orders/api/datasource/my_orders_remote_data_source_imp.dart new file mode 100644 index 0000000..b419023 --- /dev/null +++ b/lib/features/my_orders/api/datasource/my_orders_remote_data_source_imp.dart @@ -0,0 +1,24 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/api_manger/api_client.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/app/core/network/safe_api_call.dart'; +import 'package:tracking_app/features/my_orders/data/datasource/my_orders_remote_data_source.dart'; +import 'package:tracking_app/features/my_orders/data/models/response/my_order_response.dart'; + +@Injectable(as: MyOrdersRemoteDataSource) +class MyOrdersRemoteDataSourceImp extends MyOrdersRemoteDataSource { + final ApiClient apiClient; + MyOrdersRemoteDataSourceImp(this.apiClient); + + @override + Future> getAllOrders({ + required String token, + int limit = 10, + int page = 1, + }) { + return safeApiCall( + call: () => + apiClient.getAllOrders(token: token, limit: limit, page: page), + ); + } +} diff --git a/lib/features/my_orders/data/datasource/my_orders_remote_data_source.dart b/lib/features/my_orders/data/datasource/my_orders_remote_data_source.dart new file mode 100644 index 0000000..8648ffa --- /dev/null +++ b/lib/features/my_orders/data/datasource/my_orders_remote_data_source.dart @@ -0,0 +1,10 @@ +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/my_orders/data/models/response/my_order_response.dart'; + +abstract class MyOrdersRemoteDataSource { + Future> getAllOrders({ + required String token, + int limit = 10, + int page = 1, + }); +} diff --git a/lib/features/my_orders/data/mappers/metadata_mapper.dart b/lib/features/my_orders/data/mappers/metadata_mapper.dart new file mode 100644 index 0000000..3b64bf2 --- /dev/null +++ b/lib/features/my_orders/data/mappers/metadata_mapper.dart @@ -0,0 +1,15 @@ +import 'package:tracking_app/features/my_orders/data/models/meta_data_dto.dart'; +import 'package:tracking_app/features/my_orders/domain/models/meta_data_entity.dart'; + +extension MetadataMapper on Metadata { + MetadataEntity toEntity() { + return MetadataEntity( + currentPage: currentPage ?? 0, + totalPages: totalPages ?? 0, + totalItems: totalItems ?? 0, + limit: limit ?? 10, + cancelledCount: cancelledCount ?? 0, + completedCount: completedCount ?? 0, + ); + } +} diff --git a/lib/features/my_orders/data/mappers/order_item_mapper.dart b/lib/features/my_orders/data/mappers/order_item_mapper.dart new file mode 100644 index 0000000..c36c2b9 --- /dev/null +++ b/lib/features/my_orders/data/mappers/order_item_mapper.dart @@ -0,0 +1,17 @@ +import 'package:tracking_app/features/my_orders/domain/models/order_item_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/product_entity.dart'; + +import '../models/order_item_model.dart'; +import 'product_mapper.dart'; + +extension OrderItemMapper on OrderItem { + OrderItemEntity toEntity() { + return OrderItemEntity( + product: + product?.toEntity() ?? + ProductEntity(id: '', price: 0, title: '', image: ''), + price: price ?? 0, + quantity: quantity ?? 0, + ); + } +} diff --git a/lib/features/my_orders/data/mappers/order_mapper.dart b/lib/features/my_orders/data/mappers/order_mapper.dart new file mode 100644 index 0000000..06571e0 --- /dev/null +++ b/lib/features/my_orders/data/mappers/order_mapper.dart @@ -0,0 +1,25 @@ +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; + +import '../models/order_model.dart'; +import 'order_item_mapper.dart'; +import 'user_mapper.dart'; +import 'store_mapper.dart'; + +extension OrderMapper on Order { + OrderEntity toEntity() { + return OrderEntity( + id: id ?? '', + user: user!.toEntity(), + store: store?.toEntity(), + address: address ?? '', + items: orderItems?.map((e) => e.toEntity()).toList() ?? [], + totalPrice: totalPrice ?? 0, + paymentType: paymentType ?? '', + isPaid: isPaid ?? false, + isDelivered: isDelivered ?? false, + state: state ?? '', + createdAt: createdAt ?? '', + orderNumber: orderNumber ?? '', + ); + } +} diff --git a/lib/features/my_orders/data/mappers/orders_list_mapper.dart b/lib/features/my_orders/data/mappers/orders_list_mapper.dart new file mode 100644 index 0000000..d1be05b --- /dev/null +++ b/lib/features/my_orders/data/mappers/orders_list_mapper.dart @@ -0,0 +1,9 @@ +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; +import '../models/order_model.dart'; +import 'order_mapper.dart'; + +extension OrdersListMapper on List { + List toEntityList() { + return map((e) => e.toEntity()).toList(); + } +} diff --git a/lib/features/my_orders/data/mappers/product_mapper.dart b/lib/features/my_orders/data/mappers/product_mapper.dart new file mode 100644 index 0000000..c7010f5 --- /dev/null +++ b/lib/features/my_orders/data/mappers/product_mapper.dart @@ -0,0 +1,13 @@ +import 'package:tracking_app/features/my_orders/domain/models/product_entity.dart'; +import '../models/product_model.dart'; + +extension ProductMapper on Product { + ProductEntity toEntity() { + return ProductEntity( + id: id ?? '', + title: title ?? '', + image: image ?? '', + price: price ?? 0, + ); + } +} diff --git a/lib/features/my_orders/data/mappers/store_mapper.dart b/lib/features/my_orders/data/mappers/store_mapper.dart new file mode 100644 index 0000000..3f4b806 --- /dev/null +++ b/lib/features/my_orders/data/mappers/store_mapper.dart @@ -0,0 +1,13 @@ +import 'package:tracking_app/features/my_orders/domain/models/store_entity.dart'; +import '../models/store_model.dart'; + +extension StoreMapper on Store { + StoreEntity toEntity() { + return StoreEntity( + name: name ?? '', + image: image ?? '', + address: address ?? '', + phoneNumber: phoneNumber ?? '', + ); + } +} diff --git a/lib/features/my_orders/data/mappers/user_mapper.dart b/lib/features/my_orders/data/mappers/user_mapper.dart new file mode 100644 index 0000000..9feb6e1 --- /dev/null +++ b/lib/features/my_orders/data/mappers/user_mapper.dart @@ -0,0 +1,14 @@ +import 'package:tracking_app/features/my_orders/domain/models/user_entity.dart'; +import '../models/user_model.dart'; + +extension UserMapper on User { + UserEntity toEntity() { + return UserEntity( + id: id ?? '', + firstName: firstName ?? '', + lastName: lastName ?? '', + phone: phone ?? '', + photo: photo ?? '', + ); + } +} diff --git a/lib/features/my_orders/data/models/meta_data_dto.dart b/lib/features/my_orders/data/models/meta_data_dto.dart new file mode 100644 index 0000000..017f445 --- /dev/null +++ b/lib/features/my_orders/data/models/meta_data_dto.dart @@ -0,0 +1,36 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'meta_data_dto.g.dart'; + +@JsonSerializable() +class Metadata { + @JsonKey(name: "currentPage") + final int? currentPage; + @JsonKey(name: "totalPages") + final int? totalPages; + @JsonKey(name: "totalItems") + final int? totalItems; + @JsonKey(name: "limit") + final int? limit; + @JsonKey(name: "cancelledCount") + final int? cancelledCount; + @JsonKey(name: "completedCount") + final int? completedCount; + + Metadata({ + this.currentPage, + this.totalPages, + required this.totalItems, + required this.limit, + this.cancelledCount = 0, + this.completedCount = 0, + }); + + factory Metadata.fromJson(Map json) { + return _$MetadataFromJson(json); + } + + Map toJson() { + return _$MetadataToJson(this); + } +} diff --git a/lib/features/my_orders/data/models/order_item_model.dart b/lib/features/my_orders/data/models/order_item_model.dart new file mode 100644 index 0000000..b53bf5e --- /dev/null +++ b/lib/features/my_orders/data/models/order_item_model.dart @@ -0,0 +1,26 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'product_model.dart'; + +part 'order_item_model.g.dart'; + +@JsonSerializable() +class OrderItem { + @JsonKey(name: "_id") + final String? id; + + @JsonKey(name: "product") + final Product? product; + + @JsonKey(name: "price") + final int? price; + + @JsonKey(name: "quantity") + final int? quantity; + + OrderItem({this.id, this.product, this.price, this.quantity}); + + factory OrderItem.fromJson(Map json) => + _$OrderItemFromJson(json); + + Map toJson() => _$OrderItemToJson(this); +} diff --git a/lib/features/my_orders/data/models/order_model.dart b/lib/features/my_orders/data/models/order_model.dart new file mode 100644 index 0000000..761a46e --- /dev/null +++ b/lib/features/my_orders/data/models/order_model.dart @@ -0,0 +1,72 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'order_item_model.dart'; +import 'user_model.dart'; +import 'store_model.dart'; + +part 'order_model.g.dart'; + +@JsonSerializable() +class Order { + @JsonKey(name: "_id") + final String? id; + + @JsonKey(name: "user") + final User? user; + + @JsonKey(name: "store") + final Store? store; + + @JsonKey(name: "address") + final String? address; + + @JsonKey(name: "orderItems") + final List? orderItems; + + @JsonKey(name: "totalPrice") + final int? totalPrice; + + @JsonKey(name: "paymentType") + final String? paymentType; + + @JsonKey(name: "isPaid") + final bool? isPaid; + + @JsonKey(name: "isDelivered") + final bool? isDelivered; + + @JsonKey(name: "state") + final String? state; + + @JsonKey(name: "createdAt") + final String? createdAt; + + @JsonKey(name: "updatedAt") + final String? updatedAt; + + @JsonKey(name: "orderNumber") + final String? orderNumber; + + @JsonKey(name: "__v") + final int? v; + + Order({ + this.id, + this.user, + this.store, + this.address, + this.orderItems, + this.totalPrice, + this.paymentType, + this.isPaid, + this.isDelivered, + this.state, + this.createdAt, + this.updatedAt, + this.orderNumber, + this.v, + }); + + factory Order.fromJson(Map json) => _$OrderFromJson(json); + + Map toJson() => _$OrderToJson(this); +} diff --git a/lib/features/my_orders/data/models/product_model.dart b/lib/features/my_orders/data/models/product_model.dart new file mode 100644 index 0000000..359f9ac --- /dev/null +++ b/lib/features/my_orders/data/models/product_model.dart @@ -0,0 +1,25 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'product_model.g.dart'; + +@JsonSerializable() +class Product { + @JsonKey(name: "_id") + final String? id; + + @JsonKey(name: "title") + final String? title; + + @JsonKey(name: "image") + final String? image; + + @JsonKey(name: "price") + final int? price; + + Product({this.id, this.title, this.image, this.price}); + + factory Product.fromJson(Map json) => + _$ProductFromJson(json); + + Map toJson() => _$ProductToJson(this); +} diff --git a/lib/features/my_orders/data/models/response/my_order_response.dart b/lib/features/my_orders/data/models/response/my_order_response.dart new file mode 100644 index 0000000..0a298e3 --- /dev/null +++ b/lib/features/my_orders/data/models/response/my_order_response.dart @@ -0,0 +1,24 @@ +import 'package:json_annotation/json_annotation.dart'; +import '../meta_data_dto.dart'; +import '../order_model.dart'; + +part 'my_order_response.g.dart'; + +@JsonSerializable() +class MyOrderResponse { + @JsonKey(name: "message") + final String? message; + + @JsonKey(name: "metadata") + final Metadata? metadata; + + @JsonKey(name: "orders") + final List? orders; + + MyOrderResponse({this.message, this.metadata, this.orders}); + + factory MyOrderResponse.fromJson(Map json) => + _$MyOrderResponseFromJson(json); + + Map toJson() => _$MyOrderResponseToJson(this); +} diff --git a/lib/features/my_orders/data/models/store_model.dart b/lib/features/my_orders/data/models/store_model.dart new file mode 100644 index 0000000..ceff9dd --- /dev/null +++ b/lib/features/my_orders/data/models/store_model.dart @@ -0,0 +1,27 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'store_model.g.dart'; + +@JsonSerializable() +class Store { + @JsonKey(name: "name") + final String? name; + + @JsonKey(name: "image") + final String? image; + + @JsonKey(name: "address") + final String? address; + + @JsonKey(name: "phoneNumber") + final String? phoneNumber; + + @JsonKey(name: "latLong") + final String? latLong; + + Store({this.name, this.image, this.address, this.phoneNumber, this.latLong}); + + factory Store.fromJson(Map json) => _$StoreFromJson(json); + + Map toJson() => _$StoreToJson(this); +} diff --git a/lib/features/my_orders/data/models/user_model.dart b/lib/features/my_orders/data/models/user_model.dart new file mode 100644 index 0000000..c302aac --- /dev/null +++ b/lib/features/my_orders/data/models/user_model.dart @@ -0,0 +1,45 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'user_model.g.dart'; + +@JsonSerializable() +class User { + @JsonKey(name: "_id") + final String? id; + + @JsonKey(name: "firstName") + final String? firstName; + + @JsonKey(name: "lastName") + final String? lastName; + + @JsonKey(name: "email") + final String? email; + + @JsonKey(name: "gender") + final String? gender; + + @JsonKey(name: "phone") + final String? phone; + + @JsonKey(name: "photo") + final String? photo; + + @JsonKey(name: "passwordChangedAt") + final String? passwordChangedAt; + + User({ + this.id, + this.firstName, + this.lastName, + this.email, + this.gender, + this.phone, + this.photo, + this.passwordChangedAt, + }); + + factory User.fromJson(Map json) => _$UserFromJson(json); + + Map toJson() => _$UserToJson(this); +} diff --git a/lib/features/my_orders/data/repo/my_orders_repo_imp.dart b/lib/features/my_orders/data/repo/my_orders_repo_imp.dart new file mode 100644 index 0000000..f7f3ed4 --- /dev/null +++ b/lib/features/my_orders/data/repo/my_orders_repo_imp.dart @@ -0,0 +1,178 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/my_orders/data/datasource/my_orders_remote_data_source.dart'; +import 'package:tracking_app/features/my_orders/data/mappers/metadata_mapper.dart'; +import 'package:tracking_app/features/my_orders/data/mappers/order_mapper.dart'; +import 'package:tracking_app/features/my_orders/data/models/response/my_order_response.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_item_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/product_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/store_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/repo/my_orders_repo.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/meta_data_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/user_entity.dart'; + +@Injectable(as: MyOrdersRepo) +class MyOrdersRepoImpl implements MyOrdersRepo { + final MyOrdersRemoteDataSource remoteDataSource; + + MyOrdersRepoImpl(this.remoteDataSource); + + @override + Future> getAllOrders({ + required String token, + int limit = 10, + int page = 1, + }) async { + try { + final result = await remoteDataSource.getAllOrders( + token: token, + limit: limit, + page: page, + ); + + if (result is SuccessApiResult) { + final response = result.data; + List orders = + response.orders?.map((e) => e.toEntity()).toList() ?? []; + MetadataEntity? metadata = response.metadata?.toEntity(); + + if (orders.isEmpty) { + orders = _getDummyOrders(); + metadata = const MetadataEntity( + currentPage: 1, + totalPages: 1, + totalItems: 4, + limit: 10, + cancelledCount: 1, + completedCount: 3, + ); + } + + return SuccessApiResult( + data: MyOrdersResult(orders: orders, metadata: metadata), + ); + } else if (result is ErrorApiResult) { + return ErrorApiResult(error: result.error); + } else { + return ErrorApiResult(error: 'Unknown error'); + } + } catch (e) { + return ErrorApiResult(error: e.toString()); + } + } + + List _getDummyOrders() { + final dummyItems = [ + OrderItemEntity( + product: ProductEntity( + id: "p1", + title: "Red roses, 15 Pink Rose Bouquet", + image: + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT6-k6E9vG_c9B_I0m_K-7J1f8e6C9F5G1g5A&s", + price: 600, + ), + price: 600, + quantity: 1, + ), + OrderItemEntity( + product: ProductEntity( + id: "p2", + title: "Red roses, 15 Pink Rose Bouquet", + image: + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT6-k6E9vG_c9B_I0m_K-7J1f8e6C9F5G1g5A&s", + price: 600, + ), + price: 600, + quantity: 4, + ), + ]; + + return [ + OrderEntity( + id: "123456", + user: UserEntity( + id: "u1", + firstName: "Noor", + lastName: "mohamed", + phone: "01012345678", + photo: "https://i.pravatar.cc/150?u=u1", + ), + store: StoreEntity( + name: "Flowery store", + image: + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT6-k6E9vG_c9B_I0m_K-7J1f8e6C9F5G1g5A&s", + address: "20th st, Sheikh Zayed, Giza", + phoneNumber: "01012345678", + ), + address: "20th st, Sheikh Zayed, Giza", + items: dummyItems, + totalPrice: 3000, + paymentType: "Cash on delivery", + isPaid: true, + isDelivered: true, + state: "Completed", + createdAt: DateTime.now() + .subtract(const Duration(hours: 2)) + .toIso8601String(), + orderNumber: "123456", + ), + OrderEntity( + id: "123457", + user: UserEntity( + id: "u1", + firstName: "Nooor", + lastName: "mohamed", + phone: "01012345678", + photo: "https://i.pravatar.cc/150?u=u1", + ), + store: StoreEntity( + name: "Flowery store", + image: + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT6-k6E9vG_c9B_I0m_K-7J1f8e6C9F5G1g5A&s", + address: "20th st, Sheikh Zayed, Giza", + phoneNumber: "01012345678", + ), + address: "20th st, Sheikh Zayed, Giza", + items: dummyItems, + totalPrice: 3000, + paymentType: "Cash on delivery", + isPaid: false, + isDelivered: false, + state: "Cancelled", + createdAt: DateTime.now() + .subtract(const Duration(hours: 4)) + .toIso8601String(), + orderNumber: "123456", + ), + OrderEntity( + id: "123458", + user: UserEntity( + id: "u1", + firstName: "Noor", + lastName: "mohamed", + phone: "01012345678", + photo: "https://i.pravatar.cc/150?u=u1", + ), + store: StoreEntity( + name: "Flowery store", + image: + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT6-k6E9vG_c9B_I0m_K-7J1f8e6C9F5G1g5A&s", + address: "20th st, Sheikh Zayed, Giza", + phoneNumber: "01012345678", + ), + address: "20th st, Sheikh Zayed, Giza", + items: dummyItems, + totalPrice: 3000, + paymentType: "Cash on delivery", + isPaid: false, + isDelivered: false, + state: "Pending", + createdAt: DateTime.now() + .subtract(const Duration(hours: 6)) + .toIso8601String(), + orderNumber: "123456", + ), + ]; + } +} diff --git a/lib/features/my_orders/domain/models/meta_data_entity.dart b/lib/features/my_orders/domain/models/meta_data_entity.dart new file mode 100644 index 0000000..b22d3e1 --- /dev/null +++ b/lib/features/my_orders/domain/models/meta_data_entity.dart @@ -0,0 +1,17 @@ +class MetadataEntity { + final int currentPage; + final int totalPages; + final int totalItems; + final int limit; + final int cancelledCount; + final int completedCount; + + const MetadataEntity({ + required this.currentPage, + required this.totalPages, + required this.totalItems, + required this.limit, + this.cancelledCount = 0, + this.completedCount = 0, + }); +} diff --git a/lib/features/my_orders/domain/models/order_entity.dart b/lib/features/my_orders/domain/models/order_entity.dart new file mode 100644 index 0000000..36acd73 --- /dev/null +++ b/lib/features/my_orders/domain/models/order_entity.dart @@ -0,0 +1,33 @@ +import 'package:tracking_app/features/my_orders/domain/models/order_item_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/user_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/store_entity.dart'; + +class OrderEntity { + final String id; + final UserEntity user; + final StoreEntity? store; + final String address; + final List items; + final int totalPrice; + final String paymentType; + final bool isPaid; + final bool isDelivered; + final String state; + final String createdAt; + final String orderNumber; + + OrderEntity({ + required this.id, + required this.user, + this.store, + this.address = '', + required this.items, + required this.totalPrice, + required this.paymentType, + required this.isPaid, + required this.isDelivered, + required this.state, + required this.createdAt, + required this.orderNumber, + }); +} diff --git a/lib/features/my_orders/domain/models/order_item_entity.dart b/lib/features/my_orders/domain/models/order_item_entity.dart new file mode 100644 index 0000000..b9f2977 --- /dev/null +++ b/lib/features/my_orders/domain/models/order_item_entity.dart @@ -0,0 +1,13 @@ +import 'package:tracking_app/features/my_orders/domain/models/product_entity.dart'; + +class OrderItemEntity { + final ProductEntity product; + final int price; + final int quantity; + + OrderItemEntity({ + required this.product, + required this.price, + required this.quantity, + }); +} diff --git a/lib/features/my_orders/domain/models/product_entity.dart b/lib/features/my_orders/domain/models/product_entity.dart new file mode 100644 index 0000000..64bbd78 --- /dev/null +++ b/lib/features/my_orders/domain/models/product_entity.dart @@ -0,0 +1,13 @@ +class ProductEntity { + final String id; + final String title; + final String image; + final int price; + + ProductEntity({ + required this.id, + required this.title, + required this.image, + required this.price, + }); +} diff --git a/lib/features/my_orders/domain/models/store_entity.dart b/lib/features/my_orders/domain/models/store_entity.dart new file mode 100644 index 0000000..62a61d8 --- /dev/null +++ b/lib/features/my_orders/domain/models/store_entity.dart @@ -0,0 +1,13 @@ +class StoreEntity { + final String name; + final String image; + final String address; + final String phoneNumber; + + StoreEntity({ + required this.name, + required this.image, + required this.address, + required this.phoneNumber, + }); +} diff --git a/lib/features/my_orders/domain/models/user_entity.dart b/lib/features/my_orders/domain/models/user_entity.dart new file mode 100644 index 0000000..9dbd361 --- /dev/null +++ b/lib/features/my_orders/domain/models/user_entity.dart @@ -0,0 +1,15 @@ +class UserEntity { + final String id; + final String firstName; + final String lastName; + final String phone; + final String photo; + + UserEntity({ + required this.id, + required this.firstName, + required this.lastName, + required this.phone, + required this.photo, + }); +} diff --git a/lib/features/my_orders/domain/repo/my_orders_repo.dart b/lib/features/my_orders/domain/repo/my_orders_repo.dart new file mode 100644 index 0000000..b129443 --- /dev/null +++ b/lib/features/my_orders/domain/repo/my_orders_repo.dart @@ -0,0 +1,18 @@ +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/my_orders/domain/models/meta_data_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; + +class MyOrdersResult { + final List orders; + final MetadataEntity? metadata; + + MyOrdersResult({required this.orders, this.metadata}); +} + +abstract class MyOrdersRepo { + Future> getAllOrders({ + required String token, + int limit, + int page, + }); +} diff --git a/lib/features/my_orders/domain/usecases/get_order_use_case.dart b/lib/features/my_orders/domain/usecases/get_order_use_case.dart new file mode 100644 index 0000000..6137a31 --- /dev/null +++ b/lib/features/my_orders/domain/usecases/get_order_use_case.dart @@ -0,0 +1,18 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/my_orders/domain/repo/my_orders_repo.dart'; + +@injectable +class GetOrderUseCase { + final MyOrdersRepo repo; + + GetOrderUseCase(this.repo); + + Future> call({ + required String token, + int page = 1, + int limit = 10, + }) { + return repo.getAllOrders(token: token, page: page, limit: limit); + } +} diff --git a/lib/features/my_orders/presentation/manager/my_orders_cubit.dart b/lib/features/my_orders/presentation/manager/my_orders_cubit.dart new file mode 100644 index 0000000..2709eba --- /dev/null +++ b/lib/features/my_orders/presentation/manager/my_orders_cubit.dart @@ -0,0 +1,134 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/my_orders/domain/usecases/get_order_use_case.dart'; + +import 'my_orders_intent.dart'; +import 'my_orders_state.dart'; + +@injectable +class MyOrdersCubit extends Cubit { + final GetOrderUseCase _getOrdersUseCase; + final AuthStorage _authStorage; + + int _page = 1; + bool _hasMore = true; + + MyOrdersCubit(this._getOrdersUseCase, this._authStorage) + : super(MyOrdersState()); + + void doIntent(MyOrdersIntent intent) { + switch (intent.runtimeType) { + case GetMyOrdersIntent: + _getOrders(intent as GetMyOrdersIntent); + break; + + case LoadMoreOrdersIntent: + _loadMore(); + break; + + case OpenOrderDetailsIntent: + emit( + state.copyWith( + selectedOrder: (intent as OpenOrderDetailsIntent).order, + ), + ); + break; + + case FilterCompletedOrdersIntent: + _filterCompleted(); + break; + + case FilterCancelledOrdersIntent: + _filterCancelled(); + break; + } + } + + Future _getOrders(GetMyOrdersIntent intent) async { + emit(state.copyWith(ordersResource: Resource.loading())); + + final token = await _authStorage.getToken(); + if (token == null || token.isEmpty) { + emit(state.copyWith(ordersResource: Resource.error("Token not found"))); + return; + } + _hasMore = true; + + final result = await _getOrdersUseCase.call( + token: 'Bearer $token', + page: intent.page, + limit: intent.limit, + ); + + if (isClosed) return; + switch (result) { + case SuccessApiResult(): + final data = result.data; + _hasMore = data.metadata != null && _page < data.metadata!.totalPages; + + emit( + state.copyWith( + orders: data.orders, + metadata: data.metadata, + ordersResource: Resource.success(data), + ), + ); + break; + + case ErrorApiResult(): + emit(state.copyWith(ordersResource: Resource.error(result.error))); + break; + } + } + + Future _loadMore() async { + if (!_hasMore || state.isLoadingMore) return; + + emit(state.copyWith(isLoadingMore: true)); + + final token = await _authStorage.getToken(); + if (token == null || token.isEmpty) { + emit(state.copyWith(isLoadingMore: false)); + return; + } + + _page++; + + final result = await _getOrdersUseCase.call( + token: 'Bearer $token', + page: _page, + ); + + if (isClosed) return; + + switch (result) { + case SuccessApiResult(): + emit( + state.copyWith( + orders: [...state.orders, ...result.data.orders], + metadata: result.data.metadata, + isLoadingMore: false, + ), + ); + break; + + case ErrorApiResult(): + emit(state.copyWith(isLoadingMore: false)); + break; + } + } + + void _filterCompleted() { + final filtered = state.orders.where((e) => e.isDelivered == true).toList(); + + emit(state.copyWith(orders: filtered)); + } + + void _filterCancelled() { + final filtered = state.orders.where((e) => e.state == 'cancelled').toList(); + emit(state.copyWith(orders: filtered)); + } +} diff --git a/lib/features/my_orders/presentation/manager/my_orders_intent.dart b/lib/features/my_orders/presentation/manager/my_orders_intent.dart new file mode 100644 index 0000000..ddcd989 --- /dev/null +++ b/lib/features/my_orders/presentation/manager/my_orders_intent.dart @@ -0,0 +1,22 @@ +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; + +sealed class MyOrdersIntent {} + +class GetMyOrdersIntent extends MyOrdersIntent { + final int page; + final int limit; + + GetMyOrdersIntent({this.page = 1, this.limit = 10}); +} + +class LoadMoreOrdersIntent extends MyOrdersIntent {} + +class OpenOrderDetailsIntent extends MyOrdersIntent { + final OrderEntity order; + + OpenOrderDetailsIntent(this.order); +} + +class FilterCompletedOrdersIntent extends MyOrdersIntent {} + +class FilterCancelledOrdersIntent extends MyOrdersIntent {} diff --git a/lib/features/my_orders/presentation/manager/my_orders_state.dart b/lib/features/my_orders/presentation/manager/my_orders_state.dart new file mode 100644 index 0000000..9401a4d --- /dev/null +++ b/lib/features/my_orders/presentation/manager/my_orders_state.dart @@ -0,0 +1,35 @@ +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/features/my_orders/domain/models/meta_data_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; + +class MyOrdersState { + final Resource ordersResource; + final List orders; + final MetadataEntity? metadata; + final OrderEntity? selectedOrder; + final bool isLoadingMore; + + MyOrdersState({ + Resource? ordersResource, + this.orders = const [], + this.metadata, + this.selectedOrder, + this.isLoadingMore = false, + }) : ordersResource = ordersResource ?? Resource.initial(); + + MyOrdersState copyWith({ + Resource? ordersResource, + List? orders, + MetadataEntity? metadata, + OrderEntity? selectedOrder, + bool? isLoadingMore, + }) { + return MyOrdersState( + ordersResource: ordersResource ?? this.ordersResource, + orders: orders ?? this.orders, + metadata: metadata ?? this.metadata, + selectedOrder: selectedOrder ?? this.selectedOrder, + isLoadingMore: isLoadingMore ?? this.isLoadingMore, + ); + } +} diff --git a/lib/features/my_orders/presentation/pages/my_orders_page.dart b/lib/features/my_orders/presentation/pages/my_orders_page.dart new file mode 100644 index 0000000..cf578e8 --- /dev/null +++ b/lib/features/my_orders/presentation/pages/my_orders_page.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_cubit.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_intent.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/my_orders_page_body.dart'; + +class MyOrdersPage extends StatelessWidget { + const MyOrdersPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + getIt() + ..doIntent(GetMyOrdersIntent(page: 1, limit: 10)), + child: Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + title: const Text( + "My orders", + style: TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + centerTitle: false, + ), + backgroundColor: Colors.white, + body: const MyOrdersPageBody(), + ), + ); + } +} diff --git a/lib/features/my_orders/presentation/pages/order_details_page.dart b/lib/features/my_orders/presentation/pages/order_details_page.dart new file mode 100644 index 0000000..f9ea715 --- /dev/null +++ b/lib/features/my_orders/presentation/pages/order_details_page.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/address_title.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/order_item_tile.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/section_lable.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/summary_row.dart'; + +class OrderDetailsPage extends StatelessWidget { + final OrderEntity order; + + const OrderDetailsPage({super.key, required this.order}); + + @override + Widget build(BuildContext context) { + final isCancelled = order.state.toLowerCase() == 'cancelled'; + + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.black, size: 20), + onPressed: () => context.pop(), + ), + title: const Text( + "Order details", + style: TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + centerTitle: false, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + isCancelled ? Icons.cancel : Icons.check_circle, + size: 20, + color: isCancelled ? AppColors.red : AppColors.green, + ), + const SizedBox(width: 8), + Text( + order.state, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: isCancelled ? AppColors.red : AppColors.green, + ), + ), + ], + ), + Text( + "# ${order.orderNumber}", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.blackColor, + ), + ), + ], + ), + const SizedBox(height: 24), + const SectionLabel(label: "Pickup address"), + const SizedBox(height: 8), + AddressTile( + title: order.store?.name ?? "Unknown Store", + address: order.store?.address ?? "No Address Provided", + image: order.store?.image ?? "https://i.pravatar.cc/150?u=s1", + isStore: true, + ), + const SizedBox(height: 20), + const SectionLabel(label: "User address"), + const SizedBox(height: 8), + AddressTile( + title: "${order.user.firstName} ${order.user.lastName}", + address: order.address.isNotEmpty + ? order.address + : "No Address Provided", + image: order.user.photo, + isStore: false, + ), + const SizedBox(height: 24), + const SectionLabel(label: "Order details"), + const SizedBox(height: 12), + ...order.items.map((item) => OrderItemTile(item: item)), + const SizedBox(height: 12), + SummaryRow(label: "Total", value: "Egp ${order.totalPrice}"), + const SizedBox(height: 12), + SummaryRow(label: "Payment method", value: order.paymentType), + ], + ), + ), + ); + } +} diff --git a/lib/features/my_orders/presentation/widgets/address_title.dart b/lib/features/my_orders/presentation/widgets/address_title.dart new file mode 100644 index 0000000..fc249bc --- /dev/null +++ b/lib/features/my_orders/presentation/widgets/address_title.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; + +class AddressTile extends StatelessWidget { + final String title; + final String address; + final String image; + final bool isStore; + + const AddressTile({ + super.key, + required this.title, + required this.address, + required this.image, + required this.isStore, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade100), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + image: NetworkImage(image), + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.blackColor, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + const Icon( + Icons.location_on_outlined, + size: 14, + color: AppColors.grey2, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + address, + style: const TextStyle( + fontSize: 12, + color: AppColors.grey2, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/my_orders/presentation/widgets/my_orders_page_body.dart b/lib/features/my_orders/presentation/widgets/my_orders_page_body.dart new file mode 100644 index 0000000..f672487 --- /dev/null +++ b/lib/features/my_orders/presentation/widgets/my_orders_page_body.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tracking_app/app/core/widgets/show_snak_bar.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_cubit.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_state.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/orders_filters_row.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/orders_list_view.dart'; + +class MyOrdersPageBody extends StatelessWidget { + const MyOrdersPageBody({super.key}); + + @override + Widget build(BuildContext context) { + return BlocListener( + listenWhen: (prev, curr) => prev.ordersResource != curr.ordersResource, + listener: (context, state) { + if (state.ordersResource.isError == true) { + showAppSnackbar( + context, + state.ordersResource.error ?? "Failed to load orders", + ); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + const OrdersFiltersRow(), + const SizedBox(height: 20), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text( + "Recent orders", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.blackColor, + ), + ), + ), + const SizedBox(height: 12), + const Expanded(child: OrdersListView()), + ], + ), + ); + } +} diff --git a/lib/features/my_orders/presentation/widgets/order_card.dart b/lib/features/my_orders/presentation/widgets/order_card.dart new file mode 100644 index 0000000..3c833bb --- /dev/null +++ b/lib/features/my_orders/presentation/widgets/order_card.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/address_title.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/section_lable.dart'; + +class OrderCard extends StatelessWidget { + final OrderEntity order; + final VoidCallback onTap; + + const OrderCard({super.key, required this.order, required this.onTap}); + + @override + Widget build(BuildContext context) { + final isCancelled = order.state.toLowerCase() == 'cancelled'; + + return GestureDetector( + onTap: onTap, + child: Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF9F9F9), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Flower order", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.grey, + ), + ), + Row( + children: [ + Icon( + isCancelled ? Icons.cancel : Icons.check_circle, + size: 18, + color: isCancelled ? AppColors.red : AppColors.green, + ), + const SizedBox(width: 4), + Text( + order.state, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isCancelled ? AppColors.red : AppColors.green, + ), + ), + ], + ), + Text( + "# ${order.orderNumber}", + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: AppColors.blackColor, + ), + ), + ], + ), + const SizedBox(height: 12), + SectionLabel(label: "Pickup address"), + const SizedBox(height: 8), + AddressTile( + title: order.store?.name ?? "Unknown Store", + address: order.store?.address ?? "No Address Provided", + image: + order.store?.image ?? + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT6-k6E9vG_c9B_I0m_K-7J1f8e6C9F5G1g5A&s", + isStore: true, + ), + const SizedBox(height: 12), + SectionLabel(label: "User address"), + const SizedBox(height: 8), + AddressTile( + title: "${order.user.firstName} ${order.user.lastName}", + address: order.address.isNotEmpty + ? order.address + : "No Address Provided", + image: order.user.photo, + isStore: false, + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/my_orders/presentation/widgets/order_item_tile.dart b/lib/features/my_orders/presentation/widgets/order_item_tile.dart new file mode 100644 index 0000000..8448837 --- /dev/null +++ b/lib/features/my_orders/presentation/widgets/order_item_tile.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_item_entity.dart'; + +class OrderItemTile extends StatelessWidget { + final OrderItemEntity item; + + const OrderItemTile({super.key, required this.item}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade100), + ), + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + image: NetworkImage(item.product.image), + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.product.title, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.blackColor, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + "EGP ${item.price}", + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: AppColors.blackColor, + ), + ), + ], + ), + ), + Text( + "X${item.quantity}", + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: AppColors.red, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/my_orders/presentation/widgets/orders_filters_row.dart b/lib/features/my_orders/presentation/widgets/orders_filters_row.dart new file mode 100644 index 0000000..7b6a160 --- /dev/null +++ b/lib/features/my_orders/presentation/widgets/orders_filters_row.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_cubit.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_intent.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_state.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/summary_card.dart'; + +class OrdersFiltersRow extends StatelessWidget { + const OrdersFiltersRow({super.key}); + + @override + Widget build(BuildContext context) { + final cubit = context.read(); + + return BlocBuilder( + builder: (context, state) { + final metadata = state.metadata; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: SummaryCard( + title: "Cancelled", + count: "${metadata?.cancelledCount ?? 0}", + color: AppColors.red, + icon: Icons.cancel_outlined, + onTap: () => cubit.doIntent(FilterCancelledOrdersIntent()), + ), + ), + const SizedBox(width: 16), + Expanded( + child: SummaryCard( + title: "Completed", + count: "${metadata?.completedCount ?? 0}", + color: AppColors.green, + icon: Icons.check_circle_outline, + onTap: () => cubit.doIntent(FilterCompletedOrdersIntent()), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/features/my_orders/presentation/widgets/orders_list_view.dart b/lib/features/my_orders/presentation/widgets/orders_list_view.dart new file mode 100644 index 0000000..e9e034d --- /dev/null +++ b/lib/features/my_orders/presentation/widgets/orders_list_view.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tracking_app/app/core/router/route_names.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_cubit.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_intent.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_state.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/order_card.dart'; + +class OrdersListView extends StatelessWidget { + const OrdersListView({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.ordersResource.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (state.orders.isEmpty) { + return const Center(child: Text("No orders found")); + } + + return ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: state.orders.length + (state.isLoadingMore ? 1 : 0), + itemBuilder: (context, index) { + if (index == state.orders.length) { + return const Padding( + padding: EdgeInsets.all(12), + child: Center(child: CircularProgressIndicator()), + ); + } + + final order = state.orders[index]; + + return OrderCard( + order: order, + onTap: () { + context.read().doIntent( + OpenOrderDetailsIntent(order), + ); + context.push(RouteNames.orderDetails, extra: order); + }, + ); + }, + ); + }, + ); + } +} diff --git a/lib/features/my_orders/presentation/widgets/section_lable.dart b/lib/features/my_orders/presentation/widgets/section_lable.dart new file mode 100644 index 0000000..6805822 --- /dev/null +++ b/lib/features/my_orders/presentation/widgets/section_lable.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; + +class SectionLabel extends StatelessWidget { + final String label; + + const SectionLabel({super.key, required this.label}); + + @override + Widget build(BuildContext context) { + return Text( + label, + style: const TextStyle( + fontSize: 12, + color: AppColors.grey2, + fontWeight: FontWeight.w500, + ), + ); + } +} diff --git a/lib/features/my_orders/presentation/widgets/summary_card.dart b/lib/features/my_orders/presentation/widgets/summary_card.dart new file mode 100644 index 0000000..127d7fa --- /dev/null +++ b/lib/features/my_orders/presentation/widgets/summary_card.dart @@ -0,0 +1,60 @@ +import 'package:flutter/widgets.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; + +class SummaryCard extends StatelessWidget { + final String title; + final String count; + final Color color; + final IconData icon; + final VoidCallback onTap; + + const SummaryCard({ + required this.title, + required this.count, + required this.color, + required this.icon, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: const Color(0xFFFDF0F3), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + count, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColors.blackColor, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Icon(icon, size: 16, color: color), + const SizedBox(width: 6), + Text( + title, + style: TextStyle( + fontSize: 13, + color: color, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/my_orders/presentation/widgets/summary_row.dart b/lib/features/my_orders/presentation/widgets/summary_row.dart new file mode 100644 index 0000000..9c0d692 --- /dev/null +++ b/lib/features/my_orders/presentation/widgets/summary_row.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/ui_helper/color/colors.dart'; + +class SummaryRow extends StatelessWidget { + final String label; + final String value; + + const SummaryRow({super.key, required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF9F9F9), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade100), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: AppColors.blackColor, + ), + ), + Text( + value, + style: const TextStyle( + fontSize: 14, + color: AppColors.grey2, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/profile/data/models/requests/edit_profile_request.g.dart b/lib/features/profile/data/models/requests/edit_profile_request.g.dart index b16fe9a..b30edf7 100644 --- a/lib/features/profile/data/models/requests/edit_profile_request.g.dart +++ b/lib/features/profile/data/models/requests/edit_profile_request.g.dart @@ -17,21 +17,13 @@ EditProfileRequest _$EditProfileRequestFromJson(Map json) => vehicleLicense: json['vehicleLicense'] as String?, ); -Map _$EditProfileRequestToJson(EditProfileRequest instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('firstName', instance.firstName); - writeNotNull('lastName', instance.lastName); - writeNotNull('email', instance.email); - writeNotNull('phone', instance.phone); - writeNotNull('vehicleType', instance.vehicleType); - writeNotNull('vehicleNumber', instance.vehicleNumber); - writeNotNull('vehicleLicense', instance.vehicleLicense); - return val; -} +Map _$EditProfileRequestToJson(EditProfileRequest instance) => + { + 'firstName': ?instance.firstName, + 'lastName': ?instance.lastName, + 'email': ?instance.email, + 'phone': ?instance.phone, + 'vehicleType': ?instance.vehicleType, + 'vehicleNumber': ?instance.vehicleNumber, + 'vehicleLicense': ?instance.vehicleLicense, + }; diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 47bfd51..8716e79 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -186,12 +186,12 @@ abstract class LocaleKeys { static const failed_to_save_address = 'failed_to_save_address'; static const addNewAddress = 'addNewAddress'; static const savedAddress = 'savedAddress'; - static const discount = 'discount'; static const sortBy = 'sortBy'; static const lowestPrice = 'lowestPrice'; static const highestPrice = 'highestPrice'; static const newest = 'newest'; static const oldest = 'oldest'; + static const discount = 'discount'; static const filter = 'filter'; static const active = 'active'; static const completed = 'completed'; @@ -246,4 +246,27 @@ abstract class LocaleKeys { static const editDriverProfile = 'editDriverProfile'; static const editVehicle = 'editVehicle'; static const cannotBeSame = 'cannotBeSame'; + static const orderDetails = 'orderDetails'; + static const status = 'status'; + static const orderId = 'orderId'; + static const pickupAddress = 'pickupAddress'; + static const floweryStore = 'floweryStore'; + static const userAddress = 'userAddress'; + static const arrivedAtPickupPoint = 'arrivedAtPickupPoint'; + static const startDelivery = 'startDelivery'; + static const arriverAtDestination = 'arriverAtDestination'; + static const confirmDelivery = 'confirmDelivery'; + static const deliveryConfirmed = 'deliveryConfirmed'; + static const orderCompleted = 'orderCompleted'; + static const accepted = 'accepted'; + static const pickedUp = 'pickedUp'; + static const outForDelivery = 'outForDelivery'; + static const arrived = 'arrived'; + static const driverOrderTitle = 'driverOrderTitle'; + static const unknownStore = 'unknownStore'; + static const noAddress = 'noAddress'; + static const accept = 'accept'; + static const reject = 'reject'; + static const noPendingOrders = 'noPendingOrders'; + static const floweryRider = 'floweryRider'; } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cac8596..cd23da7 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,22 +5,30 @@ import FlutterMacOS import Foundation +import cloud_firestore import file_selector_macos import firebase_core import firebase_crashlytics import firebase_messaging import flutter_local_notifications import geolocator_apple +import package_info_plus +import path_provider_foundation import shared_preferences_foundation +import sqflite_darwin 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")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 8f59585..4a204ad 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "85.0.0" _flutterfire_internals: dependency: transitive description: @@ -21,18 +21,26 @@ packages: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d" url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "7.7.1" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" archive: dependency: transitive description: name: archive - sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff url: "https://pub.dev" source: hosted - version: "4.0.7" + version: "4.0.9" args: dependency: transitive description: @@ -77,18 +85,18 @@ packages: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: ce76b1d48875e3233fde17717c23d1f60a91cc631597e49a400c89b475395b1d url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "3.1.0" build_config: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.0" build_daemon: dependency: transitive description: @@ -101,26 +109,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: d1d57f7807debd7349b4726a19fd32ec8bc177c71ad0febf91a20f84cd2d4b46 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "3.0.3" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: b24597fceb695969d47025c958f3837f9f0122e237c6a22cb082a5ac66c3ca30 url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.7.1" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + sha256: "066dda7f73d8eb48ba630a55acb50c4a84a2e6b453b1cb4567f581729e794f7b" url: "https://pub.dev" source: hosted - version: "7.3.2" + version: "9.3.1" built_collection: dependency: transitive description: @@ -137,6 +145,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.12.3" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" characters: dependency: transitive description: @@ -169,6 +201,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + cloud_firestore: + dependency: "direct main" + 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: @@ -237,10 +293,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "3.1.1" dbus: dependency: transitive description: @@ -309,10 +365,10 @@ packages: dependency: transitive description: name: ffi - sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" file: dependency: transitive description: @@ -438,6 +494,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.1.1" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_lints: dependency: "direct dev" description: @@ -450,10 +514,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 +538,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 @@ -525,22 +589,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + geoclue: + dependency: transitive + description: + name: geoclue + sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f + url: "https://pub.dev" + source: hosted + version: "0.1.1" geolocator: dependency: "direct main" description: name: geolocator - sha256: f4efb8d3c4cdcad2e226af9661eb1a0dd38c71a9494b22526f9da80ab79520e5 + sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516" url: "https://pub.dev" source: hosted - version: "10.1.1" + version: "14.0.2" geolocator_android: dependency: transitive description: name: geolocator_android - sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d + sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a" url: "https://pub.dev" source: hosted - version: "4.6.2" + version: "5.0.2" geolocator_apple: dependency: transitive description: @@ -549,6 +621,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.13" + geolocator_linux: + dependency: transitive + description: + name: geolocator_linux + sha256: d64112a205931926f4363bb6bd48f14cb38e7326833041d170615586cd143797 + url: "https://pub.dev" + source: hosted + version: "0.2.4" geolocator_platform_interface: dependency: transitive description: @@ -561,10 +641,10 @@ packages: dependency: transitive description: name: geolocator_web - sha256: "102e7da05b48ca6bf0a5bda0010f886b171d1a08059f01bfe02addd0175ebece" + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "4.1.3" geolocator_windows: dependency: transitive description: @@ -577,10 +657,10 @@ packages: dependency: "direct main" description: name: get_it - sha256: "1d648d2dd2047d7f7450d5727ca24ee435f240385753d90b49650e3cdff32e56" + sha256: "568d62f0e68666fb5d95519743b3c24a34c7f19d834b0658c46e26d778461f66" url: "https://pub.dev" source: hosted - version: "9.2.0" + version: "9.2.1" glob: dependency: transitive description: @@ -593,10 +673,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: b465e99ce64ba75e61c8c0ce3d87b66d8ac07f0b35d0a7e0263fcfc10f99e836 + sha256: "7974313e217a7771557add6ff2238acb63f635317c35fa590d348fb238f00896" url: "https://pub.dev" source: hosted - version: "13.2.5" + version: "17.1.0" google_maps: dependency: transitive description: @@ -609,34 +689,34 @@ packages: dependency: "direct main" description: name: google_maps_flutter - sha256: "819985697596a42e1054b5feb2f407ba1ac92262e02844a40168e742b9f36dca" + sha256: "9b0d6dab3de6955837575dc371dd772fcb5d0a90f6a4954e8c066472f9938550" url: "https://pub.dev" source: hosted - version: "2.14.0" + version: "2.14.2" google_maps_flutter_android: dependency: transitive description: name: google_maps_flutter_android - sha256: "98d7f5354f770f3e993db09fc798d40aeb6a254f04c1c468a94818ec2086e83e" + sha256: "8b569c7abc52bc62d4502bf93847d487c0843f3e6a2a8e122b72e98843b2ab4c" url: "https://pub.dev" source: hosted - version: "2.18.12" + version: "2.19.1" google_maps_flutter_ios: dependency: transitive description: name: google_maps_flutter_ios - sha256: "0504508a024410979936bd22bc2dc10a0df5cb1d15a21618d6cfbd973832464f" + sha256: a2e3c7ad2392ea65d6775704716d0aa3c3d226cb984fd0a688bca40f6be1a451 url: "https://pub.dev" source: hosted - version: "2.17.1" + version: "2.17.4" google_maps_flutter_platform_interface: dependency: transitive description: name: google_maps_flutter_platform_interface - sha256: e8b1232419fcdd35c1fdafff96843f5a40238480365599d8ca661dde96d283dd + sha256: "0f8c6674d70c7e9a09cd34f63b18ebaf8a5822e85b558128eae0fdf02b4a3e93" url: "https://pub.dev" source: hosted - version: "2.14.1" + version: "2.14.2" google_maps_flutter_web: dependency: transitive description: @@ -653,6 +733,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + gsettings: + dependency: transitive + description: + name: gsettings + sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c" + url: "https://pub.dev" + source: hosted + version: "0.2.8" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b + url: "https://pub.dev" + source: hosted + version: "4.3.0" html: dependency: transitive description: @@ -697,10 +793,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "518a16108529fc18657a3e6dde4a043dc465d16596d20ab2abd49a4cac2e703d" + sha256: eda9b91b7e266d9041084a42d605a74937d996b87083395c5e47835916a86156 url: "https://pub.dev" source: hosted - version: "0.8.13+13" + version: "0.8.13+14" image_picker_for_web: dependency: transitive description: @@ -753,18 +849,18 @@ packages: dependency: "direct main" description: name: injectable - sha256: "32e9bac6fe9c84339c5add60478d27a01e363ce1ad5c22ca7e525c6b28a7559c" + sha256: "32b36a9d87f18662bee0b1951b81f47a01f2bf28cd6ea94f60bc5453c7bf598c" url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.7.1+4" injectable_generator: dependency: "direct dev" description: name: injectable_generator - sha256: af403d76c7b18b4217335e0075e950cd0579fd7f8d7bd47ee7c85ada31680ba1 + sha256: "09c55dba52b53d17411b90134a6751270b8930abd2529e2637d700fc99b0d5b5" url: "https://pub.dev" source: hosted - version: "2.6.2" + version: "2.8.1" intl: dependency: "direct main" description: @@ -801,10 +897,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3 url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.11.2" leak_tracker: dependency: transitive description: @@ -829,6 +925,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + lean_builder: + dependency: transitive + description: + name: lean_builder + sha256: ef5cd5f907157eb7aa87d1704504b5a6386d2cbff88a3c2b3344477bab323ee9 + url: "https://pub.dev" + source: hosted + version: "0.1.2" lints: dependency: transitive description: @@ -873,10 +977,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -889,10 +993,10 @@ packages: dependency: "direct dev" description: name: mockito - sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + sha256: "2314cbe9165bcd16106513df9cf3c3224713087f09723b128928dc11a4379f99" url: "https://pub.dev" source: hosted - version: "5.4.4" + version: "5.5.0" mocktail: dependency: "direct dev" description: @@ -925,6 +1029,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" package_config: dependency: transitive description: @@ -933,6 +1045,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d + url: "https://pub.dev" + source: hosted + version: "9.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" path: dependency: transitive description: @@ -949,6 +1077,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.dev" + source: hosted + version: "2.5.1" path_provider_linux: dependency: transitive description: @@ -977,10 +1129,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.2" platform: dependency: transitive description: @@ -1009,10 +1161,10 @@ packages: dependency: transitive description: name: posix - sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" url: "https://pub.dev" source: hosted - version: "6.0.3" + version: "6.5.0" pretty_dio_logger: dependency: "direct main" description: @@ -1021,6 +1173,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "2fcc8a202ca7ec17dab7c97d6b6d91cf03aa07fe6f65f8afbb6dfa52cc5bd902" + url: "https://pub.dev" + source: hosted + version: "5.1.0" provider: dependency: "direct main" description: @@ -1065,10 +1225,18 @@ packages: dependency: "direct dev" description: name: retrofit_generator - sha256: "9499eb46b3657a62192ddbc208ff7e6c6b768b19e83c1ee6f6b119c864b99690" + sha256: "7ec323f3329ad2ca0bcdc96fe02ec7f2486ecfac6cd2d035b03c398ef6f42308" url: "https://pub.dev" source: hosted - version: "7.0.8" + version: "10.2.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" sanitize_html: dependency: transitive description: @@ -1089,10 +1257,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f + sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41" url: "https://pub.dev" source: hosted - version: "2.4.20" + version: "2.4.21" shared_preferences_foundation: dependency: transitive description: @@ -1161,10 +1329,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" shimmer: dependency: "direct main" description: @@ -1177,10 +1345,10 @@ packages: dependency: "direct main" description: name: skeletonizer - sha256: "83157d8e2e41f0252079cfec496281c16e4c63660052dab8d4cd72a206bb7109" + sha256: "9f38f9b47ec3cf2235a6a4f154a88a95432bc55ba98b3e2eb6ced5c1974bc122" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" sky_engine: dependency: transitive description: flutter @@ -1190,18 +1358,18 @@ packages: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "3.1.0" source_helper: dependency: transitive description: name: source_helper - sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" url: "https://pub.dev" source: hosted - version: "1.3.5" + version: "1.3.8" source_map_stack_trace: dependency: transitive description: @@ -1226,6 +1394,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.2" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: @@ -1258,6 +1466,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" term_glyph: dependency: transitive description: @@ -1270,26 +1486,26 @@ packages: dependency: transitive description: name: test - sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" url: "https://pub.dev" source: hosted - version: "1.26.2" + version: "1.26.3" test_api: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" test_core: dependency: transitive description: name: test_core - sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" url: "https://pub.dev" source: hosted - version: "0.6.11" + version: "0.6.12" timezone: dependency: transitive description: @@ -1306,14 +1522,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" - tuple: - dependency: transitive - description: - name: tuple - sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 - url: "https://pub.dev" - source: hosted - version: "2.0.2" typed_data: dependency: transitive description: @@ -1390,10 +1598,10 @@ packages: dependency: transitive description: name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.5.3" vector_graphics: dependency: transitive description: @@ -1414,10 +1622,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "201e876b5d52753626af64b6359cd13ac6011b80728731428fd34bc840f71c9b" + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" url: "https://pub.dev" source: hosted - version: "1.1.20" + version: "1.2.0" vector_math: dependency: transitive description: @@ -1474,6 +1682,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" xdg_directories: dependency: transitive description: @@ -1490,6 +1706,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" + xxh3: + dependency: transitive + description: + name: xxh3 + sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916" + url: "https://pub.dev" + source: hosted + version: "1.2.0" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bb7cff1..1266188 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,25 +18,27 @@ dependencies: flutter_otp_text_field: ^1.5.1+1 flutter_svg: ^2.2.3 get_it: ^9.2.0 - go_router: ^13.2.0 - injectable: 2.7.0 + go_router: ^17.1.0 + injectable: ^2.7.1+4 intl: ^0.20.2 json_annotation: ^4.9.0 pretty_dio_logger: ^1.4.0 provider: ^6.1.5+1 - retrofit: ^4.4.1 + retrofit: 4.9.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 + geolocator: ^14.0.2 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 + cloud_firestore: ^6.1.2 + cached_network_image: ^3.3.1 dev_dependencies: bloc_test: ^10.0.0 @@ -45,7 +47,7 @@ dev_dependencies: injectable_generator: ^2.4.1 json_serializable: ^6.8.0 mockito: ^5.4.4 - retrofit_generator: 7.0.8 + retrofit_generator: 10.2.0 network_image_mock: ^2.1.1 mocktail: ^1.0.3 @@ -72,4 +74,4 @@ flutter: # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 + # weight: 700 \ No newline at end of file diff --git a/test/features/app_sections/presentation/widgets/app_section_view_test.dart b/test/features/app_sections/presentation/widgets/app_section_view_test.dart index 0084266..58c58f6 100644 --- a/test/features/app_sections/presentation/widgets/app_section_view_test.dart +++ b/test/features/app_sections/presentation/widgets/app_section_view_test.dart @@ -2,23 +2,30 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/config/di/di.dart'; import 'package:tracking_app/features/app_sections/presentation/manager/app_section_cubit.dart'; import 'package:tracking_app/features/app_sections/presentation/manager/app_section_states.dart'; import 'package:tracking_app/features/app_sections/presentation/pages/home_page_test.dart'; import 'package:tracking_app/features/app_sections/presentation/pages/orders_page_test.dart'; import 'package:tracking_app/features/app_sections/presentation/pages/profile_page_test.dart'; import 'package:tracking_app/features/app_sections/presentation/widgets/app_section_view.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderCubit.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderStates.dart'; +import 'package:tracking_app/features/home/presentation/pages/driverOrderScreen.dart'; import 'package:tracking_app/features/profile/presentation/pages/profile_page.dart'; import 'app_section_view_test.mocks.dart'; -@GenerateMocks([AppSectionCubit]) +@GenerateNiceMocks([MockSpec(), MockSpec()]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); - late MockAppSectionCubit mockCubit; + late MockAppSectionCubit mockAppSectionCubit; + late MockDriverOrderCubit mockDriverOrderCubit; setUpAll(() async { SharedPreferences.setMockInitialValues({}); @@ -26,7 +33,18 @@ void main() { }); setUp(() { - mockCubit = MockAppSectionCubit(); + mockAppSectionCubit = MockAppSectionCubit(); + mockDriverOrderCubit = MockDriverOrderCubit(); + if (getIt.isRegistered()) { + getIt.unregister(); + } + getIt.registerFactory(() => mockDriverOrderCubit); + }); + + tearDown(() { + if (getIt.isRegistered()) { + getIt.unregister(); + } }); Widget buildTestableWidget() { @@ -35,8 +53,11 @@ void main() { path: 'assets/translations', fallbackLocale: const Locale('en'), child: MaterialApp( - home: BlocProvider( - create: (_) => mockCubit, + home: MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => mockAppSectionCubit), + BlocProvider(create: (_) => mockDriverOrderCubit), + ], child: AppSectionsView(), ), ), @@ -44,45 +65,62 @@ void main() { } group('AppSectionsView Widget Test', () { - testWidgets('should show Home page by default', ( + testWidgets('should show DriverOrderScreen by default (index 0)', ( WidgetTester tester, ) async { - when(mockCubit.state).thenReturn(AppSectionStates(selectedIndex: 0)); - when(mockCubit.stream).thenAnswer( + when( + mockAppSectionCubit.state, + ).thenReturn(AppSectionStates(selectedIndex: 0)); + when(mockAppSectionCubit.stream).thenAnswer( (_) => Stream.value(AppSectionStates(selectedIndex: 0)), ); + // Stub DriverOrderCubit + when( + mockDriverOrderCubit.state, + ).thenReturn(DriverOrderState(orderResource: Resource.loading())); + when( + mockDriverOrderCubit.stream, + ).thenAnswer((_) => Stream.empty()); + await tester.pumpWidget(buildTestableWidget()); - await tester.tap(find.byIcon(Icons.home)); - await tester.pump(); + // No tap needed for default - expect(find.byType(HomePageTest), findsOneWidget); - expect(find.byType(OrdersPageTest), findsNothing); - expect(find.byType(ProfilePageTest), findsNothing); + expect(find.byType(DriverOrderScreen), findsOneWidget); }); - testWidgets('should navigate to Orders page when tapping Orders', ( - WidgetTester tester, - ) async { - when(mockCubit.state).thenReturn(AppSectionStates(selectedIndex: 1)); - when(mockCubit.stream).thenAnswer( - (_) => - Stream.value(AppSectionStates(selectedIndex: 1)), - ); + // testWidgets('should navigate to Orders page when tapping Orders', ( + // WidgetTester tester, + // ) async { + // when( + // mockAppSectionCubit.state, + // ).thenReturn(AppSectionStates(selectedIndex: 1)); + // when(mockAppSectionCubit.stream).thenAnswer( + // (_) => + // Stream.value(AppSectionStates(selectedIndex: 1)), + // ); - await tester.pumpWidget(buildTestableWidget()); - await tester.tap(find.byIcon(Icons.fact_check_outlined)); - await tester.pump(); + // // Stub DriverOrderCubit just in case (though not used in index 1 view) + // when( + // mockDriverOrderCubit.state, + // ).thenReturn(DriverOrderState(orderResource: Resource.loading())); + // when( + // mockDriverOrderCubit.stream, + // ).thenAnswer((_) => Stream.empty()); - expect(find.byType(OrdersPageTest), findsOneWidget); - }); + // await tester.pumpWidget(buildTestableWidget()); + // await tester.tap(find.byIcon(Icons.fact_check_outlined)); + // await tester.pump(); + + // expect(find.byType(OrdersPageTest), findsOneWidget); + // }); // testWidgets('should navigate to Profile page when tapping Profile', ( // WidgetTester tester, // ) async { - // when(mockCubit.state).thenReturn(AppSectionStates(selectedIndex: 2)); - // when(mockCubit.stream).thenAnswer( + // when(mockAppSectionCubit.state).thenReturn(AppSectionStates(selectedIndex: 2)); + // when(mockAppSectionCubit.stream).thenAnswer( // (_) => Stream.value(AppSectionStates(selectedIndex: 2)), // ); // await tester.pumpWidget(buildTestableWidget()); diff --git a/test/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl_test.dart b/test/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl_test.dart new file mode 100644 index 0000000..bc715b1 --- /dev/null +++ b/test/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl_test.dart @@ -0,0 +1,74 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/api/datasource/order_details_remote_datasource_impl.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; +import 'order_details_remote_datasource_impl_test.mocks.dart'; + +@GenerateMocks([ + FirebaseFirestore, + CollectionReference, + DocumentReference, + DocumentSnapshot, +]) +void main() { + late OrderDetailsRemoteDatasourceImpl dataSource; + late MockFirebaseFirestore mockFirestore; + late MockCollectionReference> mockCollection; + late MockDocumentReference> mockDocument; + late MockDocumentSnapshot> mockSnapshot; + + const String tOrderId = 'pxkMaEmWYVuvV5jkW0JK'; + + setUp(() { + mockFirestore = MockFirebaseFirestore(); + mockCollection = MockCollectionReference(); + mockDocument = MockDocumentReference(); + mockSnapshot = MockDocumentSnapshot(); + + dataSource = OrderDetailsRemoteDatasourceImpl(firestore: mockFirestore); + }); + group('getOrderStream', () { + final tOrderJson = { + 'driver_id': '1', + 'user_id': 'U11', + 'userAddress': {'name': 'mariam', 'address': 'alex', 'userId': 'U11'}, + 'oder_dt': { + 'items': [], + 'status': 'accepted', + 'totalPrice': 500.0, + 'orderId': tOrderId, + 'userAddress': 'alex', + 'pickupAddress': {'name': 'mariam', 'address': 'alex'}, + }, + }; + + test('should return SuccessApiResult with Stream of OrderDto', () async { + when(mockFirestore.collection('orders')).thenReturn(mockCollection); + when(mockCollection.doc(tOrderId)).thenReturn(mockDocument); + + when(mockSnapshot.exists).thenReturn(true); + when(mockSnapshot.data()).thenReturn(tOrderJson); + when(mockSnapshot.id).thenReturn(tOrderId); + + when( + mockDocument.snapshots(), + ).thenAnswer((_) => Stream.value(mockSnapshot)); + + final result = dataSource.getOrderStream(tOrderId); + + expect(result, isA>>()); + final stream = (result as SuccessApiResult>).data; + await expectLater( + stream, + emits( + isA() + .having((o) => o.orderId, 'orderId', tOrderId) + .having((o) => o.orderDetails.status, 'status', 'accepted'), + ), + ); + }); + }); +} diff --git a/test/features/driver_orders_details/data/mapper/order_dto_mapper_test.dart b/test/features/driver_orders_details/data/mapper/order_dto_mapper_test.dart new file mode 100644 index 0000000..d11f68f --- /dev/null +++ b/test/features/driver_orders_details/data/mapper/order_dto_mapper_test.dart @@ -0,0 +1,115 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/driver_orders_details/data/mapper/order_dto_mapper.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; + +void main() { + group('OrderDtoMapper', () { + test('Convert OrderDto to OrderModel correctly', () { + final tUserAddressDto = UserAddressDto( + address: 'Alex', + name: 'Mariam', + userId: 'U123', + ); + + final tOrderDto = OrderDto( + driverId: 'D123', + userAddress: tUserAddressDto, + userId: 'U789', + orderId: '22', + orderDetails: OrderDetailsDto( + items: [], + status: 'pending', + totalPrice: 500, + pickupAddress: PickedAddressDto( + name: 'Store', + address: '123 Main St', + ), + orderId: '22', + userAddress: 'alex', + ), + ); + + final result = tOrderDto.toOrderModel(); + + expect(result, isA()); + expect(result.driverId, tOrderDto.driverId); + expect(result.userAddress.name, tOrderDto.userAddress.name); + expect(result.userAddress.address, tOrderDto.userAddress.address); + expect(result.userAddress.userId, tOrderDto.userAddress.userId); + expect(result.userId, tOrderDto.userId); + }); + }); + + group('OrderDetailsDtoMapper', () { + test('Convert OrderDetailsDto to OrderDetailsModel correctly', () { + final tpickupAddressDto = PickedAddressDto( + name: 'Store', + address: '123 Main St', + ); + final tDto = OrderDetailsDto( + items: [], + status: 'pending', + totalPrice: 500, + pickupAddress: tpickupAddressDto, + orderId: '1', + userAddress: 'alex', + ); + + final result = tDto.toOrderDetailsModel(); + + expect(result, isA()); + expect(result.items, tDto.items); + expect(result.status, tDto.status); + expect(result.totalPrice, tDto.totalPrice); + expect(result.pickupAddress.name, tDto.pickupAddress.name); + expect(result.orderId, tDto.orderId); + }); + }); + + group('OrderItemDtoMapper', () { + test('Convert OrderItemDto to OrderItemModel correctly', () { + final tDto = OrderItemDto( + productId: '1', + title: 'Item 1', + price: 100, + quantity: 2, + image: 'image_url', + ); + + final result = tDto.toOrderItemModel(); + + expect(result.productId, tDto.productId); + expect(result.title, tDto.title); + expect(result.price, tDto.price); + expect(result.quantity, tDto.quantity); + expect(result.image, tDto.image); + }); + }); + + group('PickedAddressDtoMapper', () { + test('Convert PickedAddressDto to PickedAddressModel correctly', () { + final tDto = PickedAddressDto(name: 'Store', address: '123 Main St'); + + final result = tDto.toPickedAddressModel(); + + expect(result.name, tDto.name); + expect(result.address, tDto.address); + }); + }); + + group('UserAddressDtoMapper', () { + test('Convert UserAddressDto to UserAddressModel correctly', () { + final tDto = UserAddressDto( + name: 'Store', + address: '123 Main St', + userId: 'U123', + ); + + final result = tDto.toUserAddressModel(); + + expect(result.name, tDto.name); + expect(result.address, tDto.address); + }); + }); +} diff --git a/test/features/driver_orders_details/data/models/orders_dto_test.dart b/test/features/driver_orders_details/data/models/orders_dto_test.dart new file mode 100644 index 0000000..6206376 --- /dev/null +++ b/test/features/driver_orders_details/data/models/orders_dto_test.dart @@ -0,0 +1,213 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; + +void main() { + group('UserAddressDto Tests', () { + test('should return a valid UserAddressDto from JSON', () { + final Map json = { + 'adress': 'Alex', + 'name': 'Mariam', + 'user_id': 'U123', + }; + + final result = UserAddressDto.fromJson(json); + + expect(result.address, 'Alex'); + expect(result.name, 'Mariam'); + expect(result.userId, 'U123'); + }); + + test('should return a valid JSON map from UserAddressDto', () { + final dto = UserAddressDto( + address: 'Alex', + name: 'Mariam', + userId: 'U123', + ); + + final result = dto.toJson(); + + expect(result['adress'], 'Alex'); + expect(result['name'], 'Mariam'); + expect(result['user_id'], 'U123'); + }); + }); + + group('PickedAddressDto Tests', () { + test('should return a valid PickedAddressDto from JSON', () { + final Map json = {'address': 'Alex', 'name': 'Mariam'}; + + final result = PickedAddressDto.fromJson(json); + + expect(result.address, 'Alex'); + expect(result.name, 'Mariam'); + }); + + test('should return a valid JSON map from PickedAddressDto', () { + final dto = PickedAddressDto(address: 'Alex', name: 'Mariam'); + + final result = dto.toJson(); + + expect(result['address'], 'Alex'); + expect(result['name'], 'Mariam'); + }); + }); + + group('OrderItemDto Tests', () { + test('should return a valid OrderItemDto from JSON', () { + final Map json = { + 'productId': '1', + 'title': 'red flower', + 'image': 'url', + 'quantity': 1, + 'price': 100, + }; + + final result = OrderItemDto.fromJson(json); + + expect(result.image, 'url'); + expect(result.title, 'red flower'); + expect(result.quantity, 1); + expect(result.price, 100); + }); + + test('should return a valid JSON map from OrderItemDto', () { + final dto = OrderItemDto( + image: 'Alex', + productId: '1', + title: 'red flower', + quantity: 1, + price: 100, + ); + + final result = dto.toJson(); + + expect(result['image'], 'Alex'); + expect(result['title'], 'red flower'); + expect(result['quantity'], 1); + expect(result['price'], 100); + }); + }); + + group('OrderDetailsDto Tests', () { + test('should return a valid OrderDetailsDto from JSON', () { + final Map json = { + 'items': [], + 'status': 'accepted', + 'totalPrice': 100.0, + 'pickupAddress': {'name': 'Mariam', 'address': 'Alex'}, + 'orderId': 'O456', + 'userAddress': 'alex', + }; + + final result = OrderDetailsDto.fromJson(json); + + expect(result.status, 'accepted'); + expect(result.totalPrice, 100.0); + expect(result.orderId, 'O456'); + }); + + test('should return a valid JSON map from OrderDetailsDto', () { + final dto = OrderDetailsDto( + items: [ + OrderItemDto( + image: 'url', + productId: '1', + title: 'red flower', + quantity: 1, + price: 100, + ), + ], + status: 'accepted', + totalPrice: 100.0, + pickupAddress: PickedAddressDto(address: 'Alex', name: 'Mariam'), + orderId: 'O456', + userAddress: 'alex', + ); + + final result = dto.toJson(); + + expect(result['status'], 'accepted'); + expect(result['totalPrice'], 100.0); + final firstItem = result['items'][0]; + expect(firstItem['image'], 'url'); + expect(firstItem['title'], 'red flower'); + expect(firstItem['price'], 100.0); + expect(result['pickupAddress']['name'], 'Mariam'); + }); + }); + + group('OrderDto Tests', () { + final Map tOrderJson = { + 'driver_id': 'D123', + 'user_id': 'U789', + 'userAddress': { + 'name': 'Home', + 'address': 'Cairo, Egypt', + 'userId': 'U789', + }, + 'oder_dt': { + 'status': 'processing', + 'totalPrice': 250.0, + 'orderId': 'O100', + 'userAddress': 'Cairo, Egypt', + 'pickupAddress': {'name': 'Pharmacy', 'address': 'Downtown'}, + 'items': [ + { + 'productId': 'p1', + 'title': 'Panadol', + 'image': 'panadol.png', + 'quantity': 2, + 'price': 125.0, + }, + ], + }, + }; + + const String tOrderId = 'O100'; + + test('should return a valid OrderDto from JSON and ID', () { + final result = OrderDto.fromJson(tOrderJson, tOrderId); + + expect(result.orderId, tOrderId); + expect(result.driverId, 'D123'); + expect(result.userId, 'U789'); + expect(result.userAddress, isA()); + expect(result.userAddress.name, 'Home'); + + expect(result.orderDetails, isA()); + expect(result.orderDetails.status, 'processing'); + expect(result.orderDetails.items.length, 1); + expect(result.orderDetails.items[0].title, 'Panadol'); + }); + + test('should return a valid JSON map from OrderDto', () { + final dto = OrderDto( + orderId: tOrderId, + driverId: 'D123', + userId: 'U789', + userAddress: UserAddressDto( + name: 'Home', + address: 'Cairo', + userId: 'U789', + ), + orderDetails: OrderDetailsDto( + items: [], + status: 'pending', + totalPrice: 0.0, + pickupAddress: PickedAddressDto(name: 'Store', address: 'Street'), + orderId: tOrderId, + userAddress: 'Cairo', + ), + ); + + final result = dto.toJson(); + + expect(result['driver_id'], 'D123'); + expect(result['user_id'], 'U789'); + + expect(result['userAddress'], isA>()); + expect(result['oder_dt'], isA>()); + expect(result['oder_dt']['status'], 'pending'); + }); + }); +} diff --git a/test/features/driver_orders_details/data/repos/order_details_repo_impl_test.dart b/test/features/driver_orders_details/data/repos/order_details_repo_impl_test.dart new file mode 100644 index 0000000..b10ab3e --- /dev/null +++ b/test/features/driver_orders_details/data/repos/order_details_repo_impl_test.dart @@ -0,0 +1,90 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/data/datasource/order_details_remote_datasource.dart'; +import 'package:tracking_app/features/driver_orders_details/data/models/orders_dto.dart'; +import 'package:tracking_app/features/driver_orders_details/data/repos/order_details_repo_impl.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; +import 'order_details_repo_impl_test.mocks.dart'; + +@GenerateMocks([OrderDetailsRemoteDatasource, DocumentSnapshot]) +void main() { + late OrderDetailsRepoImpl repository; + late MockOrderDetailsRemoteDatasource mockRemoteDataSource; + + setUp(() { + mockRemoteDataSource = MockOrderDetailsRemoteDatasource(); + repository = OrderDetailsRepoImpl(mockRemoteDataSource); + provideDummy>>( + ErrorApiResult(error: 'dummy_error'), + ); + }); + + const tOrderId = 'pxkMaEmWYVuvV5jkW0JK'; + + final tOrderDto = OrderDto( + driverId: 'D123', + userAddress: UserAddressDto( + address: 'Alex', + name: 'Mariam', + userId: 'U123', + ), + userId: 'U789', + orderId: tOrderId, + orderDetails: OrderDetailsDto( + items: [], + status: 'accepted', + totalPrice: 150.0, + pickupAddress: PickedAddressDto(name: 'Pharmacy', address: 'Downtown'), + orderId: tOrderId, + userAddress: 'Alex', + ), + ); + + group('getOrderDetails', () { + test( + 'should emit OrderModel when the remote data source returns SuccessApiResult with Stream', + () async { + when( + mockRemoteDataSource.getOrderStream(tOrderId), + ).thenReturn(SuccessApiResult(data: Stream.value(tOrderDto))); + + final result = repository.getOrderDetails(tOrderId); + + expect(result, isA>>()); + final stream = (result as SuccessApiResult>).data; + await expectLater( + stream, + emits( + isA() + .having((o) => o.orderId, 'order id', tOrderId) + .having((o) => o.userAddress.name, 'user name', 'Mariam') + .having( + (o) => o.orderDetails.status, + 'order status', + 'accepted', + ) + .having((o) => o.orderDetails.totalPrice, 'total price', 150.0), + ), + ); + }, + ); + + test( + 'should throw an Exception when the document does not exist', + () async { + const errorMessage = "Network Error"; + when( + mockRemoteDataSource.getOrderStream(tOrderId), + ).thenReturn(ErrorApiResult(error: errorMessage)); + + final result = repository.getOrderDetails(tOrderId); + + expect(result, isA>>()); + expect((result as ErrorApiResult).error, errorMessage); + }, + ); + }); +} diff --git a/test/features/driver_orders_details/domain/models/orders_model_test.dart b/test/features/driver_orders_details/domain/models/orders_model_test.dart new file mode 100644 index 0000000..b4f986d --- /dev/null +++ b/test/features/driver_orders_details/domain/models/orders_model_test.dart @@ -0,0 +1,68 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; + +void main() { + group('OrderModel & UserAddressModel Tests', () { + test('should correctly initialize UserAddressModel with given values', () { + final tAddress = UserAddressModel( + address: 'Cairo', + name: 'Mohamed', + userId: '1', + ); + + expect(tAddress.address, 'Cairo'); + expect(tAddress.name, 'Mohamed'); + expect(tAddress.userId, '1'); + }); + + test('should correctly initialize OrderModel with given values', () { + final tUserAddress = UserAddressModel( + address: 'Cairo', + name: 'Mohamed', + userId: 'USR-555', + ); + + final tOrder = OrderModel( + driverId: 'DRV-101', + userAddress: tUserAddress, + userId: 'USR-555', + orderId: 'ORD-999', + orderDetails: OrderDetailsModel( + items: [], + status: 'picked_up', + totalPrice: 250, + pickupAddress: PickedAddressModel( + name: 'Pharmacy', + address: 'Downtown', + ), + orderId: 'ORD-999', + userAddress: 'Cairo', + ), + ); + + expect(tOrder.driverId, 'DRV-101'); + expect(tOrder.orderId, 'ORD-999'); + expect(tOrder.orderDetails.status, 'picked_up'); + expect(tOrder.orderDetails.totalPrice, 250); + expect(tOrder.userId, 'USR-555'); + + expect(tOrder.userAddress, isA()); + expect(tOrder.userAddress.name, 'Mohamed'); + }); + + test('should support equality check if needed (Optional)', () { + final address1 = UserAddressModel( + address: 'A', + name: 'B', + userId: 'USR-123', + ); + final address2 = UserAddressModel( + address: 'A', + name: 'B', + userId: 'USR-456', + ); + + expect(address1 == address2, isFalse); + }); + }); +} diff --git a/test/features/driver_orders_details/domain/usecases/get_order_details_usecase_test.dart b/test/features/driver_orders_details/domain/usecases/get_order_details_usecase_test.dart new file mode 100644 index 0000000..d27570b --- /dev/null +++ b/test/features/driver_orders_details/domain/usecases/get_order_details_usecase_test.dart @@ -0,0 +1,67 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/repos/order_details_repo.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/usecases/get_order_details_usecase.dart'; + +import 'get_order_details_usecase_test.mocks.dart'; + +@GenerateMocks([OrderDetailsRepo]) +void main() { + late GetOrderDetailsUsecase usecase; + late MockOrderDetailsRepo mockRepo; + + setUp(() { + mockRepo = MockOrderDetailsRepo(); + usecase = GetOrderDetailsUsecase(repo: mockRepo); + provideDummy>>(ErrorApiResult(error: 'dummy')); + }); + + const tOrderId = 'pxkMaEmWYVuvV5jkW0JK'; + + final tOrderModel = OrderModel( + driverId: 'D1', + userAddress: UserAddressModel(address: 'Shebin', name: 'Ali', userId: 'U1'), + userId: 'U1', + orderId: tOrderId, + orderDetails: OrderDetailsModel( + items: [], + status: 'accepted', + totalPrice: 500, + pickupAddress: PickedAddressModel(name: 'Pharmacy', address: 'Downtown'), + orderId: tOrderId, + userAddress: 'Shebin', + ), + ); + + group('GetOrderDetailsUsecase test', () { + test( + 'should return SuccessApiResult containing the Stream from the repository', + () async { + when( + mockRepo.getOrderDetails(any), + ).thenReturn(SuccessApiResult(data: Stream.value(tOrderModel))); + + final result = usecase.call(tOrderId); + + expect(result, isA>>()); + final stream = (result as SuccessApiResult>).data; + await expectLater(stream, emits(tOrderModel)); + verify(mockRepo.getOrderDetails(tOrderId)).called(1); + }, + ); + + test('should return ErrorApiResult when the repository fails', () async { + when( + mockRepo.getOrderDetails(any), + ).thenReturn(ErrorApiResult(error: 'Error from Repository')); + + final result = usecase.call(tOrderId); + + expect(result, isA>>()); + expect((result as ErrorApiResult).error, 'Error from Repository'); + }); + }); +} diff --git a/test/features/driver_orders_details/presentation/pages/drivers_orders_details_page_test.dart b/test/features/driver_orders_details/presentation/pages/drivers_orders_details_page_test.dart new file mode 100644 index 0000000..18f5d20 --- /dev/null +++ b/test/features/driver_orders_details/presentation/pages/drivers_orders_details_page_test.dart @@ -0,0 +1,123 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/features/driver_orders_details/domain/models/orders_model.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_cubit.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/manager/order_details_states.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/pages/drivers_orders_details_page.dart'; +import 'package:tracking_app/features/driver_orders_details/presentation/widgets/address_card.dart'; +import 'drivers_orders_details_page_test.mocks.dart'; + +@GenerateMocks([OrderDetailsCubit]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + late MockOrderDetailsCubit mockCubit; + + setUp(() async { + await getIt.reset(); + mockCubit = MockOrderDetailsCubit(); + getIt.registerFactory(() => mockCubit); + when(mockCubit.state).thenReturn(OrderDetailsStates()); + when(mockCubit.stream).thenAnswer((_) => const Stream.empty()); + }); + + Widget buildTestableWidget() { + return EasyLocalization( + supportedLocales: const [Locale('en')], + path: 'assets/translations', + fallbackLocale: const Locale('en'), + startLocale: const Locale('en'), + saveLocale: false, + child: Builder( + builder: (context) { + return MaterialApp( + home: BlocProvider.value( + value: mockCubit, + child: const DriversOrdersDetailsPage(), + ), + ); + }, + ), + ); + } + + final tOrderModel = OrderModel( + driverId: 'D1', + userAddress: UserAddressModel(address: 'Shebin', name: 'Ali', userId: 'U1'), + userId: 'U1', + orderId: 'N123', + orderDetails: OrderDetailsModel( + items: [], + status: 'accepted', + totalPrice: 500, + pickupAddress: PickedAddressModel(name: 'Pharmacy', address: 'Downtown'), + orderId: 'N123', + userAddress: 'Shebin', + ), + ); + + group('DriversOrdersDetailsPage Widget Tests', () { + testWidgets('should show CircularProgressIndicator when state is loading', ( + tester, + ) async { + when( + mockCubit.state, + ).thenReturn(OrderDetailsStates(data: Resource.loading())); + when(mockCubit.stream).thenAnswer( + (_) => Stream.value(OrderDetailsStates(data: Resource.loading())), + ); + + await tester.pumpWidget(buildTestableWidget()); + await tester.pump(); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }); + + testWidgets( + 'should display order details correctly when state is success', + (tester) async { + when( + mockCubit.state, + ).thenReturn(OrderDetailsStates(data: Resource.success(tOrderModel))); + when(mockCubit.stream).thenAnswer( + (_) => Stream.value( + OrderDetailsStates(data: Resource.success(tOrderModel)), + ), + ); + + await tester.pumpWidget(buildTestableWidget()); + await tester.pump(); + + expect(find.textContaining('N123'), findsOneWidget); + expect(find.text('Ali'), findsOneWidget); + expect(find.text('Shebin'), findsAtLeastNWidgets(1)); + expect(find.textContaining('500'), findsOneWidget); + expect(find.byType(AddressCard), findsAtLeastNWidgets(2)); + }, + ); + + testWidgets('should display error message when state is error', ( + tester, + ) async { + const errorMessage = 'Failed to load order'; + when( + mockCubit.state, + ).thenReturn(OrderDetailsStates(data: Resource.error(errorMessage))); + when(mockCubit.stream).thenAnswer( + (_) => Stream.value( + OrderDetailsStates(data: Resource.error(errorMessage)), + ), + ); + + await tester.pumpWidget(buildTestableWidget()); + await tester.pump(); + + expect(find.text(errorMessage), findsOneWidget); + }); + }); +} diff --git a/test/features/home/api/driverOrderDataS_imp_test.dart b/test/features/home/api/driverOrderDataS_imp_test.dart new file mode 100644 index 0000000..9071216 --- /dev/null +++ b/test/features/home/api/driverOrderDataS_imp_test.dart @@ -0,0 +1,75 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:retrofit/retrofit.dart'; +import 'package:tracking_app/app/core/api_manger/api_client.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/api/driverOrderDataS_imp.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:dio/dio.dart'; + +import 'driverOrderDataS_imp_test.mocks.dart'; + +@GenerateMocks([ApiClient]) +void main() { + late DriverOrderDataSourceImpl dataSource; + late MockApiClient mockApiClient; + + setUp(() { + mockApiClient = MockApiClient(); + dataSource = DriverOrderDataSourceImpl(mockApiClient); + }); + + group('DriverOrderDataSourceImpl', () { + const tToken = 'test_token'; + final tOrderResponse = OrderResponse(message: 'Success', orders: []); + + test( + 'should return SuccessApiResult when the call to ApiClient is successful', + () async { + // Arrange + final httpResponse = HttpResponse( + tOrderResponse, + Response( + data: tOrderResponse, + requestOptions: RequestOptions(path: ''), + statusCode: 200, + ), + ); + when( + mockApiClient.getPendingOrders(any), + ).thenAnswer((_) async => httpResponse); + + // Act + final result = await dataSource.getPendingOrders(tToken); + + // Assert + expect(result, isA>()); + verify(mockApiClient.getPendingOrders(tToken)); + verifyNoMoreInteractions(mockApiClient); + }, + ); + + test( + 'should return ErrorApiResult when the call to ApiClient throws an exception', + () async { + // Arrange + when(mockApiClient.getPendingOrders(any)).thenThrow( + DioException( + requestOptions: RequestOptions(path: ''), + error: 'Error', + type: DioExceptionType.unknown, + ), + ); + + // Act + final result = await dataSource.getPendingOrders(tToken); + + // Assert + expect(result, isA>()); + verify(mockApiClient.getPendingOrders(tToken)); + verifyNoMoreInteractions(mockApiClient); + }, + ); + }); +} diff --git a/test/features/home/data/model/response/orderRespons_test.dart b/test/features/home/data/model/response/orderRespons_test.dart new file mode 100644 index 0000000..60b946c --- /dev/null +++ b/test/features/home/data/model/response/orderRespons_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; + +void main() { + group('OrderResponse', () { + final tOrderResponse = OrderResponse(message: 'Success'); + + test('should work with copyWith', () { + final result = tOrderResponse.copyWith(message: 'New Success'); + expect(result.message, 'New Success'); + }); + + test('fromJson should return a valid model', () { + final Map jsonMap = {"message": "Success", "orders": []}; + final result = OrderResponse.fromJson(jsonMap); + expect(result, isA()); + expect(result.message, "Success"); + }); + + test('toJson should return a JSON map containing proper data', () { + final result = tOrderResponse.toJson(); + final expectedMap = { + "message": "Success", + "metadata": null, + "orders": null, + }; + expect(result, expectedMap); + }); + }); +} diff --git a/test/features/home/data/repo/driverOrderRepo_impl_test.dart b/test/features/home/data/repo/driverOrderRepo_impl_test.dart new file mode 100644 index 0000000..f1f142e --- /dev/null +++ b/test/features/home/data/repo/driverOrderRepo_impl_test.dart @@ -0,0 +1,64 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/data/datascourse/driverOrderDatascource.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/home/data/repo/driverOrderRepo_impl.dart'; + +import 'driverOrderRepo_impl_test.mocks.dart'; + +@GenerateMocks([DriverOrderDataSource]) +void main() { + late DriverOrderRepositoryImpl repository; + late MockDriverOrderDataSource mockDataSource; + + setUp(() { + provideDummy>( + SuccessApiResult(data: OrderResponse()), + ); + mockDataSource = MockDriverOrderDataSource(); + repository = DriverOrderRepositoryImpl(mockDataSource); + }); + + group('DriverOrderRepositoryImpl', () { + const tToken = 'test_token'; + final tOrderResponse = OrderResponse(message: 'Success', orders: []); + + test( + 'should return data when the call to remote data source is successful', + () async { + // Arrange + when( + mockDataSource.getPendingOrders(any), + ).thenAnswer((_) async => SuccessApiResult(data: tOrderResponse)); + + // Act + final result = await repository.getPendingOrders(tToken); + + // Assert + expect(result, isA>()); + verify(mockDataSource.getPendingOrders(tToken)); + verifyNoMoreInteractions(mockDataSource); + }, + ); + + test( + 'should return error when the call to remote data source is unsuccessful', + () async { + // Arrange + when( + mockDataSource.getPendingOrders(any), + ).thenAnswer((_) async => ErrorApiResult(error: 'Error')); + + // Act + final result = await repository.getPendingOrders(tToken); + + // Assert + expect(result, isA>()); + verify(mockDataSource.getPendingOrders(tToken)); + verifyNoMoreInteractions(mockDataSource); + }, + ); + }); +} diff --git a/test/features/home/domain/usecases/getdriverOrderUsecase_test.dart b/test/features/home/domain/usecases/getdriverOrderUsecase_test.dart new file mode 100644 index 0000000..2d21ae9 --- /dev/null +++ b/test/features/home/domain/usecases/getdriverOrderUsecase_test.dart @@ -0,0 +1,58 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/home/domain/repo/driverOrderRepo.dart'; +import 'package:tracking_app/features/home/domain/usecase/getdriverOrderUsecase.dart'; + +import 'getdriverOrderUsecase_test.mocks.dart'; + +@GenerateMocks([DriverOrderRepo]) +void main() { + late GetDriverOrdersUseCase useCase; + late MockDriverOrderRepo mockRepository; + + setUp(() { + provideDummy>( + SuccessApiResult(data: OrderResponse()), + ); + mockRepository = MockDriverOrderRepo(); + useCase = GetDriverOrdersUseCase(mockRepository); + }); + + const tToken = 'test_token'; + final tOrderResponse = OrderResponse(message: 'Success', orders: []); + + group('GetDriverOrdersUseCase', () { + test('should get pending orders from the repository', () async { + // Arrange + when( + mockRepository.getPendingOrders(any), + ).thenAnswer((_) async => SuccessApiResult(data: tOrderResponse)); + + // Act + final result = await useCase(tToken); + + // Assert + expect(result, isA>()); + verify(mockRepository.getPendingOrders(tToken)); + verifyNoMoreInteractions(mockRepository); + }); + + test('should return error value from the repository', () async { + // Arrange + when( + mockRepository.getPendingOrders(any), + ).thenAnswer((_) async => ErrorApiResult(error: 'Error')); + + // Act + final result = await useCase(tToken); + + // Assert + expect(result, isA>()); + verify(mockRepository.getPendingOrders(tToken)); + verifyNoMoreInteractions(mockRepository); + }); + }); +} diff --git a/test/features/home/presentation/manger/driverorderCubit_test.dart b/test/features/home/presentation/manger/driverorderCubit_test.dart new file mode 100644 index 0000000..41faf4b --- /dev/null +++ b/test/features/home/presentation/manger/driverorderCubit_test.dart @@ -0,0 +1,189 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/home/domain/repo/driverOrderRepo.dart'; +import 'package:tracking_app/features/home/domain/usecase/getdriverOrderUsecase.dart'; +import 'package:tracking_app/features/home/domain/usecase/getdriverOrderUsecase.dart'; +import 'package:tracking_app/features/home/domain/usecase/upload_driver_fire_data_use_case.dart'; +import 'package:tracking_app/features/home/domain/usecase/upload_order_fire_data_use_case.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderCubit.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderIntent.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderStates.dart'; + +import 'driverorderCubit_test.mocks.dart'; + +@GenerateMocks([ + DriverOrderRepo, + AuthStorage, + UploadDriverFireDataUseCase, + UploadOrderFireDataUseCase, +]) +void main() { + late DriverOrderCubit driverOrderCubit; + late MockDriverOrderRepo mockDriverOrderRepo; + late MockUploadDriverFireDataUseCase mockUploadDriverFireDataUseCase; + late MockUploadOrderFireDataUseCase mockUploadOrderFireDataUseCase; + late GetDriverOrdersUseCase getDriverOrdersUseCase; + late MockAuthStorage mockAuthStorage; + + setUp(() { + provideDummy>( + SuccessApiResult(data: OrderResponse()), + ); + mockDriverOrderRepo = MockDriverOrderRepo(); + mockAuthStorage = MockAuthStorage(); + mockUploadDriverFireDataUseCase = MockUploadDriverFireDataUseCase(); + mockUploadOrderFireDataUseCase = MockUploadOrderFireDataUseCase(); + getDriverOrdersUseCase = GetDriverOrdersUseCase(mockDriverOrderRepo); + driverOrderCubit = DriverOrderCubit( + getDriverOrdersUseCase, + mockAuthStorage, + mockUploadDriverFireDataUseCase, + mockUploadOrderFireDataUseCase, + mockDriverOrderRepo, + ); + }); + + tearDown(() { + driverOrderCubit.close(); + }); + + group('DriverOrderCubit', () { + test('initial state is DriverOrderState with Resource.initial', () { + expect(driverOrderCubit.state.orderResource.status, Status.initial); + }); + + final tOrderResponse = OrderResponse( + message: 'Success', + orders: [ + Order(id: '1', state: 'pending'), + Order(id: '2', state: 'pending'), + ], + ); + + group('GetPendingOrders', () { + blocTest( + 'emits [loading, success] when GetPendingOrders is added and token exists and api call is successful', + build: () { + when(mockAuthStorage.getToken()).thenAnswer((_) async => 'token'); + when( + mockDriverOrderRepo.getPendingOrders('token'), + ).thenAnswer((_) async => SuccessApiResult(data: tOrderResponse)); + return driverOrderCubit; + }, + act: (cubit) => cubit.onIntent(GetPendingOrders()), + expect: () => [ + isA().having( + (state) => state.orderResource.status, + 'status', + Status.loading, + ), + isA() + .having( + (state) => state.orderResource.status, + 'status', + Status.success, + ) + .having( + (state) => state.orderResource.data, + 'data', + tOrderResponse, + ), + ], + ); + + blocTest( + 'emits [loading, error] when GetPendingOrders is added and token is null', + build: () { + when(mockAuthStorage.getToken()).thenAnswer((_) async => null); + return driverOrderCubit; + }, + act: (cubit) => cubit.onIntent(GetPendingOrders()), + expect: () => [ + isA().having( + (state) => state.orderResource.status, + 'status', + Status.loading, + ), + isA() + .having( + (state) => state.orderResource.status, + 'status', + Status.error, + ) + .having( + (state) => state.orderResource.error, + 'error', + 'User not authenticated', + ), + ], + ); + + blocTest( + 'emits [loading, error] when GetPendingOrders is added and api call fails', + build: () { + when(mockAuthStorage.getToken()).thenAnswer((_) async => 'token'); + when( + mockDriverOrderRepo.getPendingOrders('token'), + ).thenAnswer((_) async => ErrorApiResult(error: 'API Error')); + return driverOrderCubit; + }, + act: (cubit) => cubit.onIntent(GetPendingOrders()), + expect: () => [ + isA().having( + (state) => state.orderResource.status, + 'status', + Status.loading, + ), + isA() + .having( + (state) => state.orderResource.status, + 'status', + Status.error, + ) + .having( + (state) => state.orderResource.error, + 'error', + 'API Error', + ), + ], + ); + }); + + group('RemoveOrder', () { + final orderToRemove = Order(id: '1', state: 'pending'); + final orderToKeep = Order(id: '2', state: 'pending'); + final initialOrders = [orderToRemove, orderToKeep]; + final initialOrderResponse = OrderResponse(orders: initialOrders); + + blocTest( + 'emits [success] with updated orders when RemoveOrder is added', + build: () => driverOrderCubit, + seed: () => DriverOrderState( + orderResource: Resource.success(initialOrderResponse), + ), + act: (cubit) => cubit.onIntent(RemoveOrder(orderToRemove)), + expect: () => [ + isA().having( + (state) => state.orderResource.data?.orders, + 'orders', + [orderToKeep], + ), + ], + ); + + blocTest( + 'does nothing when RemoveOrder is added but current state is not success', + build: () => driverOrderCubit, + seed: () => DriverOrderState(orderResource: Resource.loading()), + act: (cubit) => cubit.onIntent(RemoveOrder(orderToRemove)), + expect: () => [], + ); + }); + }); +} diff --git a/test/features/home/presentation/pages/driverOrderScreen_test.dart b/test/features/home/presentation/pages/driverOrderScreen_test.dart new file mode 100644 index 0000000..ec577d3 --- /dev/null +++ b/test/features/home/presentation/pages/driverOrderScreen_test.dart @@ -0,0 +1,126 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/home/domain/repo/driverOrderRepo.dart'; +import 'package:tracking_app/features/home/domain/usecase/getdriverOrderUsecase.dart'; +import 'package:tracking_app/features/home/domain/usecase/getdriverOrderUsecase.dart'; +import 'package:tracking_app/features/home/domain/usecase/upload_driver_fire_data_use_case.dart'; +import 'package:tracking_app/features/home/domain/usecase/upload_order_fire_data_use_case.dart'; +import 'package:tracking_app/features/home/presentation/manger/driverorderCubit.dart'; +import 'package:tracking_app/features/home/presentation/pages/driverOrderScreen.dart'; +import 'package:tracking_app/features/home/presentation/widgets/driverOrderItem.dart'; + +import 'driverOrderScreen_test.mocks.dart'; + +@GenerateMocks([ + DriverOrderRepo, + AuthStorage, + UploadDriverFireDataUseCase, + UploadOrderFireDataUseCase, +]) +void main() { + late MockDriverOrderRepo mockDriverOrderRepo; + late MockAuthStorage mockAuthStorage; + late MockUploadDriverFireDataUseCase mockUploadDriverFireDataUseCase; + late MockUploadOrderFireDataUseCase mockUploadOrderFireDataUseCase; + late GetDriverOrdersUseCase getDriverOrdersUseCase; + + setUpAll(() async { + SharedPreferences.setMockInitialValues({}); + await EasyLocalization.ensureInitialized(); + }); + + setUp(() async { + mockDriverOrderRepo = MockDriverOrderRepo(); + mockAuthStorage = MockAuthStorage(); + mockUploadDriverFireDataUseCase = MockUploadDriverFireDataUseCase(); + mockUploadOrderFireDataUseCase = MockUploadOrderFireDataUseCase(); + getDriverOrdersUseCase = GetDriverOrdersUseCase(mockDriverOrderRepo); + + provideDummy>( + SuccessApiResult(data: OrderResponse()), + ); + + await GetIt.I.reset(); + GetIt.I.registerFactory( + () => DriverOrderCubit( + getDriverOrdersUseCase, + mockAuthStorage, + mockUploadDriverFireDataUseCase, + mockUploadOrderFireDataUseCase, + mockDriverOrderRepo, + ), + ); + }); + + Widget createWidgetUnderTest() { + return EasyLocalization( + supportedLocales: const [Locale('en')], + path: 'assets/translations', + fallbackLocale: const Locale('en'), + child: const MaterialApp(home: DriverOrderScreen()), + ); + } + + group('DriverOrderScreen Integration Tests', () { + testWidgets('displays CircularProgressIndicator when loading', ( + tester, + ) async { + // Arrange + when(mockAuthStorage.getToken()).thenAnswer((_) async => 'token'); + + when(mockDriverOrderRepo.getPendingOrders(any)).thenAnswer((_) async { + await Future.delayed(const Duration(milliseconds: 100)); + return SuccessApiResult(data: OrderResponse(orders: [])); + }); + + // Act + await tester.pumpWidget(createWidgetUnderTest()); + await tester.pump(); + + // Assert + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pumpAndSettle(); + }); + + testWidgets('displays error message when error occurs', (tester) async { + // Arrange + const errorMessage = 'Network Error'; + when(mockAuthStorage.getToken()).thenAnswer((_) async => 'token'); + when( + mockDriverOrderRepo.getPendingOrders(any), + ).thenAnswer((_) async => ErrorApiResult(error: errorMessage)); + + // Act + await tester.pumpWidget(createWidgetUnderTest()); + await tester.pumpAndSettle(); + + // Assert + expect(find.text(errorMessage), findsOneWidget); + }); + + testWidgets('displays "noPendingOrders" when success but empty list', ( + tester, + ) async { + // Arrange + when(mockAuthStorage.getToken()).thenAnswer((_) async => 'token'); + when(mockDriverOrderRepo.getPendingOrders(any)).thenAnswer( + (_) async => SuccessApiResult(data: OrderResponse(orders: [])), + ); + + // Act + await tester.pumpWidget(createWidgetUnderTest()); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('noPendingOrders'), findsOneWidget); + }); + }); +} diff --git a/test/features/home/presentation/widgets/driverOrderButton_test.dart b/test/features/home/presentation/widgets/driverOrderButton_test.dart new file mode 100644 index 0000000..59fe9a8 --- /dev/null +++ b/test/features/home/presentation/widgets/driverOrderButton_test.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/home/presentation/widgets/driverOrderButton.dart'; + +void main() { + group('DriverOrderButton Widget Tests', () { + testWidgets('renders button with correct text', (tester) async { + // Arrange + const buttonText = 'Accept'; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DriverOrderButton( + text: buttonText, + onTap: () {}, + isPrimary: true, + ), + ), + ), + ); + + // Assert + expect(find.text(buttonText), findsOneWidget); + }); + + testWidgets('calls onTap when tapped', (tester) async { + // Arrange + var isTapped = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DriverOrderButton( + text: 'Tap Me', + onTap: () { + isTapped = true; + }, + isPrimary: true, + ), + ), + ), + ); + + // Act + await tester.tap(find.byType(DriverOrderButton)); + await tester.pumpAndSettle(); + + // Assert + expect(isTapped, isTrue); + }); + + testWidgets('renders primary style correctly', (tester) async { + // Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DriverOrderButton( + text: 'Primary', + onTap: () {}, + isPrimary: true, + ), + ), + ), + ); + + // Verify Container decoration + final container = tester.widget( + find.ancestor( + of: find.text('Primary'), + matching: find.byType(Container), + ), + ); + final decoration = container.decoration as BoxDecoration; + + // Assert + expect(decoration.color, const Color(0xFFE91E63)); // Primary color + expect(decoration.border, isNull); + + // Verify Text style + final text = tester.widget(find.text('Primary')); + expect(text.style?.color, Colors.white); + }); + + testWidgets('renders secondary style correctly', (tester) async { + // Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DriverOrderButton( + text: 'Secondary', + onTap: () {}, + isPrimary: false, + ), + ), + ), + ); + + // Verify Container decoration + final container = tester.widget( + find.ancestor( + of: find.text('Secondary'), + matching: find.byType(Container), + ), + ); + final decoration = container.decoration as BoxDecoration; + + // Assert + expect(decoration.color, Colors.white); + expect(decoration.border, isNotNull); + // We can check border color if needed, but existence is good for now + + // Verify Text style + final text = tester.widget(find.text('Secondary')); + expect(text.style?.color, const Color(0xFFE91E63)); + }); + }); +} diff --git a/test/features/home/presentation/widgets/driverOrderInfoCard_test.dart b/test/features/home/presentation/widgets/driverOrderInfoCard_test.dart new file mode 100644 index 0000000..303e263 --- /dev/null +++ b/test/features/home/presentation/widgets/driverOrderInfoCard_test.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:tracking_app/features/home/presentation/widgets/driverOrderInfoCard.dart'; + +void main() { + group('DriverOrderInfoCard Widget Tests', () { + testWidgets('renders correct title and subtitle', (tester) async { + const title = 'Test Title'; + const subtitle = 'Test Subtitle'; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: DriverOrderInfoCard( + image: null, + title: title, + subtitle: subtitle, + isStore: false, + ), + ), + ), + ); + + expect(find.text(title), findsOneWidget); + expect(find.text(subtitle), findsOneWidget); + }); + + testWidgets('renders store icon when isStore is true and image is null', ( + tester, + ) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: DriverOrderInfoCard( + image: null, + title: 'Store', + subtitle: 'Address', + isStore: true, + ), + ), + ), + ); + + expect(find.byIcon(Icons.store), findsOneWidget); + expect(find.byIcon(Icons.person), findsNothing); + }); + + testWidgets('renders person icon when isStore is false and image is null', ( + tester, + ) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: DriverOrderInfoCard( + image: null, + title: 'User', + subtitle: 'Address', + isStore: false, + ), + ), + ), + ); + + expect(find.byIcon(Icons.person), findsOneWidget); + expect(find.byIcon(Icons.store), findsNothing); + }); + + testWidgets('renders NetworkImage when image is provided', (tester) async { + const imageUrl = 'https://example.com/image.jpg'; + + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: DriverOrderInfoCard( + image: imageUrl, + title: 'With Image', + subtitle: 'Address', + isStore: false, + ), + ), + ), + ); + }); + + // We need to find the specific container with the image. + // The hierarchy is Container > Row > [Container(image), SizedBox, Expanded(...)] + // So let's look for a Container with a BoxDecoration that has an image. + + final imageContainer = find.byWidgetPredicate((widget) { + if (widget is Container && widget.decoration is BoxDecoration) { + final decoration = widget.decoration as BoxDecoration; + return decoration.image != null && + decoration.image!.image is NetworkImage && + (decoration.image!.image as NetworkImage).url == imageUrl; + } + return false; + }); + + expect(imageContainer, findsOneWidget); + + // Verify no fallback icon is shown + expect(find.byIcon(Icons.person), findsNothing); + expect(find.byIcon(Icons.store), findsNothing); + }); + }); +} diff --git a/test/features/home/presentation/widgets/driverOrderItem_test.dart b/test/features/home/presentation/widgets/driverOrderItem_test.dart new file mode 100644 index 0000000..1489f84 --- /dev/null +++ b/test/features/home/presentation/widgets/driverOrderItem_test.dart @@ -0,0 +1,110 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tracking_app/features/home/data/model/response/orderRespons.dart'; +import 'package:tracking_app/features/home/presentation/widgets/driverOrderButton.dart'; +import 'package:tracking_app/features/home/presentation/widgets/driverOrderInfoCard.dart'; +import 'package:tracking_app/features/home/presentation/widgets/driverOrderItem.dart'; + +void main() { + setUpAll(() async { + SharedPreferences.setMockInitialValues({}); + await EasyLocalization.ensureInitialized(); + }); + + Widget createWidgetUnderTest( + Order order, { + VoidCallback? onAccept, + VoidCallback? onReject, + }) { + return EasyLocalization( + supportedLocales: const [Locale('en')], + path: 'assets/translations', + fallbackLocale: const Locale('en'), + child: MaterialApp( + home: Scaffold( + body: DriverOrderItem( + order: order, + onAccept: onAccept ?? () {}, + onReject: onReject ?? () {}, + ), + ), + ), + ); + } + + group('DriverOrderItem Widget Tests', () { + final testOrder = Order( + id: '1', + totalPrice: 100, + store: Store( + name: 'Test Store', + address: 'Store Address', + image: 'store_image.jpg', + ), + user: User(firstName: 'John', lastName: 'Doe', photo: 'user_photo.jpg'), + shippingAddress: ShippingAddress(street: 'User Street'), + ); + + testWidgets('renders order details correctly', (tester) async { + await mockNetworkImagesFor(() async { + await tester.pumpWidget(createWidgetUnderTest(testOrder)); + await tester.pumpAndSettle(); + }); + + // Verify Store Info + expect(find.text('Test Store'), findsOneWidget); + expect(find.text('Store Address'), findsOneWidget); + + // Verify User Info + expect(find.text('John Doe'), findsOneWidget); + expect(find.text('User Street'), findsOneWidget); + + // Verify Price + expect(find.text('100 egp'), findsOneWidget); + + // Verify sub-widgets are present + expect(find.byType(DriverOrderInfoCard), findsNWidgets(2)); + expect(find.byType(DriverOrderButton), findsNWidgets(2)); + }); + + testWidgets('calls onAccept when accept button is tapped', (tester) async { + var isAccepted = false; + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + createWidgetUnderTest(testOrder, onAccept: () => isAccepted = true), + ); + await tester.pumpAndSettle(); + }); + + final acceptButtonFinder = find.descendant( + of: find.byType(DriverOrderButton), + matching: find.text('accept'), + ); + + await tester.tap(acceptButtonFinder); + expect(isAccepted, isTrue); + }); + + testWidgets('calls onReject when reject button is tapped', (tester) async { + var isRejected = false; + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + createWidgetUnderTest(testOrder, onReject: () => isRejected = true), + ); + await tester.pumpAndSettle(); + }); + + // Find reject button + final rejectButtonFinder = find.descendant( + of: find.byType(DriverOrderButton), + matching: find.text('reject'), + ); + + await tester.tap(rejectButtonFinder); + expect(isRejected, isTrue); + }); + }); +} diff --git a/test/features/my_orders/api/datasource/my_orders_remote_data_source_imp_test.dart b/test/features/my_orders/api/datasource/my_orders_remote_data_source_imp_test.dart new file mode 100644 index 0000000..55ecd3e --- /dev/null +++ b/test/features/my_orders/api/datasource/my_orders_remote_data_source_imp_test.dart @@ -0,0 +1,85 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:retrofit/retrofit.dart'; +import 'package:tracking_app/app/core/api_manger/api_client.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/my_orders/api/datasource/my_orders_remote_data_source_imp.dart'; +import 'package:tracking_app/features/my_orders/data/models/response/my_order_response.dart'; + +import 'my_orders_remote_data_source_imp_test.mocks.dart'; + +@GenerateMocks([ApiClient]) +void main() { + late MyOrdersRemoteDataSourceImp dataSource; + late MockApiClient mockApiClient; + + setUp(() { + mockApiClient = MockApiClient(); + dataSource = MyOrdersRemoteDataSourceImp(mockApiClient); + }); + + const tToken = 'token123'; + const tLimit = 10; + const tPage = 1; + final tOrderResponse = MyOrderResponse(orders: []); + + group('MyOrdersRemoteDataSourceImp', () { + test( + 'should return SuccessApiResult when apiClient call is successful', + () async { + // Arrange + final httpResponse = HttpResponse( + tOrderResponse, + Response(requestOptions: RequestOptions(path: ''), statusCode: 200), + ); + when( + mockApiClient.getAllOrders( + token: anyNamed('token'), + limit: anyNamed('limit'), + page: anyNamed('page'), + ), + ).thenAnswer((_) async => httpResponse); + + // Act + final result = await dataSource.getAllOrders( + token: tToken, + limit: tLimit, + page: tPage, + ); + + // Assert + expect(result, isA>()); + expect( + (result as SuccessApiResult).data, + tOrderResponse, + ); + verify( + mockApiClient.getAllOrders(token: tToken, limit: tLimit, page: tPage), + ).called(1); + }, + ); + + test('should return ErrorApiResult when apiClient call fails', () async { + // Arrange + when( + mockApiClient.getAllOrders( + token: anyNamed('token'), + limit: anyNamed('limit'), + page: anyNamed('page'), + ), + ).thenThrow(DioException(requestOptions: RequestOptions(path: ''))); + + // Act + final result = await dataSource.getAllOrders( + token: tToken, + limit: tLimit, + page: tPage, + ); + + // Assert + expect(result, isA>()); + }); + }); +} diff --git a/test/features/my_orders/data/mappers/metadata_mapper_test.dart b/test/features/my_orders/data/mappers/metadata_mapper_test.dart new file mode 100644 index 0000000..b7a9da7 --- /dev/null +++ b/test/features/my_orders/data/mappers/metadata_mapper_test.dart @@ -0,0 +1,52 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/data/mappers/metadata_mapper.dart'; +import 'package:tracking_app/features/my_orders/data/models/meta_data_dto.dart'; +import 'package:tracking_app/features/my_orders/domain/models/meta_data_entity.dart'; + +void main() { + group('MetadataMapper', () { + test('should map Metadata DTO to MetadataEntity correctly', () { + final dto = Metadata( + currentPage: 1, + totalPages: 10, + totalItems: 100, + limit: 10, + cancelledCount: 5, + completedCount: 95, + ); + + final result = dto.toEntity(); + + expect(result, isA()); + expect(result.currentPage, 1); + expect(result.totalPages, 10); + expect(result.totalItems, 100); + expect(result.limit, 10); + expect(result.cancelledCount, 5); + expect(result.completedCount, 95); + }); + + test( + 'should map Metadata DTO with null fields to MetadataEntity with default values', + () { + final dto = Metadata( + currentPage: null, + totalPages: null, + totalItems: null, + limit: null, + cancelledCount: null, + completedCount: null, + ); + + final result = dto.toEntity(); + + expect(result.currentPage, 0); + expect(result.totalPages, 0); + expect(result.totalItems, 0); + expect(result.limit, 10); + expect(result.cancelledCount, 0); + expect(result.completedCount, 0); + }, + ); + }); +} diff --git a/test/features/my_orders/data/mappers/order_item_mapper_test.dart b/test/features/my_orders/data/mappers/order_item_mapper_test.dart new file mode 100644 index 0000000..76dbe6f --- /dev/null +++ b/test/features/my_orders/data/mappers/order_item_mapper_test.dart @@ -0,0 +1,43 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/data/mappers/order_item_mapper.dart'; +import 'package:tracking_app/features/my_orders/data/models/order_item_model.dart'; +import 'package:tracking_app/features/my_orders/data/models/product_model.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_item_entity.dart'; + +void main() { + group('OrderItemMapper', () { + test('should map OrderItem model to OrderItemEntity correctly', () { + final model = OrderItem( + id: 'i1', + product: Product(id: 'p1', price: 100), + price: 100, + quantity: 2, + ); + + final result = model.toEntity(); + + expect(result, isA()); + expect(result.product.id, 'p1'); + expect(result.price, 100); + expect(result.quantity, 2); + }); + + test( + 'should map OrderItem model with null fields to OrderItemEntity with default values', + () { + final model = OrderItem( + id: null, + product: null, + price: null, + quantity: null, + ); + + final result = model.toEntity(); + + expect(result.product.id, ''); + expect(result.price, 0); + expect(result.quantity, 0); + }, + ); + }); +} diff --git a/test/features/my_orders/data/mappers/order_mapper_test.dart b/test/features/my_orders/data/mappers/order_mapper_test.dart new file mode 100644 index 0000000..6480014 --- /dev/null +++ b/test/features/my_orders/data/mappers/order_mapper_test.dart @@ -0,0 +1,79 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/data/mappers/order_mapper.dart'; +import 'package:tracking_app/features/my_orders/data/models/order_model.dart'; +import 'package:tracking_app/features/my_orders/data/models/user_model.dart'; +import 'package:tracking_app/features/my_orders/data/models/store_model.dart'; +import 'package:tracking_app/features/my_orders/data/models/order_item_model.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; + +void main() { + group('OrderMapper', () { + test('should map Order model to OrderEntity correctly', () { + final model = Order( + id: 'o1', + user: User(id: 'u1', firstName: 'Noor', lastName: 'Mohamed'), + store: Store(name: 'Store Name'), + address: 'User Address', + orderItems: [OrderItem(price: 100, quantity: 1)], + totalPrice: 100, + paymentType: 'Cash', + isPaid: true, + isDelivered: true, + state: 'Delivered', + createdAt: '2023-01-01', + orderNumber: 'ORD123', + ); + + final result = model.toEntity(); + + expect(result, isA()); + expect(result.id, 'o1'); + expect(result.user.id, 'u1'); + expect(result.store?.name, 'Store Name'); + expect(result.address, 'User Address'); + expect(result.items.length, 1); + expect(result.totalPrice, 100); + expect(result.paymentType, 'Cash'); + expect(result.isPaid, true); + expect(result.isDelivered, true); + expect(result.state, 'Delivered'); + expect(result.createdAt, '2023-01-01'); + expect(result.orderNumber, 'ORD123'); + }); + + test( + 'should map Order model with null fields to OrderEntity with default values', + () { + final model = Order( + id: null, + user: User(id: null), + store: null, + address: null, + orderItems: null, + totalPrice: null, + paymentType: null, + isPaid: null, + isDelivered: null, + state: null, + createdAt: null, + orderNumber: null, + ); + + final result = model.toEntity(); + + expect(result.id, ''); + expect(result.user.id, ''); + expect(result.store, isNull); + expect(result.address, ''); + expect(result.items, isEmpty); + expect(result.totalPrice, 0); + expect(result.paymentType, ''); + expect(result.isPaid, false); + expect(result.isDelivered, false); + expect(result.state, ''); + expect(result.createdAt, ''); + expect(result.orderNumber, ''); + }, + ); + }); +} diff --git a/test/features/my_orders/data/mappers/orders_list_mapper_test.dart b/test/features/my_orders/data/mappers/orders_list_mapper_test.dart new file mode 100644 index 0000000..32d0a13 --- /dev/null +++ b/test/features/my_orders/data/mappers/orders_list_mapper_test.dart @@ -0,0 +1,37 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/data/mappers/orders_list_mapper.dart'; +import 'package:tracking_app/features/my_orders/data/models/order_model.dart'; +import 'package:tracking_app/features/my_orders/data/models/user_model.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; + +void main() { + group('OrdersListMapper', () { + test('should map List to List correctly', () { + final list = [ + Order( + id: 'o1', + user: User(id: 'u1'), + ), + Order( + id: 'o2', + user: User(id: 'u2'), + ), + ]; + + final result = list.toEntityList(); + + expect(result, isA>()); + expect(result.length, 2); + expect(result[0].id, 'o1'); + expect(result[1].id, 'o2'); + }); + + test('should map empty List to empty List', () { + final list = []; + + final result = list.toEntityList(); + + expect(result, isEmpty); + }); + }); +} diff --git a/test/features/my_orders/data/mappers/product_mapper_test.dart b/test/features/my_orders/data/mappers/product_mapper_test.dart new file mode 100644 index 0000000..510cc0e --- /dev/null +++ b/test/features/my_orders/data/mappers/product_mapper_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/data/mappers/product_mapper.dart'; +import 'package:tracking_app/features/my_orders/data/models/product_model.dart'; +import 'package:tracking_app/features/my_orders/domain/models/product_entity.dart'; + +void main() { + group('ProductMapper', () { + test('should map Product model to ProductEntity correctly', () { + final model = Product(id: 'p1', price: 100); + + final result = model.toEntity(); + + expect(result, isA()); + expect(result.id, 'p1'); + expect(result.price, 100); + }); + + test( + 'should map Product model with null fields to ProductEntity with default values', + () { + final model = Product(id: null, price: null); + + final result = model.toEntity(); + + expect(result.id, ''); + expect(result.price, 0); + }, + ); + }); +} diff --git a/test/features/my_orders/data/mappers/store_mapper_test.dart b/test/features/my_orders/data/mappers/store_mapper_test.dart new file mode 100644 index 0000000..3cac0f7 --- /dev/null +++ b/test/features/my_orders/data/mappers/store_mapper_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/data/mappers/store_mapper.dart'; +import 'package:tracking_app/features/my_orders/data/models/store_model.dart'; +import 'package:tracking_app/features/my_orders/domain/models/store_entity.dart'; + +void main() { + group('StoreMapper', () { + test('should map Store model to StoreEntity correctly', () { + final model = Store( + name: 'Store Name', + image: 'image_url', + address: 'Store Address', + phoneNumber: '01012345678', + ); + + final result = model.toEntity(); + + expect(result, isA()); + expect(result.name, 'Store Name'); + expect(result.image, 'image_url'); + expect(result.address, 'Store Address'); + expect(result.phoneNumber, '01012345678'); + }); + + test( + 'should map Store model with null fields to StoreEntity with default values', + () { + final model = Store( + name: null, + image: null, + address: null, + phoneNumber: null, + ); + + final result = model.toEntity(); + + expect(result.name, ''); + expect(result.image, ''); + expect(result.address, ''); + expect(result.phoneNumber, ''); + }, + ); + }); +} diff --git a/test/features/my_orders/data/mappers/user_mapper_test.dart b/test/features/my_orders/data/mappers/user_mapper_test.dart new file mode 100644 index 0000000..93e4502 --- /dev/null +++ b/test/features/my_orders/data/mappers/user_mapper_test.dart @@ -0,0 +1,48 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/data/mappers/user_mapper.dart'; +import 'package:tracking_app/features/my_orders/data/models/user_model.dart'; +import 'package:tracking_app/features/my_orders/domain/models/user_entity.dart'; + +void main() { + group('UserMapper', () { + test('should map User model to UserEntity correctly', () { + final model = User( + id: 'u1', + firstName: 'Noor', + lastName: 'Mohamed', + phone: '01012345678', + photo: 'photo_url', + ); + + final result = model.toEntity(); + + expect(result, isA()); + expect(result.id, 'u1'); + expect(result.firstName, 'Noor'); + expect(result.lastName, 'Mohamed'); + expect(result.phone, '01012345678'); + expect(result.photo, 'photo_url'); + }); + + test( + 'should map User model with null fields to UserEntity with default values', + () { + final model = User( + id: null, + firstName: null, + lastName: null, + phone: null, + photo: null, + ); + + final result = model.toEntity(); + + expect(result.id, ''); + expect(result.firstName, ''); + expect(result.lastName, ''); + expect(result.phone, ''); + expect(result.photo, ''); + }, + ); + }); +} diff --git a/test/features/my_orders/data/repo/my_orders_repo_imp_test.dart b/test/features/my_orders/data/repo/my_orders_repo_imp_test.dart new file mode 100644 index 0000000..2d534b9 --- /dev/null +++ b/test/features/my_orders/data/repo/my_orders_repo_imp_test.dart @@ -0,0 +1,113 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/my_orders/data/datasource/my_orders_remote_data_source.dart'; +import 'package:tracking_app/features/my_orders/data/models/response/my_order_response.dart'; +import 'package:tracking_app/features/my_orders/data/models/order_model.dart'; +import 'package:tracking_app/features/my_orders/data/models/user_model.dart'; +import 'package:tracking_app/features/my_orders/data/repo/my_orders_repo_imp.dart'; +import 'package:tracking_app/features/my_orders/domain/repo/my_orders_repo.dart'; + +import 'my_orders_repo_imp_test.mocks.dart'; + +@GenerateMocks([MyOrdersRemoteDataSource]) +void main() { + late MyOrdersRepoImpl repo; + late MockMyOrdersRemoteDataSource mockRemoteDataSource; + + setUpAll(() { + provideDummy>( + SuccessApiResult(data: MyOrderResponse(orders: [])), + ); + }); + + setUp(() { + mockRemoteDataSource = MockMyOrdersRemoteDataSource(); + repo = MyOrdersRepoImpl(mockRemoteDataSource); + }); + + const tToken = 'token123'; + final tOrderModel = Order( + id: 'o1', + user: User(id: 'u1'), + ); + final tOrderResponse = MyOrderResponse(orders: [tOrderModel], metadata: null); + + group('MyOrdersRepoImpl', () { + test( + 'should return SuccessApiResult with data from remote data source when it is successful and not empty', + () async { + // Arrange + when( + mockRemoteDataSource.getAllOrders( + token: anyNamed('token'), + limit: anyNamed('limit'), + page: anyNamed('page'), + ), + ).thenAnswer((_) async => SuccessApiResult(data: tOrderResponse)); + + // Act + final result = await repo.getAllOrders(token: tToken); + + // Assert + expect(result, isA>()); + final data = (result as SuccessApiResult).data; + expect(data.orders.length, 1); + expect(data.orders[0].id, 'o1'); + verify( + mockRemoteDataSource.getAllOrders(token: tToken, limit: 10, page: 1), + ).called(1); + }, + ); + + test( + 'should return SuccessApiResult with dummy data when remote data source returns empty list', + () async { + // Arrange + final emptyResponse = MyOrderResponse(orders: [], metadata: null); + when( + mockRemoteDataSource.getAllOrders( + token: anyNamed('token'), + limit: anyNamed('limit'), + page: anyNamed('page'), + ), + ).thenAnswer((_) async => SuccessApiResult(data: emptyResponse)); + + // Act + final result = await repo.getAllOrders(token: tToken); + + // Assert + expect(result, isA>()); + final data = (result as SuccessApiResult).data; + expect(data.orders.isNotEmpty, true); + expect(data.orders[0].id, '123456'); + verify( + mockRemoteDataSource.getAllOrders(token: tToken, limit: 10, page: 1), + ).called(1); + }, + ); + + test( + 'should return ErrorApiResult when remote data source call fails', + () async { + // Arrange + const tError = 'Server error'; + when( + mockRemoteDataSource.getAllOrders( + token: anyNamed('token'), + limit: anyNamed('limit'), + page: anyNamed('page'), + ), + ).thenAnswer((_) async => ErrorApiResult(error: tError)); + + // Act + final result = await repo.getAllOrders(token: tToken); + + // Assert + expect(result, isA>()); + expect((result as ErrorApiResult).error, tError); + }, + ); + }); +} diff --git a/test/features/my_orders/domain/usecase/get_order_use_case_test.dart b/test/features/my_orders/domain/usecase/get_order_use_case_test.dart new file mode 100644 index 0000000..6c0a580 --- /dev/null +++ b/test/features/my_orders/domain/usecase/get_order_use_case_test.dart @@ -0,0 +1,99 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/my_orders/domain/repo/my_orders_repo.dart'; +import 'package:tracking_app/features/my_orders/domain/usecases/get_order_use_case.dart'; + +import 'get_order_use_case_test.mocks.dart'; + +@GenerateMocks([MyOrdersRepo]) +void main() { + late GetOrderUseCase getOrderUseCase; + late MockMyOrdersRepo mockMyOrdersRepo; + + setUpAll(() { + provideDummy>( + SuccessApiResult(data: MyOrdersResult(orders: [])), + ); + }); + + setUp(() { + mockMyOrdersRepo = MockMyOrdersRepo(); + getOrderUseCase = GetOrderUseCase(mockMyOrdersRepo); + }); + + const tToken = 'token123'; + const tPage = 1; + const tLimit = 10; + final tMyOrdersResult = MyOrdersResult(orders: []); + + group('GetOrderUseCase', () { + test( + 'should return SuccessApiResult when repo call is successful', + () async { + // Arrange + when( + mockMyOrdersRepo.getAllOrders( + token: anyNamed('token'), + page: anyNamed('page'), + limit: anyNamed('limit'), + ), + ).thenAnswer((_) async => SuccessApiResult(data: tMyOrdersResult)); + + // Act + final result = await getOrderUseCase.call( + token: tToken, + page: tPage, + limit: tLimit, + ); + + // Assert + expect(result, isA>()); + expect( + (result as SuccessApiResult).data, + tMyOrdersResult, + ); + verify( + mockMyOrdersRepo.getAllOrders( + token: tToken, + page: tPage, + limit: tLimit, + ), + ).called(1); + verifyNoMoreInteractions(mockMyOrdersRepo); + }, + ); + + test('should return ErrorApiResult when repo call fails', () async { + // Arrange + const tErrorMessage = 'An error occurred'; + when( + mockMyOrdersRepo.getAllOrders( + token: anyNamed('token'), + page: anyNamed('page'), + limit: anyNamed('limit'), + ), + ).thenAnswer((_) async => ErrorApiResult(error: tErrorMessage)); + + // Act + final result = await getOrderUseCase.call( + token: tToken, + page: tPage, + limit: tLimit, + ); + + // Assert + expect(result, isA>()); + expect((result as ErrorApiResult).error, tErrorMessage); + verify( + mockMyOrdersRepo.getAllOrders( + token: tToken, + page: tPage, + limit: tLimit, + ), + ).called(1); + verifyNoMoreInteractions(mockMyOrdersRepo); + }); + }); +} diff --git a/test/features/my_orders/presentation/manager/my_orders_cubit_test.dart b/test/features/my_orders/presentation/manager/my_orders_cubit_test.dart new file mode 100644 index 0000000..36d622b --- /dev/null +++ b/test/features/my_orders/presentation/manager/my_orders_cubit_test.dart @@ -0,0 +1,131 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/my_orders/domain/repo/my_orders_repo.dart'; +import 'package:tracking_app/features/my_orders/domain/usecases/get_order_use_case.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_cubit.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_intent.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_state.dart'; + +import 'my_orders_cubit_test.mocks.dart'; + +@GenerateMocks([GetOrderUseCase, AuthStorage]) +void main() { + late MyOrdersCubit cubit; + late MockGetOrderUseCase mockGetOrderUseCase; + late MockAuthStorage mockAuthStorage; + + setUpAll(() { + provideDummy>( + SuccessApiResult(data: MyOrdersResult(orders: [])), + ); + }); + + setUp(() { + mockGetOrderUseCase = MockGetOrderUseCase(); + mockAuthStorage = MockAuthStorage(); + cubit = MyOrdersCubit(mockGetOrderUseCase, mockAuthStorage); + }); + + tearDown(() { + cubit.close(); + }); + + const tToken = 'token123'; + final tOrdersResult = MyOrdersResult(orders: []); + + group('MyOrdersCubit', () { + test('initial state should be correct', () { + expect(cubit.state.ordersResource.status, Status.initial); + expect(cubit.state.orders, isEmpty); + expect(cubit.state.isLoadingMore, false); + }); + + blocTest( + 'emits [loading, success] when GetMyOrdersIntent is successful', + build: () { + when(mockAuthStorage.getToken()).thenAnswer((_) async => tToken); + when( + mockGetOrderUseCase.call( + token: anyNamed('token'), + page: anyNamed('page'), + limit: anyNamed('limit'), + ), + ).thenAnswer((_) async => SuccessApiResult(data: tOrdersResult)); + return cubit; + }, + act: (cubit) => cubit.doIntent(GetMyOrdersIntent(page: 1, limit: 10)), + expect: () => [ + isA().having( + (s) => s.ordersResource.status, + 'status', + Status.loading, + ), + isA().having( + (s) => s.ordersResource.status, + 'status', + Status.success, + ), + ], + verify: (_) { + verify(mockAuthStorage.getToken()).called(1); + verify( + mockGetOrderUseCase.call(token: 'Bearer $tToken', page: 1, limit: 10), + ).called(1); + }, + ); + + blocTest( + 'emits [loading, error] when GetMyOrdersIntent fails', + build: () { + when(mockAuthStorage.getToken()).thenAnswer((_) async => tToken); + when( + mockGetOrderUseCase.call( + token: anyNamed('token'), + page: anyNamed('page'), + limit: anyNamed('limit'), + ), + ).thenAnswer((_) async => ErrorApiResult(error: 'Server error')); + return cubit; + }, + act: (cubit) => cubit.doIntent(GetMyOrdersIntent(page: 1, limit: 10)), + expect: () => [ + isA().having( + (s) => s.ordersResource.status, + 'status', + Status.loading, + ), + isA().having( + (s) => s.ordersResource.status, + 'status', + Status.error, + ), + ], + ); + + blocTest( + 'emits [loading, error] when token is missing', + build: () { + when(mockAuthStorage.getToken()).thenAnswer((_) async => null); + return cubit; + }, + act: (cubit) => cubit.doIntent(GetMyOrdersIntent(page: 1, limit: 10)), + expect: () => [ + isA().having( + (s) => s.ordersResource.status, + 'status', + Status.loading, + ), + isA().having( + (s) => s.ordersResource.status, + 'status', + Status.error, + ), + ], + ); + }); +} diff --git a/test/features/my_orders/presentation/pages/my_orders_page_test.dart b/test/features/my_orders/presentation/pages/my_orders_page_test.dart new file mode 100644 index 0000000..0ebee80 --- /dev/null +++ b/test/features/my_orders/presentation/pages/my_orders_page_test.dart @@ -0,0 +1,64 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_cubit.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_intent.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_state.dart'; +import 'package:tracking_app/features/my_orders/presentation/pages/my_orders_page.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; + +class MockMyOrdersCubit extends MockCubit + implements MyOrdersCubit {} + +void main() { + late MockMyOrdersCubit mockCubit; + late GetIt getIt; + + setUpAll(() async { + TestWidgetsFlutterBinding.ensureInitialized(); + SharedPreferences.setMockInitialValues({}); + await EasyLocalization.ensureInitialized(); + registerFallbackValue(GetMyOrdersIntent(page: 1, limit: 10)); + }); + + setUp(() { + getIt = GetIt.instance; + mockCubit = MockMyOrdersCubit(); + + if (getIt.isRegistered()) { + getIt.unregister(); + } + getIt.registerSingleton(mockCubit); + + when(() => mockCubit.doIntent(any())).thenAnswer((_) async {}); + when(() => mockCubit.state).thenReturn(MyOrdersState()); + }); + + tearDown(() { + getIt.reset(); + }); + + Widget createWidgetUnderTest() { + return EasyLocalization( + supportedLocales: const [Locale('en'), Locale('ar')], + path: 'assets/translations', + fallbackLocale: const Locale('en'), + child: const MaterialApp(home: MyOrdersPage()), + ); + } + + testWidgets('MyOrdersPage renders correctly', (WidgetTester tester) async { + await mockNetworkImagesFor(() async { + await tester.pumpWidget(createWidgetUnderTest()); + await tester.pumpAndSettle(); + + expect(find.text("My orders"), findsOneWidget); + expect(find.text("Recent orders"), findsOneWidget); + }); + }); +} diff --git a/test/features/my_orders/presentation/pages/order_details_page_test.dart b/test/features/my_orders/presentation/pages/order_details_page_test.dart new file mode 100644 index 0000000..dbe4e69 --- /dev/null +++ b/test/features/my_orders/presentation/pages/order_details_page_test.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_item_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/product_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/store_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/user_entity.dart'; +import 'package:tracking_app/features/my_orders/presentation/pages/order_details_page.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/order_item_tile.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/summary_row.dart'; + +void main() { + final tOrder = OrderEntity( + id: "123456", + user: UserEntity( + id: "u1", + firstName: "Noor", + lastName: "mohamed", + phone: "01012345678", + photo: "https://example.com/user.png", + ), + store: StoreEntity( + name: "Flowery store", + image: "https://example.com/store.png", + address: "20th st, Sheikh Zayed, Giza", + phoneNumber: "01012345678", + ), + address: "20th st, Sheikh Zayed, Giza", + items: [ + OrderItemEntity( + product: ProductEntity( + id: "p1", + title: "Red roses", + image: "https://example.com/item.png", + price: 600, + ), + price: 600, + quantity: 1, + ), + ], + totalPrice: 3000, + paymentType: "Cash on delivery", + isPaid: true, + isDelivered: true, + state: "Completed", + createdAt: "2023-01-01", + orderNumber: "123456", + ); + + testWidgets('OrderDetailsPage renders correctly with given order', ( + WidgetTester tester, + ) async { + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + MaterialApp(home: OrderDetailsPage(order: tOrder)), + ); + + expect(find.text("Order details"), findsWidgets); + expect(find.text("Completed"), findsOneWidget); + expect(find.text("# 123456"), findsOneWidget); + + expect(find.text("Pickup address"), findsOneWidget); + expect(find.text("Flowery store"), findsOneWidget); + + expect(find.text("User address"), findsOneWidget); + expect(find.text("Noor mohamed"), findsOneWidget); + + expect(find.byType(OrderItemTile), findsOneWidget); + expect(find.text("Red roses"), findsOneWidget); + + expect(find.byType(SummaryRow), findsNWidgets(2)); + expect(find.text("Egp 3000"), findsOneWidget); + expect(find.text("Cash on delivery"), findsOneWidget); + }); + }); +} diff --git a/test/features/my_orders/presentation/widgets/address_tile_test.dart b/test/features/my_orders/presentation/widgets/address_tile_test.dart new file mode 100644 index 0000000..d6b2994 --- /dev/null +++ b/test/features/my_orders/presentation/widgets/address_tile_test.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/address_title.dart'; + +void main() { + testWidgets('AddressTile renders correctly with given data', ( + WidgetTester tester, + ) async { + const title = 'Store Name'; + const address = '123 Street, City'; + const imageUrl = 'https://example.com/image.png'; + + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: AddressTile( + title: title, + address: address, + image: imageUrl, + isStore: true, + ), + ), + ), + ); + + expect(find.text(title), findsOneWidget); + expect(find.text(address), findsOneWidget); + expect( + find.byType(NetworkImage), + findsNothing, + ); // Image is in BoxDecoration, not as a widget + // We can check if the container with decoration exists + expect(find.byType(Container), findsWidgets); + }); + }); +} diff --git a/test/features/my_orders/presentation/widgets/my_orders_page_body_test.dart b/test/features/my_orders/presentation/widgets/my_orders_page_body_test.dart new file mode 100644 index 0000000..cce2f6b --- /dev/null +++ b/test/features/my_orders/presentation/widgets/my_orders_page_body_test.dart @@ -0,0 +1,42 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_cubit.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_state.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/my_orders_page_body.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/orders_filters_row.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/orders_list_view.dart'; + +class MockMyOrdersCubit extends MockCubit + implements MyOrdersCubit {} + +void main() { + late MockMyOrdersCubit mockCubit; + + setUp(() { + mockCubit = MockMyOrdersCubit(); + }); + + testWidgets('MyOrdersPageBody renders components correctly', ( + WidgetTester tester, + ) async { + when(() => mockCubit.state).thenReturn(MyOrdersState()); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BlocProvider.value( + value: mockCubit, + child: const MyOrdersPageBody(), + ), + ), + ), + ); + + expect(find.byType(OrdersFiltersRow), findsOneWidget); + expect(find.text("Recent orders"), findsOneWidget); + expect(find.byType(OrdersListView), findsOneWidget); + }); +} diff --git a/test/features/my_orders/presentation/widgets/order_card_test.dart b/test/features/my_orders/presentation/widgets/order_card_test.dart new file mode 100644 index 0000000..68d18b9 --- /dev/null +++ b/test/features/my_orders/presentation/widgets/order_card_test.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/store_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/user_entity.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/order_card.dart'; + +void main() { + final tOrder = OrderEntity( + id: 'o1', + user: UserEntity( + id: 'u1', + firstName: 'Noor', + lastName: 'Mohamed', + phone: '010', + photo: 'https://example.com/u1.png', + ), + store: StoreEntity( + name: 'Test Store', + image: 'https://example.com/s1.png', + address: 'Store Address', + phoneNumber: '011', + ), + address: 'User Address', + items: [], + totalPrice: 100, + paymentType: 'Cash', + isPaid: true, + isDelivered: true, + state: 'Delivered', + createdAt: '2023-01-01', + orderNumber: 'ORD123', + ); + + testWidgets('OrderCard renders correctly and handles tap', ( + WidgetTester tester, + ) async { + bool tapped = false; + + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: OrderCard(order: tOrder, onTap: () => tapped = true), + ), + ), + ); + + expect(find.text('Delivered'), findsOneWidget); + expect(find.text('# ORD123'), findsOneWidget); + expect(find.text('Test Store'), findsOneWidget); + expect(find.text('Store Address'), findsOneWidget); + expect(find.text('Noor Mohamed'), findsOneWidget); + expect(find.text('User Address'), findsOneWidget); + + await tester.tap(find.byType(OrderCard)); + expect(tapped, true); + }); + }); +} diff --git a/test/features/my_orders/presentation/widgets/order_item_tile_test.dart b/test/features/my_orders/presentation/widgets/order_item_tile_test.dart new file mode 100644 index 0000000..b764827 --- /dev/null +++ b/test/features/my_orders/presentation/widgets/order_item_tile_test.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_item_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/product_entity.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/order_item_tile.dart'; + +void main() { + final tOrderItem = OrderItemEntity( + product: ProductEntity( + id: "p1", + title: "Red roses, 15 Pink Rose Bouquet", + image: "https://example.com/image.png", + price: 600, + ), + price: 600, + quantity: 2, + ); + + testWidgets('OrderItemTile renders correctly with given data', ( + WidgetTester tester, + ) async { + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold(body: OrderItemTile(item: tOrderItem)), + ), + ); + + expect(find.text("Red roses, 15 Pink Rose Bouquet"), findsOneWidget); + expect(find.text("EGP 600"), findsOneWidget); + expect(find.text("X2"), findsOneWidget); + expect(find.byType(Container), findsWidgets); + }); + }); +} diff --git a/test/features/my_orders/presentation/widgets/orders_filters_row_test.dart b/test/features/my_orders/presentation/widgets/orders_filters_row_test.dart new file mode 100644 index 0000000..bd46f06 --- /dev/null +++ b/test/features/my_orders/presentation/widgets/orders_filters_row_test.dart @@ -0,0 +1,96 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tracking_app/features/my_orders/domain/models/meta_data_entity.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_cubit.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_intent.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_state.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/orders_filters_row.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/summary_card.dart'; + +class MockMyOrdersCubit extends MockCubit + implements MyOrdersCubit {} + +void main() { + late MockMyOrdersCubit mockCubit; + + setUpAll(() async { + TestWidgetsFlutterBinding.ensureInitialized(); + SharedPreferences.setMockInitialValues({}); + await EasyLocalization.ensureInitialized(); + registerFallbackValue(FilterCancelledOrdersIntent()); + registerFallbackValue(FilterCompletedOrdersIntent()); + }); + + setUp(() { + mockCubit = MockMyOrdersCubit(); + when(() => mockCubit.doIntent(any())).thenAnswer((_) async {}); + }); + + Widget createWidgetUnderTest() { + return EasyLocalization( + supportedLocales: const [Locale('en'), Locale('ar')], + path: 'assets/translations', + fallbackLocale: const Locale('en'), + child: MaterialApp( + home: Scaffold( + body: BlocProvider.value( + value: mockCubit, + child: const OrdersFiltersRow(), + ), + ), + ), + ); + } + + testWidgets('OrdersFiltersRow renders correct counts from metadata', ( + WidgetTester tester, + ) async { + final state = MyOrdersState( + metadata: const MetadataEntity( + currentPage: 1, + totalPages: 1, + totalItems: 10, + limit: 10, + cancelledCount: 3, + completedCount: 7, + ), + ); + + when(() => mockCubit.state).thenReturn(state); + + await tester.pumpWidget(createWidgetUnderTest()); + await tester.pumpAndSettle(); + + expect(find.text('3'), findsOneWidget); + expect(find.text('7'), findsOneWidget); + expect(find.text('Cancelled'), findsOneWidget); + expect(find.text('Completed'), findsOneWidget); + }); + + testWidgets('OrdersFiltersRow triggers intents on tap', ( + WidgetTester tester, + ) async { + final state = MyOrdersState(); + when(() => mockCubit.state).thenReturn(state); + + await tester.pumpWidget(createWidgetUnderTest()); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Cancelled')); + await tester.pump(); + verify( + () => mockCubit.doIntent(any(that: isA())), + ).called(1); + + await tester.tap(find.text('Completed')); + await tester.pump(); + verify( + () => mockCubit.doIntent(any(that: isA())), + ).called(1); + }); +} diff --git a/test/features/my_orders/presentation/widgets/orders_list_view_test.dart b/test/features/my_orders/presentation/widgets/orders_list_view_test.dart new file mode 100644 index 0000000..d0b47ec --- /dev/null +++ b/test/features/my_orders/presentation/widgets/orders_list_view_test.dart @@ -0,0 +1,107 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/features/my_orders/domain/models/order_entity.dart'; +import 'package:tracking_app/features/my_orders/domain/models/user_entity.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_cubit.dart'; +import 'package:tracking_app/features/my_orders/presentation/manager/my_orders_state.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/orders_list_view.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/order_card.dart'; + +class MockMyOrdersCubit extends MockCubit + implements MyOrdersCubit {} + +void main() { + late MockMyOrdersCubit mockCubit; + + setUp(() { + mockCubit = MockMyOrdersCubit(); + }); + + testWidgets('OrdersListView shows loading indicator when loading', ( + WidgetTester tester, + ) async { + when( + () => mockCubit.state, + ).thenReturn(MyOrdersState(ordersResource: Resource.loading())); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BlocProvider.value( + value: mockCubit, + child: const OrdersListView(), + ), + ), + ), + ); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }); + + testWidgets('OrdersListView shows empty message when no orders', ( + WidgetTester tester, + ) async { + when(() => mockCubit.state).thenReturn( + MyOrdersState(ordersResource: Resource.success(null), orders: []), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BlocProvider.value( + value: mockCubit, + child: const OrdersListView(), + ), + ), + ), + ); + + expect(find.text("No orders found"), findsOneWidget); + }); + + testWidgets('OrdersListView renders list of orders', ( + WidgetTester tester, + ) async { + final tOrder = OrderEntity( + id: 'o1', + user: UserEntity( + id: 'u1', + firstName: 'Noor', + lastName: 'Mohamed', + phone: '01', + photo: 'https://img.com', + ), + items: [], + totalPrice: 100, + paymentType: 'Cash', + isPaid: true, + isDelivered: true, + state: 'Delivered', + createdAt: '2023', + orderNumber: '1', + ); + + when(() => mockCubit.state).thenReturn(MyOrdersState(orders: [tOrder])); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BlocProvider.value( + value: mockCubit, + child: const OrdersListView(), + ), + ), + ), + ); + + expect(find.byType(OrderCard), findsOneWidget); + expect(find.text('# 1'), findsOneWidget); + }); + }); +} diff --git a/test/features/my_orders/presentation/widgets/section_label_test.dart b/test/features/my_orders/presentation/widgets/section_label_test.dart new file mode 100644 index 0000000..60ff92f --- /dev/null +++ b/test/features/my_orders/presentation/widgets/section_label_test.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/section_lable.dart'; + +void main() { + testWidgets('SectionLabel renders correctly with given text', ( + WidgetTester tester, + ) async { + const testLabel = 'Test Label'; + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: SectionLabel(label: testLabel)), + ), + ); + + expect(find.text(testLabel), findsOneWidget); + }); +} diff --git a/test/features/my_orders/presentation/widgets/summary_card_test.dart b/test/features/my_orders/presentation/widgets/summary_card_test.dart new file mode 100644 index 0000000..7c3baa8 --- /dev/null +++ b/test/features/my_orders/presentation/widgets/summary_card_test.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/summary_card.dart'; + +void main() { + testWidgets('SummaryCard renders correctly and handles tap', ( + WidgetTester tester, + ) async { + bool tapped = false; + const title = 'Cancelled'; + const count = '5'; + const icon = Icons.cancel; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SummaryCard( + title: title, + count: count, + icon: icon, + color: Colors.red, + onTap: () => tapped = true, + ), + ), + ), + ); + + expect(find.text(title), findsOneWidget); + expect(find.text(count), findsOneWidget); + expect(find.byIcon(icon), findsOneWidget); + + await tester.tap(find.byType(SummaryCard)); + expect(tapped, true); + }); +} diff --git a/test/features/my_orders/presentation/widgets/summary_row_test.dart b/test/features/my_orders/presentation/widgets/summary_row_test.dart new file mode 100644 index 0000000..41c3d53 --- /dev/null +++ b/test/features/my_orders/presentation/widgets/summary_row_test.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/my_orders/presentation/widgets/summary_row.dart'; + +void main() { + testWidgets('SummaryRow renders correctly with given label and value', ( + WidgetTester tester, + ) async { + const label = 'Total'; + const value = 'Egp 3000'; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: SummaryRow(label: label, value: value), + ), + ), + ); + + expect(find.text(label), findsOneWidget); + expect(find.text(value), findsOneWidget); + expect(find.byType(Container), findsOneWidget); + }); +} diff --git a/web/firebase-messaging-sw.js b/web/firebase-messaging-sw.js new file mode 100644 index 0000000..32d89e8 --- /dev/null +++ b/web/firebase-messaging-sw.js @@ -0,0 +1,25 @@ +importScripts("https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js"); +importScripts("https://www.gstatic.com/firebasejs/8.10.0/firebase-messaging.js"); + +firebase.initializeApp({ + apiKey: "AIzaSyDKWdkFjeKkEAfKFrMO2svs48t2d9OqRGw", + appId: "1:725835190067:web:86225b1572d53a90e53846", + messagingSenderId: "725835190067", + projectId: "elevate-flower-app", + authDomain: "elevate-flower-app.firebaseapp.com", + storageBucket: "elevate-flower-app.firebasestorage.app" +}); + +const messaging = firebase.messaging(); + +messaging.onBackgroundMessage(function(payload) { + console.log('[firebase-messaging-sw.js] Received background message ', payload); + const notificationTitle = payload.notification.title; + const notificationOptions = { + body: payload.notification.body, + icon: '/icons/Icon-192.png' + }; + + self.registration.showNotification(notificationTitle, + notificationOptions); +}); 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