Files
unionflow-server-api/unionflow-mobile-apps/lib/features/events/presentation/pages/events_page.dart
2025-11-17 16:02:04 +00:00

1260 lines
42 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../authentication/presentation/bloc/auth_bloc.dart';
import '../../../../shared/design_system/tokens/color_tokens.dart';
/// Page de gestion des événements - Interface sophistiquée et exhaustive
///
/// Cette page offre une interface complète pour la gestion des événements
/// avec des fonctionnalités avancées de recherche, filtrage, statistiques,
/// vue calendrier et actions de gestion basées sur les permissions utilisateur.
class EventsPage extends StatefulWidget {
const EventsPage({super.key});
@override
State<EventsPage> createState() => _EventsPageState();
}
class _EventsPageState extends State<EventsPage> with TickerProviderStateMixin {
// Controllers et état
final TextEditingController _searchController = TextEditingController();
late TabController _tabController;
// État de l'interface
String _searchQuery = '';
String _selectedFilter = 'Tous';
// Données de démonstration enrichies
final List<Map<String, dynamic>> _allEvents = [
{
'id': '1',
'title': 'Assemblée Générale Annuelle 2024',
'description': 'Assemblée générale ordinaire avec présentation du bilan annuel, vote du budget et élection du bureau.',
'startDate': DateTime(2024, 10, 15, 14, 0),
'endDate': DateTime(2024, 10, 15, 17, 0),
'location': 'Salle des fêtes municipale',
'address': '12 Place de la Mairie, 75001 Paris',
'type': 'Officiel',
'status': 'Confirmé',
'maxParticipants': 100,
'currentParticipants': 67,
'organizer': 'Bureau Exécutif',
'priority': 'Haute',
'isPublic': true,
'requiresRegistration': true,
'cost': 0.0,
'tags': ['AG', 'Obligatoire', 'Annuel'],
'createdBy': 'Marie Dubois',
'createdAt': DateTime(2024, 8, 1),
'lastModified': DateTime(2024, 9, 15),
},
{
'id': '2',
'title': 'Sortie Ski de Fond - Les Rousses',
'description': 'Sortie ski de fond dans le Jura. Matériel fourni, tous niveaux acceptés. Repas chaud inclus.',
'startDate': DateTime(2024, 12, 22, 9, 0),
'endDate': DateTime(2024, 12, 22, 17, 0),
'location': 'Station des Rousses',
'address': 'Les Rousses, 39220 Jura',
'type': 'Loisir',
'status': 'En attente',
'maxParticipants': 25,
'currentParticipants': 18,
'organizer': 'Commission Sports',
'priority': 'Moyenne',
'isPublic': true,
'requiresRegistration': true,
'cost': 35.0,
'tags': ['Sport', 'Hiver', 'Nature'],
'createdBy': 'Pierre Martin',
'createdAt': DateTime(2024, 9, 10),
'lastModified': DateTime(2024, 9, 18),
},
{
'id': '3',
'title': 'Formation Premiers Secours PSC1',
'description': 'Formation complète aux gestes de premiers secours. Certification officielle délivrée.',
'startDate': DateTime(2024, 11, 5, 9, 0),
'endDate': DateTime(2024, 11, 5, 17, 0),
'location': 'Centre de Formation',
'address': '45 Avenue des Formations, 75015 Paris',
'type': 'Formation',
'status': 'Confirmé',
'maxParticipants': 12,
'currentParticipants': 10,
'organizer': 'Commission Formation',
'priority': 'Haute',
'isPublic': false,
'requiresRegistration': true,
'cost': 60.0,
'tags': ['Formation', 'Sécurité', 'Certification'],
'createdBy': 'Sophie Laurent',
'createdAt': DateTime(2024, 8, 20),
'lastModified': DateTime(2024, 9, 12),
},
{
'id': '4',
'title': 'Réunion Bureau Mensuelle',
'description': 'Réunion mensuelle du bureau pour faire le point sur les activités et prendre les décisions courantes.',
'startDate': DateTime(2024, 10, 28, 19, 30),
'endDate': DateTime(2024, 10, 28, 21, 30),
'location': 'Mairie - Salle du Conseil',
'address': '1 Place de la République, 75001 Paris',
'type': 'Administratif',
'status': 'Confirmé',
'maxParticipants': 15,
'currentParticipants': 12,
'organizer': 'Président',
'priority': 'Moyenne',
'isPublic': false,
'requiresRegistration': false,
'cost': 0.0,
'tags': ['Bureau', 'Mensuel', 'Décisions'],
'createdBy': 'Thomas Durand',
'createdAt': DateTime(2024, 9, 1),
'lastModified': DateTime(2024, 9, 20),
},
{
'id': '5',
'title': 'Soirée Galette des Rois',
'description': 'Soirée conviviale avec dégustation de galettes, animations et tirage des rois et reines.',
'startDate': DateTime(2024, 1, 13, 19, 0),
'endDate': DateTime(2024, 1, 13, 23, 0),
'location': 'Salle Communale',
'address': '8 Rue de la Convivialité, 75012 Paris',
'type': 'Social',
'status': 'Terminé',
'maxParticipants': 50,
'currentParticipants': 42,
'organizer': 'Commission Festivités',
'priority': 'Basse',
'isPublic': true,
'requiresRegistration': true,
'cost': 12.0,
'tags': ['Social', 'Tradition', 'Convivialité'],
'createdBy': 'Emma Rousseau',
'createdAt': DateTime(2023, 12, 1),
'lastModified': DateTime(2024, 1, 10),
},
{
'id': '6',
'title': 'Conférence Développement Durable',
'description': 'Conférence sur les enjeux du développement durable avec experts et table ronde.',
'startDate': DateTime(2024, 11, 20, 18, 30),
'endDate': DateTime(2024, 11, 20, 21, 0),
'location': 'Amphithéâtre Universitaire',
'address': '123 Boulevard de la Connaissance, 75013 Paris',
'type': 'Culturel',
'status': 'Confirmé',
'maxParticipants': 200,
'currentParticipants': 89,
'organizer': 'Commission Culture',
'priority': 'Moyenne',
'isPublic': true,
'requiresRegistration': true,
'cost': 5.0,
'tags': ['Conférence', 'Environnement', 'Éducation'],
'createdBy': 'Lucas Bernard',
'createdAt': DateTime(2024, 9, 5),
'lastModified': DateTime(2024, 9, 19),
},
{
'id': '7',
'title': 'Atelier Cuisine Collaborative',
'description': 'Atelier de cuisine collaborative avec préparation d\'un repas complet et dégustation.',
'startDate': DateTime(2024, 10, 25, 18, 0),
'endDate': DateTime(2024, 10, 25, 22, 0),
'location': 'Cuisine Pédagogique',
'address': '67 Rue des Saveurs, 75011 Paris',
'type': 'Loisir',
'status': 'En cours',
'maxParticipants': 16,
'currentParticipants': 14,
'organizer': 'Commission Loisirs',
'priority': 'Basse',
'isPublic': true,
'requiresRegistration': true,
'cost': 25.0,
'tags': ['Cuisine', 'Créatif', 'Partage'],
'createdBy': 'Camille Moreau',
'createdAt': DateTime(2024, 9, 8),
'lastModified': DateTime(2024, 10, 20),
},
{
'id': '8',
'title': 'Randonnée Forêt de Fontainebleau',
'description': 'Randonnée découverte de 12km en forêt de Fontainebleau avec guide naturaliste.',
'startDate': DateTime(2024, 11, 10, 9, 30),
'endDate': DateTime(2024, 11, 10, 16, 0),
'location': 'Forêt de Fontainebleau',
'address': 'Parking Carrefour de la Croix du Grand Maître, 77300 Fontainebleau',
'type': 'Sport',
'status': 'Annulé',
'maxParticipants': 20,
'currentParticipants': 8,
'organizer': 'Commission Nature',
'priority': 'Basse',
'isPublic': true,
'requiresRegistration': true,
'cost': 8.0,
'tags': ['Randonnée', 'Nature', 'Découverte'],
'createdBy': 'Marie Dubois',
'createdAt': DateTime(2024, 8, 25),
'lastModified': DateTime(2024, 10, 15),
},
];
@override
void initState() {
super.initState();
_tabController = TabController(length: 5, vsync: this);
}
@override
Widget build(BuildContext context) {
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is! AuthAuthenticated) {
return Container(
color: const Color(0xFFF8F9FA),
child: const Center(child: CircularProgressIndicator()),
);
}
return Container(
color: const Color(0xFFF8F9FA),
child: Column(
children: [
// Métriques et statistiques
_buildEventMetrics(),
// Barre de recherche et filtres
_buildSearchAndFilters(),
// Navigation par onglets
_buildTabBar(),
// Contenu des onglets
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildAllEventsView(),
_buildUpcomingEventsView(),
_buildOngoingEventsView(),
_buildPastEventsView(),
_buildCalendarView(),
],
),
),
],
),
);
},
);
}
/// Métriques et statistiques des événements
Widget _buildEventMetrics() {
final now = DateTime.now();
final upcomingEvents = _allEvents.where((event) =>
(event['startDate'] as DateTime).isAfter(now) &&
event['status'] != 'Annulé'
).length;
final ongoingEvents = _allEvents.where((event) =>
event['status'] == 'En cours'
).length;
final totalParticipants = _allEvents.fold<int>(0, (sum, event) =>
sum + (event['currentParticipants'] as int)
);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: ColorTokens.secondary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.event, color: ColorTokens.secondary),
const SizedBox(width: 8),
const Text(
'Événements',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const Spacer(),
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Créer événement - Fonctionnalité à venir')),
);
},
tooltip: 'Créer un événement',
),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatCard('À Venir', upcomingEvents.toString(), ColorTokens.success),
_buildStatCard('En Cours', ongoingEvents.toString(), ColorTokens.info),
_buildStatCard('Participants', totalParticipants.toString(), ColorTokens.secondary),
],
),
],
),
);
}
Widget _buildStatCard(String label, String value, Color color) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Column(
children: [
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
color: color.withOpacity(0.8),
),
),
],
),
);
}
/// Barre de recherche et filtres
Widget _buildSearchAndFilters() {
return Container(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Barre de recherche simple
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: _searchController,
onChanged: (value) => setState(() => _searchQuery = value),
decoration: InputDecoration(
hintText: 'Rechercher un événement...',
hintStyle: const TextStyle(color: Color(0xFF9CA3AF), fontSize: 14),
prefixIcon: const Icon(Icons.search, color: Color(0xFF6B7280), size: 20),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
onPressed: () {
_searchController.clear();
setState(() => _searchQuery = '');
},
icon: const Icon(Icons.clear, color: Color(0xFF6B7280), size: 20),
)
: null,
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
),
),
),
const SizedBox(height: 8),
// Filtres rapides simplifiés
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_buildSimpleFilter('Tous', _selectedFilter == 'Tous'),
_buildSimpleFilter('À venir', _selectedFilter == 'À venir'),
_buildSimpleFilter('En cours', _selectedFilter == 'En cours'),
_buildSimpleFilter('Terminés', _selectedFilter == 'Terminés'),
],
),
),
],
),
);
}
/// Filtre simple aligné sur le design system
Widget _buildSimpleFilter(String label, bool isSelected) {
return Container(
margin: const EdgeInsets.only(right: 6),
child: InkWell(
onTap: () {
setState(() {
_selectedFilter = isSelected ? 'Tous' : label;
});
},
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF6C5CE7) : Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? const Color(0xFF6C5CE7) : const Color(0xFFE5E7EB),
width: 1,
),
),
child: Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : const Color(0xFF6B7280),
fontSize: 12,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
),
),
);
}
/// Navigation par onglets simplifiée
Widget _buildTabBar() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: TabBar(
controller: _tabController,
isScrollable: true,
labelColor: const Color(0xFF6C5CE7),
unselectedLabelColor: const Color(0xFF6B7280),
indicatorColor: const Color(0xFF6C5CE7),
indicatorSize: TabBarIndicatorSize.tab,
labelStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
unselectedLabelStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.normal),
tabs: const [
Tab(text: 'Tous'),
Tab(text: 'À Venir'),
Tab(text: 'En Cours'),
Tab(text: 'Passés'),
Tab(text: 'Calendrier'),
],
),
);
}
/// Vue de tous les événements
Widget _buildAllEventsView() {
final filteredEvents = _getFilteredEvents(_allEvents);
return _buildEventsListView(filteredEvents, 'all');
}
/// Vue des événements à venir
Widget _buildUpcomingEventsView() {
final now = DateTime.now();
final upcomingEvents = _allEvents.where((event) =>
(event['startDate'] as DateTime).isAfter(now) &&
event['status'] != 'Annulé'
).toList();
final filteredEvents = _getFilteredEvents(upcomingEvents);
return _buildEventsListView(filteredEvents, 'upcoming');
}
/// Vue des événements en cours
Widget _buildOngoingEventsView() {
final ongoingEvents = _allEvents.where((event) =>
event['status'] == 'En cours'
).toList();
final filteredEvents = _getFilteredEvents(ongoingEvents);
return _buildEventsListView(filteredEvents, 'ongoing');
}
/// Vue des événements passés
Widget _buildPastEventsView() {
final now = DateTime.now();
final pastEvents = _allEvents.where((event) =>
(event['startDate'] as DateTime).isBefore(now) &&
event['status'] == 'Terminé'
).toList();
final filteredEvents = _getFilteredEvents(pastEvents);
return _buildEventsListView(filteredEvents, 'past');
}
/// Vue calendrier
Widget _buildCalendarView() {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: const Color(0xFF6C5CE7).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Column(
children: [
Icon(
Icons.calendar_month,
size: 48,
color: Color(0xFF6C5CE7),
),
SizedBox(height: 16),
Text(
'Vue Calendrier',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF374151),
),
),
SizedBox(height: 8),
Text(
'La vue calendrier interactive sera bientôt disponible',
style: TextStyle(
color: Color(0xFF6B7280),
),
textAlign: TextAlign.center,
),
],
),
),
],
),
);
}
/// Filtre les événements selon les critères sélectionnés
List<Map<String, dynamic>> _getFilteredEvents(List<Map<String, dynamic>> events) {
var filtered = events.where((event) {
// Filtre par recherche textuelle
if (_searchQuery.isNotEmpty) {
final query = _searchQuery.toLowerCase();
final title = (event['title'] as String).toLowerCase();
final description = (event['description'] as String).toLowerCase();
final location = (event['location'] as String).toLowerCase();
if (!title.contains(query) &&
!description.contains(query) &&
!location.contains(query)) {
return false;
}
}
// Filtre par catégorie
if (_selectedFilter != 'Tous') {
switch (_selectedFilter) {
case 'À venir':
final now = DateTime.now();
if (!(event['startDate'] as DateTime).isAfter(now) || event['status'] == 'Annulé') {
return false;
}
break;
case 'En cours':
if (event['status'] != 'En cours') return false;
break;
case 'Terminés':
if (event['status'] != 'Terminé') return false;
break;
case 'Publics':
if (!(event['isPublic'] as bool)) return false;
break;
case 'Privés':
if (event['isPublic'] as bool) return false;
break;
}
}
return true;
}).toList();
// Tri par date par défaut
filtered.sort((a, b) => (a['startDate'] as DateTime).compareTo(b['startDate'] as DateTime));
return filtered;
}
/// Liste des événements avec gestion de l'état vide
Widget _buildEventsListView(List<Map<String, dynamic>> events, String type) {
if (events.isEmpty) {
return _buildEmptyState(type);
}
return ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: events.length,
itemBuilder: (context, index) {
final event = events[index];
return _buildSimpleEventCard(event);
},
);
}
/// Carte d'événement simple alignée sur le design system
Widget _buildSimpleEventCard(Map<String, dynamic> event) {
final startDate = event['startDate'] as DateTime;
final currentParticipants = event['currentParticipants'] as int;
final maxParticipants = event['maxParticipants'] as int;
return Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: InkWell(
onTap: () => _showEventDetails(event),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header avec titre et statut
Row(
children: [
Expanded(
child: Text(
event['title'],
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF374151),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: _getStatusColor(event['status']).withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
event['status'],
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: _getStatusColor(event['status']),
),
),
),
],
),
const SizedBox(height: 8),
// Informations principales
Row(
children: [
const Icon(
Icons.calendar_today,
size: 14,
color: Color(0xFF6B7280),
),
const SizedBox(width: 4),
Text(
_formatDate(startDate),
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6B7280),
),
),
const SizedBox(width: 12),
const Icon(
Icons.location_on,
size: 14,
color: Color(0xFF6B7280),
),
const SizedBox(width: 4),
Expanded(
child: Text(
event['location'],
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6B7280),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 8),
// Footer avec type et participants
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: _getTypeColor(event['type']).withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
event['type'],
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: _getTypeColor(event['type']),
),
),
),
const Spacer(),
const Icon(
Icons.people,
size: 14,
color: Color(0xFF6B7280),
),
const SizedBox(width: 4),
Text(
'$currentParticipants/$maxParticipants',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6B7280),
fontWeight: FontWeight.w500,
),
),
],
),
],
),
),
),
);
}
/// État vide selon le type d'événements
Widget _buildEmptyState(String type) {
String title;
String subtitle;
IconData icon;
switch (type) {
case 'upcoming':
title = 'Aucun événement à venir';
subtitle = 'Aucun événement n\'est programmé prochainement';
icon = Icons.event_available;
break;
case 'ongoing':
title = 'Aucun événement en cours';
subtitle = 'Aucun événement n\'est actuellement en cours';
icon = Icons.play_circle_filled;
break;
case 'past':
title = 'Aucun événement passé';
subtitle = 'Aucun événement terminé à afficher';
icon = Icons.event_busy;
break;
default:
title = 'Aucun événement trouvé';
subtitle = 'Aucun événement ne correspond aux critères sélectionnés';
icon = Icons.event_note;
}
return SizedBox(
height: 400,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: const Color(0xFF6C5CE7).withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
icon,
size: 48,
color: const Color(0xFF6C5CE7),
),
),
const SizedBox(height: 16),
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF374151),
),
),
const SizedBox(height: 8),
Text(
subtitle,
style: const TextStyle(
color: Color(0xFF6B7280),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
setState(() {
_searchController.clear();
_searchQuery = '';
_selectedFilter = 'Tous';
});
},
icon: const Icon(Icons.refresh),
label: const Text('Réinitialiser les filtres'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6C5CE7),
foregroundColor: Colors.white,
),
),
],
),
);
}
// ═══════════════════════════════════════════════════════════════════════════
// MÉTHODES UTILITAIRES ET HELPERS
// ═══════════════════════════════════════════════════════════════════════════
/// Couleur selon le statut de l'événement
Color _getStatusColor(String status) {
switch (status) {
case 'Confirmé':
return const Color(0xFF10B981);
case 'En attente':
return const Color(0xFFF59E0B);
case 'En cours':
return const Color(0xFF3B82F6);
case 'Terminé':
return const Color(0xFF6B7280);
case 'Annulé':
return const Color(0xFFEF4444);
default:
return const Color(0xFF6B7280);
}
}
/// Couleur selon le type d'événement
Color _getTypeColor(String type) {
switch (type) {
case 'Officiel':
return const Color(0xFF3B82F6);
case 'Loisir':
return const Color(0xFF10B981);
case 'Formation':
return const Color(0xFFF59E0B);
case 'Social':
return const Color(0xFF8B5CF6);
case 'Administratif':
return const Color(0xFFEF4444);
case 'Culturel':
return const Color(0xFF06B6D4);
case 'Sport':
return const Color(0xFF84CC16);
default:
return const Color(0xFF6B7280);
}
}
/// Formatage de la date
String _formatDate(DateTime date) {
final months = [
'Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun',
'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'
];
return '${date.day} ${months[date.month - 1]} ${date.year}';
}
/// Formatage de l'heure
String _formatTime(DateTime time) {
return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
}
// ═══════════════════════════════════════════════════════════════════════════
// ACTIONS ET INTERACTIONS
// ═══════════════════════════════════════════════════════════════════════════
/// Afficher les détails d'un événement
void _showEventDetails(Map<String, dynamic> event) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => _buildEventDetailsSheet(event),
);
}
/// Sheet de détails d'un événement
Widget _buildEventDetailsSheet(Map<String, dynamic> event) {
final startDate = event['startDate'] as DateTime;
final endDate = event['endDate'] as DateTime;
final currentParticipants = event['currentParticipants'] as int;
final maxParticipants = event['maxParticipants'] as int;
return DraggableScrollableSheet(
initialChildSize: 0.8,
minChildSize: 0.5,
maxChildSize: 0.95,
builder: (context, scrollController) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
// Handle
Container(
margin: const EdgeInsets.symmetric(vertical: 8),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
// Header
Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _getTypeColor(event['type']).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
_getEventIcon(event['type']),
color: _getTypeColor(event['type']),
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
event['title'],
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF374151),
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getStatusColor(event['status']).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
event['status'],
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: _getStatusColor(event['status']),
),
),
),
],
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
),
],
),
),
// Contenu détaillé
Expanded(
child: SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Description
_buildDetailSection(
'Description',
[
Text(
event['description'],
style: const TextStyle(
fontSize: 16,
color: Color(0xFF374151),
height: 1.5,
),
),
],
),
// Informations pratiques
_buildDetailSection(
'Informations Pratiques',
[
_buildDetailItem(Icons.calendar_today, 'Date et heure',
'${_formatDate(startDate)} de ${_formatTime(startDate)} à ${_formatTime(endDate)}'),
_buildDetailItem(Icons.location_on, 'Lieu', event['location']),
_buildDetailItem(Icons.place, 'Adresse', event['address']),
_buildDetailItem(Icons.person, 'Organisateur', event['organizer']),
if ((event['cost'] as double) > 0)
_buildDetailItem(Icons.euro, 'Coût', '${event['cost']}'),
],
),
// Participation
_buildDetailSection(
'Participation',
[
_buildDetailItem(Icons.people, 'Participants',
'$currentParticipants / $maxParticipants inscrits'),
_buildDetailItem(Icons.public, 'Visibilité',
(event['isPublic'] as bool) ? 'Événement public' : 'Événement privé'),
_buildDetailItem(Icons.app_registration, 'Inscription',
(event['requiresRegistration'] as bool) ? 'Inscription requise' : 'Inscription libre'),
],
),
// Tags
if ((event['tags'] as List).isNotEmpty) ...[
_buildDetailSection(
'Tags',
[
Wrap(
spacing: 8,
runSpacing: 8,
children: (event['tags'] as List<String>).map((tag) =>
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF6C5CE7).withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: Text(
tag,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF6C5CE7),
),
),
),
).toList(),
),
],
),
],
const SizedBox(height: 20),
// Actions
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
_showEditEventDialog(event);
},
icon: const Icon(Icons.edit),
label: const Text('Modifier'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6C5CE7),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: () {
Navigator.of(context).pop();
_shareEvent(event);
},
icon: const Icon(Icons.share),
label: const Text('Partager'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
],
),
),
),
],
),
);
},
);
}
/// Section de détails
Widget _buildDetailSection(String title, List<Widget> items) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF374151),
),
),
const SizedBox(height: 12),
...items,
const SizedBox(height: 24),
],
);
}
/// Item de détail
Widget _buildDetailItem(IconData icon, String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
icon,
size: 20,
color: const Color(0xFF6B7280),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF6B7280),
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF374151),
),
),
],
),
),
],
),
);
}
/// Icône selon le type d'événement
IconData _getEventIcon(String type) {
switch (type) {
case 'Officiel':
return Icons.business;
case 'Loisir':
return Icons.sports_esports;
case 'Formation':
return Icons.school;
case 'Social':
return Icons.people;
case 'Administratif':
return Icons.admin_panel_settings;
case 'Culturel':
return Icons.theater_comedy;
case 'Sport':
return Icons.sports;
default:
return Icons.event;
}
}
@override
void dispose() {
_searchController.dispose();
_tabController.dispose();
super.dispose();
}
/// Modifier un événement
void _showEditEventDialog(Map<String, dynamic> event) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Modification de "${event['title']}" - Fonctionnalité à implémenter'),
backgroundColor: const Color(0xFF6C5CE7),
),
);
}
/// Partager un événement
void _shareEvent(Map<String, dynamic> event) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Partage de "${event['title']}" - Fonctionnalité à implémenter'),
backgroundColor: const Color(0xFF10B981),
),
);
}
}