refactor: split registration page into focused parts
This commit is contained in:
333
lib/pages/registration/dialogs/registration_dialogs_part.dart
Normal file
333
lib/pages/registration/dialogs/registration_dialogs_part.dart
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
// ignore_for_file: invalid_use_of_protected_member
|
||||||
|
|
||||||
|
part of '../../registration_page.dart';
|
||||||
|
|
||||||
|
extension _RegistrationDialogsPart on _RegistrationPageState {
|
||||||
|
void _showLocationSwitchDialog(
|
||||||
|
String newLocationCode,
|
||||||
|
CodeType newLocationType,
|
||||||
|
) {
|
||||||
|
final count = _zongpaiNos.length;
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
backgroundColor: Colors.orange.shade50,
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.warning_amber, color: Colors.orange.shade800),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text('切换货架号', style: TextStyle(color: Colors.orange.shade900)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Text(
|
||||||
|
'当前已有 $count 条待上架数据,切换货架号不会清除已扫描的总排号。',
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
},
|
||||||
|
child: const Text('取消'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
_feedbackService.trigger(FeedbackEvent.scanValid);
|
||||||
|
setState(() {
|
||||||
|
_locationCode = newLocationCode;
|
||||||
|
_locationType = newLocationType;
|
||||||
|
_clearStatusOverride();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.orange.shade700,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: const Text('确认切换'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showDuplicateDialog(String zongpaiNo, Map<String, dynamic>? info) {
|
||||||
|
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: [
|
||||||
|
Text('总排号:$zongpaiNo'),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text('已登记货位:${info?["location_code"] ?? "未知"}'),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text('登记时间:${info?["registered_at"] ?? "未知"}'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text(
|
||||||
|
'请核查实物,确认是否操作错误。',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
_feedbackService.stopAlert();
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
},
|
||||||
|
child: const Text('关闭'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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('关闭'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showCrossPaichaDialog(String newZongpaiNo) async {
|
||||||
|
final currentPaicha = _overview?.paichaNo ?? '--';
|
||||||
|
final shouldSwitch = await _confirmCrossPaichaSwitch(
|
||||||
|
newZongpaiNo: newZongpaiNo,
|
||||||
|
currentPaicha: currentPaicha,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (shouldSwitch) {
|
||||||
|
_switchToNewPaicha(newZongpaiNo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_crossPaichaPending = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _confirmCrossPaichaSwitch({
|
||||||
|
required String newZongpaiNo,
|
||||||
|
required String currentPaicha,
|
||||||
|
}) async {
|
||||||
|
final dialogFocus = FocusNode();
|
||||||
|
var dismissed = false;
|
||||||
|
var selectedIndex = 0; // 0 = "否" (default), 1 = "是"
|
||||||
|
|
||||||
|
void dismissAsNo() {
|
||||||
|
if (dismissed) return;
|
||||||
|
dismissed = true;
|
||||||
|
Navigator.of(context).pop(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void confirmSwitch() {
|
||||||
|
if (dismissed) return;
|
||||||
|
dismissed = true;
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
final dialogFuture = showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (ctx) => StatefulBuilder(
|
||||||
|
builder: (ctx, setDialogState) {
|
||||||
|
return KeyboardListener(
|
||||||
|
focusNode: dialogFocus,
|
||||||
|
onKeyEvent: (event) {
|
||||||
|
if (event is! KeyDownEvent) return;
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||||
|
setDialogState(() => selectedIndex = 0);
|
||||||
|
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||||
|
setDialogState(() => selectedIndex = 1);
|
||||||
|
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||||||
|
selectedIndex == 0 ? dismissAsNo() : confirmSwitch();
|
||||||
|
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||||
|
dismissAsNo();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: PopScope(
|
||||||
|
canPop: false,
|
||||||
|
onPopInvokedWithResult: (didPop, _) {
|
||||||
|
if (!didPop) dismissAsNo();
|
||||||
|
},
|
||||||
|
child: AlertDialog(
|
||||||
|
backgroundColor: Colors.orange.shade50,
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.warning_amber, color: Colors.orange.shade800),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'跨排产号扫描',
|
||||||
|
style: TextStyle(color: Colors.orange.shade900),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'总排号 $newZongpaiNo 不属于当前排产号($currentPaicha),'
|
||||||
|
'是否切换到新的排产号?\n\n'
|
||||||
|
'选择「是」将放弃当前已扫描的所有数据。',
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_dialogOptionBtn(
|
||||||
|
label: '否',
|
||||||
|
selected: selectedIndex == 0,
|
||||||
|
onTap: dismissAsNo,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_dialogOptionBtn(
|
||||||
|
label: '是,切换排产号',
|
||||||
|
selected: selectedIndex == 1,
|
||||||
|
onTap: confirmSwitch,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
dialogFocus.requestFocus();
|
||||||
|
});
|
||||||
|
final shouldSwitch = await dialogFuture;
|
||||||
|
return shouldSwitch ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _dialogOptionBtn({
|
||||||
|
required String label,
|
||||||
|
required bool selected,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 40,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: onTap,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: selected
|
||||||
|
? Colors.blue.shade700
|
||||||
|
: Colors.grey.shade200,
|
||||||
|
foregroundColor: selected ? Colors.white : Colors.black87,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: selected ? FontWeight.w800 : FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _switchToNewPaicha(String newZongpaiNo) {
|
||||||
|
_feedbackService.trigger(FeedbackEvent.paichanSwitch);
|
||||||
|
setState(() {
|
||||||
|
_crossPaichaPending = false;
|
||||||
|
_zongpaiNos.clear();
|
||||||
|
_zongpaiNos.add(newZongpaiNo);
|
||||||
|
_overview = null;
|
||||||
|
_overviewNotFound = false;
|
||||||
|
_overviewError = null;
|
||||||
|
_clearStatusOverride();
|
||||||
|
});
|
||||||
|
_loadOverview(newZongpaiNo);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
lib/pages/registration/parts/registration_mode_part.dart
Normal file
21
lib/pages/registration/parts/registration_mode_part.dart
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// ignore_for_file: invalid_use_of_protected_member
|
||||||
|
|
||||||
|
part of '../../registration_page.dart';
|
||||||
|
|
||||||
|
extension _RegistrationModePart on _RegistrationPageState {
|
||||||
|
void _cycleMode() {
|
||||||
|
_feedbackService.trigger(FeedbackEvent.modeSwitch);
|
||||||
|
setState(() {
|
||||||
|
switch (_mode) {
|
||||||
|
case RegistrationMode.singleCode:
|
||||||
|
_mode = RegistrationMode.multiCode;
|
||||||
|
case RegistrationMode.multiCode:
|
||||||
|
_mode = RegistrationMode.singleCode;
|
||||||
|
// 切换到单码时,只保留最后一个总排号
|
||||||
|
if (_zongpaiNos.length > 1) {
|
||||||
|
_zongpaiNos.removeRange(0, _zongpaiNos.length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
88
lib/pages/registration/parts/registration_overview_part.dart
Normal file
88
lib/pages/registration/parts/registration_overview_part.dart
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// ignore_for_file: invalid_use_of_protected_member
|
||||||
|
|
||||||
|
part of '../../registration_page.dart';
|
||||||
|
|
||||||
|
extension _RegistrationOverviewPart on _RegistrationPageState {
|
||||||
|
/// Find all scanned items that are already on shelf.
|
||||||
|
List<PaichaOverviewItem> _findOnShelfItemsInScanned() {
|
||||||
|
return registration_calculations.findOnShelfItemsInScanned(
|
||||||
|
overview: _overview,
|
||||||
|
zongpaiNos: _zongpaiNos,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find all scanned items that are already transferred.
|
||||||
|
List<PaichaOverviewItem> _findTransferredItemsInScanned() {
|
||||||
|
return registration_calculations.findTransferredItemsInScanned(
|
||||||
|
overview: _overview,
|
||||||
|
zongpaiNos: _zongpaiNos,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether any scanned item already has a location binding (on_shelf or transferred).
|
||||||
|
bool _hasAnyLocatedInScanned() {
|
||||||
|
return registration_calculations.hasAnyLocatedInScanned(
|
||||||
|
overview: _overview,
|
||||||
|
zongpaiNos: _zongpaiNos,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _baseUrl() async {
|
||||||
|
final configService = AppConfigService();
|
||||||
|
final baseUrl = await configService.getString('api_url') ?? '';
|
||||||
|
if (baseUrl.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadOverview(String zongpaiNo, {bool force = false}) async {
|
||||||
|
if (!force && _overviewZongpaiNo == zongpaiNo && _overview != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final requestId = ++_overviewRequestId;
|
||||||
|
setState(() {
|
||||||
|
_overviewZongpaiNo = zongpaiNo;
|
||||||
|
_overviewLoading = true;
|
||||||
|
_overviewNotFound = false;
|
||||||
|
_overviewError = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
final baseUrl = await _baseUrl();
|
||||||
|
if (!mounted || requestId != _overviewRequestId) return;
|
||||||
|
if (baseUrl == null) {
|
||||||
|
setState(() {
|
||||||
|
_overviewLoading = false;
|
||||||
|
_overviewError = '未配置 API 地址,请前往设置';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await _apiService.fetchPaichaOverview(
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
zongpaiNo: zongpaiNo,
|
||||||
|
);
|
||||||
|
if (!mounted || requestId != _overviewRequestId) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_overviewLoading = false;
|
||||||
|
if (result.success) {
|
||||||
|
_overview = result;
|
||||||
|
_overviewNotFound = false;
|
||||||
|
_overviewError = null;
|
||||||
|
} else if (result.notFound) {
|
||||||
|
_overview = null;
|
||||||
|
_overviewNotFound = true;
|
||||||
|
_overviewError = null;
|
||||||
|
} else {
|
||||||
|
_overviewError = result.errorMessage ?? '加载失败,点击重试';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshCurrentOverview() async {
|
||||||
|
final zongpaiNo = _overviewZongpaiNo;
|
||||||
|
if (zongpaiNo == null) return;
|
||||||
|
await _loadOverview(zongpaiNo, force: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
126
lib/pages/registration/parts/registration_scan_part.dart
Normal file
126
lib/pages/registration/parts/registration_scan_part.dart
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
// ignore_for_file: invalid_use_of_protected_member
|
||||||
|
|
||||||
|
part of '../../registration_page.dart';
|
||||||
|
|
||||||
|
extension _RegistrationScanPart on _RegistrationPageState {
|
||||||
|
void _startScanListening() {
|
||||||
|
_scanSubscription ??= _scannerService.scanResults.listen(_onScan);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _stopScanListening() async {
|
||||||
|
final subscription = _scanSubscription;
|
||||||
|
_scanSubscription = null;
|
||||||
|
await subscription?.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _restartScanListening() {
|
||||||
|
_scanSubscription?.cancel();
|
||||||
|
_scanSubscription = null;
|
||||||
|
_startScanListening();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) {
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onKeyEvent(KeyEvent event) {
|
||||||
|
if (event is! KeyDownEvent) return;
|
||||||
|
if (_isLockToggleKey(event)) {
|
||||||
|
_cycleMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||||||
|
_submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isLockToggleKey(KeyEvent event) {
|
||||||
|
final key = event.logicalKey;
|
||||||
|
return key == LogicalKeyboardKey.select ||
|
||||||
|
key == LogicalKeyboardKey.gameButtonRight1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onScan(ScanResult result) {
|
||||||
|
final parsed = CodeParser.parse(result.barcode);
|
||||||
|
switch (parsed.type) {
|
||||||
|
case CodeType.zongpaiNo:
|
||||||
|
_handleZongpaiScan(parsed.value);
|
||||||
|
case CodeType.locationNormal:
|
||||||
|
case CodeType.locationTransit:
|
||||||
|
_handleLocationScan(parsed.value, parsed.type);
|
||||||
|
case CodeType.invalid:
|
||||||
|
_feedbackService.trigger(FeedbackEvent.scanInvalid);
|
||||||
|
_showStatusOverride(
|
||||||
|
'无效码:${result.barcode}',
|
||||||
|
StatusDotColor.red,
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleLocationScan(String locationCode, CodeType locationType) {
|
||||||
|
// In singleCode mode or no existing data, switch directly
|
||||||
|
if (_mode == RegistrationMode.singleCode || _zongpaiNos.isEmpty) {
|
||||||
|
_feedbackService.trigger(FeedbackEvent.scanValid);
|
||||||
|
setState(() {
|
||||||
|
_locationCode = locationCode;
|
||||||
|
_locationType = locationType;
|
||||||
|
_clearStatusOverride();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In multiCode mode with existing data, confirm before switching
|
||||||
|
_showLocationSwitchDialog(locationCode, locationType);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleZongpaiScan(String zongpaiNo) {
|
||||||
|
// If cross-paicha dialog is showing, just vibrate and discard
|
||||||
|
if (_crossPaichaPending) {
|
||||||
|
_triggerDoubleVibration();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In multiCode mode with existing data, check for cross-paicha scan
|
||||||
|
if (_mode == RegistrationMode.multiCode &&
|
||||||
|
_zongpaiNos.isNotEmpty &&
|
||||||
|
_overview?.success == true &&
|
||||||
|
!_overview!.items.any((item) => item.zongpaiNo == zongpaiNo)) {
|
||||||
|
_crossPaichaPending = true;
|
||||||
|
_triggerDoubleVibration();
|
||||||
|
_showCrossPaichaDialog(zongpaiNo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final shouldRefresh = _overviewZongpaiNo != zongpaiNo;
|
||||||
|
_feedbackService.trigger(FeedbackEvent.scanValid);
|
||||||
|
setState(() {
|
||||||
|
if (_mode == RegistrationMode.multiCode) {
|
||||||
|
if (!_zongpaiNos.contains(zongpaiNo)) {
|
||||||
|
_zongpaiNos.add(zongpaiNo);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_zongpaiNos.isEmpty) {
|
||||||
|
_zongpaiNos.add(zongpaiNo);
|
||||||
|
} else {
|
||||||
|
_zongpaiNos[0] = zongpaiNo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_clearStatusOverride();
|
||||||
|
});
|
||||||
|
if (shouldRefresh) {
|
||||||
|
_loadOverview(zongpaiNo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _triggerDoubleVibration() {
|
||||||
|
Vibration.vibrate(pattern: [0, 200, 100, 200]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _removeZongpai(String zongpaiNo) {
|
||||||
|
setState(() {
|
||||||
|
_zongpaiNos.remove(zongpaiNo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
55
lib/pages/registration/parts/registration_status_part.dart
Normal file
55
lib/pages/registration/parts/registration_status_part.dart
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// ignore_for_file: invalid_use_of_protected_member
|
||||||
|
|
||||||
|
part of '../../registration_page.dart';
|
||||||
|
|
||||||
|
extension _RegistrationStatusPart on _RegistrationPageState {
|
||||||
|
void _clearStatusOverride() {
|
||||||
|
_statusOverrideText = null;
|
||||||
|
_statusOverrideDot = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showStatusOverride(String text, StatusDotColor dot, Duration duration) {
|
||||||
|
setState(() {
|
||||||
|
_statusOverrideText = text;
|
||||||
|
_statusOverrideDot = dot;
|
||||||
|
});
|
||||||
|
Future.delayed(duration, () {
|
||||||
|
if (mounted) setState(() => _clearStatusOverride());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateBaseStatus() {
|
||||||
|
if (_isSubmitting) {
|
||||||
|
_statusDot = StatusDotColor.orange;
|
||||||
|
_statusText = '正在提交…';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_mode == RegistrationMode.multiCode &&
|
||||||
|
_locationCode != null &&
|
||||||
|
_zongpaiNos.isEmpty) {
|
||||||
|
_statusDot = StatusDotColor.blue;
|
||||||
|
_statusText = '多码上架模式,请扫描下一张执行卡';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final hasZongpai = _zongpaiNos.isNotEmpty;
|
||||||
|
final hasLocation = _locationCode != null;
|
||||||
|
if (hasZongpai && hasLocation) {
|
||||||
|
_statusDot = StatusDotColor.blue;
|
||||||
|
_statusText = '请确认信息并提交';
|
||||||
|
} else if (hasZongpai) {
|
||||||
|
_statusDot = StatusDotColor.blue;
|
||||||
|
_statusText = '请扫描目标货位号';
|
||||||
|
} else if (hasLocation) {
|
||||||
|
_statusDot = StatusDotColor.blue;
|
||||||
|
_statusText = '请扫描执行卡';
|
||||||
|
} else {
|
||||||
|
_statusDot = StatusDotColor.blue;
|
||||||
|
_statusText = '等待扫描总排号或货位号…';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _canSubmit =>
|
||||||
|
_zongpaiNos.isNotEmpty && _locationCode != null && !_isSubmitting;
|
||||||
|
|
||||||
|
bool get _isTransitTarget => isTransitTarget(_locationType, _locationCode);
|
||||||
|
}
|
||||||
317
lib/pages/registration/parts/registration_submit_part.dart
Normal file
317
lib/pages/registration/parts/registration_submit_part.dart
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
// ignore_for_file: invalid_use_of_protected_member
|
||||||
|
|
||||||
|
part of '../../registration_page.dart';
|
||||||
|
|
||||||
|
extension _RegistrationSubmitPart on _RegistrationPageState {
|
||||||
|
Future<void> _submit() async {
|
||||||
|
if (!_canSubmit) return;
|
||||||
|
|
||||||
|
final baseUrl = await _baseUrl();
|
||||||
|
if (baseUrl == null) {
|
||||||
|
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||||||
|
_showStatusOverride(
|
||||||
|
'未配置 API 地址,请前往设置',
|
||||||
|
StatusDotColor.red,
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (_mode == RegistrationMode.multiCode && _zongpaiNos.length > 1) {
|
||||||
|
if (_isTransitTarget && !await _ensureBatchSamePaicha(baseUrl)) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _isSubmitting = false);
|
||||||
|
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||||||
|
_showStatusOverride(
|
||||||
|
batchPaichaMismatchMessage,
|
||||||
|
StatusDotColor.red,
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Pre-check: block entire batch if any item already has a location binding
|
||||||
|
if (!_isTransitTarget) {
|
||||||
|
final onShelfItems = _findOnShelfItemsInScanned();
|
||||||
|
final transferredItems = _findTransferredItemsInScanned();
|
||||||
|
if (onShelfItems.isNotEmpty || transferredItems.isNotEmpty) {
|
||||||
|
setState(() => _isSubmitting = false);
|
||||||
|
_feedbackService.trigger(FeedbackEvent.duplicateError);
|
||||||
|
_showLocationConflictDialog(
|
||||||
|
onShelfItems: onShelfItems,
|
||||||
|
transferredItems: transferredItems,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await _submitBatch(baseUrl);
|
||||||
|
} else {
|
||||||
|
await _submitOne(baseUrl, _zongpaiNos.first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _ensureBatchSamePaicha(String baseUrl) async {
|
||||||
|
final paichaByCode = <String, String>{};
|
||||||
|
final cachedOverview = _overview;
|
||||||
|
if (cachedOverview?.success == true && cachedOverview!.paichaNo != null) {
|
||||||
|
for (final item in cachedOverview.items) {
|
||||||
|
paichaByCode[item.zongpaiNo] = cachedOverview.paichaNo!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final zongpaiNo in _zongpaiNos) {
|
||||||
|
if (paichaByCode.containsKey(zongpaiNo)) continue;
|
||||||
|
final result = await _apiService.fetchPaichaOverview(
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
zongpaiNo: zongpaiNo,
|
||||||
|
);
|
||||||
|
if (!result.success || result.paichaNo == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (final item in result.items) {
|
||||||
|
paichaByCode[item.zongpaiNo] = result.paichaNo!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allSamePaicha(_zongpaiNos.map((code) => paichaByCode[code]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _submitOne(String baseUrl, String zongpaiNo) async {
|
||||||
|
final result = await _apiService.registerLocation(
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
zongpaiNo: zongpaiNo,
|
||||||
|
locationCode: _locationCode!,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() => _isSubmitting = false);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
_feedbackService.trigger(FeedbackEvent.submitSuccess);
|
||||||
|
if (_isTransitTarget) {
|
||||||
|
setState(() => _isSubmitting = false);
|
||||||
|
await _navigateToBoxing(
|
||||||
|
mode: BoxingMode.singleCode,
|
||||||
|
codes: [zongpaiNo],
|
||||||
|
codesToClear: [zongpaiNo],
|
||||||
|
clearLocation: _mode == RegistrationMode.singleCode,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_zongpaiNos.remove(zongpaiNo);
|
||||||
|
if (_mode == RegistrationMode.singleCode) {
|
||||||
|
_locationCode = null;
|
||||||
|
_locationType = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final msg = result.isOffShelfSuccess
|
||||||
|
? '下架成功'
|
||||||
|
: (_mode == RegistrationMode.multiCode ? '多码上架模式,请扫描下一张执行卡' : '上架成功');
|
||||||
|
_showStatusOverride(
|
||||||
|
msg,
|
||||||
|
StatusDotColor.green,
|
||||||
|
const Duration(milliseconds: 1500),
|
||||||
|
);
|
||||||
|
await _refreshCurrentOverview();
|
||||||
|
} else if (result.isDuplicate) {
|
||||||
|
_feedbackService.trigger(FeedbackEvent.duplicateError);
|
||||||
|
_showDuplicateDialog(zongpaiNo, result.duplicateInfo);
|
||||||
|
} else if (result.isAlreadyOffShelf) {
|
||||||
|
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||||||
|
_showStatusOverride(
|
||||||
|
result.errorMessage ?? '该总排号已下架至转运区域,不可重新上架',
|
||||||
|
StatusDotColor.red,
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final isNetwork = result.errorMessage == '网络异常,请检查网络连接';
|
||||||
|
_feedbackService.trigger(
|
||||||
|
isNetwork ? FeedbackEvent.networkError : FeedbackEvent.submitFailure,
|
||||||
|
);
|
||||||
|
_showStatusOverride(
|
||||||
|
result.errorMessage ?? '提交失败',
|
||||||
|
isNetwork ? StatusDotColor.yellow : StatusDotColor.red,
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _submitBatch(String baseUrl) async {
|
||||||
|
int successCount = 0;
|
||||||
|
int offShelfCount = 0;
|
||||||
|
final failed = <String>[];
|
||||||
|
final toRemove = <String>[];
|
||||||
|
final transitCodes = <String>[];
|
||||||
|
|
||||||
|
for (final zongpaiNo in List.of(_zongpaiNos)) {
|
||||||
|
final result = await _apiService.registerLocation(
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
zongpaiNo: zongpaiNo,
|
||||||
|
locationCode: _locationCode!,
|
||||||
|
);
|
||||||
|
if (result.success) {
|
||||||
|
successCount++;
|
||||||
|
if (result.isOffShelfSuccess) {
|
||||||
|
offShelfCount++;
|
||||||
|
}
|
||||||
|
toRemove.add(zongpaiNo);
|
||||||
|
if (_isTransitTarget) {
|
||||||
|
transitCodes.add(zongpaiNo);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failed.add(zongpaiNo);
|
||||||
|
if (result.isDuplicate) {
|
||||||
|
if (_isTransitTarget && transitCodes.isNotEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_zongpaiNos.removeWhere((item) => toRemove.contains(item));
|
||||||
|
_isSubmitting = false;
|
||||||
|
});
|
||||||
|
await _navigateToBoxing(
|
||||||
|
mode: BoxingMode.multiCode,
|
||||||
|
codes: transitCodes,
|
||||||
|
codesToClear: toRemove,
|
||||||
|
clearLocation: false,
|
||||||
|
returnMessage: '成功 $successCount 条,失败 ${failed.length} 条',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_zongpaiNos.removeWhere((item) => toRemove.contains(item));
|
||||||
|
_isSubmitting = false;
|
||||||
|
});
|
||||||
|
_feedbackService.trigger(FeedbackEvent.duplicateError);
|
||||||
|
_showDuplicateDialog(zongpaiNo, result.duplicateInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.isAlreadyOffShelf) {
|
||||||
|
if (_isTransitTarget && transitCodes.isNotEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_zongpaiNos.removeWhere((item) => toRemove.contains(item));
|
||||||
|
_isSubmitting = false;
|
||||||
|
});
|
||||||
|
await _navigateToBoxing(
|
||||||
|
mode: BoxingMode.multiCode,
|
||||||
|
codes: transitCodes,
|
||||||
|
codesToClear: toRemove,
|
||||||
|
clearLocation: false,
|
||||||
|
returnMessage: '成功 $successCount 条,失败 ${failed.length} 条',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_zongpaiNos.removeWhere((item) => toRemove.contains(item));
|
||||||
|
_isSubmitting = false;
|
||||||
|
});
|
||||||
|
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||||||
|
_showStatusOverride(
|
||||||
|
result.errorMessage ?? '该总排号已下架至转运区域,不可重新上架',
|
||||||
|
StatusDotColor.red,
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_zongpaiNos.removeWhere((item) => toRemove.contains(item));
|
||||||
|
_isSubmitting = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_isTransitTarget && transitCodes.isNotEmpty) {
|
||||||
|
await _navigateToBoxing(
|
||||||
|
mode: BoxingMode.multiCode,
|
||||||
|
codes: transitCodes,
|
||||||
|
codesToClear: toRemove,
|
||||||
|
clearLocation: failed.isEmpty,
|
||||||
|
returnMessage: failed.isEmpty
|
||||||
|
? null
|
||||||
|
: '成功 $successCount 条,失败 ${failed.length} 条',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed.isEmpty) {
|
||||||
|
_feedbackService.trigger(FeedbackEvent.submitSuccess);
|
||||||
|
final msg = offShelfCount == successCount
|
||||||
|
? '批量下架成功($successCount 条)'
|
||||||
|
: '批量上架成功($successCount 条)';
|
||||||
|
_showStatusOverride(
|
||||||
|
msg,
|
||||||
|
StatusDotColor.green,
|
||||||
|
const Duration(milliseconds: 1500),
|
||||||
|
);
|
||||||
|
await _refreshCurrentOverview();
|
||||||
|
} else {
|
||||||
|
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||||||
|
_showStatusOverride(
|
||||||
|
'成功 $successCount 条,失败 ${failed.length} 条',
|
||||||
|
StatusDotColor.red,
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _navigateToBoxing({
|
||||||
|
required BoxingMode mode,
|
||||||
|
required List<String> codes,
|
||||||
|
required List<String> codesToClear,
|
||||||
|
required bool clearLocation,
|
||||||
|
String? returnMessage,
|
||||||
|
}) async {
|
||||||
|
await _refreshCurrentOverview();
|
||||||
|
if (!mounted) return;
|
||||||
|
final navigator = Navigator.of(context);
|
||||||
|
await _stopScanListening();
|
||||||
|
await navigator.push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => BoxingPage(
|
||||||
|
arguments: BoxingPageArguments(
|
||||||
|
initialMode: mode,
|
||||||
|
autoScanCodes: codes,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 120));
|
||||||
|
if (!mounted) return;
|
||||||
|
_restartScanListening();
|
||||||
|
setState(() {
|
||||||
|
_zongpaiNos.removeWhere((item) => codesToClear.contains(item));
|
||||||
|
if (clearLocation) {
|
||||||
|
_locationCode = null;
|
||||||
|
_locationType = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await _refreshCurrentOverview();
|
||||||
|
if (!mounted) return;
|
||||||
|
if (returnMessage != null) {
|
||||||
|
_showStatusOverride(
|
||||||
|
returnMessage,
|
||||||
|
StatusDotColor.red,
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
161
lib/pages/registration/registration_calculations.dart
Normal file
161
lib/pages/registration/registration_calculations.dart
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pad_scanner/services/api_service.dart';
|
||||||
|
|
||||||
|
class RegistrationOverviewStats {
|
||||||
|
final int totalCount;
|
||||||
|
final int shelvedCount;
|
||||||
|
final int transferredCount;
|
||||||
|
|
||||||
|
const RegistrationOverviewStats({
|
||||||
|
required this.totalCount,
|
||||||
|
required this.shelvedCount,
|
||||||
|
required this.transferredCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class RegistrationOverviewSections {
|
||||||
|
final List<PaichaOverviewItem> scanned;
|
||||||
|
final List<PaichaOverviewItem> unscanned;
|
||||||
|
|
||||||
|
const RegistrationOverviewSections({
|
||||||
|
required this.scanned,
|
||||||
|
required this.unscanned,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get hasDivider => scanned.isNotEmpty && unscanned.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
RegistrationOverviewStats overviewStats(PaichaOverviewResult? overview) {
|
||||||
|
if (overview?.success != true) {
|
||||||
|
return const RegistrationOverviewStats(
|
||||||
|
totalCount: 0,
|
||||||
|
shelvedCount: 0,
|
||||||
|
transferredCount: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var shelvedCount = 0;
|
||||||
|
var transferredCount = 0;
|
||||||
|
for (final item in overview!.items) {
|
||||||
|
switch (item.status) {
|
||||||
|
case 'on_shelf':
|
||||||
|
shelvedCount++;
|
||||||
|
case 'transferred':
|
||||||
|
transferredCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RegistrationOverviewStats(
|
||||||
|
totalCount: overview.totalCount,
|
||||||
|
shelvedCount: shelvedCount,
|
||||||
|
transferredCount: transferredCount,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
RegistrationOverviewSections splitOverviewItems({
|
||||||
|
required PaichaOverviewResult overview,
|
||||||
|
required Iterable<String> zongpaiNos,
|
||||||
|
}) {
|
||||||
|
final scannedSet = zongpaiNos.toSet();
|
||||||
|
final scanned = <PaichaOverviewItem>[];
|
||||||
|
final unscanned = <PaichaOverviewItem>[];
|
||||||
|
for (final item in overview.items) {
|
||||||
|
if (scannedSet.contains(item.zongpaiNo)) {
|
||||||
|
scanned.add(item);
|
||||||
|
} else {
|
||||||
|
unscanned.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return RegistrationOverviewSections(scanned: scanned, unscanned: unscanned);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PaichaOverviewItem> findOnShelfItemsInScanned({
|
||||||
|
required PaichaOverviewResult? overview,
|
||||||
|
required Iterable<String> zongpaiNos,
|
||||||
|
}) {
|
||||||
|
return _findScannedItemsByStatus(
|
||||||
|
overview: overview,
|
||||||
|
zongpaiNos: zongpaiNos,
|
||||||
|
status: 'on_shelf',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PaichaOverviewItem> findTransferredItemsInScanned({
|
||||||
|
required PaichaOverviewResult? overview,
|
||||||
|
required Iterable<String> zongpaiNos,
|
||||||
|
}) {
|
||||||
|
return _findScannedItemsByStatus(
|
||||||
|
overview: overview,
|
||||||
|
zongpaiNos: zongpaiNos,
|
||||||
|
status: 'transferred',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasAnyLocatedInScanned({
|
||||||
|
required PaichaOverviewResult? overview,
|
||||||
|
required Iterable<String> zongpaiNos,
|
||||||
|
}) {
|
||||||
|
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'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color registrationBarColor({
|
||||||
|
required String status,
|
||||||
|
required bool isScanned,
|
||||||
|
required bool isTransitTarget,
|
||||||
|
}) {
|
||||||
|
if (isScanned) {
|
||||||
|
return status == 'on_shelf' && !isTransitTarget
|
||||||
|
? Colors.red
|
||||||
|
: const Color(0xFF43A047);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 'on_shelf':
|
||||||
|
return const Color(0xFF2196F3);
|
||||||
|
case 'transferred':
|
||||||
|
return const Color(0xFFFF9800);
|
||||||
|
default:
|
||||||
|
return Colors.grey.shade400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color registrationRowBgColor({
|
||||||
|
required String status,
|
||||||
|
required bool isScanned,
|
||||||
|
required bool isTransitTarget,
|
||||||
|
}) {
|
||||||
|
if (isScanned) {
|
||||||
|
return status == 'on_shelf' && !isTransitTarget
|
||||||
|
? Colors.red.shade50
|
||||||
|
: const Color(0xFFF1F8E9);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 'on_shelf':
|
||||||
|
return const Color(0xFFE3F2FD);
|
||||||
|
case 'transferred':
|
||||||
|
return const Color(0xFFFFF3E0);
|
||||||
|
default:
|
||||||
|
return const Color(0xFFF5F5F5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PaichaOverviewItem> _findScannedItemsByStatus({
|
||||||
|
required PaichaOverviewResult? overview,
|
||||||
|
required Iterable<String> zongpaiNos,
|
||||||
|
required String status,
|
||||||
|
}) {
|
||||||
|
if (overview?.success != true) return [];
|
||||||
|
final scannedSet = zongpaiNos.toSet();
|
||||||
|
return overview!.items
|
||||||
|
.where(
|
||||||
|
(item) => scannedSet.contains(item.zongpaiNo) && item.status == status,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
453
lib/pages/registration/widgets/registration_widgets_part.dart
Normal file
453
lib/pages/registration/widgets/registration_widgets_part.dart
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
// ignore_for_file: invalid_use_of_protected_member
|
||||||
|
|
||||||
|
part of '../../registration_page.dart';
|
||||||
|
|
||||||
|
extension _RegistrationWidgetsPart on _RegistrationPageState {
|
||||||
|
String _locationLabel(CodeType? type) {
|
||||||
|
if (type == CodeType.locationTransit) return '转运区域';
|
||||||
|
return '普通货架';
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _locationLabelColor(CodeType? type) {
|
||||||
|
if (type == CodeType.locationTransit) return Colors.orange;
|
||||||
|
return Colors.blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildModePill() {
|
||||||
|
return BoxingModePill(
|
||||||
|
isSingle: _mode == RegistrationMode.singleCode,
|
||||||
|
label: _mode == RegistrationMode.singleCode ? '单码上架' : '多码上架',
|
||||||
|
enabled: !_isSubmitting,
|
||||||
|
onTap: _cycleMode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLocationChip() {
|
||||||
|
final color = _locationLabelColor(_locationType);
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.15),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_locationLabel(_locationType),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: color,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Paicha Header ===
|
||||||
|
|
||||||
|
Widget _buildPaichaHeader() {
|
||||||
|
final overview = _overview;
|
||||||
|
final hasData = overview?.success == true;
|
||||||
|
final hasLoc = _locationCode != null;
|
||||||
|
final stats = registration_calculations.overviewStats(overview);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade50,
|
||||||
|
border: Border(bottom: BorderSide(color: Colors.grey.shade300)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
hasData ? (overview!.paichaNo ?? '--') : '--',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: hasData ? Colors.black87 : Colors.grey.shade400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (hasLoc)
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_locationCode!,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
_buildLocationChip(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_buildStatTag(
|
||||||
|
'共 ${stats.totalCount}',
|
||||||
|
const Color(0xFFE3F2FD),
|
||||||
|
const Color(0xFF1565C0),
|
||||||
|
hasData,
|
||||||
|
),
|
||||||
|
_buildStatTag(
|
||||||
|
'上架 ${stats.shelvedCount}',
|
||||||
|
const Color(0xFFE8F5E9),
|
||||||
|
const Color(0xFF2E7D32),
|
||||||
|
hasData,
|
||||||
|
),
|
||||||
|
_buildStatTag(
|
||||||
|
'转运 ${stats.transferredCount}',
|
||||||
|
const Color(0xFFFFF3E0),
|
||||||
|
const Color(0xFFE65100),
|
||||||
|
hasData,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatTag(String text, Color bg, Color fg, bool hasData) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 1),
|
||||||
|
margin: const EdgeInsets.only(left: 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: hasData ? bg : const Color(0xFFF5F5F5),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: hasData ? fg : Colors.grey.shade400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Table Head ===
|
||||||
|
|
||||||
|
Widget _buildTableHead() {
|
||||||
|
return Container(
|
||||||
|
height: 28,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
border: Border(bottom: BorderSide(color: Colors.grey.shade400)),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 22,
|
||||||
|
child: Text(
|
||||||
|
'总排号',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: _headerStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 18,
|
||||||
|
child: Text(
|
||||||
|
'工令号',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: _headerStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 12,
|
||||||
|
child: Text('数量', textAlign: TextAlign.center, style: _headerStyle),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 28,
|
||||||
|
child: Text(
|
||||||
|
'货位号',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: _headerStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === List Body ===
|
||||||
|
|
||||||
|
Widget _buildListBody() {
|
||||||
|
if (_overviewZongpaiNo == null) {
|
||||||
|
return _buildListMessage('扫描总排号后自动显示排产号上架情况');
|
||||||
|
}
|
||||||
|
if (_overviewLoading && _overview == null) {
|
||||||
|
return _buildListMessage('正在加载排产号上架情况…');
|
||||||
|
}
|
||||||
|
if (_overviewNotFound) {
|
||||||
|
return _buildListMessage('暂无排产信息');
|
||||||
|
}
|
||||||
|
if (_overviewError != null && _overview == null) {
|
||||||
|
return _buildListError(_overviewError!);
|
||||||
|
}
|
||||||
|
final overview = _overview;
|
||||||
|
if (overview == null || !overview.success) {
|
||||||
|
return _buildListMessage('扫描总排号后自动显示排产号上架情况');
|
||||||
|
}
|
||||||
|
|
||||||
|
final sections = registration_calculations.splitOverviewItems(
|
||||||
|
overview: overview,
|
||||||
|
zongpaiNos: _zongpaiNos,
|
||||||
|
);
|
||||||
|
final scanned = sections.scanned;
|
||||||
|
final unscanned = sections.unscanned;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
if (_overviewError != null)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
color: Colors.red.shade50,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_overviewError!,
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.red),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _overviewZongpaiNo == null
|
||||||
|
? null
|
||||||
|
: () => _loadOverview(_overviewZongpaiNo!, force: true),
|
||||||
|
child: const Text('重试'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount:
|
||||||
|
scanned.length +
|
||||||
|
(sections.hasDivider ? 1 : 0) +
|
||||||
|
unscanned.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index < scanned.length) {
|
||||||
|
return _buildListRow(scanned[index], isScanned: true);
|
||||||
|
}
|
||||||
|
if (index == scanned.length && sections.hasDivider) {
|
||||||
|
return _buildSectionDivider(unscanned.length);
|
||||||
|
}
|
||||||
|
final unscannedIdx =
|
||||||
|
index - scanned.length - (sections.hasDivider ? 1 : 0);
|
||||||
|
return _buildListRow(unscanned[unscannedIdx], isScanned: false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSectionDivider(int count) {
|
||||||
|
return Container(
|
||||||
|
height: 24,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(bottom: BorderSide(color: Colors.grey.shade200)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(child: Divider(height: 0)),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Text(
|
||||||
|
'以下 $count 项未操作',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Expanded(child: Divider(height: 0)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildListRow(PaichaOverviewItem item, {required bool isScanned}) {
|
||||||
|
final rowStyle = TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: isScanned ? FontWeight.w700 : FontWeight.w500,
|
||||||
|
color: Colors.black87,
|
||||||
|
);
|
||||||
|
|
||||||
|
final barColor = registration_calculations.registrationBarColor(
|
||||||
|
status: item.status,
|
||||||
|
isScanned: isScanned,
|
||||||
|
isTransitTarget: _isTransitTarget,
|
||||||
|
);
|
||||||
|
final bgColor = registration_calculations.registrationRowBgColor(
|
||||||
|
status: item.status,
|
||||||
|
isScanned: isScanned,
|
||||||
|
isTransitTarget: _isTransitTarget,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
constraints: const BoxConstraints(minHeight: 36),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bgColor,
|
||||||
|
border: Border(
|
||||||
|
left: BorderSide(color: barColor, width: 3),
|
||||||
|
bottom: BorderSide(color: Colors.grey.shade200),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 22,
|
||||||
|
child: Text(
|
||||||
|
item.zongpaiNo,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: rowStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 18,
|
||||||
|
child: Text(
|
||||||
|
item.workOrderNo ?? '--',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: rowStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 12,
|
||||||
|
child: Text(
|
||||||
|
item.quantity.toString(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: rowStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 28,
|
||||||
|
child: Text(
|
||||||
|
item.locationCode ?? '\u2014',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: rowStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isScanned && _mode == RegistrationMode.multiCode) ...[
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
SizedBox(
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(Icons.delete_outline, size: 16),
|
||||||
|
color: Colors.red,
|
||||||
|
onPressed: () => _removeZongpai(item.zongpaiNo),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildListMessage(String text) {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 13, color: Colors.grey.shade600),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildListError(String text) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 13, color: Colors.red),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _overviewZongpaiNo == null
|
||||||
|
? null
|
||||||
|
: () => _loadOverview(_overviewZongpaiNo!, force: true),
|
||||||
|
child: const Text('重试'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Bottom Bar ===
|
||||||
|
|
||||||
|
Widget _buildBottomBar(ColorScheme colorScheme) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
border: Border(top: BorderSide(color: Colors.grey.shade300)),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 34,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _canSubmit ? _submit : null,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: registrationSubmitColor(
|
||||||
|
canSubmit: _canSubmit,
|
||||||
|
isTransitTarget: _isTransitTarget,
|
||||||
|
primaryColor: colorScheme.primary,
|
||||||
|
),
|
||||||
|
foregroundColor: _canSubmit ? Colors.white : Colors.grey.shade600,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: _isSubmitting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
registrationSubmitLabel(
|
||||||
|
isTransitTarget: _isTransitTarget,
|
||||||
|
isMultiCode: _mode == RegistrationMode.multiCode,
|
||||||
|
zongpaiCount: _zongpaiNos.length,
|
||||||
|
),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _headerStyle = TextStyle(fontSize: 10, fontWeight: FontWeight.w700);
|
||||||
File diff suppressed because it is too large
Load Diff
95
test/pages/registration/registration_calculations_test.dart
Normal file
95
test/pages/registration/registration_calculations_test.dart
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pad_scanner/pages/registration/registration_calculations.dart';
|
||||||
|
import 'package:pad_scanner/services/api_service.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('registration calculations', () {
|
||||||
|
test('calculates overview stats by item status', () {
|
||||||
|
final stats = overviewStats(
|
||||||
|
_overview([
|
||||||
|
_item('R1', status: 'not_shelved'),
|
||||||
|
_item('R2', status: 'on_shelf'),
|
||||||
|
_item('R3', status: 'transferred'),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(stats.totalCount, 3);
|
||||||
|
expect(stats.shelvedCount, 1);
|
||||||
|
expect(stats.transferredCount, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('splits overview items into scanned and unscanned groups', () {
|
||||||
|
final sections = splitOverviewItems(
|
||||||
|
overview: _overview([_item('R1'), _item('R2'), _item('R3')]),
|
||||||
|
zongpaiNos: ['R1', 'R3'],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sections.scanned.map((item) => item.zongpaiNo), ['R1', 'R3']);
|
||||||
|
expect(sections.unscanned.map((item) => item.zongpaiNo), ['R2']);
|
||||||
|
expect(sections.hasDivider, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('finds located scanned items', () {
|
||||||
|
final overview = _overview([
|
||||||
|
_item('R1', status: 'on_shelf'),
|
||||||
|
_item('R2', status: 'transferred'),
|
||||||
|
_item('R3', status: 'on_shelf'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
findOnShelfItemsInScanned(
|
||||||
|
overview: overview,
|
||||||
|
zongpaiNos: ['R1', 'R2'],
|
||||||
|
).map((item) => item.zongpaiNo),
|
||||||
|
['R1'],
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
findTransferredItemsInScanned(
|
||||||
|
overview: overview,
|
||||||
|
zongpaiNos: ['R1', 'R2'],
|
||||||
|
).map((item) => item.zongpaiNo),
|
||||||
|
['R2'],
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
hasAnyLocatedInScanned(overview: overview, zongpaiNos: ['R1']),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
hasAnyLocatedInScanned(overview: overview, zongpaiNos: ['R4']),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maps row colors for scanned and unscanned states', () {
|
||||||
|
expect(
|
||||||
|
registrationBarColor(
|
||||||
|
status: 'on_shelf',
|
||||||
|
isScanned: true,
|
||||||
|
isTransitTarget: false,
|
||||||
|
),
|
||||||
|
Colors.red,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
registrationRowBgColor(
|
||||||
|
status: 'transferred',
|
||||||
|
isScanned: false,
|
||||||
|
isTransitTarget: false,
|
||||||
|
),
|
||||||
|
const Color(0xFFFFF3E0),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
PaichaOverviewResult _overview(List<PaichaOverviewItem> items) {
|
||||||
|
return PaichaOverviewResult(
|
||||||
|
success: true,
|
||||||
|
totalCount: items.length,
|
||||||
|
items: items,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PaichaOverviewItem _item(String zongpaiNo, {String status = 'not_shelved'}) {
|
||||||
|
return PaichaOverviewItem(zongpaiNo: zongpaiNo, quantity: 1, status: status);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user