expense_api.dart 14 KB

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