Initial commit - app-padrao-1.0

This commit is contained in:
Erik Silva
2025-12-19 23:29:24 -03:00
commit ec76d3d633
205 changed files with 13131 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
import 'package:hive/hive.dart';
part 'transaction_model.g.dart';
@HiveType(typeId: 3)
enum TransactionType {
@HiveField(0)
revenue, // Receita
@HiveField(1)
expense, // Despesa
}
@HiveType(typeId: 4)
class TransactionModel extends HiveObject {
@HiveField(0)
final String id;
@HiveField(1)
final String userId;
@HiveField(2)
final TransactionType type;
@HiveField(3)
final double amount;
@HiveField(4)
final String description;
@HiveField(5)
final DateTime dueDate;
@HiveField(6)
final DateTime? paidDate;
@HiveField(7)
final bool isPaid;
@HiveField(8)
final String? haircutId; // Vinculado a um corte (receita automática)
@HiveField(9)
final DateTime createdAt;
TransactionModel({
required this.id,
required this.userId,
required this.type,
required this.amount,
required this.description,
required this.dueDate,
this.paidDate,
required this.isPaid,
this.haircutId,
required this.createdAt,
});
bool get isFromHaircut => haircutId != null;
bool get isOverdue => !isPaid && dueDate.isBefore(DateTime.now());
TransactionModel copyWith({
String? id,
String? userId,
TransactionType? type,
double? amount,
String? description,
DateTime? dueDate,
DateTime? paidDate,
bool? isPaid,
String? haircutId,
DateTime? createdAt,
}) {
return TransactionModel(
id: id ?? this.id,
userId: userId ?? this.userId,
type: type ?? this.type,
amount: amount ?? this.amount,
description: description ?? this.description,
dueDate: dueDate ?? this.dueDate,
paidDate: paidDate ?? this.paidDate,
isPaid: isPaid ?? this.isPaid,
haircutId: haircutId ?? this.haircutId,
createdAt: createdAt ?? this.createdAt,
);
}
}

View File

