import 'package:flutter/material.dart'; import '../../../../app/theme/app_colors.dart'; import '../../../../app/theme/app_typography.dart'; import '../../../../app/theme/app_spacing.dart'; import '../../../../app/i18n/app_localizations.dart'; /// 对账与结算报表页面 /// /// 支持按日/周/月/季度查看对账数据 /// 包含:汇总卡片、对账明细表、自动对账状态、差异调查、导出功能 class ReconciliationPage extends StatefulWidget { const ReconciliationPage({super.key}); @override State createState() => _ReconciliationPageState(); } class _ReconciliationPageState extends State { String _selectedPeriod = 'reconciliation_period_month'; final _periodKeys = ['reconciliation_period_day', 'reconciliation_period_week', 'reconciliation_period_month', 'reconciliation_period_quarter']; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(context.t('reconciliation_title')), actions: [ IconButton( icon: const Icon(Icons.download_rounded), onPressed: () => _showExportDialog(context), tooltip: context.t('reconciliation_export_tooltip'), ), ], ), body: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Period Selector _buildPeriodSelector(), const SizedBox(height: 20), // Summary Cards _buildSummaryCards(), const SizedBox(height: 24), // Auto-reconciliation Status _buildAutoReconciliationStatus(), const SizedBox(height: 24), // Reconciliation Table _buildReconciliationTable(), const SizedBox(height: 24), // Discrepancy Details _buildDiscrepancySection(), const SizedBox(height: 24), // Export Buttons _buildExportButtons(), ], ), ), ); } // ============================================================ // Period Selector // ============================================================ Widget _buildPeriodSelector() { return Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: AppColors.gray50, borderRadius: BorderRadius.circular(AppSpacing.radiusSm), ), child: Row( children: _periodKeys.map((key) { final isSelected = _selectedPeriod == key; return Expanded( child: GestureDetector( onTap: () => setState(() => _selectedPeriod = key), child: Container( padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( color: isSelected ? AppColors.surface : Colors.transparent, borderRadius: BorderRadius.circular(AppSpacing.radiusSm - 2), boxShadow: isSelected ? AppSpacing.shadowSm : null, ), child: Center( child: Text( context.t(key), style: AppTypography.labelMedium.copyWith( color: isSelected ? AppColors.primary : AppColors.textSecondary, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, ), ), ), ), ), ); }).toList(), ), ); } // ============================================================ // Summary Cards // ============================================================ Widget _buildSummaryCards() { final summaries = [ (context.t('reconciliation_expected'), '\$132,490', AppColors.primary, Icons.account_balance_rounded), (context.t('reconciliation_settled'), '\$118,200', AppColors.success, Icons.check_circle_rounded), (context.t('reconciliation_pending'), '\$12,180', AppColors.warning, Icons.schedule_rounded), (context.t('reconciliation_discrepancy_amount'), '\$2,110', AppColors.error, Icons.error_outline_rounded), ]; return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: 1.6, ), itemCount: summaries.length, itemBuilder: (context, index) { final (label, value, color, icon) = summaries[index]; return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(AppSpacing.radiusMd), border: Border.all(color: AppColors.borderLight), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Icon(icon, color: color, size: 16), const SizedBox(width: 6), Text(label, style: AppTypography.bodySmall), ], ), Text( value, style: AppTypography.h2.copyWith(color: color), ), ], ), ); }, ); } // ============================================================ // Auto-reconciliation Status // ============================================================ Widget _buildAutoReconciliationStatus() { const matchRate = 96.8; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(AppSpacing.radiusMd), border: Border.all(color: AppColors.borderLight), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: AppColors.successLight, borderRadius: BorderRadius.circular(AppSpacing.radiusSm), ), child: const Icon(Icons.auto_fix_high_rounded, color: AppColors.success, size: 18), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(context.t('reconciliation_auto_title'), style: AppTypography.labelMedium), Text(context.t('reconciliation_auto_last_run'), style: AppTypography.caption), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: AppColors.successLight, borderRadius: BorderRadius.circular(AppSpacing.radiusFull), ), child: Text( context.t('reconciliation_auto_running'), style: AppTypography.caption.copyWith( color: AppColors.success, fontWeight: FontWeight.w600, ), ), ), ], ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(context.t('reconciliation_match_rate'), style: AppTypography.bodySmall), Text( '$matchRate%', style: AppTypography.labelMedium.copyWith(color: AppColors.success), ), ], ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: matchRate / 100, backgroundColor: AppColors.gray100, valueColor: const AlwaysStoppedAnimation(AppColors.success), minHeight: 8, ), ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildMiniStat(context.t('reconciliation_matched'), '4,832', AppColors.success), _buildMiniStat(context.t('reconciliation_to_check'), '158', AppColors.warning), _buildMiniStat(context.t('reconciliation_has_diff'), '12', AppColors.error), ], ), ], ), ); } Widget _buildMiniStat(String label, String value, Color color) { return Column( children: [ Text( value, style: AppTypography.labelLarge.copyWith(color: color), ), const SizedBox(height: 2), Text(label, style: AppTypography.caption), ], ); } // ============================================================ // Reconciliation Table // ============================================================ Widget _buildReconciliationTable() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(context.t('reconciliation_detail_title'), style: AppTypography.h3), const SizedBox(height: 12), Container( decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(AppSpacing.radiusMd), border: Border.all(color: AppColors.borderLight), ), child: Column( children: [ // Table Header Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: const BoxDecoration( color: AppColors.gray50, borderRadius: BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), ), ), child: Row( children: [ Expanded(flex: 2, child: Text(context.t('reconciliation_col_period'), style: AppTypography.labelSmall)), Expanded(flex: 2, child: Text(context.t('reconciliation_col_expected'), style: AppTypography.labelSmall)), Expanded(flex: 2, child: Text(context.t('reconciliation_col_actual'), style: AppTypography.labelSmall)), Expanded(flex: 2, child: Text(context.t('reconciliation_col_diff'), style: AppTypography.labelSmall)), Expanded(flex: 2, child: Text(context.t('reconciliation_col_status'), style: AppTypography.labelSmall)), ], ), ), // Table Rows ..._mockReconciliationData.map((row) { final (period, expected, actual, diff, status) = row; return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: const BoxDecoration( border: Border( bottom: BorderSide(color: AppColors.borderLight, width: 0.5), ), ), child: Row( children: [ Expanded( flex: 2, child: Text(period, style: AppTypography.bodySmall.copyWith(color: AppColors.textPrimary)), ), Expanded( flex: 2, child: Text(expected, style: AppTypography.bodySmall.copyWith(color: AppColors.textPrimary)), ), Expanded( flex: 2, child: Text(actual, style: AppTypography.bodySmall.copyWith(color: AppColors.textPrimary)), ), Expanded( flex: 2, child: Text( diff, style: AppTypography.bodySmall.copyWith( color: diff == '\$0' ? AppColors.textTertiary : AppColors.error, ), ), ), Expanded( flex: 2, child: _buildReconciliationStatus(status), ), ], ), ); }), ], ), ), ], ); } Widget _buildReconciliationStatus(String status) { Color color; Color bgColor; switch (status) { case '已对账': color = AppColors.success; bgColor = AppColors.successLight; break; case '有差异': color = AppColors.error; bgColor = AppColors.errorLight; break; default: color = AppColors.warning; bgColor = AppColors.warningLight; } return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(AppSpacing.radiusFull), ), child: Text( status, style: TextStyle(fontSize: 10, color: color, fontWeight: FontWeight.w600), textAlign: TextAlign.center, ), ); } // ============================================================ // Discrepancy Details // ============================================================ Widget _buildDiscrepancySection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(context.t('reconciliation_discrepancy_title'), style: AppTypography.h3), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: AppColors.errorLight, borderRadius: BorderRadius.circular(AppSpacing.radiusFull), ), child: Text( '3 ${context.t('reconciliation_pending_items')}', style: AppTypography.caption.copyWith( color: AppColors.error, fontWeight: FontWeight.w600, ), ), ), ], ), const SizedBox(height: 12), ..._mockDiscrepancies.map((d) { final (desc, amount, investigationStatus, date) = d; Color statusColor; switch (investigationStatus) { case '调查中': statusColor = AppColors.warning; break; case '已解决': statusColor = AppColors.success; break; default: statusColor = AppColors.error; } return Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(AppSpacing.radiusMd), border: Border.all(color: AppColors.borderLight), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text(desc, style: AppTypography.labelMedium), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: statusColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(AppSpacing.radiusFull), ), child: Text( investigationStatus, style: AppTypography.caption.copyWith( color: statusColor, fontWeight: FontWeight.w600, ), ), ), ], ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '${context.t('reconciliation_discrepancy_amount_label')}: $amount', style: AppTypography.bodySmall.copyWith(color: AppColors.error), ), Text(date, style: AppTypography.caption), ], ), ], ), ); }), ], ); } // ============================================================ // Export Buttons // ============================================================ Widget _buildExportButtons() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(AppSpacing.radiusMd), border: Border.all(color: AppColors.borderLight), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(context.t('reconciliation_export_title'), style: AppTypography.h3), const SizedBox(height: 14), Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: () { // TODO: Export reconciliation report as PDF }, icon: const Icon(Icons.picture_as_pdf_rounded, size: 18), label: Text(context.t('finance_export_pdf')), style: OutlinedButton.styleFrom( minimumSize: const Size(0, 48), foregroundColor: AppColors.error, side: const BorderSide(color: AppColors.error, width: 1), ), ), ), const SizedBox(width: 12), Expanded( child: OutlinedButton.icon( onPressed: () { // TODO: Export reconciliation report as Excel }, icon: const Icon(Icons.table_chart_rounded, size: 18), label: Text(context.t('finance_export_excel')), style: OutlinedButton.styleFrom( minimumSize: const Size(0, 48), foregroundColor: AppColors.success, side: const BorderSide(color: AppColors.success, width: 1), ), ), ), ], ), ], ), ); } // ============================================================ // Export Dialog // ============================================================ void _showExportDialog(BuildContext context) { showDialog( context: context, builder: (ctx) => SimpleDialog( title: Text(context.t('reconciliation_export_dialog_title')), children: [ SimpleDialogOption( onPressed: () => Navigator.pop(ctx), child: Row( children: [ const Icon(Icons.picture_as_pdf_rounded, color: AppColors.error, size: 20), const SizedBox(width: 12), Text(context.t('finance_export_pdf')), ], ), ), SimpleDialogOption( onPressed: () => Navigator.pop(ctx), child: Row( children: [ const Icon(Icons.table_chart_rounded, color: AppColors.success, size: 20), const SizedBox(width: 12), Text(context.t('finance_export_excel')), ], ), ), SimpleDialogOption( onPressed: () => Navigator.pop(ctx), child: Row( children: [ const Icon(Icons.description_rounded, color: AppColors.info, size: 20), const SizedBox(width: 12), Text(context.t('finance_export_csv')), ], ), ), ], ), ); } } // ============================================================ // Mock Data // ============================================================ const _mockReconciliationData = [ ('2026年1月', '\$32,100', '\$32,100', '\$0', '已对账'), ('2025年12月', '\$28,450', '\$28,450', '\$0', '已对账'), ('2025年11月', '\$25,800', '\$24,690', '\$1,110', '有差异'), ('2025年10月', '\$30,200', '\$30,200', '\$0', '已对账'), ('2025年9月', '\$22,940', '\$22,940', '\$0', '已对账'), ('2025年8月', '\$18,600', '\$17,600', '\$1,000', '有差异'), ('2025年7月', '\$15,400', '\$15,400', '\$0', '待对账'), ]; const _mockDiscrepancies = [ ('11月退款差异 - 部分退款未入账', '\$780', '调查中', '2025-12-15'), ('11月手续费差异 - 费率计算偏差', '\$330', '调查中', '2025-12-12'), ('8月核销结算延迟', '\$1,000', '已解决', '2025-09-20'), ];