import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:intl/intl.dart'; import '../../transactions/presentation/add_transaction_screen.dart'; import '../../transactions/presentation/all_transactions_screen.dart'; import '../../transactions/providers/providers.dart'; import '../../auth/presentation/profile_screen.dart'; import '../../transactions/data/models.dart'; import '../../budget/presentation/budget_screen.dart'; import '../providers/insight_provider.dart'; import '../../sales/providers/invoices_provider.dart'; import '../../sales/providers/customers_provider.dart'; import '../../sales/domain/invoice.dart'; import '../../sales/presentation/customers_list_screen.dart'; import 'widgets/swipeable_account_card.dart'; import 'widgets/budget_status_card.dart'; import '../../inventory/presentation/daily_rates_screen.dart'; import 'widgets/upcoming_dues_widget.dart'; import 'widgets/statistics_tab.dart'; import '../../../core/widgets/shimmer_loading.dart'; import '../../../core/theme/nature_colors.dart'; import 'wallet_ledger_screen.dart'; import 'maturity_dialog.dart'; import 'accounts_screen.dart'; import '../../business/providers/business_mode_provider.dart'; import '../../business/presentation/hub/business_hub_screen.dart'; class DashboardScreen extends ConsumerStatefulWidget { const DashboardScreen({super.key}); @override ConsumerState createState() => _DashboardScreenState(); } class _DashboardScreenState extends ConsumerState { int _currentIndex = 0; String _selectedFilter = 'All Time'; DateTimeRange? _customDateRange; Timer? _notificationTimer; final List _filters = ['Today', 'Last 3 Days', 'Week', 'Month', 'Year', 'All Time', 'Custom']; @override void initState() { super.initState(); _notificationTimer = Timer.periodic(const Duration(minutes: 2), (_) { ref.invalidate(invitationProvider); }); } @override void dispose() { _notificationTimer?.cancel(); super.dispose(); } Future _onRefresh() async { ref.invalidate(transactionProvider); ref.invalidate(walletProvider); ref.invalidate(categoryProvider); ref.invalidate(budgetProvider); ref.invalidate(invitationProvider); ref.invalidate(invoicesProvider); ref.invalidate(customersProvider); await Future.delayed(const Duration(milliseconds: 500)); } DateTimeRange _getDateRange(List all) { if (_selectedFilter == 'All Time') { DateTime start = DateTime(2000); if (all.isNotEmpty) { start = all.map((t) => t.date).reduce((a, b) => a.isBefore(b) ? a : b); } return DateTimeRange(start: start, end: DateTime.now()); } final now = DateTime.now(); DateTime start; DateTime end = now; switch (_selectedFilter) { case 'Today': start = DateTime(now.year, now.month, now.day); break; case 'Last 3 Days': start = now.subtract(const Duration(days: 3)); break; case 'Week': start = now.subtract(const Duration(days: 7)); break; case 'Month': start = DateTime(now.year, now.month - 1, now.day); break; case 'Year': start = DateTime(now.year - 1, now.month, now.day); break; case 'Custom': if (_customDateRange != null) { start = _customDateRange!.start; end = _customDateRange!.end; } else { start = DateTime(2000); if (all.isNotEmpty) { start = all.map((t) => t.date).reduce((a, b) => a.isBefore(b) ? a : b); } } break; default: start = DateTime(2000); } return DateTimeRange(start: start, end: end); } List _filterTransactions(List all) { if (_selectedFilter == 'All Time' || (_selectedFilter == 'Custom' && _customDateRange == null)) return all; final range = _getDateRange(all); final start = range.start; final end = range.end; return all.where((t) { final d = t.date; return d.isAfter(start.subtract(const Duration(seconds: 1))) && d.isBefore(end.add(const Duration(days: 1))); }).toList(); } Future _pickCustomDateRange() async { final picked = await showDateRangePicker( context: context, firstDate: DateTime(2000), lastDate: DateTime(2101), ); if (picked != null) { setState(() { _customDateRange = picked; _selectedFilter = 'Custom'; }); } } @override Widget build(BuildContext context) { final transState = ref.watch(transactionProvider); final categoriesState = ref.watch(categoryProvider); final insight = ref.watch(insightProvider); final walletsState = ref.watch(walletProvider); final invoicesState = ref.watch(invoicesProvider); final customersState = ref.watch(customersProvider); final isBusinessMode = ref.watch(businessModeProvider); final allTransactions = transState.value ?? []; final range = _getDateRange(allTransactions); final startDate = range.start; final endDate = range.end; String title; if (_currentIndex == 0) { title = 'Dashboard'; } else if (isBusinessMode) { if (_currentIndex == 1) title = 'Business Hub'; else if (_currentIndex == 2) title = 'Statistics'; else if (_currentIndex == 3) title = 'My Accounts'; else title = 'Budgets'; } else { if (_currentIndex == 1) title = 'Statistics'; else if (_currentIndex == 2) title = 'My Accounts'; else title = 'Budgets'; } return Scaffold( appBar: AppBar( title: Text(title), backgroundColor: Colors.transparent, elevation: 0, actions: [ if (isBusinessMode) IconButton( icon: const Icon(LucideIcons.trendingUp), tooltip: 'Daily Commodity Rates', onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (_) => const DailyRatesScreen()), ); }, ), Consumer( builder: (context, ref, child) { final invitationsState = ref.watch(invitationProvider); final pendingInvites = invitationsState.value?.where((inv) => inv.status == 'PENDING').toList() ?? []; return PopupMenuButton( icon: Stack( children: [ const Icon(LucideIcons.bell), if (pendingInvites.isNotEmpty) Positioned( right: 0, top: 0, child: Container( padding: const EdgeInsets.all(2), decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(6), ), constraints: const BoxConstraints(minWidth: 12, minHeight: 12), child: Text( '${pendingInvites.length}', style: const TextStyle(color: Colors.white, fontSize: 8, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), ), ) ], ), offset: const Offset(0, 50), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), itemBuilder: (context) { if (pendingInvites.isEmpty) { return [ const PopupMenuItem( enabled: false, child: Text('No new notifications'), ) ]; } return pendingInvites.map((inv) => PopupMenuItem( enabled: false, child: Container( width: 250, padding: const EdgeInsets.symmetric(vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('You have been invited to a wallet by User ${inv.inviterId}', style: const TextStyle(fontSize: 14)), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () { Navigator.pop(context); ref.read(invitationProvider.notifier).rejectInvitation(inv.id); }, style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('Reject'), ), const SizedBox(width: 8), ElevatedButton( onPressed: () { Navigator.pop(context); ref.read(invitationProvider.notifier).acceptInvitation(inv.id); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF6C63FF), foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), child: const Text('Accept'), ), ], ) ], ), ), )).toList(); }, ); } ), IconButton( icon: const Icon(LucideIcons.user), onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (_) => const ProfileScreen())); }, ), ], ), body: transState.when( loading: () => ListView.builder( padding: const EdgeInsets.all(16), itemCount: 4, itemBuilder: (context, index) => const ShimmerCard(), ), error: (err, stack) => Center(child: Text('Error: $err')), data: (allTransactions) { final transactions = _filterTransactions(allTransactions); final totalIncome = transactions.where((t) => t.type == 'INCOME').fold(0.0, (s, t) => s + t.amount); final totalExpense = transactions.where((t) => t.type == 'EXPENSE').fold(0.0, (s, t) => s + t.amount); final totalInvestment = transactions.where((t) => t.type == 'INVESTMENT').fold(0.0, (s, t) => s + t.amount); double totalPayablesPeriod = 0.0; double totalReceivablesPeriod = 0.0; final safeWallets = walletsState.hasValue ? walletsState.value! : []; for (var t in transactions) { if (t.toWalletId != null) { final w = safeWallets.where((w) => w.id == t.toWalletId); if (w.isNotEmpty) { final nature = w.first.nature; if (nature == 'PAYABLES' || nature == 'LOAN') { totalPayablesPeriod += t.amount; } else if (nature == 'RECEIVABLES' || nature == 'LENDING') { totalReceivablesPeriod += t.amount; } } } } double cashBalance = 0; double expenseBalance = 0; double savingsBalance = 0; double investmentsBalance = 0; double payablesBalance = 0; double receivablesBalance = 0; if (walletsState.hasValue) { for (var w in walletsState.value!) { final nature = w.nature ?? 'CASH'; if (nature == 'CASH') { cashBalance += w.balance; } else if (nature == 'EXPENSE') { expenseBalance += w.balance; } else if (nature == 'SAVINGS' || nature == 'INCOME') { savingsBalance += w.balance; } else if (nature == 'INVESTMENTS') { investmentsBalance += w.balance; } else if (nature == 'LOAN' || nature == 'PAYABLES') { payablesBalance += w.balance; } else if (nature == 'LENDING') { receivablesBalance += w.balance; } } } // Process invoices to get total pending double totalPendingInvoices = 0; Map pendingByCustomer = {}; if (invoicesState.hasValue && customersState.hasValue) { final invoices = invoicesState.value!; for (var inv in invoices) { if (inv.status != 'PAID' && inv.status != 'CANCELLED' && inv.customerId != null) { double paidAmount = inv.amountPaid ?? 0; double pendingAmount = inv.totalAmount - paidAmount; if (pendingAmount > 0) { pendingByCustomer[inv.customerId!] = (pendingByCustomer[inv.customerId!] ?? 0) + pendingAmount; totalPendingInvoices += pendingAmount; } } } } receivablesBalance += totalPendingInvoices; // Build Payables Carousel Items List payablesItems = []; if (walletsState.hasValue) { final payableWallets = walletsState.value!.where((w) => w.nature == 'PAYABLES' || w.nature == 'LOAN').toList(); for (var w in payableWallets) { if (w.balance > 0) { payablesItems.add(CarouselItemData( title: 'To: ${w.name}', amount: w.balance, color: NatureColors.getColor('PAYABLES'), icon: LucideIcons.userMinus, onTap: () { Navigator.push(context, MaterialPageRoute(builder: (_) => WalletLedgerScreen(wallet: w))); } )); } } } // Build Receivables Carousel Items List receivablesItems = []; if (invoicesState.hasValue && customersState.hasValue) { final customers = customersState.value!; pendingByCustomer.forEach((custId, amount) { if (amount > 0) { final customerName = customers.where((c) => c.id == custId).firstOrNull?.name ?? 'Unknown Customer'; receivablesItems.add(CarouselItemData( title: 'From: $customerName', amount: amount, color: NatureColors.getColor('RECEIVABLES'), icon: LucideIcons.userPlus, onTap: () { // Navigate to customer details or invoices in the future } )); } }); } return IndexedStack( index: _currentIndex, children: [ // ---------------- HOME TAB ---------------- RefreshIndicator( onRefresh: _onRefresh, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (insight != null) ...[ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: insight.type == 'WARNING' ? Colors.orange.shade50 : (insight.type == 'SUCCESS' ? Colors.green.shade50 : Colors.blue.shade50), borderRadius: BorderRadius.circular(16), border: Border.all(color: insight.type == 'WARNING' ? Colors.orange : (insight.type == 'SUCCESS' ? Colors.green : Colors.blue), width: 1), ), child: Row( children: [ Icon( insight.type == 'WARNING' ? LucideIcons.alertTriangle : (insight.type == 'SUCCESS' ? LucideIcons.checkCircle : LucideIcons.info), color: insight.type == 'WARNING' ? Colors.orange : (insight.type == 'SUCCESS' ? Colors.green : Colors.blue), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(insight.title, style: const TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 4), Text(insight.message, style: const TextStyle(fontSize: 12)), ], ), ), ], ), ), const SizedBox(height: 24), ], const BudgetStatusCard(), const SizedBox(height: 24), // Summary Cards Grid GridView.count( crossAxisCount: 2, crossAxisSpacing: 16, mainAxisSpacing: 16, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), childAspectRatio: 1.5, children: [ _buildSummaryCard(context, 'Balance', cashBalance, NatureColors.getColor('BALANCE'), LucideIcons.wallet, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'CASH')))), _buildSummaryCard(context, 'Expense', expenseBalance, NatureColors.getColor('EXPENSE'), LucideIcons.arrowUpRight, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'EXPENSE')))), _buildSummaryCard(context, 'Savings', savingsBalance, NatureColors.getColor('SAVINGS'), LucideIcons.piggyBank, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'SAVINGS')))), _buildSummaryCard(context, 'Investments', investmentsBalance, NatureColors.getColor('INVESTMENTS'), LucideIcons.trendingUp, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'INVESTMENTS')))), _buildSummaryCard(context, 'Total Payables', payablesBalance, NatureColors.getColor('PAYABLES'), LucideIcons.alertCircle, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen(initialFilterNature: 'PAYABLES')))), _buildSummaryCard(context, 'Total Receivables', receivablesBalance, NatureColors.getColor('RECEIVABLES'), LucideIcons.arrowDownLeft, onTap: () { Navigator.push(context, MaterialPageRoute(builder: (_) => const CustomersListScreen())); }), ], ), const SizedBox(height: 24), const UpcomingDuesWidget(), if (payablesItems.isNotEmpty) ...[ const SizedBox(height: 24), Text('Upcoming Payables', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 12), SwipeableAccountCard(items: payablesItems), ], if (receivablesItems.isNotEmpty) ...[ const SizedBox(height: 24), Text('Upcoming Receivables', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 12), SwipeableAccountCard(items: receivablesItems), ], const SizedBox(height: 32), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Recent Transactions', style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold)), TextButton( onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (_) => const AllTransactionsScreen())); }, child: const Text('See All'), ), ], ), const SizedBox(height: 8), allTransactions.isEmpty ? const Center(child: Padding(padding: EdgeInsets.all(16), child: Text('No transactions yet!'))) : ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: allTransactions.length > 5 ? 5 : allTransactions.length, itemBuilder: (context, index) { final t = allTransactions[index]; final isIncome = t.type == 'INCOME' || (t.type == 'TRANSFER' && t.fromWalletId == null); final isInvestment = t.type == 'INVESTMENT'; final isExpense = t.type == 'EXPENSE' || (t.type == 'TRANSFER' && t.toWalletId == null); final isTransfer = t.type == 'TRANSFER' && t.fromWalletId != null && t.toWalletId != null; Color typeColor = Colors.grey; IconData typeIcon = LucideIcons.arrowRightLeft; if (isIncome) { typeColor = NatureColors.getColor('INCOME'); typeIcon = LucideIcons.arrowDownCircle; } else if (isExpense) { typeColor = NatureColors.getColor('EXPENSE'); typeIcon = LucideIcons.arrowUpCircle; } else if (isInvestment) { typeColor = NatureColors.getColor('INVESTMENTS'); typeIcon = LucideIcons.trendingUp; } else if (isTransfer) { typeColor = NatureColors.getColor('TRANSFER'); if (walletsState.hasValue) { final toW = walletsState.value!.where((w) => w.id == t.toWalletId).firstOrNull; final fromW = walletsState.value!.where((w) => w.id == t.fromWalletId).firstOrNull; if (toW != null && (toW.nature == 'PAYABLES' || toW.nature == 'LOAN')) { typeColor = NatureColors.getColor('PAYABLES'); typeIcon = LucideIcons.alertCircle; } else if (toW != null && (toW.nature == 'RECEIVABLES' || toW.nature == 'LENDING')) { typeColor = NatureColors.getColor('RECEIVABLES'); typeIcon = LucideIcons.arrowDownLeft; } else if (fromW != null && (fromW.nature == 'PAYABLES' || fromW.nature == 'LOAN')) { typeColor = NatureColors.getColor('PAYABLES'); typeIcon = LucideIcons.alertCircle; } else if (fromW != null && (fromW.nature == 'RECEIVABLES' || fromW.nature == 'LENDING')) { typeColor = NatureColors.getColor('RECEIVABLES'); typeIcon = LucideIcons.arrowDownLeft; } } } String categoryName = 'Unknown'; if (categoriesState.hasValue) { final match = categoriesState.value!.where((c) => c.id == t.categoryId); if (match.isNotEmpty) categoryName = match.first.name; } String accountName = 'Unknown'; if (walletsState.hasValue) { if (isExpense && t.toWalletId != null) { final match = walletsState.value!.where((w) => w.id == t.toWalletId); if (match.isNotEmpty) accountName = match.first.name; } else if (isIncome && (t.toWalletId != null)) { final match = walletsState.value!.where((w) => w.id == t.toWalletId); if (match.isNotEmpty) accountName = match.first.name; } else if (isTransfer && t.toWalletId != null) { final match = walletsState.value!.where((w) => w.id == t.toWalletId); if (match.isNotEmpty) accountName = 'To ${match.first.name}'; } } String fromName = ''; if (t.fromWalletId != null && walletsState.hasValue) { final match = walletsState.value!.where((w) => w.id == t.fromWalletId); if (match.isNotEmpty) fromName = match.first.name; } String toName = ''; if (t.toWalletId != null && walletsState.hasValue) { final match = walletsState.value!.where((w) => w.id == t.toWalletId); if (match.isNotEmpty) toName = match.first.name; } String subtitleText = ''; if (categoryName == 'Unknown' && fromName.isNotEmpty && toName.isNotEmpty) { subtitleText = '$fromName → $toName • ${DateFormat('MMM dd, yyyy').format(t.date)}'; } else if (categoryName == 'Unknown' && toName.isNotEmpty) { subtitleText = 'To $toName • ${DateFormat('MMM dd, yyyy').format(t.date)}'; } else if (categoryName == 'Unknown' && fromName.isNotEmpty) { subtitleText = 'From $fromName • ${DateFormat('MMM dd, yyyy').format(t.date)}'; } else { subtitleText = '$categoryName • ${DateFormat('MMM dd, yyyy').format(t.date)}${fromName.isNotEmpty ? ' • 💼 $fromName' : ''}'; } return Card( margin: const EdgeInsets.only(bottom: 12), child: ListTile( onTap: () { Navigator.push(context, MaterialPageRoute(builder: (_) => AddTransactionScreen(transaction: t))); }, leading: CircleAvatar( backgroundColor: typeColor.withOpacity(0.1), child: Icon(typeIcon, color: typeColor), ), title: Text((t.description != null && t.description!.isNotEmpty) ? t.description! : accountName, style: const TextStyle(fontWeight: FontWeight.bold)), subtitle: Text(subtitleText), trailing: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '${isIncome ? '+' : (isTransfer ? '' : '-')}Rs. ${t.amount.toStringAsFixed(0)}', style: TextStyle(fontWeight: FontWeight.bold, color: typeColor), ), if (t.investmentStatus == 'OPEN' && (typeColor == NatureColors.getColor('INVESTMENTS') || typeColor == NatureColors.getColor('RECEIVABLES'))) GestureDetector( onTap: () { showDialog(context: context, builder: (_) => MaturityDialog(transaction: t)); }, child: const Padding( padding: EdgeInsets.only(top: 4.0), child: Text('Close', style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 12)), ), ), ], ), ), ); }, ) ], ), ), ), // ---------------- BUSINESS TAB ---------------- if (isBusinessMode) const BusinessHubScreen(), // ---------------- STATS TAB ---------------- StatisticsTab( transactions: transactions, wallets: safeWallets, categories: categoriesState.value ?? [], selectedFilter: _selectedFilter, startDate: startDate, endDate: endDate, filters: _filters, onFilterChanged: (val) { setState(() => _selectedFilter = val); }, onPickCustomDateRange: _pickCustomDateRange, onRefresh: _onRefresh, ), // ---------------- WALLETS TAB ---------------- const AccountsScreen(), // ---------------- BUDGETS TAB ---------------- const BudgetScreen(), ], ); }, ), bottomNavigationBar: BottomNavigationBar( currentIndex: isBusinessMode ? (_currentIndex >= 3 ? _currentIndex + 1 : _currentIndex) : (_currentIndex >= 2 ? _currentIndex + 1 : _currentIndex), type: BottomNavigationBarType.fixed, onTap: (index) { final addIndex = isBusinessMode ? 3 : 2; if (index == addIndex) { Navigator.push(context, MaterialPageRoute(builder: (_) => const AddTransactionScreen())); } else { setState(() { _currentIndex = index > addIndex ? index - 1 : index; }); } }, selectedItemColor: Theme.of(context).colorScheme.primary, unselectedItemColor: Colors.grey, showSelectedLabels: true, showUnselectedLabels: true, items: [ const BottomNavigationBarItem(icon: Icon(LucideIcons.home), label: 'Home'), if (isBusinessMode) const BottomNavigationBarItem(icon: Icon(LucideIcons.briefcase), label: 'Business'), const BottomNavigationBarItem(icon: Icon(LucideIcons.pieChart), label: 'Stats'), const BottomNavigationBarItem(icon: Icon(LucideIcons.plusCircle), label: 'Add'), const BottomNavigationBarItem(icon: Icon(LucideIcons.wallet), label: 'Wallets'), const BottomNavigationBarItem(icon: Icon(LucideIcons.target), label: 'Budgets'), ], ), ); } Widget _buildSummaryCard(BuildContext context, String title, double amount, Color color, IconData icon, {VoidCallback? onTap}) { return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(16), border: Border.all(color: color.withOpacity(0.2)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Icon(icon, color: color, size: 16), const SizedBox(width: 8), Text(title, style: TextStyle(color: color, fontWeight: FontWeight.w600)), ], ), Text( 'Rs. ${amount.toStringAsFixed(0)}', style: TextStyle(color: color, fontSize: 20, fontWeight: FontWeight.bold), ), ], ), ), ); } }