Files
geo/app/lib/presentation/user/user_history_page.dart
Pierre 242a90720e feat: Début des évolutions interfaces mobiles v3.2.4
- Préparation de la nouvelle branche pour les évolutions
- Mise à jour de la version vers 3.2.4
- Intégration des modifications en cours

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 16:49:29 +02:00

844 lines
32 KiB
Dart
Executable File

import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:flutter/material.dart';
import 'package:geosector_app/core/theme/app_theme.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart';
import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/data/models/sector_model.dart';
import 'package:geosector_app/core/repositories/sector_repository.dart';
class UserHistoryPage extends StatefulWidget {
const UserHistoryPage({super.key});
@override
State<UserHistoryPage> createState() => _UserHistoryPageState();
}
// Enum pour gérer les types de tri
enum PassageSortType {
dateDesc, // Plus récent en premier (défaut)
dateAsc, // Plus ancien en premier
addressAsc, // Adresse A-Z
addressDesc, // Adresse Z-A
}
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 = '';
// Statistiques pour l'affichage
int _totalSectors = 0;
int _sharedMembersCount = 0;
// État du tri actuel
PassageSortType _currentSort = PassageSortType.dateDesc;
// État des filtres (uniquement pour synchronisation)
int? selectedSectorId;
String selectedPeriod = 'Toutes';
DateTimeRange? selectedDateRange;
// Repository pour les secteurs
late SectorRepository _sectorRepository;
// Liste des secteurs disponibles pour l'utilisateur
List<SectorModel> _userSectors = [];
// Box des settings pour sauvegarder les préférences
late Box _settingsBox;
@override
void initState() {
super.initState();
// Initialiser le repository
_sectorRepository = sectorRepository;
// Initialiser les settings et charger les données
_initSettingsAndLoad();
}
// Initialiser les settings et charger les préférences
Future<void> _initSettingsAndLoad() async {
try {
// Ouvrir la box des settings
if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) {
_settingsBox = await Hive.openBox(AppKeys.settingsBoxName);
} else {
_settingsBox = Hive.box(AppKeys.settingsBoxName);
}
// Charger les préférences présélectionnées
_loadPreselectedFilters();
// Charger les secteurs de l'utilisateur
_loadUserSectors();
// Charger les passages
await _loadPassages();
} catch (e) {
debugPrint('Erreur lors de l\'initialisation: $e');
setState(() {
_isLoading = false;
_errorMessage = 'Erreur lors de l\'initialisation: $e';
});
}
}
// Charger les secteurs de l'utilisateur
void _loadUserSectors() {
try {
// Récupérer l'ID de l'utilisateur courant
final currentUserId = userRepository.getCurrentUser()?.id;
if (currentUserId != null) {
// Récupérer tous les secteurs
final allSectors = _sectorRepository.getAllSectors();
// Filtrer les secteurs où l'utilisateur a des passages
final userSectorIds = <int>{};
final allPassages = passageRepository.passages;
for (var passage in allPassages) {
if (passage.fkUser == currentUserId && passage.fkSector != null) {
userSectorIds.add(passage.fkSector!);
}
}
// Récupérer les secteurs correspondants
_userSectors = allSectors.where((sector) => userSectorIds.contains(sector.id)).toList();
debugPrint('Nombre de secteurs pour l\'utilisateur: ${_userSectors.length}');
}
} catch (e) {
debugPrint('Erreur lors du chargement des secteurs utilisateur: $e');
}
}
// Charger les filtres présélectionnés depuis Hive
void _loadPreselectedFilters() {
try {
// Charger le secteur présélectionné
final int? preselectedSectorId = _settingsBox.get('history_selectedSectorId');
final String? preselectedPeriod = _settingsBox.get('history_selectedPeriod');
if (preselectedSectorId != null) {
selectedSectorId = preselectedSectorId;
debugPrint('Secteur présélectionné: ID $preselectedSectorId');
}
if (preselectedPeriod != null) {
selectedPeriod = preselectedPeriod;
_updatePeriodFilter(preselectedPeriod);
debugPrint('Période présélectionnée: $preselectedPeriod');
}
// Nettoyer les valeurs après utilisation
_settingsBox.delete('history_selectedSectorId');
_settingsBox.delete('history_selectedSectorName');
_settingsBox.delete('history_selectedTypeId');
_settingsBox.delete('history_selectedPeriod');
_settingsBox.delete('history_selectedPaymentId');
} catch (e) {
debugPrint('Erreur lors du chargement des filtres présélectionnés: $e');
}
}
// Sauvegarder les préférences de filtres
void _saveFilterPreferences() {
try {
if (selectedSectorId != null) {
_settingsBox.put('history_selectedSectorId', selectedSectorId);
}
if (selectedPeriod != 'Toutes') {
_settingsBox.put('history_selectedPeriod', selectedPeriod);
}
} catch (e) {
debugPrint('Erreur lors de la sauvegarde des préférences: $e');
}
}
// Mettre à jour le filtre par secteur
void _updateSectorFilter(String sectorName, int? sectorId) {
setState(() {
selectedSectorId = sectorId;
});
_saveFilterPreferences();
}
// Mettre à jour le filtre par période
void _updatePeriodFilter(String period) {
setState(() {
selectedPeriod = period;
// Mettre à jour la plage de dates en fonction de la période
final DateTime now = DateTime.now();
switch (period) {
case 'Derniers 15 jours':
selectedDateRange = DateTimeRange(
start: now.subtract(const Duration(days: 15)),
end: now,
);
break;
case 'Dernière semaine':
selectedDateRange = DateTimeRange(
start: now.subtract(const Duration(days: 7)),
end: now,
);
break;
case 'Dernier mois':
selectedDateRange = DateTimeRange(
start: DateTime(now.year, now.month - 1, now.day),
end: now,
);
break;
case 'Tous':
selectedDateRange = null;
break;
}
});
_saveFilterPreferences();
}
// 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
final List<PassageModel> allPassages = passageRepository.passages;
debugPrint('Nombre total de passages dans la box: ${allPassages.length}');
// Filtrer les passages de l'utilisateur courant
final currentUserId = userRepository.getCurrentUser()?.id;
List<PassageModel> filtered = allPassages.where((p) => p.fkUser == currentUserId).toList();
debugPrint('Nombre de passages de l\'utilisateur: ${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');
});
// Calculer le nombre de secteurs uniques
final Set<int> uniqueSectors = {};
for (var passage in filtered) {
if (passage.fkSector != null && passage.fkSector! > 0) {
uniqueSectors.add(passage.fkSector!);
}
}
// Compter les membres partagés (autres membres dans la même amicale)
int sharedMembers = 0;
try {
final allMembers = membreRepository.membres;
// Compter les membres autres que l'utilisateur courant
sharedMembers = allMembers.where((membre) => membre.id != currentUserId).length;
debugPrint('Nombre de membres partagés: $sharedMembers');
} catch (e) {
debugPrint('Erreur lors du comptage des membres: $e');
}
// Convertir les modèles en Maps pour l'affichage
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');
}
}
debugPrint('Nombre de passages après conversion: ${passagesMap.length}');
// Trier par date (plus récent en premier)
passagesMap = _sortPassages(passagesMap);
setState(() {
_convertedPassages = passagesMap;
_totalSectors = uniqueSectors.length;
_sharedMembersCount = sharedMembers;
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = 'Erreur lors du chargement des passages: $e';
_isLoading = false;
});
debugPrint(_errorMessage);
}
}
// Filtrer les passages selon les critères sélectionnés
List<Map<String, dynamic>> _getFilteredPassages(List<Map<String, dynamic>> passages) {
return passages.where((passage) {
// Filtrer par secteur
if (selectedSectorId != null && passage['fkSector'] != selectedSectorId) {
return false;
}
// Filtrer par période/date
if (selectedDateRange != null && passage['date'] is DateTime) {
final DateTime passageDate = passage['date'] as DateTime;
if (passageDate.isBefore(selectedDateRange!.start) ||
passageDate.isAfter(selectedDateRange!.end)) {
return false;
}
}
return true;
}).toList();
}
// Convertir un modèle de passage en Map pour l'affichage
Map<String, dynamic> _convertPassageModelToMap(PassageModel passage) {
try {
// Construire l'adresse complète
String address = _buildFullAddress(passage);
// Convertir le montant en double
double amount = 0.0;
if (passage.montant.isNotEmpty) {
amount = double.tryParse(passage.montant) ?? 0.0;
}
// Récupérer la date
DateTime date = passage.passedAt ?? DateTime.now();
// Récupérer le type
int type = passage.fkType;
if (!AppKeys.typesPassages.containsKey(type)) {
type = 1; // Type 1 par défaut (Effectué)
}
// Récupérer le type de règlement
int payment = passage.fkTypeReglement;
if (!AppKeys.typesReglements.containsKey(payment)) {
payment = 0; // Type de règlement inconnu
}
// Vérifier si un reçu est disponible
bool hasReceipt = amount > 0 && type == 1 && passage.nomRecu.isNotEmpty;
// Vérifier s'il y a une erreur
bool hasError = passage.emailErreur.isNotEmpty;
// Récupérer le secteur
SectorModel? sector;
if (passage.fkSector != null) {
sector = _sectorRepository.getSectorById(passage.fkSector!);
}
return {
'id': passage.id,
'address': address,
'amount': amount,
'date': date,
'type': type,
'payment': payment,
'name': passage.name,
'notes': passage.remarque,
'hasReceipt': hasReceipt,
'hasError': hasError,
'fkUser': passage.fkUser,
'fkSector': passage.fkSector,
'sector': sector?.libelle ?? 'Secteur inconnu',
'isOwnedByCurrentUser': passage.fkUser == userRepository.getCurrentUser()?.id,
// Composants de l'adresse pour le tri
'rue': passage.rue,
'numero': passage.numero,
'rueBis': passage.rueBis,
};
} catch (e) {
debugPrint('Erreur lors de la conversion du passage: $e');
// Retourner un objet valide par défaut
final currentUserId = userRepository.getCurrentUser()?.id;
return {
'id': 0,
'address': 'Adresse non disponible',
'amount': 0.0,
'date': DateTime.now(),
'type': 1,
'payment': 1,
'name': 'Nom non disponible',
'notes': '',
'hasReceipt': false,
'hasError': true,
'fkUser': currentUserId,
'fkSector': null,
'sector': 'Secteur inconnu',
'rue': '',
'numero': '',
'rueBis': '',
};
}
}
// Méthode pour trier les passages selon le type de tri sélectionné
List<Map<String, dynamic>> _sortPassages(List<Map<String, dynamic>> passages) {
final sortedPassages = List<Map<String, dynamic>>.from(passages);
switch (_currentSort) {
case PassageSortType.dateDesc:
sortedPassages.sort((a, b) {
try {
return (b['date'] as DateTime).compareTo(a['date'] as DateTime);
} catch (e) {
return 0;
}
});
break;
case PassageSortType.dateAsc:
sortedPassages.sort((a, b) {
try {
return (a['date'] as DateTime).compareTo(b['date'] as DateTime);
} catch (e) {
return 0;
}
});
break;
case PassageSortType.addressAsc:
sortedPassages.sort((a, b) {
try {
// Tri intelligent par rue, numéro, rueBis
final String rueA = a['rue'] ?? '';
final String rueB = b['rue'] ?? '';
final String numeroA = a['numero'] ?? '';
final String numeroB = b['numero'] ?? '';
final String rueBisA = a['rueBis'] ?? '';
final String rueBisB = b['rueBis'] ?? '';
// D'abord comparer les rues
int rueCompare = rueA.toLowerCase().compareTo(rueB.toLowerCase());
if (rueCompare != 0) return rueCompare;
// Si les rues sont identiques, comparer les numéros (numériquement)
int numA = int.tryParse(numeroA) ?? 0;
int numB = int.tryParse(numeroB) ?? 0;
int numCompare = numA.compareTo(numB);
if (numCompare != 0) return numCompare;
// Si les numéros sont identiques, comparer les rueBis
return rueBisA.toLowerCase().compareTo(rueBisB.toLowerCase());
} catch (e) {
return 0;
}
});
break;
case PassageSortType.addressDesc:
sortedPassages.sort((a, b) {
try {
// Tri intelligent inversé
final String rueA = a['rue'] ?? '';
final String rueB = b['rue'] ?? '';
final String numeroA = a['numero'] ?? '';
final String numeroB = b['numero'] ?? '';
final String rueBisA = a['rueBis'] ?? '';
final String rueBisB = b['rueBis'] ?? '';
// D'abord comparer les rues (inversé)
int rueCompare = rueB.toLowerCase().compareTo(rueA.toLowerCase());
if (rueCompare != 0) return rueCompare;
// Si les rues sont identiques, comparer les numéros (inversé)
int numA = int.tryParse(numeroA) ?? 0;
int numB = int.tryParse(numeroB) ?? 0;
int numCompare = numB.compareTo(numA);
if (numCompare != 0) return numCompare;
// Si les numéros sont identiques, comparer les rueBis (inversé)
return rueBisB.toLowerCase().compareTo(rueBisA.toLowerCase());
} catch (e) {
return 0;
}
});
break;
}
return sortedPassages;
}
// 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(', ');
}
// 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['sector'] != null)
_buildDetailRow('Secteur', passage['sector']),
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) {
debugPrint('Édition du passage ${passage['id']}');
}
// Méthode pour afficher un reçu
void _showReceipt(Map<String, dynamic> passage) {
debugPrint('Affichage du reçu pour le passage ${passage['id']}');
}
// 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,
),
),
],
),
);
}
// Les filtres sont maintenant gérés directement dans le PassagesListWidget
// Méthodes de filtre retirées car maintenant gérées dans le widget
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
backgroundColor: Colors.transparent,
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Les filtres sont maintenant intégrés dans le PassagesListWidget
// 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: TextStyle(
fontSize: AppTheme.r(context, 22),
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
Expanded(
child: Container(
color: Colors.transparent,
child: Column(
children: [
// Widget de liste des passages avec ValueListenableBuilder
Expanded(
child: ValueListenableBuilder(
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
builder: (context, Box<PassageModel> passagesBox, child) {
// Reconvertir les passages à chaque changement
final currentUserId = userRepository.getCurrentUser()?.id;
final List<PassageModel> allPassages = passagesBox.values
.where((p) => p.fkUser == currentUserId)
.toList();
// Appliquer le même filtrage et conversion
List<Map<String, dynamic>> passagesMap = [];
for (var passage in allPassages) {
try {
final Map<String, dynamic> passageMap = _convertPassageModelToMap(passage);
passagesMap.add(passageMap);
} catch (e) {
debugPrint('Erreur lors de la conversion du passage en map: $e');
}
}
// Appliquer le tri sélectionné
passagesMap = _sortPassages(passagesMap);
return PassagesListWidget(
// Données
passages: passagesMap,
// Activation des filtres
showFilters: true,
showSearch: true,
showTypeFilter: true,
showPaymentFilter: true,
showSectorFilter: true,
showUserFilter: false, // Pas de filtre membre pour la page user
showPeriodFilter: true,
// Données pour les filtres
sectors: _userSectors,
members: null, // Pas de filtre membre pour la page user
// Valeurs initiales
initialSectorId: selectedSectorId,
initialPeriod: selectedPeriod,
dateRange: selectedDateRange,
// Filtre par utilisateur courant
filterByUserId: currentUserId,
// Bouton d'ajout
showAddButton: true,
onAddPassage: () async {
// Ouvrir le dialogue de création de passage
await showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return PassageFormDialog(
title: 'Nouveau passage',
passageRepository: passageRepository,
userRepository: userRepository,
operationRepository: operationRepository,
onSuccess: () {
// Le widget se rafraîchira automatiquement via ValueListenableBuilder
},
);
},
);
},
sortingButtons: Row(
children: [
// Bouton tri par date avec icône calendrier
IconButton(
icon: Icon(
Icons.calendar_today,
size: 20,
color: _currentSort == PassageSortType.dateDesc ||
_currentSort == PassageSortType.dateAsc
? theme.colorScheme.primary
: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
tooltip: _currentSort == PassageSortType.dateAsc
? 'Tri par date (ancien en premier)'
: 'Tri par date (récent en premier)',
onPressed: () {
setState(() {
if (_currentSort == PassageSortType.dateDesc) {
_currentSort = PassageSortType.dateAsc;
} else {
_currentSort = PassageSortType.dateDesc;
}
});
},
),
// Indicateur de direction pour la date
if (_currentSort == PassageSortType.dateDesc ||
_currentSort == PassageSortType.dateAsc)
Icon(
_currentSort == PassageSortType.dateAsc
? Icons.arrow_upward
: Icons.arrow_downward,
size: 14,
color: theme.colorScheme.primary,
),
const SizedBox(width: 4),
// Bouton tri par adresse avec icône maison
IconButton(
icon: Icon(
Icons.home,
size: 20,
color: _currentSort == PassageSortType.addressDesc ||
_currentSort == PassageSortType.addressAsc
? theme.colorScheme.primary
: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
tooltip: _currentSort == PassageSortType.addressAsc
? 'Tri par adresse (A-Z)'
: 'Tri par adresse (Z-A)',
onPressed: () {
setState(() {
if (_currentSort == PassageSortType.addressAsc) {
_currentSort = PassageSortType.addressDesc;
} else {
_currentSort = PassageSortType.addressAsc;
}
});
},
),
// Indicateur de direction pour l'adresse
if (_currentSort == PassageSortType.addressDesc ||
_currentSort == PassageSortType.addressAsc)
Icon(
_currentSort == PassageSortType.addressAsc
? Icons.arrow_upward
: Icons.arrow_downward,
size: 14,
color: theme.colorScheme.primary,
),
],
),
// Actions
showActions: true,
key: const ValueKey('user_passages_list'),
// Callback pour synchroniser les filtres
onFiltersChanged: (filters) {
setState(() {
selectedSectorId = filters['sectorId'];
selectedPeriod = filters['period'] ?? 'Toutes';
selectedDateRange = filters['dateRange'];
});
},
onDetailsView: (passage) {
debugPrint('Affichage des détails: ${passage['id']}');
_showPassageDetails(passage);
},
onPassageEdit: (passage) {
debugPrint('Modification du passage: ${passage['id']}');
_editPassage(passage);
},
onReceiptView: (passage) {
debugPrint('Affichage du reçu pour le passage: ${passage['id']}');
_showReceipt(passage);
},
onPassageDelete: (passage) {
// Pas besoin de recharger, le ValueListenableBuilder
// se rafraîchira automatiquement après la suppression
},
);
},
),
),
],
),
),
),
],
),
),
);
}
@override
void dispose() {
super.dispose();
}
}