update
This commit is contained in:
@@ -6,16 +6,19 @@ import '../../domain/entities/product_stage_entity.dart';
|
||||
|
||||
/// Product detail page
|
||||
/// Displays product stages as chips and shows selected stage information
|
||||
/// If [stageId] is provided, only that stage is shown, otherwise all stages are shown
|
||||
class ProductDetailPage extends ConsumerStatefulWidget {
|
||||
final int warehouseId;
|
||||
final int productId;
|
||||
final String warehouseName;
|
||||
final int? stageId;
|
||||
|
||||
const ProductDetailPage({
|
||||
super.key,
|
||||
required this.warehouseId,
|
||||
required this.productId,
|
||||
required this.warehouseName,
|
||||
this.stageId,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -31,11 +34,22 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
_providerKey = '${widget.warehouseId}_${widget.productId}';
|
||||
|
||||
// Load product stages when page is initialized
|
||||
Future.microtask(() {
|
||||
ref.read(productDetailProvider(_providerKey).notifier).loadProductDetail(
|
||||
Future.microtask(() async {
|
||||
await ref.read(productDetailProvider(_providerKey).notifier).loadProductDetail(
|
||||
widget.warehouseId,
|
||||
widget.productId,
|
||||
);
|
||||
|
||||
// If stageId is provided, auto-select that stage
|
||||
if (widget.stageId != null) {
|
||||
final stages = ref.read(productDetailProvider(_providerKey)).stages;
|
||||
final stageIndex = stages.indexWhere(
|
||||
(stage) => stage.productStageId == widget.stageId,
|
||||
);
|
||||
if (stageIndex != -1) {
|
||||
ref.read(productDetailProvider(_providerKey).notifier).selectStage(stageIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -166,76 +180,162 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// Filter stages if stageId is provided
|
||||
final displayStages = widget.stageId != null
|
||||
? stages.where((stage) => stage.productStageId == widget.stageId).toList()
|
||||
: stages;
|
||||
|
||||
// When stageId is provided but no matching stage found
|
||||
if (widget.stageId != null && displayStages.isEmpty && stages.isNotEmpty) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search_off,
|
||||
size: 64,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Stage Not Found',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Stage with ID ${widget.stageId} was not found in this product.',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
label: const Text('Go Back'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
if (displayStages.isNotEmpty)
|
||||
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),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.stageId != null
|
||||
? 'Selected Stage'
|
||||
: 'Production Stages (${displayStages.length})',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
if (widget.stageId != null) ...[
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'ID: ${widget.stageId}',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: List.generate(displayStages.length, (index) {
|
||||
final stage = displayStages[index];
|
||||
// When stageId is provided, check if this stage matches the stageId
|
||||
// Otherwise, check if the index matches the selected index
|
||||
final isSelected = widget.stageId != null
|
||||
? stage.productStageId == widget.stageId
|
||||
: stages.indexOf(stage) == selectedIndex;
|
||||
|
||||
return FilterChip(
|
||||
selected: isSelected,
|
||||
label: Text(stage.displayName),
|
||||
onSelected: widget.stageId == null
|
||||
? (_) {
|
||||
ref
|
||||
.read(productDetailProvider(_providerKey).notifier)
|
||||
.selectStage(stages.indexOf(stage));
|
||||
}
|
||||
: null, // Disable selection when specific stage is shown
|
||||
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(
|
||||
child: () {
|
||||
// When stageId is provided, use the filtered stage
|
||||
final stageToShow = widget.stageId != null && displayStages.isNotEmpty
|
||||
? displayStages.first
|
||||
: selectedStage;
|
||||
|
||||
if (stageToShow == null) {
|
||||
return const Center(child: Text('No stage selected'));
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Stage header
|
||||
_buildStageHeader(selectedStage, theme),
|
||||
_buildStageHeader(stageToShow, theme),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Quantity information
|
||||
@@ -244,16 +344,16 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
title: 'Quantities',
|
||||
icon: Icons.inventory_outlined,
|
||||
children: [
|
||||
_buildInfoRow('Passed Quantity', '${selectedStage.passedQuantity}'),
|
||||
_buildInfoRow('Passed Quantity', '${stageToShow.passedQuantity}'),
|
||||
_buildInfoRow(
|
||||
'Passed Weight',
|
||||
'${selectedStage.passedQuantityWeight.toStringAsFixed(2)} kg',
|
||||
'${stageToShow.passedQuantityWeight.toStringAsFixed(2)} kg',
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildInfoRow('Issued Quantity', '${selectedStage.issuedQuantity}'),
|
||||
_buildInfoRow('Issued Quantity', '${stageToShow.issuedQuantity}'),
|
||||
_buildInfoRow(
|
||||
'Issued Weight',
|
||||
'${selectedStage.issuedQuantityWeight.toStringAsFixed(2)} kg',
|
||||
'${stageToShow.issuedQuantityWeight.toStringAsFixed(2)} kg',
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -265,21 +365,22 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
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),
|
||||
_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),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Status indicators
|
||||
_buildStatusCards(selectedStage, theme),
|
||||
_buildStatusCards(stageToShow, theme),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
|
||||
import '../../../../core/di/providers.dart';
|
||||
import '../../../../core/router/app_router.dart';
|
||||
@@ -41,6 +42,141 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
|
||||
await ref.read(productsProvider.notifier).refreshProducts();
|
||||
}
|
||||
|
||||
void _showBarcodeScanner() {
|
||||
final controller = MobileScannerController(
|
||||
formats: const [BarcodeFormat.code128],
|
||||
facing: CameraFacing.back,
|
||||
);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => Container(
|
||||
height: MediaQuery.of(context).size.height * 0.7,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.black,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade900,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.qr_code_scanner,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Scan Barcode',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: () {
|
||||
controller.dispose();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Scanner
|
||||
Expanded(
|
||||
child: MobileScanner(
|
||||
controller: controller,
|
||||
onDetect: (capture) {
|
||||
final List<Barcode> barcodes = capture.barcodes;
|
||||
if (barcodes.isNotEmpty) {
|
||||
final barcode = barcodes.first.rawValue;
|
||||
if (barcode != null) {
|
||||
controller.dispose();
|
||||
Navigator.pop(context);
|
||||
_handleScannedBarcode(barcode);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
// Instructions
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: Colors.grey.shade900,
|
||||
child: const Text(
|
||||
'Position the Code 128 barcode within the frame to scan',
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
).whenComplete(() => controller.dispose());
|
||||
}
|
||||
|
||||
void _handleScannedBarcode(String barcode) {
|
||||
// Parse barcode to extract productId and optional stageId
|
||||
// Format 1: "123" (only productId)
|
||||
// Format 2: "123-456" (productId-stageId)
|
||||
|
||||
int? productId;
|
||||
int? stageId;
|
||||
|
||||
if (barcode.contains('-')) {
|
||||
// Format: productId-stageId
|
||||
final parts = barcode.split('-');
|
||||
if (parts.length == 2) {
|
||||
productId = int.tryParse(parts[0]);
|
||||
stageId = int.tryParse(parts[1]);
|
||||
}
|
||||
} else {
|
||||
// Format: productId only
|
||||
productId = int.tryParse(barcode);
|
||||
}
|
||||
|
||||
if (productId == null) {
|
||||
// Invalid barcode format
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Invalid barcode format: "$barcode"'),
|
||||
backgroundColor: Colors.red,
|
||||
action: SnackBarAction(
|
||||
label: 'OK',
|
||||
textColor: Colors.white,
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to product detail with productId and optional stageId
|
||||
context.goToProductDetail(
|
||||
warehouseId: widget.warehouseId,
|
||||
productId: productId,
|
||||
warehouseName: widget.warehouseName,
|
||||
stageId: stageId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -83,6 +219,13 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
|
||||
products: products,
|
||||
theme: theme,
|
||||
),
|
||||
floatingActionButton: products.isNotEmpty
|
||||
? FloatingActionButton(
|
||||
onPressed: _showBarcodeScanner,
|
||||
tooltip: 'Scan Barcode',
|
||||
child: const Icon(Icons.qr_code_scanner),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user