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:
@@ -224,16 +224,33 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
|
||||
bool get _isTransitTarget => isTransitTarget(_locationType, _locationCode);
|
||||
|
||||
/// Find the first scanned item that is already on shelf (for batch submit blocking).
|
||||
PaichaOverviewItem? _findOnShelfInScanned() {
|
||||
if (_overview?.success != true) return null;
|
||||
/// Find all scanned items that are already on shelf.
|
||||
List<PaichaOverviewItem> _findOnShelfItemsInScanned() {
|
||||
if (_overview?.success != true) return [];
|
||||
final scannedSet = _zongpaiNos.toSet();
|
||||
for (final item in _overview!.items) {
|
||||
if (scannedSet.contains(item.zongpaiNo) && item.status == 'on_shelf') {
|
||||
return item;
|
||||
return _overview!.items
|
||||
.where((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 {
|
||||
@@ -311,6 +328,21 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
|
||||
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 (_isTransitTarget && !await _ensureBatchSamePaicha(baseUrl)) {
|
||||
if (!mounted) return;
|
||||
@@ -323,15 +355,16 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
);
|
||||
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) {
|
||||
final onShelfItem = _findOnShelfInScanned();
|
||||
if (onShelfItem != null) {
|
||||
final onShelfItems = _findOnShelfItemsInScanned();
|
||||
final transferredItems = _findTransferredItemsInScanned();
|
||||
if (onShelfItems.isNotEmpty || transferredItems.isNotEmpty) {
|
||||
setState(() => _isSubmitting = false);
|
||||
_feedbackService.trigger(FeedbackEvent.duplicateError);
|
||||
_showDuplicateDialog(
|
||||
onShelfItem.zongpaiNo,
|
||||
{'location_code': onShelfItem.locationCode ?? '未知'},
|
||||
_showLocationConflictDialog(
|
||||
onShelfItems: onShelfItems,
|
||||
transferredItems: transferredItems,
|
||||
);
|
||||
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) {
|
||||
final currentPaicha = _overview?.paichaNo ?? '--';
|
||||
final dialogFocus = FocusNode();
|
||||
|
||||
Reference in New Issue
Block a user