Compare commits
2 Commits
9b08b2896c
...
7ed90db7a8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ed90db7a8 | ||
|
|
d305364328 |
@@ -35,6 +35,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"firestore": {
|
"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 '../../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(
|
||||||
|
|||||||
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