720 lines
34 KiB
Dart
720 lines
34 KiB
Dart
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<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;
|
|
|
|
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<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);
|
|
},
|
|
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! : <Wallet>[];
|
|
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<int, double> 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<CarouselItemData> 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<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: () {
|
|
// 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),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|