From 00104462989315345d00ae6244fb0af2de21d83f Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Tue, 28 Oct 2025 15:51:48 +0700 Subject: [PATCH] fix --- lib/core/constants/api_endpoints.dart | 9 +- lib/docs/api.sh | 39 ++- .../products_remote_datasource.dart | 9 +- .../pages/product_detail_page.dart | 244 ++++++++++++++++-- .../presentation/pages/products_page.dart | 77 +++++- .../pages/warehouse_selection_page.dart | 13 +- 6 files changed, 352 insertions(+), 39 deletions(-) diff --git a/lib/core/constants/api_endpoints.dart b/lib/core/constants/api_endpoints.dart index 0e880ce..6dbfa36 100644 --- a/lib/core/constants/api_endpoints.dart +++ b/lib/core/constants/api_endpoints.dart @@ -53,11 +53,18 @@ class ApiEndpoints { // ==================== Product Endpoints ==================== - /// Get products for a warehouse + /// Get products for import (all products) /// GET: /portalProduct/getAllProduct (requires auth token) /// Response: List of products static const String products = '/portalProduct/getAllProduct'; + /// Get products for export (products in specific warehouse) + /// GET: /portalWareHouse/GetAllProductsInWareHouse?warehouseId={id} (requires auth token) + /// Query param: warehouseId (int) + /// Response: List of products in warehouse + static String productsForExport(int warehouseId) => + '/portalWareHouse/GetAllProductsInWareHouse?warehouseId=$warehouseId'; + /// Get product stage in warehouse /// POST: /portalWareHouse/GetProductStageInWareHouse /// Body: { "WareHouseId": int, "ProductId": int } diff --git a/lib/docs/api.sh b/lib/docs/api.sh index 5ac64a0..ea20d43 100644 --- a/lib/docs/api.sh +++ b/lib/docs/api.sh @@ -37,7 +37,7 @@ curl --request POST \ }' -#Get products +#Get products for import curl --request GET \ --url https://dotnet.elidev.info:8157/ws/portalProduct/getAllProduct \ --compressed \ @@ -54,6 +54,22 @@ curl --request GET \ --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' + +#Get product for export +curl 'https://dotnet.elidev.info:8157/ws/portalWareHouse/GetAllProductsInWareHouse?warehouseId=1' \ + -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0' \ + -H 'Accept: application/json, text/plain, */*' \ + -H 'Accept-Language: en-US,en;q=0.5' \ + -H 'Accept-Encoding: gzip, deflate, br, zstd' \ + -H 'AccessToken: 1k5fXyQVXtGkfjwS4g2USldGyLSA7Zwa2jdj5tLe+3YfSDlk02aYgqsFh5xArkdL4N529x7IYJOGrLJJgLBPNVgD51zFfEYBzfJmMH2RUm7iegvDJaMCLISySw0zd6kcsqeJi7vtuybgY2NDPxDgiSOj4wX417PzB8AVg5bl1ZAAJ3LcVAqqtA1PDTU5ZU1QQYapNeBNxAHjnd2ojTZK1GJBIyY5Gd8P9gB880ppAKq8manNMZYsa4d8tkYf0SJUul2aqLIWJAwDGORpPmfjqkN4hMh85xAfPTZi6m4DdI0u2rHDMLaZ8eIsV16qA8wimSDnWi0VeG0SZ4ugbCdJAi3t1/uICTftiy06PJEkulBLV+h2xS/7SlmEY2xoN5ISi++3FNqsFPGa9QH6akGu2C7IXEUBCg3iGJx0uL+vULmVqk5OJIXdqiKVQ366hvhPlK2AM1zbh49x/ngibe08483WTL5uAY/fsKuBxQCpTc2368Gqhpd7QRtZFKpzikhyTWsR3nQIi6ExSstCeFbe8ehgo0PuTPZNHH5IHTc49snH6IZrSbR+F62Wu/D+4DlvMTK/ktG6LVQ3r3jSJC5MAQDV5Q9WK3RvsWMPvZrsaVW/Exz0GBgWP4W0adADg7MFSlnGDOJm6I4fCLHZIJCUww50L6iNmzvrdibrQT5jKACVgNquMZCfeZlf3m2BwUx9T6J45lAePpJ+QaMh+2voFqRiOLi98MLqOG6TW7z96sadzFVR9YU1xwM51jQDjnUlrXt0+msq29Jqt8LoCyQsG4r3RgS/tUJhximq11MDXsSXanpYM7jesjr8mAG4qjYN6z6c1Gl5N0dhcDF4HeEaIlNIgZ75FqtXZnLqvhHPyk6L2iR2ZT15nobZxLzOUad4a0OymUDUv7xuEBdEk5kmzZLDpbOxrKiyMpGSlbBhEoBMoA0u6ZKtBGQfCJ02s6Ri0WhLLM4XJCjGrpoEkTUuZ7YG39Zva19HGV0kkxeFYkG0lnZBO6jCggem5f+S2NQvXP/kUrWX1GeQFCq5PScvwJexLsbh0LKC2MGovkecoBKtNIK21V6ztvWL8lThJAl9' \ + -H 'AppID: Minhthu2016' \ + -H 'Origin: https://dotnet.elidev.info:8158' \ + -H 'Connection: keep-alive' \ + -H 'Referer: https://dotnet.elidev.info:8158/' \ + -H 'Sec-Fetch-Dest: empty' \ + -H 'Sec-Fetch-Mode: cors' \ + -H 'Sec-Fetch-Site: same-site' + #Get product by id curl --request POST \ --url https://dotnet.elidev.info:8157/ws/portalWareHouse/GetProductStageInWareHouse \ @@ -75,4 +91,23 @@ curl --request POST \ --data '{ "WareHouseId": 7, "ProductId": 11 -}' \ No newline at end of file +}' + +#Create import product +curl 'https://dotnet.elidev.info:8157/ws/portalWareHouse/createProductWareHouse' \ + -X POST \ + -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0' \ + -H 'Accept: application/json, text/plain, */*' \ + -H 'Accept-Language: en-US,en;q=0.5' \ + -H 'Accept-Encoding: gzip, deflate, br, zstd' \ + -H 'AccessToken: 1k5fXyQVXtGkfjwS4g2USldGyLSA7Zwa2jdj5tLe+3YfSDlk02aYgqsFh5xArkdL4N529x7IYJOGrLJJgLBPNVgD51zFfEYBzfJmMH2RUm7iegvDJaMCLISySw0zd6kcsqeJi7vtuybgY2NDPxDgiSOj4wX417PzB8AVg5bl1ZAAJ3LcVAqqtA1PDTU5ZU1QQYapNeBNxAHjnd2ojTZK1GJBIyY5Gd8P9gB880ppAKq8manNMZYsa4d8tkYf0SJUul2aqLIWJAwDGORpPmfjqkN4hMh85xAfPTZi6m4DdI0u2rHDMLaZ8eIsV16qA8wimSDnWi0VeG0SZ4ugbCdJAi3t1/uICTftiy06PJEkulBLV+h2xS/7SlmEY2xoN5ISi++3FNqsFPGa9QH6akGu2C7IXEUBCg3iGJx0uL+vULmVqk5OJIXdqiKVQ366hvhPlK2AM1zbh49x/ngibe08483WTL5uAY/fsKuBxQCpTc2368Gqhpd7QRtZFKpzikhyTWsR3nQIi6ExSstCeFbe8ehgo0PuTPZNHH5IHTc49snH6IZrSbR+F62Wu/D+4DlvMTK/ktG6LVQ3r3jSJC5MAQDV5Q9WK3RvsWMPvZrsaVW/Exz0GBgWP4W0adADg7MFSlnGDOJm6I4fCLHZIJCUww50L6iNmzvrdibrQT5jKACVgNquMZCfeZlf3m2BwUx9T6J45lAePpJ+QaMh+2voFqRiOLi98MLqOG6TW7z96sadzFVR9YU1xwM51jQDjnUlrXt0+msq29Jqt8LoCyQsG4r3RgS/tUJhximq11MDXsSXanpYM7jesjr8mAG4qjYN6z6c1Gl5N0dhcDF4HeEaIlNIgZ75FqtXZnLqvhHPyk6L2iR2ZT15nobZxLzOUad4a0OymUDUv7xuEBdEk5kmzZLDpbOxrKiyMpGSlbBhEoBMoA0u6ZKtBGQfCJ02s6Ri0WhLLM4XJCjGrpoEkTUuZ7YG39Zva19HGV0kkxeFYkG0lnZBO6jCggem5f+S2NQvXP/kUrWX1GeQFCq5PScvwJexLsbh0LKC2MGovkecoBKtNIK21V6ztvWL8lThJAl9' \ + -H 'AppID: Minhthu2016' \ + -H 'Content-Type: application/json' \ + -H 'Origin: https://dotnet.elidev.info:8158' \ + -H 'Connection: keep-alive' \ + -H 'Referer: https://dotnet.elidev.info:8158/' \ + -H 'Sec-Fetch-Dest: empty' \ + -H 'Sec-Fetch-Mode: cors' \ + -H 'Sec-Fetch-Site: same-site' \ + -H 'Priority: u=0' \ + --data-raw $'[{"TypeId":4,"ProductId":11,"StageId":3,"OrderId":null,"RecordDate":"2025-10-28T08:19:20.418Z","PassedQuantityWeight":0.5,"PassedQuantity":5,"IssuedQuantityWeight":0.1,"IssuedQuantity":1,"ResponsibleUserId":12043,"Description":"","ProductName":"Th\xe9p 435","ProductCode":"SCM435","StockPassedQuantityWeight":0,"StockPassedQuantity":0,"StockIssuedQuantity":0,"StockIssuedQuantityWeight":0,"ReceiverUserId":12120,"ActionTypeId":1,"WareHouseId":1,"ProductStageId":3,"IsConfirm":true}]' \ No newline at end of file diff --git a/lib/features/products/data/datasources/products_remote_datasource.dart b/lib/features/products/data/datasources/products_remote_datasource.dart index 96d9fdc..19a202b 100644 --- a/lib/features/products/data/datasources/products_remote_datasource.dart +++ b/lib/features/products/data/datasources/products_remote_datasource.dart @@ -35,8 +35,13 @@ class ProductsRemoteDataSourceImpl implements ProductsRemoteDataSource { @override Future> getProducts(int warehouseId, String type) async { try { - // Make API call to get all products - final response = await apiClient.get('/portalProduct/getAllProduct'); + // Choose endpoint based on operation type + final endpoint = type == 'export' + ? ApiEndpoints.productsForExport(warehouseId) + : ApiEndpoints.products; + + // Make API call to get products + final response = await apiClient.get(endpoint); // Parse the API response using ApiResponse wrapper final apiResponse = ApiResponse.fromJson( diff --git a/lib/features/products/presentation/pages/product_detail_page.dart b/lib/features/products/presentation/pages/product_detail_page.dart index 5ef8751..dbfdc04 100644 --- a/lib/features/products/presentation/pages/product_detail_page.dart +++ b/lib/features/products/presentation/pages/product_detail_page.dart @@ -28,6 +28,12 @@ class ProductDetailPage extends ConsumerStatefulWidget { class _ProductDetailPageState extends ConsumerState { late String _providerKey; + // Text editing controllers for quantity fields + final TextEditingController _passedQuantityController = TextEditingController(); + final TextEditingController _passedWeightController = TextEditingController(); + final TextEditingController _issuedQuantityController = TextEditingController(); + final TextEditingController _issuedWeightController = TextEditingController(); + @override void initState() { super.initState(); @@ -53,6 +59,15 @@ class _ProductDetailPageState extends ConsumerState { }); } + @override + void dispose() { + _passedQuantityController.dispose(); + _passedWeightController.dispose(); + _issuedQuantityController.dispose(); + _issuedWeightController.dispose(); + super.dispose(); + } + Future _onRefresh() async { await ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail( widget.warehouseId, @@ -60,6 +75,13 @@ class _ProductDetailPageState extends ConsumerState { ); } + void _clearControllers() { + _passedQuantityController.clear(); + _passedWeightController.clear(); + _issuedQuantityController.clear(); + _issuedWeightController.clear(); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -333,16 +355,30 @@ class _ProductDetailPageState extends ConsumerState { padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16, children: [ // Stage header _buildStageHeader(stageToShow, theme), - const SizedBox(height: 16), - // Quantity information _buildSectionCard( theme: theme, - title: 'Quantities', - icon: Icons.inventory_outlined, + title: 'Stage Information', + icon: Icons.info_outlined, + children: [ + _buildInfoRow('Product ID', '${stageToShow.productId}'), + if (stageToShow.productStageId != null) + _buildInfoRow('Stage ID', '${stageToShow.productStageId}'), + if (stageToShow.actionTypeId != null) + _buildInfoRow('Action Type ID', '${stageToShow.actionTypeId}'), + _buildInfoRow('Stage Name', stageToShow.displayName), + ], + ), + + // Current Quantity information + _buildSectionCard( + theme: theme, + title: 'Current Quantities', + icon: Icons.info_outlined, children: [ _buildInfoRow('Passed Quantity', '${stageToShow.passedQuantity}'), _buildInfoRow( @@ -357,26 +393,59 @@ class _ProductDetailPageState extends ConsumerState { ), ], ), - const SizedBox(height: 16), - // Stage information + // Add New Quantities section _buildSectionCard( theme: theme, - title: 'Stage Information', - icon: Icons.info_outlined, + title: 'Add New Quantities', + icon: Icons.add_circle_outline, children: [ - _buildInfoRow('Product ID', '${stageToShow.productId}'), - if (stageToShow.productStageId != null) - _buildInfoRow('Stage ID', '${stageToShow.productStageId}'), - if (stageToShow.actionTypeId != null) - _buildInfoRow('Action Type ID', '${stageToShow.actionTypeId}'), - _buildInfoRow('Stage Name', stageToShow.displayName), + _buildTextField( + label: 'Passed Quantity', + controller: _passedQuantityController, + keyboardType: TextInputType.number, + theme: theme, + ), + const SizedBox(height: 12), + _buildTextField( + label: 'Passed Weight (kg)', + controller: _passedWeightController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + theme: theme, + ), + const Divider(height: 24), + _buildTextField( + label: 'Issued Quantity', + controller: _issuedQuantityController, + keyboardType: TextInputType.number, + theme: theme, + ), + const SizedBox(height: 12), + _buildTextField( + label: 'Issued Weight (kg)', + controller: _issuedWeightController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + theme: theme, + ), ], ), - const SizedBox(height: 16), - // Status indicators - _buildStatusCards(stageToShow, theme), + // Stage information + + + + // Add button + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: () => _addNewQuantities(stageToShow), + icon: const Icon(Icons.add), + label: const Text('Add Quantities'), + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + ), + ), ], ), ); @@ -387,6 +456,48 @@ class _ProductDetailPageState extends ConsumerState { ); } + void _addNewQuantities(ProductStageEntity stage) { + // Parse the values from text fields + final passedQuantity = int.tryParse(_passedQuantityController.text) ?? 0; + final passedWeight = double.tryParse(_passedWeightController.text) ?? 0.0; + final issuedQuantity = int.tryParse(_issuedQuantityController.text) ?? 0; + final issuedWeight = double.tryParse(_issuedWeightController.text) ?? 0.0; + + // Validate that at least one field has a value + if (passedQuantity == 0 && passedWeight == 0.0 && + issuedQuantity == 0 && issuedWeight == 0.0) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please enter at least one quantity or weight value'), + backgroundColor: Colors.orange, + ), + ); + return; + } + + // TODO: Implement API call to add new quantities + // For now, just show a success message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Added: Passed Q=$passedQuantity, W=$passedWeight | Issued Q=$issuedQuantity, W=$issuedWeight', + ), + backgroundColor: Colors.green, + duration: const Duration(seconds: 2), + ), + ); + + // Log the values for debugging + debugPrint('Adding new quantities for stage ${stage.productStageId}:'); + debugPrint(' Passed Quantity: $passedQuantity'); + debugPrint(' Passed Weight: $passedWeight'); + debugPrint(' Issued Quantity: $issuedQuantity'); + debugPrint(' Issued Weight: $issuedWeight'); + + // Clear the text fields after successful add + _clearControllers(); + } + Widget _buildStageHeader(ProductStageEntity stage, ThemeData theme) { return Card( elevation: 2, @@ -501,6 +612,105 @@ class _ProductDetailPageState extends ConsumerState { ); } + Widget _buildTextField({ + required String label, + required TextEditingController controller, + required TextInputType keyboardType, + required ThemeData theme, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: controller, + keyboardType: keyboardType, + decoration: InputDecoration( + labelText: label, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: theme.colorScheme.outline, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: theme.colorScheme.primary, + width: 2, + ), + ), + filled: true, + fillColor: theme.colorScheme.surface, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => _incrementValue(controller, 0.1), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 8), + ), + child: const Text('+0.1'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( + onPressed: () => _incrementValue(controller, 0.5), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 8), + ), + child: const Text('+0.5'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( + onPressed: () => _incrementValue(controller, 1), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 8), + ), + child: const Text('+1'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( + onPressed: () => controller.clear(), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 8), + foregroundColor: theme.colorScheme.error, + ), + child: const Text('C'), + ), + ), + ], + ), + ], + ); + } + + void _incrementValue(TextEditingController controller, double increment) { + final currentValue = double.tryParse(controller.text) ?? 0.0; + final newValue = currentValue + increment; + + // Format the value based on whether it's a whole number or has decimals + if (newValue == newValue.toInt()) { + controller.text = newValue.toInt().toString(); + } else { + controller.text = newValue.toStringAsFixed(1); + } + } + Widget _buildStatusCards(ProductStageEntity stage, ThemeData theme) { return Row( children: [ diff --git a/lib/features/products/presentation/pages/products_page.dart b/lib/features/products/presentation/pages/products_page.dart index 28d2ccc..861b867 100644 --- a/lib/features/products/presentation/pages/products_page.dart +++ b/lib/features/products/presentation/pages/products_page.dart @@ -7,7 +7,7 @@ import '../../../../core/router/app_router.dart'; import '../widgets/product_list_item.dart'; /// Products list page -/// Displays products for a specific warehouse and operation type +/// Displays products for a specific warehouse with import/export tabs class ProductsPage extends ConsumerStatefulWidget { final int warehouseId; final String warehouseName; @@ -24,20 +24,61 @@ class ProductsPage extends ConsumerStatefulWidget { ConsumerState createState() => _ProductsPageState(); } -class _ProductsPageState extends ConsumerState { +class _ProductsPageState extends ConsumerState + with SingleTickerProviderStateMixin { + late TabController _tabController; + String _currentOperationType = 'import'; + @override void initState() { super.initState(); + + // Initialize tab controller + _tabController = TabController( + length: 2, + vsync: this, + initialIndex: widget.operationType == 'export' ? 1 : 0, + ); + + _currentOperationType = widget.operationType; + + // Listen to tab changes + _tabController.addListener(() { + if (_tabController.indexIsChanging) { + return; + } + + final newOperationType = _tabController.index == 0 ? 'import' : 'export'; + if (_currentOperationType != newOperationType) { + setState(() { + _currentOperationType = newOperationType; + }); + + // Load products for new operation type + ref.read(productsProvider.notifier).loadProducts( + widget.warehouseId, + widget.warehouseName, + _currentOperationType, + ); + } + }); + // Load products when page is initialized Future.microtask(() { ref.read(productsProvider.notifier).loadProducts( widget.warehouseId, widget.warehouseName, - widget.operationType, + _currentOperationType, ); }); } + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + Future _onRefresh() async { await ref.read(productsProvider.notifier).refreshProducts(); } @@ -194,7 +235,7 @@ class _ProductsPageState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Products (${_getOperationTypeDisplay()})', + 'Products', style: textTheme.titleMedium, ), Text( @@ -212,6 +253,19 @@ class _ProductsPageState extends ConsumerState { tooltip: 'Refresh', ), ], + bottom: TabBar( + controller: _tabController, + tabs: const [ + Tab( + icon: Icon(Icons.arrow_downward), + text: 'Import', + ), + Tab( + icon: Icon(Icons.arrow_upward), + text: 'Export', + ), + ], + ), ), body: _buildBody( isLoading: isLoading, @@ -253,10 +307,10 @@ class _ProductsPageState extends ConsumerState { child: Row( children: [ Icon( - widget.operationType == 'import' + _currentOperationType == 'import' ? Icons.arrow_downward : Icons.arrow_upward, - color: widget.operationType == 'import' + color: _currentOperationType == 'import' ? Colors.green : Colors.orange, ), @@ -266,7 +320,9 @@ class _ProductsPageState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - _getOperationTypeDisplay(), + _currentOperationType == 'import' + ? 'Import Products' + : 'Export Products', style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, ), @@ -417,11 +473,4 @@ class _ProductsPageState extends ConsumerState { ), ); } - - /// Get display text for operation type - String _getOperationTypeDisplay() { - return widget.operationType == 'import' - ? 'Import Products' - : 'Export Products'; - } } diff --git a/lib/features/warehouse/presentation/pages/warehouse_selection_page.dart b/lib/features/warehouse/presentation/pages/warehouse_selection_page.dart index 8c17829..32f325c 100644 --- a/lib/features/warehouse/presentation/pages/warehouse_selection_page.dart +++ b/lib/features/warehouse/presentation/pages/warehouse_selection_page.dart @@ -170,11 +170,18 @@ class _WarehouseSelectionPageState return WarehouseCard( warehouse: warehouse, onTap: () { - // Select warehouse and navigate to operations + // Select warehouse and navigate directly to products page ref.read(warehouseProvider.notifier).selectWarehouse(warehouse); - // Navigate to operations page - context.push('/operations', extra: warehouse); + // Navigate to products page with warehouse data + context.push( + '/products', + extra: { + 'warehouse': warehouse, + 'warehouseName': warehouse.name, + 'operationType': 'import', // Default to import + }, + ); }, ); },