- Added TripInvitationRepository for managing trip invitations. - Created TripInvitation model with serialization methods. - Implemented notification payload parser for handling FCM notifications. - Enhanced NotificationService to manage trip invitations and related actions. - Updated UserRepository to include user search functionality. - Modified AuthRepository to store multiple FCM tokens. - Added tests for trip invitation logic and notification payload parsing. - Updated pubspec.yaml and pubspec.lock for dependency management.
275 lines
8.6 KiB
Dart
275 lines
8.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:travel_mate/blocs/user/user_bloc.dart';
|
|
import 'package:travel_mate/blocs/user/user_state.dart' as user_state;
|
|
import 'package:travel_mate/models/trip_invitation.dart';
|
|
import 'package:travel_mate/repositories/trip_invitation_repository.dart';
|
|
import 'package:travel_mate/services/error_service.dart';
|
|
|
|
/// Affiche la boîte de réception des invitations de voyage.
|
|
///
|
|
/// Cette page permet de consulter toutes les invitations reçues et de répondre
|
|
/// aux invitations en attente (`pending`) via les actions accepter/refuser.
|
|
class TripInvitationsPage extends StatelessWidget {
|
|
/// Repository utilisé pour charger et mettre à jour les invitations.
|
|
final TripInvitationDataSource repository;
|
|
|
|
/// Identifiant utilisateur injecté pour les tests ou contextes spécifiques.
|
|
final String? userIdOverride;
|
|
|
|
/// Crée la page des invitations.
|
|
///
|
|
/// [repository] peut être injecté pour les tests; sinon un repository réel est utilisé.
|
|
TripInvitationsPage({
|
|
super.key,
|
|
TripInvitationDataSource? repository,
|
|
this.userIdOverride,
|
|
}) : repository = repository ?? TripInvitationRepository();
|
|
|
|
/// Retourne les invitations correspondant au [status] demandé.
|
|
///
|
|
/// [status] peut valoir `pending`, `accepted` ou `rejected`.
|
|
List<TripInvitation> _filterByStatus(
|
|
List<TripInvitation> invitations,
|
|
String status,
|
|
) {
|
|
return invitations
|
|
.where((invitation) => invitation.status == status)
|
|
.toList(growable: false);
|
|
}
|
|
|
|
/// Envoie la réponse utilisateur pour une invitation.
|
|
///
|
|
/// [isAccepted] à `true` accepte l'invitation, sinon la refuse.
|
|
/// Un feedback visuel est affiché à l'utilisateur.
|
|
Future<void> _respondToInvitation({
|
|
required BuildContext context,
|
|
required String invitationId,
|
|
required bool isAccepted,
|
|
}) async {
|
|
try {
|
|
await repository.respondToInvitation(
|
|
invitationId: invitationId,
|
|
isAccepted: isAccepted,
|
|
);
|
|
|
|
if (!context.mounted) {
|
|
return;
|
|
}
|
|
|
|
ErrorService().showSnackbar(
|
|
message: isAccepted ? 'Invitation acceptée' : 'Invitation refusée',
|
|
isError: false,
|
|
);
|
|
} catch (e) {
|
|
if (!context.mounted) {
|
|
return;
|
|
}
|
|
|
|
ErrorService().showError(message: 'Erreur lors de la réponse: $e');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (userIdOverride != null && userIdOverride!.isNotEmpty) {
|
|
return _InvitationsScaffold(
|
|
userId: userIdOverride!,
|
|
repository: repository,
|
|
onRespond: ({required String invitationId, required bool isAccepted}) {
|
|
return _respondToInvitation(
|
|
context: context,
|
|
invitationId: invitationId,
|
|
isAccepted: isAccepted,
|
|
);
|
|
},
|
|
filterByStatus: _filterByStatus,
|
|
);
|
|
}
|
|
|
|
return BlocBuilder<UserBloc, user_state.UserState>(
|
|
builder: (context, state) {
|
|
if (state is! user_state.UserLoaded) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('Invitations')),
|
|
body: const Center(child: Text('Utilisateur non chargé')),
|
|
);
|
|
}
|
|
|
|
return _InvitationsScaffold(
|
|
userId: state.user.id,
|
|
repository: repository,
|
|
onRespond:
|
|
({required String invitationId, required bool isAccepted}) {
|
|
return _respondToInvitation(
|
|
context: context,
|
|
invitationId: invitationId,
|
|
isAccepted: isAccepted,
|
|
);
|
|
},
|
|
filterByStatus: _filterByStatus,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Échafaudage principal des invitations avec onglets par statut.
|
|
class _InvitationsScaffold extends StatelessWidget {
|
|
/// Identifiant utilisateur des invitations à afficher.
|
|
final String userId;
|
|
|
|
/// Source de données des invitations.
|
|
final TripInvitationDataSource repository;
|
|
|
|
/// Callback appelé lors d'une réponse utilisateur.
|
|
final Future<void> Function({
|
|
required String invitationId,
|
|
required bool isAccepted,
|
|
})
|
|
onRespond;
|
|
|
|
/// Fonction de filtrage par statut.
|
|
final List<TripInvitation> Function(
|
|
List<TripInvitation> invitations,
|
|
String status,
|
|
)
|
|
filterByStatus;
|
|
|
|
/// Crée l'échafaudage d'affichage des invitations.
|
|
const _InvitationsScaffold({
|
|
required this.userId,
|
|
required this.repository,
|
|
required this.onRespond,
|
|
required this.filterByStatus,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return DefaultTabController(
|
|
length: 3,
|
|
child: Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Invitations'),
|
|
bottom: const TabBar(
|
|
tabs: [
|
|
Tab(text: 'En attente'),
|
|
Tab(text: 'Acceptées'),
|
|
Tab(text: 'Refusées'),
|
|
],
|
|
),
|
|
),
|
|
body: StreamBuilder<List<TripInvitation>>(
|
|
stream: repository.watchInvitationsForUser(userId),
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
|
|
if (snapshot.hasError) {
|
|
return Center(
|
|
child: Text('Erreur de chargement: ${snapshot.error}'),
|
|
);
|
|
}
|
|
|
|
final invitations = snapshot.data ?? const <TripInvitation>[];
|
|
final pending = filterByStatus(invitations, 'pending');
|
|
final accepted = filterByStatus(invitations, 'accepted');
|
|
final rejected = filterByStatus(invitations, 'rejected');
|
|
|
|
return TabBarView(
|
|
children: [
|
|
_InvitationsList(
|
|
invitations: pending,
|
|
onAccept: (id) =>
|
|
onRespond(invitationId: id, isAccepted: true),
|
|
onReject: (id) =>
|
|
onRespond(invitationId: id, isAccepted: false),
|
|
),
|
|
_InvitationsList(invitations: accepted),
|
|
_InvitationsList(invitations: rejected),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Affiche une liste d'invitations avec actions éventuelles.
|
|
class _InvitationsList extends StatelessWidget {
|
|
/// Invitations à afficher.
|
|
final List<TripInvitation> invitations;
|
|
|
|
/// Callback appelé lors d'une acceptation.
|
|
final Future<void> Function(String invitationId)? onAccept;
|
|
|
|
/// Callback appelé lors d'un refus.
|
|
final Future<void> Function(String invitationId)? onReject;
|
|
|
|
/// Crée une liste d'invitations.
|
|
const _InvitationsList({
|
|
required this.invitations,
|
|
this.onAccept,
|
|
this.onReject,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (invitations.isEmpty) {
|
|
return const Center(child: Text('Aucune invitation'));
|
|
}
|
|
|
|
return ListView.separated(
|
|
padding: const EdgeInsets.all(12),
|
|
itemCount: invitations.length,
|
|
separatorBuilder: (context, _) => const SizedBox(height: 8),
|
|
itemBuilder: (context, index) {
|
|
final invitation = invitations[index];
|
|
final isPending = invitation.status == 'pending';
|
|
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
invitation.tripTitle,
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text('Invité par: ${invitation.inviterName}'),
|
|
Text(
|
|
'Reçu: ${invitation.createdAt.day}/${invitation.createdAt.month}/${invitation.createdAt.year}',
|
|
),
|
|
if (isPending) ...[
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
OutlinedButton(
|
|
onPressed: onReject == null
|
|
? null
|
|
: () => onReject!(invitation.id),
|
|
child: const Text('Refuser'),
|
|
),
|
|
const SizedBox(width: 8),
|
|
ElevatedButton(
|
|
onPressed: onAccept == null
|
|
? null
|
|
: () => onAccept!(invitation.id),
|
|
child: const Text('Accepter'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|