import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import '../../../../core/constants/app_constants.dart'; import '../../../../shared/design_system/unionflow_design_system.dart'; import '../../../authentication/data/models/user_role.dart'; import '../../../authentication/presentation/bloc/auth_bloc.dart'; import '../../bloc/evenements_bloc.dart'; import '../../bloc/evenements_event.dart'; import '../../data/models/evenement_model.dart'; import 'event_detail_page.dart'; /// Page Événements - Design UnionFlow class EventsPageWithData extends StatefulWidget { final List events; final int totalCount; final int currentPage; final int totalPages; final void Function(String? query)? onSearch; final VoidCallback? onAddEvent; final void Function(int page, String? recherche)? onPageChanged; const EventsPageWithData({ super.key, required this.events, required this.totalCount, required this.currentPage, required this.totalPages, this.onSearch, this.onAddEvent, this.onPageChanged, }); @override State createState() => _EventsPageWithDataState(); } class _EventsPageWithDataState extends State with TickerProviderStateMixin { final TextEditingController _searchController = TextEditingController(); late TabController _tabController; String _searchQuery = ''; Timer? _searchDebounce; @override void initState() { super.initState(); _tabController = TabController(length: 4, vsync: this); } @override void dispose() { _searchDebounce?.cancel(); _searchController.dispose(); _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { if (state is! AuthAuthenticated) { return const Scaffold(body: Center(child: CircularProgressIndicator())); } final canManageEvents = state.effectiveRole.level >= UserRole.moderator.level; return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: UFAppBar( title: 'Événements', moduleGradient: ModuleColors.evenementsGradient, actions: [ if (canManageEvents && widget.onAddEvent != null) IconButton( icon: const Icon(Icons.add_circle_outline), color: Colors.white, onPressed: widget.onAddEvent, tooltip: 'Créer un événement', ), const SizedBox(width: 8), ], bottom: TabBar( controller: _tabController, isScrollable: true, labelColor: Colors.white, unselectedLabelColor: Colors.white70, indicatorColor: Colors.white, indicatorSize: TabBarIndicatorSize.label, labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold), tabs: const [ Tab(child: Text('TOUS')), Tab(child: Text('À VENIR')), Tab(child: Text('EN COURS')), Tab(child: Text('PASSÉS')), ], ), ), body: Column( children: [ _buildHeader(context), _buildSearchBar(context), Expanded( child: TabBarView( controller: _tabController, children: [ _buildEventsList(context, widget.events, 'tous'), _buildEventsList(context, widget.events.where((e) => e.estAVenir).toList(), 'à venir'), _buildEventsList(context, widget.events.where((e) => e.estEnCours).toList(), 'en cours'), _buildEventsList(context, widget.events.where((e) => e.estPasse).toList(), 'passés'), ], ), ), if (widget.totalPages > 1) _buildPagination(context), ], ), ); }, ); } Widget _buildHeader(BuildContext context) { final scheme = Theme.of(context).colorScheme; final upcoming = widget.events.where((e) => e.estAVenir).length; final ongoing = widget.events.where((e) => e.estEnCours).length; final total = widget.totalCount; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: scheme.surface, border: Border(bottom: BorderSide(color: scheme.outlineVariant.withOpacity(0.5))), ), child: Row( children: [ Expanded(child: _buildStatCard(context, Icons.event_available, 'À venir', upcoming.toString(), ColorTokens.success)), const SizedBox(width: 12), Expanded(child: _buildStatCard(context, Icons.play_circle_outline, 'En cours', ongoing.toString(), ColorTokens.warningLight)), const SizedBox(width: 12), Expanded(child: _buildStatCard(context, Icons.calendar_today, 'Total', total.toString(), ModuleColors.evenements)), ], ), ); } Widget _buildStatCard(BuildContext context, IconData icon, String label, String value, Color color) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withOpacity(0.3)), ), child: Column( children: [ Icon(icon, size: 20, color: color), const SizedBox(height: 6), Text(value, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: color)), const SizedBox(height: 2), Text(label, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: color), textAlign: TextAlign.center), ], ), ); } Widget _buildSearchBar(BuildContext context) { final scheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: scheme.surface, border: Border(bottom: BorderSide(color: scheme.outlineVariant.withOpacity(0.5))), ), child: TextField( controller: _searchController, onChanged: (v) { setState(() => _searchQuery = v); _searchDebounce?.cancel(); _searchDebounce = Timer(AppConstants.searchDebounce, () { widget.onSearch?.call(v.isEmpty ? null : v); }); }, style: TextStyle(fontSize: 14, color: scheme.onSurface), decoration: InputDecoration( hintText: 'Rechercher un événement...', hintStyle: TextStyle(fontSize: 13, color: scheme.onSurfaceVariant), prefixIcon: Icon(Icons.search, size: 20, color: scheme.onSurfaceVariant), suffixIcon: _searchQuery.isNotEmpty ? IconButton( icon: Icon(Icons.clear, size: 18, color: scheme.onSurfaceVariant), onPressed: () { _searchDebounce?.cancel(); _searchController.clear(); setState(() => _searchQuery = ''); widget.onSearch?.call(null); }, ) : null, contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: scheme.outlineVariant)), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: scheme.outlineVariant)), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: ModuleColors.evenements, width: 1.5)), filled: true, fillColor: scheme.surfaceContainerHigh.withOpacity(0.5), ), ), ); } // _buildTabs() supprimé : migré dans UFAppBar.bottom (pattern Adhésions) Widget _buildEventsList(BuildContext context, List events, String type) { final filtered = _searchQuery.isEmpty ? events : events.where((e) => e.titre.toLowerCase().contains(_searchQuery.toLowerCase()) || (e.lieu?.toLowerCase().contains(_searchQuery.toLowerCase()) ?? false)).toList(); if (filtered.isEmpty) return _buildEmptyState(context, type); return RefreshIndicator( onRefresh: () async => context.read().add(const LoadEvenements()), color: ModuleColors.evenements, child: ListView.separated( padding: const EdgeInsets.all(12), itemCount: filtered.length, separatorBuilder: (_, __) => const SizedBox(height: 6), itemBuilder: (context, index) => _buildEventCard(context, filtered[index]), ), ); } Widget _buildEventCard(BuildContext context, EvenementModel event) { final scheme = Theme.of(context).colorScheme; final df = DateFormat('dd MMM yyyy, HH:mm'); final statutColor = _getStatutColor(event.statut); return GestureDetector( onTap: () => _showEventDetails(context, event), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), decoration: BoxDecoration( color: scheme.surface, borderRadius: BorderRadius.circular(10), border: Border(left: BorderSide(color: statutColor, width: 4)), boxShadow: [BoxShadow(color: scheme.shadow.withOpacity(0.04), blurRadius: 4, offset: const Offset(0, 2))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ _buildBadge(_mapStatut(event.statut), statutColor), const SizedBox(width: 8), _buildBadge(_mapType(event.type), scheme.onSurfaceVariant), const Spacer(), Icon(Icons.chevron_right, size: 18, color: scheme.onSurfaceVariant), ], ), const SizedBox(height: 12), Text(event.titre, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: scheme.onSurface), maxLines: 2, overflow: TextOverflow.ellipsis), const SizedBox(height: 12), Row( children: [ Icon(Icons.access_time, size: 14, color: scheme.onSurfaceVariant), const SizedBox(width: 6), Text(df.format(event.dateDebut), style: TextStyle(fontSize: 12, color: scheme.onSurfaceVariant)), ], ), if (event.lieu != null) ...[ const SizedBox(height: 6), Row( children: [ Icon(Icons.location_on_outlined, size: 14, color: scheme.onSurfaceVariant), const SizedBox(width: 6), Expanded(child: Text(event.lieu!, style: TextStyle(fontSize: 12, color: scheme.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis)), ], ), ], const SizedBox(height: 12), Row( children: [ Container( width: 24, height: 24, decoration: BoxDecoration( gradient: LinearGradient(colors: ModuleColors.evenementsGradient, begin: Alignment.topLeft, end: Alignment.bottomRight), shape: BoxShape.circle, ), alignment: Alignment.center, child: Text(event.organisateurNom?[0] ?? 'O', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 12)), ), const SizedBox(width: 8), Expanded(child: Text(event.organisateurNom ?? 'Organisateur', style: TextStyle(fontSize: 11, color: scheme.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis)), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: ModuleColors.membres.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: ModuleColors.membres.withOpacity(0.3)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.people_outline, size: 12, color: ModuleColors.membres), const SizedBox(width: 4), Text('${event.participantsActuels}/${event.maxParticipants ?? "∞"}', style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: ModuleColors.membres)), ], ), ), ], ), ], ), ), ); } Widget _buildBadge(String text, Color color) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(8), border: Border.all(color: color.withOpacity(0.3)), ), child: Text(text, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: color)), ); } Widget _buildEmptyState(BuildContext context, String type) { final scheme = Theme.of(context).colorScheme; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: ModuleColors.evenements.withOpacity(0.1), shape: BoxShape.circle), child: Icon(Icons.event_busy, size: 40, color: ModuleColors.evenements), ), const SizedBox(height: 12), Text('Aucun événement $type', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: scheme.onSurface)), const SizedBox(height: 8), Text( _searchQuery.isEmpty ? 'La liste est vide pour le moment' : 'Essayez une autre recherche', style: TextStyle(fontSize: 13, color: scheme.onSurfaceVariant), ), ], ), ); } Widget _buildPagination(BuildContext context) { final scheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: scheme.surface, border: Border(top: BorderSide(color: scheme.outlineVariant.withOpacity(0.5))), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( icon: const Icon(Icons.chevron_left, size: 24), color: widget.currentPage > 0 ? ModuleColors.evenements : scheme.onSurfaceVariant.withOpacity(0.4), onPressed: widget.currentPage > 0 && widget.onPageChanged != null ? () => widget.onPageChanged!(widget.currentPage - 1, _searchQuery.isEmpty ? null : _searchQuery) : null, ), Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( gradient: LinearGradient(colors: ModuleColors.evenementsGradient, begin: Alignment.topLeft, end: Alignment.bottomRight), borderRadius: BorderRadius.circular(20), ), child: Text('Page ${widget.currentPage + 1} / ${widget.totalPages}', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Colors.white)), ), IconButton( icon: const Icon(Icons.chevron_right, size: 24), color: widget.currentPage < widget.totalPages - 1 ? ModuleColors.evenements : scheme.onSurfaceVariant.withOpacity(0.4), onPressed: widget.currentPage < widget.totalPages - 1 && widget.onPageChanged != null ? () => widget.onPageChanged!(widget.currentPage + 1, _searchQuery.isEmpty ? null : _searchQuery) : null, ), ], ), ); } String _mapStatut(StatutEvenement s) { switch (s) { case StatutEvenement.planifie: return 'Planifié'; case StatutEvenement.confirme: return 'Confirmé'; case StatutEvenement.enCours: return 'En cours'; case StatutEvenement.termine: return 'Terminé'; case StatutEvenement.annule: return 'Annulé'; case StatutEvenement.reporte: return 'Reporté'; } } Color _getStatutColor(StatutEvenement s) { switch (s) { case StatutEvenement.planifie: return ColorTokens.info; case StatutEvenement.confirme: return ColorTokens.success; case StatutEvenement.enCours: return ColorTokens.warningLight; case StatutEvenement.termine: return ColorTokens.textSecondary; case StatutEvenement.annule: return ColorTokens.error; case StatutEvenement.reporte: return ColorTokens.warning; } } String _mapType(TypeEvenement t) { switch (t) { case TypeEvenement.assembleeGenerale: return 'AG'; case TypeEvenement.reunion: return 'Réunion'; case TypeEvenement.formation: return 'Formation'; case TypeEvenement.conference: return 'Conférence'; case TypeEvenement.atelier: return 'Atelier'; case TypeEvenement.seminaire: return 'Séminaire'; case TypeEvenement.evenementSocial: return 'Social'; case TypeEvenement.manifestation: return 'Manif.'; case TypeEvenement.celebration: return 'Célébr.'; case TypeEvenement.autre: return 'Autre'; } } void _showEventDetails(BuildContext context, EvenementModel event) { final bloc = context.read(); Navigator.of(context).push( MaterialPageRoute( builder: (_) => BlocProvider.value( value: bloc, child: EventDetailPage(evenement: event), ), ), ); } }