This commit is contained in:
Phuoc Nguyen
2025-10-28 15:51:48 +07:00
parent e14ae56c3c
commit 0010446298
6 changed files with 352 additions and 39 deletions

View File

@@ -35,8 +35,13 @@ class ProductsRemoteDataSourceImpl implements ProductsRemoteDataSource {
@override
Future<List<ProductModel>> 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(

View File

@@ -28,6 +28,12 @@ class ProductDetailPage extends ConsumerStatefulWidget {
class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
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<ProductDetailPage> {
});
}
@override
void dispose() {
_passedQuantityController.dispose();
_passedWeightController.dispose();
_issuedQuantityController.dispose();
_issuedWeightController.dispose();
super.dispose();
}
Future<void> _onRefresh() async {
await ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail(
widget.warehouseId,
@@ -60,6 +75,13 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
);
}
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<ProductDetailPage> {
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<ProductDetailPage> {
),
],
),
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<ProductDetailPage> {
);
}
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<ProductDetailPage> {
);
}
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: [

View File

@@ -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<ProductsPage> createState() => _ProductsPageState();
}
class _ProductsPageState extends ConsumerState<ProductsPage> {
class _ProductsPageState extends ConsumerState<ProductsPage>
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<void> _onRefresh() async {
await ref.read(productsProvider.notifier).refreshProducts();
}
@@ -194,7 +235,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Products (${_getOperationTypeDisplay()})',
'Products',
style: textTheme.titleMedium,
),
Text(
@@ -212,6 +253,19 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
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<ProductsPage> {
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<ProductsPage> {
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<ProductsPage> {
),
);
}
/// Get display text for operation type
String _getOperationTypeDisplay() {
return widget.operationType == 'import'
? 'Import Products'
: 'Export Products';
}
}

View File

@@ -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
},
);
},
);
},