expense_apply_api.dart 7.8 KB

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