feat: Version 3.5.2 - Configuration Stripe et gestion des immeubles

- Configuration complète Stripe pour les 3 environnements (DEV/REC/PROD)
  * DEV: Clés TEST Pierre (mode test)
  * REC: Clés TEST Client (mode test)
  * PROD: Clés LIVE Client (mode live)
- Ajout de la gestion des bases de données immeubles/bâtiments
  * Configuration buildings_database pour DEV/REC/PROD
  * Service BuildingService pour enrichissement des adresses
- Optimisations pages et améliorations ergonomie
- Mises à jour des dépendances Composer
- Nettoyage des fichiers obsolètes

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
pierre
2025-11-09 18:26:27 +01:00
parent 21657a3820
commit 2f5946a184
812 changed files with 142105 additions and 25992 deletions

View File

@@ -6,7 +6,6 @@ import 'package:syncfusion_flutter_charts/charts.dart';
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';
@@ -190,7 +189,8 @@ class _ActivityChartState extends State<ActivityChart>
}
/// Calcule les données d'activité depuis la Hive box
List<ActivityData> _calculateActivityData(Box<PassageModel> passagesBox, int daysToShow) {
List<ActivityData> _calculateActivityData(
Box<PassageModel> passagesBox, int daysToShow) {
try {
final passages = passagesBox.values.toList();
final currentUser = userRepository.getCurrentUser();
@@ -200,7 +200,8 @@ class _ActivityChartState extends State<ActivityChart>
if (!widget.showAllPassages && currentUser != null) {
final userSectors = userRepository.getUserSectors();
userSectorIds = userSectors.map((sector) => sector.id).toSet();
debugPrint('ActivityChart: Mode USER - Secteurs assignés: $userSectorIds');
debugPrint(
'ActivityChart: Mode USER - Secteurs assignés: $userSectorIds');
} else {
debugPrint('ActivityChart: Mode ADMIN - Tous les passages');
}
@@ -209,7 +210,8 @@ class _ActivityChartState extends State<ActivityChart>
final endDate = DateTime.now();
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: 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
@@ -232,29 +234,25 @@ class _ActivityChartState extends State<ActivityChart>
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)) {
if (userSectorIds != null &&
!userSectorIds.contains(passage.fkSector)) {
shouldInclude = false;
excludeReason = 'Secteur ${passage.fkSector} non assigné';
}
// 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 (shouldInclude && (passageDate == null ||
passageDate.isBefore(startDate) ||
passageDate.isAfter(endDate))) {
if (shouldInclude &&
(passageDate == null ||
passageDate.isBefore(startDate) ||
passageDate.isAfter(endDate))) {
shouldInclude = false;
excludeReason = passageDate == null
? 'Date null'
: 'Hors période (${DateFormat('yyyy-MM-dd').format(passageDate)})';
}
if (shouldInclude && passageDate != null) {
@@ -264,12 +262,16 @@ class _ActivityChartState extends State<ActivityChart>
(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'})');
}
// Debug désactivé pour éviter la pollution de la console avec les passages type 2 sans date
// 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');
debugPrint(
'ActivityChart: Passages inclus dans le graphique: $includedCount');
// Convertir en liste d'ActivityData
final List<ActivityData> chartData = [];
@@ -520,9 +522,11 @@ 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,
onPointTap: widget.showPeriodButtons
? (ChartPointDetails details) {
_handlePointTap(details, typeId);
}
: null,
),
);
}
@@ -537,11 +541,6 @@ class _ActivityChartState extends State<ActivityChart>
// 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);
@@ -562,11 +561,13 @@ class _ActivityChartState extends State<ActivityChart>
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);
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);
final endDateTime = DateTime(
clickedDate.year, clickedDate.month, clickedDate.day, 23, 59, 59);
settingsBox.put('history_endDate', endDateTime.millisecondsSinceEpoch);
// Naviguer vers la page historique
@@ -592,7 +593,7 @@ class _ActivityChartState extends State<ActivityChart>
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).colorScheme.primary
: Colors.grey.withValues(alpha: 0.1),
: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: isSelected

View File

