feat: Ajout filtres membre/secteur dans historique admin (#42)

- Ajout de 2 dropdowns de filtres dans history_page.dart (admin uniquement)
- Filtre par membre (fkUser) : liste dynamique depuis passages
- Filtre par secteur (fkSector) : liste dynamique depuis passages
- Valeurs par défaut : "Tous" pour chaque filtre
- Tri alphabétique des dropdowns
- Mise à jour du planning : #42 validée (26/01)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-26 10:53:07 +01:00
parent 5b6808db25
commit a392305820
2 changed files with 157 additions and 8 deletions

140
app/lib/presentation/pages/history_page.dart Normal file → Executable file
View File

@@ -55,6 +55,8 @@ class _HistoryContentState extends State<HistoryContent> {
String _selectedTypeFilter = 'Tous les types'; String _selectedTypeFilter = 'Tous les types';
String _searchQuery = ''; String _searchQuery = '';
int? selectedTypeId; int? selectedTypeId;
int? _selectedMemberId; // null = "Tous" (admin uniquement)
int? _selectedSectorId; // null = "Tous" (admin uniquement)
// Contrôleur de recherche // Contrôleur de recherche
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
@@ -221,6 +223,20 @@ class _HistoryContentState extends State<HistoryContent> {
} }
} }
// Filtre par membre (admin uniquement)
if (isAdmin && _selectedMemberId != null) {
if (passage.fkUser != _selectedMemberId) {
return false;
}
}
// Filtre par secteur (admin uniquement)
if (isAdmin && _selectedSectorId != null) {
if (passage.fkSector != _selectedSectorId) {
return false;
}
}
// Filtre par recherche textuelle // Filtre par recherche textuelle
if (_searchQuery.isNotEmpty) { if (_searchQuery.isNotEmpty) {
final query = _searchQuery.toLowerCase(); final query = _searchQuery.toLowerCase();
@@ -302,6 +318,7 @@ class _HistoryContentState extends State<HistoryContent> {
children: [ children: [
// Barre de recherche // Barre de recherche
Expanded( Expanded(
flex: 3,
child: TextFormField( child: TextFormField(
controller: _searchController, controller: _searchController,
decoration: const InputDecoration( decoration: const InputDecoration(
@@ -319,6 +336,21 @@ class _HistoryContentState extends State<HistoryContent> {
}, },
), ),
), ),
// Filtres admin uniquement
if (isAdmin) ...[
const SizedBox(width: 8),
// Filtre par membre
Expanded(
flex: 2,
child: _buildMemberDropdown(),
),
const SizedBox(width: 8),
// Filtre par secteur
Expanded(
flex: 2,
child: _buildSectorDropdown(),
),
],
], ],
), ),
), ),
@@ -563,4 +595,112 @@ class _HistoryContentState extends State<HistoryContent> {
} }
} }
} }
/// Construit le dropdown de sélection de membre (admin uniquement)
Widget _buildMemberDropdown() {
// Récupérer les membres uniques depuis les passages de l'opération courante
final memberIds = <int>{};
final memberNames = <int, String>{};
for (final passage in _originalPassages) {
if (!memberIds.contains(passage.fkUser)) {
memberIds.add(passage.fkUser);
// Utiliser le nom du passage (qui contient prenom + nom)
memberNames[passage.fkUser] = passage.name.isNotEmpty ? passage.name : 'Membre #${passage.fkUser}';
}
}
// Trier par nom
final sortedMembers = memberIds.toList()
..sort((a, b) => (memberNames[a] ?? '').compareTo(memberNames[b] ?? ''));
return DropdownButtonFormField<int?>(
value: _selectedMemberId,
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
isDense: true,
labelText: 'Membre',
prefixIcon: Icon(Icons.person, size: 20),
),
items: [
const DropdownMenuItem<int?>(
value: null,
child: Text('Tous'),
),
...sortedMembers.map((memberId) {
return DropdownMenuItem<int?>(
value: memberId,
child: Text(
memberNames[memberId] ?? 'Membre #$memberId',
overflow: TextOverflow.ellipsis,
),
);
}),
],
onChanged: (int? newValue) {
setState(() {
_selectedMemberId = newValue;
});
_applyFilters();
},
);
}
/// Construit le dropdown de sélection de secteur (admin uniquement)
Widget _buildSectorDropdown() {
// Récupérer les secteurs uniques depuis les passages de l'opération courante
final sectorIds = <int>{};
for (final passage in _originalPassages) {
if (passage.fkSector != null && !sectorIds.contains(passage.fkSector)) {
sectorIds.add(passage.fkSector!);
}
}
// Récupérer les noms des secteurs depuis le repository
final allSectors = sectorRepository.getAllSectors();
final sectorNames = <int, String>{};
for (final sector in allSectors) {
if (sectorIds.contains(sector.id)) {
sectorNames[sector.id] = sector.libelle;
}
}
// Trier par nom
final sortedSectors = sectorIds.toList()
..sort((a, b) => (sectorNames[a] ?? '').compareTo(sectorNames[b] ?? ''));
return DropdownButtonFormField<int?>(
value: _selectedSectorId,
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
isDense: true,
labelText: 'Secteur',
prefixIcon: Icon(Icons.location_on, size: 20),
),
items: [
const DropdownMenuItem<int?>(
value: null,
child: Text('Tous'),
),
...sortedSectors.map((sectorId) {
return DropdownMenuItem<int?>(
value: sectorId,
child: Text(
sectorNames[sectorId] ?? 'Secteur #$sectorId',
overflow: TextOverflow.ellipsis,
),
);
}),
],
onChanged: (int? newValue) {
setState(() {
_selectedSectorId = newValue;
});
_applyFilters();
},
);
}
} }

