feat: Ajout de la gestion des utilisateurs et optimisation du chargement des données

Cette mise à jour introduit la gestion complète des utilisateurs (création, mise à jour, suppression) via des Cloud Functions et optimise de manière significative le chargement des données dans toute l'application.

**Features :**
- **Gestion des utilisateurs (Backend & Frontend) :**
    - Ajout des Cloud Functions `getUser`, `updateUser` et `deleteUser` pour gérer les utilisateurs de manière sécurisée, en respectant les permissions des rôles.
    - L'authentification passe désormais par `onCall` pour plus de sécurité.
- **Optimisation du chargement des données :**
    - Introduction de nouvelles Cloud Functions `getEquipmentsByIds` et `getContainersByIds` pour récupérer uniquement les documents nécessaires, réduisant ainsi la charge sur le client et Firestore.
    - Les fournisseurs (`EquipmentProvider`, `ContainerProvider`) ont été refactorisés pour utiliser un chargement à la demande (`ensureLoaded`) et mettre en cache les données récupérées.
    - Les écrans de détails et de préparation d'événements n'utilisent plus de `Stream` globaux, mais chargent les équipements et boites spécifiques via ces nouvelles fonctions, améliorant considérablement les performances.

**Refactorisation et Améliorations :**
- **Backend (Cloud Functions) :**
    - Le service de vérification de disponibilité (`checkEquipmentAvailability`) est désormais une Cloud Function, déplaçant la logique métier côté serveur.
    - La récupération des données (utilisateurs, événements, alertes) a été centralisée derrière des Cloud Functions, remplaçant les appels directs à Firestore depuis le client.
    - Amélioration de la sérialisation des données (timestamps, références) dans les réponses des fonctions.
- **Frontend (Flutter) :**
    - `LocalUserProvider` charge désormais les informations de l'utilisateur connecté via la fonction `getCurrentUser`, incluant son rôle et ses permissions en un seul appel.
    - `AlertProvider` utilise des fonctions pour charger et manipuler les alertes, abandonnant le `Stream` Firestore.
    - `EventAvailabilityService` utilise maintenant la Cloud Function `checkEquipmentAvailability` au lieu d'une logique client complexe.
    - Correction de la gestion des références de rôles (`roles/ADMIN`) et des `DocumentReference` pour les utilisateurs dans l'ensemble de l'application.
    - Le service d'export ICS (`IcsExportService`) a été simplifié, partant du principe que les données nécessaires (utilisateurs, options) sont déjà chargées dans l'application.
This commit is contained in:
ElPoyo
2026-01-13 01:40:28 +01:00
parent f38d75362c
commit 2bcd1ca4c3
16 changed files with 916 additions and 374 deletions

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/models/container_model.dart';
@@ -10,6 +9,7 @@ import 'package:em2rp/providers/event_provider.dart';
import 'package:em2rp/providers/local_user_provider.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
import 'package:em2rp/services/api_service.dart';
import 'package:em2rp/views/widgets/equipment/equipment_checklist_item.dart' show EquipmentChecklistItem, ChecklistStep;
import 'package:em2rp/views/widgets/equipment/missing_equipment_dialog.dart';
import 'package:em2rp/utils/colors.dart';
@@ -96,7 +96,7 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
duration: const Duration(milliseconds: 500),
);
// Vérification de sécurité : bloquer l'accès si toutes les étapes sont complétées
// Vérification de sécurité et chargement après le premier frame
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_isCurrentStepCompleted()) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -108,9 +108,10 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
Navigator.of(context).pop();
return;
}
});
_loadEquipmentAndContainers();
// Charger les équipements après le premier frame pour éviter setState pendant build
_loadEquipmentAndContainers();
});
}
/// Vérifie si l'étape actuelle est déjà complétée
@@ -140,6 +141,10 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
final equipmentProvider = context.read<EquipmentProvider>();
final containerProvider = context.read<ContainerProvider>();
// S'assurer que les équipements sont chargés
await equipmentProvider.ensureLoaded();
await containerProvider.ensureLoaded();
final equipment = await equipmentProvider.equipmentStream.first;
final containers = await containerProvider.containersStream.first;

View File

@@ -104,10 +104,26 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
@override
void initState() {
super.initState();
_initializeAlreadyAssigned();
_loadAvailableQuantities();
_loadEquipmentConflicts();
_loadContainerConflicts();
// Charger après le premier frame pour éviter setState pendant build
WidgetsBinding.instance.addPostFrameCallback((_) {
_ensureEquipmentsLoaded();
_initializeAlreadyAssigned();
_loadAvailableQuantities();
_loadEquipmentConflicts();
_loadContainerConflicts();
});
}
/// S'assure que les équipements sont chargés avant d'utiliser le dialog
Future<void> _ensureEquipmentsLoaded() async {
final equipmentProvider = context.read<EquipmentProvider>();
final containerProvider = context.read<ContainerProvider>();
await equipmentProvider.ensureLoaded();
// Charger aussi les conteneurs si nécessaire
if (!containerProvider.isLoading && containerProvider.containers.isEmpty) {
await containerProvider.loadContainers();
}
}
/// Initialise la sélection avec le matériel déjà assigné

View File

@@ -63,9 +63,16 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
final equipmentProvider = context.read<EquipmentProvider>();
final containerProvider = context.read<ContainerProvider>();
// Charger depuis les streams
final equipment = await equipmentProvider.equipmentStream.first;
final containers = await containerProvider.containersStream.first;
// Extraire les IDs des équipements assignés
final equipmentIds = widget.assignedEquipment
.map((eq) => eq.equipmentId)
.toList();
// Charger UNIQUEMENT les équipements nécessaires (optimisé)
final equipment = await equipmentProvider.getEquipmentsByIds(equipmentIds);
// Charger UNIQUEMENT les conteneurs nécessaires (optimisé)
final containers = await containerProvider.getContainersByIds(widget.assignedContainers);
// Créer le cache des équipements
for (var eq in widget.assignedEquipment) {

View File

@@ -42,9 +42,17 @@ class _EditUserDialogState extends State<EditUserDialog> {
availableRoles = rolesData
.map((data) => RoleModel.fromMap(data, data['id'] as String))
.toList();
selectedRoleId = widget.user.role.isEmpty
? (availableRoles.isNotEmpty ? availableRoles.first.id : null)
// Extraire l'ID du rôle depuis le path "roles/ADMIN" -> "ADMIN"
String roleId = widget.user.role.isEmpty
? (availableRoles.isNotEmpty ? availableRoles.first.id : '')
: widget.user.role;
// Si le rôle contient un slash, extraire seulement l'ID
if (roleId.contains('/')) {
roleId = roleId.split('/').last;
}
selectedRoleId = roleId;
isLoadingRoles = false;
});
} catch (e) {

View File

@@ -73,6 +73,14 @@ class _UserCardState extends State<UserCard> {
}
}
/// Extrait le nom du rôle depuis le path "roles/ADMIN" -> "ADMIN"
String _extractRoleName(String rolePath) {
if (rolePath.contains('/')) {
return rolePath.split('/').last;
}
return rolePath;
}
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
@@ -178,7 +186,7 @@ class _UserCardState extends State<UserCard> {
if (widget.user.role.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
widget.user.role,
_extractRoleName(widget.user.role),
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: AppColors.gris,
fontSize: 11,