feat: handle off-shelf registration results
This commit is contained in:
@@ -49,8 +49,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onKeyEvent(KeyEvent event) {
|
void _onKeyEvent(KeyEvent event) {
|
||||||
if (event is KeyDownEvent &&
|
if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.enter) {
|
||||||
event.logicalKey == LogicalKeyboardKey.enter) {
|
|
||||||
_submit();
|
_submit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,11 +194,24 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
_locationType = null;
|
_locationType = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
final msg = _isLocked ? '货位已锁定,请扫描下一张执行卡' : '上架成功';
|
final msg = result.isOffShelfSuccess
|
||||||
_showStatusOverride(msg, StatusDotColor.green, const Duration(milliseconds: 1500));
|
? '下架成功'
|
||||||
|
: (_isLocked ? '货位已锁定,请扫描下一张执行卡' : '上架成功');
|
||||||
|
_showStatusOverride(
|
||||||
|
msg,
|
||||||
|
StatusDotColor.green,
|
||||||
|
const Duration(milliseconds: 1500),
|
||||||
|
);
|
||||||
} else if (result.isDuplicate) {
|
} else if (result.isDuplicate) {
|
||||||
_feedbackService.trigger(FeedbackEvent.duplicateError);
|
_feedbackService.trigger(FeedbackEvent.duplicateError);
|
||||||
_showDuplicateDialog(zongpaiNo, result.duplicateInfo);
|
_showDuplicateDialog(zongpaiNo, result.duplicateInfo);
|
||||||
|
} else if (result.isAlreadyOffShelf) {
|
||||||
|
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||||||
|
_showStatusOverride(
|
||||||
|
result.errorMessage ?? '该总排号已下架至转运区域,不可重新上架',
|
||||||
|
StatusDotColor.red,
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
final isNetwork = result.errorMessage == '网络异常,请检查网络连接';
|
final isNetwork = result.errorMessage == '网络异常,请检查网络连接';
|
||||||
_feedbackService.trigger(
|
_feedbackService.trigger(
|
||||||
@@ -215,6 +227,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
|
|
||||||
Future<void> _submitBatch(String baseUrl) async {
|
Future<void> _submitBatch(String baseUrl) async {
|
||||||
int successCount = 0;
|
int successCount = 0;
|
||||||
|
int offShelfCount = 0;
|
||||||
final failed = <String>[];
|
final failed = <String>[];
|
||||||
final toRemove = <String>[];
|
final toRemove = <String>[];
|
||||||
|
|
||||||
@@ -226,6 +239,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
);
|
);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
successCount++;
|
successCount++;
|
||||||
|
if (result.isOffShelfSuccess) {
|
||||||
|
offShelfCount++;
|
||||||
|
}
|
||||||
toRemove.add(zp);
|
toRemove.add(zp);
|
||||||
} else {
|
} else {
|
||||||
failed.add(zp);
|
failed.add(zp);
|
||||||
@@ -238,6 +254,19 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
_showDuplicateDialog(zp, result.duplicateInfo);
|
_showDuplicateDialog(zp, result.duplicateInfo);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (result.isAlreadyOffShelf) {
|
||||||
|
setState(() {
|
||||||
|
_zongpaiNos.removeWhere((e) => toRemove.contains(e));
|
||||||
|
_isSubmitting = false;
|
||||||
|
});
|
||||||
|
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||||||
|
_showStatusOverride(
|
||||||
|
result.errorMessage ?? '该总排号已下架至转运区域,不可重新上架',
|
||||||
|
StatusDotColor.red,
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,8 +279,11 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
|
|
||||||
if (failed.isEmpty) {
|
if (failed.isEmpty) {
|
||||||
_feedbackService.trigger(FeedbackEvent.submitSuccess);
|
_feedbackService.trigger(FeedbackEvent.submitSuccess);
|
||||||
|
final msg = offShelfCount == successCount
|
||||||
|
? '批量下架成功($successCount 条)'
|
||||||
|
: '批量上架成功($successCount 条)';
|
||||||
_showStatusOverride(
|
_showStatusOverride(
|
||||||
'批量上架成功($successCount 条)',
|
msg,
|
||||||
StatusDotColor.green,
|
StatusDotColor.green,
|
||||||
const Duration(milliseconds: 1500),
|
const Duration(milliseconds: 1500),
|
||||||
);
|
);
|
||||||
@@ -492,7 +524,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(14),
|
padding: const EdgeInsets.all(14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: Colors.grey.shade400),
|
border: Border.all(
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
@@ -509,7 +543,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return Dismissible(
|
return Dismissible(
|
||||||
key: ValueKey('${_zongpaiNos[index]}-$index'),
|
key: ValueKey(
|
||||||
|
'${_zongpaiNos[index]}-$index',
|
||||||
|
),
|
||||||
direction: DismissDirection.endToStart,
|
direction: DismissDirection.endToStart,
|
||||||
onDismissed: (_) => _removeZongpai(index),
|
onDismissed: (_) => _removeZongpai(index),
|
||||||
background: Container(
|
background: Container(
|
||||||
|
|||||||
@@ -5,23 +5,41 @@ import 'package:http/http.dart' as http;
|
|||||||
class RegistrationResult {
|
class RegistrationResult {
|
||||||
final bool success;
|
final bool success;
|
||||||
final bool isDuplicate;
|
final bool isDuplicate;
|
||||||
|
final bool isAlreadyOffShelf;
|
||||||
|
final bool isOffShelfSuccess;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
final Map<String, dynamic>? duplicateInfo;
|
final Map<String, dynamic>? duplicateInfo;
|
||||||
|
final Map<String, dynamic>? conflictInfo;
|
||||||
|
|
||||||
RegistrationResult({
|
RegistrationResult({
|
||||||
required this.success,
|
required this.success,
|
||||||
this.isDuplicate = false,
|
this.isDuplicate = false,
|
||||||
|
this.isAlreadyOffShelf = false,
|
||||||
|
this.isOffShelfSuccess = false,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
this.duplicateInfo,
|
this.duplicateInfo,
|
||||||
|
this.conflictInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory RegistrationResult.ok() => RegistrationResult(success: true);
|
factory RegistrationResult.ok() => RegistrationResult(success: true);
|
||||||
|
|
||||||
|
factory RegistrationResult.offShelfSuccess() =>
|
||||||
|
RegistrationResult(success: true, isOffShelfSuccess: true);
|
||||||
|
|
||||||
factory RegistrationResult.duplicate(Map<String, dynamic> info) =>
|
factory RegistrationResult.duplicate(Map<String, dynamic> info) =>
|
||||||
RegistrationResult(
|
RegistrationResult(
|
||||||
success: false,
|
success: false,
|
||||||
isDuplicate: true,
|
isDuplicate: true,
|
||||||
duplicateInfo: info,
|
duplicateInfo: info,
|
||||||
|
conflictInfo: info,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory RegistrationResult.alreadyOffShelf(Map<String, dynamic> info) =>
|
||||||
|
RegistrationResult(
|
||||||
|
success: false,
|
||||||
|
isAlreadyOffShelf: true,
|
||||||
|
errorMessage: info['message']?.toString() ?? '该总排号已下架至转运区域,不可重新上架',
|
||||||
|
conflictInfo: info,
|
||||||
);
|
);
|
||||||
|
|
||||||
factory RegistrationResult.error(String message) =>
|
factory RegistrationResult.error(String message) =>
|
||||||
@@ -228,6 +246,10 @@ class ApiService {
|
|||||||
|
|
||||||
switch (response.statusCode) {
|
switch (response.statusCode) {
|
||||||
case 200:
|
case 200:
|
||||||
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
if (body['previous_location'] != null) {
|
||||||
|
return RegistrationResult.offShelfSuccess();
|
||||||
|
}
|
||||||
return RegistrationResult.ok();
|
return RegistrationResult.ok();
|
||||||
case 400:
|
case 400:
|
||||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
@@ -235,7 +257,15 @@ class ApiService {
|
|||||||
return RegistrationResult.error(msg);
|
return RegistrationResult.error(msg);
|
||||||
case 409:
|
case 409:
|
||||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
return RegistrationResult.duplicate(body);
|
final errorCode = body['error_code']?.toString();
|
||||||
|
if (errorCode == 'DUPLICATE_LOCATION') {
|
||||||
|
return RegistrationResult.duplicate(body);
|
||||||
|
}
|
||||||
|
if (errorCode == 'ALREADY_OFF_SHELF') {
|
||||||
|
return RegistrationResult.alreadyOffShelf(body);
|
||||||
|
}
|
||||||
|
final msg = body['message']?.toString() ?? '提交失败';
|
||||||
|
return RegistrationResult.error(msg);
|
||||||
default:
|
default:
|
||||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
final msg =
|
final msg =
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ void main() {
|
|||||||
test('returns ok on 200', () async {
|
test('returns ok on 200', () async {
|
||||||
final mockClient = _MockClient((request) async {
|
final mockClient = _MockClient((request) async {
|
||||||
return http.Response(
|
return http.Response(
|
||||||
jsonEncode({'zongpai_no': '26B1', 'location_code': 'A01-02-03', 'created_at': '2026-05-11T10:00:00'}),
|
jsonEncode({
|
||||||
|
'zongpai_no': '26B1',
|
||||||
|
'location_code': 'A01-02-03',
|
||||||
|
'created_at': '2026-05-11T10:00:00',
|
||||||
|
}),
|
||||||
200,
|
200,
|
||||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
);
|
);
|
||||||
@@ -21,16 +25,69 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(result.success, isTrue);
|
expect(result.success, isTrue);
|
||||||
expect(result.isDuplicate, isFalse);
|
expect(result.isDuplicate, isFalse);
|
||||||
|
expect(result.isOffShelfSuccess, isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns duplicate on 409 with location_code and registered_at', () async {
|
test('returns off-shelf success on 200 with previous_location', () async {
|
||||||
final mockClient = _MockClient((request) async {
|
final mockClient = _MockClient((request) async {
|
||||||
return http.Response(
|
return http.Response(
|
||||||
jsonEncode({
|
jsonEncode({
|
||||||
'error_code': 'DUPLICATE_LOCATION',
|
'zongpai_no': '26B1',
|
||||||
'message': '该总排号已存在货位记录',
|
'location_code': 'TRANS-01',
|
||||||
'location_code': 'A01-02-03',
|
'previous_location': 'A01-02-03',
|
||||||
'registered_at': '2026-05-11T10:00:00',
|
'created_at': '2026-05-13T10:00:00',
|
||||||
|
}),
|
||||||
|
200,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.registerLocation(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
zongpaiNo: '26B1',
|
||||||
|
locationCode: 'TRANS-01',
|
||||||
|
);
|
||||||
|
expect(result.success, isTrue);
|
||||||
|
expect(result.isOffShelfSuccess, isTrue);
|
||||||
|
expect(result.isDuplicate, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'returns duplicate on 409 with location_code and registered_at',
|
||||||
|
() async {
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'error_code': 'DUPLICATE_LOCATION',
|
||||||
|
'message': '该总排号已存在货位记录',
|
||||||
|
'location_code': 'A01-02-03',
|
||||||
|
'registered_at': '2026-05-11T10:00:00',
|
||||||
|
}),
|
||||||
|
409,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.registerLocation(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
zongpaiNo: '26B1',
|
||||||
|
locationCode: 'A02-01-01',
|
||||||
|
);
|
||||||
|
expect(result.success, isFalse);
|
||||||
|
expect(result.isDuplicate, isTrue);
|
||||||
|
expect(result.duplicateInfo?['location_code'], 'A01-02-03');
|
||||||
|
expect(result.duplicateInfo?['registered_at'], '2026-05-11T10:00:00');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('returns already off shelf on 409 ALREADY_OFF_SHELF', () async {
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'error_code': 'ALREADY_OFF_SHELF',
|
||||||
|
'message': '该总排号已下架至转运区域,不可重新上架',
|
||||||
|
'location_code': 'TRANS-01',
|
||||||
|
'registered_at': '2026-05-13T10:00:00',
|
||||||
}),
|
}),
|
||||||
409,
|
409,
|
||||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
@@ -43,15 +100,19 @@ void main() {
|
|||||||
locationCode: 'A02-01-01',
|
locationCode: 'A02-01-01',
|
||||||
);
|
);
|
||||||
expect(result.success, isFalse);
|
expect(result.success, isFalse);
|
||||||
expect(result.isDuplicate, isTrue);
|
expect(result.isDuplicate, isFalse);
|
||||||
expect(result.duplicateInfo?['location_code'], 'A01-02-03');
|
expect(result.isAlreadyOffShelf, isTrue);
|
||||||
expect(result.duplicateInfo?['registered_at'], '2026-05-11T10:00:00');
|
expect(result.errorMessage, '该总排号已下架至转运区域,不可重新上架');
|
||||||
|
expect(result.conflictInfo?['location_code'], 'TRANS-01');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns error on 400 with message', () async {
|
test('returns error on 400 with message', () async {
|
||||||
final mockClient = _MockClient((request) async {
|
final mockClient = _MockClient((request) async {
|
||||||
return http.Response(
|
return http.Response(
|
||||||
jsonEncode({'error_code': 'INVALID_ZONGPAI', 'message': 'INVALID_ZONGPAI'}),
|
jsonEncode({
|
||||||
|
'error_code': 'INVALID_ZONGPAI',
|
||||||
|
'message': 'INVALID_ZONGPAI',
|
||||||
|
}),
|
||||||
400,
|
400,
|
||||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user