Selaa lähdekoodia

doing #19407 优化费用申请单、费用报销单接口

chengc 2 viikkoa sitten
vanhempi
commit
50cb9e6e9e

+ 48 - 28
lib/core/network/mock_interceptor.dart

@@ -19,37 +19,36 @@ class MockInterceptor extends Interceptor {
 
     if (mockData != null) {
       Future.delayed(const Duration(milliseconds: 300), () {
-        handler.resolve(Response(
-          requestOptions: options,
-          statusCode: 200,
-          data: mockData,
-        ));
+        handler.resolve(
+          Response(requestOptions: options, statusCode: 200, data: mockData),
+        );
       });
     } else {
-      handler.resolve(Response(
-        requestOptions: options,
-        statusCode: 200,
-        data: {'code': 0, 'message': 'ok', 'data': null},
-      ));
+      handler.resolve(
+        Response(
+          requestOptions: options,
+          statusCode: 200,
+          data: {'code': 0, 'message': 'ok', 'data': null},
+        ),
+      );
     }
   }
 
-  Map<String, dynamic>? _matchMock(
-      String path, Map<String, dynamic> params) {
+  Map<String, dynamic>? _matchMock(String path, Map<String, dynamic> params) {
     final status = params['status'] as String? ?? '';
     final page = params['page'] as String? ?? '1';
 
     if (path == '/home/summary') return MockData.homeSummary;
 
     // ==================== 费用报销 ====================
-    if (path == '/OA/GetExpenseReports') {
+    if (path == '/OA/GetExpenseList') {
       var list = MockData.expenseList;
       if (status.isNotEmpty) {
         list = list.where((e) => e['status'] == status).toList();
       }
       return _paginate(list, page);
     }
-    if (path == '/OA/GetExpenseReportDetail') {
+    if (path == '/OA/GetExpenseDetail') {
       final billNo = params['billNo'] ?? '';
       return MockData.expenseDetail(billNo);
     }
@@ -58,7 +57,7 @@ class MockInterceptor extends Interceptor {
     }
 
     // ==================== 事前申请 ====================
-    if (path == '/OA/GetExpenseApplies') {
+    if (path == '/OA/GetExpenseApplyList') {
       var list = MockData.expenseApplicationList;
       if (status.isNotEmpty) {
         list = list.where((e) => e['status'] == status).toList();
@@ -70,22 +69,46 @@ class MockInterceptor extends Interceptor {
       return MockData.expenseApplicationDetail(billNo);
     }
     if (path == '/OA/GetCurrencies') {
-      return {'code': 0, 'message': 'success', 'data': _paginate(MockData.currencies, page)};
+      return {
+        'code': 0,
+        'message': 'success',
+        'data': _paginate(MockData.currencies, page),
+      };
     }
     if (path == '/OA/GetCostTypes') {
-      return {'code': 0, 'message': 'success', 'data': _paginate(MockData.costTypes, page)};
+      return {
+        'code': 0,
+        'message': 'success',
+        'data': _paginate(MockData.costTypes, page),
+      };
     }
     if (path == '/OA/GetProjectCodes') {
-      return {'code': 0, 'message': 'success', 'data': _paginate(MockData.projectCodes, page)};
+      return {
+        'code': 0,
+        'message': 'success',
+        'data': _paginate(MockData.projectCodes, page),
+      };
     }
     if (path == '/OA/GetDepartments') {
-      return {'code': 0, 'message': 'success', 'data': _paginate(MockData.departments, page)};
+      return {
+        'code': 0,
+        'message': 'success',
+        'data': _paginate(MockData.departments, page),
+      };
     }
     if (path == '/OA/GetCustomers') {
-      return {'code': 0, 'message': 'success', 'data': _paginate(MockData.customers, page)};
+      return {
+        'code': 0,
+        'message': 'success',
+        'data': _paginate(MockData.customers, page),
+      };
     }
     if (path == '/OA/GetApprovalTimeline') {
-      return {'code': 0, 'message': 'success', 'data': MockData.approvalTimeline};
+      return {
+        'code': 0,
+        'message': 'success',
+        'data': MockData.approvalTimeline,
+      };
     }
     if (path == '/OA/GetApprovalList') {
       var list = MockData.approvalList;
@@ -186,18 +209,15 @@ class MockInterceptor extends Interceptor {
   }
 
   Map<String, dynamic> _paginate(
-      List<Map<String, dynamic>> list, String pageStr) {
+    List<Map<String, dynamic>> list,
+    String pageStr,
+  ) {
     final page = int.tryParse(pageStr) ?? 1;
     const size = 20;
     return {
       'code': 0,
       'message': 'ok',
-      'data': {
-        'list': list,
-        'page': page,
-        'size': size,
-        'total': list.length,
-      },
+      'data': {'list': list, 'page': page, 'size': size, 'total': list.length},
     };
   }
 }

+ 228 - 116
lib/features/expense/expense_api.dart

@@ -24,7 +24,12 @@ class CostTypeItem {
   final String typeName;
   final String accNo;
   final String accName;
-  const CostTypeItem({required this.typeNo, required this.typeName, required this.accNo, required this.accName});
+  const CostTypeItem({
+    required this.typeNo,
+    required this.typeName,
+    required this.accNo,
+    required this.accName,
+  });
   factory CostTypeItem.fromJson(Map<String, dynamic> json) => CostTypeItem(
     typeNo: json['typeNo'] as String? ?? '',
     typeName: json['typeName'] as String? ?? '',
@@ -37,10 +42,11 @@ class ProjectCodeItem {
   final String objNo;
   final String name;
   const ProjectCodeItem({required this.objNo, required this.name});
-  factory ProjectCodeItem.fromJson(Map<String, dynamic> json) => ProjectCodeItem(
-    objNo: json['objNo'] as String? ?? '',
-    name: json['name'] as String? ?? '',
-  );
+  factory ProjectCodeItem.fromJson(Map<String, dynamic> json) =>
+      ProjectCodeItem(
+        objNo: json['objNo'] as String? ?? '',
+        name: json['name'] as String? ?? '',
+      );
 }
 
 class DepartmentItem {
@@ -82,7 +88,16 @@ class EmployeeItem {
   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 = ''});
+  const EmployeeItem({
+    required this.salNo,
+    required this.name,
+    this.dep = '',
+    this.tel = '',
+    this.email = '',
+    this.bnkNo = '',
+    this.bnkId = '',
+    this.accName = '',
+  });
   factory EmployeeItem.fromJson(Map<String, dynamic> json) => EmployeeItem(
     salNo: json['salNo'] as String? ?? '',
     name: json['name'] as String? ?? '',
@@ -111,7 +126,7 @@ class ExpenseApi {
     int size = 20,
   }) async {
     final response = await _client.get<Map<String, dynamic>>(
-      '/OA/GetExpenseReports',
+      '/OA/GetExpenseList',
       queryParameters: {
         'status': status,
         'keyword': keyword,
@@ -129,7 +144,7 @@ class ExpenseApi {
   /// 费用报销详情(主表+明细)
   Future<ExpenseModel> fetchDetail(String billNo) async {
     final response = await _client.get<Map<String, dynamic>>(
-      '/OA/GetExpenseReportDetail',
+      '/OA/GetExpenseDetail',
       queryParameters: {'billNo': billNo},
     );
     return ExpenseModel.fromJson(response.data!);
@@ -138,12 +153,15 @@ class ExpenseApi {
   /// 提交审批,返回报销单号(提取失败时返回 null,不影响主流程)
   /// BillSave 返回格式: { callok:true, resultData:{ BIL_NO:"BX20267020005", ... } }
   Future<String?> submit(Map<String, dynamic> data) async {
-    final response = await _client.post<Map<String, dynamic>>('/OA/BillSave', data: {
-      'erpCategory': 'MasterService',
-      'billId': 'BX',
-      'procId': '',
-      'data': data,
-    });
+    final response = await _client.post<Map<String, dynamic>>(
+      '/OA/BillSave',
+      data: {
+        'erpCategory': 'MasterService',
+        'billId': 'BX',
+        'procId': '',
+        'data': data,
+      },
+    );
     final resData = response.data;
     if (resData == null) return null;
 
@@ -167,13 +185,18 @@ class ExpenseApi {
 
   /// 下载附件文件字节
   Future<Uint8List?> downloadAttachment(String id) async {
-    return await _client.downloadFile('/OA/DownloadAttachment', queryParameters: {'id': id});
+    return await _client.downloadFile(
+      '/OA/DownloadAttachment',
+      queryParameters: {'id': id},
+    );
   }
 
   /// 检测附件服务是否可用
   Future<bool> checkAttachHealth() async {
     try {
-      final response = await _client.get<Map<String, dynamic>>('/OA/CheckAttachHealth');
+      final response = await _client.get<Map<String, dynamic>>(
+        '/OA/CheckAttachHealth',
+      );
       return response.data?['available'] as bool? ?? false;
     } catch (_) {
       return false;
@@ -186,33 +209,63 @@ class ExpenseApi {
   }
 
   /// 费用类别字典
-  Future<List<CostTypeItem>> getCostTypes({String keyword = '', String accNo = ''}) async {
+  Future<List<CostTypeItem>> getCostTypes({
+    String keyword = '',
+    String accNo = '',
+  }) async {
     final response = await _client.get<Map<String, dynamic>>(
       '/OA/GetCostTypes',
-      queryParameters: {'keyword': keyword, 'accNo': accNo, 'page': 1, 'size': 100},
+      queryParameters: {
+        'keyword': keyword,
+        'accNo': accNo,
+        'page': 1,
+        'size': 100,
+      },
     );
     final list = (response.data?['list'] as List<dynamic>?) ?? [];
-    return list.map((e) => CostTypeItem.fromJson(e as Map<String, dynamic>)).toList();
+    return list
+        .map((e) => CostTypeItem.fromJson(e as Map<String, dynamic>))
+        .toList();
   }
 
   /// 项目代号
-  Future<List<ProjectCodeItem>> getProjectCodes({String keyword = '', String billDate = ''}) async {
+  Future<List<ProjectCodeItem>> getProjectCodes({
+    String keyword = '',
+    String billDate = '',
+  }) async {
     final response = await _client.get<Map<String, dynamic>>(
       '/OA/GetProjectCodes',
-      queryParameters: {'keyword': keyword, 'billDate': billDate, 'page': 1, 'size': 100},
+      queryParameters: {
+        'keyword': keyword,
+        'billDate': billDate,
+        'page': 1,
+        'size': 100,
+      },
     );
     final list = (response.data?['list'] as List<dynamic>?) ?? [];
-    return list.map((e) => ProjectCodeItem.fromJson(e as Map<String, dynamic>)).toList();
+    return list
+        .map((e) => ProjectCodeItem.fromJson(e as Map<String, dynamic>))
+        .toList();
   }
 
   /// 部门
-  Future<List<DepartmentItem>> getDepartments({String keyword = '', bool onlyActive = true}) async {
+  Future<List<DepartmentItem>> getDepartments({
+    String keyword = '',
+    bool onlyActive = true,
+  }) async {
     final response = await _client.get<Map<String, dynamic>>(
       '/OA/GetDepartments',
-      queryParameters: {'keyword': keyword, 'onlyActive': onlyActive, 'page': 1, 'size': 100},
+      queryParameters: {
+        'keyword': keyword,
+        'onlyActive': onlyActive,
+        'page': 1,
+        'size': 100,
+      },
     );
     final list = (response.data?['list'] as List<dynamic>?) ?? [];
-    return list.map((e) => DepartmentItem.fromJson(e as Map<String, dynamic>)).toList();
+    return list
+        .map((e) => DepartmentItem.fromJson(e as Map<String, dynamic>))
+        .toList();
   }
 
   /// 客户/厂商
@@ -222,26 +275,50 @@ class ExpenseApi {
       queryParameters: {'keyword': keyword, 'page': 1, 'size': 100},
     );
     final list = (response.data?['list'] as List<dynamic>?) ?? [];
-    return list.map((e) => CustomerItem.fromJson(e as Map<String, dynamic>)).toList();
+    return list
+        .map((e) => CustomerItem.fromJson(e as Map<String, dynamic>))
+        .toList();
   }
 
   /// 员工查询
-  Future<List<EmployeeItem>> getEmployees({String keyword = '', String salNo = ''}) async {
+  Future<List<EmployeeItem>> getEmployees({
+    String keyword = '',
+    String salNo = '',
+  }) async {
     final response = await _client.get<Map<String, dynamic>>(
       '/OA/GetEmployees',
-      queryParameters: {'keyword': keyword, 'salNo': salNo, 'page': 1, 'size': 100},
+      queryParameters: {
+        'keyword': keyword,
+        'salNo': salNo,
+        'page': 1,
+        'size': 100,
+      },
     );
     final list = (response.data?['list'] as List<dynamic>?) ?? [];
-    return list.map((e) => EmployeeItem.fromJson(e as Map<String, dynamic>)).toList();
+    return list
+        .map((e) => EmployeeItem.fromJson(e as Map<String, dynamic>))
+        .toList();
   }
 
   /// 可转入报销单的费用申请明细(keyword 模糊匹配单号/事由/部门/申请人/费用类型)
-  Future<Map<String, dynamic>> getImportableExpenseApplies({
-    String keyword = '', String startDate = '', String endDate = '', int page = 1, int size = 20, String sortDir = 'DESC',
+  Future<Map<String, dynamic>> getImportableExpenseApplyList({
+    String keyword = '',
+    String startDate = '',
+    String endDate = '',
+    int page = 1,
+    int size = 20,
+    String sortDir = 'DESC',
   }) async {
     final response = await _client.get<Map<String, dynamic>>(
-      '/OA/GetImportableExpenseApplies',
-      queryParameters: {'keyword': keyword, 'startDate': startDate, 'endDate': endDate, 'page': page, 'size': size, 'sortDir': sortDir},
+      '/OA/GetImportableExpenseApplyList',
+      queryParameters: {
+        'keyword': keyword,
+        'startDate': startDate,
+        'endDate': endDate,
+        'page': page,
+        'size': size,
+        'sortDir': sortDir,
+      },
     );
     return response.data!;
   }
@@ -253,7 +330,9 @@ class ExpenseApi {
       queryParameters: {'keyword': keyword, 'page': 1, 'size': 50},
     );
     final list = (response.data?['list'] as List<dynamic>?) ?? [];
-    return list.map((e) => CurrencyItem.fromJson(e as Map<String, dynamic>)).toList();
+    return list
+        .map((e) => CurrencyItem.fromJson(e as Map<String, dynamic>))
+        .toList();
   }
 
   /// 币别查询(旧版,保留兼容)
@@ -262,8 +341,10 @@ class ExpenseApi {
     int page = 1,
     int size = 50,
   }) async {
-    return await _client.get('/OA/GetCurrencies',
-        queryParameters: {'keyword': keyword, 'page': page, 'size': size});
+    return await _client.get(
+      '/OA/GetCurrencies',
+      queryParameters: {'keyword': keyword, 'page': page, 'size': size},
+    );
   }
 
   /// 审批列表
@@ -294,7 +375,11 @@ class ExpenseApi {
   }
 
   /// 审批进度
-  Future<Map<String, dynamic>> fetchApprovalTimeline(String bilId, String bilNo, {int bilItm = 0}) async {
+  Future<Map<String, dynamic>> fetchApprovalTimeline(
+    String bilId,
+    String bilNo, {
+    int bilItm = 0,
+  }) async {
     final response = await _client.get<Map<String, dynamic>>(
       '/OA/GetApprovalTimeline',
       queryParameters: {'bilId': bilId, 'bilNo': bilNo, 'bilItm': bilItm},
@@ -303,11 +388,12 @@ class ExpenseApi {
   }
 
   /// 获取单据附件列表
-  Future<List<BillAttachment>> getAttachments(String bilId, String bilNo, {int? srcItm}) async {
-    final params = <String, dynamic>{
-      'bilId': bilId,
-      'bilNo': bilNo,
-    };
+  Future<List<BillAttachment>> getAttachments(
+    String bilId,
+    String bilNo, {
+    int? srcItm,
+  }) async {
+    final params = <String, dynamic>{'bilId': bilId, 'bilNo': bilNo};
     if (srcItm != null) params['srcItm'] = srcItm;
     final response = await _client.get<dynamic>(
       '/OA/GetAttachments',
@@ -331,7 +417,8 @@ class ExpenseApi {
     String filePath,
     Map<String, dynamic> metadata,
   ) async {
-    final fileName = (metadata['FILENAME'] as String?) ?? filePath.split('/').last;
+    final fileName =
+        (metadata['FILENAME'] as String?) ?? filePath.split('/').last;
     final response = await _client.uploadMultipart<Map<String, dynamic>>(
       '/OA/UploadAttachment',
       files: [await MultipartFile.fromFile(filePath, filename: fileName)],
@@ -342,25 +429,40 @@ class ExpenseApi {
 
   /// 审核执行(通过/驳回/反审核)
   Future<Map<String, dynamic>> 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,
+    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<Map<String, dynamic>>(
       '/OA/ExecuteApproval',
       data: {
-        'bilId': bilId, 'bilNo': bilNo, 'bilItm': bilItm,
-        'action': action, 'rem': rem, 'effDd': effDd,
-        'reason': reason, 'isPreToStart': isPreToStart,
-        'nodeIndex': nodeIndex, 'dataBx': dataBx,
+        'bilId': bilId,
+        'bilNo': bilNo,
+        'bilItm': bilItm,
+        'action': action,
+        'rem': rem,
+        'effDd': effDd,
+        'reason': reason,
+        'isPreToStart': isPreToStart,
+        'nodeIndex': nodeIndex,
+        'dataBx': dataBx,
       },
     );
     return response.data!;
   }
 
   /// 费用报销报表
-  Future<ReportData> getExpenseReport({String? startDate, String? endDate}) async {
+  Future<ReportData> getExpenseReport({
+    String? startDate,
+    String? endDate,
+  }) async {
     final params = <String, dynamic>{};
     if (startDate != null) params['startDate'] = startDate;
     if (endDate != null) params['endDate'] = endDate;
@@ -372,12 +474,20 @@ class ExpenseApi {
   }
 
   /// 费用报销报表明细(分页)
-  Future<Map<String, dynamic>> getExpenseReportDetails({
-    String? startDate, String? endDate, int page = 1, int size = 20,
+  Future<Map<String, dynamic>> getExpenseReportDetail({
+    String? startDate,
+    String? endDate,
+    int page = 1,
+    int size = 20,
   }) async {
     final response = await _client.get<Map<String, dynamic>>(
-      '/OA/GetExpenseReportDetails',
-      queryParameters: {'startDate': startDate, 'endDate': endDate, 'page': page, 'size': size},
+      '/OA/GetExpenseReportDetail',
+      queryParameters: {
+        'startDate': startDate,
+        'endDate': endDate,
+        'page': page,
+        'size': size,
+      },
     );
     return response.data!;
   }
@@ -418,68 +528,70 @@ class ExpenseApprovalListItem {
       );
 
   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(),
-      );
+    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<List<ExpenseModel>, 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<dynamic>?)?.map((e) => ExpenseApprovalListItem.fromJson(e as Map<String, dynamic>).toExpenseModel()).toList() ?? [];
-  },
-);
+final expenseApprovalListProvider = FutureProvider.autoDispose
+    .family<List<ExpenseModel>, 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<dynamic>?)
+              ?.map(
+                (e) => ExpenseApprovalListItem.fromJson(
+                  e as Map<String, dynamic>,
+                ).toExpenseModel(),
+              )
+              .toList() ??
+          [];
+    });
 
 /// "我的报销单" 列表(制单人=当前用户,非审批流)
-final expenseMyListProvider =
-    FutureProvider.autoDispose.family<List<ExpenseModel>, 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;
-  },
-);
+final expenseMyListProvider = FutureProvider.autoDispose
+    .family<List<ExpenseModel>, 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;
+    });

+ 499 - 181
lib/features/expense/expense_apply_import_page.dart

@@ -34,12 +34,26 @@ class ImportableItem {
   bool selected = false;
 
   ImportableItem({
-    required this.aeNo, required this.aeDd, required this.reason,
-    required this.headAmtnYj, required this.itm, required this.sqMan, required this.sqName,
-    required this.typeNo, required this.typeName, required this.amtnYj,
-    required this.accNo, required this.accName, required this.dep,
-    required this.depName, required this.objNo, required this.objName,
-    required this.startDd, required this.endDd, required this.priority, required this.rem,
+    required this.aeNo,
+    required this.aeDd,
+    required this.reason,
+    required this.headAmtnYj,
+    required this.itm,
+    required this.sqMan,
+    required this.sqName,
+    required this.typeNo,
+    required this.typeName,
+    required this.amtnYj,
+    required this.accNo,
+    required this.accName,
+    required this.dep,
+    required this.depName,
+    required this.objNo,
+    required this.objName,
+    required this.startDd,
+    required this.endDd,
+    required this.priority,
+    required this.rem,
   });
 
   factory ImportableItem.fromJson(Map<String, dynamic> json) => ImportableItem(
@@ -80,7 +94,8 @@ class ExpenseApplyImportPage extends ConsumerStatefulWidget {
   const ExpenseApplyImportPage({super.key});
 
   @override
-  ConsumerState<ExpenseApplyImportPage> createState() => _ExpenseApplyImportPageState();
+  ConsumerState<ExpenseApplyImportPage> createState() =>
+      _ExpenseApplyImportPageState();
 }
 
 class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
@@ -102,8 +117,10 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
   void initState() {
     super.initState();
     final now = DateTime.now();
-    _startDateCtrl.text = '${now.year}-${now.month.toString().padLeft(2, '0')}-01';
-    _endDateCtrl.text = '${now.year}-${now.month.toString().padLeft(2, '0')}-${DateTime(now.year, now.month + 1, 0).day.toString().padLeft(2, '0')}';
+    _startDateCtrl.text =
+        '${now.year}-${now.month.toString().padLeft(2, '0')}-01';
+    _endDateCtrl.text =
+        '${now.year}-${now.month.toString().padLeft(2, '0')}-${DateTime(now.year, now.month + 1, 0).day.toString().padLeft(2, '0')}';
     _scrollCtrl = ScrollController()..addListener(_onScroll);
     _refreshCtrl = EasyRefreshController();
     WidgetsBinding.instance.addObserver(this);
@@ -113,7 +130,9 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
   @override
   void dispose() {
     WidgetsBinding.instance.removeObserver(this);
-    _aeNoCtrl.dispose(); _startDateCtrl.dispose(); _endDateCtrl.dispose();
+    _aeNoCtrl.dispose();
+    _startDateCtrl.dispose();
+    _endDateCtrl.dispose();
     _scrollCtrl.dispose();
     _refreshCtrl.dispose();
     super.dispose();
@@ -125,15 +144,24 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
       FocusScope.of(context).unfocus();
       _aeNoCtrl.clear();
       final now = DateTime.now();
-      _startDateCtrl.text = '${now.year}-${now.month.toString().padLeft(2, '0')}-01';
-      _endDateCtrl.text = '${now.year}-${now.month.toString().padLeft(2, '0')}-${DateTime(now.year, now.month + 1, 0).day.toString().padLeft(2, '0')}';
-      _items = []; _page = 1; _hasMore = true; _initialLoaded = false; _generation++;
+      _startDateCtrl.text =
+          '${now.year}-${now.month.toString().padLeft(2, '0')}-01';
+      _endDateCtrl.text =
+          '${now.year}-${now.month.toString().padLeft(2, '0')}-${DateTime(now.year, now.month + 1, 0).day.toString().padLeft(2, '0')}';
+      _items = [];
+      _page = 1;
+      _hasMore = true;
+      _initialLoaded = false;
+      _generation++;
       _load();
     }
   }
 
   void _onScroll() {
-    if (!_loading && _hasMore && _scrollCtrl.position.pixels >= _scrollCtrl.position.maxScrollExtent - 200) {
+    if (!_loading &&
+        _hasMore &&
+        _scrollCtrl.position.pixels >=
+            _scrollCtrl.position.maxScrollExtent - 200) {
       _load(append: true);
     }
   }
@@ -146,7 +174,7 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
     setState(() => _loading = true);
     try {
       final api = ref.read(expenseApiProvider);
-      final result = await api.getImportableExpenseApplies(
+      final result = await api.getImportableExpenseApplyList(
         keyword: _aeNoCtrl.text.trim(),
         startDate: _startDateCtrl.text,
         endDate: _endDateCtrl.text,
@@ -155,11 +183,19 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
       );
       if (!mounted) return;
       if (gen != _generation) return;
-      final list = (result['list'] as List<dynamic>?)
-          ?.map((e) => ImportableItem.fromJson(e as Map<String, dynamic>))
-          .toList() ?? [];
+      final list =
+          (result['list'] as List<dynamic>?)
+              ?.map((e) => ImportableItem.fromJson(e as Map<String, dynamic>))
+              .toList() ??
+          [];
       setState(() {
-        if (append) { _items.addAll(list); _page++; } else { _items = list; _page = 2; }
+        if (append) {
+          _items.addAll(list);
+          _page++;
+        } else {
+          _items = list;
+          _page = 2;
+        }
         _loading = false;
         _initialLoaded = true;
         _hasMore = list.length >= 20;
@@ -171,8 +207,13 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
 
   void _search() {
     FocusScope.of(context).unfocus();
-    if (_startDateCtrl.text.isNotEmpty && _endDateCtrl.text.isNotEmpty && _startDateCtrl.text.compareTo(_endDateCtrl.text) > 0) {
-      TDToast.showText(AppLocalizations.of(context).get('filterDateStartAfterEnd'), context: context);
+    if (_startDateCtrl.text.isNotEmpty &&
+        _endDateCtrl.text.isNotEmpty &&
+        _startDateCtrl.text.compareTo(_endDateCtrl.text) > 0) {
+      TDToast.showText(
+        AppLocalizations.of(context).get('filterDateStartAfterEnd'),
+        context: context,
+      );
       return;
     }
     _refreshCtrl.callRefresh();
@@ -194,7 +235,9 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
       final items = _items.where((e) => e.aeNo == aeNo).toList();
       final allSelected = items.every((e) => e.selected);
       final newVal = !allSelected;
-      for (final e in items) { e.selected = newVal; }
+      for (final e in items) {
+        e.selected = newVal;
+      }
     });
   }
 
@@ -227,13 +270,19 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
       context,
       title: l10n.get('selectDate'),
       backgroundColor: colors.bgCard,
-      useYear: true, useMonth: true, useDay: true,
-      useHour: false, useMinute: false, useSecond: false, useWeekDay: false,
+      useYear: true,
+      useMonth: true,
+      useDay: true,
+      useHour: false,
+      useMinute: false,
+      useSecond: false,
+      useWeekDay: false,
       dateStart: const [2020, 1, 1],
       dateEnd: [now.year + 1, 12, 31],
       initialDate: [now.year, now.month, now.day],
       onConfirm: (selected) {
-        final d = '${selected['year']}-${selected['month'].toString().padLeft(2, '0')}-${selected['day'].toString().padLeft(2, '0')}';
+        final d =
+            '${selected['year']}-${selected['month'].toString().padLeft(2, '0')}-${selected['day'].toString().padLeft(2, '0')}';
         if (_validateDateRange(ctrl, d)) {
           ctrl.text = d;
           setState(() {});
@@ -247,7 +296,10 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
     final start = changed == _startDateCtrl ? newValue : _startDateCtrl.text;
     final end = changed == _endDateCtrl ? newValue : _endDateCtrl.text;
     if (start.isNotEmpty && end.isNotEmpty && start.compareTo(end) > 0) {
-      TDToast.showText(AppLocalizations.of(context).get('filterDateStartAfterEnd'), context: context);
+      TDToast.showText(
+        AppLocalizations.of(context).get('filterDateStartAfterEnd'),
+        context: context,
+      );
       return false;
     }
     return true;
@@ -265,85 +317,143 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
         children: [
           Padding(
             padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
-            child: Row(children: [
-              Expanded(
-                child: GestureDetector(
-                  onTap: () => _pickDate(_startDateCtrl),
-                  child: _dateChip(_startDateCtrl, l10n.get('filterStartDate'), tdTheme, colors),
+            child: Row(
+              children: [
+                Expanded(
+                  child: GestureDetector(
+                    onTap: () => _pickDate(_startDateCtrl),
+                    child: _dateChip(
+                      _startDateCtrl,
+                      l10n.get('filterStartDate'),
+                      tdTheme,
+                      colors,
+                    ),
+                  ),
                 ),
-              ),
-              const SizedBox(width: 8),
-              Text('—', style: TextStyle(fontSize: 14, color: colors.textSecondary)),
-              const SizedBox(width: 8),
-              Expanded(
-                child: GestureDetector(
-                  onTap: () => _pickDate(_endDateCtrl),
-                  child: _dateChip(_endDateCtrl, l10n.get('filterEndDate'), tdTheme, colors),
+                const SizedBox(width: 8),
+                Text(
+                  '—',
+                  style: TextStyle(fontSize: 14, color: colors.textSecondary),
                 ),
-              ),
-            ]),
+                const SizedBox(width: 8),
+                Expanded(
+                  child: GestureDetector(
+                    onTap: () => _pickDate(_endDateCtrl),
+                    child: _dateChip(
+                      _endDateCtrl,
+                      l10n.get('filterEndDate'),
+                      tdTheme,
+                      colors,
+                    ),
+                  ),
+                ),
+              ],
+            ),
           ),
           const SizedBox(height: 8),
           Padding(
             padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
-            child: Row(children: [
-              Expanded(
-                child: Container(
-                  height: 40,
-                  padding: const EdgeInsets.symmetric(horizontal: 16),
-                  decoration: BoxDecoration(
-                    color: colors.bgSecondaryContainer,
-                    borderRadius: BorderRadius.circular(20),
-                    border: Border.all(color: tdTheme.componentStrokeColor),
-                  ),
-                  child: Row(children: [
-                    Expanded(
-                      child: TextField(
-                        controller: _aeNoCtrl,
-                        style: TextStyle(fontSize: 14, color: colors.textPrimary),
-                        decoration: InputDecoration(
-                          hintText: l10n.get('searchImportHint'),
-                          hintStyle: TextStyle(fontSize: 14, color: colors.textSecondary),
-                          border: InputBorder.none,
-                          isCollapsed: true,
+            child: Row(
+              children: [
+                Expanded(
+                  child: Container(
+                    height: 40,
+                    padding: const EdgeInsets.symmetric(horizontal: 16),
+                    decoration: BoxDecoration(
+                      color: colors.bgSecondaryContainer,
+                      borderRadius: BorderRadius.circular(20),
+                      border: Border.all(color: tdTheme.componentStrokeColor),
+                    ),
+                    child: Row(
+                      children: [
+                        Expanded(
+                          child: TextField(
+                            controller: _aeNoCtrl,
+                            style: TextStyle(
+                              fontSize: 14,
+                              color: colors.textPrimary,
+                            ),
+                            decoration: InputDecoration(
+                              hintText: l10n.get('searchImportHint'),
+                              hintStyle: TextStyle(
+                                fontSize: 14,
+                                color: colors.textSecondary,
+                              ),
+                              border: InputBorder.none,
+                              isCollapsed: true,
+                            ),
+                            onChanged: (_) => setState(() {}),
+                          ),
                         ),
-                        onChanged: (_) => setState(() {}),
-                      ),
+                        if (_aeNoCtrl.text.isNotEmpty)
+                          GestureDetector(
+                            onTap: () {
+                              _aeNoCtrl.clear();
+                              setState(() {});
+                            },
+                            child: Icon(
+                              Icons.close,
+                              size: 18,
+                              color: colors.textSecondary,
+                            ),
+                          ),
+                      ],
+                    ),
+                  ),
+                ),
+                const SizedBox(width: 8),
+                GestureDetector(
+                  onTap: () {
+                    setState(() => _sortDesc = !_sortDesc);
+                    _search();
+                  },
+                  child: Container(
+                    width: 40,
+                    height: 40,
+                    decoration: BoxDecoration(
+                      color: colors.primary,
+                      borderRadius: BorderRadius.circular(20),
                     ),
-                    if (_aeNoCtrl.text.isNotEmpty)
-                      GestureDetector(
-                        onTap: () { _aeNoCtrl.clear(); setState(() {}); },
-                        child: Icon(Icons.close, size: 18, color: colors.textSecondary),
+                    child: Center(
+                      child: Icon(
+                        _sortDesc ? Icons.arrow_downward : Icons.arrow_upward,
+                        color: Colors.white,
+                        size: 20,
                       ),
-                  ]),
+                    ),
+                  ),
                 ),
-              ),
-              const SizedBox(width: 8),
-              GestureDetector(
-                onTap: () { setState(() => _sortDesc = !_sortDesc); _search(); },
-                child: Container(
-                  width: 40, height: 40,
-                  decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)),
-                  child: Center(child: Icon(_sortDesc ? Icons.arrow_downward : Icons.arrow_upward, color: Colors.white, size: 20)),
+                const SizedBox(width: 8),
+                GestureDetector(
+                  onTap: _search,
+                  child: Container(
+                    width: 40,
+                    height: 40,
+                    decoration: BoxDecoration(
+                      color: colors.primary,
+                      borderRadius: BorderRadius.circular(20),
+                    ),
+                    child: const Icon(
+                      Icons.search,
+                      color: Colors.white,
+                      size: 22,
+                    ),
+                  ),
                 ),
-              ),
-              const SizedBox(width: 8),
-              GestureDetector(
-              onTap: _search,
-              child: Container(
-                width: 40, height: 40,
-                decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)),
-                child: const Icon(Icons.search, color: Colors.white, size: 22),
-              ),
+              ],
             ),
-          ]),
           ),
         ],
       ),
     );
   }
 
-  Widget _dateChip(TextEditingController ctrl, String hint, TDThemeData tdTheme, AppColorsExtension colors) {
+  Widget _dateChip(
+    TextEditingController ctrl,
+    String hint,
+    TDThemeData tdTheme,
+    AppColorsExtension colors,
+  ) {
     final text = ctrl.text;
     return Container(
       height: 40,
@@ -353,26 +463,41 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
         borderRadius: BorderRadius.circular(20),
         border: Border.all(color: tdTheme.componentStrokeColor),
       ),
-      child: Row(children: [
-        Icon(Icons.calendar_today, size: 16, color: colors.textSecondary),
-        const SizedBox(width: 6),
-        Expanded(
-          child: Text(
-            text.isNotEmpty ? text : hint,
-            maxLines: 1, overflow: TextOverflow.ellipsis,
-            style: TextStyle(fontSize: 14, color: text.isNotEmpty ? colors.textPrimary : colors.textSecondary),
-          ),
-        ),
-        if (text.isNotEmpty)
-          GestureDetector(
-            onTap: () { ctrl.clear(); setState(() {}); },
-            child: Icon(Icons.close, size: 18, color: colors.textSecondary),
+      child: Row(
+        children: [
+          Icon(Icons.calendar_today, size: 16, color: colors.textSecondary),
+          const SizedBox(width: 6),
+          Expanded(
+            child: Text(
+              text.isNotEmpty ? text : hint,
+              maxLines: 1,
+              overflow: TextOverflow.ellipsis,
+              style: TextStyle(
+                fontSize: 14,
+                color: text.isNotEmpty
+                    ? colors.textPrimary
+                    : colors.textSecondary,
+              ),
+            ),
           ),
-      ]),
+          if (text.isNotEmpty)
+            GestureDetector(
+              onTap: () {
+                ctrl.clear();
+                setState(() {});
+              },
+              child: Icon(Icons.close, size: 18, color: colors.textSecondary),
+            ),
+        ],
+      ),
     );
   }
 
-  Widget _buildListContent(AppLocalizations l10n, AppColorsExtension colors, Map<String, List<ImportableItem>> grouped) {
+  Widget _buildListContent(
+    AppLocalizations l10n,
+    AppColorsExtension colors,
+    Map<String, List<ImportableItem>> grouped,
+  ) {
     if (_loading && !_initialLoaded) {
       return EasyRefresh(
         header: TDRefreshHeader(),
@@ -389,7 +514,10 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
         controller: _refreshCtrl,
         onRefresh: _refresh,
         child: ListView(
-          children: [const SizedBox(height: 120), EmptyState(message: l10n.get('noData'))],
+          children: [
+            const SizedBox(height: 120),
+            EmptyState(message: l10n.get('noData')),
+          ],
         ),
       );
     }
@@ -412,7 +540,12 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
     );
   }
 
-  Widget _buildGroupCard(String aeNo, List<ImportableItem> items, AppLocalizations l10n, AppColorsExtension colors) {
+  Widget _buildGroupCard(
+    String aeNo,
+    List<ImportableItem> items,
+    AppLocalizations l10n,
+    AppColorsExtension colors,
+  ) {
     return Padding(
       padding: const EdgeInsets.only(bottom: 16),
       child: Container(
@@ -428,34 +561,90 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
               onTap: () => _toggleGroup(aeNo),
               child: Padding(
                 padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
-                child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
-                  Row(children: [
-                    _buildHeaderCheckbox(aeNo, colors),
-                    const SizedBox(width: 8),
-                    Expanded(child: Text(aeNo, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.textPrimary))),
-                    Column(crossAxisAlignment: CrossAxisAlignment.end, children: [
-                      Text(items.first.aeDd, style: TextStyle(fontSize: 13, color: colors.textSecondary)),
-                      const SizedBox(height: 2),
-                      _priorityChip(items.first.priority, l10n, colors),
-                    ]),
-                  ]),
-                  if (items.first.reason.isNotEmpty)
-                    Padding(
-                      padding: const EdgeInsets.only(top: 4, left: 30),
-                      child: Text(items.first.reason, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 13, color: colors.textSecondary)),
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    Row(
+                      children: [
+                        _buildHeaderCheckbox(aeNo, colors),
+                        const SizedBox(width: 8),
+                        Expanded(
+                          child: Text(
+                            aeNo,
+                            style: TextStyle(
+                              fontSize: 15,
+                              fontWeight: FontWeight.w600,
+                              color: colors.textPrimary,
+                            ),
+                          ),
+                        ),
+                        Column(
+                          crossAxisAlignment: CrossAxisAlignment.end,
+                          children: [
+                            Text(
+                              items.first.aeDd,
+                              style: TextStyle(
+                                fontSize: 13,
+                                color: colors.textSecondary,
+                              ),
+                            ),
+                            const SizedBox(height: 2),
+                            _priorityChip(items.first.priority, l10n, colors),
+                          ],
+                        ),
+                      ],
                     ),
-                ]),
+                    if (items.first.reason.isNotEmpty)
+                      Padding(
+                        padding: const EdgeInsets.only(top: 4, left: 30),
+                        child: Text(
+                          items.first.reason,
+                          maxLines: 1,
+                          overflow: TextOverflow.ellipsis,
+                          style: TextStyle(
+                            fontSize: 13,
+                            color: colors.textSecondary,
+                          ),
+                        ),
+                      ),
+                  ],
+                ),
               ),
             ),
             Divider(height: 1, color: colors.border),
             if (items.length <= 3)
-              ...items.asMap().entries.map((entry) => _buildDetailRow(entry.value, _items.indexOf(entry.value), l10n, colors))
+              ...items.asMap().entries.map(
+                (entry) => _buildDetailRow(
+                  entry.value,
+                  _items.indexOf(entry.value),
+                  l10n,
+                  colors,
+                ),
+              )
             else ...[
-              ...items.take(3).toList().asMap().entries.map((entry) => _buildDetailRow(entry.value, _items.indexOf(entry.value), l10n, colors)),
+              ...items
+                  .take(3)
+                  .toList()
+                  .asMap()
+                  .entries
+                  .map(
+                    (entry) => _buildDetailRow(
+                      entry.value,
+                      _items.indexOf(entry.value),
+                      l10n,
+                      colors,
+                    ),
+                  ),
               GestureDetector(
-                onTap: () => setState(() => _expandedGroups[aeNo] = !(_expandedGroups[aeNo] ?? false)),
+                onTap: () => setState(
+                  () =>
+                      _expandedGroups[aeNo] = !(_expandedGroups[aeNo] ?? false),
+                ),
                 child: Padding(
-                  padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
+                  padding: const EdgeInsets.symmetric(
+                    horizontal: 16,
+                    vertical: 4,
+                  ),
                   child: Row(
                     mainAxisAlignment: MainAxisAlignment.end,
                     children: [
@@ -470,14 +659,35 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
                 ),
               ),
               if (_expandedGroups[aeNo] == true)
-                ...items.skip(3).toList().asMap().entries.map((entry) => _buildDetailRow(entry.value, _items.indexOf(entry.value), l10n, colors)),
+                ...items
+                    .skip(3)
+                    .toList()
+                    .asMap()
+                    .entries
+                    .map(
+                      (entry) => _buildDetailRow(
+                        entry.value,
+                        _items.indexOf(entry.value),
+                        l10n,
+                        colors,
+                      ),
+                    ),
             ],
             if (items.length > 1)
               Padding(
                 padding: const EdgeInsets.fromLTRB(0, 4, 16, 12),
-                child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
-                  Text('${l10n.get('total')} ${items.length} ${l10n.get('unitItem')}', style: TextStyle(fontSize: 12, color: colors.textSecondary)),
-                ]),
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.end,
+                  children: [
+                    Text(
+                      '${l10n.get('total')} ${items.length} ${l10n.get('unitItem')}',
+                      style: TextStyle(
+                        fontSize: 12,
+                        color: colors.textSecondary,
+                      ),
+                    ),
+                  ],
+                ),
               )
             else
               const SizedBox(height: 8),
@@ -487,36 +697,120 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
     );
   }
 
-  Widget _buildDetailRow(ImportableItem d, int idx, AppLocalizations l10n, AppColorsExtension colors) {
+  Widget _buildDetailRow(
+    ImportableItem d,
+    int idx,
+    AppLocalizations l10n,
+    AppColorsExtension colors,
+  ) {
     return GestureDetector(
       behavior: HitTestBehavior.opaque,
       onTap: () => _toggleItem(idx),
       child: Padding(
         padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
-        child: Row(children: [
-          SizedBox(width: 30, child: _buildItemCheckbox(idx, colors)),
-          const SizedBox(width: 4),
-          Container(width: 24, alignment: Alignment.center, child: Text('#${d.itm}', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.textSecondary))),
-          const SizedBox(width: 8),
-          Expanded(
-            child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
-              Text('${d.typeName.isNotEmpty ? '${d.typeNo}/${d.typeName}' : d.typeNo}  ${d.accName}', style: TextStyle(fontSize: 14, color: colors.textPrimary)),
-              if (d.rem.isNotEmpty)
-                Padding(padding: const EdgeInsets.only(top: 2), child: Text(d.rem, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 13, color: colors.textSecondary))),
-              const SizedBox(height: 3),
-              if (d.sqMan.isNotEmpty)
-                Padding(padding: const EdgeInsets.only(bottom: 2), child: Text('${l10n.get('applicant')}: ${d.sqName.isNotEmpty ? '${d.sqMan}/${d.sqName}' : d.sqMan}', style: TextStyle(fontSize: 13, color: colors.textSecondary))),
-              Padding(padding: const EdgeInsets.only(bottom: 2), child: Text('${l10n.get('acctSubject')}: ${d.accNo}/${d.accName}', style: TextStyle(fontSize: 13, color: colors.textSecondary))),
-              if (d.depName.isNotEmpty)
-                Padding(padding: const EdgeInsets.only(bottom: 2), child: Text('${l10n.get('dept')}: ${d.dep}/${d.depName}', style: TextStyle(fontSize: 13, color: colors.textSecondary))),
-              if (d.objName.isNotEmpty)
-                Padding(padding: const EdgeInsets.only(bottom: 2), child: Text('${l10n.get('project')}: ${d.objNo}/${d.objName}', style: TextStyle(fontSize: 13, color: colors.textSecondary))),
-              if (d.startDd.isNotEmpty || d.endDd.isNotEmpty)
-                Text('${l10n.get('date')}: ${d.startDd} ~ ${d.endDd}', style: TextStyle(fontSize: 13, color: colors.textSecondary)),
-            ]),
-          ),
-          Text('¥${d.amtnYj.toStringAsFixed(2)}', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.amountPrimary)),
-        ]),
+        child: Row(
+          children: [
+            SizedBox(width: 30, child: _buildItemCheckbox(idx, colors)),
+            const SizedBox(width: 4),
+            Container(
+              width: 24,
+              alignment: Alignment.center,
+              child: Text(
+                '#${d.itm}',
+                style: TextStyle(
+                  fontSize: 13,
+                  fontWeight: FontWeight.w500,
+                  color: colors.textSecondary,
+                ),
+              ),
+            ),
+            const SizedBox(width: 8),
+            Expanded(
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Text(
+                    '${d.typeName.isNotEmpty ? '${d.typeNo}/${d.typeName}' : d.typeNo}  ${d.accName}',
+                    style: TextStyle(fontSize: 14, color: colors.textPrimary),
+                  ),
+                  if (d.rem.isNotEmpty)
+                    Padding(
+                      padding: const EdgeInsets.only(top: 2),
+                      child: Text(
+                        d.rem,
+                        maxLines: 2,
+                        overflow: TextOverflow.ellipsis,
+                        style: TextStyle(
+                          fontSize: 13,
+                          color: colors.textSecondary,
+                        ),
+                      ),
+                    ),
+                  const SizedBox(height: 3),
+                  if (d.sqMan.isNotEmpty)
+                    Padding(
+                      padding: const EdgeInsets.only(bottom: 2),
+                      child: Text(
+                        '${l10n.get('applicant')}: ${d.sqName.isNotEmpty ? '${d.sqMan}/${d.sqName}' : d.sqMan}',
+                        style: TextStyle(
+                          fontSize: 13,
+                          color: colors.textSecondary,
+                        ),
+                      ),
+                    ),
+                  Padding(
+                    padding: const EdgeInsets.only(bottom: 2),
+                    child: Text(
+                      '${l10n.get('acctSubject')}: ${d.accNo}/${d.accName}',
+                      style: TextStyle(
+                        fontSize: 13,
+                        color: colors.textSecondary,
+                      ),
+                    ),
+                  ),
+                  if (d.depName.isNotEmpty)
+                    Padding(
+                      padding: const EdgeInsets.only(bottom: 2),
+                      child: Text(
+                        '${l10n.get('dept')}: ${d.dep}/${d.depName}',
+                        style: TextStyle(
+                          fontSize: 13,
+                          color: colors.textSecondary,
+                        ),
+                      ),
+                    ),
+                  if (d.objName.isNotEmpty)
+                    Padding(
+                      padding: const EdgeInsets.only(bottom: 2),
+                      child: Text(
+                        '${l10n.get('project')}: ${d.objNo}/${d.objName}',
+                        style: TextStyle(
+                          fontSize: 13,
+                          color: colors.textSecondary,
+                        ),
+                      ),
+                    ),
+                  if (d.startDd.isNotEmpty || d.endDd.isNotEmpty)
+                    Text(
+                      '${l10n.get('date')}: ${d.startDd} ~ ${d.endDd}',
+                      style: TextStyle(
+                        fontSize: 13,
+                        color: colors.textSecondary,
+                      ),
+                    ),
+                ],
+              ),
+            ),
+            Text(
+              '¥${d.amtnYj.toStringAsFixed(2)}',
+              style: TextStyle(
+                fontSize: 15,
+                fontWeight: FontWeight.w600,
+                color: colors.amountPrimary,
+              ),
+            ),
+          ],
+        ),
       ),
     );
   }
@@ -554,9 +848,21 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
     );
   }
 
-  Widget _priorityChip(String priority, AppLocalizations l10n, AppColorsExtension colors) {
-    final label = priority == '3' ? l10n.get('filterCritical') : priority == '2' ? l10n.get('urgent') : l10n.get('normal');
-    final chipColor = priority == '3' ? colors.danger : priority == '2' ? colors.warning : colors.primary;
+  Widget _priorityChip(
+    String priority,
+    AppLocalizations l10n,
+    AppColorsExtension colors,
+  ) {
+    final label = priority == '3'
+        ? l10n.get('filterCritical')
+        : priority == '2'
+        ? l10n.get('urgent')
+        : l10n.get('normal');
+    final chipColor = priority == '3'
+        ? colors.danger
+        : priority == '2'
+        ? colors.warning
+        : colors.primary;
     return Container(
       padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
       decoration: BoxDecoration(
@@ -564,7 +870,14 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
         borderRadius: BorderRadius.circular(4),
         border: Border.all(color: chipColor, width: 0.5),
       ),
-      child: Text(label, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: chipColor)),
+      child: Text(
+        label,
+        style: TextStyle(
+          fontSize: 11,
+          fontWeight: FontWeight.w500,
+          color: chipColor,
+        ),
+      ),
     );
   }
 
@@ -577,32 +890,37 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
       grouped.putIfAbsent(item.aeNo, () => []).add(item);
     }
 
-    return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
-      Expanded(
-        child: GestureDetector(
-          behavior: HitTestBehavior.translucent,
-          onTap: () => FocusScope.of(context).unfocus(),
-          child: Column(children: [
-            _buildSearchBar(l10n, colors),
-            const SizedBox(height: 8),
-            Expanded(
-              child: _buildListContent(l10n, colors, grouped),
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.stretch,
+      children: [
+        Expanded(
+          child: GestureDetector(
+            behavior: HitTestBehavior.translucent,
+            onTap: () => FocusScope.of(context).unfocus(),
+            child: Column(
+              children: [
+                _buildSearchBar(l10n, colors),
+                const SizedBox(height: 8),
+                Expanded(child: _buildListContent(l10n, colors, grouped)),
+              ],
             ),
-          ]),
+          ),
         ),
-      ),
-      SafeArea(
-        top: false, left: false, right: false,
-        child: Padding(
-          padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
-          child: TDButton(
-            text: l10n.get('confirmImport'),
-            size: TDButtonSize.large,
-            theme: TDButtonTheme.primary,
-            onTap: _confirmImport,
+        SafeArea(
+          top: false,
+          left: false,
+          right: false,
+          child: Padding(
+            padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
+            child: TDButton(
+              text: l10n.get('confirmImport'),
+              size: TDButtonSize.large,
+              theme: TDButtonTheme.primary,
+              onTap: _confirmImport,
+            ),
           ),
         ),
-      ),
-    ]);
+      ],
+    );
   }
 }

+ 118 - 43
lib/features/expense_apply/expense_apply_api.dart

@@ -20,7 +20,12 @@ class CostTypeItem {
   final String typeName;
   final String accNo;
   final String accName;
-  const CostTypeItem({required this.typeNo, required this.typeName, required this.accNo, required this.accName});
+  const CostTypeItem({
+    required this.typeNo,
+    required this.typeName,
+    required this.accNo,
+    required this.accName,
+  });
   factory CostTypeItem.fromJson(Map<String, dynamic> json) => CostTypeItem(
     typeNo: json['typeNo'] as String? ?? '',
     typeName: json['typeName'] as String? ?? '',
@@ -33,10 +38,11 @@ class ProjectCodeItem {
   final String objNo;
   final String name;
   const ProjectCodeItem({required this.objNo, required this.name});
-  factory ProjectCodeItem.fromJson(Map<String, dynamic> json) => ProjectCodeItem(
-    objNo: json['objNo'] as String? ?? '',
-    name: json['name'] as String? ?? '',
-  );
+  factory ProjectCodeItem.fromJson(Map<String, dynamic> json) =>
+      ProjectCodeItem(
+        objNo: json['objNo'] as String? ?? '',
+        name: json['name'] as String? ?? '',
+      );
 }
 
 class DepartmentItem {
@@ -65,7 +71,7 @@ class ExpenseApplyApi {
     int size = 20,
   }) async {
     final response = await _client.get<Map<String, dynamic>>(
-      '/OA/GetExpenseApplies',
+      '/OA/GetExpenseApplyList',
       queryParameters: {
         'status': status,
         'keyword': keyword,
@@ -90,33 +96,63 @@ class ExpenseApplyApi {
   }
 
   /// 费用类别字典
-  Future<List<CostTypeItem>> getCostTypes({String keyword = '', String accNo = ''}) async {
+  Future<List<CostTypeItem>> getCostTypes({
+    String keyword = '',
+    String accNo = '',
+  }) async {
     final response = await _client.get<Map<String, dynamic>>(
       '/OA/GetCostTypes',
-      queryParameters: {'keyword': keyword, 'accNo': accNo, 'page': 1, 'size': 100},
+      queryParameters: {
+        'keyword': keyword,
+        'accNo': accNo,
+        'page': 1,
+        'size': 100,
+      },
     );
     final list = (response.data?['list'] as List<dynamic>?) ?? [];
-    return list.map((e) => CostTypeItem.fromJson(e as Map<String, dynamic>)).toList();
+    return list
+        .map((e) => CostTypeItem.fromJson(e as Map<String, dynamic>))
+        .toList();
   }
 
   /// 项目代号
-  Future<List<ProjectCodeItem>> getProjectCodes({String keyword = '', String billDate = ''}) async {
+  Future<List<ProjectCodeItem>> getProjectCodes({
+    String keyword = '',
+    String billDate = '',
+  }) async {
     final response = await _client.get<Map<String, dynamic>>(
       '/OA/GetProjectCodes',
-      queryParameters: {'keyword': keyword, 'billDate': billDate, 'page': 1, 'size': 100},
+      queryParameters: {
+        'keyword': keyword,
+        'billDate': billDate,
+        'page': 1,
+        'size': 100,
+      },
     );
     final list = (response.data?['list'] as List<dynamic>?) ?? [];
-    return list.map((e) => ProjectCodeItem.fromJson(e as Map<String, dynamic>)).toList();
+    return list
+        .map((e) => ProjectCodeItem.fromJson(e as Map<String, dynamic>))
+        .toList();
   }
 
   /// 部门
-  Future<List<DepartmentItem>> getDepartments({String keyword = '', bool onlyActive = true}) async {
+  Future<List<DepartmentItem>> getDepartments({
+    String keyword = '',
+    bool onlyActive = true,
+  }) async {
     final response = await _client.get<Map<String, dynamic>>(
       '/OA/GetDepartments',
-      queryParameters: {'keyword': keyword, 'onlyActive': onlyActive, 'page': 1, 'size': 100},
+      queryParameters: {
+        'keyword': keyword,
+        'onlyActive': onlyActive,
+        'page': 1,
+        'size': 100,
+      },
     );
     final list = (response.data?['list'] as List<dynamic>?) ?? [];
-    return list.map((e) => DepartmentItem.fromJson(e as Map<String, dynamic>)).toList();
+    return list
+        .map((e) => DepartmentItem.fromJson(e as Map<String, dynamic>))
+        .toList();
   }
 
   /// 会计科目树(级联选择器数据源)
@@ -128,12 +164,15 @@ class ExpenseApplyApi {
   /// 提交审批,返回申请单号(提取失败时返回 null,不影响主流程)
   /// BillSave 返回格式: { callok:true, resultData:{ BIL_NO:"AE20267020005", ... } }
   Future<String?> submit(Map<String, dynamic> data) async {
-    final response = await _client.post<Map<String, dynamic>>('/OA/BillSave', data: {
-      'erpCategory': 'MasterService',
-      'billId': 'AE',
-      'procId': '',
-      'data': data,
-    });
+    final response = await _client.post<Map<String, dynamic>>(
+      '/OA/BillSave',
+      data: {
+        'erpCategory': 'MasterService',
+        'billId': 'AE',
+        'procId': '',
+        'data': data,
+      },
+    );
     final resData = response.data;
     if (resData == null) return null;
 
@@ -161,15 +200,22 @@ class ExpenseApplyApi {
 
   /// 下载附件文件字节
   Future<Uint8List?> downloadAttachment(String id) async {
-    return await _client.downloadFile('/OA/DownloadAttachment', queryParameters: {'id': id});
+    return await _client.downloadFile(
+      '/OA/DownloadAttachment',
+      queryParameters: {'id': id},
+    );
   }
 
   /// 检测附件服务是否可用
   Future<bool> checkAttachHealth() async {
     try {
-      final response = await _client.get<Map<String, dynamic>>('/OA/CheckAttachHealth');
+      final response = await _client.get<Map<String, dynamic>>(
+        '/OA/CheckAttachHealth',
+      );
       debugPrint('[checkAttachHealth] response.data: ${response.data}');
-      debugPrint('[checkAttachHealth] response.data type: ${response.data.runtimeType}');
+      debugPrint(
+        '[checkAttachHealth] response.data type: ${response.data.runtimeType}',
+      );
       final available = response.data?['available'] as bool? ?? false;
       debugPrint('[checkAttachHealth] available: $available');
       return available;
@@ -180,7 +226,11 @@ class ExpenseApplyApi {
   }
 
   /// 审批进度
-  Future<Map<String, dynamic>> fetchApprovalTimeline(String bilId, String bilNo, {int bilItm = 0}) async {
+  Future<Map<String, dynamic>> fetchApprovalTimeline(
+    String bilId,
+    String bilNo, {
+    int bilItm = 0,
+  }) async {
     final response = await _client.get<Map<String, dynamic>>(
       '/OA/GetApprovalTimeline',
       queryParameters: {'bilId': bilId, 'bilNo': bilNo, 'bilItm': bilItm},
@@ -189,11 +239,12 @@ class ExpenseApplyApi {
   }
 
   /// 获取单据附件列表
-  Future<List<BillAttachment>> getAttachments(String bilId, String bilNo, {int? srcItm}) async {
-    final params = <String, dynamic>{
-      'bilId': bilId,
-      'bilNo': bilNo,
-    };
+  Future<List<BillAttachment>> getAttachments(
+    String bilId,
+    String bilNo, {
+    int? srcItm,
+  }) async {
+    final params = <String, dynamic>{'bilId': bilId, 'bilNo': bilNo};
     if (srcItm != null) params['srcItm'] = srcItm;
     final response = await _client.get<dynamic>(
       '/OA/GetAttachments',
@@ -216,7 +267,8 @@ class ExpenseApplyApi {
     String filePath,
     Map<String, dynamic> metadata,
   ) async {
-    final fileName = (metadata['FILENAME'] as String?) ?? filePath.split('/').last;
+    final fileName =
+        (metadata['FILENAME'] as String?) ?? filePath.split('/').last;
     final response = await _client.uploadMultipart<Map<String, dynamic>>(
       '/OA/UploadAttachment',
       files: [await MultipartFile.fromFile(filePath, filename: fileName)],
@@ -227,25 +279,40 @@ class ExpenseApplyApi {
 
   /// 审核执行(通过/驳回/反审核)
   Future<Map<String, dynamic>> 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,
+    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<Map<String, dynamic>>(
       '/OA/ExecuteApproval',
       data: {
-        'bilId': bilId, 'bilNo': bilNo, 'bilItm': bilItm,
-        'action': action, 'rem': rem, 'effDd': effDd,
-        'reason': reason, 'isPreToStart': isPreToStart,
-        'nodeIndex': nodeIndex, 'dataBx': dataBx,
+        'bilId': bilId,
+        'bilNo': bilNo,
+        'bilItm': bilItm,
+        'action': action,
+        'rem': rem,
+        'effDd': effDd,
+        'reason': reason,
+        'isPreToStart': isPreToStart,
+        'nodeIndex': nodeIndex,
+        'dataBx': dataBx,
       },
     );
     return response.data!;
   }
 
   /// 费用申请报表
-  Future<ReportData> getExpenseApplyReport({String? startDate, String? endDate}) async {
+  Future<ReportData> getExpenseApplyReport({
+    String? startDate,
+    String? endDate,
+  }) async {
     final params = <String, dynamic>{};
     if (startDate != null) params['startDate'] = startDate;
     if (endDate != null) params['endDate'] = endDate;
@@ -257,12 +324,20 @@ class ExpenseApplyApi {
   }
 
   /// 费用申请报表明细(分页)
-  Future<Map<String, dynamic>> getExpenseApplyReportDetails({
-    String? startDate, String? endDate, int page = 1, int size = 20,
+  Future<Map<String, dynamic>> getExpenseApplyReportDetail({
+    String? startDate,
+    String? endDate,
+    int page = 1,
+    int size = 20,
   }) async {
     final response = await _client.get<Map<String, dynamic>>(
-      '/OA/GetExpenseApplyReportDetails',
-      queryParameters: {'startDate': startDate, 'endDate': endDate, 'page': page, 'size': size},
+      '/OA/GetExpenseApplyReportDetail',
+      queryParameters: {
+        'startDate': startDate,
+        'endDate': endDate,
+        'page': page,
+        'size': size,
+      },
     );
     return response.data!;
   }

+ 358 - 118
lib/features/report/expense_apply_detail_report_page.dart

@@ -58,9 +58,19 @@ class _ExpenseApplyDetailReportPageState
         startDate: _startCtrl.text.isNotEmpty ? _startCtrl.text : null,
         endDate: _endCtrl.text.isNotEmpty ? _endCtrl.text : null,
       );
-      if (mounted) { setState(() { _data = data; _loading = false; }); _loadDetails(); }
+      if (mounted) {
+        setState(() {
+          _data = data;
+          _loading = false;
+        });
+        _loadDetails();
+      }
     } catch (e) {
-      if (mounted) setState(() { _error = e.toString(); _loading = false; });
+      if (mounted)
+        setState(() {
+          _error = e.toString();
+          _loading = false;
+        });
     }
   }
 
@@ -69,18 +79,25 @@ class _ExpenseApplyDetailReportPageState
     setState(() => _detailLoading = true);
     try {
       final api = ref.read(expenseApplyApiProvider);
-      final result = await api.getExpenseApplyReportDetails(
+      final result = await api.getExpenseApplyReportDetail(
         startDate: _startCtrl.text.isNotEmpty ? _startCtrl.text : null,
         endDate: _endCtrl.text.isNotEmpty ? _endCtrl.text : null,
         page: append ? _detailPage : 1,
       );
       if (!mounted) return;
-      final list = (result['list'] as List<dynamic>?)
-          ?.map((e) => ReportDetailItem.fromJson(e as Map<String, dynamic>))
-          .toList() ?? [];
+      final list =
+          (result['list'] as List<dynamic>?)
+              ?.map((e) => ReportDetailItem.fromJson(e as Map<String, dynamic>))
+              .toList() ??
+          [];
       setState(() {
-        if (append) { _details.addAll(list); _detailPage++; }
-        else { _details = list; _detailPage = 2; }
+        if (append) {
+          _details.addAll(list);
+          _detailPage++;
+        } else {
+          _details = list;
+          _detailPage = 2;
+        }
         _detailTotal = result['total'] as int? ?? 0;
         _detailLoading = false;
       });
@@ -117,7 +134,9 @@ class _ExpenseApplyDetailReportPageState
   }
 
   void _applyFilter() {
-    _details = []; _detailPage = 1; _detailTotal = 0;
+    _details = [];
+    _detailPage = 1;
+    _detailTotal = 0;
     _isInitialLoad = false;
     _loadData();
     _loadDetails();
@@ -132,24 +151,44 @@ class _ExpenseApplyDetailReportPageState
     }
     if (_loading) {
       return SingleChildScrollView(
-        child: Column(children: [
-          _buildDateFilter(),
-          const SizedBox(height: 16),
-          Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Bone(height: 80, borderRadius: BorderRadius.circular(12))),
-          const SizedBox(height: 16),
-          Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Bone(height: 200, borderRadius: BorderRadius.circular(12))),
-          const SizedBox(height: 16),
-          Center(child: TDLoading(size: TDLoadingSize.medium, icon: TDLoadingIcon.activity)),
-        ]),
+        child: Column(
+          children: [
+            _buildDateFilter(),
+            const SizedBox(height: 16),
+            Padding(
+              padding: const EdgeInsets.symmetric(horizontal: 12),
+              child: Bone(height: 80, borderRadius: BorderRadius.circular(12)),
+            ),
+            const SizedBox(height: 16),
+            Padding(
+              padding: const EdgeInsets.symmetric(horizontal: 12),
+              child: Bone(height: 200, borderRadius: BorderRadius.circular(12)),
+            ),
+            const SizedBox(height: 16),
+            Center(
+              child: TDLoading(
+                size: TDLoadingSize.medium,
+                icon: TDLoadingIcon.activity,
+              ),
+            ),
+          ],
+        ),
       );
     }
     if (_error != null) {
       return Center(
-        child: Column(mainAxisSize: MainAxisSize.min, children: [
-          Text(_error!, style: TextStyle(color: colors.textSecondary)),
-          const SizedBox(height: 12),
-          TDButton(text: l10n.get('retry'), theme: TDButtonTheme.primary, onTap: _loadData),
-        ]),
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            Text(_error!, style: TextStyle(color: colors.textSecondary)),
+            const SizedBox(height: 12),
+            TDButton(
+              text: l10n.get('retry'),
+              theme: TDButtonTheme.primary,
+              onTap: _loadData,
+            ),
+          ],
+        ),
       );
     }
     return SingleChildScrollView(
@@ -177,33 +216,99 @@ class _ExpenseApplyDetailReportPageState
       ),
       child: Padding(
         padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
-        child: Row(children: [
-          Expanded(child: GestureDetector(behavior: HitTestBehavior.opaque, onTap: () => _pickDate(_startCtrl), child: _dateChip(_startCtrl, l10n.get('filterStartDate'), tdTheme, colors))),
-          const SizedBox(width: 8),
-          Text('—', style: TextStyle(fontSize: 14, color: colors.textSecondary)),
-          const SizedBox(width: 8),
-          Expanded(child: GestureDetector(behavior: HitTestBehavior.opaque, onTap: () => _pickDate(_endCtrl), child: _dateChip(_endCtrl, l10n.get('filterEndDate'), tdTheme, colors))),
-          const SizedBox(width: 8),
-          GestureDetector(
-            onTap: _applyFilter,
-            child: Container(width: 40, height: 40, decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)), child: const Icon(Icons.search, color: Colors.white, size: 22)),
-          ),
-        ]),
+        child: Row(
+          children: [
+            Expanded(
+              child: GestureDetector(
+                behavior: HitTestBehavior.opaque,
+                onTap: () => _pickDate(_startCtrl),
+                child: _dateChip(
+                  _startCtrl,
+                  l10n.get('filterStartDate'),
+                  tdTheme,
+                  colors,
+                ),
+              ),
+            ),
+            const SizedBox(width: 8),
+            Text(
+              '—',
+              style: TextStyle(fontSize: 14, color: colors.textSecondary),
+            ),
+            const SizedBox(width: 8),
+            Expanded(
+              child: GestureDetector(
+                behavior: HitTestBehavior.opaque,
+                onTap: () => _pickDate(_endCtrl),
+                child: _dateChip(
+                  _endCtrl,
+                  l10n.get('filterEndDate'),
+                  tdTheme,
+                  colors,
+                ),
+              ),
+            ),
+            const SizedBox(width: 8),
+            GestureDetector(
+              onTap: _applyFilter,
+              child: Container(
+                width: 40,
+                height: 40,
+                decoration: BoxDecoration(
+                  color: colors.primary,
+                  borderRadius: BorderRadius.circular(20),
+                ),
+                child: const Icon(Icons.search, color: Colors.white, size: 22),
+              ),
+            ),
+          ],
+        ),
       ),
     );
   }
 
   Widget _dateChip(
-      TextEditingController ctrl, String hint, TDThemeData tdTheme, AppColorsExtension colors) {
+    TextEditingController ctrl,
+    String hint,
+    TDThemeData tdTheme,
+    AppColorsExtension colors,
+  ) {
     final text = ctrl.text;
     return Container(
-      height: 40, padding: const EdgeInsets.symmetric(horizontal: 8),
-      decoration: BoxDecoration(color: colors.bgSecondaryContainer, borderRadius: BorderRadius.circular(20), border: Border.all(color: tdTheme.componentStrokeColor)),
-      child: Row(children: [
-        Icon(Icons.calendar_today, size: 14, color: colors.textSecondary), const SizedBox(width: 4),
-        Expanded(child: Text(text.isNotEmpty ? text : hint, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: text.isNotEmpty ? colors.textPrimary : colors.textSecondary))),
-        if (text.isNotEmpty) GestureDetector(onTap: () { ctrl.clear(); setState(() {}); }, child: Icon(Icons.close, size: 16, color: colors.textSecondary)),
-      ]),
+      height: 40,
+      padding: const EdgeInsets.symmetric(horizontal: 8),
+      decoration: BoxDecoration(
+        color: colors.bgSecondaryContainer,
+        borderRadius: BorderRadius.circular(20),
+        border: Border.all(color: tdTheme.componentStrokeColor),
+      ),
+      child: Row(
+        children: [
+          Icon(Icons.calendar_today, size: 14, color: colors.textSecondary),
+          const SizedBox(width: 4),
+          Expanded(
+            child: Text(
+              text.isNotEmpty ? text : hint,
+              maxLines: 1,
+              overflow: TextOverflow.ellipsis,
+              style: TextStyle(
+                fontSize: 12,
+                color: text.isNotEmpty
+                    ? colors.textPrimary
+                    : colors.textSecondary,
+              ),
+            ),
+          ),
+          if (text.isNotEmpty)
+            GestureDetector(
+              onTap: () {
+                ctrl.clear();
+                setState(() {});
+              },
+              child: Icon(Icons.close, size: 16, color: colors.textSecondary),
+            ),
+        ],
+      ),
     );
   }
 
@@ -335,10 +440,7 @@ class _ExpenseApplyDetailReportPageState
               ],
             ),
             const SizedBox(height: 12),
-            SizedBox(
-              height: 200,
-              child: _buildDualLineChart(),
-            ),
+            SizedBox(height: 200, child: _buildDualLineChart()),
           ],
         ),
       ),
@@ -507,97 +609,235 @@ class _ExpenseApplyDetailReportPageState
     if (_detailLoading && _details.isEmpty) {
       return const Padding(
         padding: EdgeInsets.only(top: 32),
-        child: Center(child: TDLoading(size: TDLoadingSize.medium, icon: TDLoadingIcon.activity)),
+        child: Center(
+          child: TDLoading(
+            size: TDLoadingSize.medium,
+            icon: TDLoadingIcon.activity,
+          ),
+        ),
       );
     }
     if (_details.isEmpty) return const SizedBox.shrink();
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    final headerStyle = TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: colors.textSecondary);
+    final headerStyle = TextStyle(
+      fontSize: 12,
+      fontWeight: FontWeight.w600,
+      color: colors.textSecondary,
+    );
     final rowStyle = TextStyle(fontSize: 13, color: colors.textPrimary);
-    final amountStyle = TextStyle(fontSize: 13, color: colors.amountPrimary, fontWeight: FontWeight.w600);
+    final amountStyle = TextStyle(
+      fontSize: 13,
+      color: colors.amountPrimary,
+      fontWeight: FontWeight.w600,
+    );
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
       child: Container(
         width: double.infinity,
-        decoration: BoxDecoration(color: colors.bgCard, borderRadius: BorderRadius.circular(8)),
-        child: Column(children: [
-          Padding(padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), child: Row(children: [
-            Text(l10n.get('detailList'), style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.textPrimary)),
-            const Spacer(),
-            Text('${_details.length} ${l10n.get('items')}', style: TextStyle(fontSize: 12, color: colors.textSecondary)),
-          ])),
-          Container(
-            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
-            color: colors.bgDisabled,
-            child: Row(children: [
-              Expanded(flex: 3, child: Text(l10n.get('expenseApplyNo'), style: headerStyle)),
-              const SizedBox(width: 8),
-              SizedBox(width: 80, child: Text(l10n.get('date'), style: headerStyle)),
-              const SizedBox(width: 8),
-              SizedBox(width: 80, child: Text(l10n.get('estimatedAmount'), textAlign: TextAlign.right, style: headerStyle)),
-              const SizedBox(width: 8),
-              SizedBox(width: 60, child: Text(l10n.get('filterStatus'), textAlign: TextAlign.center, style: headerStyle)),
-            ]),
-          ),
-          ..._details.map((d) {
-            final approved = d.isApproved;
-            final chipColor = approved ? colors.success : colors.warning;
-            return Padding(
-              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
-              child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
-                Expanded(flex: 3, child: Text(d.billNo, style: rowStyle, maxLines: 2, overflow: TextOverflow.ellipsis)),
-                const SizedBox(width: 8),
-                SizedBox(width: 80, child: Text(d.billDate != null && d.billDate!.length >= 10 ? d.billDate!.substring(0, 10) : '-', style: rowStyle, maxLines: 2)),
-                const SizedBox(width: 8),
-                SizedBox(width: 80, child: Text('¥${d.amount.toStringAsFixed(2)}', textAlign: TextAlign.right, style: amountStyle, maxLines: 1, overflow: TextOverflow.ellipsis)),
-                const SizedBox(width: 8),
-                SizedBox(width: 60, child: Container(
-                  padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
-                  decoration: BoxDecoration(
-                    color: chipColor.withValues(alpha: 0.1),
-                    borderRadius: BorderRadius.circular(10),
-                    border: Border.all(color: chipColor, width: 0.5),
+        decoration: BoxDecoration(
+          color: colors.bgCard,
+          borderRadius: BorderRadius.circular(8),
+        ),
+        child: Column(
+          children: [
+            Padding(
+              padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
+              child: Row(
+                children: [
+                  Text(
+                    l10n.get('detailList'),
+                    style: TextStyle(
+                      fontSize: 14,
+                      fontWeight: FontWeight.w600,
+                      color: colors.textPrimary,
+                    ),
                   ),
-                  child: Text(
-                    approved ? l10n.get('approved') : l10n.get('pending'),
-                    textAlign: TextAlign.center,
-                    style: TextStyle(fontSize: 11, color: chipColor, fontWeight: FontWeight.w500),
+                  const Spacer(),
+                  Text(
+                    '${_details.length} ${l10n.get('items')}',
+                    style: TextStyle(fontSize: 12, color: colors.textSecondary),
                   ),
-                )),
-              ]),
-            );
-          }),
-          // 分页栏
-          if (_detailTotal > 0)
-            Container(
-              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
-              decoration: BoxDecoration(
-                border: Border(top: BorderSide(color: colors.border, width: 0.5)),
+                ],
               ),
+            ),
+            Container(
+              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+              color: colors.bgDisabled,
               child: Row(
-                mainAxisAlignment: MainAxisAlignment.center,
                 children: [
-                  GestureDetector(
-                    onTap: _detailPage > 2 ? () { _detailPage -= 2; _loadDetails(append: false); } : null,
-                    child: Icon(Icons.chevron_left, size: 20, color: _detailPage > 2 ? colors.textPrimary : colors.textPlaceholder),
+                  Expanded(
+                    flex: 3,
+                    child: Text(l10n.get('expenseApplyNo'), style: headerStyle),
+                  ),
+                  const SizedBox(width: 8),
+                  SizedBox(
+                    width: 80,
+                    child: Text(l10n.get('date'), style: headerStyle),
+                  ),
+                  const SizedBox(width: 8),
+                  SizedBox(
+                    width: 80,
+                    child: Text(
+                      l10n.get('estimatedAmount'),
+                      textAlign: TextAlign.right,
+                      style: headerStyle,
+                    ),
                   ),
-                  const SizedBox(width: 16),
-                  Text(_detailPage > 1 ? '${_detailPage - 1}' : '1', style: TextStyle(fontSize: 13, color: colors.textPrimary)),
-                  const SizedBox(width: 4),
-                  Text('/ ${(_detailTotal / 20).ceil()}', style: TextStyle(fontSize: 13, color: colors.textSecondary)),
-                  const SizedBox(width: 16),
-                  GestureDetector(
-                    onTap: (_detailPage - 1) * 20 < _detailTotal ? () { _loadDetails(append: true); } : null,
-                    child: Icon(Icons.chevron_right, size: 20, color: (_detailPage - 1) * 20 < _detailTotal ? colors.textPrimary : colors.textPlaceholder),
+                  const SizedBox(width: 8),
+                  SizedBox(
+                    width: 60,
+                    child: Text(
+                      l10n.get('filterStatus'),
+                      textAlign: TextAlign.center,
+                      style: headerStyle,
+                    ),
                   ),
-                  const SizedBox(width: 16),
-                  Text('${l10n.get('total')} $_detailTotal ${l10n.get('items')}', style: TextStyle(fontSize: 12, color: colors.textSecondary)),
                 ],
               ),
             ),
-          const SizedBox(height: 8),
-        ]),
+            ..._details.map((d) {
+              final approved = d.isApproved;
+              final chipColor = approved ? colors.success : colors.warning;
+              return Padding(
+                padding: const EdgeInsets.symmetric(
+                  horizontal: 16,
+                  vertical: 8,
+                ),
+                child: Row(
+                  crossAxisAlignment: CrossAxisAlignment.center,
+                  children: [
+                    Expanded(
+                      flex: 3,
+                      child: Text(
+                        d.billNo,
+                        style: rowStyle,
+                        maxLines: 2,
+                        overflow: TextOverflow.ellipsis,
+                      ),
+                    ),
+                    const SizedBox(width: 8),
+                    SizedBox(
+                      width: 80,
+                      child: Text(
+                        d.billDate != null && d.billDate!.length >= 10
+                            ? d.billDate!.substring(0, 10)
+                            : '-',
+                        style: rowStyle,
+                        maxLines: 2,
+                      ),
+                    ),
+                    const SizedBox(width: 8),
+                    SizedBox(
+                      width: 80,
+                      child: Text(
+                        '¥${d.amount.toStringAsFixed(2)}',
+                        textAlign: TextAlign.right,
+                        style: amountStyle,
+                        maxLines: 1,
+                        overflow: TextOverflow.ellipsis,
+                      ),
+                    ),
+                    const SizedBox(width: 8),
+                    SizedBox(
+                      width: 60,
+                      child: Container(
+                        padding: const EdgeInsets.symmetric(
+                          horizontal: 6,
+                          vertical: 2,
+                        ),
+                        decoration: BoxDecoration(
+                          color: chipColor.withValues(alpha: 0.1),
+                          borderRadius: BorderRadius.circular(10),
+                          border: Border.all(color: chipColor, width: 0.5),
+                        ),
+                        child: Text(
+                          approved ? l10n.get('approved') : l10n.get('pending'),
+                          textAlign: TextAlign.center,
+                          style: TextStyle(
+                            fontSize: 11,
+                            color: chipColor,
+                            fontWeight: FontWeight.w500,
+                          ),
+                        ),
+                      ),
+                    ),
+                  ],
+                ),
+              );
+            }),
+            // 分页栏
+            if (_detailTotal > 0)
+              Container(
+                padding: const EdgeInsets.symmetric(
+                  horizontal: 16,
+                  vertical: 10,
+                ),
+                decoration: BoxDecoration(
+                  border: Border(
+                    top: BorderSide(color: colors.border, width: 0.5),
+                  ),
+                ),
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.center,
+                  children: [
+                    GestureDetector(
+                      onTap: _detailPage > 2
+                          ? () {
+                              _detailPage -= 2;
+                              _loadDetails(append: false);
+                            }
+                          : null,
+                      child: Icon(
+                        Icons.chevron_left,
+                        size: 20,
+                        color: _detailPage > 2
+                            ? colors.textPrimary
+                            : colors.textPlaceholder,
+                      ),
+                    ),
+                    const SizedBox(width: 16),
+                    Text(
+                      _detailPage > 1 ? '${_detailPage - 1}' : '1',
+                      style: TextStyle(fontSize: 13, color: colors.textPrimary),
+                    ),
+                    const SizedBox(width: 4),
+                    Text(
+                      '/ ${(_detailTotal / 20).ceil()}',
+                      style: TextStyle(
+                        fontSize: 13,
+                        color: colors.textSecondary,
+                      ),
+                    ),
+                    const SizedBox(width: 16),
+                    GestureDetector(
+                      onTap: (_detailPage - 1) * 20 < _detailTotal
+                          ? () {
+                              _loadDetails(append: true);
+                            }
+                          : null,
+                      child: Icon(
+                        Icons.chevron_right,
+                        size: 20,
+                        color: (_detailPage - 1) * 20 < _detailTotal
+                            ? colors.textPrimary
+                            : colors.textPlaceholder,
+                      ),
+                    ),
+                    const SizedBox(width: 16),
+                    Text(
+                      '${l10n.get('total')} $_detailTotal ${l10n.get('items')}',
+                      style: TextStyle(
+                        fontSize: 12,
+                        color: colors.textSecondary,
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            const SizedBox(height: 8),
+          ],
+        ),
       ),
     );
   }

+ 399 - 135
lib/features/report/expense_detail_report_page.dart

@@ -80,18 +80,25 @@ class _ExpenseDetailReportPageState
     setState(() => _detailLoading = true);
     try {
       final api = ref.read(expenseApiProvider);
-      final result = await api.getExpenseReportDetails(
+      final result = await api.getExpenseReportDetail(
         startDate: _startCtrl.text.isNotEmpty ? _startCtrl.text : null,
         endDate: _endCtrl.text.isNotEmpty ? _endCtrl.text : null,
         page: append ? _detailPage : 1,
       );
       if (!mounted) return;
-      final list = (result['list'] as List<dynamic>?)
-          ?.map((e) => ReportDetailItem.fromJson(e as Map<String, dynamic>))
-          .toList() ?? [];
+      final list =
+          (result['list'] as List<dynamic>?)
+              ?.map((e) => ReportDetailItem.fromJson(e as Map<String, dynamic>))
+              .toList() ??
+          [];
       setState(() {
-        if (append) { _details.addAll(list); _detailPage++; }
-        else { _details = list; _detailPage = 2; }
+        if (append) {
+          _details.addAll(list);
+          _detailPage++;
+        } else {
+          _details = list;
+          _detailPage = 2;
+        }
         _detailTotal = result['total'] as int? ?? 0;
         _detailLoading = false;
       });
@@ -106,13 +113,22 @@ class _ExpenseDetailReportPageState
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final now = DateTime.now();
     TDPicker.showDatePicker(
-      context, title: l10n.get('selectDate'), backgroundColor: colors.bgCard,
-      useYear: true, useMonth: true, useDay: true,
-      useHour: false, useMinute: false, useSecond: false, useWeekDay: false,
-      dateStart: const [2020, 1, 1], dateEnd: [now.year + 1, 12, 31],
+      context,
+      title: l10n.get('selectDate'),
+      backgroundColor: colors.bgCard,
+      useYear: true,
+      useMonth: true,
+      useDay: true,
+      useHour: false,
+      useMinute: false,
+      useSecond: false,
+      useWeekDay: false,
+      dateStart: const [2020, 1, 1],
+      dateEnd: [now.year + 1, 12, 31],
       initialDate: [now.year, now.month, now.day],
       onConfirm: (selected) {
-        ctrl.text = '${selected['year']}-${selected['month'].toString().padLeft(2, '0')}-${selected['day'].toString().padLeft(2, '0')}';
+        ctrl.text =
+            '${selected['year']}-${selected['month'].toString().padLeft(2, '0')}-${selected['day'].toString().padLeft(2, '0')}';
         setState(() {});
         Navigator.of(context).pop();
       },
@@ -120,16 +136,48 @@ class _ExpenseDetailReportPageState
   }
 
   // ── 日期 chip ──
-  Widget _dateChip(TextEditingController ctrl, String hint, TDThemeData tdTheme, AppColorsExtension colors) {
+  Widget _dateChip(
+    TextEditingController ctrl,
+    String hint,
+    TDThemeData tdTheme,
+    AppColorsExtension colors,
+  ) {
     final text = ctrl.text;
     return Container(
-      height: 40, padding: const EdgeInsets.symmetric(horizontal: 8),
-      decoration: BoxDecoration(color: colors.bgSecondaryContainer, borderRadius: BorderRadius.circular(20), border: Border.all(color: tdTheme.componentStrokeColor)),
-      child: Row(children: [
-        Icon(Icons.calendar_today, size: 14, color: colors.textSecondary), const SizedBox(width: 4),
-        Expanded(child: Text(text.isNotEmpty ? text : hint, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: text.isNotEmpty ? colors.textPrimary : colors.textSecondary))),
-        if (text.isNotEmpty) GestureDetector(onTap: () { ctrl.clear(); setState(() {}); }, child: Icon(Icons.close, size: 16, color: colors.textSecondary)),
-      ]),
+      height: 40,
+      padding: const EdgeInsets.symmetric(horizontal: 8),
+      decoration: BoxDecoration(
+        color: colors.bgSecondaryContainer,
+        borderRadius: BorderRadius.circular(20),
+        border: Border.all(color: tdTheme.componentStrokeColor),
+      ),
+      child: Row(
+        children: [
+          Icon(Icons.calendar_today, size: 14, color: colors.textSecondary),
+          const SizedBox(width: 4),
+          Expanded(
+            child: Text(
+              text.isNotEmpty ? text : hint,
+              maxLines: 1,
+              overflow: TextOverflow.ellipsis,
+              style: TextStyle(
+                fontSize: 12,
+                color: text.isNotEmpty
+                    ? colors.textPrimary
+                    : colors.textSecondary,
+              ),
+            ),
+          ),
+          if (text.isNotEmpty)
+            GestureDetector(
+              onTap: () {
+                ctrl.clear();
+                setState(() {});
+              },
+              child: Icon(Icons.close, size: 16, color: colors.textSecondary),
+            ),
+        ],
+      ),
     );
   }
 
@@ -145,23 +193,60 @@ class _ExpenseDetailReportPageState
       ),
       child: Padding(
         padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
-        child: Row(children: [
-          Expanded(child: GestureDetector(behavior: HitTestBehavior.opaque, onTap: () => _pickDate(_startCtrl), child: _dateChip(_startCtrl, l10n.get('filterStartDate'), tdTheme, colors))),
-          const SizedBox(width: 8),
-          Text('—', style: TextStyle(fontSize: 14, color: colors.textSecondary)),
-          const SizedBox(width: 8),
-          Expanded(child: GestureDetector(behavior: HitTestBehavior.opaque, onTap: () => _pickDate(_endCtrl), child: _dateChip(_endCtrl, l10n.get('filterEndDate'), tdTheme, colors))),
-          const SizedBox(width: 8),
-          GestureDetector(
-            onTap: () {
-              _details = []; _detailPage = 1; _detailTotal = 0;
-              _isInitialLoad = false;
-              _loadData();
-              _loadDetails();
-            },
-            child: Container(width: 40, height: 40, decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)), child: const Icon(Icons.search, color: Colors.white, size: 22)),
-          ),
-        ]),
+        child: Row(
+          children: [
+            Expanded(
+              child: GestureDetector(
+                behavior: HitTestBehavior.opaque,
+                onTap: () => _pickDate(_startCtrl),
+                child: _dateChip(
+                  _startCtrl,
+                  l10n.get('filterStartDate'),
+                  tdTheme,
+                  colors,
+                ),
+              ),
+            ),
+            const SizedBox(width: 8),
+            Text(
+              '—',
+              style: TextStyle(fontSize: 14, color: colors.textSecondary),
+            ),
+            const SizedBox(width: 8),
+            Expanded(
+              child: GestureDetector(
+                behavior: HitTestBehavior.opaque,
+                onTap: () => _pickDate(_endCtrl),
+                child: _dateChip(
+                  _endCtrl,
+                  l10n.get('filterEndDate'),
+                  tdTheme,
+                  colors,
+                ),
+              ),
+            ),
+            const SizedBox(width: 8),
+            GestureDetector(
+              onTap: () {
+                _details = [];
+                _detailPage = 1;
+                _detailTotal = 0;
+                _isInitialLoad = false;
+                _loadData();
+                _loadDetails();
+              },
+              child: Container(
+                width: 40,
+                height: 40,
+                decoration: BoxDecoration(
+                  color: colors.primary,
+                  borderRadius: BorderRadius.circular(20),
+                ),
+                child: const Icon(Icons.search, color: Colors.white, size: 22),
+              ),
+            ),
+          ],
+        ),
       ),
     );
   }
@@ -295,10 +380,7 @@ class _ExpenseDetailReportPageState
               ],
             ),
             const SizedBox(height: 12),
-            SizedBox(
-              height: 200,
-              child: _buildDualLineChart(),
-            ),
+            SizedBox(height: 200, child: _buildDualLineChart()),
           ],
         ),
       ),
@@ -327,7 +409,20 @@ class _ExpenseDetailReportPageState
   Widget _buildDualLineChart() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final monthly = _data?.monthly ?? [];
-    final months = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];
+    final months = [
+      '1月',
+      '2月',
+      '3月',
+      '4月',
+      '5月',
+      '6月',
+      '7月',
+      '8月',
+      '9月',
+      '10月',
+      '11月',
+      '12月',
+    ];
 
     // 构建 12 个月的点,API 未返回的月份填 0
     final amountSpots = List.generate(12, (i) {
@@ -345,7 +440,9 @@ class _ExpenseDetailReportPageState
       allValues.add(m.amount);
       allValues.add(m.approvedAmount);
     }
-    final maxVal = allValues.isEmpty ? 100.0 : allValues.reduce((a, b) => a > b ? a : b);
+    final maxVal = allValues.isEmpty
+        ? 100.0
+        : allValues.reduce((a, b) => a > b ? a : b);
     final maxY = maxVal * 1.2;
 
     return LineChart(
@@ -471,33 +568,62 @@ class _ExpenseDetailReportPageState
     }
     if (_loading) {
       return SingleChildScrollView(
-        child: Column(children: [
-          _buildDateFilter(),
-          const SizedBox(height: 16),
-          Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Bone(height: 80, borderRadius: BorderRadius.circular(12))),
-          const SizedBox(height: 16),
-          Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Bone(height: 200, borderRadius: BorderRadius.circular(12))),
-          const SizedBox(height: 16),
-          Center(child: TDLoading(size: TDLoadingSize.medium, icon: TDLoadingIcon.activity)),
-        ]),
+        child: Column(
+          children: [
+            _buildDateFilter(),
+            const SizedBox(height: 16),
+            Padding(
+              padding: const EdgeInsets.symmetric(horizontal: 12),
+              child: Bone(height: 80, borderRadius: BorderRadius.circular(12)),
+            ),
+            const SizedBox(height: 16),
+            Padding(
+              padding: const EdgeInsets.symmetric(horizontal: 12),
+              child: Bone(height: 200, borderRadius: BorderRadius.circular(12)),
+            ),
+            const SizedBox(height: 16),
+            Center(
+              child: TDLoading(
+                size: TDLoadingSize.medium,
+                icon: TDLoadingIcon.activity,
+              ),
+            ),
+          ],
+        ),
       );
     }
     if (_error != null) {
       return Center(
-        child: Column(mainAxisSize: MainAxisSize.min, children: [
-          Icon(Icons.error_outline, size: 48, color: colors.textPlaceholder),
-          const SizedBox(height: 12),
-          Text(_error!, style: TextStyle(color: colors.textSecondary), textAlign: TextAlign.center),
-          const SizedBox(height: 16),
-          GestureDetector(
-            onTap: _loadData,
-            child: Container(
-              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
-              decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(8)),
-              child: Text(l10n.get('retry'), style: TextStyle(color: Colors.white, fontSize: 14)),
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            Icon(Icons.error_outline, size: 48, color: colors.textPlaceholder),
+            const SizedBox(height: 12),
+            Text(
+              _error!,
+              style: TextStyle(color: colors.textSecondary),
+              textAlign: TextAlign.center,
             ),
-          ),
-        ]),
+            const SizedBox(height: 16),
+            GestureDetector(
+              onTap: _loadData,
+              child: Container(
+                padding: const EdgeInsets.symmetric(
+                  horizontal: 16,
+                  vertical: 8,
+                ),
+                decoration: BoxDecoration(
+                  color: colors.primary,
+                  borderRadius: BorderRadius.circular(8),
+                ),
+                child: Text(
+                  l10n.get('retry'),
+                  style: TextStyle(color: Colors.white, fontSize: 14),
+                ),
+              ),
+            ),
+          ],
+        ),
       );
     }
     return SingleChildScrollView(
@@ -517,97 +643,235 @@ class _ExpenseDetailReportPageState
     if (_detailLoading && _details.isEmpty) {
       return const Padding(
         padding: EdgeInsets.only(top: 32),
-        child: Center(child: TDLoading(size: TDLoadingSize.medium, icon: TDLoadingIcon.activity)),
+        child: Center(
+          child: TDLoading(
+            size: TDLoadingSize.medium,
+            icon: TDLoadingIcon.activity,
+          ),
+        ),
       );
     }
     if (_details.isEmpty) return const SizedBox.shrink();
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    final headerStyle = TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: colors.textSecondary);
+    final headerStyle = TextStyle(
+      fontSize: 12,
+      fontWeight: FontWeight.w600,
+      color: colors.textSecondary,
+    );
     final rowStyle = TextStyle(fontSize: 13, color: colors.textPrimary);
-    final amountStyle = TextStyle(fontSize: 13, color: colors.amountPrimary, fontWeight: FontWeight.w600);
+    final amountStyle = TextStyle(
+      fontSize: 13,
+      color: colors.amountPrimary,
+      fontWeight: FontWeight.w600,
+    );
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
       child: Container(
         width: double.infinity,
-        decoration: BoxDecoration(color: colors.bgCard, borderRadius: BorderRadius.circular(8)),
-        child: Column(children: [
-          Padding(padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), child: Row(children: [
-            Text(l10n.get('detailList'), style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.textPrimary)),
-            const Spacer(),
-            Text('${_details.length} ${l10n.get('items')}', style: TextStyle(fontSize: 12, color: colors.textSecondary)),
-          ])),
-          Container(
-            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
-            color: colors.bgDisabled,
-            child: Row(children: [
-              Expanded(flex: 3, child: Text(l10n.get('expenseNo'), style: headerStyle)),
-              const SizedBox(width: 8),
-              SizedBox(width: 80, child: Text(l10n.get('date'), style: headerStyle)),
-              const SizedBox(width: 8),
-              SizedBox(width: 80, child: Text(l10n.get('expenseAmount'), textAlign: TextAlign.right, style: headerStyle)),
-              const SizedBox(width: 8),
-              SizedBox(width: 60, child: Text(l10n.get('filterStatus'), textAlign: TextAlign.center, style: headerStyle)),
-            ]),
-          ),
-          ..._details.map((d) {
-            final approved = d.isApproved;
-            final chipColor = approved ? colors.success : colors.warning;
-            return Padding(
-              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
-              child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
-                Expanded(flex: 3, child: Text(d.billNo, style: rowStyle, maxLines: 2, overflow: TextOverflow.ellipsis)),
-                const SizedBox(width: 8),
-                SizedBox(width: 80, child: Text(d.billDate != null && d.billDate!.length >= 10 ? d.billDate!.substring(0, 10) : '-', style: rowStyle, maxLines: 2)),
-                const SizedBox(width: 8),
-                SizedBox(width: 80, child: Text('¥${d.amount.toStringAsFixed(2)}', textAlign: TextAlign.right, style: amountStyle, maxLines: 1, overflow: TextOverflow.ellipsis)),
-                const SizedBox(width: 8),
-                SizedBox(width: 60, child: Container(
-                  padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
-                  decoration: BoxDecoration(
-                    color: chipColor.withValues(alpha: 0.1),
-                    borderRadius: BorderRadius.circular(10),
-                    border: Border.all(color: chipColor, width: 0.5),
+        decoration: BoxDecoration(
+          color: colors.bgCard,
+          borderRadius: BorderRadius.circular(8),
+        ),
+        child: Column(
+          children: [
+            Padding(
+              padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
+              child: Row(
+                children: [
+                  Text(
+                    l10n.get('detailList'),
+                    style: TextStyle(
+                      fontSize: 14,
+                      fontWeight: FontWeight.w600,
+                      color: colors.textPrimary,
+                    ),
                   ),
-                  child: Text(
-                    approved ? l10n.get('approved') : l10n.get('pending'),
-                    textAlign: TextAlign.center,
-                    style: TextStyle(fontSize: 11, color: chipColor, fontWeight: FontWeight.w500),
+                  const Spacer(),
+                  Text(
+                    '${_details.length} ${l10n.get('items')}',
+                    style: TextStyle(fontSize: 12, color: colors.textSecondary),
                   ),
-                )),
-              ]),
-            );
-          }),
-          // 分页栏
-          if (_detailTotal > 0)
-            Container(
-              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
-              decoration: BoxDecoration(
-                border: Border(top: BorderSide(color: colors.border, width: 0.5)),
+                ],
               ),
+            ),
+            Container(
+              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+              color: colors.bgDisabled,
               child: Row(
-                mainAxisAlignment: MainAxisAlignment.center,
                 children: [
-                  GestureDetector(
-                    onTap: _detailPage > 2 ? () { _detailPage -= 2; _loadDetails(append: false); } : null,
-                    child: Icon(Icons.chevron_left, size: 20, color: _detailPage > 2 ? colors.textPrimary : colors.textPlaceholder),
+                  Expanded(
+                    flex: 3,
+                    child: Text(l10n.get('expenseNo'), style: headerStyle),
                   ),
-                  const SizedBox(width: 16),
-                  Text(_detailPage > 1 ? '${_detailPage - 1}' : '1', style: TextStyle(fontSize: 13, color: colors.textPrimary)),
-                  const SizedBox(width: 4),
-                  Text('/ ${(_detailTotal / 20).ceil()}', style: TextStyle(fontSize: 13, color: colors.textSecondary)),
-                  const SizedBox(width: 16),
-                  GestureDetector(
-                    onTap: (_detailPage - 1) * 20 < _detailTotal ? () { _loadDetails(append: true); } : null,
-                    child: Icon(Icons.chevron_right, size: 20, color: (_detailPage - 1) * 20 < _detailTotal ? colors.textPrimary : colors.textPlaceholder),
+                  const SizedBox(width: 8),
+                  SizedBox(
+                    width: 80,
+                    child: Text(l10n.get('date'), style: headerStyle),
+                  ),
+                  const SizedBox(width: 8),
+                  SizedBox(
+                    width: 80,
+                    child: Text(
+                      l10n.get('expenseAmount'),
+                      textAlign: TextAlign.right,
+                      style: headerStyle,
+                    ),
+                  ),
+                  const SizedBox(width: 8),
+                  SizedBox(
+                    width: 60,
+                    child: Text(
+                      l10n.get('filterStatus'),
+                      textAlign: TextAlign.center,
+                      style: headerStyle,
+                    ),
                   ),
-                  const SizedBox(width: 16),
-                  Text('${l10n.get('total')} $_detailTotal ${l10n.get('items')}', style: TextStyle(fontSize: 12, color: colors.textSecondary)),
                 ],
               ),
             ),
-          const SizedBox(height: 8),
-        ]),
+            ..._details.map((d) {
+              final approved = d.isApproved;
+              final chipColor = approved ? colors.success : colors.warning;
+              return Padding(
+                padding: const EdgeInsets.symmetric(
+                  horizontal: 16,
+                  vertical: 8,
+                ),
+                child: Row(
+                  crossAxisAlignment: CrossAxisAlignment.center,
+                  children: [
+                    Expanded(
+                      flex: 3,
+                      child: Text(
+                        d.billNo,
+                        style: rowStyle,
+                        maxLines: 2,
+                        overflow: TextOverflow.ellipsis,
+                      ),
+                    ),
+                    const SizedBox(width: 8),
+                    SizedBox(
+                      width: 80,
+                      child: Text(
+                        d.billDate != null && d.billDate!.length >= 10
+                            ? d.billDate!.substring(0, 10)
+                            : '-',
+                        style: rowStyle,
+                        maxLines: 2,
+                      ),
+                    ),
+                    const SizedBox(width: 8),
+                    SizedBox(
+                      width: 80,
+                      child: Text(
+                        '¥${d.amount.toStringAsFixed(2)}',
+                        textAlign: TextAlign.right,
+                        style: amountStyle,
+                        maxLines: 1,
+                        overflow: TextOverflow.ellipsis,
+                      ),
+                    ),
+                    const SizedBox(width: 8),
+                    SizedBox(
+                      width: 60,
+                      child: Container(
+                        padding: const EdgeInsets.symmetric(
+                          horizontal: 6,
+                          vertical: 2,
+                        ),
+                        decoration: BoxDecoration(
+                          color: chipColor.withValues(alpha: 0.1),
+                          borderRadius: BorderRadius.circular(10),
+                          border: Border.all(color: chipColor, width: 0.5),
+                        ),
+                        child: Text(
+                          approved ? l10n.get('approved') : l10n.get('pending'),
+                          textAlign: TextAlign.center,
+                          style: TextStyle(
+                            fontSize: 11,
+                            color: chipColor,
+                            fontWeight: FontWeight.w500,
+                          ),
+                        ),
+                      ),
+                    ),
+                  ],
+                ),
+              );
+            }),
+            // 分页栏
+            if (_detailTotal > 0)
+              Container(
+                padding: const EdgeInsets.symmetric(
+                  horizontal: 16,
+                  vertical: 10,
+                ),
+                decoration: BoxDecoration(
+                  border: Border(
+                    top: BorderSide(color: colors.border, width: 0.5),
+                  ),
+                ),
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.center,
+                  children: [
+                    GestureDetector(
+                      onTap: _detailPage > 2
+                          ? () {
+                              _detailPage -= 2;
+                              _loadDetails(append: false);
+                            }
+                          : null,
+                      child: Icon(
+                        Icons.chevron_left,
+                        size: 20,
+                        color: _detailPage > 2
+                            ? colors.textPrimary
+                            : colors.textPlaceholder,
+                      ),
+                    ),
+                    const SizedBox(width: 16),
+                    Text(
+                      _detailPage > 1 ? '${_detailPage - 1}' : '1',
+                      style: TextStyle(fontSize: 13, color: colors.textPrimary),
+                    ),
+                    const SizedBox(width: 4),
+                    Text(
+                      '/ ${(_detailTotal / 20).ceil()}',
+                      style: TextStyle(
+                        fontSize: 13,
+                        color: colors.textSecondary,
+                      ),
+                    ),
+                    const SizedBox(width: 16),
+                    GestureDetector(
+                      onTap: (_detailPage - 1) * 20 < _detailTotal
+                          ? () {
+                              _loadDetails(append: true);
+                            }
+                          : null,
+                      child: Icon(
+                        Icons.chevron_right,
+                        size: 20,
+                        color: (_detailPage - 1) * 20 < _detailTotal
+                            ? colors.textPrimary
+                            : colors.textPlaceholder,
+                      ),
+                    ),
+                    const SizedBox(width: 16),
+                    Text(
+                      '${l10n.get('total')} $_detailTotal ${l10n.get('items')}',
+                      style: TextStyle(
+                        fontSize: 12,
+                        color: colors.textSecondary,
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            const SizedBox(height: 8),
+          ],
+        ),
       ),
     );
   }