Compare commits

...

1 Commits

Author SHA1 Message Date
Misaka_Company
9ab7a51dfb 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>
2026-05-21 16:57:22 +08:00

View File

@@ -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 null;
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();
}
/// 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();