refactor: Remplacement de l'accès direct à Firestore par des Cloud Functions

Migration complète du backend pour utiliser des Cloud Functions comme couche API sécurisée, en remplacement des appels directs à Firestore depuis le client.

**Backend (Cloud Functions):**
- **Centralisation CORS :** Ajout d'un middleware `withCors` et d'une configuration `httpOptions` pour gérer uniformément les en-têtes CORS et les requêtes `OPTIONS` sur toutes les fonctions.
- **Nouvelles Fonctions de Lecture (GET) :**
    - `getEquipments`, `getContainers`, `getEvents`, `getUsers`, `getOptions`, `getEventTypes`, `getRoles`, `getMaintenances`, `getAlerts`.
    - Ces fonctions gèrent les permissions côté serveur, masquant les données sensibles (ex: prix des équipements) pour les utilisateurs non-autorisés.
    - `getEvents` retourne également une map des utilisateurs (`usersMap`) pour optimiser le chargement des données de la main d'œuvre.
- **Nouvelle Fonction de Recherche :**
    - `getContainersByEquipment` : Endpoint dédié pour trouver efficacement tous les containers qui contiennent un équipement spécifique.
- **Nouvelles Fonctions d'Écriture (CRUD) :**
    - Fonctions CRUD complètes pour `eventTypes` (`create`, `update`, `delete`), incluant la validation (unicité du nom, vérification des événements futurs avant suppression).
- **Mise à jour de Fonctions Existantes :**
    - Toutes les fonctions CRUD existantes (`create/update/deleteEquipment`, `create/update/deleteContainer`, etc.) sont wrappées avec le nouveau gestionnaire CORS.

**Frontend (Flutter):**
- **Introduction du `DataService` :** Nouveau service centralisant tous les appels aux Cloud Functions, servant d'intermédiaire entre l'UI/Providers et l'API.
- **Refactorisation des Providers :**
    - `EquipmentProvider`, `ContainerProvider`, `EventProvider`, `UsersProvider`, `MaintenanceProvider` et `AlertProvider` ont été refactorisés pour utiliser le `DataService` au lieu d'accéder directement à Firestore.
    - Les `Stream` Firestore sont remplacés par des chargements de données via des méthodes `Future` (`loadEquipments`, `loadEvents`, etc.).
- **Gestion des Relations Équipement-Container :**
    - Le modèle `EquipmentModel` ne stocke plus `parentBoxIds`.
    - La relation est maintenant gérée par le `ContainerModel` qui contient `equipmentIds`.
    - Le `ContainerEquipmentService` est introduit pour utiliser la nouvelle fonction `getContainersByEquipment`.
    - L'affichage des boîtes parentes (`EquipmentParentContainers`) et le formulaire d'équipement (`EquipmentFormPage`) ont été mis à jour pour refléter ce nouveau modèle de données, synchronisant les ajouts/suppressions d'équipements dans les containers.
- **Amélioration de l'UI :**
    - Nouveau widget `ParentBoxesSelector` pour une sélection améliorée et visuelle des boîtes parentes dans le formulaire d'équipement.
    - Refonte visuelle de `EquipmentParentContainers` pour une meilleure présentation.
This commit is contained in:
ElPoyo
2026-01-12 20:38:46 +01:00
parent 13a890606d
commit f38d75362c
46 changed files with 3367 additions and 1510 deletions

View File

