expense_api.dart 15 KB

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