Files
geo/app/lib/app.dart
Pierre 0687900564 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
2026-01-26 16:57:08 +01:00

505 lines
19 KiB
Dart
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:geosector_app/core/theme/app_theme.dart';
import 'package:go_router/go_router.dart';
// Import conditionnel pour le web
import 'package:universal_html/html.dart' as html;
import 'package:geosector_app/core/services/current_user_service.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/core/repositories/operation_repository.dart';
import 'package:geosector_app/core/repositories/passage_repository.dart';
import 'package:geosector_app/core/repositories/sector_repository.dart';
import 'package:geosector_app/core/repositories/membre_repository.dart';
import 'package:geosector_app/core/repositories/amicale_repository.dart';
import 'package:geosector_app/core/services/sync_service.dart';
import 'package:geosector_app/core/services/connectivity_service.dart';
import 'package:geosector_app/core/services/chat_manager.dart';
import 'package:geosector_app/presentation/auth/splash_page.dart';
import 'package:geosector_app/presentation/auth/login_page.dart';
import 'package:geosector_app/presentation/auth/register_page.dart';
import 'package:geosector_app/presentation/pages/history_page.dart';
import 'package:geosector_app/presentation/pages/home_page.dart';
import 'package:geosector_app/presentation/pages/map_page.dart';
import 'package:geosector_app/presentation/pages/messages_page.dart';
import 'package:geosector_app/presentation/pages/amicale_page.dart';
import 'package:geosector_app/presentation/pages/operations_page.dart';
import 'package:geosector_app/presentation/pages/field_mode_page.dart';
import 'package:geosector_app/presentation/pages/connexions_page.dart';
// Instances globales des repositories (plus besoin d'injecter ApiService)
final operationRepository = OperationRepository();
final passageRepository = PassageRepository();
final userRepository = UserRepository();
final sectorRepository = SectorRepository();
final membreRepository = MembreRepository();
final amicaleRepository = AmicaleRepository();
final syncService = SyncService(userRepository: userRepository);
final connectivityService = ConnectivityService();
class GeosectorApp extends StatefulWidget {
const GeosectorApp({super.key});
@override
State<GeosectorApp> createState() => _GeosectorAppState();
}
class _GeosectorAppState extends State<GeosectorApp> with WidgetsBindingObserver {
// Clé globale pour accéder au contexte de l'app (pour les dialogues)
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// Sur Web, intercepter F5 / Ctrl+R pour proposer un refresh des données
if (kIsWeb) {
_setupF5Interceptor();
}
}
/// Configure l'interception de F5/Ctrl+R sur Web
void _setupF5Interceptor() {
html.window.onKeyDown.listen((event) {
// Détecter F5 ou Ctrl+R
final isF5 = event.key == 'F5';
final isCtrlR = (event.ctrlKey || event.metaKey) && event.key?.toLowerCase() == 'r';
if (isF5 || isCtrlR) {
event.preventDefault();
debugPrint('🔄 F5/Ctrl+R intercepté - Affichage du dialogue de refresh');
_showRefreshDialog();
}
});
debugPrint('🌐 Intercepteur F5/Ctrl+R configuré pour Web');
}
/// Affiche le dialogue de confirmation de refresh
void _showRefreshDialog() {
final context = navigatorKey.currentContext;
if (context == null) {
debugPrint('⚠️ Impossible d\'afficher le dialogue - contexte non disponible');
return;
}
showDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: const Row(
children: [
Icon(Icons.refresh, color: Colors.blue),
SizedBox(width: 12),
Text('Recharger les données ?'),
],
),
content: const Text(
'Voulez-vous actualiser vos données depuis le serveur ?\n\n'
'Vos modifications non synchronisées seront conservées.',
),
actions: [
TextButton(
onPressed: () {
Navigator.of(dialogContext).pop();
debugPrint('❌ Refresh annulé par l\'utilisateur');
},
child: const Text('Non'),
),
ElevatedButton(
onPressed: () {
Navigator.of(dialogContext).pop();
debugPrint('✅ Refresh demandé par l\'utilisateur');
// TODO: Implémenter le refresh des données via API
},
child: const Text('Oui, recharger'),
),
],
);
},
);
}
@override
void dispose() {
// Arrêter le chat quand l'app se ferme
ChatManager.instance.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
// App revenue au premier plan - relancer les syncs
debugPrint('📱 App au premier plan - Reprise des syncs chat');
ChatManager.instance.resumeSyncs();
break;
case AppLifecycleState.paused:
// App mise en arrière-plan - arrêter les syncs pour économiser la batterie
debugPrint('⏸️ App en arrière-plan - Pause des syncs chat');
ChatManager.instance.pauseSyncs();
break;
case AppLifecycleState.inactive:
// État transitoire (ex: appel entrant) - ne rien faire
debugPrint('💤 App inactive temporairement');
break;
case AppLifecycleState.detached:
// App vraiment fermée (rare sur mobile) - nettoyer complètement
debugPrint('🛑 App fermée complètement - Arrêt total du chat');
ChatManager.instance.dispose();
break;
case AppLifecycleState.hidden:
// État masqué (Flutter 3.13+) - traiter comme paused
debugPrint('👻 App masquée');
ChatManager.instance.pauseSyncs();
break;
}
}
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'GeoSector',
theme: AppTheme.lightTheme,
// Pas de darkTheme - thème clair forcé pour éviter les problèmes d'affichage
themeMode: ThemeMode.light,
routerConfig: _createRouter(),
debugShowCheckedModeBanner: false,
// Builder pour appliquer le theme responsive à toute l'app
builder: (context, child) {
return LayoutBuilder(
builder: (context, constraints) {
// Thème clair uniquement
const textColor = AppTheme.textLightColor;
// Débogage en mode développement
final width = constraints.maxWidth;
final scaleFactor = AppTheme.getFontScaleFactor(width);
// Afficher le debug uniquement lors du changement de taille
if (width < AppTheme.breakpointMobileSmall) {
debugPrint('📱 Mode: Très petit mobile (${width.toStringAsFixed(0)}px) → Facteur: ×$scaleFactor');
} else if (width < AppTheme.breakpointMobile) {
debugPrint('📱 Mode: Mobile (${width.toStringAsFixed(0)}px) → Facteur: ×$scaleFactor');
} else if (width < AppTheme.breakpointTablet) {
debugPrint('📱 Mode: Tablette (${width.toStringAsFixed(0)}px) → Facteur: ×$scaleFactor');
} else {
debugPrint('🖥️ Mode: Desktop (${width.toStringAsFixed(0)}px) → Facteur: ×$scaleFactor');
}
// Appliquer le TextTheme responsive
return Theme(
data: Theme.of(context).copyWith(
textTheme: AppTheme.getResponsiveTextTheme(context, textColor),
),
child: child ?? const SizedBox.shrink(),
);
},
);
},
// Configuration des localisations pour le français
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('fr', 'FR'), // Français comme langue principale
Locale('en', 'US'), // Anglais en fallback
],
locale: const Locale('fr', 'FR'), // Forcer le français par défaut
);
}
/// Création du routeur avec configuration pour URLs propres
GoRouter _createRouter() {
return GoRouter(
navigatorKey: navigatorKey,
initialLocation: '/',
routes: [
GoRoute(
path: '/',
name: 'splash',
builder: (context, state) {
// Récupérer les paramètres de query pour redirection automatique
final action = state.uri.queryParameters['action'];
final type = state.uri.queryParameters['type'];
debugPrint('GoRoute: Affichage de SplashPage avec action=$action, type=$type');
return SplashPage(action: action, type: type);
},
),
GoRoute(
path: '/login',
name: 'login',
builder: (context, state) {
// Récupérer le type depuis les query parameters ou extra data
final type = state.uri.queryParameters['type'] ?? (state.extra as Map<String, dynamic>?)?['type'] as String?;
debugPrint('GoRoute: Affichage de LoginPage avec type: $type');
return LoginPage(loginType: type);
},
),
// Routes spécifiques pour chaque type de login
GoRoute(
path: '/login/user',
name: 'login-user',
builder: (context, state) {
debugPrint('GoRoute: Affichage de LoginPage pour utilisateur');
return const LoginPage(loginType: 'user');
},
),
GoRoute(
path: '/login/admin',
name: 'login-admin',
builder: (context, state) {
debugPrint('GoRoute: Affichage de LoginPage pour admin');
return const LoginPage(loginType: 'admin');
},
),
GoRoute(
path: '/register',
name: 'register',
builder: (context, state) {
debugPrint('GoRoute: Affichage de RegisterPage');
return const RegisterPage();
},
),
// NOUVELLE ARCHITECTURE: Pages user avec sous-routes comme admin
GoRoute(
path: '/user',
name: 'user',
builder: (context, state) {
debugPrint('GoRoute: Redirection vers /user/dashboard');
// Rediriger directement vers dashboard au lieu d'utiliser UserDashboardPage
return const HomePage();
},
routes: [
// Sous-route pour le dashboard/home
GoRoute(
path: 'dashboard',
name: 'user-dashboard',
builder: (context, state) {
debugPrint('GoRoute: Affichage de HomePage (unifiée)');
return const HomePage();
},
),
// Sous-route pour l'historique
GoRoute(
path: 'history',
name: 'user-history',
builder: (context, state) {
debugPrint('GoRoute: Affichage de HistoryPage (unifiée)');
return const HistoryPage();
},
),
// Sous-route pour les messages
GoRoute(
path: 'messages',
name: 'user-messages',
builder: (context, state) {
debugPrint('GoRoute: Affichage de MessagesPage (unifiée)');
return const MessagesPage();
},
),
// Sous-route pour la carte
GoRoute(
path: 'map',
name: 'user-map',
builder: (context, state) {
debugPrint('GoRoute: Affichage de MapPage (unifiée)');
return const MapPage();
},
),
// Sous-route pour le mode terrain
GoRoute(
path: 'field-mode',
name: 'user-field-mode',
builder: (context, state) {
debugPrint('GoRoute: Affichage de FieldModePage (unifiée)');
return const FieldModePage();
},
),
],
),
// NOUVELLE ARCHITECTURE: Pages admin autonomes
GoRoute(
path: '/admin',
name: 'admin',
builder: (context, state) {
debugPrint('GoRoute: Affichage de HomePage (unifiée)');
return const HomePage();
},
routes: [
// Sous-route pour l'historique avec membre optionnel
GoRoute(
path: 'history',
name: 'admin-history',
builder: (context, state) {
final memberId = state.uri.queryParameters['memberId'];
debugPrint('GoRoute: Affichage de HistoryPage (admin) avec memberId=$memberId');
return HistoryPage(
memberId: memberId != null ? int.tryParse(memberId) : null,
);
},
),
// Sous-route pour la carte
GoRoute(
path: 'map',
name: 'admin-map',
builder: (context, state) {
debugPrint('GoRoute: Affichage de MapPage pour admin');
return const MapPage();
},
),
// Sous-route pour les messages
GoRoute(
path: 'messages',
name: 'admin-messages',
builder: (context, state) {
debugPrint('GoRoute: Affichage de MessagesPage (unifiée)');
return const MessagesPage();
},
),
// Sous-route pour amicale & membres (role 2 uniquement)
GoRoute(
path: 'amicale',
name: 'admin-amicale',
builder: (context, state) {
debugPrint('GoRoute: Affichage de AmicalePage (unifiée)');
return const AmicalePage();
},
),
// Sous-route pour opérations (role 2 uniquement)
GoRoute(
path: 'operations',
name: 'admin-operations',
builder: (context, state) {
debugPrint('GoRoute: Affichage de OperationsPage (unifiée)');
return const OperationsPage();
},
),
// Sous-route pour connexions (role 2+ uniquement)
GoRoute(
path: 'connexions',
name: 'admin-connexions',
builder: (context, state) {
debugPrint('GoRoute: Affichage de ConnexionsPage (unifiée)');
return const ConnexionsPage();
},
),
],
),
],
redirect: (context, state) {
final currentPath = state.uri.path;
debugPrint('GoRouter.redirect: currentPath = $currentPath');
// Pour la page racine, toujours autoriser l'affichage de la splash page
if (currentPath == '/') {
debugPrint('GoRouter.redirect: Autorisation splash page');
return null;
}
// Pages publiques qui ne nécessitent pas d'authentification
final publicPaths = ['/login', '/login/user', '/login/admin', '/register'];
if (publicPaths.any((path) => currentPath.startsWith(path))) {
debugPrint('GoRouter.redirect: Page publique autorisée: $currentPath');
return null;
}
// Vérifier l'authentification pour les pages protégées
try {
// Utiliser le nouveau CurrentUserService
final userService = CurrentUserService.instance;
final isAuthenticated = userService.isLoggedIn;
final currentUser = userService.currentUser;
debugPrint('GoRouter.redirect: isAuthenticated = $isAuthenticated');
debugPrint('GoRouter.redirect: currentUser = ${currentUser?.email}');
// Si pas authentifié, rediriger vers la splash page
if (!isAuthenticated) {
debugPrint('GoRouter.redirect: Non authentifié, redirection vers /');
return '/';
}
// Vérifier les permissions pour les pages admin
if (currentPath.startsWith('/admin')) {
final userRole = userService.userRole;
final isAdmin = userService.canAccessAdmin;
debugPrint('GoRouter.redirect: userRole = $userRole, canAccessAdmin = $isAdmin');
if (!isAdmin) {
debugPrint('GoRouter.redirect: Pas admin, redirection vers /user');
return '/user';
}
}
// Si on arrive ici, l'utilisateur a les permissions nécessaires
debugPrint('GoRouter.redirect: Accès autorisé à $currentPath');
return null;
} catch (e) {
debugPrint('GoRouter.redirect: Erreur lors de la vérification auth: $e');
// En cas d'erreur, rediriger vers la splash page pour sécurité
return '/';
}
},
// Listener pour déboguer les changements de route
refreshListenable: CurrentUserService.instance, // Écouter les changements dans CurrentUserService
debugLogDiagnostics: true, // Activer les logs de débogage
errorBuilder: (context, state) {
debugPrint('GoRouter.errorBuilder: Erreur pour ${state.uri.path}');
return Scaffold(
appBar: AppBar(
title: const Text('Erreur de navigation'),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(
'Page non trouvée',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(
'Chemin: ${state.uri.path}',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () {
debugPrint('GoRouter.errorBuilder: Retour vers /');
context.go('/');
},
icon: const Icon(Icons.home),
label: const Text('Retour à l\'accueil'),
),
],
),
),
),
);
},
);
}
}