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:
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user