25
docs/PLANNING-2026-Q1.md Normal file → Executable file
View File

@@ -31,11 +31,9 @@
|-------|------------|--------------------------------------------|--------| |-------|------------|--------------------------------------------|--------|
| 19/01 | `#13` Jour 1 | ✅ `#204` Design couleurs flashy | à livrer v3.6.3 | | 19/01 | `#13` Jour 1 | ✅ `#204` Design couleurs flashy | à livrer v3.6.3 |
| 19/01 | | ✅ `#205` Écrans utilisateurs simplifiés | à livrer v3.6.3 | | 19/01 | | ✅ `#205` Écrans utilisateurs simplifiés | à livrer v3.6.3 |
| 20/01 | `#13` Jour 2 | `#113` Couleur repasses orange | | | 20/01 | `#13` Jour 2 | `#113` Couleur repasses orange | ✅ Validé 26/01 |
| 20/01 | | `#72`Épaisseur police lisibilité | | | 20/01 | | `#72` Épaisseur police lisibilité (theme + BtnPassages) | ✅ Livré 26/01 |
| 21/01 | `#13` Jour 3 | `#71`Visibilité bouton "Envoyer message" | | | 21/01 | `#13` Jour 3 | `#42` Filtres membre/secteur history (admin) | ✅ Livré 26/01 |
| 21/01 | | `#59`Listing rues invisible (clavier) | |
| 22/01 | `#13` Jour 4 | `#42`Historique adresses cliquables | |
| 23/01 | `#13` Jour 5 | `#74`Simplifier DashboardLayout/AppScaffold | | | 23/01 | `#13` Jour 5 | `#74`Simplifier DashboardLayout/AppScaffold | |
| 24/01 | | `#28`Gestion reçus Flutter nouveaux champs | | | 24/01 | | `#28`Gestion reçus Flutter nouveaux champs | |
| 25/01 | | `#50`Modifier secteur au clic | | | 25/01 | | `#50`Modifier secteur au clic | |
@@ -143,17 +141,28 @@
--- ---
## PHASE 7 : DIVERS (sans date)
### Tâches diverses - À planifier ultérieurement
| ID | Tâche | Cat | Statut |
|------|----------------------------------------|------|--------|
| `#71` | Visibilité bouton "Envoyer message" | UX | |
| `#59` | Listing rues invisible (clavier) | UX | |
---
## RÉCAPITULATIF ## RÉCAPITULATIF
| Phase | Période | Jours | Tâches | Focus | | Phase | Période | Jours | Tâches | Focus |
|-----------|--------------|-------|--------|---------------------| |-----------|--------------|-------|--------|---------------------|
| 1 | 16-18/01 | 3 | 5 | Bugs critiques | | 1 | 16-18/01 | 3 | 7 | Bugs critiques |
| 2 | 19-25/01 | 7 | 10 | Stripe iOS + UX | | 2 | 19-25/01 | 7 | 8 | Stripe iOS + UX |
| 3 | 26/01-07/02 | 10 | 25 | MAP / Carte | | 3 | 26/01-07/02 | 10 | 25 | MAP / Carte |
| 4 | 08-14/02 | 6 | 11 | Stripe + Passages | | 4 | 08-14/02 | 6 | 11 | Stripe + Passages |
| 5 | 15-22/02 | 7 | 22 | Admin + Membres | | 5 | 15-22/02 | 7 | 22 | Admin + Membres |
| 6 | 23-28/02 | 5 | 15 | Export + Divers | | 6 | 23-28/02 | 5 | 15 | Export + Divers |
| **TOTAL** | **44 jours** | | **88** | | | 7 | Sans date | - | 2 | Divers |
| **TOTAL** | **44 jours** | | **90** | |
--- ---