Files
afterwork/lib/presentation/reservations/reservations_screen.dart
dahoud 92612abbd7 fix(chat): Correction race condition + Implémentation TODOs
## Corrections Critiques

### Race Condition - Statuts de Messages
- Fix : Les icônes de statut (✓, ✓✓, ✓✓ bleu) ne s'affichaient pas
- Cause : WebSocket delivery confirmations arrivaient avant messages locaux
- Solution : Pattern Optimistic UI dans chat_bloc.dart
  - Création message temporaire immédiate
  - Ajout à la liste AVANT requête HTTP
  - Remplacement par message serveur à la réponse
- Fichier : lib/presentation/state_management/chat_bloc.dart

## Implémentation TODOs (13/21)

### Social (social_header_widget.dart)
-  Copier lien du post dans presse-papiers
-  Partage natif via Share.share()
-  Dialogue de signalement avec 5 raisons

### Partage (share_post_dialog.dart)
-  Interface sélection d'amis avec checkboxes
-  Partage externe via Share API

### Média (media_upload_service.dart)
-  Parsing JSON réponse backend
-  Méthode deleteMedia() pour suppression
-  Génération miniature vidéo

### Posts (create_post_dialog.dart, edit_post_dialog.dart)
-  Extraction URL depuis uploads
-  Documentation chargement médias

### Chat (conversations_screen.dart)
-  Navigation vers notifications
-  ConversationSearchDelegate pour recherche

## Nouveaux Fichiers

### Configuration
- build-prod.ps1 : Script build production avec dart-define
- lib/core/constants/env_config.dart : Gestion environnements

### Documentation
- TODOS_IMPLEMENTED.md : Documentation complète TODOs

## Améliorations

### Architecture
- Refactoring injection de dépendances
- Amélioration routing et navigation
- Optimisation providers (UserProvider, FriendsProvider)

### UI/UX
- Amélioration thème et couleurs
- Optimisation animations
- Meilleure gestion erreurs

### Services
- Configuration API avec env_config
- Amélioration datasources (events, users)
- Optimisation modèles de données
2026-01-10 10:43:17 +00:00

