diff --git a/lib/pages/boxing_detail_page.dart b/lib/pages/boxing_detail_page.dart index a304eb3..2289f85 100644 --- a/lib/pages/boxing_detail_page.dart +++ b/lib/pages/boxing_detail_page.dart @@ -61,14 +61,37 @@ class BoxingDetailPage extends StatelessWidget { } Widget _buildBoxGroup(BuildContext context, BoxDetailData box) { + final boxTotalQuantity = box.items.fold( + 0, + (sum, item) => sum + item.quantity, + ); + return Padding( padding: const EdgeInsets.only(bottom: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - '箱号 ${box.boxNo}', - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + Row( + children: [ + Expanded( + child: Text( + '箱号 ${box.boxNo}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + Text( + '共 $boxTotalQuantity 件', + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], ), const SizedBox(height: 4), Container( diff --git a/lib/pages/boxing_page.dart b/lib/pages/boxing_page.dart index 5b075a3..d8c0bde 100644 --- a/lib/pages/boxing_page.dart +++ b/lib/pages/boxing_page.dart @@ -102,6 +102,9 @@ class _BoxingPageState extends State { int? _editingPackedItemId; String _editingPackedQuantity = ''; int? _deletingPackedItemId; + int? _editingAssignedItemId; + String _editingAssignedQuantity = ''; + int? _deletingAssignedItemId; String? _lastScannedPaichanNo; String? _paichanSwitchNotice; @@ -187,6 +190,9 @@ class _BoxingPageState extends State { _editingPackedItemId = null; _editingPackedQuantity = ''; _deletingPackedItemId = null; + _editingAssignedItemId = null; + _editingAssignedQuantity = ''; + _deletingAssignedItemId = null; _lastScannedPaichanNo = null; _paichanSwitchNotice = null; _isSubmitting = false; @@ -264,6 +270,9 @@ class _BoxingPageState extends State { _phase = _Phase.scanned; _isDuplicateBoxNo = false; _editingBox = null; + _editingAssignedItemId = null; + _editingAssignedQuantity = ''; + _deletingAssignedItemId = null; _statusOverrideText = null; if (_mode == BoxingMode.singleCode) { _paichanSwitchNotice = null; @@ -322,7 +331,14 @@ class _BoxingPageState extends State { _boxNoController.text = boxNoToApply.toString(); } _lastScannedPaichanNo = _paichanNo; - _quantityController.text = (_erpQuantity ?? 0).toString(); + final remaining = _remainingQuantity; + if (remaining <= 0) { + _quantityController.clear(); + _statusOverrideText = '该总排号已全部装箱完毕'; + _statusOverrideDot = StatusDotColor.red; + return; + } + _quantityController.text = remaining.toString(); _quantityController.selection = TextSelection( baseOffset: 0, extentOffset: _quantityController.text.length, @@ -386,6 +402,23 @@ class _BoxingPageState extends State { return true; } + void _submitFromKeyboard() { + if (_canSubmit) { + _submit(); + } + } + + void _focusQuantityInput() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _quantityFocusNode.requestFocus(); + _quantityController.selection = TextSelection( + baseOffset: 0, + extentOffset: _quantityController.text.length, + ); + }); + } + int? get _currentManyToOneBoxNo { final boxNo = int.tryParse(_boxNoController.text.trim()); if (boxNo == null || boxNo <= 0) return null; @@ -526,6 +559,7 @@ class _BoxingPageState extends State { ); _isDuplicateBoxNo = false; }); + _focusQuantityInput(); _showStatusOverride( '请继续完成剩余数量装箱', StatusDotColor.green, @@ -687,11 +721,9 @@ class _BoxingPageState extends State { void _startEditAssigned(CurrentZongpaiBoxData item) { setState(() { - _editingBox = item; - _phase = _Phase.scanned; - _boxNoController.text = item.boxNo.toString(); - _quantityController.text = item.quantity.toString(); - _isDuplicateBoxNo = false; + _editingAssignedItemId = item.boxItemId; + _editingAssignedQuantity = item.quantity.toString(); + _deletingAssignedItemId = null; _statusOverrideText = null; _statusOverrideDot = null; }); @@ -699,13 +731,158 @@ class _BoxingPageState extends State { void _cancelEditAssigned() { setState(() { - _editingBox = null; - _boxNoController.text = ((_lastBoxNo ?? _maxBoxNo) + 1).toString(); - _quantityController.clear(); - _isDuplicateBoxNo = false; + _editingAssignedItemId = null; + _editingAssignedQuantity = ''; }); } + int _maxAssignedQuantity(CurrentZongpaiBoxData item) { + final total = _erpQuantity ?? 0; + final packedWithoutItem = _packedQuantity - item.quantity; + return total - packedWithoutItem; + } + + void _refreshSingleCodeNewInput({bool focusQuantity = true}) { + _lastBoxNo = _maxBoxNo; + _boxNoController.text = (_maxBoxNo + 1).toString(); + final remaining = _remainingQuantity; + if (remaining > 0) { + _quantityController.text = remaining.toString(); + _quantityController.selection = TextSelection( + baseOffset: 0, + extentOffset: _quantityController.text.length, + ); + if (focusQuantity) { + _focusQuantityInput(); + } + } else { + _quantityController.clear(); + } + _isDuplicateBoxNo = false; + } + + Future _saveAssignedItem(CurrentZongpaiBoxData item) async { + final quantity = int.tryParse(_editingAssignedQuantity); + if (quantity == null || quantity <= 0) { + _showStatusOverride( + '请输入有效数量', + StatusDotColor.red, + const Duration(seconds: 2), + ); + return; + } + if (quantity > _maxAssignedQuantity(item)) { + _showStatusOverride( + '超出可装数量上限', + StatusDotColor.red, + const Duration(seconds: 2), + ); + return; + } + + final configService = AppConfigService(); + final baseUrl = await configService.getString('api_url') ?? ''; + if (baseUrl.isEmpty) { + _showStatusOverride( + '未配置 API 地址,请前往设置', + StatusDotColor.red, + const Duration(seconds: 2), + ); + return; + } + + setState(() => _isSubmitting = true); + final result = await _apiService.updateBoxRecord( + baseUrl: baseUrl, + boxItemId: item.boxItemId, + boxNo: item.boxNo, + quantity: quantity, + ); + if (!mounted) return; + + setState(() => _isSubmitting = false); + if (result.success) { + setState(() { + _currentZongpaiBoxes = _currentZongpaiBoxes.map((box) { + if (box.boxItemId != item.boxItemId) return box; + return CurrentZongpaiBoxData( + boxItemId: box.boxItemId, + boxNo: box.boxNo, + quantity: quantity, + ); + }).toList(); + _replaceExistingBoxItem(item, item.boxNo, quantity); + _editingAssignedItemId = null; + _editingAssignedQuantity = ''; + _refreshSingleCodeNewInput(); + }); + _showStatusOverride( + '修改成功', + StatusDotColor.green, + const Duration(milliseconds: 1500), + ); + } else { + final dot = result.isDuplicate + ? StatusDotColor.amber + : StatusDotColor.red; + _showStatusOverride( + result.errorMessage ?? '修改失败', + dot, + const Duration(seconds: 2), + ); + } + } + + Future _deleteAssignedItem(CurrentZongpaiBoxData item) async { + final configService = AppConfigService(); + final baseUrl = await configService.getString('api_url') ?? ''; + if (baseUrl.isEmpty) { + _showStatusOverride( + '未配置 API 地址,请前往设置', + StatusDotColor.red, + const Duration(seconds: 2), + ); + return; + } + + setState(() => _isSubmitting = true); + final result = await _apiService.deleteBoxRecord( + baseUrl: baseUrl, + boxItemId: item.boxItemId, + ); + if (!mounted) return; + + setState(() { + _isSubmitting = false; + if (result.success) { + _currentZongpaiBoxes = _currentZongpaiBoxes + .where((box) => box.boxItemId != item.boxItemId) + .toList(); + _removeExistingBoxItem(item.boxItemId); + if (_editingAssignedItemId == item.boxItemId) { + _editingAssignedItemId = null; + _editingAssignedQuantity = ''; + } + _deletingAssignedItemId = null; + _refreshSingleCodeNewInput(); + } + }); + + if (result.success) { + _showStatusOverride( + '删除成功', + StatusDotColor.green, + const Duration(milliseconds: 1500), + ); + } else { + _showStatusOverride( + result.errorMessage ?? '删除失败', + StatusDotColor.red, + const Duration(seconds: 2), + ); + } + } + void _startEditPackedItem(_ManyToOnePackedItem item) { setState(() { _editingPackedItemId = item.boxItemId; @@ -1123,43 +1300,132 @@ class _BoxingPageState extends State { style: TextStyle(fontSize: 13, color: Colors.black54), ), const SizedBox(height: 6), - ..._currentZongpaiBoxes.map((item) { - final editing = _editingBox?.boxItemId == item.boxItemId; - return Container( - margin: const EdgeInsets.only(bottom: 6), - decoration: BoxDecoration( - color: editing ? Colors.blue.shade50 : Colors.grey.shade50, - border: Border.all( - color: editing ? Colors.blue : Colors.grey.shade300, - ), - borderRadius: BorderRadius.circular(6), - ), - child: ListTile( - dense: true, - contentPadding: const EdgeInsets.only(left: 10, right: 4), - onTap: () => _startEditAssigned(item), - title: Text( - '${item.boxNo}号箱 / 数量 ${item.quantity}', - style: TextStyle( - fontSize: 14, - fontWeight: editing ? FontWeight.w700 : FontWeight.w500, + ..._currentZongpaiBoxes.map(_buildAssignedBoxRow), + ], + ); + } + + Widget _buildAssignedBoxRow(CurrentZongpaiBoxData item) { + final editing = _editingAssignedItemId == item.boxItemId; + final deleting = _deletingAssignedItemId == item.boxItemId; + return Container( + margin: const EdgeInsets.only(bottom: 6), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: editing ? Colors.orange.shade50 : Colors.grey.shade50, + border: Border.all( + color: editing ? Colors.orange : Colors.grey.shade300, + ), + borderRadius: BorderRadius.circular(6), + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + '${item.boxNo}号箱', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + ), + overflow: TextOverflow.ellipsis, ), ), - trailing: Row( - mainAxisSize: MainAxisSize.min, + if (editing) + SizedBox( + width: 56, + height: 32, + child: TextField( + keyboardType: TextInputType.none, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + textAlign: TextAlign.center, + controller: + TextEditingController(text: _editingAssignedQuantity) + ..selection = TextSelection.collapsed( + offset: _editingAssignedQuantity.length, + ), + onChanged: (value) => _editingAssignedQuantity = value, + onEditingComplete: () {}, + onSubmitted: (_) => _saveAssignedItem(item), + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric(horizontal: 6), + border: OutlineInputBorder(), + ), + ), + ) + else + Text( + '数量 ${item.quantity}', + style: const TextStyle(fontSize: 13), + ), + const SizedBox(width: 4), + if (editing) ...[ + IconButton( + tooltip: '保存', + visualDensity: VisualDensity.compact, + icon: const Icon(Icons.check, size: 18), + onPressed: _isSubmitting + ? null + : () => _saveAssignedItem(item), + ), + IconButton( + tooltip: '取消', + visualDensity: VisualDensity.compact, + icon: const Icon(Icons.close, size: 18), + onPressed: _isSubmitting ? null : _cancelEditAssigned, + ), + ] else ...[ + IconButton( + tooltip: '修改', + visualDensity: VisualDensity.compact, + icon: const Icon(Icons.edit, size: 18), + onPressed: _isSubmitting + ? null + : () => _startEditAssigned(item), + ), + IconButton( + tooltip: '删除', + visualDensity: VisualDensity.compact, + icon: const Icon(Icons.delete_outline, size: 18), + onPressed: _isSubmitting + ? null + : () { + setState(() { + _deletingAssignedItemId = item.boxItemId; + _editingAssignedItemId = null; + _editingAssignedQuantity = ''; + }); + }, + ), + ], + ], + ), + if (deleting) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Row( children: [ - IconButton( - tooltip: '修改', - visualDensity: VisualDensity.compact, - icon: const Icon(Icons.edit, size: 18), - onPressed: () => _startEditAssigned(item), + const Expanded( + child: Text('确认删除此条记录?', style: TextStyle(fontSize: 13)), + ), + TextButton( + onPressed: _isSubmitting + ? null + : () => _deleteAssignedItem(item), + child: const Text('是'), + ), + TextButton( + onPressed: _isSubmitting + ? null + : () => setState(() => _deletingAssignedItemId = null), + child: const Text('否'), ), ], ), ), - ); - }), - ], + ], + ), ); } @@ -1206,6 +1472,8 @@ class _BoxingPageState extends State { keyboardType: TextInputType.none, inputFormatters: [FilteringTextInputFormatter.digitsOnly], textAlign: TextAlign.center, + onEditingComplete: () {}, + onSubmitted: (_) => _submitFromKeyboard(), decoration: InputDecoration( contentPadding: EdgeInsets.zero, border: OutlineInputBorder( @@ -1474,6 +1742,8 @@ class _BoxingPageState extends State { keyboardType: TextInputType.none, inputFormatters: [FilteringTextInputFormatter.digitsOnly], onChanged: (_) => setState(() {}), + onEditingComplete: () {}, + onSubmitted: (_) => _submitFromKeyboard(), decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 10), border: const OutlineInputBorder(), @@ -1722,6 +1992,8 @@ class _BoxingPageState extends State { keyboardType: TextInputType.none, inputFormatters: [FilteringTextInputFormatter.digitsOnly], onChanged: (_) => _checkDuplicateBoxNo(), + onEditingComplete: () {}, + onSubmitted: (_) => _submitFromKeyboard(), decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 10), border: const OutlineInputBorder(), @@ -1763,6 +2035,8 @@ class _BoxingPageState extends State { keyboardType: TextInputType.none, inputFormatters: [FilteringTextInputFormatter.digitsOnly], onChanged: (_) => setState(() {}), + onEditingComplete: () {}, + onSubmitted: (_) => _submitFromKeyboard(), decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 10), border: const OutlineInputBorder(),