966 lines
38 KiB
Dart
966 lines
38 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: [
|
|
Builder(builder: (context) {
|
|
// Récupérer l'opération actuelle
|
|
final operation = userRepository.getCurrentOperation();
|
|
if (operation != null) {
|
|
return Text(
|
|
'${operation.name} (${_formatDate(operation.dateDebut)}-${_formatDate(operation.dateFin)})',
|
|
style: theme.textTheme.headlineMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
);
|
|
} else {
|
|
return Text(
|
|
'Tableau de bord',
|
|
style: theme.textTheme.headlineMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
);
|
|
}
|
|
}),
|
|
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),
|
|
],
|
|
);
|
|
}
|
|
|
|
// Méthode pour charger les données de règlements de manière asynchrone
|
|
Future<Map<String, dynamic>> _loadPaymentData() async {
|
|
// Utiliser un délai plus long pour s'assurer que les données sont chargées
|
|
await Future.delayed(const Duration(milliseconds: 1500));
|
|
|
|
// Utiliser les instances globales définies dans app.dart
|
|
final currentUser = userRepository.getCurrentUser();
|
|
final int? currentUserId = currentUser?.id;
|
|
|
|
// Récupérer tous les passages
|
|
final passages = passageRepository.getAllPassages();
|
|
|
|
// Vérifier si les données sont complètement chargées
|
|
final int totalPassages = passages.length;
|
|
debugPrint(
|
|
'Nombre total de passages chargés pour règlements: $totalPassages');
|
|
|
|
// Si le nombre de passages est trop faible, on considère que les données ne sont pas complètement chargées
|
|
if (totalPassages < 100 && !_dataFullyLoaded) {
|
|
// Attendre un peu plus et réessayer
|
|
await Future.delayed(const Duration(milliseconds: 1000));
|
|
// Récupérer à nouveau les passages
|
|
final newPassages = passageRepository.getAllPassages();
|
|
final newTotalPassages = newPassages.length;
|
|
debugPrint(
|
|
'Nouveau nombre total de passages chargés pour règlements: $newTotalPassages');
|
|
|
|
// Si le nombre a augmenté, utiliser les nouvelles données
|
|
if (newTotalPassages > totalPassages) {
|
|
passages.clear();
|
|
passages.addAll(newPassages);
|
|
debugPrint(
|
|
'Utilisation des nouvelles données de passages pour règlements');
|
|
}
|
|
}
|
|
|
|
// 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'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Afficher les montants par type de règlement pour le débogage
|
|
debugPrint('=== MONTANTS PAR TYPE DE RÈGLEMENT ===');
|
|
paymentAmounts.forEach((typeId, amount) {
|
|
final typeTitle = AppKeys.typesReglements[typeId]?['titre'] ?? 'Inconnu';
|
|
debugPrint('Type $typeId ($typeTitle): ${amount.toStringAsFixed(2)} €');
|
|
});
|
|
debugPrint('=====================================');
|
|
|
|
// 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);
|
|
|
|
// Vérifier si des types de règlement ont un montant de 0
|
|
// Si c'est le cas, ajouter un petit montant pour qu'ils apparaissent dans le graphique
|
|
for (var payment in paymentDataList) {
|
|
if (payment.amount == 0 && payment.typeId != 0) {
|
|
// Ignorer le type 0 (Pas de règlement)
|
|
debugPrint(
|
|
'Type ${payment.typeId} (${payment.title}) a un montant de 0, ajout d\'un petit montant pour l\'affichage');
|
|
// Trouver l'index dans la liste
|
|
final index = paymentDataList.indexOf(payment);
|
|
// Remplacer par un nouvel objet avec un petit montant
|
|
paymentDataList[index] = PaymentData(
|
|
typeId: payment.typeId,
|
|
amount: 0.01, // Petit montant pour qu'il apparaisse dans le graphique
|
|
color: payment.color,
|
|
icon: payment.icon,
|
|
title: payment.title,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Retourner les données calculées
|
|
return {
|
|
'paymentAmounts': paymentAmounts,
|
|
'totalPayments': totalPayments,
|
|
'passagesWithPaymentCount': passagesWithPaymentCount,
|
|
'paymentDataList': paymentDataList,
|
|
};
|
|
}
|
|
|
|
// Construction d'une carte combinée pour les règlements (liste + graphique)
|
|
Widget _buildCombinedPaymentsCard(bool isDesktop) {
|
|
return FutureBuilder<Map<String, dynamic>>(
|
|
// Utiliser un Future pour s'assurer que les données sont chargées
|
|
future: _loadPaymentData(),
|
|
builder: (context, snapshot) {
|
|
// Afficher un spinner pendant le chargement
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return Card(
|
|
elevation: 4,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: const SizedBox(
|
|
height: 300,
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
CircularProgressIndicator(),
|
|
SizedBox(height: 16),
|
|
Text('Chargement des données de règlements...'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// En cas d'erreur
|
|
if (snapshot.hasError) {
|
|
return Card(
|
|
elevation: 4,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: SizedBox(
|
|
height: 300,
|
|
child: Center(
|
|
child: Text(
|
|
'Erreur lors du chargement des données: ${snapshot.error}'),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Si les données sont disponibles
|
|
if (snapshot.hasData) {
|
|
final data = snapshot.data!;
|
|
final paymentAmounts =
|
|
Map<int, double>.from(data['paymentAmounts'] as Map);
|
|
final totalPayments = data['totalPayments'] as double;
|
|
final passagesWithPaymentCount =
|
|
data['passagesWithPaymentCount'] as int;
|
|
final paymentDataList = data['paymentDataList'] as List<PaymentData>;
|
|
|
|
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(
|
|
'Mes 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: Container(
|
|
padding: const EdgeInsets.all(4.0),
|
|
// Réduire légèrement la taille pour éviter la troncature
|
|
child: FittedBox(
|
|
fit: BoxFit.contain,
|
|
child: SizedBox(
|
|
width:
|
|
200, // Taille réduite pour éviter la troncature
|
|
height: 200,
|
|
child: PaymentPieChart(
|
|
payments: paymentDataList,
|
|
size:
|
|
200, // Taille fixe au lieu de double.infinity
|
|
labelSize:
|
|
10, // Réduire davantage la taille des étiquettes
|
|
showPercentage: true,
|
|
showIcons: false, // Désactiver les icônes
|
|
showLegend: false,
|
|
isDonut: true,
|
|
innerRadius:
|
|
'55%', // Augmenter légèrement le rayon interne
|
|
enable3DEffect:
|
|
false, // Désactiver l'effet 3D pour préserver les couleurs originales
|
|
effect3DIntensity:
|
|
0.0, // Pas d'intensité 3D
|
|
enableEnhancedExplode:
|
|
false, // Désactiver l'effet d'explosion amélioré
|
|
useGradient:
|
|
false, // Ne pas utiliser de dégradés
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Par défaut, retourner un widget vide
|
|
return const SizedBox.shrink();
|
|
},
|
|
);
|
|
}
|
|
|
|
// Variable pour suivre si les données sont complètement chargées
|
|
bool _dataFullyLoaded = false;
|
|
|
|
// Méthode pour charger les données de passages de manière asynchrone
|
|
Future<Map<String, dynamic>> _loadPassageData() async {
|
|
// Utiliser un délai plus long pour s'assurer que les données sont chargées
|
|
await Future.delayed(const Duration(milliseconds: 1500));
|
|
|
|
// Utiliser les instances globales définies dans app.dart
|
|
final currentUser = userRepository.getCurrentUser();
|
|
final int? currentUserId = currentUser?.id;
|
|
|
|
// Récupérer tous les passages
|
|
final passages = passageRepository.getAllPassages();
|
|
|
|
// Vérifier si les données sont complètement chargées
|
|
final int totalPassages = passages.length;
|
|
debugPrint('Nombre total de passages chargés: $totalPassages');
|
|
|
|
// Si le nombre de passages est trop faible, on considère que les données ne sont pas complètement chargées
|
|
if (totalPassages < 100 && !_dataFullyLoaded) {
|
|
// Attendre un peu plus et réessayer
|
|
await Future.delayed(const Duration(milliseconds: 1000));
|
|
// Récupérer à nouveau les passages
|
|
final newPassages = passageRepository.getAllPassages();
|
|
final newTotalPassages = newPassages.length;
|
|
debugPrint('Nouveau nombre total de passages chargés: $newTotalPassages');
|
|
|
|
// Si le nombre a augmenté, utiliser les nouvelles données
|
|
if (newTotalPassages > totalPassages) {
|
|
passages.clear();
|
|
passages.addAll(newPassages);
|
|
debugPrint('Utilisation des nouvelles données de passages');
|
|
}
|
|
}
|
|
|
|
// Marquer les données comme complètement chargées pour éviter de refaire cette vérification
|
|
_dataFullyLoaded = true;
|
|
|
|
// 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'
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculer le total des passages pour l'utilisateur (somme des valeurs dans userTypesCount)
|
|
final int totalUserPassages =
|
|
userTypesCount.values.fold(0, (sum, count) => sum + count);
|
|
|
|
// Retourner les données calculées
|
|
return {
|
|
'passagesCounts': passagesCounts,
|
|
'totalUserPassages': totalUserPassages,
|
|
};
|
|
}
|
|
|
|
// Construction d'une carte combinée pour les passages (liste + graphique)
|
|
Widget _buildCombinedPassagesCard(BuildContext context, bool isDesktop) {
|
|
return FutureBuilder<Map<String, dynamic>>(
|
|
// Utiliser un Future pour s'assurer que les données sont chargées
|
|
future: _loadPassageData(),
|
|
builder: (context, snapshot) {
|
|
// Afficher un spinner pendant le chargement
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return Card(
|
|
elevation: 4,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: const SizedBox(
|
|
height: 300,
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
CircularProgressIndicator(),
|
|
SizedBox(height: 16),
|
|
Text('Chargement des données de passages...'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// En cas d'erreur
|
|
if (snapshot.hasError) {
|
|
return Card(
|
|
elevation: 4,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: SizedBox(
|
|
height: 300,
|
|
child: Center(
|
|
child: Text(
|
|
'Erreur lors du chargement des données: ${snapshot.error}'),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Si les données sont disponibles
|
|
if (snapshot.hasData) {
|
|
final data = snapshot.data!;
|
|
final passagesCounts =
|
|
Map<int, int>.from(data['passagesCounts'] as Map);
|
|
final totalUserPassages = data['totalUserPassages'] as int;
|
|
|
|
return Card(
|
|
elevation: 4,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
|
|
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) {
|
|
return Text(
|
|
'Mes passages',
|
|
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);
|
|
final IconData iconData =
|
|
typeData['icon_data'] as IconData;
|
|
|
|
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,
|
|
labelSize: 12,
|
|
showPercentage: true,
|
|
showIcons: false,
|
|
showLegend: false,
|
|
isDonut: true,
|
|
innerRadius: '50%',
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Par défaut, retourner un widget vide
|
|
return const SizedBox.shrink();
|
|
},
|
|
);
|
|
}
|
|
|
|
// 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];
|
|
|
|
// Utiliser le même mécanisme de chargement asynchrone que pour les autres graphiques
|
|
return FutureBuilder<Map<String, dynamic>>(
|
|
// Utiliser le même Future que pour le graphique des passages
|
|
future: _loadPassageData(),
|
|
builder: (context, snapshot) {
|
|
// Afficher un spinner pendant le chargement
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return Card(
|
|
elevation: 4,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: const SizedBox(
|
|
height: 350,
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
CircularProgressIndicator(),
|
|
SizedBox(height: 16),
|
|
Text('Chargement des données d\'activité...'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// En cas d'erreur
|
|
if (snapshot.hasError) {
|
|
return Card(
|
|
elevation: 4,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: SizedBox(
|
|
height: 350,
|
|
child: Center(
|
|
child: Text(
|
|
'Erreur lors du chargement des données: ${snapshot.error}'),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Si les données sont disponibles, afficher le graphique
|
|
return Card(
|
|
elevation: 4,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
|
|
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 le même mécanisme de chargement asynchrone que pour les autres widgets
|
|
return FutureBuilder<Map<String, dynamic>>(
|
|
// Utiliser le même Future que pour les autres widgets
|
|
future: _loadPassageData(),
|
|
builder: (context, snapshot) {
|
|
// Afficher un spinner pendant le chargement
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return Card(
|
|
elevation: 4,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: const SizedBox(
|
|
height: 300,
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
CircularProgressIndicator(),
|
|
SizedBox(height: 16),
|
|
Text('Chargement des derniers passages...'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// En cas d'erreur
|
|
if (snapshot.hasError) {
|
|
return Card(
|
|
elevation: 4,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: SizedBox(
|
|
height: 300,
|
|
child: Center(
|
|
child: Text(
|
|
'Erreur lors du chargement des données: ${snapshot.error}'),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Si les données sont disponibles, afficher la liste des passages récents
|
|
// Utiliser les instances globales définies dans app.dart
|
|
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
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|