feat: Début des évolutions interfaces mobiles v3.2.4

- Préparation de la nouvelle branche pour les évolutions
- Mise à jour de la version vers 3.2.4
- Intégration des modifications en cours

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-04 16:49:29 +02:00
parent 338294601f
commit 242a90720e
186 changed files with 3510 additions and 441354 deletions

View File

@@ -101,31 +101,37 @@ class _GeosectorAppState extends State<GeosectorApp> with WidgetsBindingObserver
debugShowCheckedModeBanner: false,
// Builder pour appliquer le theme responsive à toute l'app
builder: (context, child) {
return MediaQuery(
// Conserver les données MediaQuery existantes
data: MediaQuery.of(context),
child: Builder(
builder: (context) {
// Récupérer le theme actuel (clair ou sombre)
final brightness = Theme.of(context).brightness;
final textColor = brightness == Brightness.light
? AppTheme.textLightColor
: AppTheme.textDarkColor;
// Débogage en mode développement
final width = MediaQuery.of(context).size.width;
final scaleFactor = AppTheme.getFontScaleFactor(width);
debugPrint('📱 Largeur écran: ${width.toStringAsFixed(0)}px → Facteur: ×$scaleFactor');
// Appliquer le TextTheme responsive
return Theme(
data: Theme.of(context).copyWith(
textTheme: AppTheme.getResponsiveTextTheme(context, textColor),
),
child: child ?? const SizedBox.shrink(),
);
},
),
return LayoutBuilder(
builder: (context, constraints) {
// Récupérer le theme actuel (clair ou sombre)
final brightness = Theme.of(context).brightness;
final textColor = brightness == Brightness.light
? AppTheme.textLightColor
: AppTheme.textDarkColor;
// Débogage en mode développement
final width = constraints.maxWidth;
final scaleFactor = AppTheme.getFontScaleFactor(width);
// Afficher le debug uniquement lors du changement de taille
if (width < AppTheme.breakpointMobileSmall) {
debugPrint('📱 Mode: Très petit mobile (${width.toStringAsFixed(0)}px) → Facteur: ×$scaleFactor');
} else if (width < AppTheme.breakpointMobile) {
debugPrint('📱 Mode: Mobile (${width.toStringAsFixed(0)}px) → Facteur: ×$scaleFactor');
} else if (width < AppTheme.breakpointTablet) {
debugPrint('📱 Mode: Tablette (${width.toStringAsFixed(0)}px) → Facteur: ×$scaleFactor');
} else {
debugPrint('🖥️ Mode: Desktop (${width.toStringAsFixed(0)}px) → Facteur: ×$scaleFactor');
}
// Appliquer le TextTheme responsive
return Theme(
data: Theme.of(context).copyWith(
textTheme: AppTheme.getResponsiveTextTheme(context, textColor),
),
child: child ?? const SizedBox.shrink(),
);
},
);
},
// Configuration des localisations pour le français

View File

@@ -30,12 +30,12 @@ class AppKeys {
static const int roleAdmin3 = 9;
// URLs API pour les différents environnements
static const String baseApiUrlDev = 'https://dapp.geosector.fr/api';
static const String baseApiUrlDev = 'https://app.geo.dev/api';
static const String baseApiUrlRec = 'https://rapp.geosector.fr/api';
static const String baseApiUrlProd = 'https://app.geosector.fr/api';
// Identifiants d'application pour les différents environnements
static const String appIdentifierDev = 'dapp.geosector.fr';
static const String appIdentifierDev = 'app.geo.dev';
static const String appIdentifierRec = 'rapp.geosector.fr';
static const String appIdentifierProd = 'app.geosector.fr';
@@ -85,7 +85,7 @@ class AppKeys {
try {
final String currentUrl = Uri.base.toString().toLowerCase();
if (currentUrl.contains('dapp.geosector.fr')) {
if (currentUrl.contains('app.geo.dev')) {
return mapboxApiKeyDev;
} else if (currentUrl.contains('rapp.geosector.fr')) {
return mapboxApiKeyRec;

View File

@@ -150,7 +150,7 @@ class ApiService {
final currentUrl = html.window.location.href.toLowerCase();
if (currentUrl.contains('dapp.geosector.fr')) {
if (currentUrl.contains('app.geo.dev')) {
return 'DEV';
} else if (currentUrl.contains('rapp.geosector.fr')) {
return 'REC';

View File

@@ -82,7 +82,7 @@ class StripeConnectService {
debugPrint('📋 Génération du lien d\'onboarding pour account: $accountId');
// URLs de retour après onboarding
const baseUrl = 'https://dapp.geosector.fr'; // À adapter selon l'environnement
const baseUrl = 'https://app.geo.dev'; // À adapter selon l'environnement
final returnUrl = Uri.encodeFull('$baseUrl/stripe/success');
final refreshUrl = Uri.encodeFull('$baseUrl/stripe/refresh');

View File

@@ -403,4 +403,16 @@ class AppTheme {
labelSmall: TextStyle(fontFamily: 'Figtree', color: textColor.withValues(alpha: 0.7), fontSize: 11),
);
}
/// Helper pour obtenir des espacements responsives
static double getResponsiveSpacing(double screenWidth, double baseSpacing) {
final scaleFactor = getFontScaleFactor(screenWidth);
return baseSpacing * scaleFactor;
}
/// Helper court pour espacements responsives
static double s(BuildContext context, double baseSpacing) {
final width = MediaQuery.of(context).size.width;
return getResponsiveSpacing(width, baseSpacing);
}
}

View File

@@ -2,6 +2,8 @@ import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:math' as math;
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/data/models/sector_model.dart';
import 'package:geosector_app/presentation/widgets/sector_distribution_card.dart';
import 'package:geosector_app/presentation/widgets/charts/charts.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
@@ -197,7 +199,7 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
final currentOperation = userRepository.getCurrentOperation();
// Titre dynamique avec l'ID et le nom de l'opération
final String title = currentOperation != null ? 'Synthèse de l\'opération #${currentOperation.id} ${currentOperation.name}' : 'Synthèse de l\'opération';
final String title = currentOperation != null ? 'Opération #${currentOperation.id} ${currentOperation.name}' : 'Opération';
return Stack(children: [
// Fond dégradé avec petits points blancs
@@ -264,10 +266,16 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
const SizedBox(height: AppTheme.spacingL),
// LIGNE 2 : Carte de répartition par secteur (pleine largeur)
SectorDistributionCard(
key: ValueKey('sector_distribution_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'),
title: 'Répartition sur les 31 secteurs',
height: 500, // Hauteur maximale pour afficher tous les secteurs
ValueListenableBuilder<Box<SectorModel>>(
valueListenable: Hive.box<SectorModel>(AppKeys.sectorsBoxName).listenable(),
builder: (context, Box<SectorModel> box, child) {
final sectorCount = box.values.length;
return SectorDistributionCard(
key: ValueKey('sector_distribution_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'),
title: '$sectorCount secteurs',
height: 500, // Hauteur maximale pour afficher tous les secteurs
);
},
),
const SizedBox(height: AppTheme.spacingL),
@@ -345,7 +353,7 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
// Construit la carte de répartition par type de passage avec liste
Widget _buildPassageTypeCard(BuildContext context) {
return PassageSummaryCard(
title: 'Répartition par type de passage',
title: 'Passages',
titleColor: AppTheme.primaryColor,
titleIcon: Icons.route,
height: 300,
@@ -365,7 +373,7 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
// Construit la carte de répartition par mode de paiement
Widget _buildPaymentTypeCard(BuildContext context) {
return PaymentSummaryCard(
title: 'Répartition par mode de paiement',
title: 'Règlements',
titleColor: AppTheme.buttonSuccessColor,
titleIcon: Icons.euro,
height: 300,

View File

@@ -1,12 +1,12 @@
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/theme/app_theme.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/data/models/user_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';
@@ -54,24 +54,13 @@ class AdminHistoryPage extends StatefulWidget {
}
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
// Filtres présélectionnés depuis une autre page
int? selectedSectorId;
int? selectedUserId;
String selectedSector = 'Tous';
String selectedType = 'Tous';
// Listes pour les filtres
List<SectorModel> _sectors = [];
@@ -170,15 +159,10 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
// Initialiser les filtres
void _initializeFilters() {
// Par défaut, on n'applique pas de filtre par utilisateur ou secteur
// Par défaut, on n'applique pas de filtre présélectionné
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;
selectedSector = 'Tous';
selectedType = 'Tous';
}
// Charger les filtres présélectionnés depuis Hive
@@ -219,258 +203,9 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
}
}
@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;
}
// 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) {
@@ -525,25 +260,22 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
// Contenu de la page
LayoutBuilder(
builder: (context, constraints) {
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: [
// 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(
// Padding responsive : réduit sur mobile pour maximiser l'espace
final screenWidth = MediaQuery.of(context).size.width;
final horizontalPadding = screenWidth < 600 ? 8.0 : 16.0;
final verticalPadding = 16.0;
return Padding(
padding: EdgeInsets.symmetric(
horizontal: horizontalPadding,
vertical: verticalPadding,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Widget de liste des passages avec ValueListenableBuilder
Expanded(
child: ValueListenableBuilder(
valueListenable:
Hive.box<PassageModel>(AppKeys.passagesBoxName)
.listenable(),
@@ -558,14 +290,28 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
allPassages,
_sectorRepository,
_membreRepository);
// Appliquer les filtres
final filteredPassages =
_getFilteredPassagesFromList(formattedPassages);
// Récupérer les UserModel depuis les MembreModel
final users = _membres.map((membre) {
return userRepository.getUserById(membre.id);
}).where((user) => user != null).toList();
return PassagesListWidget(
showAddButton:
true, // Activer le bouton de création
// Données
passages: formattedPassages,
// Activation des filtres
showFilters: true,
showSearch: true,
showTypeFilter: true,
showPaymentFilter: true,
showSectorFilter: true,
showUserFilter: true,
showPeriodFilter: true,
// Données pour les filtres
sectors: _sectors,
members: users.cast<UserModel>(),
// Bouton d'ajout
showAddButton: true,
onAddPassage: () async {
// Ouvrir le dialogue de création de passage
await showDialog(
@@ -674,9 +420,7 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
),
],
),
passages: filteredPassages,
showFilters: false,
showSearch: false,
// Actions
showActions: true,
// Le widget gère maintenant le flux conditionnel par défaut
onPassageSelected: null,
@@ -695,9 +439,8 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
);
},
),
),
],
),
),
],
),
);
},
@@ -993,437 +736,6 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
);
}
// 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: [
// 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 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),
hint: const Text('Sélectionner un secteur'),
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 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),
hint: const Text('Sélectionner un membre'),
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: [
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),
hint: const Text('Sélectionner une période'),
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 TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Rechercher par adresse, nom, secteur ou membre...',
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 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),
hint: const Text('Sélectionner un type de passage'),
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();
@@ -1631,46 +943,4 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
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 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),
hint: const Text('Sélectionner un mode de règlement'),
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;
});
}
},
),
),
);
}
}

