import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:uuid/uuid.dart'; import '../../core/constants.dart'; import '../../shared/models/transaction.dart'; import '../dashboard/provider.dart'; import '../settings/provider.dart'; import 'provider.dart'; const _uuid = Uuid(); class AddTransactionScreen extends ConsumerStatefulWidget { final Transaction? initial; const AddTransactionScreen({super.key, this.initial}); @override ConsumerState createState() => _AddTransactionScreenState(); } class _AddTransactionScreenState extends ConsumerState { final _formKey = GlobalKey(); final _amountController = TextEditingController(); final _noteController = TextEditingController(); late FocusNode _amountFocusNode; bool _amountFocused = false; @override void initState() { super.initState(); _amountFocusNode = FocusNode(); _amountFocusNode.addListener(() { setState(() => _amountFocused = _amountFocusNode.hasFocus); }); if (widget.initial != null) { _amountController.text = widget.initial!.amount.toString(); _noteController.text = widget.initial!.note ?? ''; } else { WidgetsBinding.instance.addPostFrameCallback((_) { final curr = ref.read(currencyProvider); ref.read(addTransactionProvider(null).notifier).setCurrency(curr.symbol, curr.code); }); } } @override void dispose() { _amountController.dispose(); _noteController.dispose(); _amountFocusNode.dispose(); super.dispose(); } Future _submit() async { if (!_formKey.currentState!.validate()) return; final state = ref.read(addTransactionProvider(widget.initial)); ref.read(addTransactionProvider(widget.initial).notifier).setSubmitting(true); final note = _noteController.text.trim(); final tx = Transaction( id: state.editingId ?? _uuid.v4(), amount: state.amount!, category: state.category, type: state.type, date: state.date, note: note.isEmpty ? null : note, currency: state.overrideCurrency, currencyCode: state.overrideCurrencyCode, ); if (state.isEditing) { await ref.read(transactionsProvider.notifier).update(tx); } else { await ref.read(transactionsProvider.notifier).add(tx); } ref.read(addTransactionProvider(widget.initial).notifier).setSubmitting(false); if (mounted) context.pop(); } Future _pickDate() async { final state = ref.read(addTransactionProvider(widget.initial)); final picked = await showDatePicker( context: context, initialDate: state.date, firstDate: DateTime(2000), lastDate: DateTime.now(), builder: (context, child) => Theme( data: Theme.of(context).copyWith( colorScheme: Theme.of(context).colorScheme.copyWith( primary: AppColors.accent, ), ), child: child!, ), ); if (picked != null) { ref.read(addTransactionProvider(widget.initial).notifier).setDate(picked); } } @override Widget build(BuildContext context) { final state = ref.watch(addTransactionProvider(widget.initial)); final categories = ref.watch(availableCategoriesProvider(widget.initial)); final overrideCurrency = state.overrideCurrency; final isDark = Theme.of(context).brightness == Brightness.dark; return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( title: Text(state.isEditing ? 'Edit Transaction' : 'Add Transaction'), leading: IconButton( icon: const Icon(Icons.close_rounded), onPressed: () => context.pop(), ), actions: [ if (state.isEditing) IconButton( icon: const Icon(Icons.delete_outline_rounded), color: const Color(0xFFE05C6B), onPressed: () { showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Delete transaction?'), content: const Text('This action cannot be undone.'), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Cancel'), ), TextButton( onPressed: () { ref.read(transactionsProvider.notifier).delete(widget.initial!.id); Navigator.pop(ctx); context.pop(); }, style: TextButton.styleFrom( foregroundColor: const Color(0xFFE05C6B), ), child: const Text('Delete'), ), ], ), ); }, ), ], ), body: SafeArea( child: Form( key: _formKey, child: ListView( padding: const EdgeInsets.all(20), children: [ _TypeToggle( selected: state.type, onChanged: (t) => ref.read(addTransactionProvider(widget.initial).notifier).setType(t), ), const SizedBox(height: 24), _SectionLabel('Amount'), const SizedBox(height: 8), Container( decoration: BoxDecoration( color: Theme.of(context).inputDecorationTheme.fillColor, borderRadius: BorderRadius.circular(12), border: Border.all( color: _amountFocused ? Theme.of(context).colorScheme.primary : Colors.transparent, width: 1.5, ), ), child: Row( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Text( overrideCurrency, style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurface, ), ), ), Expanded( child: TextFormField( controller: _amountController, focusNode: _amountFocusNode, keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}')), ], style: Theme.of(context).textTheme.headlineSmall?.copyWith( color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.w600, ), decoration: const InputDecoration( hintText: '0.00', border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, filled: false, contentPadding: EdgeInsets.symmetric(horizontal: 0, vertical: 16), ), onChanged: (v) { final parsed = double.tryParse(v); ref.read(addTransactionProvider(widget.initial).notifier).setAmount(parsed); }, validator: (v) { if (v == null || v.isEmpty) return 'Enter an amount'; if (double.tryParse(v) == null) return 'Invalid amount'; if (double.parse(v) <= 0) return 'Amount must be > 0'; return null; }, ), ), ], ), ), const SizedBox(height: 20), Text( 'Currency', style: TextStyle( fontSize: 13, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), ), ), const SizedBox(height: 8), _CurrencyPicker( selected: state.overrideCurrencyCode, onChanged: (symbol, code) => ref.read(addTransactionProvider(widget.initial).notifier).setCurrency(symbol, code), ), const SizedBox(height: 20), _SectionLabel('Category'), const SizedBox(height: 8), _CategoryPicker( categories: categories, selected: state.category, onChanged: (c) => ref.read(addTransactionProvider(widget.initial).notifier).setCategory(c), ), const SizedBox(height: 20), _SectionLabel('Date'), const SizedBox(height: 8), InkWell( onTap: _pickDate, borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(12), border: isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1), ), child: Row( children: [ Icon(Icons.calendar_today_rounded, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), size: 18), const SizedBox(width: 10), Text( DateFormat('MMMM d, yyyy').format(state.date), style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface, ), ), ], ), ), ), const SizedBox(height: 20), _SectionLabel('Note (optional)'), const SizedBox(height: 8), TextFormField( controller: _noteController, maxLines: 2, maxLength: 20, decoration: const InputDecoration( hintText: 'Add a note...', ), onChanged: (v) => ref.read(addTransactionProvider(widget.initial).notifier).setNote(v.trim()), ), const SizedBox(height: 32), AnimatedContainer( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, child: Builder( builder: (context) { final selectedType = state.type; final typeColor = selectedType == TransactionType.income ? const Color(0xFF4CAF8C) : const Color(0xFFE05C6B); return SizedBox( width: double.infinity, child: OutlinedButton( onPressed: state.isSubmitting ? null : _submit, style: OutlinedButton.styleFrom( backgroundColor: typeColor.withOpacity(0.1), side: BorderSide(color: typeColor, width: 2), padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), foregroundColor: typeColor, ), child: state.isSubmitting ? SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, color: typeColor, ), ) : Text( state.isEditing ? 'Save Changes' : 'Add Transaction', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: typeColor, ), ), ), ); }, ), ), ], ), ), ), ); } } class _SectionLabel extends StatelessWidget { final String text; const _SectionLabel(this.text); @override Widget build(BuildContext context) { return Text( text, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), fontWeight: FontWeight.w600, letterSpacing: 0.5, ), ); } } class _TypeToggle extends StatelessWidget { final TransactionType selected; final ValueChanged onChanged; const _TypeToggle({required this.selected, required this.onChanged}); @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(14), border: isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1), ), child: Row( children: [ _TypeOption( label: 'Income', icon: Icons.arrow_downward_rounded, color: AppColors.income, isSelected: selected == TransactionType.income, onTap: () => onChanged(TransactionType.income), ), _TypeOption( label: 'Expense', icon: Icons.arrow_upward_rounded, color: AppColors.expense, isSelected: selected == TransactionType.expense, onTap: () => onChanged(TransactionType.expense), ), ], ), ); } } class _TypeOption extends StatelessWidget { final String label; final IconData icon; final Color color; final bool isSelected; final VoidCallback onTap; const _TypeOption({ required this.label, required this.icon, required this.color, required this.isSelected, required this.onTap, }); @override Widget build(BuildContext context) { return Expanded( child: GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(vertical: 14), decoration: BoxDecoration( color: isSelected ? color.withOpacity(0.15) : Colors.transparent, borderRadius: BorderRadius.circular(13), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: isSelected ? color : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), size: 18), const SizedBox(width: 6), Text( label, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: isSelected ? color : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), ), ], ), ), ), ); } } class _CategoryPicker extends StatelessWidget { final List categories; final String selected; final ValueChanged onChanged; const _CategoryPicker({ required this.categories, required this.selected, required this.onChanged, }); @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return Wrap( spacing: 8, runSpacing: 8, children: categories.map((cat) { final isSelected = cat == selected; final color = AppCategories.colors[cat] ?? AppColors.accent; final icon = AppCategories.icons[cat] ?? Icons.category_rounded; return GestureDetector( onTap: () => onChanged(cat), child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: isSelected ? color.withOpacity(0.2) : Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(12), border: isSelected ? Border.all(color: color, width: 1.5) : (isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, color: isSelected ? color : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), size: 16), const SizedBox(width: 6), Text( cat, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: isSelected ? color : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), ), ], ), ), ); }).toList(), ); } } class _CurrencyPicker extends StatelessWidget { final String selected; final void Function(String symbol, String code) onChanged; const _CurrencyPicker({ required this.selected, required this.onChanged, }); @override Widget build(BuildContext context) { final currencies = [ ('USD', '\$'), ('EUR', '€'), ('BYN', 'Br'), ('RUB', '₽'), ]; final colorScheme = Theme.of(context).colorScheme; return Row( children: currencies.map((c) { final isSelected = c.$1 == selected; return Expanded( child: GestureDetector( onTap: () => onChanged(c.$2, c.$1), child: Container( margin: EdgeInsets.only(right: c.$1 == currencies.last.$1 ? 0 : 8), padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( color: isSelected ? const Color(0xFF7C6DED).withOpacity(0.15) : Theme.of(context).cardColor, borderRadius: BorderRadius.circular(10), border: Border.all( color: isSelected ? const Color(0xFF7C6DED) : Colors.transparent, width: 1.5, ), ), child: Column( children: [ Text( c.$2, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: isSelected ? const Color(0xFF7C6DED) : colorScheme.onSurface, ), ), Text( c.$1, style: TextStyle( fontSize: 10, color: colorScheme.onSurface.withOpacity(0.5), ), ), ], ), ), ), ); }).toList(), ); } }