Files
Kifi/kifi-app/lib/features/dashboard/presentation/dashboard_screen.dart

816 lines
35 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons_flutter/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/presentation/customers_list_screen.dart';
import 'widgets/swipeable_account_card.dart';
import 'widgets/budget_status_card.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 'maturity_dialog.dart';
import 'accounts_screen.dart';
import '../../business/providers/business_mode_provider.dart';
import '../../business/presentation/hub/business_hub_screen.dart';
import '../../../core/widgets/responsive_layout.dart';
import '../../../core/widgets/desktop_sidebar.dart';
class DashboardScreen extends ConsumerStatefulWidget {
const DashboardScreen({super.key});
@override
ConsumerState<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends ConsumerState<DashboardScreen> {
int _currentIndex = 0;
String _selectedFilter = 'All Time';
DateTimeRange? _customDateRange;
Timer? _notificationTimer;
final List<String> _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<void> _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<Transaction> 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<Transaction> _filterTransactions(List<Transaction> 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<void> _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;
final isDesktop = ResponsiveLayout.isDesktop(context);
return Scaffold(
appBar: isDesktop
? null
: AppBar(
title: const Text('Dashboard'),
backgroundColor: Colors.transparent,
elevation: 0,
actions: [
_buildNotificationBell(context),
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 safeWallets = walletsState.hasValue ? walletsState.value! : <Wallet>[];
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
Map<int, double> pendingByCustomer = {};
if (invoicesState.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;
}
}
}
}
List<CarouselItemData> payablesItems = [];
if (walletsState.hasValue) {
final wallets = walletsState.value!;
for (var w in wallets) {
if ((w.nature == 'PAYABLES' || w.nature == 'LOAN') && w.balance > 0) {
payablesItems.add(CarouselItemData(
title: w.name,
amount: w.balance,
color: NatureColors.getColor('PAYABLES'),
icon: LucideIcons.alertCircle,
));
}
}
}
List<CarouselItemData> 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: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => const CustomersListScreen()));
},
));
}
});
}
// Desktop multi-column dashboard body
Widget desktopDashboardView = RefreshIndicator(
onRefresh: _onRefresh,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 28.0, vertical: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top Row Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Financial Overview',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.w800,
letterSpacing: -0.5,
),
),
const SizedBox(height: 4),
Text(
DateFormat('EEEE, MMMM d, yyyy').format(DateTime.now()),
style: TextStyle(
fontSize: 13,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white60
: Colors.black54,
),
),
],
),
Row(
children: [
_buildNotificationBell(context),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const AddTransactionScreen()),
);
},
icon: const Icon(LucideIcons.plus, size: 16),
label: const Text('Add Transaction'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2563EB),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
),
],
),
],
),
const SizedBox(height: 24),
if (insight != null) ...[
_buildInsightCard(insight),
const SizedBox(height: 24),
],
// 3-Column Summary Cards Grid on Desktop
GridView.count(
crossAxisCount: 3,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
childAspectRatio: 2.2,
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: 28),
// 2-Column Split Body Layout on Desktop
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Left Column (60%): Budget status & Recent Transactions
Expanded(
flex: 6,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const BudgetStatusCard(),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Recent Transactions', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => const AllTransactionsScreen()));
},
child: const Text('See All'),
),
],
),
const SizedBox(height: 8),
_buildRecentTransactionsList(allTransactions, safeWallets),
],
),
),
const SizedBox(width: 24),
// Right Column (40%): Upcoming Dues & Payables / Receivables
Expanded(
flex: 4,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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),
],
],
),
),
],
),
],
),
),
);
// Mobile single-column scrollable dashboard body
Widget mobileDashboardView = RefreshIndicator(
onRefresh: _onRefresh,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (insight != null) ...[
_buildInsightCard(insight),
const SizedBox(height: 24),
],
const BudgetStatusCard(),
const SizedBox(height: 24),
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),
_buildRecentTransactionsList(allTransactions, safeWallets),
],
),
),
);
if (isDesktop) {
final List<Widget> desktopScreens = [
desktopDashboardView,
const AllTransactionsScreen(),
const AccountsScreen(),
const BudgetScreen(),
if (isBusinessMode) const BusinessHubScreen(),
];
int safeDesktopIndex = _currentIndex;
if (safeDesktopIndex >= desktopScreens.length) {
safeDesktopIndex = 0;
}
return Row(
children: [
DesktopSidebar(
selectedIndex: safeDesktopIndex,
onDestinationSelected: (idx) {
setState(() => _currentIndex = idx);
},
),
Expanded(
child: MaxContentWidth(
maxWidth: 1400,
child: IndexedStack(
index: safeDesktopIndex,
children: desktopScreens,
),
),
),
],
);
}
// Mobile View
final List<Widget> mobileScreens = [
mobileDashboardView,
if (isBusinessMode) const BusinessHubScreen(),
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,
),
const AccountsScreen(),
const BudgetScreen(),
];
int safeMobileIndex = _currentIndex;
if (safeMobileIndex >= mobileScreens.length) {
safeMobileIndex = 0;
}
return IndexedStack(
index: safeMobileIndex,
children: mobileScreens,
);
},
),
bottomNavigationBar: isDesktop
? null
: Builder(
builder: (context) {
final isBusinessMode = ref.watch(businessModeProvider);
final List<BottomNavigationBarItem> navItems = [];
navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.home), label: 'Home'));
if (isBusinessMode) navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.briefcase), label: 'Business'));
navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.pieChart), label: 'Stats'));
navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.plusCircle), label: 'Add'));
navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.wallet), label: 'Wallets'));
navItems.add(const BottomNavigationBarItem(icon: Icon(LucideIcons.target), label: 'Budgets'));
int addIdx = 1;
if (isBusinessMode) addIdx++;
addIdx++; // For Stats
int displayIndex = _currentIndex;
if (_currentIndex >= addIdx) displayIndex++;
if (displayIndex >= navItems.length) displayIndex = navItems.length - 1;
return BottomNavigationBar(
currentIndex: displayIndex,
type: BottomNavigationBarType.fixed,
onTap: (index) {
if (index == addIdx) {
Navigator.push(context, MaterialPageRoute(builder: (_) => const AddTransactionScreen()));
} else {
setState(() {
_currentIndex = index > addIdx ? index - 1 : index;
});
}
},
selectedItemColor: Theme.of(context).colorScheme.primary,
unselectedItemColor: Colors.grey,
showSelectedLabels: true,
showUnselectedLabels: true,
items: navItems,
);
},
),
);
}
Widget _buildInsightCard(dynamic insight) {
return 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)),
],
),
),
],
),
);
}
Widget _buildRecentTransactionsList(List<Transaction> allTransactions, List<Wallet> safeWallets) {
if (allTransactions.isEmpty) {
return const Center(child: Padding(padding: EdgeInsets.all(16), child: Text('No transactions yet!')));
}
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: allTransactions.length > 6 ? 6 : 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');
final w = safeWallets.where((w) => w.id == t.toWalletId);
if (w.isNotEmpty) {
typeColor = NatureColors.getColor(w.first.nature ?? 'TRANSFER');
}
}
String accountName = 'Unknown Wallet';
final targetWalletId = t.toWalletId ?? t.fromWalletId;
if (targetWalletId != null) {
final w = safeWallets.where((w) => w.id == targetWalletId);
if (w.isNotEmpty) accountName = w.first.name;
}
String subtitleText = DateFormat('MMM dd, yyyy').format(t.date);
if (t.description != null && t.description!.isNotEmpty) {
subtitleText = '$accountName$subtitleText';
}
return Card(
margin: const EdgeInsets.only(bottom: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ListTile(
leading: CircleAvatar(
backgroundColor: typeColor.withValues(alpha: 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: Text(
'${isIncome ? '+' : (isTransfer ? '' : '-')}Rs. ${t.amount.toStringAsFixed(0)}',
style: TextStyle(fontWeight: FontWeight.bold, color: typeColor),
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => AddTransactionScreen(transaction: t)),
);
},
),
);
},
);
}
Widget _buildNotificationBell(BuildContext context) {
return Consumer(
builder: (context, ref, child) {
final invitationsState = ref.watch(invitationProvider);
final pendingInvites = invitationsState.value?.where((inv) => inv.status == 'PENDING').toList() ?? [];
return PopupMenuButton<String>(
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<String>(
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);
},
child: const Text('Decline', style: TextStyle(color: Colors.red)),
),
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();
},
);
},
);
}
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.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: color.withValues(alpha: 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),
),
],
),
),
);
}
}