expense_apply_api.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. import 'dart:convert';
  2. import 'package:flutter/foundation.dart';
  3. import 'package:dio/dio.dart';
  4. import 'package:flutter_riverpod/flutter_riverpod.dart';
  5. import '../../core/network/api_client.dart';
  6. import '../../app.dart';
  7. import '../../shared/models/pagination_model.dart';
  8. import '../../shared/models/bill_attachment.dart';
  9. import '../../shared/models/bill_file_rights.dart';
  10. import 'expense_apply_model.dart';
  11. import 'report_model.dart';
  12. final expenseApplyApiProvider = Provider<ExpenseApplyApi>(
  13. (ref) => ExpenseApplyApi(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({
  22. required this.typeNo,
  23. required this.typeName,
  24. required this.accNo,
  25. required this.accName,
  26. });
  27. factory CostTypeItem.fromJson(Map<String, dynamic> json) => CostTypeItem(
  28. typeNo: json['typeNo'] as String? ?? '',
  29. typeName: json['typeName'] as String? ?? '',
  30. accNo: json['accNo'] as String? ?? '',
  31. accName: json['accName'] as String? ?? '',
  32. );
  33. }
  34. class ProjectCodeItem {
  35. final String objNo;
  36. final String name;
  37. const ProjectCodeItem({required this.objNo, required this.name});
  38. factory ProjectCodeItem.fromJson(Map<String, dynamic> json) =>
  39. ProjectCodeItem(
  40. objNo: json['objNo'] as String? ?? '',
  41. name: json['name'] as String? ?? '',
  42. );
  43. }
  44. class DepartmentItem {
  45. final String dep;
  46. final String name;
  47. const DepartmentItem({required this.dep, required this.name});
  48. factory DepartmentItem.fromJson(Map<String, dynamic> json) => DepartmentItem(
  49. dep: json['dep'] as String? ?? '',
  50. name: json['name'] as String? ?? '',
  51. );
  52. }
  53. class ExpenseApplyApi {
  54. final ApiClient _client;
  55. ExpenseApplyApi(this._client);
  56. /// 费用申请列表(分页)
  57. Future<PaginatedData<ExpenseApplyModel>> fetchList({
  58. String status = '',
  59. String keyword = '',
  60. String startDate = '',
  61. String endDate = '',
  62. String usr = '',
  63. String sortDir = 'DESC',
  64. int page = 1,
  65. int size = 20,
  66. }) async {
  67. final response = await _client.get<Map<String, dynamic>>(
  68. '/OA/GetExpenseApplyList',
  69. queryParameters: {
  70. 'status': status,
  71. 'keyword': keyword,
  72. 'startDate': startDate,
  73. 'endDate': endDate,
  74. 'usr': usr,
  75. 'sortDir': sortDir,
  76. 'page': page,
  77. 'size': size,
  78. },
  79. );
  80. return PaginatedData.fromJson(response.data!, ExpenseApplyModel.fromJson);
  81. }
  82. /// 费用申请详情(主表+明细)
  83. Future<ExpenseApplyModel> fetchDetail(String billNo) async {
  84. final response = await _client.get<Map<String, dynamic>>(
  85. '/OA/GetExpenseApplyDetail',
  86. queryParameters: {'billNo': billNo},
  87. );
  88. return ExpenseApplyModel.fromJson(response.data!);
  89. }
  90. /// 费用类别字典
  91. Future<List<CostTypeItem>> getCostTypes({
  92. String keyword = '',
  93. String accNo = '',
  94. }) async {
  95. final response = await _client.get<Map<String, dynamic>>(
  96. '/OA/GetCostTypes',
  97. queryParameters: {
  98. 'keyword': keyword,
  99. 'accNo': accNo,
  100. 'page': 1,
  101. 'size': 100,
  102. },
  103. );
  104. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  105. return list
  106. .map((e) => CostTypeItem.fromJson(e as Map<String, dynamic>))
  107. .toList();
  108. }
  109. /// 项目代号
  110. Future<List<ProjectCodeItem>> getProjectCodes({
  111. String keyword = '',
  112. String billDate = '',
  113. }) async {
  114. final response = await _client.get<Map<String, dynamic>>(
  115. '/OA/GetProjectCodes',
  116. queryParameters: {
  117. 'keyword': keyword,
  118. 'billDate': billDate,
  119. 'page': 1,
  120. 'size': 100,
  121. },
  122. );
  123. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  124. return list
  125. .map((e) => ProjectCodeItem.fromJson(e as Map<String, dynamic>))
  126. .toList();
  127. }
  128. /// 部门
  129. Future<List<DepartmentItem>> getDepartments({
  130. String keyword = '',
  131. bool onlyActive = true,
  132. }) async {
  133. final response = await _client.get<Map<String, dynamic>>(
  134. '/OA/GetDepartments',
  135. queryParameters: {
  136. 'keyword': keyword,
  137. 'onlyActive': onlyActive,
  138. 'page': 1,
  139. 'size': 100,
  140. },
  141. );
  142. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  143. return list
  144. .map((e) => DepartmentItem.fromJson(e as Map<String, dynamic>))
  145. .toList();
  146. }
  147. /// 会计科目树(级联选择器数据源)
  148. Future<dynamic> getAcctSubjects() async {
  149. final response = await _client.get('/OA/GetAcctSubjects');
  150. return response.data;
  151. }
  152. /// 提交审批,返回申请单号(提取失败时返回 null,不影响主流程)
  153. /// BillSave 返回格式: { callok:true, resultData:{ BIL_NO:"AE20267020005", ... } }
  154. Future<String?> submit(Map<String, dynamic> data) async {
  155. final response = await _client.post<Map<String, dynamic>>(
  156. '/OA/BillSave',
  157. data: {
  158. 'erpCategory': 'MasterService',
  159. 'billId': 'AE',
  160. 'procId': '',
  161. 'data': data,
  162. },
  163. );
  164. final resData = response.data;
  165. if (resData == null) return null;
  166. // 从 resultData 中取(BillSave 实际返回的嵌套格式)
  167. final resultData = resData['resultData'];
  168. if (resultData is Map<String, dynamic>) {
  169. // BIL_NO 是 BillSave 返回的通用单号字段(不区分 AE/BX)
  170. final bilNo = resultData['BIL_NO'] as String?;
  171. if (bilNo != null && bilNo.isNotEmpty) return bilNo;
  172. }
  173. // resultData 可能是 JSON 字符串
  174. if (resultData is String && resultData.isNotEmpty) {
  175. try {
  176. final parsed = json.decode(resultData) as Map<String, dynamic>;
  177. final bilNo = parsed['BIL_NO'] as String?;
  178. if (bilNo != null && bilNo.isNotEmpty) return bilNo;
  179. } catch (_) {}
  180. }
  181. // 兜底: resData 根级 BIL_NO
  182. final rootBilNo = resData['BIL_NO'] as String?;
  183. if (rootBilNo != null && rootBilNo.isNotEmpty) return rootBilNo;
  184. return null;
  185. }
  186. /// 下载附件文件字节
  187. Future<Uint8List?> downloadAttachment(String id) async {
  188. return await _client.downloadFile(
  189. '/OA/DownloadAttachment',
  190. queryParameters: {'id': id},
  191. );
  192. }
  193. /// 检测附件服务是否可用
  194. Future<bool> checkAttachHealth() async {
  195. try {
  196. final response = await _client.get<Map<String, dynamic>>(
  197. '/OA/CheckAttachHealth',
  198. );
  199. debugPrint('[checkAttachHealth] response.data: ${response.data}');
  200. debugPrint(
  201. '[checkAttachHealth] response.data type: ${response.data.runtimeType}',
  202. );
  203. final available = response.data?['available'] as bool? ?? false;
  204. debugPrint('[checkAttachHealth] available: $available');
  205. return available;
  206. } catch (e) {
  207. debugPrint('[checkAttachHealth] error: $e');
  208. return false;
  209. }
  210. }
  211. /// 审批进度
  212. Future<Map<String, dynamic>> fetchApprovalTimeline(
  213. String bilId,
  214. String bilNo, {
  215. int bilItm = 0,
  216. }) async {
  217. final response = await _client.get<Map<String, dynamic>>(
  218. '/OA/GetApprovalTimeline',
  219. queryParameters: {'bilId': bilId, 'bilNo': bilNo, 'bilItm': bilItm},
  220. );
  221. return response.data!;
  222. }
  223. /// 获取单据附件列表
  224. Future<List<BillAttachment>> getAttachments(
  225. String bilId,
  226. String bilNo, {
  227. int? srcItm,
  228. }) async {
  229. final params = <String, dynamic>{'bilId': bilId, 'bilNo': bilNo};
  230. if (srcItm != null) params['srcItm'] = srcItm;
  231. final response = await _client.get<dynamic>(
  232. '/OA/GetAttachments',
  233. queryParameters: params,
  234. );
  235. final body = response.data;
  236. if (body is! Map) return [];
  237. final result = body['Result'];
  238. if (result is! Map) return [];
  239. final documents = result['documents'];
  240. if (documents is! List) return [];
  241. return documents
  242. .whereType<Map<String, dynamic>>()
  243. .map((e) => BillAttachment.fromJson(e))
  244. .toList();
  245. }
  246. /// 上传附件
  247. Future<Map<String, dynamic>> uploadAttachment(
  248. String filePath,
  249. Map<String, dynamic> metadata,
  250. ) async {
  251. final fileName =
  252. (metadata['FILENAME'] as String?) ?? filePath.split('/').last;
  253. final response = await _client.uploadMultipart<Map<String, dynamic>>(
  254. '/OA/UploadAttachment',
  255. files: [await MultipartFile.fromFile(filePath, filename: fileName)],
  256. extraFields: {'metadata': json.encode(metadata)},
  257. );
  258. return response.data ?? {};
  259. }
  260. /// 审核执行(通过/驳回/反审核)
  261. Future<Map<String, dynamic>> executeApproval({
  262. required String bilId,
  263. required String bilNo,
  264. int bilItm = 0,
  265. required String action,
  266. String rem = '',
  267. String effDd = '',
  268. String reason = '',
  269. bool isPreToStart = false,
  270. int nodeIndex = -1,
  271. String dataBx = '',
  272. }) async {
  273. final response = await _client.post<Map<String, dynamic>>(
  274. '/OA/ExecuteApproval',
  275. data: {
  276. 'bilId': bilId,
  277. 'bilNo': bilNo,
  278. 'bilItm': bilItm,
  279. 'action': action,
  280. 'rem': rem,
  281. 'effDd': effDd,
  282. 'reason': reason,
  283. 'isPreToStart': isPreToStart,
  284. 'nodeIndex': nodeIndex,
  285. 'dataBx': dataBx,
  286. },
  287. );
  288. return response.data!;
  289. }
  290. /// 费用申请报表
  291. Future<ReportData> getExpenseApplyReport({
  292. String? startDate,
  293. String? endDate,
  294. }) async {
  295. final params = <String, dynamic>{};
  296. if (startDate != null) params['startDate'] = startDate;
  297. if (endDate != null) params['endDate'] = endDate;
  298. final response = await _client.get<Map<String, dynamic>>(
  299. '/OA/GetExpenseApplyReport',
  300. queryParameters: params,
  301. );
  302. return ReportData.fromJson(response.data!);
  303. }
  304. /// 费用申请报表明细(分页)
  305. Future<Map<String, dynamic>> getExpenseApplyReportDetail({
  306. String? startDate,
  307. String? endDate,
  308. int page = 1,
  309. int size = 20,
  310. }) async {
  311. final response = await _client.get<Map<String, dynamic>>(
  312. '/OA/GetExpenseApplyReportDetail',
  313. queryParameters: {
  314. 'startDate': startDate,
  315. 'endDate': endDate,
  316. 'page': page,
  317. 'size': size,
  318. },
  319. );
  320. return response.data!;
  321. }
  322. /// 获取单据状态
  323. Future<Map<String, dynamic>> getBillStatus(String billNo) async {
  324. final response = await _client.get<Map<String, dynamic>>(
  325. '/OA/GetBillStatus',
  326. queryParameters: {'billId': 'AE', 'billNo': billNo},
  327. );
  328. return response.data!;
  329. }
  330. /// 获取指定单据别的附件操作权限
  331. Future<BillFileRights> getBillFileRights(String billId) async {
  332. final response = await _client.get<Map<String, dynamic>>(
  333. '/OA/GetBillFileRights',
  334. queryParameters: {'billId': billId},
  335. );
  336. return BillFileRights.fromJson(response.data!);
  337. }
  338. /// 员工查询
  339. Future<List<EmployeeItem>> getEmployees({
  340. String keyword = '',
  341. String salNo = '',
  342. }) async {
  343. final response = await _client.get<Map<String, dynamic>>(
  344. '/OA/GetEmployees',
  345. queryParameters: {
  346. 'keyword': keyword,
  347. 'salNo': salNo,
  348. 'page': 1,
  349. 'size': 100,
  350. },
  351. );
  352. final list = (response.data?['list'] as List<dynamic>?) ?? [];
  353. return list
  354. .map((e) => EmployeeItem.fromJson(e as Map<String, dynamic>))
  355. .toList();
  356. }
  357. }
  358. class EmployeeItem {
  359. final String salNo;
  360. final String name;
  361. const EmployeeItem({required this.salNo, required this.name});
  362. factory EmployeeItem.fromJson(Map<String, dynamic> json) => EmployeeItem(
  363. salNo: json['salNo'] as String? ?? '',
  364. name: json['name'] as String? ?? '',
  365. );
  366. }