feat: add pre-check to block batch operations when scanned items

already have location bindings (on_shelf or transferred)

- Replace single on_shelf pre-check with unified conflict detection
  for both normal shelf and transit targets
- Show conflict dialog listing duplicate and/or transferred items
  with details before any API calls are made
- Prevents partial batch failures where some items succeed before
  a conflicting item is encountered

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-05-21 16:57:22 +08:00
parent 950effc164
commit 9ab7a51dfb

View File

@@ -224,16 +224,33 @@ class _RegistrationPageState extends State<RegistrationPage> {
bool get _isTransitTarget => isTransitTarget(_locationType, _locationCode); bool get _isTransitTarget => isTransitTarget(_locationType, _locationCode);
/// Find the first scanned item that is already on shelf (for batch submit blocking). /// Find all scanned items that are already on shelf.
PaichaOverviewItem? _findOnShelfInScanned() { List<PaichaOverviewItem> _findOnShelfItemsInScanned() {
if (_overview?.success != true) return null; if (_overview?.success != true) return [];
final scannedSet = _zongpaiNos.toSet(); final scannedSet = _zongpaiNos.toSet();
for (final item in _overview!.items) { return _overview!.items
if (scannedSet.contains(item.zongpaiNo) && item.status == 'on_shelf') { .where((item) =>
return item; scannedSet.contains(item.zongpaiNo) && item.status == 'on_shelf')
.toList();
} }
/// Find all scanned items that are already transferred.
List<PaichaOverviewItem> _findTransferredItemsInScanned() {
if (_overview?.success != true) return [];
final scannedSet = _zongpaiNos.toSet();
return _overview!.items
.where((item) =>
scannedSet.contains(item.zongpaiNo) && item.status == 'transferred')
.toList();
} }
return null;
/// Whether any scanned item already has a location binding (on_shelf or transferred).
bool _hasAnyLocatedInScanned() {
if (_overview?.success != true) return false;
final scannedSet = _zongpaiNos.toSet();
return _overview!.items.any((item) =>
scannedSet.contains(item.zongpaiNo) &&
(item.status == 'on_shelf' || item.status == 'transferred'));
} }
Future<String?> _baseUrl() async { Future<String?> _baseUrl() async {
@@ -311,6 +328,21 @@ class _RegistrationPageState extends State<RegistrationPage> {
setState(() => _isSubmitting = true); setState(() => _isSubmitting = true);
// For transit target: block if any scanned item already has a location
if (_isTransitTarget) {
if (_hasAnyLocatedInScanned()) {
final onShelfItems = _findOnShelfItemsInScanned();
final transferredItems = _findTransferredItemsInScanned();
setState(() => _isSubmitting = false);
_feedbackService.trigger(FeedbackEvent.submitFailure);
_showLocationConflictDialog(
onShelfItems: onShelfItems,
transferredItems: transferredItems,
);
return;
}
}
if (_isLocked && _zongpaiNos.length > 1) { if (_isLocked && _zongpaiNos.length > 1) {
if (_isTransitTarget && !await _ensureBatchSamePaicha(baseUrl)) { if (_isTransitTarget && !await _ensureBatchSamePaicha(baseUrl)) {
if (!mounted) return; if (!mounted) return;
@@ -323,15 +355,16 @@ class _RegistrationPageState extends State<RegistrationPage> {
); );
return; return;
} }
// Pre-check: block entire batch if any item is already on shelf (normal shelf) // Pre-check: block entire batch if any item already has a location binding
if (!_isTransitTarget) { if (!_isTransitTarget) {
final onShelfItem = _findOnShelfInScanned(); final onShelfItems = _findOnShelfItemsInScanned();
if (onShelfItem != null) { final transferredItems = _findTransferredItemsInScanned();
if (onShelfItems.isNotEmpty || transferredItems.isNotEmpty) {
setState(() => _isSubmitting = false); setState(() => _isSubmitting = false);
_feedbackService.trigger(FeedbackEvent.duplicateError); _feedbackService.trigger(FeedbackEvent.duplicateError);
_showDuplicateDialog( _showLocationConflictDialog(
onShelfItem.zongpaiNo, onShelfItems: onShelfItems,
{'location_code': onShelfItem.locationCode ?? '未知'}, transferredItems: transferredItems,
); );
return; return;
} }
@@ -633,6 +666,92 @@ class _RegistrationPageState extends State<RegistrationPage> {
); );
} }
void _showLocationConflictDialog({
required List<PaichaOverviewItem> onShelfItems,
required List<PaichaOverviewItem> transferredItems,
}) {
final contentParts = <Widget>[];
if (onShelfItems.isNotEmpty) {
contentParts.add(
Text(
'重复上架',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.red.shade700,
),
),
);
for (final item in onShelfItems) {
contentParts.addAll([
Text('总排号:${item.zongpaiNo}'),
Text('已登记货位:${item.locationCode ?? "未知"}'),
const SizedBox(height: 6),
]);
}
}
if (transferredItems.isNotEmpty) {
if (onShelfItems.isNotEmpty) {
contentParts.add(const Divider());
contentParts.add(const SizedBox(height: 4));
}
contentParts.add(
Text(
'货物已转运',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.orange.shade700,
),
),
);
for (final item in transferredItems) {
contentParts.addAll([
Text('总排号:${item.zongpaiNo}'),
Text('转运货位:${item.locationCode ?? "未知"}'),
const SizedBox(height: 6),
]);
}
}
contentParts.add(const SizedBox(height: 4));
contentParts.add(
const Text(
'请核查实物,确认是否操作错误。',
style: TextStyle(fontWeight: FontWeight.bold),
),
);
showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
backgroundColor: Colors.red.shade50,
title: const Row(
children: [
Icon(Icons.warning, color: Colors.red),
SizedBox(width: 8),
Text('操作冲突', style: TextStyle(color: Colors.red)),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: contentParts,
),
actions: [
TextButton(
onPressed: () {
_feedbackService.stopAlert();
Navigator.pop(ctx);
},
child: const Text('关闭'),
),
],
),
);
}
void _showCrossPaichaDialog(String newZongpaiNo) { void _showCrossPaichaDialog(String newZongpaiNo) {
final currentPaicha = _overview?.paichaNo ?? '--'; final currentPaicha = _overview?.paichaNo ?? '--';
final dialogFocus = FocusNode(); final dialogFocus = FocusNode();