@@ -6,8 +6,10 @@ import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/models/container_model.dart';
import 'package:em2rp/providers/equipment_provider.dart';
import 'package:em2rp/providers/container_provider.dart';
import 'package:em2rp/services/event_preparation_service.dart';
import 'package:em2rp/services/event_preparation_service_extended.dart';
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/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';
@@ -34,9 +36,8 @@ class EventPreparationPage extends StatefulWidget {
}
class _EventPreparationPageState extends State<EventPreparationPage> with SingleTickerProviderStateMixin {
final EventPreparationService _preparationService = EventPreparationService();
final EventPreparationServiceExtended _extendedService = EventPreparationServiceExtended();
late AnimationController _animationController;
late final DataService _dataService;
Map<String, EquipmentModel> _equipmentCache = {};
Map<String, ContainerModel> _containerCache = {};
@@ -89,6 +90,7 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
void initState() {
super.initState();
_currentEvent = widget.initialEvent;
_dataService = DataService(FirebaseFunctionsApiService());
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
@@ -131,24 +133,6 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
super.dispose();
}
/// Recharger l'événement depuis Firestore
Future<void> _reloadEvent() async {
try {
final doc = await FirebaseFirestore.instance
.collection('events')
.doc(_currentEvent.id)
.get();
if (doc.exists) {
setState(() {
_currentEvent = EventModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
});
}
} catch (e) {
print('[EventPreparationPage] Error reloading event: $e');
}
}
Future<void> _loadEquipmentAndContainers() async {
setState(() => _isLoading = true);
@@ -293,11 +277,15 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
break;
}
// Sauvegarder dans Firestore
await FirebaseFirestore.instance
.collection('events')
.doc(_currentEvent.id)
.update(updateData);
// Sauvegarder dans Firestore via l'API
await _dataService.updateEventEquipment(
eventId: _currentEvent.id,
assignedEquipment: updatedEquipment.map((e) => e.toMap()).toList(),
preparationStatus: updateData['preparationStatus'],
loadingStatus: updateData['loadingStatus'],
unloadingStatus: updateData['unloadingStatus'],
returnStatus: updateData['returnStatus'],
);
// Mettre à jour les statuts des équipements si nécessaire
if (_currentStep == PreparationStep.preparation ||
@@ -305,6 +293,14 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
await _updateEquipmentStatuses(updatedEquipment);
}
// Recharger l'événement depuis le provider
final eventProvider = context.read<EventProvider>();
// Recharger la liste des événements pour rafraîchir les données
final userId = context.read<LocalUserProvider>().uid;
if (userId != null) {
await eventProvider.loadUserEvents(userId, canViewAllEvents: true);
}
setState(() => _showSuccessAnimation = true);
_animationController.forward();
@@ -338,52 +334,37 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
Future<void> _updateEquipmentStatuses(List<EventEquipment> equipment) async {
for (var eq in equipment) {
try {
final doc = await FirebaseFirestore.instance
.collection('equipments')
.doc(eq.equipmentId)
.get();
final equipmentData = _equipmentCache[eq.equipmentId];
if (equipmentData == null) continue;
if (doc.exists) {
final equipmentData = EquipmentModel.fromMap(
doc.data() as Map<String, dynamic>,
doc.id,
// Déterminer le nouveau statut
EquipmentStatus newStatus;
if (eq.isReturned) {
newStatus = EquipmentStatus.available;
} else if (eq.isPrepared || eq.isLoaded) {
newStatus = EquipmentStatus.inUse;
} else {
continue; // Pas de changement
}
// Ne mettre à jour que les équipements non quantifiables
if (!equipmentData.hasQuantity) {
await _dataService.updateEquipmentStatusOnly(
equipmentId: eq.equipmentId,
status: equipmentStatusToString(newStatus),
);
}
// Déterminer le nouveau statut
EquipmentStatus newStatus;
if (eq.isReturned) {
newStatus = EquipmentStatus.available;
} else if (eq.isPrepared || eq.isLoaded) {
newStatus = EquipmentStatus.inUse;
} else {
continue; // Pas de changement
}
// Ne mettre à jour que les équipements non quantifiables
if (!equipmentData.hasQuantity) {
await FirebaseFirestore.instance
.collection('equipments')
.doc(eq.equipmentId)
.update({
'status': equipmentStatusToString(newStatus),
'updatedAt': Timestamp.fromDate(DateTime.now()),
});
}
// Gérer les stocks pour les consommables
if (equipmentData.hasQuantity && eq.isReturned && eq.returnedQuantity != null) {
final currentAvailable = equipmentData.availableQuantity ?? 0;
await FirebaseFirestore.instance
.collection('equipments')
.doc(eq.equipmentId)
.update({
'availableQuantity': currentAvailable + eq.returnedQuantity!,
'updatedAt': Timestamp.fromDate(DateTime.now()),
});
}
// Gérer les stocks pour les consommables
if (equipmentData.hasQuantity && eq.isReturned && eq.returnedQuantity != null) {
final currentAvailable = equipmentData.availableQuantity ?? 0;
await _dataService.updateEquipmentStatusOnly(
equipmentId: eq.equipmentId,
availableQuantity: currentAvailable + eq.returnedQuantity!,
);
}
} catch (e) {
print('Error updating equipment status for ${eq.equipmentId}: $e');
// Erreur silencieuse pour ne pas bloquer le processus
}
}
}