Files
geo/app/lib/presentation/admin/admin_dashboard_home_page.dart

1114 lines
43 KiB
Dart

import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:math' as math;
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/presentation/widgets/charts/activity_chart.dart';
import 'package:geosector_app/presentation/widgets/charts/passage_pie_chart.dart';
import 'package:geosector_app/presentation/widgets/charts/payment_pie_chart.dart';
import 'package:geosector_app/presentation/widgets/charts/payment_data.dart';
import 'package:geosector_app/presentation/widgets/sector_distribution_card.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/core/repositories/passage_repository.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/data/models/operation_model.dart';
import 'package:geosector_app/core/data/models/sector_model.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/shared/app_theme.dart';
/// Class pour dessiner les petits points blancs sur le fond
class DotsPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.5)
..style = PaintingStyle.fill;
final random = math.Random(42); // Seed fixe pour consistance
final numberOfDots = (size.width * size.height) ~/ 1500;
for (int i = 0; i < numberOfDots; i++) {
final x = random.nextDouble() * size.width;
final y = random.nextDouble() * size.height;
final radius = 1.0 + random.nextDouble() * 2.0;
canvas.drawCircle(Offset(x, y), radius, paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
class AdminDashboardHomePage extends StatefulWidget {
const AdminDashboardHomePage({Key? key}) : super(key: key);
@override
State<AdminDashboardHomePage> createState() => _AdminDashboardHomePageState();
}
class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
// Données pour le tableau de bord
int totalPassages = 0;
double totalAmounts = 0.0;
List<Map<String, dynamic>> memberStats = [];
bool isDataLoaded = false;
bool isLoading = true;
bool isFirstLoad = true; // Pour suivre le premier chargement
// Données pour les graphiques
List<PaymentData> paymentData = [];
Map<int, int> passagesByType = {};
// Future pour initialiser les boîtes Hive
late Future<void> _initFuture;
@override
void initState() {
super.initState();
// Initialiser les boîtes Hive avant de charger les données
_initFuture = _initHiveBoxes().then((_) {
// Charger les données une fois les boîtes initialisées
_loadDashboardData();
// Après l'affichage des logs "VERIFICATION FINALE DES DONNEES",
// attendre un court délai puis rafraîchir automatiquement les données
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
setState(() {
isLoading = true; // Afficher le spinner pendant le rafraîchissement
});
_loadDashboardData(); // Rafraîchir les données
}
});
});
}
// Méthode pour initialiser les boîtes Hive nécessaires
Future<void> _initHiveBoxes() async {
try {
debugPrint('AdminDashboardHomePage: Initialisation des boîtes Hive...');
// Liste des boîtes à ouvrir
final boxesToOpen = [
{
'name': AppKeys.operationsBoxName,
'type': 'OperationModel',
'opened': false
},
{
'name': AppKeys.passagesBoxName,
'type': 'PassageModel',
'opened': false
},
{
'name': AppKeys.sectorsBoxName,
'type': 'SectorModel',
'opened': false
},
];
// Ouvrir chaque boîte
for (final boxInfo in boxesToOpen) {
final boxName = boxInfo['name'] as String;
if (!Hive.isBoxOpen(boxName)) {
debugPrint(
'AdminDashboardHomePage: Ouverture de la boîte $boxName...');
try {
switch (boxInfo['type']) {
case 'OperationModel':
await Hive.openBox<OperationModel>(boxName);
break;
case 'PassageModel':
await Hive.openBox<PassageModel>(boxName);
break;
case 'SectorModel':
await Hive.openBox<SectorModel>(boxName);
break;
}
boxInfo['opened'] = true;
debugPrint(
'AdminDashboardHomePage: Boîte $boxName ouverte avec succès');
} catch (boxError) {
debugPrint(
'AdminDashboardHomePage: Erreur lors de l\'ouverture de la boîte $boxName: $boxError');
// Continuer malgré l'erreur
}
} else {
boxInfo['opened'] = true;
debugPrint('AdminDashboardHomePage: Boîte $boxName déjà ouverte');
}
}
// Vérifier si toutes les boîtes ont été ouvertes
final allBoxesOpened = boxesToOpen.every((box) => box['opened'] == true);
if (allBoxesOpened) {
debugPrint(
'AdminDashboardHomePage: Toutes les boîtes Hive ont été ouvertes avec succès');
} else {
// Identifier les boîtes qui n'ont pas pu être ouvertes
final failedBoxes = boxesToOpen
.where((box) => box['opened'] == false)
.map((box) => box['name'])
.join(', ');
debugPrint(
'AdminDashboardHomePage: Certaines boîtes n\'ont pas pu être ouvertes: $failedBoxes');
}
// Afficher le nombre d'éléments dans chaque boîte
debugPrint('VERIFICATION FINALE DES DONNEES');
if (Hive.isBoxOpen(AppKeys.operationsBoxName)) {
final operationsBox =
Hive.box<OperationModel>(AppKeys.operationsBoxName);
debugPrint('Nombre d\'opérations: ${operationsBox.length}');
}
if (Hive.isBoxOpen(AppKeys.passagesBoxName)) {
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
debugPrint('Nombre de passages: ${passagesBox.length}');
}
if (Hive.isBoxOpen(AppKeys.sectorsBoxName)) {
final sectorsBox = Hive.box<SectorModel>(AppKeys.sectorsBoxName);
debugPrint('Nombre de secteurs: ${sectorsBox.length}');
}
debugPrint(
'AdminDashboardHomePage: Initialisation des boîtes Hive terminée');
} catch (e) {
debugPrint(
'AdminDashboardHomePage: Erreur lors de l\'initialisation des boîtes Hive: $e');
// Ne pas propager l'erreur, mais retourner normalement
// pour éviter que le FutureBuilder ne reste bloqué en état d'erreur
}
}
/// Prépare les données pour le graphique de paiement
void _preparePaymentData(List<dynamic> passages) {
// Réinitialiser les données
paymentData = [];
// Compter les montants par type de règlement
Map<int, double> paymentAmounts = {};
// Initialiser les compteurs pour tous les types de règlement
for (final typeId in AppKeys.typesReglements.keys) {
paymentAmounts[typeId] = 0.0;
}
// Calculer les montants par type de règlement
for (final passage in passages) {
if (passage.fkTypeReglement != null &&
passage.montant != null &&
passage.montant.isNotEmpty) {
final typeId = passage.fkTypeReglement;
final amount = double.tryParse(passage.montant) ?? 0.0;
paymentAmounts[typeId] = (paymentAmounts[typeId] ?? 0.0) + amount;
}
}
// Créer les objets PaymentData
paymentAmounts.forEach((typeId, amount) {
if (amount > 0 && AppKeys.typesReglements.containsKey(typeId)) {
final typeInfo = AppKeys.typesReglements[typeId]!;
paymentData.add(PaymentData(
typeId: typeId,
amount: amount,
title: typeInfo['titre'] as String,
color: Color(typeInfo['couleur'] as int),
icon: typeInfo['icon_data'] as IconData,
));
}
});
}
Future<void> _loadDashboardData() async {
if (mounted) {
setState(() {
isLoading = true;
});
}
try {
debugPrint(
'AdminDashboardHomePage: Chargement des données du tableau de bord...');
// Utiliser les instances globales définies dans app.dart
// Pas besoin de Provider.of car les instances sont déjà disponibles
// S'assurer que la boîte des opérations est ouverte avant d'y accéder
OperationModel? currentOperation;
try {
// Vérifier si la boîte Hive est ouverte
if (!Hive.isBoxOpen(AppKeys.operationsBoxName)) {
debugPrint(
'AdminDashboardHomePage: Ouverture de la boîte operations dans _loadDashboardData...');
try {
await Hive.openBox<OperationModel>(AppKeys.operationsBoxName);
debugPrint(
'AdminDashboardHomePage: Boîte operations ouverte avec succès dans _loadDashboardData');
} catch (boxError) {
debugPrint(
'AdminDashboardHomePage: Erreur lors de l\'ouverture de la boîte operations dans _loadDashboardData: $boxError');
// Continuer malgré l'erreur
}
}
// Récupérer l'opération en cours
debugPrint(
'AdminDashboardHomePage: Récupération de l\'opération en cours...');
currentOperation = userRepository.getCurrentOperation();
debugPrint(
'AdminDashboardHomePage: Opération récupérée: ${currentOperation?.id ?? "null"}');
} catch (boxError) {
debugPrint(
'AdminDashboardHomePage: Erreur lors de la récupération de l\'opération: $boxError');
// Afficher un message d'erreur ou gérer l'erreur de manière appropriée
}
if (currentOperation != null) {
// Charger les passages pour l'opération en cours
debugPrint(
'AdminDashboardHomePage: Chargement des passages pour l\'opération ${currentOperation.id}...');
final passages =
passageRepository.getPassagesByOperation(currentOperation.id);
debugPrint(
'AdminDashboardHomePage: ${passages.length} passages récupérés');
// Calculer le nombre total de passages
totalPassages = passages.length;
// Calculer le montant total collecté
totalAmounts = passages.fold(
0.0,
(sum, passage) =>
sum +
(passage.montant != null && passage.montant.isNotEmpty
? double.tryParse(passage.montant) ?? 0.0
: 0.0));
// Préparer les données pour le graphique de paiement
_preparePaymentData(passages);
// Compter les passages par type
passagesByType = {};
for (final passage in passages) {
final typeId = passage.fkType;
passagesByType[typeId] = (passagesByType[typeId] ?? 0) + 1;
}
// Afficher les comptages par type pour le débogage
debugPrint('AdminDashboardHomePage: Comptage des passages par type:');
passagesByType.forEach((typeId, count) {
final typeInfo = AppKeys.typesPassages[typeId];
final typeName = typeInfo != null ? typeInfo['titre'] : 'Inconnu';
debugPrint(
'AdminDashboardHomePage: Type $typeId ($typeName): $count passages');
});
// Charger les statistiques par membre
memberStats = [];
final Map<int, int> memberCounts = {};
// Compter les passages par membre
for (final passage in passages) {
if (passage.fkUser != null) {
memberCounts[passage.fkUser!] =
(memberCounts[passage.fkUser!] ?? 0) + 1;
}
}
// Récupérer les informations des membres
for (final entry in memberCounts.entries) {
final user = userRepository.getUserById(entry.key);
if (user != null) {
memberStats.add({
'name': '${user.firstName ?? ''} ${user.name ?? ''}'.trim(),
'count': entry.value,
});
}
}
// Trier les membres par nombre de passages (décroissant)
memberStats
.sort((a, b) => (b['count'] as int).compareTo(a['count'] as int));
} else {
debugPrint(
'AdminDashboardHomePage: Aucune opération en cours, impossible de charger les passages');
}
if (mounted) {
setState(() {
isDataLoaded = true;
isLoading = false;
isFirstLoad = false; // Marquer que le premier chargement est terminé
});
}
// Vérifier si les données sont correctement chargées
debugPrint(
'AdminDashboardHomePage: Données chargées: isDataLoaded=$isDataLoaded, totalPassages=$totalPassages, passagesByType=${passagesByType.length} types');
} catch (e) {
debugPrint(
'AdminDashboardHomePage: Erreur lors du chargement des données: $e');
if (mounted) {
setState(() {
isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
debugPrint('Building AdminDashboardHomePage');
return FutureBuilder<void>(
future: _initFuture,
builder: (context, snapshot) {
// Afficher un indicateur de chargement pendant l'initialisation des boîtes Hive
if (snapshot.connectionState == ConnectionState.waiting) {
debugPrint('FutureBuilder: ConnectionState.waiting');
return const Center(
child: CircularProgressIndicator(),
);
}
// Même si nous avons une erreur, nous continuons à afficher le contenu
// car nous avons modifié _initHiveBoxes pour ne pas propager les erreurs
if (snapshot.hasError) {
debugPrint('FutureBuilder: hasError - ${snapshot.error}');
// Nous affichons un message d'erreur mais continuons à afficher le contenu
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text('Erreur lors de l\'initialisation: ${snapshot.error}'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 5),
action: SnackBarAction(
label: 'Réessayer',
onPressed: () {
setState(() {
_initFuture = _initHiveBoxes().then((_) {
_loadDashboardData();
});
});
},
),
),
);
} else {
debugPrint('FutureBuilder: Initialisation réussie');
}
// L'initialisation a réussi, afficher le contenu
final screenWidth = MediaQuery.of(context).size.width;
final isDesktop = screenWidth > 800;
// Utiliser l'instance globale définie dans app.dart
// Récupérer l'opération en cours (les boîtes sont déjà ouvertes)
final currentOperation = userRepository.getCurrentOperation();
// Titre dynamique avec l'ID et le nom de l'opération
final String title = currentOperation != null
? 'Synthèse de l\'opération #${currentOperation.id} ${currentOperation.name}'
: 'Synthèse de l\'opération';
return Stack(children: [
// Fond dégradé avec petits points blancs
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.white, Colors.red.shade300],
),
),
child: CustomPaint(
painter: DotsPainter(),
child: Container(width: double.infinity, height: double.infinity),
),
),
// Contenu de la page
SingleChildScrollView(
padding: const EdgeInsets.all(AppTheme.spacingL),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre avec bouton de rafraîchissement sur la même ligne
Row(
children: [
Expanded(
child: Text(
title,
style:
Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
// Bouton de rafraîchissement
if (!isLoading)
IconButton(
icon: const Icon(Icons.refresh),
tooltip: 'Rafraîchir les données',
onPressed: _loadDashboardData,
)
else
const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
),
const SizedBox(height: AppTheme.spacingM),
// Afficher un indicateur de chargement si les données ne sont pas encore chargées
if (isLoading && !isDataLoaded)
const Center(
child: Padding(
padding: EdgeInsets.all(32.0),
child: CircularProgressIndicator(),
),
),
// Afficher le contenu seulement si les données sont chargées ou en cours de mise à jour
if (isDataLoaded || isLoading) ...[
// Cartes de synthèse
isDesktop
? Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: _buildSummaryCard(
context,
'Passages totaux',
totalPassages.toString(),
Icons.map_outlined,
AppTheme.primaryColor,
),
),
const SizedBox(width: AppTheme.spacingM),
Expanded(
flex: 2,
child: _buildSummaryCard(
context,
'Montant collecté',
'${totalAmounts.toStringAsFixed(2)}',
Icons.euro_outlined,
AppTheme.buttonSuccessColor,
),
),
const SizedBox(width: AppTheme.spacingM),
Expanded(
flex: 3,
child: SectorDistributionCard(
key: ValueKey(
'sector_distribution_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'),
height: 200,
forceRefresh: !isFirstLoad,
),
),
],
)
: Column(
children: [
_buildSummaryCard(
context,
'Passages totaux',
totalPassages.toString(),
Icons.map_outlined,
AppTheme.primaryColor,
),
const SizedBox(height: AppTheme.spacingM),
_buildSummaryCard(
context,
'Montant collecté',
'${totalAmounts.toStringAsFixed(2)}',
Icons.euro_outlined,
AppTheme.buttonSuccessColor,
),
const SizedBox(height: AppTheme.spacingM),
SectorDistributionCard(
key: ValueKey(
'sector_distribution_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'),
height: 200,
forceRefresh: !isFirstLoad,
),
],
),
const SizedBox(height: AppTheme.spacingL),
// Graphique d'activité
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.circular(AppTheme.borderRadiusMedium),
boxShadow: AppTheme.cardShadow,
),
child: ActivityChart(
key: ValueKey(
'activity_chart_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'),
height: 350,
loadFromHive: true,
showAllPassages:
true, // Tous les passages, pas seulement ceux de l'utilisateur courant
title: 'Passages réalisés par jour (15 derniers jours)',
daysToShow: 15,
forceRefresh: !isFirstLoad,
),
// Si vous avez besoin de passer l'ID de l'opération en cours, décommentez les lignes suivantes
// child: ActivityChart(
// height: 350,
// loadFromHive: true,
// showAllPassages: true,
// title: 'Passages réalisés par jour (15 derniers jours)',
// daysToShow: 15,
// operationId: userRepository.getCurrentOperation()?.id,
// ),
),
const SizedBox(height: AppTheme.spacingL),
// Graphiques de répartition
isDesktop
? Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _buildPassageTypeCard(context),
),
const SizedBox(width: AppTheme.spacingM),
Expanded(
child: _buildPaymentTypeCard(context),
),
],
)
: Column(
children: [
_buildPassageTypeCard(context),
const SizedBox(height: AppTheme.spacingM),
_buildPaymentTypeCard(context),
],
),
const SizedBox(height: AppTheme.spacingL),
// Actions rapides - uniquement visible sur le web
if (kIsWeb) ...[
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.circular(AppTheme.borderRadiusMedium),
boxShadow: AppTheme.cardShadow,
),
padding: const EdgeInsets.all(AppTheme.spacingM),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Actions sur cette opération',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: AppTheme.primaryColor,
),
),
const SizedBox(height: AppTheme.spacingM),
Wrap(
spacing: AppTheme.spacingM,
runSpacing: AppTheme.spacingM,
children: [
_buildActionButton(
context,
'Exporter les données',
Icons.file_download_outlined,
AppTheme.buttonPrimaryColor,
() {},
),
_buildActionButton(
context,
'Gérer les secteurs',
Icons.map_outlined,
AppTheme.accentColor,
() {},
),
],
),
],
),
),
],
],
],
),
)
]);
},
);
}
Widget _buildSummaryCard(
BuildContext context,
String label,
String value,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(AppTheme.spacingM),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
boxShadow: AppTheme.cardShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius:
BorderRadius.circular(AppTheme.borderRadiusSmall),
),
child: Icon(
icon,
color: color,
size: 24,
),
),
const SizedBox(width: AppTheme.spacingM),
Text(
label,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
const SizedBox(height: AppTheme.spacingM),
Text(
value,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 24,
color: color,
),
),
],
),
);
}
Widget _buildChartCard(
BuildContext context,
String title,
Widget chart,
) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
boxShadow: AppTheme.cardShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(AppTheme.spacingM),
child: Text(
title,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
chart,
],
),
);
}
// Construit la carte de répartition par type de passage avec liste
Widget _buildPassageTypeCard(BuildContext context) {
return Container(
height: 300, // Hauteur fixe de 300px
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
boxShadow: AppTheme.cardShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(AppTheme.spacingM),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Répartition par type de passage',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
'$totalPassages passages',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: AppTheme.primaryColor,
),
),
],
),
),
Expanded(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: AppTheme.spacingM),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Graphique à gauche
Expanded(
flex: 1,
child: SizedBox(
height: 180, // Taille réduite
child: Builder(
builder: (context) {
// Vérifier si nous avons des données de passages
if (passagesByType.isEmpty) {
debugPrint(
'AdminDashboardHomePage: Aucune donnée de passage disponible pour le graphique');
return const Center(
child: Text('Aucune donnée disponible'),
);
}
// Si nous avons des données, afficher le graphique
// Mais d'abord, vérifier si tous les passages sont de type 2 (à finaliser)
// qui est exclu par défaut dans PassagePieChart
bool hasNonType2Passages = passagesByType.entries.any(
(entry) => entry.key != 2 && entry.value > 0);
debugPrint(
'AdminDashboardHomePage: Données pour le graphique: $passagesByType');
// Créer un widget personnalisé pour afficher le graphique ou un message
// selon le contenu des données
if (passagesByType.isEmpty) {
debugPrint(
'AdminDashboardHomePage: Aucune donnée de passage disponible');
return const Center(
child: Text('Aucune donnée disponible'),
);
}
// Vérifier si nous avons des données pour au moins un type
int totalPassages = 0;
passagesByType
.forEach((_, count) => totalPassages += count);
if (totalPassages == 0) {
debugPrint(
'AdminDashboardHomePage: Aucun passage trouvé');
return const Center(
child: Text('Aucun passage trouvé'),
);
}
// Vérifier si tous les passages sont de type 2 (à finaliser)
if (!hasNonType2Passages) {
debugPrint(
'AdminDashboardHomePage: Tous les passages sont de type 2 (à finaliser)');
// Créer un widget personnalisé pour afficher un message
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.info_outline,
color: Colors.orange,
size: 40,
),
const SizedBox(height: 8),
const Text(
'Uniquement des passages à finaliser',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'${passagesByType[2] ?? 0} passages',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.orange,
fontWeight: FontWeight.bold,
),
),
],
);
}
// Sinon, afficher le graphique avec les données
debugPrint(
'AdminDashboardHomePage: Affichage du graphique avec ${passagesByType.length} types');
return PassagePieChart(
size: 180,
passagesByType: passagesByType,
loadFromHive: false,
isDonut: true,
innerRadius: '50%',
showIcons: false,
showLegend: false,
);
},
),
),
),
// Liste des types à droite
Expanded(
flex: 1,
child: Padding(
padding: const EdgeInsets.only(left: AppTheme.spacingM),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.end, // Alignement à droite
mainAxisAlignment: MainAxisAlignment.center,
children: [
...AppKeys.typesPassages.entries.map((entry) {
final int typeId = entry.key;
final Map<String, dynamic> typeInfo = entry.value;
final int count = passagesByType[typeId] ?? 0;
final Color color =
Color(typeInfo['couleur2'] as int);
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment
.end, // Alignement à droite
children: [
Expanded(
child: Text(
'$count ${typeInfo['titres']}',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: color,
),
textAlign: TextAlign
.right, // Texte aligné à droite
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
],
),
);
}).toList(),
],
),
),
),
],
),
),
),
],
),
);
}
// Construit la carte de répartition par mode de paiement
Widget _buildPaymentTypeCard(BuildContext context) {
return Container(
height: 300, // Hauteur fixe de 300px
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
boxShadow: AppTheme.cardShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(AppTheme.spacingM),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Répartition par mode de paiement',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
'${totalAmounts.toStringAsFixed(2)}',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: AppTheme.buttonSuccessColor,
),
),
],
),
),
Expanded(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: AppTheme.spacingM),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Graphique à gauche
Expanded(
flex: 1,
child: SizedBox(
height: 180, // Taille réduite
child: PaymentPieChart(
size: 180,
payments: paymentData,
isDonut: true,
innerRadius: '50%',
showIcons: false,
showLegend: false,
enable3DEffect:
false, // Désactiver l'effet 3D pour conserver les couleurs originales
effect3DIntensity: 0.0, // Pas d'intensité 3D
enableEnhancedExplode: false, // Désactiver l'explosion
useGradient:
false, // Ne pas utiliser de dégradé pour conserver les couleurs originales
),
),
),
// Liste des types de règlement à droite
Expanded(
flex: 1,
child: Padding(
padding: const EdgeInsets.only(left: AppTheme.spacingM),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.end, // Alignement à droite
mainAxisAlignment: MainAxisAlignment.center,
children: [
...[1, 2, 3].map((typeId) {
// Uniquement les types 1, 2 et 3
if (!AppKeys.typesReglements.containsKey(typeId)) {
return const SizedBox
.shrink(); // Ignorer si le type n'existe pas
}
final Map<String, dynamic> typeInfo =
AppKeys.typesReglements[typeId]!;
// Calculer le montant total pour ce type de règlement
double amount = 0.0;
for (final payment in paymentData) {
if (payment.typeId == typeId) {
amount = payment.amount;
break;
}
}
// Ne pas afficher si le montant est 0
if (amount <= 0) {
return const SizedBox.shrink();
}
final Color color =
Color(typeInfo['couleur'] as int);
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment
.end, // Alignement à droite
children: [
Expanded(
child: Text(
'${amount.toStringAsFixed(2)}${typeInfo['titre']}',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: color,
),
textAlign: TextAlign
.right, // Texte aligné à droite
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
],
),
);
}).toList(),
],
),
),
),
],
),
),
),
],
),
);
}
Widget _buildActionButton(
BuildContext context,
String label,
IconData icon,
Color color,
VoidCallback onPressed,
) {
return ElevatedButton.icon(
onPressed: onPressed,
icon: Icon(icon),
label: Text(label),
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: AppTheme.spacingL,
vertical: AppTheme.spacingM,
),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
),
),
);
}
}