feat(android): add attachment boxing FJ- scan and submit
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -112,6 +112,85 @@ class BoxingApiActions {
|
||||
);
|
||||
}
|
||||
|
||||
Future<BoxingActionResult<AttachmentBoxResult>> saveAttachmentBox({
|
||||
required String zongpaiNo,
|
||||
required int categoryId,
|
||||
required int boxNo,
|
||||
required int quantity,
|
||||
}) async {
|
||||
final baseUrl = await _requireBaseUrl();
|
||||
if (baseUrl == null) return _missingApiUrl();
|
||||
final result = await _apiService.saveAttachmentBox(
|
||||
baseUrl: baseUrl,
|
||||
zongpaiNo: zongpaiNo,
|
||||
categoryId: categoryId,
|
||||
boxNo: boxNo,
|
||||
quantity: quantity,
|
||||
);
|
||||
if (result.success) return BoxingActionResult.ok(result);
|
||||
if (result.isDuplicate) {
|
||||
return BoxingActionResult.error(
|
||||
kind: BoxingActionErrorKind.duplicate,
|
||||
message: result.errorMessage ?? '提交失败',
|
||||
boxNo: result.boxNo,
|
||||
);
|
||||
}
|
||||
return BoxingActionResult.error(
|
||||
kind: _kindForMessage(result.errorMessage),
|
||||
message: result.errorMessage ?? '提交失败',
|
||||
);
|
||||
}
|
||||
|
||||
Future<BoxingActionResult<AttachmentBoxResult>> updateAttachmentBox({
|
||||
required int boxItemId,
|
||||
required int boxNo,
|
||||
required int quantity,
|
||||
}) async {
|
||||
final baseUrl = await _requireBaseUrl();
|
||||
if (baseUrl == null) return _missingApiUrl();
|
||||
final result = await _apiService.updateAttachmentBox(
|
||||
baseUrl: baseUrl,
|
||||
boxItemId: boxItemId,
|
||||
boxNo: boxNo,
|
||||
quantity: quantity,
|
||||
);
|
||||
if (result.success) return BoxingActionResult.ok(result);
|
||||
return BoxingActionResult.error(
|
||||
kind: _kindForMessage(result.errorMessage),
|
||||
message: result.errorMessage ?? '修改失败',
|
||||
);
|
||||
}
|
||||
|
||||
Future<BoxingActionResult<AttachmentBoxDeleteResult>> deleteAttachmentBox({
|
||||
required int boxItemId,
|
||||
}) async {
|
||||
final baseUrl = await _requireBaseUrl();
|
||||
if (baseUrl == null) return _missingApiUrl();
|
||||
final result = await _apiService.deleteAttachmentBox(
|
||||
baseUrl: baseUrl,
|
||||
boxItemId: boxItemId,
|
||||
);
|
||||
if (result.success) return BoxingActionResult.ok(result);
|
||||
return BoxingActionResult.error(
|
||||
kind: _kindForMessage(result.errorMessage),
|
||||
message: result.errorMessage ?? '删除失败',
|
||||
);
|
||||
}
|
||||
|
||||
Future<BoxingActionResult<AttachmentCategoryResult>>
|
||||
fetchAttachmentCategories() async {
|
||||
final baseUrl = await _requireBaseUrl();
|
||||
if (baseUrl == null) return _missingApiUrl();
|
||||
final result = await _apiService.fetchAttachmentCategories(
|
||||
baseUrl: baseUrl,
|
||||
);
|
||||
if (result.success) return BoxingActionResult.ok(result);
|
||||
return BoxingActionResult.error(
|
||||
kind: _kindForMessage(result.errorMessage),
|
||||
message: result.errorMessage ?? '加载失败',
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> _requireBaseUrl() async {
|
||||
final baseUrl = await _loadApiUrl() ?? '';
|
||||
if (baseUrl.isEmpty) return null;
|
||||
|
||||
86
lib/pages/boxing/dialogs/attachment_category_picker.dart
Normal file
86
lib/pages/boxing/dialogs/attachment_category_picker.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pad_scanner/services/api_service.dart';
|
||||
|
||||
/// Show a single-category picker for attachment boxing.
|
||||
/// Returns the selected category, or null.
|
||||
Future<AttachmentCategoryData?> showBoxingAttachmentCategoryPicker({
|
||||
required BuildContext context,
|
||||
required List<AttachmentCategoryData> categories,
|
||||
}) {
|
||||
return showDialog<AttachmentCategoryData>(
|
||||
context: context,
|
||||
builder: (ctx) => _BoxingCategoryPickerDialog(categories: categories),
|
||||
);
|
||||
}
|
||||
|
||||
class _BoxingCategoryPickerDialog extends StatefulWidget {
|
||||
final List<AttachmentCategoryData> categories;
|
||||
const _BoxingCategoryPickerDialog({required this.categories});
|
||||
|
||||
@override
|
||||
State<_BoxingCategoryPickerDialog> createState() =>
|
||||
_BoxingCategoryPickerDialogState();
|
||||
}
|
||||
|
||||
class _BoxingCategoryPickerDialogState
|
||||
extends State<_BoxingCategoryPickerDialog> {
|
||||
int? _selectedId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('选择附件类型'),
|
||||
content: SizedBox(
|
||||
width: 400,
|
||||
child: RadioGroup<int>(
|
||||
groupValue: _selectedId,
|
||||
onChanged: (v) => setState(() => _selectedId = v),
|
||||
child: widget.categories.length <= 6
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.categories
|
||||
.map(
|
||||
(cat) => RadioListTile<int>(
|
||||
dense: true,
|
||||
title: Text(cat.name),
|
||||
value: cat.id,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.categories
|
||||
.map(
|
||||
(cat) => RadioListTile<int>(
|
||||
dense: true,
|
||||
title: Text(cat.name),
|
||||
value: cat.id,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _selectedId != null
|
||||
? () {
|
||||
final cat = widget.categories.firstWhere(
|
||||
(c) => c.id == _selectedId,
|
||||
);
|
||||
Navigator.pop(context, cat);
|
||||
}
|
||||
: null,
|
||||
child: const Text('确认'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,11 @@ extension _BoxingScanPart on _BoxingPageState {
|
||||
|
||||
final parsed = CodeParser.parse(result.barcode);
|
||||
|
||||
if (parsed.type == CodeType.attachmentCode) {
|
||||
this._handleAttachmentBoxScan(parsed.value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.type != CodeType.zongpaiNo) {
|
||||
_feedbackService.trigger(FeedbackEvent.scanInvalid);
|
||||
this._showStatusOverride(
|
||||
@@ -92,6 +97,119 @@ extension _BoxingScanPart on _BoxingPageState {
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _handleAttachmentBoxScan(String zongpai) async {
|
||||
if (_isAutoProcessing || _crossPaichaPending) return;
|
||||
|
||||
// 1. Fetch categories
|
||||
final catAction = await _apiActions.fetchAttachmentCategories();
|
||||
if (!mounted) return;
|
||||
if (!catAction.success) {
|
||||
_feedbackService.trigger(FeedbackEvent.scanInvalid);
|
||||
_showStatusOverride(
|
||||
catAction.errorMessage ?? '加载附件类型失败',
|
||||
StatusDotColor.red,
|
||||
const Duration(seconds: 2),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Pick category
|
||||
final picked = await showBoxingAttachmentCategoryPicker(
|
||||
context: context,
|
||||
categories: catAction.data!.categories,
|
||||
);
|
||||
if (picked == null || !mounted) return;
|
||||
|
||||
setState(() {
|
||||
_attachmentCategoryId = picked.id;
|
||||
_attachmentCategoryName = picked.name;
|
||||
});
|
||||
|
||||
// 3. Fetch box info (server-extended: includes attachment_summary)
|
||||
final action = await _apiActions.fetchBoxInfo(zongpai);
|
||||
if (!mounted) return;
|
||||
|
||||
if (!action.success) {
|
||||
_feedbackService.trigger(switch (action.errorKind) {
|
||||
BoxingActionErrorKind.network => FeedbackEvent.networkError,
|
||||
BoxingActionErrorKind.missingApiUrl => FeedbackEvent.submitFailure,
|
||||
_ => FeedbackEvent.scanInvalid,
|
||||
});
|
||||
_showStatusOverride(
|
||||
action.errorMessage ?? '查询失败',
|
||||
_dotForActionError(action.errorKind),
|
||||
const Duration(seconds: 2),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final result = action.data!;
|
||||
final totalQuantity = result.quantity ?? 0;
|
||||
|
||||
// 4. Derive attachment qty context from the box-info attachment_summary
|
||||
// or from a separate status call
|
||||
final baseUrl = await _baseUrl();
|
||||
int expectedQty = totalQuantity;
|
||||
int boxedQty = 0;
|
||||
|
||||
if (baseUrl != null) {
|
||||
final statusResult = await _apiService.fetchAttachmentStatus(
|
||||
baseUrl: baseUrl,
|
||||
zongpaiNo: zongpai,
|
||||
);
|
||||
if (statusResult.success) {
|
||||
for (final item in statusResult.items) {
|
||||
if (item.categoryId == _attachmentCategoryId) {
|
||||
expectedQty = item.expectedQty;
|
||||
boxedQty = item.boxedQty;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final remaining = expectedQty - boxedQty;
|
||||
final alreadyCompleted = remaining <= 0;
|
||||
|
||||
_feedbackService.trigger(
|
||||
alreadyCompleted
|
||||
? FeedbackEvent.alreadyCompleted
|
||||
: FeedbackEvent.scanValid,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_zongpaiNo = zongpai;
|
||||
_paichanNo = result.paichanNo;
|
||||
_workOrderNo = result.workOrderNo;
|
||||
_erpQuantity = result.quantity;
|
||||
_currentZongpaiBoxes = result.currentZongpaiBoxes;
|
||||
_existingBoxes = result.existingBoxes;
|
||||
_maxBoxNo = result.maxBoxNo;
|
||||
_attachmentExpectedQty = expectedQty;
|
||||
_attachmentBoxedQty = boxedQty;
|
||||
_phase = BoxingPhase.scanned;
|
||||
_isDuplicateBoxNo = false;
|
||||
_editingBox = null;
|
||||
_completedJumpBoxNo = null;
|
||||
_statusOverrideText = null;
|
||||
});
|
||||
|
||||
// Auto-fill: box_no = maxBoxNo+1, quantity = remaining
|
||||
if (alreadyCompleted) {
|
||||
_boxNoController.clear();
|
||||
_quantityController.clear();
|
||||
_statusOverrideText = '该类型附件已全部装箱完毕';
|
||||
_statusOverrideDot = StatusDotColor.red;
|
||||
return;
|
||||
}
|
||||
_boxNoController.text = (_maxBoxNo + 1).toString();
|
||||
_quantityController.text = remaining.toString();
|
||||
_quantityController.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: _quantityController.text.length,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _processAutoScanCodes(List<String> codes) async {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
|
||||
@@ -6,6 +6,53 @@ extension _BoxingSubmitPart on _BoxingPageState {
|
||||
Future<bool> _submit() async {
|
||||
if (!_canSubmit) return false;
|
||||
|
||||
final attachCatId = _attachmentCategoryId;
|
||||
if (attachCatId != null) {
|
||||
// Attachment boxing submit
|
||||
setState(() => _isSubmitting = true);
|
||||
final action = await _apiActions.saveAttachmentBox(
|
||||
zongpaiNo: _zongpaiNo!,
|
||||
categoryId: attachCatId,
|
||||
boxNo: int.parse(_boxNoController.text),
|
||||
quantity: int.parse(_quantityController.text),
|
||||
);
|
||||
if (!mounted) return false;
|
||||
setState(() => _isSubmitting = false);
|
||||
|
||||
if (action.success) {
|
||||
_feedbackService.trigger(FeedbackEvent.submitSuccess);
|
||||
// Reset to waiting for next scan
|
||||
setState(() {
|
||||
_zongpaiNo = null;
|
||||
_phase = BoxingPhase.waiting;
|
||||
_attachmentCategoryId = null;
|
||||
_attachmentCategoryName = null;
|
||||
});
|
||||
_showStatusOverride(
|
||||
'附件装箱成功',
|
||||
StatusDotColor.green,
|
||||
const Duration(milliseconds: 1500),
|
||||
);
|
||||
_requestFocus();
|
||||
return true;
|
||||
} else if (action.errorKind == BoxingActionErrorKind.duplicate) {
|
||||
_feedbackService.trigger(FeedbackEvent.duplicateBoxNo);
|
||||
_showStatusOverride(
|
||||
action.errorMessage ?? '该箱号已存在同类型附件',
|
||||
StatusDotColor.red,
|
||||
const Duration(seconds: 2),
|
||||
);
|
||||
} else {
|
||||
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||||
_showStatusOverride(
|
||||
action.errorMessage ?? '提交失败',
|
||||
StatusDotColor.red,
|
||||
const Duration(seconds: 2),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
final boxNo = int.parse(_boxNoController.text);
|
||||
final quantity = int.parse(_quantityController.text);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user