@@ -1,10 +1,6 @@
/// Bibliothèque de widgets de graphiques pour l'application GeoSector
library geosector_charts;
export 'payment_data.dart';
export 'payment_summary_card.dart';
export 'passage_data.dart';
export 'passage_utils.dart';
export 'passage_summary_card.dart';
export 'activity_chart.dart';
export 'combined_chart.dart';

View File

@@ -1,313 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:geosector_app/presentation/widgets/charts/passage_data.dart';
import 'package:geosector_app/presentation/widgets/charts/passage_utils.dart';
import 'package:intl/intl.dart';
/// Widget de graphique combiné pour afficher les passages et règlements
class CombinedChart extends StatelessWidget {
/// Liste des données de passage par type
final List<Map<String, dynamic>> passageData;
/// Liste des données de règlement par type
final List<Map<String, dynamic>> paymentData;
/// Type de période (Jour, Semaine, Mois, Année)
final String periodType;
/// Hauteur du graphique
final double height;
/// Largeur des barres
final double barWidth;
/// Rayon des points sur les lignes
final double dotRadius;
/// Épaisseur des lignes
final double lineWidth;
/// Montant maximum pour l'axe Y des règlements
final double? maxYAmount;
/// Nombre maximum pour l'axe Y des passages
final int? maxYCount;
const CombinedChart({
super.key,
required this.passageData,
required this.paymentData,
this.periodType = 'Jour',
this.height = 300,
this.barWidth = 16,
this.dotRadius = 4,
this.lineWidth = 3,
this.maxYAmount,
this.maxYCount,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// Convertir les données brutes en modèles structurés
final passagesByType = PassageUtils.getPassageDataByType(passageData);
final paymentsByType = PassageUtils.getPaymentDataByType(paymentData);
// Extraire les dates uniques pour l'axe X
final List<DateTime> allDates = [];
for (final data in passageData) {
final DateTime date = data['date'] is DateTime
? data['date']
: DateTime.parse(data['date']);
if (!allDates.any((d) =>
d.year == date.year && d.month == date.month && d.day == date.day)) {
allDates.add(date);
}
}
// Trier les dates
allDates.sort((a, b) => a.compareTo(b));
// Calculer le maximum pour les axes Y
double maxAmount = 0;
for (final typeData in paymentsByType) {
for (final data in typeData) {
if (data.amount > maxAmount) {
maxAmount = data.amount;
}
}
}
int maxCount = 0;
for (final typeData in passagesByType) {
for (final data in typeData) {
if (data.count > maxCount) {
maxCount = data.count;
}
}
}
// Utiliser les maximums fournis ou calculés
final effectiveMaxYAmount = maxYAmount ?? (maxAmount * 1.2).ceilToDouble();
final effectiveMaxYCount = maxYCount ?? (maxCount * 1.2).ceil();
return SizedBox(
height: height,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: effectiveMaxYCount.toDouble(),
barTouchData: BarTouchData(
touchTooltipData: BarTouchTooltipData(
tooltipPadding: const EdgeInsets.all(8),
tooltipMargin: 8,
getTooltipItem: (group, groupIndex, rod, rodIndex) {
final date = allDates[group.x.toInt()];
final formattedDate = DateFormat('dd/MM').format(date);
// Calculer le total des passages pour cette date
int totalPassages = 0;
for (final typeData in passagesByType) {
for (final data in typeData) {
if (data.date.year == date.year &&
data.date.month == date.month &&
data.date.day == date.day) {
totalPassages += data.count;
}
}
}
return BarTooltipItem(
'$formattedDate: $totalPassages passages',
TextStyle(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
);
},
),
),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: (value, meta) {
if (value >= 0 && value < allDates.length) {
final date = allDates[value.toInt()];
final formattedDate =
PassageUtils.formatDateForChart(date, periodType);
return SideTitleWidget(
meta: meta,
space: 8,
child: Text(
formattedDate,
style: TextStyle(
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
fontSize: 10,
),
),
);
}
return const SizedBox();
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
return SideTitleWidget(
meta: meta,
space: 8,
child: Text(
value.toInt().toString(),
style: TextStyle(
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
fontSize: 10,
),
),
);
},
reservedSize: 30,
),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
// Convertir la valeur de l'axe Y des passages à l'échelle des montants
final amountValue =
(value / effectiveMaxYCount) * effectiveMaxYAmount;
return SideTitleWidget(
meta: meta,
space: 8,
child: Text(
'${amountValue.toInt()}',
style: TextStyle(
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
fontSize: 10,
),
),
);
},
reservedSize: 40,
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
gridData: FlGridData(
show: true,
getDrawingHorizontalLine: (value) {
return FlLine(
color: theme.dividerColor.withValues(alpha: 0.2),
strokeWidth: 1,
);
},
drawVerticalLine: false,
),
borderData: FlBorderData(show: false),
barGroups: _createBarGroups(allDates, passagesByType),
extraLinesData: const ExtraLinesData(
horizontalLines: [],
verticalLines: [],
extraLinesOnTop: true,
),
),
duration: const Duration(milliseconds: 250),
),
);
}
/// Créer les groupes de barres pour les passages
List<BarChartGroupData> _createBarGroups(
List<DateTime> allDates,
List<List<PassageData>> passagesByType,
) {
final List<BarChartGroupData> groups = [];
for (int i = 0; i < allDates.length; i++) {
final date = allDates[i];
// Calculer le total des passages pour cette date
int totalPassages = 0;
for (final typeData in passagesByType) {
for (final data in typeData) {
if (data.date.year == date.year &&
data.date.month == date.month &&
data.date.day == date.day) {
totalPassages += data.count;
}
}
}
// Créer un groupe de barres pour cette date
groups.add(
BarChartGroupData(
x: i,
barRods: [
BarChartRodData(
toY: totalPassages.toDouble(),
color: Colors.blue.shade700,
width: barWidth,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(6),
topRight: Radius.circular(6),
),
),
],
),
);
}
return groups;
}
}
/// Widget de légende pour le graphique combiné
class CombinedChartLegend extends StatelessWidget {
const CombinedChartLegend({super.key});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 16,
runSpacing: 8,
children: [
_buildLegendItem('Passages', Colors.blue.shade700, isBar: true),
_buildLegendItem('Espèces', const Color(0xFF4CAF50)),
_buildLegendItem('Chèques', const Color(0xFF2196F3)),
_buildLegendItem('CB', const Color(0xFFF44336)),
],
);
}
/// Créer un élément de légende
Widget _buildLegendItem(String label, Color color, {bool isBar = false}) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: color,
shape: isBar ? BoxShape.rectangle : BoxShape.circle,
borderRadius: isBar ? BorderRadius.circular(3) : null,
),
),
const SizedBox(width: 4),
Text(
label,
style: const TextStyle(fontSize: 12),
),
],
);
}
}

