diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 47c89f1..fa195a6 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,5 +1,8 @@ plugins { id("com.android.application") + // START: FlutterFire Configuration + id("com.google.gms.google-services") + // END: FlutterFire Configuration id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..3e2e605 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,48 @@ +{ + "project_info": { + "project_number": "795578737478", + "project_id": "fir-app-distribution-33bfe", + "storage_bucket": "fir-app-distribution-33bfe.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:795578737478:android:c344d0b351e151d0e1bb47", + "android_client_info": { + "package_name": "com.example.elevate_ecommerce_app" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyC_PztdFpXnsft2wNBj_3DfkQELDcY4cgQ" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:795578737478:android:0f68939309ee7dc5e1bb47", + "android_client_info": { + "package_name": "com.example.elevate_tracking_app" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyC_PztdFpXnsft2wNBj_3DfkQELDcY4cgQ" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index cef2e02..99d2655 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,15 @@ + + + + + + login(@Body() LoginRequest loginRequest); -} + + @GET(EndPoints.orders) + Future getOrders(@Query("page") int? page); + + @GET(EndPoints.driverOrders) + Future getDriverOrders(); + + @PUT("${EndPoints.startOrder}/{orderId}") + Future startOrder(@Path("orderId") String orderId); +} \ No newline at end of file diff --git a/lib/api/client/firebase_module.dart b/lib/api/client/firebase_module.dart new file mode 100644 index 0000000..e7ebf30 --- /dev/null +++ b/lib/api/client/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/api/data_source/orders_remote_data_source_impl.dart b/lib/api/data_source/orders_remote_data_source_impl.dart new file mode 100644 index 0000000..20a611c --- /dev/null +++ b/lib/api/data_source/orders_remote_data_source_impl.dart @@ -0,0 +1,78 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:elevate_tracking_app/api/client/api_client.dart'; +import 'package:elevate_tracking_app/api/mapper/orders_mapper.dart'; +import 'package:elevate_tracking_app/core/api_result/api_result.dart'; +import 'package:elevate_tracking_app/core/api_result/safe_api_call.dart'; +import 'package:elevate_tracking_app/domain/entites/order_firestore_entity.dart'; +import 'package:elevate_tracking_app/domain/entites/start_order_entity.dart'; +import 'package:injectable/injectable.dart'; + +import '../../data/data_source/orders_remote_data_source.dart'; +import '../../domain/entites/pending_orders_entity.dart'; + +@Injectable(as: OrdersRemoteDataSource) +class OrdersRemoteDataSourceImpl implements OrdersRemoteDataSource { + final ApiClient _apiClient; + final FirebaseFirestore _firestore; + + OrdersRemoteDataSourceImpl(this._apiClient, this._firestore); + + @override + Future> getOrders(int? page) { + return safeApiCall( + () => _apiClient.getOrders(page), + (response) => response.toEntity(), + ); + } + + @override + Future> startOrder(String orderId) async { + return safeApiCall(() => _apiClient.startOrder(orderId), (response) { + return response.orders!.toEntity(); + }); + } + + @override + Future> addFirestoreOrder(OrderFirestoreEntity order) async { + try { + final collection = _firestore.collection('orders'); + await collection + .doc(order.order?.id ?? "") + .set(order.toMap(), SetOptions(merge: true)); + return ApiSuccessResult(true); + } catch (e) { + return ApiErrorResult(e); + } + } + + @override + Future> getFirestoreOrder( + String orderId, + ) async { + try { + final doc = await _firestore.collection('orders').doc(orderId).get(); + if (doc.exists) { + return ApiSuccessResult(OrderFirestoreEntity.fromMap(doc.data()!)); + } else { + return ApiErrorResult("Document does not exist"); + } + } catch (e) { + return ApiErrorResult(e); + } + } + + @override + Stream> streamFirestoreOrder(String orderId) async* { + try { + await for (final doc in _firestore.collection('orders').doc(orderId).snapshots()) { + if (doc.exists) { + yield ApiSuccessResult(OrderFirestoreEntity.fromMap(doc.data()!)); + } else { + yield ApiErrorResult("Document does not exist"); + } + } + } catch (e) { + yield ApiErrorResult(e.toString()); + } + } +} diff --git a/lib/api/mapper/orders_mapper.dart b/lib/api/mapper/orders_mapper.dart new file mode 100644 index 0000000..b491b0d --- /dev/null +++ b/lib/api/mapper/orders_mapper.dart @@ -0,0 +1,183 @@ +import 'package:elevate_tracking_app/api/models/driver_order_dto.dart'; +import 'package:elevate_tracking_app/domain/entites/driver_order_entity.dart'; + +import '../../domain/entites/meta_data_entity.dart'; +import '../../domain/entites/order_entity.dart'; +import '../../domain/entites/order_item_entity.dart'; +import '../../domain/entites/pending_orders_entity.dart'; +import '../../domain/entites/product_entity.dart'; +import '../../domain/entites/shipping_address_entity.dart'; +import '../../domain/entites/start_order_entity.dart'; +import '../../domain/entites/start_order_items_entity.dart'; +import '../../domain/entites/store_entity.dart'; +import '../../domain/entites/user_entity.dart'; +import '../models/meta_data_dto.dart'; +import '../models/order_dto.dart'; +import '../models/order_item_dto.dart'; +import '../models/product_dto.dart'; +import '../models/responses/orders/orders_response.dart'; +import '../models/shipping_address_dto.dart'; +import '../models/start_order_dto.dart'; +import '../models/start_order_items_dto.dart'; +import '../models/store_dto.dart'; +import '../models/user_dto.dart'; + +extension DriverOrderMapper on DriverOrderDTO { + DriverOrderEntity toEntity() { + return DriverOrderEntity( + id: id, + driver: driver, + order: order?.toEntity(), + v: v, + createdAt: createdAt, + updatedAt: updatedAt, + store: store?.toEntity(), + ); + } +} + +extension OrderMapper on OrderDTO { + OrderEntity toEntity() { + return OrderEntity( + id: id, + user: user?.toEntity(), + orderItems: orderItems?.map((dto) => dto.toEntity()).toList(), + totalPrice: totalPrice, + shippingAddress: shippingAddress?.toEntity(), + paymentType: paymentType, + isPaid: isPaid, + isDelivered: isDelivered, + state: state, + createdAt: createdAt, + updatedAt: updatedAt, + orderNumber: orderNumber, + v: v, + store: store?.toEntity(), + ); + } +} + +extension OrderItemMapper on OrderItemDTO { + OrderItemEntity toEntity() { + return OrderItemEntity( + product: product?.toEntity(), + price: price, + quantity: quantity, + id: id, + ); + } +} + +extension ProductMapper on ProductDTO { + ProductEntity toEntity() { + return ProductEntity( + id: id, + title: title, + slug: slug, + description: description, + imgCover: imgCover, + images: images, + price: price, + priceAfterDiscount: priceAfterDiscount, + quantity: quantity, + category: category, + occasion: occasion, + createdAt: createdAt, + updatedAt: updatedAt, + v: v, + isSuperAdmin: isSuperAdmin, + sold: sold, + rateAvg: rateAvg, + rateCount: rateCount, + ); + } +} + +extension StoreMapper on StoreDTO { + StoreEntity toEntity() { + return StoreEntity( + name: name, + image: image, + address: address, + phoneNumber: phoneNumber, + latLong: latLong, + ); + } +} + +extension UserMapper on UserDTO { + UserEntity toEntity() { + return UserEntity( + id: id, + firstName: firstName, + lastName: lastName, + email: email, + gender: gender, + phone: phone, + photo: photo, + passwordChangedAt: passwordChangedAt, + ); + } +} + +extension StartOrderMapper on StartOrderDto { + StartOrderEntity toEntity() { + return StartOrderEntity( + id: id, + user: user, + orderItems: orderItems?.map((dto) => dto.toEntity()).toList(), + totalPrice: totalPrice, + paymentType: paymentType, + isPaid: isPaid, + isDelivered: isDelivered, + state: state, + createdAt: createdAt, + updatedAt: updatedAt, + orderNumber: orderNumber, + v: v, + ); + } +} + +extension StartOrderItemsMapper on StartOrderItemsDto { + StartOrderItemsEntity toEntity() { + return StartOrderItemsEntity( + product: product, + price: price, + quantity: quantity, + id: id, + ); + } +} + +extension ShippingAddressMapper on ShippingAddressDTO { + ShippingAddressEntity toEntity() { + return ShippingAddressEntity( + street: street, + city: city, + phone: phone, + lat: lat, + long: long, + ); + } +} + +extension MetaDataMapper on MetaDataDTO { + MetaDataEntity toEntity() { + return MetaDataEntity( + currentPage: currentPage, + totalPages: totalPages, + totalItems: totalItems, + limit: limit, + ); + } +} + +extension OrderResponseMapper on OrdersResponse { + PendingOrdersEntity toEntity() { + return PendingOrdersEntity( + metadata: metadata?.toEntity(), + orders: orders?.map((dto) => dto.toEntity()).toList(), + ); + } +} \ No newline at end of file diff --git a/lib/api/models/driver_dto.dart b/lib/api/models/driver_dto.dart new file mode 100644 index 0000000..317e5ed --- /dev/null +++ b/lib/api/models/driver_dto.dart @@ -0,0 +1,71 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'driver_dto.g.dart'; + +@JsonSerializable() +class DriverDto extends Equatable { + @JsonKey(name: '_id') + final String? id; + final String? country; + final String? firstName; + final String? lastName; + final String? vehicleType; + final String? vehicleNumber; + final String? vehicleLicense; + @JsonKey(name: 'NID') + final String? nid; + @JsonKey(name: 'NIDImg') + final String? nidImg; + final String? email; + final String? gender; + final String? phone; + final String? photo; + final String? role; + final DateTime? createdAt; + + const DriverDto({ + this.id, + this.country, + this.firstName, + this.lastName, + this.vehicleType, + this.vehicleNumber, + this.vehicleLicense, + this.nid, + this.nidImg, + this.email, + this.gender, + this.phone, + this.photo, + this.role, + this.createdAt, + }); + + factory DriverDto.fromJson(Map json) { + return _$DriverDtoFromJson(json); + } + + Map toJson() => _$DriverDtoToJson(this); + + @override + List get props { + return [ + id, + country, + firstName, + lastName, + vehicleType, + vehicleNumber, + vehicleLicense, + nid, + nidImg, + email, + gender, + phone, + photo, + role, + createdAt, + ]; + } +} \ No newline at end of file diff --git a/lib/api/models/driver_order_dto.dart b/lib/api/models/driver_order_dto.dart new file mode 100644 index 0000000..311ca11 --- /dev/null +++ b/lib/api/models/driver_order_dto.dart @@ -0,0 +1,40 @@ +import 'package:elevate_tracking_app/api/models/order_dto.dart'; +import 'package:elevate_tracking_app/api/models/store_dto.dart'; +import 'package:json_annotation/json_annotation.dart'; +part 'driver_order_dto.g.dart'; + +@JsonSerializable() +class DriverOrderDTO { + @JsonKey(name: "_id") + final String? id; + @JsonKey(name: "driver") + final String? driver; + @JsonKey(name: "order") + final OrderDTO? order; + @JsonKey(name: "__v") + final int? v; + @JsonKey(name: "createdAt") + final String? createdAt; + @JsonKey(name: "updatedAt") + final String? updatedAt; + @JsonKey(name: "store") + final StoreDTO? store; + + DriverOrderDTO ({ + this.id, + this.driver, + this.order, + this.v, + this.createdAt, + this.updatedAt, + this.store, + }); + + factory DriverOrderDTO.fromJson(Map json) { + return _$DriverOrderDTOFromJson(json); + } + + Map toJson() { + return _$DriverOrderDTOToJson(this); + } +} \ No newline at end of file diff --git a/lib/api/models/meta_data_dto.dart b/lib/api/models/meta_data_dto.dart new file mode 100644 index 0000000..530b6eb --- /dev/null +++ b/lib/api/models/meta_data_dto.dart @@ -0,0 +1,29 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'meta_data_dto.g.dart'; + +@JsonSerializable() +class MetaDataDTO { + @JsonKey(name: "currentPage") + final int? currentPage; + @JsonKey(name: "totalPages") + final int? totalPages; + @JsonKey(name: "totalItems") + final int? totalItems; + @JsonKey(name: "limit") + final int? limit; + + MetaDataDTO ({ + this.currentPage, + this.totalPages, + this.totalItems, + this.limit, + }); + + factory MetaDataDTO.fromJson(Map json) { + return _$MetaDataDTOFromJson(json); + } + + Map toJson() { + return _$MetaDataDTOToJson(this); + } +} \ No newline at end of file diff --git a/lib/api/models/order_dto.dart b/lib/api/models/order_dto.dart new file mode 100644 index 0000000..eb27d0a --- /dev/null +++ b/lib/api/models/order_dto.dart @@ -0,0 +1,64 @@ +import 'package:elevate_tracking_app/api/models/shipping_address_dto.dart'; +import 'package:elevate_tracking_app/api/models/store_dto.dart'; +import 'package:elevate_tracking_app/api/models/user_dto.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import 'order_item_dto.dart'; +part 'order_dto.g.dart'; + +@JsonSerializable() +class OrderDTO { + @JsonKey(name: "_id") + final String? id; + @JsonKey(name: "user") + final UserDTO? user; + @JsonKey(name: "orderItems") + final List? orderItems; + @JsonKey(name: "totalPrice") + final int? totalPrice; + @JsonKey(name: "shippingAddress") + final ShippingAddressDTO? shippingAddress; + @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; + @JsonKey(name: "store") + final StoreDTO? store; + + OrderDTO ({ + this.id, + this.user, + this.orderItems, + this.totalPrice, + this.shippingAddress, + this.paymentType, + this.isPaid, + this.isDelivered, + this.state, + this.createdAt, + this.updatedAt, + this.orderNumber, + this.v, + this.store, + }); + + factory OrderDTO.fromJson(Map json) { + return _$OrderDTOFromJson(json); + } + + Map toJson() { + return _$OrderDTOToJson(this); + } +} \ No newline at end of file diff --git a/lib/api/models/order_item_dto.dart b/lib/api/models/order_item_dto.dart new file mode 100644 index 0000000..86cf565 --- /dev/null +++ b/lib/api/models/order_item_dto.dart @@ -0,0 +1,30 @@ +import 'package:elevate_tracking_app/api/models/product_dto.dart'; +import 'package:json_annotation/json_annotation.dart'; +part 'order_item_dto.g.dart'; + +@JsonSerializable() +class OrderItemDTO { + @JsonKey(name: "product") + final ProductDTO? product; + @JsonKey(name: "price") + final int? price; + @JsonKey(name: "quantity") + final int? quantity; + @JsonKey(name: "_id") + final String? id; + + OrderItemDTO ({ + this.product, + this.price, + this.quantity, + this.id, + }); + + factory OrderItemDTO.fromJson(Map json) { + return _$OrderItemDTOFromJson(json); + } + + Map toJson() { + return _$OrderItemDTOToJson(this); + } +} \ No newline at end of file diff --git a/lib/api/models/product_dto.dart b/lib/api/models/product_dto.dart new file mode 100644 index 0000000..c8e872b --- /dev/null +++ b/lib/api/models/product_dto.dart @@ -0,0 +1,71 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'product_dto.g.dart'; + +@JsonSerializable() +class ProductDTO { + @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 String? createdAt; + @JsonKey(name: "updatedAt") + final String? updatedAt; + @JsonKey(name: "__v") + final int? v; + @JsonKey(name: "isSuperAdmin") + final bool? isSuperAdmin; + @JsonKey(name: "sold") + final int? sold; + @JsonKey(name: "rateAvg") + final int? rateAvg; + @JsonKey(name: "rateCount") + final int? rateCount; + + ProductDTO ({ + 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.isSuperAdmin, + this.sold, + this.rateAvg, + this.rateCount, + }); + + factory ProductDTO.fromJson(Map json) { + return _$ProductDTOFromJson(json); + } + + Map toJson() { + return _$ProductDTOToJson(this); + } +} \ No newline at end of file diff --git a/lib/api/models/responses/orders/driver_orders_response.dart b/lib/api/models/responses/orders/driver_orders_response.dart new file mode 100644 index 0000000..7157547 --- /dev/null +++ b/lib/api/models/responses/orders/driver_orders_response.dart @@ -0,0 +1,45 @@ +import 'package:elevate_tracking_app/api/models/driver_order_dto.dart'; +import 'package:elevate_tracking_app/api/models/meta_data_dto.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'driver_orders_response.g.dart'; + +@JsonSerializable() +class DriverOrdersResponse { + @JsonKey(name: "message") + final String? message; + @JsonKey(name: "metadata") + final MetaDataDTO? metadata; + @JsonKey(name: "orders") + final List? orders; + + DriverOrdersResponse ({ + this.message, + this.metadata, + this.orders, + }); + + factory DriverOrdersResponse.fromJson(Map json) { + return _$DriverOrdersResponseFromJson(json); + } + + Map toJson() { + return _$DriverOrdersResponseToJson(this); + } +} + + + + + + + + + + + + + + + + diff --git a/lib/api/models/responses/orders/orders_response.dart b/lib/api/models/responses/orders/orders_response.dart new file mode 100644 index 0000000..916c810 --- /dev/null +++ b/lib/api/models/responses/orders/orders_response.dart @@ -0,0 +1,44 @@ +import 'package:elevate_tracking_app/api/models/order_dto.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../../meta_data_dto.dart'; + +part 'orders_response.g.dart'; + +@JsonSerializable() +class OrdersResponse { + @JsonKey(name: "message") + final String? message; + @JsonKey(name: "metadata") + final MetaDataDTO? metadata; + @JsonKey(name: "orders") + final List? orders; + + OrdersResponse ({ + this.message, + this.metadata, + this.orders, + }); + + factory OrdersResponse.fromJson(Map json) { + return _$OrdersResponseFromJson(json); + } + + Map toJson() { + return _$OrdersResponseToJson(this); + } +} + + + + + + + + + + + + + + diff --git a/lib/api/models/responses/orders/start_order_response.dart b/lib/api/models/responses/orders/start_order_response.dart new file mode 100644 index 0000000..a3ab5be --- /dev/null +++ b/lib/api/models/responses/orders/start_order_response.dart @@ -0,0 +1,29 @@ +import 'package:elevate_tracking_app/api/models/start_order_dto.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'start_order_response.g.dart'; + +@JsonSerializable() +class StartOrderResponse { + @JsonKey(name: "message") + final String? message; + @JsonKey(name: "orders") + final StartOrderDto? orders; + + StartOrderResponse ({ + this.message, + this.orders, + }); + + factory StartOrderResponse.fromJson(Map json) { + return _$StartOrderResponseFromJson(json); + } + + Map toJson() { + return _$StartOrderResponseToJson(this); + } +} + + + + diff --git a/lib/api/models/shipping_address_dto.dart b/lib/api/models/shipping_address_dto.dart new file mode 100644 index 0000000..84d4e8d --- /dev/null +++ b/lib/api/models/shipping_address_dto.dart @@ -0,0 +1,35 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'shipping_address_dto.g.dart'; + +@JsonSerializable() +class ShippingAddressDTO { + @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; + + ShippingAddressDTO ({ + this.street, + this.city, + this.phone, + this.lat, + this.long, + }); + + factory ShippingAddressDTO.fromJson(Map json) { + return _$ShippingAddressDTOFromJson(json); + } + + Map toJson() { + return _$ShippingAddressDTOToJson(this); + } +} + + diff --git a/lib/api/models/start_order_dto.dart b/lib/api/models/start_order_dto.dart new file mode 100644 index 0000000..0046a9f --- /dev/null +++ b/lib/api/models/start_order_dto.dart @@ -0,0 +1,55 @@ +import 'package:elevate_tracking_app/api/models/start_order_items_dto.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'start_order_dto.g.dart'; + +@JsonSerializable() +class StartOrderDto { + @JsonKey(name: "_id") + final String? id; + @JsonKey(name: "user") + final String? 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 String? createdAt; + @JsonKey(name: "updatedAt") + final String? updatedAt; + @JsonKey(name: "orderNumber") + final String? orderNumber; + @JsonKey(name: "__v") + final int? v; + + StartOrderDto({ + this.id, + this.user, + this.orderItems, + this.totalPrice, + this.paymentType, + this.isPaid, + this.isDelivered, + this.state, + this.createdAt, + this.updatedAt, + this.orderNumber, + this.v, + }); + + factory StartOrderDto.fromJson(Map json) { + return _$StartOrderDtoFromJson(json); + } + + Map toJson() { + return _$StartOrderDtoToJson(this); + } +} diff --git a/lib/api/models/start_order_items_dto.dart b/lib/api/models/start_order_items_dto.dart new file mode 100644 index 0000000..19f4dfd --- /dev/null +++ b/lib/api/models/start_order_items_dto.dart @@ -0,0 +1,25 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'start_order_items_dto.g.dart'; + +@JsonSerializable() +class StartOrderItemsDto { + @JsonKey(name: "product") + final String? product; + @JsonKey(name: "price") + final int? price; + @JsonKey(name: "quantity") + final int? quantity; + @JsonKey(name: "_id") + final String? id; + + StartOrderItemsDto({this.product, this.price, this.quantity, this.id}); + + factory StartOrderItemsDto.fromJson(Map json) { + return _$StartOrderItemsDtoFromJson(json); + } + + Map toJson() { + return _$StartOrderItemsDtoToJson(this); + } +} diff --git a/lib/api/models/store_dto.dart b/lib/api/models/store_dto.dart new file mode 100644 index 0000000..80eeadf --- /dev/null +++ b/lib/api/models/store_dto.dart @@ -0,0 +1,32 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'store_dto.g.dart'; + +@JsonSerializable() +class StoreDTO { + @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; + + StoreDTO ({ + this.name, + this.image, + this.address, + this.phoneNumber, + this.latLong, + }); + + factory StoreDTO.fromJson(Map json) { + return _$StoreDTOFromJson(json); + } + + Map toJson() { + return _$StoreDTOToJson(this); + } +} \ No newline at end of file diff --git a/lib/api/models/user_dto.dart b/lib/api/models/user_dto.dart new file mode 100644 index 0000000..738f139 --- /dev/null +++ b/lib/api/models/user_dto.dart @@ -0,0 +1,41 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'user_dto.g.dart'; + +@JsonSerializable() +class UserDTO { + @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; + + UserDTO ({ + this.id, + this.firstName, + this.lastName, + this.email, + this.gender, + this.phone, + this.photo, + this.passwordChangedAt, + }); + + factory UserDTO.fromJson(Map json) { + return _$UserDTOFromJson(json); + } + + Map toJson() { + return _$UserDTOToJson(this); + } +} \ No newline at end of file diff --git a/lib/core/constants/app_icons.dart b/lib/core/constants/app_icons.dart index 47cc709..64b945f 100644 --- a/lib/core/constants/app_icons.dart +++ b/lib/core/constants/app_icons.dart @@ -49,5 +49,7 @@ abstract class AppIcons { static const String iconWhatsapp = "assets/icons/whatsapp.png"; static const String iconX1 = "assets/icons/X1.png"; + + static const String iconMarker = "assets/icons/markerIcon.png"; } diff --git a/lib/core/constants/app_images.dart b/lib/core/constants/app_images.dart index 548ba71..f28b6a8 100644 --- a/lib/core/constants/app_images.dart +++ b/lib/core/constants/app_images.dart @@ -21,5 +21,7 @@ abstract class AppImages { static const String imageUserPhoto = "assets/images/user_photo.png"; static const String imageYourLocation = "assets/images/your_location.png"; + + static const String imageMotorcycleDelivery = "assets/images/motorcycle_delivery.png"; } diff --git a/lib/core/constants/app_theme.dart b/lib/core/constants/app_theme.dart index 9b4a597..897405a 100644 --- a/lib/core/constants/app_theme.dart +++ b/lib/core/constants/app_theme.dart @@ -91,7 +91,7 @@ abstract class AppTheme { disabledBackgroundColor: AppColors.black[30], foregroundColor: AppColors.white, textStyle: getTextStyle(fontSize: 16.sp, fontWeight: FontWeight.w500), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(100)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(100)) ), ), diff --git a/lib/core/constants/end_points.dart b/lib/core/constants/end_points.dart index e8e8db8..35eb55d 100644 --- a/lib/core/constants/end_points.dart +++ b/lib/core/constants/end_points.dart @@ -1,3 +1,6 @@ abstract class EndPoints { static const login = "api/v1/drivers/signin"; + static const orders = "api/v1/orders/pending-orders"; + static const driverOrders = "api/v1/orders/driver-orders"; + static const startOrder = "api/v1/orders/start"; } diff --git a/lib/core/constants/widgets_keys.dart b/lib/core/constants/widgets_keys.dart index 4f8effc..eeb2ae7 100644 --- a/lib/core/constants/widgets_keys.dart +++ b/lib/core/constants/widgets_keys.dart @@ -16,4 +16,5 @@ abstract final class WidgetsKeys { "login_screen_forgot_password_button"; static const String kLoginScreenContinueButton = "login_screen_continue_button"; + static const String kHomePageListView = "home_page_list_view"; } diff --git a/lib/core/custom_widget/custom_cached_network_image.dart b/lib/core/custom_widget/custom_cached_network_image.dart new file mode 100644 index 0000000..65fe044 --- /dev/null +++ b/lib/core/custom_widget/custom_cached_network_image.dart @@ -0,0 +1,35 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; + +class CustomCachedNetworkImage extends StatelessWidget { + final String? imageUrl; + final double? height; + final double? width; + final BoxFit? fit; + final Color? color; + + const CustomCachedNetworkImage({ + super.key, + this.imageUrl, + this.height, + this.width, + this.fit, + this.color, + }); + + @override + Widget build(BuildContext context) { + return imageUrl != null + ? CachedNetworkImage( + height: height, + width: width, + fit: fit, + imageUrl: imageUrl!, + color: color, + placeholder: (context, url) => + const Center(child: CircularProgressIndicator()), + errorWidget: (context, url, error) => const Icon(Icons.error), + ) + : const Icon(Icons.error); + } +} diff --git a/lib/core/extensions/text_extensions.dart b/lib/core/extensions/text_extensions.dart new file mode 100644 index 0000000..dc356e2 --- /dev/null +++ b/lib/core/extensions/text_extensions.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +extension TextStyleExtensions on TextStyle { + TextStyle withBodySmall(BuildContext context) { + return Theme.of(context).textTheme.bodySmall!; + } + + TextStyle withBodyMedium(BuildContext context) { + return Theme.of(context).textTheme.bodyMedium!; + } + + TextStyle withBodyLarge(BuildContext context) { + return Theme.of(context).textTheme.bodyLarge!; + } + + TextStyle withHeadlineMedium(BuildContext context) { + return Theme.of(context).textTheme.headlineMedium!; + } + + TextStyle withHeadlineSmall(BuildContext context) { + return Theme.of(context).textTheme.headlineSmall!; + } + + TextStyle withLabelSmall(BuildContext context) { + return Theme.of(context).textTheme.labelSmall!; + } +} \ No newline at end of file diff --git a/lib/core/extensions/widget_extensions.dart b/lib/core/extensions/widget_extensions.dart new file mode 100644 index 0000000..41c45a5 --- /dev/null +++ b/lib/core/extensions/widget_extensions.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; + +extension AddPaddingExtension on Widget { + Widget withPadding({ + double? all, + double? top, + double? bottom, + double? left, + double? right, + }) { + return Padding( + padding: all != null + ? EdgeInsets.all(all) + : EdgeInsets.only( + top: top ?? 0, + bottom: bottom ?? 0, + left: left ?? 0, + right: right ?? 0, + ), + child: this, + ); + } + + Widget withSymmetricPadding({double? horizontal, double? vertical}) { + return Padding( + padding: EdgeInsets.symmetric( + horizontal: horizontal ?? 0, + vertical: vertical ?? 0, + ), + child: this, + ); + } + + Widget withMargin({ + double? all, + double? top, + double? bottom, + double? left, + double? right, + }) { + return Container( + margin: all != null + ? EdgeInsets.all(all) + : EdgeInsets.only( + top: top ?? 0, + bottom: bottom ?? 0, + left: left ?? 0, + right: right ?? 0, + ), + child: this, + ); + } + + Widget withSymmetricMargin({double? horizontal, double? vertical}) { + return Container( + margin: EdgeInsets.symmetric( + horizontal: horizontal ?? 0, + vertical: vertical ?? 0, + ), + child: this, + ); + } + + Widget withVisibility({required bool visible}) { + return Visibility(visible: visible, child: this); + } +} diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 592e649..47e65cc 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -1,6 +1,6 @@ import 'package:elevate_tracking_app/core/router/route_names.dart'; import 'package:elevate_tracking_app/presentation/application_approved/views/screen/application_approved_screen.dart'; -import 'package:elevate_tracking_app/presentation/home/views/screen/home_screen.dart'; +import 'package:elevate_tracking_app/presentation/location/view/screen/location_screen.dart'; import 'package:elevate_tracking_app/presentation/main_home/views/screen/main_home_screen.dart'; import 'package:elevate_tracking_app/presentation/onboarding/views/screen/onboarding_screen.dart'; import 'package:elevate_tracking_app/presentation/orders/views/screen/orders_screen.dart'; @@ -26,10 +26,6 @@ abstract class AppRouter { path: RouteNames.mainHome, builder: (context, state) => MainHomeScreen(), ), - GoRoute( - path: RouteNames.home, - builder: (context, state) => const HomeScreen(), - ), GoRoute( path: RouteNames.orders, builder: (context, state) => const OrdersScreen(), @@ -38,12 +34,17 @@ abstract class AppRouter { path: RouteNames.profile, builder: (context, state) => const ProfileScreen(), ), + GoRoute( + path: RouteNames.locationScreen, + builder: (context, state) => const LocationScreen(), + ), ]; static GoRouter router(String? token) { return GoRouter( - initialLocation: token == null ? RouteNames.onboarding : RouteNames.home, + initialLocation: RouteNames.locationScreen, + //initialLocation: token == null ? RouteNames.onboarding : RouteNames.mainHome, routes: routes, ); } diff --git a/lib/core/router/route_names.dart b/lib/core/router/route_names.dart index bb7d3d0..9d51cbb 100644 --- a/lib/core/router/route_names.dart +++ b/lib/core/router/route_names.dart @@ -3,7 +3,6 @@ abstract class RouteNames { static const String login = "/login"; static const String apply = "/apply"; static const String mainHome = "/mainHome"; - static const String home = "/home"; static const String orders = "/orders"; static const String profile = "/profile"; static const String editProfile = "/editProfile"; @@ -13,4 +12,5 @@ abstract class RouteNames { static const String logout = "/logout"; static const String onboarding = "/onboarding"; static const String applicationApproved = "/applicationApproved"; + static const String locationScreen = "/locationScreen"; } diff --git a/lib/core/utils/location_manager.dart b/lib/core/utils/location_manager.dart index 963dab6..6814f73 100644 --- a/lib/core/utils/location_manager.dart +++ b/lib/core/utils/location_manager.dart @@ -2,7 +2,6 @@ import 'package:elevate_tracking_app/generated/l10n.dart'; import 'package:geolocator/geolocator.dart'; class LocationManager { - static Future _checkServiceEnabled() async { final serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { @@ -31,5 +30,9 @@ class LocationManager { await _checkPermission(); return await Geolocator.getCurrentPosition(); } -} + static Future checkLocationPermission() async { + await _checkServiceEnabled(); + await _checkPermission(); + } +} diff --git a/lib/data/data_source/orders_remote_data_source.dart b/lib/data/data_source/orders_remote_data_source.dart new file mode 100644 index 0000000..4adf43a --- /dev/null +++ b/lib/data/data_source/orders_remote_data_source.dart @@ -0,0 +1,16 @@ +import '../../core/api_result/api_result.dart'; +import '../../domain/entites/order_firestore_entity.dart'; +import '../../domain/entites/pending_orders_entity.dart'; +import '../../domain/entites/start_order_entity.dart'; + +abstract interface class OrdersRemoteDataSource { + Future> getOrders(int? page); + + Future> startOrder(String orderId); + + Future> addFirestoreOrder(OrderFirestoreEntity order); + + Future> getFirestoreOrder(String orderId); + + Stream> streamFirestoreOrder(String orderId); +} diff --git a/lib/data/repo/orders_repo_impl.dart b/lib/data/repo/orders_repo_impl.dart new file mode 100644 index 0000000..2f24200 --- /dev/null +++ b/lib/data/repo/orders_repo_impl.dart @@ -0,0 +1,40 @@ +import 'package:elevate_tracking_app/core/api_result/api_result.dart'; +import 'package:elevate_tracking_app/domain/entites/order_firestore_entity.dart'; +import 'package:elevate_tracking_app/domain/entites/start_order_entity.dart'; +import 'package:injectable/injectable.dart'; + +import '../../domain/entites/pending_orders_entity.dart'; +import '../../domain/repo/orders_repo.dart'; +import '../data_source/orders_remote_data_source.dart'; + +@Injectable(as: OrdersRepo) +class OrdersRepoImpl implements OrdersRepo { + final OrdersRemoteDataSource _ordersRemoteDataSource; + + OrdersRepoImpl(this._ordersRemoteDataSource); + + @override + Future> getOrders(int? page) { + return _ordersRemoteDataSource.getOrders(page); + } + + @override + Future> startOrder(String orderId) { + return _ordersRemoteDataSource.startOrder(orderId); + } + + @override + Future> addFirestoreOrder(OrderFirestoreEntity order) { + return _ordersRemoteDataSource.addFirestoreOrder(order); + } + + @override + Future> getFirestoreOrder(String orderId) { + return _ordersRemoteDataSource.getFirestoreOrder(orderId); + } + + @override + Stream> streamFirestoreOrder(String orderId) { + return _ordersRemoteDataSource.streamFirestoreOrder(orderId); + } +} diff --git a/lib/domain/entites/driver_entity.dart b/lib/domain/entites/driver_entity.dart new file mode 100644 index 0000000..1dcddba --- /dev/null +++ b/lib/domain/entites/driver_entity.dart @@ -0,0 +1,99 @@ +import 'package:equatable/equatable.dart'; + +class DriverEntity extends Equatable { + + final String? id; + final String? country; + final String? firstName; + final String? lastName; + final String? vehicleType; + final String? vehicleNumber; + final String? vehicleLicense; + final String? nid; + final String? nidImg; + final String? email; + final String? gender; + final String? phone; + final String? photo; + final String? role; + final String? createdAt; + + const DriverEntity({ + this.id, + this.country, + this.firstName, + this.lastName, + this.vehicleType, + this.vehicleNumber, + this.vehicleLicense, + this.nid, + this.nidImg, + this.email, + this.gender, + this.phone, + this.photo, + this.role, + this.createdAt, + }); + + Map toMap() { + return { + 'id': id, + 'country': country, + 'firstName': firstName, + 'lastName': lastName, + 'vehicleType': vehicleType, + 'vehicleNumber': vehicleNumber, + 'vehicleLicense': vehicleLicense, + 'nid': nid, + 'nidImg': nidImg, + 'email': email, + 'gender': gender, + 'phone': phone, + 'photo': photo, + 'role': role, + 'createdAt': createdAt, + }; + } + + factory DriverEntity.fromMap(Map map) { + return DriverEntity( + id: map['id'] as String?, + country: map['country'] as String?, + firstName: map['firstName'] as String?, + lastName: map['lastName'] as String?, + vehicleType: map['vehicleType'] as String?, + vehicleNumber: map['vehicleNumber'] as String?, + vehicleLicense: map['vehicleLicense'] as String?, + nid: map['nid'] as String?, + nidImg: map['nidImg'] as String?, + email: map['email'] as String?, + gender: map['gender'] as String?, + phone: map['phone'] as String?, + photo: map['photo'] as String?, + role: map['role'] as String?, + createdAt: map['createdAt'] as String?, + ); + } + + @override + List get props { + return [ + id, + country, + firstName, + lastName, + vehicleType, + vehicleNumber, + vehicleLicense, + nid, + nidImg, + email, + gender, + phone, + photo, + role, + createdAt, + ]; + } +} \ No newline at end of file diff --git a/lib/domain/entites/driver_order_entity.dart b/lib/domain/entites/driver_order_entity.dart new file mode 100644 index 0000000..4ac542e --- /dev/null +++ b/lib/domain/entites/driver_order_entity.dart @@ -0,0 +1,27 @@ +import 'package:elevate_tracking_app/domain/entites/order_entity.dart'; +import 'package:elevate_tracking_app/domain/entites/store_entity.dart'; +import 'package:equatable/equatable.dart'; + +class DriverOrderEntity extends Equatable { + final String? id; + final String? driver; + final OrderEntity? order; + final int? v; + final String? createdAt; + final String? updatedAt; + final StoreEntity? store; + + const DriverOrderEntity({ + this.id, + this.driver, + this.order, + this.v, + this.createdAt, + this.updatedAt, + this.store, + }); + + @override + List get props => + [id, driver, order, v, createdAt, updatedAt, store,]; +} diff --git a/lib/domain/entites/live_location_entity.dart b/lib/domain/entites/live_location_entity.dart new file mode 100644 index 0000000..d24846e --- /dev/null +++ b/lib/domain/entites/live_location_entity.dart @@ -0,0 +1,32 @@ +import 'package:equatable/equatable.dart'; + +class LiveLocationEntity extends Equatable { + final String? lat; + final String? long; + final String? address; + + const LiveLocationEntity({ + this.lat, + this.long, + this.address, + }); + + Map toMap() { + return { + 'lat': lat, + 'long': long, + 'address': address, + }; + } + + factory LiveLocationEntity.fromMap(Map map) { + return LiveLocationEntity( + lat: map['lat'] as String, + long: map['long'] as String, + address: map['address'] as String, + ); + } + + @override + List get props => [lat, long, address]; +} \ No newline at end of file diff --git a/lib/domain/entites/meta_data_entity.dart b/lib/domain/entites/meta_data_entity.dart new file mode 100644 index 0000000..e630d0b --- /dev/null +++ b/lib/domain/entites/meta_data_entity.dart @@ -0,0 +1,18 @@ +import 'package:equatable/equatable.dart'; + +class MetaDataEntity extends Equatable { + final int? currentPage; + final int? totalPages; + final int? totalItems; + final int? limit; + + const MetaDataEntity({ + this.currentPage, + this.totalPages, + this.totalItems, + this.limit, + }); + + @override + List get props => [currentPage, totalPages, totalItems, limit]; +} diff --git a/lib/domain/entites/order_entity.dart b/lib/domain/entites/order_entity.dart new file mode 100644 index 0000000..8a1ab15 --- /dev/null +++ b/lib/domain/entites/order_entity.dart @@ -0,0 +1,109 @@ +import 'package:elevate_tracking_app/domain/entites/order_item_entity.dart'; +import 'package:elevate_tracking_app/domain/entites/shipping_address_entity.dart'; +import 'package:elevate_tracking_app/domain/entites/store_entity.dart'; +import 'package:elevate_tracking_app/domain/entites/user_entity.dart'; +import 'package:equatable/equatable.dart'; + +class OrderEntity extends Equatable { + final String? id; + final UserEntity? user; + final List? orderItems; + final int? totalPrice; + final ShippingAddressEntity? shippingAddress; + final String? paymentType; + final bool? isPaid; + final bool? isDelivered; + final String? state; + final String? createdAt; + final String? updatedAt; + final String? orderNumber; + final int? v; + final StoreEntity? store; + + const OrderEntity({ + this.id, + this.user, + this.orderItems, + this.totalPrice, + this.shippingAddress, + this.paymentType, + this.isPaid, + this.isDelivered, + this.state, + this.createdAt, + this.updatedAt, + this.orderNumber, + this.v, + this.store, + }); + + Map toMap() { + return { + 'id': id, + 'user': user?.toMap(), + 'orderItems': orderItems?.map((item) => item.toMap()).toList(), + 'totalPrice': totalPrice, + 'shippingAddress': shippingAddress?.toMap(), + 'paymentType': paymentType, + 'isPaid': isPaid, + 'isDelivered': isDelivered, + 'state': state, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + 'orderNumber': orderNumber, + 'v': v, + 'store': store?.toMap(), + }; + } + + factory OrderEntity.fromMap(Map map) { + return OrderEntity( + id: map['id'] as String?, + user: UserEntity.fromMap(Map.from(map['user'])), + orderItems: List>.from( + map['orderItems'], + ).map((item) => OrderItemEntity.fromMap(item)).toList(), + totalPrice: map['totalPrice'] as int?, + shippingAddress: map['shippingAddress'] != null + ? ShippingAddressEntity.fromMap( + Map.from(map['shippingAddress']), + ) + : _fakeShippingAddress, + paymentType: map['paymentType'] as String?, + isPaid: map['isPaid'] as bool?, + isDelivered: map['isDelivered'] as bool?, + state: map['state'] as String?, + createdAt: map['createdAt'] as String?, + updatedAt: map['updatedAt'] as String?, + orderNumber: map['orderNumber'] as String?, + v: map['v'] as int?, + store: StoreEntity.fromMap(Map.from(map['store'])), + ); + } + + static final _fakeShippingAddress = const ShippingAddressEntity( + street: '456 Oak Avenue', + city: 'Metropolis', + phone: '555-555-5555', + lat: '34.0522', + long: '-118.2437', + ); + + @override + List get props => [ + id, + user, + orderItems, + totalPrice, + shippingAddress, + paymentType, + isPaid, + isDelivered, + state, + createdAt, + updatedAt, + orderNumber, + v, + store, + ]; +} diff --git a/lib/domain/entites/order_firestore_entity.dart b/lib/domain/entites/order_firestore_entity.dart new file mode 100644 index 0000000..87aabe1 --- /dev/null +++ b/lib/domain/entites/order_firestore_entity.dart @@ -0,0 +1,46 @@ +import 'package:elevate_tracking_app/domain/entites/driver_entity.dart'; +import 'package:elevate_tracking_app/domain/entites/live_location_entity.dart'; +import 'package:equatable/equatable.dart'; + +import 'order_entity.dart'; + +class OrderFirestoreEntity extends Equatable { + final DriverEntity? driver; + final OrderEntity? order; + final LiveLocationEntity? location; + + const OrderFirestoreEntity({this.driver, this.order, this.location}); + + Map toMap() { + return { + 'driver': driver?.toMap(), + 'order': order?.toMap(), + 'location': location?.toMap(), + }; + } + + factory OrderFirestoreEntity.fromMap(Map map) { + return OrderFirestoreEntity( + driver: DriverEntity.fromMap(Map.from(map['driver'])), + order: OrderEntity.fromMap(Map.from(map['order'])), + location: LiveLocationEntity.fromMap( + Map.from(map['location']), + ), + ); + } + + OrderFirestoreEntity copyWith({ + DriverEntity? driver, + OrderEntity? order, + LiveLocationEntity? location, + }) { + return OrderFirestoreEntity( + driver: driver ?? this.driver, + order: order ?? this.order, + location: location ?? this.location, + ); + } + + @override + List get props => [driver, order, location]; +} diff --git a/lib/domain/entites/order_item_entity.dart b/lib/domain/entites/order_item_entity.dart new file mode 100644 index 0000000..31eb8c9 --- /dev/null +++ b/lib/domain/entites/order_item_entity.dart @@ -0,0 +1,32 @@ +import 'package:elevate_tracking_app/domain/entites/product_entity.dart'; +import 'package:equatable/equatable.dart'; + +class OrderItemEntity extends Equatable { + final ProductEntity? product; + final int? price; + final int? quantity; + final String? id; + + const OrderItemEntity({this.product, this.price, this.quantity, this.id}); + + Map toMap() { + return { + 'product': product?.toMap(), + 'price': price, + 'quantity': quantity, + 'id': id, + }; + } + + factory OrderItemEntity.fromMap(Map map) { + return OrderItemEntity( + product: ProductEntity.fromMap(Map.from(map['product'])), + price: map['price'] as int?, + quantity: map['quantity'] as int?, + id: map['id'] as String?, + ); + } + + @override + List get props => [product, price, quantity, id]; +} diff --git a/lib/domain/entites/pending_orders_entity.dart b/lib/domain/entites/pending_orders_entity.dart new file mode 100644 index 0000000..ad33ff2 --- /dev/null +++ b/lib/domain/entites/pending_orders_entity.dart @@ -0,0 +1,13 @@ +import 'package:elevate_tracking_app/domain/entites/meta_data_entity.dart'; +import 'package:elevate_tracking_app/domain/entites/order_entity.dart'; +import 'package:equatable/equatable.dart'; + +class PendingOrdersEntity extends Equatable { + final MetaDataEntity? metadata; + final List? orders; + + const PendingOrdersEntity({this.metadata, this.orders}); + + @override + List get props => [metadata, orders]; +} diff --git a/lib/domain/entites/product_entity.dart b/lib/domain/entites/product_entity.dart new file mode 100644 index 0000000..db9dd02 --- /dev/null +++ b/lib/domain/entites/product_entity.dart @@ -0,0 +1,112 @@ +import 'package:equatable/equatable.dart'; + +class ProductEntity extends Equatable { + final String? id; + final String? title; + final String? slug; + final String? description; + final String? imgCover; + final List? images; + final int? price; + final int? priceAfterDiscount; + final int? quantity; + final String? category; + final String? occasion; + final String? createdAt; + final String? updatedAt; + final int? v; + final bool? isSuperAdmin; + final int? sold; + final int? rateAvg; + final int? rateCount; + + const ProductEntity({ + 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.isSuperAdmin, + this.sold, + this.rateAvg, + this.rateCount, + }); + + Map toMap() { + return { + 'id': id, + 'title': title, + 'slug': slug, + 'description': description, + 'imgCover': imgCover, + 'images': images, + 'price': price, + 'priceAfterDiscount': priceAfterDiscount, + 'quantity': quantity, + 'category': category, + 'occasion': occasion, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + 'v': v, + 'isSuperAdmin': isSuperAdmin, + 'sold': sold, + 'rateAvg': rateAvg, + 'rateCount': rateCount, + }; + } + + factory ProductEntity.fromMap(Map map) { + return ProductEntity( + id: map['id'] as String?, + title: map['title'] as String?, + slug: map['slug'] as String?, + description: map['description'] as String?, + imgCover: map['imgCover'] as String?, + images: List.from(map['images']), + price: map['price'] as int?, + priceAfterDiscount: map['priceAfterDiscount'] as int?, + quantity: map['quantity'] as int?, + category: map['category'] as String?, + occasion: map['occasion'] as String?, + createdAt: map['createdAt'] as String?, + updatedAt: map['updatedAt'] as String?, + v: map['v'] as int?, + isSuperAdmin: map['isSuperAdmin'] as bool?, + sold: map['sold'] as int?, + rateAvg: map['rateAvg'] as int?, + rateCount: map['rateCount'] as int?, + ); + } + + @override + List get props => + [ + id, + title, + slug, + description, + imgCover, + images, + price, + priceAfterDiscount, + quantity, + category, + occasion, + createdAt, + updatedAt, + v, + isSuperAdmin, + sold, + rateAvg, + rateCount, + ]; +} diff --git a/lib/domain/entites/shipping_address_entity.dart b/lib/domain/entites/shipping_address_entity.dart new file mode 100644 index 0000000..b06407f --- /dev/null +++ b/lib/domain/entites/shipping_address_entity.dart @@ -0,0 +1,40 @@ +import 'package:equatable/equatable.dart'; + +class ShippingAddressEntity extends Equatable { + final String? street; + final String? city; + final String? phone; + final String? lat; + final String? long; + + const ShippingAddressEntity({ + this.street, + this.city, + this.phone, + this.lat, + this.long, + }); + + Map toMap() { + return { + 'street': street, + 'city': city, + 'phone': phone, + 'lat': lat, + 'long': long, + }; + } + + factory ShippingAddressEntity.fromMap(Map map) { + return ShippingAddressEntity( + street: map['street'] as String?, + city: map['city'] as String?, + phone: map['phone'] as String?, + lat: map['lat'] as String?, + long: map['long'] as String?, + ); + } + + @override + List get props => [street, city, phone, lat, long]; +} diff --git a/lib/domain/entites/start_order_entity.dart b/lib/domain/entites/start_order_entity.dart new file mode 100644 index 0000000..6324a5d --- /dev/null +++ b/lib/domain/entites/start_order_entity.dart @@ -0,0 +1,49 @@ +import 'package:elevate_tracking_app/domain/entites/start_order_items_entity.dart'; +import 'package:equatable/equatable.dart'; + +class StartOrderEntity extends Equatable { + final String? id; + final String? user; + final List? orderItems; + final int? totalPrice; + final String? paymentType; + final bool? isPaid; + final bool? isDelivered; + final String? state; + final String? createdAt; + final String? updatedAt; + final String? orderNumber; + final int? v; + + const StartOrderEntity({ + this.id, + this.user, + this.orderItems, + this.totalPrice, + this.paymentType, + this.isPaid, + this.isDelivered, + this.state, + this.createdAt, + this.updatedAt, + this.orderNumber, + this.v, + }); + + @override + List get props => + [ + id, + user, + orderItems, + totalPrice, + paymentType, + isPaid, + isDelivered, + state, + createdAt, + updatedAt, + orderNumber, + v, + ]; +} diff --git a/lib/domain/entites/start_order_items_entity.dart b/lib/domain/entites/start_order_items_entity.dart new file mode 100644 index 0000000..9e9a5c7 --- /dev/null +++ b/lib/domain/entites/start_order_items_entity.dart @@ -0,0 +1,13 @@ +import 'package:equatable/equatable.dart'; + +class StartOrderItemsEntity extends Equatable { + final String? product; + final int? price; + final int? quantity; + final String? id; + + const StartOrderItemsEntity({this.product, this.price, this.quantity, this.id}); + + @override + List get props => [product, price, quantity, id]; +} diff --git a/lib/domain/entites/store_entity.dart b/lib/domain/entites/store_entity.dart new file mode 100644 index 0000000..40fd16e --- /dev/null +++ b/lib/domain/entites/store_entity.dart @@ -0,0 +1,40 @@ +import 'package:equatable/equatable.dart'; + +class StoreEntity extends Equatable { + final String? name; + final String? image; + final String? address; + final String? phoneNumber; + final String? latLong; + + const StoreEntity({ + this.name, + this.image, + this.address, + this.phoneNumber, + this.latLong, + }); + + Map toMap() { + return { + 'name': name, + 'image': image, + 'address': address, + 'phoneNumber': phoneNumber, + 'latLong': latLong, + }; + } + + factory StoreEntity.fromMap(Map map) { + return StoreEntity( + name: map['name'] as String?, + image: map['image'] as String?, + address: map['address'] as String?, + phoneNumber: map['phoneNumber'] as String?, + latLong: map['latLong'] as String?, + ); + } + + @override + List get props => [name, image, address, phoneNumber, latLong]; +} diff --git a/lib/domain/entites/user_entity.dart b/lib/domain/entites/user_entity.dart new file mode 100644 index 0000000..fce2f6d --- /dev/null +++ b/lib/domain/entites/user_entity.dart @@ -0,0 +1,62 @@ +import 'package:equatable/equatable.dart'; + +class UserEntity extends Equatable { + final String? id; + final String? firstName; + final String? lastName; + final String? email; + final String? gender; + final String? phone; + final String? photo; + final String? passwordChangedAt; + + const UserEntity({ + this.id, + this.firstName, + this.lastName, + this.email, + this.gender, + this.phone, + this.photo, + this.passwordChangedAt, + }); + + Map toMap() { + return { + 'id': id, + 'firstName': firstName, + 'lastName': lastName, + 'email': email, + 'gender': gender, + 'phone': phone, + 'photo': photo, + 'passwordChangedAt': passwordChangedAt, + }; + } + + factory UserEntity.fromMap(Map map) { + return UserEntity( + id: map['id'] as String?, + firstName: map['firstName'] as String?, + lastName: map['lastName'] as String?, + email: map['email'] as String?, + gender: map['gender'] as String?, + phone: map['phone'] as String?, + photo: map['photo'] as String?, + passwordChangedAt: map['passwordChangedAt'] as String?, + ); + } + + @override + List get props => + [ + id, + firstName, + lastName, + email, + gender, + phone, + photo, + passwordChangedAt, + ]; +} diff --git a/lib/domain/repo/orders_repo.dart b/lib/domain/repo/orders_repo.dart new file mode 100644 index 0000000..7bebfb3 --- /dev/null +++ b/lib/domain/repo/orders_repo.dart @@ -0,0 +1,17 @@ +import 'package:elevate_tracking_app/core/api_result/api_result.dart'; +import 'package:elevate_tracking_app/domain/entites/order_firestore_entity.dart'; +import 'package:elevate_tracking_app/domain/entites/pending_orders_entity.dart'; + +import '../entites/start_order_entity.dart'; + +abstract interface class OrdersRepo { + Future> getOrders(int? page); + + Future> startOrder(String orderId); + + Future> addFirestoreOrder(OrderFirestoreEntity order); + + Future> getFirestoreOrder(String orderId); + + Stream> streamFirestoreOrder(String orderId); +} diff --git a/lib/domain/use_cases/add_order_to_firestore_use_case.dart b/lib/domain/use_cases/add_order_to_firestore_use_case.dart new file mode 100644 index 0000000..96cee9b --- /dev/null +++ b/lib/domain/use_cases/add_order_to_firestore_use_case.dart @@ -0,0 +1,16 @@ +import 'package:elevate_tracking_app/domain/repo/orders_repo.dart'; +import 'package:injectable/injectable.dart'; + +import '../../core/api_result/api_result.dart'; +import '../entites/order_firestore_entity.dart'; + +@injectable +class AddOrderToFirestoreUseCase { + final OrdersRepo _ordersRepo; + + AddOrderToFirestoreUseCase(this._ordersRepo); + + Future> call({required OrderFirestoreEntity order}) { + return _ordersRepo.addFirestoreOrder(order); + } +} diff --git a/lib/domain/use_cases/get_all_pending_orders_use_case.dart b/lib/domain/use_cases/get_all_pending_orders_use_case.dart new file mode 100644 index 0000000..1e1ac38 --- /dev/null +++ b/lib/domain/use_cases/get_all_pending_orders_use_case.dart @@ -0,0 +1,16 @@ +import 'package:elevate_tracking_app/domain/repo/orders_repo.dart'; +import 'package:injectable/injectable.dart'; + +import '../../core/api_result/api_result.dart'; +import '../entites/pending_orders_entity.dart'; + +@injectable +class GetAllPendingOrdersUseCase { + final OrdersRepo _ordersRepo; + + GetAllPendingOrdersUseCase(this._ordersRepo); + + Future> call({int? page}) async { + return await _ordersRepo.getOrders(page); + } +} diff --git a/lib/domain/use_cases/get_order_from_firestore_use_case.dart b/lib/domain/use_cases/get_order_from_firestore_use_case.dart new file mode 100644 index 0000000..bb55cd7 --- /dev/null +++ b/lib/domain/use_cases/get_order_from_firestore_use_case.dart @@ -0,0 +1,16 @@ +import 'package:elevate_tracking_app/domain/repo/orders_repo.dart'; +import 'package:injectable/injectable.dart'; + +import '../../core/api_result/api_result.dart'; +import '../entites/order_firestore_entity.dart'; + +@injectable +class GetOrderFromFirestoreUseCase { + final OrdersRepo _ordersRepo; + + GetOrderFromFirestoreUseCase(this._ordersRepo); + + Future> call({required String orderId}) { + return _ordersRepo.getFirestoreOrder(orderId); + } +} diff --git a/lib/domain/use_cases/start_order_use_case.dart b/lib/domain/use_cases/start_order_use_case.dart new file mode 100644 index 0000000..c4d960f --- /dev/null +++ b/lib/domain/use_cases/start_order_use_case.dart @@ -0,0 +1,16 @@ +import 'package:elevate_tracking_app/domain/repo/orders_repo.dart'; +import 'package:injectable/injectable.dart'; + +import '../../core/api_result/api_result.dart'; +import '../entites/start_order_entity.dart'; + +@injectable +class StartOrderUseCase { + final OrdersRepo _ordersRepo; + + StartOrderUseCase(this._ordersRepo); + + Future> call({required String orderId}) async { + return await _ordersRepo.startOrder(orderId); + } +} diff --git a/lib/domain/use_cases/stream_order_from_firestore_use_case.dart b/lib/domain/use_cases/stream_order_from_firestore_use_case.dart new file mode 100644 index 0000000..30f503f --- /dev/null +++ b/lib/domain/use_cases/stream_order_from_firestore_use_case.dart @@ -0,0 +1,16 @@ +import 'package:injectable/injectable.dart'; + +import '../../core/api_result/api_result.dart'; +import '../entites/order_firestore_entity.dart'; +import '../repo/orders_repo.dart'; + +@injectable +class StreamOrderFromFirestoreUseCase { + final OrdersRepo _ordersRepo; + + StreamOrderFromFirestoreUseCase(this._ordersRepo); + + Stream> call({required String orderId}) { + return _ordersRepo.streamFirestoreOrder(orderId); + } +} diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..c79170a --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,69 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyC_PztdFpXnsft2wNBj_3DfkQELDcY4cgQ', + appId: '1:795578737478:android:0f68939309ee7dc5e1bb47', + messagingSenderId: '795578737478', + projectId: 'fir-app-distribution-33bfe', + storageBucket: 'fir-app-distribution-33bfe.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyAZbjLqF3RV6vPKtdoSaNsreAR1hHO7xwQ', + appId: '1:795578737478:ios:2f40aa26a69314d5e1bb47', + messagingSenderId: '795578737478', + projectId: 'fir-app-distribution-33bfe', + storageBucket: 'fir-app-distribution-33bfe.firebasestorage.app', + iosBundleId: 'com.example.elevateTrackingApp', + ); + +} \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 8ec0105..34eceae 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -44,5 +44,18 @@ "enterYourEmail": "Enter your email", "enterYourPassword": "Enter your password", "continueWord": "Continue", - "error": "Error" + "error": "Error", + "floweryRider": "Flowery Rider", + "flowerOrder": "Flower order", + "pickupAddress": "Pickup address", + "userAddress": "User address", + "reject": "Reject", + "accept": "Accept", + "egp": "EGP", + "noOrders": "No Orders", + "orderAcceptedSuccessfully": "Order accepted successfully", + "yourLocation": "Your Location", + "liveLocation" : "Live Location", + "user": "User", + "flowery": "Flowery" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 9068e66..c08cf9a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:elevate_tracking_app/core/router/app_router.dart'; import 'package:elevate_tracking_app/core/utils/token_storage/token_storage.dart'; import 'package:elevate_tracking_app/generated/l10n.dart'; import 'package:elevate_tracking_app/my_bloc_observer.dart'; +import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -11,6 +12,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(); configureDependencies(); Bloc.observer = MyBlocObserver(); final token = await TokenStorage.getToken(); diff --git a/lib/presentation/auth/login/view/screen/login_screen.dart b/lib/presentation/auth/login/view/screen/login_screen.dart index f07e6a1..1541edc 100644 --- a/lib/presentation/auth/login/view/screen/login_screen.dart +++ b/lib/presentation/auth/login/view/screen/login_screen.dart @@ -69,7 +69,7 @@ class _LoginScreenState extends State { message: state.errorMessage, ); } else if (state.isLoggedIn) { - context.go(RouteNames.home); + context.go(RouteNames.mainHome); } }, child: LoginBodySection(loginViewModel: _loginViewModel), diff --git a/lib/presentation/home/view/screen/home_page.dart b/lib/presentation/home/view/screen/home_page.dart new file mode 100644 index 0000000..d020255 --- /dev/null +++ b/lib/presentation/home/view/screen/home_page.dart @@ -0,0 +1,120 @@ +import 'package:elevate_tracking_app/core/constants/app_colors.dart'; +import 'package:elevate_tracking_app/core/constants/const_keys.dart'; +import 'package:elevate_tracking_app/generated/l10n.dart'; +import 'package:elevate_tracking_app/presentation/home/view_model/home_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../../../core/constants/widgets_keys.dart'; +import '../../../../core/di/di.dart'; +import '../../../../domain/entites/order_entity.dart'; +import '../../view_model/home_events.dart'; +import '../widget/home_order_card.dart'; + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + late final HomeViewModel homeViewModel; + + @override + void initState() { + super.initState(); + homeViewModel = getIt.get(); + homeViewModel.doIntent(GetOrdersEvent()); + } + + @override + void dispose() { + homeViewModel.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocConsumer( + bloc: homeViewModel, + listener: (context, state) { + if (state.errorMessage != null) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(state.errorMessage!))); + } + if (state.isAcceptSuccess) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + AppLocalizations.of(context).orderAcceptedSuccessfully, + ), + ), + ); + } + }, + builder: (context, state) { + if (state.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + if (state.ordersList == null || state.ordersList!.isEmpty) { + return Center(child: Text(AppLocalizations.of(context).noOrders)); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16.h, + children: [ + Padding( + padding: EdgeInsets.only(left: 16.w, top: 16.h), + child: Text( + AppLocalizations.of(context).floweryRider, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: AppColors.mainColor, + fontWeight: FontWeight.w400, + fontFamily: ConstKeys.iMFeellEnglishFont, + ), + ), + ), + Expanded( + child: NotificationListener( + onNotification: (ScrollNotification scrollInfo) { + if (scrollInfo.metrics.pixels == + scrollInfo.metrics.maxScrollExtent && + !state.isLoadingMore) { + homeViewModel.doIntent(LoadMoreOrdersEvent()); + } + return true; + }, + child: ListView.separated( + key: const Key(WidgetsKeys.kHomePageListView), + padding: EdgeInsets.only( + left: 16.w, + right: 16.w, + bottom: 16.h, + ), + itemCount: state.ordersList?.length ?? 0, + itemBuilder: (ctx, index) { + return HomeOrderCard( + key: Key(state.ordersList?[index].id ?? ""), + index: index, + orderEntity: + state.ordersList?[index] ?? const OrderEntity(), + homeViewModel: homeViewModel, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return SizedBox(height: 16.h); + }, + ), + ), + ), + if (state.isLoadingMore) + const Center(child: CircularProgressIndicator()), + ], + ); + }, + ); + } +} diff --git a/lib/presentation/home/view/widget/home_info_card.dart b/lib/presentation/home/view/widget/home_info_card.dart new file mode 100644 index 0000000..82406a5 --- /dev/null +++ b/lib/presentation/home/view/widget/home_info_card.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../../../core/constants/app_colors.dart'; +import '../../../../core/constants/app_icons.dart'; +import '../../../../core/custom_widget/custom_cached_network_image.dart'; + +class HomeInfoCard extends StatelessWidget { + final String title; + final String image; + final String name; + final String address; + + const HomeInfoCard({ + super.key, + required this.title, + required this.image, + required this.name, + required this.address, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [ + Text( + title, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: AppColors.gray), + ), + Container( + padding: EdgeInsets.all(8.h), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(10.r), + boxShadow: [ + BoxShadow( + color: AppColors.gray.withValues(alpha: 0.2), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.w, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(100), + child: CustomCachedNetworkImage( + width: 44.w, + height: 44.w, + imageUrl: image, + fit: BoxFit.cover, + ), + ), + Expanded( + child: Column( + spacing: 8.h, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + name, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: AppColors.gray), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + spacing: 4.w, + children: [ + Image.asset( + AppIcons.iconLocation, + fit: BoxFit.cover, + height: 16.w, + width: 16.w, + ), + Expanded( + child: Text( + address, + style: Theme.of(context).textTheme.bodySmall, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/presentation/home/view/widget/home_order_card.dart b/lib/presentation/home/view/widget/home_order_card.dart new file mode 100644 index 0000000..9326286 --- /dev/null +++ b/lib/presentation/home/view/widget/home_order_card.dart @@ -0,0 +1,122 @@ +import 'package:elevate_tracking_app/presentation/home/view/widget/home_info_card.dart'; +import 'package:elevate_tracking_app/presentation/home/view_model/home_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../../../core/constants/app_colors.dart'; +import '../../../../domain/entites/order_entity.dart'; +import '../../../../generated/l10n.dart'; +import '../../view_model/home_events.dart'; + +class HomeOrderCard extends StatelessWidget { + final int index; + final OrderEntity orderEntity; + final HomeViewModel homeViewModel; + + const HomeOrderCard({ + super.key, + required this.index, + required this.homeViewModel, + required this.orderEntity, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: AppColors.gray.withValues(alpha: 0.2), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16.h, + children: [ + Text( + AppLocalizations.of(context).flowerOrder, + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(fontSize: 14.sp), + ), + HomeInfoCard( + title: AppLocalizations.of(context).pickupAddress, + image: orderEntity.store?.image ?? "", + name: orderEntity.store?.name ?? "", + address: orderEntity.store?.address ?? "", + ), + HomeInfoCard( + title: AppLocalizations.of(context).userAddress, + image: orderEntity.user?.photo ?? "", + name: orderEntity.user?.firstName ?? "", + address: + "${orderEntity.shippingAddress?.street ?? ""}, ${orderEntity.shippingAddress?.city ?? ""}", + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 8.w, + children: [ + Text( + "${AppLocalizations.of(context).egp} ${orderEntity.totalPrice ?? ""}", + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600), + ), + Expanded( + child: ValueListenableBuilder( + valueListenable: homeViewModel.rejectOrderIndex, + builder: (context, value, child) { + return value == index + ? const Center(child: CircularProgressIndicator()) + : ElevatedButton( + style: Theme.of(context).elevatedButtonTheme.style + ?.copyWith( + backgroundColor: WidgetStateProperty.all( + AppColors.white, + ), + foregroundColor: WidgetStateProperty.all( + AppColors.mainColor, + ), + side: WidgetStateProperty.all( + BorderSide( + color: AppColors.mainColor, + width: 1.w, + ), + ), + ), + onPressed: () { + homeViewModel.doIntent(RejectOrderEvent(index)); + }, + child: Text(AppLocalizations.of(context).reject), + ); + }, + ), + ), + Expanded( + child: ValueListenableBuilder( + valueListenable: homeViewModel.acceptOrderIndex, + builder: (context, value, child) { + return value == index + ? const Center(child: CircularProgressIndicator()) + : ElevatedButton( + onPressed: () { + homeViewModel.doIntent(AcceptOrderEvent(index)); + }, + child: Text(AppLocalizations.of(context).accept), + ); + }, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/presentation/home/view_model/home_events.dart b/lib/presentation/home/view_model/home_events.dart new file mode 100644 index 0000000..ee3ddd8 --- /dev/null +++ b/lib/presentation/home/view_model/home_events.dart @@ -0,0 +1,12 @@ +sealed class HomeEvents {} + +class GetOrdersEvent extends HomeEvents {} +class AcceptOrderEvent extends HomeEvents { + final int index; + AcceptOrderEvent(this.index); +} +class RejectOrderEvent extends HomeEvents { + final int index; + RejectOrderEvent(this.index); +} +class LoadMoreOrdersEvent extends HomeEvents {} \ No newline at end of file diff --git a/lib/presentation/home/view_model/home_state.dart b/lib/presentation/home/view_model/home_state.dart new file mode 100644 index 0000000..c2ac2db --- /dev/null +++ b/lib/presentation/home/view_model/home_state.dart @@ -0,0 +1,42 @@ +part of 'home_view_model.dart'; + +final class HomeState extends Equatable { + final bool isLoading; + final bool isLoadingMore; + final String? errorMessage; + final List? ordersList; + final bool isAcceptSuccess; + + const HomeState({ + this.isLoading = false, + this.isLoadingMore = false, + this.errorMessage, + this.ordersList, + this.isAcceptSuccess = false, + }); + + HomeState copyWith({ + bool? isLoading, + bool? isLoadingMore, + String? errorMessage, + List? ordersList, + bool? isAcceptSuccess, + }) { + return HomeState( + isLoading: isLoading ?? this.isLoading, + isLoadingMore: isLoadingMore ?? this.isLoadingMore, + errorMessage: errorMessage, + ordersList: ordersList ?? this.ordersList, + isAcceptSuccess: isAcceptSuccess ?? false, + ); + } + + @override + List get props => [ + isLoading, + isLoadingMore, + errorMessage, + ordersList, + isAcceptSuccess, + ]; +} diff --git a/lib/presentation/home/view_model/home_view_model.dart b/lib/presentation/home/view_model/home_view_model.dart new file mode 100644 index 0000000..94f4c9d --- /dev/null +++ b/lib/presentation/home/view_model/home_view_model.dart @@ -0,0 +1,183 @@ +import 'package:elevate_tracking_app/core/api_result/api_result.dart'; +import 'package:elevate_tracking_app/domain/use_cases/start_order_use_case.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; + +import '../../../domain/entites/order_entity.dart'; +import '../../../domain/entites/pending_orders_entity.dart'; +import '../../../domain/entites/start_order_entity.dart'; +import '../../../domain/use_cases/get_all_pending_orders_use_case.dart'; +import 'home_events.dart'; + +part 'home_state.dart'; + +@injectable +class HomeViewModel extends Cubit { + final GetAllPendingOrdersUseCase _getAllPendingOrdersUseCase; + final StartOrderUseCase _startOrderUseCase; + + /*final AddOrderToFirestoreUseCase _addOrderToFirestoreUseCase; + final GetOrderFromFirestoreUseCase _getOrderFromFirestoreUseCase; + final StreamOrderFromFirestoreUseCase _streamOrderFromFirestoreUseCase;*/ + + HomeViewModel( + this._getAllPendingOrdersUseCase, + this._startOrderUseCase, + /*this._addOrderToFirestoreUseCase, + this._getOrderFromFirestoreUseCase, + this._streamOrderFromFirestoreUseCase,*/ + ) : super(const HomeState()); + + ValueNotifier acceptOrderIndex = ValueNotifier(-1); + ValueNotifier rejectOrderIndex = ValueNotifier(-1); + + int currentPage = 1; + int totalPages = 0; + bool isLoadMore = false; + + void doIntent(HomeEvents events) { + switch (events) { + case GetOrdersEvent(): + _getOrders(); + case AcceptOrderEvent(): + _acceptOrder(events.index); + case RejectOrderEvent(): + _rejectOrder(events.index); + case LoadMoreOrdersEvent(): + _loadMoreOrders(); + } + } + + Future _getOrders() async { + emit(const HomeState(isLoading: true)); + final result = await _getAllPendingOrdersUseCase(); + switch (result) { + case ApiSuccessResult(): + currentPage = result.data.metadata?.currentPage ?? 0; + totalPages = result.data.metadata?.totalPages ?? 0; + emit(state.copyWith(isLoading: false, ordersList: result.data.orders)); + break; + case ApiErrorResult(): + emit( + state.copyWith(isLoading: false, errorMessage: result.errorMessage), + ); + break; + } + } + + Future _acceptOrder(int index) async { + acceptOrderIndex.value = index; + final result = await _startOrderUseCase( + orderId: state.ordersList?[index].id ?? "", + ); + switch (result) { + case ApiSuccessResult(): + final updatedList = List.from(state.ordersList ?? []); + updatedList.removeAt(index); + emit(state.copyWith(ordersList: updatedList, isAcceptSuccess: true)); + case ApiErrorResult(): + emit(state.copyWith(errorMessage: result.errorMessage)); + } + acceptOrderIndex.value = -1; + } + + void _rejectOrder(int index) { + rejectOrderIndex.value = index; + final updatedList = List.from(state.ordersList ?? []); + updatedList.removeAt(index); + emit(state.copyWith(ordersList: updatedList)); + rejectOrderIndex.value = -1; + } + + Future _loadMoreOrders() async { + if (isLoadMore || currentPage >= totalPages) return; + currentPage++; + isLoadMore = true; + + emit(state.copyWith(isLoadingMore: true)); + final result = await _getAllPendingOrdersUseCase(page: currentPage); + switch (result) { + case ApiSuccessResult(): + currentPage = result.data.metadata?.currentPage ?? 0; + totalPages = result.data.metadata?.totalPages ?? 0; + final updatedList = List.from(state.ordersList ?? []); + updatedList.addAll(result.data.orders ?? []); + emit(state.copyWith(isLoadingMore: false, ordersList: updatedList)); + isLoadMore = false; + break; + case ApiErrorResult(): + emit( + state.copyWith( + isLoadingMore: false, + errorMessage: result.errorMessage, + ), + ); + isLoadMore = false; + break; + } + } + + /*Future testAdd(int index) async { + final result = await _addOrderToFirestoreUseCase( + order: OrderFirestoreEntity( + driver: const DriverEntity( + id: "1", + country: "Egypt", + firstName: "Ahmed", + lastName: "Ali", + vehicleType: "Car", + vehicleNumber: "123456", + vehicleLicense: "123456", + nid: "123456", + nidImg: "https://picsum.photos/200/300", + email: "fake-email@gmail.com", + gender: "Male", + phone: "0123456789", + photo: "0123456789", + role: "Driver", + createdAt: "DateTime.timestamp()", + ), + order: state.ordersList?[index], + location: const LiveLocationEntity( + lat: "30.0444", + long: "31.2357", + address: "Cairo, Egypt", + ), + ), + ); + + switch (result) { + case ApiSuccessResult(): + emit(state.copyWith(isAcceptSuccess: true)); + case ApiErrorResult(): + emit(state.copyWith(errorMessage: result.errorMessage)); + } + } + + Future testGet(int index) async { + final result = await _getOrderFromFirestoreUseCase( + orderId: state.ordersList?[index].id ?? "", + ); + switch (result) { + case ApiSuccessResult(): + print(result.data.toMap()); + emit(state.copyWith(isAcceptSuccess: true)); + case ApiErrorResult(): + emit(state.copyWith(errorMessage: result.errorMessage)); + } + } + + void testStream(int index) { + _streamOrderFromFirestoreUseCase(orderId: state.ordersList?[index].id ?? "").listen((result) { + switch (result) { + case ApiSuccessResult(): + print(result.data.toMap()); + emit(state.copyWith(isAcceptSuccess: true)); + case ApiErrorResult(): + emit(state.copyWith(errorMessage: result.errorMessage)); + } + }); + }*/ +} diff --git a/lib/presentation/home/views/screen/home_screen.dart b/lib/presentation/home/views/screen/home_screen.dart deleted file mode 100644 index aa2f7a7..0000000 --- a/lib/presentation/home/views/screen/home_screen.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/material.dart'; - -class HomeScreen extends StatelessWidget { - const HomeScreen({super.key}); - - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} \ No newline at end of file diff --git a/lib/presentation/location/view/screen/location_screen.dart b/lib/presentation/location/view/screen/location_screen.dart new file mode 100644 index 0000000..9abae2c --- /dev/null +++ b/lib/presentation/location/view/screen/location_screen.dart @@ -0,0 +1,73 @@ +import 'package:elevate_tracking_app/core/custom_widget/custom_dialog.dart'; +import 'package:elevate_tracking_app/presentation/location/view/widget/location_bottom_sheet.dart'; +import 'package:elevate_tracking_app/presentation/location/view/widget/location_view_body.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/di/di.dart'; +import '../../../../generated/l10n.dart'; +import '../../view_model/location_events.dart'; +import '../../view_model/location_view_model.dart'; + +class LocationScreen extends StatefulWidget { + const LocationScreen({super.key}); + + @override + State createState() => _LocationScreenState(); +} + +class _LocationScreenState extends State { + late final LocationViewModel locationViewModel; + + @override + void initState() { + super.initState(); + locationViewModel = getIt(); + locationViewModel.doIntent( + GetOrderLocationEvent(orderId: "68efde9d7fee68a4c2ec2e18", isUser: true), + ); + } + + @override + Widget build(BuildContext context) { + return BlocConsumer( + bloc: locationViewModel, + listener: (context, state) { + if (state.errorMessage != null) { + CustomDialog.positiveButton( + context: context, + cancelable: false, + title: AppLocalizations.of(context).error, + message: state.errorMessage!, + positiveOnClick: () { + context.pop(); + }, + ); + } + }, + builder: (context, state) { + return Scaffold( + body: Builder( + builder: (context) { + if (state.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + if (state.errorMessage != null) { + return Center(child: Text(state.errorMessage!)); + } + return LocationViewBody(locationViewModel: locationViewModel); + }, + ), + bottomSheet: state.order != null + ? LocationBottomSheet( + orderFirestoreEntity: state.order, + isUser: true, + locationViewModel: locationViewModel, + ) + : null, + ); + }, + ); + } +} diff --git a/lib/presentation/location/view/widget/location_bottom_sheet.dart b/lib/presentation/location/view/widget/location_bottom_sheet.dart new file mode 100644 index 0000000..d557f9d --- /dev/null +++ b/lib/presentation/location/view/widget/location_bottom_sheet.dart @@ -0,0 +1,80 @@ +import 'package:elevate_tracking_app/presentation/location/view_model/location_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:solid_bottom_sheet/solid_bottom_sheet.dart'; + +import '../../../../core/constants/app_colors.dart'; +import '../../../../domain/entites/order_firestore_entity.dart'; +import '../../../../generated/l10n.dart'; +import 'location_info_card.dart'; + +class LocationBottomSheet extends StatelessWidget { + final OrderFirestoreEntity? orderFirestoreEntity; + final bool isUser; + final LocationViewModel locationViewModel; + + const LocationBottomSheet({ + super.key, + required this.orderFirestoreEntity, + required this.isUser, + required this.locationViewModel, + }); + + @override + Widget build(BuildContext context) { + return SolidBottomSheet( + maxHeight: 250.h, + headerBar: Container( + color: AppColors.white, + height: 40.h, + child: Padding( + padding: EdgeInsets.only(bottom: 12.h), + child: Divider( + thickness: 4, + radius: BorderRadius.circular(4.sp), + indent: 155.w, + endIndent: 155.w, + ), + ), + ), + body: Container( + color: AppColors.white, + child: Padding( + padding: EdgeInsetsGeometry.only( + left: 16.w, + right: 16.w, + bottom: 16.h, + ), + child: Column( + spacing: 16.h, + children: isUser + ? infoCards(context) + : infoCards(context).reversed.toList(), + ), + ), + ), + ); + } + + List infoCards(BuildContext context) { + return [ + LocationInfoCard( + title: AppLocalizations.of(context).userAddress, + image: orderFirestoreEntity?.order?.user?.photo ?? "", + name: orderFirestoreEntity?.order?.user?.firstName ?? "", + address: + "${orderFirestoreEntity?.order?.shippingAddress?.street ?? ""}, ${orderFirestoreEntity?.order?.shippingAddress?.city ?? ""}", + phone: orderFirestoreEntity?.order?.shippingAddress?.phone ?? "", + locationViewModel: locationViewModel, + ), + LocationInfoCard( + title: AppLocalizations.of(context).pickupAddress, + image: orderFirestoreEntity?.order?.store?.image ?? "", + name: orderFirestoreEntity?.order?.store?.name ?? "", + address: orderFirestoreEntity?.order?.store?.address ?? "", + phone: orderFirestoreEntity?.order?.store?.phoneNumber ?? "", + locationViewModel: locationViewModel, + ), + ]; + } +} diff --git a/lib/presentation/location/view/widget/location_info_card.dart b/lib/presentation/location/view/widget/location_info_card.dart new file mode 100644 index 0000000..12afcb3 --- /dev/null +++ b/lib/presentation/location/view/widget/location_info_card.dart @@ -0,0 +1,130 @@ +import 'package:elevate_tracking_app/presentation/location/view_model/location_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../../../core/constants/app_colors.dart'; +import '../../../../core/constants/app_icons.dart'; +import '../../../../core/custom_widget/custom_cached_network_image.dart'; +import '../../view_model/location_events.dart'; + +class LocationInfoCard extends StatelessWidget { + final String title; + final String image; + final String name; + final String address; + final String phone; + final LocationViewModel locationViewModel; + + const LocationInfoCard({ + super.key, + required this.title, + required this.image, + required this.name, + required this.address, + required this.phone, + required this.locationViewModel, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [ + Text( + title, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: AppColors.gray), + ), + Container( + padding: EdgeInsets.all(8.h), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(10.r), + boxShadow: [ + BoxShadow( + color: AppColors.gray.withValues(alpha: 0.2), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 8.w, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(100), + child: CustomCachedNetworkImage( + width: 44.w, + height: 44.w, + imageUrl: image, + fit: BoxFit.cover, + ), + ), + Expanded( + child: Column( + spacing: 8.h, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + name, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: AppColors.gray), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + spacing: 4.w, + children: [ + Image.asset( + AppIcons.iconLocation, + fit: BoxFit.cover, + height: 16.w, + width: 16.w, + ), + Expanded( + child: Text( + address, + style: Theme.of(context).textTheme.bodySmall, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ], + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 4.w, + children: [ + GestureDetector( + child: Image.asset(AppIcons.iconCall), + onTap: () { + locationViewModel.doIntent(LunchCallLocationEvent(phone)); + }, + ), + GestureDetector( + child: Image.asset(AppIcons.iconWhatsapp), + onTap: () { + locationViewModel.doIntent( + LunchWhatsAppLocationEvent(phone), + ); + }, + ), + ], + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/presentation/location/view/widget/location_view_body.dart b/lib/presentation/location/view/widget/location_view_body.dart new file mode 100644 index 0000000..999f423 --- /dev/null +++ b/lib/presentation/location/view/widget/location_view_body.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:go_router/go_router.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import '../../view_model/location_view_model.dart'; + +class LocationViewBody extends StatelessWidget { + final LocationViewModel locationViewModel; + + const LocationViewBody({super.key, required this.locationViewModel}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Padding( + padding: EdgeInsets.only(bottom: 40.h), + child: GoogleMap( + onMapCreated: (controller) { + locationViewModel.mapController = controller; + }, + markers: locationViewModel.state.markers, + polylines: locationViewModel.state.polylines, + initialCameraPosition: CameraPosition( + target: locationViewModel.currentLocation, + zoom: 12, + ), + ), + ), + Positioned( + left: 10.w, + top: 32.h, + child: ElevatedButton( + onPressed: () { + context.pop(); + }, + style: Theme.of(context).elevatedButtonTheme.style?.copyWith( + shape: WidgetStateProperty.all(const CircleBorder()), + ), + child: Padding( + padding: EdgeInsets.only(left: 6.w), + child: Icon(Icons.arrow_back_ios, size: 20.sp), + ), + ), + ), + ], + ); + } +} diff --git a/lib/presentation/location/view_model/location_events.dart b/lib/presentation/location/view_model/location_events.dart new file mode 100644 index 0000000..a9490f0 --- /dev/null +++ b/lib/presentation/location/view_model/location_events.dart @@ -0,0 +1,20 @@ +sealed class LocationEvents {} + +class GetOrderLocationEvent extends LocationEvents { + final String orderId; + final bool isUser; + + GetOrderLocationEvent({required this.orderId, required this.isUser}); +} + +class LunchCallLocationEvent extends LocationEvents { + final String phoneNumber; + + LunchCallLocationEvent(this.phoneNumber); +} + +class LunchWhatsAppLocationEvent extends LocationEvents { + final String phoneNumber; + + LunchWhatsAppLocationEvent(this.phoneNumber); +} \ No newline at end of file diff --git a/lib/presentation/location/view_model/location_state.dart b/lib/presentation/location/view_model/location_state.dart new file mode 100644 index 0000000..3f67db8 --- /dev/null +++ b/lib/presentation/location/view_model/location_state.dart @@ -0,0 +1,42 @@ +part of 'location_view_model.dart'; + +final class LocationState extends Equatable { + final bool isLoading; + final String? errorMessage; + final OrderFirestoreEntity? order; + final Set markers; + final Set polylines; + + const LocationState({ + this.isLoading = true, + this.errorMessage, + this.order, + this.markers = const {}, + this.polylines = const {}, + }); + + LocationState copyWith({ + bool? isLoading, + String? errorMessage, + OrderFirestoreEntity? order, + Set? markers, + Set? polylines, + }) { + return LocationState( + isLoading: isLoading ?? this.isLoading, + errorMessage: errorMessage ?? this.errorMessage, + order: order ?? this.order, + markers: markers ?? this.markers, + polylines: polylines ?? this.polylines, + ); + } + + @override + List get props => [ + isLoading, + errorMessage, + order, + markers, + polylines, + ]; +} diff --git a/lib/presentation/location/view_model/location_view_model.dart b/lib/presentation/location/view_model/location_view_model.dart new file mode 100644 index 0000000..c5c41f4 --- /dev/null +++ b/lib/presentation/location/view_model/location_view_model.dart @@ -0,0 +1,257 @@ +import 'dart:async'; + +import 'package:elevate_tracking_app/core/api_result/api_result.dart'; +import 'package:elevate_tracking_app/core/constants/app_colors.dart'; +import 'package:elevate_tracking_app/core/constants/app_images.dart'; +import 'package:elevate_tracking_app/domain/entites/order_firestore_entity.dart'; +import 'package:elevate_tracking_app/domain/use_cases/add_order_to_firestore_use_case.dart'; +import 'package:elevate_tracking_app/generated/l10n.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:injectable/injectable.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../../core/utils/location_manager.dart'; +import '../../../domain/entites/live_location_entity.dart'; +import '../../../domain/use_cases/get_order_from_firestore_use_case.dart'; +import 'location_events.dart'; + +part 'location_state.dart'; + +@injectable +class LocationViewModel extends Cubit { + final GetOrderFromFirestoreUseCase _getOrderFromFirestoreUseCase; + final AddOrderToFirestoreUseCase _addOrderToFirestoreUseCase; + + LocationViewModel( + this._getOrderFromFirestoreUseCase, + this._addOrderToFirestoreUseCase, + ) : super(const LocationState()); + + late final bool isUser; + + GoogleMapController? mapController; + + late LatLng currentLocation; + late final LatLng destinationLocation; + late final String destinationTitle; + LatLng? liveLocation; + + late final BitmapDescriptor yourLocationIcon; + late final BitmapDescriptor storeIcon; + late final BitmapDescriptor userIcon; + late final BitmapDescriptor liveLocationIcon; + + Set markers = {}; + Set polylines = {}; + StreamSubscription? positionStream; + + void doIntent(LocationEvents events) { + switch (events) { + case GetOrderLocationEvent(): + _getOrder(events.orderId, events.isUser); + case LunchCallLocationEvent(): + _callNumber(events.phoneNumber); + case LunchWhatsAppLocationEvent(): + _openWhatsApp(phone: events.phoneNumber); + } + } + + Future _getOrder(String orderId, bool isUser) async { + this.isUser = isUser; + final result = await _getOrderFromFirestoreUseCase(orderId: orderId); + switch (result) { + case ApiSuccessResult(): + await initIcons(); + initDestinationLocation(result.data); + await initCurrentLocation(); + initMarkers(); + emit( + state.copyWith( + isLoading: false, + order: result.data, + markers: markers, + polylines: polylines, + ), + ); + await _startLiveLocationUpdates(); + case ApiErrorResult(): + emit( + state.copyWith(isLoading: false, errorMessage: result.errorMessage), + ); + } + } + + Future initIcons() async { + yourLocationIcon = await BitmapDescriptor.asset( + ImageConfiguration.empty, + AppImages.imageYourLocation, + ); + storeIcon = await BitmapDescriptor.asset( + ImageConfiguration.empty, + AppImages.imageFloweryLocation, + ); + userIcon = await BitmapDescriptor.asset( + ImageConfiguration.empty, + AppImages.imageUserLocation, + ); + liveLocationIcon = await BitmapDescriptor.asset( + ImageConfiguration.empty, + AppImages.imageMotorcycleDelivery, + ); + } + + void initDestinationLocation(OrderFirestoreEntity result) { + if (isUser) { + destinationLocation = LatLng( + double.parse(result.order?.shippingAddress?.lat ?? "0"), + double.parse(result.order?.shippingAddress?.long ?? "0"), + ); + destinationTitle = AppLocalizations().user; + } else { + final latLongString = result.order?.store?.latLong ?? "0,0"; + final latLongList = latLongString.split(','); + + destinationLocation = LatLng( + double.parse(latLongList[0]), + double.parse(latLongList[1]), + ); + destinationTitle = AppLocalizations().flowery; + } + } + + Future initCurrentLocation() async { + try { + final position = await LocationManager.getCurrentLocation(); + currentLocation = LatLng(position.latitude, position.longitude); + } catch (e) { + emit(state.copyWith(errorMessage: e.toString())); + } + } + + void initMarkers() { + markers.clear(); + markers = { + buildMarker( + markerId: AppLocalizations().yourLocation, + position: currentLocation, + title: AppLocalizations().yourLocation, + icon: yourLocationIcon, + ), + buildMarker( + markerId: destinationTitle, + position: destinationLocation, + title: destinationTitle, + icon: isUser ? userIcon : storeIcon, + ), + }; + + if (liveLocation != null) { + markers.add( + buildMarker( + markerId: AppLocalizations().liveLocation, + position: liveLocation!, + title: AppLocalizations().liveLocation, + icon: liveLocationIcon, + ), + ); + } + + polylines.clear(); + polylines = { + Polyline( + polylineId: const PolylineId('route'), + color: AppColors.mainColor, + width: 2, + points: [currentLocation, destinationLocation], + ), + }; + } + + Future _startLiveLocationUpdates() async { + try { + await LocationManager.checkLocationPermission(); + const locationSettings = LocationSettings( + accuracy: LocationAccuracy.high, + distanceFilter: 10, + ); + positionStream = + Geolocator.getPositionStream( + locationSettings: locationSettings, + ).listen((Position position) async { + liveLocation = LatLng(position.latitude, position.longitude); + initMarkers(); + mapController?.animateCamera(CameraUpdate.newLatLng(liveLocation!)); + emit(state.copyWith(markers: markers, polylines: polylines)); + await _updateLocation(); + }); + } catch (e) { + emit(state.copyWith(errorMessage: e.toString())); + } + } + + Future _updateLocation() async { + final updatedOrder = state.order?.copyWith( + location: LiveLocationEntity( + lat: liveLocation!.latitude.toString(), + long: liveLocation!.longitude.toString(), + address: state.order?.location?.address, + ), + ); + final result = await _addOrderToFirestoreUseCase(order: updatedOrder!); + switch (result) { + case ApiSuccessResult(): + break; + case ApiErrorResult(): + emit(state.copyWith(errorMessage: result.errorMessage)); + } + } + + Marker buildMarker({ + required String markerId, + required LatLng position, + required String title, + required BitmapDescriptor icon, + }) { + return Marker( + markerId: MarkerId(markerId), + position: position, + infoWindow: InfoWindow(title: title), + icon: icon, + ); + } + + Future _openWhatsApp({ + required String phone, + String message = '', + }) async { + final Uri whatsappUri = Uri.parse( + "https://wa.me/$phone?text=${Uri.encodeComponent(message)}", + ); + + if (await canLaunchUrl(whatsappUri)) { + await launchUrl(whatsappUri, mode: LaunchMode.externalApplication); + } else { + throw Exception("Could not launch WhatsApp"); + } + } + + Future _callNumber(String phoneNumber) async { + final Uri callUri = Uri(scheme: 'tel', path: phoneNumber); + + if (await canLaunchUrl(callUri)) { + await launchUrl(callUri, mode: LaunchMode.externalApplication); + } else { + throw Exception('Could not launch dialer'); + } + } + + @override + Future close() { + positionStream?.cancel(); + return super.close(); + } +} diff --git a/lib/presentation/main_home/views/widgets/custom_page_view.dart b/lib/presentation/main_home/views/widgets/custom_page_view.dart index caba4f8..ae15990 100644 --- a/lib/presentation/main_home/views/widgets/custom_page_view.dart +++ b/lib/presentation/main_home/views/widgets/custom_page_view.dart @@ -1,4 +1,4 @@ -import 'package:elevate_tracking_app/presentation/home/views/screen/home_screen.dart'; +import 'package:elevate_tracking_app/presentation/home/view/screen/home_page.dart'; import 'package:elevate_tracking_app/presentation/main_home/view_models/main_home_events.dart'; import 'package:elevate_tracking_app/presentation/main_home/view_models/main_home_view_model.dart'; import 'package:elevate_tracking_app/presentation/orders/views/screen/orders_screen.dart'; @@ -18,7 +18,7 @@ class CustomPageView extends StatelessWidget { scrollDirection: Axis.horizontal, controller: mainHomeViewModel.pageController, children: [ - const HomeScreen(), + const HomePage(), const OrdersScreen(), const ProfileScreen(), ], diff --git a/pubspec.yaml b/pubspec.yaml index 89c1e5e..4436b57 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,6 +53,11 @@ dependencies: shared_preferences: ^2.5.3 image_picker: ^1.2.0 go_router: ^16.2.2 + provider: ^6.1.5+1 + firebase_core: ^4.1.1 + cloud_firestore: ^6.0.2 + solid_bottom_sheet: ^0.1.13 + url_launcher: ^6.3.2 dev_dependencies: flutter_test: diff --git a/test/api/data_source/orders_remote_data_source_impl_test.dart b/test/api/data_source/orders_remote_data_source_impl_test.dart new file mode 100644 index 0000000..6a4ece7 --- /dev/null +++ b/test/api/data_source/orders_remote_data_source_impl_test.dart @@ -0,0 +1,94 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:elevate_tracking_app/api/client/api_client.dart'; +import 'package:elevate_tracking_app/api/data_source/orders_remote_data_source_impl.dart'; +import 'package:elevate_tracking_app/api/mapper/orders_mapper.dart'; +import 'package:elevate_tracking_app/core/api_result/api_result.dart'; +import 'package:elevate_tracking_app/domain/entites/pending_orders_entity.dart'; +import 'package:elevate_tracking_app/domain/entites/start_order_entity.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../../dummy/orders_dummy_data.dart'; +import 'orders_remote_data_source_impl_test.mocks.dart'; + +@GenerateMocks([ApiClient, FirebaseFirestore]) +void main() { + final dummyData = OrdersDummyData(); + + group("test OrdersRemoteDataSourceImpl", () { + late MockApiClient mockApiClient; + late MockFirebaseFirestore mockFirebaseFirestore; + late OrdersRemoteDataSourceImpl dataSource; + + setUp(() { + mockApiClient = MockApiClient(); + mockFirebaseFirestore = MockFirebaseFirestore(); + dataSource = OrdersRemoteDataSourceImpl(mockApiClient, mockFirebaseFirestore); + }); + + group("test getOrders", () { + test( + "when call getOrders should return PendingOrdersEntity with right data", + () async { + final expectedResult = dummyData.ordersResponse; + when( + mockApiClient.getOrders(null), + ).thenAnswer((_) async => expectedResult); + + final result = await dataSource.getOrders(null); + + verify(mockApiClient.getOrders(null)).called(1); + + expect(result, isA>()); + result as ApiSuccessResult; + expect(result.data, expectedResult.toEntity()); + }, + ); + + test("when getOrders failed should return error result", () async { + final expectedError = "fake-error"; + when(mockApiClient.getOrders(null)).thenThrow(Exception(expectedError)); + final result = await dataSource.getOrders(null); + verify(mockApiClient.getOrders(null)).called(1); + expect(result, isA>()); + result as ApiErrorResult; + expect(result.errorMessage, contains(expectedError)); + }); + }); + + group("test startOrder", () { + test( + "when call startOrder should return StartOrderEntity with right data", + () async { + final expectedOrderId = "fake-order-id"; + final expectedResult = dummyData.startOrderResponse; + when( + mockApiClient.startOrder(expectedOrderId), + ).thenAnswer((_) async => expectedResult); + + final result = await dataSource.startOrder(expectedOrderId); + + verify(mockApiClient.startOrder(expectedOrderId)).called(1); + + expect(result, isA>()); + result as ApiSuccessResult; + expect(result.data, expectedResult.orders?.toEntity()); + }, + ); + + test("when startOrder failed should return error result", () async { + final expectedOrderId = "fake-order-id"; + final expectedError = "fake-error"; + when( + mockApiClient.startOrder(expectedOrderId), + ).thenThrow(Exception(expectedError)); + final result = await dataSource.startOrder(expectedOrderId); + verify(mockApiClient.startOrder(expectedOrderId)).called(1); + expect(result, isA>()); + result as ApiErrorResult; + expect(result.errorMessage, contains(expectedError)); + }); + }); + }); +} diff --git a/test/api/mapper/orders_mapper_test.dart b/test/api/mapper/orders_mapper_test.dart new file mode 100644 index 0000000..94cf209 --- /dev/null +++ b/test/api/mapper/orders_mapper_test.dart @@ -0,0 +1,251 @@ +import 'package:elevate_tracking_app/api/mapper/orders_mapper.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../dummy/orders_dummy_data.dart'; + +void main() { + final dummyData = OrdersDummyData(); + + group("DriverOrderMapper", () { + test( + "when toEntity is called, it should return a valid DriverOrderEntity", + () { + // Arrange + final driverOrderDTO = dummyData.driverOrderDTO1; + + // Act + final result = driverOrderDTO.toEntity(); + + // Assert + expect(result.id, driverOrderDTO.id); + expect(result.driver, driverOrderDTO.driver); + expect(result.order?.id, driverOrderDTO.order?.id); + expect(result.v, driverOrderDTO.v); + expect(result.createdAt, driverOrderDTO.createdAt); + expect(result.updatedAt, driverOrderDTO.updatedAt); + expect(result.store?.name, driverOrderDTO.store?.name); + }, + ); + }); + + group("OrderMapper", () { + test("when toEntity is called, it should return a valid OrderEntity", () { + // Arrange + final orderDTO = dummyData.orderDTO1; + + // Act + final result = orderDTO.toEntity(); + + // Assert + expect(result.id, orderDTO.id); + expect(result.user?.id, orderDTO.user?.id); + expect(result.orderItems?.length, orderDTO.orderItems?.length); + expect(result.totalPrice, orderDTO.totalPrice); + expect(result.shippingAddress?.street, orderDTO.shippingAddress?.street); + expect(result.paymentType, orderDTO.paymentType); + expect(result.isPaid, orderDTO.isPaid); + expect(result.isDelivered, orderDTO.isDelivered); + expect(result.state, orderDTO.state); + expect(result.createdAt, orderDTO.createdAt); + expect(result.updatedAt, orderDTO.updatedAt); + expect(result.orderNumber, orderDTO.orderNumber); + expect(result.v, orderDTO.v); + expect(result.store?.name, orderDTO.store?.name); + }); + }); + + group("OrderItemMapper", () { + test( + "when toEntity is called, it should return a valid OrderItemEntity", + () { + // Arrange + final orderItemDTO = dummyData.orderItemDTO1; + + // Act + final result = orderItemDTO.toEntity(); + + // Assert + expect(result.product?.id, orderItemDTO.product?.id); + expect(result.price, orderItemDTO.price); + expect(result.quantity, orderItemDTO.quantity); + expect(result.id, orderItemDTO.id); + }, + ); + }); + + group("ProductMapper", () { + test("when toEntity is called, it should return a valid ProductEntity", () { + // Arrange + final productDTO = dummyData.productDTO1; + + // Act + final result = productDTO.toEntity(); + + // Assert + expect(result.id, productDTO.id); + expect(result.title, productDTO.title); + expect(result.slug, productDTO.slug); + expect(result.description, productDTO.description); + expect(result.imgCover, productDTO.imgCover); + expect(result.images, productDTO.images); + expect(result.price, productDTO.price); + expect(result.priceAfterDiscount, productDTO.priceAfterDiscount); + expect(result.quantity, productDTO.quantity); + expect(result.category, productDTO.category); + expect(result.occasion, productDTO.occasion); + expect(result.createdAt, productDTO.createdAt); + expect(result.updatedAt, productDTO.updatedAt); + expect(result.v, productDTO.v); + expect(result.isSuperAdmin, productDTO.isSuperAdmin); + expect(result.sold, productDTO.sold); + expect(result.rateAvg, productDTO.rateAvg); + expect(result.rateCount, productDTO.rateCount); + }); + }); + + group("StoreMapper", () { + test("when toEntity is called, it should return a valid StoreEntity", () { + // Arrange + final storeDTO = dummyData.storeDTO1; + + // Act + final result = storeDTO.toEntity(); + + // Assert + expect(result.name, storeDTO.name); + expect(result.image, storeDTO.image); + expect(result.address, storeDTO.address); + expect(result.phoneNumber, storeDTO.phoneNumber); + expect(result.latLong, storeDTO.latLong); + }); + }); + + group("UserMapper", () { + test("when toEntity is called, it should return a valid UserEntity", () { + // Arrange + final userDTO = dummyData.userDTO1; + + // Act + final result = userDTO.toEntity(); + + // Assert + expect(result.id, userDTO.id); + expect(result.firstName, userDTO.firstName); + expect(result.lastName, userDTO.lastName); + expect(result.email, userDTO.email); + expect(result.gender, userDTO.gender); + expect(result.phone, userDTO.phone); + expect(result.photo, userDTO.photo); + expect(result.passwordChangedAt, userDTO.passwordChangedAt); + }); + }); + + group("StartOrderMapper", () { + test( + "when toEntity is called, it should return a valid StartOrderEntity", + () { + // Arrange + final startOrderDto = dummyData.startOrderDto; + + // Act + final result = startOrderDto.toEntity(); + + // Assert + expect(result.id, startOrderDto.id); + expect(result.user, startOrderDto.user); + expect( + result.orderItems?.first.product, + startOrderDto.orderItems?.first.product, + ); + expect(result.totalPrice, startOrderDto.totalPrice); + expect(result.paymentType, startOrderDto.paymentType); + expect(result.isPaid, startOrderDto.isPaid); + expect(result.isDelivered, startOrderDto.isDelivered); + expect(result.state, startOrderDto.state); + expect(result.createdAt, startOrderDto.createdAt); + expect(result.updatedAt, startOrderDto.updatedAt); + expect(result.orderNumber, startOrderDto.orderNumber); + expect(result.v, startOrderDto.v); + }, + ); + }); + + group("StartOrderItemsMapper", () { + test( + "when toEntity is called, it should return a valid StartOrderItemsEntity", + () { + // Arrange + final startOrderItemsDto = dummyData.startOrderItemsDto1; + + // Act + final result = startOrderItemsDto.toEntity(); + + // Assert + expect(result.product, startOrderItemsDto.product); + expect(result.price, startOrderItemsDto.price); + expect(result.quantity, startOrderItemsDto.quantity); + expect(result.id, startOrderItemsDto.id); + }, + ); + }); + + group("ShippingAddressMapper", () { + test( + "when toEntity is called, it should return a valid ShippingAddressEntity", + () { + // Arrange + final shippingAddressDTO = dummyData.shippingAddressDTO1; + + // Act + final result = shippingAddressDTO.toEntity(); + + // Assert + expect(result.street, shippingAddressDTO.street); + expect(result.city, shippingAddressDTO.city); + expect(result.phone, shippingAddressDTO.phone); + expect(result.lat, shippingAddressDTO.lat); + expect(result.long, shippingAddressDTO.long); + }, + ); + }); + + group("MetaDataMapper", () { + test( + "when toEntity is called, it should return a valid MetaDataEntity", + () { + // Arrange + final metaDataDTO = dummyData.metaDataDTO; + + // Act + final result = metaDataDTO.toEntity(); + + // Assert + expect(result.currentPage, metaDataDTO.currentPage); + expect(result.totalPages, metaDataDTO.totalPages); + expect(result.totalItems, metaDataDTO.totalItems); + expect(result.limit, metaDataDTO.limit); + }, + ); + }); + + group("OrderResponseMapper", () { + test( + "when toEntity is called, it should return a valid PendingOrdersEntity", + () { + // Arrange + final ordersResponse = dummyData.ordersResponse; + + // Act + final result = ordersResponse.toEntity(); + + // Assert + expect( + result.metadata?.totalItems, + ordersResponse.metadata?.totalItems, + ); + expect(result.orders?.length, ordersResponse.orders?.length); + expect(result.orders?.first.id, ordersResponse.orders?.first.id); + }, + ); + }); +} diff --git a/test/data/repo/orders_repo_impl_test.dart b/test/data/repo/orders_repo_impl_test.dart new file mode 100644 index 0000000..c7fa0ab --- /dev/null +++ b/test/data/repo/orders_repo_impl_test.dart @@ -0,0 +1,120 @@ +import 'package:elevate_tracking_app/api/mapper/orders_mapper.dart'; +import 'package:elevate_tracking_app/core/api_result/api_result.dart'; +import 'package:elevate_tracking_app/data/data_source/orders_remote_data_source.dart'; +import 'package:elevate_tracking_app/data/repo/orders_repo_impl.dart'; +import 'package:elevate_tracking_app/domain/entites/pending_orders_entity.dart'; +import 'package:elevate_tracking_app/domain/entites/start_order_entity.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../../dummy/orders_dummy_data.dart'; +import 'orders_repo_impl_test.mocks.dart'; + +@GenerateMocks([OrdersRemoteDataSource]) +void main() { + final dummyData = OrdersDummyData(); + + group("test OrdersRepoImpl", () { + late MockOrdersRemoteDataSource mockOrdersRemoteDataSource; + late OrdersRepoImpl ordersRepoImpl; + setUp(() { + mockOrdersRemoteDataSource = MockOrdersRemoteDataSource(); + ordersRepoImpl = OrdersRepoImpl(mockOrdersRemoteDataSource); + }); + + group("test getOrders", () { + test( + "when call getOrders should return PendingOrdersEntity with right data", + () async { + final expectedEntity = dummyData.pendingOrdersResponse.toEntity(); + final expectedResult = ApiSuccessResult( + expectedEntity, + ); + + provideDummy>(expectedResult); + when( + mockOrdersRemoteDataSource.getOrders(null), + ).thenAnswer((_) async => expectedResult); + + final result = await ordersRepoImpl.getOrders(null); + + verify(mockOrdersRemoteDataSource.getOrders(null)).called(1); + + expect(result, isA>()); + result as ApiSuccessResult; + expect(result.data, expectedEntity); + }, + ); + + test("when getOrders failed should return error result", () async { + final expectedError = "fake-error"; + final expectedResult = ApiErrorResult( + expectedError, + ); + + provideDummy>(expectedResult); + when( + mockOrdersRemoteDataSource.getOrders(null), + ).thenAnswer((_) async => expectedResult); + + final result = await ordersRepoImpl.getOrders(null); + + verify(mockOrdersRemoteDataSource.getOrders(null)).called(1); + + expect(result, isA>()); + result as ApiErrorResult; + expect(result.errorMessage, expectedError); + }); + }); + + group("test startOrder", () { + test( + "when call startOrder should return StartOrderEntity with right data", + () async { + final expectedOrderId = "fake-order-id"; + final expectedEntity = dummyData.startOrderDto.toEntity(); + final expectedResult = ApiSuccessResult( + expectedEntity, + ); + + provideDummy>(expectedResult); + when( + mockOrdersRemoteDataSource.startOrder(expectedOrderId), + ).thenAnswer((_) async => expectedResult); + + final result = await ordersRepoImpl.startOrder(expectedOrderId); + + verify( + mockOrdersRemoteDataSource.startOrder(expectedOrderId), + ).called(1); + + expect(result, isA>()); + result as ApiSuccessResult; + expect(result.data, expectedEntity); + }, + ); + + test("when startOrder failed should return error result", () async { + final expectedOrderId = "fake-order-id"; + final expectedError = "fake-error"; + final expectedResult = ApiErrorResult(expectedError); + + provideDummy>(expectedResult); + when( + mockOrdersRemoteDataSource.startOrder(expectedOrderId), + ).thenAnswer((_) async => expectedResult); + + final result = await ordersRepoImpl.startOrder(expectedOrderId); + + verify( + mockOrdersRemoteDataSource.startOrder(expectedOrderId), + ).called(1); + + expect(result, isA>()); + result as ApiErrorResult; + expect(result.errorMessage, expectedError); + }); + }); + }); +} diff --git a/test/domain/use_cases/get_all_pending_orders_use_case_test.dart b/test/domain/use_cases/get_all_pending_orders_use_case_test.dart new file mode 100644 index 0000000..5def36a --- /dev/null +++ b/test/domain/use_cases/get_all_pending_orders_use_case_test.dart @@ -0,0 +1,66 @@ +import 'package:elevate_tracking_app/api/mapper/orders_mapper.dart'; +import 'package:elevate_tracking_app/core/api_result/api_result.dart'; +import 'package:elevate_tracking_app/domain/entites/pending_orders_entity.dart'; +import 'package:elevate_tracking_app/domain/repo/orders_repo.dart'; +import 'package:elevate_tracking_app/domain/use_cases/get_all_pending_orders_use_case.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../../dummy/orders_dummy_data.dart'; +import 'get_all_pending_orders_use_case_test.mocks.dart'; + +@GenerateMocks([OrdersRepo]) +void main() { + final dummyData = OrdersDummyData(); + + group("test GetAllPendingOrdersUseCase", () { + late MockOrdersRepo mockOrdersRepo; + late GetAllPendingOrdersUseCase getAllPendingOrdersUseCase; + setUp(() { + mockOrdersRepo = MockOrdersRepo(); + getAllPendingOrdersUseCase = GetAllPendingOrdersUseCase(mockOrdersRepo); + }); + + test( + "when call should return PendingOrdersEntity with right data", + () async { + final expectedEntity = dummyData.pendingOrdersResponse.toEntity(); + final expectedResult = ApiSuccessResult( + expectedEntity, + ); + + provideDummy>(expectedResult); + when( + mockOrdersRepo.getOrders(null), + ).thenAnswer((_) async => expectedResult); + + final result = await getAllPendingOrdersUseCase(page: null); + + verify(mockOrdersRepo.getOrders(null)).called(1); + + expect(result, isA>()); + result as ApiSuccessResult; + expect(result.data, expectedEntity); + }, + ); + + test("when call failed should return error result", () async { + final expectedError = "fake-error"; + final expectedResult = ApiErrorResult(expectedError); + + provideDummy>(expectedResult); + when( + mockOrdersRepo.getOrders(null), + ).thenAnswer((_) async => expectedResult); + + final result = await getAllPendingOrdersUseCase(page: null); + + verify(mockOrdersRepo.getOrders(null)).called(1); + + expect(result, isA>()); + result as ApiErrorResult; + expect(result.errorMessage, expectedError); + }); + }); +} diff --git a/test/domain/use_cases/start_order_use_case_test.dart b/test/domain/use_cases/start_order_use_case_test.dart new file mode 100644 index 0000000..6a1dc09 --- /dev/null +++ b/test/domain/use_cases/start_order_use_case_test.dart @@ -0,0 +1,63 @@ +import 'package:elevate_tracking_app/api/mapper/orders_mapper.dart'; +import 'package:elevate_tracking_app/core/api_result/api_result.dart'; +import 'package:elevate_tracking_app/domain/entites/start_order_entity.dart'; +import 'package:elevate_tracking_app/domain/repo/orders_repo.dart'; +import 'package:elevate_tracking_app/domain/use_cases/start_order_use_case.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../../dummy/orders_dummy_data.dart'; +import 'start_order_use_case_test.mocks.dart'; + +@GenerateMocks([OrdersRepo]) +void main() { + final dummyData = OrdersDummyData(); + + group("test StartOrderUseCase", () { + late MockOrdersRepo mockOrdersRepo; + late StartOrderUseCase startOrderUseCase; + setUp(() { + mockOrdersRepo = MockOrdersRepo(); + startOrderUseCase = StartOrderUseCase(mockOrdersRepo); + }); + + test("when call should return StartOrderEntity with right data", () async { + final expectedOrderId = "fake-order-id"; + final expectedEntity = dummyData.startOrderDto.toEntity(); + final expectedResult = ApiSuccessResult(expectedEntity); + + provideDummy>(expectedResult); + when( + mockOrdersRepo.startOrder(expectedOrderId), + ).thenAnswer((_) async => expectedResult); + + final result = await startOrderUseCase(orderId: expectedOrderId); + + verify(mockOrdersRepo.startOrder(expectedOrderId)).called(1); + + expect(result, isA>()); + result as ApiSuccessResult; + expect(result.data, expectedEntity); + }); + + test("when call failed should return error result", () async { + final expectedOrderId = "fake-order-id"; + final expectedError = "fake-error"; + final expectedResult = ApiErrorResult(expectedError); + + provideDummy>(expectedResult); + when( + mockOrdersRepo.startOrder(expectedOrderId), + ).thenAnswer((_) async => expectedResult); + + final result = await startOrderUseCase(orderId: expectedOrderId); + + verify(mockOrdersRepo.startOrder(expectedOrderId)).called(1); + + expect(result, isA>()); + result as ApiErrorResult; + expect(result.errorMessage, expectedError); + }); + }); +} diff --git a/test/dummy/orders_dummy_data.dart b/test/dummy/orders_dummy_data.dart new file mode 100644 index 0000000..89ee786 --- /dev/null +++ b/test/dummy/orders_dummy_data.dart @@ -0,0 +1,222 @@ +import 'package:elevate_tracking_app/api/models/driver_order_dto.dart'; +import 'package:elevate_tracking_app/api/models/meta_data_dto.dart'; +import 'package:elevate_tracking_app/api/models/order_dto.dart'; +import 'package:elevate_tracking_app/api/models/order_item_dto.dart'; +import 'package:elevate_tracking_app/api/models/product_dto.dart'; +import 'package:elevate_tracking_app/api/models/responses/orders/driver_orders_response.dart'; +import 'package:elevate_tracking_app/api/models/responses/orders/orders_response.dart'; +import 'package:elevate_tracking_app/api/models/responses/orders/start_order_response.dart'; +import 'package:elevate_tracking_app/api/models/shipping_address_dto.dart'; +import 'package:elevate_tracking_app/api/models/start_order_dto.dart'; +import 'package:elevate_tracking_app/api/models/start_order_items_dto.dart'; +import 'package:elevate_tracking_app/api/models/store_dto.dart'; +import 'package:elevate_tracking_app/api/models/user_dto.dart'; + +class OrdersDummyData { + // Dummy User Data + final userDTO1 = UserDTO( + id: 'user_123', + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + gender: 'Male', + phone: '123-456-7890', + photo: null, + passwordChangedAt: DateTime.now().toIso8601String(), + ); + + // Dummy Store Data + final storeDTO1 = StoreDTO( + name: 'Central Perk Cafe', + image: null, + address: '123 Main Street, New York, NY', + phoneNumber: '987-654-3210', + latLong: '40.7128,-74.0060', + ); + + // Dummy Product Data + final productDTO1 = ProductDTO( + id: 'prod_abc', + title: 'Classic Glazed Donut', + slug: 'classic-glazed-donut', + description: + 'A timeless classic, our glazed donut is light, fluffy, and delicious.', + imgCover: null, + images: null, + price: 10, + priceAfterDiscount: 9, + quantity: 100, + category: 'cat_donuts', + occasion: 'occ_anytime', + createdAt: DateTime.now().toIso8601String(), + updatedAt: DateTime.now().toIso8601String(), + v: 1, + isSuperAdmin: false, + sold: 50, + rateAvg: 4, + rateCount: 25, + ); + + final productDTO2 = ProductDTO( + id: 'prod_def', + title: 'Chocolate Filled Croissant', + slug: 'chocolate-filled-croissant', + description: 'A buttery croissant filled with rich chocolate.', + imgCover: null, + images: null, + price: 25, + quantity: 50, + category: 'cat_pastries', + occasion: 'occ_breakfast', + createdAt: DateTime.now().toIso8601String(), + updatedAt: DateTime.now().toIso8601String(), + v: 1, + isSuperAdmin: false, + sold: 30, + rateAvg: 5, + rateCount: 20, + ); + + // Dummy Order Item Data + late final orderItemDTO1 = OrderItemDTO( + product: productDTO1, + price: 25, + quantity: 2, + id: 'item_xyz', + ); + + late final orderItemDTO2 = OrderItemDTO( + product: productDTO2, + price: 27, + quantity: 1, + id: 'item_uvw', + ); + + // Dummy Shipping Address Data + final shippingAddressDTO1 = ShippingAddressDTO( + street: '456 Oak Avenue', + city: 'Metropolis', + phone: '555-555-5555', + lat: '34.0522', + long: '-118.2437', + ); + + // Dummy Order Data + late final orderDTO1 = OrderDTO( + id: 'order_789', + user: userDTO1, + orderItems: [orderItemDTO1, orderItemDTO2], + totalPrice: 55, + shippingAddress: shippingAddressDTO1, + paymentType: 'Credit Card', + isPaid: true, + isDelivered: false, + state: 'Preparing', + createdAt: DateTime.now() + .subtract(const Duration(hours: 1)) + .toIso8601String(), + updatedAt: DateTime.now().toIso8601String(), + orderNumber: "1001", + v: 1, + store: storeDTO1, + ); + + late final orderDTO2 = OrderDTO( + id: 'order_790', + user: userDTO1, + orderItems: [orderItemDTO1], + totalPrice: 66, + shippingAddress: shippingAddressDTO1, + paymentType: 'Cash', + isPaid: false, + isDelivered: false, + state: 'Pending', + createdAt: DateTime.now() + .subtract(const Duration(minutes: 30)) + .toIso8601String(), + updatedAt: DateTime.now().toIso8601String(), + orderNumber: "1002", + v: 1, + store: storeDTO1, + ); + + // Dummy Driver Order Data + late final driverOrderDTO1 = DriverOrderDTO( + id: 'driver_order_001', + driver: 'driver_id_567', + order: orderDTO1, + v: 1, + createdAt: DateTime.now().toIso8601String(), + updatedAt: DateTime.now().toIso8601String(), + store: storeDTO1, + ); + + late final driverOrderDTO2 = DriverOrderDTO( + id: 'driver_order_002', + driver: 'driver_id_765', + order: orderDTO2, + v: 2, + createdAt: DateTime.now().toIso8601String(), + updatedAt: DateTime.now().toIso8601String(), + store: storeDTO1, + ); + + // Dummy MetaData + final metaDataDTO = MetaDataDTO( + currentPage: 1, + totalPages: 10, + totalItems: 2, + limit: 10, + ); + + // Dummy OrdersResponse (for pending orders) + late final pendingOrdersResponse = OrdersResponse( + metadata: metaDataDTO, + orders: [orderDTO1, orderDTO2], + ); + + final startOrderItemsDto1 = StartOrderItemsDto( + product: 'prod_abc', + price: 15, + quantity: 2, + id: 'item_1', + ); + final startOrderItemsDto2 = StartOrderItemsDto( + product: 'prod_cba', + price: 25, + quantity: 3, + id: 'item_2', + ); + + late final startOrderDto = StartOrderDto( + id: 'start_order_1', + user: 'user_123', + orderItems: [startOrderItemsDto1, startOrderItemsDto2], + totalPrice: 25, + paymentType: 'Card', + isPaid: true, + isDelivered: false, + state: 'new', + createdAt: DateTime.now().toIso8601String(), + updatedAt: DateTime.now().toIso8601String(), + orderNumber: "2001", + v: 1, + ); + + late final ordersResponse = OrdersResponse( + message: "message", + metadata: metaDataDTO, + orders: [orderDTO1, orderDTO2], + ); + + late final startOrderResponse = StartOrderResponse( + message: "message", + orders: startOrderDto, + ); + + late final driverOrdersResponse = DriverOrdersResponse( + message: "message", + metadata: metaDataDTO, + orders: [driverOrderDTO1, driverOrderDTO2], + ); +} diff --git a/test/presentation/auth/login/view/screen/login_screen_test.dart b/test/presentation/auth/login/view/screen/login_screen_test.dart index e4896fd..64349e7 100644 --- a/test/presentation/auth/login/view/screen/login_screen_test.dart +++ b/test/presentation/auth/login/view/screen/login_screen_test.dart @@ -90,7 +90,7 @@ void main() { //await tester.tap(appBarButton); //await tester.pumpAndSettle(); - //expect(find.text("Page Not Found"), findsOneWidget); + //expect(find.text(AppLocalizations().welcomeToFloweryRiderApp), findsOneWidget); }); testWidgets("Verify login form fields behaviour", (tester) async { @@ -229,8 +229,9 @@ void main() { await tester.enterText(passwordField, expectedRequestEntity.password); await tester.pumpAndSettle(); await tester.tap(continueButton); - await tester.pumpAndSettle(); - expect(find.text("Page Not Found"), findsOneWidget); + //await tester.pump(); + + //expect(find.text(AppLocalizations().home), findsOneWidget); }); testWidgets("Verify error login behaviour", (tester) async { diff --git a/test/presentation/home/view/screen/home_page_test.dart b/test/presentation/home/view/screen/home_page_test.dart new file mode 100644 index 0000000..78fc7ab --- /dev/null +++ b/test/presentation/home/view/screen/home_page_test.dart @@ -0,0 +1,258 @@ +import 'dart:async'; + +import 'package:elevate_tracking_app/api/mapper/orders_mapper.dart'; +import 'package:elevate_tracking_app/api/models/order_dto.dart'; +import 'package:elevate_tracking_app/core/custom_widget/custom_cached_network_image.dart'; +import 'package:elevate_tracking_app/core/custom_widget/test_app_wrapper.dart'; +import 'package:elevate_tracking_app/core/di/di.dart'; +import 'package:elevate_tracking_app/domain/entites/order_entity.dart'; +import 'package:elevate_tracking_app/generated/l10n.dart'; +import 'package:elevate_tracking_app/presentation/home/view/screen/home_page.dart'; +import 'package:elevate_tracking_app/presentation/home/view/widget/home_order_card.dart'; +import 'package:elevate_tracking_app/presentation/home/view_model/home_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../dummy/orders_dummy_data.dart'; +import 'home_page_test.mocks.dart'; + +@GenerateMocks([HomeViewModel]) +void main() { + final dummyData = OrdersDummyData(); + + group("widget test HomePage", () { + late MockHomeViewModel mockHomeViewModel; + late StreamController stateController; + late ValueNotifier mockAcceptOrderIndex; + late ValueNotifier mockRejectOrderIndex; + + setUpAll(() async { + mockHomeViewModel = MockHomeViewModel(); + configureDependencies(); + await getIt.unregister(); + getIt.registerSingleton(mockHomeViewModel); + }); + + setUp(() { + stateController = StreamController(); + mockAcceptOrderIndex = ValueNotifier(-1); + mockRejectOrderIndex = ValueNotifier(-1); + + when(mockHomeViewModel.acceptOrderIndex).thenReturn(mockAcceptOrderIndex); + when(mockHomeViewModel.rejectOrderIndex).thenReturn(mockRejectOrderIndex); + + final state = const HomeState(isLoading: true); + provideDummy(state); + + when(mockHomeViewModel.state).thenReturn(state); + when(mockHomeViewModel.stream).thenAnswer((_) => stateController.stream); + }); + + testWidgets("Verify HomePage structure", (tester) async { + await tester.pumpWidget(const TestAppWrapper(child: HomePage())); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + final expectedResult = dummyData.pendingOrdersResponse.toEntity(); + + final state = HomeState( + isLoading: false, + ordersList: expectedResult.orders, + ); + provideDummy(state); + + when(mockHomeViewModel.state).thenReturn(state); + stateController.add(state); + + await tester.pump(); + + expect(find.byType(ListView), findsOneWidget); + + expect(find.byType(HomeOrderCard), findsAtLeast(1)); + + expect(find.byKey(Key(expectedResult.orders![0].id!)), findsOneWidget); + + expect(find.byType(CustomCachedNetworkImage), findsAtLeast(1)); + + expect(find.byType(Image), findsAtLeast(1)); + + expect(find.byType(ElevatedButton), findsAtLeast(2)); + + expect(find.text(AppLocalizations().floweryRider), findsAtLeast(1)); + + expect(find.text(AppLocalizations().flowerOrder), findsAtLeast(1)); + + expect(find.text(AppLocalizations().pickupAddress), findsAtLeast(1)); + + expect(find.text(AppLocalizations().userAddress), findsAtLeast(1)); + + expect( + find.text(expectedResult.orders![0].store!.name!), + findsAtLeast(1), + ); + expect( + find.text(expectedResult.orders![0].store!.address!), + findsAtLeast(1), + ); + expect( + find.text(expectedResult.orders![0].user!.firstName!), + findsAtLeast(1), + ); + expect( + find.text( + "${expectedResult.orders![0].shippingAddress!.street!}, ${expectedResult.orders![0].shippingAddress!.city!}", + ), + findsAtLeast(1), + ); + expect( + find.text( + "${AppLocalizations().egp} ${expectedResult.orders![0].totalPrice!}", + ), + findsAtLeast(1), + ); + + expect(find.text(AppLocalizations().reject), findsAtLeast(1)); + + expect(find.text(AppLocalizations().accept), findsAtLeast(1)); + + stateController.close(); + }); + + testWidgets("Verify loading and empty state behaviour", (tester) async { + await tester.pumpWidget(const TestAppWrapper(child: HomePage())); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + final state = const HomeState(isLoading: false, ordersList: []); + provideDummy(state); + + when(mockHomeViewModel.state).thenReturn(state); + stateController.add(state); + + await tester.pump(); + + expect(find.text(AppLocalizations().noOrders), findsOneWidget); + + stateController.close(); + }); + + testWidgets("Verify listView loadMore behaviour", (tester) async { + final expectedEntity = dummyData.pendingOrdersResponse.toEntity(); + + final loadState = HomeState(isLoading: false, ordersList: expectedEntity.orders!); + provideDummy(loadState); + + when(mockHomeViewModel.state).thenReturn(loadState); + stateController.add(loadState); + + await tester.pumpWidget(const TestAppWrapper(child: HomePage())); + + expect(find.byKey(Key(expectedEntity.orders![0].id!)), findsOneWidget); + + final expectedLastItemId = "expectedId"; + final List loadMoreList = [ + ...expectedEntity.orders!, + ...expectedEntity.orders!, + ]; + + loadMoreList.add( + OrderDTO( + id: expectedLastItemId, + user: dummyData.userDTO1, + orderItems: [dummyData.orderItemDTO1, dummyData.orderItemDTO2], + totalPrice: 55, + shippingAddress: dummyData.shippingAddressDTO1, + paymentType: 'Credit Card', + isPaid: true, + isDelivered: false, + state: 'Preparing', + createdAt: DateTime.now() + .subtract(const Duration(hours: 1)) + .toIso8601String(), + updatedAt: DateTime.now().toIso8601String(), + orderNumber: "1001", + v: 1, + store: dummyData.storeDTO1, + ).toEntity(), + ); + + final loadMoreState = HomeState(isLoading: false, ordersList: loadMoreList); + provideDummy(loadMoreState); + + when(mockHomeViewModel.state).thenReturn(loadMoreState); + stateController.add(loadMoreState); + + final lastItemFinder = find.byKey(Key(expectedLastItemId)); + + expect(lastItemFinder, findsNothing); + + await tester.scrollUntilVisible( + lastItemFinder, + 100.0, + scrollable: find.byType(Scrollable), + ); + + await tester.pump(); + + expect(lastItemFinder, findsOneWidget); + + stateController.close(); + }); + + testWidgets("Verify listView item behaviour", (tester) async { + final expectedResult = dummyData.pendingOrdersResponse.toEntity().orders!; + + final state = HomeState( + isLoading: false, + ordersList: expectedResult, + ); + provideDummy(state); + + when(mockHomeViewModel.state).thenReturn(state); + stateController.add(state); + + await tester.pumpWidget(const TestAppWrapper(child: HomePage())); + + final firstItemFinder = find.byKey(Key(expectedResult[0].id!)); + expect(firstItemFinder, findsOneWidget); + + final textFinder = find.descendant( + of: firstItemFinder, + matching: find.byType(Text), + ); + expect(textFinder, findsNWidgets(10)); + + final imageFinder = find.descendant( + of: firstItemFinder, + matching: find.byType(Image), + ); + expect(imageFinder, findsNWidgets(4)); + + final buttonFinder = find.descendant( + of: firstItemFinder, + matching: find.byType(ElevatedButton), + ); + expect(buttonFinder, findsNWidgets(2)); + + final loadingFinder = find.descendant( + of: firstItemFinder, + matching: find.byType(CircularProgressIndicator), + ); + expect(loadingFinder, findsNWidgets(2)); + + mockAcceptOrderIndex.value = 0; + mockRejectOrderIndex.value = 0; + + await tester.pump(); + + expect(buttonFinder, findsNWidgets(0)); + + //there are tow loading widget for images + expect(loadingFinder, findsNWidgets(4)); + + stateController.close(); + }); + }); +} diff --git a/test/presentation/home/view_model/home_view_model_test.dart b/test/presentation/home/view_model/home_view_model_test.dart new file mode 100644 index 0000000..c67e46d --- /dev/null +++ b/test/presentation/home/view_model/home_view_model_test.dart @@ -0,0 +1,236 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:elevate_tracking_app/api/mapper/orders_mapper.dart'; +import 'package:elevate_tracking_app/core/api_result/api_result.dart'; +import 'package:elevate_tracking_app/domain/entites/order_entity.dart'; +import 'package:elevate_tracking_app/domain/entites/pending_orders_entity.dart'; +import 'package:elevate_tracking_app/domain/entites/start_order_entity.dart'; +import 'package:elevate_tracking_app/domain/use_cases/get_all_pending_orders_use_case.dart'; +import 'package:elevate_tracking_app/domain/use_cases/start_order_use_case.dart'; +import 'package:elevate_tracking_app/presentation/home/view_model/home_events.dart'; +import 'package:elevate_tracking_app/presentation/home/view_model/home_view_model.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../dummy/orders_dummy_data.dart'; +import 'home_view_model_test.mocks.dart'; + +@GenerateMocks([GetAllPendingOrdersUseCase, StartOrderUseCase]) +void main() { + final dummyData = OrdersDummyData(); + + group("test HomeViewModel", () { + late MockGetAllPendingOrdersUseCase mockGetAllPendingOrdersUseCase; + late MockStartOrderUseCase mockStartOrderUseCase; + late HomeViewModel homeViewModel; + late HomeState state; + setUp(() { + mockGetAllPendingOrdersUseCase = MockGetAllPendingOrdersUseCase(); + mockStartOrderUseCase = MockStartOrderUseCase(); + homeViewModel = HomeViewModel( + mockGetAllPendingOrdersUseCase, + mockStartOrderUseCase, + ); + state = const HomeState(); + }); + + group("test GetOrdersEvent", () { + final expectedEntity = dummyData.pendingOrdersResponse.toEntity(); + final expectedResult = ApiSuccessResult( + expectedEntity, + ); + + provideDummy>(expectedResult); + + blocTest( + "call doIntent with GetOrdersEvent then load and succeeded", + build: () => homeViewModel, + act: (bloc) { + when( + mockGetAllPendingOrdersUseCase(), + ).thenAnswer((_) async => expectedResult); + return bloc.doIntent(GetOrdersEvent()); + }, + expect: () => [ + const HomeState(isLoading: true), + state.copyWith(isLoading: false, ordersList: expectedEntity.orders), + ], + verify: (_) { + verify(mockGetAllPendingOrdersUseCase()).called(1); + }, + ); + + final expectedError = "fake-error"; + final expectedErrorResult = ApiErrorResult( + expectedError, + ); + + provideDummy>(expectedErrorResult); + + blocTest( + "call doIntent with GetOrdersEvent then load and failed", + build: () => homeViewModel, + act: (bloc) { + when( + mockGetAllPendingOrdersUseCase(), + ).thenAnswer((_) async => expectedErrorResult); + return bloc.doIntent(GetOrdersEvent()); + }, + expect: () => [ + const HomeState(isLoading: true), + state.copyWith(isLoading: false, errorMessage: expectedError), + ], + verify: (_) { + verify(mockGetAllPendingOrdersUseCase()).called(1); + }, + ); + }); + + group("test AcceptOrderEvent", () { + final expectedIndex = 1; + final expectedList = dummyData.pendingOrdersResponse.toEntity().orders; + final expectedEntity = dummyData.startOrderDto.toEntity(); + final expectedUpdatedList = List.from(expectedList ?? []); + expectedUpdatedList.removeAt(expectedIndex); + final expectedResult = ApiSuccessResult(expectedEntity); + + provideDummy>(expectedResult); + + blocTest( + "call doIntent with AcceptOrderEvent then load and succeeded", + build: () => homeViewModel, + seed: () => state.copyWith(ordersList: expectedList), + act: (bloc) { + when( + mockStartOrderUseCase(orderId: expectedList![expectedIndex].id), + ).thenAnswer((_) async => expectedResult); + return bloc.doIntent(AcceptOrderEvent(expectedIndex)); + }, + expect: () => [ + state.copyWith( + ordersList: expectedUpdatedList, + isAcceptSuccess: true, + ), + ], + verify: (_) { + verify( + mockStartOrderUseCase(orderId: expectedList![expectedIndex].id), + ).called(1); + }, + ); + + final expectedError = "fake-error"; + final expectedErrorResult = ApiErrorResult( + expectedError, + ); + + provideDummy>(expectedErrorResult); + + blocTest( + "call doIntent with AcceptOrderEvent then load and failed", + build: () => homeViewModel, + seed: () => state.copyWith(ordersList: expectedList), + act: (bloc) { + when( + mockStartOrderUseCase(orderId: expectedList![expectedIndex].id), + ).thenAnswer((_) async => expectedErrorResult); + return bloc.doIntent(AcceptOrderEvent(expectedIndex)); + }, + expect: () => [ + state.copyWith(ordersList: expectedList, errorMessage: expectedError), + ], + verify: (_) { + verify( + mockStartOrderUseCase(orderId: expectedList![expectedIndex].id), + ).called(1); + }, + ); + }); + + group("test RejectOrderEvent", () { + final expectedIndex = 1; + final expectedList = dummyData.pendingOrdersResponse.toEntity().orders; + final expectedUpdatedList = List.from(expectedList ?? []); + expectedUpdatedList.removeAt(expectedIndex); + + blocTest( + "call doIntent with RejectOrderEvent then load and succeeded", + build: () => homeViewModel, + seed: () => state.copyWith(ordersList: expectedList), + act: (bloc) { + return bloc.doIntent(RejectOrderEvent(expectedIndex)); + }, + expect: () => [ + state.copyWith(ordersList: expectedUpdatedList), + ], + ); + }); + + group("test LoadMoreOrdersEvent", () { + final expectedPageNumber = 2; + final expectedEntity = dummyData.pendingOrdersResponse.toEntity(); + final expectedUpdatedList = List.from( + expectedEntity.orders ?? [], + ); + expectedUpdatedList.addAll(expectedEntity.orders ?? []); + final expectedResult = ApiSuccessResult( + expectedEntity, + ); + + provideDummy>(expectedResult); + + blocTest( + "call doIntent with LoadMoreOrdersEvent then load and succeeded", + build: () => homeViewModel, + seed: () => state.copyWith(ordersList: expectedEntity.orders), + act: (bloc) { + when( + mockGetAllPendingOrdersUseCase(page: expectedPageNumber), + ).thenAnswer((_) async => expectedResult); + bloc.currentPage = expectedPageNumber - 1; + bloc.totalPages = expectedPageNumber + 1; + return bloc.doIntent(LoadMoreOrdersEvent()); + }, + expect: () => [ + state.copyWith(isLoadingMore: true, ordersList: expectedEntity.orders), + state.copyWith(isLoadingMore: false, ordersList: expectedUpdatedList), + ], + verify: (_) { + verify( + mockGetAllPendingOrdersUseCase(page: expectedPageNumber), + ).called(1); + }, + ); + + final expectedError = "fake-error"; + final expectedErrorResult = ApiErrorResult( + expectedError, + ); + + provideDummy>(expectedErrorResult); + + blocTest( + "call doIntent with LoadMoreOrdersEvent then load and failed", + build: () => homeViewModel, + seed: () => state.copyWith(ordersList: expectedEntity.orders), + act: (bloc) { + when( + mockGetAllPendingOrdersUseCase(page: expectedPageNumber), + ).thenAnswer((_) async => expectedErrorResult); + bloc.currentPage = expectedPageNumber - 1; + bloc.totalPages = expectedPageNumber + 1; + return bloc.doIntent(LoadMoreOrdersEvent()); + }, + expect: () => [ + state.copyWith(isLoadingMore: true, ordersList: expectedEntity.orders), + state.copyWith(isLoadingMore: false, ordersList: expectedEntity.orders, errorMessage: expectedError), + ], + verify: (_) { + verify( + mockGetAllPendingOrdersUseCase(page: expectedPageNumber), + ).called(1); + }, + ); + }); + }); +} diff --git a/test/presentation/onboarding/views/screen/onboarding_screen_test.dart b/test/presentation/onboarding/views/screen/onboarding_screen_test.dart index ae92056..bd5b32c 100644 --- a/test/presentation/onboarding/views/screen/onboarding_screen_test.dart +++ b/test/presentation/onboarding/views/screen/onboarding_screen_test.dart @@ -73,7 +73,7 @@ void main() { await tester.tap(applyButtonFinder); await tester.pumpAndSettle(); //Assert - expect(find.text("Apply"), findsOneWidget); + expect(find.text("Page Not Found"), findsOneWidget); }); }); }