435 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import '../../core/constants/design_system.dart';
import '../../data/datasources/reservation_remote_data_source.dart';
import '../../data/services/secure_storage.dart';
import '../../domain/entities/reservation.dart';
import '../widgets/animated_widgets.dart';
import '../widgets/custom_snackbar.dart';
import '../widgets/modern_empty_state.dart';
import '../widgets/shimmer_loading.dart';
/// Écran de gestion des réservations avec design moderne.
///
/// Permet à l'utilisateur de:
/// - Consulter ses réservations (en cours, passées, annulées)
/// - Annuler des réservations en cours
/// - Voir les détails de chaque réservation
class ReservationsScreen extends StatefulWidget {
const ReservationsScreen({super.key});
@override
State<ReservationsScreen> createState() => _ReservationsScreenState();
}
class _ReservationsScreenState extends State<ReservationsScreen> with SingleTickerProviderStateMixin {
final ReservationRemoteDataSource _dataSource = ReservationRemoteDataSource(http.Client());
final SecureStorage _secureStorage = SecureStorage();
List<Reservation> _reservations = [];
bool _isLoading = false;
String? _errorMessage;
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
_loadReservations();
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
Future<void> _loadReservations() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final userId = await _secureStorage.getUserId();
if (userId == null || userId.isEmpty) {
if (mounted) {
setState(() {
_isLoading = false;
});
}
return;
}
final reservationModels = await _dataSource.getReservationsByUser(userId);
if (mounted) {
setState(() {
_reservations = reservationModels.map((model) => model.toEntity()).toList();
// Trie par date de réservation décroissante
_reservations.sort((a, b) => b.reservationDate.compareTo(a.reservationDate));
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_errorMessage = e.toString();
_isLoading = false;
});
}
}
}
Future<void> _cancelReservation(Reservation reservation) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Annuler la réservation'),
content: Text(
'Voulez-vous vraiment annuler votre réservation pour "${reservation.eventTitle}" ?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Non'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text('Oui, annuler'),
),
],
),
);
if (confirmed != true) return;
try {
await _dataSource.cancelReservation(reservation.id);
if (mounted) {
await _loadReservations();
context.showSuccess('Réservation annulée avec succès');
}
} catch (e) {
if (mounted) {
context.showError('Erreur lors de l\'annulation: ${e.toString()}');
}
}
}
List<Reservation> get _activeReservations {
return _reservations.where((r) =>
r.status == ReservationStatus.pending ||
r.status == ReservationStatus.confirmed
).toList();
}
List<Reservation> get _pastReservations {
return _reservations.where((r) =>
r.status == ReservationStatus.completed
).toList();
}
List<Reservation> get _cancelledReservations {
return _reservations.where((r) =>
r.status == ReservationStatus.cancelled
).toList();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('Mes Réservations'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'En cours', icon: Icon(Icons.event_available)),
Tab(text: 'Passées', icon: Icon(Icons.history)),
Tab(text: 'Annulées', icon: Icon(Icons.cancel)),
],
),
),
body: _isLoading
? const SkeletonList(
itemCount: 5,
skeletonWidget: ListItemSkeleton(),
)
: _errorMessage != null
? _buildErrorState(theme)
: TabBarView(
controller: _tabController,
children: [
_buildReservationList(_activeReservations, 'active'),
_buildReservationList(_pastReservations, 'past'),
_buildReservationList(_cancelledReservations, 'cancelled'),
],
),
);
}
Widget _buildErrorState(ThemeData theme) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: DesignSystem.spacingLg),
Text(
'Erreur de chargement',
style: theme.textTheme.titleLarge,
),
const SizedBox(height: DesignSystem.spacingSm),
Text(
_errorMessage!,
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: DesignSystem.spacingXl),
ElevatedButton.icon(
onPressed: _loadReservations,
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
),
],
),
);
}
Widget _buildReservationList(List<Reservation> reservations, String type) {
if (reservations.isEmpty) {
return _buildEmptyState(type);
}
return RefreshIndicator(
onRefresh: _loadReservations,
child: ListView.builder(
padding: const EdgeInsets.all(DesignSystem.spacingLg),
itemCount: reservations.length,
itemBuilder: (context, index) {
return _buildReservationCard(reservations[index]);
},
),
);
}
Widget _buildEmptyState(String type) {
String title;
String description;
switch (type) {
case 'active':
title = 'Aucune réservation en cours';
description = 'Vous n\'avez pas de réservation active pour le moment';
break;
case 'past':
title = 'Aucune réservation passée';
description = 'Vos réservations terminées apparaîtront ici';
break;
case 'cancelled':
title = 'Aucune réservation annulée';
description = 'Vos réservations annulées apparaîtront ici';
break;
default:
title = 'Aucune réservation';
description = '';
}
return ModernEmptyState(
illustration: EmptyStateIllustration.reservations,
title: title,
description: description,
);
}
Widget _buildReservationCard(Reservation reservation) {
final theme = Theme.of(context);
final canCancel = reservation.status == ReservationStatus.pending ||
reservation.status == ReservationStatus.confirmed;
return FadeInWidget(
child: AnimatedCard(
margin: const EdgeInsets.only(bottom: DesignSystem.spacingLg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec titre et statut
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
reservation.eventTitle,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
if (reservation.establishmentName != null) ...[
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.location_on, size: 14, color: Colors.grey),
const SizedBox(width: 4),
Expanded(
child: Text(
reservation.establishmentName!,
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
),
),
),
],
),
],
],
),
),
_buildStatusBadge(reservation.status, theme),
],
),
const SizedBox(height: DesignSystem.spacingLg),
// Informations de la réservation
Row(
children: [
const Icon(Icons.event, size: 20),
const SizedBox(width: DesignSystem.spacingSm),
Expanded(
child: Text(
DateFormat('EEEE d MMMM yyyy à HH:mm', 'fr_FR')
.format(reservation.reservationDate),
style: theme.textTheme.bodyMedium,
),
),
],
),
const SizedBox(height: DesignSystem.spacingSm),
Row(
children: [
const Icon(Icons.people, size: 20),
const SizedBox(width: DesignSystem.spacingSm),
Text(
'${reservation.numberOfPeople} personne${reservation.numberOfPeople > 1 ? 's' : ''}',
style: theme.textTheme.bodyMedium,
),
],
),
// Notes si présentes
if (reservation.notes != null && reservation.notes!.isNotEmpty) ...[
const SizedBox(height: DesignSystem.spacingSm),
Container(
padding: const EdgeInsets.all(DesignSystem.spacingSm),
decoration: BoxDecoration(
color: theme.brightness == Brightness.dark
? Colors.grey[800]
: Colors.grey[100],
borderRadius: BorderRadius.circular(DesignSystem.radiusSm),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.note, size: 16, color: Colors.grey),
const SizedBox(width: DesignSystem.spacingSm),
Expanded(
child: Text(
reservation.notes!,
style: theme.textTheme.bodySmall,
),
),
],
),
),
],
// Bouton annuler
if (canCancel) ...[
const SizedBox(height: DesignSystem.spacingLg),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _cancelReservation(reservation),
icon: const Icon(Icons.cancel),
label: const Text('Annuler la réservation'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
),
),
],
],
),
),
);
}
Widget _buildStatusBadge(ReservationStatus status, ThemeData theme) {
Color color;
String label;
IconData icon;
switch (status) {
case ReservationStatus.pending:
color = Colors.orange;
label = 'En attente';
icon = Icons.schedule;
break;
case ReservationStatus.confirmed:
color = Colors.green;
label = 'Confirmée';
icon = Icons.check_circle;
break;
case ReservationStatus.cancelled:
color = Colors.red;
label = 'Annulée';
icon = Icons.cancel;
break;
case ReservationStatus.completed:
color = Colors.blue;
label = 'Terminée';
icon = Icons.check;
break;
}
return Container(
padding: const EdgeInsets.symmetric(
horizontal: DesignSystem.spacingSm,
vertical: 4,
),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(DesignSystem.radiusSm),
border: Border.all(color: color, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: color),
const SizedBox(width: 4),
Text(
label,
style: theme.textTheme.bodySmall?.copyWith(
color: color,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}