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_v2.dart'; import '../../../../shared/design_system/components/uf_app_bar.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: UnionFlowColors.background, appBar: UFAppBar( title: 'Événements', backgroundColor: UnionFlowColors.surface, foregroundColor: UnionFlowColors.textPrimary, actions: [ if (canManageEvents && widget.onAddEvent != null) IconButton( icon: const Icon(Icons.add_circle_outline), color: UnionFlowColors.unionGreen, onPressed: widget.onAddEvent, tooltip: 'Créer un événement', ), const SizedBox(width: 8), ], ), body: Column( children: [ _buildHeader(), _buildSearchBar(), _buildTabs(), 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'), ], ), ), if (widget.totalPages > 1) _buildPagination(), ], ), ); }, ); } Widget _buildHeader() { 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(20), decoration: BoxDecoration( color: UnionFlowColors.surface, border: Border(bottom: BorderSide(color: UnionFlowColors.border.withOpacity(0.5), width: 1)), ), child: Row( children: [ Expanded(child: _buildStatCard(Icons.event_available, 'À venir', upcoming.toString(), UnionFlowColors.success)), const SizedBox(width: 12), Expanded(child: _buildStatCard(Icons.play_circle_outline, 'En cours', ongoing.toString(), UnionFlowColors.amber)), const SizedBox(width: 12), Expanded(child: _buildStatCard(Icons.calendar_today, 'Total', total.toString(), UnionFlowColors.unionGreen)), ], ), ); } Widget _buildStatCard(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), width: 1), ), 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() { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: UnionFlowColors.surface, border: Border(bottom: BorderSide(color: UnionFlowColors.border.withOpacity(0.5), width: 1)), ), child: TextField( controller: _searchController, onChanged: (v) { setState(() => _searchQuery = v); _searchDebounce?.cancel(); _searchDebounce = Timer(AppConstants.searchDebounce, () { widget.onSearch?.call(v.isEmpty ? null : v); }); }, style: const TextStyle(fontSize: 14, color: UnionFlowColors.textPrimary), decoration: InputDecoration( hintText: 'Rechercher un événement...', hintStyle: const TextStyle(fontSize: 13, color: UnionFlowColors.textTertiary), prefixIcon: const Icon(Icons.search, size: 20, color: UnionFlowColors.textSecondary), suffixIcon: _searchQuery.isNotEmpty ? IconButton( icon: const Icon(Icons.clear, size: 18, color: UnionFlowColors.textSecondary), 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: UnionFlowColors.border.withOpacity(0.3))), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: UnionFlowColors.border.withOpacity(0.3))), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: UnionFlowColors.unionGreen, width: 1.5)), filled: true, fillColor: UnionFlowColors.surfaceVariant.withOpacity(0.3), ), ), ); } Widget _buildTabs() { return Container( decoration: BoxDecoration( color: UnionFlowColors.surface, border: Border(bottom: BorderSide(color: UnionFlowColors.border.withOpacity(0.5), width: 1)), ), child: TabBar( controller: _tabController, labelColor: UnionFlowColors.unionGreen, unselectedLabelColor: UnionFlowColors.textSecondary, indicatorColor: UnionFlowColors.unionGreen, indicatorWeight: 3, labelStyle: const TextStyle(fontSize: 13, fontWeight: FontWeight.w700), unselectedLabelStyle: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600), tabs: const [Tab(text: 'Tous'), Tab(text: 'À venir'), Tab(text: 'En cours'), Tab(text: 'Passés')], ), ); } 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: UnionFlowColors.unionGreen, child: ListView.separated( padding: const EdgeInsets.all(16), itemCount: filtered.length, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (context, index) => _buildEventCard(filtered[index]), ), ); } Widget _buildEventCard(EvenementModel event) { final df = DateFormat('dd MMM yyyy, HH:mm'); return GestureDetector( onTap: () => _showEventDetails(event), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: UnionFlowColors.surface, borderRadius: BorderRadius.circular(16), boxShadow: UnionFlowColors.softShadow, border: Border(left: BorderSide(color: _getStatutColor(event.statut), width: 4)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ _buildBadge(_mapStatut(event.statut), _getStatutColor(event.statut)), const SizedBox(width: 8), _buildBadge(_mapType(event.type), UnionFlowColors.textSecondary), const Spacer(), const Icon(Icons.chevron_right, size: 18, color: UnionFlowColors.textTertiary), ], ), const SizedBox(height: 12), Text(event.titre, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary), maxLines: 2, overflow: TextOverflow.ellipsis), const SizedBox(height: 12), Row( children: [ const Icon(Icons.access_time, size: 14, color: UnionFlowColors.textSecondary), const SizedBox(width: 6), Text(df.format(event.dateDebut), style: const TextStyle(fontSize: 12, color: UnionFlowColors.textSecondary)), ], ), if (event.lieu != null) ...[ const SizedBox(height: 6), Row( children: [ const Icon(Icons.location_on_outlined, size: 14, color: UnionFlowColors.textSecondary), const SizedBox(width: 6), Expanded(child: Text(event.lieu!, style: const TextStyle(fontSize: 12, color: UnionFlowColors.textSecondary), maxLines: 1, overflow: TextOverflow.ellipsis)), ], ), ], const SizedBox(height: 12), Row( children: [ Container( width: 24, height: 24, decoration: const BoxDecoration(gradient: UnionFlowColors.primaryGradient, 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: const TextStyle(fontSize: 11, color: UnionFlowColors.textSecondary), maxLines: 1, overflow: TextOverflow.ellipsis)), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: UnionFlowColors.goldPale, borderRadius: BorderRadius.circular(12), border: Border.all(color: UnionFlowColors.gold.withOpacity(0.3), width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.people_outline, size: 12, color: UnionFlowColors.gold), const SizedBox(width: 4), Text('${event.participantsActuels}/${event.maxParticipants ?? "∞"}', style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: UnionFlowColors.gold)), ], ), ), ], ), ], ), ), ); } 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), width: 1), ), child: Text(text, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: color)), ); } Widget _buildEmptyState(String type) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(24), decoration: const BoxDecoration(color: UnionFlowColors.goldPale, shape: BoxShape.circle), child: const Icon(Icons.event_busy, size: 64, color: UnionFlowColors.gold), ), const SizedBox(height: 24), Text('Aucun événement $type', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary)), const SizedBox(height: 8), Text(_searchQuery.isEmpty ? 'La liste est vide pour le moment' : 'Essayez une autre recherche', style: const TextStyle(fontSize: 13, color: UnionFlowColors.textSecondary)), ], ), ); } Widget _buildPagination() { return Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: UnionFlowColors.surface, border: Border(top: BorderSide(color: UnionFlowColors.border.withOpacity(0.5), width: 1)), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( icon: const Icon(Icons.chevron_left, size: 24), color: widget.currentPage > 0 ? UnionFlowColors.unionGreen : UnionFlowColors.textTertiary, 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: UnionFlowColors.primaryGradient, 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 ? UnionFlowColors.unionGreen : UnionFlowColors.textTertiary, 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 UnionFlowColors.info; case StatutEvenement.confirme: return UnionFlowColors.success; case StatutEvenement.enCours: return UnionFlowColors.amber; case StatutEvenement.termine: return UnionFlowColors.textSecondary; case StatutEvenement.annule: return UnionFlowColors.error; case StatutEvenement.reporte: return UnionFlowColors.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(EvenementModel event) { final bloc = context.read(); Navigator.of(context).push( MaterialPageRoute( builder: (_) => BlocProvider.value( value: bloc, child: EventDetailPage(evenement: event), ), ), ); } }