expense_apply_api.dart 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import 'dart:convert';
  2. import 'package:flutter/foundation.dart';
  3. import 'package:dio/dio.dart';
  4. import 'package:flutter_riverpod/flutter_riverpod.dart';
  5. import '../../core/network/api_client.dart';
  6. import '../../app.dart';
  7. import '../../shared/models/pagination_model.dart';
  8. import '../../shared/models/bill_attachment.dart';
  9. import 'expense_apply_model.dart';
  10. import 'report_model.dart';
  11. final expenseApplyApiProvider = Provider<ExpenseApplyApi>(
  12. (ref) => ExpenseApplyApi(ref.read(apiClientProvider)),
  13. );
  14. // ═══ 参考数据模型(API 返回) ═══
  15. class CostTypeItem {
  16. final String typeNo;
  17. final String typeName;
  18. final String accNo;
  19. final String accName;
  20. const CostTypeItem({required this.typeNo, required this.typeName, required this.accNo, required this.accName});
  21. factory CostTypeItem.fromJson(Map<String, dynamic> json) => CostTypeItem(
  22. typeNo: json['typeNo'] as String? ?? '',
  23. typeName: json['typeName'] as String? ?? '',
  24. accNo: json['accNo'] as String? ?? '',
  25. accName: json['accName'] as String? ?? '',
  26. );
  27. }
  28. class ProjectCodeItem {
  29. final String objNo;
  30. final String name;
  31. const ProjectCodeItem({required this.objNo, required this.name});
  32. factory ProjectCodeItem.fromJson(Map<String, dynamic> json) => ProjectCodeItem(
  33. objNo: json['objNo'] as String? ?? '',
  34. name: json['name'] as String? ?? '',
  35. );
  36. }
  37. class DepartmentItem {
  38. final String dep;
  39. final String name;
  40. const DepartmentItem({required this.dep, required this.name});
  41. factory DepartmentItem.fromJson(Map<String, dynamic> json) => DepartmentItem(
  42. dep: json['dep'] as String? ?? '',
  43. name: json['name'] as String? ?? '',
  44. );
  45. }
  46. class ExpenseApplyApi {
  47. final ApiClient _client;
  48. ExpenseApplyApi(this._client);
  49. /// 费用申请列表(分页)
  50. Future<PaginatedData<ExpenseApplyModel>> fetchList({
  51. String status = '',
  52. String keyword = '',
  53. String startDate = '',
  54. String endDate = '',
  55. String usr = '',
  56. String sortDir = 'DESC',
  57. int page = 1,
  58. int size = 20,
  59. }) async {
  60. final response = await _client.get<Map<String, dynamic>>(
  61. '/OA/GetExpenseApplies',
  62. queryParameters: {
  63. 'status': status,
  64. 'keyword': keyword,
  65. 'startDate': startDate,
  66. 'endDate': endDate,
  67. 'usr': usr,
  68. 'sortDir': sortDir,
  69. 'page': page,
  70. 'size': size,
  71. },
  72. );
  73. return PaginatedData.fromJson(response.data!, ExpenseApplyModel.fromJson);
  74. }
  75. /// 费用申请详情(主表+明细)
  76. Future<ExpenseApplyModel> fetchDetail(String billNo) async {
  77. final response = await _client.get<Map<String, dynamic>>(
  78. '/OA/GetExpenseApplyDetail',
  79. queryParameters: {'billNo': billNo},
  80. );
  81. return ExpenseApplyModel.fromJson(response.data!);
  82. }
  83. /// 费用类别字典
  84. Future<List<CostTypeItem>> getCostTypes({String keyword = '', String accNo = ''}) async {
  85. final response = await _client.get<Map<String, dynamic>>(
  86. '/OA/GetCostTypes',
  87. queryParameters: {'keyword': keyword, 'accNo': accNo, 'page': 1, 'size': 100},
  88. );
  89. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  90. return list.map((e) => CostTypeItem.fromJson(e as Map<String, dynamic>)).toList();
  91. }
  92. /// 项目代号
  93. Future<List<ProjectCodeItem>> getProjectCodes({String keyword = '', String billDate = ''}) async {
  94. final response = await _client.get<Map<String, dynamic>>(
  95. '/OA/GetProjectCodes',
  96. queryParameters: {'keyword': keyword, 'billDate': billDate, 'page': 1, 'size': 100},
  97. );
  98. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  99. return list.map((e) => ProjectCodeItem.fromJson(e as Map<String, dynamic>)).toList();
  100. }
  101. /// 部门
  102. Future<List<DepartmentItem>> getDepartments({String keyword = '', bool onlyActive = true}) async {
  103. final response = await _client.get<Map<String, dynamic>>(
  104. '/OA/GetDepartments',
  105. queryParameters: {'keyword': keyword, 'onlyActive': onlyActive, 'page': 1, 'size': 100},
  106. );
  107. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  108. return list.map((e) => DepartmentItem.fromJson(e as Map<String, dynamic>)).toList();
  109. }
  110. /// 会计科目树(级联选择器数据源)
  111. Future<dynamic> getAcctSubjects() async {
  112. final response = await _client.get('/OA/GetAcctSubjects');
  113. return response.data;
  114. }
  115. /// 提交审批,返回申请单号(提取失败时返回 null,不影响主流程)
  116. /// BillSave 返回格式: { callok:true, resultData:{ BIL_NO:"AE20267020005", ... } }
  117. Future<String?> submit(Map<String, dynamic> data) async {
  118. final response = await _client.post<Map<String, dynamic>>('/OA/BillSave', data: {
  119. 'erpCategory': 'MasterService',
  120. 'billId': 'AE',
  121. 'procId': '',
  122. 'data': data,
  123. });
  124. final resData = response.data;
  125. if (resData == null) return null;
  126. // 从 resultData 中取(BillSave 实际返回的嵌套格式)
  127. final resultData = resData['resultData'];
  128. if (resultData is Map<String, dynamic>) {
  129. // BIL_NO 是 BillSave 返回的通用单号字段(不区分 AE/BX)
  130. final bilNo = resultData['BIL_NO'] as String?;
  131. if (bilNo != null && bilNo.isNotEmpty) return bilNo;
  132. }
  133. // resultData 可能是 JSON 字符串
  134. if (resultData is String && resultData.isNotEmpty) {
  135. try {
  136. final parsed = json.decode(resultData) as Map<String, dynamic>;
  137. final bilNo = parsed['BIL_NO'] as String?;
  138. if (bilNo != null && bilNo.isNotEmpty) return bilNo;
  139. } catch (_) {}
  140. }
  141. // 兜底: resData 根级 BIL_NO
  142. final rootBilNo = resData['BIL_NO'] as String?;
  143. if (rootBilNo != null && rootBilNo.isNotEmpty) return rootBilNo;
  144. return null;
  145. }
  146. /// 下载附件文件字节
  147. Future<Uint8List?> downloadAttachment(String id) async {
  148. return await _client.downloadFile('/OA/DownloadAttachment', queryParameters: {'id': id});
  149. }
  150. /// 检测附件服务是否可用
  151. Future<bool> checkAttachHealth() async {
  152. try {
  153. final response = await _client.get<Map<String, dynamic>>('/OA/CheckAttachHealth');
  154. debugPrint('[checkAttachHealth] response.data: ${response.data}');
  155. debugPrint('[checkAttachHealth] response.data type: ${response.data.runtimeType}');
  156. final available = response.data?['available'] as bool? ?? false;
  157. debugPrint('[checkAttachHealth] available: $available');
  158. return available;
  159. } catch (e) {
  160. debugPrint('[checkAttachHealth] error: $e');
  161. return false;
  162. }
  163. }
  164. /// 审批进度
  165. Future<Map<String, dynamic>> fetchApprovalTimeline(String bilId, String bilNo, {int bilItm = 0}) async {
  166. final response = await _client.get<Map<String, dynamic>>(
  167. '/OA/GetApprovalTimeline',
  168. queryParameters: {'bilId': bilId, 'bilNo': bilNo, 'bilItm': bilItm},
  169. );
  170. return response.data!;
  171. }
  172. /// 获取单据附件列表
  173. Future<List<BillAttachment>> getAttachments(String bilId, String bilNo, {int? srcItm}) async {
  174. final params = <String, dynamic>{
  175. 'bilId': bilId,
  176. 'bilNo': bilNo,
  177. };
  178. if (srcItm != null) params['srcItm'] = srcItm;
  179. final response = await _client.get<dynamic>(
  180. '/OA/GetAttachments',
  181. queryParameters: params,
  182. );
  183. final body = response.data;
  184. if (body is! Map) return [];
  185. final result = body['Result'];
  186. if (result is! Map) return [];
  187. final documents = result['documents'];
  188. if (documents is! List) return [];
  189. return documents
  190. .whereType<Map<String, dynamic>>()
  191. .map((e) => BillAttachment.fromJson(e))
  192. .toList();
  193. }
  194. /// 上传附件
  195. Future<Map<String, dynamic>> uploadAttachment(
  196. String filePath,
  197. Map<String, dynamic> metadata,
  198. ) async {
  199. final fileName = (metadata['FILENAME'] as String?) ?? filePath.split('/').last;
  200. final response = await _client.uploadMultipart<Map<String, dynamic>>(
  201. '/OA/UploadAttachment',
  202. files: [await MultipartFile.fromFile(filePath, filename: fileName)],
  203. extraFields: {'metadata': json.encode(metadata)},
  204. );
  205. return response.data ?? {};
  206. }
  207. /// 审核执行(通过/驳回/反审核)
  208. Future<Map<String, dynamic>> executeApproval({
  209. required String bilId, required String bilNo, int bilItm = 0,
  210. required String action, String rem = '', String effDd = '',
  211. String reason = '', bool isPreToStart = false, int nodeIndex = -1,
  212. String dataBx = '',
  213. }) async {
  214. final response = await _client.post<Map<String, dynamic>>(
  215. '/OA/ExecuteApproval',
  216. data: {
  217. 'bilId': bilId, 'bilNo': bilNo, 'bilItm': bilItm,
  218. 'action': action, 'rem': rem, 'effDd': effDd,
  219. 'reason': reason, 'isPreToStart': isPreToStart,
  220. 'nodeIndex': nodeIndex, 'dataBx': dataBx,
  221. },
  222. );
  223. return response.data!;
  224. }
  225. /// 费用申请报表
  226. Future<ReportData> getExpenseApplyReport({String? startDate, String? endDate}) async {
  227. final params = <String, dynamic>{};
  228. if (startDate != null) params['startDate'] = startDate;
  229. if (endDate != null) params['endDate'] = endDate;
  230. final response = await _client.get<Map<String, dynamic>>(
  231. '/OA/GetExpenseApplyReport',
  232. queryParameters: params,
  233. );
  234. return ReportData.fromJson(response.data!);
  235. }
  236. /// 费用申请报表明细(分页)
  237. Future<Map<String, dynamic>> getExpenseApplyReportDetails({
  238. String? startDate, String? endDate, int page = 1, int size = 20,
  239. }) async {
  240. final response = await _client.get<Map<String, dynamic>>(
  241. '/OA/GetExpenseApplyReportDetails',
  242. queryParameters: {'startDate': startDate, 'endDate': endDate, 'page': page, 'size': size},
  243. );
  244. return response.data!;
  245. }
  246. }