expense_apply_api.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  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 '../../shared/models/bill_file_rights.dart';
  10. import 'expense_apply_model.dart';
  11. import 'report_model.dart';
  12. final expenseApplyApiProvider = Provider<ExpenseApplyApi>(
  13. (ref) => ExpenseApplyApi(ref.read(apiClientProvider)),
  14. );
  15. // ═══ 参考数据模型(API 返回) ═══
  16. class CostTypeItem {
  17. final String typeNo;
  18. final String typeName;
  19. final String accNo;
  20. final String accName;
  21. const CostTypeItem({
  22. required this.typeNo,
  23. required this.typeName,
  24. required this.accNo,
  25. required this.accName,
  26. });
  27. factory CostTypeItem.fromJson(Map<String, dynamic> json) => CostTypeItem(
  28. typeNo: json['typeNo'] as String? ?? '',
  29. typeName: json['typeName'] as String? ?? '',
  30. accNo: json['accNo'] as String? ?? '',
  31. accName: json['accName'] as String? ?? '',
  32. );
  33. }
  34. class CostProjectItem {
  35. final String idx1;
  36. final String name;
  37. final String accNo;
  38. final String accName;
  39. const CostProjectItem({
  40. required this.idx1,
  41. required this.name,
  42. required this.accNo,
  43. required this.accName,
  44. });
  45. factory CostProjectItem.fromJson(Map<String, dynamic> json) =>
  46. CostProjectItem(
  47. idx1: json['idx1'] as String? ?? '',
  48. name: json['name'] as String? ?? '',
  49. accNo: json['accNo'] as String? ?? '',
  50. accName: json['accName'] as String? ?? '',
  51. );
  52. }
  53. class ProjectCodeItem {
  54. final String objNo;
  55. final String name;
  56. const ProjectCodeItem({required this.objNo, required this.name});
  57. factory ProjectCodeItem.fromJson(Map<String, dynamic> json) =>
  58. ProjectCodeItem(
  59. objNo: json['objNo'] as String? ?? '',
  60. name: json['name'] as String? ?? '',
  61. );
  62. }
  63. class DepartmentItem {
  64. final String dep;
  65. final String name;
  66. const DepartmentItem({required this.dep, required this.name});
  67. factory DepartmentItem.fromJson(Map<String, dynamic> json) => DepartmentItem(
  68. dep: json['dep'] as String? ?? '',
  69. name: json['name'] as String? ?? '',
  70. );
  71. }
  72. class ExpenseApplyApi {
  73. final ApiClient _client;
  74. ExpenseApplyApi(this._client);
  75. /// 费用申请列表(分页)
  76. Future<PaginatedData<ExpenseApplyModel>> fetchList({
  77. String status = '',
  78. String keyword = '',
  79. String startDate = '',
  80. String endDate = '',
  81. String usr = '',
  82. String sortDir = 'DESC',
  83. int page = 1,
  84. int size = 20,
  85. }) async {
  86. final response = await _client.get<Map<String, dynamic>>(
  87. '/OA/GetExpenseApplyList',
  88. queryParameters: {
  89. 'status': status,
  90. 'keyword': keyword,
  91. 'startDate': startDate,
  92. 'endDate': endDate,
  93. 'usr': usr,
  94. 'sortDir': sortDir,
  95. 'page': page,
  96. 'size': size,
  97. },
  98. );
  99. return PaginatedData.fromJson(response.data!, ExpenseApplyModel.fromJson);
  100. }
  101. /// 费用申请详情(主表+明细)
  102. Future<ExpenseApplyModel> fetchDetail(String billNo) async {
  103. final response = await _client.get<Map<String, dynamic>>(
  104. '/OA/GetExpenseApplyDetail',
  105. queryParameters: {'billNo': billNo},
  106. );
  107. return ExpenseApplyModel.fromJson(response.data!);
  108. }
  109. /// 费用类别字典
  110. Future<List<CostTypeItem>> getCostTypes({
  111. String keyword = '',
  112. String accNo = '',
  113. int page = 1,
  114. int size = 100,
  115. }) async {
  116. final response = await _client.get<Map<String, dynamic>>(
  117. '/OA/GetCostTypes',
  118. queryParameters: {
  119. 'keyword': keyword,
  120. 'accNo': accNo,
  121. 'page': page,
  122. 'size': size,
  123. },
  124. );
  125. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  126. return list
  127. .map((e) => CostTypeItem.fromJson(e as Map<String, dynamic>))
  128. .toList();
  129. }
  130. /// 费用项目字典(分页)
  131. Future<List<CostProjectItem>> getCostProjects({
  132. String keyword = '',
  133. String accNo = '',
  134. bool onlyActive = true,
  135. int page = 1,
  136. int size = 20,
  137. }) async {
  138. final response = await _client.get<Map<String, dynamic>>(
  139. '/OA/GetCostProjects',
  140. queryParameters: {
  141. 'keyword': keyword,
  142. 'accNo': accNo,
  143. 'onlyActive': onlyActive,
  144. 'page': page,
  145. 'size': size,
  146. },
  147. );
  148. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  149. return list
  150. .map((e) => CostProjectItem.fromJson(e as Map<String, dynamic>))
  151. .toList();
  152. }
  153. /// 项目代号
  154. Future<List<ProjectCodeItem>> getProjectCodes({
  155. String keyword = '',
  156. String billDate = '',
  157. int page = 1,
  158. int size = 100,
  159. }) async {
  160. final response = await _client.get<Map<String, dynamic>>(
  161. '/OA/GetProjectCodes',
  162. queryParameters: {
  163. 'keyword': keyword,
  164. 'billDate': billDate,
  165. 'page': page,
  166. 'size': size,
  167. },
  168. );
  169. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  170. return list
  171. .map((e) => ProjectCodeItem.fromJson(e as Map<String, dynamic>))
  172. .toList();
  173. }
  174. /// 部门
  175. Future<List<DepartmentItem>> getDepartments({
  176. String keyword = '',
  177. bool onlyActive = true,
  178. int page = 1,
  179. int size = 100,
  180. }) async {
  181. final response = await _client.get<Map<String, dynamic>>(
  182. '/OA/GetDepartments',
  183. queryParameters: {
  184. 'keyword': keyword,
  185. 'onlyActive': onlyActive,
  186. 'page': page,
  187. 'size': size,
  188. },
  189. );
  190. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  191. return list
  192. .map((e) => DepartmentItem.fromJson(e as Map<String, dynamic>))
  193. .toList();
  194. }
  195. /// 会计科目树(级联选择器数据源)
  196. Future<dynamic> getAcctSubjects() async {
  197. final response = await _client.get('/OA/GetAcctSubjects');
  198. return response.data;
  199. }
  200. /// 提交审批,返回申请单号(提取失败时返回 null,不影响主流程)
  201. /// BillSave 返回格式: { callok:true, resultData:{ BIL_NO:"AE20267020005", ... } }
  202. Future<String?> submit(Map<String, dynamic> data) async {
  203. final response = await _client.post<Map<String, dynamic>>(
  204. '/OA/BillSave',
  205. data: {
  206. 'erpCategory': 'MasterService',
  207. 'billId': 'AE',
  208. 'procId': '',
  209. 'data': data,
  210. },
  211. );
  212. final resData = response.data;
  213. if (resData == null) return null;
  214. // 从 resultData 中取(BillSave 实际返回的嵌套格式)
  215. final resultData = resData['resultData'];
  216. if (resultData is Map<String, dynamic>) {
  217. // BIL_NO 是 BillSave 返回的通用单号字段(不区分 AE/BX)
  218. final bilNo = resultData['BIL_NO'] as String?;
  219. if (bilNo != null && bilNo.isNotEmpty) return bilNo;
  220. }
  221. // resultData 可能是 JSON 字符串
  222. if (resultData is String && resultData.isNotEmpty) {
  223. try {
  224. final parsed = json.decode(resultData) as Map<String, dynamic>;
  225. final bilNo = parsed['BIL_NO'] as String?;
  226. if (bilNo != null && bilNo.isNotEmpty) return bilNo;
  227. } catch (_) {}
  228. }
  229. // 兜底: resData 根级 BIL_NO
  230. final rootBilNo = resData['BIL_NO'] as String?;
  231. if (rootBilNo != null && rootBilNo.isNotEmpty) return rootBilNo;
  232. return null;
  233. }
  234. /// 下载附件文件字节
  235. Future<Uint8List?> downloadAttachment(String id) async {
  236. return await _client.downloadFile(
  237. '/OA/DownloadAttachment',
  238. queryParameters: {'id': id},
  239. );
  240. }
  241. /// 检测附件服务是否可用
  242. Future<bool> checkAttachHealth() async {
  243. try {
  244. final response = await _client.get<Map<String, dynamic>>(
  245. '/OA/CheckAttachHealth',
  246. );
  247. debugPrint('[checkAttachHealth] response.data: ${response.data}');
  248. debugPrint(
  249. '[checkAttachHealth] response.data type: ${response.data.runtimeType}',
  250. );
  251. final available = response.data?['available'] as bool? ?? false;
  252. debugPrint('[checkAttachHealth] available: $available');
  253. return available;
  254. } catch (e) {
  255. debugPrint('[checkAttachHealth] error: $e');
  256. return false;
  257. }
  258. }
  259. /// 审批进度
  260. Future<Map<String, dynamic>> fetchApprovalTimeline(
  261. String bilId,
  262. String bilNo, {
  263. int bilItm = 0,
  264. }) async {
  265. final response = await _client.get<Map<String, dynamic>>(
  266. '/OA/GetApprovalTimeline',
  267. queryParameters: {'bilId': bilId, 'bilNo': bilNo, 'bilItm': bilItm},
  268. );
  269. return response.data!;
  270. }
  271. /// 获取单据附件列表
  272. Future<List<BillAttachment>> getAttachments(
  273. String bilId,
  274. String bilNo, {
  275. int? srcItm,
  276. }) async {
  277. final params = <String, dynamic>{'bilId': bilId, 'bilNo': bilNo};
  278. if (srcItm != null) params['srcItm'] = srcItm;
  279. final response = await _client.get<dynamic>(
  280. '/OA/GetAttachments',
  281. queryParameters: params,
  282. );
  283. final body = response.data;
  284. if (body is! Map) return [];
  285. final result = body['Result'];
  286. if (result is! Map) return [];
  287. final documents = result['documents'];
  288. if (documents is! List) return [];
  289. return documents
  290. .whereType<Map<String, dynamic>>()
  291. .map((e) => BillAttachment.fromJson(e))
  292. .toList();
  293. }
  294. /// 上传附件
  295. Future<Map<String, dynamic>> uploadAttachment(
  296. String filePath,
  297. Map<String, dynamic> metadata,
  298. ) async {
  299. final fileName =
  300. (metadata['FILENAME'] as String?) ?? filePath.split('/').last;
  301. final response = await _client.uploadMultipart<Map<String, dynamic>>(
  302. '/OA/UploadAttachment',
  303. files: [await MultipartFile.fromFile(filePath, filename: fileName)],
  304. extraFields: {'metadata': json.encode(metadata)},
  305. );
  306. return response.data ?? {};
  307. }
  308. /// 审核执行(通过/驳回/反审核)
  309. Future<Map<String, dynamic>> executeApproval({
  310. required String bilId,
  311. required String bilNo,
  312. int bilItm = 0,
  313. required String action,
  314. String rem = '',
  315. String effDd = '',
  316. String reason = '',
  317. bool isPreToStart = false,
  318. int nodeIndex = -1,
  319. String dataBx = '',
  320. }) async {
  321. final response = await _client.post<Map<String, dynamic>>(
  322. '/OA/ExecuteApproval',
  323. data: {
  324. 'bilId': bilId,
  325. 'bilNo': bilNo,
  326. 'bilItm': bilItm,
  327. 'action': action,
  328. 'rem': rem,
  329. 'effDd': effDd,
  330. 'reason': reason,
  331. 'isPreToStart': isPreToStart,
  332. 'nodeIndex': nodeIndex,
  333. 'dataBx': dataBx,
  334. },
  335. );
  336. return response.data!;
  337. }
  338. /// 费用申请报表
  339. Future<ReportData> getExpenseApplyReport({
  340. String? startDate,
  341. String? endDate,
  342. }) async {
  343. final params = <String, dynamic>{};
  344. if (startDate != null) params['startDate'] = startDate;
  345. if (endDate != null) params['endDate'] = endDate;
  346. final response = await _client.get<Map<String, dynamic>>(
  347. '/OA/GetExpenseApplyReport',
  348. queryParameters: params,
  349. );
  350. return ReportData.fromJson(response.data!);
  351. }
  352. /// 费用申请报表明细(分页)
  353. Future<Map<String, dynamic>> getExpenseApplyReportDetail({
  354. String? startDate,
  355. String? endDate,
  356. int page = 1,
  357. int size = 20,
  358. }) async {
  359. final response = await _client.get<Map<String, dynamic>>(
  360. '/OA/GetExpenseApplyReportDetail',
  361. queryParameters: {
  362. 'startDate': startDate,
  363. 'endDate': endDate,
  364. 'page': page,
  365. 'size': size,
  366. },
  367. );
  368. return response.data!;
  369. }
  370. /// 获取单据状态
  371. Future<Map<String, dynamic>> getBillStatus(String billNo) async {
  372. final response = await _client.get<Map<String, dynamic>>(
  373. '/OA/GetBillStatus',
  374. queryParameters: {'billId': 'AE', 'billNo': billNo},
  375. );
  376. return response.data!;
  377. }
  378. /// 获取指定单据别的附件操作权限
  379. Future<BillFileRights> getBillFileRights(String billId) async {
  380. final response = await _client.get<Map<String, dynamic>>(
  381. '/OA/GetBillFileRights',
  382. queryParameters: {'billId': billId},
  383. );
  384. return BillFileRights.fromJson(response.data!);
  385. }
  386. /// 员工查询
  387. Future<List<EmployeeItem>> getEmployees({
  388. String keyword = '',
  389. String salNo = '',
  390. int page = 1,
  391. int size = 100,
  392. }) async {
  393. final response = await _client.get<Map<String, dynamic>>(
  394. '/OA/GetEmployees',
  395. queryParameters: {
  396. 'keyword': keyword,
  397. 'salNo': salNo,
  398. 'page': page,
  399. 'size': size,
  400. },
  401. );
  402. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  403. return list
  404. .map((e) => EmployeeItem.fromJson(e as Map<String, dynamic>))
  405. .toList();
  406. }
  407. }
  408. class EmployeeItem {
  409. final String salNo;
  410. final String name;
  411. const EmployeeItem({required this.salNo, required this.name});
  412. factory EmployeeItem.fromJson(Map<String, dynamic> json) => EmployeeItem(
  413. salNo: json['salNo'] as String? ?? '',
  414. name: json['name'] as String? ?? '',
  415. );
  416. }