import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:intl/intl.dart'; import 'package:kzeduca_app/services/transaction_list_notifier.dart'; import 'package:kzeduca_app/models/transaction.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:csv/csv.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:printing/printing.dart'; import 'dart:typed_data'; import 'package:kzeduca_app/services/i18n_service.dart'; // Definimos uma paleta de cores para o design futurista e luxuoso. const Color kPrimaryDark = Color(0xFF0A0A1A); const Color kSecondaryDark = Color(0xFF16162E); const Color kAccentColor = Color(0xFF8B5CF6); const Color kTextPrimary = Color.fromARGB(255, 24, 52, 65); const Color kTextSecondary = Color.fromARGB(255, 18, 49, 37); const Color kIncomeColor = Color(0xFF10B981); const Color kExpenseColor = Color(0xFFEF4444); // A tela principal com um design aprimorado. class ReceiveHistoryScreen extends StatefulWidget { const ReceiveHistoryScreen({super.key}); @override State createState() => _ReceiveHistoryScreenState(); } class _ReceiveHistoryScreenState extends State { String _search = ''; String? _selectedCategory; String? _selectedType; @override Widget build(BuildContext context) { final i18n = Provider.of(context, listen: false); return Scaffold( appBar: _buildAppBar(), body: Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [kPrimaryDark, Color(0xFF16162E)], ), ), child: Column( children: [ _TransactionFilterBar( search: _search, selectedCategory: _selectedCategory, selectedType: _selectedType, onSearch: (v) => setState(() => _search = v), onCategory: (v) => setState(() => _selectedCategory = v), onType: (v) => setState(() => _selectedType = v), ), Expanded( child: Consumer( builder: (context, txnList, _) { final transactions = _filterTransactions(txnList.transactions); if (transactions.isEmpty) { return Center( child: Text( 'Nenhuma transação encontrada.', style: TextStyle(color: kTextSecondary.withOpacity(0.7), fontSize: 18), ), ); } // Agrupar por data final Map> grouped = {}; for (final t in transactions) { final dateKey = DateFormat('dd MMM yyyy', 'pt_BR').format(t.date); grouped.putIfAbsent(dateKey, () => []).add(t); } final sortedKeys = grouped.keys.toList()..sort((a, b) => DateFormat('dd MMM yyyy', 'pt_BR').parse(b).compareTo(DateFormat('dd MMM yyyy', 'pt_BR').parse(a))); return ListView( padding: const EdgeInsets.all(16), children: [ for (final date in sortedKeys) ...[ Padding( padding: const EdgeInsets.symmetric(vertical: 12.0), child: Text( date, style: const TextStyle(color: kTextPrimary, fontSize: 18, fontWeight: FontWeight.bold), ), ), ...grouped[date]!.map((t) => _TransactionTile(transaction: t)).toList(), ] ], ); }, ), ), ], ), ), ); } AppBar _buildAppBar() { final i18n = Provider.of(context, listen: false); return AppBar( title: const Text( 'Histórico', style: TextStyle( color: kTextPrimary, fontWeight: FontWeight.bold, fontSize: 24, ), ), centerTitle: false, backgroundColor: Colors.transparent, elevation: 0, actions: [ IconButton( icon: const Icon(Icons.show_chart_rounded, color: kTextPrimary), tooltip: 'Ver Gráficos', onPressed: () { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (_) => DraggableScrollableSheet( initialChildSize: 0.8, minChildSize: 0.5, maxChildSize: 0.95, builder: (_, controller) => Container( decoration: const BoxDecoration( color: kSecondaryDark, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), child: SingleChildScrollView( controller: controller, child: _TransactionCharts( search: _search, category: _selectedCategory, type: _selectedType, ), ), ), ), ); }, ), IconButton( icon: const Icon(Icons.download_rounded, color: kTextPrimary), tooltip: 'Exportar CSV', onPressed: () async { final txnList = Provider.of(context, listen: false); final filtered = _filterTransactions(txnList.transactions); final csvData = [ ['Título', 'Valor', 'Tipo', 'Categoria', 'Data', 'Nota'], ...filtered.map((t) => [ t.title, t.amount.toStringAsFixed(2), t.type.name, t.category.name, DateFormat('dd/MM/yyyy HH:mm').format(t.date), t.note ?? '', ]) ]; final csv = const ListToCsvConverter().convert(csvData); await Printing.sharePdf(bytes: Uint8List.fromList(csv.codeUnits), filename: 'transacoes.csv'); }, ), IconButton( icon: const Icon(Icons.picture_as_pdf_rounded, color: kTextPrimary), tooltip: 'Exportar PDF', onPressed: () async { final txnList = Provider.of(context, listen: false); final filtered = _filterTransactions(txnList.transactions); final pdf = pw.Document(); pdf.addPage( pw.Page( build: (pw.Context context) { return pw.Table.fromTextArray( headers: ['Título', 'Valor', 'Tipo', 'Categoria', 'Data', 'Nota'], data: filtered.map((t) => [ t.title, t.amount.toStringAsFixed(2), t.type.name, t.category.name, DateFormat('dd/MM/yyyy HH:mm').format(t.date), t.note ?? '', ]).toList(), ); }, ), ); await Printing.layoutPdf(onLayout: (format) async => pdf.save()); }, ), ], ); } List _filterTransactions(List txns) { return txns.where((t) { final matchesSearch = _search.isEmpty || t.title.toLowerCase().contains(_search.toLowerCase()) || (t.note ?? '').toLowerCase().contains(_search.toLowerCase()); final matchesCategory = _selectedCategory == null || t.category.name == _selectedCategory; final matchesType = _selectedType == null || t.type.name == _selectedType; return matchesSearch && matchesCategory && matchesType; }).toList(); } } // Uma barra de filtro mais elegante e moderna. class _TransactionFilterBar extends StatelessWidget { final String search; final String? selectedCategory; final String? selectedType; final ValueChanged onSearch; final ValueChanged onCategory; final ValueChanged onType; const _TransactionFilterBar({ Key? key, required this.search, required this.selectedCategory, required this.selectedType, required this.onSearch, required this.onCategory, required this.onType, }) : super(key: key); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Column( children: [ _StyledTextField( hintText: 'Buscar transações...', onChanged: onSearch, ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _StyledDropdown( value: selectedCategory, hintText: 'Categoria', items: const ['food', 'transport', 'social', 'education', 'medical', 'shopping', 'others', 'salary', 'invest', 'business'], onChanged: onCategory, ), _StyledDropdown( value: selectedType, hintText: 'Tipo', items: const ['income', 'expense'], onChanged: onType, ), ], ), ], ), ); } } // Estilização para o campo de busca. class _StyledTextField extends StatelessWidget { final String hintText; final ValueChanged onChanged; const _StyledTextField({required this.hintText, required this.onChanged}); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: kSecondaryDark.withOpacity(0.5), borderRadius: BorderRadius.circular(16), border: Border.all(color: kAccentColor.withOpacity(0.2)), ), child: TextField( decoration: InputDecoration( hintText: hintText, prefixIcon: const Icon(Icons.search, color: kTextSecondary), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), hintStyle: const TextStyle(color: kTextSecondary), ), style: const TextStyle(color: kTextPrimary), onChanged: onChanged, ), ); } } // Estilização para os dropdowns de filtro. class _StyledDropdown extends StatelessWidget { final String? value; final String hintText; final List items; final ValueChanged onChanged; const _StyledDropdown({ required this.value, required this.hintText, required this.items, required this.onChanged, }); @override Widget build(BuildContext context) { return Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: kSecondaryDark.withOpacity(0.5), borderRadius: BorderRadius.circular(16), border: Border.all(color: kAccentColor.withOpacity(0.2)), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: value, isExpanded: true, dropdownColor: kSecondaryDark, hint: Text(hintText, style: const TextStyle(color: kTextSecondary)), items: [null, ...items] .map((item) => DropdownMenuItem( value: item, child: Text( item == null ? 'Todos' : item, style: const TextStyle(color: kTextPrimary), ), )) .toList(), onChanged: onChanged, ), ), ), ); } } // Design para cada item da transação na lista. class _TransactionTile extends StatelessWidget { final Transaction transaction; const _TransactionTile({required this.transaction}); @override Widget build(BuildContext context) { final isIncome = transaction.type == TransactionType.income; final color = isIncome ? kIncomeColor : kExpenseColor; final sign = isIncome ? '+' : '-'; final formattedAmount = NumberFormat.currency(locale: 'pt_BR', symbol: 'Kz', decimalDigits: 2).format(transaction.amount); final formattedTime = DateFormat('HH:mm').format(transaction.date); return Container( margin: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: kSecondaryDark, borderRadius: BorderRadius.circular(20), border: Border.all(color: color.withOpacity(0.4), width: 1.5), boxShadow: [ BoxShadow( color: color.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 5), ), ], ), child: Row( children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: color.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: Icon( isIncome ? Icons.arrow_downward_rounded : Icons.arrow_upward_rounded, color: color, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( transaction.title, style: const TextStyle(color: kTextPrimary, fontWeight: FontWeight.bold, fontSize: 18), ), const SizedBox(height: 4), Text( transaction.category.name, style: const TextStyle(color: kTextSecondary), ), if (transaction.note != null && transaction.note!.isNotEmpty) ...[ const SizedBox(height: 4), Text( 'Obs: ${transaction.note}', style: const TextStyle(color: kTextSecondary, fontSize: 12), ), ], ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '$sign$formattedAmount', style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 18), ), const SizedBox(height: 4), Text( formattedTime, style: const TextStyle(color: kTextSecondary, fontSize: 12), ), ], ), ], ), ); } } // Estilização para os gráficos no bottom sheet. class _TransactionCharts extends StatelessWidget { final String search; final String? category; final String? type; const _TransactionCharts({this.search = '', this.category, this.type}); @override Widget build(BuildContext context) { final txnList = Provider.of(context, listen: false); final transactions = _filterTransactions(txnList.transactions); // Agrupa por categoria final Map byCategory = {}; for (final t in transactions) { final cat = t.category.name; byCategory[cat] = (byCategory[cat] ?? 0) + t.amount * (t.type == TransactionType.expense ? -1 : 1); } final List sections = byCategory.entries.map((e) { final isNegative = e.value < 0; final color = isNegative ? kExpenseColor : kIncomeColor; return PieChartSectionData( color: color, value: e.value.abs(), title: '${e.key}\n${e.value.toStringAsFixed(2)}', radius: 60, titleStyle: const TextStyle(fontSize: 12, color: kTextPrimary, fontWeight: FontWeight.bold), ); }).toList(); // Evolução do saldo 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.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( height: 5, width: 40, decoration: BoxDecoration( color: kTextSecondary.withOpacity(0.5), borderRadius: BorderRadius.circular(20), ), ), ), const SizedBox(height: 24), const Text( 'Distribuição por Categoria', style: TextStyle(color: kTextPrimary, fontWeight: FontWeight.bold, fontSize: 20), ), const SizedBox(height: 16), SizedBox( height: 250, child: PieChart( PieChartData( sections: sections, centerSpaceRadius: 50, sectionsSpace: 4, ), ), ), const SizedBox(height: 32), const Text( 'Evolução do Saldo', style: TextStyle(color: kTextPrimary, fontWeight: FontWeight.bold, fontSize: 20), ), const SizedBox(height: 16), Container( height: 200, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: kSecondaryDark, borderRadius: BorderRadius.circular(20), border: Border.all(color: kAccentColor.withOpacity(0.2)), ), child: LineChart( LineChartData( lineBarsData: [ LineChartBarData( spots: saldoSpots, isCurved: true, color: kAccentColor, barWidth: 4, isStrokeCapRound: true, dotData: FlDotData(show: false), ), ], titlesData: FlTitlesData(show: false), gridData: FlGridData( show: true, drawVerticalLine: false, getDrawingHorizontalLine: (value) { return FlLine(color: kTextSecondary.withOpacity(0.2), strokeWidth: 1); }, ), borderData: FlBorderData( show: true, border: Border.all(color: kTextSecondary.withOpacity(0.2)), ), minX: 0, maxX: saldoSpots.isNotEmpty ? saldoSpots.length.toDouble() - 1 : 0, minY: saldoSpots.isNotEmpty ? saldoSpots.map((e) => e.y).reduce((a, b) => a < b ? a : b) : 0, maxY: saldoSpots.isNotEmpty ? saldoSpots.map((e) => e.y).reduce((a, b) => a > b ? a : b) : 0, ), ), ), ], ), ); } List _filterTransactions(List txns) { return txns.where((t) { final matchesSearch = search.isEmpty || t.title.toLowerCase().contains(search.toLowerCase()) || (t.note ?? '').toLowerCase().contains(search.toLowerCase()); final matchesCategory = category == null || t.category.name == category; final matchesType = type == null || t.type.name == type; return matchesSearch && matchesCategory && matchesType; }).toList(); } }