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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user