import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'screens/dashboard_screen.dart'; import 'screens/budget_screen.dart'; import 'screens/accounts_screen.dart'; import 'screens/transactions_screen.dart'; import 'providers/db_provider.dart'; import 'data/db.dart'; final _selectedIndexProvider = StateProvider((ref) => 0); class YnabApp extends ConsumerStatefulWidget { const YnabApp({super.key}); @override ConsumerState createState() => _YnabAppState(); } class _YnabAppState extends ConsumerState { late final List _screens; late final List<_NavItem> _navItems; final _searchController = TextEditingController(); bool _searchActive = false; @override void initState() { super.initState(); _screens = const [ DashboardScreen(), BudgetScreen(), AccountsScreen(), TransactionsScreen(), ]; _navItems = const [ _NavItem(icon: Icons.dashboard_outlined, selectedIcon: Icons.dashboard, label: 'Tableau de bord'), _NavItem(icon: Icons.pie_chart_outline, selectedIcon: Icons.pie_chart, label: 'Budget'), _NavItem(icon: Icons.account_balance_outlined, selectedIcon: Icons.account_balance, label: 'Comptes'), _NavItem(icon: Icons.receipt_long_outlined, selectedIcon: Icons.receipt_long, label: 'Transactions'), ]; } @override void dispose() { _searchController.dispose(); super.dispose(); } Future _addTransaction() async { final accounts = ref.read(openAccountsProvider); final db = ref.read(dbProvider); if (accounts.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Créez d\'abord un compte')), ); return; } final payeeCtrl = TextEditingController(); final amountCtrl = TextEditingController(); int? accountId = accounts.first.id; DateTime date = DateTime.now(); bool isExpense = true; final colorScheme = Theme.of(context).colorScheme; await showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, backgroundColor: Colors.transparent, builder: (ctx) => StatefulBuilder( builder: (context, setSheetState) => Container( decoration: BoxDecoration( color: isExpense ? colorScheme.errorContainer : colorScheme.primaryContainer, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Padding( padding: EdgeInsets.fromLTRB(24, 0, 24, MediaQuery.of(context).viewInsets.bottom + 24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 12), Center( child: Container( width: 32, height: 4, decoration: BoxDecoration( color: colorScheme.onSurfaceVariant.withAlpha(80), borderRadius: BorderRadius.circular(2), ), ), ), const SizedBox(height: 16), Text( 'Nouvelle transaction', textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 20), TextField( controller: payeeCtrl, decoration: const InputDecoration(labelText: 'Bénéficiaire', hintText: 'Ex: Supermarché'), autofocus: true, ), const SizedBox(height: 12), TextField( controller: amountCtrl, decoration: const InputDecoration(labelText: 'Montant', hintText: '0,00'), keyboardType: const TextInputType.numberWithOptions(decimal: true), ), const SizedBox(height: 12), SegmentedButton( segments: const [ ButtonSegment(value: true, label: Text('Dépense'), icon: Icon(Icons.arrow_upward)), ButtonSegment(value: false, label: Text('Revenu'), icon: Icon(Icons.arrow_downward)), ], selected: {isExpense}, onSelectionChanged: (v) => setSheetState(() => isExpense = v.first), style: ButtonStyle( visualDensity: VisualDensity.compact, ), ), const SizedBox(height: 12), DropdownMenu( initialSelection: accountId, label: const Text('Compte'), expandedInsets: EdgeInsets.zero, dropdownMenuEntries: accounts .map((a) => DropdownMenuEntry(value: a.id, label: a.name)) .toList(), onSelected: (v) { if (v != null) accountId = v; }, ), const SizedBox(height: 24), FilledButton( onPressed: () async { final payee = payeeCtrl.text.trim(); final amountStr = amountCtrl.text.trim().replaceAll(',', '.'); final raw = double.tryParse(amountStr); if (payee.isEmpty || raw == null || accountId == null) return; final amount = isExpense ? -raw.abs() : raw.abs(); await db.into(db.transactions).insert(TransactionsCompanion.insert( accountId: accountId!, payee: payee, amount: amount, date: date, )); if (context.mounted) Navigator.pop(context); }, child: const Text('Ajouter'), ), const SizedBox(height: 8), ], ), ), ), ), ); } void _toggleSearch() { setState(() { _searchActive = !_searchActive; if (!_searchActive) { _searchController.clear(); ref.read(searchQueryProvider.notifier).state = ''; } }); } @override Widget build(BuildContext context) { final selectedIndex = ref.watch(_selectedIndexProvider); final theme = Theme.of(context); final colorScheme = theme.colorScheme; return Scaffold( appBar: AppBar( title: _searchActive ? SearchBar( controller: _searchController, hintText: 'Rechercher…', leading: const Icon(Icons.search, size: 20), padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(horizontal: 4)), constraints: BoxConstraints.tightFor(height: 44), onChanged: (v) => ref.read(searchQueryProvider.notifier).state = v, ) : Text(_navItems[selectedIndex].label), actions: [ if (_searchActive) IconButton( icon: const Icon(Icons.close), onPressed: _toggleSearch, tooltip: 'Fermer la recherche', ) else ...[ if (selectedIndex == 0) IconButton( icon: Icon(Icons.add, color: colorScheme.primary), onPressed: _addTransaction, tooltip: 'Ajouter une transaction', ), if (selectedIndex >= 1) IconButton( icon: Icon(Icons.search, color: colorScheme.onSurfaceVariant), onPressed: _toggleSearch, tooltip: 'Rechercher', ), ], ], ), body: AnimatedSwitcher( duration: const Duration(milliseconds: 200), child: _screens[selectedIndex], ), floatingActionButton: selectedIndex == 3 ? FloatingActionButton.extended( icon: const Icon(Icons.add), label: const Text('Transaction'), onPressed: _addTransaction, ) : null, bottomNavigationBar: NavigationBar( selectedIndex: selectedIndex, onDestinationSelected: (int index) { setState(() { _searchActive = false; _searchController.clear(); ref.read(searchQueryProvider.notifier).state = ''; }); ref.read(_selectedIndexProvider.notifier).state = index; }, destinations: _navItems.map((item) { return NavigationDestination( icon: Icon(item.icon), selectedIcon: Icon(item.selectedIcon), label: item.label, ); }).toList(), ), ); } } class _NavItem { final IconData icon; final IconData selectedIcon; final String label; const _NavItem({required this.icon, required this.selectedIcon, required this.label}); }