expense_api.dart 16 KB

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