expense_apply_api.dart 15 KB

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