import 'dart:convert'; import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/network/api_client.dart'; import '../../core/network/api_response.dart'; import '../../app.dart'; import '../../shared/models/pagination_model.dart'; import '../../shared/models/bill_attachment.dart'; import '../../core/navigation/host_app_channel.dart'; import '../../core/utils/date_utils.dart' as du; import 'expense_model.dart'; import 'expense_list_controller.dart'; import '../expense_apply/report_model.dart'; final expenseApiProvider = Provider( (ref) => ExpenseApi(ref.read(apiClientProvider)), ); // ═══ 参考数据模型(API 返回) ═══ class CostTypeItem { final String typeNo; final String typeName; final String accNo; final String accName; const CostTypeItem({required this.typeNo, required this.typeName, required this.accNo, required this.accName}); factory CostTypeItem.fromJson(Map json) => CostTypeItem( typeNo: json['typeNo'] as String? ?? '', typeName: json['typeName'] as String? ?? '', accNo: json['accNo'] as String? ?? '', accName: json['accName'] as String? ?? '', ); } class ProjectCodeItem { final String objNo; final String name; const ProjectCodeItem({required this.objNo, required this.name}); factory ProjectCodeItem.fromJson(Map json) => ProjectCodeItem( objNo: json['objNo'] as String? ?? '', name: json['name'] as String? ?? '', ); } class DepartmentItem { final String dep; final String name; const DepartmentItem({required this.dep, required this.name}); factory DepartmentItem.fromJson(Map json) => DepartmentItem( dep: json['dep'] as String? ?? '', name: json['name'] as String? ?? '', ); } class CustomerItem { final String cusNo; final String name; const CustomerItem({required this.cusNo, required this.name}); factory CustomerItem.fromJson(Map json) => CustomerItem( cusNo: json['cusNo'] as String? ?? '', name: json['name'] as String? ?? '', ); } class CurrencyItem { final String curId; final String name; const CurrencyItem({required this.curId, required this.name}); factory CurrencyItem.fromJson(Map json) => CurrencyItem( curId: json['curId'] as String? ?? '', name: json['name'] as String? ?? '', ); } class EmployeeItem { final String salNo; final String name; final String dep; final String tel; final String email; final String bnkNo; final String bnkId; final String accName; const EmployeeItem({required this.salNo, required this.name, this.dep = '', this.tel = '', this.email = '', this.bnkNo = '', this.bnkId = '', this.accName = ''}); factory EmployeeItem.fromJson(Map json) => EmployeeItem( salNo: json['salNo'] as String? ?? '', name: json['name'] as String? ?? '', dep: json['dep'] as String? ?? '', tel: json['tel'] as String? ?? '', email: json['email'] as String? ?? '', bnkNo: json['bnkNo'] as String? ?? '', bnkId: json['bnkId'] as String? ?? '', accName: json['accName'] as String? ?? '', ); } class ExpenseApi { final ApiClient _client; ExpenseApi(this._client); /// 费用报销列表(分页) Future> fetchList({ String status = '', String keyword = '', String startDate = '', String endDate = '', String usr = '', String sortDir = 'DESC', int page = 1, int size = 20, }) async { final response = await _client.get>( '/OA/GetExpenseReports', queryParameters: { 'status': status, 'keyword': keyword, 'startDate': startDate, 'endDate': endDate, 'usr': usr, 'sortDir': sortDir, 'page': page, 'size': size, }, ); return PaginatedData.fromJson(response.data!, ExpenseModel.fromJson); } /// 费用报销详情(主表+明细) Future fetchDetail(String billNo) async { final response = await _client.get>( '/OA/GetExpenseReportDetail', queryParameters: {'billNo': billNo}, ); return ExpenseModel.fromJson(response.data!); } /// 提交审批,返回报销单号(提取失败时返回 null,不影响主流程) /// BillSave 返回格式: { callok:true, resultData:{ BIL_NO:"BX20267020005", ... } } Future submit(Map data) async { final response = await _client.post>('/OA/BillSave', data: { 'erpCategory': 'MasterService', 'billId': 'BX', 'procId': '', 'data': data, }); final resData = response.data; if (resData == null) return null; final resultData = resData['resultData']; if (resultData is Map) { final bilNo = resultData['BIL_NO'] as String?; if (bilNo != null && bilNo.isNotEmpty) return bilNo; } if (resultData is String && resultData.isNotEmpty) { try { final parsed = json.decode(resultData) as Map; final bilNo = parsed['BIL_NO'] as String?; if (bilNo != null && bilNo.isNotEmpty) return bilNo; } catch (_) {} } final rootBilNo = resData['BIL_NO'] as String?; if (rootBilNo != null && rootBilNo.isNotEmpty) return rootBilNo; return null; } /// 下载附件文件字节 Future downloadAttachment(String id) async { return await _client.downloadFile('/OA/DownloadAttachment', queryParameters: {'id': id}); } /// 检测附件服务是否可用 Future checkAttachHealth() async { try { final response = await _client.get>('/OA/CheckAttachHealth'); return response.data?['available'] as bool? ?? false; } catch (_) { return false; } } /// 财务核销 Future verify(Map data) async { await _client.post('/OA/ExpenseVerify', data: data); } /// 费用类别字典 Future> getCostTypes({String keyword = '', String accNo = ''}) async { final response = await _client.get>( '/OA/GetCostTypes', queryParameters: {'keyword': keyword, 'accNo': accNo, 'page': 1, 'size': 100}, ); final list = (response.data?['list'] as List?) ?? []; return list.map((e) => CostTypeItem.fromJson(e as Map)).toList(); } /// 项目代号 Future> getProjectCodes({String keyword = '', String billDate = ''}) async { final response = await _client.get>( '/OA/GetProjectCodes', queryParameters: {'keyword': keyword, 'billDate': billDate, 'page': 1, 'size': 100}, ); final list = (response.data?['list'] as List?) ?? []; return list.map((e) => ProjectCodeItem.fromJson(e as Map)).toList(); } /// 部门 Future> getDepartments({String keyword = '', bool onlyActive = true}) async { final response = await _client.get>( '/OA/GetDepartments', queryParameters: {'keyword': keyword, 'onlyActive': onlyActive, 'page': 1, 'size': 100}, ); final list = (response.data?['list'] as List?) ?? []; return list.map((e) => DepartmentItem.fromJson(e as Map)).toList(); } /// 客户/厂商 Future> getCustomers({String keyword = ''}) async { final response = await _client.get>( '/OA/GetCustomers', queryParameters: {'keyword': keyword, 'page': 1, 'size': 100}, ); final list = (response.data?['list'] as List?) ?? []; return list.map((e) => CustomerItem.fromJson(e as Map)).toList(); } /// 员工查询 Future> getEmployees({String keyword = '', String salNo = ''}) async { final response = await _client.get>( '/OA/GetEmployees', queryParameters: {'keyword': keyword, 'salNo': salNo, 'page': 1, 'size': 100}, ); final list = (response.data?['list'] as List?) ?? []; return list.map((e) => EmployeeItem.fromJson(e as Map)).toList(); } /// 可转入报销单的费用申请明细(keyword 模糊匹配单号/事由/部门/申请人/费用类型) Future> getImportableExpenseApplies({ String keyword = '', String startDate = '', String endDate = '', int page = 1, int size = 20, String sortDir = 'DESC', }) async { final response = await _client.get>( '/OA/GetImportableExpenseApplies', queryParameters: {'keyword': keyword, 'startDate': startDate, 'endDate': endDate, 'page': page, 'size': size, 'sortDir': sortDir}, ); return response.data!; } /// 币别 Future> getCurrencies({String keyword = ''}) async { final response = await _client.get>( '/OA/GetCurrencies', queryParameters: {'keyword': keyword, 'page': 1, 'size': 50}, ); final list = (response.data?['list'] as List?) ?? []; return list.map((e) => CurrencyItem.fromJson(e as Map)).toList(); } /// 币别查询(旧版,保留兼容) Future>> fetchCurrencies({ String keyword = '', int page = 1, int size = 50, }) async { return await _client.get('/OA/GetCurrencies', queryParameters: {'keyword': keyword, 'page': page, 'size': size}); } /// 审批列表 Future> fetchApprovalList({ required String bilId, required String status, String erpCategory = 'MasterService', String keyword = '', String startDate = '', String endDate = '', int page = 1, int size = 20, }) async { final response = await _client.get>( '/OA/GetApprovalList', queryParameters: { 'bilId': bilId, 'status': status, 'erpCategory': erpCategory, 'keyword': keyword, 'startDate': startDate, 'endDate': endDate, 'page': page, 'size': size, }, ); return response.data!; } /// 审批进度 Future> fetchApprovalTimeline(String bilId, String bilNo, {int bilItm = 0}) async { final response = await _client.get>( '/OA/GetApprovalTimeline', queryParameters: {'bilId': bilId, 'bilNo': bilNo, 'bilItm': bilItm}, ); return response.data!; } /// 获取单据附件列表 Future> getAttachments(String bilId, String bilNo, {int? srcItm}) async { final params = { 'bilId': bilId, 'bilNo': bilNo, }; if (srcItm != null) params['srcItm'] = srcItm; final response = await _client.get( '/OA/GetAttachments', queryParameters: params, ); final body = response.data; if (body is! Map) return []; final result = body['Result']; if (result is! Map) return []; final documents = result['documents']; if (documents is! List) return []; return documents .whereType>() .map((e) => BillAttachment.fromJson(e)) .toList(); } /// 上传附件 /// [metadata] Map: { BIL_ID, BIL_NO, SRCITM, ITM, TAG, EFF_DD, USR, FILENAME, EXT } Future> uploadAttachment( String filePath, Map metadata, ) async { final fileName = (metadata['FILENAME'] as String?) ?? filePath.split('/').last; final response = await _client.uploadMultipart>( '/OA/UploadAttachment', files: [await MultipartFile.fromFile(filePath, filename: fileName)], extraFields: {'metadata': json.encode(metadata)}, // proper JSON encoding ); return response.data ?? {}; } /// 审核执行(通过/驳回/反审核) Future> executeApproval({ required String bilId, required String bilNo, int bilItm = 0, required String action, String rem = '', String effDd = '', String reason = '', bool isPreToStart = false, int nodeIndex = -1, String dataBx = '', }) async { final response = await _client.post>( '/OA/ExecuteApproval', data: { 'bilId': bilId, 'bilNo': bilNo, 'bilItm': bilItm, 'action': action, 'rem': rem, 'effDd': effDd, 'reason': reason, 'isPreToStart': isPreToStart, 'nodeIndex': nodeIndex, 'dataBx': dataBx, }, ); return response.data!; } /// 费用报销报表 Future getExpenseReport({String? startDate, String? endDate}) async { final params = {}; if (startDate != null) params['startDate'] = startDate; if (endDate != null) params['endDate'] = endDate; final response = await _client.get>( '/OA/GetExpenseReport', queryParameters: params, ); return ReportData.fromJson(response.data!); } } /// 费用报销审批列表项 class ExpenseApprovalListItem { final String bilId; final String bilNo; final String bilDate; final String applicant; final String dept; final double amount; final String remark; final String status; const ExpenseApprovalListItem({ required this.bilId, required this.bilNo, required this.bilDate, required this.applicant, required this.dept, required this.amount, required this.remark, required this.status, }); factory ExpenseApprovalListItem.fromJson(Map json) => ExpenseApprovalListItem( bilId: json['bilId'] as String? ?? '', bilNo: json['bilNo'] as String? ?? '', bilDate: json['bilDate'] as String? ?? '', applicant: json['applicant'] as String? ?? '', dept: json['dept'] as String? ?? '', amount: (json['amount'] as num?)?.toDouble() ?? 0, remark: json['remark'] as String? ?? '', status: json['status'] as String? ?? 'pending', ); ExpenseModel toExpenseModel() => ExpenseModel( id: bilNo, expenseNo: bilNo, expenseDate: bilDate.isNotEmpty ? DateTime.tryParse(bilDate) : null, applicantId: applicant, applicantName: applicant, deptId: dept, deptName: dept, totalAmount: amount, purpose: remark, status: status == 'approved' ? 'approved' : 'pending', paymentStatus: 'unpaid', createTime: DateTime.now(), updateTime: DateTime.now(), ); } final expenseApprovalListProvider = FutureProvider.autoDispose.family, String>( (ref, status) async { ref.watch(expenseRefreshProvider); ref.watch(expenseDateStartProvider); ref.watch(expenseDateEndProvider); ref.watch(expenseKeywordProvider); final api = ref.read(expenseApiProvider); final dateStart = ref.read(expenseDateStartProvider); final dateEnd = ref.read(expenseDateEndProvider); final keyword = ref.read(expenseKeywordProvider); final response = await api.fetchApprovalList( bilId: 'BX', keyword: keyword, status: status.isEmpty ? '' : status, startDate: dateStart != null ? du.DateUtils.formatDate(dateStart) : '', endDate: dateEnd != null ? du.DateUtils.formatDate(dateEnd) : '', ); return (response['list'] as List?)?.map((e) => ExpenseApprovalListItem.fromJson(e as Map).toExpenseModel()).toList() ?? []; }, ); /// "我的报销单" 列表(制单人=当前用户,非审批流) final expenseMyListProvider = FutureProvider.autoDispose.family, String>( (ref, status) async { ref.watch(expenseRefreshProvider); ref.watch(expenseDateStartProvider); ref.watch(expenseDateEndProvider); ref.watch(expenseKeywordProvider); final api = ref.read(expenseApiProvider); final dateStart = ref.read(expenseDateStartProvider); final dateEnd = ref.read(expenseDateEndProvider); final keyword = ref.read(expenseKeywordProvider); final result = await api.fetchList( keyword: keyword, startDate: dateStart != null ? du.DateUtils.formatDate(dateStart) : '', endDate: dateEnd != null ? du.DateUtils.formatDate(dateEnd) : '', usr: HostAppChannel.usr, sortDir: ref.watch(expenseSortDirProvider), ); return result.list; }, );