Files
geo/app/lib/presentation/admin/admin_statistics_page.dart

629 lines
23 KiB
Dart
Executable File

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.withOpacity(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<AdminStatisticsPage> createState() => _AdminStatisticsPageState();
}
class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
// Filtres
String _selectedPeriod = 'Jour';
String _selectedSector = 'Tous';
String _selectedMember = 'Tous';
int _daysToShow = 15;
// Liste des périodes
final List<String> _periods = ['Jour', 'Semaine', 'Mois', 'Année'];
// Listes dynamiques pour les secteurs et membres
List<String> _sectors = ['Tous'];
List<String> _members = ['Tous'];
// Listes complètes (non filtrées) pour réinitialisation
List<SectorModel> _allSectors = [];
List<MembreModel> _allMembers = [];
List<UserSectorModel> _userSectors = [];
// Map pour stocker les IDs correspondants
final Map<String, int> _sectorIds = {};
final Map<String, int> _memberIds = {};
@override
void initState() {
super.initState();
_loadData();
}
void _loadData() {
// Charger les secteurs depuis Hive
if (Hive.isBoxOpen(AppKeys.sectorsBoxName)) {
final sectorsBox = Hive.box<SectorModel>(AppKeys.sectorsBoxName);
_allSectors = sectorsBox.values.toList();
}
// Charger les membres depuis Hive
if (Hive.isBoxOpen(AppKeys.membresBoxName)) {
final membresBox = Hive.box<MembreModel>(AppKeys.membresBoxName);
_allMembers = membresBox.values.toList();
}
// Charger les associations user-sector depuis Hive
if (Hive.isBoxOpen(AppKeys.userSectorBoxName)) {
final userSectorBox = Hive.box<UserSectorModel>(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<SectorModel> 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<MembreModel> 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: [
// Titre et description
Text(
'Analyse des statistiques',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: AppTheme.spacingS),
Text(
'Visualisez les statistiques de passages et de collecte pour votre amicale.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
),
const SizedBox(height: AppTheme.spacingL),
// 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<String>(
value: _selectedPeriod,
isDense: true,
isExpanded: true,
items: _periods.map((String period) {
return DropdownMenuItem<String>(
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<int>(
value: _daysToShow,
isDense: true,
isExpanded: true,
items: [7, 15, 30, 60, 90, 180, 365].map((int days) {
return DropdownMenuItem<int>(
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<String>(
value: _selectedSector,
isDense: true,
isExpanded: true,
items: _sectors.map((String sector) {
return DropdownMenuItem<String>(
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<String>(
value: _selectedMember,
isDense: true,
isExpanded: true,
items: _members.map((String member) {
return DropdownMenuItem<String>(
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
List<int> _getMemberIdsForSector(int sectorId) {
return _userSectors
.where((us) => us.fkSector == sectorId)
.map((us) => us.id)
.toList();
}
// Méthode pour déterminer quel userId utiliser pour les graphiques
int? _getUserIdForCharts() {
// Si un membre spécifique est sélectionné, utiliser son ID
if (_selectedMember != 'Tous') {
return _getMemberIdFromName(_selectedMember);
}
// Si un secteur est sélectionné mais pas de membre spécifique
// Les widgets actuels ne supportent pas plusieurs userIds
// Donc on ne peut pas filtrer par secteur pour le moment
// TODO: Implémenter le support multi-users ou sectorId dans les widgets
return null; // Afficher tous les passages
}
// Méthode pour déterminer si on doit afficher tous les passages
bool _shouldShowAllPassages() {
// Afficher tous les passages seulement si aucun filtre n'est appliqué
return _selectedMember == 'Tous' && _selectedSector == 'Tous';
}
}