import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import '../../../../core/utils/logger.dart'; import '../../../../shared/widgets/info_badge.dart'; import '../../../../shared/widgets/mini_avatar.dart'; import '../../../../shared/design_system/unionflow_design_system.dart'; import '../../../../shared/design_system/tokens/app_typography.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'; /// Page de gestion des événements - UI/UX Premium class EventsPageWithData extends StatefulWidget { final List events; final int totalCount; final int currentPage; final int totalPages; const EventsPageWithData({ super.key, required this.events, required this.totalCount, required this.currentPage, required this.totalPages, }); @override State createState() => _EventsPageWithDataState(); } class _EventsPageWithDataState extends State with TickerProviderStateMixin { final TextEditingController _searchController = TextEditingController(); late TabController _tabController; String _searchQuery = ''; bool _isSearchExpanded = false; @override void initState() { super.initState(); _tabController = TabController(length: 5, vsync: this); } @override void dispose() { _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: ColorTokens.background, appBar: UFAppBar( title: 'ÉVÉNEMENTS', automaticallyImplyLeading: false, actions: [ // Search toggle button IconButton( icon: Icon( _isSearchExpanded ? Icons.close : Icons.search, color: ColorTokens.onSurface, size: 20, ), onPressed: () { setState(() { _isSearchExpanded = !_isSearchExpanded; if (!_isSearchExpanded) { _searchController.clear(); _searchQuery = ''; } }); }, ), if (canManageEvents) IconButton( icon: const Icon( Icons.add_circle_outline, color: ColorTokens.primary, size: 22, ), onPressed: () => AppLogger.info('Add Event clicked'), ), const SizedBox(width: SpacingTokens.xs), ], ), body: Column( children: [ // Header avec stats compactes _buildCompactHeader(), // Search bar (expandable) if (_isSearchExpanded) _buildSearchBar(), // Tabs avec badges _buildEnhancedTabBar(), // Liste d'événements Expanded( child: TabBarView( controller: _tabController, children: [ _buildEventsList(widget.events, 'tous'), _buildEventsList(widget.events.where((e) => e.estAVenir).toList(), 'à venir'), _buildEventsList(widget.events.where((e) => e.estEnCours).toList(), 'en cours'), _buildEventsList(widget.events.where((e) => e.estPasse).toList(), 'passés'), _buildCalendarView(), ], ), ), // Pagination if (widget.totalPages > 1) _buildEnhancedPagination(), ], ), ); }, ); } /// Header compact avec 3 métriques en ligne Widget _buildCompactHeader() { 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.symmetric( horizontal: SpacingTokens.md, vertical: SpacingTokens.sm, ), decoration: BoxDecoration( color: ColorTokens.surface, border: Border( bottom: BorderSide( color: ColorTokens.outlineVariant.withOpacity(0.5), width: 1, ), ), ), child: Row( children: [ _buildCompactMetric( icon: Icons.event_available, label: 'À venir', value: upcoming.toString(), color: ColorTokens.success, ), const SizedBox(width: SpacingTokens.md), _buildCompactMetric( icon: Icons.play_circle_outline, label: 'En cours', value: ongoing.toString(), color: ColorTokens.primary, ), const Spacer(), _buildTotalBadge(total), ], ), ); } Widget _buildCompactMetric({ required IconData icon, required String label, required String value, required Color color, }) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(RadiusTokens.sm), ), child: Icon(icon, size: 14, color: color), ), const SizedBox(width: SpacingTokens.xs), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( value, style: AppTypography.headerSmall.copyWith( fontSize: 16, height: 1.2, ), ), Text( label, style: TypographyTokens.labelSmall.copyWith( fontSize: 10, color: ColorTokens.onSurfaceVariant, ), ), ], ), ], ); } Widget _buildTotalBadge(int total) { return Container( padding: const EdgeInsets.symmetric( horizontal: SpacingTokens.sm, vertical: SpacingTokens.xs, ), decoration: BoxDecoration( color: ColorTokens.secondaryContainer, borderRadius: BorderRadius.circular(RadiusTokens.round), border: Border.all( color: ColorTokens.secondary.withOpacity(0.3), width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( total.toString(), style: AppTypography.actionText.copyWith( color: ColorTokens.secondary, fontWeight: FontWeight.bold, fontSize: 14, ), ), const SizedBox(width: 4), Text( 'TOTAL', style: TypographyTokens.labelSmall.copyWith( color: ColorTokens.secondary, fontSize: 10, fontWeight: FontWeight.w600, ), ), ], ), ); } /// Search bar avec animation Widget _buildSearchBar() { return AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric( horizontal: SpacingTokens.md, vertical: SpacingTokens.sm, ), decoration: BoxDecoration( color: ColorTokens.surface, border: Border( bottom: BorderSide( color: ColorTokens.outlineVariant.withOpacity(0.5), width: 1, ), ), ), child: TextField( controller: _searchController, autofocus: true, onChanged: (v) => setState(() => _searchQuery = v), style: TypographyTokens.bodyMedium, decoration: InputDecoration( hintText: 'Rechercher un événement...', hintStyle: TypographyTokens.bodySmall.copyWith( color: ColorTokens.onSurfaceVariant, ), prefixIcon: const Icon( Icons.search, size: 18, color: ColorTokens.onSurfaceVariant, ), suffixIcon: _searchQuery.isNotEmpty ? IconButton( icon: const Icon( Icons.clear, size: 18, color: ColorTokens.onSurfaceVariant, ), onPressed: () { setState(() { _searchController.clear(); _searchQuery = ''; }); }, ) : null, contentPadding: const EdgeInsets.symmetric( vertical: SpacingTokens.sm, horizontal: SpacingTokens.xs, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(RadiusTokens.md), borderSide: BorderSide( color: ColorTokens.outline.withOpacity(0.3), ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(RadiusTokens.md), borderSide: BorderSide( color: ColorTokens.outline.withOpacity(0.3), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(RadiusTokens.md), borderSide: const BorderSide( color: ColorTokens.primary, width: 1.5, ), ), filled: true, fillColor: ColorTokens.surfaceVariant.withOpacity(0.3), ), ), ); } /// TabBar amélioré avec badges de comptage Widget _buildEnhancedTabBar() { final allCount = widget.events.length; final upcomingCount = widget.events.where((e) => e.estAVenir).length; final ongoingCount = widget.events.where((e) => e.estEnCours).length; final pastCount = widget.events.where((e) => e.estPasse).length; return Container( decoration: BoxDecoration( color: ColorTokens.surface, border: Border( bottom: BorderSide( color: ColorTokens.outlineVariant.withOpacity(0.5), width: 1, ), ), ), child: TabBar( controller: _tabController, labelColor: ColorTokens.primary, unselectedLabelColor: ColorTokens.onSurfaceVariant, indicatorColor: ColorTokens.primary, indicatorWeight: 2.5, labelStyle: TypographyTokens.labelMedium.copyWith( fontWeight: FontWeight.bold, ), unselectedLabelStyle: TypographyTokens.labelMedium, tabs: [ _buildTabWithBadge('Tous', allCount), _buildTabWithBadge('À venir', upcomingCount), _buildTabWithBadge('En cours', ongoingCount), _buildTabWithBadge('Passés', pastCount), const Tab( child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.calendar_month, size: 14), SizedBox(width: 4), Text('Calendrier'), ], ), ), ], ), ); } Widget _buildTabWithBadge(String label, int count) { return Tab( child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(label), if (count > 0) ...[ const SizedBox(width: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: ColorTokens.primary.withOpacity(0.15), borderRadius: BorderRadius.circular(RadiusTokens.round), ), child: Text( count.toString(), style: TypographyTokens.labelSmall.copyWith( fontSize: 9, fontWeight: FontWeight.bold, color: ColorTokens.primary, ), ), ), ], ], ), ); } /// Liste d'événements optimisée Widget _buildEventsList(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(type); } return RefreshIndicator( onRefresh: () async => context.read().add(const LoadEvenements()), color: ColorTokens.primary, child: ListView.separated( padding: const EdgeInsets.all(SpacingTokens.md), itemCount: filtered.length, separatorBuilder: (_, __) => const SizedBox(height: SpacingTokens.sm), itemBuilder: (context, index) => _buildEnhancedEventCard(filtered[index]), ), ); } /// Empty state élégant Widget _buildEmptyState(String type) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(SpacingTokens.lg), decoration: BoxDecoration( color: ColorTokens.surfaceVariant.withOpacity(0.3), shape: BoxShape.circle, ), child: Icon( Icons.event_busy, size: 48, color: ColorTokens.onSurfaceVariant.withOpacity(0.5), ), ), const SizedBox(height: SpacingTokens.md), Text( 'Aucun événement $type', style: AppTypography.headerSmall.copyWith( color: ColorTokens.onSurfaceVariant, ), ), const SizedBox(height: SpacingTokens.xs), Text( _searchQuery.isEmpty ? 'La liste est vide pour le moment' : 'Aucun résultat pour "$_searchQuery"', style: TypographyTokens.bodySmall.copyWith( color: ColorTokens.onSurfaceVariant.withOpacity(0.7), ), ), ], ), ); } /// Card événement améliorée Widget _buildEnhancedEventCard(EvenementModel event) { final df = DateFormat('dd MMM yyyy, HH:mm'); final isUrgent = event.estAVenir && event.dateDebut.difference(DateTime.now()).inDays <= 2; return UFCard( margin: EdgeInsets.zero, onTap: () => _showEventDetails(event), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header avec badges Row( children: [ InfoBadge( text: _mapStatut(event.statut), backgroundColor: _getStatutColor(event.statut).withOpacity(0.12), textColor: _getStatutColor(event.statut), ), const SizedBox(width: SpacingTokens.xs), InfoBadge.neutral(_mapType(event.type)), if (isUrgent) ...[ const SizedBox(width: SpacingTokens.xs), InfoBadge( text: 'URGENT', backgroundColor: ColorTokens.error.withOpacity(0.12), textColor: ColorTokens.error, ), ], const Spacer(), Icon( Icons.chevron_right, size: 16, color: ColorTokens.onSurfaceVariant.withOpacity(0.5), ), ], ), const SizedBox(height: SpacingTokens.sm), // Titre Text( event.titre, style: AppTypography.headerSmall.copyWith(fontSize: 15), maxLines: 2, overflow: TextOverflow.ellipsis, ), // Date et lieu const SizedBox(height: SpacingTokens.sm), Row( children: [ Icon( Icons.access_time, size: 12, color: ColorTokens.onSurfaceVariant, ), const SizedBox(width: 4), Expanded( child: Text( df.format(event.dateDebut), style: AppTypography.subtitleSmall, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), if (event.lieu != null) ...[ const SizedBox(height: 4), Row( children: [ const Icon( Icons.location_on_outlined, size: 12, color: ColorTokens.onSurfaceVariant, ), const SizedBox(width: 4), Expanded( child: Text( event.lieu!, style: AppTypography.subtitleSmall, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ], // Footer avec organisateur et participants const SizedBox(height: SpacingTokens.md), Row( children: [ MiniAvatar( fallbackText: event.organisateurNom?[0] ?? 'O', size: 20, ), const SizedBox(width: SpacingTokens.xs), Expanded( child: Text( event.organisateurNom ?? 'Organisateur', style: TypographyTokens.labelSmall.copyWith(fontSize: 11), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), Container( padding: const EdgeInsets.symmetric( horizontal: SpacingTokens.xs, vertical: 2, ), decoration: BoxDecoration( color: ColorTokens.surfaceVariant, borderRadius: BorderRadius.circular(RadiusTokens.sm), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.people_outline, size: 11, color: ColorTokens.onSurfaceVariant, ), const SizedBox(width: 4), Text( '${event.participantsActuels}/${event.maxParticipants ?? "∞"}', style: AppTypography.badgeText.copyWith(fontSize: 10), ), ], ), ), ], ), ], ), ); } /// Vue calendrier placeholder Widget _buildCalendarView() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(SpacingTokens.xl), decoration: BoxDecoration( color: ColorTokens.surfaceVariant.withOpacity(0.3), shape: BoxShape.circle, ), child: Icon( Icons.calendar_month, size: 64, color: ColorTokens.onSurfaceVariant.withOpacity(0.5), ), ), const SizedBox(height: SpacingTokens.lg), Text( 'Vue Calendrier', style: AppTypography.headerSmall.copyWith(fontSize: 16), ), const SizedBox(height: SpacingTokens.xs), Text( 'Bientôt disponible', style: TypographyTokens.bodySmall.copyWith( color: ColorTokens.onSurfaceVariant, ), ), ], ), ); } /// Pagination améliorée Widget _buildEnhancedPagination() { return Container( padding: const EdgeInsets.symmetric(vertical: SpacingTokens.sm), decoration: BoxDecoration( color: ColorTokens.surface, border: Border( top: BorderSide( color: ColorTokens.outlineVariant.withOpacity(0.5), width: 1, ), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( icon: const Icon(Icons.chevron_left, size: 20), color: widget.currentPage > 0 ? ColorTokens.primary : ColorTokens.onSurfaceVariant.withOpacity(0.3), onPressed: widget.currentPage > 0 ? () => context.read().add( LoadEvenements(page: widget.currentPage - 1), ) : null, ), Container( padding: const EdgeInsets.symmetric( horizontal: SpacingTokens.md, vertical: SpacingTokens.xs, ), decoration: BoxDecoration( color: ColorTokens.primaryContainer.withOpacity(0.3), borderRadius: BorderRadius.circular(RadiusTokens.md), ), child: Text( 'Page ${widget.currentPage + 1} / ${widget.totalPages}', style: TypographyTokens.labelMedium.copyWith( fontWeight: FontWeight.bold, color: ColorTokens.primary, ), ), ), IconButton( icon: const Icon(Icons.chevron_right, size: 20), color: widget.currentPage < widget.totalPages - 1 ? ColorTokens.primary : ColorTokens.onSurfaceVariant.withOpacity(0.3), onPressed: widget.currentPage < widget.totalPages - 1 ? () => context.read().add( LoadEvenements(page: widget.currentPage + 1), ) : 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.primary; case StatutEvenement.termine: return ColorTokens.secondary; 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 'Manifestation'; case TypeEvenement.celebration: return 'Célébration'; case TypeEvenement.autre: return 'Autre'; } } void _showEventDetails(EvenementModel event) { showModalBottomSheet( context: context, backgroundColor: ColorTokens.surface, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(RadiusTokens.lg)), ), builder: (context) => Container( padding: const EdgeInsets.all(SpacingTokens.xl), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Row( children: [ Expanded( child: Text( event.titre, style: AppTypography.headerSmall.copyWith(fontSize: 16), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), IconButton( icon: const Icon(Icons.close, size: 20), onPressed: () => Navigator.pop(context), ), ], ), const SizedBox(height: SpacingTokens.md), // Badges Wrap( spacing: SpacingTokens.xs, runSpacing: SpacingTokens.xs, children: [ InfoBadge( text: _mapStatut(event.statut), backgroundColor: _getStatutColor(event.statut).withOpacity(0.12), textColor: _getStatutColor(event.statut), ), InfoBadge.neutral(_mapType(event.type)), ], ), const SizedBox(height: SpacingTokens.lg), // Description if (event.description != null && event.description!.isNotEmpty) ...[ Text( 'Description', style: AppTypography.actionText.copyWith(fontSize: 12), ), const SizedBox(height: SpacingTokens.xs), Text( event.description!, style: TypographyTokens.bodySmall, ), const SizedBox(height: SpacingTokens.lg), ], // Actions Row( children: [ Expanded( child: UFSecondaryButton( label: 'Partager', onPressed: () => AppLogger.info('Share event'), ), ), const SizedBox(width: SpacingTokens.sm), Expanded( child: UFPrimaryButton( label: 'Détails', onPressed: () => AppLogger.info('View details'), ), ), ], ), ], ), ), ); } }