import 'package:flutter/material.dart'; import '../models/account.dart'; import '../models/category.dart'; import '../services/sync_service.dart'; class QuickTransactionScreen extends StatefulWidget { const QuickTransactionScreen({ super.key, required this.syncService, required this.accounts, required this.categories, required this.onSaved, }); final SyncService syncService; final List accounts; final List categories; /// Important: this is async because the home screen must reload the local /// SQLite snapshot before the quick form is closed. final Future Function() onSaved; @override State createState() => _QuickTransactionScreenState(); } class _QuickTransactionScreenState extends State { final _formKey = GlobalKey(); final _amountController = TextEditingController(); final _labelController = TextEditingController(); final _noteController = TextEditingController(); String _type = 'expense'; int? _accountId; int? _targetAccountId; int? _categoryId; bool _saving = false; static const _quickAmounts = [10, 20, 50, 100, 200, 500, 1000]; static const _expenseTemplates = <_QuickTemplate>[ _QuickTemplate(label: 'Courses', categoryHint: 'course'), _QuickTemplate(label: 'Essence', categoryHint: 'essence'), _QuickTemplate(label: 'Restaurant', categoryHint: 'restaurant'), _QuickTemplate(label: 'Café', categoryHint: 'café'), _QuickTemplate(label: 'Transport', categoryHint: 'transport'), _QuickTemplate(label: 'Santé', categoryHint: 'santé'), _QuickTemplate(label: 'Abonnement', categoryHint: 'abonnement'), _QuickTemplate(label: 'Retrait cash', categoryHint: 'cash'), ]; static const _incomeTemplates = <_QuickTemplate>[ _QuickTemplate(label: 'Salaire', categoryHint: 'salaire'), _QuickTemplate(label: 'Remboursement', categoryHint: 'remboursement'), _QuickTemplate(label: 'Prime', categoryHint: 'prime'), _QuickTemplate(label: 'Vente', categoryHint: 'vente'), ]; static const _transferTemplates = <_QuickTemplate>[ _QuickTemplate(label: 'Épargne'), _QuickTemplate(label: 'Provision'), _QuickTemplate(label: 'Banque → Cash'), _QuickTemplate(label: 'Cash → Banque'), ]; @override void initState() { super.initState(); if (widget.accounts.isNotEmpty) { _accountId = widget.accounts.first.id; _targetAccountId = widget.accounts.length > 1 ? widget.accounts[1].id : null; } _setDefaultCategory(); } @override void dispose() { _amountController.dispose(); _labelController.dispose(); _noteController.dispose(); super.dispose(); } List<_QuickTemplate> get _templates { if (_type == 'income') return _incomeTemplates; if (_type == 'transfer') return _transferTemplates; return _expenseTemplates; } void _setDefaultCategory({String? preferredHint}) { final filtered = widget.categories.where((c) => c.type == _type).toList(); if (filtered.isEmpty) { _categoryId = null; return; } if (preferredHint != null && preferredHint.trim().isNotEmpty) { final normalizedHint = _normalize(preferredHint); for (final category in filtered) { final normalizedCategory = _normalize(category.name); if (normalizedCategory.contains(normalizedHint) || normalizedHint.contains(normalizedCategory)) { _categoryId = category.id; return; } } } _categoryId = filtered.first.id; } String _normalize(String value) { return value .toLowerCase() .replaceAll('é', 'e') .replaceAll('è', 'e') .replaceAll('ê', 'e') .replaceAll('à', 'a') .replaceAll('â', 'a') .replaceAll('ù', 'u') .replaceAll('û', 'u') .replaceAll('î', 'i') .replaceAll('ï', 'i') .replaceAll('ô', 'o') .replaceAll('ç', 'c') .trim(); } List get _visibleCategories { return widget.categories.where((c) => c.type == _type).toList(); } void _applyTemplate(_QuickTemplate template) { if (_saving) return; setState(() { _labelController.text = template.label; if (template.amount != null) { _amountController.text = _formatAmount(template.amount!); } if (_type != 'transfer') { _setDefaultCategory(preferredHint: template.categoryHint ?? template.label); } }); } void _applyAmount(double amount) { if (_saving) return; setState(() { _amountController.text = _formatAmount(amount); }); } String _formatAmount(double amount) { if (amount == amount.roundToDouble()) return amount.toInt().toString(); return amount.toStringAsFixed(2); } int? _firstOtherAccountId() { for (final account in widget.accounts) { if (account.id != _accountId) return account.id; } return null; } Future _save() async { if (!_formKey.currentState!.validate()) return; final accountId = _accountId; if (accountId == null) { _showSnack('Compte obligatoire.'); return; } final targetAccountId = _targetAccountId; if (_type == 'transfer') { if (targetAccountId == null) { _showSnack('Compte cible obligatoire.'); return; } if (targetAccountId == accountId) { _showSnack('Source et cible doivent être différents.'); return; } } setState(() => _saving = true); try { final synced = await widget.syncService.createTransactionSmart( label: _labelController.text.trim(), amount: double.parse(_amountController.text.replaceAll(',', '.')), type: _type, accountId: accountId, targetAccountId: _type == 'transfer' ? targetAccountId : null, categoryId: _type == 'transfer' ? null : _categoryId, note: _noteController.text.trim().isEmpty ? null : _noteController.text.trim(), ); if (!mounted) return; await widget.onSaved(); if (!mounted) return; _showSnack( synced ? 'Transaction synchronisée.' : 'Backend absent : opération gardée localement.', ); if (mounted) { Navigator.of(context).pop(); } } catch (e) { if (!mounted) return; _showSnack('Erreur : $e'); } finally { if (mounted) setState(() => _saving = false); } } void _showSnack(String text) { final messenger = ScaffoldMessenger.maybeOf(context); messenger?.clearSnackBars(); messenger?.showSnackBar(SnackBar(content: Text(text))); } Widget _buildSaveButton() { return FilledButton.icon( onPressed: _saving ? null : _save, icon: _saving ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.save), label: Text(_saving ? 'Enregistrement...' : 'Enregistrer'), ); } @override Widget build(BuildContext context) { final categories = _visibleCategories; return Scaffold( resizeToAvoidBottomInset: true, appBar: AppBar( title: const Text('Ajout rapide'), ), bottomNavigationBar: SafeArea( minimum: const EdgeInsets.fromLTRB(16, 8, 16, 12), child: _buildSaveButton(), ), body: Form( key: _formKey, child: ListView( padding: const EdgeInsets.all(16), children: [ SegmentedButton( segments: const [ ButtonSegment(value: 'expense', label: Text('Dépense')), ButtonSegment(value: 'income', label: Text('Recette')), ButtonSegment(value: 'transfer', label: Text('Transfert')), ], selected: {_type}, onSelectionChanged: _saving ? null : (values) { setState(() { _type = values.first; _setDefaultCategory(); if (_type == 'transfer' && _targetAccountId == _accountId) { _targetAccountId = _firstOtherAccountId(); } }); }, ), const SizedBox(height: 16), _QuickChipsSection( title: _type == 'transfer' ? 'Transferts rapides' : _type == 'income' ? 'Recettes rapides' : 'Dépenses rapides', chips: _templates .map((template) => ActionChip( label: Text(template.label), onPressed: _saving ? null : () => _applyTemplate(template), )) .toList(), ), const SizedBox(height: 16), TextFormField( controller: _amountController, decoration: const InputDecoration( labelText: 'Montant', border: OutlineInputBorder(), ), keyboardType: const TextInputType.numberWithOptions(decimal: true), validator: (value) { final parsed = double.tryParse((value ?? '').replaceAll(',', '.')); if (parsed == null || parsed <= 0) return 'Montant invalide'; return null; }, ), const SizedBox(height: 8), _QuickChipsSection( title: 'Montants rapides', chips: _quickAmounts .map((amount) => ChoiceChip( label: Text(_formatAmount(amount)), selected: _amountController.text == _formatAmount(amount), onSelected: _saving ? null : (_) => _applyAmount(amount), )) .toList(), dense: true, ), const SizedBox(height: 12), TextFormField( controller: _labelController, decoration: const InputDecoration( labelText: 'Libellé', border: OutlineInputBorder(), ), validator: (value) { if ((value ?? '').trim().isEmpty) return 'Libellé obligatoire'; return null; }, ), const SizedBox(height: 12), DropdownButtonFormField( value: _accountId, decoration: const InputDecoration( labelText: 'Compte source', border: OutlineInputBorder(), ), items: widget.accounts .map((a) => DropdownMenuItem(value: a.id, child: Text(a.name))) .toList(), onChanged: _saving ? null : (value) { setState(() { _accountId = value; if (_type == 'transfer' && _targetAccountId == _accountId) { _targetAccountId = _firstOtherAccountId(); } }); }, validator: (value) => value == null ? 'Compte obligatoire' : null, ), if (_type == 'transfer') ...[ const SizedBox(height: 12), DropdownButtonFormField( value: _targetAccountId, decoration: const InputDecoration( labelText: 'Compte cible', border: OutlineInputBorder(), ), items: widget.accounts .map((a) => DropdownMenuItem(value: a.id, child: Text(a.name))) .toList(), onChanged: _saving ? null : (value) => setState(() => _targetAccountId = value), validator: (value) { if (value == null) return 'Compte cible obligatoire'; if (value == _accountId) return 'Source et cible doivent être différents'; return null; }, ), ], if (_type != 'transfer') ...[ const SizedBox(height: 12), DropdownButtonFormField( value: _categoryId, decoration: const InputDecoration( labelText: 'Catégorie', border: OutlineInputBorder(), ), items: categories .map((c) => DropdownMenuItem(value: c.id, child: Text(c.name))) .toList(), onChanged: _saving ? null : (value) => setState(() => _categoryId = value), ), ], const SizedBox(height: 12), TextFormField( controller: _noteController, decoration: const InputDecoration( labelText: 'Note', border: OutlineInputBorder(), ), minLines: 2, maxLines: 4, ), const SizedBox(height: 120), ], ), ), ); } } class _QuickTemplate { const _QuickTemplate({ required this.label, this.categoryHint, this.amount, }); final String label; final String? categoryHint; final double? amount; } class _QuickChipsSection extends StatelessWidget { const _QuickChipsSection({ required this.title, required this.chips, this.dense = false, }); final String title; final List chips; final bool dense; @override Widget build(BuildContext context) { if (chips.isEmpty) return const SizedBox.shrink(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: dense ? Theme.of(context).textTheme.bodyMedium : Theme.of(context).textTheme.titleSmall, ), SizedBox(height: dense ? 4 : 8), Wrap( spacing: 8, runSpacing: dense ? 4 : 8, children: chips, ), ], ); } }