Files
geo/app/lib/presentation/admin/admin_history_page.dart

1916 lines
68 KiB
Dart
Executable File

import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.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/data/models/membre_model.dart';
import 'package:geosector_app/core/repositories/passage_repository.dart';
import 'package:geosector_app/core/repositories/sector_repository.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/core/repositories/membre_repository.dart';
import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart';
import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart';
import 'dart:math' as math;
/// Class pour dessiner les petits points blancs sur le fond
class DotsPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.5)
..style = PaintingStyle.fill;
final random = math.Random(42); // Seed fixe pour consistance
final numberOfDots = (size.width * size.height) ~/ 1500;
for (int i = 0; i < numberOfDots; i++) {
final x = random.nextDouble() * size.width;
final y = random.nextDouble() * size.height;
final radius = 1.0 + random.nextDouble() * 2.0;
canvas.drawCircle(Offset(x, y), radius, paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
// 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 AdminHistoryPage extends StatefulWidget {
const AdminHistoryPage({super.key});
@override
State<AdminHistoryPage> createState() => _AdminHistoryPageState();
}
class _AdminHistoryPageState extends State<AdminHistoryPage> {
// État des filtres
String searchQuery = '';
String selectedSector = 'Tous';
String selectedUser = 'Tous';
String selectedType = 'Tous';
String selectedPaymentMethod = 'Tous';
String selectedPeriod = 'Tous'; // Période par défaut
DateTimeRange? selectedDateRange;
// État du tri actuel
PassageSortType _currentSort = PassageSortType.dateDesc;
// Contrôleur pour la recherche
final TextEditingController _searchController = TextEditingController();
// IDs pour les filtres
int? selectedSectorId;
int? selectedUserId;
// Listes pour les filtres
List<SectorModel> _sectors = [];
List<MembreModel> _membres = [];
// Repositories
late PassageRepository _passageRepository;
late SectorRepository _sectorRepository;
late UserRepository _userRepository;
late MembreRepository _membreRepository;
// Passages formatés pour l'affichage
List<Map<String, dynamic>> _formattedPassages = [];
// Passages originaux pour l'édition
List<PassageModel> _originalPassages = [];
// État de chargement
bool _isLoading = true;
String _errorMessage = '';
@override
void initState() {
super.initState();
// Initialiser les filtres
_initializeFilters();
// Charger les filtres présélectionnés depuis Hive si disponibles
_loadPreselectedFilters();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Récupérer les repositories une seule fois
_loadRepositories();
}
// Charger les repositories et les données
void _loadRepositories() {
try {
// Utiliser les instances globales définies dans app.dart
_passageRepository = passageRepository;
_userRepository = userRepository;
_sectorRepository = sectorRepository;
_membreRepository = membreRepository;
// Charger les secteurs et les membres
_loadSectorsAndMembres();
// Charger les passages
_loadPassages();
} catch (e) {
setState(() {
_isLoading = false;
_errorMessage = 'Erreur lors du chargement des repositories: $e';
});
}
}
// Charger les secteurs et les membres
void _loadSectorsAndMembres() {
try {
// Récupérer la liste des secteurs
_sectors = _sectorRepository.getAllSectors();
debugPrint('Nombre de secteurs récupérés: ${_sectors.length}');
// Récupérer la liste des membres
_membres = _membreRepository.getAllMembres();
debugPrint('Nombre de membres récupérés: ${_membres.length}');
} catch (e) {
debugPrint('Erreur lors du chargement des secteurs et membres: $e');
}
}
// Charger les passages
void _loadPassages() {
setState(() {
_isLoading = true;
});
try {
// Récupérer les passages
final List<PassageModel> allPassages =
_passageRepository.getAllPassages();
// Stocker les passages originaux pour l'édition
_originalPassages = allPassages;
// Convertir les passages en format attendu par PassagesListWidget
_formattedPassages = _formatPassagesForWidget(
allPassages, _sectorRepository, _membreRepository);
setState(() {
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
_errorMessage = 'Erreur lors du chargement des passages: $e';
});
}
}
// Initialiser les filtres
void _initializeFilters() {
// Par défaut, on n'applique pas de filtre par utilisateur ou secteur
selectedSectorId = null;
selectedUserId = null;
// Période par défaut : toutes les périodes
selectedPeriod = 'Tous';
// Plage de dates par défaut : aucune restriction
selectedDateRange = null;
}
// Charger les filtres présélectionnés depuis Hive
void _loadPreselectedFilters() {
try {
// Utiliser Hive directement sans async
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
final settingsBox = Hive.box(AppKeys.settingsBoxName);
// 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');
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');
}
// Nettoyer les valeurs après utilisation pour ne pas les réutiliser la prochaine fois
settingsBox.delete('history_selectedSectorId');
settingsBox.delete('history_selectedSectorName');
settingsBox.delete('history_selectedTypeId');
}
} catch (e) {
debugPrint('Erreur lors du chargement des filtres présélectionnés: $e');
}
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
// Nouvelle méthode pour filtrer une liste de passages déjà formatés
List<Map<String, dynamic>> _getFilteredPassagesFromList(List<Map<String, dynamic>> passages) {
try {
var filtered = passages.where((passage) {
try {
// Ne plus exclure automatiquement les passages de type 2
// car on propose maintenant un filtre par type dans les "Filtres avancés"
// Filtrer par utilisateur
if (selectedUserId != null &&
passage.containsKey('fkUser') &&
passage['fkUser'] != selectedUserId) {
return false;
}
// Filtrer par secteur
if (selectedSectorId != null &&
passage.containsKey('fkSector') &&
passage['fkSector'] != selectedSectorId) {
return false;
}
// Filtrer par type de passage
if (selectedType != 'Tous') {
try {
final int? selectedTypeId = int.tryParse(selectedType);
if (selectedTypeId != null) {
if (!passage.containsKey('type') ||
passage['type'] != selectedTypeId) {
return false;
}
}
} catch (e) {
debugPrint('Erreur de filtrage par type: $e');
}
}
// Filtrer par mode de règlement
if (selectedPaymentMethod != 'Tous') {
try {
final int? selectedPaymentId =
int.tryParse(selectedPaymentMethod);
if (selectedPaymentId != null) {
if (!passage.containsKey('payment') ||
passage['payment'] != selectedPaymentId) {
return false;
}
}
} catch (e) {
debugPrint('Erreur de filtrage par mode de règlement: $e');
}
}
// Filtrer par recherche
if (searchQuery.isNotEmpty) {
try {
final query = searchQuery.toLowerCase();
final address = passage.containsKey('address')
? passage['address']?.toString().toLowerCase() ?? ''
: '';
final name = passage.containsKey('name')
? passage['name']?.toString().toLowerCase() ?? ''
: '';
final notes = passage.containsKey('notes')
? passage['notes']?.toString().toLowerCase() ?? ''
: '';
if (!address.contains(query) &&
!name.contains(query) &&
!notes.contains(query)) {
return false;
}
} catch (e) {
debugPrint('Erreur de filtrage par recherche: $e');
return false;
}
}
// Filtrer par période/date
if (selectedDateRange != null) {
try {
if (passage.containsKey('date') && passage['date'] is DateTime) {
final DateTime passageDate = passage['date'] as DateTime;
if (passageDate.isBefore(selectedDateRange!.start) ||
passageDate.isAfter(selectedDateRange!.end)) {
return false;
}
}
} catch (e) {
debugPrint('Erreur de filtrage par date: $e');
}
}
return true;
} catch (e) {
debugPrint('Erreur lors du filtrage d\'un passage: $e');
return false;
}
}).toList();
// Appliquer le tri sélectionné
filtered = _sortPassages(filtered);
debugPrint(
'Passages filtrés: ${filtered.length}/${passages.length}');
return filtered;
} catch (e) {
debugPrint('Erreur globale lors du filtrage: $e');
return passages;
}
}
// 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 (numérique), 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é par rue, numéro (numérique), 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 (inversé)
int rueCompare = rueB.toLowerCase().compareTo(rueA.toLowerCase());
if (rueCompare != 0) return rueCompare;
// Si les rues sont identiques, comparer les numéros (inversé numériquement)
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;
}
// Méthode pour appliquer tous les filtres (utilisée dans le build)
List<Map<String, dynamic>> _getFilteredPassages() {
return _getFilteredPassagesFromList(_formattedPassages);
}
// Mettre à jour le filtre par secteur
void _updateSectorFilter(String sectorName, int? sectorId) {
setState(() {
selectedSector = sectorName;
selectedSectorId = sectorId;
});
}
// Mettre à jour le filtre par utilisateur
void _updateUserFilter(String userName, int? userId) {
setState(() {
selectedUser = userName;
selectedUserId = userId;
});
}
// 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;
}
});
}
@override
Widget build(BuildContext context) {
// Afficher un widget de chargement ou d'erreur si nécessaire
if (_isLoading) {
return Stack(
children: [
// Fond dégradé avec petits points blancs
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.white, Colors.red.shade300],
),
),
child: CustomPaint(
painter: DotsPainter(),
child: const SizedBox(
width: double.infinity, height: double.infinity),
),
),
const Center(
child: CircularProgressIndicator(),
),
],
);
}
if (_errorMessage.isNotEmpty) {
return _buildErrorWidget(_errorMessage);
}
// Retourner le widget principal avec les données chargées
return Stack(
children: [
// Fond dégradé avec petits points blancs
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.white, Colors.red.shade300],
),
),
child: CustomPaint(
painter: DotsPainter(),
child:
const SizedBox(width: double.infinity, height: double.infinity),
),
),
// Contenu de la page
LayoutBuilder(
builder: (context, constraints) {
final passages = _getFilteredPassages();
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - 32, // Moins le padding
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre de la page
Text(
'Historique des passages',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 16),
// Filtres supplémentaires (secteur, utilisateur, période)
_buildAdditionalFilters(context),
const SizedBox(height: 16),
// Widget de liste des passages avec hauteur fixe et ValueListenableBuilder
SizedBox(
height: constraints.maxHeight * 0.7, // 70% de la hauteur disponible
child: ValueListenableBuilder(
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
builder: (context, Box<PassageModel> passagesBox, child) {
// Reconvertir les passages à chaque changement
final List<PassageModel> allPassages = passagesBox.values.toList();
// Convertir et formater les passages
final formattedPassages = _formatPassagesForWidget(
allPassages,
_sectorRepository,
_membreRepository
);
// Appliquer les filtres
final filteredPassages = _getFilteredPassagesFromList(formattedPassages);
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.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface.withOpacity(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.of(context).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.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface.withOpacity(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.of(context).colorScheme.primary,
),
],
),
passages: filteredPassages,
showFilters: false,
showSearch: false,
showActions: true,
// Le widget gère maintenant le flux conditionnel par défaut
onPassageSelected: null,
onReceiptView: (passage) {
_showReceiptDialog(context, passage);
},
onDetailsView: (passage) {
_showDetailsDialog(context, passage);
},
onPassageEdit: (passage) {
// Action pour modifier le passage
},
onPassageDelete: (passage) {
_showDeleteConfirmationDialog(passage);
},
);
},
),
),
],
),
),
);
},
),
],
);
}
// Widget d'erreur pour afficher un message d'erreur
Widget _buildErrorWidget(String message) {
return Stack(
children: [
// Fond dégradé avec petits points blancs
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.white, Colors.red.shade300],
),
),
child: CustomPaint(
painter: DotsPainter(),
child:
const SizedBox(width: double.infinity, height: double.infinity),
),
),
Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 64,
),
const SizedBox(height: 16),
Text(
'Erreur',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.red[700],
),
),
const SizedBox(height: 8),
Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
// Recharger la page
setState(() {});
},
child: const Text('Réessayer'),
),
],
),
),
),
],
);
}
// Convertir les passages du modèle Hive vers le format attendu par le widget
List<Map<String, dynamic>> _formatPassagesForWidget(
List<PassageModel> passages,
SectorRepository sectorRepository,
MembreRepository membreRepository) {
return passages.map((passage) {
// Récupérer le secteur associé au passage (si fkSector n'est pas null)
final SectorModel? sector = passage.fkSector != null
? sectorRepository.getSectorById(passage.fkSector!)
: null;
// Récupérer le membre associé au passage
final MembreModel? membre =
membreRepository.getMembreById(passage.fkUser);
// Construire l'adresse complète
final String address =
'${passage.numero} ${passage.rue}${passage.rueBis.isNotEmpty ? ' ${passage.rueBis}' : ''}, ${passage.ville}';
// Déterminer si le passage a une erreur d'envoi de reçu
final bool hasError = passage.emailErreur.isNotEmpty;
// Récupérer l'ID de l'utilisateur courant pour déterminer la propriété
final currentUserId = _userRepository.getCurrentUser()?.id;
return {
'id': passage.id,
if (passage.passedAt != null) 'date': passage.passedAt!,
'address': address, // Adresse complète pour l'affichage
// Champs séparés pour l'édition
'numero': passage.numero,
'rueBis': passage.rueBis,
'rue': passage.rue,
'ville': passage.ville,
'residence': passage.residence,
'appt': passage.appt,
'niveau': passage.niveau,
'fkHabitat': passage.fkHabitat,
'fkSector': passage.fkSector,
'sector': sector?.libelle ?? 'Secteur inconnu',
'fkUser': passage.fkUser,
'user': membre?.name ?? 'Membre inconnu',
'type': passage.fkType,
'amount': double.tryParse(passage.montant) ?? 0.0,
'payment': passage.fkTypeReglement,
'email': passage.email,
'hasReceipt': passage.nomRecu.isNotEmpty,
'hasError': hasError,
'notes': passage.remarque,
'name': passage.name,
'phone': passage.phone,
'montant': passage.montant,
'remarque': passage.remarque,
// Autres champs utiles
'fkOperation': passage.fkOperation,
'passedAt': passage.passedAt,
'lastSyncedAt': passage.lastSyncedAt,
'isActive': passage.isActive,
'isSynced': passage.isSynced,
'isOwnedByCurrentUser': passage.fkUser == currentUserId, // Ajout du champ pour le widget
};
}).toList();
}
void _showReceiptDialog(BuildContext context, Map<String, dynamic> passage) {
final int passageId = passage['id'] as int;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Reçu du passage #$passageId'),
content: const SizedBox(
width: 500,
height: 600,
child: Center(
child: Text('Aperçu du reçu PDF'),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Fermer'),
),
ElevatedButton(
onPressed: () {
// Action pour télécharger le reçu
Navigator.pop(context);
},
child: const Text('Télécharger'),
),
],
),
);
}
// Afficher les détails avec option de modification
void _showDetailsDialogWithEditOption(BuildContext context, Map<String, dynamic> passage, PassageModel passageModel) {
final int passageId = passage['id'] as int;
final DateTime date = passage['date'] as DateTime;
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Row(
children: [
const Icon(Icons.info_outline, color: Colors.blue),
const SizedBox(width: 8),
Text('Détails du passage #$passageId'),
],
),
content: SizedBox(
width: 500,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_buildDetailRow('Date',
'${date.day}/${date.month}/${date.year} à ${date.hour}h${date.minute.toString().padLeft(2, '0')}'),
_buildDetailRow('Adresse', passage['address'] as String),
_buildDetailRow('Secteur', passage['sector'] as String),
_buildDetailRow('Collecteur', passage['user'] as String),
_buildDetailRow(
'Type',
AppKeys.typesPassages[passage['type']]?['titre'] ??
'Inconnu'),
_buildDetailRow('Montant', '${passage['amount']}'),
_buildDetailRow(
'Mode de paiement',
AppKeys.typesReglements[passage['payment']]?['titre'] ??
'Inconnu'),
_buildDetailRow('Email', passage['email'] as String),
_buildDetailRow(
'Reçu envoyé', passage['hasReceipt'] ? 'Oui' : 'Non'),
_buildDetailRow(
'Erreur d\'envoi', passage['hasError'] ? 'Oui' : 'Non'),
_buildDetailRow(
'Notes',
(passage['notes'] as String).isEmpty
? '-'
: passage['notes'] as String),
const SizedBox(height: 16),
const Text(
'Historique des actions',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHistoryItem(
date,
passage['user'] as String,
'Création du passage',
),
if (passage['hasReceipt'])
_buildHistoryItem(
date.add(const Duration(minutes: 5)),
'Système',
'Envoi du reçu par email',
),
if (passage['hasError'])
_buildHistoryItem(
date.add(const Duration(minutes: 6)),
'Système',
'Erreur lors de l\'envoi du reçu',
),
],
),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('Fermer'),
),
ElevatedButton.icon(
icon: const Icon(Icons.edit),
onPressed: () {
// Fermer le dialog de détails
Navigator.pop(dialogContext);
// Ouvrir le formulaire de modification
_showEditDialog(context, passageModel);
},
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
),
label: const Text('Modifier'),
),
],
),
);
}
// Méthode pour conserver l'ancienne _showDetailsDialog pour les autres usages
void _showDetailsDialog(BuildContext context, Map<String, dynamic> passage) {
final int passageId = passage['id'] as int;
final DateTime date = passage['date'] as DateTime;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Détails du passage #$passageId'),
content: SizedBox(
width: 500,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_buildDetailRow('Date',
'${date.day}/${date.month}/${date.year} à ${date.hour}h${date.minute.toString().padLeft(2, '0')}'),
_buildDetailRow('Adresse', passage['address'] as String),
_buildDetailRow('Secteur', passage['sector'] as String),
_buildDetailRow('Collecteur', passage['user'] as String),
_buildDetailRow(
'Type',
AppKeys.typesPassages[passage['type']]?['titre'] ??
'Inconnu'),
_buildDetailRow('Montant', '${passage['amount']}'),
_buildDetailRow(
'Mode de paiement',
AppKeys.typesReglements[passage['payment']]?['titre'] ??
'Inconnu'),
_buildDetailRow('Email', passage['email'] as String),
_buildDetailRow(
'Reçu envoyé', passage['hasReceipt'] ? 'Oui' : 'Non'),
_buildDetailRow(
'Erreur d\'envoi', passage['hasError'] ? 'Oui' : 'Non'),
_buildDetailRow(
'Notes',
(passage['notes'] as String).isEmpty
? '-'
: passage['notes'] as String),
const SizedBox(height: 16),
const Text(
'Historique des actions',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHistoryItem(
date,
passage['user'] as String,
'Création du passage',
),
if (passage['hasReceipt'])
_buildHistoryItem(
date.add(const Duration(minutes: 5)),
'Système',
'Envoi du reçu par email',
),
if (passage['hasError'])
_buildHistoryItem(
date.add(const Duration(minutes: 6)),
'Système',
'Erreur lors de l\'envoi du reçu',
),
],
),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Fermer'),
),
],
),
);
}
void _openPassageEditDialog(
BuildContext context, Map<String, dynamic> passage) async {
try {
debugPrint('=== DEBUT _openPassageEditDialog ===');
// Récupérer l'ID et le type du passage
final int passageId = passage['id'] as int;
final int passageType = passage['type'] as int? ?? 1;
debugPrint('Passage ID: $passageId, Type: $passageType');
// Trouver le PassageModel original dans la liste
final PassageModel? passageModel =
_originalPassages.where((p) => p.id == passageId).firstOrNull;
if (passageModel == null) {
throw Exception('Passage original introuvable avec l\'ID: $passageId');
}
debugPrint('PassageModel original trouvé');
if (!mounted) {
debugPrint('Widget non monté, abandon');
return;
}
// Flux conditionnel selon le type de passage
if (passageType == 2) {
// Type 2 ("À finaliser") : Ouvrir directement le formulaire de modification
debugPrint('Passage type 2 - Ouverture directe du formulaire');
_showEditDialog(context, passageModel);
} else {
// Autres types : Afficher d'abord les détails avec option de modification
debugPrint('Passage type $passageType - Affichage des détails d\'abord');
_showDetailsDialogWithEditOption(context, passage, passageModel);
}
debugPrint('=== FIN _openPassageEditDialog ===');
} catch (e, stackTrace) {
debugPrint('=== ERREUR _openPassageEditDialog ===');
debugPrint('Erreur: $e');
debugPrint('StackTrace: $stackTrace');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de l\'ouverture: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
// Méthode extraite pour ouvrir le dialog de modification
void _showEditDialog(BuildContext context, PassageModel passageModel) {
debugPrint('Ouverture du formulaire de modification pour le passage ${passageModel.id}');
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => PassageFormDialog(
passage: passageModel,
title: 'Modifier le passage',
passageRepository: _passageRepository,
userRepository: _userRepository,
operationRepository: operationRepository,
onSuccess: () {
debugPrint('Dialog fermé avec succès');
// Recharger les données après modification
_loadPassages();
},
),
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 150,
child: Text(
'$label :',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
Expanded(
child: Text(value),
),
],
),
);
}
Widget _buildHistoryItem(DateTime date, String user, String action) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${date.day}/${date.month}/${date.year} à ${date.hour}h${date.minute.toString().padLeft(2, '0')}',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
),
Text('$user - $action'),
const Divider(),
],
),
);
}
// Construction des filtres supplémentaires
Widget _buildAdditionalFilters(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, // Fond opaque
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Filtres avancés',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
const SizedBox(height: 16),
// Champ de recherche
_buildSearchField(theme),
const SizedBox(height: 16),
// Disposition des filtres en fonction de la taille de l'écran
isDesktop
? Column(
children: [
// Première ligne : Secteur, Utilisateur, Période
Row(
children: [
// Filtre par secteur
Expanded(
child: _buildSectorFilter(theme, _sectors),
),
const SizedBox(width: 16),
// Filtre par membre
Expanded(
child: _buildMembreFilter(theme, _membres),
),
const SizedBox(width: 16),
// Filtre par période
Expanded(
child: _buildPeriodFilter(theme),
),
],
),
const SizedBox(height: 16),
// Deuxième ligne : Type de passage, Mode de règlement
Row(
children: [
// Filtre par type de passage
Expanded(
child: _buildTypeFilter(theme),
),
const SizedBox(width: 16),
// Filtre par mode de règlement
Expanded(
child: _buildPaymentFilter(theme),
),
// Espacement pour équilibrer avec la ligne du dessus (3 colonnes)
const Expanded(child: SizedBox()),
],
),
],
)
: Column(
children: [
// Filtre par secteur
_buildSectorFilter(theme, _sectors),
const SizedBox(height: 16),
// Filtre par membre
_buildMembreFilter(theme, _membres),
const SizedBox(height: 16),
// Filtre par période
_buildPeriodFilter(theme),
const SizedBox(height: 16),
// Filtre par type de passage
_buildTypeFilter(theme),
const SizedBox(height: 16),
// Filtre par mode de règlement
_buildPaymentFilter(theme),
],
),
],
),
),
);
}
// Construction du filtre par secteur
Widget _buildSectorFilter(ThemeData theme, List<SectorModel> sectors) {
// Vérifier si la liste des secteurs est vide ou si selectedSector n'est pas dans la liste
bool isSelectedSectorValid = selectedSector == 'Tous' ||
sectors.any((s) => s.libelle == selectedSector);
// Si selectedSector n'est pas valide, le réinitialiser à 'Tous'
if (!isSelectedSectorValid) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
selectedSector = 'Tous';
selectedSectorId = null;
});
}
});
}
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: isSelectedSectorValid ? selectedSector : 'Tous',
isExpanded: true,
icon: const Icon(Icons.arrow_drop_down),
items: [
const DropdownMenuItem<String>(
value: 'Tous',
child: Text('Tous les secteurs'),
),
...sectors.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 {
// Trouver le secteur correspondant
final sector = sectors.firstWhere(
(s) => s.libelle == value,
orElse: () => sectors.isNotEmpty
? sectors.first
: throw Exception('Liste de secteurs vide'),
);
// Convertir sector.id en int? si nécessaire
_updateSectorFilter(value, sector.id);
} catch (e) {
debugPrint('Erreur lors de la sélection du secteur: $e');
_updateSectorFilter('Tous', null);
}
}
}
},
),
),
),
],
);
}
// Construction du filtre par membre
Widget _buildMembreFilter(ThemeData theme, List<MembreModel> membres) {
// Fonction pour formater le nom d'affichage d'un membre
String formatMembreDisplayName(MembreModel membre) {
final String firstName = membre.firstName ?? '';
final String name = membre.name ?? '';
final String sectName = membre.sectName ?? '';
// Construire le nom de base
String displayName = '';
if (firstName.isNotEmpty && name.isNotEmpty) {
displayName = '$firstName $name';
} else if (name.isNotEmpty) {
displayName = name;
} else if (firstName.isNotEmpty) {
displayName = firstName;
} else {
displayName = 'Membre inconnu';
}
// Ajouter le sectName entre parenthèses s'il existe
if (sectName.isNotEmpty) {
displayName = '$displayName ($sectName)';
}
return displayName;
}
// Trier les membres par nom de famille
final List<MembreModel> sortedMembres = [...membres];
sortedMembres.sort((a, b) {
final String nameA = a.name ?? '';
final String nameB = b.name ?? '';
return nameA.compareTo(nameB);
});
// Créer une map pour retrouver les membres par leur nom d'affichage
final Map<String, MembreModel> membreDisplayMap = {};
for (final membre in sortedMembres) {
final displayName = formatMembreDisplayName(membre);
membreDisplayMap[displayName] = membre;
}
// Vérifier si la liste des membres est vide ou si selectedUser n'est pas dans la liste
bool isSelectedUserValid =
selectedUser == 'Tous' || membreDisplayMap.containsKey(selectedUser);
// Si selectedUser n'est pas valide, le réinitialiser à 'Tous'
if (!isSelectedUserValid) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
selectedUser = 'Tous';
selectedUserId = null;
});
}
});
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Membre',
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: isSelectedUserValid ? selectedUser : 'Tous',
isExpanded: true,
icon: const Icon(Icons.arrow_drop_down),
items: [
const DropdownMenuItem<String>(
value: 'Tous',
child: Text('Tous les membres'),
),
...membreDisplayMap.entries.map((entry) {
final String displayName = entry.key;
return DropdownMenuItem<String>(
value: displayName,
child: Text(
displayName,
overflow: TextOverflow.ellipsis,
),
);
}),
],
onChanged: (String? value) {
if (value != null) {
if (value == 'Tous') {
_updateUserFilter('Tous', null);
} else {
try {
// Trouver le membre correspondant dans la map
final membre = membreDisplayMap[value];
if (membre != null) {
final int membreId = membre.id;
_updateUserFilter(value, membreId);
} else {
throw Exception('Membre non trouvé: $value');
}
} catch (e) {
debugPrint('Erreur lors de la sélection du membre: $e');
_updateUserFilter('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,
),
),
],
),
),
],
);
}
// Construction du champ de recherche
Widget _buildSearchField(ThemeData theme) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Recherche',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Rechercher par adresse ou nom...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
setState(() {
_searchController.clear();
searchQuery = '';
});
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onChanged: (value) {
setState(() {
searchQuery = value;
});
},
),
],
);
}
// Construction du filtre par type de passage
Widget _buildTypeFilter(ThemeData theme) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Type de passage',
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: selectedType,
isExpanded: true,
icon: const Icon(Icons.arrow_drop_down),
items: [
const DropdownMenuItem<String>(
value: 'Tous',
child: Text('Tous les types'),
),
...AppKeys.typesPassages.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key.toString(),
child: Text(
entry.value['titre'] as String,
overflow: TextOverflow.ellipsis,
),
);
}),
],
onChanged: (String? value) {
if (value != null) {
setState(() {
selectedType = value;
});
}
},
),
),
),
],
);
}
// Afficher le dialog de confirmation de suppression
void _showDeleteConfirmationDialog(Map<String, dynamic> passage) {
final TextEditingController confirmController = TextEditingController();
// Récupérer l'ID du passage et trouver le PassageModel original
final int passageId = passage['id'] as int;
final PassageModel? passageModel =
_originalPassages.where((p) => p.id == passageId).firstOrNull;
if (passageModel == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Impossible de trouver le passage'),
backgroundColor: Colors.red,
),
);
return;
}
final String streetNumber = passageModel.numero ?? '';
final String fullAddress = '${passageModel.numero ?? ''} ${passageModel.rueBis ?? ''} ${passageModel.rue ?? ''}'.trim();
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: const Row(
children: [
Icon(Icons.warning, color: Colors.red, size: 28),
SizedBox(width: 8),
Text('Confirmation de suppression'),
],
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'ATTENTION : Cette action est irréversible !',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.red,
fontSize: 16,
),
),
const SizedBox(height: 16),
Text(
'Vous êtes sur le point de supprimer définitivement le passage :',
style: TextStyle(color: Colors.grey[800]),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[300]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
fullAddress.isEmpty ? 'Adresse inconnue' : fullAddress,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
const SizedBox(height: 4),
if (passage['user'] != null)
Text(
'Collecteur: ${passage['user']}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
if (passage['date'] != null)
Text(
'Date: ${_formatDate(passage['date'] as DateTime)}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
const SizedBox(height: 20),
const Text(
'Pour confirmer la suppression, veuillez saisir le numéro de rue de ce passage :',
style: TextStyle(fontWeight: FontWeight.w500),
),
const SizedBox(height: 12),
TextField(
controller: confirmController,
decoration: InputDecoration(
labelText: 'Numéro de rue',
hintText: streetNumber.isNotEmpty ? 'Ex: $streetNumber' : 'Saisir le numéro',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.home),
),
keyboardType: TextInputType.text,
textCapitalization: TextCapitalization.characters,
),
],
),
),
actions: [
TextButton(
onPressed: () {
confirmController.dispose();
Navigator.of(dialogContext).pop();
},
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () async {
// Vérifier que le numéro saisi correspond
final enteredNumber = confirmController.text.trim();
if (enteredNumber.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez saisir le numéro de rue'),
backgroundColor: Colors.orange,
),
);
return;
}
if (streetNumber.isNotEmpty && enteredNumber.toUpperCase() != streetNumber.toUpperCase()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Le numéro de rue ne correspond pas'),
backgroundColor: Colors.red,
),
);
return;
}
// Fermer le dialog
confirmController.dispose();
Navigator.of(dialogContext).pop();
// Effectuer la suppression
await _deletePassage(passageModel);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('Supprimer définitivement'),
),
],
);
},
);
}
// Supprimer un passage
Future<void> _deletePassage(PassageModel passage) async {
try {
// Appeler le repository pour supprimer via l'API
final success = await _passageRepository.deletePassageViaApi(passage.id);
if (success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Passage supprimé avec succès'),
backgroundColor: Colors.green,
),
);
// Pas besoin de recharger, le ValueListenableBuilder
// se rafraîchira automatiquement après la suppression dans Hive
} else if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Erreur lors de la suppression du passage'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
debugPrint('Erreur suppression passage: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
// Formater une date
String _formatDate(DateTime date) {
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
}
// Construction du filtre par mode de règlement
Widget _buildPaymentFilter(ThemeData theme) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Mode de règlement',
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: selectedPaymentMethod,
isExpanded: true,
icon: const Icon(Icons.arrow_drop_down),
items: [
const DropdownMenuItem<String>(
value: 'Tous',
child: Text('Tous les modes'),
),
...AppKeys.typesReglements.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key.toString(),
child: Text(
entry.value['titre'] as String,
overflow: TextOverflow.ellipsis,
),
);
}),
],
onChanged: (String? value) {
if (value != null) {
setState(() {
selectedPaymentMethod = value;
});
}
},
),
),
),
],
);
}
}