Cette mise à jour refactorise en profondeur le chargement des événements sur la page Calendrier pour améliorer drastiquement les performances et la réactivité de l'application, en particulier pour les utilisateurs avec un grand nombre d'événements. Le système de chargement initial de tous les événements est remplacé par un mécanisme de lazy loading qui ne récupère que les données du mois affiché.
**Changements majeurs :**
- **Lazy Loading Côté Client (`EventProvider`) :**
- Une nouvelle méthode `loadMonthEvents` a été introduite pour charger uniquement les événements d'un mois spécifique (`year`, `month`).
- Un cache par mois (`_eventsByMonth`) a été mis en place pour éviter les rechargements inutiles lors de la navigation entre des mois déjà consultés.
- Ajout d'une fonction `preloadAdjacentMonths` qui charge en arrière-plan et silencieusement les mois précédent et suivant, assurant une navigation fluide dans le calendrier.
- **Nouveau Endpoint Backend (`getEventsByMonth`) :**
- Création d'un nouvel endpoint Cloud Function `getEventsByMonth` optimisé pour ne requêter que les événements dans une plage de dates (début et fin du mois).
- La fonction récupère les utilisateurs associés de manière optimisée en parallélisant les requêtes Firestore (Promise.all).
- La limite du nombre d'IDs par requête 'in' a été augmentée de 10 à 30 pour réduire le nombre d'appels à la base de données.
- **Intégration au Calendrier (`CalendarPage`) :**
- La page charge désormais les événements pour le mois courant au démarrage via `_loadCurrentMonthEvents`.
- Lorsqu'un utilisateur change de mois (`onPageChanged`), la page déclenche le chargement des données pour le nouveau mois, avec un préchargement des mois adjacents pour anticiper la navigation.
- Le chargement initial de tous les événements (`_loadEventsAsync`) a été déprécié.
- **Correction de la Séquence de Démarrage (`main.dart`) :**
- L'appel à `_autoLogin` est maintenant enveloppé dans `WidgetsBinding.instance.addPostFrameCallback`. Cela garantit que la navigation ne se produit qu'après le premier rendu de l'interface, évitant ainsi des erreurs potentielles de build/navigation concurrentes et fiabilisant le chargement initial des données utilisateur.
302 lines
10 KiB
Dart
302 lines
10 KiB
Dart
import 'package:em2rp/providers/users_provider.dart';
|
|
import 'package:em2rp/providers/event_provider.dart';
|
|
import 'package:em2rp/providers/equipment_provider.dart';
|
|
import 'package:em2rp/providers/container_provider.dart';
|
|
import 'package:em2rp/providers/maintenance_provider.dart';
|
|
import 'package:em2rp/providers/alert_provider.dart';
|
|
import 'package:em2rp/utils/auth_guard_widget.dart';
|
|
import 'package:em2rp/utils/performance_monitor.dart';
|
|
import 'package:em2rp/views/alerts_page.dart';
|
|
import 'package:em2rp/views/calendar_page.dart';
|
|
import 'package:em2rp/views/login_page.dart';
|
|
import 'package:em2rp/views/equipment_management_page.dart';
|
|
import 'package:em2rp/views/container_management_page.dart';
|
|
import 'package:em2rp/views/container_form_page.dart';
|
|
import 'package:em2rp/views/container_detail_page.dart';
|
|
import 'package:em2rp/views/event_preparation_page.dart';
|
|
import 'package:em2rp/models/container_model.dart';
|
|
import 'package:em2rp/models/event_model.dart';
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:firebase_core/firebase_core.dart';
|
|
import 'firebase_options.dart';
|
|
import 'utils/colors.dart';
|
|
import 'views/my_account_page.dart';
|
|
import 'views/user_management_page.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'providers/local_user_provider.dart';
|
|
import 'views/reset_password_page.dart';
|
|
import 'config/env.dart';
|
|
import 'config/api_config.dart';
|
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
|
import 'views/widgets/common/update_dialog.dart';
|
|
|
|
void main() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
await Firebase.initializeApp(
|
|
options: DefaultFirebaseOptions.currentPlatform,
|
|
);
|
|
|
|
// Configuration des émulateurs en mode développement
|
|
if (ApiConfig.isDevelopment) {
|
|
print('🔧 Mode développement activé - Utilisation des émulateurs');
|
|
|
|
// Configurer l'émulateur Auth
|
|
await FirebaseAuth.instance.useAuthEmulator('localhost', 9199);
|
|
print('✓ Auth émulateur configuré: localhost:9199');
|
|
|
|
// Configurer l'émulateur Firestore
|
|
FirebaseFirestore.instance.useFirestoreEmulator('localhost', 8088);
|
|
print('✓ Firestore émulateur configuré: localhost:8088');
|
|
}
|
|
|
|
await FirebaseAuth.instance.setPersistence(Persistence.LOCAL);
|
|
|
|
runApp(
|
|
MultiProvider(
|
|
providers: [
|
|
// LocalUserProvider pour la gestion de l'authentification
|
|
ChangeNotifierProvider<LocalUserProvider>(
|
|
create: (context) => LocalUserProvider()),
|
|
|
|
// UsersProvider migré vers l'API
|
|
ChangeNotifierProvider<UsersProvider>(
|
|
create: (context) => UsersProvider(),
|
|
),
|
|
|
|
// EventProvider migré vers l'API
|
|
ChangeNotifierProvider<EventProvider>(
|
|
create: (context) => EventProvider(),
|
|
),
|
|
|
|
// EquipmentProvider migré vers l'API
|
|
ChangeNotifierProvider<EquipmentProvider>(
|
|
create: (context) => EquipmentProvider(),
|
|
),
|
|
|
|
// ContainerProvider migré vers l'API
|
|
ChangeNotifierProvider<ContainerProvider>(
|
|
create: (context) => ContainerProvider(),
|
|
),
|
|
|
|
// MaintenanceProvider migré vers l'API
|
|
ChangeNotifierProvider<MaintenanceProvider>(
|
|
create: (context) => MaintenanceProvider(),
|
|
),
|
|
ChangeNotifierProvider<AlertProvider>(
|
|
create: (context) => AlertProvider(),
|
|
),
|
|
],
|
|
child: const MyApp(),
|
|
),
|
|
);
|
|
}
|
|
|
|
class MyApp extends StatelessWidget {
|
|
const MyApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return UpdateChecker(
|
|
child: MaterialApp(
|
|
title: 'EM2 Hub',
|
|
theme: ThemeData(
|
|
primarySwatch: Colors.red,
|
|
primaryColor: AppColors.noir,
|
|
colorScheme:
|
|
ColorScheme.fromSwatch().copyWith(secondary: AppColors.rouge),
|
|
textTheme: const TextTheme(
|
|
bodyMedium: TextStyle(color: AppColors.noir),
|
|
),
|
|
inputDecorationTheme: const InputDecorationTheme(
|
|
focusedBorder: OutlineInputBorder(
|
|
borderSide: BorderSide(color: AppColors.noir),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderSide: BorderSide(color: AppColors.gris),
|
|
),
|
|
labelStyle: TextStyle(color: AppColors.noir),
|
|
hintStyle: TextStyle(color: AppColors.gris),
|
|
),
|
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
style: ElevatedButton.styleFrom(
|
|
foregroundColor: AppColors.blanc,
|
|
backgroundColor: AppColors.noir,
|
|
),
|
|
),
|
|
),
|
|
locale: const Locale('fr', 'FR'),
|
|
supportedLocales: const [
|
|
Locale('fr', 'FR'),
|
|
],
|
|
localizationsDelegates: const [
|
|
GlobalMaterialLocalizations.delegate,
|
|
GlobalWidgetsLocalizations.delegate,
|
|
GlobalCupertinoLocalizations.delegate,
|
|
],
|
|
initialRoute: '/',
|
|
routes: {
|
|
'/': (context) => const AutoLoginWrapper(),
|
|
'/login': (context) => const LoginPage(),
|
|
'/alerts': (context) => const AuthGuard(child: AlertsPage()),
|
|
'/calendar': (context) => const AuthGuard(child: CalendarPage()),
|
|
'/my_account': (context) => const AuthGuard(child: MyAccountPage()),
|
|
'/user_management': (context) => const AuthGuard(
|
|
requiredPermission: "view_all_users", child: UserManagementPage()),
|
|
'/reset_password': (context) {
|
|
final args = ModalRoute.of(context)!.settings.arguments
|
|
as Map<String, dynamic>;
|
|
return ResetPasswordPage(
|
|
email: args['email'] as String,
|
|
actionCode: args['actionCode'] as String,
|
|
);
|
|
},
|
|
'/equipment_management': (context) => const AuthGuard(
|
|
requiredPermission: "view_equipment",
|
|
child: EquipmentManagementPage()),
|
|
'/container_management': (context) => const AuthGuard(
|
|
requiredPermission: "view_equipment",
|
|
child: ContainerManagementPage()),
|
|
'/container_form': (context) {
|
|
final args = ModalRoute.of(context)?.settings.arguments;
|
|
return AuthGuard(
|
|
requiredPermission: "manage_equipment",
|
|
child: ContainerFormPage(
|
|
container: args as ContainerModel?,
|
|
),
|
|
);
|
|
},
|
|
'/container_detail': (context) {
|
|
final container = ModalRoute.of(context)!.settings.arguments as ContainerModel;
|
|
return AuthGuard(
|
|
requiredPermission: "view_equipment",
|
|
child: ContainerDetailPage(container: container),
|
|
);
|
|
},
|
|
'/event_preparation': (context) {
|
|
final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
|
|
final event = args['event'] as EventModel;
|
|
return AuthGuard(
|
|
child: EventPreparationPage(
|
|
initialEvent: event,
|
|
),
|
|
);
|
|
},
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AutoLoginWrapper extends StatefulWidget {
|
|
const AutoLoginWrapper({super.key});
|
|
|
|
@override
|
|
State<AutoLoginWrapper> createState() => _AutoLoginWrapperState();
|
|
}
|
|
|
|
class _AutoLoginWrapperState extends State<AutoLoginWrapper> {
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// Attendre la fin du premier build avant de naviguer
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_autoLogin();
|
|
});
|
|
}
|
|
|
|
Future<void> _autoLogin() async {
|
|
PerformanceMonitor.start('App.autoLogin');
|
|
try {
|
|
final localAuthProvider =
|
|
Provider.of<LocalUserProvider>(context, listen: false);
|
|
|
|
// Vérifier si l'utilisateur est déjà connecté
|
|
if (FirebaseAuth.instance.currentUser == null && Env.isDevelopment) {
|
|
PerformanceMonitor.start('App.signIn');
|
|
// Connexion automatique en mode développement
|
|
await localAuthProvider.signInWithEmailAndPassword(
|
|
Env.devAdminEmail,
|
|
Env.devAdminPassword,
|
|
);
|
|
PerformanceMonitor.end('App.signIn');
|
|
}
|
|
|
|
if (mounted) {
|
|
// MODIFIÉ : Vérifier si une route spécifique est demandée dans l'URL
|
|
// En Flutter Web, on peut vérifier window.location.hash
|
|
final currentUri = Uri.base;
|
|
final fragment = currentUri.fragment; // Ex: "/alerts" si URL est /#/alerts
|
|
|
|
print('[AutoLoginWrapper] Fragment URL: $fragment');
|
|
|
|
// Navigation immédiate sans attendre le chargement des données
|
|
if (fragment.isNotEmpty && fragment != '/' && fragment != '/calendar') {
|
|
print('[AutoLoginWrapper] Redirection vers: $fragment');
|
|
Navigator.of(context).pushReplacementNamed(fragment);
|
|
} else {
|
|
// Route par défaut : calendrier
|
|
print('[AutoLoginWrapper] Redirection vers: /calendar (défaut)');
|
|
Navigator.of(context).pushReplacementNamed('/calendar');
|
|
}
|
|
|
|
PerformanceMonitor.end('App.autoLogin');
|
|
PerformanceMonitor.printSummary();
|
|
|
|
// Charger les données utilisateur en arrière-plan
|
|
localAuthProvider.loadUserData().catchError((e) {
|
|
print('Error loading user data: $e');
|
|
});
|
|
}
|
|
} catch (e) {
|
|
print('Auto login failed: $e');
|
|
PerformanceMonitor.end('App.autoLogin');
|
|
if (mounted) {
|
|
Navigator.of(context).pushReplacementNamed('/login');
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: Colors.white,
|
|
body: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
// Logo de l'application
|
|
Image.asset(
|
|
'assets/logos/RectangleLogoBlack.png',
|
|
width: 200,
|
|
height: 200,
|
|
fit: BoxFit.contain,
|
|
errorBuilder: (context, error, stackTrace) {
|
|
return const Icon(
|
|
Icons.event_available,
|
|
size: 80,
|
|
color: AppColors.rouge,
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 40),
|
|
const CircularProgressIndicator(
|
|
valueColor: AlwaysStoppedAnimation<Color>(AppColors.rouge),
|
|
),
|
|
const SizedBox(height: 20),
|
|
const Text(
|
|
'Chargement...',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
color: Colors.grey,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|