kz_educa/lib/screens/add_transaction_screen.dart

595 lines
21 KiB
Dart
Executable File

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart';
// Importações originais mantidas
import 'package:kzeduca_app/models/transaction.dart';
import 'package:kzeduca_app/widgets/transaction_calculator.dart';
import 'package:kzeduca_app/services/i18n_service.dart';
import 'package:kzeduca_app/services/user_state_service.dart';
import 'package:kzeduca_app/services/transaction_list_notifier.dart';
// KzEduca Palette
const _kGradientStart = Color(0xFF512DA8); // Roxo vibrante
const _kGradientEnd = Color(0xFF000000); // Preto profundo
const _kAccent = Color(0xFF00BFA5); // Verde água
const _kAction = Color(0xFFFFD600); // Amarelo/dourado
const _kExpenseColor = Color(0xFFFF5252); // Vermelho de erro
const _kIncomeColor = Color(0xFF69F0AE); // Verde de sucesso
class AddTransactionScreen extends StatefulWidget {
const AddTransactionScreen({super.key});
@override
State<AddTransactionScreen> createState() => _AddTransactionScreenState();
}
class _AddTransactionScreenState extends State<AddTransactionScreen> {
TransactionType _selectedType = TransactionType.expense;
double _currentAmount = 0.0;
TransactionCategory _selectedCategory = TransactionCategory.food;
DateTime _selectedDate = DateTime.now();
final TextEditingController _noteController = TextEditingController();
final TextEditingController _titleController = TextEditingController();
bool _showCalculator = false;
final Map<TransactionCategory, String> _expenseCategoryNames = {
TransactionCategory.food: 'food',
TransactionCategory.transport: 'transport',
TransactionCategory.social: 'social',
TransactionCategory.education: 'education',
TransactionCategory.medical: 'medical',
TransactionCategory.shopping: 'shopping',
TransactionCategory.others: 'others',
TransactionCategory.house: 'house',
TransactionCategory.utilities: 'utilities',
TransactionCategory.subscriptions: 'subscriptions',
TransactionCategory.leisure: 'leisure',
TransactionCategory.gym: 'gym',
TransactionCategory.gifts: 'gifts',
TransactionCategory.pets: 'pets',
TransactionCategory.tax: 'tax',
};
final Map<TransactionCategory, String> _incomeCategoryNames = {
TransactionCategory.salary: 'salary',
TransactionCategory.invest: 'invest',
TransactionCategory.business: 'business',
TransactionCategory.freelance: 'freelance',
TransactionCategory.dividends: 'dividends',
TransactionCategory.loan: 'loan',
TransactionCategory.refund: 'refund',
TransactionCategory.others: 'others',
TransactionCategory.allowance: 'allowance',
TransactionCategory.bonus: 'bonus',
};
final Map<TransactionCategory, IconData> _categoryIcons = {
TransactionCategory.food: Icons.restaurant_menu_rounded,
TransactionCategory.transport: Icons.directions_car_rounded,
TransactionCategory.social: Icons.people_rounded,
TransactionCategory.education: Icons.school_rounded,
TransactionCategory.medical: Icons.medical_services_rounded,
TransactionCategory.shopping: Icons.shopping_bag_rounded,
TransactionCategory.house: Icons.home_rounded,
TransactionCategory.utilities: Icons.lightbulb_rounded,
TransactionCategory.subscriptions: Icons.subscriptions_rounded,
TransactionCategory.leisure: Icons.sports_esports_rounded,
TransactionCategory.gym: Icons.fitness_center_rounded,
TransactionCategory.gifts: Icons.card_giftcard_rounded,
TransactionCategory.pets: Icons.pets_rounded,
TransactionCategory.salary: Icons.monetization_on_rounded,
TransactionCategory.invest: Icons.trending_up_rounded,
TransactionCategory.business: Icons.work_rounded,
TransactionCategory.freelance: Icons.laptop_chromebook_rounded,
TransactionCategory.dividends: Icons.show_chart_rounded,
TransactionCategory.loan: Icons.account_balance_rounded,
TransactionCategory.refund: Icons.undo_rounded,
TransactionCategory.others: Icons.category_rounded,
TransactionCategory.allowance: Icons.card_giftcard_rounded,
TransactionCategory.bonus: Icons.stars_rounded,
TransactionCategory.tax: Icons.account_balance_wallet_rounded,
};
void _onAmountChanged(double newAmount) {
setState(() {
_currentAmount = newAmount;
// UX: Garante que a calculadora esteja visível se o valor for alterado.
if (newAmount > 0.0) {
_showCalculator = true;
}
});
}
Future<void> _selectDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _selectedDate,
firstDate: DateTime(2000),
lastDate: DateTime(2101),
locale: const Locale('pt', 'BR'),
builder: (context, child) {
return Theme(
data: ThemeData.dark().copyWith(
colorScheme: ColorScheme.dark(
primary: _kAccent,
onPrimary: Colors.white,
surface: Colors.grey.shade900,
onSurface: Colors.white,
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: _kAccent,
),
),
),
child: child!,
);
},
);
if (picked != null && picked != _selectedDate) {
setState(() {
_selectedDate = picked;
});
}
}
void _saveTransaction() async {
// Usamos 'listen: false' para evitar rebuilds desnecessários no método.
final i18n = Provider.of<I18nService>(context, listen: false);
if (_currentAmount <= 0) {
_showSnackBar(i18n.t('error campo de entrada valor monetario'), isError: true);
return;
}
if (_titleController.text.trim().isEmpty) {
_showSnackBar(i18n.t('erro campo entrada titulo '), isError: true);
return;
}
// A classe Transaction deve vir do seu arquivo 'models/transaction.dart'
final newTransaction = Transaction(
title: _titleController.text.trim(),
amount: _currentAmount,
type: _selectedType,
category: _selectedCategory,
date: _selectedDate,
note: _noteController.text.isEmpty ? null : _noteController.text.trim(),
);
try {
final txnList = Provider.of<TransactionListNotifier>(context, listen: false);
await txnList.addTransaction(newTransaction);
final userStateService = Provider.of<UserStateService>(context, listen: false);
await userStateService.recalculateBalanceFromHive();
_showSnackBar(i18n.t('transaction salva com success'));
Navigator.pop(context, true);
} catch (e) {
_showSnackBar('${i18n.t('erro ao salvar transaction')}: $e', isError: true);
}
}
void _showSnackBar(String message, {bool isError = false}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
message,
style: GoogleFonts.montserrat(color: Colors.white, fontWeight: FontWeight.w600),
),
// UX: Usar a cor de destaque para sucesso e a cor de erro para falha
backgroundColor: isError ? _kExpenseColor.withOpacity(0.9) : _kIncomeColor.withOpacity(0.9),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
behavior: SnackBarBehavior.floating,
),
);
}
@override
Widget build(BuildContext context) {
final i18n = Provider.of<I18nService>(context);
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Colors.transparent,
appBar: AppBar(
title: Text(
i18n.t('add_transaction_title'),
style: GoogleFonts.montserrat(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
elevation: 0,
centerTitle: true,
),
body: Stack(
children: [
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [_kGradientStart, _kGradientEnd],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(25.0),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
height: MediaQuery.of(context).size.height * 0.85,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(25.0),
border: Border.all(color: Colors.white.withOpacity(0.2)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.4),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
children: [
_buildHeader(i18n),
const SizedBox(height: 20),
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildFormFields(i18n),
const SizedBox(height: 20),
_buildCategorySection(i18n),
const SizedBox(height: 20),
],
),
),
),
// MELHORIA UX: Usar AnimatedSize para transição mais suave da Calculadora
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
child: _showCalculator
? Column(
children: [
TransactionCalculator(
// A calculadora é responsável por chamar _onAmountChanged
onAmountChanged: _onAmountChanged,
onSave: _saveTransaction,
),
const SizedBox(height: 15),
],
)
: const SizedBox(height: 0), // Oculta a calculadora
),
// O botão Salvar aparece sempre, exceto se for integrado na calculadora
_buildSaveButton(i18n),
],
),
),
),
),
),
],
),
);
}
Widget _buildSaveButton(I18nService i18n) {
// Se a calculadora estiver visível, ela deve ter seu próprio botão de salvar
// A implementação do seu código original a colocava fora do bloco da calculadora, mantive assim para consistência
// No entanto, se o TransactionCalculator tiver um botão "Salvar" interno, este aqui pode ser oculto
// Se você não quiser o botão de salvar duplicado quando a calculadora estiver visível:
// if (_showCalculator) return const SizedBox.shrink();
return ElevatedButton(
onPressed: _saveTransaction,
style: ElevatedButton.styleFrom(
backgroundColor: _kAccent,
foregroundColor: Colors.black87,
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
elevation: 8,
shadowColor: _kAccent.withOpacity(0.5),
),
child: Text(
i18n.t('save_transaction'),
style: GoogleFonts.montserrat(fontSize: 16, fontWeight: FontWeight.bold),
),
);
}
Widget _buildHeader(I18nService i18n) {
return Column(
children: [
_buildTypeSelection(i18n),
const SizedBox(height: 30),
_buildAmountDisplay(i18n),
],
);
}
Widget _buildTypeSelection(I18nService i18n) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildTypeButton(i18n.t('expense'), TransactionType.expense, Icons.arrow_downward_rounded),
const SizedBox(width: 15),
_buildTypeButton(i18n.t('income'), TransactionType.income, Icons.arrow_upward_rounded),
],
);
}
Widget _buildTypeButton(String label, TransactionType type, IconData icon) {
final isSelected = _selectedType == type;
final Color selectedColor = type == TransactionType.expense ? _kExpenseColor : _kIncomeColor;
final Color textColor = isSelected ? Colors.white : Colors.white.withOpacity(0.7);
return Expanded(
child: GestureDetector(
onTap: () {
setState(() {
_selectedType = type;
// Garante que a categoria inicial do novo tipo seja selecionada
_selectedCategory = type == TransactionType.expense
? _expenseCategoryNames.keys.first
: _incomeCategoryNames.keys.first;
// UX: Zera o valor ao mudar o tipo de transação
_currentAmount = 0.0;
_showCalculator = false;
});
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
decoration: BoxDecoration(
gradient: isSelected
? LinearGradient(colors: [selectedColor.withOpacity(0.7), selectedColor.withOpacity(0.5)])
: null,
color: isSelected ? null : Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(15),
border: Border.all(color: isSelected ? selectedColor : Colors.white.withOpacity(0.2)),
boxShadow: [
BoxShadow(
color: isSelected ? selectedColor.withOpacity(0.4) : Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: textColor),
const SizedBox(width: 8),
Text(
label,
style: GoogleFonts.montserrat(
fontSize: 16,
color: textColor,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
],
),
),
),
);
}
Widget _buildAmountDisplay(I18nService i18n) {
return GestureDetector(
// UX: Clicar no valor alterna a visibilidade da calculadora
onTap: () {
setState(() {
_showCalculator = !_showCalculator;
});
},
child: Center(
child: Text(
'${i18n.t('kwanza_symbol')} ${NumberFormat.currency(locale: 'pt_BR', symbol: '').format(_currentAmount)}',
style: GoogleFonts.montserrat(
fontSize: 50,
fontWeight: FontWeight.w900,
color: _selectedType == TransactionType.expense
? _kExpenseColor
: _kIncomeColor,
),
),
),
);
}
Widget _buildFormFields(I18nService i18n) {
return Column(
children: [
_buildCustomTextField(
controller: _titleController,
label: i18n.t('transaction_title'),
hint: i18n.t('example_title'),
icon: Icons.description_rounded,
),
const SizedBox(height: 20),
_buildCustomTextField(
controller: _noteController,
label: i18n.t('notes_optional'),
hint: i18n.t('example_notes'),
icon: Icons.notes_rounded,
maxLines: 3,
),
const SizedBox(height: 20),
_buildDatePickerField(i18n),
],
);
}
Widget _buildCustomTextField({
required TextEditingController controller,
required String label,
required String hint,
required IconData icon,
int maxLines = 1,
}) {
// MELHORIA UX: Cores de texto, ícone e hint ajustadas para o tema escuro.
const Color lightTextColor = Colors.white;
final Color iconColor = lightTextColor.withOpacity(0.7);
final Color hintColor = lightTextColor.withOpacity(0.5);
return Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(15),
// MELHORIA UX: Borda consistente com o restante da interface
border: Border.all(color: Colors.white.withOpacity(0.2)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: TextField(
controller: controller,
maxLines: maxLines,
textCapitalization: TextCapitalization.sentences,
// MELHORIA UX: Cor do texto digitado ajustada para branco
style: GoogleFonts.montserrat(color: lightTextColor, fontWeight: FontWeight.w500),
decoration: InputDecoration(
labelText: label,
hintText: hint,
// MELHORIA UX: Cor do ícone ajustada para o tema escuro
prefixIcon: Icon(icon, color: iconColor),
// MELHORIA UX: Cor do label e hint ajustada para o tema escuro
labelStyle: GoogleFonts.montserrat(color: iconColor),
hintStyle: GoogleFonts.montserrat(color: hintColor),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
),
),
);
}
Widget _buildDatePickerField(I18nService i18n) {
return GestureDetector(
onTap: () => _selectDate(context),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.white.withOpacity(0.2)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Icon(Icons.calendar_today_rounded, color: Colors.white.withOpacity(0.7)),
const SizedBox(width: 15),
Expanded(
child: Text(
DateFormat('dd \'de\' MMM \'de\' yyyy', 'pt_BR').format(_selectedDate),
style: GoogleFonts.montserrat(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w500),
),
),
],
),
),
);
}
Widget _buildCategorySection(I18nService i18n) {
final categories = _selectedType == TransactionType.expense
? _expenseCategoryNames
: _incomeCategoryNames;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
i18n.t('select_category'),
style: GoogleFonts.montserrat(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
const SizedBox(height: 15),
Wrap(
spacing: 12.0,
runSpacing: 12.0,
children: categories.entries.map((entry) {
return _buildCategoryButton(i18n, entry.key, i18n.t(entry.value));
}).toList(),
),
],
);
}
Widget _buildCategoryButton(I18nService i18n, TransactionCategory category, String label) {
final isSelected = _selectedCategory == category;
final selectedColor = _selectedType == TransactionType.expense ? _kExpenseColor : _kIncomeColor;
final textColor = isSelected ? Colors.white : Colors.white.withOpacity(0.7);
return GestureDetector(
onTap: () {
setState(() {
_selectedCategory = category;
});
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isSelected ? selectedColor.withOpacity(0.2) : Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: isSelected ? selectedColor : Colors.white.withOpacity(0.2)),
boxShadow: isSelected
? [
BoxShadow(
color: selectedColor.withOpacity(0.4),
blurRadius: 10,
offset: const Offset(0, 4),
),
]
: [],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_categoryIcons[category],
color: textColor,
),
const SizedBox(width: 8),
Text(
label,
style: GoogleFonts.montserrat(
color: textColor,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
],
),
),
);
}
}