diff --git a/lib/core/constants/api_endpoints.dart b/lib/core/constants/api_endpoints.dart index 679d85d..0e880ce 100644 --- a/lib/core/constants/api_endpoints.dart +++ b/lib/core/constants/api_endpoints.dart @@ -58,6 +58,12 @@ class ApiEndpoints { /// Response: List of products static const String products = '/portalProduct/getAllProduct'; + /// Get product stage in warehouse + /// POST: /portalWareHouse/GetProductStageInWareHouse + /// Body: { "WareHouseId": int, "ProductId": int } + /// Response: Product details with stage information + static const String productDetail = '/portalWareHouse/GetProductStageInWareHouse'; + /// Get product by ID /// GET: (requires auth token) /// Parameter: productId diff --git a/lib/core/di/providers.dart b/lib/core/di/providers.dart index b2fd04a..86c4790 100644 --- a/lib/core/di/providers.dart +++ b/lib/core/di/providers.dart @@ -7,8 +7,11 @@ import '../../features/auth/domain/usecases/login_usecase.dart'; import '../../features/auth/presentation/providers/auth_provider.dart'; import '../../features/products/data/datasources/products_remote_datasource.dart'; import '../../features/products/data/repositories/products_repository_impl.dart'; +import '../../features/products/domain/entities/product_stage_entity.dart'; import '../../features/products/domain/repositories/products_repository.dart'; +import '../../features/products/domain/usecases/get_product_detail_usecase.dart'; import '../../features/products/domain/usecases/get_products_usecase.dart'; +import '../../features/products/presentation/providers/product_detail_provider.dart'; import '../../features/products/presentation/providers/products_provider.dart'; import '../../features/warehouse/data/datasources/warehouse_remote_datasource.dart'; import '../../features/warehouse/data/repositories/warehouse_repository_impl.dart'; @@ -269,6 +272,13 @@ final getProductsUseCaseProvider = Provider((ref) { return GetProductsUseCase(repository); }); +/// Get product detail use case provider +/// Encapsulates product detail fetching business logic +final getProductDetailUseCaseProvider = Provider((ref) { + final repository = ref.watch(productsRepositoryProvider); + return GetProductDetailUseCase(repository); +}); + // Presentation Layer /// Products state notifier provider @@ -340,6 +350,48 @@ final productsErrorProvider = Provider((ref) { return productsState.error; }); +/// Product detail state notifier provider +/// Manages product detail state for a specific product in a warehouse +/// This needs to be a family provider to support multiple product details +final productDetailProvider = + StateNotifierProvider.family( + (ref, _) { + final getProductDetailUseCase = ref.watch(getProductDetailUseCaseProvider); + return ProductDetailNotifier(getProductDetailUseCase); + }, +); + +/// Convenient providers for product detail state + +/// Provider to get product stages list +/// Usage: ref.watch(productStagesProvider(key)) +final productStagesProvider = Provider.family, String>((ref, key) { + final state = ref.watch(productDetailProvider(key)); + return state.stages; +}); + +/// Provider to get selected product stage +/// Usage: ref.watch(selectedProductStageProvider(key)) +final selectedProductStageProvider = Provider.family((ref, key) { + final state = ref.watch(productDetailProvider(key)); + return state.selectedStage; +}); + +/// Provider to check if product detail is loading +/// Usage: ref.watch(isProductDetailLoadingProvider(key)) +final isProductDetailLoadingProvider = Provider.family((ref, key) { + final state = ref.watch(productDetailProvider(key)); + return state.isLoading; +}); + +/// Provider to get product detail error +/// Returns null if no error +/// Usage: ref.watch(productDetailErrorProvider(key)) +final productDetailErrorProvider = Provider.family((ref, key) { + final state = ref.watch(productDetailProvider(key)); + return state.error; +}); + /// ======================================================================== /// USAGE EXAMPLES /// ======================================================================== diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 7e5c0b4..61bbabb 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -7,6 +7,7 @@ import '../../features/auth/di/auth_dependency_injection.dart'; import '../../features/warehouse/presentation/pages/warehouse_selection_page.dart'; import '../../features/operation/presentation/pages/operation_selection_page.dart'; import '../../features/products/presentation/pages/products_page.dart'; +import '../../features/products/presentation/pages/product_detail_page.dart'; import '../../features/warehouse/domain/entities/warehouse_entity.dart'; import '../storage/secure_storage.dart'; @@ -121,6 +122,49 @@ class AppRouter { ); }, ), + + /// Product Detail Route + /// Path: /product-detail + /// Takes warehouseId, productId, and warehouseName as extra parameter + /// Shows detailed information for a specific product + GoRoute( + path: '/product-detail', + name: 'product-detail', + builder: (context, state) { + final params = state.extra as Map?; + + if (params == null) { + // If no params, redirect to warehouses + WidgetsBinding.instance.addPostFrameCallback((_) { + context.go('/warehouses'); + }); + return const _ErrorScreen( + message: 'Product detail parameters are required', + ); + } + + // Extract required parameters + final warehouseId = params['warehouseId'] as int?; + final productId = params['productId'] as int?; + final warehouseName = params['warehouseName'] as String?; + + // Validate parameters + if (warehouseId == null || productId == null || warehouseName == null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + context.go('/warehouses'); + }); + return const _ErrorScreen( + message: 'Invalid product detail parameters', + ); + } + + return ProductDetailPage( + warehouseId: warehouseId, + productId: productId, + warehouseName: warehouseName, + ); + }, + ), ], // ==================== Error Handling ==================== @@ -326,6 +370,26 @@ extension AppRouterExtension on BuildContext { ); } + /// Navigate to product detail page + /// + /// [warehouseId] - ID of the warehouse + /// [productId] - ID of the product to view + /// [warehouseName] - Name of the warehouse (for display) + void goToProductDetail({ + required int warehouseId, + required int productId, + required String warehouseName, + }) { + push( + '/product-detail', + extra: { + 'warehouseId': warehouseId, + 'productId': productId, + 'warehouseName': warehouseName, + }, + ); + } + /// Pop current route void goBack() => pop(); } diff --git a/lib/docs/api.sh b/lib/docs/api.sh index 9e241c8..5ac64a0 100644 --- a/lib/docs/api.sh +++ b/lib/docs/api.sh @@ -52,4 +52,27 @@ curl --request GET \ --header 'Sec-Fetch-Dest: empty' \ --header 'Sec-Fetch-Mode: cors' \ --header 'Sec-Fetch-Site: same-site' \ - --header 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0' \ No newline at end of file + --header 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0' + +#Get product by id +curl --request POST \ + --url https://dotnet.elidev.info:8157/ws/portalWareHouse/GetProductStageInWareHouse \ + --compressed \ + --header 'Accept: application/json, text/plain, */*' \ + --header 'Accept-Encoding: gzip, deflate, br, zstd' \ + --header 'Accept-Language: en-US,en;q=0.5' \ + --header 'AccessToken: 1k5fXyQVXtGkfjwS4g2USldGyLSA7Zwa2jdj5tLe+3YfSDlk02aYgqsFh5xArkdL4N529x7IYJOGrLJJgLBPNVgD51zFfEYBzfJmMH2RUm7iegvDJaMCLISySw0zd6kcsqeJi7vtuybgY2NDPxDgiSOj4wX417PzB8AVg5bl1ZAAJ3LcVAqqtA1PDTU5ZU1QQYapNeBNxAHjnd2ojTZK1GJBIyY5Gd8P9gB880ppAKq8manNMZYsa4d8tkYf0SJUul2aqLIWJAwDGORpPmfjqkN4hMh85xAfPTZi6m4DdI0u2rHDMLaZ8eIsV16qA8wimSDnWi0VeG0SZ4ugbCdJAi3t1/uICTftiy06PJEkulBLV+h2xS/7SlmEY2xoN5ISi++3FNqsFPGa9QH6akGu2C7IXEUBCg3iGJx0uL+vULmVqk5OJIXdqiKVQ366hvhPlK2AM1zbh49x/ngibe08483WTL5uAY/fsKuBxQCpTc2368Gqhpd7QRtZFKpzikhyTWsR3nQIi6ExSstCeFbe8ehgo0PuTPZNHH5IHTc49snH6IZrSbR+F62Wu/D+4DlvMTK/ktG6LVQ3r3jSJC5MAQDV5Q9WK3RvsWMPvZrsaVW/Exz0GBgWP4W0adADg7MFSlnGDOJm6I4fCLHZIJCUww50L6iNmzvrdibrQT5jKACVgNquMZCfeZlf3m2BwUx9T6J45lAePpJ+QaMh+2voFqRiOLi98MLqOG6TW7z96sadzFVR9YU1xwM51jQDjnUlrXt0+msq29Jqt8LoCyQsG4r3RgS/tUJhximq11MDXsQV51BDt6Umpp4VKXfWllZcI1W9et5G18msjj8GtRXqaApWsfVcrnXk3s8rJVjeZocqi7vKw361ZLjd8onMzte884jxAx7qq/7Tdt6eQwSdzTHLwzxB3x+hvpbSPQQTkMrV4TLy7VuKLt7+duGDNPYGypFW1kamS3jZYmv26Pkr4xW257BEXduflDRKOOMjsr4K0d2KyYn0fJA6RzZoKWrUqBQyukkX6I8tzjopaTn0bKGCN32/lGVZ4bB3BMJMEphdFqaTyjS2p9k5/GcOt0qmrwztEinb+epzYJjsLXZheg==' \ + --header 'AppID: Minhthu2016' \ + --header 'Connection: keep-alive' \ + --header 'Origin: https://dotnet.elidev.info:8158' \ + --header 'Priority: u=0' \ + --header 'Referer: https://dotnet.elidev.info:8158/' \ + --header 'Sec-Fetch-Dest: empty' \ + --header 'Sec-Fetch-Mode: cors' \ + --header 'Sec-Fetch-Site: same-site' \ + --header 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0' \ + --header 'content-type: application/json' \ + --data '{ + "WareHouseId": 7, + "ProductId": 11 +}' \ No newline at end of file diff --git a/lib/features/operation/presentation/pages/operation_selection_page.dart b/lib/features/operation/presentation/pages/operation_selection_page.dart index 0f1b486..ca91761 100644 --- a/lib/features/operation/presentation/pages/operation_selection_page.dart +++ b/lib/features/operation/presentation/pages/operation_selection_page.dart @@ -144,7 +144,7 @@ class OperationSelectionPage extends ConsumerWidget { WarehouseEntity warehouse, String operationType, ) { - context.goNamed( + context.pushNamed( 'products', extra: { 'warehouse': warehouse, diff --git a/lib/features/products/data/datasources/products_remote_datasource.dart b/lib/features/products/data/datasources/products_remote_datasource.dart index cbad27e..96d9fdc 100644 --- a/lib/features/products/data/datasources/products_remote_datasource.dart +++ b/lib/features/products/data/datasources/products_remote_datasource.dart @@ -1,7 +1,10 @@ +import '../../../../core/constants/api_endpoints.dart'; import '../../../../core/errors/exceptions.dart'; import '../../../../core/network/api_client.dart'; import '../../../../core/network/api_response.dart'; +import '../models/product_detail_request_model.dart'; import '../models/product_model.dart'; +import '../models/product_stage_model.dart'; /// Abstract interface for products remote data source abstract class ProductsRemoteDataSource { @@ -13,6 +16,14 @@ abstract class ProductsRemoteDataSource { /// Returns List /// Throws [ServerException] if the API call fails Future> getProducts(int warehouseId, String type); + + /// Get product stages for a product in a warehouse + /// + /// [request] - Request containing warehouseId and productId + /// + /// Returns List with all stages for the product + /// Throws [ServerException] if the API call fails + Future> getProductDetail(ProductDetailRequestModel request); } /// Implementation of ProductsRemoteDataSource using ApiClient @@ -59,4 +70,55 @@ class ProductsRemoteDataSourceImpl implements ProductsRemoteDataSource { throw ServerException('Failed to get products: ${e.toString()}'); } } + + @override + Future> getProductDetail( + ProductDetailRequestModel request) async { + try { + // Make API call to get product stages + final response = await apiClient.post( + ApiEndpoints.productDetail, + data: request.toJson(), + ); + + // Parse the API response - the Value field contains a list of stage objects + final apiResponse = ApiResponse.fromJson( + response.data as Map, + (json) { + // The API returns a list of stages for the product + final list = json as List; + if (list.isEmpty) { + throw const ServerException('Product stages not found'); + } + // Parse all stages from the list + return list + .map((e) => ProductStageModel.fromJson(e as Map)) + .toList(); + }, + ); + + // Check if the API call was successful + if (apiResponse.isSuccess && apiResponse.value != null) { + return apiResponse.value!; + } else { + // Throw exception with error message from API + throw ServerException( + apiResponse.errors.isNotEmpty + ? apiResponse.errors.first + : 'Failed to get product stages', + ); + } + } catch (e) { + // Re-throw ServerException as-is + if (e is ServerException) { + rethrow; + } + // Re-throw NetworkException as-is + if (e is NetworkException) { + rethrow; + } + // Wrap other exceptions in ServerException + throw ServerException('Failed to get product stages: ${e.toString()}'); + } + } } diff --git a/lib/features/products/data/models/product_detail_request_model.dart b/lib/features/products/data/models/product_detail_request_model.dart new file mode 100644 index 0000000..f651880 --- /dev/null +++ b/lib/features/products/data/models/product_detail_request_model.dart @@ -0,0 +1,25 @@ +/// Request model for getting product details in a warehouse +/// +/// Used to fetch product stage information for a specific warehouse and product +class ProductDetailRequestModel { + final int warehouseId; + final int productId; + + const ProductDetailRequestModel({ + required this.warehouseId, + required this.productId, + }); + + /// Convert to JSON for API request + Map toJson() { + return { + 'WareHouseId': warehouseId, + 'ProductId': productId, + }; + } + + @override + String toString() { + return 'ProductDetailRequestModel(warehouseId: $warehouseId, productId: $productId)'; + } +} diff --git a/lib/features/products/data/models/product_stage_model.dart b/lib/features/products/data/models/product_stage_model.dart new file mode 100644 index 0000000..206784e --- /dev/null +++ b/lib/features/products/data/models/product_stage_model.dart @@ -0,0 +1,67 @@ +import '../../domain/entities/product_stage_entity.dart'; + +/// Product stage model for data layer +/// Represents a product at a specific production stage +class ProductStageModel extends ProductStageEntity { + const ProductStageModel({ + required super.productId, + required super.productStageId, + required super.actionTypeId, + required super.passedQuantity, + required super.issuedQuantity, + required super.issuedQuantityWeight, + required super.passedQuantityWeight, + required super.stageName, + required super.createdDate, + }); + + /// Create ProductStageModel from JSON + factory ProductStageModel.fromJson(Map json) { + return ProductStageModel( + productId: json['ProductId'] as int, + productStageId: json['ProductStageId'] as int?, + actionTypeId: json['ActionTypeId'] as int?, + passedQuantity: json['PassedQuantity'] as int, + issuedQuantity: json['IssuedQuantity'] as int, + issuedQuantityWeight: (json['IssuedQuantityWeight'] as num).toDouble(), + passedQuantityWeight: (json['PassedQuantityWeight'] as num).toDouble(), + stageName: json['StageName'] as String?, + createdDate: json['CreatedDate'] as String, + ); + } + + /// Convert to JSON + Map toJson() { + return { + 'ProductId': productId, + 'ProductStageId': productStageId, + 'ActionTypeId': actionTypeId, + 'PassedQuantity': passedQuantity, + 'IssuedQuantity': issuedQuantity, + 'IssuedQuantityWeight': issuedQuantityWeight, + 'PassedQuantityWeight': passedQuantityWeight, + 'StageName': stageName, + 'CreatedDate': createdDate, + }; + } + + /// Convert to entity + ProductStageEntity toEntity() { + return ProductStageEntity( + productId: productId, + productStageId: productStageId, + actionTypeId: actionTypeId, + passedQuantity: passedQuantity, + issuedQuantity: issuedQuantity, + issuedQuantityWeight: issuedQuantityWeight, + passedQuantityWeight: passedQuantityWeight, + stageName: stageName, + createdDate: createdDate, + ); + } + + @override + String toString() { + return 'ProductStageModel(productId: $productId, stageName: $stageName, passedQuantity: $passedQuantity)'; + } +} diff --git a/lib/features/products/data/repositories/products_repository_impl.dart b/lib/features/products/data/repositories/products_repository_impl.dart index 86c2247..504a070 100644 --- a/lib/features/products/data/repositories/products_repository_impl.dart +++ b/lib/features/products/data/repositories/products_repository_impl.dart @@ -2,8 +2,10 @@ import 'package:dartz/dartz.dart'; import '../../../../core/errors/exceptions.dart'; import '../../../../core/errors/failures.dart'; import '../../domain/entities/product_entity.dart'; +import '../../domain/entities/product_stage_entity.dart'; import '../../domain/repositories/products_repository.dart'; import '../datasources/products_remote_datasource.dart'; +import '../models/product_detail_request_model.dart'; /// Implementation of ProductsRepository /// Handles data operations and error conversion @@ -34,4 +36,33 @@ class ProductsRepositoryImpl implements ProductsRepository { return Left(ServerFailure('Unexpected error: ${e.toString()}')); } } + + @override + Future>> getProductDetail( + int warehouseId, + int productId, + ) async { + try { + // Create request model + final request = ProductDetailRequestModel( + warehouseId: warehouseId, + productId: productId, + ); + + // Fetch product stages from remote data source + final stages = await remoteDataSource.getProductDetail(request); + + // Convert models to entities and return success + return Right(stages.map((stage) => stage.toEntity()).toList()); + } on ServerException catch (e) { + // Convert ServerException to ServerFailure + return Left(ServerFailure(e.message)); + } on NetworkException catch (e) { + // Convert NetworkException to NetworkFailure + return Left(NetworkFailure(e.message)); + } catch (e) { + // Handle any other exceptions + return Left(ServerFailure('Unexpected error: ${e.toString()}')); + } + } } diff --git a/lib/features/products/domain/entities/product_stage_entity.dart b/lib/features/products/domain/entities/product_stage_entity.dart new file mode 100644 index 0000000..0b11f78 --- /dev/null +++ b/lib/features/products/domain/entities/product_stage_entity.dart @@ -0,0 +1,58 @@ +import 'package:equatable/equatable.dart'; + +/// Product stage entity - pure domain model +/// Represents a product at a specific production stage with quantities +class ProductStageEntity extends Equatable { + final int productId; + final int? productStageId; + final int? actionTypeId; + final int passedQuantity; + final int issuedQuantity; + final double issuedQuantityWeight; + final double passedQuantityWeight; + final String? stageName; + final String createdDate; + + const ProductStageEntity({ + required this.productId, + required this.productStageId, + required this.actionTypeId, + required this.passedQuantity, + required this.issuedQuantity, + required this.issuedQuantityWeight, + required this.passedQuantityWeight, + required this.stageName, + required this.createdDate, + }); + + /// Get display name for the stage + /// Returns "No Stage" if stageName is null + String get displayName => stageName ?? 'No Stage'; + + /// Check if this is a valid stage (has a stage name) + bool get hasStage => stageName != null && stageName!.isNotEmpty; + + /// Check if this stage has any passed quantity + bool get hasPassedQuantity => passedQuantity > 0; + + /// Check if this stage has any issued quantity + bool get hasIssuedQuantity => issuedQuantity > 0; + + @override + List get props => [ + productId, + productStageId, + actionTypeId, + passedQuantity, + issuedQuantity, + issuedQuantityWeight, + passedQuantityWeight, + stageName, + createdDate, + ]; + + @override + String toString() { + return 'ProductStageEntity(productId: $productId, stageName: $stageName, passedQuantity: $passedQuantity)'; + } +} diff --git a/lib/features/products/domain/repositories/products_repository.dart b/lib/features/products/domain/repositories/products_repository.dart index a97a627..f075eb6 100644 --- a/lib/features/products/domain/repositories/products_repository.dart +++ b/lib/features/products/domain/repositories/products_repository.dart @@ -1,6 +1,7 @@ import 'package:dartz/dartz.dart'; import '../../../../core/errors/failures.dart'; import '../entities/product_entity.dart'; +import '../entities/product_stage_entity.dart'; /// Abstract repository interface for products /// Defines the contract for product data operations @@ -15,4 +16,15 @@ abstract class ProductsRepository { int warehouseId, String type, ); + + /// Get product stages for a product in a warehouse + /// + /// [warehouseId] - The ID of the warehouse + /// [productId] - The ID of the product + /// + /// Returns Either> + Future>> getProductDetail( + int warehouseId, + int productId, + ); } diff --git a/lib/features/products/domain/usecases/get_product_detail_usecase.dart b/lib/features/products/domain/usecases/get_product_detail_usecase.dart new file mode 100644 index 0000000..0a5c4d5 --- /dev/null +++ b/lib/features/products/domain/usecases/get_product_detail_usecase.dart @@ -0,0 +1,25 @@ +import 'package:dartz/dartz.dart'; +import '../../../../core/errors/failures.dart'; +import '../entities/product_stage_entity.dart'; +import '../repositories/products_repository.dart'; + +/// Use case for getting product stages +/// Encapsulates the business logic for fetching product stage information +class GetProductDetailUseCase { + final ProductsRepository repository; + + GetProductDetailUseCase(this.repository); + + /// Execute the use case + /// + /// [warehouseId] - The ID of the warehouse + /// [productId] - The ID of the product + /// + /// Returns Either> + Future>> call( + int warehouseId, + int productId, + ) async { + return await repository.getProductDetail(warehouseId, productId); + } +} diff --git a/lib/features/products/presentation/pages/product_detail_page.dart b/lib/features/products/presentation/pages/product_detail_page.dart new file mode 100644 index 0000000..d380bb4 --- /dev/null +++ b/lib/features/products/presentation/pages/product_detail_page.dart @@ -0,0 +1,460 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/di/providers.dart'; +import '../../domain/entities/product_stage_entity.dart'; + +/// Product detail page +/// Displays product stages as chips and shows selected stage information +class ProductDetailPage extends ConsumerStatefulWidget { + final int warehouseId; + final int productId; + final String warehouseName; + + const ProductDetailPage({ + super.key, + required this.warehouseId, + required this.productId, + required this.warehouseName, + }); + + @override + ConsumerState createState() => _ProductDetailPageState(); +} + +class _ProductDetailPageState extends ConsumerState { + late String _providerKey; + + @override + void initState() { + super.initState(); + _providerKey = '${widget.warehouseId}_${widget.productId}'; + + // Load product stages when page is initialized + Future.microtask(() { + ref.read(productDetailProvider(_providerKey).notifier).loadProductDetail( + widget.warehouseId, + widget.productId, + ); + }); + } + + Future _onRefresh() async { + await ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail( + widget.warehouseId, + widget.productId, + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + + // Watch the product detail state + final productDetailState = ref.watch(productDetailProvider(_providerKey)); + final stages = productDetailState.stages; + final selectedStage = productDetailState.selectedStage; + final isLoading = productDetailState.isLoading; + final error = productDetailState.error; + final selectedIndex = productDetailState.selectedStageIndex; + + return Scaffold( + appBar: AppBar( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Product Stages', + style: textTheme.titleMedium, + ), + Text( + widget.warehouseName, + style: textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _onRefresh, + tooltip: 'Refresh', + ), + ], + ), + body: _buildBody( + isLoading: isLoading, + error: error, + stages: stages, + selectedStage: selectedStage, + selectedIndex: selectedIndex, + theme: theme, + ), + ); + } + + Widget _buildBody({ + required bool isLoading, + required String? error, + required List stages, + required ProductStageEntity? selectedStage, + required int selectedIndex, + required ThemeData theme, + }) { + if (isLoading && stages.isEmpty) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (error != null && stages.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 48, + color: theme.colorScheme.error, + ), + const SizedBox(height: 16), + Text( + 'Error', + style: theme.textTheme.titleLarge?.copyWith( + color: theme.colorScheme.error, + ), + ), + const SizedBox(height: 8), + Text( + error, + textAlign: TextAlign.center, + style: theme.textTheme.bodyMedium, + ), + const SizedBox(height: 24), + FilledButton.icon( + onPressed: _onRefresh, + icon: const Icon(Icons.refresh), + label: const Text('Retry'), + ), + ], + ), + ), + ); + } + + if (stages.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inventory_2_outlined, + size: 48, + color: theme.colorScheme.onSurface.withValues(alpha: 0.5), + ), + const SizedBox(height: 16), + Text( + 'No stages found', + style: theme.textTheme.titleLarge, + ), + ], + ), + ); + } + + return RefreshIndicator( + onRefresh: _onRefresh, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Stage chips section + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + border: Border( + bottom: BorderSide( + color: theme.colorScheme.outline.withValues(alpha: 0.2), + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Production Stages (${stages.length})', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: List.generate(stages.length, (index) { + final stage = stages[index]; + final isSelected = index == selectedIndex; + + return FilterChip( + selected: isSelected, + label: Text(stage.displayName), + onSelected: (_) { + ref + .read(productDetailProvider(_providerKey).notifier) + .selectStage(index); + }, + backgroundColor: theme.colorScheme.surface, + selectedColor: theme.colorScheme.primaryContainer, + checkmarkColor: theme.colorScheme.primary, + labelStyle: TextStyle( + color: isSelected + ? theme.colorScheme.onPrimaryContainer + : theme.colorScheme.onSurface, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), + ); + }), + ), + ], + ), + ), + + // Stage details section + Expanded( + child: selectedStage == null + ? const Center(child: Text('No stage selected')) + : SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Stage header + _buildStageHeader(selectedStage, theme), + const SizedBox(height: 16), + + // Quantity information + _buildSectionCard( + theme: theme, + title: 'Quantities', + icon: Icons.inventory_outlined, + children: [ + _buildInfoRow('Passed Quantity', '${selectedStage.passedQuantity}'), + _buildInfoRow( + 'Passed Weight', + '${selectedStage.passedQuantityWeight.toStringAsFixed(2)} kg', + ), + const Divider(height: 24), + _buildInfoRow('Issued Quantity', '${selectedStage.issuedQuantity}'), + _buildInfoRow( + 'Issued Weight', + '${selectedStage.issuedQuantityWeight.toStringAsFixed(2)} kg', + ), + ], + ), + const SizedBox(height: 16), + + // Stage information + _buildSectionCard( + theme: theme, + title: 'Stage Information', + icon: Icons.info_outlined, + children: [ + _buildInfoRow('Product ID', '${selectedStage.productId}'), + if (selectedStage.productStageId != null) + _buildInfoRow('Stage ID', '${selectedStage.productStageId}'), + if (selectedStage.actionTypeId != null) + _buildInfoRow('Action Type ID', '${selectedStage.actionTypeId}'), + _buildInfoRow('Stage Name', selectedStage.displayName), + ], + ), + const SizedBox(height: 16), + + // Status indicators + _buildStatusCards(selectedStage, theme), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildStageHeader(ProductStageEntity stage, ThemeData theme) { + return Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.build_circle_outlined, + size: 32, + color: theme.colorScheme.primary, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + stage.displayName, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + 'Product ID: ${stage.productId}', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildSectionCard({ + required ThemeData theme, + required String title, + required IconData icon, + required List children, + }) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + icon, + size: 20, + color: theme.colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + title, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + ...children, + ], + ), + ), + ); + } + + Widget _buildInfoRow(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: Text( + label, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 3, + child: Text( + value, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.end, + ), + ), + ], + ), + ); + } + + Widget _buildStatusCards(ProductStageEntity stage, ThemeData theme) { + return Row( + children: [ + Expanded( + child: Card( + color: stage.hasPassedQuantity + ? Colors.green.withValues(alpha: 0.1) + : Colors.grey.withValues(alpha: 0.1), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Icon( + stage.hasPassedQuantity ? Icons.check_circle : Icons.cancel, + color: stage.hasPassedQuantity ? Colors.green : Colors.grey, + size: 32, + ), + const SizedBox(height: 8), + Text( + 'Has Passed', + style: theme.textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Card( + color: stage.hasIssuedQuantity + ? Colors.blue.withValues(alpha: 0.1) + : Colors.grey.withValues(alpha: 0.1), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Icon( + stage.hasIssuedQuantity ? Icons.check_circle : Icons.cancel, + color: stage.hasIssuedQuantity ? Colors.blue : Colors.grey, + size: 32, + ), + const SizedBox(height: 8), + Text( + 'Has Issued', + style: theme.textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/features/products/presentation/pages/products_page.dart b/lib/features/products/presentation/pages/products_page.dart index 6d45c36..a3d4c15 100644 --- a/lib/features/products/presentation/pages/products_page.dart +++ b/lib/features/products/presentation/pages/products_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/di/providers.dart'; +import '../../../../core/router/app_router.dart'; import '../widgets/product_list_item.dart'; /// Products list page @@ -261,13 +262,11 @@ class _ProductsPageState extends ConsumerState { return ProductListItem( product: product, onTap: () { - // Handle product tap if needed - // For now, just show a snackbar - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Selected: ${product.fullName}'), - duration: const Duration(seconds: 1), - ), + // Navigate to product detail page + context.goToProductDetail( + warehouseId: widget.warehouseId, + productId: product.id, + warehouseName: widget.warehouseName, ); }, ); diff --git a/lib/features/products/presentation/providers/product_detail_provider.dart b/lib/features/products/presentation/providers/product_detail_provider.dart new file mode 100644 index 0000000..412391b --- /dev/null +++ b/lib/features/products/presentation/providers/product_detail_provider.dart @@ -0,0 +1,106 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../domain/entities/product_stage_entity.dart'; +import '../../domain/usecases/get_product_detail_usecase.dart'; + +/// Product detail state class +/// Holds the current state of product stages and selection +class ProductDetailState { + final List stages; + final int selectedStageIndex; + final bool isLoading; + final String? error; + + const ProductDetailState({ + this.stages = const [], + this.selectedStageIndex = 0, + this.isLoading = false, + this.error, + }); + + /// Get the currently selected stage + ProductStageEntity? get selectedStage { + if (stages.isEmpty || selectedStageIndex >= stages.length) { + return null; + } + return stages[selectedStageIndex]; + } + + /// Check if there are any stages loaded + bool get hasStages => stages.isNotEmpty; + + ProductDetailState copyWith({ + List? stages, + int? selectedStageIndex, + bool? isLoading, + String? error, + }) { + return ProductDetailState( + stages: stages ?? this.stages, + selectedStageIndex: selectedStageIndex ?? this.selectedStageIndex, + isLoading: isLoading ?? this.isLoading, + error: error, + ); + } +} + +/// Product detail notifier +/// Manages the product detail state and business logic +class ProductDetailNotifier extends StateNotifier { + final GetProductDetailUseCase getProductDetailUseCase; + + ProductDetailNotifier(this.getProductDetailUseCase) + : super(const ProductDetailState()); + + /// Load product stages for a specific warehouse and product + /// + /// [warehouseId] - The ID of the warehouse + /// [productId] - The ID of the product + Future loadProductDetail(int warehouseId, int productId) async { + // Set loading state + state = state.copyWith( + isLoading: true, + error: null, + ); + + // Call the use case + final result = await getProductDetailUseCase(warehouseId, productId); + + // Handle the result + result.fold( + (failure) { + // Handle failure + state = state.copyWith( + isLoading: false, + error: failure.message, + stages: [], + ); + }, + (stages) { + // Handle success - load all stages + state = state.copyWith( + isLoading: false, + error: null, + stages: stages, + selectedStageIndex: 0, // Select first stage by default + ); + }, + ); + } + + /// Select a stage by index + void selectStage(int index) { + if (index >= 0 && index < state.stages.length) { + state = state.copyWith(selectedStageIndex: index); + } + } + + /// Refresh product stages + Future refreshProductDetail(int warehouseId, int productId) async { + await loadProductDetail(warehouseId, productId); + } + + /// Clear product detail + void clearProductDetail() { + state = const ProductDetailState(); + } +} diff --git a/lib/features/warehouse/ARCHITECTURE.md b/lib/features/warehouse/ARCHITECTURE.md deleted file mode 100644 index 3f53293..0000000 --- a/lib/features/warehouse/ARCHITECTURE.md +++ /dev/null @@ -1,398 +0,0 @@ -# Warehouse Feature - Architecture Diagram - -## Clean Architecture Layers - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ PRESENTATION LAYER │ -│ (UI, State Management, User Interactions) │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────┐ ┌──────────────────────────────────┐ │ -│ │ WarehouseCard │ │ WarehouseSelectionPage │ │ -│ │ - Shows warehouse │ │ - Displays warehouse list │ │ -│ │ information │ │ - Handles user selection │ │ -│ └─────────────────────┘ │ - Pull to refresh │ │ -│ │ - Loading/Error/Empty states │ │ -│ └──────────────────────────────────┘ │ -│ ↓ │ -│ ┌──────────────────────────────────┐ │ -│ │ WarehouseNotifier │ │ -│ │ (StateNotifier) │ │ -│ │ - loadWarehouses() │ │ -│ │ - selectWarehouse() │ │ -│ │ - refresh() │ │ -│ └──────────────────────────────────┘ │ -│ ↓ │ -│ ┌──────────────────────────────────┐ │ -│ │ WarehouseState │ │ -│ │ - warehouses: List │ │ -│ │ - selectedWarehouse: Warehouse? │ │ -│ │ - isLoading: bool │ │ -│ │ - error: String? │ │ -│ └──────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ uses -┌─────────────────────────────────────────────────────────────────┐ -│ DOMAIN LAYER │ -│ (Business Logic, Entities, Use Cases) │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ GetWarehousesUseCase │ │ -│ │ - Encapsulates business logic for fetching warehouses │ │ -│ │ - Single responsibility │ │ -│ │ - Returns Either> │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ ↓ uses │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ WarehouseRepository (Interface) │ │ -│ │ + getWarehouses(): Either> │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ WarehouseEntity │ │ -│ │ - id: int │ │ -│ │ - name: String │ │ -│ │ - code: String │ │ -│ │ - description: String? │ │ -│ │ - isNGWareHouse: bool │ │ -│ │ - totalCount: int │ │ -│ │ + hasItems: bool │ │ -│ │ + isNGType: bool │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ implements -┌─────────────────────────────────────────────────────────────────┐ -│ DATA LAYER │ -│ (API Calls, Data Sources, Models) │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ WarehouseRepositoryImpl │ │ -│ │ - Implements WarehouseRepository interface │ │ -│ │ - Coordinates data sources │ │ -│ │ - Converts exceptions to failures │ │ -│ │ - Maps models to entities │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ ↓ uses │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ WarehouseRemoteDataSource (Interface) │ │ -│ │ + getWarehouses(): Future> │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ ↓ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ WarehouseRemoteDataSourceImpl │ │ -│ │ - Makes API calls using ApiClient │ │ -│ │ - Parses ApiResponse wrapper │ │ -│ │ - Throws ServerException or NetworkException │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ ↓ uses │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ WarehouseModel │ │ -│ │ - Extends WarehouseEntity │ │ -│ │ - Adds JSON serialization (fromJson, toJson) │ │ -│ │ - Maps API fields to entity fields │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ ↓ uses │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ ApiClient (Core) │ │ -│ │ - Dio HTTP client wrapper │ │ -│ │ - Adds authentication headers │ │ -│ │ - Handles 401 errors │ │ -│ │ - Logging and error handling │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -## Data Flow - -### 1. Loading Warehouses Flow - -``` -User Action (Pull to Refresh / Page Load) - ↓ -WarehouseSelectionPage - ↓ calls -ref.read(warehouseProvider.notifier).loadWarehouses() - ↓ -WarehouseNotifier.loadWarehouses() - ↓ sets state -state = state.setLoading() → UI shows loading indicator - ↓ calls -GetWarehousesUseCase.call() - ↓ calls -WarehouseRepository.getWarehouses() - ↓ calls -WarehouseRemoteDataSource.getWarehouses() - ↓ makes HTTP request -ApiClient.get('/warehouses') - ↓ API Response -{ - "Value": [...], - "IsSuccess": true, - "IsFailure": false, - "Errors": [], - "ErrorCodes": [] -} - ↓ parse -List from JSON - ↓ convert -List - ↓ wrap -Right(warehouses) or Left(failure) - ↓ update state -state = state.setSuccess(warehouses) - ↓ -UI rebuilds with warehouse list -``` - -### 2. Error Handling Flow - -``` -API Error / Network Error - ↓ -ApiClient throws DioException - ↓ -_handleDioError() converts to custom exception - ↓ -ServerException or NetworkException - ↓ -WarehouseRemoteDataSource catches and rethrows - ↓ -WarehouseRepositoryImpl catches exception - ↓ -Converts to Failure: - - ServerException → ServerFailure - - NetworkException → NetworkFailure - ↓ -Returns Left(failure) - ↓ -GetWarehousesUseCase returns Left(failure) - ↓ -WarehouseNotifier receives Left(failure) - ↓ -state = state.setError(failure.message) - ↓ -UI shows error state with retry button -``` - -### 3. Warehouse Selection Flow - -``` -User taps on WarehouseCard - ↓ -onTap callback triggered - ↓ -_onWarehouseSelected(warehouse) - ↓ -ref.read(warehouseProvider.notifier).selectWarehouse(warehouse) - ↓ -state = state.setSelectedWarehouse(warehouse) - ↓ -Navigation: context.push('/operations', extra: warehouse) - ↓ -OperationSelectionPage receives warehouse -``` - -## Dependency Graph - -``` -┌─────────────────────────────────────────────────┐ -│ Riverpod Providers │ -├─────────────────────────────────────────────────┤ -│ │ -│ secureStorageProvider │ -│ ↓ │ -│ apiClientProvider │ -│ ↓ │ -│ warehouseRemoteDataSourceProvider │ -│ ↓ │ -│ warehouseRepositoryProvider │ -│ ↓ │ -│ getWarehousesUseCaseProvider │ -│ ↓ │ -│ warehouseProvider (StateNotifierProvider) │ -│ ↓ │ -│ WarehouseSelectionPage watches this provider │ -│ │ -└─────────────────────────────────────────────────┘ -``` - -## File Dependencies - -``` -warehouse_selection_page.dart - ↓ imports - - warehouse_entity.dart - - warehouse_card.dart - - warehouse_provider.dart (via DI setup) - -warehouse_card.dart - ↓ imports - - warehouse_entity.dart - -warehouse_provider.dart - ↓ imports - - warehouse_entity.dart - - get_warehouses_usecase.dart - -get_warehouses_usecase.dart - ↓ imports - - warehouse_entity.dart - - warehouse_repository.dart (interface) - -warehouse_repository_impl.dart - ↓ imports - - warehouse_entity.dart - - warehouse_repository.dart (interface) - - warehouse_remote_datasource.dart - -warehouse_remote_datasource.dart - ↓ imports - - warehouse_model.dart - - api_client.dart - - api_response.dart - -warehouse_model.dart - ↓ imports - - warehouse_entity.dart -``` - -## State Transitions - -``` -┌──────────────┐ -│ Initial │ -│ isLoading: F │ -│ error: null │ -│ warehouses:[]│ -└──────────────┘ - ↓ - loadWarehouses() - ↓ -┌──────────────┐ -│ Loading │ -│ isLoading: T │────────────────┐ -│ error: null │ │ -│ warehouses:[]│ │ -└──────────────┘ │ - ↓ │ - Success Failure - ↓ ↓ -┌──────────────┐ ┌──────────────┐ -│ Success │ │ Error │ -│ isLoading: F │ │ isLoading: F │ -│ error: null │ │ error: "..." │ -│ warehouses:[…]│ │ warehouses:[]│ -└──────────────┘ └──────────────┘ - ↓ ↓ - Selection Retry - ↓ ↓ -┌──────────────┐ (back to Loading) -│ Selected │ -│ selected: W │ -└──────────────┘ -``` - -## API Response Parsing - -``` -Raw API Response (JSON) - ↓ -{ - "Value": [ - { - "Id": 1, - "Name": "Warehouse A", - "Code": "001", - ... - } - ], - "IsSuccess": true, - ... -} - ↓ -ApiResponse.fromJson() parses wrapper - ↓ -ApiResponse> { - value: [WarehouseModel, WarehouseModel, ...], - isSuccess: true, - isFailure: false, - errors: [], - errorCodes: [] -} - ↓ -Check isSuccess - ↓ -if (isSuccess && value != null) - return value! -else - throw ServerException(errors.first) - ↓ -List - ↓ -map((model) => model.toEntity()) - ↓ -List -``` - -## Separation of Concerns - -### Domain Layer -- **No dependencies** on Flutter, Dio, or other frameworks -- Contains **pure business logic** -- Defines **contracts** (repository interfaces) -- **Independent** and **testable** - -### Data Layer -- **Implements** domain contracts -- Handles **external dependencies** (API, database) -- **Converts** between models and entities -- **Transforms** exceptions to failures - -### Presentation Layer -- **Depends** only on domain layer -- Handles **UI rendering** and **user interactions** -- Manages **local state** with Riverpod -- **Observes** changes and **reacts** to state updates - -## Testing Strategy - -``` -Unit Tests -├── Domain Layer -│ ├── Test entities (equality, methods) -│ ├── Test use cases (mock repository) -│ └── Verify business logic -├── Data Layer -│ ├── Test models (JSON serialization) -│ ├── Test data sources (mock ApiClient) -│ └── Test repository (mock data source) -└── Presentation Layer - ├── Test notifier (mock use case) - └── Test state transitions - -Widget Tests -├── Test UI rendering -├── Test user interactions -└── Test state-based UI changes - -Integration Tests -├── Test complete flow -└── Test with real dependencies -``` - -## Benefits of This Architecture - -1. **Testability**: Each layer can be tested independently with mocks -2. **Maintainability**: Changes in one layer don't affect others -3. **Scalability**: Easy to add new features following the same pattern -4. **Reusability**: Domain entities and use cases can be reused -5. **Separation**: Clear boundaries between UI, business logic, and data -6. **Flexibility**: Easy to swap implementations (e.g., change API client) - ---- - -**Last Updated:** 2025-10-27 -**Version:** 1.0.0 diff --git a/lib/features/warehouse/README.md b/lib/features/warehouse/README.md deleted file mode 100644 index e0fa3f8..0000000 --- a/lib/features/warehouse/README.md +++ /dev/null @@ -1,649 +0,0 @@ -# Warehouse Feature - -Complete implementation of the warehouse feature following **Clean Architecture** principles. - -## Architecture Overview - -This feature follows a three-layer clean architecture pattern: - -``` -Presentation Layer (UI) - ↓ (uses) -Domain Layer (Business Logic) - ↓ (uses) -Data Layer (API & Data Sources) -``` - -### Key Principles - -- **Separation of Concerns**: Each layer has a single responsibility -- **Dependency Inversion**: Outer layers depend on inner layers, not vice versa -- **Testability**: Each layer can be tested independently -- **Maintainability**: Changes in one layer don't affect others - -## Project Structure - -``` -lib/features/warehouse/ -├── data/ -│ ├── datasources/ -│ │ └── warehouse_remote_datasource.dart # API calls using ApiClient -│ ├── models/ -│ │ └── warehouse_model.dart # Data transfer objects with JSON serialization -│ └── repositories/ -│ └── warehouse_repository_impl.dart # Repository implementation -├── domain/ -│ ├── entities/ -│ │ └── warehouse_entity.dart # Pure business models -│ ├── repositories/ -│ │ └── warehouse_repository.dart # Repository interface/contract -│ └── usecases/ -│ └── get_warehouses_usecase.dart # Business logic use cases -├── presentation/ -│ ├── pages/ -│ │ └── warehouse_selection_page.dart # Main warehouse selection screen -│ ├── providers/ -│ │ └── warehouse_provider.dart # Riverpod state management -│ └── widgets/ -│ └── warehouse_card.dart # Reusable warehouse card widget -├── warehouse_exports.dart # Barrel file for clean imports -├── warehouse_provider_setup_example.dart # Provider setup guide -└── README.md # This file -``` - -## Layer Details - -### 1. Domain Layer (Core Business Logic) - -The innermost layer that contains business entities, repository interfaces, and use cases. **No dependencies on external frameworks or packages** (except dartz for Either). - -#### Entities - -`domain/entities/warehouse_entity.dart` - -- Pure Dart class representing a warehouse -- No JSON serialization logic -- Contains business rules and validations -- Extends Equatable for value comparison - -```dart -class WarehouseEntity extends Equatable { - final int id; - final String name; - final String code; - final String? description; - final bool isNGWareHouse; - final int totalCount; - - bool get hasItems => totalCount > 0; - bool get isNGType => isNGWareHouse; -} -``` - -#### Repository Interface - -`domain/repositories/warehouse_repository.dart` - -- Abstract interface defining data operations -- Returns `Either` for error handling -- Implementation is provided by the data layer - -```dart -abstract class WarehouseRepository { - Future>> getWarehouses(); -} -``` - -#### Use Cases - -`domain/usecases/get_warehouses_usecase.dart` - -- Single responsibility: fetch warehouses -- Encapsulates business logic -- Depends only on repository interface - -```dart -class GetWarehousesUseCase { - final WarehouseRepository repository; - - Future>> call() async { - return await repository.getWarehouses(); - } -} -``` - -### 2. Data Layer (External Data Management) - -Handles all data operations including API calls, JSON serialization, and error handling. - -#### Models - -`data/models/warehouse_model.dart` - -- Extends domain entity -- Adds JSON serialization (`fromJson`, `toJson`) -- Maps API response format to domain entities -- Matches API field naming (PascalCase) - -```dart -class WarehouseModel extends WarehouseEntity { - factory WarehouseModel.fromJson(Map json) { - return WarehouseModel( - id: json['Id'] ?? 0, - name: json['Name'] ?? '', - code: json['Code'] ?? '', - description: json['Description'], - isNGWareHouse: json['IsNGWareHouse'] ?? false, - totalCount: json['TotalCount'] ?? 0, - ); - } -} -``` - -#### Data Sources - -`data/datasources/warehouse_remote_datasource.dart` - -- Interface + implementation pattern -- Makes API calls using `ApiClient` -- Parses `ApiResponse` wrapper -- Throws typed exceptions (`ServerException`, `NetworkException`) - -```dart -class WarehouseRemoteDataSourceImpl implements WarehouseRemoteDataSource { - Future> getWarehouses() async { - final response = await apiClient.get('/warehouses'); - final apiResponse = ApiResponse.fromJson( - response.data, - (json) => (json as List).map((e) => WarehouseModel.fromJson(e)).toList(), - ); - - if (apiResponse.isSuccess && apiResponse.value != null) { - return apiResponse.value!; - } else { - throw ServerException(apiResponse.errors.first); - } - } -} -``` - -#### Repository Implementation - -`data/repositories/warehouse_repository_impl.dart` - -- Implements domain repository interface -- Coordinates data sources -- Converts exceptions to failures -- Maps models to entities - -```dart -class WarehouseRepositoryImpl implements WarehouseRepository { - @override - Future>> getWarehouses() async { - try { - final warehouses = await remoteDataSource.getWarehouses(); - final entities = warehouses.map((model) => model.toEntity()).toList(); - return Right(entities); - } on ServerException catch (e) { - return Left(ServerFailure(e.message)); - } on NetworkException catch (e) { - return Left(NetworkFailure(e.message)); - } - } -} -``` - -### 3. Presentation Layer (UI & State Management) - -Handles UI rendering, user interactions, and state management using Riverpod. - -#### State Management - -`presentation/providers/warehouse_provider.dart` - -- `WarehouseState`: Immutable state class - - `warehouses`: List of warehouses - - `selectedWarehouse`: Currently selected warehouse - - `isLoading`: Loading indicator - - `error`: Error message - -- `WarehouseNotifier`: StateNotifier managing state - - `loadWarehouses()`: Fetch warehouses from API - - `selectWarehouse()`: Select a warehouse - - `refresh()`: Reload warehouses - - `clearError()`: Clear error state - -```dart -class WarehouseState { - final List warehouses; - final WarehouseEntity? selectedWarehouse; - final bool isLoading; - final String? error; -} - -class WarehouseNotifier extends StateNotifier { - Future loadWarehouses() async { - state = state.setLoading(); - final result = await getWarehousesUseCase(); - result.fold( - (failure) => state = state.setError(failure.message), - (warehouses) => state = state.setSuccess(warehouses), - ); - } -} -``` - -#### Pages - -`presentation/pages/warehouse_selection_page.dart` - -- ConsumerStatefulWidget using Riverpod -- Loads warehouses on initialization -- Displays different UI states: - - Loading: CircularProgressIndicator - - Error: Error message with retry button - - Empty: No warehouses message - - Success: List of warehouse cards -- Pull-to-refresh support -- Navigation to operations page on selection - -#### Widgets - -`presentation/widgets/warehouse_card.dart` - -- Reusable warehouse card component -- Displays: - - Warehouse name (title) - - Code (with QR icon) - - Total items count (with inventory icon) - - Description (if available) - - NG warehouse badge (if applicable) -- Material Design 3 styling -- Tap to select functionality - -## API Integration - -### Endpoint - -``` -GET /warehouses -``` - -### Request - -```bash -curl -X GET https://api.example.com/warehouses \ - -H "Authorization: Bearer {access_token}" -``` - -### Response Format - -```json -{ - "Value": [ - { - "Id": 1, - "Name": "Kho nguyên vật liệu", - "Code": "001", - "Description": "Kho chứa nguyên vật liệu", - "IsNGWareHouse": false, - "TotalCount": 8 - }, - { - "Id": 2, - "Name": "Kho bán thành phẩm công đoạn", - "Code": "002", - "Description": null, - "IsNGWareHouse": false, - "TotalCount": 12 - } - ], - "IsSuccess": true, - "IsFailure": false, - "Errors": [], - "ErrorCodes": [] -} -``` - -## Setup & Integration - -### 1. Install Dependencies - -Ensure your `pubspec.yaml` includes: - -```yaml -dependencies: - flutter_riverpod: ^2.4.9 - dio: ^5.3.2 - dartz: ^0.10.1 - equatable: ^2.0.5 - flutter_secure_storage: ^9.0.0 -``` - -### 2. Set Up Providers - -Create or update your provider configuration file (e.g., `lib/core/di/providers.dart`): - -```dart -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../features/warehouse/warehouse_exports.dart'; - -// Core providers (if not already set up) -final secureStorageProvider = Provider((ref) { - return SecureStorage(); -}); - -final apiClientProvider = Provider((ref) { - final secureStorage = ref.watch(secureStorageProvider); - return ApiClient(secureStorage); -}); - -// Warehouse data layer providers -final warehouseRemoteDataSourceProvider = Provider((ref) { - final apiClient = ref.watch(apiClientProvider); - return WarehouseRemoteDataSourceImpl(apiClient); -}); - -final warehouseRepositoryProvider = Provider((ref) { - final remoteDataSource = ref.watch(warehouseRemoteDataSourceProvider); - return WarehouseRepositoryImpl(remoteDataSource); -}); - -// Warehouse domain layer providers -final getWarehousesUseCaseProvider = Provider((ref) { - final repository = ref.watch(warehouseRepositoryProvider); - return GetWarehousesUseCase(repository); -}); - -// Warehouse presentation layer providers -final warehouseProvider = StateNotifierProvider((ref) { - final getWarehousesUseCase = ref.watch(getWarehousesUseCaseProvider); - return WarehouseNotifier(getWarehousesUseCase); -}); -``` - -### 3. Update WarehouseSelectionPage - -Replace the TODO comments in `warehouse_selection_page.dart`: - -```dart -@override -void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - ref.read(warehouseProvider.notifier).loadWarehouses(); - }); -} - -@override -Widget build(BuildContext context) { - final state = ref.watch(warehouseProvider); - - // Rest of the implementation... -} -``` - -### 4. Add to Router - -Using go_router: - -```dart -GoRoute( - path: '/warehouses', - name: 'warehouses', - builder: (context, state) => const WarehouseSelectionPage(), -), -GoRoute( - path: '/operations', - name: 'operations', - builder: (context, state) { - final warehouse = state.extra as WarehouseEntity; - return OperationSelectionPage(warehouse: warehouse); - }, -), -``` - -### 5. Navigate to Warehouse Page - -```dart -// From login page after successful authentication -context.go('/warehouses'); - -// Or using Navigator -Navigator.of(context).pushNamed('/warehouses'); -``` - -## Usage Examples - -### Loading Warehouses - -```dart -// In a widget -ElevatedButton( - onPressed: () { - ref.read(warehouseProvider.notifier).loadWarehouses(); - }, - child: const Text('Load Warehouses'), -) -``` - -### Watching State - -```dart -// In a ConsumerWidget -@override -Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(warehouseProvider); - - if (state.isLoading) { - return const CircularProgressIndicator(); - } - - if (state.error != null) { - return Text('Error: ${state.error}'); - } - - return ListView.builder( - itemCount: state.warehouses.length, - itemBuilder: (context, index) { - final warehouse = state.warehouses[index]; - return ListTile(title: Text(warehouse.name)); - }, - ); -} -``` - -### Selecting a Warehouse - -```dart -// Select warehouse and navigate -void onWarehouseTap(WarehouseEntity warehouse) { - ref.read(warehouseProvider.notifier).selectWarehouse(warehouse); - context.push('/operations', extra: warehouse); -} -``` - -### Pull to Refresh - -```dart -RefreshIndicator( - onRefresh: () => ref.read(warehouseProvider.notifier).refresh(), - child: ListView(...), -) -``` - -### Accessing Selected Warehouse - -```dart -// In another page -final state = ref.watch(warehouseProvider); -final selectedWarehouse = state.selectedWarehouse; - -if (selectedWarehouse != null) { - Text('Current: ${selectedWarehouse.name}'); -} -``` - -## Error Handling - -The feature uses dartz's `Either` type for functional error handling: - -```dart -// In use case or repository -Future>> getWarehouses() async { - try { - final warehouses = await remoteDataSource.getWarehouses(); - return Right(warehouses); // Success - } on ServerException catch (e) { - return Left(ServerFailure(e.message)); // Failure - } -} - -// In presentation layer -result.fold( - (failure) => print('Error: ${failure.message}'), - (warehouses) => print('Success: ${warehouses.length} items'), -); -``` - -### Failure Types - -- `ServerFailure`: API errors, HTTP errors -- `NetworkFailure`: Connection issues, timeouts -- `CacheFailure`: Local storage errors (if implemented) - -## Testing - -### Unit Tests - -**Test Use Case:** -```dart -test('should get warehouses from repository', () async { - // Arrange - when(mockRepository.getWarehouses()) - .thenAnswer((_) async => Right(mockWarehouses)); - - // Act - final result = await useCase(); - - // Assert - expect(result, Right(mockWarehouses)); - verify(mockRepository.getWarehouses()); -}); -``` - -**Test Repository:** -```dart -test('should return warehouses when remote call is successful', () async { - // Arrange - when(mockRemoteDataSource.getWarehouses()) - .thenAnswer((_) async => mockWarehouseModels); - - // Act - final result = await repository.getWarehouses(); - - // Assert - expect(result.isRight(), true); -}); -``` - -**Test Notifier:** -```dart -test('should emit loading then success when warehouses are loaded', () async { - // Arrange - when(mockUseCase()).thenAnswer((_) async => Right(mockWarehouses)); - - // Act - await notifier.loadWarehouses(); - - // Assert - expect(notifier.state.isLoading, false); - expect(notifier.state.warehouses, mockWarehouses); -}); -``` - -### Widget Tests - -```dart -testWidgets('should display warehouse list when loaded', (tester) async { - // Arrange - final container = ProviderContainer( - overrides: [ - warehouseProvider.overrideWith((ref) => MockWarehouseNotifier()), - ], - ); - - // Act - await tester.pumpWidget( - UncontrolledProviderScope( - container: container, - child: const WarehouseSelectionPage(), - ), - ); - - // Assert - expect(find.byType(WarehouseCard), findsWidgets); -}); -``` - -## Best Practices - -1. **Always use Either for error handling** - Don't throw exceptions across layers -2. **Keep domain layer pure** - No Flutter/external dependencies -3. **Use value objects** - Entities should be immutable -4. **Single responsibility** - Each class has one reason to change -5. **Dependency inversion** - Depend on abstractions, not concretions -6. **Test each layer independently** - Use mocks and test doubles - -## Common Issues - -### Provider Not Found - -**Error:** `ProviderNotFoundException` - -**Solution:** Make sure you've set up all providers in your provider configuration file and wrapped your app with `ProviderScope`. - -### Null Safety Issues - -**Error:** `Null check operator used on a null value` - -**Solution:** Always check for null before accessing optional fields: -```dart -if (warehouse.description != null) { - Text(warehouse.description!); -} -``` - -### API Response Format Mismatch - -**Error:** `ServerException: Invalid response format` - -**Solution:** Verify that the API response matches the expected format in `ApiResponse.fromJson` and `WarehouseModel.fromJson`. - -## Future Enhancements - -- [ ] Add caching with Hive for offline support -- [ ] Implement warehouse search functionality -- [ ] Add warehouse filtering (by type, name, etc.) -- [ ] Add pagination for large warehouse lists -- [ ] Implement warehouse CRUD operations -- [ ] Add warehouse analytics and statistics - -## Related Features - -- **Authentication**: `/lib/features/auth/` - User login and token management -- **Operations**: `/lib/features/operation/` - Import/Export selection -- **Products**: `/lib/features/products/` - Product listing per warehouse - -## References - -- [Clean Architecture by Uncle Bob](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) -- [Flutter Riverpod Documentation](https://riverpod.dev/) -- [Dartz Package for Functional Programming](https://pub.dev/packages/dartz) -- [Material Design 3](https://m3.material.io/) - ---- - -**Last Updated:** 2025-10-27 -**Version:** 1.0.0 -**Author:** Claude Code diff --git a/lib/features/warehouse/presentation/pages/warehouse_selection_page.dart b/lib/features/warehouse/presentation/pages/warehouse_selection_page.dart index ceb05d5..8c17829 100644 --- a/lib/features/warehouse/presentation/pages/warehouse_selection_page.dart +++ b/lib/features/warehouse/presentation/pages/warehouse_selection_page.dart @@ -174,7 +174,7 @@ class _WarehouseSelectionPageState ref.read(warehouseProvider.notifier).selectWarehouse(warehouse); // Navigate to operations page - context.go('/operations', extra: warehouse); + context.push('/operations', extra: warehouse); }, ); }, diff --git a/lib/features/warehouse/presentation/pages/warehouse_selection_page_example.dart b/lib/features/warehouse/presentation/pages/warehouse_selection_page_example.dart deleted file mode 100644 index 3feb9b9..0000000 --- a/lib/features/warehouse/presentation/pages/warehouse_selection_page_example.dart +++ /dev/null @@ -1,396 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import '../../domain/entities/warehouse_entity.dart'; -import '../widgets/warehouse_card.dart'; -import '../../../../core/router/app_router.dart'; - -/// EXAMPLE: Warehouse selection page with proper navigation integration -/// -/// This is a complete example showing how to integrate the warehouse selection -/// page with the new router. Use this as a reference when implementing the -/// actual warehouse provider and state management. -/// -/// Key Features: -/// - Uses type-safe navigation with extension methods -/// - Proper error handling -/// - Loading states -/// - Pull to refresh -/// - Integration with router -class WarehouseSelectionPageExample extends ConsumerStatefulWidget { - const WarehouseSelectionPageExample({super.key}); - - @override - ConsumerState createState() => - _WarehouseSelectionPageExampleState(); -} - -class _WarehouseSelectionPageExampleState - extends ConsumerState { - @override - void initState() { - super.initState(); - // Load warehouses when page is first created - WidgetsBinding.instance.addPostFrameCallback((_) { - // TODO: Replace with actual provider - // ref.read(warehouseProvider.notifier).loadWarehouses(); - }); - } - - @override - Widget build(BuildContext context) { - // TODO: Replace with actual provider - // final state = ref.watch(warehouseProvider); - - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - return Scaffold( - appBar: AppBar( - title: const Text('Select Warehouse'), - backgroundColor: colorScheme.primaryContainer, - foregroundColor: colorScheme.onPrimaryContainer, - actions: [ - IconButton( - icon: const Icon(Icons.logout), - onPressed: () => _handleLogout(context), - tooltip: 'Logout', - ), - ], - ), - body: _buildBody(context), - ); - } - - Widget _buildBody(BuildContext context) { - // For demonstration, showing example warehouse list - // In actual implementation, use state from provider: - // if (state.isLoading) return _buildLoadingState(); - // if (state.error != null) return _buildErrorState(context, state.error!); - // if (!state.hasWarehouses) return _buildEmptyState(context); - // return _buildWarehouseList(state.warehouses); - - // Example warehouses for demonstration - final exampleWarehouses = _getExampleWarehouses(); - return _buildWarehouseList(exampleWarehouses); - } - - /// Build loading state UI - Widget _buildLoadingState() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator( - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(height: 16), - Text( - 'Loading warehouses...', - style: Theme.of(context).textTheme.bodyLarge, - ), - ], - ), - ); - } - - /// Build error state UI - Widget _buildErrorState(BuildContext context, String error) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - return Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.error_outline, - size: 64, - color: colorScheme.error, - ), - const SizedBox(height: 16), - Text( - 'Error Loading Warehouses', - style: theme.textTheme.titleLarge?.copyWith( - color: colorScheme.error, - ), - ), - const SizedBox(height: 8), - Text( - error, - textAlign: TextAlign.center, - style: theme.textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 24), - FilledButton.icon( - onPressed: () { - // TODO: Replace with actual provider - // ref.read(warehouseProvider.notifier).loadWarehouses(); - }, - icon: const Icon(Icons.refresh), - label: const Text('Retry'), - ), - ], - ), - ), - ); - } - - /// Build empty state UI - Widget _buildEmptyState(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - return Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.inventory_2_outlined, - size: 64, - color: colorScheme.onSurfaceVariant, - ), - const SizedBox(height: 16), - Text( - 'No Warehouses Available', - style: theme.textTheme.titleLarge, - ), - const SizedBox(height: 8), - Text( - 'There are no warehouses to display.', - textAlign: TextAlign.center, - style: theme.textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 24), - OutlinedButton.icon( - onPressed: () { - // TODO: Replace with actual provider - // ref.read(warehouseProvider.notifier).loadWarehouses(); - }, - icon: const Icon(Icons.refresh), - label: const Text('Refresh'), - ), - ], - ), - ), - ); - } - - /// Build warehouse list UI - Widget _buildWarehouseList(List warehouses) { - return RefreshIndicator( - onRefresh: () async { - // TODO: Replace with actual provider - // await ref.read(warehouseProvider.notifier).refresh(); - }, - child: ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 8), - itemCount: warehouses.length, - itemBuilder: (context, index) { - final warehouse = warehouses[index]; - return WarehouseCard( - warehouse: warehouse, - onTap: () => _onWarehouseSelected(context, warehouse), - ); - }, - ), - ); - } - - /// Handle warehouse selection - /// - /// This is the key integration point with the router! - /// Uses the type-safe extension method to navigate to operations page - void _onWarehouseSelected(BuildContext context, WarehouseEntity warehouse) { - // TODO: Optionally save selected warehouse to provider - // ref.read(warehouseProvider.notifier).selectWarehouse(warehouse); - - // Navigate to operations page using type-safe extension method - context.goToOperations(warehouse); - } - - /// Handle logout - void _handleLogout(BuildContext context) async { - final confirmed = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Logout'), - content: const Text('Are you sure you want to logout?'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), - ), - FilledButton( - onPressed: () => Navigator.of(context).pop(true), - child: const Text('Logout'), - ), - ], - ), - ); - - if (confirmed == true && context.mounted) { - // TODO: Call logout from auth provider - // await ref.read(authProvider.notifier).logout(); - - // Router will automatically redirect to login page - // due to authentication state change - } - } - - /// Get example warehouses for demonstration - List _getExampleWarehouses() { - return [ - WarehouseEntity( - id: 1, - name: 'Kho nguyên vật liệu', - code: '001', - description: 'Warehouse for raw materials', - isNGWareHouse: false, - totalCount: 8, - ), - WarehouseEntity( - id: 2, - name: 'Kho bán thành phẩm công đoạn', - code: '002', - description: 'Semi-finished goods warehouse', - isNGWareHouse: false, - totalCount: 8, - ), - WarehouseEntity( - id: 3, - name: 'Kho thành phẩm', - code: '003', - description: 'Finished goods warehouse', - isNGWareHouse: false, - totalCount: 8, - ), - WarehouseEntity( - id: 4, - name: 'Kho tiêu hao', - code: '004', - description: 'Để chứa phụ tùng', - isNGWareHouse: false, - totalCount: 8, - ), - WarehouseEntity( - id: 5, - name: 'Kho NG', - code: '005', - description: 'Non-conforming products warehouse', - isNGWareHouse: true, - totalCount: 3, - ), - ]; - } -} - -/// EXAMPLE: Alternative approach using named routes -/// -/// This shows how to use named routes instead of path-based navigation -class WarehouseSelectionWithNamedRoutesExample extends ConsumerWidget { - const WarehouseSelectionWithNamedRoutesExample({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - // Example warehouse for demonstration - final warehouse = WarehouseEntity( - id: 1, - name: 'Example Warehouse', - code: '001', - isNGWareHouse: false, - totalCount: 10, - ); - - return Scaffold( - appBar: AppBar(title: const Text('Named Routes Example')), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () { - // Using named route navigation - context.goToOperationsNamed(warehouse); - }, - child: const Text('Go to Operations (Named)'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - // Using path-based navigation - context.goToOperations(warehouse); - }, - child: const Text('Go to Operations (Path)'), - ), - ], - ), - ), - ); - } -} - -/// EXAMPLE: Navigation from operation to products -/// -/// Shows how to navigate from operation selection to products page -class OperationNavigationExample extends StatelessWidget { - final WarehouseEntity warehouse; - - const OperationNavigationExample({ - super.key, - required this.warehouse, - }); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Operation Navigation Example')), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton.icon( - onPressed: () { - // Navigate to products with import operation - context.goToProducts( - warehouse: warehouse, - operationType: 'import', - ); - }, - icon: const Icon(Icons.arrow_downward), - label: const Text('Import Products'), - ), - const SizedBox(height: 16), - ElevatedButton.icon( - onPressed: () { - // Navigate to products with export operation - context.goToProducts( - warehouse: warehouse, - operationType: 'export', - ); - }, - icon: const Icon(Icons.arrow_upward), - label: const Text('Export Products'), - ), - const SizedBox(height: 32), - OutlinedButton( - onPressed: () { - // Navigate back - context.goBack(); - }, - child: const Text('Go Back'), - ), - ], - ), - ), - ); - } -}