import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../../core/constants.dart'; import '../../shared/models/transaction.dart'; import '../settings/provider.dart'; import 'provider.dart'; final _currencyFmt = NumberFormat.currency(symbol: '\$', decimalDigits: 2); class DashboardScreen extends ConsumerStatefulWidget { const DashboardScreen({super.key}); @override ConsumerState createState() => _DashboardScreenState(); } class _DashboardScreenState extends ConsumerState { final _searchController = TextEditingController(); @override void dispose() { _searchController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final balance = ref.watch(totalBalanceProvider); final income = ref.watch(totalIncomeProvider); final expense = ref.watch(totalExpenseProvider); final monthExpense = ref.watch(currentMonthExpenseProvider); final budget = ref.watch(budgetProvider); final recent = ref.watch(recentTransactionsProvider); final filter = ref.watch(transactionFilterProvider); final budgetExceeded = budget != null && monthExpense > budget; return Scaffold( backgroundColor: AppColors.background, body: SafeArea( child: CustomScrollView( slivers: [ SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'My Finances', style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), ), Text( DateFormat('MMMM yyyy').format(DateTime.now()), style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppColors.textSecondary, ), ), const SizedBox(height: 24), _BalanceCard(balance: balance), const SizedBox(height: 16), _SummaryRow(income: income, expense: expense), if (budget != null) ...[ const SizedBox(height: 16), _BudgetProgress(spent: monthExpense, budget: budget), ], if (budgetExceeded) ...[ const SizedBox(height: 12), _BudgetWarning(spent: monthExpense, budget: budget), ], const SizedBox(height: 24), _SearchBar(controller: _searchController, ref: ref), const SizedBox(height: 12), _FilterChips(selected: filter, ref: ref), const SizedBox(height: 20), Text( 'Transactions', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), const SizedBox(height: 12), ], ), ), ), if (recent.isEmpty) const SliverFillRemaining( hasScrollBody: false, child: _EmptyState(), ) else SliverPadding( padding: const EdgeInsets.fromLTRB(20, 0, 20, 100), sliver: SliverList( delegate: SliverChildBuilderDelegate( (context, i) => Padding( padding: const EdgeInsets.only(bottom: 10), child: _TransactionTile(transaction: recent[i], ref: ref), ), childCount: recent.length, ), ), ), ], ), ), ); } } class _SearchBar extends StatelessWidget { final TextEditingController controller; final WidgetRef ref; const _SearchBar({required this.controller, required this.ref}); @override Widget build(BuildContext context) { return TextField( controller: controller, decoration: InputDecoration( hintText: 'Search transactions...', prefixIcon: const Icon(Icons.search_rounded, color: AppColors.textSecondary), suffixIcon: controller.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear_rounded, size: 20), color: AppColors.textSecondary, onPressed: () { controller.clear(); ref.read(searchQueryProvider.notifier).state = ''; }, ) : null, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), onChanged: (v) => ref.read(searchQueryProvider.notifier).state = v, ); } } class _FilterChips extends StatelessWidget { final TransactionFilter selected; final WidgetRef ref; const _FilterChips({required this.selected, required this.ref}); @override Widget build(BuildContext context) { return Row( children: [ _FilterChip( label: 'All', isSelected: selected == TransactionFilter.all, onTap: () => ref.read(transactionFilterProvider.notifier).state = TransactionFilter.all, ), const SizedBox(width: 8), _FilterChip( label: 'Income', isSelected: selected == TransactionFilter.income, color: AppColors.income, onTap: () => ref.read(transactionFilterProvider.notifier).state = TransactionFilter.income, ), const SizedBox(width: 8), _FilterChip( label: 'Expense', isSelected: selected == TransactionFilter.expense, color: AppColors.expense, onTap: () => ref.read(transactionFilterProvider.notifier).state = TransactionFilter.expense, ), ], ); } } class _FilterChip extends StatelessWidget { final String label; final bool isSelected; final Color? color; final VoidCallback onTap; const _FilterChip({ required this.label, required this.isSelected, this.color, required this.onTap, }); @override Widget build(BuildContext context) { final chipColor = color ?? AppColors.accent; return GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( color: isSelected ? chipColor.withOpacity(0.2) : AppColors.surface, borderRadius: BorderRadius.circular(20), border: Border.all( color: isSelected ? chipColor : AppColors.divider, width: isSelected ? 1.5 : 1, ), ), child: Text( label, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: isSelected ? chipColor : AppColors.textSecondary, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), ), ), ); } } class _BudgetProgress extends StatelessWidget { final double spent; final double budget; const _BudgetProgress({required this.spent, required this.budget}); @override Widget build(BuildContext context) { final ratio = spent / budget; final color = ratio >= 1.0 ? AppColors.expense : ratio >= 0.8 ? AppColors.warning : AppColors.income; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.divider), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Monthly Budget', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppColors.textSecondary, ), ), Text( '${(ratio * 100).toStringAsFixed(0)}%', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: color, fontWeight: FontWeight.w600, ), ), ], ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(8), child: LinearProgressIndicator( value: ratio.clamp(0.0, 1.0), backgroundColor: AppColors.divider, valueColor: AlwaysStoppedAnimation(color), minHeight: 8, ), ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Spent: ${_currencyFmt.format(spent)}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppColors.textSecondary, ), ), Text( 'Limit: ${_currencyFmt.format(budget)}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppColors.textSecondary, ), ), ], ), ], ), ); } } class _BudgetWarning extends StatelessWidget { final double spent; final double budget; const _BudgetWarning({required this.spent, required this.budget}); @override Widget build(BuildContext context) { final over = spent - budget; return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppColors.expense.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.expense.withOpacity(0.3)), ), child: Row( children: [ const Icon(Icons.warning_rounded, color: AppColors.expense, size: 20), const SizedBox(width: 10), Expanded( child: Text( 'Budget exceeded by ${_currencyFmt.format(over)}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppColors.expense, fontWeight: FontWeight.w600, ), ), ), ], ), ); } } class _BalanceCard extends StatelessWidget { final double balance; const _BalanceCard({required this.balance}); @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.all(24), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF7C6DED), Color(0xFF5A4FBF)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: AppColors.accent.withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 8), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Total Balance', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.white70, ), ), const SizedBox(height: 8), Text( _currencyFmt.format(balance), style: Theme.of(context).textTheme.headlineMedium?.copyWith( color: Colors.white, fontWeight: FontWeight.w700, letterSpacing: -0.5, ), ), ], ), ); } } class _SummaryRow extends StatelessWidget { final double income; final double expense; const _SummaryRow({required this.income, required this.expense}); @override Widget build(BuildContext context) { return Row( children: [ Expanded(child: _SummaryCard(label: 'Income', amount: income, color: AppColors.income, icon: Icons.arrow_downward_rounded)), const SizedBox(width: 12), Expanded(child: _SummaryCard(label: 'Expenses', amount: expense, color: AppColors.expense, icon: Icons.arrow_upward_rounded)), ], ); } } class _SummaryCard extends StatelessWidget { final String label; final double amount; final Color color; final IconData icon; const _SummaryCard({required this.label, required this.amount, required this.color, required this.icon}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.divider), ), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: color.withOpacity(0.15), borderRadius: BorderRadius.circular(10), ), child: Icon(icon, color: color, size: 18), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: AppColors.textSecondary)), const SizedBox(height: 2), Text( _currencyFmt.format(amount), style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: color, fontWeight: FontWeight.w600, ), overflow: TextOverflow.ellipsis, ), ], ), ), ], ), ); } } class _TransactionTile extends StatelessWidget { final Transaction transaction; final WidgetRef ref; const _TransactionTile({required this.transaction, required this.ref}); void _showUndoSnackBar(BuildContext context, Transaction tx) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Transaction deleted'), duration: const Duration(seconds: 5), action: SnackBarAction( label: 'Undo', onPressed: () { ref.read(transactionsProvider.notifier).restore(tx); }, ), backgroundColor: AppColors.surface, ), ); } @override Widget build(BuildContext context) { final isIncome = transaction.type == TransactionType.income; final color = isIncome ? AppColors.income : AppColors.expense; final catColor = AppCategories.colors[transaction.category] ?? AppColors.accent; final catIcon = AppCategories.icons[transaction.category] ?? Icons.category_rounded; return Dismissible( key: Key(transaction.id), direction: DismissDirection.endToStart, background: Container( alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 20), decoration: BoxDecoration( color: AppColors.expense.withOpacity(0.15), borderRadius: BorderRadius.circular(16), ), child: const Icon(Icons.delete_outline_rounded, color: AppColors.expense), ), onDismissed: (_) { ref.read(transactionsProvider.notifier).delete(transaction.id); _showUndoSnackBar(context, transaction); }, child: GestureDetector( onTap: () => context.push('/add', extra: transaction), child: Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.divider), ), child: Row( children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: catColor.withOpacity(0.15), borderRadius: BorderRadius.circular(12), ), child: Icon(catIcon, color: catColor, size: 20), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( transaction.category, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), if (transaction.note != null && transaction.note!.isNotEmpty) Text( transaction.note!, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppColors.textSecondary, ), overflow: TextOverflow.ellipsis, ) else Text( DateFormat('MMM d, yyyy').format(transaction.date), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppColors.textSecondary, ), ), ], ), ), Text( '${isIncome ? '+' : '-'}${_currencyFmt.format(transaction.amount)}', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: color, fontWeight: FontWeight.w700, ), ), ], ), ), ), ); } } class _EmptyState extends StatelessWidget { const _EmptyState(); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(24), decoration: const BoxDecoration( color: AppColors.surface, shape: BoxShape.circle, ), child: const Icon( Icons.receipt_long_rounded, size: 48, color: AppColors.textSecondary, ), ), const SizedBox(height: 16), Text( 'No transactions found', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: AppColors.textPrimary, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 6), Text( 'Tap + to add your first transaction', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppColors.textSecondary, ), ), ], ), ); } }