From 18ac49e87e53029368ce2d5966674ae749f41a01 Mon Sep 17 00:00:00 2001 From: youssefmdev22 Date: Fri, 3 Oct 2025 01:30:01 +0300 Subject: [PATCH 1/4] add home page --- lib/api/client/api_client.dart | 8 ++ .../orders_remote_data_source_impl.dart | 23 ++++ lib/api/mapper/orders_mapper.dart | 110 ++++++++++++++++++ lib/api/models/driver_order_dto.dart | 40 +++++++ lib/api/models/meta_data_dto.dart | 29 +++++ lib/api/models/order_dto.dart | 60 ++++++++++ lib/api/models/order_item_dto.dart | 30 +++++ lib/api/models/product_dto.dart | 71 +++++++++++ .../orders/driver_orders_response.dart | 45 +++++++ .../responses/orders/orders_response.dart | 44 +++++++ lib/api/models/store_dto.dart | 32 +++++ lib/api/models/user_dto.dart | 41 +++++++ lib/core/constants/end_points.dart | 2 + .../orders_remote_data_source.dart | 6 + lib/data/repo/orders_repo_impl.dart | 18 +++ lib/domain/entites/driver_order_entity.dart | 22 ++++ lib/domain/entites/order_entity.dart | 35 ++++++ lib/domain/entites/order_item_entity.dart | 10 ++ lib/domain/entites/product_entity.dart | 41 +++++++ lib/domain/entites/store_entity.dart | 15 +++ lib/domain/entites/user_entity.dart | 21 ++++ lib/domain/repo/orders_repo.dart | 7 ++ .../get_all_pending_orders_use_case.dart | 16 +++ lib/l10n/intl_en.arb | 5 +- .../home/view/screen/home_page.dart | 10 ++ .../home/view_model/home_events.dart | 3 + .../home/view_model/home_state.dart | 24 ++++ .../home/view_model/home_view_model.dart | 38 ++++++ pubspec.yaml | 1 + 29 files changed, 806 insertions(+), 1 deletion(-) create mode 100644 lib/api/data_source/orders_remote_data_source_impl.dart create mode 100644 lib/api/mapper/orders_mapper.dart create mode 100644 lib/api/models/driver_order_dto.dart create mode 100644 lib/api/models/meta_data_dto.dart create mode 100644 lib/api/models/order_dto.dart create mode 100644 lib/api/models/order_item_dto.dart create mode 100644 lib/api/models/product_dto.dart create mode 100644 lib/api/models/responses/orders/driver_orders_response.dart create mode 100644 lib/api/models/responses/orders/orders_response.dart create mode 100644 lib/api/models/store_dto.dart create mode 100644 lib/api/models/user_dto.dart create mode 100644 lib/data/data_source/orders_remote_data_source.dart create mode 100644 lib/data/repo/orders_repo_impl.dart create mode 100644 lib/domain/entites/driver_order_entity.dart create mode 100644 lib/domain/entites/order_entity.dart create mode 100644 lib/domain/entites/order_item_entity.dart create mode 100644 lib/domain/entites/product_entity.dart create mode 100644 lib/domain/entites/store_entity.dart create mode 100644 lib/domain/entites/user_entity.dart create mode 100644 lib/domain/repo/orders_repo.dart create mode 100644 lib/domain/use_cases/get_all_pending_orders_use_case.dart create mode 100644 lib/presentation/home/view/screen/home_page.dart create mode 100644 lib/presentation/home/view_model/home_events.dart create mode 100644 lib/presentation/home/view_model/home_state.dart create mode 100644 lib/presentation/home/view_model/home_view_model.dart diff --git a/lib/api/client/api_client.dart b/lib/api/client/api_client.dart index 84154eb..b8e5f46 100644 --- a/lib/api/client/api_client.dart +++ b/lib/api/client/api_client.dart @@ -1,10 +1,12 @@ import 'package:dio/dio.dart'; import 'package:elevate_tracking_app/api/models/requests/login_request.dart'; import 'package:elevate_tracking_app/api/models/responses/login_response.dart'; +import 'package:elevate_tracking_app/api/models/responses/orders/driver_orders_response.dart'; import 'package:injectable/injectable.dart'; import 'package:retrofit/retrofit.dart'; import '../../core/constants/end_points.dart'; +import '../models/responses/orders/orders_response.dart'; part 'api_client.g.dart'; @@ -16,4 +18,10 @@ abstract class ApiClient { @POST(EndPoints.login) Future login(@Body() LoginRequest loginRequest); + + @GET(EndPoints.orders) + Future getOrders(); + + @GET(EndPoints.driverOrders) + Future getDriverOrders(); } 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..7eef931 --- /dev/null +++ b/lib/api/data_source/orders_remote_data_source_impl.dart @@ -0,0 +1,23 @@ +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_entity.dart'; +import 'package:injectable/injectable.dart'; + +import '../../data/data_source/orders_remote_data_source.dart'; + +@Injectable(as: OrdersRemoteDataSource) +class OrdersRemoteDataSourceImpl implements OrdersRemoteDataSource { + final ApiClient _apiClient; + + OrdersRemoteDataSourceImpl(this._apiClient); + + @override + Future>> getOrders() { + return safeApiCall( + () => _apiClient.getOrders(), + (response) => response.orders!.map((dto) => dto.toEntity()).toList(), + ); + } +} diff --git a/lib/api/mapper/orders_mapper.dart b/lib/api/mapper/orders_mapper.dart new file mode 100644 index 0000000..150a31e --- /dev/null +++ b/lib/api/mapper/orders_mapper.dart @@ -0,0 +1,110 @@ +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/order_entity.dart'; +import '../../domain/entites/order_item_entity.dart'; +import '../../domain/entites/product_entity.dart'; +import '../../domain/entites/store_entity.dart'; +import '../../domain/entites/user_entity.dart'; +import '../models/order_dto.dart'; +import '../models/order_item_dto.dart'; +import '../models/product_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, + 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, + ); + } +} 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..fa5467f --- /dev/null +++ b/lib/api/models/order_dto.dart @@ -0,0 +1,60 @@ +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: "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.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/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/end_points.dart b/lib/core/constants/end_points.dart index e8e8db8..66a6430 100644 --- a/lib/core/constants/end_points.dart +++ b/lib/core/constants/end_points.dart @@ -1,3 +1,5 @@ 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"; } 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..8d102e0 --- /dev/null +++ b/lib/data/data_source/orders_remote_data_source.dart @@ -0,0 +1,6 @@ +import '../../core/api_result/api_result.dart'; +import '../../domain/entites/order_entity.dart'; + +abstract interface class OrdersRemoteDataSource { + Future>> getOrders(); +} \ No newline at end of file diff --git a/lib/data/repo/orders_repo_impl.dart b/lib/data/repo/orders_repo_impl.dart new file mode 100644 index 0000000..111be98 --- /dev/null +++ b/lib/data/repo/orders_repo_impl.dart @@ -0,0 +1,18 @@ +import 'package:elevate_tracking_app/core/api_result/api_result.dart'; + +import 'package:elevate_tracking_app/domain/entites/order_entity.dart'; +import 'package:injectable/injectable.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() { + return _ordersRemoteDataSource.getOrders(); + } +} \ 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..508f10d --- /dev/null +++ b/lib/domain/entites/driver_order_entity.dart @@ -0,0 +1,22 @@ +import 'package:elevate_tracking_app/domain/entites/order_entity.dart'; +import 'package:elevate_tracking_app/domain/entites/store_entity.dart'; + +class DriverOrderEntity { + final String? id; + final String? driver; + final OrderEntity? order; + final int? v; + final String? createdAt; + final String? updatedAt; + final StoreEntity? store; + + DriverOrderEntity({ + this.id, + this.driver, + this.order, + this.v, + this.createdAt, + this.updatedAt, + this.store, + }); +} diff --git a/lib/domain/entites/order_entity.dart b/lib/domain/entites/order_entity.dart new file mode 100644 index 0000000..b9582d4 --- /dev/null +++ b/lib/domain/entites/order_entity.dart @@ -0,0 +1,35 @@ +import 'package:elevate_tracking_app/domain/entites/order_item_entity.dart'; +import 'package:elevate_tracking_app/domain/entites/store_entity.dart'; +import 'package:elevate_tracking_app/domain/entites/user_entity.dart'; + +class OrderEntity { + final String? id; + final UserEntity? 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; + final StoreEntity? store; + + OrderEntity({ + this.id, + this.user, + this.orderItems, + this.totalPrice, + this.paymentType, + this.isPaid, + this.isDelivered, + this.state, + this.createdAt, + this.updatedAt, + this.orderNumber, + this.v, + this.store, + }); +} diff --git a/lib/domain/entites/order_item_entity.dart b/lib/domain/entites/order_item_entity.dart new file mode 100644 index 0000000..91e8446 --- /dev/null +++ b/lib/domain/entites/order_item_entity.dart @@ -0,0 +1,10 @@ +import 'package:elevate_tracking_app/domain/entites/product_entity.dart'; + +class OrderItemEntity { + final ProductEntity? product; + final int? price; + final int? quantity; + final String? id; + + OrderItemEntity({this.product, this.price, this.quantity, this.id}); +} diff --git a/lib/domain/entites/product_entity.dart b/lib/domain/entites/product_entity.dart new file mode 100644 index 0000000..4013789 --- /dev/null +++ b/lib/domain/entites/product_entity.dart @@ -0,0 +1,41 @@ +class ProductEntity { + 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; + + 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, + }); +} diff --git a/lib/domain/entites/store_entity.dart b/lib/domain/entites/store_entity.dart new file mode 100644 index 0000000..5eae253 --- /dev/null +++ b/lib/domain/entites/store_entity.dart @@ -0,0 +1,15 @@ +class StoreEntity { + final String? name; + final String? image; + final String? address; + final String? phoneNumber; + final String? latLong; + + StoreEntity({ + this.name, + this.image, + this.address, + this.phoneNumber, + this.latLong, + }); +} diff --git a/lib/domain/entites/user_entity.dart b/lib/domain/entites/user_entity.dart new file mode 100644 index 0000000..23f8fd5 --- /dev/null +++ b/lib/domain/entites/user_entity.dart @@ -0,0 +1,21 @@ +class UserEntity { + final String? id; + final String? firstName; + final String? lastName; + final String? email; + final String? gender; + final String? phone; + final String? photo; + final String? passwordChangedAt; + + UserEntity({ + this.id, + this.firstName, + this.lastName, + this.email, + this.gender, + this.phone, + this.photo, + this.passwordChangedAt, + }); +} diff --git a/lib/domain/repo/orders_repo.dart b/lib/domain/repo/orders_repo.dart new file mode 100644 index 0000000..c4e7693 --- /dev/null +++ b/lib/domain/repo/orders_repo.dart @@ -0,0 +1,7 @@ +import 'package:elevate_tracking_app/core/api_result/api_result.dart'; + +import '../entites/order_entity.dart'; + +abstract interface class OrdersRepo { + Future>> getOrders(); +} 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..2c23db6 --- /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/order_entity.dart'; + +@injectable +class GetAllPendingOrdersUseCase { + final OrdersRepo _ordersRepo; + + GetAllPendingOrdersUseCase(this._ordersRepo); + + Future>> call() async { + return await _ordersRepo.getOrders(); + } +} diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 1838905..999bf92 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -37,5 +37,8 @@ "enterYourEmail": "Enter your email", "enterYourPassword": "Enter your password", "continueWord": "Continue", - "error": "Error" + "error": "Error", + "home": "Home", + "orders": "Orders", + "profile": "Profile" } \ No newline at end of file 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..e6e71aa --- /dev/null +++ b/lib/presentation/home/view/screen/home_page.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Placeholder(); + } +} \ No newline at end of file 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..68c1d11 --- /dev/null +++ b/lib/presentation/home/view_model/home_events.dart @@ -0,0 +1,3 @@ +sealed class HomeEvents {} + +class TestEvent extends HomeEvents {} 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..11f80d5 --- /dev/null +++ b/lib/presentation/home/view_model/home_state.dart @@ -0,0 +1,24 @@ +part of 'home_view_model.dart'; + +final class HomeState extends Equatable { + final bool isLoading; + final String? errorMessage; + final List? orders; + + const HomeState({this.isLoading = false, this.errorMessage, this.orders}); + + HomeState copyWith({ + bool? isLoading, + String? errorMessage, + List? orders, + }) { + return HomeState( + isLoading: isLoading ?? this.isLoading, + errorMessage: errorMessage ?? this.errorMessage, + orders: orders ?? this.orders, + ); + } + + @override + List get props => [isLoading, errorMessage, orders]; +} 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..73765f8 --- /dev/null +++ b/lib/presentation/home/view_model/home_view_model.dart @@ -0,0 +1,38 @@ +import 'package:bloc/bloc.dart'; +import 'package:elevate_tracking_app/core/api_result/api_result.dart'; +import 'package:equatable/equatable.dart'; +import 'package:injectable/injectable.dart'; + +import '../../../domain/entites/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; + + HomeViewModel(this._getAllPendingOrdersUseCase) : super(const HomeState()); + + void doIntent(HomeEvents events) { + switch (events) { + case TestEvent(): + } + } + + Future getOrders() async { + emit(const HomeState(isLoading: true)); + final result = await _getAllPendingOrdersUseCase(); + switch (result) { + case ApiSuccessResult>(): + emit(state.copyWith(isLoading: false, orders: result.data)); + break; + case ApiErrorResult>(): + emit( + state.copyWith(isLoading: false, errorMessage: result.errorMessage), + ); + break; + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 89c1e5e..3a4ba45 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,6 +53,7 @@ dependencies: shared_preferences: ^2.5.3 image_picker: ^1.2.0 go_router: ^16.2.2 + provider: ^6.1.5+1 dev_dependencies: flutter_test: From d255a8511b3bf4f4e183e458e49ff1e845402fe2 Mon Sep 17 00:00:00 2001 From: youssefmdev22 Date: Fri, 3 Oct 2025 17:07:09 +0300 Subject: [PATCH 2/4] update home page --- lib/api/client/api_client.dart | 6 +- .../orders_remote_data_source_impl.dart | 9 +++ lib/api/mapper/orders_mapper.dart | 34 ++++++++++++ .../orders/start_order_response.dart | 29 ++++++++++ lib/api/models/start_order_dto.dart | 55 +++++++++++++++++++ lib/api/models/start_order_items_dto.dart | 25 +++++++++ lib/core/constants/end_points.dart | 1 + lib/core/router/app_router.dart | 7 +-- lib/core/router/route_names.dart | 1 - .../orders_remote_data_source.dart | 2 + lib/data/repo/orders_repo_impl.dart | 6 ++ lib/domain/entites/start_order_entity.dart | 31 +++++++++++ .../entites/start_order_items_entity.dart | 8 +++ lib/domain/repo/orders_repo.dart | 2 + .../use_cases/start_order_use_case.dart | 16 ++++++ lib/l10n/intl_en.arb | 4 +- .../auth/login/view/screen/login_screen.dart | 2 +- .../home/view/screen/home_page.dart | 42 +++++++++++++- .../home/view/widget/home_info_card.dart | 0 .../home/view/widget/home_order_card.dart | 12 ++++ .../home/view_model/home_view_model.dart | 5 +- .../home/views/screen/home_screen.dart | 10 ---- .../views/widgets/custom_page_view.dart | 4 +- 23 files changed, 283 insertions(+), 28 deletions(-) create mode 100644 lib/api/models/responses/orders/start_order_response.dart create mode 100644 lib/api/models/start_order_dto.dart create mode 100644 lib/api/models/start_order_items_dto.dart create mode 100644 lib/domain/entites/start_order_entity.dart create mode 100644 lib/domain/entites/start_order_items_entity.dart create mode 100644 lib/domain/use_cases/start_order_use_case.dart create mode 100644 lib/presentation/home/view/widget/home_info_card.dart create mode 100644 lib/presentation/home/view/widget/home_order_card.dart delete mode 100644 lib/presentation/home/views/screen/home_screen.dart diff --git a/lib/api/client/api_client.dart b/lib/api/client/api_client.dart index b8e5f46..1cf239a 100644 --- a/lib/api/client/api_client.dart +++ b/lib/api/client/api_client.dart @@ -2,6 +2,7 @@ import 'package:dio/dio.dart'; import 'package:elevate_tracking_app/api/models/requests/login_request.dart'; import 'package:elevate_tracking_app/api/models/responses/login_response.dart'; import 'package:elevate_tracking_app/api/models/responses/orders/driver_orders_response.dart'; +import 'package:elevate_tracking_app/api/models/responses/orders/start_order_response.dart'; import 'package:injectable/injectable.dart'; import 'package:retrofit/retrofit.dart'; @@ -24,4 +25,7 @@ abstract class ApiClient { @GET(EndPoints.driverOrders) Future getDriverOrders(); -} + + @POST("${EndPoints.startOrder}/{orderId}") + Future startOrder(@Path("orderId") String orderId); +} \ No newline at end of file diff --git a/lib/api/data_source/orders_remote_data_source_impl.dart b/lib/api/data_source/orders_remote_data_source_impl.dart index 7eef931..21b35ab 100644 --- a/lib/api/data_source/orders_remote_data_source_impl.dart +++ b/lib/api/data_source/orders_remote_data_source_impl.dart @@ -3,6 +3,7 @@ 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_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'; @@ -20,4 +21,12 @@ class OrdersRemoteDataSourceImpl implements OrdersRemoteDataSource { (response) => response.orders!.map((dto) => dto.toEntity()).toList(), ); } + + @override + Future> startOrder(String orderId) { + return safeApiCall( + () => _apiClient.startOrder(orderId), + (response) => response.orders!.toEntity(), + ); + } } diff --git a/lib/api/mapper/orders_mapper.dart b/lib/api/mapper/orders_mapper.dart index 150a31e..a769c9a 100644 --- a/lib/api/mapper/orders_mapper.dart +++ b/lib/api/mapper/orders_mapper.dart @@ -4,11 +4,15 @@ import 'package:elevate_tracking_app/domain/entites/driver_order_entity.dart'; import '../../domain/entites/order_entity.dart'; import '../../domain/entites/order_item_entity.dart'; import '../../domain/entites/product_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/order_dto.dart'; import '../models/order_item_dto.dart'; import '../models/product_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'; @@ -108,3 +112,33 @@ extension UserMapper on UserDTO { ); } } + +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, + ); + } +} 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/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/core/constants/end_points.dart b/lib/core/constants/end_points.dart index 66a6430..35eb55d 100644 --- a/lib/core/constants/end_points.dart +++ b/lib/core/constants/end_points.dart @@ -2,4 +2,5 @@ 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/router/app_router.dart b/lib/core/router/app_router.dart index 592e649..9ec59b1 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -1,6 +1,5 @@ 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/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 +25,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(), @@ -43,7 +38,7 @@ abstract class AppRouter { static GoRouter router(String? token) { return GoRouter( - initialLocation: token == null ? RouteNames.onboarding : RouteNames.home, + 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..0ef6aa4 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"; diff --git a/lib/data/data_source/orders_remote_data_source.dart b/lib/data/data_source/orders_remote_data_source.dart index 8d102e0..82e489f 100644 --- a/lib/data/data_source/orders_remote_data_source.dart +++ b/lib/data/data_source/orders_remote_data_source.dart @@ -1,6 +1,8 @@ import '../../core/api_result/api_result.dart'; import '../../domain/entites/order_entity.dart'; +import '../../domain/entites/start_order_entity.dart'; abstract interface class OrdersRemoteDataSource { Future>> getOrders(); + Future> startOrder(String orderId); } \ No newline at end of file diff --git a/lib/data/repo/orders_repo_impl.dart b/lib/data/repo/orders_repo_impl.dart index 111be98..a935cb2 100644 --- a/lib/data/repo/orders_repo_impl.dart +++ b/lib/data/repo/orders_repo_impl.dart @@ -1,6 +1,7 @@ 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/start_order_entity.dart'; import 'package:injectable/injectable.dart'; import '../../domain/repo/orders_repo.dart'; @@ -15,4 +16,9 @@ class OrdersRepoImpl implements OrdersRepo { Future>> getOrders() { return _ordersRemoteDataSource.getOrders(); } + + @override + Future> startOrder(String orderId) { + return _ordersRemoteDataSource.startOrder(orderId); + } } \ No newline at end of file diff --git a/lib/domain/entites/start_order_entity.dart b/lib/domain/entites/start_order_entity.dart new file mode 100644 index 0000000..f97faa1 --- /dev/null +++ b/lib/domain/entites/start_order_entity.dart @@ -0,0 +1,31 @@ +import 'package:elevate_tracking_app/domain/entites/start_order_items_entity.dart'; + +class StartOrderEntity { + 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; + + 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, + }); +} 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..6f29440 --- /dev/null +++ b/lib/domain/entites/start_order_items_entity.dart @@ -0,0 +1,8 @@ +class StartOrderItemsEntity { + final String? product; + final int? price; + final int? quantity; + final String? id; + + StartOrderItemsEntity({this.product, this.price, this.quantity, this.id}); +} diff --git a/lib/domain/repo/orders_repo.dart b/lib/domain/repo/orders_repo.dart index c4e7693..2b26a8c 100644 --- a/lib/domain/repo/orders_repo.dart +++ b/lib/domain/repo/orders_repo.dart @@ -1,7 +1,9 @@ import 'package:elevate_tracking_app/core/api_result/api_result.dart'; import '../entites/order_entity.dart'; +import '../entites/start_order_entity.dart'; abstract interface class OrdersRepo { Future>> getOrders(); + Future> startOrder(String 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/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 458d976..2aab673 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -45,7 +45,5 @@ "enterYourPassword": "Enter your password", "continueWord": "Continue", "error": "Error", - "home": "Home", - "orders": "Orders", - "profile": "Profile" + "floweryRider": "Flowery Rider" } \ No newline at end of file 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 index e6e71aa..3d38d2b 100644 --- a/lib/presentation/home/view/screen/home_page.dart +++ b/lib/presentation/home/view/screen/home_page.dart @@ -1,10 +1,46 @@ +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'; -class HomePage extends StatelessWidget { +import '../../../../core/di/di.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(); + } + + @override + void dispose() { + homeViewModel.close(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return Placeholder(); + return Column( + children: [ + Text( + AppLocalizations.of(context).floweryRider, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: AppColors.mainColor, + fontWeight: FontWeight.w400, + fontFamily: ConstKeys.iMFeellEnglishFont, + ), + ), + ], + ); } -} \ No newline at end of file +} 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..e69de29 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..82509dd --- /dev/null +++ b/lib/presentation/home/view/widget/home_order_card.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class HomeOrderCard extends StatelessWidget { + const HomeOrderCard({super.key}); + + @override + Widget build(BuildContext context) { + return Card( + + ); + } +} \ No newline at end of file diff --git a/lib/presentation/home/view_model/home_view_model.dart b/lib/presentation/home/view_model/home_view_model.dart index 73765f8..60ef418 100644 --- a/lib/presentation/home/view_model/home_view_model.dart +++ b/lib/presentation/home/view_model/home_view_model.dart @@ -1,5 +1,6 @@ import 'package:bloc/bloc.dart'; 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:injectable/injectable.dart'; @@ -12,8 +13,10 @@ part 'home_state.dart'; @injectable class HomeViewModel extends Cubit { final GetAllPendingOrdersUseCase _getAllPendingOrdersUseCase; + final StartOrderUseCase _startOrderUseCase; - HomeViewModel(this._getAllPendingOrdersUseCase) : super(const HomeState()); + HomeViewModel(this._getAllPendingOrdersUseCase, this._startOrderUseCase) + : super(const HomeState()); void doIntent(HomeEvents events) { switch (events) { 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/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(), ], From 09df70fc720957bb05769e866f0ea843f07099df Mon Sep 17 00:00:00 2001 From: youssefmdev22 Date: Sat, 4 Oct 2025 12:16:49 +0300 Subject: [PATCH 3/4] update home page --- lib/api/client/api_client.dart | 6 +- .../orders_remote_data_source_impl.dart | 8 +- lib/api/mapper/orders_mapper.dart | 39 ++++ lib/api/models/order_dto.dart | 4 + lib/api/models/shipping_address_dto.dart | 35 ++++ lib/core/constants/app_theme.dart | 4 + .../custom_cached_network_image.dart | 35 ++++ .../orders_remote_data_source.dart | 7 +- lib/data/repo/orders_repo_impl.dart | 10 +- lib/domain/entites/meta_data_entity.dart | 13 ++ lib/domain/entites/order_entity.dart | 3 + lib/domain/entites/pending_orders_entity.dart | 9 + .../entites/shipping_address_entity.dart | 15 ++ lib/domain/repo/orders_repo.dart | 3 +- .../get_all_pending_orders_use_case.dart | 6 +- lib/l10n/intl_en.arb | 9 +- lib/orders_dummy_data.dart | 166 ++++++++++++++++++ .../home/view/screen/home_page.dart | 78 ++++++-- .../home/view/widget/home_info_card.dart | 92 ++++++++++ .../home/view/widget/home_order_card.dart | 112 +++++++++++- .../home/view_model/home_events.dart | 11 +- .../home/view_model/home_state.dart | 28 ++- .../home/view_model/home_view_model.dart | 79 ++++++++- 23 files changed, 725 insertions(+), 47 deletions(-) create mode 100644 lib/api/models/shipping_address_dto.dart create mode 100644 lib/core/custom_widget/custom_cached_network_image.dart create mode 100644 lib/domain/entites/meta_data_entity.dart create mode 100644 lib/domain/entites/pending_orders_entity.dart create mode 100644 lib/domain/entites/shipping_address_entity.dart create mode 100644 lib/orders_dummy_data.dart diff --git a/lib/api/client/api_client.dart b/lib/api/client/api_client.dart index 1cf239a..d9191f1 100644 --- a/lib/api/client/api_client.dart +++ b/lib/api/client/api_client.dart @@ -20,12 +20,12 @@ abstract class ApiClient { @POST(EndPoints.login) Future login(@Body() LoginRequest loginRequest); - @GET(EndPoints.orders) - Future getOrders(); + @GET("${EndPoints.orders}/{page}") + Future getOrders(@Path("page") int? page); @GET(EndPoints.driverOrders) Future getDriverOrders(); - @POST("${EndPoints.startOrder}/{orderId}") + @PUT("${EndPoints.startOrder}/{orderId}") Future startOrder(@Path("orderId") String orderId); } \ No newline at end of file diff --git a/lib/api/data_source/orders_remote_data_source_impl.dart b/lib/api/data_source/orders_remote_data_source_impl.dart index 21b35ab..d82c7c5 100644 --- a/lib/api/data_source/orders_remote_data_source_impl.dart +++ b/lib/api/data_source/orders_remote_data_source_impl.dart @@ -2,11 +2,11 @@ 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_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 { @@ -15,10 +15,10 @@ class OrdersRemoteDataSourceImpl implements OrdersRemoteDataSource { OrdersRemoteDataSourceImpl(this._apiClient); @override - Future>> getOrders() { + Future> getOrders(int? page) { return safeApiCall( - () => _apiClient.getOrders(), - (response) => response.orders!.map((dto) => dto.toEntity()).toList(), + () => _apiClient.getOrders(page), + (response) => response.toEntity(), ); } diff --git a/lib/api/mapper/orders_mapper.dart b/lib/api/mapper/orders_mapper.dart index a769c9a..afabeee 100644 --- a/lib/api/mapper/orders_mapper.dart +++ b/lib/api/mapper/orders_mapper.dart @@ -1,16 +1,22 @@ 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'; @@ -37,6 +43,7 @@ extension OrderMapper on OrderDTO { user: user?.toEntity(), orderItems: orderItems?.map((dto) => dto.toEntity()).toList(), totalPrice: totalPrice, + shippingAddress: shippingAddress?.toEntity(), paymentType: paymentType, isPaid: isPaid, isDelivered: isDelivered, @@ -142,3 +149,35 @@ extension StartOrderItemsMapper on StartOrderItemsDto { ); } } + +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(), + ); + } +} diff --git a/lib/api/models/order_dto.dart b/lib/api/models/order_dto.dart index fa5467f..eb27d0a 100644 --- a/lib/api/models/order_dto.dart +++ b/lib/api/models/order_dto.dart @@ -1,3 +1,4 @@ +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'; @@ -15,6 +16,8 @@ class OrderDTO { final List? orderItems; @JsonKey(name: "totalPrice") final int? totalPrice; + @JsonKey(name: "shippingAddress") + final ShippingAddressDTO? shippingAddress; @JsonKey(name: "paymentType") final String? paymentType; @JsonKey(name: "isPaid") @@ -39,6 +42,7 @@ class OrderDTO { this.user, this.orderItems, this.totalPrice, + this.shippingAddress, this.paymentType, this.isPaid, this.isDelivered, 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/core/constants/app_theme.dart b/lib/core/constants/app_theme.dart index 9b4a597..d63d8b5 100644 --- a/lib/core/constants/app_theme.dart +++ b/lib/core/constants/app_theme.dart @@ -92,6 +92,10 @@ abstract class AppTheme { foregroundColor: AppColors.white, textStyle: getTextStyle(fontSize: 16.sp, fontWeight: FontWeight.w500), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(100)), + side: BorderSide( + color: AppColors.mainColor, + width: 1.w, + ) ), ), 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/data/data_source/orders_remote_data_source.dart b/lib/data/data_source/orders_remote_data_source.dart index 82e489f..4db59ec 100644 --- a/lib/data/data_source/orders_remote_data_source.dart +++ b/lib/data/data_source/orders_remote_data_source.dart @@ -1,8 +1,9 @@ import '../../core/api_result/api_result.dart'; -import '../../domain/entites/order_entity.dart'; +import '../../domain/entites/pending_orders_entity.dart'; import '../../domain/entites/start_order_entity.dart'; abstract interface class OrdersRemoteDataSource { - Future>> getOrders(); + Future> getOrders(int? page); + Future> startOrder(String orderId); -} \ No newline at end of file +} diff --git a/lib/data/repo/orders_repo_impl.dart b/lib/data/repo/orders_repo_impl.dart index a935cb2..57b57d4 100644 --- a/lib/data/repo/orders_repo_impl.dart +++ b/lib/data/repo/orders_repo_impl.dart @@ -1,24 +1,24 @@ 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/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() { - return _ordersRemoteDataSource.getOrders(); + Future> getOrders(int? page) { + return _ordersRemoteDataSource.getOrders(page); } @override Future> startOrder(String orderId) { return _ordersRemoteDataSource.startOrder(orderId); } -} \ 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..4dbbf0e --- /dev/null +++ b/lib/domain/entites/meta_data_entity.dart @@ -0,0 +1,13 @@ +class MetaDataEntity { + final int? currentPage; + final int? totalPages; + final int? totalItems; + final int? limit; + + MetaDataEntity({ + this.currentPage, + this.totalPages, + this.totalItems, + this.limit, + }); +} diff --git a/lib/domain/entites/order_entity.dart b/lib/domain/entites/order_entity.dart index b9582d4..8f876b7 100644 --- a/lib/domain/entites/order_entity.dart +++ b/lib/domain/entites/order_entity.dart @@ -1,4 +1,5 @@ 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'; @@ -7,6 +8,7 @@ class OrderEntity { final UserEntity? user; final List? orderItems; final int? totalPrice; + final ShippingAddressEntity? shippingAddress; final String? paymentType; final bool? isPaid; final bool? isDelivered; @@ -22,6 +24,7 @@ class OrderEntity { this.user, this.orderItems, this.totalPrice, + this.shippingAddress, this.paymentType, this.isPaid, this.isDelivered, diff --git a/lib/domain/entites/pending_orders_entity.dart b/lib/domain/entites/pending_orders_entity.dart new file mode 100644 index 0000000..543b998 --- /dev/null +++ b/lib/domain/entites/pending_orders_entity.dart @@ -0,0 +1,9 @@ +import 'package:elevate_tracking_app/domain/entites/meta_data_entity.dart'; +import 'package:elevate_tracking_app/domain/entites/order_entity.dart'; + +class PendingOrdersEntity { + final MetaDataEntity? metadata; + final List? orders; + + PendingOrdersEntity({this.metadata, this.orders}); +} diff --git a/lib/domain/entites/shipping_address_entity.dart b/lib/domain/entites/shipping_address_entity.dart new file mode 100644 index 0000000..823172b --- /dev/null +++ b/lib/domain/entites/shipping_address_entity.dart @@ -0,0 +1,15 @@ +class ShippingAddressEntity { + final String? street; + final String? city; + final String? phone; + final String? lat; + final String? long; + + ShippingAddressEntity({ + this.street, + this.city, + this.phone, + this.lat, + this.long, + }); +} diff --git a/lib/domain/repo/orders_repo.dart b/lib/domain/repo/orders_repo.dart index 2b26a8c..dada3ba 100644 --- a/lib/domain/repo/orders_repo.dart +++ b/lib/domain/repo/orders_repo.dart @@ -1,9 +1,10 @@ import 'package:elevate_tracking_app/core/api_result/api_result.dart'; +import 'package:elevate_tracking_app/domain/entites/pending_orders_entity.dart'; import '../entites/order_entity.dart'; import '../entites/start_order_entity.dart'; abstract interface class OrdersRepo { - Future>> getOrders(); + Future> getOrders(int? page); Future> startOrder(String orderId); } 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 index 2c23db6..1e1ac38 100644 --- a/lib/domain/use_cases/get_all_pending_orders_use_case.dart +++ b/lib/domain/use_cases/get_all_pending_orders_use_case.dart @@ -2,7 +2,7 @@ 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_entity.dart'; +import '../entites/pending_orders_entity.dart'; @injectable class GetAllPendingOrdersUseCase { @@ -10,7 +10,7 @@ class GetAllPendingOrdersUseCase { GetAllPendingOrdersUseCase(this._ordersRepo); - Future>> call() async { - return await _ordersRepo.getOrders(); + Future> call({int? page}) async { + return await _ordersRepo.getOrders(page); } } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 2aab673..0a89b23 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -45,5 +45,12 @@ "enterYourPassword": "Enter your password", "continueWord": "Continue", "error": "Error", - "floweryRider": "Flowery Rider" + "floweryRider": "Flowery Rider", + "flowerOrder": "Flower order", + "pickupAddress": "Pickup address", + "userAddress": "User address", + "reject": "Reject", + "accept": "Accept", + "egp": "EGP", + "noOrders": "No Orders" } \ No newline at end of file diff --git a/lib/orders_dummy_data.dart b/lib/orders_dummy_data.dart new file mode 100644 index 0000000..b3bd183 --- /dev/null +++ b/lib/orders_dummy_data.dart @@ -0,0 +1,166 @@ +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/orders_response.dart'; +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'; + +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: 'https://example.com/profile_pic.jpg', + passwordChangedAt: DateTime.now().toIso8601String(), + ); + + // Dummy Store Data + final storeDTO1 = StoreDTO( + name: 'Central Perk Cafe', + image: 'https://example.com/store_image.jpg', + 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: 'https://example.com/donut_cover.jpg', + images: [ + 'https://example.com/donut_1.jpg', + 'https://example.com/donut_2.jpg', + ], + 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: 'https://example.com/croissant_cover.jpg', + images: ['https://example.com/croissant_1.jpg'], + 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, + ); + + // 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], + ); +} diff --git a/lib/presentation/home/view/screen/home_page.dart b/lib/presentation/home/view/screen/home_page.dart index 3d38d2b..ba87cb6 100644 --- a/lib/presentation/home/view/screen/home_page.dart +++ b/lib/presentation/home/view/screen/home_page.dart @@ -3,8 +3,13 @@ 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/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}); @@ -20,6 +25,7 @@ class _HomePageState extends State { void initState() { super.initState(); homeViewModel = getIt.get(); + homeViewModel.doIntent(GetOrdersEvent()); } @override @@ -30,17 +36,67 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { - return Column( - children: [ - Text( - AppLocalizations.of(context).floweryRider, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: AppColors.mainColor, - fontWeight: FontWeight.w400, - fontFamily: ConstKeys.iMFeellEnglishFont, - ), - ), - ], + return BlocConsumer( + bloc: homeViewModel, + listener: (context, state) { + if (state.errorMessage != null) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(state.errorMessage!))); + } + }, + 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.symmetric(horizontal: 16.w), + 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) { + homeViewModel.doIntent(LoadMoreOrdersEvent()); + } + return true; + }, + child: ListView.separated( + padding: EdgeInsets.only(left: 16.w, right: 16.w), + itemCount: state.ordersList?.length ?? 0, + itemBuilder: (ctx, index) { + return HomeOrderCard( + index: index, + orderEntity: state.ordersList?[index] ?? 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 index e69de29..7df7f75 100644 --- a/lib/presentation/home/view/widget/home_info_card.dart +++ b/lib/presentation/home/view/widget/home_info_card.dart @@ -0,0 +1,92 @@ +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, + ), + ), + 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, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 4.w, + children: [ + Image.asset(AppIcons.iconLocation, fit: BoxFit.cover), + Text( + address, + style: Theme.of(context).textTheme.bodySmall, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ], + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/presentation/home/view/widget/home_order_card.dart b/lib/presentation/home/view/widget/home_order_card.dart index 82509dd..9745a47 100644 --- a/lib/presentation/home/view/widget/home_order_card.dart +++ b/lib/presentation/home/view/widget/home_order_card.dart @@ -1,12 +1,116 @@ +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 { - const HomeOrderCard({super.key}); + 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 Card( - + 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 ElevatedButton( + style: Theme.of(context).elevatedButtonTheme.style + ?.copyWith( + backgroundColor: WidgetStateProperty.all( + AppColors.white, + ), + foregroundColor: WidgetStateProperty.all( + AppColors.mainColor, + ), + ), + onPressed: () { + homeViewModel.doIntent(RejectOrderEvent(index)); + }, + child: value == index + ? const CircularProgressIndicator() + : Text(AppLocalizations.of(context).reject), + ); + }, + ), + ), + Expanded( + child: ValueListenableBuilder( + valueListenable: homeViewModel.acceptOrderIndex, + builder: (context, value, child) { + return ElevatedButton( + onPressed: () { + homeViewModel.doIntent(AcceptOrderEvent(index)); + }, + child: value == index + ? const CircularProgressIndicator() + : Text(AppLocalizations.of(context).accept), + ); + }, + ), + ), + ], + ), + ], + ), ); } -} \ No newline at end of file +} diff --git a/lib/presentation/home/view_model/home_events.dart b/lib/presentation/home/view_model/home_events.dart index 68c1d11..ee3ddd8 100644 --- a/lib/presentation/home/view_model/home_events.dart +++ b/lib/presentation/home/view_model/home_events.dart @@ -1,3 +1,12 @@ sealed class HomeEvents {} -class TestEvent extends 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 index 11f80d5..5f9c801 100644 --- a/lib/presentation/home/view_model/home_state.dart +++ b/lib/presentation/home/view_model/home_state.dart @@ -2,23 +2,41 @@ part of 'home_view_model.dart'; final class HomeState extends Equatable { final bool isLoading; + final bool isLoadingMore; final String? errorMessage; - final List? orders; + final List? ordersList; + final StartOrderEntity? startOrderEntity; - const HomeState({this.isLoading = false, this.errorMessage, this.orders}); + const HomeState({ + this.isLoading = false, + this.isLoadingMore = false, + this.errorMessage, + this.ordersList, + this.startOrderEntity, + }); HomeState copyWith({ bool? isLoading, + bool? isLoadingMore, String? errorMessage, - List? orders, + List? ordersList, + StartOrderEntity? startOrderEntity, }) { return HomeState( isLoading: isLoading ?? this.isLoading, + isLoadingMore: isLoadingMore ?? this.isLoadingMore, errorMessage: errorMessage ?? this.errorMessage, - orders: orders ?? this.orders, + ordersList: ordersList ?? this.ordersList, + startOrderEntity: startOrderEntity ?? this.startOrderEntity, ); } @override - List get props => [isLoading, errorMessage, orders]; + List get props => [ + isLoading, + isLoadingMore, + errorMessage, + ordersList, + startOrderEntity, + ]; } diff --git a/lib/presentation/home/view_model/home_view_model.dart b/lib/presentation/home/view_model/home_view_model.dart index 60ef418..c77b34e 100644 --- a/lib/presentation/home/view_model/home_view_model.dart +++ b/lib/presentation/home/view_model/home_view_model.dart @@ -1,10 +1,13 @@ -import 'package:bloc/bloc.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/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/use_cases/get_all_pending_orders_use_case.dart'; import 'home_events.dart'; @@ -18,24 +21,88 @@ class HomeViewModel extends Cubit { HomeViewModel(this._getAllPendingOrdersUseCase, this._startOrderUseCase) : super(const HomeState()); + int currentPage = 1; + int totalPages = 0; + + List? ordersList; + + ValueNotifier acceptOrderIndex = ValueNotifier(-1); + ValueNotifier rejectOrderIndex = ValueNotifier(-1); + void doIntent(HomeEvents events) { switch (events) { - case TestEvent(): + case GetOrdersEvent(): + _getOrders(); + case AcceptOrderEvent(): + _acceptOrder(events.index); + case RejectOrderEvent(): + _rejectOrder(events.index); + case LoadMoreOrdersEvent(): + _loadMoreOrders(); } } - Future getOrders() async { + Future _getOrders() async { emit(const HomeState(isLoading: true)); final result = await _getAllPendingOrdersUseCase(); switch (result) { - case ApiSuccessResult>(): - emit(state.copyWith(isLoading: false, orders: result.data)); + case ApiSuccessResult(): + currentPage = result.data.metadata?.currentPage ?? 0; + totalPages = result.data.metadata?.totalPages ?? 0; + ordersList = result.data.orders; + emit(state.copyWith(isLoading: false, ordersList: ordersList)); break; - case ApiErrorResult>(): + case ApiErrorResult(): emit( state.copyWith(isLoading: false, errorMessage: result.errorMessage), ); break; } } + + Future _acceptOrder(int index) async { + acceptOrderIndex.value = index; + final result = await _startOrderUseCase( + orderId: ordersList?[index].id ?? "", + ); + switch (result) { + case ApiSuccessResult(): + ordersList?.removeAt(index); + emit(state.copyWith(ordersList: ordersList)); + case ApiErrorResult(): + emit(state.copyWith(errorMessage: result.errorMessage)); + } + acceptOrderIndex.value = -1; + } + + void _rejectOrder(int index) { + rejectOrderIndex.value = index; + ordersList?.removeAt(index); + emit(state.copyWith(ordersList: ordersList)); + rejectOrderIndex.value = -1; + } + + Future _loadMoreOrders() async { + if (currentPage >= totalPages) return; + currentPage++; + + 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; + ordersList?.addAll(result.data.orders ?? []); + emit(state.copyWith(isLoadingMore: false, ordersList: ordersList)); + break; + case ApiErrorResult(): + emit( + state.copyWith( + isLoadingMore: false, + errorMessage: result.errorMessage, + ), + ); + break; + } + } } From 3d3d9ff489fbb0500937431394677955503f3231 Mon Sep 17 00:00:00 2001 From: youssefmdev22 Date: Tue, 7 Oct 2025 04:50:06 +0300 Subject: [PATCH 4/4] add unit test and widget test --- lib/api/client/api_client.dart | 4 +- lib/core/constants/app_theme.dart | 6 +- lib/core/constants/widgets_keys.dart | 1 + lib/domain/entites/driver_order_entity.dart | 9 +- lib/domain/entites/meta_data_entity.dart | 9 +- lib/domain/entites/order_entity.dart | 24 +- lib/domain/entites/order_item_entity.dart | 8 +- lib/domain/entites/pending_orders_entity.dart | 8 +- lib/domain/entites/product_entity.dart | 29 +- .../entites/shipping_address_entity.dart | 9 +- lib/domain/entites/start_order_entity.dart | 22 +- .../entites/start_order_items_entity.dart | 9 +- lib/domain/entites/store_entity.dart | 9 +- lib/domain/entites/user_entity.dart | 19 +- lib/domain/repo/orders_repo.dart | 2 +- lib/l10n/intl_en.arb | 3 +- .../home/view/screen/home_page.dart | 26 +- .../home/view/widget/home_info_card.dart | 62 +++-- .../home/view/widget/home_order_card.dart | 56 ++-- .../home/view_model/home_state.dart | 12 +- .../home/view_model/home_view_model.dart | 34 ++- .../orders_remote_data_source_impl_test.dart | 91 ++++++ test/api/mapper/orders_mapper_test.dart | 251 +++++++++++++++++ test/data/repo/orders_repo_impl_test.dart | 120 ++++++++ .../get_all_pending_orders_use_case_test.dart | 66 +++++ .../use_cases/start_order_use_case_test.dart | 63 +++++ {lib => test/dummy}/orders_dummy_data.dart | 74 ++++- .../login/view/screen/login_screen_test.dart | 7 +- .../home/view/screen/home_page_test.dart | 258 ++++++++++++++++++ .../home/view_model/home_view_model_test.dart | 236 ++++++++++++++++ .../views/screen/onboarding_screen_test.dart | 2 +- 31 files changed, 1410 insertions(+), 119 deletions(-) create mode 100644 test/api/data_source/orders_remote_data_source_impl_test.dart create mode 100644 test/api/mapper/orders_mapper_test.dart create mode 100644 test/data/repo/orders_repo_impl_test.dart create mode 100644 test/domain/use_cases/get_all_pending_orders_use_case_test.dart create mode 100644 test/domain/use_cases/start_order_use_case_test.dart rename {lib => test/dummy}/orders_dummy_data.dart (70%) create mode 100644 test/presentation/home/view/screen/home_page_test.dart create mode 100644 test/presentation/home/view_model/home_view_model_test.dart diff --git a/lib/api/client/api_client.dart b/lib/api/client/api_client.dart index d9191f1..733ccb6 100644 --- a/lib/api/client/api_client.dart +++ b/lib/api/client/api_client.dart @@ -20,8 +20,8 @@ abstract class ApiClient { @POST(EndPoints.login) Future login(@Body() LoginRequest loginRequest); - @GET("${EndPoints.orders}/{page}") - Future getOrders(@Path("page") int? page); + @GET(EndPoints.orders) + Future getOrders(@Query("page") int? page); @GET(EndPoints.driverOrders) Future getDriverOrders(); diff --git a/lib/core/constants/app_theme.dart b/lib/core/constants/app_theme.dart index d63d8b5..897405a 100644 --- a/lib/core/constants/app_theme.dart +++ b/lib/core/constants/app_theme.dart @@ -91,11 +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)), - side: BorderSide( - color: AppColors.mainColor, - width: 1.w, - ) + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(100)) ), ), 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/domain/entites/driver_order_entity.dart b/lib/domain/entites/driver_order_entity.dart index 508f10d..4ac542e 100644 --- a/lib/domain/entites/driver_order_entity.dart +++ b/lib/domain/entites/driver_order_entity.dart @@ -1,7 +1,8 @@ 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 { +class DriverOrderEntity extends Equatable { final String? id; final String? driver; final OrderEntity? order; @@ -10,7 +11,7 @@ class DriverOrderEntity { final String? updatedAt; final StoreEntity? store; - DriverOrderEntity({ + const DriverOrderEntity({ this.id, this.driver, this.order, @@ -19,4 +20,8 @@ class DriverOrderEntity { this.updatedAt, this.store, }); + + @override + List get props => + [id, driver, order, v, createdAt, updatedAt, store,]; } diff --git a/lib/domain/entites/meta_data_entity.dart b/lib/domain/entites/meta_data_entity.dart index 4dbbf0e..e630d0b 100644 --- a/lib/domain/entites/meta_data_entity.dart +++ b/lib/domain/entites/meta_data_entity.dart @@ -1,13 +1,18 @@ -class MetaDataEntity { +import 'package:equatable/equatable.dart'; + +class MetaDataEntity extends Equatable { final int? currentPage; final int? totalPages; final int? totalItems; final int? limit; - MetaDataEntity({ + 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 index 8f876b7..f348f05 100644 --- a/lib/domain/entites/order_entity.dart +++ b/lib/domain/entites/order_entity.dart @@ -2,8 +2,9 @@ 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 { +class OrderEntity extends Equatable { final String? id; final UserEntity? user; final List? orderItems; @@ -19,7 +20,7 @@ class OrderEntity { final int? v; final StoreEntity? store; - OrderEntity({ + const OrderEntity({ this.id, this.user, this.orderItems, @@ -35,4 +36,23 @@ class OrderEntity { this.v, this.store, }); + + @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_item_entity.dart b/lib/domain/entites/order_item_entity.dart index 91e8446..7ababb1 100644 --- a/lib/domain/entites/order_item_entity.dart +++ b/lib/domain/entites/order_item_entity.dart @@ -1,10 +1,14 @@ import 'package:elevate_tracking_app/domain/entites/product_entity.dart'; +import 'package:equatable/equatable.dart'; -class OrderItemEntity { +class OrderItemEntity extends Equatable { final ProductEntity? product; final int? price; final int? quantity; final String? id; - OrderItemEntity({this.product, this.price, this.quantity, this.id}); + const OrderItemEntity({this.product, this.price, this.quantity, this.id}); + + @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 index 543b998..ad33ff2 100644 --- a/lib/domain/entites/pending_orders_entity.dart +++ b/lib/domain/entites/pending_orders_entity.dart @@ -1,9 +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 { +class PendingOrdersEntity extends Equatable { final MetaDataEntity? metadata; final List? orders; - PendingOrdersEntity({this.metadata, this.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 index 4013789..45b4e76 100644 --- a/lib/domain/entites/product_entity.dart +++ b/lib/domain/entites/product_entity.dart @@ -1,4 +1,6 @@ -class ProductEntity { +import 'package:equatable/equatable.dart'; + +class ProductEntity extends Equatable { final String? id; final String? title; final String? slug; @@ -18,7 +20,7 @@ class ProductEntity { final int? rateAvg; final int? rateCount; - ProductEntity({ + const ProductEntity({ this.id, this.title, this.slug, @@ -38,4 +40,27 @@ class ProductEntity { this.rateAvg, this.rateCount, }); + + @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 index 823172b..12c57f6 100644 --- a/lib/domain/entites/shipping_address_entity.dart +++ b/lib/domain/entites/shipping_address_entity.dart @@ -1,15 +1,20 @@ -class ShippingAddressEntity { +import 'package:equatable/equatable.dart'; + +class ShippingAddressEntity extends Equatable { final String? street; final String? city; final String? phone; final String? lat; final String? long; - ShippingAddressEntity({ + const ShippingAddressEntity({ this.street, this.city, this.phone, this.lat, this.long, }); + + @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 index f97faa1..6324a5d 100644 --- a/lib/domain/entites/start_order_entity.dart +++ b/lib/domain/entites/start_order_entity.dart @@ -1,6 +1,7 @@ import 'package:elevate_tracking_app/domain/entites/start_order_items_entity.dart'; +import 'package:equatable/equatable.dart'; -class StartOrderEntity { +class StartOrderEntity extends Equatable { final String? id; final String? user; final List? orderItems; @@ -14,7 +15,7 @@ class StartOrderEntity { final String? orderNumber; final int? v; - StartOrderEntity({ + const StartOrderEntity({ this.id, this.user, this.orderItems, @@ -28,4 +29,21 @@ class StartOrderEntity { 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 index 6f29440..9e9a5c7 100644 --- a/lib/domain/entites/start_order_items_entity.dart +++ b/lib/domain/entites/start_order_items_entity.dart @@ -1,8 +1,13 @@ -class StartOrderItemsEntity { +import 'package:equatable/equatable.dart'; + +class StartOrderItemsEntity extends Equatable { final String? product; final int? price; final int? quantity; final String? id; - StartOrderItemsEntity({this.product, this.price, this.quantity, this.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 index 5eae253..179df2a 100644 --- a/lib/domain/entites/store_entity.dart +++ b/lib/domain/entites/store_entity.dart @@ -1,15 +1,20 @@ -class StoreEntity { +import 'package:equatable/equatable.dart'; + +class StoreEntity extends Equatable { final String? name; final String? image; final String? address; final String? phoneNumber; final String? latLong; - StoreEntity({ + const StoreEntity({ this.name, this.image, this.address, this.phoneNumber, this.latLong, }); + + @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 index 23f8fd5..aefdc2e 100644 --- a/lib/domain/entites/user_entity.dart +++ b/lib/domain/entites/user_entity.dart @@ -1,4 +1,6 @@ -class UserEntity { +import 'package:equatable/equatable.dart'; + +class UserEntity extends Equatable { final String? id; final String? firstName; final String? lastName; @@ -8,7 +10,7 @@ class UserEntity { final String? photo; final String? passwordChangedAt; - UserEntity({ + const UserEntity({ this.id, this.firstName, this.lastName, @@ -18,4 +20,17 @@ class UserEntity { this.photo, this.passwordChangedAt, }); + + @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 index dada3ba..aca5d63 100644 --- a/lib/domain/repo/orders_repo.dart +++ b/lib/domain/repo/orders_repo.dart @@ -1,10 +1,10 @@ import 'package:elevate_tracking_app/core/api_result/api_result.dart'; import 'package:elevate_tracking_app/domain/entites/pending_orders_entity.dart'; -import '../entites/order_entity.dart'; import '../entites/start_order_entity.dart'; abstract interface class OrdersRepo { Future> getOrders(int? page); + Future> startOrder(String orderId); } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 0a89b23..2b8ec23 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -52,5 +52,6 @@ "reject": "Reject", "accept": "Accept", "egp": "EGP", - "noOrders": "No Orders" + "noOrders": "No Orders", + "orderAcceptedSuccessfully": "Order accepted successfully" } \ No newline at end of file diff --git a/lib/presentation/home/view/screen/home_page.dart b/lib/presentation/home/view/screen/home_page.dart index ba87cb6..d020255 100644 --- a/lib/presentation/home/view/screen/home_page.dart +++ b/lib/presentation/home/view/screen/home_page.dart @@ -6,6 +6,7 @@ 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'; @@ -44,6 +45,15 @@ class _HomePageState extends State { 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) { @@ -57,7 +67,7 @@ class _HomePageState extends State { spacing: 16.h, children: [ Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w), + padding: EdgeInsets.only(left: 16.w, top: 16.h), child: Text( AppLocalizations.of(context).floweryRider, style: Theme.of(context).textTheme.bodyLarge?.copyWith( @@ -71,18 +81,26 @@ class _HomePageState extends State { child: NotificationListener( onNotification: (ScrollNotification scrollInfo) { if (scrollInfo.metrics.pixels == - scrollInfo.metrics.maxScrollExtent) { + scrollInfo.metrics.maxScrollExtent && + !state.isLoadingMore) { homeViewModel.doIntent(LoadMoreOrdersEvent()); } return true; }, child: ListView.separated( - padding: EdgeInsets.only(left: 16.w, right: 16.w), + 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] ?? OrderEntity(), + orderEntity: + state.ordersList?[index] ?? const OrderEntity(), homeViewModel: homeViewModel, ); }, diff --git a/lib/presentation/home/view/widget/home_info_card.dart b/lib/presentation/home/view/widget/home_info_card.dart index 7df7f75..82406a5 100644 --- a/lib/presentation/home/view/widget/home_info_card.dart +++ b/lib/presentation/home/view/widget/home_info_card.dart @@ -57,31 +57,43 @@ class HomeInfoCard extends StatelessWidget { fit: BoxFit.cover, ), ), - 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, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 4.w, - children: [ - Image.asset(AppIcons.iconLocation, fit: BoxFit.cover), - Text( - address, - style: Theme.of(context).textTheme.bodySmall, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ], + 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 index 9745a47..9326286 100644 --- a/lib/presentation/home/view/widget/home_order_card.dart +++ b/lib/presentation/home/view/widget/home_order_card.dart @@ -72,23 +72,29 @@ class HomeOrderCard extends StatelessWidget { child: ValueListenableBuilder( valueListenable: homeViewModel.rejectOrderIndex, builder: (context, value, child) { - return ElevatedButton( - style: Theme.of(context).elevatedButtonTheme.style - ?.copyWith( - backgroundColor: WidgetStateProperty.all( - AppColors.white, - ), - foregroundColor: WidgetStateProperty.all( - AppColors.mainColor, - ), - ), - onPressed: () { - homeViewModel.doIntent(RejectOrderEvent(index)); - }, - child: value == index - ? const CircularProgressIndicator() - : Text(AppLocalizations.of(context).reject), - ); + 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), + ); }, ), ), @@ -96,14 +102,14 @@ class HomeOrderCard extends StatelessWidget { child: ValueListenableBuilder( valueListenable: homeViewModel.acceptOrderIndex, builder: (context, value, child) { - return ElevatedButton( - onPressed: () { - homeViewModel.doIntent(AcceptOrderEvent(index)); - }, - child: value == index - ? const CircularProgressIndicator() - : Text(AppLocalizations.of(context).accept), - ); + 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_state.dart b/lib/presentation/home/view_model/home_state.dart index 5f9c801..c2ac2db 100644 --- a/lib/presentation/home/view_model/home_state.dart +++ b/lib/presentation/home/view_model/home_state.dart @@ -5,14 +5,14 @@ final class HomeState extends Equatable { final bool isLoadingMore; final String? errorMessage; final List? ordersList; - final StartOrderEntity? startOrderEntity; + final bool isAcceptSuccess; const HomeState({ this.isLoading = false, this.isLoadingMore = false, this.errorMessage, this.ordersList, - this.startOrderEntity, + this.isAcceptSuccess = false, }); HomeState copyWith({ @@ -20,14 +20,14 @@ final class HomeState extends Equatable { bool? isLoadingMore, String? errorMessage, List? ordersList, - StartOrderEntity? startOrderEntity, + bool? isAcceptSuccess, }) { return HomeState( isLoading: isLoading ?? this.isLoading, isLoadingMore: isLoadingMore ?? this.isLoadingMore, - errorMessage: errorMessage ?? this.errorMessage, + errorMessage: errorMessage, ordersList: ordersList ?? this.ordersList, - startOrderEntity: startOrderEntity ?? this.startOrderEntity, + isAcceptSuccess: isAcceptSuccess ?? false, ); } @@ -37,6 +37,6 @@ final class HomeState extends Equatable { isLoadingMore, errorMessage, ordersList, - startOrderEntity, + isAcceptSuccess, ]; } diff --git a/lib/presentation/home/view_model/home_view_model.dart b/lib/presentation/home/view_model/home_view_model.dart index c77b34e..0f8d5cc 100644 --- a/lib/presentation/home/view_model/home_view_model.dart +++ b/lib/presentation/home/view_model/home_view_model.dart @@ -21,14 +21,13 @@ class HomeViewModel extends Cubit { HomeViewModel(this._getAllPendingOrdersUseCase, this._startOrderUseCase) : super(const HomeState()); - int currentPage = 1; - int totalPages = 0; - - List? ordersList; - 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(): @@ -49,8 +48,7 @@ class HomeViewModel extends Cubit { case ApiSuccessResult(): currentPage = result.data.metadata?.currentPage ?? 0; totalPages = result.data.metadata?.totalPages ?? 0; - ordersList = result.data.orders; - emit(state.copyWith(isLoading: false, ordersList: ordersList)); + emit(state.copyWith(isLoading: false, ordersList: result.data.orders)); break; case ApiErrorResult(): emit( @@ -63,12 +61,13 @@ class HomeViewModel extends Cubit { Future _acceptOrder(int index) async { acceptOrderIndex.value = index; final result = await _startOrderUseCase( - orderId: ordersList?[index].id ?? "", + orderId: state.ordersList?[index].id ?? "", ); switch (result) { case ApiSuccessResult(): - ordersList?.removeAt(index); - emit(state.copyWith(ordersList: ordersList)); + final updatedList = List.from(state.ordersList ?? []); + updatedList.removeAt(index); + emit(state.copyWith(ordersList: updatedList, isAcceptSuccess: true)); case ApiErrorResult(): emit(state.copyWith(errorMessage: result.errorMessage)); } @@ -77,14 +76,16 @@ class HomeViewModel extends Cubit { void _rejectOrder(int index) { rejectOrderIndex.value = index; - ordersList?.removeAt(index); - emit(state.copyWith(ordersList: ordersList)); + final updatedList = List.from(state.ordersList ?? []); + updatedList.removeAt(index); + emit(state.copyWith(ordersList: updatedList)); rejectOrderIndex.value = -1; } Future _loadMoreOrders() async { - if (currentPage >= totalPages) return; + if (isLoadMore || currentPage >= totalPages) return; currentPage++; + isLoadMore = true; emit(state.copyWith(isLoadingMore: true)); final result = await _getAllPendingOrdersUseCase(page: currentPage); @@ -92,8 +93,10 @@ class HomeViewModel extends Cubit { case ApiSuccessResult(): currentPage = result.data.metadata?.currentPage ?? 0; totalPages = result.data.metadata?.totalPages ?? 0; - ordersList?.addAll(result.data.orders ?? []); - emit(state.copyWith(isLoadingMore: false, ordersList: ordersList)); + final updatedList = List.from(state.ordersList ?? []); + updatedList.addAll(result.data.orders ?? []); + emit(state.copyWith(isLoadingMore: false, ordersList: updatedList)); + isLoadMore = false; break; case ApiErrorResult(): emit( @@ -102,6 +105,7 @@ class HomeViewModel extends Cubit { errorMessage: result.errorMessage, ), ); + isLoadMore = false; break; } } 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..040a5cd --- /dev/null +++ b/test/api/data_source/orders_remote_data_source_impl_test.dart @@ -0,0 +1,91 @@ +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]) +void main() { + final dummyData = OrdersDummyData(); + + group("test OrdersRemoteDataSourceImpl", () { + late MockApiClient mockApiClient; + late OrdersRemoteDataSourceImpl dataSource; + + setUp(() { + mockApiClient = MockApiClient(); + dataSource = OrdersRemoteDataSourceImpl(mockApiClient); + }); + + 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/lib/orders_dummy_data.dart b/test/dummy/orders_dummy_data.dart similarity index 70% rename from lib/orders_dummy_data.dart rename to test/dummy/orders_dummy_data.dart index b3bd183..89ee786 100644 --- a/lib/orders_dummy_data.dart +++ b/test/dummy/orders_dummy_data.dart @@ -3,8 +3,12 @@ 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'; @@ -17,14 +21,14 @@ class OrdersDummyData { email: 'john.doe@example.com', gender: 'Male', phone: '123-456-7890', - photo: 'https://example.com/profile_pic.jpg', + photo: null, passwordChangedAt: DateTime.now().toIso8601String(), ); // Dummy Store Data final storeDTO1 = StoreDTO( name: 'Central Perk Cafe', - image: 'https://example.com/store_image.jpg', + image: null, address: '123 Main Street, New York, NY', phoneNumber: '987-654-3210', latLong: '40.7128,-74.0060', @@ -37,11 +41,8 @@ class OrdersDummyData { slug: 'classic-glazed-donut', description: 'A timeless classic, our glazed donut is light, fluffy, and delicious.', - imgCover: 'https://example.com/donut_cover.jpg', - images: [ - 'https://example.com/donut_1.jpg', - 'https://example.com/donut_2.jpg', - ], + imgCover: null, + images: null, price: 10, priceAfterDiscount: 9, quantity: 100, @@ -61,8 +62,8 @@ class OrdersDummyData { title: 'Chocolate Filled Croissant', slug: 'chocolate-filled-croissant', description: 'A buttery croissant filled with rich chocolate.', - imgCover: 'https://example.com/croissant_cover.jpg', - images: ['https://example.com/croissant_1.jpg'], + imgCover: null, + images: null, price: 25, quantity: 50, category: 'cat_pastries', @@ -150,6 +151,16 @@ class OrdersDummyData { 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, @@ -163,4 +174,49 @@ class OrdersDummyData { 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); }); }); }