import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:provider/provider.dart'; import '../models/transaction.dart'; import '../models/budget.dart'; import '../services/hive_service.dart'; import '../services/i18n_service.dart'; class StatisticsScreen extends StatefulWidget { const StatisticsScreen({super.key}); @override State createState() => _StatisticsScreenState(); } class _StatisticsScreenState extends State { List _transactions = []; double _totalExpenses = 0.0; double _totalIncome = 0.0; Map _expensesByCategory = {}; List _budgets = []; final Map _budgetSpent = {}; int _selectedMonth = DateTime.now().month; int _selectedYear = DateTime.now().year; String _currentCurrency = 'Kz'; late HiveService _hiveService; @override void initState() { super.initState(); _hiveService = HiveService(); _loadCurrency(); WidgetsBinding.instance.addPostFrameCallback((_) { _loadDataForPeriod(); }); } Future _loadCurrency() async { final prefs = await SharedPreferences.getInstance(); setState(() { _currentCurrency = prefs.getString('currencySymbol') ?? 'Kz'; }); } Future _loadDataForPeriod() async { final allTransactions = await _hiveService.getTransactions(); final allBudgets = await _hiveService.getBudgets(); // Filtrar transações do mês/ano selecionado final fetchedTransactions = allTransactions.where((t) => t.date.year == _selectedYear && t.date.month == _selectedMonth ).toList(); double expenses = 0.0; double income = 0.0; for (var t in fetchedTransactions) { if (t.type == TransactionType.expense) { expenses += t.amount; } else { income += t.amount; } } // Calcular despesas por categoria final Map expensesByCategoryTemp = {}; for (var t in fetchedTransactions) { if (t.type == TransactionType.expense) { expensesByCategoryTemp.update( t.category, (value) => value + t.amount, ifAbsent: () => t.amount, ); } } // Calcular gastos por orçamento final Map budgetSpentTemp = {}; for (var i = 0; i < allBudgets.length; i++) { final budget = allBudgets[i]; if (_isBudgetInSelectedPeriod(budget)) { final spentAmount = _getSpentAmountForBudget(budget, fetchedTransactions); budgetSpentTemp[i] = spentAmount; } } if (mounted) { setState(() { _transactions = fetchedTransactions; _totalExpenses = expenses; _totalIncome = income; _expensesByCategory = expensesByCategoryTemp; _budgets = allBudgets.where((b) => _isBudgetInSelectedPeriod(b)).toList(); _budgetSpent.clear(); _budgetSpent.addAll(budgetSpentTemp); }); } } bool _isBudgetInSelectedPeriod(Budget budget) { final startOfSelectedMonth = DateTime(_selectedYear, _selectedMonth, 1); final endOfSelectedMonth = DateTime(_selectedYear, _selectedMonth + 1, 0); final isOverlapping = budget.startDate.isBefore(endOfSelectedMonth.add(const Duration(days: 1))) && budget.endDate.isAfter(startOfSelectedMonth.subtract(const Duration(days: 1))); return isOverlapping; } double _getSpentAmountForBudget(Budget budget, List transactions) { final relevantTransactions = transactions.where((t) { final isCategoryMatch = t.category == budget.category; final isDateMatch = t.date.isAfter(budget.startDate.subtract(const Duration(days: 1))) && t.date.isBefore(budget.endDate.add(const Duration(days: 1))); return isCategoryMatch && isDateMatch && t.type == TransactionType.expense; }).toList(); return relevantTransactions.fold(0.0, (sum, item) => sum + item.amount); } String _getMonthName(int month, String languageCode) { return DateFormat.MMMM(languageCode).format(DateTime(_selectedYear, month)); } String _getCategoryName(TransactionCategory category, I18nService i18n) { return { TransactionCategory.food: i18n.t('food'), TransactionCategory.transport: i18n.t('transport'), TransactionCategory.social: i18n.t('social'), TransactionCategory.education: i18n.t('education'), TransactionCategory.medical: i18n.t('medical'), TransactionCategory.shopping: i18n.t('shopping'), TransactionCategory.salary: i18n.t('salary'), TransactionCategory.invest: i18n.t('invest'), TransactionCategory.business: i18n.t('business'), TransactionCategory.others: i18n.t('others'), }[category] ?? i18n.t('unknown'); } IconData _getCategoryIcon(TransactionCategory category) { switch (category) { case TransactionCategory.food: return Icons.fastfood; case TransactionCategory.transport: return Icons.directions_car; case TransactionCategory.social: return Icons.group; case TransactionCategory.education: return Icons.school; case TransactionCategory.medical: return Icons.medical_services; case TransactionCategory.shopping: return Icons.shopping_bag; case TransactionCategory.salary: return Icons.attach_money; case TransactionCategory.invest: return Icons.trending_up; case TransactionCategory.business: return Icons.business_center; case TransactionCategory.others: return Icons.category; case TransactionCategory.house: return Icons.home; case TransactionCategory.utilities: return Icons.lightbulb; case TransactionCategory.subscriptions: return Icons.subscriptions; case TransactionCategory.leisure: return Icons.movie; case TransactionCategory.gym: return Icons.fitness_center; case TransactionCategory.gifts: return Icons.card_giftcard; case TransactionCategory.pets: return Icons.pets; case TransactionCategory.freelance: return Icons.work; case TransactionCategory.dividends: return Icons.monetization_on; case TransactionCategory.loan: return Icons.account_balance; case TransactionCategory.refund: return Icons.reply; case TransactionCategory.tax: return Icons.account_balance_wallet; case TransactionCategory.bonus: return Icons.star; case TransactionCategory.allowance: return Icons.money_off; case TransactionCategory.pension: return Icons.elderly; case TransactionCategory.scholarship: return Icons.school; case TransactionCategory.grant: return Icons.card_giftcard; } } Widget _buildSummaryCard(I18nService i18n) { return Card( color: const Color(0xFF23204A), elevation: 6, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Padding( padding: const EdgeInsets.all(20.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildSummaryItem( title: i18n.t('income'), amount: _totalIncome, color: Colors.greenAccent, icon: Icons.arrow_upward, i18n: i18n, ), _buildSummaryItem( title: i18n.t('expenses'), amount: _totalExpenses, color: Colors.redAccent, icon: Icons.arrow_downward, i18n: i18n, ), ], ), ), ); } Widget _buildSummaryItem({ required String title, required double amount, required Color color, required IconData icon, required I18nService i18n, }) { return Column( children: [ CircleAvatar( backgroundColor: color.withOpacity(0.1), child: Icon(icon, color: color), ), const SizedBox(height: 8), Text( title, style: const TextStyle(color: Colors.white70, fontSize: 14), ), const SizedBox(height: 4), Text( '$_currentCurrency ${amount.toStringAsFixed(2)}', style: TextStyle( color: color, fontWeight: FontWeight.bold, fontSize: 20, ), ), ], ); } Widget _buildPeriodSelector(I18nService i18n) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildDropdown( label: i18n.t('month'), value: _selectedMonth, items: List.generate(12, (i) => i + 1).map((m) => DropdownMenuItem( value: m, child: Text(DateFormat.MMMM(i18n.locale.languageCode).format(DateTime(0, m))), )).toList(), onChanged: (v) { if (v != null) { setState(() { _selectedMonth = v; _loadDataForPeriod(); }); } }, i18n: i18n, ), _buildDropdown( label: i18n.t('year'), value: _selectedYear, items: List.generate(5, (i) => DateTime.now().year - i).map((y) => DropdownMenuItem( value: y, child: Text(y.toString()), )).toList(), onChanged: (v) { if (v != null) { setState(() { _selectedYear = v; _loadDataForPeriod(); }); } }, i18n: i18n, ), ], ), ); } Widget _buildDropdown({ required String label, required int value, required List> items, required ValueChanged onChanged, required I18nService i18n, }) { return Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(color: Colors.white70, fontSize: 12)), const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: const Color(0xFF23204A), borderRadius: BorderRadius.circular(12), ), child: DropdownButtonHideUnderline( child: DropdownButton( isExpanded: true, value: value, dropdownColor: const Color(0xFF23204A), icon: const Icon(Icons.arrow_drop_down, color: Colors.white), style: const TextStyle(color: Colors.white, fontSize: 16), items: items, onChanged: onChanged, ), ), ), ], ), ); } // Refatorando a lista de estatísticas extras Widget _buildExtraStatsList(I18nService i18n) { if (_transactions.isEmpty) return Container(); final biggestExpense = _transactions .where((t) => t.type == TransactionType.expense) .fold(null, (prev, t) => prev == null || t.amount > prev.amount ? t : prev); final mostUsedCategory = _expensesByCategory.entries.isEmpty ? null : _expensesByCategory.entries.reduce((a, b) => a.value > b.value ? a : b).key; final avgExpense = _transactions .where((t) => t.type == TransactionType.expense) .fold(0.0, (sum, t) => sum + t.amount) / (_transactions.where((t) => t.type == TransactionType.expense).length == 0 ? 1 : _transactions.where((t) => t.type == TransactionType.expense).length); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Column( children: [ _buildStatListItem( icon: Icons.trending_down, color: Colors.redAccent, title: i18n.t('biggest_expense'), value: biggestExpense != null ? '${_currentCurrency} ${biggestExpense.amount.toStringAsFixed(2)}' : '--', subtitle: biggestExpense != null ? biggestExpense.title : i18n.t('no_data'), ), _buildStatListItem( icon: Icons.category, color: Colors.blueAccent, title: i18n.t('most_used_category'), value: mostUsedCategory != null ? _getCategoryName(mostUsedCategory, i18n) : '--', subtitle: '', ), _buildStatListItem( icon: Icons.calculate, color: Colors.orangeAccent, title: i18n.t('avg_expense'), value: '${_currentCurrency} ${avgExpense.isNaN ? '0.00' : avgExpense.toStringAsFixed(2)}', subtitle: '', ), _buildStatListItem( icon: Icons.list, color: Colors.purpleAccent, title: i18n.t('total_transactions'), value: _transactions.length.toString(), subtitle: '', ), ], ), ); } Widget _buildStatListItem({ required IconData icon, required Color color, required String title, required String value, required String subtitle, }) { return Card( elevation: 3, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), color: const Color(0xFF23204A), margin: const EdgeInsets.only(bottom: 12), child: Padding( padding: const EdgeInsets.all(16.0), child: Row( children: [ CircleAvatar( backgroundColor: color.withOpacity(0.15), child: Icon(icon, color: color), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white70)), const SizedBox(height: 4), Text(value, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: color)), ], ), ), if (subtitle.isNotEmpty) Text(subtitle, style: const TextStyle(fontSize: 12, color: Colors.white54)), ], ), ), ); } // Gráfico de linha para evolução do saldo Widget _buildBalanceLineChart(I18nService i18n) { if (_transactions.isEmpty) return Container(); final sorted = List.of(_transactions)..sort((a, b) => a.date.compareTo(b.date)); double running = 0; final List saldoSpots = []; for (int i = 0; i < sorted.length; i++) { running += sorted[i].type == TransactionType.income ? sorted[i].amount : -sorted[i].amount; saldoSpots.add(FlSpot(i.toDouble(), running)); } return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Card( elevation: 4, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), color: const Color(0xFF23204A), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(i18n.t('balance_evolution'), style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white)), const SizedBox(height: 12), SizedBox( height: 180, child: LineChart( LineChartData( lineBarsData: [ LineChartBarData( spots: saldoSpots, isCurved: true, color: Colors.blueAccent, barWidth: 3, dotData: FlDotData(show: false), ), ], titlesData: FlTitlesData(show: false), gridData: FlGridData(show: false), borderData: FlBorderData(show: false), ), ), ), ], ), ), ), ); } // Gráfico de pizza bonito e responsivo Widget _buildBeautifulPieChart(I18nService i18n) { if (_expensesByCategory.isEmpty) { return Container(); } final totalExpenses = _expensesByCategory.values.fold(0.0, (sum, amount) => sum + amount); final List colors = [ Colors.blueAccent, Colors.redAccent, Colors.greenAccent, Colors.purpleAccent, Colors.orangeAccent, Colors.teal, Colors.brown, Colors.pinkAccent, Colors.amber, Colors.cyan ]; int colorIndex = 0; // Responsividade final double chartSize = MediaQuery.of(context).size.width * 0.7; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Card( elevation: 6, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), color: const Color(0xFF23204A), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( i18n.t('expenses_by_category'), style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white), ), SizedBox( height: chartSize, width: chartSize, child: PieChart( PieChartData( sections: _expensesByCategory.entries.map((entry) { final percentage = (entry.value / totalExpenses) * 100; final categoryName = _getCategoryName(entry.key, i18n); final color = colors[colorIndex++ % colors.length]; return PieChartSectionData( color: color, value: entry.value, title: '${percentage.toStringAsFixed(1)}%', radius: chartSize / 3, titleStyle: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white, shadows: [Shadow(color: Colors.black45, blurRadius: 2)], ), badgeWidget: _buildCategoryBadge(entry.key, color, i18n), badgePositionPercentageOffset: 1.15, ); }).toList(), sectionsSpace: 3, centerSpaceRadius: chartSize / 6, ), ), ), const SizedBox(height: 16), _buildLegend(colors, i18n), ], ), ), ), ); } Widget _buildCategoryBadge(TransactionCategory category, Color color, I18nService i18n) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: color.withOpacity(0.85), borderRadius: BorderRadius.circular(12), boxShadow: [BoxShadow(color: color.withOpacity(0.3), blurRadius: 4)], ), child: Text( _getCategoryName(category, i18n), style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold, ), ), ); } Widget _buildLegend(List colors, I18nService i18n) { int colorIndex = 0; return Wrap( spacing: 16, runSpacing: 8, children: _expensesByCategory.entries.map((entry) { final color = colors[colorIndex++ % colors.length]; return Row( mainAxisSize: MainAxisSize.min, children: [ Container(width: 14, height: 14, decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(4))), const SizedBox(width: 6), Text(_getCategoryName(entry.key, i18n), style: const TextStyle(color: Colors.white)), const SizedBox(width: 6), Text( '${NumberFormat.currency(locale: i18n.locale.languageCode, symbol: _currentCurrency, decimalDigits: 2).format(entry.value)}', style: const TextStyle(color: Colors.white70, fontWeight: FontWeight.bold), ), ], ); }).toList(), ); } // Orçamentos do mês Widget _buildBudgetsChart(I18nService i18n) { if (_budgets.isEmpty) return Container(); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Card( color: const Color(0xFF23204A), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 4, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(i18n.t('budgets'), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18)), const SizedBox(height: 8), ..._budgets.asMap().entries.map((entry) { final idx = entry.key; final budget = entry.value; final spent = _budgetSpent[idx] ?? 0.0; final percent = budget.amount == 0 ? 0.0 : (spent / budget.amount).clamp(0.0, 1.0); return Padding( padding: const EdgeInsets.symmetric(vertical: 6.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${_getCategoryName(budget.category, i18n)}: $_currentCurrency ${spent.toStringAsFixed(2)} / ${budget.amount.toStringAsFixed(2)}', style: const TextStyle(color: Colors.white70), ), LinearProgressIndicator( value: percent, backgroundColor: Colors.white12, color: percent < 0.8 ? Colors.greenAccent : Colors.redAccent, minHeight: 8, ), ], ), ); }), ], ), ), ), ); } // Lista de transações do mês Widget _buildTransactionsList(I18nService i18n) { if (_transactions.isEmpty) return Container(); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Card( color: const Color(0xFF23204A), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 4, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(i18n.t('transactions'), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18)), const SizedBox(height: 8), ..._transactions.take(10).map((t) => ListTile( leading: Icon(_getCategoryIcon(t.category), color: Colors.white70), title: Text(t.title, style: const TextStyle(color: Colors.white)), subtitle: Text(DateFormat('dd/MM/yyyy').format(t.date), style: const TextStyle(color: Colors.white54)), trailing: Text( '${t.type == TransactionType.income ? '+' : '-'}$_currentCurrency ${t.amount.toStringAsFixed(2)}', style: TextStyle( color: t.type == TransactionType.income ? Colors.greenAccent : Colors.redAccent, fontWeight: FontWeight.bold, ), ), )), ], ), ), ), ); } @override Widget build(BuildContext context) { final i18n = Provider.of(context); bool hasData = _transactions.isNotEmpty || _budgets.isNotEmpty; return Scaffold( appBar: AppBar( title: Text(i18n.t('statistics_title')), centerTitle: true, backgroundColor: const Color(0xFF1E1C3A), elevation: 0, ), backgroundColor: const Color(0xFF1E1C3A), body: !hasData ? Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset('assets/images/no_budget.png', width: 100, height: 100), const SizedBox(height: 10), Text(i18n.t('no_data'), style: const TextStyle(fontSize: 18, color: Colors.grey, fontWeight: FontWeight.bold)), const SizedBox(height: 5), Text(i18n.t('no_data_subtitle'), style: const TextStyle(color: Colors.grey)), ], ), ), ) : SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 16), _buildPeriodSelector(i18n), _buildSummaryCard(i18n), _buildExtraStatsList(i18n), _buildBeautifulPieChart(i18n), _buildBalanceLineChart(i18n), const SizedBox(height: 16), if (_budgets.isNotEmpty) _buildBudgetsChart(i18n), const SizedBox(height: 16), _buildTransactionsList(i18n), const SizedBox(height: 16), ], ), ), ); } }