import 'package:flutter/material.dart'; import 'package:geosector_app/core/repositories/user_repository.dart'; import 'package:geosector_app/core/repositories/amicale_repository.dart'; import 'package:geosector_app/core/services/event_stats_service.dart'; import 'package:geosector_app/core/services/current_user_service.dart'; import 'package:geosector_app/core/data/models/event_stats_model.dart'; import 'package:geosector_app/core/data/models/amicale_model.dart'; import 'package:geosector_app/core/utils/api_exception.dart'; import 'package:intl/intl.dart'; /// Mode de sélection de la période enum DateSelectionMode { /// Une seule date singleDay, /// Plage de dates personnalisée dateRange, /// Derniers X jours lastDays, } /// Page d'administration des connexions et événements. /// /// Affiche les statistiques de connexion et d'activité : /// - Pour les admins amicale : données de leur amicale uniquement /// - Pour les super-admins : données globales avec filtre par amicale class AdminConnexionsPage extends StatefulWidget { final UserRepository userRepository; final AmicaleRepository amicaleRepository; const AdminConnexionsPage({ super.key, required this.userRepository, required this.amicaleRepository, }); @override State createState() => _AdminConnexionsPageState(); } class _AdminConnexionsPageState extends State { final _eventStatsService = EventStatsService.instance; final _dateFormat = DateFormat('dd/MM/yyyy'); final _timeFormat = DateFormat('HH:mm'); // État de chargement bool _isLoading = true; String? _error; // Données EventSummary? _summary; DailyStats? _dailyStats; EventDetails? _details; // Mode de sélection de période DateSelectionMode _selectionMode = DateSelectionMode.lastDays; // Pour mode "Jour unique" DateTime _singleDate = DateTime.now(); // Pour mode "Période personnalisée" DateTime _startDate = DateTime.now().subtract(const Duration(days: 6)); DateTime _endDate = DateTime.now(); // Pour mode "Derniers jours" int _lastDays = 7; // Filtre par type d'événement String? _selectedEventType; // Filtre par entité (super-admin uniquement) int? _selectedEntityId; List _amicales = []; bool _isSuperAdmin = false; @override void initState() { super.initState(); _isSuperAdmin = CurrentUserService.instance.isSuperAdmin; if (_isSuperAdmin) { _loadAmicales(); } _loadData(); } /// Charge la liste des amicales pour le filtre super-admin void _loadAmicales() { try { _amicales = widget.amicaleRepository.getAllAmicales(); // Trier par nom _amicales.sort((a, b) => a.name.compareTo(b.name)); } catch (e) { debugPrint('Erreur chargement amicales: $e'); } } /// Calcule les dates de début et fin selon le mode sélectionné (DateTime, DateTime) _getDateRange() { switch (_selectionMode) { case DateSelectionMode.singleDay: return (_singleDate, _singleDate); case DateSelectionMode.dateRange: return (_startDate, _endDate); case DateSelectionMode.lastDays: final end = DateTime.now(); final start = end.subtract(Duration(days: _lastDays - 1)); return (start, end); } } /// Vérifie si on affiche une période (plusieurs jours) bool get _isMultipleDays { final (start, end) = _getDateRange(); return !start.isAtSameMomentAs(end) && (end.difference(start).inDays > 0 || _selectionMode != DateSelectionMode.singleDay); } Future _loadData() async { setState(() { _isLoading = true; _error = null; }); try { final (startDate, endDate) = _getDateRange(); // Charger les données selon le mode if (_selectionMode == DateSelectionMode.singleDay) { // Mode jour unique : résumé + détails du jour final results = await Future.wait([ _eventStatsService.getSummary( date: _singleDate, entityId: _selectedEntityId, ), _eventStatsService.getDetails( date: _singleDate, event: _selectedEventType, entityId: _selectedEntityId, limit: 50, ), ]); setState(() { _summary = results[0] as EventSummary; _dailyStats = null; // Pas de graphique en mode jour unique _details = results[1] as EventDetails; _isLoading = false; }); } else { // Mode période : stats quotidiennes + détails du dernier jour final results = await Future.wait([ _eventStatsService.getDailyStats( from: startDate, to: endDate, entityId: _selectedEntityId, ), _eventStatsService.getDetails( date: endDate, event: _selectedEventType, entityId: _selectedEntityId, limit: 50, ), ]); final dailyStats = results[0] as DailyStats; // Calculer le résumé agrégé depuis les stats quotidiennes setState(() { _dailyStats = dailyStats; _summary = _computeAggregatedSummary(dailyStats); _details = results[1] as EventDetails; _isLoading = false; }); } } catch (e) { setState(() { _error = e is ApiException ? e.message : 'Erreur lors du chargement des données'; _isLoading = false; }); } } /// Calcule un résumé agrégé à partir des stats quotidiennes EventSummary _computeAggregatedSummary(DailyStats dailyStats) { int authSuccess = 0; int authFailed = 0; int authLogout = 0; int passagesCreated = 0; double passagesAmount = 0; int stripeSuccess = 0; double stripeAmount = 0; int totalEvents = 0; final uniqueUsersSet = {}; for (final day in dailyStats.days) { totalEvents += day.totalCount; // Auth events if (day.events.containsKey('login_success')) { authSuccess += day.events['login_success']!.count; } if (day.events.containsKey('login_failed')) { authFailed += day.events['login_failed']!.count; } if (day.events.containsKey('logout')) { authLogout += day.events['logout']!.count; } // Passages if (day.events.containsKey('passage_created')) { passagesCreated += day.events['passage_created']!.count; passagesAmount += day.events['passage_created']!.sumAmount; } // Stripe if (day.events.containsKey('stripe_payment_success')) { stripeSuccess += day.events['stripe_payment_success']!.count; stripeAmount += day.events['stripe_payment_success']!.sumAmount; } } return EventSummary( date: dailyStats.to, stats: DayStats( auth: AuthStats(success: authSuccess, failed: authFailed, logout: authLogout), passages: PassageStats(created: passagesCreated, updated: 0, deleted: 0, amount: passagesAmount), users: const UserStats(created: 0, updated: 0, deleted: 0), sectors: const SectorStats(created: 0, updated: 0, deleted: 0), stripe: StripeStats(created: 0, success: stripeSuccess, failed: 0, cancelled: 0, amount: stripeAmount), ), totals: DayTotals(events: totalEvents, uniqueUsers: uniqueUsersSet.length), ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final size = MediaQuery.of(context).size; final isMobile = size.width <= 600; return SafeArea( child: RefreshIndicator( onRefresh: _loadData, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: EdgeInsets.all(isMobile ? 12 : 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Titre _buildTitle(theme), const SizedBox(height: 16), // Filtres de période _buildPeriodFilters(theme, isMobile), const SizedBox(height: 16), // Contenu principal if (_isLoading) const Center( child: Padding( padding: EdgeInsets.all(48), child: CircularProgressIndicator(), ), ) else if (_error != null) _buildErrorCard(theme) else ...[ // Cards de résumé _buildSummaryCards(theme, isMobile), const SizedBox(height: 24), // Graphique d'évolution (seulement si période > 1 jour) if (_isMultipleDays) ...[ _buildChartSection(theme, isMobile), const SizedBox(height: 24), ], // Tableau des événements détaillés _buildDetailsTable(theme, isMobile), ], ], ), ), ), ); } Widget _buildTitle(ThemeData theme) { return Text( 'Connexions et activité', style: theme.textTheme.headlineSmall?.copyWith( color: theme.colorScheme.primary, fontWeight: FontWeight.bold, ), ); } Widget _buildPeriodFilters(ThemeData theme, bool isMobile) { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Sélecteur de mode Wrap( spacing: 8, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ Text( 'Période :', style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, ), ), SegmentedButton( segments: const [ ButtonSegment( value: DateSelectionMode.singleDay, label: Text('Jour'), icon: Icon(Icons.today, size: 18), ), ButtonSegment( value: DateSelectionMode.dateRange, label: Text('Période'), icon: Icon(Icons.date_range, size: 18), ), ButtonSegment( value: DateSelectionMode.lastDays, label: Text('Derniers jours'), icon: Icon(Icons.history, size: 18), ), ], selected: {_selectionMode}, onSelectionChanged: (values) { setState(() => _selectionMode = values.first); _loadData(); }, style: const ButtonStyle( visualDensity: VisualDensity.compact, ), ), ], ), const SizedBox(height: 12), // Contrôles spécifiques selon le mode Wrap( spacing: 12, runSpacing: 12, crossAxisAlignment: WrapCrossAlignment.center, children: [ // Sélecteurs de date selon le mode ..._buildDateSelectors(theme), // Séparateur vertical if (!isMobile) Container( height: 36, width: 1, color: theme.colorScheme.outlineVariant, ), // Filtre par type d'événement _buildEventTypeFilter(theme), // Filtre par entité (super-admin uniquement) if (_isSuperAdmin) ...[ if (!isMobile) Container( height: 36, width: 1, color: theme.colorScheme.outlineVariant, ), _buildEntityFilter(theme), ], ], ), ], ), ), ); } List _buildDateSelectors(ThemeData theme) { switch (_selectionMode) { case DateSelectionMode.singleDay: return [ _buildDateButton( theme: theme, label: 'Date', date: _singleDate, onTap: () => _selectDate( initialDate: _singleDate, onSelected: (date) { setState(() => _singleDate = date); _loadData(); }, ), ), ]; case DateSelectionMode.dateRange: return [ _buildDateButton( theme: theme, label: 'Du', date: _startDate, onTap: () => _selectDate( initialDate: _startDate, lastDate: _endDate, onSelected: (date) { setState(() => _startDate = date); _loadData(); }, ), ), const Icon(Icons.arrow_forward, size: 16), _buildDateButton( theme: theme, label: 'Au', date: _endDate, onTap: () => _selectDate( initialDate: _endDate, firstDate: _startDate, onSelected: (date) { setState(() => _endDate = date); _loadData(); }, ), ), ]; case DateSelectionMode.lastDays: return [ SegmentedButton( segments: const [ ButtonSegment(value: 7, label: Text('7 jours')), ButtonSegment(value: 14, label: Text('14 jours')), ButtonSegment(value: 21, label: Text('21 jours')), ButtonSegment(value: 30, label: Text('30 jours')), ], selected: {_lastDays}, onSelectionChanged: (values) { setState(() => _lastDays = values.first); _loadData(); }, style: const ButtonStyle( visualDensity: VisualDensity.compact, ), ), ]; } } Widget _buildDateButton({ required ThemeData theme, required String label, required DateTime date, required VoidCallback onTap, }) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( border: Border.all(color: theme.colorScheme.outline), borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( '$label : ', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), Icon(Icons.calendar_today, size: 16, color: theme.colorScheme.primary), const SizedBox(width: 6), Text( _dateFormat.format(date), style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w500, ), ), ], ), ), ); } Future _selectDate({ required DateTime initialDate, DateTime? firstDate, DateTime? lastDate, required void Function(DateTime) onSelected, }) async { final picked = await showDatePicker( context: context, initialDate: initialDate, firstDate: firstDate ?? DateTime(2024), lastDate: lastDate ?? DateTime.now(), locale: const Locale('fr', 'FR'), ); if (picked != null) { onSelected(picked); } } Widget _buildEventTypeFilter(ThemeData theme) { return DropdownButtonHideUnderline( child: Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( border: Border.all(color: theme.colorScheme.outline), borderRadius: BorderRadius.circular(8), ), child: DropdownButton( value: _selectedEventType, hint: const Text('Tous les événements'), items: const [ DropdownMenuItem(value: null, child: Text('Tous')), DropdownMenuItem(value: 'login_success', child: Text('Connexions')), DropdownMenuItem(value: 'login_failed', child: Text('Échecs connexion')), DropdownMenuItem(value: 'passage_created', child: Text('Passages créés')), DropdownMenuItem(value: 'stripe_payment_success', child: Text('Paiements Stripe')), ], onChanged: (value) { setState(() => _selectedEventType = value); _loadData(); }, ), ), ); } Widget _buildEntityFilter(ThemeData theme) { return DropdownButtonHideUnderline( child: Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( border: Border.all(color: theme.colorScheme.outline), borderRadius: BorderRadius.circular(8), color: _selectedEntityId != null ? theme.colorScheme.primaryContainer.withOpacity(0.3) : null, ), child: DropdownButton( value: _selectedEntityId, hint: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.business, size: 16, color: theme.colorScheme.primary), const SizedBox(width: 6), const Text('Toutes les amicales'), ], ), selectedItemBuilder: (context) { return [ // Item pour "Toutes" Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.business, size: 16, color: theme.colorScheme.primary), const SizedBox(width: 6), const Text('Toutes les amicales'), ], ), // Items pour chaque amicale ..._amicales.map((amicale) => Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.business, size: 16, color: theme.colorScheme.primary), const SizedBox(width: 6), ConstrainedBox( constraints: const BoxConstraints(maxWidth: 150), child: Text( amicale.name, overflow: TextOverflow.ellipsis, ), ), ], )), ]; }, items: [ const DropdownMenuItem( value: null, child: Text('Toutes les amicales'), ), ..._amicales.map((amicale) => DropdownMenuItem( value: amicale.id, child: Text( amicale.name, overflow: TextOverflow.ellipsis, ), )), ], onChanged: (value) { setState(() => _selectedEntityId = value); _loadData(); }, ), ), ); } Widget _buildErrorCard(ThemeData theme) { return Card( color: theme.colorScheme.errorContainer, child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Icon(Icons.error_outline, color: theme.colorScheme.error), const SizedBox(width: 12), Expanded( child: Text( _error!, style: TextStyle(color: theme.colorScheme.onErrorContainer), ), ), TextButton( onPressed: _loadData, child: const Text('Réessayer'), ), ], ), ), ); } Widget _buildSummaryCards(ThemeData theme, bool isMobile) { if (_summary == null) return const SizedBox.shrink(); final stats = _summary!.stats; final totals = _summary!.totals; final (startDate, endDate) = _getDateRange(); // Libellé de la période final periodLabel = _selectionMode == DateSelectionMode.singleDay ? _dateFormat.format(_singleDate) : '${_dateFormat.format(startDate)} - ${_dateFormat.format(endDate)}'; // Disposition responsive : 2 colonnes sur mobile, 4 sur desktop final crossAxisCount = isMobile ? 2 : 4; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Période affichée Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( 'Résumé : $periodLabel', style: theme.textTheme.titleSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ), GridView.count( crossAxisCount: crossAxisCount, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: isMobile ? 1.3 : 1.8, children: [ // Connexions _buildStatCard( theme: theme, icon: Icons.login, iconColor: Colors.green, title: 'Connexions', value: stats.auth.success.toString(), subtitle: '${stats.auth.failed} échecs', ), // Passages _buildStatCard( theme: theme, icon: Icons.receipt_long, iconColor: Colors.blue, title: 'Passages', value: stats.passages.created.toString(), subtitle: '${_formatAmount(stats.passages.amount)} collectés', ), // Paiements Stripe _buildStatCard( theme: theme, icon: Icons.credit_card, iconColor: Colors.purple, title: 'Paiements', value: stats.stripe.success.toString(), subtitle: _formatAmount(stats.stripe.amount), ), // Utilisateurs actifs _buildStatCard( theme: theme, icon: Icons.people, iconColor: Colors.orange, title: 'Événements', value: totals.events.toString(), subtitle: '${totals.uniqueUsers} utilisateurs', ), ], ), ], ); } Widget _buildStatCard({ required ThemeData theme, required IconData icon, required Color iconColor, required String title, required String value, required String subtitle, }) { return Card( elevation: 2, child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: iconColor.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: iconColor, size: 20), ), const SizedBox(width: 8), Expanded( child: Text( title, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), overflow: TextOverflow.ellipsis, ), ), ], ), Text( value, style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.onSurface, ), ), Text( subtitle, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), overflow: TextOverflow.ellipsis, ), ], ), ), ); } Widget _buildChartSection(ThemeData theme, bool isMobile) { if (_dailyStats == null || _dailyStats!.days.isEmpty) { return Card( child: Padding( padding: const EdgeInsets.all(24), child: Center( child: Text( 'Aucune donnée pour cette période', style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ), ), ); } final (startDate, endDate) = _getDateRange(); final daysDiff = endDate.difference(startDate).inDays + 1; return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Évolution sur $daysDiff jours', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), SizedBox( height: isMobile ? 200 : 250, child: _buildSimpleBarChart(theme), ), ], ), ), ); } Widget _buildSimpleBarChart(ThemeData theme) { final days = _dailyStats!.days; if (days.isEmpty) return const SizedBox.shrink(); // Trouver le max pour le scaling final maxCount = days.map((d) => d.totalCount).reduce((a, b) => a > b ? a : b); final scale = maxCount > 0 ? 1.0 / maxCount : 1.0; return LayoutBuilder( builder: (context, constraints) { final barWidth = (constraints.maxWidth - (days.length - 1) * 4) / days.length; final clampedBarWidth = barWidth.clamp(8.0, 40.0); return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.end, children: days.map((day) { final height = (constraints.maxHeight - 30) * day.totalCount * scale; return Tooltip( message: '${_dateFormat.format(day.date)}\n${day.totalCount} événements', child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ Container( width: clampedBarWidth, height: height.clamp(4.0, constraints.maxHeight - 30), decoration: BoxDecoration( color: theme.colorScheme.primary, borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), ), ), const SizedBox(height: 4), SizedBox( width: clampedBarWidth + 8, child: Text( DateFormat('dd').format(day.date), style: theme.textTheme.labelSmall, textAlign: TextAlign.center, overflow: TextOverflow.clip, ), ), ], ), ); }).toList(), ); }, ); } Widget _buildDetailsTable(ThemeData theme, bool isMobile) { final (_, endDate) = _getDateRange(); final detailsDateLabel = _selectionMode == DateSelectionMode.singleDay ? _dateFormat.format(_singleDate) : _dateFormat.format(endDate); if (_details == null || _details!.events.isEmpty) { return Card( child: Padding( padding: const EdgeInsets.all(24), child: Center( child: Text( 'Aucun événement pour le $detailsDateLabel', style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ), ), ); } return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Détail des événements', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), Text( detailsDateLabel, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), Text( '${_details!.pagination.total} événements', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), const SizedBox(height: 16), // Table responsive if (isMobile) _buildMobileEventsList(theme) else _buildDesktopEventsTable(theme), // Pagination if (_details!.pagination.hasMore) Padding( padding: const EdgeInsets.only(top: 16), child: Center( child: TextButton.icon( onPressed: _loadMoreDetails, icon: const Icon(Icons.expand_more), label: const Text('Charger plus'), ), ), ), ], ), ), ); } Widget _buildMobileEventsList(ThemeData theme) { return ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: _details!.events.length, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, index) { final event = _details!.events[index]; return ListTile( contentPadding: EdgeInsets.zero, leading: _buildEventIcon(event.event), title: Text( EventTypes.getLabel(event.event), style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w500, ), ), subtitle: Text( '${event.username ?? 'Anonyme'} - ${_timeFormat.format(event.timestamp)}', style: theme.textTheme.bodySmall, ), trailing: event.platform != null ? _buildPlatformChip(event.platform!, theme) : null, ); }, ); } Widget _buildDesktopEventsTable(ThemeData theme) { return SingleChildScrollView( scrollDirection: Axis.horizontal, child: DataTable( headingRowHeight: 40, dataRowMinHeight: 40, dataRowMaxHeight: 50, columnSpacing: 24, columns: const [ DataColumn(label: Text('Heure')), DataColumn(label: Text('Type')), DataColumn(label: Text('Utilisateur')), DataColumn(label: Text('Plateforme')), DataColumn(label: Text('IP')), DataColumn(label: Text('Détails')), ], rows: _details!.events.map((event) { return DataRow( cells: [ DataCell(Text(_timeFormat.format(event.timestamp))), DataCell(Row( mainAxisSize: MainAxisSize.min, children: [ _buildEventIcon(event.event), const SizedBox(width: 8), Text(EventTypes.getLabel(event.event)), ], )), DataCell(Text(event.username ?? 'Anonyme')), DataCell(event.platform != null ? _buildPlatformChip(event.platform!, theme) : const Text('-')), DataCell(Text(event.ip ?? '-')), DataCell(Text(event.reason ?? '-')), ], ); }).toList(), ), ); } Widget _buildEventIcon(String eventType) { IconData icon; Color color; switch (eventType) { case 'login_success': icon = Icons.login; color = Colors.green; break; case 'login_failed': icon = Icons.error_outline; color = Colors.red; break; case 'logout': icon = Icons.logout; color = Colors.orange; break; case 'passage_created': icon = Icons.add_circle_outline; color = Colors.blue; break; case 'passage_updated': icon = Icons.edit; color = Colors.teal; break; case 'passage_deleted': icon = Icons.delete_outline; color = Colors.red; break; case 'stripe_payment_success': icon = Icons.credit_card; color = Colors.green; break; case 'stripe_payment_failed': icon = Icons.credit_card_off; color = Colors.red; break; default: icon = Icons.circle; color = Colors.grey; } return Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: Icon(icon, size: 16, color: color), ); } Widget _buildPlatformChip(String platform, ThemeData theme) { IconData icon; switch (platform.toLowerCase()) { case 'ios': icon = Icons.phone_iphone; break; case 'android': icon = Icons.phone_android; break; case 'web': icon = Icons.computer; break; default: icon = Icons.devices; } return Chip( avatar: Icon(icon, size: 14), label: Text( platform.toUpperCase(), style: theme.textTheme.labelSmall, ), padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ); } Future _loadMoreDetails() async { if (_details == null || !_details!.pagination.hasMore) return; final (_, endDate) = _getDateRange(); final detailsDate = _selectionMode == DateSelectionMode.singleDay ? _singleDate : endDate; try { final moreDetails = await _eventStatsService.getDetails( date: detailsDate, event: _selectedEventType, entityId: _selectedEntityId, limit: 50, offset: _details!.events.length, ); setState(() { _details = EventDetails( date: _details!.date, events: [..._details!.events, ...moreDetails.events], pagination: moreDetails.pagination, ); }); } catch (e) { if (mounted) { ApiException.showError(context, e); } } } String _formatAmount(double amount) { return NumberFormat.currency(locale: 'fr_FR', symbol: '€').format(amount); } }