1260 lines
42 KiB
Dart
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),
|
|
),
|
|
);
|
|
}
|
|
}
|