refactor: Amélioration des performances et migration des Cloud Functions
Cette mise à jour majeure vise à améliorer significativement les performances de l'application, en particulier au démarrage, et à standardiser l'infrastructure backend. Les principaux changements incluent la migration de toutes les Cloud Functions vers une région européenne (`europe-west9`), l'optimisation du chargement des données, et l'introduction d'un moniteur de performance pour le débogage.
**Changements Backend (Cloud Functions) :**
- **Migration de la Région :**
- Toutes les Cloud Functions ont été déplacées de `us-central1` à `europe-west9` (Paris) pour réduire la latence pour les utilisateurs européens. Cela concerne les appels depuis le frontend (ex: `api_config.dart`, `email_service.dart`) et les définitions des fonctions elles-mêmes (`index.js`, etc.).
- **Standardisation des Fonctions :**
- La plupart des fonctions `onCall` (v1) ont été migrées vers le format `onRequest` (v2) avec une gestion d'authentification et de CORS unifiée, améliorant la robustesse et la cohérence.
- Les triggers Firestore (`onDocumentCreated`, `onDocumentUpdated`) et les tâches planifiées (`onSchedule`) ont été mis à jour pour spécifier explicitement la région `europe-west9`.
- **Mise à jour des Index Firestore :**
- Les index `firestore.indexes.json` ont été mis à jour pour supporter les nouvelles requêtes de l'application et optimiser les performances de filtrage.
**Améliorations des Performances Frontend :**
- **Chargement Asynchrone et Mis en Cache :**
- Le chargement des données utilisateur (`LocalUserProvider`) et des événements (`EventProvider`) a été optimisé pour utiliser un cache local à court terme (5 minutes pour l'utilisateur, 30 secondes pour les événements).
- Les données ne sont rechargées que si le cache a expiré ou si un rechargement est forcé, évitant des appels réseau redondants et accélérant la navigation.
- **Démarrage de l'Application Optimisé :**
- Le processus de connexion automatique (`main.dart`) a été revu. L'application navigue désormais immédiatement vers la page demandée sans attendre la fin du chargement des données utilisateur, qui s'effectue en arrière-plan.
- Un écran de chargement plus esthétique avec le logo de l'entreprise a été ajouté, remplaçant l'indicateur de chargement simple.
- **Chargement de la Page Calendrier :**
- Le chargement et la sélection de l'événement par défaut sur la page `CalendarPage` sont maintenant entièrement asynchrones, rendant l'affichage de la page quasi instantané.
**Nouveaux Outils et Améliorations UX :**
- **Moniteur de Performance :**
- Ajout d'un nouvel outil `PerformanceMonitor` (`lib/utils/performance_monitor.dart`) pour mesurer précisément le temps d'exécution des opérations critiques (appels API, parsing, etc.) en mode débogage. Il aide à identifier les goulots d'étranglement.
- **Amélioration du Formulaire de Connexion :**
- Les champs "Email" et "Mot de passe" sur la page de connexion (`LoginPage`) supportent désormais l'autocomplétion du navigateur (`AutofillGroup`).
- Appuyer sur "Entrée" dans l'un des champs déclenche désormais la connexion, améliorant l'ergonomie.
**Mise à jour de la version :**
- La version de l'application a été incrémentée à `1.0.9`.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'package:em2rp/providers/local_user_provider.dart';
|
||||
import 'package:em2rp/providers/event_provider.dart';
|
||||
import 'package:em2rp/utils/performance_monitor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
||||
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
|
||||
@@ -35,52 +36,75 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
initializeDateFormatting('fr_FR', null);
|
||||
Future.microtask(() => _loadEvents());
|
||||
// Sélection automatique de l'événement le plus proche de maintenant
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
||||
final events = eventProvider.events;
|
||||
if (events.isNotEmpty) {
|
||||
final now = DateTime.now();
|
||||
// Pour mobile : sélectionner le premier événement du jour ou le prochain événement à venir
|
||||
final todayEvents = events
|
||||
.where((e) =>
|
||||
e.startDateTime.year == now.year &&
|
||||
e.startDateTime.month == now.month &&
|
||||
e.startDateTime.day == now.day)
|
||||
.toList()
|
||||
..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||
EventModel? selected;
|
||||
DateTime? selectedDay;
|
||||
if (todayEvents.isNotEmpty) {
|
||||
selected = todayEvents[0];
|
||||
selectedDay = DateTime(now.year, now.month, now.day);
|
||||
} else {
|
||||
// Chercher le prochain événement à venir
|
||||
final futureEvents = events
|
||||
.where((e) => e.startDateTime.isAfter(now))
|
||||
.toList()
|
||||
..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||
if (futureEvents.isNotEmpty) {
|
||||
selected = futureEvents[0];
|
||||
selectedDay = DateTime(selected.startDateTime.year,
|
||||
selected.startDateTime.month, selected.startDateTime.day);
|
||||
} else {
|
||||
// Aucun événement à venir, prendre le plus proche dans le passé
|
||||
events.sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||
selected = events.last;
|
||||
selectedDay = DateTime(selected.startDateTime.year,
|
||||
selected.startDateTime.month, selected.startDateTime.day);
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_selectedDay = selectedDay;
|
||||
_focusedDay = selectedDay!;
|
||||
_selectedEventIndex = 0;
|
||||
_selectedEvent = selected;
|
||||
});
|
||||
// Charger les événements de manière asynchrone sans bloquer l'UI
|
||||
_loadEventsAsync();
|
||||
}
|
||||
|
||||
/// Charge les événements de manière asynchrone et sélectionne l'événement approprié
|
||||
Future<void> _loadEventsAsync() async {
|
||||
PerformanceMonitor.start('CalendarPage.loadEventsAsync');
|
||||
await _loadEvents();
|
||||
|
||||
// Sélectionner l'événement approprié après le chargement
|
||||
if (mounted) {
|
||||
PerformanceMonitor.start('CalendarPage.selectDefaultEvent');
|
||||
_selectDefaultEvent();
|
||||
PerformanceMonitor.end('CalendarPage.selectDefaultEvent');
|
||||
}
|
||||
PerformanceMonitor.end('CalendarPage.loadEventsAsync');
|
||||
}
|
||||
|
||||
/// Sélectionne automatiquement l'événement le plus proche de maintenant
|
||||
void _selectDefaultEvent() {
|
||||
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
||||
final events = eventProvider.events;
|
||||
|
||||
if (events.isEmpty) return;
|
||||
|
||||
final now = DateTime.now();
|
||||
|
||||
// Trouver les événements d'aujourd'hui
|
||||
final todayEvents = events.where((e) {
|
||||
final start = e.startDateTime;
|
||||
return start.year == now.year &&
|
||||
start.month == now.month &&
|
||||
start.day == now.day;
|
||||
}).toList()..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||
|
||||
EventModel? selected;
|
||||
DateTime? selectedDay;
|
||||
|
||||
if (todayEvents.isNotEmpty) {
|
||||
selected = todayEvents[0];
|
||||
selectedDay = DateTime(now.year, now.month, now.day);
|
||||
} else {
|
||||
// Chercher le prochain événement à venir
|
||||
final futureEvents = events
|
||||
.where((e) => e.startDateTime.isAfter(now))
|
||||
.toList()..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||
|
||||
if (futureEvents.isNotEmpty) {
|
||||
selected = futureEvents[0];
|
||||
final start = selected.startDateTime;
|
||||
selectedDay = DateTime(start.year, start.month, start.day);
|
||||
} else {
|
||||
// Aucun événement à venir, prendre le plus récent
|
||||
final sortedEvents = events.toList()
|
||||
..sort((a, b) => b.startDateTime.compareTo(a.startDateTime));
|
||||
selected = sortedEvents.first;
|
||||
final start = selected.startDateTime;
|
||||
selectedDay = DateTime(start.year, start.month, start.day);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_selectedDay = selectedDay;
|
||||
_focusedDay = selectedDay!;
|
||||
_selectedEventIndex = 0;
|
||||
_selectedEvent = selected;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadEvents() async {
|
||||
|
||||
@@ -430,7 +430,7 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
||||
};
|
||||
}).toList();
|
||||
|
||||
final result = await FirebaseFunctions.instanceFor(region: 'us-central1')
|
||||
final result = await FirebaseFunctions.instanceFor(region: 'europe-west9')
|
||||
.httpsCallable('processEquipmentValidation')
|
||||
.call({
|
||||
'eventId': _currentEvent.id,
|
||||
|
||||
@@ -58,41 +58,45 @@ class LoginPage extends StatelessWidget {
|
||||
Widget _buildLoginForm(BuildContext context) {
|
||||
return Consumer<LoginViewModel>(
|
||||
builder: (context, loginViewModel, child) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
const LogoWidget(),
|
||||
const SizedBox(height: 30),
|
||||
const WelcomeTextWidget(),
|
||||
const SizedBox(height: 40),
|
||||
EmailTextFieldWidget(
|
||||
emailController: loginViewModel.emailController,
|
||||
highlightEmailField: loginViewModel.highlightEmailField,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
PasswordTextFieldWidget(
|
||||
passwordController: loginViewModel.passwordController,
|
||||
obscurePassword: loginViewModel.obscurePassword,
|
||||
highlightPasswordField: loginViewModel.highlightPasswordField,
|
||||
onTogglePasswordVisibility:
|
||||
loginViewModel.togglePasswordVisibility,
|
||||
),
|
||||
ForgotPasswordButtonWidget(
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) =>
|
||||
const ForgotPasswordDialogWidget(),
|
||||
return AutofillGroup(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
const LogoWidget(),
|
||||
const SizedBox(height: 30),
|
||||
const WelcomeTextWidget(),
|
||||
const SizedBox(height: 40),
|
||||
EmailTextFieldWidget(
|
||||
emailController: loginViewModel.emailController,
|
||||
highlightEmailField: loginViewModel.highlightEmailField,
|
||||
onSubmitted: () => loginViewModel.signIn(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
LoginButtonWidget(
|
||||
isLoading: loginViewModel.isLoading,
|
||||
onPressed: () => loginViewModel.signIn(context),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ErrorMessageWidget(errorMessage: loginViewModel.errorMessage),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
PasswordTextFieldWidget(
|
||||
passwordController: loginViewModel.passwordController,
|
||||
obscurePassword: loginViewModel.obscurePassword,
|
||||
highlightPasswordField: loginViewModel.highlightPasswordField,
|
||||
onTogglePasswordVisibility:
|
||||
loginViewModel.togglePasswordVisibility,
|
||||
onSubmitted: () => loginViewModel.signIn(context),
|
||||
),
|
||||
ForgotPasswordButtonWidget(
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) =>
|
||||
const ForgotPasswordDialogWidget(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
LoginButtonWidget(
|
||||
isLoading: loginViewModel.isLoading,
|
||||
onPressed: () => loginViewModel.signIn(context),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ErrorMessageWidget(errorMessage: loginViewModel.errorMessage),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -4,11 +4,13 @@ import 'package:flutter/material.dart';
|
||||
class EmailTextFieldWidget extends StatelessWidget {
|
||||
final TextEditingController emailController;
|
||||
final bool highlightEmailField;
|
||||
final VoidCallback? onSubmitted;
|
||||
|
||||
const EmailTextFieldWidget({
|
||||
super.key,
|
||||
required this.emailController,
|
||||
required this.highlightEmailField,
|
||||
this.onSubmitted,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -16,6 +18,9 @@ class EmailTextFieldWidget extends StatelessWidget {
|
||||
return TextField(
|
||||
controller: emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints: const [AutofillHints.email, AutofillHints.username],
|
||||
textInputAction: TextInputAction.next,
|
||||
onSubmitted: (_) => onSubmitted?.call(),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(
|
||||
|
||||
@@ -7,6 +7,7 @@ class PasswordTextFieldWidget extends StatelessWidget {
|
||||
final bool obscurePassword;
|
||||
final bool highlightPasswordField;
|
||||
final VoidCallback onTogglePasswordVisibility;
|
||||
final VoidCallback? onSubmitted;
|
||||
|
||||
const PasswordTextFieldWidget({
|
||||
super.key,
|
||||
@@ -14,6 +15,7 @@ class PasswordTextFieldWidget extends StatelessWidget {
|
||||
required this.obscurePassword,
|
||||
required this.highlightPasswordField,
|
||||
required this.onTogglePasswordVisibility,
|
||||
this.onSubmitted,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -21,6 +23,9 @@ class PasswordTextFieldWidget extends StatelessWidget {
|
||||
return TextField(
|
||||
controller: passwordController,
|
||||
obscureText: obscurePassword,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => onSubmitted?.call(),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Mot de passe',
|
||||
border: OutlineInputBorder(
|
||||
|
||||
Reference in New Issue
Block a user