Amélioration de la splash_page et du login
This commit is contained in:
@@ -1,18 +1,17 @@
|
||||
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
||||
import 'package:flutter/foundation.dart' show listEquals;
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/core/repositories/passage_repository.dart';
|
||||
import 'package:geosector_app/core/repositories/user_repository.dart';
|
||||
import 'package:geosector_app/core/services/passage_data_service.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||
|
||||
/// Widget de graphique d'activité affichant les passages
|
||||
class ActivityChart extends StatefulWidget {
|
||||
/// Liste des données de passage par date et type (si fournie directement)
|
||||
/// Format attendu: [{"date": String, "type_passage": int, "nb": int}, ...]
|
||||
/// Si useValueListenable est true, ce paramètre est ignoré
|
||||
final List<Map<String, dynamic>>? passageData;
|
||||
|
||||
/// Type de période (Jour, Semaine, Mois, Année)
|
||||
@@ -30,9 +29,6 @@ class ActivityChart extends StatefulWidget {
|
||||
/// Types de passages à exclure (par défaut [2] = "À finaliser")
|
||||
final List<int> excludePassageTypes;
|
||||
|
||||
/// Indique si les données doivent être chargées depuis la Hive box
|
||||
final bool loadFromHive;
|
||||
|
||||
/// Callback appelé lorsque la période change
|
||||
final Function(int days)? onPeriodChanged;
|
||||
|
||||
@@ -51,8 +47,8 @@ class ActivityChart extends StatefulWidget {
|
||||
/// Si vrai, n'applique aucun filtrage par utilisateur (affiche tous les passages)
|
||||
final bool showAllPassages;
|
||||
|
||||
/// Si vrai, force le rechargement des données
|
||||
final bool forceRefresh;
|
||||
/// Utiliser ValueListenableBuilder pour la mise à jour automatique
|
||||
final bool useValueListenable;
|
||||
|
||||
const ActivityChart({
|
||||
super.key,
|
||||
@@ -62,16 +58,14 @@ class ActivityChart extends StatefulWidget {
|
||||
this.daysToShow = 15,
|
||||
this.userId,
|
||||
this.excludePassageTypes = const [2],
|
||||
this.loadFromHive = false,
|
||||
this.onPeriodChanged,
|
||||
this.title = 'Dernière activité enregistrée sur 15 jours',
|
||||
this.showDataLabels = true,
|
||||
this.columnWidth = 0.8,
|
||||
this.columnSpacing = 0.2,
|
||||
this.showAllPassages = false,
|
||||
this.forceRefresh = false,
|
||||
}) : assert(loadFromHive || passageData != null,
|
||||
'Soit loadFromHive doit être true, soit passageData doit être fourni');
|
||||
this.useValueListenable = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ActivityChart> createState() => _ActivityChartState();
|
||||
@@ -96,16 +90,6 @@ class _ActivityChartState extends State<ActivityChart>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
|
||||
// Données pour les graphiques
|
||||
List<Map<String, dynamic>> _passageData = [];
|
||||
List<ActivityData> _chartData = [];
|
||||
bool _isLoading = true;
|
||||
bool _hasData = false;
|
||||
bool _dataLoaded = false;
|
||||
|
||||
// Période sélectionnée en jours
|
||||
int _selectedDays = 15;
|
||||
|
||||
// Contrôleur de zoom pour le graphique
|
||||
late ZoomPanBehavior _zoomPanBehavior;
|
||||
|
||||
@@ -117,9 +101,6 @@ class _ActivityChartState extends State<ActivityChart>
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
);
|
||||
|
||||
// Initialiser la période sélectionnée avec la valeur par défaut du widget
|
||||
_selectedDays = widget.daysToShow;
|
||||
|
||||
// Initialiser le contrôleur de zoom
|
||||
_zoomPanBehavior = ZoomPanBehavior(
|
||||
enablePinching: true,
|
||||
@@ -128,83 +109,29 @@ class _ActivityChartState extends State<ActivityChart>
|
||||
zoomMode: ZoomMode.x,
|
||||
);
|
||||
|
||||
_loadData();
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
/// Trouve la date du passage le plus récent
|
||||
DateTime _getMostRecentDate() {
|
||||
final allDates = [
|
||||
..._passageData.map((data) => DateTime.parse(data['date'] as String)),
|
||||
];
|
||||
if (allDates.isEmpty) {
|
||||
return DateTime.now();
|
||||
}
|
||||
return allDates.reduce((a, b) => a.isAfter(b) ? a : b);
|
||||
}
|
||||
@override
|
||||
void didUpdateWidget(ActivityChart oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
void _loadData() {
|
||||
// Marquer comme chargé immédiatement pour éviter les appels multiples pendant le chargement
|
||||
// Mais permettre un rechargement ultérieur si nécessaire
|
||||
if (_dataLoaded && _hasData) return;
|
||||
// Vérifier si les propriétés importantes ont changé
|
||||
final bool periodChanged = oldWidget.periodType != widget.periodType ||
|
||||
oldWidget.daysToShow != widget.daysToShow;
|
||||
final bool dataSourceChanged = widget.useValueListenable
|
||||
? false
|
||||
: oldWidget.passageData != widget.passageData;
|
||||
final bool filteringChanged = oldWidget.userId != widget.userId ||
|
||||
!listEquals(
|
||||
oldWidget.excludePassageTypes, widget.excludePassageTypes) ||
|
||||
oldWidget.showAllPassages != widget.showAllPassages ||
|
||||
oldWidget.useValueListenable != widget.useValueListenable;
|
||||
|
||||
_dataLoaded = true;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
if (widget.loadFromHive) {
|
||||
// Charger les données depuis Hive
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Éviter de recharger si le widget a été démonté entre-temps
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
// Utiliser les instances globales définies dans app.dart
|
||||
|
||||
// Créer une instance du service de données
|
||||
final passageDataService = PassageDataService(
|
||||
passageRepository: passageRepository,
|
||||
userRepository: userRepository,
|
||||
);
|
||||
|
||||
// Utiliser le service pour charger les données
|
||||
_passageData = passageDataService.loadPassageData(
|
||||
daysToShow: _selectedDays,
|
||||
excludePassageTypes: widget.excludePassageTypes,
|
||||
userId: widget.userId,
|
||||
showAllPassages: widget.showAllPassages,
|
||||
);
|
||||
|
||||
_prepareChartData();
|
||||
|
||||
// Mettre à jour l'état une seule fois après avoir préparé les données
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_hasData = _chartData.isNotEmpty;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// En cas d'erreur, réinitialiser l'état pour permettre une future tentative
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_hasData = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Utiliser les données fournies directement
|
||||
_passageData = widget.passageData ?? [];
|
||||
_prepareChartData();
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_hasData = _chartData.isNotEmpty;
|
||||
});
|
||||
// Si des paramètres importants ont changé, relancer l'animation
|
||||
if (periodChanged || dataSourceChanged || filteringChanged) {
|
||||
_animationController.reset();
|
||||
_animationController.forward();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,47 +142,141 @@ class _ActivityChartState extends State<ActivityChart>
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ActivityChart oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
// Vérifier si les propriétés importantes ont changé
|
||||
final bool periodChanged = oldWidget.periodType != widget.periodType ||
|
||||
oldWidget.daysToShow != widget.daysToShow;
|
||||
final bool dataSourceChanged = widget.loadFromHive
|
||||
? false
|
||||
: oldWidget.passageData != widget.passageData;
|
||||
final bool filteringChanged = oldWidget.userId != widget.userId ||
|
||||
!listEquals(
|
||||
oldWidget.excludePassageTypes, widget.excludePassageTypes) ||
|
||||
oldWidget.showAllPassages != widget.showAllPassages;
|
||||
final bool refreshForced = widget.forceRefresh && !oldWidget.forceRefresh;
|
||||
|
||||
// Si des paramètres importants ont changé ou si forceRefresh est passé à true, recharger les données
|
||||
if (periodChanged ||
|
||||
dataSourceChanged ||
|
||||
filteringChanged ||
|
||||
refreshForced) {
|
||||
_selectedDays = widget.daysToShow;
|
||||
_dataLoaded = false; // Réinitialiser l'état pour forcer le rechargement
|
||||
_loadData();
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.useValueListenable) {
|
||||
return _buildWithValueListenable();
|
||||
} else {
|
||||
return _buildWithStaticData();
|
||||
}
|
||||
}
|
||||
|
||||
// La méthode _loadPassageDataFromHive a été intégrée directement dans _loadData
|
||||
// pour éviter les appels multiples et les problèmes de cycle de vie
|
||||
/// Construction du widget avec ValueListenableBuilder pour mise à jour automatique
|
||||
Widget _buildWithValueListenable() {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable:
|
||||
Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
|
||||
builder: (context, Box<PassageModel> passagesBox, child) {
|
||||
final chartData = _calculateActivityData(passagesBox);
|
||||
return _buildChart(chartData);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Prépare les données pour le graphique
|
||||
void _prepareChartData() {
|
||||
/// Construction du widget avec des données statiques
|
||||
Widget _buildWithStaticData() {
|
||||
if (widget.passageData == null) {
|
||||
return SizedBox(
|
||||
height: widget.height,
|
||||
child: const Center(
|
||||
child: Text('Aucune donnée fournie'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final chartData = _prepareChartDataFromPassageData(widget.passageData!);
|
||||
return _buildChart(chartData);
|
||||
}
|
||||
|
||||
/// Calcule les données d'activité depuis la Hive box
|
||||
List<ActivityData> _calculateActivityData(Box<PassageModel> passagesBox) {
|
||||
try {
|
||||
// Vérifier que les données sont disponibles
|
||||
if (_passageData.isEmpty) {
|
||||
_chartData = [];
|
||||
return;
|
||||
final passages = passagesBox.values.toList();
|
||||
final currentUser = userRepository.getCurrentUser();
|
||||
|
||||
// Déterminer l'utilisateur cible selon les filtres
|
||||
final int? targetUserId =
|
||||
widget.showAllPassages ? null : (widget.userId ?? currentUser?.id);
|
||||
|
||||
// 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));
|
||||
|
||||
// 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++) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parcourir les passages et les compter par date et type
|
||||
for (final passage in passages) {
|
||||
// Appliquer les filtres
|
||||
bool shouldInclude = true;
|
||||
|
||||
// Filtrer par utilisateur si nécessaire
|
||||
if (targetUserId != null && passage.fkUser != targetUserId) {
|
||||
shouldInclude = false;
|
||||
}
|
||||
|
||||
// Exclure certains types
|
||||
if (widget.excludePassageTypes.contains(passage.fkType)) {
|
||||
shouldInclude = false;
|
||||
}
|
||||
|
||||
// Vérifier si le passage est dans la période
|
||||
final passageDate = passage.passedAt;
|
||||
if (passageDate.isBefore(startDate) || passageDate.isAfter(endDate)) {
|
||||
shouldInclude = false;
|
||||
}
|
||||
|
||||
if (shouldInclude) {
|
||||
final dateStr = DateFormat('yyyy-MM-dd').format(passageDate);
|
||||
if (dataByDate.containsKey(dateStr)) {
|
||||
dataByDate[dateStr]![passage.fkType] =
|
||||
(dataByDate[dateStr]![passage.fkType] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convertir en liste d'ActivityData
|
||||
final List<ActivityData> chartData = [];
|
||||
dataByDate.forEach((dateStr, passagesByType) {
|
||||
final dateParts = dateStr.split('-');
|
||||
if (dateParts.length == 3) {
|
||||
try {
|
||||
final date = DateTime(
|
||||
int.parse(dateParts[0]),
|
||||
int.parse(dateParts[1]),
|
||||
int.parse(dateParts[2]),
|
||||
);
|
||||
|
||||
chartData.add(ActivityData(
|
||||
date: date,
|
||||
dateStr: dateStr,
|
||||
passagesByType: passagesByType,
|
||||
));
|
||||
} catch (e) {
|
||||
debugPrint('Erreur de conversion de date: $dateStr');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Trier par date
|
||||
chartData.sort((a, b) => a.date.compareTo(b.date));
|
||||
return chartData;
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du calcul des données d\'activité: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Prépare les données pour le graphique à partir des données de passage brutes (ancien système)
|
||||
List<ActivityData> _prepareChartDataFromPassageData(
|
||||
List<Map<String, dynamic>> passageData) {
|
||||
try {
|
||||
// Obtenir toutes les dates uniques
|
||||
final Set<String> uniqueDatesSet = {};
|
||||
for (final data in _passageData) {
|
||||
for (final data in passageData) {
|
||||
if (data.containsKey('date') && data['date'] != null) {
|
||||
uniqueDatesSet.add(data['date'] as String);
|
||||
}
|
||||
@@ -266,7 +287,7 @@ class _ActivityChartState extends State<ActivityChart>
|
||||
uniqueDates.sort();
|
||||
|
||||
// Créer les données pour chaque date
|
||||
_chartData = [];
|
||||
final List<ActivityData> chartData = [];
|
||||
for (final dateStr in uniqueDates) {
|
||||
final passagesByType = <int, int>{};
|
||||
|
||||
@@ -278,7 +299,7 @@ class _ActivityChartState extends State<ActivityChart>
|
||||
}
|
||||
|
||||
// Remplir les données de passage
|
||||
for (final data in _passageData) {
|
||||
for (final data in passageData) {
|
||||
if (data.containsKey('date') &&
|
||||
data['date'] == dateStr &&
|
||||
data.containsKey('type_passage') &&
|
||||
@@ -301,37 +322,29 @@ class _ActivityChartState extends State<ActivityChart>
|
||||
final date = DateTime(year, month, day);
|
||||
|
||||
// Ajouter les données à la liste
|
||||
_chartData.add(ActivityData(
|
||||
chartData.add(ActivityData(
|
||||
date: date,
|
||||
dateStr: dateStr,
|
||||
passagesByType: passagesByType,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
// Silencieux lors des erreurs de conversion de date pour éviter les logs excessifs
|
||||
debugPrint('Erreur de conversion de date: $dateStr');
|
||||
}
|
||||
}
|
||||
|
||||
// Trier les données par date
|
||||
_chartData.sort((a, b) => a.date.compareTo(b.date));
|
||||
chartData.sort((a, b) => a.date.compareTo(b.date));
|
||||
return chartData;
|
||||
} catch (e) {
|
||||
// Erreur silencieuse pour éviter les logs excessifs
|
||||
_chartData = [];
|
||||
debugPrint('Erreur lors de la préparation des données: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return SizedBox(
|
||||
height: widget.height,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!_hasData || _chartData.isEmpty) {
|
||||
/// Construit le graphique avec les données fournies
|
||||
Widget _buildChart(List<ActivityData> chartData) {
|
||||
if (chartData.isEmpty) {
|
||||
return SizedBox(
|
||||
height: widget.height,
|
||||
child: const Center(
|
||||
@@ -340,17 +353,12 @@ class _ActivityChartState extends State<ActivityChart>
|
||||
);
|
||||
}
|
||||
|
||||
// Préparer les données si nécessaire
|
||||
if (_chartData.isEmpty) {
|
||||
_prepareChartData();
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: widget.height,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Titre (conservé)
|
||||
// Titre
|
||||
if (widget.title.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
@@ -362,13 +370,13 @@ class _ActivityChartState extends State<ActivityChart>
|
||||
),
|
||||
),
|
||||
),
|
||||
// Graphique (occupe maintenant plus d'espace)
|
||||
// Graphique
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 16.0),
|
||||
child: SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
legend: Legend(
|
||||
legend: const Legend(
|
||||
isVisible: true,
|
||||
position: LegendPosition.bottom,
|
||||
overflowMode: LegendItemOverflowMode.wrap,
|
||||
@@ -378,30 +386,25 @@ class _ActivityChartState extends State<ActivityChart>
|
||||
intervalType: DateTimeIntervalType.days,
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
labelStyle: const TextStyle(fontSize: 10),
|
||||
// Définir explicitement la plage de dates à afficher
|
||||
minimum: _chartData.isNotEmpty ? _chartData.first.date : null,
|
||||
maximum: _chartData.isNotEmpty ? _chartData.last.date : null,
|
||||
// Assurer que tous les jours sont affichés
|
||||
minimum: chartData.isNotEmpty ? chartData.first.date : null,
|
||||
maximum: chartData.isNotEmpty ? chartData.last.date : null,
|
||||
interval: 1,
|
||||
axisLabelFormatter: (AxisLabelRenderDetails details) {
|
||||
return ChartAxisLabel(details.text, details.textStyle);
|
||||
},
|
||||
),
|
||||
primaryYAxis: NumericAxis(
|
||||
labelStyle: const TextStyle(fontSize: 10),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
majorGridLines: const MajorGridLines(
|
||||
primaryYAxis: const NumericAxis(
|
||||
labelStyle: TextStyle(fontSize: 10),
|
||||
axisLine: AxisLine(width: 0),
|
||||
majorTickLines: MajorTickLines(size: 0),
|
||||
majorGridLines: MajorGridLines(
|
||||
width: 0.5,
|
||||
color: Colors.grey,
|
||||
dashArray: <double>[5, 5], // Motif de pointillés
|
||||
dashArray: <double>[5, 5],
|
||||
),
|
||||
title: const AxisTitle(
|
||||
title: AxisTitle(
|
||||
text: 'Passages',
|
||||
textStyle: TextStyle(fontSize: 10, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
series: _buildSeries(),
|
||||
series: _buildSeries(chartData),
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
zoomPanBehavior: _zoomPanBehavior,
|
||||
),
|
||||
@@ -413,11 +416,12 @@ class _ActivityChartState extends State<ActivityChart>
|
||||
}
|
||||
|
||||
/// Construit les séries de données pour le graphique
|
||||
List<CartesianSeries<ActivityData, DateTime>> _buildSeries() {
|
||||
List<CartesianSeries<ActivityData, DateTime>> _buildSeries(
|
||||
List<ActivityData> chartData) {
|
||||
final List<CartesianSeries<ActivityData, DateTime>> series = [];
|
||||
|
||||
// Vérifier que les données sont disponibles
|
||||
if (_chartData.isEmpty) {
|
||||
if (chartData.isEmpty) {
|
||||
return series;
|
||||
}
|
||||
|
||||
@@ -444,25 +448,19 @@ class _ActivityChartState extends State<ActivityChart>
|
||||
|
||||
// Calculer le total pour ce type pour déterminer s'il faut l'afficher
|
||||
int totalForType = 0;
|
||||
for (final data in _chartData) {
|
||||
for (final data in chartData) {
|
||||
totalForType += data.passagesByType[typeId] ?? 0;
|
||||
}
|
||||
|
||||
// On peut décider de ne pas afficher les types sans données
|
||||
final addZeroValueTypes = false;
|
||||
|
||||
// Ajouter la série pour ce type
|
||||
if (totalForType > 0 || addZeroValueTypes) {
|
||||
// Ajouter la série pour ce type si elle a des données
|
||||
if (totalForType > 0) {
|
||||
series.add(
|
||||
StackedColumnSeries<ActivityData, DateTime>(
|
||||
name: typeName,
|
||||
dataSource: _chartData,
|
||||
dataSource: chartData,
|
||||
xValueMapper: (ActivityData data, _) => data.date,
|
||||
yValueMapper: (ActivityData data, _) {
|
||||
final value = data.passagesByType.containsKey(typeId)
|
||||
? data.passagesByType[typeId]!
|
||||
: 0;
|
||||
return value;
|
||||
return data.passagesByType[typeId] ?? 0;
|
||||
},
|
||||
color: typeColor,
|
||||
width: widget.columnWidth,
|
||||
|
||||
Reference in New Issue
Block a user