expense_api.dart 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820
  1. import 'dart:convert';
  2. import 'dart:typed_data';
  3. import 'package:dio/dio.dart';
  4. import 'package:flutter_riverpod/flutter_riverpod.dart';
  5. import '../../core/network/api_client.dart';
  6. import '../../core/network/api_response.dart';
  7. import '../../app.dart';
  8. import '../../shared/models/pagination_model.dart';
  9. import '../../shared/models/bill_attachment.dart';
  10. import '../../shared/models/bill_file_rights.dart';
  11. import '../../core/navigation/host_app_channel.dart';
  12. import '../../core/utils/date_utils.dart' as du;
  13. import '../../core/data/api_cache.dart';
  14. import 'expense_model.dart';
  15. import 'expense_list_controller.dart';
  16. import '../expense_apply/report_model.dart';
  17. final expenseApiProvider = Provider<ExpenseApi>(
  18. (ref) => ExpenseApi(ref.read(apiClientProvider)),
  19. );
  20. // ═══ 参考数据模型(API 返回) ═══
  21. class CostTypeItem {
  22. final String typeNo;
  23. final String typeName;
  24. final String accNo;
  25. final String accName;
  26. const CostTypeItem({
  27. required this.typeNo,
  28. required this.typeName,
  29. required this.accNo,
  30. required this.accName,
  31. });
  32. factory CostTypeItem.fromJson(Map<String, dynamic> json) => CostTypeItem(
  33. typeNo: json['typeNo'] as String? ?? '',
  34. typeName: json['typeName'] as String? ?? '',
  35. accNo: json['accNo'] as String? ?? '',
  36. accName: json['accName'] as String? ?? '',
  37. );
  38. }
  39. class CostProjectItem {
  40. final String idx1;
  41. final String name;
  42. final String accNo;
  43. final String accName;
  44. const CostProjectItem({
  45. required this.idx1,
  46. required this.name,
  47. required this.accNo,
  48. required this.accName,
  49. });
  50. factory CostProjectItem.fromJson(Map<String, dynamic> json) =>
  51. CostProjectItem(
  52. idx1: json['idx1'] as String? ?? '',
  53. name: json['name'] as String? ?? '',
  54. accNo: json['accNo'] as String? ?? '',
  55. accName: json['accName'] as String? ?? '',
  56. );
  57. }
  58. class ProjectCodeItem {
  59. final String objNo;
  60. final String name;
  61. const ProjectCodeItem({required this.objNo, required this.name});
  62. factory ProjectCodeItem.fromJson(Map<String, dynamic> json) =>
  63. ProjectCodeItem(
  64. objNo: json['objNo'] as String? ?? '',
  65. name: json['name'] as String? ?? '',
  66. );
  67. }
  68. class DepartmentItem {
  69. final String dep;
  70. final String name;
  71. const DepartmentItem({required this.dep, required this.name});
  72. factory DepartmentItem.fromJson(Map<String, dynamic> json) => DepartmentItem(
  73. dep: json['dep'] as String? ?? '',
  74. name: json['name'] as String? ?? '',
  75. );
  76. }
  77. class CustomerItem {
  78. final String cusNo;
  79. final String name;
  80. const CustomerItem({required this.cusNo, required this.name});
  81. factory CustomerItem.fromJson(Map<String, dynamic> json) => CustomerItem(
  82. cusNo: json['cusNo'] as String? ?? '',
  83. name: json['name'] as String? ?? '',
  84. );
  85. }
  86. class CurrencyItem {
  87. final String curId;
  88. final String name;
  89. final double excRto;
  90. const CurrencyItem({
  91. required this.curId,
  92. required this.name,
  93. this.excRto = 1.0,
  94. });
  95. factory CurrencyItem.fromJson(Map<String, dynamic> json) => CurrencyItem(
  96. curId: json['curId'] as String? ?? '',
  97. name: json['name'] as String? ?? '',
  98. excRto: (json['excRto'] as num?)?.toDouble() ?? 1.0,
  99. );
  100. }
  101. class EmployeeItem {
  102. final String salNo;
  103. final String name;
  104. final String dep;
  105. final String tel;
  106. final String email;
  107. final String bnkNo;
  108. final String bnkId;
  109. final String accName;
  110. const EmployeeItem({
  111. required this.salNo,
  112. required this.name,
  113. this.dep = '',
  114. this.tel = '',
  115. this.email = '',
  116. this.bnkNo = '',
  117. this.bnkId = '',
  118. this.accName = '',
  119. });
  120. factory EmployeeItem.fromJson(Map<String, dynamic> json) => EmployeeItem(
  121. salNo: json['salNo'] as String? ?? '',
  122. name: json['name'] as String? ?? '',
  123. dep: json['dep'] as String? ?? '',
  124. tel: json['tel'] as String? ?? '',
  125. email: json['email'] as String? ?? '',
  126. bnkNo: json['bnkNo'] as String? ?? '',
  127. bnkId: json['bnkId'] as String? ?? '',
  128. accName: json['accName'] as String? ?? '',
  129. );
  130. }
  131. class ExpenseApi {
  132. final ApiClient _client;
  133. final _cache = ApiCache(
  134. keyPrefix:
  135. '${HostAppChannel.sn}_${HostAppChannel.compNo}_${HostAppChannel.usr}',
  136. );
  137. ExpenseApi(this._client);
  138. /// 清除所有基础资料缓存(picker 下拉刷新时调用)。
  139. void clearRefCache() => _cache.clear();
  140. /// 费用报销列表(分页)
  141. Future<PaginatedData<ExpenseModel>> fetchList({
  142. String status = '',
  143. String keyword = '',
  144. String startDate = '',
  145. String endDate = '',
  146. String usr = '',
  147. String sortDir = 'DESC',
  148. int page = 1,
  149. int size = 20,
  150. }) async {
  151. final response = await _client.get<Map<String, dynamic>>(
  152. '/OA/GetExpenseList',
  153. queryParameters: {
  154. 'status': status,
  155. 'keyword': keyword,
  156. 'startDate': startDate,
  157. 'endDate': endDate,
  158. 'usr': usr,
  159. 'sortDir': sortDir,
  160. 'page': page,
  161. 'size': size,
  162. },
  163. );
  164. return PaginatedData.fromJson(response.data!, ExpenseModel.fromJson);
  165. }
  166. /// 费用报销详情(主表+明细)
  167. Future<ExpenseModel> fetchDetail(String billNo) async {
  168. final response = await _client.get<Map<String, dynamic>>(
  169. '/OA/GetExpenseDetail',
  170. queryParameters: {'billNo': billNo},
  171. );
  172. return ExpenseModel.fromJson(response.data!);
  173. }
  174. /// 提交审批,返回报销单号(提取失败时返回 null,不影响主流程)
  175. /// BillSave 返回格式: { callok:true, resultData:{ BIL_NO:"BX20267020005", ... } }
  176. Future<String?> submit(Map<String, dynamic> data) async {
  177. final response = await _client.post<Map<String, dynamic>>(
  178. '/OA/BillSave',
  179. data: {
  180. 'erpCategory': 'MasterService',
  181. 'billId': 'BX',
  182. 'procId': '',
  183. 'data': data,
  184. },
  185. );
  186. final resData = response.data;
  187. if (resData == null) return null;
  188. final resultData = resData['resultData'];
  189. if (resultData is Map<String, dynamic>) {
  190. final bilNo = resultData['BIL_NO'] as String?;
  191. if (bilNo != null && bilNo.isNotEmpty) return bilNo;
  192. }
  193. if (resultData is String && resultData.isNotEmpty) {
  194. try {
  195. final parsed = json.decode(resultData) as Map<String, dynamic>;
  196. final bilNo = parsed['BIL_NO'] as String?;
  197. if (bilNo != null && bilNo.isNotEmpty) return bilNo;
  198. } catch (_) {}
  199. }
  200. final rootBilNo = resData['BIL_NO'] as String?;
  201. if (rootBilNo != null && rootBilNo.isNotEmpty) return rootBilNo;
  202. return null;
  203. }
  204. /// 提交审核流
  205. Future<bool> shSubmit({
  206. required String bilNo,
  207. required String bilDd,
  208. String dep = '',
  209. String rem = '',
  210. String usr = '',
  211. bool isCancel = false,
  212. }) async {
  213. try {
  214. final response = await _client.post<Map<String, dynamic>>(
  215. '/OA/SHSubmit',
  216. data: {
  217. 'erpCategory': 'MasterService',
  218. 'bilId': 'BX',
  219. 'bilNo': bilNo,
  220. 'bilDd': bilDd,
  221. 'dep': dep,
  222. 'rem': rem,
  223. 'usr': usr,
  224. 'isCancel': isCancel,
  225. },
  226. );
  227. return response.data?['code'] == 0;
  228. } catch (_) {
  229. return false;
  230. }
  231. }
  232. /// 下载附件文件字节
  233. Future<Uint8List?> downloadAttachment(String id) async {
  234. return await _client.downloadFile(
  235. '/OA/DownloadAttachment',
  236. queryParameters: {'id': id},
  237. );
  238. }
  239. /// 检测附件服务是否可用
  240. Future<bool> checkAttachHealth() async {
  241. try {
  242. final response = await _client.get<Map<String, dynamic>>(
  243. '/OA/CheckAttachHealth',
  244. );
  245. return response.data?['available'] as bool? ?? false;
  246. } catch (_) {
  247. return false;
  248. }
  249. }
  250. /// 财务核销
  251. Future<void> verify(Map<String, dynamic> data) async {
  252. await _client.post('/OA/ExpenseVerify', data: data);
  253. }
  254. /// 费用类别字典
  255. Future<List<CostTypeItem>> getCostTypes({
  256. String keyword = '',
  257. String accNo = '',
  258. int page = 1,
  259. int size = 100,
  260. }) async {
  261. final cacheKey = 'getCostTypes_${keyword}_${accNo}_$page';
  262. return _cache.getOrFetch(cacheKey, () async {
  263. final response = await _client.get<Map<String, dynamic>>(
  264. '/OA/GetCostTypes',
  265. queryParameters: {
  266. 'keyword': keyword,
  267. 'accNo': accNo,
  268. 'page': page,
  269. 'size': size,
  270. },
  271. );
  272. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  273. return list
  274. .map((e) => CostTypeItem.fromJson(e as Map<String, dynamic>))
  275. .toList();
  276. });
  277. }
  278. /// 费用项目字典(分页)
  279. Future<List<CostProjectItem>> getCostProjects({
  280. String keyword = '',
  281. String accNo = '',
  282. bool onlyActive = true,
  283. int page = 1,
  284. int size = 20,
  285. }) async {
  286. final cacheKey = 'getCostProjects_${keyword}_${accNo}_${onlyActive}_$page';
  287. return _cache.getOrFetch(cacheKey, () async {
  288. final response = await _client.get<Map<String, dynamic>>(
  289. '/OA/GetCostProjects',
  290. queryParameters: {
  291. 'keyword': keyword,
  292. 'accNo': accNo,
  293. 'onlyActive': onlyActive,
  294. 'page': page,
  295. 'size': size,
  296. },
  297. );
  298. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  299. return list
  300. .map((e) => CostProjectItem.fromJson(e as Map<String, dynamic>))
  301. .toList();
  302. });
  303. }
  304. /// 项目代号
  305. Future<List<ProjectCodeItem>> getProjectCodes({
  306. String keyword = '',
  307. String billDate = '',
  308. int page = 1,
  309. int size = 100,
  310. }) async {
  311. final cacheKey = 'getProjectCodes_${keyword}_${billDate}_$page';
  312. return _cache.getOrFetch(cacheKey, () async {
  313. final response = await _client.get<Map<String, dynamic>>(
  314. '/OA/GetProjectCodes',
  315. queryParameters: {
  316. 'keyword': keyword,
  317. 'billDate': billDate,
  318. 'page': page,
  319. 'size': size,
  320. },
  321. );
  322. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  323. return list
  324. .map((e) => ProjectCodeItem.fromJson(e as Map<String, dynamic>))
  325. .toList();
  326. });
  327. }
  328. /// 部门
  329. Future<List<DepartmentItem>> getDepartments({
  330. String keyword = '',
  331. bool onlyActive = true,
  332. int page = 1,
  333. int size = 100,
  334. }) async {
  335. final cacheKey = 'getDepartments_${keyword}_${onlyActive}_$page';
  336. return _cache.getOrFetch(cacheKey, () async {
  337. final response = await _client.get<Map<String, dynamic>>(
  338. '/OA/GetDepartments',
  339. queryParameters: {
  340. 'keyword': keyword,
  341. 'onlyActive': onlyActive,
  342. 'page': page,
  343. 'size': size,
  344. },
  345. );
  346. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  347. return list
  348. .map((e) => DepartmentItem.fromJson(e as Map<String, dynamic>))
  349. .toList();
  350. });
  351. }
  352. /// 客户/厂商
  353. Future<List<CustomerItem>> getCustomers({
  354. String keyword = '',
  355. int page = 1,
  356. int size = 100,
  357. }) async {
  358. final cacheKey = 'getCustomers_${keyword}_$page';
  359. return _cache.getOrFetch(cacheKey, () async {
  360. final response = await _client.get<Map<String, dynamic>>(
  361. '/OA/GetCustomers',
  362. queryParameters: {'keyword': keyword, 'page': page, 'size': size},
  363. );
  364. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  365. return list
  366. .map((e) => CustomerItem.fromJson(e as Map<String, dynamic>))
  367. .toList();
  368. });
  369. }
  370. /// 员工查询
  371. Future<List<EmployeeItem>> getEmployees({
  372. String keyword = '',
  373. String salNo = '',
  374. int page = 1,
  375. int size = 100,
  376. }) async {
  377. final cacheKey = 'getEmployees_${keyword}_${salNo}_$page';
  378. return _cache.getOrFetch(cacheKey, () async {
  379. final response = await _client.get<Map<String, dynamic>>(
  380. '/OA/GetEmployees',
  381. queryParameters: {
  382. 'keyword': keyword,
  383. 'salNo': salNo,
  384. 'page': page,
  385. 'size': size,
  386. },
  387. );
  388. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  389. return list
  390. .map((e) => EmployeeItem.fromJson(e as Map<String, dynamic>))
  391. .toList();
  392. });
  393. }
  394. /// 可转入报销单的费用申请明细(keyword 模糊匹配单号/事由/部门/申请人/费用类型)
  395. Future<Map<String, dynamic>> getImportableExpenseApplyList({
  396. String keyword = '',
  397. String startDate = '',
  398. String endDate = '',
  399. int page = 1,
  400. int size = 20,
  401. String sortDir = 'DESC',
  402. }) async {
  403. final response = await _client.get<Map<String, dynamic>>(
  404. '/OA/GetImportableExpenseApplyList',
  405. queryParameters: {
  406. 'keyword': keyword,
  407. 'startDate': startDate,
  408. 'endDate': endDate,
  409. 'page': page,
  410. 'size': size,
  411. 'sortDir': sortDir,
  412. },
  413. );
  414. return response.data!;
  415. }
  416. /// 币别
  417. Future<List<CurrencyItem>> getCurrencies({
  418. String keyword = '',
  419. int page = 1,
  420. int size = 50,
  421. }) async {
  422. final cacheKey = 'getCurrencies_${keyword}_$page';
  423. return _cache.getOrFetch(cacheKey, () async {
  424. final response = await _client.get<Map<String, dynamic>>(
  425. '/OA/GetCurrencies',
  426. queryParameters: {'keyword': keyword, 'page': page, 'size': size},
  427. );
  428. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  429. return list
  430. .map((e) => CurrencyItem.fromJson(e as Map<String, dynamic>))
  431. .toList();
  432. });
  433. }
  434. /// 币别查询(旧版,保留兼容)
  435. Future<ApiResponse<Map<String, dynamic>>> fetchCurrencies({
  436. String keyword = '',
  437. int page = 1,
  438. int size = 50,
  439. }) async {
  440. return await _client.get(
  441. '/OA/GetCurrencies',
  442. queryParameters: {'keyword': keyword, 'page': page, 'size': size},
  443. );
  444. }
  445. /// 审批列表
  446. Future<Map<String, dynamic>> fetchApprovalList({
  447. required String bilId,
  448. required String status,
  449. String erpCategory = 'MasterService',
  450. String keyword = '',
  451. String startDate = '',
  452. String endDate = '',
  453. int page = 1,
  454. int size = 20,
  455. }) async {
  456. final response = await _client.get<Map<String, dynamic>>(
  457. '/OA/GetApprovalList',
  458. queryParameters: {
  459. 'bilId': bilId,
  460. 'status': status,
  461. 'erpCategory': erpCategory,
  462. 'keyword': keyword,
  463. 'startDate': startDate,
  464. 'endDate': endDate,
  465. 'page': page,
  466. 'size': size,
  467. },
  468. );
  469. return response.data!;
  470. }
  471. /// 审批进度
  472. Future<Map<String, dynamic>> fetchApprovalTimeline(
  473. String bilId,
  474. String bilNo, {
  475. int bilItm = 0,
  476. }) async {
  477. final response = await _client.get<Map<String, dynamic>>(
  478. '/OA/GetApprovalTimeline',
  479. queryParameters: {'bilId': bilId, 'bilNo': bilNo, 'bilItm': bilItm},
  480. );
  481. return response.data!;
  482. }
  483. /// 获取单据附件列表
  484. Future<List<BillAttachment>> getAttachments(
  485. String bilId,
  486. String bilNo, {
  487. int? srcItm,
  488. }) async {
  489. final params = <String, dynamic>{'bilId': bilId, 'bilNo': bilNo};
  490. if (srcItm != null) params['srcItm'] = srcItm;
  491. final response = await _client.get<dynamic>(
  492. '/OA/GetAttachments',
  493. queryParameters: params,
  494. );
  495. final body = response.data;
  496. if (body is! Map) return [];
  497. final result = body['Result'];
  498. if (result is! Map) return [];
  499. final documents = result['documents'];
  500. if (documents is! List) return [];
  501. return documents
  502. .whereType<Map<String, dynamic>>()
  503. .map((e) => BillAttachment.fromJson(e))
  504. .toList();
  505. }
  506. /// 上传附件
  507. /// [metadata] Map: { BIL_ID, BIL_NO, SRCITM, ITM, TAG, EFF_DD, USR, FILENAME, EXT }
  508. Future<Map<String, dynamic>> uploadAttachment(
  509. String filePath,
  510. Map<String, dynamic> metadata,
  511. ) async {
  512. final fileName =
  513. (metadata['FILENAME'] as String?) ?? filePath.split('/').last;
  514. final response = await _client.uploadMultipart<Map<String, dynamic>>(
  515. '/OA/UploadAttachment',
  516. files: [await MultipartFile.fromFile(filePath, filename: fileName)],
  517. extraFields: {'metadata': json.encode(metadata)}, // proper JSON encoding
  518. );
  519. return response.data ?? {};
  520. }
  521. /// 审核执行(通过/驳回/反审核)
  522. Future<Map<String, dynamic>> executeApproval({
  523. required String bilId,
  524. required String bilNo,
  525. int bilItm = 0,
  526. required String action,
  527. String rem = '',
  528. String effDd = '',
  529. String reason = '',
  530. bool isPreToStart = false,
  531. int nodeIndex = -1,
  532. String dataBx = '',
  533. }) async {
  534. final response = await _client.post<Map<String, dynamic>>(
  535. '/OA/ExecuteApproval',
  536. data: {
  537. 'bilId': bilId,
  538. 'bilNo': bilNo,
  539. 'bilItm': bilItm,
  540. 'action': action,
  541. 'rem': rem,
  542. 'effDd': effDd,
  543. 'reason': reason,
  544. 'isPreToStart': isPreToStart,
  545. 'nodeIndex': nodeIndex,
  546. 'dataBx': dataBx,
  547. },
  548. );
  549. return response.data!;
  550. }
  551. /// 费用报销报表
  552. Future<ReportData> getExpenseReport({
  553. String? startDate,
  554. String? endDate,
  555. }) async {
  556. final params = <String, dynamic>{};
  557. if (startDate != null) params['startDate'] = startDate;
  558. if (endDate != null) params['endDate'] = endDate;
  559. final response = await _client.get<Map<String, dynamic>>(
  560. '/OA/GetExpenseReport',
  561. queryParameters: params,
  562. );
  563. return ReportData.fromJson(response.data!);
  564. }
  565. /// 费用报销报表明细(分页)
  566. Future<Map<String, dynamic>> getExpenseReportDetail({
  567. String? startDate,
  568. String? endDate,
  569. int page = 1,
  570. int size = 20,
  571. }) async {
  572. final response = await _client.get<Map<String, dynamic>>(
  573. '/OA/GetExpenseReportDetail',
  574. queryParameters: {
  575. 'startDate': startDate,
  576. 'endDate': endDate,
  577. 'page': page,
  578. 'size': size,
  579. },
  580. );
  581. return response.data!;
  582. }
  583. /// 下属报销金额对比
  584. Future<List<SubordinateReportItem>> getExpenseSubordinateReport({
  585. String? startDate,
  586. String? endDate,
  587. }) async {
  588. final params = <String, dynamic>{};
  589. if (startDate != null) params['startDate'] = startDate;
  590. if (endDate != null) params['endDate'] = endDate;
  591. final response = await _client.get<dynamic>(
  592. '/OA/GetExpenseSubordinateReport',
  593. queryParameters: params,
  594. );
  595. final list =
  596. (response.data as List<dynamic>?)
  597. ?.map(
  598. (e) => SubordinateReportItem.fromJson(e as Map<String, dynamic>),
  599. )
  600. .toList() ??
  601. [];
  602. return list;
  603. }
  604. /// 费用报销单表身明细(分页)
  605. Future<Map<String, dynamic>> getExpenseBodyReport({
  606. String? startDate,
  607. String? endDate,
  608. int page = 1,
  609. int size = 20,
  610. }) async {
  611. final response = await _client.get<Map<String, dynamic>>(
  612. '/OA/GetExpenseBodyReport',
  613. queryParameters: {
  614. // ignore: use_null_aware_elements
  615. if (startDate != null) 'startDate': startDate,
  616. // ignore: use_null_aware_elements
  617. if (endDate != null) 'endDate': endDate,
  618. 'page': page,
  619. 'size': size,
  620. },
  621. );
  622. return response.data!;
  623. }
  624. /// 审核过程明细
  625. Future<List<Map<String, dynamic>>> getAuditTrail(
  626. String billId,
  627. String billNo,
  628. ) async {
  629. final response = await _client.get(
  630. '/OA/GetAuditTrail',
  631. queryParameters: {'billId': billId, 'billNo': billNo},
  632. );
  633. final list =
  634. (response.data as List<dynamic>?)?.cast<Map<String, dynamic>>() ?? [];
  635. return list;
  636. }
  637. /// 会计科目树(级联选择器数据源)
  638. Future<dynamic> getAcctSubjects() async {
  639. final response = await _client.get('/OA/GetAcctSubjects');
  640. return response.data;
  641. }
  642. /// 获取单据状态
  643. Future<Map<String, dynamic>> getBillAuditConfig(String bilId) async {
  644. final response = await _client.get<Map<String, dynamic>>(
  645. '/OA/GetBillAuditConfig',
  646. queryParameters: {'bilId': bilId},
  647. );
  648. return response.data!;
  649. }
  650. Future<Map<String, dynamic>> getBillStatus(String billNo) async {
  651. final response = await _client.get<Map<String, dynamic>>(
  652. '/OA/GetBillStatus',
  653. queryParameters: {'billId': 'BX', 'billNo': billNo},
  654. );
  655. return response.data!;
  656. }
  657. /// 检查当前用户是否拥有指定特殊密码权限
  658. Future<bool> checkSpcRight(String ctrlID) async {
  659. final response = await _client.get<Map<String, dynamic>>(
  660. '/OA/CheckSpcRight',
  661. queryParameters: {'ctrlID': ctrlID},
  662. );
  663. return response.data?['hasRight'] == true;
  664. }
  665. /// 获取指定单据别的附件操作权限
  666. Future<BillFileRights> getBillFileRights(String billId) async {
  667. final response = await _client.get<Map<String, dynamic>>(
  668. '/OA/GetBillFileRights',
  669. queryParameters: {'billId': billId},
  670. );
  671. return BillFileRights.fromJson(response.data!);
  672. }
  673. }
  674. /// 费用报销审批列表项
  675. class ExpenseApprovalListItem {
  676. final String bilId;
  677. final String bilNo;
  678. final String bilDate;
  679. final String applicant;
  680. final String dept;
  681. final double amount;
  682. final String remark;
  683. final String status;
  684. const ExpenseApprovalListItem({
  685. required this.bilId,
  686. required this.bilNo,
  687. required this.bilDate,
  688. required this.applicant,
  689. required this.dept,
  690. required this.amount,
  691. required this.remark,
  692. required this.status,
  693. });
  694. factory ExpenseApprovalListItem.fromJson(Map<String, dynamic> json) =>
  695. ExpenseApprovalListItem(
  696. bilId: json['bilId'] as String? ?? '',
  697. bilNo: json['bilNo'] as String? ?? '',
  698. bilDate: json['bilDate'] as String? ?? '',
  699. applicant: json['applicant'] as String? ?? '',
  700. dept: json['dept'] as String? ?? '',
  701. amount: (json['amount'] as num?)?.toDouble() ?? 0,
  702. remark: json['remark'] as String? ?? '',
  703. status: json['status'] as String? ?? 'pending',
  704. );
  705. ExpenseModel toExpenseModel() => ExpenseModel(
  706. id: bilNo,
  707. expenseNo: bilNo,
  708. expenseDate: bilDate.isNotEmpty ? DateTime.tryParse(bilDate) : null,
  709. applicantId: applicant,
  710. applicantName: applicant,
  711. deptId: dept,
  712. deptName: dept,
  713. totalAmount: amount,
  714. purpose: remark,
  715. status: status == 'approved' ? 'approved' : 'pending',
  716. paymentStatus: 'unpaid',
  717. createTime: DateTime.now(),
  718. updateTime: DateTime.now(),
  719. );
  720. }
  721. final expenseApprovalListProvider = FutureProvider.autoDispose
  722. .family<List<ExpenseModel>, String>((ref, status) async {
  723. ref.watch(expenseRefreshProvider);
  724. ref.watch(expenseDateStartProvider);
  725. ref.watch(expenseDateEndProvider);
  726. ref.watch(expenseKeywordProvider);
  727. final api = ref.read(expenseApiProvider);
  728. final dateStart = ref.read(expenseDateStartProvider);
  729. final dateEnd = ref.read(expenseDateEndProvider);
  730. final keyword = ref.read(expenseKeywordProvider);
  731. final response = await api.fetchApprovalList(
  732. bilId: 'BX',
  733. keyword: keyword,
  734. status: status.isEmpty ? '' : status,
  735. startDate: dateStart != null ? du.DateUtils.formatDate(dateStart) : '',
  736. endDate: dateEnd != null ? du.DateUtils.formatDate(dateEnd) : '',
  737. );
  738. return (response['list'] as List<dynamic>?)
  739. ?.map(
  740. (e) => ExpenseApprovalListItem.fromJson(
  741. e as Map<String, dynamic>,
  742. ).toExpenseModel(),
  743. )
  744. .toList() ??
  745. [];
  746. });
  747. /// "我的报销单" 列表(制单人=当前用户,非审批流)
  748. final expenseMyListProvider = FutureProvider.autoDispose
  749. .family<List<ExpenseModel>, String>((ref, status) async {
  750. ref.watch(expenseRefreshProvider);
  751. ref.watch(expenseDateStartProvider);
  752. ref.watch(expenseDateEndProvider);
  753. ref.watch(expenseKeywordProvider);
  754. final api = ref.read(expenseApiProvider);
  755. final dateStart = ref.read(expenseDateStartProvider);
  756. final dateEnd = ref.read(expenseDateEndProvider);
  757. final keyword = ref.read(expenseKeywordProvider);
  758. final result = await api.fetchList(
  759. keyword: keyword,
  760. startDate: dateStart != null ? du.DateUtils.formatDate(dateStart) : '',
  761. endDate: dateEnd != null ? du.DateUtils.formatDate(dateEnd) : '',
  762. usr: HostAppChannel.usr,
  763. sortDir: ref.watch(expenseSortDirProvider),
  764. );
  765. return result.list;
  766. });