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);
|
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();
|
||||||
|
|||||||
Reference in New Issue
Block a user