@@ -0,0 +1,107 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'transaction_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class TransactionModelAdapter extends TypeAdapter<TransactionModel> {
@override
final int typeId = 4;
@override
TransactionModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return TransactionModel(
id: fields[0] as String,
userId: fields[1] as String,
type: fields[2] as TransactionType,
amount: fields[3] as double,
description: fields[4] as String,
dueDate: fields[5] as DateTime,
paidDate: fields[6] as DateTime?,
isPaid: fields[7] as bool,
haircutId: fields[8] as String?,
createdAt: fields[9] as DateTime,
);
}
@override
void write(BinaryWriter writer, TransactionModel obj) {
writer
..writeByte(10)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.userId)
..writeByte(2)
..write(obj.type)
..writeByte(3)
..write(obj.amount)
..writeByte(4)
..write(obj.description)
..writeByte(5)
..write(obj.dueDate)
..writeByte(6)
..write(obj.paidDate)
..writeByte(7)
..write(obj.isPaid)
..writeByte(8)
..write(obj.haircutId)
..writeByte(9)
..write(obj.createdAt);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is TransactionModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class TransactionTypeAdapter extends TypeAdapter<TransactionType> {
@override
final int typeId = 3;
@override
TransactionType read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return TransactionType.revenue;
case 1:
return TransactionType.expense;
default:
return TransactionType.revenue;
}
}
@override
void write(BinaryWriter writer, TransactionType obj) {
switch (obj) {
case TransactionType.revenue:
writer.writeByte(0);
break;
case TransactionType.expense:
writer.writeByte(1);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is TransactionTypeAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,217 @@
import 'package:uuid/uuid.dart';
import 'package:barber_app/core/database/database_service.dart';
import 'package:barber_app/features/finances/data/models/transaction_model.dart';
class TransactionRepository {
final _uuid = const Uuid();
String? get _currentUserId => DatabaseService.getCurrentUserId();
// Criar transação de receita a partir de um corte
Future<TransactionModel?> createRevenueFromHaircut({
required String haircutId,
required double amount,
required String clientName,
required String serviceType,
bool isPaid = true,
}) async {
if (_currentUserId == null) return null;
final transaction = TransactionModel(
id: _uuid.v4(),
userId: _currentUserId!,
type: TransactionType.revenue,
amount: amount,
description: '$serviceType - $clientName',
dueDate: DateTime.now(),
isPaid: isPaid,
paidDate: isPaid ? DateTime.now() : null,
haircutId: haircutId,
createdAt: DateTime.now(),
);
await DatabaseService.transactionsBoxInstance.put(
transaction.id,
transaction,
);
return transaction;
}
// Criar despesa
Future<TransactionModel?> createExpense({
required double amount,
required String description,
required DateTime dueDate,
bool isPaid = false,
}) async {
if (_currentUserId == null) return null;
final transaction = TransactionModel(
id: _uuid.v4(),
userId: _currentUserId!,
type: TransactionType.expense,
amount: amount,
description: description.trim(),
dueDate: dueDate,
isPaid: isPaid,
paidDate: isPaid ? DateTime.now() : null,
createdAt: DateTime.now(),
);
await DatabaseService.transactionsBoxInstance.put(
transaction.id,
transaction,
);
return transaction;
}
// Criar receita manual
Future<TransactionModel?> createRevenue({
required double amount,
required String description,
required DateTime dueDate,
bool isPaid = false,
}) async {
if (_currentUserId == null) return null;
final transaction = TransactionModel(
id: _uuid.v4(),
userId: _currentUserId!,
type: TransactionType.revenue,
amount: amount,
description: description.trim(),
dueDate: dueDate,
isPaid: isPaid,
paidDate: isPaid ? DateTime.now() : null,
createdAt: DateTime.now(),
);
await DatabaseService.transactionsBoxInstance.put(
transaction.id,
transaction,
);
return transaction;
}
// Listar todas as transações
List<TransactionModel> getAllTransactions() {
if (_currentUserId == null) return [];
return DatabaseService.transactionsBoxInstance.values
.where((t) => t.userId == _currentUserId)
.toList()
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
}
// Receitas
List<TransactionModel> getRevenues() {
return getAllTransactions()
.where((t) => t.type == TransactionType.revenue)
.toList();
}
// Despesas
List<TransactionModel> getExpenses() {
return getAllTransactions()
.where((t) => t.type == TransactionType.expense)
.toList();
}
// Pendentes a receber
double getPendingReceivables() {
return getRevenues()
.where((t) => !t.isPaid)
.fold(0.0, (sum, t) => sum + t.amount);
}
// Pendentes a pagar
double getPendingPayables() {
return getExpenses()
.where((t) => !t.isPaid)
.fold(0.0, (sum, t) => sum + t.amount);
}
// Total recebido (pago)
double getTotalReceived() {
return getRevenues()
.where((t) => t.isPaid)
.fold(0.0, (sum, t) => sum + t.amount);
}
// Total pago
double getTotalPaid() {
return getExpenses()
.where((t) => t.isPaid)
.fold(0.0, (sum, t) => sum + t.amount);
}
// Dashboard Helpers
List<double> getLast7DaysRevenue() {
final now = DateTime.now();
final List<double> dailyRevenue = List.filled(7, 0.0);
// Obtém todas as transações
final allTransactions =
getAllTransactions(); // Use o helper que filtra pelo usuário
if (allTransactions.isEmpty) return dailyRevenue;
for (int i = 0; i < 7; i++) {
final targetDate = now.subtract(Duration(days: 6 - i));
final dayRevenue = allTransactions
.where(
(t) =>
t.type == TransactionType.revenue &&
t.dueDate.year == targetDate.year &&
t.dueDate.month == targetDate.month &&
t.dueDate.day == targetDate.day,
)
.fold(0.0, (sum, t) => sum + t.amount);
dailyRevenue[i] = dayRevenue;
}
return dailyRevenue;
}
// Saldo
double getBalance() {
return getTotalReceived() - getTotalPaid();
}
// Marcar como pago
Future<TransactionModel?> markAsPaid(String id) async {
final transaction = DatabaseService.transactionsBoxInstance.get(id);
if (transaction == null) return null;
final updated = transaction.copyWith(
isPaid: true,
paidDate: DateTime.now(),
);
await DatabaseService.transactionsBoxInstance.put(id, updated);
return updated;
}
// Deletar transação
Future<void> deleteTransaction(String id) async {
await DatabaseService.transactionsBoxInstance.delete(id);
}
// Transações do mês
List<TransactionModel> getMonthTransactions() {
final now = DateTime.now();
final monthStart = DateTime(now.year, now.month, 1);
return getAllTransactions()
.where((t) => t.createdAt.isAfter(monthStart))
.toList();
}
Future<void> addTransaction(TransactionModel transaction) async {
await DatabaseService.transactionsBoxInstance.put(
transaction.id,
transaction,
);
}
}

View File

@@ -0,0 +1,271 @@
import 'package:flutter/material.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/shared/widgets/custom_text_field.dart';
import 'package:barber_app/shared/widgets/loading_button.dart';
class AddTransactionPage extends StatefulWidget {
const AddTransactionPage({super.key});
@override
State<AddTransactionPage> createState() => _AddTransactionPageState();
}
class _AddTransactionPageState extends State<AddTransactionPage> {
final _formKey = GlobalKey<FormState>();
final _descriptionController = TextEditingController();
final _amountController = TextEditingController();
TransactionType _type = TransactionType.expense;
DateTime _dueDate = DateTime.now();
bool _isPaid = false;
bool _isLoading = false;
@override
void dispose() {
_descriptionController.dispose();
_amountController.dispose();
super.dispose();
}
Future<void> _selectDate() async {
final date = await showDatePicker(
context: context,
initialDate: _dueDate,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.dark(
primary: AppColors.primaryColor,
surface: AppColors.surface,
),
),
child: child!,
);
},
);
if (date != null) {
setState(() => _dueDate = date);
}
}
Future<void> _saveTransaction() async {
if (!(_formKey.currentState?.validate() ?? false)) return;
setState(() => _isLoading = true);
try {
final repo = TransactionRepository();
if (_type == TransactionType.expense) {
await repo.createExpense(
amount: double.parse(_amountController.text.replaceAll(',', '.')),
description: _descriptionController.text,
dueDate: _dueDate,
isPaid: _isPaid,
);
} else {
await repo.createRevenue(
amount: double.parse(_amountController.text.replaceAll(',', '.')),
description: _descriptionController.text,
dueDate: _dueDate,
isPaid: _isPaid,
);
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(AppStrings.savedSuccessfully),
backgroundColor: AppColors.success,
),
);
Navigator.pop(context);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erro: $e'), backgroundColor: AppColors.error),
);
}
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Nova Transação'),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () => Navigator.pop(context),
),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Tipo
const Text(
'Tipo',
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildTypeButton(
'Despesa',
TransactionType.expense,
Icons.arrow_upward,
AppColors.error,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildTypeButton(
'Receita',
TransactionType.revenue,
Icons.arrow_downward,
AppColors.success,
),
),
],
),
const SizedBox(height: 24),
CustomTextField(
controller: _descriptionController,
label: AppStrings.description,
hint: 'Ex: Aluguel, Luz, Produto...',
prefixIcon: Icons.description_outlined,
validator: (v) =>
v?.isEmpty ?? true ? 'Informe a descrição' : null,
),
const SizedBox(height: 20),
CustomTextField(
controller: _amountController,
label: 'Valor',
hint: '0,00',
prefixIcon: Icons.attach_money,
keyboardType: TextInputType.number,
validator: (v) =>
v?.isEmpty ?? true ? 'Informe o valor' : null,
),
const SizedBox(height: 20),
// Data
const Text(
AppStrings.dueDate,
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
GestureDetector(
onTap: _selectDate,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.surfaceLight,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(
Icons.calendar_today,
color: AppColors.textSecondary,
),
const SizedBox(width: 12),
Text(
'${_dueDate.day.toString().padLeft(2, '0')}/${_dueDate.month.toString().padLeft(2, '0')}/${_dueDate.year}',
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: 16,
),
),
],
),
),
),
const SizedBox(height: 20),
// Já pago?
SwitchListTile(
title: const Text(
'Já foi pago?',
style: TextStyle(color: AppColors.textPrimary),
),
value: _isPaid,
onChanged: (v) => setState(() => _isPaid = v),
activeThumbColor: AppColors.primaryColor,
contentPadding: EdgeInsets.zero,
),
const SizedBox(height: 32),
LoadingButton(
text: AppStrings.save,
isLoading: _isLoading,
onPressed: _saveTransaction,
),
],
),
),
),
),
);
}
Widget _buildTypeButton(
String label,
TransactionType type,
IconData icon,
Color color,
) {
final isSelected = _type == type;
return GestureDetector(
onTap: () => setState(() => _type = type),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isSelected ? color.withValues(alpha: 0.2) : AppColors.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? color : AppColors.surfaceLight,
width: isSelected ? 2 : 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: isSelected ? color : AppColors.textSecondary),
const SizedBox(width: 8),
Text(
label,
style: TextStyle(
color: isSelected ? color : AppColors.textSecondary,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,807 @@
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<FinancesPage> createState() => _FinancesPageState();
}
class _FinancesPageState extends State<FinancesPage>
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<void> _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<void> _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<TransactionModel> transactions) {
final Map<int, double> 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<FlSpot> 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<TransactionModel> 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<String, List<TransactionModel>> 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),
),
),
],
),
);
}
}