expense_apply_api.dart 16 KB

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