- 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>
314 lines
9.6 KiB
Dart
Executable File
314 lines
9.6 KiB
Dart
Executable File
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.withOpacity(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.withOpacity(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.withOpacity(0.6),
|
|
fontSize: 10,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
reservedSize: 40,
|
|
),
|
|
),
|
|
topTitles: const AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
),
|
|
gridData: FlGridData(
|
|
show: true,
|
|
getDrawingHorizontalLine: (value) {
|
|
return FlLine(
|
|
color: theme.dividerColor.withOpacity(0.2),
|
|
strokeWidth: 1,
|
|
);
|
|
},
|
|
drawVerticalLine: false,
|
|
),
|
|
borderData: FlBorderData(show: false),
|
|
barGroups: _createBarGroups(allDates, passagesByType),
|
|
extraLinesData: const ExtraLinesData(
|
|
horizontalLines: [],
|
|
verticalLines: [],
|
|
extraLinesOnTop: true,
|
|
),
|
|
),
|
|
swapAnimationDuration: 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),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|