From e3baad4dfada893e9ef9659c4bca1bf972a618ec Mon Sep 17 00:00:00 2001 From: alexis Date: Sat, 11 Jul 2026 22:05:37 +0200 Subject: [PATCH] feat: wire + (add tx dialog) and search (filter lists) --- lib/app.dart | 155 ++++++++++++++++++++++++--- lib/providers/db_provider.dart | 2 + lib/screens/accounts_screen.dart | 6 +- lib/screens/transactions_screen.dart | 6 +- 4 files changed, 149 insertions(+), 20 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 50c4252..3a0df12 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -5,6 +5,8 @@ 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); @@ -18,6 +20,8 @@ class YnabApp extends ConsumerStatefulWidget { class _YnabAppState extends ConsumerState { late final List _screens; late final List<_NavItem> _navItems; + final _searchController = TextEditingController(); + bool _searchActive = false; @override void initState() { @@ -36,6 +40,96 @@ class _YnabAppState extends ConsumerState { ]; } + @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(); + + await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Nouvelle transaction'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + 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), + DropdownButtonFormField( + value: accountId, + decoration: const InputDecoration(labelText: 'Compte'), + items: accounts + .map((a) => DropdownMenuItem(value: a.id, child: Text(a.name))) + .toList(), + onChanged: (v) => accountId = v, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Annuler'), + ), + FilledButton( + onPressed: () async { + final payee = payeeCtrl.text.trim(); + final amountStr = amountCtrl.text.trim().replaceAll(',', '.'); + final amount = double.tryParse(amountStr); + if (payee.isEmpty || amount == null || accountId == null) return; + await db.into(db.transactions).insert(TransactionsCompanion.insert( + accountId: accountId!, + payee: payee, + amount: amount, + date: date, + )); + if (ctx.mounted) Navigator.pop(ctx); + }, + child: const Text('Ajouter'), + ), + ], + ), + ); + payeeCtrl.dispose(); + amountCtrl.dispose(); + } + + void _toggleSearch() { + setState(() { + _searchActive = !_searchActive; + if (!_searchActive) { + _searchController.clear(); + ref.read(searchQueryProvider.notifier).state = ''; + } + }); + } + @override Widget build(BuildContext context) { final selectedIndex = ref.watch(_selectedIndexProvider); @@ -46,6 +140,11 @@ class _YnabAppState extends ConsumerState { 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) { @@ -70,25 +169,47 @@ class _YnabAppState extends ConsumerState { ), child: Row( children: [ - Text( - _navItems[selectedIndex].label, - style: TextStyle( - color: colorScheme.onSurface, - fontSize: 18, - fontWeight: FontWeight.w600, + if (_searchActive) + Expanded( + child: TextField( + controller: _searchController, + autofocus: true, + decoration: const InputDecoration( + hintText: 'Rechercher…', + border: InputBorder.none, + isDense: true, + ), + onChanged: (v) => ref.read(searchQueryProvider.notifier).state = v, + ), + ) + else + Text( + _navItems[selectedIndex].label, + style: TextStyle( + color: colorScheme.onSurface, + fontSize: 18, + fontWeight: FontWeight.w600, + ), ), - ), const Spacer(), - IconButton( - icon: Icon(Icons.add, color: colorScheme.primary), - onPressed: () {}, - tooltip: 'Ajouter une transaction', - ), - IconButton( - icon: Icon(Icons.search, color: colorScheme.onSurfaceVariant), - onPressed: () {}, - tooltip: 'Rechercher', - ), + if (_searchActive) + IconButton( + icon: const Icon(Icons.close), + onPressed: _toggleSearch, + tooltip: 'Fermer la recherche', + ) + else ...[ + IconButton( + icon: Icon(Icons.add, color: colorScheme.primary), + onPressed: _addTransaction, + tooltip: 'Ajouter une transaction', + ), + IconButton( + icon: Icon(Icons.search, color: colorScheme.onSurfaceVariant), + onPressed: _toggleSearch, + tooltip: 'Rechercher', + ), + ], ], ), ), diff --git a/lib/providers/db_provider.dart b/lib/providers/db_provider.dart index 9f20e08..dabbe22 100644 --- a/lib/providers/db_provider.dart +++ b/lib/providers/db_provider.dart @@ -11,6 +11,8 @@ final dbProvider = Provider((ref) { return db; }); +final searchQueryProvider = StateProvider((ref) => ''); + // ── Accounts ──────────────────────────────────────────────────────────── final accountsProvider = StreamProvider>((ref) { final db = ref.watch(dbProvider); diff --git a/lib/screens/accounts_screen.dart b/lib/screens/accounts_screen.dart index 3777ec7..ce8d461 100644 --- a/lib/screens/accounts_screen.dart +++ b/lib/screens/accounts_screen.dart @@ -8,7 +8,11 @@ class AccountsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final accounts = ref.watch(openAccountsProvider); + final all = ref.watch(openAccountsProvider); + final query = ref.watch(searchQueryProvider).toLowerCase(); + final accounts = query.isEmpty + ? all + : all.where((a) => a.name.toLowerCase().contains(query)).toList(); return ListView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 90), diff --git a/lib/screens/transactions_screen.dart b/lib/screens/transactions_screen.dart index 500c6c9..2876912 100644 --- a/lib/screens/transactions_screen.dart +++ b/lib/screens/transactions_screen.dart @@ -10,12 +10,14 @@ class TransactionsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final all = ref.watch(allTransactionsProvider).valueOrNull ?? []; + final query = ref.watch(searchQueryProvider).toLowerCase(); + final filtered = query.isEmpty ? all : all.where((t) => t.payee.toLowerCase().contains(query)).toList(); return ListView.builder( padding: const EdgeInsets.fromLTRB(16, 8, 16, 90), - itemCount: all.length, + itemCount: filtered.length, itemBuilder: (context, index) { - final t = all[index]; + final t = filtered[index]; return Card( margin: const EdgeInsets.only(bottom: 4), child: ListTile(