171 lines
5.8 KiB
Dart
171 lines
5.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../providers/db_provider.dart';
|
|
import '../theme/app_theme.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);
|
|
|
|
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('Juillet 2026', style: Theme.of(context).textTheme.bodySmall),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
...groups.map((group) {
|
|
final cats = catsByGroup[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<int> 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: [
|
|
InkWell(
|
|
onTap: () => ref.read(_expandedGroupProvider(group.id).notifier).toggle(),
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
expanded ? Icons.keyboard_arrow_down : Icons.keyboard_arrow_right,
|
|
size: 18,
|
|
color: kTextSecondary,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(group.name,
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.titleSmall
|
|
?.copyWith(fontWeight: FontWeight.w600)),
|
|
const Spacer(),
|
|
Text(
|
|
formatCurrency(
|
|
cats.fold(0.0, (s, c) => s + ref.watch(categoryRemainingProvider(c.id))),
|
|
),
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (expanded)
|
|
...cats.map((c) => _CategoryRow(categoryId: c.id)),
|
|
const Divider(height: 1),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.only(left: 8, bottom: 4),
|
|
child: Card(
|
|
margin: EdgeInsets.zero,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(category.name, style: Theme.of(context).textTheme.bodyMedium),
|
|
),
|
|
Text(
|
|
formatCurrency(remaining),
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
color: remaining >= 0 ? kGreenAccent : kRedAccent,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: LinearProgressIndicator(
|
|
value: pct,
|
|
backgroundColor: kDivider,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
remaining >= 0 ? kGreenAccent : kRedAccent,
|
|
),
|
|
minHeight: 4,
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Row(
|
|
children: [
|
|
Text('Budget: ${formatCurrency(budgeted)}',
|
|
style: Theme.of(context).textTheme.bodySmall),
|
|
const SizedBox(width: 12),
|
|
Text('Dépensé: ${formatCurrency(spent.abs())}',
|
|
style: Theme.of(context).textTheme.bodySmall),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ExpandedGroupNotifier extends StateNotifier<bool> {
|
|
_ExpandedGroupNotifier() : super(true);
|
|
|
|
void toggle() => state = !state;
|
|
}
|
|
|
|
final _expandedGroupProvider =
|
|
StateNotifierProvider.family<_ExpandedGroupNotifier, bool, int>(
|
|
(ref, id) => _ExpandedGroupNotifier());
|