Restructuration majeure du projet: migration de flutt vers app, ajout de l'API et mise à jour du site web
This commit is contained in:
313
app/lib/presentation/widgets/charts/combined_chart.dart
Normal file
313
app/lib/presentation/widgets/charts/combined_chart.dart
Normal file
@@ -0,0 +1,313 @@
|
||||
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: 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: 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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user