Files
geo/app/lib/presentation/user/user_history_page.dart
pierre 1018b86537 feat: Gestion des secteurs et migration v3.0.4+304
- Ajout système complet de gestion des secteurs avec contours géographiques
- Import des contours départementaux depuis GeoJSON
- API REST pour la gestion des secteurs (/api/sectors)
- Service de géolocalisation pour déterminer les secteurs
- Migration base de données avec tables x_departements_contours et sectors_adresses
- Interface Flutter pour visualisation et gestion des secteurs
- Ajout thème sombre dans l'application
- Corrections diverses et optimisations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-07 11:01:45 +02:00

575 lines
20 KiB
Dart
Executable File

import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:flutter/material.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/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 (exclure les passages sans date)
final sortedByDate =
List<PassageModel>.from(filtered.where((p) => p.passedAt != null));
if (sortedByDate.isNotEmpty) {
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) {
// Ignorer les passages sans date
if (passage.passedAt != null) {
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);
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 ?? DateTime.now();
} 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: const 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: const Text('Fermer'),
),
if (passage['hasReceipt'] == true)
TextButton(
onPressed: () {
Navigator.of(context).pop();
_showReceipt(passage);
},
child: const Text('Voir le reçu'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
_editPassage(passage);
},
child: const 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: const TextStyle(fontWeight: FontWeight.bold))),
Expanded(
child: Text(
value,
style: isError ? const TextStyle(color: Colors.red) : null,
),
),
],
),
);
}
// Variable pour gérer la recherche
final 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: const [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);
},
),
),
],
),
],
),
),
);
}
}