expense_api.dart 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  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 'expense_model.dart';
  14. import 'expense_list_controller.dart';
  15. import '../expense_apply/report_model.dart';
  16. final expenseApiProvider = Provider<ExpenseApi>(
  17. (ref) => ExpenseApi(ref.read(apiClientProvider)),
  18. );
  19. // ═══ 参考数据模型(API 返回) ═══
  20. class CostTypeItem {
  21. final String typeNo;
  22. final String typeName;
  23. final String accNo;
  24. final String accName;
  25. const CostTypeItem({
  26. required this.typeNo,
  27. required this.typeName,
  28. required this.accNo,
  29. required this.accName,
  30. });
  31. factory CostTypeItem.fromJson(Map<String, dynamic> json) => CostTypeItem(
  32. typeNo: json['typeNo'] as String? ?? '',
  33. typeName: json['typeName'] as String? ?? '',
  34. accNo: json['accNo'] as String? ?? '',
  35. accName: json['accName'] as String? ?? '',
  36. );
  37. }
  38. class ProjectCodeItem {
  39. final String objNo;
  40. final String name;
  41. const ProjectCodeItem({required this.objNo, required this.name});
  42. factory ProjectCodeItem.fromJson(Map<String, dynamic> json) =>
  43. ProjectCodeItem(
  44. objNo: json['objNo'] as String? ?? '',
  45. name: json['name'] as String? ?? '',
  46. );
  47. }
  48. class DepartmentItem {
  49. final String dep;
  50. final String name;
  51. const DepartmentItem({required this.dep, required this.name});
  52. factory DepartmentItem.fromJson(Map<String, dynamic> json) => DepartmentItem(
  53. dep: json['dep'] as String? ?? '',
  54. name: json['name'] as String? ?? '',
  55. );
  56. }
  57. class CustomerItem {
  58. final String cusNo;
  59. final String name;
  60. const CustomerItem({required this.cusNo, required this.name});
  61. factory CustomerItem.fromJson(Map<String, dynamic> json) => CustomerItem(
  62. cusNo: json['cusNo'] as String? ?? '',
  63. name: json['name'] as String? ?? '',
  64. );
  65. }
  66. class CurrencyItem {
  67. final String curId;
  68. final String name;
  69. const CurrencyItem({required this.curId, required this.name});
  70. factory CurrencyItem.fromJson(Map<String, dynamic> json) => CurrencyItem(
  71. curId: json['curId'] as String? ?? '',
  72. name: json['name'] as String? ?? '',
  73. );
  74. }
  75. class EmployeeItem {
  76. final String salNo;
  77. final String name;
  78. final String dep;
  79. final String tel;
  80. final String email;
  81. final String bnkNo;
  82. final String bnkId;
  83. final String accName;
  84. const EmployeeItem({
  85. required this.salNo,
  86. required this.name,
  87. this.dep = '',
  88. this.tel = '',
  89. this.email = '',
  90. this.bnkNo = '',
  91. this.bnkId = '',
  92. this.accName = '',
  93. });
  94. factory EmployeeItem.fromJson(Map<String, dynamic> json) => EmployeeItem(
  95. salNo: json['salNo'] as String? ?? '',
  96. name: json['name'] as String? ?? '',
  97. dep: json['dep'] as String? ?? '',
  98. tel: json['tel'] as String? ?? '',
  99. email: json['email'] as String? ?? '',
  100. bnkNo: json['bnkNo'] as String? ?? '',
  101. bnkId: json['bnkId'] as String? ?? '',
  102. accName: json['accName'] as String? ?? '',
  103. );
  104. }
  105. class ExpenseApi {
  106. final ApiClient _client;
  107. ExpenseApi(this._client);
  108. /// 费用报销列表(分页)
  109. Future<PaginatedData<ExpenseModel>> fetchList({
  110. String status = '',
  111. String keyword = '',
  112. String startDate = '',
  113. String endDate = '',
  114. String usr = '',
  115. String sortDir = 'DESC',
  116. int page = 1,
  117. int size = 20,
  118. }) async {
  119. final response = await _client.get<Map<String, dynamic>>(
  120. '/OA/GetExpenseList',
  121. queryParameters: {
  122. 'status': status,
  123. 'keyword': keyword,
  124. 'startDate': startDate,
  125. 'endDate': endDate,
  126. 'usr': usr,
  127. 'sortDir': sortDir,
  128. 'page': page,
  129. 'size': size,
  130. },
  131. );
  132. return PaginatedData.fromJson(response.data!, ExpenseModel.fromJson);
  133. }
  134. /// 费用报销详情(主表+明细)
  135. Future<ExpenseModel> fetchDetail(String billNo) async {
  136. final response = await _client.get<Map<String, dynamic>>(
  137. '/OA/GetExpenseDetail',
  138. queryParameters: {'billNo': billNo},
  139. );
  140. return ExpenseModel.fromJson(response.data!);
  141. }
  142. /// 提交审批,返回报销单号(提取失败时返回 null,不影响主流程)
  143. /// BillSave 返回格式: { callok:true, resultData:{ BIL_NO:"BX20267020005", ... } }
  144. Future<String?> submit(Map<String, dynamic> data) async {
  145. final response = await _client.post<Map<String, dynamic>>(
  146. '/OA/BillSave',
  147. data: {
  148. 'erpCategory': 'MasterService',
  149. 'billId': 'BX',
  150. 'procId': '',
  151. 'data': data,
  152. },
  153. );
  154. final resData = response.data;
  155. if (resData == null) return null;
  156. final resultData = resData['resultData'];
  157. if (resultData is Map<String, dynamic>) {
  158. final bilNo = resultData['BIL_NO'] as String?;
  159. if (bilNo != null && bilNo.isNotEmpty) return bilNo;
  160. }
  161. if (resultData is String && resultData.isNotEmpty) {
  162. try {
  163. final parsed = json.decode(resultData) as Map<String, dynamic>;
  164. final bilNo = parsed['BIL_NO'] as String?;
  165. if (bilNo != null && bilNo.isNotEmpty) return bilNo;
  166. } catch (_) {}
  167. }
  168. final rootBilNo = resData['BIL_NO'] as String?;
  169. if (rootBilNo != null && rootBilNo.isNotEmpty) return rootBilNo;
  170. return null;
  171. }
  172. /// 下载附件文件字节
  173. Future<Uint8List?> downloadAttachment(String id) async {
  174. return await _client.downloadFile(
  175. '/OA/DownloadAttachment',
  176. queryParameters: {'id': id},
  177. );
  178. }
  179. /// 检测附件服务是否可用
  180. Future<bool> checkAttachHealth() async {
  181. try {
  182. final response = await _client.get<Map<String, dynamic>>(
  183. '/OA/CheckAttachHealth',
  184. );
  185. return response.data?['available'] as bool? ?? false;
  186. } catch (_) {
  187. return false;
  188. }
  189. }
  190. /// 财务核销
  191. Future<void> verify(Map<String, dynamic> data) async {
  192. await _client.post('/OA/ExpenseVerify', data: data);
  193. }
  194. /// 费用类别字典
  195. Future<List<CostTypeItem>> getCostTypes({
  196. String keyword = '',
  197. String accNo = '',
  198. }) async {
  199. final response = await _client.get<Map<String, dynamic>>(
  200. '/OA/GetCostTypes',
  201. queryParameters: {
  202. 'keyword': keyword,
  203. 'accNo': accNo,
  204. 'page': 1,
  205. 'size': 100,
  206. },
  207. );
  208. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  209. return list
  210. .map((e) => CostTypeItem.fromJson(e as Map<String, dynamic>))
  211. .toList();
  212. }
  213. /// 项目代号
  214. Future<List<ProjectCodeItem>> getProjectCodes({
  215. String keyword = '',
  216. String billDate = '',
  217. }) async {
  218. final response = await _client.get<Map<String, dynamic>>(
  219. '/OA/GetProjectCodes',
  220. queryParameters: {
  221. 'keyword': keyword,
  222. 'billDate': billDate,
  223. 'page': 1,
  224. 'size': 100,
  225. },
  226. );
  227. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  228. return list
  229. .map((e) => ProjectCodeItem.fromJson(e as Map<String, dynamic>))
  230. .toList();
  231. }
  232. /// 部门
  233. Future<List<DepartmentItem>> getDepartments({
  234. String keyword = '',
  235. bool onlyActive = true,
  236. }) async {
  237. final response = await _client.get<Map<String, dynamic>>(
  238. '/OA/GetDepartments',
  239. queryParameters: {
  240. 'keyword': keyword,
  241. 'onlyActive': onlyActive,
  242. 'page': 1,
  243. 'size': 100,
  244. },
  245. );
  246. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  247. return list
  248. .map((e) => DepartmentItem.fromJson(e as Map<String, dynamic>))
  249. .toList();
  250. }
  251. /// 客户/厂商
  252. Future<List<CustomerItem>> getCustomers({String keyword = ''}) async {
  253. final response = await _client.get<Map<String, dynamic>>(
  254. '/OA/GetCustomers',
  255. queryParameters: {'keyword': keyword, 'page': 1, 'size': 100},
  256. );
  257. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  258. return list
  259. .map((e) => CustomerItem.fromJson(e as Map<String, dynamic>))
  260. .toList();
  261. }
  262. /// 员工查询
  263. Future<List<EmployeeItem>> getEmployees({
  264. String keyword = '',
  265. String salNo = '',
  266. }) async {
  267. final response = await _client.get<Map<String, dynamic>>(
  268. '/OA/GetEmployees',
  269. queryParameters: {
  270. 'keyword': keyword,
  271. 'salNo': salNo,
  272. 'page': 1,
  273. 'size': 100,
  274. },
  275. );
  276. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  277. return list
  278. .map((e) => EmployeeItem.fromJson(e as Map<String, dynamic>))
  279. .toList();
  280. }
  281. /// 可转入报销单的费用申请明细(keyword 模糊匹配单号/事由/部门/申请人/费用类型)
  282. Future<Map<String, dynamic>> getImportableExpenseApplyList({
  283. String keyword = '',
  284. String startDate = '',
  285. String endDate = '',
  286. int page = 1,
  287. int size = 20,
  288. String sortDir = 'DESC',
  289. }) async {
  290. final response = await _client.get<Map<String, dynamic>>(
  291. '/OA/GetImportableExpenseApplyList',
  292. queryParameters: {
  293. 'keyword': keyword,
  294. 'startDate': startDate,
  295. 'endDate': endDate,
  296. 'page': page,
  297. 'size': size,
  298. 'sortDir': sortDir,
  299. },
  300. );
  301. return response.data!;
  302. }
  303. /// 币别
  304. Future<List<CurrencyItem>> getCurrencies({String keyword = ''}) async {
  305. final response = await _client.get<Map<String, dynamic>>(
  306. '/OA/GetCurrencies',
  307. queryParameters: {'keyword': keyword, 'page': 1, 'size': 50},
  308. );
  309. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  310. return list
  311. .map((e) => CurrencyItem.fromJson(e as Map<String, dynamic>))
  312. .toList();
  313. }
  314. /// 币别查询(旧版,保留兼容)
  315. Future<ApiResponse<Map<String, dynamic>>> fetchCurrencies({
  316. String keyword = '',
  317. int page = 1,
  318. int size = 50,
  319. }) async {
  320. return await _client.get(
  321. '/OA/GetCurrencies',
  322. queryParameters: {'keyword': keyword, 'page': page, 'size': size},
  323. );
  324. }
  325. /// 审批列表
  326. Future<Map<String, dynamic>> fetchApprovalList({
  327. required String bilId,
  328. required String status,
  329. String erpCategory = 'MasterService',
  330. String keyword = '',
  331. String startDate = '',
  332. String endDate = '',
  333. int page = 1,
  334. int size = 20,
  335. }) async {
  336. final response = await _client.get<Map<String, dynamic>>(
  337. '/OA/GetApprovalList',
  338. queryParameters: {
  339. 'bilId': bilId,
  340. 'status': status,
  341. 'erpCategory': erpCategory,
  342. 'keyword': keyword,
  343. 'startDate': startDate,
  344. 'endDate': endDate,
  345. 'page': page,
  346. 'size': size,
  347. },
  348. );
  349. return response.data!;
  350. }
  351. /// 审批进度
  352. Future<Map<String, dynamic>> fetchApprovalTimeline(
  353. String bilId,
  354. String bilNo, {
  355. int bilItm = 0,
  356. }) async {
  357. final response = await _client.get<Map<String, dynamic>>(
  358. '/OA/GetApprovalTimeline',
  359. queryParameters: {'bilId': bilId, 'bilNo': bilNo, 'bilItm': bilItm},
  360. );
  361. return response.data!;
  362. }
  363. /// 获取单据附件列表
  364. Future<List<BillAttachment>> getAttachments(
  365. String bilId,
  366. String bilNo, {
  367. int? srcItm,
  368. }) async {
  369. final params = <String, dynamic>{'bilId': bilId, 'bilNo': bilNo};
  370. if (srcItm != null) params['srcItm'] = srcItm;
  371. final response = await _client.get<dynamic>(
  372. '/OA/GetAttachments',
  373. queryParameters: params,
  374. );
  375. final body = response.data;
  376. if (body is! Map) return [];
  377. final result = body['Result'];
  378. if (result is! Map) return [];
  379. final documents = result['documents'];
  380. if (documents is! List) return [];
  381. return documents
  382. .whereType<Map<String, dynamic>>()
  383. .map((e) => BillAttachment.fromJson(e))
  384. .toList();
  385. }
  386. /// 上传附件
  387. /// [metadata] Map: { BIL_ID, BIL_NO, SRCITM, ITM, TAG, EFF_DD, USR, FILENAME, EXT }
  388. Future<Map<String, dynamic>> uploadAttachment(
  389. String filePath,
  390. Map<String, dynamic> metadata,
  391. ) async {
  392. final fileName =
  393. (metadata['FILENAME'] as String?) ?? filePath.split('/').last;
  394. final response = await _client.uploadMultipart<Map<String, dynamic>>(
  395. '/OA/UploadAttachment',
  396. files: [await MultipartFile.fromFile(filePath, filename: fileName)],
  397. extraFields: {'metadata': json.encode(metadata)}, // proper JSON encoding
  398. );
  399. return response.data ?? {};
  400. }
  401. /// 审核执行(通过/驳回/反审核)
  402. Future<Map<String, dynamic>> executeApproval({
  403. required String bilId,
  404. required String bilNo,
  405. int bilItm = 0,
  406. required String action,
  407. String rem = '',
  408. String effDd = '',
  409. String reason = '',
  410. bool isPreToStart = false,
  411. int nodeIndex = -1,
  412. String dataBx = '',
  413. }) async {
  414. final response = await _client.post<Map<String, dynamic>>(
  415. '/OA/ExecuteApproval',
  416. data: {
  417. 'bilId': bilId,
  418. 'bilNo': bilNo,
  419. 'bilItm': bilItm,
  420. 'action': action,
  421. 'rem': rem,
  422. 'effDd': effDd,
  423. 'reason': reason,
  424. 'isPreToStart': isPreToStart,
  425. 'nodeIndex': nodeIndex,
  426. 'dataBx': dataBx,
  427. },
  428. );
  429. return response.data!;
  430. }
  431. /// 费用报销报表
  432. Future<ReportData> getExpenseReport({
  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<Map<String, dynamic>>(
  440. '/OA/GetExpenseReport',
  441. queryParameters: params,
  442. );
  443. return ReportData.fromJson(response.data!);
  444. }
  445. /// 费用报销报表明细(分页)
  446. Future<Map<String, dynamic>> getExpenseReportDetail({
  447. String? startDate,
  448. String? endDate,
  449. int page = 1,
  450. int size = 20,
  451. }) async {
  452. final response = await _client.get<Map<String, dynamic>>(
  453. '/OA/GetExpenseReportDetail',
  454. queryParameters: {
  455. 'startDate': startDate,
  456. 'endDate': endDate,
  457. 'page': page,
  458. 'size': size,
  459. },
  460. );
  461. return response.data!;
  462. }
  463. /// 获取单据状态
  464. Future<Map<String, dynamic>> getBillStatus(String billNo) async {
  465. final response = await _client.get<Map<String, dynamic>>(
  466. '/OA/GetBillStatus',
  467. queryParameters: {'billId': 'BX', 'billNo': billNo},
  468. );
  469. return response.data!;
  470. }
  471. /// 检查当前用户是否拥有指定特殊密码权限
  472. Future<bool> checkSpcRight(String ctrlID) async {
  473. final response = await _client.get<Map<String, dynamic>>(
  474. '/OA/CheckSpcRight',
  475. queryParameters: {'ctrlID': ctrlID},
  476. );
  477. return response.data?['hasRight'] == true;
  478. }
  479. /// 获取指定单据别的附件操作权限
  480. Future<BillFileRights> getBillFileRights(String billId) async {
  481. final response = await _client.get<Map<String, dynamic>>(
  482. '/OA/GetBillFileRights',
  483. queryParameters: {'billId': billId},
  484. );
  485. return BillFileRights.fromJson(response.data!);
  486. }
  487. }
  488. /// 费用报销审批列表项
  489. class ExpenseApprovalListItem {
  490. final String bilId;
  491. final String bilNo;
  492. final String bilDate;
  493. final String applicant;
  494. final String dept;
  495. final double amount;
  496. final String remark;
  497. final String status;
  498. const ExpenseApprovalListItem({
  499. required this.bilId,
  500. required this.bilNo,
  501. required this.bilDate,
  502. required this.applicant,
  503. required this.dept,
  504. required this.amount,
  505. required this.remark,
  506. required this.status,
  507. });
  508. factory ExpenseApprovalListItem.fromJson(Map<String, dynamic> json) =>
  509. ExpenseApprovalListItem(
  510. bilId: json['bilId'] as String? ?? '',
  511. bilNo: json['bilNo'] as String? ?? '',
  512. bilDate: json['bilDate'] as String? ?? '',
  513. applicant: json['applicant'] as String? ?? '',
  514. dept: json['dept'] as String? ?? '',
  515. amount: (json['amount'] as num?)?.toDouble() ?? 0,
  516. remark: json['remark'] as String? ?? '',
  517. status: json['status'] as String? ?? 'pending',
  518. );
  519. ExpenseModel toExpenseModel() => ExpenseModel(
  520. id: bilNo,
  521. expenseNo: bilNo,
  522. expenseDate: bilDate.isNotEmpty ? DateTime.tryParse(bilDate) : null,
  523. applicantId: applicant,
  524. applicantName: applicant,
  525. deptId: dept,
  526. deptName: dept,
  527. totalAmount: amount,
  528. purpose: remark,
  529. status: status == 'approved' ? 'approved' : 'pending',
  530. paymentStatus: 'unpaid',
  531. createTime: DateTime.now(),
  532. updateTime: DateTime.now(),
  533. );
  534. }
  535. final expenseApprovalListProvider = FutureProvider.autoDispose
  536. .family<List<ExpenseModel>, String>((ref, status) async {
  537. ref.watch(expenseRefreshProvider);
  538. ref.watch(expenseDateStartProvider);
  539. ref.watch(expenseDateEndProvider);
  540. ref.watch(expenseKeywordProvider);
  541. final api = ref.read(expenseApiProvider);
  542. final dateStart = ref.read(expenseDateStartProvider);
  543. final dateEnd = ref.read(expenseDateEndProvider);
  544. final keyword = ref.read(expenseKeywordProvider);
  545. final response = await api.fetchApprovalList(
  546. bilId: 'BX',
  547. keyword: keyword,
  548. status: status.isEmpty ? '' : status,
  549. startDate: dateStart != null ? du.DateUtils.formatDate(dateStart) : '',
  550. endDate: dateEnd != null ? du.DateUtils.formatDate(dateEnd) : '',
  551. );
  552. return (response['list'] as List<dynamic>?)
  553. ?.map(
  554. (e) => ExpenseApprovalListItem.fromJson(
  555. e as Map<String, dynamic>,
  556. ).toExpenseModel(),
  557. )
  558. .toList() ??
  559. [];
  560. });
  561. /// "我的报销单" 列表(制单人=当前用户,非审批流)
  562. final expenseMyListProvider = FutureProvider.autoDispose
  563. .family<List<ExpenseModel>, String>((ref, status) async {
  564. ref.watch(expenseRefreshProvider);
  565. ref.watch(expenseDateStartProvider);
  566. ref.watch(expenseDateEndProvider);
  567. ref.watch(expenseKeywordProvider);
  568. final api = ref.read(expenseApiProvider);
  569. final dateStart = ref.read(expenseDateStartProvider);
  570. final dateEnd = ref.read(expenseDateEndProvider);
  571. final keyword = ref.read(expenseKeywordProvider);
  572. final result = await api.fetchList(
  573. keyword: keyword,
  574. startDate: dateStart != null ? du.DateUtils.formatDate(dateStart) : '',
  575. endDate: dateEnd != null ? du.DateUtils.formatDate(dateEnd) : '',
  576. usr: HostAppChannel.usr,
  577. sortDir: ref.watch(expenseSortDirProvider),
  578. );
  579. return result.list;
  580. });