fix: Récupérer l'opération active depuis la table operations
- Corrige l'erreur SQL 'Unknown column fk_operation in users' - L'opération active est récupérée depuis operations.chk_active = 1 - Jointure avec users pour filtrer par entité de l'admin créateur - Query: SELECT o.id FROM operations o INNER JOIN users u ON u.fk_entite = o.fk_entite WHERE u.id = ? AND o.chk_active = 1
This commit is contained in:
0
app/lib/presentation/admin/admin_connexions_page.dart
Normal file → Executable file
0
app/lib/presentation/admin/admin_connexions_page.dart
Normal file → Executable file
@@ -78,11 +78,8 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
/// Effectue un nettoyage sélectif du cache
|
||||
/// Préserve la box pending_requests et les données critiques
|
||||
Future<void> _performSelectiveCleanup({bool manual = false}) async {
|
||||
debugPrint('🧹 === DÉBUT DU NETTOYAGE DU CACHE === 🧹');
|
||||
debugPrint('📌 Type: ${manual ? "MANUEL" : "AUTOMATIQUE"}');
|
||||
debugPrint('📱 Platform: ${kIsWeb ? "WEB" : "MOBILE"}');
|
||||
debugPrint('📦 Version actuelle: $_appVersion');
|
||||
|
||||
debugPrint('🧹 Nettoyage du cache (${manual ? "manuel" : "auto"})...');
|
||||
|
||||
try {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@@ -94,27 +91,21 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
|
||||
// Étape 1: Nettoyer le Service Worker (Web uniquement)
|
||||
if (kIsWeb) {
|
||||
debugPrint('🔄 Nettoyage du Service Worker...');
|
||||
try {
|
||||
// Désenregistrer tous les service workers
|
||||
final registrations = await html.window.navigator.serviceWorker?.getRegistrations();
|
||||
if (registrations != null) {
|
||||
for (final registration in registrations) {
|
||||
await registration.unregister();
|
||||
debugPrint('✅ Service Worker désenregistré');
|
||||
}
|
||||
}
|
||||
|
||||
// Nettoyer les caches du navigateur
|
||||
if (html.window.caches != null) {
|
||||
final cacheNames = await html.window.caches!.keys();
|
||||
for (final cacheName in cacheNames) {
|
||||
await html.window.caches!.delete(cacheName);
|
||||
debugPrint('✅ Cache "$cacheName" supprimé');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur lors du nettoyage Service Worker: $e');
|
||||
debugPrint('⚠️ Erreur Service Worker: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,28 +117,20 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
}
|
||||
|
||||
// Étape 2: Sauvegarder les données critiques (pending_requests + app_version)
|
||||
debugPrint('💾 Sauvegarde des données critiques...');
|
||||
List<dynamic>? pendingRequests;
|
||||
String? savedAppVersion;
|
||||
try {
|
||||
// Sauvegarder pending_requests
|
||||
if (Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
|
||||
final pendingBox = Hive.box(AppKeys.pendingRequestsBoxName);
|
||||
pendingRequests = pendingBox.values.toList();
|
||||
debugPrint('📊 ${pendingRequests.length} requêtes en attente sauvegardées');
|
||||
await pendingBox.close();
|
||||
}
|
||||
|
||||
// Sauvegarder app_version pour éviter de perdre l'info de version
|
||||
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
|
||||
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
||||
savedAppVersion = settingsBox.get('app_version') as String?;
|
||||
if (savedAppVersion != null) {
|
||||
debugPrint('📦 Version sauvegardée: $savedAppVersion');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur lors de la sauvegarde: $e');
|
||||
debugPrint('⚠️ Erreur sauvegarde données critiques: $e');
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
@@ -173,17 +156,14 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
];
|
||||
|
||||
// Étape 4: Fermer et supprimer les boxes
|
||||
debugPrint('🗑️ Nettoyage des boxes Hive...');
|
||||
for (final boxName in boxesToClean) {
|
||||
try {
|
||||
if (Hive.isBoxOpen(boxName)) {
|
||||
await Hive.box(boxName).close();
|
||||
debugPrint('📦 Box "$boxName" fermée');
|
||||
}
|
||||
await Hive.deleteBoxFromDisk(boxName);
|
||||
debugPrint('✅ Box "$boxName" supprimée');
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur lors du nettoyage de "$boxName": $e');
|
||||
debugPrint('⚠️ Erreur nettoyage box "$boxName": $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,27 +175,21 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
}
|
||||
|
||||
// Étape 5: Réinitialiser Hive proprement
|
||||
debugPrint('🔄 Réinitialisation de Hive...');
|
||||
await Hive.close();
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
await Hive.initFlutter();
|
||||
|
||||
// Étape 6: Restaurer les données critiques
|
||||
if (pendingRequests != null && pendingRequests.isNotEmpty) {
|
||||
debugPrint('♻️ Restauration des requêtes en attente...');
|
||||
final pendingBox = await Hive.openBox(AppKeys.pendingRequestsBoxName);
|
||||
for (final request in pendingRequests) {
|
||||
await pendingBox.add(request);
|
||||
}
|
||||
debugPrint('✅ ${pendingRequests.length} requêtes restaurées');
|
||||
}
|
||||
|
||||
// Restaurer app_version pour maintenir la détection de changement de version
|
||||
if (savedAppVersion != null) {
|
||||
debugPrint('♻️ Restauration de la version...');
|
||||
final settingsBox = await Hive.openBox(AppKeys.settingsBoxName);
|
||||
await settingsBox.put('app_version', savedAppVersion);
|
||||
debugPrint('✅ Version restaurée: $savedAppVersion');
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
@@ -225,7 +199,7 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
});
|
||||
}
|
||||
|
||||
debugPrint('🎉 === NETTOYAGE TERMINÉ AVEC SUCCÈS === 🎉');
|
||||
debugPrint('✅ Nettoyage du cache terminé');
|
||||
|
||||
// Petit délai pour voir le message de succès
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
@@ -238,7 +212,7 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('❌ ERREUR CRITIQUE lors du nettoyage: $e');
|
||||
debugPrint('❌ Erreur nettoyage cache: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isCleaningCache = false;
|
||||
@@ -260,45 +234,29 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
/// Réinitialise le cache de tous les repositories après nettoyage complet
|
||||
void _resetAllRepositoriesCache() {
|
||||
try {
|
||||
debugPrint('🔄 === RESET DU CACHE DES REPOSITORIES === 🔄');
|
||||
|
||||
// Reset du cache des 3 repositories qui utilisent le pattern de cache
|
||||
passageRepository.resetCache();
|
||||
sectorRepository.resetCache();
|
||||
membreRepository.resetCache();
|
||||
|
||||
debugPrint('✅ Cache de tous les repositories réinitialisé');
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur lors du reset des caches: $e');
|
||||
// Ne pas faire échouer le processus si le reset échoue
|
||||
debugPrint('⚠️ Erreur reset caches repositories: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Détecte et gère le refresh (F5) avec session existante
|
||||
/// Retourne true si une session a été restaurée, false sinon
|
||||
Future<bool> _handleSessionRefreshIfNeeded() async {
|
||||
if (!kIsWeb) {
|
||||
debugPrint('📱 Plateforme mobile - pas de gestion F5');
|
||||
return false;
|
||||
}
|
||||
if (!kIsWeb) return false;
|
||||
|
||||
try {
|
||||
debugPrint('🔍 Vérification d\'une session existante (F5)...');
|
||||
|
||||
// Charger l'utilisateur depuis Hive
|
||||
await CurrentUserService.instance.loadFromHive();
|
||||
|
||||
final isLoggedIn = CurrentUserService.instance.isLoggedIn;
|
||||
final displayMode = CurrentUserService.instance.displayMode;
|
||||
final sessionId = CurrentUserService.instance.sessionId;
|
||||
|
||||
if (!isLoggedIn || sessionId == null) {
|
||||
debugPrint('ℹ️ Aucune session active - affichage normal de la splash');
|
||||
return false;
|
||||
}
|
||||
if (!isLoggedIn || sessionId == null) return false;
|
||||
|
||||
debugPrint('🔄 Session active détectée - mode: $displayMode');
|
||||
debugPrint('🔄 Rechargement des données depuis l\'API...');
|
||||
debugPrint('🔄 Session active détectée, restauration...');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@@ -312,9 +270,6 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
|
||||
// Appeler le nouvel endpoint API pour restaurer la session
|
||||
// IMPORTANT: Utiliser getWithoutQueue() pour ne JAMAIS mettre cette requête en file d'attente
|
||||
// Les refresh de session sont liés à une session spécifique et ne doivent pas être rejoués
|
||||
debugPrint('🔄 Appel API: user/session avec sessionId: ${sessionId.substring(0, 10)}...');
|
||||
|
||||
final response = await ApiService.instance.getWithoutQueue(
|
||||
'user/session',
|
||||
queryParameters: {'mode': displayMode},
|
||||
@@ -327,10 +282,7 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
if (response.data is String) {
|
||||
final dataStr = response.data as String;
|
||||
if (dataStr.contains('<!DOCTYPE') || dataStr.contains('<html')) {
|
||||
debugPrint('❌ ERREUR: L\'API a retourné du HTML au lieu de JSON !');
|
||||
debugPrint('❌ StatusCode: $statusCode');
|
||||
debugPrint('❌ URL de base: ${ApiService.instance.baseUrl}');
|
||||
debugPrint('❌ Début de la réponse: ${dataStr.substring(0, 100)}...');
|
||||
debugPrint('❌ L\'API a retourné du HTML au lieu de JSON');
|
||||
await CurrentUserService.instance.clearUser();
|
||||
return false;
|
||||
}
|
||||
@@ -340,15 +292,12 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
|
||||
switch (statusCode) {
|
||||
case 200:
|
||||
// Succès - traiter les données
|
||||
if (data == null || data['success'] != true) {
|
||||
debugPrint('❌ Format de réponse invalide (200 mais pas success=true)');
|
||||
debugPrint('❌ Format de réponse session invalide');
|
||||
await CurrentUserService.instance.clearUser();
|
||||
return false;
|
||||
}
|
||||
|
||||
debugPrint('✅ Données reçues de l\'API, traitement...');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Chargement de vos données...";
|
||||
@@ -356,7 +305,6 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
});
|
||||
}
|
||||
|
||||
// Traiter les données avec DataLoadingService
|
||||
final apiData = data['data'] as Map<String, dynamic>?;
|
||||
if (apiData == null) {
|
||||
debugPrint('❌ Données manquantes dans la réponse');
|
||||
@@ -365,11 +313,9 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
}
|
||||
|
||||
await DataLoadingService.instance.processLoginData(apiData);
|
||||
debugPrint('✅ Session restaurée avec succès');
|
||||
break;
|
||||
|
||||
case 400:
|
||||
// Paramètre mode invalide - erreur technique
|
||||
debugPrint('❌ Paramètre mode invalide: $displayMode');
|
||||
await CurrentUserService.instance.clearUser();
|
||||
if (mounted) {
|
||||
@@ -380,8 +326,7 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
return false;
|
||||
|
||||
case 401:
|
||||
// Session invalide ou expirée
|
||||
debugPrint('⚠️ Session invalide ou expirée');
|
||||
debugPrint('⚠️ Session expirée');
|
||||
await CurrentUserService.instance.clearUser();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@@ -391,9 +336,8 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
return false;
|
||||
|
||||
case 403:
|
||||
// Accès interdit (membre → admin) ou entité inactive
|
||||
final message = data?['message'] ?? 'Accès interdit';
|
||||
debugPrint('🚫 Accès interdit: $message');
|
||||
debugPrint('❌ Accès interdit: $message');
|
||||
await CurrentUserService.instance.clearUser();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@@ -410,7 +354,6 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
return false;
|
||||
|
||||
case 500:
|
||||
// Erreur serveur
|
||||
final message = data?['message'] ?? 'Erreur serveur';
|
||||
debugPrint('❌ Erreur serveur: $message');
|
||||
if (mounted) {
|
||||
@@ -425,11 +368,9 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
),
|
||||
);
|
||||
}
|
||||
// Ne pas effacer la session en cas d'erreur serveur
|
||||
return false;
|
||||
|
||||
default:
|
||||
// Code de retour inattendu
|
||||
debugPrint('❌ Code HTTP inattendu: $statusCode');
|
||||
await CurrentUserService.instance.clearUser();
|
||||
return false;
|
||||
@@ -449,12 +390,11 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
if (!mounted) return true;
|
||||
|
||||
if (displayMode == 'admin') {
|
||||
debugPrint('🔀 Redirection vers interface admin');
|
||||
context.go('/admin/home');
|
||||
} else {
|
||||
debugPrint('🔀 Redirection vers interface user');
|
||||
context.go('/user/field-mode');
|
||||
}
|
||||
debugPrint('✅ Session restaurée → $displayMode');
|
||||
|
||||
return true;
|
||||
|
||||
@@ -477,53 +417,32 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
|
||||
/// Vérifie si une nouvelle version est disponible et nettoie si nécessaire
|
||||
Future<void> _checkVersionAndCleanIfNeeded() async {
|
||||
if (!kIsWeb) {
|
||||
debugPrint('📱 Plateforme mobile - pas de nettoyage automatique');
|
||||
return;
|
||||
}
|
||||
if (!kIsWeb) return;
|
||||
|
||||
try {
|
||||
String lastVersion = '';
|
||||
|
||||
// Lire la version depuis Hive settings
|
||||
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
|
||||
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
||||
lastVersion = settingsBox.get('app_version', defaultValue: '') as String;
|
||||
}
|
||||
|
||||
debugPrint('🔍 Vérification de version:');
|
||||
debugPrint(' Version stockée: $lastVersion');
|
||||
debugPrint(' Version actuelle: $_appVersion');
|
||||
|
||||
// Si changement de version détecté
|
||||
if (lastVersion.isNotEmpty && lastVersion != _appVersion) {
|
||||
debugPrint('🆕 NOUVELLE VERSION DÉTECTÉE !');
|
||||
debugPrint(' Migration de $lastVersion vers $_appVersion');
|
||||
|
||||
debugPrint('🆕 Nouvelle version: $lastVersion → $_appVersion');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Nouvelle version détectée, mise à jour...";
|
||||
});
|
||||
}
|
||||
|
||||
// Effectuer le nettoyage automatique
|
||||
await _performSelectiveCleanup(manual: false);
|
||||
|
||||
// Reset du cache des repositories après nettoyage
|
||||
_resetAllRepositoriesCache();
|
||||
} else if (lastVersion.isEmpty) {
|
||||
// Première installation
|
||||
debugPrint('🎉 Première installation détectée');
|
||||
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
|
||||
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
||||
await settingsBox.put('app_version', _appVersion);
|
||||
debugPrint('💾 Version initiale sauvegardée dans Hive: $_appVersion');
|
||||
}
|
||||
} else {
|
||||
debugPrint('✅ Même version - pas de nettoyage nécessaire');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur lors de la vérification de version: $e');
|
||||
debugPrint('⚠️ Erreur vérification version: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,7 +479,7 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
|
||||
void _startInitialization() async {
|
||||
try {
|
||||
debugPrint('🚀 Début de l\'initialisation complète de l\'application...');
|
||||
debugPrint('🚀 Initialisation de l\'application...');
|
||||
|
||||
// Étape 1: Vérification des permissions GPS (obligatoire) - 0 à 10%
|
||||
if (!kIsWeb) {
|
||||
@@ -577,7 +496,6 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
final errorMessage = await LocationService.getLocationErrorMessage();
|
||||
|
||||
if (!hasPermission) {
|
||||
// Si les permissions ne sont pas accordées, on arrête tout
|
||||
debugPrint('❌ Permissions GPS refusées');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@@ -616,10 +534,7 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
}
|
||||
|
||||
// === GESTION F5 WEB : Vérifier session AVANT de détruire les données ===
|
||||
// Sur Web, on essaie d'abord de récupérer une session existante
|
||||
if (kIsWeb) {
|
||||
debugPrint('🌐 Web détecté - tentative de récupération de session existante...');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Vérification de session...";
|
||||
@@ -627,12 +542,9 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
});
|
||||
}
|
||||
|
||||
// Initialisation légère qui préserve les données
|
||||
final hasExistingSession = await HiveService.instance.initializeWithoutReset();
|
||||
|
||||
if (hasExistingSession) {
|
||||
debugPrint('✅ Session existante détectée, tentative de restauration...');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Restauration de la session...";
|
||||
@@ -640,17 +552,8 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
});
|
||||
}
|
||||
|
||||
// Tenter la restauration via l'API
|
||||
final sessionRestored = await _handleSessionRefreshIfNeeded();
|
||||
if (sessionRestored) {
|
||||
debugPrint('✅ Session restaurée via F5 - fin de l\'initialisation');
|
||||
return;
|
||||
}
|
||||
|
||||
// Si la restauration API échoue, on continue vers le login
|
||||
debugPrint('⚠️ Restauration API échouée, passage au login normal');
|
||||
} else {
|
||||
debugPrint('ℹ️ Pas de session existante, initialisation normale');
|
||||
if (sessionRestored) return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,22 +592,11 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
|
||||
// Gérer la box pending_requests séparément pour préserver les données
|
||||
try {
|
||||
debugPrint('📦 Gestion de la box pending_requests...');
|
||||
if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
|
||||
// Importer PendingRequest si nécessaire
|
||||
final pendingRequestBox = await Hive.openBox(AppKeys.pendingRequestsBoxName);
|
||||
final pendingCount = pendingRequestBox.length;
|
||||
if (pendingCount > 0) {
|
||||
debugPrint('⏳ $pendingCount requêtes en attente trouvées dans la box');
|
||||
} else {
|
||||
debugPrint('✅ Box pending_requests ouverte (vide)');
|
||||
}
|
||||
} else {
|
||||
debugPrint('✅ Box pending_requests déjà ouverte');
|
||||
await Hive.openBox(AppKeys.pendingRequestsBoxName);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur lors de l\'ouverture de la box pending_requests: $e');
|
||||
// On continue quand même, ce n'est pas critique pour le démarrage
|
||||
debugPrint('⚠️ Erreur ouverture pending_requests: $e');
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
@@ -717,8 +609,7 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
// Étape 4: Vérification finale - 80 à 95%
|
||||
final allBoxesOpen = HiveService.instance.areAllBoxesOpen();
|
||||
if (!allBoxesOpen) {
|
||||
final diagnostic = HiveService.instance.getDiagnostic();
|
||||
debugPrint('❌ Diagnostic des Box: $diagnostic');
|
||||
debugPrint('❌ Erreur: certaines boxes Hive non ouvertes');
|
||||
throw Exception('Une erreur est survenue lors de l\'initialisation');
|
||||
}
|
||||
|
||||
@@ -744,10 +635,9 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
||||
await settingsBox.put('hive_initialized', true);
|
||||
await settingsBox.put('hive_initialized_at', DateTime.now().toIso8601String());
|
||||
debugPrint('✅ Clé hive_initialized définie à true dans settings');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Impossible de définir la clé hive_initialized: $e');
|
||||
debugPrint('⚠️ Erreur hive_initialized: $e');
|
||||
}
|
||||
|
||||
// Attendre un court instant pour que l'utilisateur voie "Application prête !"
|
||||
@@ -761,39 +651,59 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
if (widget.action != null) {
|
||||
await _handleAutoRedirect();
|
||||
} else {
|
||||
setState(() {
|
||||
_showButtons = true;
|
||||
});
|
||||
// Sur mobile natif ou petit écran, rediriger directement vers connexion utilisateur
|
||||
// L'interface admin n'est disponible que sur Web avec grand écran
|
||||
if (mounted) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final isSmallScreen = screenWidth < 600;
|
||||
|
||||
if (!kIsWeb || isSmallScreen) {
|
||||
context.go('/login/user');
|
||||
} else {
|
||||
setState(() {
|
||||
_showButtons = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ Initialisation complète de l\'application terminée avec succès');
|
||||
debugPrint('✅ Initialisation terminée');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors de l\'initialisation: $e');
|
||||
debugPrint('❌ Erreur initialisation: $e');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Erreur de chargement - Veuillez redémarrer l'application";
|
||||
_progress = 1.0;
|
||||
_isInitializing = false;
|
||||
_showButtons = true;
|
||||
});
|
||||
|
||||
// Sur mobile natif ou petit écran, rediriger vers connexion utilisateur même en cas d'erreur
|
||||
// L'interface admin n'est disponible que sur Web avec grand écran
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final isSmallScreen = screenWidth < 600;
|
||||
|
||||
if (!kIsWeb || isSmallScreen) {
|
||||
context.go('/login/user');
|
||||
} else {
|
||||
setState(() {
|
||||
_showButtons = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère la redirection automatique après l'initialisation
|
||||
Future<void> _handleAutoRedirect() async {
|
||||
// Petit délai pour voir le message "Application prête !"
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
final action = widget.action?.toLowerCase();
|
||||
final type = widget.type?.toLowerCase();
|
||||
|
||||
debugPrint('🔄 Redirection automatique: action=$action, type=$type');
|
||||
|
||||
// Afficher un message de redirection avant de naviguer
|
||||
setState(() {
|
||||
_statusMessage = action == 'login'
|
||||
@@ -805,8 +715,8 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
switch (action) {
|
||||
case 'login':
|
||||
if (type == 'admin') {
|
||||
@@ -1227,7 +1137,6 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
);
|
||||
|
||||
if (confirm == true) {
|
||||
debugPrint('👤 Utilisateur a demandé un nettoyage manuel');
|
||||
await _performSelectiveCleanup(manual: true);
|
||||
|
||||
// Reset du cache des repositories après nettoyage
|
||||
|
||||
0
app/lib/presentation/chat/chat_communication_page.dart
Normal file → Executable file
0
app/lib/presentation/chat/chat_communication_page.dart
Normal file → Executable file
0
app/lib/presentation/dialogs/sector_action_result_dialog.dart
Normal file → Executable file
0
app/lib/presentation/dialogs/sector_action_result_dialog.dart
Normal file → Executable file
0
app/lib/presentation/dialogs/sector_dialog.dart
Normal file → Executable file
0
app/lib/presentation/dialogs/sector_dialog.dart
Normal file → Executable file
0
app/lib/presentation/pages/amicale_page.dart
Normal file → Executable file
0
app/lib/presentation/pages/amicale_page.dart
Normal file → Executable file
0
app/lib/presentation/pages/connexions_page.dart
Normal file → Executable file
0
app/lib/presentation/pages/connexions_page.dart
Normal file → Executable file
0
app/lib/presentation/pages/field_mode_page.dart
Normal file → Executable file
0
app/lib/presentation/pages/field_mode_page.dart
Normal file → Executable file
0
app/lib/presentation/pages/home_page.dart
Normal file → Executable file
0
app/lib/presentation/pages/home_page.dart
Normal file → Executable file
0
app/lib/presentation/pages/messages_page.dart
Normal file → Executable file
0
app/lib/presentation/pages/messages_page.dart
Normal file → Executable file
0
app/lib/presentation/pages/operations_page.dart
Normal file → Executable file
0
app/lib/presentation/pages/operations_page.dart
Normal file → Executable file
@@ -1,341 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/core/services/theme_service.dart';
|
||||
import 'package:geosector_app/presentation/widgets/theme_switcher.dart';
|
||||
|
||||
/// Page de paramètres pour la gestion du thème
|
||||
class ThemeSettingsPage extends StatelessWidget {
|
||||
const ThemeSettingsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Paramètres d\'affichage'),
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
foregroundColor: theme.colorScheme.onSurface,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Section informations
|
||||
_buildInfoSection(context),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Section sélection du thème
|
||||
_buildThemeSection(context),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Section aperçu
|
||||
_buildPreviewSection(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoSection(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'À propos des thèmes',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'• Mode Automatique : Suit les préférences de votre système\n'
|
||||
'• Mode Clair : Interface claire en permanence\n'
|
||||
'• Mode Sombre : Interface sombre en permanence\n\n'
|
||||
'Le mode automatique détecte automatiquement si votre appareil '
|
||||
'est configuré en mode sombre ou clair et adapte l\'interface en conséquence.',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildThemeSection(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.palette_outlined, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Choix du thème',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Thème actuel
|
||||
AnimatedBuilder(
|
||||
animation: ThemeService.instance,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.primary.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
ThemeService.instance.themeModeIcon,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Thème actuel',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
Text(
|
||||
ThemeService.instance.themeModeDescription,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Boutons de sélection style segments
|
||||
Text(
|
||||
'Sélectionner un thème :',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Center(
|
||||
child: ThemeSwitcher(
|
||||
style: ThemeSwitcherStyle.segmentedButton,
|
||||
onThemeChanged: () {
|
||||
// Optionnel: feedback haptic ou autres actions
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Thème changé vers ${ThemeService.instance.themeModeDescription}'),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Options alternatives
|
||||
Text(
|
||||
'Autres options :',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Dropdown
|
||||
const Row(
|
||||
children: [
|
||||
Text('Menu déroulant : '),
|
||||
ThemeSwitcher(
|
||||
style: ThemeSwitcherStyle.dropdown,
|
||||
showLabel: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Toggle buttons
|
||||
const Row(
|
||||
children: [
|
||||
Text('Boutons : '),
|
||||
ThemeSwitcher(style: ThemeSwitcherStyle.toggleButtons),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPreviewSection(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.preview, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Aperçu des couleurs',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Grille de couleurs
|
||||
GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
children: [
|
||||
_buildColorSample('Primary', theme.colorScheme.primary,
|
||||
theme.colorScheme.onPrimary),
|
||||
_buildColorSample('Secondary', theme.colorScheme.secondary,
|
||||
theme.colorScheme.onSecondary),
|
||||
_buildColorSample('Surface', theme.colorScheme.surface,
|
||||
theme.colorScheme.onSurface),
|
||||
_buildColorSample('Background', theme.colorScheme.surface,
|
||||
theme.colorScheme.onSurface),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Exemples de composants
|
||||
Text(
|
||||
'Exemples de composants :',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Bouton'),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Bouton'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Bouton'),
|
||||
),
|
||||
const Chip(
|
||||
label: Text('Chip'),
|
||||
avatar: Icon(Icons.star, size: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildColorSample(String label, Color color, Color onColor) {
|
||||
return Container(
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.3)),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: onColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dialog simple pour les paramètres de thème
|
||||
class ThemeSettingsDialog extends StatelessWidget {
|
||||
const ThemeSettingsDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.palette_outlined, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Apparence'),
|
||||
],
|
||||
),
|
||||
content: const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ThemeInfo(),
|
||||
SizedBox(height: 16),
|
||||
ThemeSwitcher(
|
||||
style: ThemeSwitcherStyle.segmentedButton,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
0
app/lib/presentation/user/user_field_mode_page.dart
Normal file → Executable file
0
app/lib/presentation/user/user_field_mode_page.dart
Normal file → Executable file
0
app/lib/presentation/widgets/badged_navigation_destination.dart
Normal file → Executable file
0
app/lib/presentation/widgets/badged_navigation_destination.dart
Normal file → Executable file
10
app/lib/presentation/widgets/btn_passages.dart
Normal file → Executable file
10
app/lib/presentation/widgets/btn_passages.dart
Normal file → Executable file
@@ -193,7 +193,8 @@ class BtnPassages extends StatelessWidget {
|
||||
Text(
|
||||
total > 1 ? 'passages' : 'passage',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
@@ -297,7 +298,8 @@ class BtnPassages extends StatelessWidget {
|
||||
child: Text(
|
||||
titre,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
@@ -371,9 +373,9 @@ class BtnPassages extends StatelessWidget {
|
||||
Text(
|
||||
'Nouveau',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[700],
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
0
app/lib/presentation/widgets/grouped_passages_dialog.dart
Normal file → Executable file
0
app/lib/presentation/widgets/grouped_passages_dialog.dart
Normal file → Executable file
0
app/lib/presentation/widgets/loading_spin_overlay.dart
Normal file → Executable file
0
app/lib/presentation/widgets/loading_spin_overlay.dart
Normal file → Executable file
48
app/lib/presentation/widgets/members_board_passages.dart
Normal file → Executable file
48
app/lib/presentation/widgets/members_board_passages.dart
Normal file → Executable file
@@ -402,14 +402,14 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
color: Colors.green.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[1]!['couleur2'] as int).withOpacity(0.2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.task_alt,
|
||||
size: 16,
|
||||
color: Colors.green,
|
||||
color: Color(AppKeys.typesPassages[1]!['couleur2'] as int),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_buildHeaderText('Effectués', 2, headerStyle),
|
||||
@@ -436,14 +436,14 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
color: Colors.orange.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[2]!['couleur2'] as int).withOpacity(0.2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.refresh,
|
||||
size: 16,
|
||||
color: Colors.orange,
|
||||
color: Color(AppKeys.typesPassages[2]!['couleur2'] as int),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_buildHeaderText('À finaliser', 4, headerStyle),
|
||||
@@ -460,14 +460,14 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
color: Colors.red.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[3]!['couleur2'] as int).withOpacity(0.2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.block,
|
||||
size: 16,
|
||||
color: Colors.red,
|
||||
color: Color(AppKeys.typesPassages[3]!['couleur2'] as int),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_buildHeaderText('Refusés', 5, headerStyle),
|
||||
@@ -484,14 +484,14 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
color: Colors.lightBlue.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[4]!['couleur2'] as int).withOpacity(0.2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.volunteer_activism,
|
||||
size: 16,
|
||||
color: Colors.lightBlue,
|
||||
color: Color(AppKeys.typesPassages[4]!['couleur2'] as int),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_buildHeaderText('Dons', 6, headerStyle),
|
||||
@@ -509,14 +509,14 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
color: Colors.blue.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[5]!['couleur2'] as int).withOpacity(0.2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.layers,
|
||||
size: 16,
|
||||
color: Colors.blue,
|
||||
color: Color(AppKeys.typesPassages[5]!['couleur2'] as int),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_buildHeaderText('Lots', 7, headerStyle),
|
||||
@@ -533,14 +533,14 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[6]!['couleur2'] as int).withOpacity(0.2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.home_outlined,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
color: Color(AppKeys.typesPassages[6]!['couleur2'] as int),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_buildHeaderText('Vides', showLotType ? 8 : 7, headerStyle),
|
||||
@@ -712,7 +712,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.green.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[1]!['couleur2'] as int),
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -752,7 +752,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.orange.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[2]!['couleur2'] as int),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
aFinaliserCount.toString(),
|
||||
@@ -773,7 +773,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.red.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[3]!['couleur2'] as int),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
refuseCount.toString(),
|
||||
@@ -794,7 +794,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.lightBlue.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[4]!['couleur2'] as int),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
donCount.toString(),
|
||||
@@ -816,7 +816,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.blue.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[5]!['couleur2'] as int),
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -846,7 +846,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[6]!['couleur2'] as int),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
videCount.toString(),
|
||||
@@ -1074,7 +1074,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
color: Color(AppKeys.typesPassages[1]!['couleur2'] as int).withOpacity(0.8),
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -1109,7 +1109,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.orange.withOpacity(0.1),
|
||||
color: Color(AppKeys.typesPassages[2]!['couleur2'] as int).withOpacity(0.8),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
aFinaliserCount.toString(),
|
||||
@@ -1130,7 +1130,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.red.withOpacity(0.1),
|
||||
color: Color(AppKeys.typesPassages[3]!['couleur2'] as int).withOpacity(0.8),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
refuseCount.toString(),
|
||||
@@ -1150,7 +1150,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.lightBlue.withOpacity(0.1),
|
||||
color: Color(AppKeys.typesPassages[4]!['couleur2'] as int).withOpacity(0.8),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
donCount.toString(),
|
||||
@@ -1171,7 +1171,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
color: Color(AppKeys.typesPassages[5]!['couleur2'] as int).withOpacity(0.8),
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -1201,7 +1201,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
color: Color(AppKeys.typesPassages[6]!['couleur2'] as int).withOpacity(0.8),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
videCount.toString(),
|
||||
|
||||
0
app/lib/presentation/widgets/offline_test_button.dart
Normal file → Executable file
0
app/lib/presentation/widgets/offline_test_button.dart
Normal file → Executable file
@@ -89,17 +89,13 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
|
||||
// Helpers de validation
|
||||
String? _validateNumero(String? value) {
|
||||
debugPrint('🔍 [VALIDATOR] _validateNumero appelé avec: "$value"');
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
debugPrint('❌ [VALIDATOR] Numéro vide -> retourne erreur');
|
||||
return 'Le numéro est obligatoire';
|
||||
}
|
||||
final numero = int.tryParse(value.trim());
|
||||
if (numero == null || numero <= 0) {
|
||||
debugPrint('❌ [VALIDATOR] Numéro invalide: $value -> retourne erreur');
|
||||
return 'Numéro invalide';
|
||||
}
|
||||
debugPrint('✅ [VALIDATOR] Numéro valide: $numero');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -166,30 +162,11 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
super.initState();
|
||||
|
||||
try {
|
||||
debugPrint('=== DEBUT PassageFormDialog.initState ===');
|
||||
|
||||
// Accéder à la settingsBox (déjà ouverte dans l'app)
|
||||
_settingsBox = Hive.box(AppKeys.settingsBoxName);
|
||||
|
||||
// Initialize controllers with passage data if available
|
||||
final passage = widget.passage;
|
||||
debugPrint('Passage reçu: ${passage != null}');
|
||||
|
||||
if (passage != null) {
|
||||
debugPrint('Passage ID: ${passage.id}');
|
||||
debugPrint('Passage fkType: ${passage.fkType}');
|
||||
debugPrint('Passage numero: ${passage.numero}');
|
||||
debugPrint('Passage rueBis: ${passage.rueBis}');
|
||||
debugPrint('Passage rue: ${passage.rue}');
|
||||
debugPrint('Passage ville: ${passage.ville}');
|
||||
debugPrint('Passage name: ${passage.name}');
|
||||
debugPrint('Passage email: ${passage.email}');
|
||||
debugPrint('Passage phone: ${passage.phone}');
|
||||
debugPrint('Passage montant: ${passage.montant}');
|
||||
debugPrint('Passage remarque: ${passage.remarque}');
|
||||
debugPrint('Passage fkHabitat: ${passage.fkHabitat}');
|
||||
debugPrint('Passage fkTypeReglement: ${passage.fkTypeReglement}');
|
||||
}
|
||||
|
||||
_selectedPassageType = passage?.fkType;
|
||||
_showForm = false; // Toujours commencer par la sélection de type
|
||||
@@ -199,8 +176,6 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
// Section Adresse : ouverte si nouveau passage, fermée si modification
|
||||
_isAddressSectionExpanded = passage == null;
|
||||
|
||||
debugPrint('Initialisation des controllers...');
|
||||
|
||||
// S'assurer que toutes les valeurs null deviennent des chaînes vides
|
||||
String numero = passage?.numero.toString() ?? '';
|
||||
String rueBis = passage?.rueBis.toString() ?? '';
|
||||
@@ -222,7 +197,6 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
|
||||
// Si nouveau passage, charger les valeurs mémorisées de la dernière adresse
|
||||
if (passage == null) {
|
||||
debugPrint('Nouveau passage: chargement des valeurs mémorisées...');
|
||||
numero = _settingsBox.get('lastPassageNumero', defaultValue: '') as String;
|
||||
rueBis = _settingsBox.get('lastPassageRueBis', defaultValue: '') as String;
|
||||
rue = _settingsBox.get('lastPassageRue', defaultValue: '') as String;
|
||||
@@ -231,8 +205,6 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
_fkHabitat = _settingsBox.get('lastPassageFkHabitat', defaultValue: 1) as int;
|
||||
appt = _settingsBox.get('lastPassageAppt', defaultValue: '') as String;
|
||||
niveau = _settingsBox.get('lastPassageNiveau', defaultValue: '') as String;
|
||||
|
||||
debugPrint('Valeurs chargées: numero="$numero", rue="$rue", ville="$ville"');
|
||||
}
|
||||
|
||||
// Initialiser la date de passage
|
||||
@@ -242,20 +214,6 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
final String timeFormatted =
|
||||
'${_passedAt.hour.toString().padLeft(2, '0')}:${_passedAt.minute.toString().padLeft(2, '0')}';
|
||||
|
||||
debugPrint('Valeurs pour controllers:');
|
||||
debugPrint(' numero: "$numero"');
|
||||
debugPrint(' rueBis: "$rueBis"');
|
||||
debugPrint(' rue: "$rue"');
|
||||
debugPrint(' ville: "$ville"');
|
||||
debugPrint(' name: "$name"');
|
||||
debugPrint(' email: "$email"');
|
||||
debugPrint(' phone: "$phone"');
|
||||
debugPrint(' montant: "$montant"');
|
||||
debugPrint(' remarque: "$remarque"');
|
||||
debugPrint(' passedAt: "$_passedAt"');
|
||||
debugPrint(' dateFormatted: "$dateFormatted"');
|
||||
debugPrint(' timeFormatted: "$timeFormatted"');
|
||||
|
||||
_numeroController = TextEditingController(text: numero);
|
||||
_rueBisController = TextEditingController(text: rueBis);
|
||||
_rueController = TextEditingController(text: rue);
|
||||
@@ -280,12 +238,8 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
debugPrint('=== FIN PassageFormDialog.initState ===');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('=== ERREUR PassageFormDialog.initState ===');
|
||||
debugPrint('Erreur: $e');
|
||||
debugPrint('StackTrace: $stackTrace');
|
||||
debugPrint('❌ Erreur initState PassageFormDialog: $e\n$stackTrace');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -334,20 +288,11 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
}
|
||||
|
||||
void _handleSubmit() async {
|
||||
debugPrint('🔵 [SUBMIT] Début _handleSubmit');
|
||||
|
||||
if (_isSubmitting) {
|
||||
debugPrint('⚠️ [SUBMIT] Déjà en cours de soumission, abandon');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('🔵 [SUBMIT] Vérification de l\'état du formulaire');
|
||||
debugPrint('🔵 [SUBMIT] _formKey: $_formKey');
|
||||
debugPrint('🔵 [SUBMIT] _formKey.currentState: ${_formKey.currentState}');
|
||||
if (_isSubmitting) return;
|
||||
|
||||
// Validation avec protection contre le null
|
||||
if (_formKey.currentState == null) {
|
||||
debugPrint('❌ [SUBMIT] ERREUR: _formKey.currentState est null !');
|
||||
debugPrint('❌ _formKey.currentState est null');
|
||||
if (mounted) {
|
||||
await ResultDialog.show(
|
||||
context: context,
|
||||
@@ -358,14 +303,9 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('🔵 [SUBMIT] Validation du formulaire...');
|
||||
final isValid = _formKey.currentState!.validate();
|
||||
debugPrint('🔵 [SUBMIT] Résultat validation: $isValid');
|
||||
|
||||
if (!isValid) {
|
||||
debugPrint('⚠️ [SUBMIT] Validation échouée, abandon');
|
||||
|
||||
// Afficher un dialog d'erreur clair à l'utilisateur
|
||||
if (mounted) {
|
||||
await ResultDialog.show(
|
||||
context: context,
|
||||
@@ -376,109 +316,73 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('✅ [SUBMIT] Validation OK, appel _savePassage()');
|
||||
await _savePassage();
|
||||
debugPrint('🔵 [SUBMIT] Fin _handleSubmit');
|
||||
}
|
||||
|
||||
Future<void> _savePassage() async {
|
||||
debugPrint('🟢 [SAVE] Début _savePassage');
|
||||
if (_isSubmitting) return;
|
||||
|
||||
if (_isSubmitting) {
|
||||
debugPrint('⚠️ [SAVE] Déjà en cours de soumission, abandon');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('🟢 [SAVE] Mise à jour état _isSubmitting = true');
|
||||
setState(() {
|
||||
_isSubmitting = true;
|
||||
});
|
||||
|
||||
// Afficher l'overlay de chargement
|
||||
debugPrint('🟢 [SAVE] Affichage overlay de chargement');
|
||||
final overlay = LoadingSpinOverlayUtils.show(
|
||||
context: context,
|
||||
message: 'Enregistrement en cours...',
|
||||
);
|
||||
|
||||
try {
|
||||
debugPrint('🟢 [SAVE] Récupération utilisateur actuel');
|
||||
final currentUser = widget.userRepository.getCurrentUser();
|
||||
debugPrint('🟢 [SAVE] currentUser: ${currentUser?.id} - ${currentUser?.name}');
|
||||
|
||||
if (currentUser == null) {
|
||||
debugPrint('❌ [SAVE] ERREUR: Utilisateur non connecté');
|
||||
throw Exception("Utilisateur non connecté");
|
||||
}
|
||||
|
||||
debugPrint('🟢 [SAVE] Récupération opération active');
|
||||
final currentOperation = widget.operationRepository.getCurrentOperation();
|
||||
debugPrint('🟢 [SAVE] currentOperation: ${currentOperation?.id} - ${currentOperation?.name}');
|
||||
|
||||
if (currentOperation == null && widget.passage == null) {
|
||||
debugPrint('❌ [SAVE] ERREUR: Aucune opération active trouvée');
|
||||
throw Exception("Aucune opération active trouvée");
|
||||
}
|
||||
|
||||
// Déterminer les valeurs de montant et type de règlement selon le type de passage
|
||||
debugPrint('🟢 [SAVE] Calcul des valeurs finales');
|
||||
debugPrint('🟢 [SAVE] _selectedPassageType: $_selectedPassageType');
|
||||
|
||||
final String finalMontant =
|
||||
(_selectedPassageType == 1 || _selectedPassageType == 5)
|
||||
? _montantController.text.trim().replaceAll(',', '.')
|
||||
: '0';
|
||||
debugPrint('🟢 [SAVE] finalMontant: $finalMontant');
|
||||
|
||||
// Déterminer le type de règlement final selon le type de passage
|
||||
final int finalTypeReglement;
|
||||
if (_selectedPassageType == 1 || _selectedPassageType == 5) {
|
||||
// Pour les types 1 et 5, utiliser la valeur sélectionnée (qui a été validée)
|
||||
finalTypeReglement = _fkTypeReglement;
|
||||
} else {
|
||||
// Pour tous les autres types, forcer "Non renseigné"
|
||||
finalTypeReglement = 4;
|
||||
}
|
||||
debugPrint('🟢 [SAVE] finalTypeReglement: $finalTypeReglement');
|
||||
|
||||
// Déterminer la valeur de nbPassages selon le type de passage
|
||||
final int finalNbPassages;
|
||||
if (widget.passage != null) {
|
||||
// Modification d'un passage existant
|
||||
if (_selectedPassageType == 2) {
|
||||
// Type 2 (À finaliser) : toujours incrémenter
|
||||
finalNbPassages = widget.passage!.nbPassages + 1;
|
||||
} else {
|
||||
// Autres types : mettre à 1 si actuellement 0, sinon conserver
|
||||
final currentNbPassages = widget.passage!.nbPassages;
|
||||
finalNbPassages = currentNbPassages == 0 ? 1 : currentNbPassages;
|
||||
}
|
||||
} else {
|
||||
// Nouveau passage : toujours 1
|
||||
finalNbPassages = 1;
|
||||
}
|
||||
debugPrint('🟢 [SAVE] finalNbPassages: $finalNbPassages');
|
||||
|
||||
// Récupérer les coordonnées GPS pour un nouveau passage
|
||||
String finalGpsLat = '0.0';
|
||||
String finalGpsLng = '0.0';
|
||||
if (widget.passage == null) {
|
||||
// Nouveau passage : tenter de récupérer la position GPS actuelle
|
||||
debugPrint('🟢 [SAVE] Récupération de la position GPS...');
|
||||
try {
|
||||
final position = await LocationService.getCurrentPosition();
|
||||
if (position != null) {
|
||||
finalGpsLat = position.latitude.toString();
|
||||
finalGpsLng = position.longitude.toString();
|
||||
debugPrint('🟢 [SAVE] GPS récupéré: lat=$finalGpsLat, lng=$finalGpsLng');
|
||||
} else {
|
||||
debugPrint('🟢 [SAVE] GPS non disponible, utilisation de 0.0 (l\'API utilisera le géocodage)');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ [SAVE] Erreur récupération GPS: $e - l\'API utilisera le géocodage');
|
||||
}
|
||||
} catch (_) {}
|
||||
} else {
|
||||
// Modification : conserver les coordonnées existantes
|
||||
finalGpsLat = widget.passage!.gpsLat;
|
||||
finalGpsLng = widget.passage!.gpsLng;
|
||||
}
|
||||
@@ -537,38 +441,25 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
isSynced: false,
|
||||
);
|
||||
|
||||
// Sauvegarder le passage d'abord
|
||||
debugPrint('🟢 [SAVE] Préparation sauvegarde passage');
|
||||
// Sauvegarder le passage
|
||||
PassageModel? savedPassage;
|
||||
if (widget.passage == null || widget.passage!.id == 0) {
|
||||
// Création d'un nouveau passage (passage null OU id=0)
|
||||
debugPrint('🟢 [SAVE] Création d\'un nouveau passage');
|
||||
savedPassage = await widget.passageRepository.createPassageWithReturn(passageData);
|
||||
debugPrint('🟢 [SAVE] Passage créé avec ID: ${savedPassage?.id}');
|
||||
|
||||
if (savedPassage == null) {
|
||||
debugPrint('❌ [SAVE] ERREUR: savedPassage est null après création');
|
||||
throw Exception("Échec de la création du passage");
|
||||
}
|
||||
} else {
|
||||
// Mise à jour d'un passage existant
|
||||
debugPrint('🟢 [SAVE] Mise à jour passage existant ID: ${widget.passage!.id}');
|
||||
await widget.passageRepository.updatePassage(passageData);
|
||||
debugPrint('🟢 [SAVE] Mise à jour réussie');
|
||||
savedPassage = passageData;
|
||||
}
|
||||
|
||||
// Garantir le type non-nullable après la vérification
|
||||
final confirmedPassage = savedPassage;
|
||||
debugPrint('✅ [SAVE] Passage sauvegardé avec succès ID: ${confirmedPassage.id}');
|
||||
|
||||
// Mémoriser l'adresse pour la prochaine création de passage
|
||||
debugPrint('🟢 [SAVE] Mémorisation adresse');
|
||||
await _saveLastPassageAddress();
|
||||
|
||||
// Propager la résidence aux autres passages de l'immeuble si nécessaire
|
||||
if (_fkHabitat == 2 && _residenceController.text.trim().isNotEmpty) {
|
||||
debugPrint('🟢 [SAVE] Propagation résidence à l\'immeuble');
|
||||
await _propagateResidenceToBuilding(confirmedPassage);
|
||||
}
|
||||
|
||||
@@ -605,16 +496,12 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
final paymentSuccess = await _attemptTapToPayWithPassage(confirmedPassage, montant);
|
||||
|
||||
if (paymentSuccess) {
|
||||
// Fermer le formulaire en cas de succès
|
||||
if (mounted) {
|
||||
Navigator.of(context, rootNavigator: false).pop();
|
||||
widget.onSuccess?.call();
|
||||
}
|
||||
} else {
|
||||
debugPrint('⚠️ Paiement Tap to Pay échoué - formulaire reste ouvert');
|
||||
// Ne pas fermer le formulaire en cas d'échec
|
||||
// L'utilisateur peut réessayer ou annuler
|
||||
}
|
||||
// Si échec, le formulaire reste ouvert pour réessayer
|
||||
},
|
||||
onQRCodeCompleted: () {
|
||||
// Pour QR Code: fermer le formulaire après l'affichage du QR
|
||||
@@ -667,17 +554,10 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
}
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
// Masquer le loading
|
||||
debugPrint('❌ [SAVE] ERREUR CAPTURÉE');
|
||||
debugPrint('❌ [SAVE] Type erreur: ${e.runtimeType}');
|
||||
debugPrint('❌ [SAVE] Message erreur: $e');
|
||||
debugPrint('❌ [SAVE] Stack trace:\n$stackTrace');
|
||||
|
||||
debugPrint('❌ Erreur sauvegarde passage: $e\n$stackTrace');
|
||||
LoadingSpinOverlayUtils.hideSpecific(overlay);
|
||||
|
||||
// Afficher l'erreur
|
||||
final errorMessage = ApiException.fromError(e).message;
|
||||
debugPrint('❌ [SAVE] Message d\'erreur formaté: $errorMessage');
|
||||
|
||||
if (mounted) {
|
||||
await ResultDialog.show(
|
||||
@@ -687,23 +567,17 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
debugPrint('🟢 [SAVE] Bloc finally - Nettoyage');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
debugPrint('🟢 [SAVE] _isSubmitting = false');
|
||||
}
|
||||
debugPrint('🟢 [SAVE] Fin _savePassage');
|
||||
}
|
||||
}
|
||||
|
||||
/// Mémoriser l'adresse du passage pour la prochaine création
|
||||
Future<void> _saveLastPassageAddress() async {
|
||||
try {
|
||||
debugPrint('🟡 [ADDRESS] Début mémorisation adresse');
|
||||
debugPrint('🟡 [ADDRESS] _settingsBox.isOpen: ${_settingsBox.isOpen}');
|
||||
|
||||
await _settingsBox.put('lastPassageNumero', _numeroController.text.trim());
|
||||
await _settingsBox.put('lastPassageRueBis', _rueBisController.text.trim());
|
||||
await _settingsBox.put('lastPassageRue', _rueController.text.trim());
|
||||
@@ -712,61 +586,34 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
await _settingsBox.put('lastPassageFkHabitat', _fkHabitat);
|
||||
await _settingsBox.put('lastPassageAppt', _apptController.text.trim());
|
||||
await _settingsBox.put('lastPassageNiveau', _niveauController.text.trim());
|
||||
|
||||
debugPrint('✅ [ADDRESS] Adresse mémorisée avec succès');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('❌ [ADDRESS] Erreur lors de la mémorisation: $e');
|
||||
debugPrint('❌ [ADDRESS] Stack trace:\n$stackTrace');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur mémorisation adresse: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Propager la résidence aux autres passages de l'immeuble (fkType=2, même adresse, résidence vide)
|
||||
Future<void> _propagateResidenceToBuilding(PassageModel savedPassage) async {
|
||||
try {
|
||||
debugPrint('🟡 [PROPAGATE] Début propagation résidence');
|
||||
|
||||
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
||||
debugPrint('🟡 [PROPAGATE] passagesBox.isOpen: ${passagesBox.isOpen}');
|
||||
debugPrint('🟡 [PROPAGATE] passagesBox.length: ${passagesBox.length}');
|
||||
|
||||
final residence = _residenceController.text.trim();
|
||||
debugPrint('🟡 [PROPAGATE] résidence: "$residence"');
|
||||
|
||||
// Clé d'adresse du passage sauvegardé
|
||||
final addressKey = '${savedPassage.numero}|${savedPassage.rueBis}|${savedPassage.rue}|${savedPassage.ville}';
|
||||
debugPrint('🟡 [PROPAGATE] addressKey: "$addressKey"');
|
||||
|
||||
int updatedCount = 0;
|
||||
|
||||
// Parcourir tous les passages
|
||||
for (int i = 0; i < passagesBox.length; i++) {
|
||||
final passage = passagesBox.getAt(i);
|
||||
if (passage != null) {
|
||||
// Vérifier les critères
|
||||
final passageAddressKey = '${passage.numero}|${passage.rueBis}|${passage.rue}|${passage.ville}';
|
||||
|
||||
if (passage.id != savedPassage.id && // Pas le passage actuel
|
||||
passage.fkHabitat == 2 && // Appartement
|
||||
passageAddressKey == addressKey && // Même adresse
|
||||
passage.residence.trim().isEmpty) { // Résidence vide
|
||||
|
||||
debugPrint('🟡 [PROPAGATE] Mise à jour passage ID: ${passage.id}');
|
||||
// Mettre à jour la résidence dans Hive
|
||||
if (passage.id != savedPassage.id &&
|
||||
passage.fkHabitat == 2 &&
|
||||
passageAddressKey == addressKey &&
|
||||
passage.residence.trim().isEmpty) {
|
||||
final updatedPassage = passage.copyWith(residence: residence);
|
||||
await passagesBox.put(passage.key, updatedPassage);
|
||||
updatedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedCount > 0) {
|
||||
debugPrint('✅ [PROPAGATE] Résidence propagée à $updatedCount passage(s)');
|
||||
} else {
|
||||
debugPrint('✅ [PROPAGATE] Aucun passage à mettre à jour');
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('❌ [PROPAGATE] Erreur lors de la propagation: $e');
|
||||
debugPrint('❌ [PROPAGATE] Stack trace:\n$stackTrace');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur propagation résidence: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -780,24 +627,110 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
if (currentUser != null && currentUser.fkEntite != null) {
|
||||
final userAmicale = widget.amicaleRepository.getAmicaleById(currentUser.fkEntite!);
|
||||
if (userAmicale != null) {
|
||||
// Si chkLotActif = false (0), on ne doit pas afficher le type Lot (5)
|
||||
showLotType = userAmicale.chkLotActif;
|
||||
debugPrint('Amicale ${userAmicale.name}: chkLotActif = $showLotType');
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer les types de passages en fonction de chkLotActif
|
||||
final filteredTypes = Map<int, Map<String, dynamic>>.from(AppKeys.typesPassages);
|
||||
if (!showLotType) {
|
||||
filteredTypes.remove(5); // Retirer le type "Lot" si chkLotActif = 0
|
||||
debugPrint('Type Lot (5) masqué car chkLotActif = false');
|
||||
filteredTypes.remove(5);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Afficher les infos du passage si modification
|
||||
if (widget.passage != null) ...[
|
||||
// Adresse du passage
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.primary.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Adresse principale
|
||||
Text(
|
||||
'${widget.passage!.numero} ${widget.passage!.rueBis} ${widget.passage!.rue}'.trim().replaceAll(RegExp(r'\s+'), ' '),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.passage!.ville,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
// Infos appartement si fkHabitat == 2
|
||||
if (widget.passage!.fkHabitat == 2) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
if (widget.passage!.niveau.isNotEmpty) ...[
|
||||
Icon(Icons.stairs, size: 16, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 4),
|
||||
Text('Niveau ${widget.passage!.niveau}'),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
if (widget.passage!.appt.isNotEmpty) ...[
|
||||
Icon(Icons.door_front_door, size: 16, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 4),
|
||||
Text('Appt ${widget.passage!.appt}'),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
// Afficher le nom de l'habitant (pour maison et appartement)
|
||||
if (widget.passage!.name.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.person, size: 16, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(child: Text(widget.passage!.name)),
|
||||
],
|
||||
),
|
||||
],
|
||||
// Afficher la remarque si renseignée
|
||||
if (widget.passage!.remarque.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.note, size: 16, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.passage!.remarque,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
Text(
|
||||
'Type de passage',
|
||||
widget.passage != null
|
||||
? 'Choisir le nouveau type de ce passage'
|
||||
: 'Type de passage',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
@@ -810,7 +743,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio:
|
||||
MediaQuery.of(context).size.width < 600 ? 1.8 : 2.5,
|
||||
MediaQuery.of(context).size.width < 600 ? 1.4 : 2.5,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
@@ -821,7 +754,6 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
final typeData = filteredTypes[typeId];
|
||||
|
||||
if (typeData == null) {
|
||||
debugPrint('ERREUR: typeData null pour typeId: $typeId');
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
@@ -881,8 +813,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('ERREUR dans itemBuilder pour index $index: $e');
|
||||
} catch (_) {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
@@ -893,9 +824,6 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
|
||||
Widget _buildPassageForm() {
|
||||
try {
|
||||
debugPrint('=== DEBUT _buildPassageForm ===');
|
||||
|
||||
debugPrint('Building Form...');
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
@@ -1362,11 +1290,8 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
],
|
||||
),
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('=== ERREUR _buildPassageForm ===');
|
||||
debugPrint('Erreur: $e');
|
||||
debugPrint('StackTrace: $stackTrace');
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur _buildPassageForm: $e');
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
@@ -1526,17 +1451,10 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!_showForm) ...[
|
||||
() {
|
||||
debugPrint('Building passage type selection...');
|
||||
return _buildPassageTypeSelection();
|
||||
}(),
|
||||
] else ...[
|
||||
() {
|
||||
debugPrint('Building passage form...');
|
||||
return _buildPassageForm();
|
||||
}(),
|
||||
],
|
||||
if (!_showForm)
|
||||
_buildPassageTypeSelection()
|
||||
else
|
||||
_buildPassageForm(),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -1690,10 +1608,9 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
);
|
||||
|
||||
// Envoyer la mise à jour à l'API (sera fait de manière asynchrone)
|
||||
widget.passageRepository.updatePassage(updatedPassage).then((_) {
|
||||
debugPrint('✅ Passage mis à jour avec stripe_payment_id: $paymentIntentId');
|
||||
}).catchError((error) {
|
||||
debugPrint('❌ Erreur mise à jour passage: $error');
|
||||
widget.passageRepository.updatePassage(updatedPassage).catchError((error) {
|
||||
debugPrint('❌ Erreur mise à jour passage stripe: $error');
|
||||
return false;
|
||||
});
|
||||
|
||||
setState(() {
|
||||
@@ -1720,7 +1637,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('Erreur Tap to Pay: $e');
|
||||
debugPrint('❌ Erreur Tap to Pay: $e');
|
||||
if (mounted) {
|
||||
await ResultDialog.show(
|
||||
context: context,
|
||||
@@ -1735,10 +1652,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
try {
|
||||
debugPrint('=== DEBUT PassageFormDialog.build ===');
|
||||
|
||||
final isMobile = _isMobile(context);
|
||||
debugPrint('Platform mobile détectée: $isMobile');
|
||||
|
||||
if (isMobile) {
|
||||
// Mode plein écran pour mobile
|
||||
@@ -1786,12 +1700,8 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('=== ERREUR PassageFormDialog.build ===');
|
||||
debugPrint('Erreur: $e');
|
||||
debugPrint('StackTrace: $stackTrace');
|
||||
|
||||
// Retourner un widget d'erreur simple
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur PassageFormDialog.build: $e');
|
||||
return Dialog(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -1980,9 +1890,7 @@ class _TapToPayFlowDialogState extends State<_TapToPayFlowDialog> {
|
||||
|
||||
// Annuler le PaymentIntent si créé pour permettre une nouvelle tentative
|
||||
if (shouldCancelPayment && _paymentIntentId != null) {
|
||||
StripeTapToPayService.instance.cancelPayment(_paymentIntentId!).catchError((cancelError) {
|
||||
debugPrint('⚠️ Erreur annulation PaymentIntent: $cancelError');
|
||||
});
|
||||
StripeTapToPayService.instance.cancelPayment(_paymentIntentId!).catchError((_) {});
|
||||
}
|
||||
|
||||
setState(() {
|
||||
|
||||
0
app/lib/presentation/widgets/passage_map_dialog.dart
Normal file → Executable file
0
app/lib/presentation/widgets/passage_map_dialog.dart
Normal file → Executable file
0
app/lib/presentation/widgets/payment_method_selection_dialog.dart
Normal file → Executable file
0
app/lib/presentation/widgets/payment_method_selection_dialog.dart
Normal file → Executable file
0
app/lib/presentation/widgets/pending_requests_counter.dart
Normal file → Executable file
0
app/lib/presentation/widgets/pending_requests_counter.dart
Normal file → Executable file
0
app/lib/presentation/widgets/qr_code_payment_dialog.dart
Normal file → Executable file
0
app/lib/presentation/widgets/qr_code_payment_dialog.dart
Normal file → Executable file
0
app/lib/presentation/widgets/result_dialog.dart
Normal file → Executable file
0
app/lib/presentation/widgets/result_dialog.dart
Normal file → Executable file
@@ -1,231 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/core/services/theme_service.dart';
|
||||
|
||||
/// Widget pour basculer entre les thèmes clair/sombre/automatique
|
||||
class ThemeSwitcher extends StatelessWidget {
|
||||
/// Style d'affichage du sélecteur
|
||||
final ThemeSwitcherStyle style;
|
||||
|
||||
/// Afficher le texte descriptif
|
||||
final bool showLabel;
|
||||
|
||||
/// Callback optionnel appelé après changement de thème
|
||||
final VoidCallback? onThemeChanged;
|
||||
|
||||
const ThemeSwitcher({
|
||||
super.key,
|
||||
this.style = ThemeSwitcherStyle.iconButton,
|
||||
this.showLabel = false,
|
||||
this.onThemeChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: ThemeService.instance,
|
||||
builder: (context, child) {
|
||||
switch (style) {
|
||||
case ThemeSwitcherStyle.iconButton:
|
||||
return _buildIconButton(context);
|
||||
case ThemeSwitcherStyle.dropdown:
|
||||
return _buildDropdown(context);
|
||||
case ThemeSwitcherStyle.segmentedButton:
|
||||
return _buildSegmentedButton(context);
|
||||
case ThemeSwitcherStyle.toggleButtons:
|
||||
return _buildToggleButtons(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Bouton icône simple (bascule entre clair/sombre)
|
||||
Widget _buildIconButton(BuildContext context) {
|
||||
final themeService = ThemeService.instance;
|
||||
|
||||
return IconButton(
|
||||
icon: Icon(themeService.themeModeIcon),
|
||||
tooltip: 'Changer le thème (${themeService.themeModeDescription})',
|
||||
onPressed: () async {
|
||||
await themeService.toggleTheme();
|
||||
onThemeChanged?.call();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Dropdown avec toutes les options
|
||||
Widget _buildDropdown(BuildContext context) {
|
||||
final themeService = ThemeService.instance;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return DropdownButton<ThemeMode>(
|
||||
value: themeService.themeMode,
|
||||
icon: Icon(Icons.arrow_drop_down, color: theme.colorScheme.onSurface),
|
||||
underline: Container(),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: ThemeMode.system,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.brightness_auto, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Automatique'),
|
||||
if (showLabel) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'(${themeService.isSystemDark ? 'sombre' : 'clair'})',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const DropdownMenuItem(
|
||||
value: ThemeMode.light,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.light_mode, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text('Clair'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const DropdownMenuItem(
|
||||
value: ThemeMode.dark,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.dark_mode, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text('Sombre'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (ThemeMode? mode) async {
|
||||
if (mode != null) {
|
||||
await themeService.setThemeMode(mode);
|
||||
onThemeChanged?.call();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Boutons segmentés (Material 3)
|
||||
Widget _buildSegmentedButton(BuildContext context) {
|
||||
final themeService = ThemeService.instance;
|
||||
|
||||
return SegmentedButton<ThemeMode>(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: ThemeMode.light,
|
||||
icon: Icon(Icons.light_mode, size: 16),
|
||||
label: Text('Clair'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: ThemeMode.system,
|
||||
icon: Icon(Icons.brightness_auto, size: 16),
|
||||
label: Text('Auto'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: ThemeMode.dark,
|
||||
icon: Icon(Icons.dark_mode, size: 16),
|
||||
label: Text('Sombre'),
|
||||
),
|
||||
],
|
||||
selected: {themeService.themeMode},
|
||||
onSelectionChanged: (Set<ThemeMode> selection) async {
|
||||
if (selection.isNotEmpty) {
|
||||
await themeService.setThemeMode(selection.first);
|
||||
onThemeChanged?.call();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Boutons à bascule
|
||||
Widget _buildToggleButtons(BuildContext context) {
|
||||
final themeService = ThemeService.instance;
|
||||
|
||||
return ToggleButtons(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
constraints: const BoxConstraints(minHeight: 40, minWidth: 60),
|
||||
isSelected: [
|
||||
themeService.themeMode == ThemeMode.light,
|
||||
themeService.themeMode == ThemeMode.system,
|
||||
themeService.themeMode == ThemeMode.dark,
|
||||
],
|
||||
onPressed: (int index) async {
|
||||
final modes = [ThemeMode.light, ThemeMode.system, ThemeMode.dark];
|
||||
await themeService.setThemeMode(modes[index]);
|
||||
onThemeChanged?.call();
|
||||
},
|
||||
children: const [
|
||||
Icon(Icons.light_mode, size: 20),
|
||||
Icon(Icons.brightness_auto, size: 20),
|
||||
Icon(Icons.dark_mode, size: 20),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget d'information sur le thème actuel
|
||||
class ThemeInfo extends StatelessWidget {
|
||||
const ThemeInfo({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: ThemeService.instance,
|
||||
builder: (context, child) {
|
||||
final themeService = ThemeService.instance;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outline.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
themeService.themeModeIcon,
|
||||
size: 16,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
themeService.themeModeDescription,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Styles d'affichage pour le ThemeSwitcher
|
||||
enum ThemeSwitcherStyle {
|
||||
/// Bouton icône simple qui bascule entre clair/sombre
|
||||
iconButton,
|
||||
|
||||
/// Menu déroulant avec toutes les options
|
||||
dropdown,
|
||||
|
||||
/// Boutons segmentés (Material 3)
|
||||
segmentedButton,
|
||||
|
||||
/// Boutons à bascule
|
||||
toggleButtons,
|
||||
}
|
||||
Reference in New Issue
Block a user