feat: Gestion des secteurs et migration v3.0.4+304

- Ajout système complet de gestion des secteurs avec contours géographiques
- Import des contours départementaux depuis GeoJSON
- API REST pour la gestion des secteurs (/api/sectors)
- Service de géolocalisation pour déterminer les secteurs
- Migration base de données avec tables x_departements_contours et sectors_adresses
- Interface Flutter pour visualisation et gestion des secteurs
- Ajout thème sombre dans l'application
- Corrections diverses et optimisations

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
pierre
2025-08-07 11:01:45 +02:00
parent 6a609fb467
commit 599b9fcda0
662 changed files with 213221 additions and 174243 deletions

715
app/lib/presentation/admin/admin_history_page.dart Normal file → Executable file
View File

@@ -1,15 +1,15 @@
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:flutter/material.dart';
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:geosector_app/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/user_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/theme/app_theme.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
@@ -36,7 +36,7 @@ class DotsPainter extends CustomPainter {
}
class AdminHistoryPage extends StatefulWidget {
const AdminHistoryPage({Key? key}) : super(key: key);
const AdminHistoryPage({super.key});
@override
State<AdminHistoryPage> createState() => _AdminHistoryPageState();
@@ -49,25 +49,32 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
String selectedUser = 'Tous';
String selectedType = 'Tous';
String selectedPaymentMethod = 'Tous';
String selectedPeriod = 'Dernier mois'; // Période par défaut
String selectedPeriod = 'Tous'; // Période par défaut
DateTimeRange? selectedDateRange;
// 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<UserModel> _users = [];
List<MembreModel> _membres = [];
// Repositories
late PassageRepository _passageRepository;
late SectorRepository _sectorRepository;
late UserRepository _userRepository;
late MembreRepository _membreRepository;
// Passages formatés
// 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 = '';
@@ -93,9 +100,10 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
_passageRepository = passageRepository;
_userRepository = userRepository;
_sectorRepository = sectorRepository;
_membreRepository = membreRepository;
// Charger les secteurs et les utilisateurs
_loadSectorsAndUsers();
// Charger les secteurs et les membres
_loadSectorsAndMembres();
// Charger les passages
_loadPassages();
@@ -107,18 +115,18 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
}
}
// Charger les secteurs et les utilisateurs
void _loadSectorsAndUsers() {
// 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 utilisateurs
_users = _userRepository.getAllUsers();
debugPrint('Nombre d\'utilisateurs récupérés: ${_users.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 utilisateurs: $e');
debugPrint('Erreur lors du chargement des secteurs et membres: $e');
}
}
@@ -133,9 +141,12 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
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, _userRepository);
allPassages, _sectorRepository, _membreRepository);
setState(() {
_isLoading = false;
@@ -154,13 +165,137 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
selectedSectorId = null;
selectedUserId = null;
// Période par défaut : dernier mois
selectedPeriod = 'Dernier mois';
// Période par défaut : toutes les périodes
selectedPeriod = 'Tous';
// Plage de dates par défaut : dernier mois
final DateTime now = DateTime.now();
final DateTime oneMonthAgo = DateTime(now.year, now.month - 1, now.day);
selectedDateRange = DateTimeRange(start: oneMonthAgo, end: now);
// Plage de dates par défaut : aucune restriction
selectedDateRange = null;
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
// Méthode pour appliquer tous les filtres
List<Map<String, dynamic>> _getFilteredPassages() {
try {
var filtered = _formattedPassages.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();
// Trier par date décroissante (plus récent en premier)
filtered.sort((a, b) {
try {
final DateTime dateA = a['date'] as DateTime;
final DateTime dateB = b['date'] as DateTime;
return dateB.compareTo(dateA);
} catch (e) {
return 0;
}
});
debugPrint(
'Passages filtrés: ${filtered.length}/${_formattedPassages.length}');
return filtered;
} catch (e) {
debugPrint('Erreur globale lors du filtrage: $e');
return _formattedPassages;
}
}
// Mettre à jour le filtre par secteur
@@ -230,7 +365,8 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
),
child: CustomPaint(
painter: DotsPainter(),
child: Container(width: double.infinity, height: double.infinity),
child: const SizedBox(
width: double.infinity, height: double.infinity),
),
),
const Center(
@@ -258,67 +394,71 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
),
child: CustomPaint(
painter: DotsPainter(),
child: Container(width: double.infinity, height: double.infinity),
child:
const SizedBox(width: double.infinity, height: double.infinity),
),
),
// Contenu de la page
Padding(
padding: const EdgeInsets.all(16.0),
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,
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),
const SizedBox(height: 16),
// Filtres supplémentaires (secteur, utilisateur, période)
_buildAdditionalFilters(context),
// Filtres supplémentaires (secteur, utilisateur, période)
_buildAdditionalFilters(context),
const SizedBox(height: 16),
const SizedBox(height: 16),
// Widget de liste des passages
Expanded(
child: PassagesListWidget(
passages: _formattedPassages,
showFilters: true,
showSearch: true,
showActions: true,
initialSearchQuery: searchQuery,
initialTypeFilter: selectedType,
initialPaymentFilter: selectedPaymentMethod,
// Exclure les passages de type 2 (À finaliser)
excludePassageTypes: [2],
// Filtres par utilisateur et secteur
filterByUserId: selectedUserId,
filterBySectorId: selectedSectorId,
// Période par défaut (dernier mois)
periodFilter: 'lastMonth',
// Plage de dates personnalisée si définie
dateRange: selectedDateRange,
onPassageSelected: (passage) {
_showDetailsDialog(context, passage);
},
onReceiptView: (passage) {
_showReceiptDialog(context, passage);
},
onDetailsView: (passage) {
_showDetailsDialog(context, passage);
},
onPassageEdit: (passage) {
// Action pour modifier le passage
// Cette fonctionnalité pourrait être implémentée ultérieurement
},
// Widget de liste des passages avec hauteur fixe
SizedBox(
height: constraints.maxHeight * 0.7, // 70% de la hauteur disponible
child: PassagesListWidget(
passages: passages,
showFilters:
false, // Désactivé car les filtres sont maintenant dans la card "Filtres avancés"
showSearch:
false, // Désactivé car la recherche est maintenant dans la card "Filtres avancés"
showActions: true,
// Ne plus passer les filtres individuels car ils sont maintenant appliqués dans _getFilteredPassages()
onPassageSelected: (passage) {
_openPassageEditDialog(context, passage);
},
onReceiptView: (passage) {
_showReceiptDialog(context, passage);
},
onDetailsView: (passage) {
_showDetailsDialog(context, passage);
},
onPassageEdit: (passage) {
// Action pour modifier le passage
// Cette fonctionnalité pourrait être implémentée ultérieurement
},
),
),
],
),
),
],
),
);
},
),
],
);
@@ -339,7 +479,8 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
),
child: CustomPaint(
painter: DotsPainter(),
child: Container(width: double.infinity, height: double.infinity),
child:
const SizedBox(width: double.infinity, height: double.infinity),
),
),
Center(
@@ -388,14 +529,16 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
List<Map<String, dynamic>> _formatPassagesForWidget(
List<PassageModel> passages,
SectorRepository sectorRepository,
UserRepository userRepository) {
MembreRepository membreRepository) {
return passages.map((passage) {
// Récupérer le secteur associé au passage
final SectorModel? sector =
sectorRepository.getSectorById(passage.fkSector);
// 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 l'utilisateur associé au passage
final UserModel? user = userRepository.getUserById(passage.fkUser);
// Récupérer le membre associé au passage
final MembreModel? membre =
membreRepository.getMembreById(passage.fkUser);
// Construire l'adresse complète
final String address =
@@ -406,12 +549,21 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
return {
'id': passage.id,
'date': passage.passedAt,
'address': address,
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': user?.name ?? 'Utilisateur inconnu',
'user': membre?.name ?? 'Membre inconnu',
'type': passage.fkType,
'amount': double.tryParse(passage.montant) ?? 0.0,
'payment': passage.fkTypeReglement,
@@ -421,7 +573,14 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
'notes': passage.remarque,
'name': passage.name,
'phone': passage.phone,
// Ajouter d'autres champs nécessaires pour le widget
'montant': passage.montant,
'remarque': passage.remarque,
// Autres champs utiles
'fkOperation': passage.fkOperation,
'passedAt': passage.passedAt,
'lastSyncedAt': passage.lastSyncedAt,
'isActive': passage.isActive,
'isSynced': passage.isSynced,
};
}).toList();
}
@@ -552,6 +711,63 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
);
}
void _openPassageEditDialog(
BuildContext context, Map<String, dynamic> passage) async {
try {
debugPrint('=== DEBUT _openPassageEditDialog ===');
// Récupérer l'ID du passage
final int passageId = passage['id'] as int;
debugPrint('Recherche du passage ID: $passageId');
// 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;
}
debugPrint('Ouverture du dialog...');
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();
},
),
);
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 du formulaire: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
@@ -616,25 +832,52 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
),
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
? Row(
? Column(
children: [
// Filtre par secteur
Expanded(
child: _buildSectorFilter(theme, _sectors),
),
const SizedBox(width: 16),
// Première ligne : Secteur, Utilisateur, Période
Row(
children: [
// Filtre par secteur
Expanded(
child: _buildSectorFilter(theme, _sectors),
),
const SizedBox(width: 16),
// Filtre par utilisateur
Expanded(
child: _buildUserFilter(theme, _users),
),
const SizedBox(width: 16),
// Filtre par membre
Expanded(
child: _buildMembreFilter(theme, _membres),
),
const SizedBox(width: 16),
// Filtre par période
Expanded(
child: _buildPeriodFilter(theme),
// 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()),
],
),
],
)
@@ -644,12 +887,20 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
_buildSectorFilter(theme, _sectors),
const SizedBox(height: 16),
// Filtre par utilisateur
_buildUserFilter(theme, _users),
// 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),
],
),
],
@@ -714,7 +965,7 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
overflow: TextOverflow.ellipsis,
),
);
}).toList(),
}),
],
onChanged: (String? value) {
if (value != null) {
@@ -745,11 +996,52 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
);
}
// Construction du filtre par utilisateur
Widget _buildUserFilter(ThemeData theme, List<UserModel> users) {
// Vérifier si la liste des utilisateurs est vide ou si selectedUser n'est pas dans la liste
bool isSelectedUserValid = selectedUser == 'Tous' ||
users.any((u) => (u.name ?? 'Utilisateur inconnu') == selectedUser);
// 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) {
@@ -767,7 +1059,7 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Utilisateur',
'Membre',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
@@ -788,19 +1080,18 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
items: [
const DropdownMenuItem<String>(
value: 'Tous',
child: Text('Tous les utilisateurs'),
child: Text('Tous les membres'),
),
...users.map((user) {
// S'assurer que user.name n'est pas null
final String userName = user.name ?? 'Utilisateur inconnu';
...membreDisplayMap.entries.map((entry) {
final String displayName = entry.key;
return DropdownMenuItem<String>(
value: userName,
value: displayName,
child: Text(
userName,
displayName,
overflow: TextOverflow.ellipsis,
),
);
}).toList(),
}),
],
onChanged: (String? value) {
if (value != null) {
@@ -808,21 +1099,16 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
_updateUserFilter('Tous', null);
} else {
try {
// Trouver l'utilisateur correspondant
final user = users.firstWhere(
(u) => (u.name ?? 'Utilisateur inconnu') == value,
orElse: () => users.isNotEmpty
? users.first
: throw Exception('Liste d\'utilisateurs vide'),
);
// S'assurer que user.name et user.id ne sont pas null
final String userName =
user.name ?? 'Utilisateur inconnu';
final int? userId = user.id;
_updateUserFilter(userName, userId);
// 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 de l\'utilisateur: $e');
debugPrint('Erreur lors de la sélection du membre: $e');
_updateUserFilter('Tous', null);
}
}
@@ -912,34 +1198,155 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
);
}
void _showResendConfirmation(BuildContext context, int passageId) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Renvoyer le reçu'),
content: Text(
'Êtes-vous sûr de vouloir renvoyer le reçu du passage #$passageId ?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
// 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,
),
ElevatedButton(
onPressed: () {
// Action pour renvoyer le reçu
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text('Reçu du passage #$passageId renvoyé avec succès'),
backgroundColor: Colors.green,
),
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'),
),
);
},
child: const Text('Renvoyer'),
...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;
});
}
},
),
),
],
),
),
],
);
}
// 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;
});
}
},
),
),
),
],
);
}
}