View File

@@ -15,22 +15,19 @@ class UserDashboardHomePage extends StatefulWidget {
}
class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
// Formater une date au format JJ/MM/YYYY
String _formatDate(DateTime date) {
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final size = MediaQuery.of(context).size;
final isDesktop = size.width > 900;
final isMobile = size.width < 600;
final double horizontalPadding = isMobile ? 8.0 : 16.0;
return Scaffold(
backgroundColor: Colors.transparent,
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
padding: EdgeInsets.all(horizontalPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -39,7 +36,7 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
final operation = userRepository.getCurrentOperation();
if (operation != null) {
return Text(
'${operation.name} (${_formatDate(operation.dateDebut)}-${_formatDate(operation.dateFin)})',
operation.name,
style: TextStyle(
fontSize: AppTheme.r(context, 20),
fontWeight: FontWeight.bold,
@@ -92,9 +89,9 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
// Construction d'une carte combinée pour les règlements (liste + graphique)
Widget _buildCombinedPaymentsCard(bool isDesktop) {
return PaymentSummaryCard(
title: 'Mes règlements',
title: 'Règlements',
titleColor: AppTheme.accentColor,
titleIcon: Icons.payments,
titleIcon: Icons.euro,
height: 300,
useValueListenable: true,
userId: userRepository.getCurrentUser()?.id,
@@ -105,27 +102,7 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
backgroundIconOpacity: 0.07,
backgroundIconSize: 180,
customTotalDisplay: (totalAmount) {
// Calculer le nombre de passages avec règlement pour le titre personnalisé
final currentUser = userRepository.getCurrentUser();
if (currentUser == null) return '${totalAmount.toStringAsFixed(2)}';
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
int passagesCount = 0;
for (final passage in passagesBox.values) {
if (passage.fkUser == currentUser.id) {
double montant = 0.0;
try {
String montantStr = passage.montant.replaceAll(',', '.');
montant = double.tryParse(montantStr) ?? 0.0;
} catch (e) {
// Ignorer les erreurs
}
if (montant > 0) passagesCount++;
}
}
return '${totalAmount.toStringAsFixed(2)} € sur $passagesCount passages';
return '${totalAmount.toStringAsFixed(2)}';
},
);
}
@@ -133,7 +110,7 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
// Construction d'une carte combinée pour les passages (liste + graphique)
Widget _buildCombinedPassagesCard(BuildContext context, bool isDesktop) {
return PassageSummaryCard(
title: 'Mes passages',
title: 'Passages',
titleColor: AppTheme.primaryColor,
titleIcon: Icons.route,
height: 300,
@@ -179,7 +156,7 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
// Construction de la liste des derniers passages
Widget _buildRecentPassages(BuildContext context, ThemeData theme) {
// Utilisation directe du widget PassagesListWidget sans Card wrapper
// Utilisation directe du widget PassagesListWidget
return ValueListenableBuilder(
valueListenable:
Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
@@ -196,14 +173,14 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
child: const Padding(
padding: EdgeInsets.all(32.0),
child: Center(
child: Text(
'Aucun passage récent',
style: TextStyle(
color: Colors.grey,
fontSize: AppTheme.r(context, 14),
fontSize: 14,
),
),
),
@@ -211,40 +188,15 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
);
}
// Utiliser une hauteur fixe pour le widget dans le dashboard
return SizedBox(
height:
450, // Hauteur légèrement augmentée pour compenser l'absence de Card
child: PassagesListWidget(
passages: recentPassages,
showFilters: false,
showSearch: false,
showActions: true,
maxPassages: 20,
// Ne pas appliquer de filtres supplémentaires car les passages
// sont déjà filtrés dans _getRecentPassages
excludePassageTypes:
null, // Pas de filtre, déjà géré dans _getRecentPassages
filterByUserId:
null, // Pas de filtre, déjà géré dans _getRecentPassages
periodFilter: null, // Pas de filtre de période
// Le widget gère maintenant le flux conditionnel par défaut
onPassageSelected: null,
onDetailsView: (passage) {
debugPrint('Affichage des détails: ${passage['id']}');
},
onPassageEdit: (passage) {
debugPrint('Modification du passage: ${passage['id']}');
},
onReceiptView: (passage) {
debugPrint('Affichage du reçu pour le passage: ${passage['id']}');
},
onPassageDelete: (passage) {
// Pas besoin de faire quoi que ce soit ici
// Le ValueListenableBuilder se rafraîchira automatiquement
// après la suppression dans Hive via le repository
},
),
// Utiliser PassagesListWidget sans hauteur fixe - laisse le widget gérer sa propre taille
return PassagesListWidget(
passages: recentPassages,
showFilters: false,
showSearch: false,
showActions: true,
maxPassages: 20,
showAddButton: false,
sortBy: 'date',
);
},
);
@@ -261,8 +213,9 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
final allPassages = passagesBox.values.where((p) {
if (p.passedAt == null) return false;
if (p.fkType == 2) return false; // Exclure les passages "À finaliser"
if (currentUserId != null && p.fkUser != currentUserId)
if (currentUserId != null && p.fkUser != currentUserId) {
return false; // Filtrer par utilisateur
}
return true;
}).toList();

View File

@@ -40,15 +40,10 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
// État du tri actuel
PassageSortType _currentSort = PassageSortType.dateDesc;
// État des filtres
String selectedSector = 'Tous';
String selectedPeriod = 'Tous';
String selectedType = 'Tous';
String selectedPaymentMethod = 'Tous';
DateTimeRange? selectedDateRange;
// IDs pour les filtres
// État des filtres (uniquement pour synchronisation)
int? selectedSectorId;
String selectedPeriod = 'Toutes';
DateTimeRange? selectedDateRange;
// Repository pour les secteurs
late SectorRepository _sectorRepository;
@@ -130,20 +125,11 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
try {
// Charger le secteur présélectionné
final int? preselectedSectorId = _settingsBox.get('history_selectedSectorId');
final String? preselectedSectorName = _settingsBox.get('history_selectedSectorName');
final int? preselectedTypeId = _settingsBox.get('history_selectedTypeId');
final String? preselectedPeriod = _settingsBox.get('history_selectedPeriod');
final int? preselectedPaymentId = _settingsBox.get('history_selectedPaymentId');
if (preselectedSectorId != null && preselectedSectorName != null) {
if (preselectedSectorId != 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');
debugPrint('Secteur présélectionné: ID $preselectedSectorId');
}
if (preselectedPeriod != null) {
@@ -152,11 +138,6 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
debugPrint('Période présélectionnée: $preselectedPeriod');
}
if (preselectedPaymentId != null) {
selectedPaymentMethod = preselectedPaymentId.toString();
debugPrint('Mode de règlement présélectionné: $preselectedPaymentId');
}
// Nettoyer les valeurs après utilisation
_settingsBox.delete('history_selectedSectorId');
_settingsBox.delete('history_selectedSectorName');
@@ -173,26 +154,11 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
try {
if (selectedSectorId != null) {
_settingsBox.put('history_selectedSectorId', selectedSectorId);
_settingsBox.put('history_selectedSectorName', selectedSector);
}
if (selectedType != 'Tous') {
final typeId = int.tryParse(selectedType);
if (typeId != null) {
_settingsBox.put('history_selectedTypeId', typeId);
}
}
if (selectedPeriod != 'Tous') {
if (selectedPeriod != 'Toutes') {
_settingsBox.put('history_selectedPeriod', selectedPeriod);
}
if (selectedPaymentMethod != 'Tous') {
final paymentId = int.tryParse(selectedPaymentMethod);
if (paymentId != null) {
_settingsBox.put('history_selectedPaymentId', paymentId);
}
}
} catch (e) {
debugPrint('Erreur lors de la sauvegarde des préférences: $e');
}
@@ -201,7 +167,6 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
// Mettre à jour le filtre par secteur
void _updateSectorFilter(String sectorName, int? sectorId) {
setState(() {
selectedSector = sectorName;
selectedSectorId = sectorId;
});
_saveFilterPreferences();
@@ -328,21 +293,6 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
return false;
}
// Filtrer par type
if (selectedType != 'Tous') {
final typeId = int.tryParse(selectedType);
if (typeId != null && passage['type'] != typeId) {
return false;
}
}
// Filtrer par mode de règlement
if (selectedPaymentMethod != 'Tous') {
final paymentId = int.tryParse(selectedPaymentMethod);
if (paymentId != null && passage['payment'] != paymentId) {
return false;
}
}
// Filtrer par période/date
if (selectedDateRange != null && passage['date'] is DateTime) {
@@ -654,210 +604,9 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
);
}
// Construction des filtres
Widget _buildFilters(BuildContext context) {
final theme = Theme.of(context);
final size = MediaQuery.of(context).size;
final isDesktop = size.width > 900;
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
color: Colors.white.withValues(alpha: 0.95),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Filtres',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
const SizedBox(height: 16),
if (isDesktop)
Row(
children: [
// Filtre par secteur (si plusieurs secteurs)
if (_userSectors.length > 1)
Expanded(
child: _buildSectorFilter(theme),
),
if (_userSectors.length > 1)
const SizedBox(width: 16),
// Filtre par période
Expanded(
child: _buildPeriodFilter(theme),
),
],
)
else
Column(
children: [
// Filtre par secteur (si plusieurs secteurs)
if (_userSectors.length > 1) ...[
_buildSectorFilter(theme),
const SizedBox(height: 16),
],
// Filtre par période
_buildPeriodFilter(theme),
],
),
],
),
),
);
}
// Les filtres sont maintenant gérés directement dans le PassagesListWidget
// Construction du filtre par secteur
Widget _buildSectorFilter(ThemeData theme) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Secteur',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12.0),
decoration: BoxDecoration(
border: Border.all(color: theme.colorScheme.outline),
borderRadius: BorderRadius.circular(8.0),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedSector,
isExpanded: true,
icon: const Icon(Icons.arrow_drop_down),
items: [
const DropdownMenuItem<String>(
value: 'Tous',
child: Text('Tous les secteurs'),
),
..._userSectors.map((sector) {
final String libelle = sector.libelle.isNotEmpty
? sector.libelle
: 'Secteur ${sector.id}';
return DropdownMenuItem<String>(
value: libelle,
child: Text(
libelle,
overflow: TextOverflow.ellipsis,
),
);
}),
],
onChanged: (String? value) {
if (value != null) {
if (value == 'Tous') {
_updateSectorFilter('Tous', null);
} else {
try {
final sector = _userSectors.firstWhere(
(s) => s.libelle == value,
);
_updateSectorFilter(value, sector.id);
} catch (e) {
debugPrint('Erreur lors de la sélection du secteur: $e');
_updateSectorFilter('Tous', null);
}
}
}
},
),
),
),
],
);
}
// Construction du filtre par période
Widget _buildPeriodFilter(ThemeData theme) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Période',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12.0),
decoration: BoxDecoration(
border: Border.all(color: theme.colorScheme.outline),
borderRadius: BorderRadius.circular(8.0),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedPeriod,
isExpanded: true,
icon: const Icon(Icons.arrow_drop_down),
items: const [
DropdownMenuItem<String>(
value: 'Tous',
child: Text('Toutes les périodes'),
),
DropdownMenuItem<String>(
value: 'Derniers 15 jours',
child: Text('Derniers 15 jours'),
),
DropdownMenuItem<String>(
value: 'Dernière semaine',
child: Text('Dernière semaine'),
),
DropdownMenuItem<String>(
value: 'Dernier mois',
child: Text('Dernier mois'),
),
],
onChanged: (String? value) {
if (value != null) {
_updatePeriodFilter(value);
}
},
),
),
),
// Afficher la plage de dates sélectionnée si elle existe
if (selectedDateRange != null && selectedPeriod != 'Tous')
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
Icon(
Icons.date_range,
size: 16,
color: theme.colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'Du ${selectedDateRange!.start.day}/${selectedDateRange!.start.month}/${selectedDateRange!.start.year} au ${selectedDateRange!.end.day}/${selectedDateRange!.end.month}/${selectedDateRange!.end.year}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
);
}
// Méthodes de filtre retirées car maintenant gérées dans le widget
@override
Widget build(BuildContext context) {
@@ -869,18 +618,7 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Filtres avec bouton de rafraîchissement
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Filtres (secteur et période) avec bouton rafraîchir
if (!_isLoading && (_userSectors.length > 1 || selectedPeriod != 'Tous'))
_buildFilters(context),
],
),
),
// Les filtres sont maintenant intégrés dans le PassagesListWidget
// Affichage du chargement ou des erreurs
if (_isLoading)
@@ -944,14 +682,31 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
}
}
// Appliquer les filtres
passagesMap = _getFilteredPassages(passagesMap);
// Appliquer le tri sélectionné
passagesMap = _sortPassages(passagesMap);
return PassagesListWidget(
showAddButton: true, // Activer le bouton de création
// Données
passages: passagesMap,
// Activation des filtres
showFilters: true,
showSearch: true,
showTypeFilter: true,
showPaymentFilter: true,
showSectorFilter: true,
showUserFilter: false, // Pas de filtre membre pour la page user
showPeriodFilter: true,
// Données pour les filtres
sectors: _userSectors,
members: null, // Pas de filtre membre pour la page user
// Valeurs initiales
initialSectorId: selectedSectorId,
initialPeriod: selectedPeriod,
dateRange: selectedDateRange,
// Filtre par utilisateur courant
filterByUserId: currentUserId,
// Bouton d'ajout
showAddButton: true,
onAddPassage: () async {
// Ouvrir le dialogue de création de passage
await showDialog(
@@ -1041,17 +796,17 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
),
],
),
passages: passagesMap,
showFilters: true,
showSearch: true,
// Actions
showActions: true,
initialSearchQuery: '',
initialTypeFilter: selectedType,
initialPaymentFilter: selectedPaymentMethod,
excludePassageTypes: const [],
filterByUserId: null, // Déjà filtré en amont
key: const ValueKey('user_passages_list'),
onPassageSelected: null,
// Callback pour synchroniser les filtres
onFiltersChanged: (filters) {
setState(() {
selectedSectorId = filters['sectorId'];
selectedPeriod = filters['period'] ?? 'Toutes';
selectedDateRange = filters['dateRange'];
});
},
onDetailsView: (passage) {
debugPrint('Affichage des détails: ${passage['id']}');
_showPassageDetails(passage);

View File

@@ -274,30 +274,8 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
/// Construction du titre de l'AppBar
Widget _buildTitle(BuildContext context) {
// Si aucun titre de page n'est fourni, afficher simplement le titre principal
if (pageTitle == null) {
return Text(title);
}
// Utiliser LayoutBuilder pour détecter la largeur disponible
return LayoutBuilder(
builder: (context, constraints) {
// Déterminer si on est sur mobile ou écran étroit
final isNarrowScreen = constraints.maxWidth < 600;
final isMobilePlatform =
Theme.of(context).platform == TargetPlatform.android ||
Theme.of(context).platform == TargetPlatform.iOS;
// Sur mobile ou écrans étroits, afficher seulement le titre principal
if (isNarrowScreen || isMobilePlatform) {
return Text(title);
}
// Sur écrans larges (web desktop), afficher le titre de la page ou le titre principal
// Pour les admins, on affiche directement le titre de la page sans préfixe
return Text(pageTitle!);
},
);
// Titre vide pour économiser de l'espace sur mobile
return const Text('');
}
@override

View File

@@ -6,6 +6,8 @@ import 'package:geosector_app/core/services/current_amicale_service.dart';
import 'package:geosector_app/core/utils/api_exception.dart';
import 'package:geosector_app/app.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/presentation/widgets/passage_form_dialog.dart';
import 'package:hive_flutter/hive_flutter.dart';
@@ -25,6 +27,13 @@ class PassagesListWidget extends StatefulWidget {
/// Si vrai, la barre de recherche sera affichée
final bool showSearch;
/// Contrôle de l'affichage des filtres individuels
final bool showTypeFilter;
final bool showPaymentFilter;
final bool showSectorFilter;
final bool showUserFilter;
final bool showPeriodFilter;
/// Si vrai, les boutons d'action (détails, modifier, etc.) seront affichés
final bool showActions;
@@ -76,6 +85,18 @@ class PassagesListWidget extends StatefulWidget {
/// Callback appelé lorsque le bouton d'ajout est cliqué
final VoidCallback? onAddPassage;
/// Données pour les filtres avancés
final List<SectorModel>? sectors;
final List<UserModel>? members;
/// Valeurs initiales pour les filtres avancés
final int? initialSectorId;
final int? initialUserId;
final String? initialPeriod;
/// Callback appelé lorsque les filtres changent
final Function(Map<String, dynamic>)? onFiltersChanged;
const PassagesListWidget({
super.key,
@@ -85,6 +106,11 @@ class PassagesListWidget extends StatefulWidget {
this.showFilters = true,
this.showSearch = true,
this.showActions = true,
this.showTypeFilter = true,
this.showPaymentFilter = true,
this.showSectorFilter = false,
this.showUserFilter = false,
this.showPeriodFilter = false,
this.onPassageSelected,
this.onPassageEdit,
this.onReceiptView,
@@ -102,6 +128,12 @@ class PassagesListWidget extends StatefulWidget {
this.sortingButtons,
this.showAddButton = false,
this.onAddPassage,
this.sectors,
this.members,
this.initialSectorId,
this.initialUserId,
this.initialPeriod,
this.onFiltersChanged,
});
@override
@@ -113,6 +145,10 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
late String _selectedTypeFilter;
late String _selectedPaymentFilter;
late String _searchQuery;
late int? _selectedSectorId;
late int? _selectedUserId;
late String _selectedPeriod;
DateTimeRange? _selectedDateRange;
// Contrôleur de recherche
final TextEditingController _searchController = TextEditingController();
@@ -121,10 +157,29 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
void initState() {
super.initState();
// Initialiser les filtres
_selectedTypeFilter = widget.initialTypeFilter ?? 'Tous';
_selectedPaymentFilter = widget.initialPaymentFilter ?? 'Tous';
_selectedTypeFilter = widget.initialTypeFilter ?? 'Tous les types';
_selectedPaymentFilter = widget.initialPaymentFilter ?? 'Tous les règlements';
_searchQuery = widget.initialSearchQuery ?? '';
_searchController.text = _searchQuery;
_selectedSectorId = widget.initialSectorId;
_selectedUserId = widget.initialUserId;
_selectedPeriod = widget.initialPeriod ?? 'Toutes les périodes';
_selectedDateRange = widget.dateRange;
}
// Notifier les changements de filtres
void _notifyFiltersChanged() {
if (widget.onFiltersChanged != null) {
widget.onFiltersChanged!({
'typeFilter': _selectedTypeFilter,
'paymentFilter': _selectedPaymentFilter,
'searchQuery': _searchQuery,
'sectorId': _selectedSectorId,
'userId': _selectedUserId,
'period': _selectedPeriod,
'dateRange': _selectedDateRange,
});
}
}
// Vérifier si l'amicale autorise la suppression des passages
@@ -204,13 +259,13 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
width: 40,
height: 40,
decoration: BoxDecoration(
color: Color(typeInfo?['couleur1'] ?? Colors.blue.value)
color: Color(typeInfo?['couleur1'] ?? Colors.blue.toARGB32())
.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
typeInfo?['icon_data'] ?? Icons.receipt_long,
color: Color(typeInfo?['couleur1'] ?? Colors.blue.value),
color: Color(typeInfo?['couleur1'] ?? Colors.blue.toARGB32()),
size: 24,
),
),
@@ -231,7 +286,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color:
Color(typeInfo?['couleur1'] ?? Colors.blue.value)
Color(typeInfo?['couleur1'] ?? Colors.blue.toARGB32())
.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
@@ -239,7 +294,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
typeInfo?['titre'] ?? 'Inconnu',
style: TextStyle(
color: Color(
typeInfo?['couleur1'] ?? Colors.blue.value),
typeInfo?['couleur1'] ?? Colors.blue.toARGB32()),
fontSize: AppTheme.r(context, 12),
fontWeight: FontWeight.w600,
),
@@ -323,7 +378,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Color(paymentInfo?['couleur'] ??
Colors.grey.value)
Colors.grey.toARGB32())
.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
@@ -331,7 +386,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
paymentInfo?['titre'] ?? 'Inconnu',
style: TextStyle(
color: Color(paymentInfo?['couleur'] ??
Colors.grey.value),
Colors.grey.toARGB32()),
fontSize: AppTheme.r(context, 12),
fontWeight: FontWeight.w600,
),
@@ -749,14 +804,58 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
}
// Filtrer par secteur
if (widget.filterBySectorId != null &&
if (_selectedSectorId != null &&
passage.containsKey('fkSector') &&
passage['fkSector'] != widget.filterBySectorId) {
passage['fkSector'] != _selectedSectorId) {
return false;
}
// Filtrer par membre/utilisateur
if (_selectedUserId != null &&
passage.containsKey('fkUser') &&
passage['fkUser'] != _selectedUserId) {
// Les passages de type 2 sont partagés
if (passage.containsKey('type') && passage['type'] == 2) {
// Ne pas filtrer les passages type 2
} else {
return false;
}
}
// Filtrer par période
if (_selectedPeriod != 'Toutes les périodes' && passage.containsKey('date')) {
final DateTime passageDate = passage['date'] as DateTime;
final DateTime now = DateTime.now();
switch (_selectedPeriod) {
case 'Dernières 24h':
if (now.difference(passageDate).inHours > 24) return false;
break;
case 'Dernières 48h':
if (now.difference(passageDate).inHours > 48) return false;
break;
case 'Derniers 7 jours':
if (now.difference(passageDate).inDays > 7) return false;
break;
case 'Derniers 15 jours':
if (now.difference(passageDate).inDays > 15) return false;
break;
case 'Dernier mois':
if (now.difference(passageDate).inDays > 30) return false;
break;
case 'Personnalisée':
if (_selectedDateRange != null) {
if (passageDate.isBefore(_selectedDateRange!.start) ||
passageDate.isAfter(_selectedDateRange!.end.add(const Duration(days: 1)))) {
return false;
}
}
break;
}
}
// Filtre par type
if (_selectedTypeFilter != 'Tous') {
if (_selectedTypeFilter != 'Tous les types') {
try {
final typeEntries = AppKeys.typesPassages.entries.where(
(entry) => entry.value['titre'] == _selectedTypeFilter);
@@ -774,7 +873,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
}
// Filtre par type de règlement
if (_selectedPaymentFilter != 'Tous') {
if (_selectedPaymentFilter != 'Tous les règlements') {
try {
final paymentEntries = AppKeys.typesReglements.entries.where(
(entry) => entry.value['titre'] == _selectedPaymentFilter);
@@ -1043,9 +1142,17 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
// Dans les pages user, seuls les passages de l'utilisateur courant sont affichés normalement
final bool shouldGreyOut = !isAdminPage && !isOwnedByCurrentUser;
final bool isClickable = isAdminPage || isOwnedByCurrentUser;
// Dimensions responsives
final screenWidth = MediaQuery.of(context).size.width;
final bool isMobile = screenWidth < 600;
final cardMargin = isMobile ? 4.0 : 6.0;
final horizontalPadding = isMobile ? 10.0 : 12.0;
final verticalPadding = isMobile ? 8.0 : 10.0;
final iconSize = isMobile ? 32.0 : 36.0;
return Card(
margin: const EdgeInsets.only(bottom: 6), // Réduit de 8 à 6
margin: EdgeInsets.only(bottom: cardMargin),
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
@@ -1059,8 +1166,9 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
onTap: isClickable ? () => _handlePassageClick(passage) : null,
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0, vertical: 10.0), // Réduit de 16 à 12/10
padding: EdgeInsets.symmetric(
horizontal: horizontalPadding,
vertical: verticalPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -1069,8 +1177,8 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
children: [
// Icône du type de passage avec bordure couleur2
Container(
width: 36, // Réduit de 40 à 36
height: 36,
width: iconSize,
height: iconSize,
decoration: BoxDecoration(
color: Color(typePassage['couleur1'] as int)
.withValues(alpha: 0.1),
@@ -1296,13 +1404,15 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
) {
return Row(
children: [
Text(
'$label:',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
if (label.isNotEmpty) ...[
Text(
'$label:',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 8),
const SizedBox(width: 8),
],
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
@@ -1337,6 +1447,190 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
],
);
}
// Construction du filtre de secteur
Widget _buildSectorFilter(ThemeData theme, bool isCompact) {
if (widget.sectors == null || widget.sectors!.isEmpty) {
return const SizedBox();
}
final options = ['Tous les secteurs'] +
widget.sectors!.map((s) => s.libelle).toList();
final selectedValue = _selectedSectorId == null
? 'Tous les secteurs'
: () {
final sector = widget.sectors!.firstWhere((s) => s.id == _selectedSectorId,
orElse: () => widget.sectors!.first);
return sector.libelle;
}();
return isCompact
? _buildCompactDropdownFilter(
'Secteur',
selectedValue,
options,
(value) {
setState(() {
if (value == 'Tous les secteurs') {
_selectedSectorId = null;
} else {
_selectedSectorId = widget.sectors!.firstWhere((s) => s.libelle == value).id;
}
_notifyFiltersChanged();
});
},
theme,
)
: _buildDropdownFilter(
'',
selectedValue,
options,
(value) {
setState(() {
if (value == 'Tous les secteurs') {
_selectedSectorId = null;
} else {
_selectedSectorId = widget.sectors!.firstWhere((s) => s.libelle == value).id;
}
_notifyFiltersChanged();
});
},
theme,
);
}
// Construction du filtre de membre/utilisateur
Widget _buildUserFilter(ThemeData theme, bool isCompact) {
if (widget.members == null || widget.members!.isEmpty) {
return const SizedBox();
}
final options = ['Tous les membres'] +
widget.members!.map((u) => '${u.firstName} ${u.name}'.trim()).toList();
final selectedValue = _selectedUserId == null
? 'Tous les membres'
: () {
final user = widget.members!.firstWhere((u) => u.id == _selectedUserId,
orElse: () => widget.members!.first);
return '${user.firstName} ${user.name}'.trim();
}();
return isCompact
? _buildCompactDropdownFilter(
'Membre',
selectedValue,
options,
(value) {
setState(() {
if (value == 'Tous les membres') {
_selectedUserId = null;
} else {
_selectedUserId = widget.members!.firstWhere(
(u) => '${u.firstName} ${u.name}'.trim() == value
).id;
}
_notifyFiltersChanged();
});
},
theme,
)
: _buildDropdownFilter(
'',
selectedValue,
options,
(value) {
setState(() {
if (value == 'Tous les membres') {
_selectedUserId = null;
} else {
_selectedUserId = widget.members!.firstWhere(
(u) => '${u.firstName} ${u.name}'.trim() == value
).id;
}
_notifyFiltersChanged();
});
},
theme,
);
}
// Construction du filtre de période
Widget _buildPeriodFilter(ThemeData theme, bool isCompact) {
final options = [
'Toutes les périodes',
'Dernières 24h',
'Dernières 48h',
'Derniers 7 jours',
'Derniers 15 jours',
'Dernier mois',
];
if (_selectedDateRange != null && _selectedPeriod == 'Personnalisée') {
options.add('Personnalisée');
}
return isCompact
? _buildCompactDropdownFilter(
'Période',
_selectedPeriod,
options,
(value) async {
if (value == 'Personnalisée') {
final picked = await showDateRangePicker(
context: context,
firstDate: DateTime.now().subtract(const Duration(days: 365)),
lastDate: DateTime.now(),
initialDateRange: _selectedDateRange,
);
if (picked != null) {
setState(() {
_selectedDateRange = picked;
_selectedPeriod = 'Personnalisée';
_notifyFiltersChanged();
});
}
} else {
setState(() {
_selectedPeriod = value;
_selectedDateRange = null;
_notifyFiltersChanged();
});
}
},
theme,
)
: _buildDropdownFilter(
'',
_selectedPeriod,
options,
(value) async {
if (value == 'Personnalisée') {
final picked = await showDateRangePicker(
context: context,
firstDate: DateTime.now().subtract(const Duration(days: 365)),
lastDate: DateTime.now(),
initialDateRange: _selectedDateRange,
);
if (picked != null) {
setState(() {
_selectedDateRange = picked;
_selectedPeriod = 'Personnalisée';
_notifyFiltersChanged();
});
}
} else {
setState(() {
_selectedPeriod = value;
_selectedDateRange = null;
_notifyFiltersChanged();
});
}
},
theme,
);
}
@override
Widget build(BuildContext context) {
@@ -1536,185 +1830,236 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isDesktop)
// Version compacte pour le web (desktop)
// Barre de recherche (si activée) - toujours en premier
if (widget.showSearch)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Barre de recherche (si activée)
if (widget.showSearch)
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Rechercher par adresse ou nom...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
setState(() {
_searchQuery = '';
});
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide(
color: theme.colorScheme.outline,
width: 1.0,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 14.0),
),
onChanged: (value) {
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Rechercher par adresse ou nom...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
setState(() {
_searchQuery = value;
_searchQuery = '';
_notifyFiltersChanged();
});
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide(
color: theme.colorScheme.outline,
width: 1.0,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 14.0),
),
onChanged: (value) {
setState(() {
_searchQuery = value;
_notifyFiltersChanged();
});
},
),
),
if (isDesktop)
// Version compacte pour le web (desktop)
Column(
children: [
// Première ligne : Type, Règlement, Secteur
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Filtre par type de passage
if (widget.showTypeFilter)
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: _buildCompactDropdownFilter(
'Type',
_selectedTypeFilter,
[
'Tous les types',
...AppKeys.typesPassages.values
.map((type) => type['titre'] as String)
],
(value) {
setState(() {
_selectedTypeFilter = value;
_notifyFiltersChanged();
});
},
theme,
),
),
),
),
// Filtre par type de passage
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: _buildCompactDropdownFilter(
'Type',
_selectedTypeFilter,
[
'Tous',
...AppKeys.typesPassages.values
.map((type) => type['titre'] as String)
],
(value) {
setState(() {
_selectedTypeFilter = value;
});
},
theme,
// Filtre par type de règlement
if (widget.showPaymentFilter)
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: _buildCompactDropdownFilter(
'Règlement',
_selectedPaymentFilter,
[
'Tous les règlements',
...AppKeys.typesReglements.values
.map((type) => type['titre'] as String)
],
(value) {
setState(() {
_selectedPaymentFilter = value;
_notifyFiltersChanged();
});
},
theme,
),
),
),
),
),
// Filtre par type de règlement
Expanded(
child: _buildCompactDropdownFilter(
'Règlement',
_selectedPaymentFilter,
[
'Tous',
...AppKeys.typesReglements.values
.map((type) => type['titre'] as String)
// Filtre par secteur
if (widget.showSectorFilter && widget.sectors != null)
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: _buildSectorFilter(theme, true),
),
),
],
),
// Deuxième ligne : Membre et Période (si nécessaire)
if (widget.showUserFilter || widget.showPeriodFilter)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Filtre par membre
if (widget.showUserFilter && widget.members != null)
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: _buildUserFilter(theme, true),
),
),
// Filtre par période
if (widget.showPeriodFilter)
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: _buildPeriodFilter(theme, true),
),
),
// Spacer si un seul filtre sur la deuxième ligne
if ((widget.showUserFilter && !widget.showPeriodFilter) ||
(!widget.showUserFilter && widget.showPeriodFilter))
const Expanded(child: SizedBox()),
],
(value) {
setState(() {
_selectedPaymentFilter = value;
});
},
theme,
),
),
],
),
],
)
else
// Version mobile (non-desktop)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Barre de recherche (si activée)
if (widget.showSearch)
// Première ligne : Type et Règlement
if (widget.showTypeFilter || widget.showPaymentFilter)
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Rechercher par adresse ou nom...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: [
// Filtre par type de passage
if (widget.showTypeFilter)
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: _buildDropdownFilter(
'',
_selectedTypeFilter,
[
'Tous les types',
...AppKeys.typesPassages.values
.map((type) => type['titre'] as String)
],
(value) {
setState(() {
_searchQuery = '';
_selectedTypeFilter = value;
_notifyFiltersChanged();
});
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide(
color: theme.colorScheme.outline,
width: 1.0,
theme,
),
),
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 14.0),
),
onChanged: (value) {
setState(() {
_searchQuery = value;
});
},
// Filtre par type de règlement
if (widget.showPaymentFilter)
Expanded(
child: _buildDropdownFilter(
'',
_selectedPaymentFilter,
[
'Tous les règlements',
...AppKeys.typesReglements.values
.map((type) => type['titre'] as String)
],
(value) {
setState(() {
_selectedPaymentFilter = value;
_notifyFiltersChanged();
});
},
theme,
),
),
],
),
),
// Filtres
Row(
children: [
// Filtre par type de passage
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: _buildDropdownFilter(
'Type',
_selectedTypeFilter,
[
'Tous',
...AppKeys.typesPassages.values
.map((type) => type['titre'] as String)
],
(value) {
setState(() {
_selectedTypeFilter = value;
});
},
theme,
),
),
// Deuxième ligne : Secteur et Période
if (widget.showSectorFilter || widget.showPeriodFilter)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: [
// Filtre par secteur
if (widget.showSectorFilter && widget.sectors != null)
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: _buildSectorFilter(theme, false),
),
),
// Filtre par période
if (widget.showPeriodFilter)
Expanded(
child: _buildPeriodFilter(theme, false),
),
],
),
// Filtre par type de règlement
Expanded(
child: _buildDropdownFilter(
'Règlement',
_selectedPaymentFilter,
[
'Tous',
...AppKeys.typesReglements.values
.map((type) => type['titre'] as String)
],
(value) {
setState(() {
_selectedPaymentFilter = value;
});
},
theme,
),
),
],
),
),
// Troisième ligne : Membre (si nécessaire)
if (widget.showUserFilter && widget.members != null)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: _buildUserFilter(theme, false),
),
],
),
],