expense_api.dart 17 KB

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