595 lines
21 KiB
Dart
Executable File
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} |