import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:barber_app/core/theme/app_theme.dart'; import 'package:barber_app/core/constants/app_strings.dart'; import 'package:barber_app/features/finances/data/models/transaction_model.dart'; import 'package:barber_app/features/finances/data/repositories/transaction_repository.dart'; import 'package:barber_app/features/finances/presentation/pages/add_transaction_page.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:barber_app/core/database/database_service.dart'; class FinancesPage extends StatefulWidget { const FinancesPage({super.key}); @override State createState() => _FinancesPageState(); } class _FinancesPageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; final _transactionRepo = TransactionRepository(); final _currencyFormat = NumberFormat.currency(locale: 'pt_BR', symbol: 'R\$'); DateTime _selectedDate = DateTime.now(); String _searchQuery = ''; @override void initState() { super.initState(); _tabController = TabController(length: 4, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } void _refreshData() => setState(() {}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text(AppStrings.finances), bottom: PreferredSize( preferredSize: const Size.fromHeight(110), child: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: TextField( onChanged: (value) => setState(() => _searchQuery = value), decoration: InputDecoration( hintText: 'Buscar transação...', prefixIcon: Icon( Icons.search, color: AppColors.textSecondary, ), filled: true, fillColor: AppColors.surface, contentPadding: const EdgeInsets.symmetric(vertical: 0), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), ), ), ), TabBar( controller: _tabController, labelColor: AppColors.primaryColor, unselectedLabelColor: AppColors.textSecondary, indicatorColor: AppColors.primaryColor, tabs: const [ Tab(text: 'Resumo'), Tab(text: 'A Receber'), Tab(text: 'A Pagar'), Tab(text: 'Extrato'), ], ), ], ), ), actions: [ IconButton( icon: const Icon(Icons.psychology_alt_rounded), tooltip: 'Gerar Dados Teste', onPressed: _generateMockData, ), IconButton( icon: const Icon(Icons.calendar_month), onPressed: _showMonthPicker, ), ], ), body: ValueListenableBuilder( valueListenable: DatabaseService.transactionsBoxInstance.listenable(), builder: (context, _, _) { final balance = _transactionRepo.getBalance(); final pendingReceivables = _transactionRepo.getPendingReceivables(); final pendingPayables = _transactionRepo.getPendingPayables(); return TabBarView( controller: _tabController, children: [ _buildSummaryTab(balance, pendingReceivables, pendingPayables), _buildTransactionList(TransactionType.revenue), _buildTransactionList(TransactionType.expense), _buildAllTransactionsList(), ], ); }, ), floatingActionButton: Padding( padding: const EdgeInsets.only(bottom: 80), child: FloatingActionButton.extended( heroTag: 'finances_fab', onPressed: () async { await Navigator.push( context, MaterialPageRoute(builder: (_) => const AddTransactionPage()), ); _refreshData(); }, icon: const Icon(Icons.add), label: const Text('Nova Transação'), ), ), ); } Future _generateMockData() async { final now = DateTime.now(); final random = DateTime.now().millisecondsSinceEpoch; for (int i = 0; i < 20; i++) { final day = (i % 28) + 1; await _transactionRepo.addTransaction( TransactionModel( id: 'mock_rev_${random}_$i', userId: DatabaseService.getCurrentUserId() ?? 'user_1', type: TransactionType.revenue, amount: (100 + (i * 15)).toDouble(), description: 'Corte Cliente ${i + 1}', dueDate: DateTime(now.year, now.month, day), isPaid: i % 3 != 0, createdAt: DateTime.now(), paidDate: i % 3 != 0 ? DateTime(now.year, now.month, day) : null, ), ); } for (int i = 0; i < 8; i++) { final day = (i * 3) + 2; if (day > 28) { continue; } await _transactionRepo.addTransaction( TransactionModel( id: 'mock_exp_${random}_$i', userId: DatabaseService.getCurrentUserId() ?? 'user_1', type: TransactionType.expense, amount: (40 + (i * 20)).toDouble(), description: 'Despesa Insumos $i', dueDate: DateTime(now.year, now.month, day), isPaid: true, createdAt: DateTime.now(), paidDate: DateTime(now.year, now.month, day), ), ); } _refreshData(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Dashboard populada com dados de teste! 🚀'), ), ); } } Future _showMonthPicker() async { final DateTime? picked = await showDatePicker( context: context, initialDate: _selectedDate, firstDate: DateTime(2020), lastDate: DateTime(2030), initialDatePickerMode: DatePickerMode.year, ); if (picked != null && (picked.month != _selectedDate.month || picked.year != _selectedDate.year)) { setState(() { _selectedDate = picked; }); } } Widget _buildSummaryTab(double balance, double receivables, double payables) { final monthlyTransactions = _transactionRepo.getAllTransactions().where(( t, ) { return t.dueDate.month == _selectedDate.month && t.dueDate.year == _selectedDate.year; }).toList(); double monthlyReceived = 0; double monthlyPaid = 0; double monthlyProjectedRevenue = 0; double monthlyProjectedExpense = 0; for (var t in monthlyTransactions) { if (t.type == TransactionType.revenue) { monthlyProjectedRevenue += t.amount; if (t.isPaid) { monthlyReceived += t.amount; } } else { monthlyProjectedExpense += t.amount; if (t.isPaid) { monthlyPaid += t.amount; } } } return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildMonthHeader(), const SizedBox(height: 24), Row( children: [ Expanded( child: _buildSummaryCard( 'Faturamento', monthlyReceived, monthlyProjectedRevenue, AppColors.success, Icons.trending_up_rounded, ), ), const SizedBox(width: 16), Expanded( child: _buildSummaryCard( 'Despesas', monthlyPaid, monthlyProjectedExpense, AppColors.error, Icons.trending_down_rounded, ), ), ], ), const SizedBox(height: 24), const Text( 'DESEMPENHO DIÁRIO', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w900, color: AppColors.textSecondary, letterSpacing: 1.5, ), ), const SizedBox(height: 16), _buildYieldChart(monthlyTransactions), const SizedBox(height: 24), Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(24), border: Border.all(color: Colors.white.withAlpha(10)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Resumo de Caixa', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), ), const SizedBox(height: 24), _buildBarChartRow( 'Entradas Realizadas', monthlyReceived, monthlyProjectedRevenue, AppColors.success, ), const SizedBox(height: 20), _buildBarChartRow( 'Saídas Efetuadas', monthlyPaid, monthlyProjectedExpense, AppColors.error, ), ], ), ), const SizedBox(height: 120), ], ), ); } Widget _buildMonthHeader() { return Center( child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration( color: AppColors.primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(30), border: Border.all( color: AppColors.primaryColor.withValues(alpha: 0.2), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.analytics_outlined, size: 18, color: AppColors.primaryColor, ), const SizedBox(width: 10), Text( DateFormat( 'MMMM yyyy', 'pt_BR', ).format(_selectedDate).toUpperCase(), style: TextStyle( color: AppColors.primaryColor, fontWeight: FontWeight.w900, fontSize: 13, letterSpacing: 1, ), ), ], ), ), ); } Widget _buildYieldChart(List transactions) { final Map dailyRevenue = {}; for (var t in transactions) { if (t.type == TransactionType.revenue && t.isPaid) { final day = t.dueDate.day; dailyRevenue[day] = (dailyRevenue[day] ?? 0) + t.amount; } } final List spots = []; for (int i = 1; i <= 31; i++) { spots.add(FlSpot(i.toDouble(), dailyRevenue[i] ?? 0)); } return Container( height: 220, padding: const EdgeInsets.fromLTRB(10, 20, 20, 10), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(28), border: Border.all(color: Colors.white.withAlpha(10)), ), child: LineChart( LineChartData( gridData: FlGridData( show: true, drawVerticalLine: false, getDrawingHorizontalLine: (value) => FlLine(color: Colors.white.withAlpha(5), strokeWidth: 1), ), titlesData: FlTitlesData( show: true, rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), leftTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 30, interval: 5, getTitlesWidget: (value, meta) { return Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( '${value.toInt()}', style: TextStyle( color: AppColors.textSecondary.withValues(alpha: 0.5), fontSize: 10, fontWeight: FontWeight.bold, ), ), ); }, ), ), ), borderData: FlBorderData(show: false), lineBarsData: [ LineChartBarData( spots: spots, isCurved: true, gradient: LinearGradient( colors: [ AppColors.primaryColor, AppColors.primaryColor.withValues(alpha: 0.5), ], ), barWidth: 4, isStrokeCapRound: true, dotData: const FlDotData(show: false), belowBarData: BarAreaData( show: true, gradient: LinearGradient( colors: [ AppColors.primaryColor.withValues(alpha: 0.2), AppColors.primaryColor.withValues(alpha: 0.0), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), ), ], ), ), ); } Widget _buildSummaryCard( String title, double realized, double projected, Color color, IconData icon, ) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(24), border: Border.all(color: color.withValues(alpha: 0.1)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: color, size: 24), const SizedBox(height: 16), Text( title, style: TextStyle( color: AppColors.textSecondary, fontSize: 12, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 6), FittedBox( fit: BoxFit.scaleDown, child: Text( _currencyFormat.format(realized), style: TextStyle( color: AppColors.textPrimary, fontSize: 20, fontWeight: FontWeight.w900, ), ), ), if (projected > realized) Padding( padding: const EdgeInsets.only(top: 4), child: Text( 'Meta: ${_currencyFormat.format(projected)}', style: TextStyle( color: color.withValues(alpha: 0.5), fontSize: 10, fontWeight: FontWeight.bold, ), ), ), ], ), ); } Widget _buildBarChartRow( String label, double value, double total, Color color, ) { if (total == 0) { total = 1; } final percentage = (value / total).clamp(0.0, 1.0); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: const TextStyle( color: AppColors.textSecondary, fontSize: 13, fontWeight: FontWeight.w600, ), ), Text( _currencyFormat.format(value), style: TextStyle(color: color, fontWeight: FontWeight.w900), ), ], ), const SizedBox(height: 10), Stack( children: [ Container( height: 12, width: double.infinity, decoration: BoxDecoration( color: AppColors.surfaceLight, borderRadius: BorderRadius.circular(6), ), ), FractionallySizedBox( widthFactor: percentage, child: Container( height: 12, decoration: BoxDecoration( gradient: LinearGradient( colors: [color, color.withValues(alpha: 0.6)], ), borderRadius: BorderRadius.circular(6), boxShadow: [ BoxShadow( color: color.withValues(alpha: 0.3), blurRadius: 8, ), ], ), ), ), ], ), ], ); } Widget _buildGroupedList(List transactions) { if (transactions.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.receipt_long_outlined, size: 64, color: AppColors.textSecondary.withValues(alpha: 0.2), ), const SizedBox(height: 16), const Text( 'Nenhuma transação encontrada', style: TextStyle(color: AppColors.textSecondary), ), ], ), ); } transactions.sort((a, b) => b.dueDate.compareTo(a.dueDate)); var filtered = transactions; if (_searchQuery.isNotEmpty) { filtered = filtered .where( (t) => t.description.toLowerCase().contains( _searchQuery.toLowerCase(), ), ) .toList(); } if (filtered.isEmpty) { return const Center( child: Text( 'Nenhum resultado para a busca', style: TextStyle(color: AppColors.textSecondary), ), ); } final Map> grouped = {}; for (var t in filtered) { final key = DateFormat('yyyy-MM-dd').format(t.dueDate); grouped.putIfAbsent(key, () => []).add(t); } return ListView.builder( padding: const EdgeInsets.fromLTRB(16, 16, 16, 100), itemCount: grouped.keys.length, itemBuilder: (context, index) { final dateKey = grouped.keys.elementAt(index); final dayTransactions = grouped[dateKey]!; final date = DateTime.parse(dateKey); String header; final now = DateTime.now(); if (date.year == now.year && date.month == now.month && date.day == now.day) { header = 'HOJE'; } else if (date.year == now.year && date.month == now.month && date.day == now.day - 1) { header = 'ONTEM'; } else { header = DateFormat('dd MMM', 'pt_BR').format(date).toUpperCase(); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(left: 4, top: 20, bottom: 12), child: Text( header, style: const TextStyle( color: AppColors.textSecondary, fontWeight: FontWeight.w900, fontSize: 12, letterSpacing: 1, ), ), ), ...dayTransactions.map((t) => _buildTransactionCard(t)), ], ); }, ); } Widget _buildTransactionList(TransactionType type) { final filtered = _transactionRepo.getAllTransactions().where((t) { return t.type == type && t.dueDate.month == _selectedDate.month && t.dueDate.year == _selectedDate.year; }).toList(); return _buildGroupedList(filtered); } Widget _buildAllTransactionsList() { final filtered = _transactionRepo.getAllTransactions().where((t) { return t.dueDate.month == _selectedDate.month && t.dueDate.year == _selectedDate.year; }).toList(); return _buildGroupedList(filtered); } Widget _buildTransactionCard(TransactionModel transaction) { final isRevenue = transaction.type == TransactionType.revenue; return GestureDetector( onTap: transaction.isPaid ? null : () => _markAsPaid(transaction), child: Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(20), border: Border.all( color: transaction.isOverdue ? AppColors.error.withValues(alpha: 0.3) : Colors.white.withAlpha(5), ), ), child: Row( children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: (isRevenue ? AppColors.success : AppColors.error) .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(14), ), child: Icon( isRevenue ? Icons.add_rounded : Icons.remove_rounded, color: isRevenue ? AppColors.success : AppColors.error, size: 20, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( transaction.description, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, color: AppColors.textPrimary, ), ), const SizedBox(height: 4), Text( DateFormat('HH:mm').format(transaction.createdAt) == '00:00' ? DateFormat('dd/MM').format(transaction.dueDate) : DateFormat( 'HH:mm - dd/MM', ).format(transaction.dueDate), style: TextStyle( color: AppColors.textSecondary, fontSize: 11, ), ), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( _currencyFormat.format(transaction.amount), style: TextStyle( fontWeight: FontWeight.w900, fontSize: 15, color: isRevenue ? AppColors.success : AppColors.error, ), ), const SizedBox(height: 6), if (!transaction.isPaid) _buildBadge(AppStrings.pending, AppColors.primaryColor) else _buildBadge('Pago', AppColors.success), ], ), ], ), ), ); } Widget _buildBadge(String text, Color color) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), ), child: Text( text, style: TextStyle( color: color, fontSize: 10, fontWeight: FontWeight.bold, ), ), ); } void _markAsPaid(TransactionModel transaction) { showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: AppColors.surface, title: const Text('Confirmar Pagamento'), content: Text( 'Confirmar o recebimento/pagamento de ${_currencyFormat.format(transaction.amount)}?', ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancelar'), ), TextButton( onPressed: () async { final navigator = Navigator.of(context); await _transactionRepo.markAsPaid(transaction.id); navigator.pop(); _refreshData(); }, child: const Text( 'Confirmar', style: TextStyle(color: AppColors.success), ), ), ], ), ); } }