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:
2026-01-26 16:57:08 +01:00
parent c24a3afe6a
commit 0687900564
3040 changed files with 77204 additions and 1578 deletions

0
app/lib/presentation/admin/admin_connexions_page.dart Normal file → Executable file
View File

View 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
View File

View File

0
app/lib/presentation/dialogs/sector_dialog.dart Normal file → Executable file
View File

0
app/lib/presentation/pages/amicale_page.dart Normal file → Executable file
View File

0
app/lib/presentation/pages/connexions_page.dart Normal file → Executable file
View File

0
app/lib/presentation/pages/field_mode_page.dart Normal file → Executable file
View File

0
app/lib/presentation/pages/home_page.dart Normal file → Executable file
View File

0
app/lib/presentation/pages/messages_page.dart Normal file → Executable file
View File

0
app/lib/presentation/pages/operations_page.dart Normal file → Executable file
View File

View 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
View File

View File

10
app/lib/presentation/widgets/btn_passages.dart Normal file → Executable file
View 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,
),

View File

0
app/lib/presentation/widgets/loading_spin_overlay.dart Normal file → Executable file
View File

View 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
View File

View 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
View File

View File

View File

View File

0
app/lib/presentation/widgets/result_dialog.dart Normal file → Executable file
View File

View 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,
}