import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/theme/app_theme.dart'; import '../../models/finance.dart'; import '../../providers/finance_provider.dart'; import '../../widgets/data_table_card.dart'; import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButton, FilterableColumnHeader; import '../../widgets/page_scaffold.dart'; import '../../providers/connectivity_provider.dart'; import '../../core/utils/export_util.dart'; class FinanceScreen extends ConsumerWidget { const FinanceScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return PageScaffold( title: '财务管理', tabs: const [ Tab(text: '全部记录'), Tab(text: '应付账款'), Tab(text: '应收账款'), ], tabViews: [ _FinanceTab(typeFilter: ''), _FinanceTab(typeFilter: 'payable'), _FinanceTab(typeFilter: 'receivable'), ], ); } } // Each tab has its own independent state — avoids shared-provider conflicts class _FinanceTab extends ConsumerStatefulWidget { final String typeFilter; const _FinanceTab({required this.typeFilter}); @override ConsumerState<_FinanceTab> createState() => _FinanceTabState(); } class _FinanceTabState extends ConsumerState<_FinanceTab> { late String _month; int _page = 1; int _pageSize = 20; // We drive fetches by maintaining a Future locally, bypassing the global provider late Future> _future; List _allRecords = []; int _total = 0; Set _filterType = {}; Set _filterPartner = {}; Set _hiddenCols = {}; static const _colDefs = [ ColDef('date', '日期', required: true), ColDef('type', '类型'), ColDef('partner', '往来单位'), ColDef('ref', '关联单据', minWidth: 900), ColDef('amount', '金额'), ColDef('balance', '余额'), ColDef('remark', '备注', minWidth: 1000), ]; @override void initState() { super.initState(); final now = DateTime.now(); _month = '${now.year}-${now.month.toString().padLeft(2, '0')}'; _fetch(); } void _fetch() { _future = ref .read(financeRepositoryProvider) .listRecords( type: widget.typeFilter.isEmpty ? null : widget.typeFilter, month: _month, page: _page, pageSize: _pageSize, ) .then((r) { _allRecords = r.data; _total = r.total; return r.data; }); } void _refetch() { setState(() => _fetch()); } List _applyFilters(List all) { return all.where((r) { if (_filterType.isNotEmpty && !_filterType.contains(r.typeLabel)) { return false; } if (_filterPartner.isNotEmpty && !_filterPartner.contains(r.partnerName ?? '')) { return false; } return true; }).toList(); } @override Widget build(BuildContext context) { // 网络恢复时自动刷新 ref.listen(networkRecoveryCountProvider, (_, __) => _refetch()); return FutureBuilder>( future: _future, builder: (context, snap) { if (snap.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snap.hasError) { if (_allRecords.isNotEmpty) { return Column( children: [ _OfflineBanner(onRetry: _refetch), Expanded(child: _buildContent(_applyFilters(_allRecords))), ], ); } return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.cloud_off, size: 40, color: AppTheme.textSecondary), const SizedBox(height: 12), const Text('暂无数据,网络不可用', style: TextStyle(color: AppTheme.textSecondary)), const SizedBox(height: 12), ElevatedButton(onPressed: _refetch, child: const Text('重试')), ], ), ); } final filtered = _applyFilters(_allRecords); return _buildContent(filtered); }, ); } Widget _buildContent(List records) { final totalAmount = records.fold(0.0, (s, r) => s + r.amount); final totalBalance = records.fold(0.0, (s, r) => s + r.balance); final totalPaid = totalAmount - totalBalance; // Derive filter options from all loaded records final typeOptions = _allRecords .map((r) => r.typeLabel) .toSet() .toList() ..sort(); final partnerOptions = _allRecords .map((r) => r.partnerName ?? '') .where((s) => s.isNotEmpty) .toSet() .toList() ..sort(); // Build visible columns (manual hide + responsive auto-hide by screen width) final screenWidth = MediaQuery.of(context).size.width; final visibleCols = _colDefs .where((c) => !_hiddenCols.contains(c.key) && (c.minWidth == null || screenWidth >= c.minWidth!)) .toList(); final columns = visibleCols.map((c) { final label = switch (c.key) { 'type' => FilterableColumnHeader( text: c.label, options: typeOptions, selected: _filterType, onChanged: (v) => setState(() => _filterType = v), ), 'partner' => FilterableColumnHeader( text: c.label, options: partnerOptions, selected: _filterPartner, onChanged: (v) => setState(() => _filterPartner = v), ), _ => Text(c.label), }; return DataColumn( label: label, numeric: c.key == 'amount' || c.key == 'balance'); }).toList(); DataCell buildFinanceCell(String key, FinanceRecord r) { switch (key) { case 'date': return DataCell(Text( r.recordDate?.substring(0, 10) ?? '-', style: const TextStyle(fontSize: 12), )); case 'type': return DataCell(_TypeBadge(r.typeLabel)); case 'partner': return DataCell(SizedBox( width: 160, child: Text(r.partnerName ?? '-', overflow: TextOverflow.ellipsis), )); case 'ref': return DataCell(Text( r.refType != null && r.refId != null ? '${r.refType!.replaceAll('_', '-')}#${r.refId}' : '-', style: const TextStyle( fontSize: 11, fontFamily: 'monospace', color: AppTheme.primary), )); case 'amount': return DataCell(Text( '¥${r.amount.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.w500), )); case 'balance': return DataCell(Text( '¥${r.balance.toStringAsFixed(2)}', style: TextStyle( color: r.balance > 0 ? AppTheme.danger : AppTheme.textSecondary, fontWeight: FontWeight.w600, ), )); case 'remark': return DataCell(SizedBox( width: 160, child: Text(r.remark ?? '-', overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary)), )); default: return const DataCell(SizedBox()); } } final rows = records.isEmpty ? [ DataRow( cells: List.generate( visibleCols.length, (i) => i == 0 ? const DataCell(Text('暂无记录', style: TextStyle(color: AppTheme.textSecondary))) : const DataCell(SizedBox()), ), ), ] : records .map((r) => DataRow( cells: visibleCols .map((c) => buildFinanceCell(c.key, r)) .toList(), )) .toList(); return Column( children: [ // Summary bar (only for type-filtered tabs) if (widget.typeFilter.isNotEmpty && records.isNotEmpty) Container( color: AppTheme.background, padding: const EdgeInsets.all(12), child: Row( children: [ _SummaryCard( title: '本期总额', value: '¥${(totalAmount / 10000).toStringAsFixed(2)}万', icon: Icons.account_balance_wallet, color: AppTheme.primary, ), const SizedBox(width: 12), _SummaryCard( title: '已结清', value: '¥${(totalPaid / 10000).toStringAsFixed(2)}万', icon: Icons.check_circle, color: AppTheme.success, ), const SizedBox(width: 12), _SummaryCard( title: '未结清', value: '¥${(totalBalance / 10000).toStringAsFixed(2)}万', icon: Icons.pending_actions, color: AppTheme.danger, ), ], ), ), if (widget.typeFilter.isNotEmpty && records.isNotEmpty) const Divider(height: 1), Expanded( child: DataTableCard( totalCount: _total > 0 ? _total : records.length, page: _page, pageSize: _pageSize, onPageChanged: (p) { setState(() { _page = p; _fetch(); }); }, onPageSizeChanged: (s) { setState(() { _pageSize = s; _page = 1; _fetch(); }); }, toolbar: Row( children: [ OutlinedButton.icon( onPressed: () { final tabName = widget.typeFilter.isEmpty ? '财务全部记录' : widget.typeFilter == 'payable' ? '应付账款' : '应收账款'; exportExcel( filename: tabName, headers: ['日期', '类型', '往来单位', '关联单据', '金额', '余额', '备注'], rows: records.map((r) => [ r.recordDate?.substring(0, 10) ?? '', r.typeLabel, r.partnerName ?? '', r.refType != null && r.refId != null ? '${r.refType!.replaceAll('_', '-')}#${r.refId}' : '', r.amount, r.balance, r.remark ?? '', ]).toList(), ); }, icon: const Icon(Icons.download, size: 16), label: const Text('导出'), ), const Spacer(), _MonthSelector( value: _month, onChanged: (v) { _month = v; _page = 1; _refetch(); }, ), const SizedBox(width: 8), ColumnToggleButton( columns: _colDefs, hidden: _hiddenCols, onChanged: (v) => setState(() => _hiddenCols = v), ), ], ), columns: columns, rows: rows, ), ), ], ); } } class _TypeBadge extends StatelessWidget { final String label; const _TypeBadge(this.label); @override Widget build(BuildContext context) { Color bg, fg; switch (label) { case '应收账款': bg = const Color(0xFFE3F2FD); fg = AppTheme.primary; break; case '应付账款': bg = const Color(0xFFFFEBEE); fg = AppTheme.danger; break; case '收款': bg = const Color(0xFFE8F5E9); fg = AppTheme.success; break; case '付款': bg = const Color(0xFFFFF3E0); fg = AppTheme.accent; break; default: bg = const Color(0xFFF5F5F5); fg = AppTheme.textSecondary; } return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(3)), child: Text(label, style: TextStyle( color: fg, fontSize: 12, fontWeight: FontWeight.w500)), ); } } class _SummaryCard extends StatelessWidget { final String title; final String value; final IconData icon; final Color color; const _SummaryCard({ required this.title, required this.value, required this.icon, required this.color, }); @override Widget build(BuildContext context) { return Expanded( child: Container( height: 72, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: AppTheme.surface, borderRadius: BorderRadius.circular(4), border: Border.all(color: AppTheme.border, width: 0.5), ), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: color, size: 22), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text(title, style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary)), const SizedBox(height: 4), Text(value, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: color)), ], ), ], ), ), ); } } class _MonthSelector extends StatelessWidget { final String value; final ValueChanged onChanged; const _MonthSelector({required this.value, required this.onChanged}); @override Widget build(BuildContext context) { final now = DateTime.now(); final months = List.generate(12, (i) { final d = DateTime(now.year, now.month - i, 1); return '${d.year}-${d.month.toString().padLeft(2, '0')}'; }); final safeValue = months.contains(value) ? value : months.first; return Container( height: 36, padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( border: Border.all(color: AppTheme.border), borderRadius: BorderRadius.circular(4), color: AppTheme.surface, ), child: DropdownButtonHideUnderline( child: DropdownButton( value: safeValue, items: months .map((m) => DropdownMenuItem( value: m, child: Text(m, style: const TextStyle(fontSize: 13)))) .toList(), onChanged: (v) => onChanged(v!), style: const TextStyle(fontSize: 13, color: AppTheme.textPrimary), ), ), ); } } class _OfflineBanner extends StatelessWidget { final VoidCallback onRetry; const _OfflineBanner({required this.onRetry}); @override Widget build(BuildContext context) { return Container( width: double.infinity, color: const Color(0xFFFFF8E1), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: Row( children: [ const Icon(Icons.cloud_off, size: 14, color: Color(0xFFF57F17)), const SizedBox(width: 8), const Expanded( child: Text('网络不可用,当前显示离线缓存数据', style: TextStyle(color: Color(0xFFF57F17), fontSize: 12)), ), TextButton( onPressed: onRetry, style: TextButton.styleFrom( foregroundColor: const Color(0xFFF57F17), padding: const EdgeInsets.symmetric(horizontal: 8)), child: const Text('重试', style: TextStyle(fontSize: 12)), ), ], ), ); } }