feat: link transit registration to boxing
This commit is contained in:
@@ -7,6 +7,8 @@ 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/feedback_service.dart';
|
||||
import 'package:pad_scanner/services/registration_linking.dart';
|
||||
import 'package:pad_scanner/pages/boxing_page.dart';
|
||||
import 'package:pad_scanner/widgets/status_bar.dart';
|
||||
|
||||
class RegistrationPage extends StatefulWidget {
|
||||
@@ -44,7 +46,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scanSubscription = _scannerService.scanResults.listen(_onScan);
|
||||
_startScanListening();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_focusNode.requestFocus();
|
||||
});
|
||||
@@ -58,6 +60,27 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
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 && event.logicalKey == LogicalKeyboardKey.enter) {
|
||||
_submit();
|
||||
@@ -78,7 +101,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
_locationType = parsed.type;
|
||||
_clearStatusOverride();
|
||||
});
|
||||
}
|
||||
} else {}
|
||||
case CodeType.invalid:
|
||||
_feedbackService.trigger(FeedbackEvent.scanInvalid);
|
||||
_showStatusOverride(
|
||||
@@ -163,6 +186,8 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
bool get _canSubmit =>
|
||||
_zongpaiNos.isNotEmpty && _locationCode != null && !_isSubmitting;
|
||||
|
||||
bool get _isTransitTarget => isTransitTarget(_locationType, _locationCode);
|
||||
|
||||
Future<String?> _baseUrl() async {
|
||||
final configService = AppConfigService();
|
||||
final baseUrl = await configService.getString('api_url') ?? '';
|
||||
@@ -239,12 +264,49 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
setState(() => _isSubmitting = true);
|
||||
|
||||
if (_isLocked && _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;
|
||||
}
|
||||
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,
|
||||
@@ -258,6 +320,16 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
|
||||
if (result.success) {
|
||||
_feedbackService.trigger(FeedbackEvent.submitSuccess);
|
||||
if (_isTransitTarget) {
|
||||
setState(() => _isSubmitting = false);
|
||||
await _navigateToBoxing(
|
||||
mode: BoxingMode.singleCode,
|
||||
codes: [zongpaiNo],
|
||||
codesToClear: [zongpaiNo],
|
||||
clearLocation: !_isLocked,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_zongpaiNos.remove(zongpaiNo);
|
||||
if (!_isLocked) {
|
||||
@@ -302,6 +374,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
int offShelfCount = 0;
|
||||
final failed = <String>[];
|
||||
final toRemove = <String>[];
|
||||
final transitCodes = <String>[];
|
||||
|
||||
for (final zongpaiNo in List.of(_zongpaiNos)) {
|
||||
final result = await _apiService.registerLocation(
|
||||
@@ -315,9 +388,26 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
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;
|
||||
@@ -327,6 +417,20 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
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;
|
||||
@@ -349,6 +453,19 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
_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
|
||||
@@ -370,6 +487,49 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showDuplicateDialog(String zongpaiNo, Map<String, dynamic>? info) {
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -498,9 +658,11 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
child: ElevatedButton(
|
||||
onPressed: _canSubmit ? _submit : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _canSubmit
|
||||
? colorScheme.primary
|
||||
: Colors.grey.shade300,
|
||||
backgroundColor: registrationSubmitColor(
|
||||
canSubmit: _canSubmit,
|
||||
isTransitTarget: _isTransitTarget,
|
||||
primaryColor: colorScheme.primary,
|
||||
),
|
||||
foregroundColor: _canSubmit
|
||||
? Colors.white
|
||||
: Colors.grey.shade600,
|
||||
@@ -518,9 +680,11 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
_isLocked && _zongpaiNos.length > 1
|
||||
? '批量上架(${_zongpaiNos.length} 条)'
|
||||
: '确 认 上 架',
|
||||
registrationSubmitLabel(
|
||||
isTransitTarget: _isTransitTarget,
|
||||
isLocked: _isLocked,
|
||||
zongpaiCount: _zongpaiNos.length,
|
||||
),
|
||||
style: const TextStyle(fontSize: 17),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user