Initialisation du projet geosector complet (web + flutter)
This commit is contained in:
887
flutt/lib/presentation/admin/admin_dashboard_home_page.dart
Normal file
887
flutt/lib/presentation/admin/admin_dashboard_home_page.dart
Normal file
@@ -0,0 +1,887 @@
|
||||
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
||||
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 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;
|
||||
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
|
||||
// Méthode pour initialiser les boîtes Hive nécessaires
|
||||
Future<void> _initHiveBoxes() async {
|
||||
try {
|
||||
debugPrint('Initialisation des boîtes Hive...');
|
||||
|
||||
// Ouvrir la boîte des opérations si elle n'est pas déjà ouverte
|
||||
if (!Hive.isBoxOpen(AppKeys.operationsBoxName)) {
|
||||
debugPrint('Ouverture de la boîte operations...');
|
||||
try {
|
||||
await Hive.openBox<OperationModel>(AppKeys.operationsBoxName);
|
||||
debugPrint('Boîte operations ouverte avec succès');
|
||||
} catch (boxError) {
|
||||
debugPrint(
|
||||
'Erreur lors de l\'ouverture de la boîte operations: $boxError');
|
||||
// Continuer malgré l'erreur
|
||||
}
|
||||
} else {
|
||||
debugPrint('Boîte operations déjà ouverte');
|
||||
}
|
||||
|
||||
// Ouvrir la boîte des passages si elle n'est pas déjà ouverte
|
||||
if (!Hive.isBoxOpen(AppKeys.passagesBoxName)) {
|
||||
debugPrint('Ouverture de la boîte passages...');
|
||||
try {
|
||||
await Hive.openBox<PassageModel>(AppKeys.passagesBoxName);
|
||||
debugPrint('Boîte passages ouverte avec succès');
|
||||
} catch (boxError) {
|
||||
debugPrint(
|
||||
'Erreur lors de l\'ouverture de la boîte passages: $boxError');
|
||||
// Continuer malgré l'erreur
|
||||
}
|
||||
} else {
|
||||
debugPrint('Boîte passages déjà ouverte');
|
||||
}
|
||||
|
||||
// Ouvrir la boîte des secteurs si elle n'est pas déjà ouverte
|
||||
if (!Hive.isBoxOpen(AppKeys.sectorsBoxName)) {
|
||||
debugPrint('Ouverture de la boîte sectors...');
|
||||
try {
|
||||
await Hive.openBox<SectorModel>(AppKeys.sectorsBoxName);
|
||||
debugPrint('Boîte sectors ouverte avec succès');
|
||||
} catch (boxError) {
|
||||
debugPrint(
|
||||
'Erreur lors de l\'ouverture de la boîte sectors: $boxError');
|
||||
// Continuer malgré l'erreur
|
||||
}
|
||||
} else {
|
||||
debugPrint('Boîte sectors déjà ouverte');
|
||||
}
|
||||
|
||||
debugPrint('Initialisation des boîtes Hive terminée');
|
||||
} catch (e) {
|
||||
debugPrint('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 {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
debugPrint('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(
|
||||
'Ouverture de la boîte operations dans _loadDashboardData...');
|
||||
try {
|
||||
await Hive.openBox<OperationModel>(AppKeys.operationsBoxName);
|
||||
debugPrint(
|
||||
'Boîte operations ouverte avec succès dans _loadDashboardData');
|
||||
} catch (boxError) {
|
||||
debugPrint(
|
||||
'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('Récupération de l\'opération en cours...');
|
||||
currentOperation = userRepository.getCurrentOperation();
|
||||
debugPrint('Opération récupérée: ${currentOperation?.id ?? "null"}');
|
||||
} catch (boxError) {
|
||||
debugPrint('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
|
||||
final passages =
|
||||
passageRepository.getPassagesByOperation(currentOperation.id);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
setState(() {
|
||||
isDataLoaded = true;
|
||||
isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du chargement des données: $e');
|
||||
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 SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingL),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style:
|
||||
Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
// Réduire la taille de police en version web
|
||||
fontSize: isDesktop ? 18 : null,
|
||||
),
|
||||
overflow: TextOverflow
|
||||
.ellipsis, // Tronquer avec ... si trop long
|
||||
maxLines: 1, // Forcer une seule ligne
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// 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(
|
||||
height: 200,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: 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(
|
||||
height: 200,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: AppTheme.spacingL),
|
||||
|
||||
// Graphique d'activité
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppTheme.borderRadiusMedium),
|
||||
boxShadow: AppTheme.cardShadow,
|
||||
),
|
||||
child: const ActivityChart(
|
||||
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,
|
||||
),
|
||||
// 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
|
||||
Text(
|
||||
'Actions rapides',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
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,
|
||||
'Envoyer un message',
|
||||
Icons.message_outlined,
|
||||
AppTheme.buttonSuccessColor,
|
||||
() {},
|
||||
),
|
||||
_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: const PassagePieChart(
|
||||
size: 180,
|
||||
loadFromHive: true,
|
||||
showAllPassages: true,
|
||||
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: true,
|
||||
effect3DIntensity: 1.5,
|
||||
enableEnhancedExplode: false, // Désactiver l'explosion
|
||||
useGradient: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user