feat(boxing): handle multi-code paichan switches
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:pad_scanner/services/app_config_service.dart';
|
||||
import 'package:pad_scanner/services/scanner_service.dart';
|
||||
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_detail_page.dart';
|
||||
import 'package:pad_scanner/widgets/status_bar.dart';
|
||||
@@ -101,6 +102,8 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
int? _editingPackedItemId;
|
||||
String _editingPackedQuantity = '';
|
||||
int? _deletingPackedItemId;
|
||||
String? _lastScannedPaichanNo;
|
||||
String? _paichanSwitchNotice;
|
||||
|
||||
// 提交中
|
||||
bool _isSubmitting = false;
|
||||
@@ -184,6 +187,8 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
_editingPackedItemId = null;
|
||||
_editingPackedQuantity = '';
|
||||
_deletingPackedItemId = null;
|
||||
_lastScannedPaichanNo = null;
|
||||
_paichanSwitchNotice = null;
|
||||
_isSubmitting = false;
|
||||
_lastBoxNo = null;
|
||||
_editingBox = null;
|
||||
@@ -260,6 +265,9 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
_isDuplicateBoxNo = false;
|
||||
_editingBox = null;
|
||||
_statusOverrideText = null;
|
||||
if (_mode == BoxingMode.singleCode) {
|
||||
_paichanSwitchNotice = null;
|
||||
}
|
||||
|
||||
// 根据模式自动填充
|
||||
_applyAutoFill();
|
||||
@@ -293,9 +301,27 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
}
|
||||
});
|
||||
case BoxingMode.multiCode:
|
||||
if (_boxNoController.text.trim().isEmpty) {
|
||||
_boxNoController.text = (_maxBoxNo + 1).toString();
|
||||
final decision = decideMultiCodePaichanContext(
|
||||
lastPaichanNo: _lastScannedPaichanNo,
|
||||
currentPaichanNo: _paichanNo,
|
||||
currentBoxNoText: _boxNoController.text,
|
||||
maxBoxNo: _maxBoxNo,
|
||||
);
|
||||
if (decision.isSwitched) {
|
||||
_manyToOnePackedItems.clear();
|
||||
_editingPackedItemId = null;
|
||||
_editingPackedQuantity = '';
|
||||
_deletingPackedItemId = null;
|
||||
_paichanSwitchNotice = '排产号已切换:${_paichanNo ?? "--"}';
|
||||
_feedbackService.trigger(FeedbackEvent.paichanSwitch);
|
||||
} else {
|
||||
_paichanSwitchNotice = null;
|
||||
}
|
||||
final boxNoToApply = decision.boxNoToApply;
|
||||
if (boxNoToApply != null) {
|
||||
_boxNoController.text = boxNoToApply.toString();
|
||||
}
|
||||
_lastScannedPaichanNo = _paichanNo;
|
||||
_quantityController.text = (_erpQuantity ?? 0).toString();
|
||||
_quantityController.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
@@ -508,6 +534,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
}
|
||||
|
||||
case BoxingMode.multiCode:
|
||||
_lastScannedPaichanNo = _paichanNo;
|
||||
setState(() {
|
||||
if (boxItemId != null) {
|
||||
_manyToOnePackedItems.add(
|
||||
@@ -643,6 +670,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
void _finishManyToOneBox() {
|
||||
final currentBoxNo = int.tryParse(_boxNoController.text.trim());
|
||||
setState(() {
|
||||
_manyToOnePackedItems.clear();
|
||||
_editingPackedItemId = null;
|
||||
_editingPackedQuantity = '';
|
||||
_deletingPackedItemId = null;
|
||||
@@ -913,6 +941,11 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
_statusText = '超出可装数量上限';
|
||||
return;
|
||||
}
|
||||
if (_paichanSwitchNotice != null) {
|
||||
_statusDot = StatusDotColor.blue;
|
||||
_statusText = '排产号已切换,箱号已重置';
|
||||
return;
|
||||
}
|
||||
switch (_phase) {
|
||||
case _Phase.waiting:
|
||||
_statusDot = StatusDotColor.blue;
|
||||
@@ -978,6 +1011,16 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
body: Column(
|
||||
children: [
|
||||
// 重复箱号警告条(保留,因为这是输入区关联的即时反馈)
|
||||
if (_paichanSwitchNotice != null)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
color: Colors.blue.shade50,
|
||||
child: Text(
|
||||
_paichanSwitchNotice!,
|
||||
style: TextStyle(color: Colors.blue.shade900, fontSize: 13),
|
||||
),
|
||||
),
|
||||
if (_isDuplicateBoxNo && _phase == _Phase.scanned)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
|
||||
38
lib/services/boxing_context.dart
Normal file
38
lib/services/boxing_context.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
enum PaichanContextChange { firstScan, samePaichan, switchedPaichan }
|
||||
|
||||
class PaichanContextDecision {
|
||||
final PaichanContextChange change;
|
||||
final int? boxNoToApply;
|
||||
|
||||
const PaichanContextDecision({required this.change, this.boxNoToApply});
|
||||
|
||||
bool get isSwitched => change == PaichanContextChange.switchedPaichan;
|
||||
}
|
||||
|
||||
PaichanContextDecision decideMultiCodePaichanContext({
|
||||
required String? lastPaichanNo,
|
||||
required String? currentPaichanNo,
|
||||
required String currentBoxNoText,
|
||||
required int maxBoxNo,
|
||||
}) {
|
||||
final recommendedBoxNo = maxBoxNo + 1;
|
||||
if (currentPaichanNo == null || currentPaichanNo.isEmpty) {
|
||||
return const PaichanContextDecision(change: PaichanContextChange.firstScan);
|
||||
}
|
||||
if (lastPaichanNo == null || lastPaichanNo.isEmpty) {
|
||||
return PaichanContextDecision(
|
||||
change: PaichanContextChange.firstScan,
|
||||
boxNoToApply: currentBoxNoText.trim().isEmpty ? recommendedBoxNo : null,
|
||||
);
|
||||
}
|
||||
if (lastPaichanNo == currentPaichanNo) {
|
||||
return PaichanContextDecision(
|
||||
change: PaichanContextChange.samePaichan,
|
||||
boxNoToApply: currentBoxNoText.trim().isEmpty ? recommendedBoxNo : null,
|
||||
);
|
||||
}
|
||||
return PaichanContextDecision(
|
||||
change: PaichanContextChange.switchedPaichan,
|
||||
boxNoToApply: recommendedBoxNo,
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ enum FeedbackEvent {
|
||||
networkError, // Network error → failure sound + short vibrate
|
||||
duplicateBoxNo, // Duplicate box no → failure sound + short vibrate
|
||||
modeSwitch, // Mode toggle → beep only
|
||||
paichanSwitch, // Paichan changed → beep + short vibrate
|
||||
}
|
||||
|
||||
/// Centralized feedback dispatcher.
|
||||
@@ -56,6 +57,10 @@ class FeedbackService {
|
||||
|
||||
case FeedbackEvent.modeSwitch:
|
||||
_soundService.playBeep();
|
||||
|
||||
case FeedbackEvent.paichanSwitch:
|
||||
_soundService.playBeep();
|
||||
_vibrateShort();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
60
test/services/boxing_context_test.dart
Normal file
60
test/services/boxing_context_test.dart
Normal file
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pad_scanner/services/boxing_context.dart';
|
||||
|
||||
void main() {
|
||||
group('decideMultiCodePaichanContext', () {
|
||||
test('first scan records context and fills recommended box if empty', () {
|
||||
final decision = decideMultiCodePaichanContext(
|
||||
lastPaichanNo: null,
|
||||
currentPaichanNo: 'W00009',
|
||||
currentBoxNoText: '',
|
||||
maxBoxNo: 3,
|
||||
);
|
||||
|
||||
expect(decision.change, PaichanContextChange.firstScan);
|
||||
expect(decision.boxNoToApply, 4);
|
||||
expect(decision.isSwitched, isFalse);
|
||||
});
|
||||
|
||||
test('same paichan keeps current box number', () {
|
||||
final decision = decideMultiCodePaichanContext(
|
||||
lastPaichanNo: 'W00009',
|
||||
currentPaichanNo: 'W00009',
|
||||
currentBoxNoText: '8',
|
||||
maxBoxNo: 3,
|
||||
);
|
||||
|
||||
expect(decision.change, PaichanContextChange.samePaichan);
|
||||
expect(decision.boxNoToApply, isNull);
|
||||
expect(decision.isSwitched, isFalse);
|
||||
});
|
||||
|
||||
test('switched paichan resets to new paichan max box plus one', () {
|
||||
final decision = decideMultiCodePaichanContext(
|
||||
lastPaichanNo: 'W00009',
|
||||
currentPaichanNo: 'W00010',
|
||||
currentBoxNoText: '8',
|
||||
maxBoxNo: 1,
|
||||
);
|
||||
|
||||
expect(decision.change, PaichanContextChange.switchedPaichan);
|
||||
expect(decision.boxNoToApply, 2);
|
||||
expect(decision.isSwitched, isTrue);
|
||||
});
|
||||
|
||||
test(
|
||||
'same paichan defensively fills recommended box when box is empty',
|
||||
() {
|
||||
final decision = decideMultiCodePaichanContext(
|
||||
lastPaichanNo: 'W00009',
|
||||
currentPaichanNo: 'W00009',
|
||||
currentBoxNoText: '',
|
||||
maxBoxNo: 4,
|
||||
);
|
||||
|
||||
expect(decision.change, PaichanContextChange.samePaichan);
|
||||
expect(decision.boxNoToApply, 5);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user