expense_apply_api.dart 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  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({
  21. required this.typeNo,
  22. required this.typeName,
  23. required this.accNo,
  24. required this.accName,
  25. });
  26. factory CostTypeItem.fromJson(Map<String, dynamic> json) => CostTypeItem(
  27. typeNo: json['typeNo'] as String? ?? '',
  28. typeName: json['typeName'] as String? ?? '',
  29. accNo: json['accNo'] as String? ?? '',
  30. accName: json['accName'] as String? ?? '',
  31. );
  32. }
  33. class ProjectCodeItem {
  34. final String objNo;
  35. final String name;
  36. const ProjectCodeItem({required this.objNo, required this.name});
  37. factory ProjectCodeItem.fromJson(Map<String, dynamic> json) =>
  38. ProjectCodeItem(
  39. objNo: json['objNo'] as String? ?? '',
  40. name: json['name'] as String? ?? '',
  41. );
  42. }
  43. class DepartmentItem {
  44. final String dep;
  45. final String name;
  46. const DepartmentItem({required this.dep, required this.name});
  47. factory DepartmentItem.fromJson(Map<String, dynamic> json) => DepartmentItem(
  48. dep: json['dep'] as String? ?? '',
  49. name: json['name'] as String? ?? '',
  50. );
  51. }
  52. class ExpenseApplyApi {
  53. final ApiClient _client;
  54. ExpenseApplyApi(this._client);
  55. /// 费用申请列表(分页)
  56. Future<PaginatedData<ExpenseApplyModel>> fetchList({
  57. String status = '',
  58. String keyword = '',
  59. String startDate = '',
  60. String endDate = '',
  61. String usr = '',
  62. String sortDir = 'DESC',
  63. int page = 1,
  64. int size = 20,
  65. }) async {
  66. final response = await _client.get<Map<String, dynamic>>(
  67. '/OA/GetExpenseApplyList',
  68. queryParameters: {
  69. 'status': status,
  70. 'keyword': keyword,
  71. 'startDate': startDate,
  72. 'endDate': endDate,
  73. 'usr': usr,
  74. 'sortDir': sortDir,
  75. 'page': page,
  76. 'size': size,
  77. },
  78. );
  79. return PaginatedData.fromJson(response.data!, ExpenseApplyModel.fromJson);
  80. }
  81. /// 费用申请详情(主表+明细)
  82. Future<ExpenseApplyModel> fetchDetail(String billNo) async {
  83. final response = await _client.get<Map<String, dynamic>>(
  84. '/OA/GetExpenseApplyDetail',
  85. queryParameters: {'billNo': billNo},
  86. );
  87. return ExpenseApplyModel.fromJson(response.data!);
  88. }
  89. /// 费用类别字典
  90. Future<List<CostTypeItem>> getCostTypes({
  91. String keyword = '',
  92. String accNo = '',
  93. }) async {
  94. final response = await _client.get<Map<String, dynamic>>(
  95. '/OA/GetCostTypes',
  96. queryParameters: {
  97. 'keyword': keyword,
  98. 'accNo': accNo,
  99. 'page': 1,
  100. 'size': 100,
  101. },
  102. );
  103. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  104. return list
  105. .map((e) => CostTypeItem.fromJson(e as Map<String, dynamic>))
  106. .toList();
  107. }
  108. /// 项目代号
  109. Future<List<ProjectCodeItem>> getProjectCodes({
  110. String keyword = '',
  111. String billDate = '',
  112. }) async {
  113. final response = await _client.get<Map<String, dynamic>>(
  114. '/OA/GetProjectCodes',
  115. queryParameters: {
  116. 'keyword': keyword,
  117. 'billDate': billDate,
  118. 'page': 1,
  119. 'size': 100,
  120. },
  121. );
  122. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  123. return list
  124. .map((e) => ProjectCodeItem.fromJson(e as Map<String, dynamic>))
  125. .toList();
  126. }
  127. /// 部门
  128. Future<List<DepartmentItem>> getDepartments({
  129. String keyword = '',
  130. bool onlyActive = true,
  131. }) async {
  132. final response = await _client.get<Map<String, dynamic>>(
  133. '/OA/GetDepartments',
  134. queryParameters: {
  135. 'keyword': keyword,
  136. 'onlyActive': onlyActive,
  137. 'page': 1,
  138. 'size': 100,
  139. },
  140. );
  141. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  142. return list
  143. .map((e) => DepartmentItem.fromJson(e as Map<String, dynamic>))
  144. .toList();
  145. }
  146. /// 会计科目树(级联选择器数据源)
  147. Future<dynamic> getAcctSubjects() async {
  148. final response = await _client.get('/OA/GetAcctSubjects');
  149. return response.data;
  150. }
  151. /// 提交审批,返回申请单号(提取失败时返回 null,不影响主流程)
  152. /// BillSave 返回格式: { callok:true, resultData:{ BIL_NO:"AE20267020005", ... } }
  153. Future<String?> submit(Map<String, dynamic> data) async {
  154. final response = await _client.post<Map<String, dynamic>>(
  155. '/OA/BillSave',
  156. data: {
  157. 'erpCategory': 'MasterService',
  158. 'billId': 'AE',
  159. 'procId': '',
  160. 'data': data,
  161. },
  162. );
  163. final resData = response.data;
  164. if (resData == null) return null;
  165. // 从 resultData 中取(BillSave 实际返回的嵌套格式)
  166. final resultData = resData['resultData'];
  167. if (resultData is Map<String, dynamic>) {
  168. // BIL_NO 是 BillSave 返回的通用单号字段(不区分 AE/BX)
  169. final bilNo = resultData['BIL_NO'] as String?;
  170. if (bilNo != null && bilNo.isNotEmpty) return bilNo;
  171. }
  172. // resultData 可能是 JSON 字符串
  173. if (resultData is String && resultData.isNotEmpty) {
  174. try {
  175. final parsed = json.decode(resultData) as Map<String, dynamic>;
  176. final bilNo = parsed['BIL_NO'] as String?;
  177. if (bilNo != null && bilNo.isNotEmpty) return bilNo;
  178. } catch (_) {}
  179. }
  180. // 兜底: resData 根级 BIL_NO
  181. final rootBilNo = resData['BIL_NO'] as String?;
  182. if (rootBilNo != null && rootBilNo.isNotEmpty) return rootBilNo;
  183. return null;
  184. }
  185. /// 下载附件文件字节
  186. Future<Uint8List?> downloadAttachment(String id) async {
  187. return await _client.downloadFile(
  188. '/OA/DownloadAttachment',
  189. queryParameters: {'id': id},
  190. );
  191. }
  192. /// 检测附件服务是否可用
  193. Future<bool> checkAttachHealth() async {
  194. try {
  195. final response = await _client.get<Map<String, dynamic>>(
  196. '/OA/CheckAttachHealth',
  197. );
  198. debugPrint('[checkAttachHealth] response.data: ${response.data}');
  199. debugPrint(
  200. '[checkAttachHealth] response.data type: ${response.data.runtimeType}',
  201. );
  202. final available = response.data?['available'] as bool? ?? false;
  203. debugPrint('[checkAttachHealth] available: $available');
  204. return available;
  205. } catch (e) {
  206. debugPrint('[checkAttachHealth] error: $e');
  207. return false;
  208. }
  209. }
  210. /// 审批进度
  211. Future<Map<String, dynamic>> fetchApprovalTimeline(
  212. String bilId,
  213. String bilNo, {
  214. int bilItm = 0,
  215. }) async {
  216. final response = await _client.get<Map<String, dynamic>>(
  217. '/OA/GetApprovalTimeline',
  218. queryParameters: {'bilId': bilId, 'bilNo': bilNo, 'bilItm': bilItm},
  219. );
  220. return response.data!;
  221. }
  222. /// 获取单据附件列表
  223. Future<List<BillAttachment>> getAttachments(
  224. String bilId,
  225. String bilNo, {
  226. int? srcItm,
  227. }) async {
  228. final params = <String, dynamic>{'bilId': bilId, 'bilNo': bilNo};
  229. if (srcItm != null) params['srcItm'] = srcItm;
  230. final response = await _client.get<dynamic>(
  231. '/OA/GetAttachments',
  232. queryParameters: params,
  233. );
  234. final body = response.data;
  235. if (body is! Map) return [];
  236. final result = body['Result'];
  237. if (result is! Map) return [];
  238. final documents = result['documents'];
  239. if (documents is! List) return [];
  240. return documents
  241. .whereType<Map<String, dynamic>>()
  242. .map((e) => BillAttachment.fromJson(e))
  243. .toList();
  244. }
  245. /// 上传附件
  246. Future<Map<String, dynamic>> uploadAttachment(
  247. String filePath,
  248. Map<String, dynamic> metadata,
  249. ) async {
  250. final fileName =
  251. (metadata['FILENAME'] as String?) ?? filePath.split('/').last;
  252. final response = await _client.uploadMultipart<Map<String, dynamic>>(
  253. '/OA/UploadAttachment',
  254. files: [await MultipartFile.fromFile(filePath, filename: fileName)],
  255. extraFields: {'metadata': json.encode(metadata)},
  256. );
  257. return response.data ?? {};
  258. }
  259. /// 审核执行(通过/驳回/反审核)
  260. Future<Map<String, dynamic>> executeApproval({
  261. required String bilId,
  262. required String bilNo,
  263. int bilItm = 0,
  264. required String action,
  265. String rem = '',
  266. String effDd = '',
  267. String reason = '',
  268. bool isPreToStart = false,
  269. int nodeIndex = -1,
  270. String dataBx = '',
  271. }) async {
  272. final response = await _client.post<Map<String, dynamic>>(
  273. '/OA/ExecuteApproval',
  274. data: {
  275. 'bilId': bilId,
  276. 'bilNo': bilNo,
  277. 'bilItm': bilItm,
  278. 'action': action,
  279. 'rem': rem,
  280. 'effDd': effDd,
  281. 'reason': reason,
  282. 'isPreToStart': isPreToStart,
  283. 'nodeIndex': nodeIndex,
  284. 'dataBx': dataBx,
  285. },
  286. );
  287. return response.data!;
  288. }
  289. /// 费用申请报表
  290. Future<ReportData> getExpenseApplyReport({
  291. String? startDate,
  292. String? endDate,
  293. }) async {
  294. final params = <String, dynamic>{};
  295. if (startDate != null) params['startDate'] = startDate;
  296. if (endDate != null) params['endDate'] = endDate;
  297. final response = await _client.get<Map<String, dynamic>>(
  298. '/OA/GetExpenseApplyReport',
  299. queryParameters: params,
  300. );
  301. return ReportData.fromJson(response.data!);
  302. }
  303. /// 费用申请报表明细(分页)
  304. Future<Map<String, dynamic>> getExpenseApplyReportDetail({
  305. String? startDate,
  306. String? endDate,
  307. int page = 1,
  308. int size = 20,
  309. }) async {
  310. final response = await _client.get<Map<String, dynamic>>(
  311. '/OA/GetExpenseApplyReportDetail',
  312. queryParameters: {
  313. 'startDate': startDate,
  314. 'endDate': endDate,
  315. 'page': page,
  316. 'size': size,
  317. },
  318. );
  319. return response.data!;
  320. }
  321. }