import 'dart:convert'; import 'package:http/http.dart' as http; /// Result of a registration API call. class RegistrationResult { final bool success; final bool isDuplicate; final bool isAlreadyOffShelf; final bool isOffShelfSuccess; final String? errorMessage; final Map? duplicateInfo; final Map? conflictInfo; RegistrationResult({ required this.success, this.isDuplicate = false, this.isAlreadyOffShelf = false, this.isOffShelfSuccess = false, this.errorMessage, this.duplicateInfo, this.conflictInfo, }); factory RegistrationResult.ok() => RegistrationResult(success: true); factory RegistrationResult.offShelfSuccess() => RegistrationResult(success: true, isOffShelfSuccess: true); factory RegistrationResult.duplicate(Map info) => RegistrationResult( success: false, isDuplicate: true, duplicateInfo: info, conflictInfo: info, ); factory RegistrationResult.alreadyOffShelf(Map info) => RegistrationResult( success: false, isAlreadyOffShelf: true, errorMessage: info['message']?.toString() ?? '该总排号已下架至转运区域,不可重新上架', conflictInfo: info, ); factory RegistrationResult.error(String message) => RegistrationResult(success: false, errorMessage: message); } class PaichaOverviewItem { final String zongpaiNo; final String? workOrderNo; final int quantity; final String? locationCode; final String status; PaichaOverviewItem({ required this.zongpaiNo, this.workOrderNo, required this.quantity, this.locationCode, required this.status, }); factory PaichaOverviewItem.fromJson(Map json) { return PaichaOverviewItem( zongpaiNo: json['zongpai_no'] as String, workOrderNo: json['work_order_no']?.toString(), quantity: json['quantity'] as int? ?? 0, locationCode: json['location_code']?.toString(), status: json['status']?.toString() ?? 'not_shelved', ); } } class PaichaOverviewResult { final bool success; final bool notFound; final String? errorMessage; final String? paichaNo; final int totalCount; final List items; PaichaOverviewResult({ required this.success, this.notFound = false, this.errorMessage, this.paichaNo, this.totalCount = 0, this.items = const [], }); factory PaichaOverviewResult.ok(Map json) { final items = (json['items'] as List?) ?.map( (item) => PaichaOverviewItem.fromJson(item as Map), ) .toList() ?? []; return PaichaOverviewResult( success: true, paichaNo: json['paicha_no'] as String?, totalCount: json['total_count'] as int? ?? items.length, items: items, ); } factory PaichaOverviewResult.notFoundResult() => PaichaOverviewResult( success: false, notFound: true, errorMessage: '暂无排产信息', ); factory PaichaOverviewResult.error(String message) => PaichaOverviewResult(success: false, errorMessage: message); } // === 装箱模块数据类 === /// 箱号内单个总排号明细 class BoxItemData { final int? boxItemId; final String zongpaiNo; final String? workOrderNo; final int quantity; final int? totalQuantity; BoxItemData({ this.boxItemId, required this.zongpaiNo, this.workOrderNo, required this.quantity, this.totalQuantity, }); factory BoxItemData.fromJson(Map json) { return BoxItemData( boxItemId: json['box_item_id'] as int?, zongpaiNo: json['zongpai_no'] as String, workOrderNo: json['work_order_no']?.toString(), quantity: json['quantity'] as int, totalQuantity: json['total_quantity'] as int?, ); } } /// 箱号明细 class BoxDetailData { final int boxNo; final List items; BoxDetailData({required this.boxNo, required this.items}); factory BoxDetailData.fromJson(Map json) { final items = (json['items'] as List?) ?.map((i) => BoxItemData.fromJson(i as Map)) .toList() ?? []; return BoxDetailData(boxNo: json['box_no'] as int, items: items); } } /// 当前总排号已分配的装箱明细 class CurrentZongpaiBoxData { final int boxItemId; final int boxNo; final int quantity; CurrentZongpaiBoxData({ required this.boxItemId, required this.boxNo, required this.quantity, }); factory CurrentZongpaiBoxData.fromJson(Map json) { return CurrentZongpaiBoxData( boxItemId: json['box_item_id'] as int, boxNo: json['box_no'] as int, quantity: json['quantity'] as int, ); } } /// 装箱信息查询结果 class BoxInfoResult { final bool success; final String? errorMessage; final String? zongpaiNo; final String? paichanNo; final String? workOrderNo; final int? quantity; final List currentZongpaiBoxes; final List existingBoxes; final int maxBoxNo; final int suggestedBoxNo; final Map? attachmentSummary; BoxInfoResult({ required this.success, this.errorMessage, this.zongpaiNo, this.paichanNo, this.workOrderNo, this.quantity, this.currentZongpaiBoxes = const [], this.existingBoxes = const [], this.maxBoxNo = 0, this.suggestedBoxNo = 1, this.attachmentSummary, }); factory BoxInfoResult.ok(Map json) { final boxes = (json['existing_boxes'] as List?) ?.map((b) => BoxDetailData.fromJson(b as Map)) .toList() ?? []; final currentBoxes = (json['current_zongpai_boxes'] as List?) ?.map( (b) => CurrentZongpaiBoxData.fromJson(b as Map), ) .toList() ?? []; return BoxInfoResult( success: true, zongpaiNo: json['zongpai_no'] as String?, paichanNo: json['paichan_no'] as String?, workOrderNo: json['work_order_no']?.toString(), quantity: json['quantity'] as int?, currentZongpaiBoxes: currentBoxes, existingBoxes: boxes, maxBoxNo: json['max_box_no'] as int? ?? 0, suggestedBoxNo: json['suggested_box_no'] as int? ?? 1, attachmentSummary: json['attachment_summary'] as Map?, ); } factory BoxInfoResult.error(String message) { return BoxInfoResult(success: false, errorMessage: message); } } /// 装箱保存结果 class BoxSaveResult { final bool success; final bool isDuplicate; final String? errorMessage; final String? errorCode; final int? boxItemId; final String? paichanNo; final String? zongpaiNo; final int? boxNo; final int? quantity; BoxSaveResult({ required this.success, this.isDuplicate = false, this.errorMessage, this.errorCode, this.boxItemId, this.paichanNo, this.zongpaiNo, this.boxNo, this.quantity, }); factory BoxSaveResult.ok(Map json) { return BoxSaveResult( success: true, boxItemId: json['box_item_id'] as int?, paichanNo: json['paichan_no'] as String?, zongpaiNo: json['zongpai_no'] as String?, boxNo: json['box_no'] as int?, quantity: json['quantity'] as int?, ); } factory BoxSaveResult.duplicate(Map json) { return BoxSaveResult( success: false, isDuplicate: true, errorCode: json['error_code']?.toString(), errorMessage: json['message']?.toString(), paichanNo: json['paichan_no'] as String?, zongpaiNo: json['zongpai_no'] as String?, boxNo: json['box_no'] as int?, ); } factory BoxSaveResult.error(String message) { return BoxSaveResult(success: false, errorMessage: message); } } class BoxDeleteResult { final bool success; final String? errorMessage; BoxDeleteResult({required this.success, this.errorMessage}); } // === 附件模块数据类 === class AttachmentCategoryData { final int id; final String name; final int sortOrder; AttachmentCategoryData({ required this.id, required this.name, required this.sortOrder, }); factory AttachmentCategoryData.fromJson(Map json) { return AttachmentCategoryData( id: json['id'] as int, name: json['name'] as String, sortOrder: json['sort_order'] as int, ); } } class AttachmentCategoryResult { final bool success; final String? errorMessage; final List categories; AttachmentCategoryResult({ required this.success, this.errorMessage, this.categories = const [], }); } class AttachmentStatusItemData { final int categoryId; final String name; final int expectedQty; final int boxedQty; final String? locationCode; final bool complete; AttachmentStatusItemData({ required this.categoryId, required this.name, required this.expectedQty, required this.boxedQty, this.locationCode, required this.complete, }); factory AttachmentStatusItemData.fromJson(Map json) { return AttachmentStatusItemData( categoryId: json['category_id'] as int, name: json['name'] as String, expectedQty: json['expected_qty'] as int, boxedQty: json['boxed_qty'] as int, locationCode: json['location_code']?.toString(), complete: json['complete'] as bool, ); } } class AttachmentStatusResult { final bool success; final String? errorMessage; final String determination; final bool allComplete; final List items; AttachmentStatusResult({ required this.success, this.errorMessage, this.determination = 'undetermined', this.allComplete = false, this.items = const [], }); } class AttachmentConfigItemData { final int categoryId; final int expectedQty; const AttachmentConfigItemData({ required this.categoryId, required this.expectedQty, }); Map toJson() => { 'category_id': categoryId, 'expected_qty': expectedQty, }; } class AttachmentConfigResult { final bool success; final String? errorMessage; final String? errorCode; final String determination; final bool allComplete; final List items; AttachmentConfigResult({ required this.success, this.errorMessage, this.errorCode, this.determination = 'undetermined', this.allComplete = false, this.items = const [], }); } class AttachmentLocationResult { final bool success; final bool isDuplicate; final bool isAlreadyOffShelf; final String? errorMessage; final String? locationCode; final String? previousLocation; final Map? conflictInfo; AttachmentLocationResult({ required this.success, this.isDuplicate = false, this.isAlreadyOffShelf = false, this.errorMessage, this.locationCode, this.previousLocation, this.conflictInfo, }); } class AttachmentBoxResult { final bool success; final bool isDuplicate; final String? errorMessage; final int? boxItemId; final String? paichanNo; final int? boxNo; final String? zongpaiNo; final int? categoryId; final int? quantity; AttachmentBoxResult({ required this.success, this.isDuplicate = false, this.errorMessage, this.boxItemId, this.paichanNo, this.boxNo, this.zongpaiNo, this.categoryId, this.quantity, }); } class AttachmentBoxDeleteResult { final bool success; final String? errorMessage; AttachmentBoxDeleteResult({required this.success, this.errorMessage}); } class ApiService { final http.Client _client; final Duration timeout; ApiService({http.Client? client, this.timeout = const Duration(seconds: 5)}) : _client = client ?? http.Client(); /// Submit a shelf registration (上架登记). Future registerLocation({ required String baseUrl, required String zongpaiNo, required String locationCode, }) async { final uri = Uri.parse('$baseUrl/CargoTrace/location'); try { final response = await _client .post( uri, headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'zongpai_no': zongpaiNo, 'location_code': locationCode, }), ) .timeout(timeout); switch (response.statusCode) { case 200: final body = jsonDecode(response.body) as Map; if (body['previous_location'] != null) { return RegistrationResult.offShelfSuccess(); } return RegistrationResult.ok(); case 400: final body = jsonDecode(response.body) as Map; final msg = body['message']?.toString() ?? '请求参数错误'; return RegistrationResult.error(msg); case 409: final body = jsonDecode(response.body) as Map; 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: final body = jsonDecode(response.body) as Map; final msg = body['message'] ?? body['error'] ?? 'Unknown error (${response.statusCode})'; return RegistrationResult.error(msg.toString()); } } catch (e) { return RegistrationResult.error('网络异常,请检查网络连接'); } } Future fetchPaichaOverview({ required String baseUrl, required String zongpaiNo, }) async { final uri = Uri.parse( '$baseUrl/CargoTrace/location/paicha-overview', ).replace(queryParameters: {'zongpai_no': zongpaiNo}); try { final response = await _client .get(uri, headers: {'Content-Type': 'application/json'}) .timeout(timeout); switch (response.statusCode) { case 200: final body = jsonDecode(response.body) as Map; return PaichaOverviewResult.ok(body); case 400: return PaichaOverviewResult.error('无效的总排号格式'); case 404: return PaichaOverviewResult.notFoundResult(); default: return PaichaOverviewResult.error('加载失败,点击重试'); } } catch (e) { return PaichaOverviewResult.error('加载失败,点击重试'); } } /// 查询装箱信息 — GET /CargoTrace/box/info Future fetchBoxInfo({ required String baseUrl, required String zongpaiNo, }) async { final uri = Uri.parse( '$baseUrl/CargoTrace/box/info', ).replace(queryParameters: {'zongpai_no': zongpaiNo}); try { final response = await _client .get(uri, headers: {'Content-Type': 'application/json'}) .timeout(timeout); switch (response.statusCode) { case 200: final body = jsonDecode(response.body) as Map; return BoxInfoResult.ok(body); case 400: return BoxInfoResult.error('无效的总排号格式'); case 404: return BoxInfoResult.error('未找到该总排号对应的排产号信息'); default: return BoxInfoResult.error('查询失败 (${response.statusCode})'); } } catch (e) { return BoxInfoResult.error('网络异常,请检查网络连接'); } } /// 保存装箱记录 — POST /CargoTrace/box Future saveBoxRecord({ required String baseUrl, required String zongpaiNo, required int boxNo, required int quantity, }) async { final uri = Uri.parse('$baseUrl/CargoTrace/box'); try { final response = await _client .post( uri, headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'zongpai_no': zongpaiNo, 'box_no': boxNo, 'quantity': quantity, }), ) .timeout(timeout); switch (response.statusCode) { case 200: final body = jsonDecode(response.body) as Map; return BoxSaveResult.ok(body); case 400: final body = jsonDecode(response.body) as Map; final msg = body['message']?.toString() ?? '请求参数错误'; return BoxSaveResult.error(msg); case 409: final body = jsonDecode(response.body) as Map; return BoxSaveResult.duplicate(body); case 404: return BoxSaveResult.error('未找到该总排号对应的排产号信息'); default: return BoxSaveResult.error('提交失败 (${response.statusCode})'); } } catch (e) { return BoxSaveResult.error('网络异常,请检查网络连接'); } } /// 更新装箱明细 — PATCH /CargoTrace/box/{boxItemId} Future updateBoxRecord({ required String baseUrl, required int boxItemId, required int boxNo, required int quantity, }) async { final uri = Uri.parse('$baseUrl/CargoTrace/box/$boxItemId'); try { final response = await _client .patch( uri, headers: {'Content-Type': 'application/json'}, body: jsonEncode({'box_no': boxNo, 'quantity': quantity}), ) .timeout(timeout); switch (response.statusCode) { case 200: final body = jsonDecode(response.body) as Map; return BoxSaveResult.ok(body); case 400: final body = jsonDecode(response.body) as Map; final msg = body['message']?.toString() ?? '请求参数错误'; return BoxSaveResult.error(msg); case 409: final body = jsonDecode(response.body) as Map; return BoxSaveResult.duplicate(body); case 404: return BoxSaveResult.error('指定装箱明细不存在'); default: return BoxSaveResult.error('修改失败 (${response.statusCode})'); } } catch (e) { return BoxSaveResult.error('网络异常,请检查网络连接'); } } /// 删除装箱明细 — DELETE /CargoTrace/box/{boxItemId} Future deleteBoxRecord({ required String baseUrl, required int boxItemId, }) async { final uri = Uri.parse('$baseUrl/CargoTrace/box/$boxItemId'); try { final response = await _client .delete(uri, headers: {'Content-Type': 'application/json'}) .timeout(timeout); switch (response.statusCode) { case 200: return BoxDeleteResult(success: true); case 404: return BoxDeleteResult(success: false, errorMessage: '指定装箱明细不存在'); default: return BoxDeleteResult( success: false, errorMessage: '删除失败 (${response.statusCode})', ); } } catch (e) { return BoxDeleteResult(success: false, errorMessage: '网络异常,请检查网络连接'); } } /// Test connectivity by making a HEAD request to the base URL. Future testConnection(String baseUrl) async { try { final uri = Uri.parse(baseUrl); final response = await _client.head(uri).timeout(timeout); return response.statusCode < 500; } catch (_) { return false; } } /// 查询附件类型列表 — GET /CargoTrace/attachment/categories Future fetchAttachmentCategories({ required String baseUrl, }) async { final uri = Uri.parse('$baseUrl/CargoTrace/attachment/categories'); try { final response = await _client .get(uri, headers: {'Content-Type': 'application/json'}) .timeout(timeout); if (response.statusCode == 200) { final list = (jsonDecode(response.body) as List) .map( (e) => AttachmentCategoryData.fromJson(e as Map), ) .toList(); return AttachmentCategoryResult(success: true, categories: list); } return AttachmentCategoryResult( success: false, errorMessage: '加载失败 (${response.statusCode})', ); } catch (e) { return AttachmentCategoryResult( success: false, errorMessage: '网络异常,请检查网络连接', ); } } /// 查询附件状态 — GET /CargoTrace/attachment/status Future fetchAttachmentStatus({ required String baseUrl, required String zongpaiNo, }) async { final uri = Uri.parse( '$baseUrl/CargoTrace/attachment/status', ).replace(queryParameters: {'zongpai_no': zongpaiNo}); try { final response = await _client .get(uri, headers: {'Content-Type': 'application/json'}) .timeout(timeout); if (response.statusCode == 200) { final body = jsonDecode(response.body) as Map; final items = (body['items'] as List?) ?.map( (e) => AttachmentStatusItemData.fromJson( e as Map, ), ) .toList() ?? []; return AttachmentStatusResult( success: true, determination: body['determination']?.toString() ?? 'undetermined', allComplete: body['all_complete'] as bool? ?? false, items: items, ); } return AttachmentStatusResult( success: false, errorMessage: '查询失败 (${response.statusCode})', ); } catch (e) { return AttachmentStatusResult( success: false, errorMessage: '网络异常,请检查网络连接', ); } } /// 定稿附件清单 — PUT /CargoTrace/attachment/config Future putAttachmentConfig({ required String baseUrl, required String zongpaiNo, required String determination, required List items, }) async { final uri = Uri.parse('$baseUrl/CargoTrace/attachment/config'); try { final response = await _client .put( uri, headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'zongpai_no': zongpaiNo, 'determination': determination, 'items': items.map((i) => i.toJson()).toList(), }), ) .timeout(timeout); if (response.statusCode == 200) { final body = jsonDecode(response.body) as Map; final respItems = (body['items'] as List?) ?.map( (e) => AttachmentStatusItemData.fromJson( e as Map, ), ) .toList() ?? []; return AttachmentConfigResult( success: true, determination: body['determination']?.toString() ?? 'has', allComplete: body['all_complete'] as bool? ?? false, items: respItems, ); } final body = jsonDecode(response.body) as Map; return AttachmentConfigResult( success: false, errorMessage: body['message']?.toString() ?? '配置失败', errorCode: body['error_code']?.toString(), ); } catch (e) { return AttachmentConfigResult( success: false, errorMessage: '网络异常,请检查网络连接', ); } } /// 附件上架 — POST /CargoTrace/attachment/location Future registerAttachmentLocation({ required String baseUrl, required String zongpaiNo, required int categoryId, required String locationCode, }) async { final uri = Uri.parse('$baseUrl/CargoTrace/attachment/location'); try { final response = await _client .post( uri, headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'zongpai_no': zongpaiNo, 'category_id': categoryId, 'location_code': locationCode, }), ) .timeout(timeout); switch (response.statusCode) { case 200: final body = jsonDecode(response.body) as Map; return AttachmentLocationResult( success: true, locationCode: body['location_code']?.toString(), previousLocation: body['previous_location']?.toString(), ); case 400: final body = jsonDecode(response.body) as Map; return AttachmentLocationResult( success: false, errorMessage: body['message']?.toString() ?? '请求参数错误', ); case 409: final body = jsonDecode(response.body) as Map; final errorCode = body['error_code']?.toString(); if (errorCode == 'DUPLICATE_LOCATION') { return AttachmentLocationResult( success: false, isDuplicate: true, conflictInfo: body, ); } if (errorCode == 'ALREADY_OFF_SHELF') { return AttachmentLocationResult( success: false, isAlreadyOffShelf: true, errorMessage: body['message']?.toString(), conflictInfo: body, ); } return AttachmentLocationResult( success: false, errorMessage: body['message']?.toString() ?? '提交失败', ); default: return AttachmentLocationResult( success: false, errorMessage: '提交失败 (${response.statusCode})', ); } } catch (e) { return AttachmentLocationResult( success: false, errorMessage: '网络异常,请检查网络连接', ); } } /// 附件装箱 — POST /CargoTrace/attachment/box Future saveAttachmentBox({ required String baseUrl, required String zongpaiNo, required int categoryId, required int boxNo, required int quantity, }) async { final uri = Uri.parse('$baseUrl/CargoTrace/attachment/box'); try { final response = await _client .post( uri, headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'zongpai_no': zongpaiNo, 'category_id': categoryId, 'box_no': boxNo, 'quantity': quantity, }), ) .timeout(timeout); switch (response.statusCode) { case 200: final body = jsonDecode(response.body) as Map; return AttachmentBoxResult( success: true, boxItemId: body['box_item_id'] as int?, paichanNo: body['paichan_no']?.toString(), boxNo: body['box_no'] as int?, zongpaiNo: body['zongpai_no']?.toString(), categoryId: body['category_id'] as int?, quantity: body['quantity'] as int?, ); case 400: final body = jsonDecode(response.body) as Map; return AttachmentBoxResult( success: false, errorMessage: body['message']?.toString() ?? '请求参数错误', ); case 404: return AttachmentBoxResult( success: false, errorMessage: '未找到该总排号或附件类型', ); case 409: final body = jsonDecode(response.body) as Map; return AttachmentBoxResult( success: false, isDuplicate: true, errorMessage: body['message']?.toString(), paichanNo: body['paichan_no']?.toString(), boxNo: body['box_no'] as int?, ); default: return AttachmentBoxResult( success: false, errorMessage: '提交失败 (${response.statusCode})', ); } } catch (e) { return AttachmentBoxResult(success: false, errorMessage: '网络异常,请检查网络连接'); } } /// 修改附件装箱明细 — PATCH /CargoTrace/attachment/box/{itemId} Future updateAttachmentBox({ required String baseUrl, required int boxItemId, required int boxNo, required int quantity, }) async { final uri = Uri.parse('$baseUrl/CargoTrace/attachment/box/$boxItemId'); try { final response = await _client .patch( uri, headers: {'Content-Type': 'application/json'}, body: jsonEncode({'box_no': boxNo, 'quantity': quantity}), ) .timeout(timeout); switch (response.statusCode) { case 200: final body = jsonDecode(response.body) as Map; return AttachmentBoxResult( success: true, boxItemId: body['box_item_id'] as int?, paichanNo: body['paichan_no']?.toString(), boxNo: body['box_no'] as int?, zongpaiNo: body['zongpai_no']?.toString(), categoryId: body['category_id'] as int?, quantity: body['quantity'] as int?, ); case 400: final body = jsonDecode(response.body) as Map; return AttachmentBoxResult( success: false, errorMessage: body['message']?.toString() ?? '请求参数错误', ); case 404: return AttachmentBoxResult(success: false, errorMessage: '指定装箱明细不存在'); default: return AttachmentBoxResult( success: false, errorMessage: '修改失败 (${response.statusCode})', ); } } catch (e) { return AttachmentBoxResult(success: false, errorMessage: '网络异常,请检查网络连接'); } } /// 删除附件装箱明细 — DELETE /CargoTrace/attachment/box/{itemId} Future deleteAttachmentBox({ required String baseUrl, required int boxItemId, }) async { final uri = Uri.parse('$baseUrl/CargoTrace/attachment/box/$boxItemId'); try { final response = await _client .delete(uri, headers: {'Content-Type': 'application/json'}) .timeout(timeout); switch (response.statusCode) { case 200: return AttachmentBoxDeleteResult(success: true); case 404: return AttachmentBoxDeleteResult( success: false, errorMessage: '指定装箱明细不存在', ); default: return AttachmentBoxDeleteResult( success: false, errorMessage: '删除失败 (${response.statusCode})', ); } } catch (e) { return AttachmentBoxDeleteResult( success: false, errorMessage: '网络异常,请检查网络连接', ); } } }