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:
@@ -77,7 +77,7 @@ class EventDetails extends StatelessWidget {
|
||||
EventDetailsDescription(event: event),
|
||||
EventDetailsDocuments(documents: event.documents),
|
||||
const SizedBox(height: 16),
|
||||
EventDetailsEquipe(workforce: event.workforce),
|
||||
EventDetailsEquipe(event: event),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:em2rp/models/user_model.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:em2rp/providers/event_provider.dart';
|
||||
import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart';
|
||||
|
||||
class EventDetailsEquipe extends StatelessWidget {
|
||||
final List workforce;
|
||||
final EventModel event;
|
||||
|
||||
const EventDetailsEquipe({
|
||||
super.key,
|
||||
required this.workforce,
|
||||
required this.event,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (workforce.isEmpty) {
|
||||
if (event.workforce.isEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -33,105 +35,48 @@ class EventDetailsEquipe extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return FutureBuilder<List<UserModel>>(
|
||||
future: _fetchUsers(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Equipe',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
// Récupérer les utilisateurs depuis le cache du provider
|
||||
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
||||
final workforceUsers = eventProvider.getWorkforceUsers(event);
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Equipe',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
// Convertir en UserModel
|
||||
final users = workforceUsers.map((userData) {
|
||||
return UserModel(
|
||||
uid: userData['uid'] ?? '',
|
||||
firstName: userData['firstName'] ?? '',
|
||||
lastName: userData['lastName'] ?? '',
|
||||
email: userData['email'] ?? '',
|
||||
phoneNumber: userData['phoneNumber'] ?? '',
|
||||
profilePhotoUrl: userData['profilePhotoUrl'] ?? '',
|
||||
role: '', // Pas besoin du rôle pour l'affichage
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Equipe',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Text(
|
||||
snapshot.error.toString().contains('permission-denied')
|
||||
? "Vous n'avez pas la permission de voir tous les membres de l'équipe."
|
||||
: "Erreur lors du chargement de l'équipe : ${snapshot.error}",
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (users.isEmpty)
|
||||
Text(
|
||||
'Aucun membre assigné.',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.orange[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final users = snapshot.data ?? [];
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Equipe',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (users.isEmpty)
|
||||
Text(
|
||||
'Aucun membre assigné ou erreur de chargement.',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.orange[700],
|
||||
),
|
||||
),
|
||||
if (users.isNotEmpty)
|
||||
UserChipsList(
|
||||
users: users,
|
||||
showRemove: false,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
if (users.isNotEmpty)
|
||||
UserChipsList(
|
||||
users: users,
|
||||
showRemove: false,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<UserModel>> _fetchUsers() async {
|
||||
final firestore = FirebaseFirestore.instance;
|
||||
List<UserModel> users = [];
|
||||
|
||||
for (int i = 0; i < workforce.length; i++) {
|
||||
final ref = workforce[i];
|
||||
try {
|
||||
if (ref is DocumentReference) {
|
||||
final doc = await firestore.doc(ref.path).get();
|
||||
if (doc.exists) {
|
||||
final userData = doc.data() as Map<String, dynamic>;
|
||||
users.add(UserModel.fromMap(userData, doc.id));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Log silencieux des erreurs individuelles
|
||||
debugPrint('Error fetching user $i: $e');
|
||||
}
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:em2rp/providers/local_user_provider.dart';
|
||||
import 'package:em2rp/views/event_add_page.dart';
|
||||
import 'package:em2rp/services/ics_export_service.dart';
|
||||
import 'package:em2rp/services/data_service.dart';
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
import 'dart:html' as html;
|
||||
import 'dart:convert';
|
||||
|
||||
@@ -53,24 +54,21 @@ class _EventDetailsHeaderState extends State<EventDetailsHeader> {
|
||||
return;
|
||||
}
|
||||
|
||||
final doc = await FirebaseFirestore.instance
|
||||
.collection('eventTypes')
|
||||
.doc(widget.event.eventTypeId)
|
||||
.get();
|
||||
// Charger tous les types d'événements via l'API
|
||||
final dataService = DataService(FirebaseFunctionsApiService());
|
||||
final eventTypes = await dataService.getEventTypes();
|
||||
|
||||
if (doc.exists) {
|
||||
setState(() {
|
||||
_eventTypeName = doc.data()?['name'] as String? ?? widget.event.eventTypeId;
|
||||
_isLoadingEventType = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_eventTypeName = widget.event.eventTypeId;
|
||||
_isLoadingEventType = false;
|
||||
});
|
||||
}
|
||||
// Trouver le type correspondant
|
||||
final eventType = eventTypes.firstWhere(
|
||||
(type) => type['id'] == widget.event.eventTypeId,
|
||||
orElse: () => <String, dynamic>{},
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_eventTypeName = eventType['name'] as String? ?? widget.event.eventTypeId;
|
||||
_isLoadingEventType = false;
|
||||
});
|
||||
} catch (e) {
|
||||
print('Erreur lors du chargement du type d\'événement: $e');
|
||||
setState(() {
|
||||
_eventTypeName = widget.event.eventTypeId;
|
||||
_isLoadingEventType = false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:em2rp/providers/event_provider.dart';
|
||||
import 'package:em2rp/views/event_preparation_page.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
|
||||
@@ -20,30 +21,19 @@ class EventPreparationButtons extends StatefulWidget {
|
||||
class _EventPreparationButtonsState extends State<EventPreparationButtons> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Écouter les changements de l'événement en temps réel
|
||||
return StreamBuilder<DocumentSnapshot>(
|
||||
stream: FirebaseFirestore.instance
|
||||
.collection('events')
|
||||
.doc(widget.event.id)
|
||||
.snapshots(),
|
||||
initialData: null,
|
||||
builder: (context, snapshot) {
|
||||
// Utiliser l'événement du stream si disponible, sinon l'événement initial
|
||||
final EventModel currentEvent;
|
||||
if (snapshot.hasData && snapshot.data != null && snapshot.data!.exists) {
|
||||
currentEvent = EventModel.fromMap(
|
||||
snapshot.data!.data() as Map<String, dynamic>,
|
||||
snapshot.data!.id,
|
||||
);
|
||||
} else {
|
||||
currentEvent = widget.event;
|
||||
}
|
||||
// Utiliser le provider pour récupérer l'événement à jour
|
||||
final eventProvider = context.watch<EventProvider>();
|
||||
|
||||
return _buildButtons(context, currentEvent);
|
||||
},
|
||||
// Chercher l'événement mis à jour dans le provider
|
||||
final EventModel currentEvent = eventProvider.events.firstWhere(
|
||||
(e) => e.id == widget.event.id,
|
||||
orElse: () => widget.event,
|
||||
);
|
||||
|
||||
return _buildButtons(context, currentEvent);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildButtons(BuildContext context, EventModel event) {
|
||||
// Vérifier s'il y a du matériel assigné
|
||||
final hasMaterial = event.assignedEquipment.isNotEmpty || event.assignedContainers.isNotEmpty;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:em2rp/providers/event_provider.dart';
|
||||
import 'package:em2rp/services/data_service.dart';
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
|
||||
class EventStatusButton extends StatefulWidget {
|
||||
final EventModel event;
|
||||
@@ -22,30 +23,37 @@ class EventStatusButton extends StatefulWidget {
|
||||
|
||||
class _EventStatusButtonState extends State<EventStatusButton> {
|
||||
bool _loading = false;
|
||||
final DataService _dataService = DataService(FirebaseFunctionsApiService());
|
||||
|
||||
Future<void> _changeStatus(EventStatus newStatus) async {
|
||||
if (widget.event.status == newStatus) return;
|
||||
setState(() => _loading = true);
|
||||
|
||||
try {
|
||||
await FirebaseFirestore.instance
|
||||
.collection('events')
|
||||
.doc(widget.event.id)
|
||||
.update({'status': eventStatusToString(newStatus)});
|
||||
// Mettre à jour via l'API
|
||||
await _dataService.updateEvent(widget.event.id, {
|
||||
'status': eventStatusToString(newStatus),
|
||||
});
|
||||
|
||||
final snap = await FirebaseFirestore.instance
|
||||
.collection('events')
|
||||
.doc(widget.event.id)
|
||||
.get();
|
||||
final updatedEvent = EventModel.fromMap(snap.data()!, widget.event.id);
|
||||
|
||||
widget.onSelectEvent(
|
||||
updatedEvent,
|
||||
widget.selectedDate ?? updatedEvent.startDateTime,
|
||||
// Récupérer l'événement mis à jour via l'API
|
||||
final result = await _dataService.getEvents();
|
||||
final eventsList = result['events'] as List<dynamic>;
|
||||
final eventData = eventsList.firstWhere(
|
||||
(e) => e['id'] == widget.event.id,
|
||||
orElse: () => <String, dynamic>{},
|
||||
);
|
||||
|
||||
await Provider.of<EventProvider>(context, listen: false)
|
||||
.updateEvent(updatedEvent);
|
||||
if (eventData.isNotEmpty) {
|
||||
final updatedEvent = EventModel.fromMap(eventData, widget.event.id);
|
||||
|
||||
widget.onSelectEvent(
|
||||
updatedEvent,
|
||||
widget.selectedDate ?? updatedEvent.startDateTime,
|
||||
);
|
||||
|
||||
await Provider.of<EventProvider>(context, listen: false)
|
||||
.updateEvent(updatedEvent);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
||||
Reference in New Issue
Block a user