Files
geo/app/lib/app.dart
pierre 570a1fa1f0 feat: Version 3.3.4 - Nouvelle architecture pages, optimisations widgets Flutter et API
- Mise à jour VERSION vers 3.3.4
- Optimisations et révisions architecture API (deploy-api.sh, scripts de migration)
- Ajout documentation Stripe Tap to Pay complète
- Migration vers polices Inter Variable pour Flutter
- Optimisations build Android et nettoyage fichiers temporaires
- Amélioration système de déploiement avec gestion backups
- Ajout scripts CRON et migrations base de données

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-05 20:11:15 +02:00

431 lines
17 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_localizations/flutter_localizations.dart';
import 'package:geosector_app/core/theme/app_theme.dart';
import 'package:geosector_app/core/services/theme_service.dart';
import 'package:go_router/go_router.dart';
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';
// 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();
final themeService = ThemeService.instance;
class GeosectorApp extends StatefulWidget {
const GeosectorApp({super.key});
@override
State<GeosectorApp> createState() => _GeosectorAppState();
}
class _GeosectorAppState extends State<GeosectorApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@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 AnimatedBuilder(
animation: themeService,
builder: (context, child) {
return MaterialApp.router(
title: 'GeoSector',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: themeService.themeMode,
routerConfig: _createRouter(),
debugShowCheckedModeBanner: false,
// Builder pour appliquer le theme responsive à toute l'app
builder: (context, child) {
return LayoutBuilder(
builder: (context, constraints) {
// Récupérer le theme actuel (clair ou sombre)
final brightness = Theme.of(context).brightness;
final textColor = brightness == Brightness.light
? AppTheme.textLightColor
: AppTheme.textDarkColor;
// Débogage en mode développement
final width = constraints.maxWidth;
final scaleFactor = AppTheme.getFontScaleFactor(width);
// Afficher le debug uniquement lors du changement de taille
if (width < AppTheme.breakpointMobileSmall) {
debugPrint('📱 Mode: Très petit mobile (${width.toStringAsFixed(0)}px) → Facteur: ×$scaleFactor');
} else if (width < AppTheme.breakpointMobile) {
debugPrint('📱 Mode: Mobile (${width.toStringAsFixed(0)}px) → Facteur: ×$scaleFactor');
} else if (width < AppTheme.breakpointTablet) {
debugPrint('📱 Mode: Tablette (${width.toStringAsFixed(0)}px) → Facteur: ×$scaleFactor');
} else {
debugPrint('🖥️ Mode: Desktop (${width.toStringAsFixed(0)}px) → Facteur: ×$scaleFactor');
}
// Appliquer le TextTheme responsive
return Theme(
data: Theme.of(context).copyWith(
textTheme: AppTheme.getResponsiveTextTheme(context, textColor),
),
child: child ?? const SizedBox.shrink(),
);
},
);
},
// Configuration des localisations pour le français
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(
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();
},
),
],
),
],
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'),
),
],
),
),
),
);
},
);
}
}