diff --git a/lib/application/order_bloc/order_bloc.dart b/lib/application/order_bloc/order_bloc.dart index ed8fb09..5f90509 100644 --- a/lib/application/order_bloc/order_bloc.dart +++ b/lib/application/order_bloc/order_bloc.dart @@ -12,11 +12,19 @@ part 'order_state.dart'; class OrderBloc extends Bloc { final GetOrderUseCase _getOrdersUseCase; OrderBloc(this._getOrdersUseCase) - : super(const OrderInitial( - orders: [], - params: FilterProductParams(), - )) { + : super( + OrderInitial( + orders: const [], + params: const FilterProductParams(), + metaData: PaginationMetaData( + pageSize: 10, + limit: 0, + total: 0, + ), + ), + ) { on(_onGetOrders); + on(_onLoadMoreOrders); } FutureOr _onGetOrders( @@ -26,26 +34,73 @@ class OrderBloc extends Bloc { try { emit(OrderLoading( orders: const [], + metaData: state.metaData, params: event.params, )); final result = await _getOrdersUseCase(event.params); result.fold( (failure) => emit(OrderError( orders: state.orders, + metaData: state.metaData, failure: failure, params: event.params, )), - (orders) => emit(OrderLoaded( - orders: orders, + (response) => emit(OrderLoaded( + metaData: response.paginationMetaData, + orders: response.orders, params: event.params, )), ); } catch (e) { emit(OrderError( orders: state.orders, + metaData: state.metaData, failure: ExceptionFailure(), params: event.params, )); } } + + void _onLoadMoreOrders(GetMoreOrders event, Emitter emit) async { + var state = this.state; + var limit = state.metaData.limit; + var total = state.metaData.total; + var loadedProductsLength = state.orders.length; + // check state and loaded products amount[loadedProductsLength] compare with + // number of results total[total] results available in server + if (state is OrderLoaded && (loadedProductsLength < total)) { + try { + emit(OrderLoading( + orders: state.orders, + metaData: state.metaData, + params: state.params, + )); + final result = await _getOrdersUseCase(FilterProductParams(limit: limit + 10)); + result.fold( + (failure) => emit(OrderError( + orders: state.orders, + metaData: state.metaData, + failure: failure, + params: state.params, + )), + (response) { + List products = state.orders; + products.addAll(response.orders); + emit(OrderLoaded( + metaData: state.metaData, + orders: products, + params: state.params, + )); + }, + ); + } catch (e) { + emit(OrderError( + orders: state.orders, + metaData: state.metaData, + failure: ExceptionFailure(), + params: state.params, + )); + } + } + } } diff --git a/lib/application/order_bloc/order_state.dart b/lib/application/order_bloc/order_state.dart index c12f01a..a7f7178 100644 --- a/lib/application/order_bloc/order_state.dart +++ b/lib/application/order_bloc/order_state.dart @@ -2,13 +2,19 @@ part of 'order_bloc.dart'; abstract class OrderState extends Equatable { final List orders; + final PaginationMetaData metaData; final FilterProductParams params; - const OrderState({required this.orders, required this.params}); + const OrderState({ + required this.orders, + required this.metaData, + required this.params, + }); } class OrderInitial extends OrderState { const OrderInitial({ required super.orders, + required super.metaData, required super.params, }); @override @@ -18,6 +24,7 @@ class OrderInitial extends OrderState { class OrderEmpty extends OrderState { const OrderEmpty({ required super.orders, + required super.metaData, required super.params, }); @override @@ -27,6 +34,7 @@ class OrderEmpty extends OrderState { class OrderLoading extends OrderState { const OrderLoading({ required super.orders, + required super.metaData, required super.params, }); @override @@ -36,6 +44,7 @@ class OrderLoading extends OrderState { class OrderLoaded extends OrderState { const OrderLoaded({ required super.orders, + required super.metaData, required super.params, }); @override @@ -46,6 +55,7 @@ class OrderError extends OrderState { final Failure failure; const OrderError({ required super.orders, + required super.metaData, required super.params, required this.failure, }); diff --git a/lib/data/data_sources/remote/order_remote_data_source.dart b/lib/data/data_sources/remote/order_remote_data_source.dart index 64b49b0..5a2618c 100644 --- a/lib/data/data_sources/remote/order_remote_data_source.dart +++ b/lib/data/data_sources/remote/order_remote_data_source.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:http/http.dart' as http; import '../../../core/core.dart'; @@ -7,7 +5,7 @@ import '../../../domain/domain.dart'; import '../../data.dart'; abstract class OrderRemoteDataSource { - Future> getOrders(FilterProductParams params, String token); + Future getOrders(FilterProductParams params, String token); } class OrderRemoteDataSourceImpl implements OrderRemoteDataSource { @@ -15,9 +13,9 @@ class OrderRemoteDataSourceImpl implements OrderRemoteDataSource { OrderRemoteDataSourceImpl({required this.client}); @override - Future> getOrders(FilterProductParams params, String token) async { + Future getOrders(FilterProductParams params, String token) async { final response = await client.get( - Uri.parse('$baseUrl/Goods'), + Uri.parse('$baseUrl/Goods?pageNumber=${params.offset}&pageSize=${params.limit}'), headers: { 'Content-Type': 'application/json', 'accept': '*/*', @@ -26,9 +24,7 @@ class OrderRemoteDataSourceImpl implements OrderRemoteDataSource { ); if (response.statusCode == 200) { - List jsonData = json.decode(response.body); - final list = jsonData.map((item) => OrderModel.fromJson(item)).toList(); - return list; + return orderResponseModelFromJson(response.body); } else { throw ServerException(); } diff --git a/lib/data/models/models.dart b/lib/data/models/models.dart index 1619d22..c612787 100644 --- a/lib/data/models/models.dart +++ b/lib/data/models/models.dart @@ -2,3 +2,4 @@ export 'language.dart'; export 'user/authentication_response_model.dart'; export 'user/user_model.dart'; export 'order/order_model.dart'; +export 'order/order_response_model.dart'; diff --git a/lib/data/models/order/order_response_model.dart b/lib/data/models/order/order_response_model.dart new file mode 100644 index 0000000..49462de --- /dev/null +++ b/lib/data/models/order/order_response_model.dart @@ -0,0 +1,30 @@ +import 'dart:convert'; + +import '../../../domain/domain.dart'; +import 'order_model.dart'; +import 'pagination_data_model.dart'; + +OrderResponseModel orderResponseModelFromJson(String str) => OrderResponseModel.fromJson(json.decode(str)); + +// String orderResponseModelToJson(OrderResponseModel data) => json.encode(data.toJson()); + +class OrderResponseModel extends OrderResponse { + OrderResponseModel({ + required PaginationMetaData meta, + required List data, + }) : super(orders: data, paginationMetaData: meta); + + factory OrderResponseModel.fromJson(Map json) => OrderResponseModel( + meta: PaginationMetaDataModel( + page: json['PageNumber'], + pageSize: json['PageSize'], + total: json['TotalRecords'], + ), + data: List.from(json['Data'].map((x) => OrderModel.fromJson(x))), + ); + + /* Map toJson() => { + 'meta': (paginationMetaData as PaginationMetaDataModel).toJson(), + 'Data': List.from((orders as List).map((x) => x.toJson())), + }; */ +} diff --git a/lib/data/models/order/pagination_data_model.dart b/lib/data/models/order/pagination_data_model.dart new file mode 100644 index 0000000..49e9c04 --- /dev/null +++ b/lib/data/models/order/pagination_data_model.dart @@ -0,0 +1,21 @@ +import 'package:cargo/domain/entities/order/pagination_meta_data.dart'; + +class PaginationMetaDataModel extends PaginationMetaData { + PaginationMetaDataModel({ + required int page, + required super.pageSize, + required super.total, + }) : super(limit: page); + + factory PaginationMetaDataModel.fromJson(Map json) => PaginationMetaDataModel( + page: json['PageNumber'], + pageSize: json['PageSize'], + total: json['TotalRecords'], + ); + + Map toJson() => { + 'PageNumber': limit, + 'PageSize': pageSize, + 'TotalRecords': total, + }; +} diff --git a/lib/data/repositories/order_repository_impl.dart b/lib/data/repositories/order_repository_impl.dart index ceb4c62..3410bc3 100644 --- a/lib/data/repositories/order_repository_impl.dart +++ b/lib/data/repositories/order_repository_impl.dart @@ -3,6 +3,7 @@ import 'package:dartz/dartz.dart'; import '../../core/core.dart'; import '../../domain/domain.dart'; import '../data_sources/data_sources.dart'; +import '../models/order/order_response_model.dart'; class OrderRepositoryImpl extends OrderRepository { final OrderRemoteDataSource remoteDataSource; @@ -16,7 +17,7 @@ class OrderRepositoryImpl extends OrderRepository { }); @override - Future>> getOrders(FilterProductParams params) async { + Future> getOrders(FilterProductParams params) async { if (!await networkInfo.isConnected) { return Left(NetworkFailure()); } @@ -27,8 +28,8 @@ class OrderRepositoryImpl extends OrderRepository { try { final String token = await localDataSource.getToken(); - final orders = await remoteDataSource.getOrders(params, token); - return Right(orders); + final response = await remoteDataSource.getOrders(params, token); + return Right(response); } on Failure catch (failure) { return Left(failure); } diff --git a/lib/domain/entities/entities.dart b/lib/domain/entities/entities.dart index 8eea57f..b554eae 100644 --- a/lib/domain/entities/entities.dart +++ b/lib/domain/entities/entities.dart @@ -1,3 +1,5 @@ export 'user/user.dart'; export 'order/order.dart'; export 'order/filter_params_model.dart'; +export 'order/order_response.dart'; +export 'order/pagination_meta_data.dart'; diff --git a/lib/domain/entities/order/filter_params_model.dart b/lib/domain/entities/order/filter_params_model.dart index edc9810..783217e 100644 --- a/lib/domain/entities/order/filter_params_model.dart +++ b/lib/domain/entities/order/filter_params_model.dart @@ -1,19 +1,19 @@ class FilterProductParams { - final int? limit; - final int? pageSize; + final int offset; + final int limit; const FilterProductParams({ - this.limit = 0, - this.pageSize = 10, + this.offset = 1, + this.limit = 10, }); FilterProductParams copyWith({ - int? skip, + int? offset, int? limit, - int? pageSize, - }) => - FilterProductParams( - limit: skip ?? this.limit, - pageSize: pageSize ?? this.pageSize, - ); + }) { + return FilterProductParams( + offset: offset ?? this.offset, + limit: limit ?? this.limit, + ); + } } diff --git a/lib/domain/entities/order/order_response.dart b/lib/domain/entities/order/order_response.dart new file mode 100644 index 0000000..879046e --- /dev/null +++ b/lib/domain/entities/order/order_response.dart @@ -0,0 +1,9 @@ +import 'pagination_meta_data.dart'; +import 'order.dart'; + +class OrderResponse { + final List orders; + final PaginationMetaData paginationMetaData; + + OrderResponse({required this.orders, required this.paginationMetaData}); +} diff --git a/lib/domain/entities/order/pagination_meta_data.dart b/lib/domain/entities/order/pagination_meta_data.dart new file mode 100644 index 0000000..08e8d42 --- /dev/null +++ b/lib/domain/entities/order/pagination_meta_data.dart @@ -0,0 +1,11 @@ +class PaginationMetaData { + final int limit; + final int pageSize; + final int total; + + PaginationMetaData({ + required this.limit, + required this.pageSize, + required this.total, + }); +} diff --git a/lib/domain/repositories/order_repository.dart b/lib/domain/repositories/order_repository.dart index 95ea05c..861043d 100644 --- a/lib/domain/repositories/order_repository.dart +++ b/lib/domain/repositories/order_repository.dart @@ -1,8 +1,9 @@ import 'package:dartz/dartz.dart'; import '../../core/errors/failures.dart'; +import '../../data/models/order/order_response_model.dart'; import '../domain.dart'; abstract class OrderRepository { - Future>> getOrders(FilterProductParams params); + Future> getOrders(FilterProductParams params); } diff --git a/lib/domain/usecases/order/get_orders_usecase.dart b/lib/domain/usecases/order/get_orders_usecase.dart index 6adf68d..b721a58 100644 --- a/lib/domain/usecases/order/get_orders_usecase.dart +++ b/lib/domain/usecases/order/get_orders_usecase.dart @@ -1,14 +1,15 @@ import 'package:dartz/dartz.dart'; import '../../../core/core.dart'; +import '../../../data/models/order/order_response_model.dart'; import '../../domain.dart'; -class GetOrderUseCase implements UseCase, FilterProductParams> { +class GetOrderUseCase implements UseCase { final OrderRepository repository; GetOrderUseCase(this.repository); @override - Future>> call(FilterProductParams params) async { + Future> call(FilterProductParams params) async { return await repository.getOrders(params); } } diff --git a/lib/presentation/screens/orders.dart b/lib/presentation/screens/orders.dart index 23f7278..eb85748 100644 --- a/lib/presentation/screens/orders.dart +++ b/lib/presentation/screens/orders.dart @@ -23,79 +23,86 @@ class _OrdersScreenState extends State { Widget build(BuildContext context) { App.init(context); - // Provide the OrderBloc return Scaffold( backgroundColor: AppColors.surface, - body: CustomScrollView( - slivers: [ - const SliverToBoxAdapter( - child: OrderHeader(), - ), - SliverToBoxAdapter( - child: Padding( - padding: Space.all(1, 1), - child: const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Sargytlarym', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, + body: NotificationListener( + onNotification: (ScrollNotification scrollInfo) { + if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) { + context.read().add(const GetMoreOrders()); + } + return false; + }, + child: CustomScrollView( + slivers: [ + const SliverToBoxAdapter( + child: OrderHeader(), + ), + SliverToBoxAdapter( + child: Padding( + padding: Space.all(1, 1), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Sargytlarym', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), ), - ), - Text( - 'öz ýüküňizi yzarlaň', - style: TextStyle( - color: Colors.grey, + Text( + 'öz ýüküňizi yzarlaň', + style: TextStyle( + color: Colors.grey, + ), ), - ), - ], + ], + ), ), ), - ), - // Use BlocBuilder to respond to state changes - BlocBuilder( - builder: (context, state) { - if (state is OrderLoading) { - // Display a loading indicator while fetching orders - return const SliverToBoxAdapter( - child: Center( - child: CircularProgressIndicator(), - ), - ); - } else if (state is OrderError) { - // Display an error message if there was a failure - return SliverToBoxAdapter( - child: Center( - child: Text( - 'Failed to load orders: ${state.failure}', - style: const TextStyle(color: Colors.red), + BlocBuilder( + builder: (context, state) { + if (state is OrderLoading && state.orders.isEmpty) { + return const SliverToBoxAdapter( + child: Center( + child: CircularProgressIndicator(), ), - ), - ); - } else if (state is OrderLoaded) { - // Display the list of orders - return SliverList( - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - final order = state.orders[index]; - return OrderCard(order: order); - }, - childCount: state.orders.length, - ), - ); - } else { - // Default case (initial state) - return const SliverToBoxAdapter( - child: Center( - child: Text('No orders available'), - ), - ); - } - }, - ), - ], + ); + } else if (state is OrderError && state.orders.isEmpty) { + return SliverToBoxAdapter( + child: Center( + child: RetryWidget(onRetry: () { + context.read().add(GetOrders(state.params)); + }), + ), + ); + } else if (state is OrderLoaded) { + return SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + final order = state.orders[index]; + + if (index == state.orders.length - 1) { + // Trigger loading more orders when reaching the bottom + context.read().add(const GetMoreOrders()); + } + + return OrderCard(order: order); + }, + childCount: state.orders.length, + ), + ); + } else { + return const SliverToBoxAdapter( + child: Center( + child: Text('No orders available'), + ), + ); + } + }, + ), + ], + ), ), ); } diff --git a/lib/presentation/widgets/retry_widget.dart b/lib/presentation/widgets/retry_widget.dart new file mode 100644 index 0000000..c8628ae --- /dev/null +++ b/lib/presentation/widgets/retry_widget.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +class RetryWidget extends StatelessWidget { + final VoidCallback onRetry; + final String message; + + const RetryWidget({ + super.key, + required this.onRetry, + this.message = 'Something went wrong. Please try again.', + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + message, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16, color: Colors.black54), + ), + ), + ElevatedButton( + onPressed: onRetry, + child: const Text('Retry'), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/widgets.dart b/lib/presentation/widgets/widgets.dart index 625a21e..7f71691 100644 --- a/lib/presentation/widgets/widgets.dart +++ b/lib/presentation/widgets/widgets.dart @@ -1,12 +1,13 @@ export 'auth_error_dialog.dart'; export 'bottom_navbar.dart'; export 'button.dart'; +export 'dashed_line.dart'; export 'error_dialog.dart'; export 'info_card.dart'; +export 'lang_selection.dart'; export 'location_card.dart'; export 'order_card.dart'; export 'order_header.dart'; +export 'retry_widget.dart'; export 'successful_auth_dialog.dart'; export 'vertical_line.dart'; -export 'dashed_line.dart'; -export 'lang_selection.dart';