808 lines
25 KiB
Dart
808 lines
25 KiB
Dart
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),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|