347 lines
12 KiB
Dart
347 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:lucide_icons/lucide_icons.dart';
|
|
import '../../../transactions/data/models.dart';
|
|
import '../../../../core/theme/nature_colors.dart';
|
|
import 'day_wise_spending_chart.dart';
|
|
import 'category_spending_chart.dart';
|
|
|
|
class StatisticsTab extends StatefulWidget {
|
|
final List<Transaction> transactions;
|
|
final List<Wallet> wallets;
|
|
final List<Category> categories;
|
|
final String selectedFilter;
|
|
final DateTime startDate;
|
|
final DateTime endDate;
|
|
final VoidCallback onPickCustomDateRange;
|
|
final Function(String) onFilterChanged;
|
|
final Future<void> Function() onRefresh;
|
|
final List<String> filters;
|
|
|
|
const StatisticsTab({
|
|
super.key,
|
|
required this.transactions,
|
|
required this.wallets,
|
|
required this.categories,
|
|
required this.selectedFilter,
|
|
required this.startDate,
|
|
required this.endDate,
|
|
required this.onPickCustomDateRange,
|
|
required this.onFilterChanged,
|
|
required this.onRefresh,
|
|
required this.filters,
|
|
});
|
|
|
|
@override
|
|
State<StatisticsTab> createState() => _StatisticsTabState();
|
|
}
|
|
|
|
class _StatisticsTabState extends State<StatisticsTab> {
|
|
String _selectedNature = 'EXPENSE';
|
|
|
|
Widget _buildTotalCard(
|
|
String title,
|
|
double amount,
|
|
Color color,
|
|
IconData icon,
|
|
) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
return Card(
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
side: BorderSide(
|
|
color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.2),
|
|
),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
CircleAvatar(
|
|
backgroundColor: color.withOpacity(0.1),
|
|
radius: 16,
|
|
child: Icon(icon, color: color, size: 16),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
'Rs. ${amount.toStringAsFixed(0)}',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: color,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Compute totals
|
|
final totalIncome = widget.transactions
|
|
.where(
|
|
(t) =>
|
|
t.type == 'INCOME' ||
|
|
(t.type == 'TRANSFER' && t.fromWalletId == null),
|
|
)
|
|
.fold(0.0, (s, t) => s + t.amount);
|
|
final totalExpense = widget.transactions
|
|
.where(
|
|
(t) =>
|
|
t.type == 'EXPENSE' ||
|
|
(t.type == 'TRANSFER' && t.toWalletId == null),
|
|
)
|
|
.fold(0.0, (s, t) => s + t.amount);
|
|
final totalInvestment = widget.transactions
|
|
.where((t) => t.type == 'INVESTMENT')
|
|
.fold(0.0, (s, t) => s + t.amount);
|
|
|
|
double totalPayablesPeriod = 0.0;
|
|
double totalReceivablesPeriod = 0.0;
|
|
|
|
for (var t in widget.transactions) {
|
|
if (t.toWalletId != null) {
|
|
final w = widget.wallets.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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Color barColor = NatureColors.getColor(_selectedNature);
|
|
|
|
return RefreshIndicator(
|
|
onRefresh: widget.onRefresh,
|
|
child: SingleChildScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Row(
|
|
children: widget.filters.map((f) {
|
|
final isSelected = widget.selectedFilter == f;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(right: 8),
|
|
child: ChoiceChip(
|
|
label: Text(f),
|
|
selected: isSelected,
|
|
onSelected: (val) {
|
|
if (val) {
|
|
if (f == 'Custom') {
|
|
widget.onPickCustomDateRange();
|
|
} else {
|
|
widget.onFilterChanged(f);
|
|
}
|
|
}
|
|
},
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
if (widget.selectedFilter == 'Custom')
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 8,
|
|
),
|
|
child: Text(
|
|
'${widget.startDate.day} ${widget.startDate.month} - ${widget.endDate.day} ${widget.endDate.month}',
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
),
|
|
|
|
Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
'Analytics',
|
|
style: TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 0,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: barColor.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: DropdownButton<String>(
|
|
value: _selectedNature,
|
|
underline: const SizedBox(),
|
|
icon: Icon(Icons.arrow_drop_down, color: barColor),
|
|
style: TextStyle(
|
|
color: barColor,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
items: const [
|
|
DropdownMenuItem(
|
|
value: 'EXPENSE',
|
|
child: Text('Expense'),
|
|
),
|
|
DropdownMenuItem(
|
|
value: 'INCOME',
|
|
child: Text('Income'),
|
|
),
|
|
DropdownMenuItem(
|
|
value: 'PAYABLES',
|
|
child: Text('Payables'),
|
|
),
|
|
DropdownMenuItem(
|
|
value: 'RECEIVABLES',
|
|
child: Text('Receivables'),
|
|
),
|
|
DropdownMenuItem(
|
|
value: 'INVESTMENTS',
|
|
child: Text('Investments'),
|
|
),
|
|
],
|
|
onChanged: (val) {
|
|
if (val != null) {
|
|
setState(() => _selectedNature = val);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
if (widget.transactions.isEmpty)
|
|
Card(
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(24),
|
|
side: BorderSide(
|
|
color: Theme.of(context).dividerColor.withOpacity(
|
|
Theme.of(context).brightness == Brightness.dark
|
|
? 0.1
|
|
: 0.2,
|
|
),
|
|
),
|
|
),
|
|
child: const Padding(
|
|
padding: EdgeInsets.all(32.0),
|
|
child: Center(
|
|
child: Text(
|
|
'No transactions in this period.',
|
|
style: TextStyle(color: Colors.grey),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
else ...[
|
|
DayWiseSpendingChart(
|
|
transactions: widget.transactions,
|
|
wallets: widget.wallets,
|
|
selectedNature: _selectedNature,
|
|
startDate: widget.startDate,
|
|
endDate: widget.endDate,
|
|
),
|
|
const SizedBox(height: 16),
|
|
CategorySpendingChart(
|
|
transactions: widget.transactions,
|
|
categories: widget.categories,
|
|
selectedNature: _selectedNature,
|
|
),
|
|
],
|
|
|
|
const SizedBox(height: 32),
|
|
const Text(
|
|
'Summary Totals',
|
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
GridView.count(
|
|
crossAxisCount: 2,
|
|
crossAxisSpacing: 12,
|
|
mainAxisSpacing: 12,
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
childAspectRatio: 1.5,
|
|
children: [
|
|
_buildTotalCard(
|
|
'Income',
|
|
totalIncome,
|
|
NatureColors.getColor('INCOME'),
|
|
LucideIcons.arrowDown,
|
|
),
|
|
_buildTotalCard(
|
|
'Expense',
|
|
totalExpense,
|
|
NatureColors.getColor('EXPENSE'),
|
|
LucideIcons.arrowUp,
|
|
),
|
|
_buildTotalCard(
|
|
'Investment',
|
|
totalInvestment,
|
|
NatureColors.getColor('INVESTMENTS'),
|
|
LucideIcons.trendingUp,
|
|
),
|
|
_buildTotalCard(
|
|
'Payables',
|
|
totalPayablesPeriod,
|
|
NatureColors.getColor('PAYABLES'),
|
|
LucideIcons.alertCircle,
|
|
),
|
|
_buildTotalCard(
|
|
'Receivables',
|
|
totalReceivablesPeriod,
|
|
NatureColors.getColor('RECEIVABLES'),
|
|
LucideIcons.arrowDownLeft,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 32),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|