View File

@@ -184,7 +184,7 @@ class _PassageSummaryCardState extends State<PassageSummaryCard>
widget.backgroundIcon,
size: widget.backgroundIconSize,
color: (widget.backgroundIconColor ?? AppTheme.primaryColor)
.withValues(alpha: widget.backgroundIconOpacity),
.withOpacity(widget.backgroundIconOpacity),
),
),
),

View File

@@ -161,7 +161,7 @@ class _PaymentSummaryCardState extends State<PaymentSummaryCard>
widget.backgroundIcon,
size: widget.backgroundIconSize,
color: (widget.backgroundIconColor ?? Colors.blue)
.withValues(alpha: widget.backgroundIconOpacity),
.withOpacity(widget.backgroundIconOpacity),
),
),
),
@@ -422,10 +422,10 @@ class _PaymentSummaryCardState extends State<PaymentSummaryCard>
// En mode user, filtrer uniquement les passages créés par l'utilisateur (fkUser)
final currentUser = userRepository.getCurrentUser();
final int? filterUserId = widget.showAllPayments ? null : currentUser?.id;
final int? filterUserId = widget.showAllPayments ? null : currentUser?.opeUserId;
for (final passage in passagesBox.values) {
// En mode user, ne compter que les passages de l'utilisateur
// En mode user, ne compter que les passages de l'utilisateur (comparer avec ope_users.id)
if (filterUserId != null && passage.fkUser != filterUserId) {
continue;
}