- 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>
707 lines
23 KiB
Dart
Executable File
707 lines
23 KiB
Dart
Executable File
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
|
import 'package:flutter/material.dart';
|
|
import 'package:hive_flutter/hive_flutter.dart';
|
|
import 'package:geosector_app/core/constants/app_keys.dart';
|
|
import 'package:geosector_app/core/services/current_user_service.dart';
|
|
import 'package:geosector_app/core/theme/app_theme.dart';
|
|
import 'package:geosector_app/core/data/models/passage_model.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/presentation/widgets/app_scaffold.dart';
|
|
import 'package:geosector_app/presentation/widgets/btn_passages.dart';
|
|
|
|
/// Page d'historique unifiée utilisant AppScaffold
|
|
class HistoryPage extends StatelessWidget {
|
|
/// ID du membre à filtrer (optionnel, pour les admins)
|
|
final int? memberId;
|
|
|
|
const HistoryPage({
|
|
super.key,
|
|
this.memberId,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AppScaffold(
|
|
selectedIndex: 1, // Index de la page History dans la navigation
|
|
pageTitle: 'Historique',
|
|
body: HistoryContent(memberId: memberId),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Contenu de la page historique unifié pour admin et user
|
|
class HistoryContent extends StatefulWidget {
|
|
/// ID du membre à filtrer (optionnel, pour les admins)
|
|
final int? memberId;
|
|
|
|
const HistoryContent({
|
|
super.key,
|
|
this.memberId,
|
|
});
|
|
|
|
@override
|
|
State<HistoryContent> createState() => _HistoryContentState();
|
|
}
|
|
|
|
class _HistoryContentState extends State<HistoryContent> {
|
|
// Détection du rôle et permissions
|
|
late final bool isAdmin;
|
|
late final int currentUserId; // users.id (table centrale)
|
|
late final int? currentOpeUserId; // ope_users.id (pour comparaisons avec passages)
|
|
late final bool canDeletePassages; // Permission de suppression pour les users
|
|
|
|
// Filtres
|
|
String _selectedTypeFilter = 'Tous les types';
|
|
String _searchQuery = '';
|
|
int? selectedTypeId;
|
|
int? _selectedMemberId; // null = "Tous" (admin uniquement)
|
|
int? _selectedSectorId; // null = "Tous" (admin uniquement)
|
|
|
|
// Contrôleur de recherche
|
|
final TextEditingController _searchController = TextEditingController();
|
|
|
|
// Passages originaux pour l'édition
|
|
List<PassageModel> _originalPassages = [];
|
|
List<PassageModel> _filteredPassages = [];
|
|
|
|
// État de chargement
|
|
bool _isLoading = true;
|
|
String _errorMessage = '';
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
// Déterminer le rôle et les permissions de l'utilisateur (prend en compte le mode d'affichage)
|
|
final currentUser = userRepository.getCurrentUser();
|
|
isAdmin = CurrentUserService.instance.shouldShowAdminUI;
|
|
currentUserId = currentUser?.id ?? 0;
|
|
currentOpeUserId = currentUser?.opeUserId; // ID dans ope_users pour comparaisons avec passages
|
|
|
|
// Vérifier la permission de suppression pour les users
|
|
bool userCanDelete = false;
|
|
if (!isAdmin && currentUser != null && currentUser.fkEntite != null) {
|
|
final userAmicale = amicaleRepository.getAmicaleById(currentUser.fkEntite!);
|
|
if (userAmicale != null) {
|
|
userCanDelete = userAmicale.chkUserDeletePass;
|
|
}
|
|
}
|
|
canDeletePassages = isAdmin || userCanDelete;
|
|
|
|
// Charger les filtres présélectionnés depuis Hive
|
|
_loadPreselectedFilters();
|
|
|
|
// Initialiser les données
|
|
_initializeData();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_searchController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
// Callback pour gérer les clics sur les boutons de type de passage
|
|
void _handleTypeSelected(int? typeId) {
|
|
setState(() {
|
|
// Réinitialiser la recherche
|
|
_searchQuery = '';
|
|
_searchController.clear();
|
|
|
|
// Appliquer le filtre de type
|
|
if (typeId == null) {
|
|
// Tous les passages
|
|
_selectedTypeFilter = 'Tous les types';
|
|
selectedTypeId = null;
|
|
} else {
|
|
// Type spécifique
|
|
selectedTypeId = typeId;
|
|
final typeInfo = AppKeys.typesPassages[typeId];
|
|
if (typeInfo != null) {
|
|
_selectedTypeFilter = typeInfo['titre'] as String;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Appliquer les filtres
|
|
_applyFilters();
|
|
}
|
|
|
|
// Charger les filtres présélectionnés depuis Hive
|
|
Future<void> _loadPreselectedFilters() async {
|
|
try {
|
|
if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) {
|
|
await Hive.openBox(AppKeys.settingsBoxName);
|
|
}
|
|
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
|
|
|
// Charger le type de passage sélectionné
|
|
final typeId = settingsBox.get('history_selectedTypeId');
|
|
if (typeId != null && typeId is int) {
|
|
setState(() {
|
|
selectedTypeId = typeId;
|
|
final typeInfo = AppKeys.typesPassages[typeId];
|
|
if (typeInfo != null) {
|
|
_selectedTypeFilter = typeInfo['titre'] as String;
|
|
}
|
|
});
|
|
|
|
// Supprimer le typeId de Hive après l'avoir utilisé
|
|
settingsBox.delete('history_selectedTypeId');
|
|
|
|
debugPrint('HistoryPage: Type de passage présélectionné: $typeId');
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du chargement des filtres: $e');
|
|
}
|
|
}
|
|
|
|
// Initialiser les données
|
|
void _initializeData() {
|
|
try {
|
|
setState(() {
|
|
_isLoading = true;
|
|
_errorMessage = '';
|
|
});
|
|
|
|
// Charger les passages
|
|
final currentOperation = userRepository.getCurrentOperation();
|
|
if (currentOperation != null) {
|
|
if (isAdmin) {
|
|
// Admin : tous les passages de l'opération
|
|
_originalPassages = passageRepository.getPassagesByOperation(currentOperation.id);
|
|
} else {
|
|
// User : logique spéciale selon le type de passage
|
|
final allPassages = passageRepository.getPassagesByOperation(currentOperation.id);
|
|
_originalPassages = allPassages.where((p) {
|
|
// Type 2 (À finaliser) : afficher TOUS les passages
|
|
if (p.fkType == 2) {
|
|
return true;
|
|
}
|
|
// Autres types : seulement les passages de l'utilisateur
|
|
return p.fkUser == currentOpeUserId;
|
|
}).toList();
|
|
}
|
|
debugPrint('Nombre de passages récupérés: ${_originalPassages.length}');
|
|
|
|
// Initialiser les passages filtrés avec tous les passages
|
|
_filteredPassages = List.from(_originalPassages);
|
|
|
|
// Appliquer les filtres initiaux après le chargement
|
|
_applyFilters();
|
|
}
|
|
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
} catch (e) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
_errorMessage = 'Erreur lors du chargement des données: $e';
|
|
});
|
|
debugPrint('Erreur lors de l\'initialisation des données: $e');
|
|
}
|
|
}
|
|
|
|
/// Appliquer les filtres aux données
|
|
void _applyFilters() {
|
|
debugPrint('HistoryPage: Application des filtres');
|
|
debugPrint(' Type: $_selectedTypeFilter');
|
|
debugPrint(' Recherche: $_searchQuery');
|
|
|
|
// Appliquer les filtres aux passages originaux
|
|
List<PassageModel> filteredPassages = _originalPassages.where((passage) {
|
|
// Filtre par type de passage
|
|
if (_selectedTypeFilter != 'Tous les types') {
|
|
final typeInfo = AppKeys.typesPassages.entries.firstWhere(
|
|
(entry) => entry.value['titre'] == _selectedTypeFilter,
|
|
orElse: () => const MapEntry(-1, {'titre': '', 'couleur': ''}),
|
|
);
|
|
if (typeInfo.key == -1 || passage.fkType != typeInfo.key) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 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
|
|
if (_searchQuery.isNotEmpty) {
|
|
final query = _searchQuery.toLowerCase();
|
|
|
|
// Construire l'adresse complète pour la recherche
|
|
final fullAddress = '${passage.numero} ${passage.rueBis} ${passage.rue} ${passage.residence} ${passage.ville}'
|
|
.toLowerCase()
|
|
.trim();
|
|
|
|
// Ajouter le nom et l'email
|
|
final name = passage.name.toLowerCase();
|
|
final email = passage.email.toLowerCase();
|
|
|
|
// Vérifier si la recherche correspond à l'adresse, au nom ou à l'email
|
|
if (!fullAddress.contains(query) &&
|
|
!name.contains(query) &&
|
|
!email.contains(query)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}).toList();
|
|
|
|
// Mettre à jour les données filtrées
|
|
setState(() {
|
|
_filteredPassages = filteredPassages;
|
|
});
|
|
|
|
debugPrint('HistoryPage: ${filteredPassages.length} passages filtrés sur ${_originalPassages.length}');
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Le contenu sans scaffold (AppScaffold est déjà dans HistoryPage)
|
|
if (_isLoading) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
|
|
if (_errorMessage.isNotEmpty) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.error_outline, size: 64, color: Colors.red),
|
|
const SizedBox(height: 16),
|
|
Text(_errorMessage, style: const TextStyle(fontSize: 16)),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton(
|
|
onPressed: _initializeData,
|
|
child: const Text('Réessayer'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return _buildContent();
|
|
}
|
|
|
|
Widget _buildContent() {
|
|
final screenWidth = MediaQuery.of(context).size.width;
|
|
final isDesktop = screenWidth > 800;
|
|
|
|
return Column(
|
|
children: [
|
|
// 0. BtnPassages collé en haut/gauche/droite
|
|
BtnPassages(
|
|
onTypeSelected: _handleTypeSelected,
|
|
selectedTypeId: selectedTypeId,
|
|
),
|
|
// 1. Barre de recherche
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: isDesktop ? AppTheme.spacingL : AppTheme.spacingS,
|
|
vertical: AppTheme.spacingS,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
// Barre de recherche
|
|
Expanded(
|
|
flex: 3,
|
|
child: TextFormField(
|
|
controller: _searchController,
|
|
decoration: const InputDecoration(
|
|
border: OutlineInputBorder(),
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
hintText: 'Rechercher...',
|
|
prefixIcon: Icon(Icons.search, size: 20),
|
|
isDense: true,
|
|
),
|
|
onChanged: (String value) {
|
|
setState(() {
|
|
_searchQuery = value;
|
|
});
|
|
_applyFilters();
|
|
},
|
|
),
|
|
),
|
|
// 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(),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
// 2. Liste des passages - EXPANDED pour prendre tout l'espace restant
|
|
Expanded(
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: isDesktop ? AppTheme.spacingL : AppTheme.spacingS,
|
|
),
|
|
child: Card(
|
|
elevation: 2,
|
|
child: PassagesListWidget(
|
|
passages: _convertPassagesToMaps(),
|
|
showActions: true, // Actions disponibles pour tous (avec vérification des droits)
|
|
showAddButton: true, // Bouton + pour tous
|
|
onPassageEdit: _handlePassageEditMap, // Tous peuvent éditer (avec vérification)
|
|
onPassageDelete: canDeletePassages ? _handlePassageDeleteMap : null, // Selon permissions
|
|
onAddPassage: () async {
|
|
await _showPassageFormDialog(context);
|
|
},
|
|
filteredPassageType: _selectedTypeFilter != 'Tous les types' ? _selectedTypeFilter : null,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// Afficher le formulaire de création de passage
|
|
Future<void> _showPassageFormDialog(BuildContext context) async {
|
|
await showDialog<PassageModel>(
|
|
context: context,
|
|
builder: (context) => PassageFormDialog(
|
|
title: 'Nouveau passage',
|
|
readOnly: false,
|
|
passageRepository: passageRepository,
|
|
userRepository: userRepository,
|
|
operationRepository: operationRepository,
|
|
amicaleRepository: amicaleRepository,
|
|
onSuccess: () {
|
|
_initializeData(); // Recharger les données
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// Méthode helper pour convertir une valeur potentiellement null en String
|
|
String _safeString(dynamic value) {
|
|
if (value == null) return '';
|
|
return value.toString();
|
|
}
|
|
|
|
// Convertir les passages en Map pour PassagesListWidget
|
|
List<Map<String, dynamic>> _convertPassagesToMaps() {
|
|
try {
|
|
// Utiliser les passages filtrés
|
|
var passages = _filteredPassages.toList();
|
|
|
|
// Convertir chaque passage en Map
|
|
final maps = <Map<String, dynamic>>[];
|
|
for (final passage in passages) {
|
|
try {
|
|
// Construire l'adresse complète
|
|
final fullAddress = '${_safeString(passage.numero)} ${_safeString(passage.rue)} ${_safeString(passage.ville)}'.trim();
|
|
|
|
// Convertir le montant en double (normalement un String dans PassageModel)
|
|
double? montantDouble = 0.0;
|
|
final montantStr = _safeString(passage.montant);
|
|
if (montantStr.isNotEmpty) {
|
|
montantDouble = double.tryParse(montantStr) ?? 0.0;
|
|
}
|
|
|
|
// Gestion de la date selon le type de passage
|
|
// Pour les passages "À finaliser" (type 2), la date peut être null
|
|
DateTime? passageDate = passage.passedAt;
|
|
|
|
final map = <String, dynamic>{
|
|
'id': passage.id,
|
|
'fk_operation': passage.fkOperation,
|
|
'fk_sector': passage.fkSector,
|
|
'fk_user': passage.fkUser,
|
|
'fkUser': passage.fkUser,
|
|
'type': passage.fkType,
|
|
'fk_type': passage.fkType,
|
|
'payment': passage.fkTypeReglement,
|
|
'fk_type_reglement': passage.fkTypeReglement,
|
|
'fk_adresse': _safeString(passage.fkAdresse),
|
|
'address': fullAddress.isNotEmpty ? fullAddress : 'Adresse non renseignée',
|
|
'date': passageDate,
|
|
'datePassage': passageDate,
|
|
'passed_at': passage.passedAt?.toIso8601String(),
|
|
'passedAt': passageDate,
|
|
'numero': _safeString(passage.numero),
|
|
'rue': _safeString(passage.rue),
|
|
'rue_bis': _safeString(passage.rueBis),
|
|
'ville': _safeString(passage.ville),
|
|
'residence': _safeString(passage.residence),
|
|
'fk_habitat': passage.fkHabitat,
|
|
'appt': _safeString(passage.appt),
|
|
'niveau': _safeString(passage.niveau),
|
|
'gps_lat': _safeString(passage.gpsLat),
|
|
'gps_lng': _safeString(passage.gpsLng),
|
|
'nom_recu': _safeString(passage.nomRecu),
|
|
'remarque': _safeString(passage.remarque),
|
|
'notes': _safeString(passage.remarque),
|
|
'montant': _safeString(passage.montant),
|
|
'amount': montantDouble,
|
|
'email_erreur': _safeString(passage.emailErreur),
|
|
'nb_passages': passage.nbPassages,
|
|
'name': _safeString(passage.name),
|
|
'email': _safeString(passage.email),
|
|
'phone': _safeString(passage.phone),
|
|
'fullAddress': fullAddress,
|
|
};
|
|
maps.add(map);
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de la conversion du passage ${passage.id}: $e');
|
|
}
|
|
}
|
|
debugPrint('${maps.length} passages convertis avec succès');
|
|
return maps;
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de la conversion des passages en Maps: $e');
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Gérer l'édition d'un passage depuis le Map
|
|
void _handlePassageEditMap(Map<String, dynamic> passageMap) {
|
|
final passageId = passageMap['id'] as int;
|
|
final passage = _originalPassages.firstWhere(
|
|
(p) => p.id == passageId,
|
|
orElse: () => PassageModel.fromJson(passageMap),
|
|
);
|
|
|
|
// Vérifier les permissions :
|
|
// - Admin peut tout éditer
|
|
// - User peut éditer ses propres passages
|
|
// - Type 2 (À finaliser) : éditable par tous les utilisateurs
|
|
if (isAdmin || passage.fkUser == currentOpeUserId || passage.fkType == 2) {
|
|
_handlePassageEdit(passage);
|
|
} else {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Vous ne pouvez éditer que vos propres passages'),
|
|
backgroundColor: Colors.orange,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Gérer la suppression d'un passage depuis le Map
|
|
void _handlePassageDeleteMap(Map<String, dynamic> passageMap) {
|
|
final passageId = passageMap['id'] as int;
|
|
final passage = _originalPassages.firstWhere(
|
|
(p) => p.id == passageId,
|
|
orElse: () => PassageModel.fromJson(passageMap),
|
|
);
|
|
|
|
// Vérifier les permissions : admin peut tout supprimer, user seulement ses propres passages si autorisé
|
|
if (isAdmin || (canDeletePassages && passage.fkUser == currentOpeUserId)) {
|
|
_handlePassageDelete(passage);
|
|
} else {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
!canDeletePassages
|
|
? 'Vous n\'êtes pas autorisé à supprimer des passages'
|
|
: 'Vous ne pouvez supprimer que vos propres passages'
|
|
),
|
|
backgroundColor: Colors.orange,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Gérer l'édition d'un passage
|
|
void _handlePassageEdit(PassageModel passage) async {
|
|
await showDialog<PassageModel>(
|
|
context: context,
|
|
builder: (context) => PassageFormDialog(
|
|
passage: passage,
|
|
title: 'Modifier le passage',
|
|
readOnly: false,
|
|
passageRepository: passageRepository,
|
|
userRepository: userRepository,
|
|
operationRepository: operationRepository,
|
|
amicaleRepository: amicaleRepository,
|
|
onSuccess: () {
|
|
_initializeData(); // Recharger les données
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// Gérer la suppression d'un passage
|
|
void _handlePassageDelete(PassageModel passage) async {
|
|
final confirm = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Confirmer la suppression'),
|
|
content: Text(
|
|
'Êtes-vous sûr de vouloir supprimer le passage chez ${passage.name} ?',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(false),
|
|
child: const Text('Annuler'),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(true),
|
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
|
child: const Text('Supprimer'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirm == true) {
|
|
try {
|
|
await passageRepository.deletePassage(passage.id);
|
|
_initializeData(); // Recharger les données
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Passage supprimé avec succès'),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Erreur lors de la suppression: $e'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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();
|
|
},
|
|
);
|
|
}
|
|
}
|