import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/network/api_client.dart'; import '../../app.dart'; import '../../shared/models/pagination_model.dart'; import '../../shared/models/bill_attachment.dart'; import 'expense_apply_model.dart'; final expenseApplyApiProvider = Provider( (ref) => ExpenseApplyApi(ref.read(apiClientProvider)), ); // ═══ 参考数据模型(API 返回) ═══ class CostTypeItem { final String typeNo; final String typeName; final String accNo; final String accName; const CostTypeItem({required this.typeNo, required this.typeName, required this.accNo, required this.accName}); factory CostTypeItem.fromJson(Map json) => CostTypeItem( typeNo: json['typeNo'] as String? ?? '', typeName: json['typeName'] as String? ?? '', accNo: json['accNo'] as String? ?? '', accName: json['accName'] as String? ?? '', ); } class ProjectCodeItem { final String objNo; final String name; const ProjectCodeItem({required this.objNo, required this.name}); factory ProjectCodeItem.fromJson(Map json) => ProjectCodeItem( objNo: json['objNo'] as String? ?? '', name: json['name'] as String? ?? '', ); } class DepartmentItem { final String dep; final String name; const DepartmentItem({required this.dep, required this.name}); factory DepartmentItem.fromJson(Map json) => DepartmentItem( dep: json['dep'] as String? ?? '', name: json['name'] as String? ?? '', ); } class ExpenseApplyApi { final ApiClient _client; ExpenseApplyApi(this._client); /// 费用申请列表(分页) Future> fetchList({ String status = '', String keyword = '', String startDate = '', String endDate = '', String usr = '', String sortDir = 'DESC', int page = 1, int size = 20, }) async { final response = await _client.get>( '/OA/GetExpenseApplies', queryParameters: { 'status': status, 'keyword': keyword, 'startDate': startDate, 'endDate': endDate, 'usr': usr, 'sortDir': sortDir, 'page': page, 'size': size, }, ); return PaginatedData.fromJson(response.data!, ExpenseApplyModel.fromJson); } /// 费用申请详情(主表+明细) Future fetchDetail(String billNo) async { final response = await _client.get>( '/OA/GetExpenseApplyDetail', queryParameters: {'billNo': billNo}, ); return ExpenseApplyModel.fromJson(response.data!); } /// 费用类别字典 Future> getCostTypes({String keyword = '', String accNo = ''}) async { final response = await _client.get>( '/OA/GetCostTypes', queryParameters: {'keyword': keyword, 'accNo': accNo, 'page': 1, 'size': 100}, ); final list = (response.data?['list'] as List?) ?? []; return list.map((e) => CostTypeItem.fromJson(e as Map)).toList(); } /// 项目代号 Future> getProjectCodes({String keyword = '', String billDate = ''}) async { final response = await _client.get>( '/OA/GetProjectCodes', queryParameters: {'keyword': keyword, 'billDate': billDate, 'page': 1, 'size': 100}, ); final list = (response.data?['list'] as List?) ?? []; return list.map((e) => ProjectCodeItem.fromJson(e as Map)).toList(); } /// 部门 Future> getDepartments({String keyword = '', bool onlyActive = true}) async { final response = await _client.get>( '/OA/GetDepartments', queryParameters: {'keyword': keyword, 'onlyActive': onlyActive, 'page': 1, 'size': 100}, ); final list = (response.data?['list'] as List?) ?? []; return list.map((e) => DepartmentItem.fromJson(e as Map)).toList(); } /// 提交审批,返回申请单号(提取失败时返回 null,不影响主流程) /// BillSave 返回格式: { callok:true, resultData:{ BIL_NO:"AE20267020005", ... } } Future submit(Map data) async { final response = await _client.post>('/OA/BillSave', data: { 'erpCategory': 'MasterService', 'billId': 'AE', 'procId': '', 'data': data, }); final resData = response.data; if (resData == null) return null; // 从 resultData 中取(BillSave 实际返回的嵌套格式) final resultData = resData['resultData']; if (resultData is Map) { // BIL_NO 是 BillSave 返回的通用单号字段(不区分 AE/BX) final bilNo = resultData['BIL_NO'] as String?; if (bilNo != null && bilNo.isNotEmpty) return bilNo; } // resultData 可能是 JSON 字符串 if (resultData is String && resultData.isNotEmpty) { try { final parsed = json.decode(resultData) as Map; final bilNo = parsed['BIL_NO'] as String?; if (bilNo != null && bilNo.isNotEmpty) return bilNo; } catch (_) {} } // 兜底: resData 根级 BIL_NO final rootBilNo = resData['BIL_NO'] as String?; if (rootBilNo != null && rootBilNo.isNotEmpty) return rootBilNo; return null; } /// 下载附件文件字节 Future downloadAttachment(String id) async { return await _client.downloadFile('/OA/DownloadAttachment', queryParameters: {'id': id}); } /// 检测附件服务是否可用 Future checkAttachHealth() async { try { final response = await _client.get>('/OA/CheckAttachHealth'); debugPrint('[checkAttachHealth] response.data: ${response.data}'); debugPrint('[checkAttachHealth] response.data type: ${response.data.runtimeType}'); final available = response.data?['available'] as bool? ?? false; debugPrint('[checkAttachHealth] available: $available'); return available; } catch (e) { debugPrint('[checkAttachHealth] error: $e'); return false; } } /// 审批进度 Future> fetchApprovalTimeline(String bilId, String bilNo, {int bilItm = 0}) async { final response = await _client.get>( '/OA/GetApprovalTimeline', queryParameters: {'bilId': bilId, 'bilNo': bilNo, 'bilItm': bilItm}, ); return response.data!; } /// 获取单据附件列表 Future> getAttachments(String bilId, String bilNo, {int? srcItm}) async { final params = { 'bilId': bilId, 'bilNo': bilNo, }; if (srcItm != null) params['srcItm'] = srcItm; final response = await _client.get( '/OA/GetAttachments', queryParameters: params, ); final body = response.data; if (body is! Map) return []; final result = body['Result']; if (result is! Map) return []; final documents = result['documents']; if (documents is! List) return []; return documents .whereType>() .map((e) => BillAttachment.fromJson(e)) .toList(); } /// 上传附件 Future> uploadAttachment( String filePath, Map metadata, ) async { final fileName = (metadata['FILENAME'] as String?) ?? filePath.split('/').last; final response = await _client.uploadMultipart>( '/OA/UploadAttachment', files: [await MultipartFile.fromFile(filePath, filename: fileName)], extraFields: {'metadata': json.encode(metadata)}, ); return response.data ?? {}; } /// 审核执行(通过/驳回/反审核) Future> executeApproval({ required String bilId, required String bilNo, int bilItm = 0, required String action, String rem = '', String effDd = '', String reason = '', bool isPreToStart = false, int nodeIndex = -1, String dataBx = '', }) async { final response = await _client.post>( '/OA/ExecuteApproval', data: { 'bilId': bilId, 'bilNo': bilNo, 'bilItm': bilItm, 'action': action, 'rem': rem, 'effDd': effDd, 'reason': reason, 'isPreToStart': isPreToStart, 'nodeIndex': nodeIndex, 'dataBx': dataBx, }, ); return response.data!; } }