Compare commits
2 Commits
9b08b2896c
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ed90db7a8 | ||
|
|
d305364328 |
@@ -35,6 +35,7 @@
|
||||
}
|
||||
],
|
||||
"firestore": {
|
||||
"rules": "firestore.rules"
|
||||
"rules": "firestore.rules",
|
||||
"indexes": "firestore.indexes.json"
|
||||
}
|
||||
}
|
||||
|
||||
31
firestore.indexes.json
Normal file
31
firestore.indexes.json
Normal 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": []
|
||||
}
|
||||
@@ -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<HomeContent>
|
||||
/// 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<HomeContent>
|
||||
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,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
|
||||
Future<void> _showTripDetails(Trip trip) async {
|
||||
final result = await Navigator.push(
|
||||
|
||||
55
lib/services/trip_end_service.dart
Normal file
55
lib/services/trip_end_service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user