import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:geosector_app/core/theme/app_theme.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/core/data/models/sector_model.dart'; import 'package:geosector_app/core/data/models/membre_model.dart'; import 'package:geosector_app/core/data/models/user_sector_model.dart'; import 'package:geosector_app/presentation/widgets/charts/charts.dart'; import 'dart:math' as math; /// Class pour dessiner les petits points blancs sur le fond class DotsPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.white.withValues(alpha: 0.5) ..style = PaintingStyle.fill; final random = math.Random(42); // Seed fixe pour consistance final numberOfDots = (size.width * size.height) ~/ 1500; for (int i = 0; i < numberOfDots; i++) { final x = random.nextDouble() * size.width; final y = random.nextDouble() * size.height; final radius = 1.0 + random.nextDouble() * 2.0; canvas.drawCircle(Offset(x, y), radius, paint); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } class AdminStatisticsPage extends StatefulWidget { const AdminStatisticsPage({super.key}); @override State createState() => _AdminStatisticsPageState(); } class _AdminStatisticsPageState extends State { // Filtres String _selectedPeriod = 'Jour'; String _selectedSector = 'Tous'; String _selectedMember = 'Tous'; int _daysToShow = 15; // Liste des périodes final List _periods = ['Jour', 'Semaine', 'Mois', 'Année']; // Listes dynamiques pour les secteurs et membres List _sectors = ['Tous']; List _members = ['Tous']; // Listes complètes (non filtrées) pour réinitialisation List _allSectors = []; List _allMembers = []; List _userSectors = []; // Map pour stocker les IDs correspondants final Map _sectorIds = {}; final Map _memberIds = {}; @override void initState() { super.initState(); _loadData(); } void _loadData() { // Charger les secteurs depuis Hive if (Hive.isBoxOpen(AppKeys.sectorsBoxName)) { final sectorsBox = Hive.box(AppKeys.sectorsBoxName); _allSectors = sectorsBox.values.toList(); } // Charger les membres depuis Hive if (Hive.isBoxOpen(AppKeys.membresBoxName)) { final membresBox = Hive.box(AppKeys.membresBoxName); _allMembers = membresBox.values.toList(); } // Charger les associations user-sector depuis Hive if (Hive.isBoxOpen(AppKeys.userSectorBoxName)) { final userSectorBox = Hive.box(AppKeys.userSectorBoxName); _userSectors = userSectorBox.values.toList(); } // Initialiser les listes avec toutes les données _updateSectorsList(); _updateMembersList(); } // Mettre à jour la liste des secteurs (filtrée ou complète) void _updateSectorsList({int? forMemberId}) { setState(() { _sectors = ['Tous']; _sectorIds.clear(); List sectorsToShow = _allSectors; // Si un membre est sélectionné, filtrer les secteurs if (forMemberId != null) { final memberSectorIds = _userSectors .where((us) => us.id == forMemberId) .map((us) => us.fkSector) .toSet(); sectorsToShow = _allSectors .where((sector) => memberSectorIds.contains(sector.id)) .toList(); } // Ajouter les secteurs à la liste for (final sector in sectorsToShow) { _sectors.add(sector.libelle); _sectorIds[sector.libelle] = sector.id; } }); } // Mettre à jour la liste des membres (filtrée ou complète) void _updateMembersList({int? forSectorId}) { setState(() { _members = ['Tous']; _memberIds.clear(); List membersToShow = _allMembers; // Si un secteur est sélectionné, filtrer les membres if (forSectorId != null) { final sectorMemberIds = _userSectors .where((us) => us.fkSector == forSectorId) .map((us) => us.id) .toSet(); membersToShow = _allMembers .where((member) => sectorMemberIds.contains(member.id)) .toList(); } // Ajouter les membres à la liste for (final membre in membersToShow) { final fullName = '${membre.firstName} ${membre.name}'.trim(); _members.add(fullName); _memberIds[fullName] = membre.id; } }); } @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; final isDesktop = screenWidth > 800; // Utiliser un Builder simple avec listeners pour les boxes // On écoute les changements et on reconstruit le widget return Stack( children: [ // Fond dégradé avec petits points blancs Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.white, Colors.red.shade300], ), ), child: CustomPaint( painter: DotsPainter(), child: const SizedBox(width: double.infinity, height: double.infinity), ), ), // Contenu de la page SingleChildScrollView( padding: const EdgeInsets.all(AppTheme.spacingL), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Filtres Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), ), color: Colors.white, // Fond opaque child: Padding( padding: const EdgeInsets.all(AppTheme.spacingM), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Filtres', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: AppTheme.spacingM), isDesktop ? Column( children: [ Row( children: [ Expanded(child: _buildPeriodDropdown()), const SizedBox(width: AppTheme.spacingM), Expanded(child: _buildDaysDropdown()), ], ), const SizedBox(height: AppTheme.spacingM), Row( children: [ Expanded(child: _buildSectorDropdown()), const SizedBox(width: AppTheme.spacingM), Expanded(child: _buildMemberDropdown()), ], ), ], ) : Column( children: [ _buildPeriodDropdown(), const SizedBox(height: AppTheme.spacingM), _buildDaysDropdown(), const SizedBox(height: AppTheme.spacingM), _buildSectorDropdown(), const SizedBox(height: AppTheme.spacingM), _buildMemberDropdown(), ], ), ], ), ), ), const SizedBox(height: AppTheme.spacingL), // Graphique d'activité principal Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), ), color: Colors.white, // Fond opaque child: Padding( padding: const EdgeInsets.all(AppTheme.spacingM), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Évolution des passages', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: AppTheme.spacingM), ActivityChart( height: 350, showAllPassages: _selectedMember == 'Tous', // Afficher tous les passages seulement si "Tous" est sélectionné title: '', daysToShow: _daysToShow, periodType: _selectedPeriod, userId: _selectedMember != 'Tous' ? _getMemberIdFromName(_selectedMember) : null, // Note: Le filtre par secteur nécessite une modification du widget ActivityChart // Pour filtrer par secteur, il faudrait ajouter un paramètre sectorId au widget ), ], ), ), ), const SizedBox(height: AppTheme.spacingL), // Graphiques de répartition isDesktop ? Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: _buildChartCard( 'Répartition par type de passage', PassageSummaryCard( title: '', titleColor: AppTheme.primaryColor, titleIcon: Icons.pie_chart, height: 300, useValueListenable: true, showAllPassages: _selectedMember == 'Tous', excludePassageTypes: const [ 2 ], // Exclure "À finaliser" userId: _selectedMember != 'Tous' ? _getMemberIdFromName(_selectedMember) : null, // Note: Le filtre par secteur nécessite une modification du widget PassageSummaryCard isDesktop: MediaQuery.of(context).size.width > 800, ), ), ), const SizedBox(width: AppTheme.spacingM), Expanded( child: _buildChartCard( 'Répartition par mode de paiement', PaymentPieChart( useValueListenable: true, showAllPassages: _selectedMember == 'Tous', userId: _selectedMember != 'Tous' ? _getMemberIdFromName(_selectedMember) : null, size: 300, ), ), ), ], ) : Column( children: [ _buildChartCard( 'Répartition par type de passage', PassageSummaryCard( title: '', titleColor: AppTheme.primaryColor, titleIcon: Icons.pie_chart, height: 300, useValueListenable: true, showAllPassages: _selectedMember == 'Tous', excludePassageTypes: const [ 2 ], // Exclure "À finaliser" userId: _selectedMember != 'Tous' ? _getMemberIdFromName(_selectedMember) : null, // Note: Le filtre par secteur nécessite une modification du widget PassageSummaryCard isDesktop: MediaQuery.of(context).size.width > 800, ), ), const SizedBox(height: AppTheme.spacingM), _buildChartCard( 'Répartition par mode de paiement', PaymentPieChart( useValueListenable: true, showAllPassages: _selectedMember == 'Tous', userId: _selectedMember != 'Tous' ? _getMemberIdFromName(_selectedMember) : null, size: 300, ), ), ], ), ], ), ), ], ); } // Dropdown pour la période Widget _buildPeriodDropdown() { return InputDecorator( decoration: InputDecoration( labelText: 'Période', border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), ), contentPadding: const EdgeInsets.symmetric( horizontal: AppTheme.spacingM, vertical: AppTheme.spacingS, ), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: _selectedPeriod, isDense: true, isExpanded: true, items: _periods.map((String period) { return DropdownMenuItem( value: period, child: Text(period), ); }).toList(), onChanged: (String? newValue) { if (newValue != null) { setState(() { _selectedPeriod = newValue; }); } }, ), ), ); } // Dropdown pour le nombre de jours Widget _buildDaysDropdown() { return InputDecorator( decoration: InputDecoration( labelText: 'Nombre de jours', border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), ), contentPadding: const EdgeInsets.symmetric( horizontal: AppTheme.spacingM, vertical: AppTheme.spacingS, ), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: _daysToShow, isDense: true, isExpanded: true, items: [7, 15, 30, 60, 90, 180, 365].map((int days) { return DropdownMenuItem( value: days, child: Text('$days jours'), ); }).toList(), onChanged: (int? newValue) { if (newValue != null) { setState(() { _daysToShow = newValue; }); } }, ), ), ); } // Dropdown pour les secteurs Widget _buildSectorDropdown() { return InputDecorator( decoration: InputDecoration( labelText: 'Secteur', border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), ), contentPadding: const EdgeInsets.symmetric( horizontal: AppTheme.spacingM, vertical: AppTheme.spacingS, ), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: _selectedSector, isDense: true, isExpanded: true, items: _sectors.map((String sector) { return DropdownMenuItem( value: sector, child: Text(sector), ); }).toList(), onChanged: (String? newValue) { if (newValue != null) { setState(() { _selectedSector = newValue; // Si "Tous" est sélectionné, réinitialiser la liste des membres if (newValue == 'Tous') { _updateMembersList(); // Garder le membre sélectionné s'il existe } else { // Sinon, filtrer les membres pour ce secteur final sectorId = _getSectorIdFromName(newValue); _updateMembersList(forSectorId: sectorId); // Si le membre actuellement sélectionné n'est pas dans la liste filtrée if (_selectedMember == 'Tous' || !_members.contains(_selectedMember)) { // Auto-sélectionner le premier membre du secteur (après "Tous") // Puisque chaque secteur a au moins un membre, il y aura toujours un membre à sélectionner if (_members.length > 1) { _selectedMember = _members[1]; // Index 1 car 0 est "Tous" } } // Si le membre sélectionné est dans la liste, on le garde // Les graphiques afficheront ses données } }); } }, ), ), ); } // Dropdown pour les membres Widget _buildMemberDropdown() { return InputDecorator( decoration: InputDecoration( labelText: 'Membre', border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), ), contentPadding: const EdgeInsets.symmetric( horizontal: AppTheme.spacingM, vertical: AppTheme.spacingS, ), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: _selectedMember, isDense: true, isExpanded: true, items: _members.map((String member) { return DropdownMenuItem( value: member, child: Text(member), ); }).toList(), onChanged: (String? newValue) { if (newValue != null) { setState(() { _selectedMember = newValue; // Si "Tous" est sélectionné, réinitialiser la liste des secteurs if (newValue == 'Tous') { _updateSectorsList(); // On peut réinitialiser le secteur car "Tous" les membres = pas de filtre secteur pertinent _selectedSector = 'Tous'; } else { // Sinon, filtrer les secteurs pour ce membre final memberId = _getMemberIdFromName(newValue); _updateSectorsList(forMemberId: memberId); // Si le secteur actuellement sélectionné n'est plus dans la liste, réinitialiser if (_selectedSector != 'Tous' && !_sectors.contains(_selectedSector)) { _selectedSector = 'Tous'; } // Si le secteur est toujours dans la liste, on le garde sélectionné } }); } }, ), ), ); } // Widget pour envelopper un graphique dans une carte Widget _buildChartCard(String title, Widget chart) { return Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), ), color: Colors.white, // Fond opaque child: Padding( padding: const EdgeInsets.all(AppTheme.spacingM), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: AppTheme.spacingM), chart, ], ), ), ); } // Méthode utilitaire pour obtenir l'ID membre à partir de son nom int? _getMemberIdFromName(String name) { if (name == 'Tous') return null; return _memberIds[name]; } // Méthode utilitaire pour obtenir l'ID du secteur à partir de son nom int? _getSectorIdFromName(String name) { if (name == 'Tous') return null; return _sectorIds[name]; } // Méthode pour obtenir tous les IDs des membres d'un secteur // Méthode pour déterminer quel userId utiliser pour les graphiques // Méthode pour déterminer si on doit afficher tous les passages }