diff --git a/firebase.json b/firebase.json index cc9b4c1..7214b5b 100644 --- a/firebase.json +++ b/firebase.json @@ -35,6 +35,7 @@ } ], "firestore": { - "rules": "firestore.rules" + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" } } diff --git a/firestore.indexes.json b/firestore.indexes.json new file mode 100644 index 0000000..73a5af6 --- /dev/null +++ b/firestore.indexes.json @@ -0,0 +1,31 @@ +{ + "indexes": [ + { + "collectionGroup": "tripInvitations", + "queryScope": "COLLECTION", + "fields": [ + { "fieldPath": "inviteeId", "order": "ASCENDING" }, + { "fieldPath": "status", "order": "ASCENDING" }, + { "fieldPath": "createdAt", "order": "DESCENDING" } + ] + }, + { + "collectionGroup": "tripInvitations", + "queryScope": "COLLECTION", + "fields": [ + { "fieldPath": "inviteeId", "order": "ASCENDING" }, + { "fieldPath": "createdAt", "order": "DESCENDING" } + ] + }, + { + "collectionGroup": "tripInvitations", + "queryScope": "COLLECTION", + "fields": [ + { "fieldPath": "tripId", "order": "ASCENDING" }, + { "fieldPath": "inviteeId", "order": "ASCENDING" }, + { "fieldPath": "status", "order": "ASCENDING" } + ] + } + ], + "fieldOverrides": [] +} diff --git a/lib/components/home/home_content.dart b/lib/components/home/home_content.dart index 554b008..30bf01c 100644 --- a/lib/components/home/home_content.dart +++ b/lib/components/home/home_content.dart @@ -12,6 +12,7 @@ import '../../blocs/trip/trip_state.dart'; import '../../blocs/trip/trip_event.dart'; import '../../models/trip.dart'; import '../../services/error_service.dart'; +import '../../services/trip_end_service.dart'; /// Home content widget for the main application dashboard. /// @@ -45,6 +46,12 @@ class _HomeContentState extends State /// Service pour charger les images manquantes final TripImageService _tripImageService = TripImageService(); + /// Service pour détecter les voyages terminés + final TripEndService _tripEndService = TripEndService(); + + // ignore: prefer_final_fields + bool _hasCheckedFinishedTrips = false; + @override void initState() { super.initState(); @@ -91,6 +98,9 @@ class _HomeContentState extends State message: 'Voyage en cours de création...', isError: false, ); + } else if (tripState is TripLoaded && !_hasCheckedFinishedTrips) { + _hasCheckedFinishedTrips = true; + _checkFinishedTrips(tripState.trips); } }, builder: (context, tripState) { @@ -257,6 +267,61 @@ class _HomeContentState extends State }); } + /// Vérifie les voyages terminés et affiche le dialog de suppression si besoin. + Future _checkFinishedTrips(List trips) async { + final finished = await _tripEndService.getFinishedTripsNotYetPrompted(trips); + for (final trip in finished) { + if (!mounted) return; + await _showTripEndDialog(trip); + } + } + + /// Affiche le dialog demandant si l'utilisateur veut supprimer le voyage terminé. + Future _showTripEndDialog(Trip trip) async { + final tripId = trip.id!; + await _tripEndService.markTripAsPrompted(tripId); + + final settled = await _tripEndService.areAccountsSettled(tripId); + if (!mounted) return; + + final confirmed = await showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + title: const Text('Voyage terminé'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Le voyage "${trip.title}" est terminé.'), + const SizedBox(height: 12), + if (settled) + const Text('Tous les comptes sont réglés. Voulez-vous supprimer ce voyage ?') + else + const Text( + '⚠️ Des comptes ne sont pas encore réglés. Voulez-vous quand même supprimer ce voyage ?', + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('Non, garder'), + ), + TextButton( + onPressed: () => Navigator.pop(ctx, true), + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text('Supprimer'), + ), + ], + ), + ); + + if (confirmed == true && mounted) { + context.read().add(TripDeleteRequested(tripId: tripId)); + } + } + /// Navigate to trip details page Future _showTripDetails(Trip trip) async { final result = await Navigator.push( diff --git a/lib/services/trip_end_service.dart b/lib/services/trip_end_service.dart new file mode 100644 index 0000000..235efb9 --- /dev/null +++ b/lib/services/trip_end_service.dart @@ -0,0 +1,55 @@ +import 'package:shared_preferences/shared_preferences.dart'; +import '../models/trip.dart'; +import '../repositories/account_repository.dart'; +import '../repositories/balance_repository.dart'; +import '../repositories/expense_repository.dart'; +import 'logger_service.dart'; + +/// Service qui détecte les voyages terminés et vérifie si les comptes sont réglés. +class TripEndService { + static const String _prefixKey = 'trip_end_prompted_'; + + final AccountRepository _accountRepository; + final BalanceRepository _balanceRepository; + + TripEndService() + : _accountRepository = AccountRepository(), + _balanceRepository = BalanceRepository( + expenseRepository: ExpenseRepository(), + ); + + /// Retourne les voyages terminés pour lesquels l'utilisateur n'a pas encore été invité à supprimer. + Future> getFinishedTripsNotYetPrompted(List trips) async { + final now = DateTime.now(); + final prefs = await SharedPreferences.getInstance(); + + return trips.where((trip) { + if (trip.id == null) return false; + final isFinished = trip.endDate.isBefore(now); + final alreadyPrompted = prefs.getBool('$_prefixKey${trip.id}') ?? false; + return isFinished && !alreadyPrompted; + }).toList(); + } + + /// Marque un voyage comme déjà invité (ne plus afficher le dialog). + Future markTripAsPrompted(String tripId) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('$_prefixKey$tripId', true); + } + + /// Vérifie si tous les comptes du voyage sont réglés. + /// + /// Retourne `true` si aucune dépense n'est enregistrée ou si tous les soldes sont nuls. + Future areAccountsSettled(String tripId) async { + try { + final account = await _accountRepository.getAccountByTripId(tripId); + if (account == null) return true; + + final balance = await _balanceRepository.calculateGroupBalance(account.id); + return balance.settlements.isEmpty; + } catch (e) { + LoggerService.error('TripEndService: Erreur vérification comptes: $e'); + return true; + } + } +}