- 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>
431 lines
17 KiB
Dart
Executable File
431 lines
17 KiB
Dart
Executable File
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'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|