feat: Gestion des secteurs et migration v3.0.4+304

- Ajout système complet de gestion des secteurs avec contours géographiques
- Import des contours départementaux depuis GeoJSON
- API REST pour la gestion des secteurs (/api/sectors)
- Service de géolocalisation pour déterminer les secteurs
- Migration base de données avec tables x_departements_contours et sectors_adresses
- Interface Flutter pour visualisation et gestion des secteurs
- Ajout thème sombre dans l'application
- Corrections diverses et optimisations

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
pierre
2025-08-07 11:01:45 +02:00
parent 3bbc599ab4
commit 1018b86537
620 changed files with 120502 additions and 91396 deletions

View File

@@ -225,11 +225,13 @@ class _ActivityChartState extends State<ActivityChart>
// Vérifier si le passage est dans la période
final passageDate = passage.passedAt;
if (passageDate.isBefore(startDate) || passageDate.isAfter(endDate)) {
if (passageDate == null ||
passageDate.isBefore(startDate) ||
passageDate.isAfter(endDate)) {
shouldInclude = false;
}
if (shouldInclude) {
if (shouldInclude && passageDate != null) {
final dateStr = DateFormat('yyyy-MM-dd').format(passageDate);
if (dataByDate.containsKey(dateStr)) {
dataByDate[dateStr]![passage.fkType] =

0
app/lib/presentation/widgets/charts/charts.dart Normal file → Executable file
View File

View File

@@ -198,7 +198,7 @@ class CombinedChart extends StatelessWidget {
reservedSize: 40,
),
),
topTitles: AxisTitles(
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
@@ -214,7 +214,7 @@ class CombinedChart extends StatelessWidget {
),
borderData: FlBorderData(show: false),
barGroups: _createBarGroups(allDates, passagesByType),
extraLinesData: ExtraLinesData(
extraLinesData: const ExtraLinesData(
horizontalLines: [],
verticalLines: [],
extraLinesOnTop: true,

0
app/lib/presentation/widgets/charts/passage_data.dart Normal file → Executable file
View File

View File

@@ -110,13 +110,15 @@ class _PassagePieChartState extends State<PassagePieChart>
_animationController.forward();
}
@override
void didUpdateWidget(PassagePieChart oldWidget) {
super.didUpdateWidget(oldWidget);
// Relancer l'animation si les paramètres importants ont changé
final bool shouldResetAnimation = oldWidget.userId != widget.userId ||
!listEquals(oldWidget.excludePassageTypes, widget.excludePassageTypes) ||
!listEquals(
oldWidget.excludePassageTypes, widget.excludePassageTypes) ||
oldWidget.showAllPassages != widget.showAllPassages ||
oldWidget.useValueListenable != widget.useValueListenable;
@@ -144,7 +146,8 @@ class _PassagePieChartState extends State<PassagePieChart>
/// Construction du widget avec ValueListenableBuilder pour mise à jour automatique
Widget _buildWithValueListenable() {
return ValueListenableBuilder(
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
valueListenable:
Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
builder: (context, Box<PassageModel> passagesBox, child) {
final chartData = _calculatePassageData(passagesBox);
return _buildChart(chartData);
@@ -192,7 +195,7 @@ class _PassagePieChartState extends State<PassagePieChart>
if (shouldInclude) {
passagesByType[passage.fkType] =
(passagesByType[passage.fkType] ?? 0) + 1;
(passagesByType[passage.fkType] ?? 0) + 1;
}
}
@@ -204,7 +207,8 @@ class _PassagePieChartState extends State<PassagePieChart>
}
/// Prépare les données pour le graphique en camembert à partir d'une Map
List<PassageChartData> _prepareChartDataFromMap(Map<int, int> passagesByType) {
List<PassageChartData> _prepareChartDataFromMap(
Map<int, int> passagesByType) {
final List<PassageChartData> chartData = [];
// Créer les données du graphique
@@ -247,12 +251,12 @@ class _PassagePieChartState extends State<PassagePieChart>
final explodeAnimation = CurvedAnimation(
parent: _animationController,
curve: Interval(0.7, 1.0, curve: Curves.elasticOut),
curve: const Interval(0.7, 1.0, curve: Curves.elasticOut),
);
final opacityAnimation = CurvedAnimation(
parent: _animationController,
curve: Interval(0.1, 0.5, curve: Curves.easeIn),
curve: const Interval(0.1, 0.5, curve: Curves.easeIn),
);
return AnimatedBuilder(

View File

@@ -89,7 +89,8 @@ class PassageSummaryCard extends StatelessWidget {
child: Icon(
backgroundIcon,
size: backgroundIconSize,
color: (backgroundIconColor ?? AppTheme.primaryColor).withOpacity(backgroundIconOpacity),
color: (backgroundIconColor ?? AppTheme.primaryColor)
.withOpacity(backgroundIconOpacity),
),
),
),
@@ -118,7 +119,7 @@ class PassageSummaryCard extends StatelessWidget {
? _buildPassagesListWithValueListenable()
: _buildPassagesListWithStaticData(),
),
// Séparateur vertical
if (isDesktop) const VerticalDivider(width: 24),
@@ -157,7 +158,8 @@ class PassageSummaryCard extends StatelessWidget {
/// Construction du titre avec ValueListenableBuilder
Widget _buildTitleWithValueListenable() {
return ValueListenableBuilder(
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
valueListenable:
Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
builder: (context, Box<PassageModel> passagesBox, child) {
final totalUserPassages = _calculateUserPassagesCount(passagesBox);
@@ -181,7 +183,8 @@ class PassageSummaryCard extends StatelessWidget {
),
),
Text(
customTotalDisplay?.call(totalUserPassages) ?? totalUserPassages.toString(),
customTotalDisplay?.call(totalUserPassages) ??
totalUserPassages.toString(),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
@@ -196,8 +199,9 @@ class PassageSummaryCard extends StatelessWidget {
/// Construction du titre avec données statiques
Widget _buildTitleWithStaticData() {
final totalPassages = passagesByType?.values.fold(0, (sum, count) => sum + count) ?? 0;
final totalPassages =
passagesByType?.values.fold(0, (sum, count) => sum + count) ?? 0;
return Row(
children: [
if (titleIcon != null) ...[
@@ -232,7 +236,8 @@ class PassageSummaryCard extends StatelessWidget {
/// Construction de la liste des passages avec ValueListenableBuilder
Widget _buildPassagesListWithValueListenable() {
return ValueListenableBuilder(
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
valueListenable:
Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
builder: (context, Box<PassageModel> passagesBox, child) {
final passagesCounts = _calculatePassagesCounts(passagesBox);
@@ -293,7 +298,7 @@ class PassageSummaryCard extends StatelessWidget {
],
),
);
}).toList(),
}),
],
);
}
@@ -309,7 +314,7 @@ class PassageSummaryCard extends StatelessWidget {
// Pour les utilisateurs : seulement leurs passages
final currentUser = userRepository.getCurrentUser();
final targetUserId = userId ?? currentUser?.id;
if (targetUserId == null) return 0;
return passagesBox.values
@@ -338,7 +343,7 @@ class PassageSummaryCard extends StatelessWidget {
// Pour les utilisateurs : compter seulement leurs passages
final currentUser = userRepository.getCurrentUser();
final targetUserId = userId ?? currentUser?.id;
if (targetUserId != null) {
for (final passage in passagesBox.values) {
if (passage.fkUser == targetUserId) {
@@ -350,4 +355,4 @@ class PassageSummaryCard extends StatelessWidget {
return counts;
}
}
}

0
app/lib/presentation/widgets/charts/passage_utils.dart Normal file → Executable file
View File

0
app/lib/presentation/widgets/charts/payment_data.dart Normal file → Executable file
View File

View File

@@ -102,15 +102,15 @@ class _PaymentPieChartState extends State<PaymentPieChart>
} else if (!widget.useValueListenable) {
// Pour les données statiques, comparer les éléments
if (oldWidget.payments.length != widget.payments.length) {
shouldResetAnimation = true;
} else {
shouldResetAnimation = true;
} else {
for (int i = 0; i < oldWidget.payments.length; i++) {
if (i >= widget.payments.length) break;
if (oldWidget.payments[i].amount != widget.payments[i].amount ||
oldWidget.payments[i].title != widget.payments[i].title) {
shouldResetAnimation = true;
break;
}
}
}
}
}
@@ -131,20 +131,21 @@ class _PaymentPieChartState extends State<PaymentPieChart>
Widget build(BuildContext context) {
if (widget.useValueListenable) {
return _buildWithValueListenable();
} else {
} else {
return _buildWithStaticData();
}
}
}
/// Construction du widget avec ValueListenableBuilder pour mise à jour automatique
Widget _buildWithValueListenable() {
return ValueListenableBuilder(
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
valueListenable:
Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
builder: (context, Box<PassageModel> passagesBox, child) {
final paymentData = _calculatePaymentData(passagesBox);
return _buildChart(paymentData);
},
);
},
);
}
/// Construction du widget avec des données statiques
@@ -153,85 +154,86 @@ class _PaymentPieChartState extends State<PaymentPieChart>
}
/// Calcule les données de règlement depuis la Hive box
List<PaymentData> _calculatePaymentData(Box<PassageModel> passagesBox) {
try {
final passages = passagesBox.values.toList();
final currentUser = userRepository.getCurrentUser();
final int? currentUserId = widget.userId ?? currentUser?.id;
List<PaymentData> _calculatePaymentData(Box<PassageModel> passagesBox) {
try {
final passages = passagesBox.values.toList();
final currentUser = userRepository.getCurrentUser();
final int? currentUserId = widget.userId ?? currentUser?.id;
// Initialiser les montants par type de règlement
final Map<int, double> paymentAmounts = {
0: 0.0, // Pas de règlement
1: 0.0, // Espèces
2: 0.0, // Chèques
3: 0.0, // CB
};
// Initialiser les montants par type de règlement
final Map<int, double> paymentAmounts = {
0: 0.0, // Pas de règlement
1: 0.0, // Espèces
2: 0.0, // Chèques
3: 0.0, // CB
};
// Parcourir les passages et calculer les montants par type de règlement
for (final passage in passages) {
// Vérifier si le passage appartient à l'utilisateur actuel
if (currentUserId != null && passage.fkUser == currentUserId) {
final int typeReglement = passage.fkTypeReglement;
// Parcourir les passages et calculer les montants par type de règlement
for (final passage in passages) {
// Vérifier si le passage appartient à l'utilisateur actuel
if (currentUserId != null && passage.fkUser == currentUserId) {
final int typeReglement = passage.fkTypeReglement;
// Convertir la chaîne de montant en double
double montant = 0.0;
try {
// Gérer les formats possibles (virgule ou point)
String montantStr = passage.montant.replaceAll(',', '.');
montant = double.tryParse(montantStr) ?? 0.0;
} catch (e) {
debugPrint('Erreur de conversion du montant: ${passage.montant}');
}
// Convertir la chaîne de montant en double
double montant = 0.0;
try {
// Gérer les formats possibles (virgule ou point)
String montantStr = passage.montant.replaceAll(',', '.');
montant = double.tryParse(montantStr) ?? 0.0;
} catch (e) {
debugPrint('Erreur de conversion du montant: ${passage.montant}');
}
// Ne compter que les passages avec un montant > 0
if (montant > 0) {
// Ajouter au montant total par type de règlement
if (paymentAmounts.containsKey(typeReglement)) {
paymentAmounts[typeReglement] =
(paymentAmounts[typeReglement] ?? 0.0) + montant;
} else {
// Si le type n'est pas dans notre map, l'ajouter à la catégorie par défaut
paymentAmounts[0] = (paymentAmounts[0] ?? 0.0) + montant;
// Ne compter que les passages avec un montant > 0
if (montant > 0) {
// Ajouter au montant total par type de règlement
if (paymentAmounts.containsKey(typeReglement)) {
paymentAmounts[typeReglement] =
(paymentAmounts[typeReglement] ?? 0.0) + montant;
} else {
// Si le type n'est pas dans notre map, l'ajouter à la catégorie par défaut
paymentAmounts[0] = (paymentAmounts[0] ?? 0.0) + montant;
}
}
}
}
}
// Convertir le Map en List<PaymentData>
final List<PaymentData> paymentDataList = [];
// Convertir le Map en List<PaymentData>
final List<PaymentData> paymentDataList = [];
paymentAmounts.forEach((typeReglement, montant) {
if (montant > 0) { // Ne retourner que les types avec un montant > 0
// Récupérer les informations depuis AppKeys.typesReglements
final reglementInfo = AppKeys.typesReglements[typeReglement];
if (reglementInfo != null) {
paymentDataList.add(PaymentData(
typeId: typeReglement,
title: reglementInfo['titre'] as String,
amount: montant,
color: Color(reglementInfo['couleur'] as int),
icon: reglementInfo['icon_data'] as IconData,
));
} else {
// Fallback pour les types non définis
paymentDataList.add(PaymentData(
typeId: typeReglement,
title: 'Type inconnu',
amount: montant,
color: Colors.grey,
icon: Icons.help_outline,
));
paymentAmounts.forEach((typeReglement, montant) {
if (montant > 0) {
// Ne retourner que les types avec un montant > 0
// Récupérer les informations depuis AppKeys.typesReglements
final reglementInfo = AppKeys.typesReglements[typeReglement];
if (reglementInfo != null) {
paymentDataList.add(PaymentData(
typeId: typeReglement,
title: reglementInfo['titre'] as String,
amount: montant,
color: Color(reglementInfo['couleur'] as int),
icon: reglementInfo['icon_data'] as IconData,
));
} else {
// Fallback pour les types non définis
paymentDataList.add(PaymentData(
typeId: typeReglement,
title: 'Type inconnu',
amount: montant,
color: Colors.grey,
icon: Icons.help_outline,
));
}
}
});
return paymentDataList;
} catch (e) {
debugPrint('Erreur lors du calcul des données de règlement: $e');
return [];
}
}
});
return paymentDataList;
} catch (e) {
debugPrint('Erreur lors du calcul des données de règlement: $e');
return [];
}
}
/// Construit le graphique avec les données fournies
Widget _buildChart(List<PaymentData> paymentData) {
@@ -256,12 +258,12 @@ paymentAmounts.forEach((typeReglement, montant) {
final explodeAnimation = CurvedAnimation(
parent: _animationController,
curve: Interval(0.7, 1.0, curve: Curves.elasticOut),
curve: const Interval(0.7, 1.0, curve: Curves.elasticOut),
);
final opacityAnimation = CurvedAnimation(
parent: _animationController,
curve: Interval(0.1, 0.5, curve: Curves.easeIn),
curve: const Interval(0.1, 0.5, curve: Curves.easeIn),
);
return AnimatedBuilder(

View File

@@ -86,7 +86,8 @@ class PaymentSummaryCard extends StatelessWidget {
child: Icon(
backgroundIcon,
size: backgroundIconSize,
color: (backgroundIconColor ?? Colors.blue).withOpacity(backgroundIconOpacity),
color: (backgroundIconColor ?? Colors.blue)
.withOpacity(backgroundIconOpacity),
),
),
),
@@ -115,7 +116,7 @@ class PaymentSummaryCard extends StatelessWidget {
? _buildPaymentsListWithValueListenable()
: _buildPaymentsListWithStaticData(),
),
// Séparateur vertical
if (isDesktop) const VerticalDivider(width: 24),
@@ -126,7 +127,10 @@ class PaymentSummaryCard extends StatelessWidget {
padding: const EdgeInsets.all(8.0),
child: PaymentPieChart(
useValueListenable: useValueListenable,
payments: useValueListenable ? [] : _convertMapToPaymentData(paymentsByType ?? {}),
payments: useValueListenable
? []
: _convertMapToPaymentData(
paymentsByType ?? {}),
userId: showAllPayments ? null : userId,
size: double.infinity,
labelSize: 12,
@@ -157,7 +161,8 @@ class PaymentSummaryCard extends StatelessWidget {
/// Construction du titre avec ValueListenableBuilder
Widget _buildTitleWithValueListenable() {
return ValueListenableBuilder(
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
valueListenable:
Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
builder: (context, Box<PassageModel> passagesBox, child) {
final paymentStats = _calculatePaymentStats(passagesBox);
@@ -181,8 +186,8 @@ class PaymentSummaryCard extends StatelessWidget {
),
),
Text(
customTotalDisplay?.call(paymentStats['totalAmount']) ??
'${paymentStats['totalAmount'].toStringAsFixed(2)}',
customTotalDisplay?.call(paymentStats['totalAmount']) ??
'${paymentStats['totalAmount'].toStringAsFixed(2)}',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
@@ -197,8 +202,9 @@ class PaymentSummaryCard extends StatelessWidget {
/// Construction du titre avec données statiques
Widget _buildTitleWithStaticData() {
final totalAmount = paymentsByType?.values.fold(0.0, (sum, amount) => sum + amount) ?? 0.0;
final totalAmount =
paymentsByType?.values.fold(0.0, (sum, amount) => sum + amount) ?? 0.0;
return Row(
children: [
if (titleIcon != null) ...[
@@ -219,7 +225,8 @@ class PaymentSummaryCard extends StatelessWidget {
),
),
Text(
customTotalDisplay?.call(totalAmount) ?? '${totalAmount.toStringAsFixed(2)}',
customTotalDisplay?.call(totalAmount) ??
'${totalAmount.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
@@ -233,7 +240,8 @@ class PaymentSummaryCard extends StatelessWidget {
/// Construction de la liste des règlements avec ValueListenableBuilder
Widget _buildPaymentsListWithValueListenable() {
return ValueListenableBuilder(
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
valueListenable:
Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
builder: (context, Box<PassageModel> passagesBox, child) {
final paymentAmounts = _calculatePaymentAmounts(passagesBox);
@@ -294,7 +302,7 @@ class PaymentSummaryCard extends StatelessWidget {
],
),
);
}).toList(),
}),
],
);
}
@@ -330,7 +338,7 @@ class PaymentSummaryCard extends StatelessWidget {
// Pour les utilisateurs : seulement leurs règlements
final currentUser = userRepository.getCurrentUser();
final targetUserId = userId ?? currentUser?.id;
if (targetUserId == null) {
return {'passagesCount': 0, 'totalAmount': 0.0};
}
@@ -388,7 +396,8 @@ class PaymentSummaryCard extends StatelessWidget {
if (montant > 0) {
if (paymentAmounts.containsKey(typeReglement)) {
paymentAmounts[typeReglement] = (paymentAmounts[typeReglement] ?? 0.0) + montant;
paymentAmounts[typeReglement] =
(paymentAmounts[typeReglement] ?? 0.0) + montant;
} else {
paymentAmounts[0] = (paymentAmounts[0] ?? 0.0) + montant;
}
@@ -398,7 +407,7 @@ class PaymentSummaryCard extends StatelessWidget {
// Pour les utilisateurs : compter seulement leurs règlements
final currentUser = userRepository.getCurrentUser();
final targetUserId = userId ?? currentUser?.id;
if (targetUserId != null) {
for (final passage in passagesBox.values) {
if (passage.fkUser == targetUserId) {
@@ -415,7 +424,8 @@ class PaymentSummaryCard extends StatelessWidget {
if (montant > 0) {
if (paymentAmounts.containsKey(typeReglement)) {
paymentAmounts[typeReglement] = (paymentAmounts[typeReglement] ?? 0.0) + montant;
paymentAmounts[typeReglement] =
(paymentAmounts[typeReglement] ?? 0.0) + montant;
} else {
paymentAmounts[0] = (paymentAmounts[0] ?? 0.0) + montant;
}
@@ -433,10 +443,11 @@ class PaymentSummaryCard extends StatelessWidget {
final List<PaymentData> paymentDataList = [];
paymentsMap.forEach((typeReglement, montant) {
if (montant > 0) { // Ne retourner que les types avec un montant > 0
if (montant > 0) {
// Ne retourner que les types avec un montant > 0
// Récupérer les informations depuis AppKeys.typesReglements
final reglementInfo = AppKeys.typesReglements[typeReglement];
if (reglementInfo != null) {
paymentDataList.add(PaymentData(
typeId: typeReglement,
@@ -457,7 +468,7 @@ class PaymentSummaryCard extends StatelessWidget {
}
}
});
return paymentDataList;
}
}
}