Files
geo/flutt/lib/presentation/user/user_dashboard_home_page.dart

657 lines
25 KiB
Dart

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:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/core/repositories/passage_repository.dart';
import 'package:geosector_app/core/theme/app_theme.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart';
import 'package:geosector_app/presentation/widgets/charts/charts.dart';
class UserDashboardHomePage extends StatefulWidget {
const UserDashboardHomePage({super.key});
@override
State<UserDashboardHomePage> createState() => _UserDashboardHomePageState();
}
class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
// Formater une date au format JJ/MM/YYYY
String _formatDate(DateTime date) {
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// Utiliser l'instance globale définie dans app.dart
final size = MediaQuery.of(context).size;
final isDesktop = size.width > 900;
return Scaffold(
backgroundColor: Colors.transparent,
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tableau de bord',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
Builder(builder: (context) {
// Récupérer l'opération actuelle
final operation = userRepository.getCurrentOperation();
if (operation != null) {
return Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
'${operation.name} (${_formatDate(operation.dateDebut)}-${_formatDate(operation.dateFin)})',
style: theme.textTheme.titleSmall?.copyWith(
color: theme.colorScheme.primary.withOpacity(0.7),
fontWeight: FontWeight.w500,
),
),
);
} else {
return const SizedBox.shrink();
}
}),
const SizedBox(height: 24),
// Synthèse des passages
_buildSummaryCards(isDesktop),
const SizedBox(height: 24),
// Graphique des passages
_buildPassagesChart(context, theme),
const SizedBox(height: 24),
// Derniers passages
_buildRecentPassages(context, theme),
],
),
),
),
);
}
// Construction des cartes de synthèse
Widget _buildSummaryCards(bool isDesktop) {
return Column(
children: [
_buildCombinedPassagesCard(context, isDesktop),
const SizedBox(height: 16),
_buildCombinedPaymentsCard(isDesktop),
],
);
}
// Construction d'une carte combinée pour les règlements (liste + graphique)
Widget _buildCombinedPaymentsCard(bool isDesktop) {
// Utiliser les instances globales définies dans app.dart
// Récupérer l'utilisateur actuel
final currentUser = userRepository.getCurrentUser();
final int? currentUserId = currentUser?.id;
// Récupérer tous les passages
final passages = passageRepository.getAllPassages();
// Pas de log ici pour éviter les logs excessifs
// Initialiser les montants par type de règlement
final Map<int, double> paymentAmounts = {
0: 0.0, // Pas de règlement
1: 0.0, // Espèces
2: 0.0, // Chèques
3: 0.0, // CB
};
// Compteur pour les passages avec montant > 0
int passagesWithPaymentCount = 0;
// Parcourir les passages et calculer les montants par type de règlement
for (final passage in passages) {
// Vérifier si le passage appartient à l'utilisateur actuel
if (currentUserId != null && passage.fkUser == currentUserId) {
final int typeReglement = passage.fkTypeReglement;
// Convertir la chaîne de montant en double
double montant = 0.0;
try {
// Gérer les formats possibles (virgule ou point)
String montantStr = passage.montant.replaceAll(',', '.');
montant = double.tryParse(montantStr) ?? 0.0;
} catch (e) {
debugPrint('Erreur de conversion du montant: ${passage.montant}');
}
// Ne compter que les passages avec un montant > 0
if (montant > 0) {
passagesWithPaymentCount++;
// Ajouter au montant total par type de règlement
if (paymentAmounts.containsKey(typeReglement)) {
paymentAmounts[typeReglement] =
(paymentAmounts[typeReglement] ?? 0.0) + montant;
} else {
// Si le type n'est pas dans notre map, l'ajouter à la catégorie par défaut (0: Pas de règlement)
paymentAmounts[0] = (paymentAmounts[0] ?? 0.0) + montant;
// Type de règlement inconnu, ajouté à la catégorie 'Pas de règlement'
}
}
}
}
// Calculer le total des règlements
final double totalPayments =
paymentAmounts.values.fold(0.0, (sum, amount) => sum + amount);
// Convertir les montants en objets PaymentData pour le graphique
final List<PaymentData> paymentDataList =
PaymentUtils.getPaymentDataFromAmounts(paymentAmounts);
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Stack(
children: [
// Symbole euro en arrière-plan
Positioned.fill(
child: Center(
child: Icon(
Icons.euro_symbol,
size: 180,
color: Colors.blue.withOpacity(0.07), // Bleuté et estompé
),
),
),
// Contenu principal
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.payments,
color: AppTheme.accentColor,
size: 24,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Règlements sur $passagesWithPaymentCount passages',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
Text(
'${totalPayments.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppTheme.accentColor,
),
),
],
),
const Divider(height: 24),
SizedBox(
height: 250,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Liste des règlements (côté gauche)
Expanded(
flex: isDesktop ? 1 : 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...AppKeys.typesReglements.entries.map((entry) {
final int typeId = entry.key;
final Map<String, dynamic> typeData = entry.value;
final double amount =
paymentAmounts[typeId] ?? 0.0;
final Color color =
Color(typeData['couleur'] as int);
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
child: Icon(
typeData['icon_data'] as IconData,
color: Colors.white,
size: 16,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
typeData['titre'] as String,
style: const TextStyle(
fontSize: 14,
),
),
),
Text(
'${amount.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
);
}).toList(),
],
),
),
// Séparateur vertical
if (isDesktop) const VerticalDivider(width: 24),
// Graphique en camembert (côté droit)
Expanded(
flex: isDesktop ? 1 : 2,
child: PaymentPieChart(
payments: paymentDataList,
size: double
.infinity, // Utiliser tout l'espace disponible
labelSize: 12,
showPercentage: true,
showIcons: false, // Désactiver les icônes
showLegend: false,
isDonut: true,
innerRadius: '50%',
enable3DEffect: true, // Activer l'effet 3D
effect3DIntensity:
1.5, // Intensité de l'effet 3D plus forte
enableEnhancedExplode:
true, // Activer l'effet d'explosion amélioré
useGradient:
true, // Utiliser des dégradés pour renforcer l'effet 3D
),
),
],
),
),
],
),
),
],
),
);
}
// Construction d'une carte combinée pour les passages (liste + graphique)
Widget _buildCombinedPassagesCard(BuildContext context, bool isDesktop) {
// Utiliser les instances globales définies dans app.dart
// Récupérer l'utilisateur actuel
final currentUser = userRepository.getCurrentUser();
final int? currentUserId = currentUser?.id;
// Récupérer tous les passages
final passages = passageRepository.getAllPassages();
// Pas de log ici pour éviter les logs excessifs
// Compter les passages par type
final Map<int, int> passagesCounts = {
1: 0, // Effectués
2: 0, // À finaliser
3: 0, // Refusés
4: 0, // Dons
5: 0, // Lots
6: 0, // Maisons vides
};
// Créer un map pour compter les types de passages
final Map<int, int> typesCount = {};
final Map<int, int> userTypesCount = {};
// Parcourir les passages et les compter par type
for (final passage in passages) {
final typeId = passage.fkType;
final int passageUserId = passage.fkUser;
// Compter les occurrences de chaque type pour le débogage
typesCount[typeId] = (typesCount[typeId] ?? 0) + 1;
// Vérifier si le passage appartient à l'utilisateur actuel ou est de type 2
bool shouldCount = typeId == 2 ||
(currentUserId != null && passageUserId == currentUserId);
if (shouldCount) {
// Compter pour les statistiques de l'utilisateur
userTypesCount[typeId] = (userTypesCount[typeId] ?? 0) + 1;
// Ajouter au compteur des passages par type
if (passagesCounts.containsKey(typeId)) {
passagesCounts[typeId] = passagesCounts[typeId]! + 1;
} else {
// Si le type n'est pas dans notre map, l'ajouter à la catégorie par défaut (2: À finaliser)
passagesCounts[2] = passagesCounts[2]! + 1;
// Type de passage inconnu ajouté à 'A finaliser'
}
}
}
// Pas de log ici pour éviter les logs excessifs
// Calculer le total des passages pour l'utilisateur (somme des valeurs dans userTypesCount)
final int totalUserPassages =
userTypesCount.values.fold(0, (sum, count) => sum + count);
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0,
8.0), // Réduire les paddings vertical pour donner plus d'espace au graphique
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.route,
color: AppTheme.primaryColor,
size: 24,
),
const SizedBox(width: 8),
Expanded(
child: Builder(builder: (context) {
// Récupérer les secteurs de l'utilisateur
final userSectors = userRepository.getUserSectors();
final int sectorCount = userSectors.length;
// Déterminer le titre en fonction du nombre de secteurs
String title = 'Passages';
if (sectorCount > 1) {
title = 'Passages sur mes $sectorCount secteurs';
}
return Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
);
}),
),
Text(
totalUserPassages.toString(),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
],
),
const Divider(height: 24),
SizedBox(
height: 250,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Liste des passages (côté gauche)
Expanded(
flex: isDesktop ? 1 : 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...AppKeys.typesPassages.entries.map((entry) {
final int typeId = entry.key;
final Map<String, dynamic> typeData = entry.value;
final int count = passagesCounts[typeId] ?? 0;
final Color color = Color(typeData['couleur2']
as int); // Utiliser la deuxième couleur
final IconData iconData = typeData['icon_data']
as IconData; // Utiliser l'icône définie dans AppKeys
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
child: Icon(
iconData,
color: Colors.white,
size: 16,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
typeData['titres'] as String,
style: const TextStyle(
fontSize: 14,
),
),
),
Text(
count.toString(),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
);
}).toList(),
],
),
),
// Séparateur vertical
if (isDesktop) const VerticalDivider(width: 24),
// Graphique en camembert (côté droit)
Expanded(
flex: isDesktop ? 1 : 2,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: PassagePieChart(
passagesByType: passagesCounts,
size: double
.infinity, // Utiliser tout l'espace disponible
labelSize: 12,
showPercentage: true,
showIcons: false, // Désactiver les icônes
showLegend: false, // Désactiver la légende
isDonut: true, // Activer le format donut
innerRadius: '50%' // Rayon interne du donut
),
),
),
],
),
),
],
),
),
);
}
// Construction du graphique des passages
Widget _buildPassagesChart(BuildContext context, ThemeData theme) {
// Définir les types de passages à exclure
// Selon la mémoire, le type 2 correspond aux passages "A finaliser"
// et nous voulons les exclure du comptage pour l'utilisateur actuel
final List<int> excludePassageTypes = [2];
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0,
8.0), // Réduire les paddings vertical pour donner plus d'espace
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre supprimé car déjà présent dans le widget ActivityChart
SizedBox(
height:
350, // Augmentation de la hauteur à 350px pour résoudre le problème de l'axe Y
child: ActivityChart(
// Utiliser le chargement depuis Hive directement dans le widget
loadFromHive: true,
// Ne pas filtrer par utilisateur (afficher tous les passages)
showAllPassages: true,
// Exclure les passages de type 2 (A finaliser)
excludePassageTypes: excludePassageTypes,
// Afficher les 15 derniers jours
daysToShow: 15,
periodType: 'Jour',
height:
350, // Augmentation de la hauteur à 350px aussi dans le widget
),
),
],
),
),
);
}
// Construction de la liste des derniers passages
Widget _buildRecentPassages(BuildContext context, ThemeData theme) {
// Utiliser les instances globales définies dans app.dart
// Récupérer tous les passages et les trier par date (les plus récents d'abord)
final allPassages = passageRepository.getAllPassages();
allPassages.sort((a, b) => b.passedAt.compareTo(a.passedAt));
// Limiter aux 10 passages les plus récents
final recentPassagesModels = allPassages.take(10).toList();
// Convertir les modèles de passage au format attendu par le widget PassagesListWidget
final List<Map<String, dynamic>> recentPassages =
recentPassagesModels.map((passage) {
// Construire l'adresse complète à partir des champs disponibles
final String address =
'${passage.numero} ${passage.rue}${passage.rueBis.isNotEmpty ? ' ${passage.rueBis}' : ''}, ${passage.ville}';
// Convertir le montant en double
final double amount = double.tryParse(passage.montant) ?? 0.0;
return {
'id': passage.id.toString(),
'address': address,
'amount': amount,
'date': passage.passedAt,
'type': passage.fkType,
'payment': passage.fkTypeReglement,
'name': passage.name,
'notes': passage.remarque,
'hasReceipt': passage.nomRecu.isNotEmpty,
'hasError': passage.emailErreur.isNotEmpty,
'fkUser': passage.fkUser, // Ajouter l'ID de l'utilisateur
};
}).toList();
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Derniers passages',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
TextButton(
onPressed: () {
// Naviguer vers la page d'historique
},
child: const Text('Voir tout'),
),
],
),
),
// Utilisation du widget commun PassagesListWidget
PassagesListWidget(
passages: recentPassages,
showFilters: false,
showSearch: false,
showActions: true, // Activer l'affichage des boutons d'action
maxPassages: 10,
// Exclure les passages de type 2 (À finaliser)
excludePassageTypes: [2],
// Filtrer par utilisateur courant
filterByUserId: userRepository.getCurrentUser()?.id,
// Période par défaut (derniers 15 jours)
periodFilter: 'last15',
onPassageSelected: (passage) {
// Action lors de la sélection d'un passage
debugPrint('Passage sélectionné: ${passage['id']}');
},
onDetailsView: (passage) {
// Action lors de l'affichage des détails
debugPrint('Affichage des détails: ${passage['id']}');
},
// Callback pour le bouton de modification
onPassageEdit: (passage) {
// Action lors de la modification d'un passage
debugPrint('Modification du passage: ${passage['id']}');
// Ici, vous pourriez ouvrir un formulaire d'édition
},
// Callback pour le bouton de reçu (uniquement pour les passages de type 1)
onReceiptView: (passage) {
// Action lors de la demande d'affichage du reçu
debugPrint('Affichage du reçu pour le passage: ${passage['id']}');
// Ici, vous pourriez générer et afficher un PDF
},
),
],
),
);
}
}