refactor: extract boxing logic into dedicated modules with tests
- boxing_models.dart: add BoxingMode, BoxingPhase, BoxingPageArguments - boxing_box_mutations.dart: box item add/replace/remove operations - boxing_calculations.dart: pure calculation helpers (quantities, validation) - boxing_status_presenter.dart: status bar text/dot determination - dialogs/cross_paichan_dialog.dart: cross-paichan confirmation dialog - widgets/boxing_notice_banners.dart: notice banner widgets - Add unit tests for mutations, calculations, and status presenter Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
135
lib/pages/boxing/boxing_box_mutations.dart
Normal file
135
lib/pages/boxing/boxing_box_mutations.dart
Normal file
@@ -0,0 +1,135 @@
|
||||
import 'package:pad_scanner/pages/boxing/boxing_models.dart';
|
||||
import 'package:pad_scanner/services/api_service.dart';
|
||||
|
||||
class BoxMutationResult {
|
||||
final List<BoxDetailData> boxes;
|
||||
final int maxBoxNo;
|
||||
|
||||
const BoxMutationResult({required this.boxes, required this.maxBoxNo});
|
||||
}
|
||||
|
||||
int maxBoxNoFor(List<BoxDetailData> boxes) {
|
||||
return boxes.fold<int>(0, (max, box) => box.boxNo > max ? box.boxNo : max);
|
||||
}
|
||||
|
||||
BoxMutationResult addExistingBoxItem({
|
||||
required List<BoxDetailData> existingBoxes,
|
||||
required int boxNo,
|
||||
required int quantity,
|
||||
required int? boxItemId,
|
||||
required String zongpaiNo,
|
||||
required String? workOrderNo,
|
||||
required int? totalQuantity,
|
||||
}) {
|
||||
final item = BoxItemData(
|
||||
boxItemId: boxItemId,
|
||||
zongpaiNo: zongpaiNo,
|
||||
workOrderNo: workOrderNo,
|
||||
quantity: quantity,
|
||||
totalQuantity: totalQuantity,
|
||||
);
|
||||
final index = existingBoxes.indexWhere((box) => box.boxNo == boxNo);
|
||||
if (index < 0) {
|
||||
final boxes = [
|
||||
...existingBoxes,
|
||||
BoxDetailData(boxNo: boxNo, items: [item]),
|
||||
]..sort((a, b) => a.boxNo.compareTo(b.boxNo));
|
||||
return BoxMutationResult(boxes: boxes, maxBoxNo: maxBoxNoFor(boxes));
|
||||
}
|
||||
|
||||
final boxes = List<BoxDetailData>.from(existingBoxes);
|
||||
final box = boxes[index];
|
||||
boxes[index] = BoxDetailData(boxNo: box.boxNo, items: [...box.items, item]);
|
||||
return BoxMutationResult(boxes: boxes, maxBoxNo: maxBoxNoFor(boxes));
|
||||
}
|
||||
|
||||
BoxMutationResult replaceExistingBoxItem({
|
||||
required List<BoxDetailData> existingBoxes,
|
||||
required CurrentZongpaiBoxData editing,
|
||||
required int boxNo,
|
||||
required int quantity,
|
||||
required String zongpaiNo,
|
||||
required String? workOrderNo,
|
||||
required int? totalQuantity,
|
||||
}) {
|
||||
final updatedItem = BoxItemData(
|
||||
boxItemId: editing.boxItemId,
|
||||
zongpaiNo: zongpaiNo,
|
||||
workOrderNo: workOrderNo,
|
||||
quantity: quantity,
|
||||
totalQuantity: totalQuantity,
|
||||
);
|
||||
return _replaceItemById(
|
||||
existingBoxes: existingBoxes,
|
||||
boxItemId: editing.boxItemId,
|
||||
targetBoxNo: boxNo,
|
||||
updatedItem: updatedItem,
|
||||
);
|
||||
}
|
||||
|
||||
BoxMutationResult replaceExistingPackedItem({
|
||||
required List<BoxDetailData> existingBoxes,
|
||||
required ManyToOnePackedItem item,
|
||||
required int boxNo,
|
||||
required int quantity,
|
||||
}) {
|
||||
final updatedItem = BoxItemData(
|
||||
boxItemId: item.boxItemId,
|
||||
zongpaiNo: item.zongpaiNo,
|
||||
workOrderNo: item.workOrderNo,
|
||||
quantity: quantity,
|
||||
totalQuantity: item.totalQuantity,
|
||||
);
|
||||
return _replaceItemById(
|
||||
existingBoxes: existingBoxes,
|
||||
boxItemId: item.boxItemId,
|
||||
targetBoxNo: boxNo,
|
||||
updatedItem: updatedItem,
|
||||
);
|
||||
}
|
||||
|
||||
BoxMutationResult removeExistingBoxItem({
|
||||
required List<BoxDetailData> existingBoxes,
|
||||
required int boxItemId,
|
||||
}) {
|
||||
final boxes = <BoxDetailData>[];
|
||||
for (final box in existingBoxes) {
|
||||
final items = box.items
|
||||
.where((item) => item.boxItemId != boxItemId)
|
||||
.toList();
|
||||
if (items.isNotEmpty) {
|
||||
boxes.add(BoxDetailData(boxNo: box.boxNo, items: items));
|
||||
}
|
||||
}
|
||||
return BoxMutationResult(boxes: boxes, maxBoxNo: maxBoxNoFor(boxes));
|
||||
}
|
||||
|
||||
BoxMutationResult _replaceItemById({
|
||||
required List<BoxDetailData> existingBoxes,
|
||||
required int boxItemId,
|
||||
required int targetBoxNo,
|
||||
required BoxItemData updatedItem,
|
||||
}) {
|
||||
final boxes = <BoxDetailData>[];
|
||||
for (final box in existingBoxes) {
|
||||
final items = box.items.where((item) {
|
||||
return item.boxItemId != boxItemId;
|
||||
}).toList();
|
||||
if (items.isNotEmpty) {
|
||||
boxes.add(BoxDetailData(boxNo: box.boxNo, items: items));
|
||||
}
|
||||
}
|
||||
|
||||
final targetIndex = boxes.indexWhere((box) => box.boxNo == targetBoxNo);
|
||||
if (targetIndex >= 0) {
|
||||
final target = boxes[targetIndex];
|
||||
boxes[targetIndex] = BoxDetailData(
|
||||
boxNo: target.boxNo,
|
||||
items: [...target.items, updatedItem],
|
||||
);
|
||||
} else {
|
||||
boxes.add(BoxDetailData(boxNo: targetBoxNo, items: [updatedItem]));
|
||||
}
|
||||
boxes.sort((a, b) => a.boxNo.compareTo(b.boxNo));
|
||||
return BoxMutationResult(boxes: boxes, maxBoxNo: maxBoxNoFor(boxes));
|
||||
}
|
||||
101
lib/pages/boxing/boxing_calculations.dart
Normal file
101
lib/pages/boxing/boxing_calculations.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
import 'package:pad_scanner/pages/boxing/boxing_models.dart';
|
||||
import 'package:pad_scanner/services/api_service.dart';
|
||||
|
||||
int packedQuantity(List<CurrentZongpaiBoxData> items) {
|
||||
return items.fold<int>(0, (sum, item) => sum + item.quantity);
|
||||
}
|
||||
|
||||
int remainingQuantity({
|
||||
required int? totalQuantity,
|
||||
required List<CurrentZongpaiBoxData> currentBoxes,
|
||||
}) {
|
||||
return (totalQuantity ?? 0) - packedQuantity(currentBoxes);
|
||||
}
|
||||
|
||||
bool quantityTooHigh({
|
||||
required int remainingQuantity,
|
||||
required String quantityText,
|
||||
}) {
|
||||
if (remainingQuantity <= 0) return false;
|
||||
final quantity = int.tryParse(quantityText);
|
||||
return quantity != null && quantity > remainingQuantity;
|
||||
}
|
||||
|
||||
bool canSubmitBoxing({
|
||||
required bool isSubmitting,
|
||||
required BoxingPhase phase,
|
||||
required String? zongpaiNo,
|
||||
required String boxNoText,
|
||||
required String quantityText,
|
||||
required int remainingQuantity,
|
||||
required bool quantityTooHigh,
|
||||
required bool isDuplicateBoxNo,
|
||||
}) {
|
||||
if (isSubmitting || phase != BoxingPhase.scanned) return false;
|
||||
if (zongpaiNo == null) return false;
|
||||
final boxNo = int.tryParse(boxNoText);
|
||||
final quantity = int.tryParse(quantityText);
|
||||
if (boxNo == null || boxNo <= 0 || quantity == null || quantity <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (remainingQuantity <= 0) return false;
|
||||
if (quantityTooHigh) return false;
|
||||
if (isDuplicateBoxNo) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
int? currentManyToOneBoxNo(String boxNoText) {
|
||||
final boxNo = int.tryParse(boxNoText.trim());
|
||||
if (boxNo == null || boxNo <= 0) return null;
|
||||
return boxNo;
|
||||
}
|
||||
|
||||
bool singleCodeBoxNoIsDuplicate({
|
||||
required String boxNoText,
|
||||
required List<CurrentZongpaiBoxData> currentBoxes,
|
||||
required int? editingBoxItemId,
|
||||
}) {
|
||||
final boxNo = int.tryParse(boxNoText);
|
||||
if (boxNo == null) return false;
|
||||
return currentBoxes.any((box) {
|
||||
if (box.boxNo != boxNo) return false;
|
||||
return editingBoxItemId == null || box.boxItemId != editingBoxItemId;
|
||||
});
|
||||
}
|
||||
|
||||
List<ManyToOnePackedItem> visibleManyToOnePackedItems({
|
||||
required int? boxNo,
|
||||
required List<BoxDetailData> existingBoxes,
|
||||
required List<ManyToOnePackedItem> packedItems,
|
||||
required String? paichanNo,
|
||||
}) {
|
||||
if (boxNo == null) return const [];
|
||||
final byItemId = <int, ManyToOnePackedItem>{};
|
||||
for (final box in existingBoxes.where((box) => box.boxNo == boxNo)) {
|
||||
for (final item in box.items) {
|
||||
final boxItemId = item.boxItemId;
|
||||
if (boxItemId == null) continue;
|
||||
byItemId[boxItemId] = ManyToOnePackedItem(
|
||||
boxItemId: boxItemId,
|
||||
zongpaiNo: item.zongpaiNo,
|
||||
paichanNo: paichanNo,
|
||||
workOrderNo: item.workOrderNo,
|
||||
boxNo: boxNo,
|
||||
quantity: item.quantity,
|
||||
totalQuantity: item.totalQuantity,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (final item in packedItems.where((item) => item.boxNo == boxNo)) {
|
||||
byItemId[item.boxItemId] = item;
|
||||
}
|
||||
return byItemId.values.toList(growable: false);
|
||||
}
|
||||
|
||||
int maxAssignedQuantity({
|
||||
required int? totalQuantity,
|
||||
required int packedQuantity,
|
||||
required int itemQuantity,
|
||||
}) {
|
||||
return (totalQuantity ?? 0) - (packedQuantity - itemQuantity);
|
||||
}
|
||||
@@ -1,3 +1,24 @@
|
||||
enum BoxingMode {
|
||||
singleCode, // 单码装箱
|
||||
multiCode, // 多码凑箱
|
||||
}
|
||||
|
||||
enum BoxingPhase {
|
||||
waiting, // 等待扫码
|
||||
scanned, // 已扫码,显示信息
|
||||
submitted, // 已提交成功
|
||||
}
|
||||
|
||||
class BoxingPageArguments {
|
||||
final BoxingMode initialMode;
|
||||
final List<String> autoScanCodes;
|
||||
|
||||
const BoxingPageArguments({
|
||||
required this.initialMode,
|
||||
required this.autoScanCodes,
|
||||
});
|
||||
}
|
||||
|
||||
class ManyToOnePackedItem {
|
||||
final int boxItemId;
|
||||
final String zongpaiNo;
|
||||
|
||||
97
lib/pages/boxing/boxing_status_presenter.dart
Normal file
97
lib/pages/boxing/boxing_status_presenter.dart
Normal file
@@ -0,0 +1,97 @@
|
||||
import 'package:pad_scanner/pages/boxing/boxing_models.dart';
|
||||
import 'package:pad_scanner/widgets/status_bar.dart';
|
||||
|
||||
class BoxingStatusPresentation {
|
||||
final StatusDotColor dot;
|
||||
final String text;
|
||||
|
||||
const BoxingStatusPresentation({required this.dot, required this.text});
|
||||
}
|
||||
|
||||
BoxingStatusPresentation boxingStatusFor({
|
||||
required bool isAutoProcessing,
|
||||
required bool isSubmitting,
|
||||
required bool isDuplicateBoxNo,
|
||||
required bool quantityTooHigh,
|
||||
required bool isEditingAssigned,
|
||||
required bool isDeletingAssigned,
|
||||
required bool hasPaichanSwitchNotice,
|
||||
required BoxingPhase phase,
|
||||
required bool isMultiCode,
|
||||
required bool hasVisibleManyToOneItems,
|
||||
required int remainingQuantity,
|
||||
required String boxNoText,
|
||||
}) {
|
||||
if (isAutoProcessing) {
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.orange,
|
||||
text: '正在同步转运数据...',
|
||||
);
|
||||
}
|
||||
if (isSubmitting) {
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.orange,
|
||||
text: '正在提交…',
|
||||
);
|
||||
}
|
||||
if (isDuplicateBoxNo && phase == BoxingPhase.scanned) {
|
||||
return BoxingStatusPresentation(
|
||||
dot: StatusDotColor.amber,
|
||||
text: '箱号 $boxNoText 已存在,请重新输入',
|
||||
);
|
||||
}
|
||||
if (quantityTooHigh && phase == BoxingPhase.scanned) {
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.red,
|
||||
text: '超出可装数量上限',
|
||||
);
|
||||
}
|
||||
if (isEditingAssigned) {
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.amber,
|
||||
text: '正在编辑已分配记录',
|
||||
);
|
||||
}
|
||||
if (isDeletingAssigned) {
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.red,
|
||||
text: '请确认是否删除该箱记录',
|
||||
);
|
||||
}
|
||||
if (hasPaichanSwitchNotice) {
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.blue,
|
||||
text: '排产号已切换,箱号已重置',
|
||||
);
|
||||
}
|
||||
|
||||
switch (phase) {
|
||||
case BoxingPhase.waiting:
|
||||
if (isMultiCode && hasVisibleManyToOneItems) {
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.blue,
|
||||
text: '请继续扫码或完成本箱',
|
||||
);
|
||||
}
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.blue,
|
||||
text: '等待扫码',
|
||||
);
|
||||
case BoxingPhase.scanned:
|
||||
if (remainingQuantity <= 0) {
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.red,
|
||||
text: '该总排号已全部装箱完毕',
|
||||
);
|
||||
}
|
||||
return const BoxingStatusPresentation(
|
||||
dot: StatusDotColor.blue,
|
||||
text: '数量已填入,请确认或修改',
|
||||
);
|
||||
case BoxingPhase.submitted:
|
||||
return BoxingStatusPresentation(
|
||||
dot: StatusDotColor.green,
|
||||
text: isMultiCode ? '请扫描下一个总排号' : '装箱成功,可继续扫码',
|
||||
);
|
||||
}
|
||||
}
|
||||
126
lib/pages/boxing/dialogs/cross_paichan_dialog.dart
Normal file
126
lib/pages/boxing/dialogs/cross_paichan_dialog.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
Future<bool> showCrossPaichanDialog({
|
||||
required BuildContext context,
|
||||
required String currentPaicha,
|
||||
required String newPaicha,
|
||||
}) async {
|
||||
final dialogFocus = FocusNode();
|
||||
var selectedIndex = 0; // 0 = "否" (default), 1 = "是"
|
||||
|
||||
final dialogFuture = showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, setDialogState) {
|
||||
void dismissAsNo() => Navigator.of(ctx).pop(false);
|
||||
void confirmSwitch() => Navigator.of(ctx).pop(true);
|
||||
|
||||
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(
|
||||
'当前排产号:$currentPaicha\n'
|
||||
'新排产号:$newPaicha\n\n'
|
||||
'是否切换到新的排产号?\n\n'
|
||||
'选择「是」将放弃当前已扫描的所有数据。',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_DialogOptionButton(
|
||||
label: '否',
|
||||
selected: selectedIndex == 0,
|
||||
onTap: dismissAsNo,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_DialogOptionButton(
|
||||
label: '是,切换排产号',
|
||||
selected: selectedIndex == 1,
|
||||
onTap: confirmSwitch,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
dialogFocus.requestFocus();
|
||||
});
|
||||
|
||||
final result = await dialogFuture;
|
||||
dialogFocus.dispose();
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
class _DialogOptionButton extends StatelessWidget {
|
||||
final String label;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _DialogOptionButton({
|
||||
required this.label,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
178
lib/pages/boxing/widgets/boxing_notice_banners.dart
Normal file
178
lib/pages/boxing/widgets/boxing_notice_banners.dart
Normal file
@@ -0,0 +1,178 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pad_scanner/pages/boxing/boxing_models.dart';
|
||||
|
||||
class BoxingNoticeBanners extends StatelessWidget {
|
||||
final bool isAutoProcessing;
|
||||
final List<String> autoWarnings;
|
||||
final String? paichanSwitchNotice;
|
||||
final bool isDuplicateBoxNo;
|
||||
final bool quantityTooHigh;
|
||||
final BoxingPhase phase;
|
||||
final String boxNoText;
|
||||
final int remainingQuantity;
|
||||
final String? zongpaiNo;
|
||||
final int? completedJumpBoxNo;
|
||||
final VoidCallback onJumpToCompletedBox;
|
||||
|
||||
const BoxingNoticeBanners({
|
||||
super.key,
|
||||
required this.isAutoProcessing,
|
||||
required this.autoWarnings,
|
||||
required this.paichanSwitchNotice,
|
||||
required this.isDuplicateBoxNo,
|
||||
required this.quantityTooHigh,
|
||||
required this.phase,
|
||||
required this.boxNoText,
|
||||
required this.remainingQuantity,
|
||||
required this.zongpaiNo,
|
||||
required this.completedJumpBoxNo,
|
||||
required this.onJumpToCompletedBox,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final banners = <Widget>[];
|
||||
if (isAutoProcessing) {
|
||||
banners.add(
|
||||
_NoticeBanner(
|
||||
icon: SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1.5,
|
||||
color: Colors.orange.shade700,
|
||||
),
|
||||
),
|
||||
text: '正在同步转运数据...',
|
||||
background: Colors.orange.shade50,
|
||||
foreground: Colors.orange.shade900,
|
||||
border: Colors.orange.shade100,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (autoWarnings.isNotEmpty) {
|
||||
banners.add(
|
||||
_NoticeBanner(
|
||||
icon: Icon(
|
||||
Icons.warning_amber,
|
||||
size: 14,
|
||||
color: Colors.amber.shade900,
|
||||
),
|
||||
text: autoWarnings.join(';'),
|
||||
background: Colors.amber.shade100,
|
||||
foreground: Colors.amber.shade900,
|
||||
border: Colors.amber.shade200,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (paichanSwitchNotice != null) {
|
||||
banners.add(
|
||||
_NoticeBanner(
|
||||
icon: Icon(Icons.info_outline, size: 14, color: Colors.blue.shade900),
|
||||
text: paichanSwitchNotice!,
|
||||
background: Colors.blue.shade50,
|
||||
foreground: Colors.blue.shade900,
|
||||
border: Colors.blue.shade100,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (isDuplicateBoxNo && phase == BoxingPhase.scanned) {
|
||||
banners.add(
|
||||
_NoticeBanner(
|
||||
icon: Icon(
|
||||
Icons.warning_amber,
|
||||
size: 14,
|
||||
color: Colors.amber.shade900,
|
||||
),
|
||||
text: '箱号 $boxNoText 已存在,请重新输入',
|
||||
background: Colors.amber.shade100,
|
||||
foreground: Colors.amber.shade900,
|
||||
border: Colors.amber.shade200,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (quantityTooHigh && phase == BoxingPhase.scanned) {
|
||||
banners.add(
|
||||
_NoticeBanner(
|
||||
icon: Icon(Icons.close, size: 14, color: Colors.red.shade800),
|
||||
text: '超出可装数量上限(最多可装 $remainingQuantity 件)',
|
||||
background: Colors.red.shade50,
|
||||
foreground: Colors.red.shade800,
|
||||
border: Colors.red.shade100,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (phase == BoxingPhase.scanned &&
|
||||
zongpaiNo != null &&
|
||||
remainingQuantity <= 0) {
|
||||
final jumpBoxNo = completedJumpBoxNo;
|
||||
if (jumpBoxNo != null) {
|
||||
banners.add(
|
||||
GestureDetector(
|
||||
onTap: onJumpToCompletedBox,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: _NoticeBanner(
|
||||
icon: Icon(Icons.warning, size: 14, color: Colors.amber.shade800),
|
||||
text: '$zongpaiNo 已完成装箱。(P4跳转所在箱号)',
|
||||
background: Colors.amber.shade50,
|
||||
foreground: Colors.amber.shade800,
|
||||
border: Colors.amber.shade200,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
banners.add(
|
||||
_NoticeBanner(
|
||||
icon: Icon(Icons.warning, size: 14, color: Colors.red.shade800),
|
||||
text: '$zongpaiNo 已全部装箱完毕,无需操作',
|
||||
background: Colors.red.shade50,
|
||||
foreground: Colors.red.shade800,
|
||||
border: Colors.red.shade100,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: banners);
|
||||
}
|
||||
}
|
||||
|
||||
class _NoticeBanner extends StatelessWidget {
|
||||
final Widget icon;
|
||||
final String text;
|
||||
final Color background;
|
||||
final Color foreground;
|
||||
final Color border;
|
||||
|
||||
const _NoticeBanner({
|
||||
required this.icon,
|
||||
required this.text,
|
||||
required this.background,
|
||||
required this.foreground,
|
||||
required this.border,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: background,
|
||||
border: Border(bottom: BorderSide(color: border)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(fontSize: 12, color: foreground),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,37 +8,22 @@ import 'package:pad_scanner/services/code_parser.dart';
|
||||
import 'package:pad_scanner/services/api_service.dart';
|
||||
import 'package:pad_scanner/services/boxing_context.dart';
|
||||
import 'package:pad_scanner/services/feedback_service.dart';
|
||||
import 'package:pad_scanner/pages/boxing/boxing_box_mutations.dart'
|
||||
as box_mutations;
|
||||
import 'package:pad_scanner/pages/boxing/boxing_calculations.dart'
|
||||
as boxing_calculations;
|
||||
import 'package:pad_scanner/pages/boxing/boxing_models.dart';
|
||||
import 'package:pad_scanner/pages/boxing/boxing_status_presenter.dart';
|
||||
import 'package:pad_scanner/pages/boxing/dialogs/cross_paichan_dialog.dart';
|
||||
import 'package:pad_scanner/pages/boxing/widgets/boxing_shared_widgets.dart';
|
||||
import 'package:pad_scanner/pages/boxing/widgets/boxing_notice_banners.dart';
|
||||
import 'package:pad_scanner/pages/boxing/widgets/multi_code_body.dart';
|
||||
import 'package:pad_scanner/pages/boxing/widgets/single_code_body.dart';
|
||||
import 'package:pad_scanner/pages/boxing_detail_page.dart';
|
||||
import 'package:pad_scanner/widgets/status_bar.dart';
|
||||
|
||||
// === 装箱模式 ===
|
||||
|
||||
enum BoxingMode {
|
||||
singleCode, // 单码装箱
|
||||
multiCode, // 多码凑箱
|
||||
}
|
||||
|
||||
class BoxingPageArguments {
|
||||
final BoxingMode initialMode;
|
||||
final List<String> autoScanCodes;
|
||||
|
||||
const BoxingPageArguments({
|
||||
required this.initialMode,
|
||||
required this.autoScanCodes,
|
||||
});
|
||||
}
|
||||
|
||||
// === 页面阶段 ===
|
||||
|
||||
enum _Phase {
|
||||
waiting, // 等待扫码
|
||||
scanned, // 已扫码,显示信息
|
||||
submitted, // 已提交成功
|
||||
}
|
||||
export 'package:pad_scanner/pages/boxing/boxing_models.dart'
|
||||
show BoxingMode, BoxingPageArguments;
|
||||
|
||||
// === 主页面 ===
|
||||
|
||||
@@ -62,7 +47,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
BoxingMode _mode = BoxingMode.singleCode;
|
||||
|
||||
// 阶段
|
||||
_Phase _phase = _Phase.waiting;
|
||||
BoxingPhase _phase = BoxingPhase.waiting;
|
||||
|
||||
// 当前总排号
|
||||
String? _zongpaiNo;
|
||||
@@ -221,7 +206,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
void _resetState() {
|
||||
_phase = _Phase.waiting;
|
||||
_phase = BoxingPhase.waiting;
|
||||
_zongpaiNo = null;
|
||||
_paichanNo = null;
|
||||
_workOrderNo = null;
|
||||
@@ -334,7 +319,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
_currentZongpaiBoxes = result.currentZongpaiBoxes;
|
||||
_existingBoxes = result.existingBoxes;
|
||||
_maxBoxNo = result.maxBoxNo;
|
||||
_phase = _Phase.scanned;
|
||||
_phase = BoxingPhase.scanned;
|
||||
_isDuplicateBoxNo = false;
|
||||
_editingBox = null;
|
||||
_editingAssignedItemId = null;
|
||||
@@ -469,98 +454,16 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
|
||||
// === 跨排产号确认对话框 ===
|
||||
|
||||
void _showCrossPaichaDialog() {
|
||||
Future<void> _showCrossPaichaDialog() async {
|
||||
final currentPaicha = _lastScannedPaichanNo ?? '--';
|
||||
final newPaicha = _paichanNo ?? '--';
|
||||
final dialogFocus = FocusNode();
|
||||
var dismissed = false;
|
||||
var selectedIndex = 0; // 0 = "否" (default), 1 = "是"
|
||||
|
||||
void dismissAsNo() {
|
||||
if (dismissed) return;
|
||||
dismissed = true;
|
||||
Navigator.of(context).pop();
|
||||
_cancelPaichanSwitch();
|
||||
}
|
||||
|
||||
void confirmSwitch() {
|
||||
if (dismissed) return;
|
||||
dismissed = true;
|
||||
Navigator.of(context).pop();
|
||||
_confirmPaichanSwitch();
|
||||
}
|
||||
|
||||
showDialog(
|
||||
final confirmed = await showCrossPaichanDialog(
|
||||
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(
|
||||
'当前排产号:$currentPaicha\n'
|
||||
'新排产号:$newPaicha\n\n'
|
||||
'是否切换到新的排产号?\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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
currentPaicha: currentPaicha,
|
||||
newPaicha: newPaicha,
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
dialogFocus.requestFocus();
|
||||
});
|
||||
if (!mounted) return;
|
||||
confirmed ? _confirmPaichanSwitch() : _cancelPaichanSwitch();
|
||||
}
|
||||
|
||||
void _confirmPaichanSwitch() {
|
||||
@@ -609,50 +512,20 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
_crossPaichaPending = false;
|
||||
_zongpaiNo = null;
|
||||
_paichanNo = _lastScannedPaichanNo;
|
||||
_phase = _Phase.waiting;
|
||||
_phase = BoxingPhase.waiting;
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// === 重复箱号检测 ===
|
||||
|
||||
bool _boxNoIsDuplicate() {
|
||||
final boxNo = int.tryParse(_boxNoController.text);
|
||||
if (boxNo == null) {
|
||||
return false;
|
||||
}
|
||||
return switch (_mode) {
|
||||
BoxingMode.multiCode => false,
|
||||
BoxingMode.singleCode => _currentZongpaiBoxes.any((b) {
|
||||
if (b.boxNo != boxNo) return false;
|
||||
return _editingBox == null || b.boxItemId != _editingBox!.boxItemId;
|
||||
}),
|
||||
BoxingMode.singleCode => boxing_calculations.singleCodeBoxNoIsDuplicate(
|
||||
boxNoText: _boxNoController.text,
|
||||
currentBoxes: _currentZongpaiBoxes,
|
||||
editingBoxItemId: _editingBox?.boxItemId,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -663,33 +536,34 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
// === 提交 ===
|
||||
|
||||
int get _packedQuantity {
|
||||
return _currentZongpaiBoxes.fold<int>(
|
||||
0,
|
||||
(sum, item) => sum + item.quantity,
|
||||
);
|
||||
return boxing_calculations.packedQuantity(_currentZongpaiBoxes);
|
||||
}
|
||||
|
||||
int get _remainingQuantity {
|
||||
final total = _erpQuantity ?? 0;
|
||||
return total - _packedQuantity;
|
||||
return boxing_calculations.remainingQuantity(
|
||||
totalQuantity: _erpQuantity,
|
||||
currentBoxes: _currentZongpaiBoxes,
|
||||
);
|
||||
}
|
||||
|
||||
bool get _quantityTooHigh {
|
||||
if (_remainingQuantity <= 0) return false;
|
||||
final qty = int.tryParse(_quantityController.text);
|
||||
return qty != null && qty > _remainingQuantity;
|
||||
return boxing_calculations.quantityTooHigh(
|
||||
remainingQuantity: _remainingQuantity,
|
||||
quantityText: _quantityController.text,
|
||||
);
|
||||
}
|
||||
|
||||
bool get _canSubmit {
|
||||
if (_isSubmitting || _phase != _Phase.scanned) return false;
|
||||
if (_zongpaiNo == null) return false;
|
||||
final boxNo = int.tryParse(_boxNoController.text);
|
||||
final qty = int.tryParse(_quantityController.text);
|
||||
if (boxNo == null || boxNo <= 0 || qty == null || qty <= 0) return false;
|
||||
if (_remainingQuantity <= 0) return false;
|
||||
if (_quantityTooHigh) return false;
|
||||
if (_isDuplicateBoxNo) return false;
|
||||
return true;
|
||||
return boxing_calculations.canSubmitBoxing(
|
||||
isSubmitting: _isSubmitting,
|
||||
phase: _phase,
|
||||
zongpaiNo: _zongpaiNo,
|
||||
boxNoText: _boxNoController.text,
|
||||
quantityText: _quantityController.text,
|
||||
remainingQuantity: _remainingQuantity,
|
||||
quantityTooHigh: _quantityTooHigh,
|
||||
isDuplicateBoxNo: _isDuplicateBoxNo,
|
||||
);
|
||||
}
|
||||
|
||||
void _submitFromKeyboard() {
|
||||
@@ -710,36 +584,16 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
int? get _currentManyToOneBoxNo {
|
||||
final boxNo = int.tryParse(_boxNoController.text.trim());
|
||||
if (boxNo == null || boxNo <= 0) return null;
|
||||
return boxNo;
|
||||
return boxing_calculations.currentManyToOneBoxNo(_boxNoController.text);
|
||||
}
|
||||
|
||||
List<ManyToOnePackedItem> get _visibleManyToOnePackedItems {
|
||||
final boxNo = _currentManyToOneBoxNo;
|
||||
if (boxNo == null) return const [];
|
||||
final byItemId = <int, ManyToOnePackedItem>{};
|
||||
for (final box in _existingBoxes.where((box) => box.boxNo == boxNo)) {
|
||||
for (final item in box.items) {
|
||||
final boxItemId = item.boxItemId;
|
||||
if (boxItemId == null) continue;
|
||||
byItemId[boxItemId] = ManyToOnePackedItem(
|
||||
boxItemId: boxItemId,
|
||||
zongpaiNo: item.zongpaiNo,
|
||||
paichanNo: _paichanNo,
|
||||
workOrderNo: item.workOrderNo,
|
||||
boxNo: boxNo,
|
||||
quantity: item.quantity,
|
||||
totalQuantity: item.totalQuantity,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (final item in _manyToOnePackedItems.where(
|
||||
(item) => item.boxNo == boxNo,
|
||||
)) {
|
||||
byItemId[item.boxItemId] = item;
|
||||
}
|
||||
return byItemId.values.toList(growable: false);
|
||||
return boxing_calculations.visibleManyToOnePackedItems(
|
||||
boxNo: _currentManyToOneBoxNo,
|
||||
existingBoxes: _existingBoxes,
|
||||
packedItems: _manyToOnePackedItems,
|
||||
paichanNo: _paichanNo,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _submit() async {
|
||||
@@ -839,7 +693,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
);
|
||||
} else {
|
||||
setState(() {
|
||||
_phase = _Phase.scanned;
|
||||
_phase = BoxingPhase.scanned;
|
||||
_boxNoController.text = (_maxBoxNo + 1).toString();
|
||||
_quantityController.text = remaining.toString();
|
||||
_quantityController.selection = TextSelection(
|
||||
@@ -872,7 +726,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
),
|
||||
);
|
||||
}
|
||||
_phase = _Phase.waiting;
|
||||
_phase = BoxingPhase.waiting;
|
||||
_zongpaiNo = null;
|
||||
_editingPackedItemId = null;
|
||||
_editingPackedQuantity = '';
|
||||
@@ -925,62 +779,31 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
int boxNo,
|
||||
int quantity,
|
||||
) {
|
||||
final nextBoxes = <BoxDetailData>[];
|
||||
for (final box in _existingBoxes) {
|
||||
final items = box.items.where((item) {
|
||||
return item.boxItemId != editing.boxItemId;
|
||||
}).toList();
|
||||
if (items.isNotEmpty) {
|
||||
nextBoxes.add(BoxDetailData(boxNo: box.boxNo, items: items));
|
||||
}
|
||||
}
|
||||
|
||||
final targetIndex = nextBoxes.indexWhere((box) => box.boxNo == boxNo);
|
||||
final updatedItem = BoxItemData(
|
||||
boxItemId: editing.boxItemId,
|
||||
final result = box_mutations.replaceExistingBoxItem(
|
||||
existingBoxes: _existingBoxes,
|
||||
editing: editing,
|
||||
boxNo: boxNo,
|
||||
quantity: quantity,
|
||||
zongpaiNo: _zongpaiNo!,
|
||||
workOrderNo: _workOrderNo,
|
||||
quantity: quantity,
|
||||
totalQuantity: _erpQuantity,
|
||||
);
|
||||
if (targetIndex >= 0) {
|
||||
final target = nextBoxes[targetIndex];
|
||||
nextBoxes[targetIndex] = BoxDetailData(
|
||||
boxNo: target.boxNo,
|
||||
items: [...target.items, updatedItem],
|
||||
);
|
||||
} else {
|
||||
nextBoxes.add(BoxDetailData(boxNo: boxNo, items: [updatedItem]));
|
||||
}
|
||||
nextBoxes.sort((a, b) => a.boxNo.compareTo(b.boxNo));
|
||||
_existingBoxes = nextBoxes;
|
||||
_maxBoxNo = nextBoxes.fold<int>(
|
||||
0,
|
||||
(max, box) => box.boxNo > max ? box.boxNo : max,
|
||||
);
|
||||
_existingBoxes = result.boxes;
|
||||
_maxBoxNo = result.maxBoxNo;
|
||||
}
|
||||
|
||||
void _addExistingBoxItem(int boxNo, int quantity, int? boxItemId) {
|
||||
final item = BoxItemData(
|
||||
final result = box_mutations.addExistingBoxItem(
|
||||
existingBoxes: _existingBoxes,
|
||||
boxNo: boxNo,
|
||||
quantity: quantity,
|
||||
boxItemId: boxItemId,
|
||||
zongpaiNo: _zongpaiNo!,
|
||||
workOrderNo: _workOrderNo,
|
||||
quantity: quantity,
|
||||
totalQuantity: _erpQuantity,
|
||||
);
|
||||
final index = _existingBoxes.indexWhere((box) => box.boxNo == boxNo);
|
||||
if (index < 0) {
|
||||
_existingBoxes = [
|
||||
..._existingBoxes,
|
||||
BoxDetailData(boxNo: boxNo, items: [item]),
|
||||
]..sort((a, b) => a.boxNo.compareTo(b.boxNo));
|
||||
return;
|
||||
}
|
||||
|
||||
final next = List<BoxDetailData>.from(_existingBoxes);
|
||||
final box = next[index];
|
||||
next[index] = BoxDetailData(boxNo: box.boxNo, items: [...box.items, item]);
|
||||
_existingBoxes = next;
|
||||
_existingBoxes = result.boxes;
|
||||
_maxBoxNo = result.maxBoxNo;
|
||||
}
|
||||
|
||||
void _finishManyToOneBox() {
|
||||
@@ -990,7 +813,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
_editingPackedQuantity = '';
|
||||
_deletingPackedItemId = null;
|
||||
_zongpaiNo = null;
|
||||
_phase = _Phase.waiting;
|
||||
_phase = BoxingPhase.waiting;
|
||||
_quantityController.clear();
|
||||
_boxNoController.text = (_maxBoxNo + 1).toString();
|
||||
_statusOverrideText = null;
|
||||
@@ -1016,9 +839,11 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
int _maxAssignedQuantity(CurrentZongpaiBoxData item) {
|
||||
final total = _erpQuantity ?? 0;
|
||||
final packedWithoutItem = _packedQuantity - item.quantity;
|
||||
return total - packedWithoutItem;
|
||||
return boxing_calculations.maxAssignedQuantity(
|
||||
totalQuantity: _erpQuantity,
|
||||
packedQuantity: _packedQuantity,
|
||||
itemQuantity: item.quantity,
|
||||
);
|
||||
}
|
||||
|
||||
void _refreshSingleCodeNewInput({bool focusQuantity = true}) {
|
||||
@@ -1246,39 +1071,14 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
int boxNo,
|
||||
int quantity,
|
||||
) {
|
||||
final nextBoxes = <BoxDetailData>[];
|
||||
for (final box in _existingBoxes) {
|
||||
final items = box.items.where((boxItem) {
|
||||
return boxItem.boxItemId != item.boxItemId;
|
||||
}).toList();
|
||||
if (items.isNotEmpty) {
|
||||
nextBoxes.add(BoxDetailData(boxNo: box.boxNo, items: items));
|
||||
}
|
||||
}
|
||||
|
||||
final updatedItem = BoxItemData(
|
||||
boxItemId: item.boxItemId,
|
||||
zongpaiNo: item.zongpaiNo,
|
||||
workOrderNo: item.workOrderNo,
|
||||
final result = box_mutations.replaceExistingPackedItem(
|
||||
existingBoxes: _existingBoxes,
|
||||
item: item,
|
||||
boxNo: boxNo,
|
||||
quantity: quantity,
|
||||
totalQuantity: item.totalQuantity,
|
||||
);
|
||||
final targetIndex = nextBoxes.indexWhere((box) => box.boxNo == boxNo);
|
||||
if (targetIndex >= 0) {
|
||||
final target = nextBoxes[targetIndex];
|
||||
nextBoxes[targetIndex] = BoxDetailData(
|
||||
boxNo: target.boxNo,
|
||||
items: [...target.items, updatedItem],
|
||||
);
|
||||
} else {
|
||||
nextBoxes.add(BoxDetailData(boxNo: boxNo, items: [updatedItem]));
|
||||
}
|
||||
nextBoxes.sort((a, b) => a.boxNo.compareTo(b.boxNo));
|
||||
_existingBoxes = nextBoxes;
|
||||
_maxBoxNo = nextBoxes.fold<int>(
|
||||
0,
|
||||
(max, box) => box.boxNo > max ? box.boxNo : max,
|
||||
);
|
||||
_existingBoxes = result.boxes;
|
||||
_maxBoxNo = result.maxBoxNo;
|
||||
}
|
||||
|
||||
Future<void> _deletePackedItem(ManyToOnePackedItem item) async {
|
||||
@@ -1331,20 +1131,12 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
void _removeExistingBoxItem(int boxItemId) {
|
||||
final nextBoxes = <BoxDetailData>[];
|
||||
for (final box in _existingBoxes) {
|
||||
final items = box.items
|
||||
.where((item) => item.boxItemId != boxItemId)
|
||||
.toList();
|
||||
if (items.isNotEmpty) {
|
||||
nextBoxes.add(BoxDetailData(boxNo: box.boxNo, items: items));
|
||||
}
|
||||
}
|
||||
_existingBoxes = nextBoxes;
|
||||
_maxBoxNo = nextBoxes.fold<int>(
|
||||
0,
|
||||
(max, box) => box.boxNo > max ? box.boxNo : max,
|
||||
final result = box_mutations.removeExistingBoxItem(
|
||||
existingBoxes: _existingBoxes,
|
||||
boxItemId: boxItemId,
|
||||
);
|
||||
_existingBoxes = result.boxes;
|
||||
_maxBoxNo = result.maxBoxNo;
|
||||
}
|
||||
|
||||
// === Status bar management ===
|
||||
@@ -1392,69 +1184,22 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
// === 状态文字 ===
|
||||
|
||||
void _updateBaseStatus() {
|
||||
if (_isAutoProcessing) {
|
||||
_statusDot = StatusDotColor.orange;
|
||||
_statusText = '正在同步转运数据...';
|
||||
return;
|
||||
}
|
||||
if (_isSubmitting) {
|
||||
_statusDot = StatusDotColor.orange;
|
||||
_statusText = '正在提交…';
|
||||
return;
|
||||
}
|
||||
if (_isDuplicateBoxNo && _phase == _Phase.scanned) {
|
||||
_statusDot = StatusDotColor.amber;
|
||||
_statusText = '箱号 ${_boxNoController.text} 已存在,请重新输入';
|
||||
return;
|
||||
}
|
||||
if (_quantityTooHigh && _phase == _Phase.scanned) {
|
||||
_statusDot = StatusDotColor.red;
|
||||
_statusText = '超出可装数量上限';
|
||||
return;
|
||||
}
|
||||
if (_editingAssignedItemId != null) {
|
||||
_statusDot = StatusDotColor.amber;
|
||||
_statusText = '正在编辑已分配记录';
|
||||
return;
|
||||
}
|
||||
if (_deletingAssignedItemId != null) {
|
||||
_statusDot = StatusDotColor.red;
|
||||
_statusText = '请确认是否删除该箱记录';
|
||||
return;
|
||||
}
|
||||
if (_paichanSwitchNotice != null) {
|
||||
_statusDot = StatusDotColor.blue;
|
||||
_statusText = '排产号已切换,箱号已重置';
|
||||
return;
|
||||
}
|
||||
switch (_phase) {
|
||||
case _Phase.waiting:
|
||||
_statusDot = StatusDotColor.blue;
|
||||
if (_mode == BoxingMode.multiCode &&
|
||||
_visibleManyToOnePackedItems.isNotEmpty) {
|
||||
_statusText = '请继续扫码或完成本箱';
|
||||
} else {
|
||||
_statusText = '等待扫码';
|
||||
}
|
||||
case _Phase.scanned:
|
||||
_statusDot = StatusDotColor.blue;
|
||||
if (_remainingQuantity <= 0) {
|
||||
_statusDot = StatusDotColor.red;
|
||||
_statusText = '该总排号已全部装箱完毕';
|
||||
return;
|
||||
}
|
||||
_statusText = _mode == BoxingMode.multiCode
|
||||
? '数量已填入,请确认或修改'
|
||||
: '数量已填入,请确认或修改';
|
||||
case _Phase.submitted:
|
||||
_statusDot = StatusDotColor.green;
|
||||
switch (_mode) {
|
||||
case BoxingMode.singleCode:
|
||||
_statusText = '装箱成功,可继续扫码';
|
||||
case BoxingMode.multiCode:
|
||||
_statusText = '请扫描下一个总排号';
|
||||
}
|
||||
}
|
||||
final status = boxingStatusFor(
|
||||
isAutoProcessing: _isAutoProcessing,
|
||||
isSubmitting: _isSubmitting,
|
||||
isDuplicateBoxNo: _isDuplicateBoxNo,
|
||||
quantityTooHigh: _quantityTooHigh,
|
||||
isEditingAssigned: _editingAssignedItemId != null,
|
||||
isDeletingAssigned: _deletingAssignedItemId != null,
|
||||
hasPaichanSwitchNotice: _paichanSwitchNotice != null,
|
||||
phase: _phase,
|
||||
isMultiCode: _mode == BoxingMode.multiCode,
|
||||
hasVisibleManyToOneItems: _visibleManyToOnePackedItems.isNotEmpty,
|
||||
remainingQuantity: _remainingQuantity,
|
||||
boxNoText: _boxNoController.text,
|
||||
);
|
||||
_statusDot = status.dot;
|
||||
_statusText = status.text;
|
||||
}
|
||||
|
||||
// === Build ===
|
||||
@@ -1487,12 +1232,25 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
..._buildNoticeBanners(),
|
||||
BoxingNoticeBanners(
|
||||
isAutoProcessing: _isAutoProcessing,
|
||||
autoWarnings: _autoWarnings,
|
||||
paichanSwitchNotice: _paichanSwitchNotice,
|
||||
isDuplicateBoxNo: _isDuplicateBoxNo,
|
||||
quantityTooHigh: _quantityTooHigh,
|
||||
phase: _phase,
|
||||
boxNoText: _boxNoController.text,
|
||||
remainingQuantity: _remainingQuantity,
|
||||
zongpaiNo: _zongpaiNo,
|
||||
completedJumpBoxNo: _completedJumpBoxNo,
|
||||
onJumpToCompletedBox: _jumpToCompletedBox,
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: _mode == BoxingMode.multiCode
|
||||
? MultiCodeBody(
|
||||
hasScan: _zongpaiNo != null && _phase == _Phase.scanned,
|
||||
hasScan:
|
||||
_zongpaiNo != null && _phase == BoxingPhase.scanned,
|
||||
zongpaiNo: _zongpaiNo,
|
||||
paichanNo: _paichanNo,
|
||||
workOrderNo: _workOrderNo,
|
||||
@@ -1532,9 +1290,10 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
setState(() => _deletingPackedItemId = null),
|
||||
)
|
||||
: SingleCodeBody(
|
||||
isWaiting: _phase == _Phase.waiting && _zongpaiNo == null,
|
||||
isWaiting:
|
||||
_phase == BoxingPhase.waiting && _zongpaiNo == null,
|
||||
isFinished:
|
||||
_phase == _Phase.scanned &&
|
||||
_phase == BoxingPhase.scanned &&
|
||||
_zongpaiNo != null &&
|
||||
_remainingQuantity <= 0,
|
||||
zongpaiNo: _zongpaiNo,
|
||||
@@ -1586,139 +1345,4 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildNoticeBanners() {
|
||||
final banners = <Widget>[];
|
||||
if (_isAutoProcessing) {
|
||||
banners.add(
|
||||
_noticeBanner(
|
||||
icon: SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1.5,
|
||||
color: Colors.orange.shade700,
|
||||
),
|
||||
),
|
||||
text: '正在同步转运数据...',
|
||||
background: Colors.orange.shade50,
|
||||
foreground: Colors.orange.shade900,
|
||||
border: Colors.orange.shade100,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_autoWarnings.isNotEmpty) {
|
||||
banners.add(
|
||||
_noticeBanner(
|
||||
icon: Icon(
|
||||
Icons.warning_amber,
|
||||
size: 14,
|
||||
color: Colors.amber.shade900,
|
||||
),
|
||||
text: _autoWarnings.join(';'),
|
||||
background: Colors.amber.shade100,
|
||||
foreground: Colors.amber.shade900,
|
||||
border: Colors.amber.shade200,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_paichanSwitchNotice != null) {
|
||||
banners.add(
|
||||
_noticeBanner(
|
||||
icon: Icon(Icons.info_outline, size: 14, color: Colors.blue.shade900),
|
||||
text: _paichanSwitchNotice!,
|
||||
background: Colors.blue.shade50,
|
||||
foreground: Colors.blue.shade900,
|
||||
border: Colors.blue.shade100,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_isDuplicateBoxNo && _phase == _Phase.scanned) {
|
||||
banners.add(
|
||||
_noticeBanner(
|
||||
icon: Icon(
|
||||
Icons.warning_amber,
|
||||
size: 14,
|
||||
color: Colors.amber.shade900,
|
||||
),
|
||||
text: '箱号 ${_boxNoController.text} 已存在,请重新输入',
|
||||
background: Colors.amber.shade100,
|
||||
foreground: Colors.amber.shade900,
|
||||
border: Colors.amber.shade200,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_quantityTooHigh && _phase == _Phase.scanned) {
|
||||
banners.add(
|
||||
_noticeBanner(
|
||||
icon: Icon(Icons.close, size: 14, color: Colors.red.shade800),
|
||||
text: '超出可装数量上限(最多可装 $_remainingQuantity 件)',
|
||||
background: Colors.red.shade50,
|
||||
foreground: Colors.red.shade800,
|
||||
border: Colors.red.shade100,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_phase == _Phase.scanned &&
|
||||
_zongpaiNo != null &&
|
||||
_remainingQuantity <= 0) {
|
||||
final jumpBoxNo = _completedJumpBoxNo;
|
||||
if (jumpBoxNo != null) {
|
||||
banners.add(
|
||||
GestureDetector(
|
||||
onTap: _jumpToCompletedBox,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: _noticeBanner(
|
||||
icon: Icon(Icons.warning, size: 14, color: Colors.amber.shade800),
|
||||
text: '${_zongpaiNo!} 已完成装箱。(P4跳转所在箱号)',
|
||||
background: Colors.amber.shade50,
|
||||
foreground: Colors.amber.shade800,
|
||||
border: Colors.amber.shade200,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
banners.add(
|
||||
_noticeBanner(
|
||||
icon: Icon(Icons.warning, size: 14, color: Colors.red.shade800),
|
||||
text: '${_zongpaiNo!} 已全部装箱完毕,无需操作',
|
||||
background: Colors.red.shade50,
|
||||
foreground: Colors.red.shade800,
|
||||
border: Colors.red.shade100,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return banners;
|
||||
}
|
||||
|
||||
Widget _noticeBanner({
|
||||
required Widget icon,
|
||||
required String text,
|
||||
required Color background,
|
||||
required Color foreground,
|
||||
required Color border,
|
||||
}) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: background,
|
||||
border: Border(bottom: BorderSide(color: border)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(fontSize: 12, color: foreground),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
88
test/pages/boxing/boxing_box_mutations_test.dart
Normal file
88
test/pages/boxing/boxing_box_mutations_test.dart
Normal file
@@ -0,0 +1,88 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pad_scanner/pages/boxing/boxing_box_mutations.dart';
|
||||
import 'package:pad_scanner/pages/boxing/boxing_models.dart';
|
||||
import 'package:pad_scanner/services/api_service.dart';
|
||||
|
||||
void main() {
|
||||
group('boxing box mutations', () {
|
||||
test('adds item to a new sorted box', () {
|
||||
final result = addExistingBoxItem(
|
||||
existingBoxes: [BoxDetailData(boxNo: 3, items: const [])],
|
||||
boxNo: 2,
|
||||
quantity: 5,
|
||||
boxItemId: 22,
|
||||
zongpaiNo: 'ZP022',
|
||||
workOrderNo: 'WO022',
|
||||
totalQuantity: 5,
|
||||
);
|
||||
|
||||
expect(result.boxes.map((box) => box.boxNo), [2, 3]);
|
||||
expect(result.maxBoxNo, 3);
|
||||
expect(result.boxes.first.items.single.zongpaiNo, 'ZP022');
|
||||
});
|
||||
|
||||
test('replaces single code item and removes empty source box', () {
|
||||
final result = replaceExistingBoxItem(
|
||||
existingBoxes: [
|
||||
BoxDetailData(
|
||||
boxNo: 1,
|
||||
items: [BoxItemData(boxItemId: 1, zongpaiNo: 'ZP001', quantity: 2)],
|
||||
),
|
||||
],
|
||||
editing: CurrentZongpaiBoxData(boxItemId: 1, boxNo: 1, quantity: 2),
|
||||
boxNo: 4,
|
||||
quantity: 3,
|
||||
zongpaiNo: 'ZP001',
|
||||
workOrderNo: 'WO001',
|
||||
totalQuantity: 5,
|
||||
);
|
||||
|
||||
expect(result.boxes.map((box) => box.boxNo), [4]);
|
||||
expect(result.boxes.single.items.single.quantity, 3);
|
||||
expect(result.maxBoxNo, 4);
|
||||
});
|
||||
|
||||
test('moves packed item to a different box', () {
|
||||
final result = replaceExistingPackedItem(
|
||||
existingBoxes: [
|
||||
BoxDetailData(
|
||||
boxNo: 2,
|
||||
items: [BoxItemData(boxItemId: 9, zongpaiNo: 'ZP009', quantity: 1)],
|
||||
),
|
||||
],
|
||||
item: const ManyToOnePackedItem(
|
||||
boxItemId: 9,
|
||||
zongpaiNo: 'ZP009',
|
||||
workOrderNo: 'WO009',
|
||||
boxNo: 2,
|
||||
quantity: 1,
|
||||
totalQuantity: 3,
|
||||
),
|
||||
boxNo: 5,
|
||||
quantity: 2,
|
||||
);
|
||||
|
||||
expect(result.boxes.map((box) => box.boxNo), [5]);
|
||||
expect(result.boxes.single.items.single.quantity, 2);
|
||||
});
|
||||
|
||||
test('removes item and drops empty box', () {
|
||||
final result = removeExistingBoxItem(
|
||||
existingBoxes: [
|
||||
BoxDetailData(
|
||||
boxNo: 1,
|
||||
items: [BoxItemData(boxItemId: 1, zongpaiNo: 'ZP001', quantity: 2)],
|
||||
),
|
||||
BoxDetailData(
|
||||
boxNo: 3,
|
||||
items: [BoxItemData(boxItemId: 2, zongpaiNo: 'ZP002', quantity: 1)],
|
||||
),
|
||||
],
|
||||
boxItemId: 1,
|
||||
);
|
||||
|
||||
expect(result.boxes.map((box) => box.boxNo), [3]);
|
||||
expect(result.maxBoxNo, 3);
|
||||
});
|
||||
});
|
||||
}
|
||||
123
test/pages/boxing/boxing_calculations_test.dart
Normal file
123
test/pages/boxing/boxing_calculations_test.dart
Normal file
@@ -0,0 +1,123 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pad_scanner/pages/boxing/boxing_calculations.dart';
|
||||
import 'package:pad_scanner/pages/boxing/boxing_models.dart';
|
||||
import 'package:pad_scanner/services/api_service.dart';
|
||||
|
||||
void main() {
|
||||
group('boxing calculations', () {
|
||||
final currentBoxes = [
|
||||
CurrentZongpaiBoxData(boxItemId: 1, boxNo: 1, quantity: 4),
|
||||
CurrentZongpaiBoxData(boxItemId: 2, boxNo: 2, quantity: 3),
|
||||
];
|
||||
|
||||
test('calculates packed and remaining quantity', () {
|
||||
expect(packedQuantity(currentBoxes), 7);
|
||||
expect(
|
||||
remainingQuantity(totalQuantity: 10, currentBoxes: currentBoxes),
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
test('detects quantity too high only when remaining is positive', () {
|
||||
expect(quantityTooHigh(remainingQuantity: 3, quantityText: '4'), isTrue);
|
||||
expect(quantityTooHigh(remainingQuantity: 3, quantityText: '3'), isFalse);
|
||||
expect(quantityTooHigh(remainingQuantity: 0, quantityText: '1'), isFalse);
|
||||
});
|
||||
|
||||
test('validates submit conditions', () {
|
||||
expect(
|
||||
canSubmitBoxing(
|
||||
isSubmitting: false,
|
||||
phase: BoxingPhase.scanned,
|
||||
zongpaiNo: 'ZP001',
|
||||
boxNoText: '1',
|
||||
quantityText: '2',
|
||||
remainingQuantity: 3,
|
||||
quantityTooHigh: false,
|
||||
isDuplicateBoxNo: false,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
expect(
|
||||
canSubmitBoxing(
|
||||
isSubmitting: true,
|
||||
phase: BoxingPhase.scanned,
|
||||
zongpaiNo: 'ZP001',
|
||||
boxNoText: '1',
|
||||
quantityText: '2',
|
||||
remainingQuantity: 3,
|
||||
quantityTooHigh: false,
|
||||
isDuplicateBoxNo: false,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('detects duplicate box number in single code mode', () {
|
||||
expect(
|
||||
singleCodeBoxNoIsDuplicate(
|
||||
boxNoText: '1',
|
||||
currentBoxes: currentBoxes,
|
||||
editingBoxItemId: null,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
singleCodeBoxNoIsDuplicate(
|
||||
boxNoText: '1',
|
||||
currentBoxes: currentBoxes,
|
||||
editingBoxItemId: 1,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('merges visible many-to-one items by box item id', () {
|
||||
final existingBoxes = [
|
||||
BoxDetailData(
|
||||
boxNo: 8,
|
||||
items: [
|
||||
BoxItemData(
|
||||
boxItemId: 11,
|
||||
zongpaiNo: 'ZP011',
|
||||
workOrderNo: 'WO011',
|
||||
quantity: 2,
|
||||
totalQuantity: 6,
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
final packedItems = [
|
||||
const ManyToOnePackedItem(
|
||||
boxItemId: 11,
|
||||
zongpaiNo: 'ZP011',
|
||||
paichanNo: 'PC001',
|
||||
workOrderNo: 'WO011',
|
||||
boxNo: 8,
|
||||
quantity: 3,
|
||||
totalQuantity: 6,
|
||||
),
|
||||
const ManyToOnePackedItem(
|
||||
boxItemId: 12,
|
||||
zongpaiNo: 'ZP012',
|
||||
paichanNo: 'PC001',
|
||||
workOrderNo: 'WO012',
|
||||
boxNo: 8,
|
||||
quantity: 1,
|
||||
totalQuantity: 4,
|
||||
),
|
||||
];
|
||||
|
||||
final visible = visibleManyToOnePackedItems(
|
||||
boxNo: 8,
|
||||
existingBoxes: existingBoxes,
|
||||
packedItems: packedItems,
|
||||
paichanNo: 'PC001',
|
||||
);
|
||||
|
||||
expect(visible.map((item) => item.boxItemId), [11, 12]);
|
||||
expect(visible.first.quantity, 3);
|
||||
});
|
||||
});
|
||||
}
|
||||
81
test/pages/boxing/boxing_status_presenter_test.dart
Normal file
81
test/pages/boxing/boxing_status_presenter_test.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pad_scanner/pages/boxing/boxing_models.dart';
|
||||
import 'package:pad_scanner/pages/boxing/boxing_status_presenter.dart';
|
||||
import 'package:pad_scanner/widgets/status_bar.dart';
|
||||
|
||||
void main() {
|
||||
BoxingStatusPresentation status({
|
||||
bool isAutoProcessing = false,
|
||||
bool isSubmitting = false,
|
||||
bool isDuplicateBoxNo = false,
|
||||
bool quantityTooHigh = false,
|
||||
bool isEditingAssigned = false,
|
||||
bool isDeletingAssigned = false,
|
||||
bool hasPaichanSwitchNotice = false,
|
||||
BoxingPhase phase = BoxingPhase.waiting,
|
||||
bool isMultiCode = false,
|
||||
bool hasVisibleManyToOneItems = false,
|
||||
int remainingQuantity = 1,
|
||||
String boxNoText = '1',
|
||||
}) {
|
||||
return boxingStatusFor(
|
||||
isAutoProcessing: isAutoProcessing,
|
||||
isSubmitting: isSubmitting,
|
||||
isDuplicateBoxNo: isDuplicateBoxNo,
|
||||
quantityTooHigh: quantityTooHigh,
|
||||
isEditingAssigned: isEditingAssigned,
|
||||
isDeletingAssigned: isDeletingAssigned,
|
||||
hasPaichanSwitchNotice: hasPaichanSwitchNotice,
|
||||
phase: phase,
|
||||
isMultiCode: isMultiCode,
|
||||
hasVisibleManyToOneItems: hasVisibleManyToOneItems,
|
||||
remainingQuantity: remainingQuantity,
|
||||
boxNoText: boxNoText,
|
||||
);
|
||||
}
|
||||
|
||||
group('boxingStatusFor', () {
|
||||
test('reports waiting and multi-code continuation states', () {
|
||||
expect(status().text, '等待扫码');
|
||||
final multi = status(isMultiCode: true, hasVisibleManyToOneItems: true);
|
||||
expect(multi.text, '请继续扫码或完成本箱');
|
||||
});
|
||||
|
||||
test('prioritizes transient and validation states', () {
|
||||
expect(status(isSubmitting: true).text, '正在提交…');
|
||||
expect(
|
||||
status(isDuplicateBoxNo: true, phase: BoxingPhase.scanned).dot,
|
||||
StatusDotColor.amber,
|
||||
);
|
||||
expect(
|
||||
status(quantityTooHigh: true, phase: BoxingPhase.scanned).text,
|
||||
'超出可装数量上限',
|
||||
);
|
||||
});
|
||||
|
||||
test('reports edit delete and switched paichan states', () {
|
||||
expect(status(isEditingAssigned: true).text, '正在编辑已分配记录');
|
||||
expect(status(isDeletingAssigned: true).dot, StatusDotColor.red);
|
||||
expect(status(hasPaichanSwitchNotice: true).text, '排产号已切换,箱号已重置');
|
||||
});
|
||||
|
||||
test('reports scanned completed and submitted states', () {
|
||||
expect(
|
||||
status(phase: BoxingPhase.scanned, remainingQuantity: 0).text,
|
||||
'该总排号已全部装箱完毕',
|
||||
);
|
||||
expect(
|
||||
status(phase: BoxingPhase.scanned, remainingQuantity: 2).text,
|
||||
'数量已填入,请确认或修改',
|
||||
);
|
||||
expect(
|
||||
status(phase: BoxingPhase.submitted, isMultiCode: false).text,
|
||||
'装箱成功,可继续扫码',
|
||||
);
|
||||
expect(
|
||||
status(phase: BoxingPhase.submitted, isMultiCode: true).text,
|
||||
'请扫描下一个总排号',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user