- Amélioration des interfaces utilisateur sur mobile - Optimisation de la responsivité des composants Flutter - Mise à jour des widgets de chat et communication - Amélioration des formulaires et tableaux - Ajout de nouveaux composants pour l'administration - Optimisation des thèmes et styles visuels 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1089 lines
40 KiB
Dart
Executable File
1089 lines
40 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
|
|
String selectedSector = 'Tous';
|
|
String selectedPeriod = 'Tous';
|
|
String selectedType = 'Tous';
|
|
String selectedPaymentMethod = 'Tous';
|
|
DateTimeRange? selectedDateRange;
|
|
|
|
// IDs pour les filtres
|
|
int? selectedSectorId;
|
|
|
|
// 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? preselectedSectorName = _settingsBox.get('history_selectedSectorName');
|
|
final int? preselectedTypeId = _settingsBox.get('history_selectedTypeId');
|
|
final String? preselectedPeriod = _settingsBox.get('history_selectedPeriod');
|
|
final int? preselectedPaymentId = _settingsBox.get('history_selectedPaymentId');
|
|
|
|
if (preselectedSectorId != null && preselectedSectorName != null) {
|
|
selectedSectorId = preselectedSectorId;
|
|
selectedSector = preselectedSectorName;
|
|
debugPrint('Secteur présélectionné: $preselectedSectorName (ID: $preselectedSectorId)');
|
|
}
|
|
|
|
if (preselectedTypeId != null) {
|
|
selectedType = preselectedTypeId.toString();
|
|
debugPrint('Type de passage présélectionné: $preselectedTypeId');
|
|
}
|
|
|
|
if (preselectedPeriod != null) {
|
|
selectedPeriod = preselectedPeriod;
|
|
_updatePeriodFilter(preselectedPeriod);
|
|
debugPrint('Période présélectionnée: $preselectedPeriod');
|
|
}
|
|
|
|
if (preselectedPaymentId != null) {
|
|
selectedPaymentMethod = preselectedPaymentId.toString();
|
|
debugPrint('Mode de règlement présélectionné: $preselectedPaymentId');
|
|
}
|
|
|
|
// 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);
|
|
_settingsBox.put('history_selectedSectorName', selectedSector);
|
|
}
|
|
|
|
if (selectedType != 'Tous') {
|
|
final typeId = int.tryParse(selectedType);
|
|
if (typeId != null) {
|
|
_settingsBox.put('history_selectedTypeId', typeId);
|
|
}
|
|
}
|
|
|
|
if (selectedPeriod != 'Tous') {
|
|
_settingsBox.put('history_selectedPeriod', selectedPeriod);
|
|
}
|
|
|
|
if (selectedPaymentMethod != 'Tous') {
|
|
final paymentId = int.tryParse(selectedPaymentMethod);
|
|
if (paymentId != null) {
|
|
_settingsBox.put('history_selectedPaymentId', paymentId);
|
|
}
|
|
}
|
|
} 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(() {
|
|
selectedSector = sectorName;
|
|
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 type
|
|
if (selectedType != 'Tous') {
|
|
final typeId = int.tryParse(selectedType);
|
|
if (typeId != null && passage['type'] != typeId) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Filtrer par mode de règlement
|
|
if (selectedPaymentMethod != 'Tous') {
|
|
final paymentId = int.tryParse(selectedPaymentMethod);
|
|
if (paymentId != null && passage['payment'] != paymentId) {
|
|
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Construction des filtres
|
|
Widget _buildFilters(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final size = MediaQuery.of(context).size;
|
|
final isDesktop = size.width > 900;
|
|
|
|
return Card(
|
|
elevation: 2,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
color: Colors.white.withValues(alpha: 0.95),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Filtres',
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
if (isDesktop)
|
|
Row(
|
|
children: [
|
|
// Filtre par secteur (si plusieurs secteurs)
|
|
if (_userSectors.length > 1)
|
|
Expanded(
|
|
child: _buildSectorFilter(theme),
|
|
),
|
|
if (_userSectors.length > 1)
|
|
const SizedBox(width: 16),
|
|
|
|
// Filtre par période
|
|
Expanded(
|
|
child: _buildPeriodFilter(theme),
|
|
),
|
|
],
|
|
)
|
|
else
|
|
Column(
|
|
children: [
|
|
// Filtre par secteur (si plusieurs secteurs)
|
|
if (_userSectors.length > 1) ...[
|
|
_buildSectorFilter(theme),
|
|
const SizedBox(height: 16),
|
|
],
|
|
|
|
// Filtre par période
|
|
_buildPeriodFilter(theme),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Construction du filtre par secteur
|
|
Widget _buildSectorFilter(ThemeData theme) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Secteur',
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: theme.colorScheme.outline),
|
|
borderRadius: BorderRadius.circular(8.0),
|
|
),
|
|
child: DropdownButtonHideUnderline(
|
|
child: DropdownButton<String>(
|
|
value: selectedSector,
|
|
isExpanded: true,
|
|
icon: const Icon(Icons.arrow_drop_down),
|
|
items: [
|
|
const DropdownMenuItem<String>(
|
|
value: 'Tous',
|
|
child: Text('Tous les secteurs'),
|
|
),
|
|
..._userSectors.map((sector) {
|
|
final String libelle = sector.libelle.isNotEmpty
|
|
? sector.libelle
|
|
: 'Secteur ${sector.id}';
|
|
return DropdownMenuItem<String>(
|
|
value: libelle,
|
|
child: Text(
|
|
libelle,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
onChanged: (String? value) {
|
|
if (value != null) {
|
|
if (value == 'Tous') {
|
|
_updateSectorFilter('Tous', null);
|
|
} else {
|
|
try {
|
|
final sector = _userSectors.firstWhere(
|
|
(s) => s.libelle == value,
|
|
);
|
|
_updateSectorFilter(value, sector.id);
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de la sélection du secteur: $e');
|
|
_updateSectorFilter('Tous', null);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// Construction du filtre par période
|
|
Widget _buildPeriodFilter(ThemeData theme) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Période',
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: theme.colorScheme.outline),
|
|
borderRadius: BorderRadius.circular(8.0),
|
|
),
|
|
child: DropdownButtonHideUnderline(
|
|
child: DropdownButton<String>(
|
|
value: selectedPeriod,
|
|
isExpanded: true,
|
|
icon: const Icon(Icons.arrow_drop_down),
|
|
items: const [
|
|
DropdownMenuItem<String>(
|
|
value: 'Tous',
|
|
child: Text('Toutes les périodes'),
|
|
),
|
|
DropdownMenuItem<String>(
|
|
value: 'Derniers 15 jours',
|
|
child: Text('Derniers 15 jours'),
|
|
),
|
|
DropdownMenuItem<String>(
|
|
value: 'Dernière semaine',
|
|
child: Text('Dernière semaine'),
|
|
),
|
|
DropdownMenuItem<String>(
|
|
value: 'Dernier mois',
|
|
child: Text('Dernier mois'),
|
|
),
|
|
],
|
|
onChanged: (String? value) {
|
|
if (value != null) {
|
|
_updatePeriodFilter(value);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
),
|
|
|
|
// Afficher la plage de dates sélectionnée si elle existe
|
|
if (selectedDateRange != null && selectedPeriod != 'Tous')
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8.0),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.date_range,
|
|
size: 16,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Du ${selectedDateRange!.start.day}/${selectedDateRange!.start.month}/${selectedDateRange!.start.year} au ${selectedDateRange!.end.day}/${selectedDateRange!.end.month}/${selectedDateRange!.end.year}',
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: theme.colorScheme.primary,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
|
|
return Scaffold(
|
|
backgroundColor: Colors.transparent,
|
|
body: SafeArea(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Filtres avec bouton de rafraîchissement
|
|
Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Filtres (secteur et période) avec bouton rafraîchir
|
|
if (!_isLoading && (_userSectors.length > 1 || selectedPeriod != 'Tous'))
|
|
_buildFilters(context),
|
|
],
|
|
),
|
|
),
|
|
|
|
// 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 les filtres
|
|
passagesMap = _getFilteredPassages(passagesMap);
|
|
|
|
// Appliquer le tri sélectionné
|
|
passagesMap = _sortPassages(passagesMap);
|
|
|
|
return PassagesListWidget(
|
|
showAddButton: true, // Activer le bouton de création
|
|
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,
|
|
),
|
|
],
|
|
),
|
|
passages: passagesMap,
|
|
showFilters: true,
|
|
showSearch: true,
|
|
showActions: true,
|
|
initialSearchQuery: '',
|
|
initialTypeFilter: selectedType,
|
|
initialPaymentFilter: selectedPaymentMethod,
|
|
excludePassageTypes: const [],
|
|
filterByUserId: null, // Déjà filtré en amont
|
|
key: const ValueKey('user_passages_list'),
|
|
onPassageSelected: null,
|
|
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();
|
|
}
|
|
} |