Compare commits

..

2 Commits

Author SHA1 Message Date
Van Leemput Dayron
7ed90db7a8 feat: Enhance trip end dialog to check trip creator and prompt accordingly 2026-03-20 10:37:04 +01:00
Van Leemput Dayron
d305364328 feat: Add TripEndService to manage finished trips and prompt for deletion 2026-03-20 10:17:12 +01:00
4 changed files with 180 additions and 1 deletions

View File

@@ -35,6 +35,7 @@
} }
], ],
"firestore": { "firestore": {
"rules": "firestore.rules" "rules": "firestore.rules",
"indexes": "firestore.indexes.json"
} }
} }

31
firestore.indexes.json Normal file
View File

@@ -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": []
}

View File

@@ -12,6 +12,7 @@ import '../../blocs/trip/trip_state.dart';
import '../../blocs/trip/trip_event.dart'; import '../../blocs/trip/trip_event.dart';
import '../../models/trip.dart'; import '../../models/trip.dart';
import '../../services/error_service.dart'; import '../../services/error_service.dart';
import '../../services/trip_end_service.dart';
/// Home content widget for the main application dashboard. /// Home content widget for the main application dashboard.
/// ///
@@ -45,6 +46,12 @@ class _HomeContentState extends State<HomeContent>
/// Service pour charger les images manquantes /// Service pour charger les images manquantes
final TripImageService _tripImageService = TripImageService(); final TripImageService _tripImageService = TripImageService();
/// Service pour détecter les voyages terminés
final TripEndService _tripEndService = TripEndService();
// ignore: prefer_final_fields
bool _hasCheckedFinishedTrips = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -91,6 +98,9 @@ class _HomeContentState extends State<HomeContent>
message: 'Voyage en cours de création...', message: 'Voyage en cours de création...',
isError: false, isError: false,
); );
} else if (tripState is TripLoaded && !_hasCheckedFinishedTrips) {
_hasCheckedFinishedTrips = true;
_checkFinishedTrips(tripState.trips);
} }
}, },
builder: (context, tripState) { builder: (context, tripState) {
@@ -257,6 +267,88 @@ class _HomeContentState extends State<HomeContent>
}); });
} }
/// Vérifie les voyages terminés et affiche le dialog de suppression si besoin.
Future<void> _checkFinishedTrips(List<Trip> trips) async {
final userState = context.read<UserBloc>().state;
if (userState is! UserLoaded) return;
final currentUserId = userState.user.id;
final finished = await _tripEndService.getFinishedTripsNotYetPrompted(trips);
for (final trip in finished) {
if (!mounted) return;
await _showTripEndDialog(trip, currentUserId);
}
}
/// Affiche le dialog demandant si l'utilisateur veut supprimer le voyage terminé.
Future<void> _showTripEndDialog(Trip trip, String currentUserId) async {
final tripId = trip.id!;
await _tripEndService.markTripAsPrompted(tripId);
final isCreator = trip.createdBy == currentUserId;
// Si l'utilisateur n'est pas le créateur, afficher uniquement une info
if (!isCreator) {
if (!mounted) return;
await showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Voyage terminé'),
content: Text(
'Le voyage "${trip.title}" est terminé. Seul le créateur du voyage peut le supprimer.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('OK'),
),
],
),
);
return;
}
final settled = await _tripEndService.areAccountsSettled(tripId);
if (!mounted) return;
final confirmed = await showDialog<bool>(
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<TripBloc>().add(TripDeleteRequested(tripId: tripId));
}
}
/// Navigate to trip details page /// Navigate to trip details page
Future<void> _showTripDetails(Trip trip) async { Future<void> _showTripDetails(Trip trip) async {
final result = await Navigator.push( final result = await Navigator.push(

View File

@@ -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<List<Trip>> getFinishedTripsNotYetPrompted(List<Trip> 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<void> 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<bool> 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;
}
}
}