import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/db_provider.dart'; import '../theme/app_theme.dart'; import 'package:drift/drift.dart' show Value; import '../data/db.dart'; class BudgetScreen extends ConsumerWidget { const BudgetScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final groups = ref.watch(categoryGroupsProvider).valueOrNull ?? []; final catsByGroup = ref.watch(categoriesByGroupProvider); final query = ref.watch(searchQueryProvider).toLowerCase(); final now = DateTime.now(); final months = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']; final monthLabel = '${months[now.month]} ${now.year}'; // filter categories and groups by search query final filteredGroups = query.isEmpty ? groups : groups.where((g) { final cats = catsByGroup[g.id] ?? []; return cats.any((c) => c.name.toLowerCase().contains(query)); }).toList(); final filteredCatsByGroup = query.isEmpty ? catsByGroup : catsByGroup.map((key, value) => MapEntry( key, value.where((c) => c.name.toLowerCase().contains(query)).toList(), )); return ListView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 90), children: [ Card( child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Budget du mois', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 4), Text(monthLabel, style: Theme.of(context).textTheme.bodySmall), ], ), ), ), const SizedBox(height: 16), ...filteredGroups.map((group) { final cats = filteredCatsByGroup[group.id] ?? []; if (cats.isEmpty) return const SizedBox.shrink(); return _CategoryGroupTile(group: group, categoryIds: cats.map((c) => c.id).toList()); }), ], ); } } class _CategoryGroupTile extends ConsumerWidget { final CategoryGroup group; final List categoryIds; const _CategoryGroupTile({required this.group, required this.categoryIds}); @override Widget build(BuildContext context, WidgetRef ref) { final expanded = ref.watch(_expandedGroupProvider(group.id)); final cats = ref.watch(categoriesByGroupProvider)[group.id] ?? []; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ListTile( dense: true, contentPadding: EdgeInsets.zero, leading: Icon( expanded ? Icons.keyboard_arrow_down : Icons.keyboard_arrow_right, size: 18, color: Theme.of(context).colorScheme.onSurfaceVariant, ), title: Text(group.name, style: Theme.of(context) .textTheme .titleSmall ?.copyWith(fontWeight: FontWeight.w600)), trailing: Text( formatCurrency( cats.fold(0.0, (s, c) => s + ref.watch(categoryRemainingProvider(c.id))), ), style: Theme.of(context).textTheme.bodySmall, ), onTap: () => ref.read(_expandedGroupProvider(group.id).notifier).toggle(), ), if (expanded) ...cats.map((c) => _CategoryRow(categoryId: c.id)), const Divider(height: 32), ], ); } } class _CategoryRow extends ConsumerWidget { final int categoryId; const _CategoryRow({required this.categoryId}); @override Widget build(BuildContext context, WidgetRef ref) { final cats = ref.watch(categoriesByGroupProvider); final category = cats.values .expand((list) => list) .firstWhere((c) => c.id == categoryId, orElse: () => throw StateError('missing')); final budgeted = ref.watch(monthlyBudgetProvider(categoryId)) ?? 0.0; final spent = ref.watch(categorySpentProvider(categoryId)); final remaining = ref.watch(categoryRemainingProvider(categoryId)); final pct = budgeted > 0 ? ((budgeted + spent) / budgeted).clamp(0.0, 1.0) : 0.0; void _editBudget() async { final month = ref.read(currentMonthProvider); final db = ref.read(dbProvider); final ctrl = TextEditingController(text: budgeted > 0 ? budgeted.toStringAsFixed(2) : ''); final newAmount = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text('Budget : ${category.name}'), content: TextField( controller: ctrl, decoration: const InputDecoration(labelText: 'Montant', hintText: '0,00'), keyboardType: const TextInputType.numberWithOptions(decimal: true), autofocus: true, ), actions: [ FilledButton.tonal(onPressed: () => Navigator.pop(ctx), child: const Text('Annuler')), FilledButton( onPressed: () { final v = double.tryParse(ctrl.text.trim().replaceAll(',', '.')); if (v != null) Navigator.pop(ctx, v); }, child: const Text('Enregistrer'), ), ], ), ); ctrl.dispose(); if (newAmount == null) return; final existing = await (db.select(db.budgets) ..where((b) => b.categoryId.equals(categoryId)) ..where((b) => b.month.equals(month))) .get(); if (existing.isNotEmpty) { await db.update(db.budgets).replace(existing.first.copyWith(budgeted: newAmount)); } else { await db.into(db.budgets).insert(BudgetsCompanion.insert( categoryId: categoryId, month: month, budgeted: Value(newAmount), )); } } return Padding( padding: const EdgeInsets.only(left: 8, bottom: 12), child: Card( margin: EdgeInsets.zero, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ListTile( dense: true, contentPadding: EdgeInsets.zero, visualDensity: VisualDensity.compact, leading: Container( width: 36, height: 36, decoration: BoxDecoration( color: Theme.of(context).colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(8), ), child: Icon( categoryIcon(category.icon), size: 18, color: Theme.of(context).colorScheme.onSecondaryContainer, ), ), title: Text(category.name, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), trailing: Text( formatCurrency(remaining), style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: remaining >= 0 ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.error, ), ), ), const SizedBox(height: 4), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: pct, backgroundColor: Theme.of(context).colorScheme.outlineVariant, valueColor: AlwaysStoppedAnimation( remaining >= 0 ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.error, ), minHeight: 4, ), ), const SizedBox(height: 2), Row( children: [ GestureDetector( onTap: _editBudget, child: Text('Budget: ${formatCurrency(budgeted)}', style: Theme.of(context).textTheme.bodySmall?.copyWith( decoration: TextDecoration.underline, color: Theme.of(context).colorScheme.primary, )), ), const SizedBox(width: 12), Text('Dépensé: ${formatCurrency(spent.abs())}', style: Theme.of(context).textTheme.bodySmall), ], ), ], ), ), ), ); } } class _ExpandedGroupNotifier extends StateNotifier { _ExpandedGroupNotifier() : super(true); void toggle() => state = !state; } final _expandedGroupProvider = StateNotifierProvider.family<_ExpandedGroupNotifier, bool, int>( (ref, id) => _ExpandedGroupNotifier()); IconData categoryIcon(String? name) { switch (name) { case 'home': return Icons.home; case 'bolt': return Icons.bolt; case 'water_drop': return Icons.water_drop; case 'shield': return Icons.shield; case 'shopping_cart': return Icons.shopping_cart; case 'restaurant': return Icons.restaurant; case 'local_gas_station': return Icons.local_gas_station; case 'directions_bus': return Icons.directions_bus; case 'tv': return Icons.tv; case 'celebration': return Icons.celebration; case 'savings': return Icons.savings; case 'flight': return Icons.flight; default: return Icons.category; } }