feat: Version 3.3.5 - Optimisations pages, améliorations ergonomie et affichages dynamiques stats

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
pierre
2025-10-06 15:32:32 +02:00
parent 570a1fa1f0
commit 21657a3820
31 changed files with 1982 additions and 1442 deletions

View File

@@ -7,6 +7,8 @@ import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/data/models/user_sector_model.dart';
import 'package:geosector_app/core/services/current_user_service.dart';
import 'package:go_router/go_router.dart';
/// Widget de graphique d'activité affichant les passages
class ActivityChart extends StatefulWidget {
@@ -51,6 +53,9 @@ class ActivityChart extends StatefulWidget {
/// Utiliser ValueListenableBuilder pour la mise à jour automatique
final bool useValueListenable;
/// Afficher les boutons de sélection de période (7j, 14j, 21j)
final bool showPeriodButtons;
const ActivityChart({
super.key,
this.passageData,
@@ -66,6 +71,7 @@ class ActivityChart extends StatefulWidget {
this.columnSpacing = 0.2,
this.showAllPassages = false,
this.useValueListenable = true,
this.showPeriodButtons = false,
});
@override
@@ -94,9 +100,14 @@ class _ActivityChartState extends State<ActivityChart>
// Contrôleur de zoom pour le graphique
late ZoomPanBehavior _zoomPanBehavior;
// Période sélectionnée pour le filtre (7, 14 ou 21 jours)
late int _selectedDays;
@override
void initState() {
super.initState();
_selectedDays = widget.daysToShow;
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
@@ -157,7 +168,7 @@ class _ActivityChartState extends State<ActivityChart>
valueListenable:
Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
builder: (context, Box<PassageModel> passagesBox, child) {
final chartData = _calculateActivityData(passagesBox);
final chartData = _calculateActivityData(passagesBox, _selectedDays);
return _buildChart(chartData);
},
);
@@ -179,7 +190,7 @@ class _ActivityChartState extends State<ActivityChart>
}
/// Calcule les données d'activité depuis la Hive box
List<ActivityData> _calculateActivityData(Box<PassageModel> passagesBox) {
List<ActivityData> _calculateActivityData(Box<PassageModel> passagesBox, int daysToShow) {
try {
final passages = passagesBox.values.toList();
final currentUser = userRepository.getCurrentUser();
@@ -187,55 +198,63 @@ class _ActivityChartState extends State<ActivityChart>
// Pour les users : récupérer les secteurs assignés
Set<int>? userSectorIds;
if (!widget.showAllPassages && currentUser != null) {
final userSectorBox = Hive.box<UserSectorModel>(AppKeys.userSectorBoxName);
userSectorIds = userSectorBox.values
.where((us) => us.id == currentUser.id)
.map((us) => us.fkSector)
.toSet();
final userSectors = userRepository.getUserSectors();
userSectorIds = userSectors.map((sector) => sector.id).toSet();
debugPrint('ActivityChart: Mode USER - Secteurs assignés: $userSectorIds');
} else {
debugPrint('ActivityChart: Mode ADMIN - Tous les passages');
}
// Calculer la date de début (nombre de jours en arrière)
final endDate = DateTime.now();
final startDate = endDate.subtract(Duration(days: widget.daysToShow - 1));
final startDate = endDate.subtract(Duration(days: daysToShow - 1));
debugPrint('ActivityChart: Période du ${DateFormat('yyyy-MM-dd').format(startDate)} au ${DateFormat('yyyy-MM-dd').format(endDate)}');
debugPrint('ActivityChart: Nombre total de passages: ${passages.length}');
// Préparer les données par date
final Map<String, Map<int, int>> dataByDate = {};
// Initialiser toutes les dates de la période
for (int i = 0; i < widget.daysToShow; i++) {
for (int i = 0; i < daysToShow; i++) {
final date = startDate.add(Duration(days: i));
final dateStr = DateFormat('yyyy-MM-dd').format(date);
dataByDate[dateStr] = {};
// Initialiser tous les types de passage possibles
for (final typeId in AppKeys.typesPassages.keys) {
if (!widget.excludePassageTypes.contains(typeId)) {
dataByDate[dateStr]![typeId] = 0;
}
dataByDate[dateStr]![typeId] = 0;
}
}
// Parcourir les passages et les compter par date et type
int includedCount = 0;
for (final passage in passages) {
// Appliquer les filtres
bool shouldInclude = true;
String excludeReason = '';
// Filtrer par secteurs assignés si nécessaire (pour les users)
if (userSectorIds != null && !userSectorIds.contains(passage.fkSector)) {
shouldInclude = false;
excludeReason = 'Secteur ${passage.fkSector} non assigné';
}
// Exclure certains types
if (widget.excludePassageTypes.contains(passage.fkType)) {
// Exclure les passages de type 2 (À finaliser) avec nbPassages = 0
if (shouldInclude && passage.fkType == 2 && passage.nbPassages == 0) {
shouldInclude = false;
excludeReason = 'Type 2 avec nbPassages=0';
}
// Vérifier si le passage est dans la période
final passageDate = passage.passedAt;
if (passageDate == null ||
if (shouldInclude && (passageDate == null ||
passageDate.isBefore(startDate) ||
passageDate.isAfter(endDate)) {
passageDate.isAfter(endDate))) {
shouldInclude = false;
excludeReason = passageDate == null
? 'Date null'
: 'Hors période (${DateFormat('yyyy-MM-dd').format(passageDate)})';
}
if (shouldInclude && passageDate != null) {
@@ -243,10 +262,15 @@ class _ActivityChartState extends State<ActivityChart>
if (dataByDate.containsKey(dateStr)) {
dataByDate[dateStr]![passage.fkType] =
(dataByDate[dateStr]![passage.fkType] ?? 0) + 1;
includedCount++;
}
} else if (!shouldInclude && userSectorIds != null) {
debugPrint('ActivityChart: Passage #${passage.id} exclu - $excludeReason (type=${passage.fkType}, secteur=${passage.fkSector}, date=${passageDate != null ? DateFormat('yyyy-MM-dd').format(passageDate) : 'null'})');
}
}
debugPrint('ActivityChart: Passages inclus dans le graphique: $includedCount');
// Convertir en liste d'ActivityData
final List<ActivityData> chartData = [];
dataByDate.forEach((dateStr, passagesByType) {
@@ -367,16 +391,32 @@ class _ActivityChartState extends State<ActivityChart>
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre
// En-tête avec titre et boutons de filtre
if (widget.title.isNotEmpty)
Padding(
padding: const EdgeInsets.only(
left: 16.0, right: 16.0, top: 16.0, bottom: 8.0),
child: Text(
widget.title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
if (widget.showPeriodButtons)
Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildPeriodButton(7),
const SizedBox(width: 4),
_buildPeriodButton(14),
const SizedBox(width: 4),
_buildPeriodButton(21),
],
),
],
),
),
// Graphique
@@ -434,10 +474,8 @@ class _ActivityChartState extends State<ActivityChart>
return series;
}
// Obtenir tous les types de passage (sauf ceux exclus)
final passageTypes = AppKeys.typesPassages.keys
.where((typeId) => !widget.excludePassageTypes.contains(typeId))
.toList();
// Obtenir tous les types de passage
final passageTypes = AppKeys.typesPassages.keys.toList();
// Créer les séries pour les passages (colonnes empilées)
for (final typeId in passageTypes) {
@@ -481,6 +519,10 @@ class _ActivityChartState extends State<ActivityChart>
),
markerSettings: const MarkerSettings(isVisible: false),
animationDuration: 1500,
// Ajouter le callback de clic uniquement depuis home_page
onPointTap: widget.showPeriodButtons ? (ChartPointDetails details) {
_handlePointTap(details, typeId);
} : null,
),
);
}
@@ -488,4 +530,86 @@ class _ActivityChartState extends State<ActivityChart>
return series;
}
/// Gère le clic sur un point du graphique
void _handlePointTap(ChartPointDetails details, int typeId) {
if (details.pointIndex == null || details.pointIndex! < 0) return;
// Récupérer les données du point cliqué
final passageBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
final passages = passageBox.values.toList();
// Calculer la date de début (nombre de jours en arrière)
final endDate = DateTime.now();
final startDate = endDate.subtract(Duration(days: _selectedDays - 1));
// Créer les données d'activité
final chartData = _calculateActivityData(passageBox, _selectedDays);
if (details.pointIndex! >= chartData.length) return;
final clickedData = chartData[details.pointIndex!];
final clickedDate = clickedData.date;
// Réinitialiser tous les filtres sauf celui sélectionné
final settingsBox = Hive.box(AppKeys.settingsBoxName);
settingsBox.delete('history_selectedPaymentTypeId');
settingsBox.delete('history_selectedSectorId');
settingsBox.delete('history_selectedSectorName');
settingsBox.delete('history_selectedMemberId');
// Sauvegarder le type de passage et les dates (début et fin de journée)
settingsBox.put('history_selectedTypeId', typeId);
// Date de début : début de la journée cliquée
final startDateTime = DateTime(clickedDate.year, clickedDate.month, clickedDate.day, 0, 0, 0);
settingsBox.put('history_startDate', startDateTime.millisecondsSinceEpoch);
// Date de fin : fin de la journée cliquée
final endDateTime = DateTime(clickedDate.year, clickedDate.month, clickedDate.day, 23, 59, 59);
settingsBox.put('history_endDate', endDateTime.millisecondsSinceEpoch);
// Naviguer vers la page historique
final bool isAdmin = CurrentUserService.instance.shouldShowAdminUI;
context.go(isAdmin ? '/admin/history' : '/user/history');
}
/// Construit un bouton de sélection de période
Widget _buildPeriodButton(int days) {
final isSelected = _selectedDays == days;
return InkWell(
onTap: () {
setState(() {
_selectedDays = days;
_animationController.reset();
_animationController.forward();
});
widget.onPeriodChanged?.call(days);
},
borderRadius: BorderRadius.circular(4),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).colorScheme.primary
: Colors.grey.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: isSelected
? Theme.of(context).colorScheme.primary
: Colors.grey.shade400,
width: 1,
),
),
child: Text(
'${days}j',
style: TextStyle(
fontSize: 12,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? Colors.white : Colors.grey.shade700,
),
),
),
);
}
}