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/scanner_service.dart';
|
||||||
import 'package:pad_scanner/services/code_parser.dart';
|
import 'package:pad_scanner/services/code_parser.dart';
|
||||||
import 'package:pad_scanner/services/api_service.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/services/feedback_service.dart';
|
||||||
import 'package:pad_scanner/pages/boxing_detail_page.dart';
|
import 'package:pad_scanner/pages/boxing_detail_page.dart';
|
||||||
import 'package:pad_scanner/widgets/status_bar.dart';
|
import 'package:pad_scanner/widgets/status_bar.dart';
|
||||||
@@ -101,6 +102,8 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
int? _editingPackedItemId;
|
int? _editingPackedItemId;
|
||||||
String _editingPackedQuantity = '';
|
String _editingPackedQuantity = '';
|
||||||
int? _deletingPackedItemId;
|
int? _deletingPackedItemId;
|
||||||
|
String? _lastScannedPaichanNo;
|
||||||
|
String? _paichanSwitchNotice;
|
||||||
|
|
||||||
// 提交中
|
// 提交中
|
||||||
bool _isSubmitting = false;
|
bool _isSubmitting = false;
|
||||||
@@ -184,6 +187,8 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
_editingPackedItemId = null;
|
_editingPackedItemId = null;
|
||||||
_editingPackedQuantity = '';
|
_editingPackedQuantity = '';
|
||||||
_deletingPackedItemId = null;
|
_deletingPackedItemId = null;
|
||||||
|
_lastScannedPaichanNo = null;
|
||||||
|
_paichanSwitchNotice = null;
|
||||||
_isSubmitting = false;
|
_isSubmitting = false;
|
||||||
_lastBoxNo = null;
|
_lastBoxNo = null;
|
||||||
_editingBox = null;
|
_editingBox = null;
|
||||||
@@ -260,6 +265,9 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
_isDuplicateBoxNo = false;
|
_isDuplicateBoxNo = false;
|
||||||
_editingBox = null;
|
_editingBox = null;
|
||||||
_statusOverrideText = null;
|
_statusOverrideText = null;
|
||||||
|
if (_mode == BoxingMode.singleCode) {
|
||||||
|
_paichanSwitchNotice = null;
|
||||||
|
}
|
||||||
|
|
||||||
// 根据模式自动填充
|
// 根据模式自动填充
|
||||||
_applyAutoFill();
|
_applyAutoFill();
|
||||||
@@ -293,9 +301,27 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
case BoxingMode.multiCode:
|
case BoxingMode.multiCode:
|
||||||
if (_boxNoController.text.trim().isEmpty) {
|
final decision = decideMultiCodePaichanContext(
|
||||||
_boxNoController.text = (_maxBoxNo + 1).toString();
|
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.text = (_erpQuantity ?? 0).toString();
|
||||||
_quantityController.selection = TextSelection(
|
_quantityController.selection = TextSelection(
|
||||||
baseOffset: 0,
|
baseOffset: 0,
|
||||||
@@ -508,6 +534,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case BoxingMode.multiCode:
|
case BoxingMode.multiCode:
|
||||||
|
_lastScannedPaichanNo = _paichanNo;
|
||||||
setState(() {
|
setState(() {
|
||||||
if (boxItemId != null) {
|
if (boxItemId != null) {
|
||||||
_manyToOnePackedItems.add(
|
_manyToOnePackedItems.add(
|
||||||
@@ -643,6 +670,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
void _finishManyToOneBox() {
|
void _finishManyToOneBox() {
|
||||||
final currentBoxNo = int.tryParse(_boxNoController.text.trim());
|
final currentBoxNo = int.tryParse(_boxNoController.text.trim());
|
||||||
setState(() {
|
setState(() {
|
||||||
|
_manyToOnePackedItems.clear();
|
||||||
_editingPackedItemId = null;
|
_editingPackedItemId = null;
|
||||||
_editingPackedQuantity = '';
|
_editingPackedQuantity = '';
|
||||||
_deletingPackedItemId = null;
|
_deletingPackedItemId = null;
|
||||||
@@ -913,6 +941,11 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
_statusText = '超出可装数量上限';
|
_statusText = '超出可装数量上限';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (_paichanSwitchNotice != null) {
|
||||||
|
_statusDot = StatusDotColor.blue;
|
||||||
|
_statusText = '排产号已切换,箱号已重置';
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (_phase) {
|
switch (_phase) {
|
||||||
case _Phase.waiting:
|
case _Phase.waiting:
|
||||||
_statusDot = StatusDotColor.blue;
|
_statusDot = StatusDotColor.blue;
|
||||||
@@ -978,6 +1011,16 @@ class _BoxingPageState extends State<BoxingPage> {
|
|||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
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)
|
if (_isDuplicateBoxNo && _phase == _Phase.scanned)
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
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
|
networkError, // Network error → failure sound + short vibrate
|
||||||
duplicateBoxNo, // Duplicate box no → failure sound + short vibrate
|
duplicateBoxNo, // Duplicate box no → failure sound + short vibrate
|
||||||
modeSwitch, // Mode toggle → beep only
|
modeSwitch, // Mode toggle → beep only
|
||||||
|
paichanSwitch, // Paichan changed → beep + short vibrate
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Centralized feedback dispatcher.
|
/// Centralized feedback dispatcher.
|
||||||
@@ -56,6 +57,10 @@ class FeedbackService {
|
|||||||
|
|
||||||
case FeedbackEvent.modeSwitch:
|
case FeedbackEvent.modeSwitch:
|
||||||
_soundService.playBeep();
|
_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