573 lines
20 KiB
Dart
573 lines
20 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/presentation/widgets/passages/passages_list_widget.dart';
|
|
import 'package:geosector_app/core/constants/app_keys.dart';
|
|
import 'package:geosector_app/core/repositories/passage_repository.dart';
|
|
import 'package:geosector_app/core/repositories/user_repository.dart';
|
|
import 'package:geosector_app/core/data/models/passage_model.dart';
|
|
|
|
class UserHistoryPage extends StatefulWidget {
|
|
const UserHistoryPage({super.key});
|
|
|
|
@override
|
|
State<UserHistoryPage> createState() => _UserHistoryPageState();
|
|
}
|
|
|
|
class _UserHistoryPageState extends State<UserHistoryPage> {
|
|
// Liste qui contiendra les passages convertis
|
|
List<Map<String, dynamic>> _convertedPassages = [];
|
|
|
|
// Variables pour indiquer l'état de chargement
|
|
bool _isLoading = true;
|
|
String _errorMessage = '';
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// Charger les passages depuis la box Hive au démarrage
|
|
_loadPassages();
|
|
}
|
|
|
|
// Méthode pour charger les passages depuis le repository
|
|
Future<void> _loadPassages() async {
|
|
setState(() {
|
|
_isLoading = true;
|
|
_errorMessage = '';
|
|
});
|
|
|
|
try {
|
|
// Utiliser l'instance globale définie dans app.dart
|
|
|
|
// Utiliser la propriété passages qui gère déjà l'ouverture de la box
|
|
final List<PassageModel> allPassages = passageRepository.passages;
|
|
|
|
debugPrint('Nombre total de passages dans la box: ${allPassages.length}');
|
|
|
|
// Filtrer pour exclure les passages de type 2
|
|
List<PassageModel> filtered = [];
|
|
for (var passage in allPassages) {
|
|
try {
|
|
if (passage.fkType != 2) {
|
|
filtered.add(passage);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du filtrage du passage: $e');
|
|
// Si nous ne pouvons pas accéder à fkType, ne pas ajouter ce passage
|
|
}
|
|
}
|
|
|
|
debugPrint(
|
|
'Nombre de passages après filtrage (fkType != 2): ${filtered.length}');
|
|
|
|
// Afficher la distribution des types de passages pour le débogage
|
|
final Map<int, int> typeCount = {};
|
|
for (var passage in filtered) {
|
|
typeCount[passage.fkType] = (typeCount[passage.fkType] ?? 0) + 1;
|
|
}
|
|
typeCount.forEach((type, count) {
|
|
debugPrint('Type de passage $type: $count passages');
|
|
});
|
|
|
|
// Afficher la plage de dates pour le débogage
|
|
if (filtered.isNotEmpty) {
|
|
// Trier par date pour trouver min et max
|
|
final sortedByDate = List<PassageModel>.from(filtered);
|
|
sortedByDate.sort((a, b) => a.passedAt.compareTo(b.passedAt));
|
|
|
|
final DateTime minDate = sortedByDate.first.passedAt;
|
|
final DateTime maxDate = sortedByDate.last.passedAt;
|
|
|
|
// Log détaillé pour débogage
|
|
debugPrint(
|
|
'Plage de dates des passages: ${minDate.toString()} à ${maxDate.toString()}');
|
|
|
|
// Afficher les 5 passages les plus anciens et les 5 plus récents pour débogage
|
|
debugPrint('\n--- 5 PASSAGES LES PLUS ANCIENS ---');
|
|
for (int i = 0; i < sortedByDate.length && i < 5; i++) {
|
|
final p = sortedByDate[i];
|
|
debugPrint(
|
|
'ID: ${p.id}, Type: ${p.fkType}, Date: ${p.passedAt}, Adresse: ${p.rue}');
|
|
}
|
|
|
|
debugPrint('\n--- 5 PASSAGES LES PLUS RÉCENTS ---');
|
|
for (int i = sortedByDate.length - 1;
|
|
i >= 0 && i >= sortedByDate.length - 5;
|
|
i--) {
|
|
final p = sortedByDate[i];
|
|
debugPrint(
|
|
'ID: ${p.id}, Type: ${p.fkType}, Date: ${p.passedAt}, Adresse: ${p.rue}');
|
|
}
|
|
|
|
// Vérifier la distribution des passages par mois
|
|
final Map<String, int> monthCount = {};
|
|
for (var passage in filtered) {
|
|
final String monthKey =
|
|
'${passage.passedAt.year}-${passage.passedAt.month.toString().padLeft(2, '0')}';
|
|
monthCount[monthKey] = (monthCount[monthKey] ?? 0) + 1;
|
|
}
|
|
|
|
debugPrint('\n--- DISTRIBUTION PAR MOIS ---');
|
|
final sortedMonths = monthCount.keys.toList()..sort();
|
|
for (var month in sortedMonths) {
|
|
debugPrint('$month: ${monthCount[month]} passages');
|
|
}
|
|
}
|
|
|
|
// Convertir les modèles en Maps pour l'affichage avec gestion d'erreurs
|
|
List<Map<String, dynamic>> passagesMap = [];
|
|
for (var passage in filtered) {
|
|
try {
|
|
final Map<String, dynamic> passageMap =
|
|
_convertPassageModelToMap(passage);
|
|
if (passageMap != null) {
|
|
passagesMap.add(passageMap);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de la conversion du passage en map: $e');
|
|
// Ignorer ce passage et continuer
|
|
}
|
|
}
|
|
|
|
debugPrint('Nombre de passages après conversion: ${passagesMap.length}');
|
|
|
|
// Trier par date (plus récent en premier) avec gestion d'erreurs
|
|
try {
|
|
passagesMap.sort((a, b) {
|
|
try {
|
|
return (b['date'] as DateTime).compareTo(a['date'] as DateTime);
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de la comparaison des dates: $e');
|
|
return 0; // Garder l'ordre actuel en cas d'erreur
|
|
}
|
|
});
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du tri des passages: $e');
|
|
// Continuer sans tri en cas d'erreur
|
|
}
|
|
|
|
// Debug: vérifier la plage de dates après conversion et tri
|
|
if (passagesMap.isNotEmpty) {
|
|
debugPrint('\n--- PLAGE DE DATES APRÈS CONVERSION ET TRI ---');
|
|
final firstDate = passagesMap.last['date'] as DateTime;
|
|
final lastDate = passagesMap.first['date'] as DateTime;
|
|
debugPrint('Premier passage: ${firstDate.toString()}');
|
|
debugPrint('Dernier passage: ${lastDate.toString()}');
|
|
}
|
|
|
|
setState(() {
|
|
_convertedPassages = passagesMap;
|
|
_isLoading = false;
|
|
});
|
|
} catch (e) {
|
|
setState(() {
|
|
_errorMessage = 'Erreur lors du chargement des passages: $e';
|
|
_isLoading = false;
|
|
});
|
|
debugPrint(_errorMessage);
|
|
}
|
|
}
|
|
|
|
// Convertir un modèle de passage en Map pour l'affichage avec gestion renforcée des erreurs
|
|
Map<String, dynamic> _convertPassageModelToMap(PassageModel passage) {
|
|
try {
|
|
// Le passage ne peut pas être null en Dart non-nullable,
|
|
// mais nous gardons cette structure pour faciliter la gestion des erreurs
|
|
|
|
// Construire l'adresse complète avec gestion des erreurs
|
|
String address = 'Adresse non disponible';
|
|
try {
|
|
address = _buildFullAddress(passage);
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de la construction de l\'adresse: $e');
|
|
}
|
|
|
|
// Convertir le montant en double avec sécurité
|
|
double amount = 0.0;
|
|
try {
|
|
if (passage.montant.isNotEmpty) {
|
|
amount = double.parse(passage.montant);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Erreur de conversion du montant: ${passage.montant}: $e');
|
|
}
|
|
|
|
// Récupérer la date avec gestion d'erreur
|
|
DateTime date;
|
|
try {
|
|
date = passage.passedAt;
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de la récupération de la date: $e');
|
|
date = DateTime.now();
|
|
}
|
|
|
|
// Récupérer le type avec gestion d'erreur
|
|
int type;
|
|
try {
|
|
type = passage.fkType;
|
|
// Si le type n'est pas dans les types connus, utiliser 0 comme valeur par défaut
|
|
if (!AppKeys.typesPassages.containsKey(type)) {
|
|
type = 0; // Type inconnu
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de la récupération du type: $e');
|
|
type = 0;
|
|
}
|
|
|
|
// Récupérer le type de règlement avec gestion d'erreur
|
|
int payment;
|
|
try {
|
|
payment = passage.fkTypeReglement;
|
|
// Si le type de règlement n'est pas dans les types connus, utiliser 0 comme valeur par défaut
|
|
if (!AppKeys.typesReglements.containsKey(payment)) {
|
|
payment = 0; // Type de règlement inconnu
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de la récupération du type de règlement: $e');
|
|
payment = 0;
|
|
}
|
|
|
|
// Gérer les champs optionnels
|
|
String name = '';
|
|
try {
|
|
name = passage.name;
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de la récupération du nom: $e');
|
|
}
|
|
|
|
String notes = '';
|
|
try {
|
|
notes = passage.remarque;
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de la récupération des remarques: $e');
|
|
}
|
|
|
|
// Vérifier si un reçu est disponible avec gestion d'erreur
|
|
bool hasReceipt = false;
|
|
try {
|
|
hasReceipt = amount > 0 && type == 1 && passage.nomRecu.isNotEmpty;
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de la vérification du reçu: $e');
|
|
}
|
|
|
|
// Vérifier s'il y a une erreur avec gestion d'erreur
|
|
bool hasError = false;
|
|
try {
|
|
hasError = passage.emailErreur.isNotEmpty;
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de la vérification des erreurs: $e');
|
|
}
|
|
|
|
// Log pour débogage
|
|
debugPrint(
|
|
'Conversion passage ID: ${passage.id}, Type: $type, Date: $date');
|
|
|
|
return {
|
|
'id': passage.id.toString(),
|
|
'address': address,
|
|
'amount': amount,
|
|
'date': date,
|
|
'type': type,
|
|
'payment': payment,
|
|
'name': name,
|
|
'notes': notes,
|
|
'hasReceipt': hasReceipt,
|
|
'hasError': hasError,
|
|
'fkUser': passage.fkUser, // Ajouter l'ID de l'utilisateur
|
|
};
|
|
} catch (e) {
|
|
debugPrint('ERREUR CRITIQUE lors de la conversion du passage: $e');
|
|
// Retourner un objet valide par défaut pour éviter les erreurs
|
|
// Récupérer l'ID de l'utilisateur courant pour l'objet par défaut
|
|
// Utiliser l'instance globale définie dans app.dart
|
|
final currentUserId = userRepository.getCurrentUser()?.id;
|
|
|
|
return {
|
|
'id': 'error',
|
|
'address': 'Adresse non disponible',
|
|
'amount': 0.0,
|
|
'date': DateTime.now(),
|
|
'type': 0,
|
|
'payment': 0,
|
|
'name': 'Nom non disponible',
|
|
'notes': '',
|
|
'hasReceipt': false,
|
|
'hasError': true,
|
|
'fkUser': currentUserId, // Ajouter l'ID de l'utilisateur courant
|
|
};
|
|
}
|
|
}
|
|
|
|
// Construire l'adresse complète à partir des composants
|
|
String _buildFullAddress(PassageModel passage) {
|
|
final List<String> addressParts = [];
|
|
|
|
// Numéro et rue
|
|
if (passage.numero.isNotEmpty) {
|
|
addressParts.add('${passage.numero} ${passage.rue}');
|
|
} else {
|
|
addressParts.add(passage.rue);
|
|
}
|
|
|
|
// Complément rue bis
|
|
if (passage.rueBis.isNotEmpty) {
|
|
addressParts.add(passage.rueBis);
|
|
}
|
|
|
|
// Résidence/Bâtiment
|
|
if (passage.residence.isNotEmpty) {
|
|
addressParts.add(passage.residence);
|
|
}
|
|
|
|
// Appartement
|
|
if (passage.appt.isNotEmpty) {
|
|
addressParts.add('Appt ${passage.appt}');
|
|
}
|
|
|
|
// Niveau
|
|
if (passage.niveau.isNotEmpty) {
|
|
addressParts.add('Niveau ${passage.niveau}');
|
|
}
|
|
|
|
// Ville
|
|
if (passage.ville.isNotEmpty) {
|
|
addressParts.add(passage.ville);
|
|
}
|
|
|
|
return addressParts.join(', ');
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
super.dispose();
|
|
}
|
|
|
|
// Méthode pour afficher les détails d'un passage
|
|
void _showPassageDetails(Map<String, dynamic> passage) {
|
|
// Récupérer les informations du type de passage et du type de règlement
|
|
final typePassage =
|
|
AppKeys.typesPassages[passage['type']] as Map<String, dynamic>;
|
|
final typeReglement =
|
|
AppKeys.typesReglements[passage['payment']] as Map<String, dynamic>;
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Text('Détails du passage'),
|
|
content: SingleChildScrollView(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
_buildDetailRow('Adresse', passage['address']),
|
|
_buildDetailRow('Nom', passage['name']),
|
|
_buildDetailRow('Date',
|
|
'${passage['date'].day}/${passage['date'].month}/${passage['date'].year}'),
|
|
_buildDetailRow('Type', typePassage['titre']),
|
|
_buildDetailRow('Règlement', typeReglement['titre']),
|
|
_buildDetailRow('Montant', '${passage['amount']}€'),
|
|
if (passage['notes'] != null &&
|
|
passage['notes'].toString().isNotEmpty)
|
|
_buildDetailRow('Notes', passage['notes']),
|
|
if (passage['hasReceipt'] == true)
|
|
_buildDetailRow('Reçu', 'Disponible'),
|
|
if (passage['hasError'] == true)
|
|
_buildDetailRow('Erreur', 'Détectée', isError: true),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: Text('Fermer'),
|
|
),
|
|
if (passage['hasReceipt'] == true)
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
_showReceipt(passage);
|
|
},
|
|
child: Text('Voir le reçu'),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
_editPassage(passage);
|
|
},
|
|
child: Text('Modifier'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Méthode pour éditer un passage
|
|
void _editPassage(Map<String, dynamic> passage) {
|
|
// Implémenter l'ouverture d'un formulaire d'édition
|
|
// Cette méthode pourrait naviguer vers une page d'édition
|
|
debugPrint('Édition du passage ${passage['id']}');
|
|
// Exemple: Navigator.of(context).push(MaterialPageRoute(builder: (_) => EditPassagePage(passage: passage)));
|
|
}
|
|
|
|
// Méthode pour afficher un reçu
|
|
void _showReceipt(Map<String, dynamic> passage) {
|
|
// Implémenter l'affichage ou la génération d'un reçu
|
|
// Cette méthode pourrait générer un PDF et l'afficher
|
|
debugPrint('Affichage du reçu pour le passage ${passage['id']}');
|
|
// Exemple: Navigator.of(context).push(MaterialPageRoute(builder: (_) => ReceiptPage(passage: passage)));
|
|
}
|
|
|
|
// Helper pour construire une ligne de détails
|
|
Widget _buildDetailRow(String label, String value, {bool isError = false}) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SizedBox(
|
|
width: 100,
|
|
child: Text('$label:',
|
|
style: TextStyle(fontWeight: FontWeight.bold))),
|
|
Expanded(
|
|
child: Text(
|
|
value,
|
|
style: isError ? TextStyle(color: Colors.red) : null,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Variable pour gérer la recherche
|
|
String _searchQuery = '';
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
|
|
return Scaffold(
|
|
backgroundColor: Colors.transparent,
|
|
body: SafeArea(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// En-tête avec bouton de rafraîchissement
|
|
Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Historique des passages',
|
|
style: theme.textTheme.headlineMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
onPressed: _loadPassages,
|
|
tooltip: 'Rafraîchir',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Affichage du chargement ou des erreurs
|
|
if (_isLoading)
|
|
const Expanded(
|
|
child: Center(
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
)
|
|
else if (_errorMessage.isNotEmpty)
|
|
Expanded(
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.error_outline,
|
|
size: 48, color: Colors.red),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Erreur de chargement',
|
|
style: theme.textTheme.titleLarge
|
|
?.copyWith(color: Colors.red),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(_errorMessage),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton(
|
|
onPressed: _loadPassages,
|
|
child: const Text('Réessayer'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
)
|
|
// Utilisation du widget PassagesListWidget pour afficher la liste des passages
|
|
else
|
|
Column(
|
|
children: [
|
|
// Stat rapide pour l'utilisateur
|
|
if (_convertedPassages.isNotEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text(
|
|
'${_convertedPassages.length} passages au total (${_convertedPassages.where((p) => (p['date'] as DateTime).isAfter(DateTime(2024, 12, 13))).length} de décembre 2024)',
|
|
style: TextStyle(
|
|
fontStyle: FontStyle.italic,
|
|
color: theme.colorScheme.primary),
|
|
),
|
|
),
|
|
// Widget de liste des passages
|
|
Expanded(
|
|
child: PassagesListWidget(
|
|
passages: _convertedPassages,
|
|
showFilters: true,
|
|
showSearch: true,
|
|
showActions: true,
|
|
initialSearchQuery: _searchQuery,
|
|
initialTypeFilter:
|
|
'Tous', // Toujours commencer avec 'Tous' pour voir tous les types
|
|
initialPaymentFilter: 'Tous',
|
|
// Exclure les passages de type 2 (À finaliser)
|
|
excludePassageTypes: [2],
|
|
// Filtrer par utilisateur courant
|
|
filterByUserId: userRepository.getCurrentUser()?.id,
|
|
// Désactiver les filtres de date implicites
|
|
key: ValueKey(
|
|
'passages_list_${DateTime.now().millisecondsSinceEpoch}'),
|
|
onPassageSelected: (passage) {
|
|
// Action lors de la sélection d'un passage
|
|
debugPrint('Passage sélectionné: ${passage['id']}');
|
|
_showPassageDetails(passage);
|
|
},
|
|
onDetailsView: (passage) {
|
|
// Action lors de l'affichage des détails
|
|
debugPrint('Affichage des détails: ${passage['id']}');
|
|
_showPassageDetails(passage);
|
|
},
|
|
onPassageEdit: (passage) {
|
|
// Action lors de la modification d'un passage
|
|
debugPrint('Modification du passage: ${passage['id']}');
|
|
_editPassage(passage);
|
|
},
|
|
onReceiptView: (passage) {
|
|
// Action lors de la demande d'affichage du reçu
|
|
debugPrint(
|
|
'Affichage du reçu pour le passage: ${passage['id']}');
|
|
_showReceipt(passage);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|