From f38d75362c3294917dabf49855320d240d6e4577 Mon Sep 17 00:00:00 2001 From: ElPoyo Date: Mon, 12 Jan 2026 20:38:46 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20Remplacement=20de=20l'acc=C3=A8s=20?= =?UTF-8?q?direct=20=C3=A0=20Firestore=20par=20des=20Cloud=20Functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- em2rp/functions/index.js | 818 +++++++++++++++++- em2rp/functions/utils/helpers.js | 62 +- .../controllers/event_form_controller.dart | 16 +- em2rp/lib/main.dart | 15 +- em2rp/lib/models/alert_model.dart | 7 +- em2rp/lib/models/event_model.dart | 9 +- em2rp/lib/models/event_type_model.dart | 12 +- em2rp/lib/providers/alert_provider_new.dart | 62 ++ em2rp/lib/providers/container_provider.dart | 27 + .../lib/providers/container_provider_new.dart | 109 +++ em2rp/lib/providers/equipment_provider.dart | 315 ++++--- em2rp/lib/providers/event_provider.dart | 160 ++-- .../providers/maintenance_provider_new.dart | 51 ++ em2rp/lib/providers/users_provider.dart | 166 +--- .../services/container_equipment_service.dart | 52 ++ em2rp/lib/services/data_service.dart | 339 ++++++++ em2rp/lib/services/equipment_service.dart | 43 +- em2rp/lib/services/event_form_service.dart | 31 +- em2rp/lib/services/ics_export_service.dart | 29 +- em2rp/lib/services/user_service.dart | 24 +- em2rp/lib/utils/firebase_storage_manager.dart | 20 +- em2rp/lib/views/calendar_page.dart | 3 - em2rp/lib/views/equipment_detail_page.dart | 13 +- em2rp/lib/views/equipment_form_page.dart | 164 ++-- .../lib/views/equipment_management_page.dart | 70 +- em2rp/lib/views/event_preparation_page.dart | 117 ++- em2rp/lib/views/user_management_page.dart | 26 +- .../calendar_widgets/event_details.dart | 2 +- .../event_details_equipe.dart | 143 +-- .../event_details_header.dart | 32 +- .../event_preparation_buttons.dart | 32 +- .../event_status_button.dart | 40 +- .../event_types_management.dart | 179 ++-- .../data_management/options_management.dart | 85 +- .../equipment_associated_events_section.dart | 32 +- .../equipment_current_events_section.dart | 32 +- .../equipment_parent_containers.dart | 369 ++++---- .../equipment_referencing_containers.dart | 374 ++++---- .../equipment/parent_boxes_selector.dart | 445 ++++++++++ .../widgets/equipment/restock_dialog.dart | 11 +- .../event_assigned_equipment_section.dart | 14 +- .../event_options_display_widget.dart | 38 +- .../views/widgets/image/profile_picture.dart | 136 +-- .../inputs/option_selector_widget.dart | 118 ++- .../user_management/edit_user_dialog.dart | 30 +- .../user_multi_select_widget.dart | 5 +- 46 files changed, 3367 insertions(+), 1510 deletions(-) create mode 100644 em2rp/lib/providers/alert_provider_new.dart create mode 100644 em2rp/lib/providers/container_provider_new.dart create mode 100644 em2rp/lib/providers/maintenance_provider_new.dart create mode 100644 em2rp/lib/services/container_equipment_service.dart create mode 100644 em2rp/lib/services/data_service.dart create mode 100644 em2rp/lib/views/widgets/equipment/parent_boxes_selector.dart diff --git a/em2rp/functions/index.js b/em2rp/functions/index.js index d9351fb..ea599e2 100644 --- a/em2rp/functions/index.js +++ b/em2rp/functions/index.js @@ -17,10 +17,54 @@ admin.initializeApp(); const storage = new Storage(); const db = admin.firestore(); +// Configuration commune pour toutes les fonctions HTTP +const httpOptions = { + cors: true, + invoker: 'public', // Permet les invocations non authentifiées (l'auth est gérée par notre token Firebase) + // Version: 2.0 - Ajout de l'invoker public pour résoudre les problèmes CORS +}; + +// ============================================================================ +// CORS Middleware +// ============================================================================ +const setCorsHeaders = (res, req) => { + // Permettre toutes les origines en développement/production + const origin = req.headers.origin || req.headers.referer || '*'; + res.set('Access-Control-Allow-Origin', origin); + res.set('Access-Control-Allow-Credentials', 'true'); + res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.set('Access-Control-Allow-Headers', 'Authorization, Content-Type, Accept, Origin, X-Requested-With'); + res.set('Access-Control-Max-Age', '3600'); +}; + +// Wrapper pour les fonctions avec CORS +const withCors = (handler) => { + return async (req, res) => { + // Définir les headers CORS pour toutes les requêtes + setCorsHeaders(res, req); + + // Gérer les requêtes preflight OPTIONS + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + // Exécuter le handler + try { + await handler(req, res); + } catch (error) { + logger.error("Unhandled error:", error); + if (!res.headersSent) { + res.status(500).json({ error: error.message }); + } + } + }; +}; + // ============================================================================ // STORAGE - Move Event File // ============================================================================ -exports.moveEventFileV2 = onRequest({ cors: true }, async (req, res) => { +exports.moveEventFileV2 = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const { sourcePath, destinationPath } = req.body.data || {}; @@ -45,14 +89,14 @@ exports.moveEventFileV2 = onRequest({ cors: true }, async (req, res) => { logger.error("Error moving file:", error); res.status(500).json({ error: error.message }); } -}); +})); // ============================================================================ // EQUIPMENT - CRUD // ============================================================================ // Créer un équipement (admin ou manage_equipment) -exports.createEquipment = onRequest({ cors: true }, async (req, res) => { +exports.createEquipment = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); @@ -89,10 +133,10 @@ exports.createEquipment = onRequest({ cors: true }, async (req, res) => { logger.error("Error creating equipment:", error); res.status(500).json({ error: error.message }); } -}); +})); // Mettre à jour un équipement -exports.updateEquipment = onRequest({ cors: true }, async (req, res) => { +exports.updateEquipment = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); @@ -126,10 +170,10 @@ exports.updateEquipment = onRequest({ cors: true }, async (req, res) => { logger.error("Error updating equipment:", error); res.status(500).json({ error: error.message }); } -}); +})); // Supprimer un équipement -exports.deleteEquipment = onRequest({ cors: true }, async (req, res) => { +exports.deleteEquipment = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); @@ -171,10 +215,10 @@ exports.deleteEquipment = onRequest({ cors: true }, async (req, res) => { logger.error("Error deleting equipment:", error); res.status(500).json({ error: error.message }); } -}); +})); // Récupérer un équipement par ID -exports.getEquipment = onRequest({ cors: true }, async (req, res) => { +exports.getEquipment = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasViewAccess = await auth.hasPermission(decodedToken.uid, 'view_equipment'); @@ -211,14 +255,14 @@ exports.getEquipment = onRequest({ cors: true }, async (req, res) => { logger.error("Error getting equipment:", error); res.status(500).json({ error: error.message }); } -}); +})); // ============================================================================ // CONTAINERS - CRUD // ============================================================================ // Créer un container -exports.createContainer = onRequest({ cors: true }, async (req, res) => { +exports.createContainer = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); @@ -251,10 +295,10 @@ exports.createContainer = onRequest({ cors: true }, async (req, res) => { logger.error("Error creating container:", error); res.status(500).json({ error: error.message }); } -}); +})); // Mettre à jour un container -exports.updateContainer = onRequest({ cors: true }, async (req, res) => { +exports.updateContainer = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); @@ -281,10 +325,10 @@ exports.updateContainer = onRequest({ cors: true }, async (req, res) => { logger.error("Error updating container:", error); res.status(500).json({ error: error.message }); } -}); +})); // Supprimer un container -exports.deleteContainer = onRequest({ cors: true }, async (req, res) => { +exports.deleteContainer = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); @@ -308,14 +352,53 @@ exports.deleteContainer = onRequest({ cors: true }, async (req, res) => { logger.error("Error deleting container:", error); res.status(500).json({ error: error.message }); } -}); +})); + +// Récupérer les containers contenant un équipement +exports.getContainersByEquipment = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const hasViewAccess = await auth.hasPermission(decodedToken.uid, 'view_equipment'); + const hasManageAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); + + if (!hasViewAccess && !hasManageAccess) { + res.status(403).json({ error: 'Forbidden: Requires view_equipment permission' }); + return; + } + + const { equipmentId } = req.body.data || req.query; + + if (!equipmentId) { + res.status(400).json({ error: 'equipmentId is required' }); + return; + } + + // Récupérer tous les containers qui contiennent cet équipement + const containersSnapshot = await db.collection('containers') + .where('equipmentIds', 'array-contains', equipmentId) + .get(); + + const containers = []; + containersSnapshot.forEach(doc => { + let data = { id: doc.id, ...doc.data() }; + data = helpers.serializeTimestamps(data); + data = helpers.serializeReferences(data); + containers.push(data); + }); + + res.status(200).json({ containers }); + } catch (error) { + logger.error("Error getting containers by equipment:", error); + res.status(500).json({ error: error.message }); + } +})); // ============================================================================ // EVENTS - CRUD // ============================================================================ // Créer un événement -exports.createEvent = onRequest({ cors: true }, async (req, res) => { +exports.createEvent = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'edit_event'); @@ -338,10 +421,10 @@ exports.createEvent = onRequest({ cors: true }, async (req, res) => { logger.error("Error creating event:", error); res.status(500).json({ error: error.message }); } -}); +})); // Mettre à jour un événement -exports.updateEvent = onRequest({ cors: true }, async (req, res) => { +exports.updateEvent = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'edit_event'); @@ -351,13 +434,22 @@ exports.updateEvent = onRequest({ cors: true }, async (req, res) => { return; } - const { eventId, data } = req.body.data; + const requestData = req.body.data; + const eventId = requestData.eventId; if (!eventId) { res.status(400).json({ error: 'Event ID is required' }); return; } + // Extraire eventId et préparer les données à sauvegarder + const { eventId: _, ...data } = requestData; + + if (!data || Object.keys(data).length === 0) { + res.status(400).json({ error: 'No data to update' }); + return; + } + delete data.id; data.updatedAt = admin.firestore.Timestamp.now(); @@ -372,10 +464,10 @@ exports.updateEvent = onRequest({ cors: true }, async (req, res) => { logger.error("Error updating event:", error); res.status(500).json({ error: error.message }); } -}); +})); // Supprimer un événement -exports.deleteEvent = onRequest({ cors: true }, async (req, res) => { +exports.deleteEvent = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'delete_event'); @@ -399,14 +491,14 @@ exports.deleteEvent = onRequest({ cors: true }, async (req, res) => { logger.error("Error deleting event:", error); res.status(500).json({ error: error.message }); } -}); +})); // ============================================================================ // MAINTENANCES - CRUD // ============================================================================ // Créer une maintenance -exports.createMaintenance = onRequest({ cors: true }, async (req, res) => { +exports.createMaintenance = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_maintenances'); @@ -429,10 +521,10 @@ exports.createMaintenance = onRequest({ cors: true }, async (req, res) => { logger.error("Error creating maintenance:", error); res.status(500).json({ error: error.message }); } -}); +})); // Mettre à jour une maintenance -exports.updateMaintenance = onRequest({ cors: true }, async (req, res) => { +exports.updateMaintenance = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_maintenances'); @@ -463,14 +555,14 @@ exports.updateMaintenance = onRequest({ cors: true }, async (req, res) => { logger.error("Error updating maintenance:", error); res.status(500).json({ error: error.message }); } -}); +})); // ============================================================================ // OPTIONS - CRUD // ============================================================================ // Créer une option -exports.createOption = onRequest({ cors: true }, async (req, res) => { +exports.createOption = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const isAdminUser = await auth.isAdmin(decodedToken.uid); @@ -495,10 +587,10 @@ exports.createOption = onRequest({ cors: true }, async (req, res) => { logger.error("Error creating option:", error); res.status(500).json({ error: error.message }); } -}); +})); // Mettre à jour une option -exports.updateOption = onRequest({ cors: true }, async (req, res) => { +exports.updateOption = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const isAdminUser = await auth.isAdmin(decodedToken.uid); @@ -524,10 +616,10 @@ exports.updateOption = onRequest({ cors: true }, async (req, res) => { logger.error("Error updating option:", error); res.status(500).json({ error: error.message }); } -}); +})); // Supprimer une option -exports.deleteOption = onRequest({ cors: true }, async (req, res) => { +exports.deleteOption = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const isAdminUser = await auth.isAdmin(decodedToken.uid); @@ -551,14 +643,14 @@ exports.deleteOption = onRequest({ cors: true }, async (req, res) => { logger.error("Error deleting option:", error); res.status(500).json({ error: error.message }); } -}); +})); // ============================================================================ // USERS - CRUD // ============================================================================ // Créer un utilisateur -exports.createUser = onRequest({ cors: true }, async (req, res) => { +exports.createUser = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const isAdminUser = await auth.isAdmin(decodedToken.uid); @@ -583,10 +675,10 @@ exports.createUser = onRequest({ cors: true }, async (req, res) => { logger.error("Error creating user:", error); res.status(500).json({ error: error.message }); } -}); +})); // Mettre à jour un utilisateur -exports.updateUser = onRequest({ cors: true }, async (req, res) => { +exports.updateUser = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const { userId, data } = req.body.data; @@ -628,14 +720,14 @@ exports.updateUser = onRequest({ cors: true }, async (req, res) => { logger.error("Error updating user:", error); res.status(500).json({ error: error.message }); } -}); +})); // ============================================================================ // EQUIPMENT STATUS - Batch Update // ============================================================================ // Mettre à jour le statut de plusieurs équipements (pour préparation/retour) -exports.updateEquipmentStatus = onRequest({ cors: true }, async (req, res) => { +exports.updateEquipmentStatus = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const { eventId, updates } = req.body.data; @@ -672,4 +764,652 @@ exports.updateEquipmentStatus = onRequest({ cors: true }, async (req, res) => { logger.error("Error updating equipment statuses:", error); res.status(500).json({ error: error.message }); } -}); +})); + +// ============================================================================ +// OPTIONS - Read (public pour utilisateurs authentifiés) +// ============================================================================ +exports.getOptions = onRequest(httpOptions, withCors(async (req, res) => { + try { + await auth.authenticateUser(req); // Juste vérifier l'auth + + const snapshot = await db.collection('options').get(); + const options = snapshot.docs.map(doc => ({ + id: doc.id, + ...helpers.serializeTimestamps(doc.data()) + })); + + res.status(200).json({ options }); + } catch (error) { + logger.error("Error fetching options:", error); + res.status(500).json({ error: error.message }); + } +})); + +// ============================================================================ +// EVENT TYPES - Read (public pour utilisateurs authentifiés) +// ============================================================================ +exports.getEventTypes = onRequest(httpOptions, withCors(async (req, res) => { + try { + await auth.authenticateUser(req); // Juste vérifier l'auth + + const snapshot = await db.collection('eventTypes').get(); + const eventTypes = snapshot.docs.map(doc => ({ + id: doc.id, + ...helpers.serializeTimestamps(doc.data()) + })); + + res.status(200).json({ eventTypes }); + } catch (error) { + logger.error("Error fetching event types:", error); + res.status(500).json({ error: error.message }); + } +})); + +// ============================================================================ +// ROLES - Read (public pour utilisateurs authentifiés) +// ============================================================================ +exports.getRoles = onRequest(httpOptions, withCors(async (req, res) => { + try { + await auth.authenticateUser(req); // Juste vérifier l'auth + + const snapshot = await db.collection('roles').get(); + const roles = snapshot.docs.map(doc => ({ + id: doc.id, + ...helpers.serializeTimestamps(doc.data()) + })); + + res.status(200).json({ roles }); + } catch (error) { + logger.error("Error fetching roles:", error); + res.status(500).json({ error: error.message }); + } +})); + +// ============================================================================ +// EVENT EQUIPMENT - Update equipment status and quantities +// ============================================================================ +exports.updateEventEquipment = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const { eventId, assignedEquipment, preparationStatus, loadingStatus, unloadingStatus, returnStatus } = req.body.data; + + if (!eventId) { + res.status(400).json({ error: 'Event ID is required' }); + return; + } + + // Vérifier les permissions + const eventDoc = await db.collection('events').doc(eventId).get(); + if (!eventDoc.exists) { + res.status(404).json({ error: 'Event not found' }); + return; + } + + const eventData = eventDoc.data(); + const isAdminUser = await auth.hasPermission(decodedToken.uid, 'edit_event'); + const isAssigned = eventData.workforce?.some(ref => ref.path.endsWith(decodedToken.uid)); + + if (!isAssigned && !isAdminUser) { + res.status(403).json({ error: 'Forbidden: Not assigned to this event' }); + return; + } + + // Préparer les données à mettre à jour + const updateData = {}; + + if (assignedEquipment) { + // Convertir les timestamps dans assignedEquipment + updateData.assignedEquipment = assignedEquipment.map(eq => + helpers.deserializeTimestamps(eq, []) + ); + } + + if (preparationStatus) updateData.preparationStatus = preparationStatus; + if (loadingStatus) updateData.loadingStatus = loadingStatus; + if (unloadingStatus) updateData.unloadingStatus = unloadingStatus; + if (returnStatus) updateData.returnStatus = returnStatus; + + // Mettre à jour l'événement + await db.collection('events').doc(eventId).update(updateData); + + res.status(200).json({ message: 'Event equipment updated successfully' }); + } catch (error) { + logger.error("Error updating event equipment:", error); + res.status(500).json({ error: error.message }); + } +})); + +// ============================================================================ +// EQUIPMENT STATUS - Update individual equipment status +// ============================================================================ +exports.updateEquipmentStatusOnly = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const { equipmentId, status, availableQuantity } = req.body.data; + + if (!equipmentId) { + res.status(400).json({ error: 'Equipment ID is required' }); + return; + } + + // Vérifier les permissions + const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); + if (!hasAccess) { + res.status(403).json({ error: 'Forbidden: Requires manage_equipment permission' }); + return; + } + + const updateData = { updatedAt: admin.firestore.Timestamp.now() }; + if (status) updateData.status = status; + if (availableQuantity !== undefined) updateData.availableQuantity = availableQuantity; + + await db.collection('equipments').doc(equipmentId).update(updateData); + + res.status(200).json({ message: 'Equipment status updated successfully' }); + } catch (error) { + logger.error("Error updating equipment status:", error); + res.status(500).json({ error: error.message }); + } +})); + +// ============================================================================ +// EVENT TYPES - CRUD Operations +// ============================================================================ + +// Récupérer les événements utilisant un type d'événement +exports.getEventsByEventType = onRequest(httpOptions, withCors(async (req, res) => { + try { + await auth.authenticateUser(req); + const { eventTypeId } = req.body.data; + + if (!eventTypeId) { + res.status(400).json({ error: 'Event type ID is required' }); + return; + } + + const eventsSnapshot = await db.collection('events') + .where('eventTypeId', '==', eventTypeId) + .get(); + + const events = eventsSnapshot.docs.map(doc => ({ + id: doc.id, + name: doc.data().name, + startDateTime: doc.data().StartDateTime, + })); + + res.status(200).json({ events }); + } catch (error) { + logger.error("Error fetching events by type:", error); + res.status(500).json({ error: error.message }); + } +})); + +// Créer un type d'événement (admin uniquement) +exports.createEventType = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const isAdmin = await auth.hasPermission(decodedToken.uid, 'edit_data'); + + if (!isAdmin) { + res.status(403).json({ error: 'Forbidden: Admin permission required' }); + return; + } + + const { name, defaultPrice } = req.body.data; + + if (!name || defaultPrice === undefined) { + res.status(400).json({ error: 'Name and defaultPrice are required' }); + return; + } + + // Vérifier l'unicité du nom + const existingSnapshot = await db.collection('eventTypes') + .where('name', '==', name) + .get(); + + if (!existingSnapshot.empty) { + res.status(409).json({ error: 'Event type name already exists' }); + return; + } + + const eventTypeData = { + name, + defaultPrice, + createdAt: admin.firestore.Timestamp.now(), + }; + + const docRef = await db.collection('eventTypes').add(eventTypeData); + + res.status(201).json({ id: docRef.id, message: 'Event type created successfully' }); + } catch (error) { + logger.error("Error creating event type:", error); + res.status(500).json({ error: error.message }); + } +})); + +// Mettre à jour un type d'événement (admin uniquement) +exports.updateEventType = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const isAdmin = await auth.hasPermission(decodedToken.uid, 'edit_data'); + + if (!isAdmin) { + res.status(403).json({ error: 'Forbidden: Admin permission required' }); + return; + } + + const { eventTypeId, name, defaultPrice } = req.body.data; + + if (!eventTypeId) { + res.status(400).json({ error: 'Event type ID is required' }); + return; + } + + // Vérifier que le document existe + const docRef = db.collection('eventTypes').doc(eventTypeId); + const doc = await docRef.get(); + + if (!doc.exists) { + res.status(404).json({ error: 'Event type not found' }); + return; + } + + // Vérifier l'unicité du nom (sauf pour le document actuel) + if (name) { + const existingSnapshot = await db.collection('eventTypes') + .where('name', '==', name) + .get(); + + const hasDuplicate = existingSnapshot.docs.some(d => d.id !== eventTypeId); + if (hasDuplicate) { + res.status(409).json({ error: 'Event type name already exists' }); + return; + } + } + + const updateData = {}; + if (name) updateData.name = name; + if (defaultPrice !== undefined) updateData.defaultPrice = defaultPrice; + + await docRef.update(updateData); + + res.status(200).json({ message: 'Event type updated successfully' }); + } catch (error) { + logger.error("Error updating event type:", error); + res.status(500).json({ error: error.message }); + } +})); + +// Supprimer un type d'événement (admin uniquement) +exports.deleteEventType = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const isAdmin = await auth.hasPermission(decodedToken.uid, 'edit_data'); + + if (!isAdmin) { + res.status(403).json({ error: 'Forbidden: Admin permission required' }); + return; + } + + const { eventTypeId } = req.body.data; + + if (!eventTypeId) { + res.status(400).json({ error: 'Event type ID is required' }); + return; + } + + // Vérifier qu'aucun événement futur n'utilise ce type + const eventsSnapshot = await db.collection('events') + .where('eventTypeId', '==', eventTypeId) + .get(); + + const now = admin.firestore.Timestamp.now(); + const futureEvents = eventsSnapshot.docs.filter(doc => { + const startDate = doc.data().StartDateTime; + return startDate && startDate > now; + }); + + if (futureEvents.length > 0) { + res.status(409).json({ + error: 'Cannot delete event type with future events', + futureEventsCount: futureEvents.length + }); + return; + } + + await db.collection('eventTypes').doc(eventTypeId).delete(); + + res.status(200).json({ message: 'Event type deleted successfully' }); + } catch (error) { + logger.error("Error deleting event type:", error); + res.status(500).json({ error: error.message }); + } +})); + +// ============================================================================ +// EVENTS - Read with permissions +// ============================================================================ +exports.getEvents = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const { userId } = req.body.data || {}; + + // Vérifier si l'utilisateur peut voir tous les événements + const canViewAll = await auth.hasPermission(decodedToken.uid, 'view_all_events'); + + let eventsSnapshot; + + if (canViewAll) { + // Admin : tous les événements + eventsSnapshot = await db.collection('events').get(); + } else { + // Utilisateur normal : seulement ses événements assignés + const userRef = db.collection('users').doc(userId || decodedToken.uid); + eventsSnapshot = await db.collection('events') + .where('workforce', 'array-contains', userRef) + .get(); + } + + // Collecter tous les UIDs utilisateurs uniques + const userIdsSet = new Set(); + + eventsSnapshot.docs.forEach(doc => { + const data = doc.data(); + if (data.workforce && Array.isArray(data.workforce)) { + data.workforce.forEach(userRef => { + if (userRef && userRef.id) { + userIdsSet.add(userRef.id); + } else if (typeof userRef === 'string' && userRef.startsWith('users/')) { + userIdsSet.add(userRef.split('/')[1]); + } + }); + } + }); + + // Récupérer tous les utilisateurs en une seule fois + const usersMap = {}; + if (userIdsSet.size > 0) { + const userIds = Array.from(userIdsSet); + + // Récupérer par batch (Firestore limite à 10 par requête 'in') + const batchSize = 10; + for (let i = 0; i < userIds.length; i += batchSize) { + const batch = userIds.slice(i, i + batchSize); + const usersSnapshot = await db.collection('users') + .where(admin.firestore.FieldPath.documentId(), 'in', batch) + .get(); + + usersSnapshot.docs.forEach(userDoc => { + const userData = userDoc.data(); + // Stocker uniquement les données publiques + usersMap[userDoc.id] = { + uid: userDoc.id, + firstName: userData.firstName || '', + lastName: userData.lastName || '', + email: userData.email || '', + phoneNumber: userData.phoneNumber || '', + profilePhotoUrl: userData.profilePhotoUrl || '', + }; + }); + } + } + + // Sérialiser les événements avec workforce comme liste d'UIDs + const events = eventsSnapshot.docs.map(doc => { + const data = doc.data(); + + // Convertir workforce en liste d'UIDs + let workforceUids = []; + if (data.workforce && Array.isArray(data.workforce)) { + workforceUids = data.workforce.map(userRef => { + if (userRef && userRef.id) { + return userRef.id; + } else if (typeof userRef === 'string' && userRef.startsWith('users/')) { + return userRef.split('/')[1]; + } + return null; + }).filter(uid => uid !== null); + } + + return { + id: doc.id, + ...helpers.serializeTimestamps(data), + workforce: workforceUids, // Liste d'UIDs au lieu de DocumentReference + }; + }); + + // Retourner events + users map + res.status(200).json({ + events, + users: usersMap // Map UID -> données utilisateur + }); + } catch (error) { + logger.error("Error fetching events:", error); + res.status(500).json({ error: error.message }); + } +})); + +// ============================================================================ +// EQUIPMENTS - Read with permissions +// ============================================================================ +exports.getEquipments = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + + // Vérifier les permissions + const canManage = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); + const canView = await auth.hasPermission(decodedToken.uid, 'view_equipment'); + + if (!canManage && !canView) { + res.status(403).json({ error: 'Forbidden: Requires equipment permissions' }); + return; + } + + const snapshot = await db.collection('equipments').get(); + const equipments = snapshot.docs.map(doc => { + const data = doc.data(); + + // Masquer les prix si l'utilisateur n'a pas manage_equipment + if (!canManage) { + delete data.purchasePrice; + delete data.rentalPrice; + } + + return { + id: doc.id, + ...helpers.serializeTimestamps(data, ['purchaseDate', 'nextMaintenanceDate', 'lastMaintenanceDate', 'createdAt', 'updatedAt']) + }; + }); + + res.status(200).json({ equipments }); + } catch (error) { + logger.error("Error fetching equipments:", error); + res.status(500).json({ error: error.message }); + } +})); + +// ============================================================================ +// CONTAINERS - Read with permissions +// ============================================================================ +exports.getContainers = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + + // Vérifier les permissions + const canView = await auth.hasPermission(decodedToken.uid, 'view_equipment'); + + if (!canView) { + res.status(403).json({ error: 'Forbidden: Requires equipment permissions' }); + return; + } + + const snapshot = await db.collection('containers').get(); + const containers = snapshot.docs.map(doc => { + const data = doc.data(); + return { + id: doc.id, + ...helpers.serializeTimestamps(data, ['createdAt', 'updatedAt']) + }; + }); + + res.status(200).json({ containers }); + } catch (error) { + logger.error("Error fetching containers:", error); + res.status(500).json({ error: error.message }); + } +})); + +// ============================================================================ +// MAINTENANCES - Read with permissions +// ============================================================================ +exports.getMaintenances = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const { equipmentId } = req.body.data || {}; + + // Vérifier les permissions + const canView = await auth.hasPermission(decodedToken.uid, 'view_equipment'); + + if (!canView) { + res.status(403).json({ error: 'Forbidden: Requires equipment permissions' }); + return; + } + + let query = db.collection('maintenances'); + + // Filtrer par équipement si spécifié + if (equipmentId) { + query = query.where('equipmentIds', 'array-contains', equipmentId); + } + + const snapshot = await query.get(); + const maintenances = snapshot.docs.map(doc => { + const data = doc.data(); + return { + id: doc.id, + ...helpers.serializeTimestamps(data, ['scheduledDate', 'completedDate', 'createdAt', 'updatedAt']) + }; + }); + + res.status(200).json({ maintenances }); + } catch (error) { + logger.error("Error fetching maintenances:", error); + res.status(500).json({ error: error.message }); + } +})); + +// ============================================================================ +// ALERTS - Read with permissions +// ============================================================================ +exports.getAlerts = onRequest(httpOptions, withCors(async (req, res) => { + try { + await auth.authenticateUser(req); + + const snapshot = await db.collection('alerts') + .orderBy('createdAt', 'desc') + .limit(100) + .get(); + + const alerts = snapshot.docs.map(doc => { + const data = doc.data(); + return { + id: doc.id, + ...helpers.serializeTimestamps(data, ['createdAt']) + }; + }); + + res.status(200).json({ alerts }); + } catch (error) { + logger.error("Error fetching alerts:", error); + res.status(500).json({ error: error.message }); + } +})); + +// ============================================================================ +// USERS - Read with permissions +// ============================================================================ +exports.getUsers = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + + // Vérifier les permissions + const canViewAll = await auth.hasPermission(decodedToken.uid, 'view_all_users'); + + if (!canViewAll) { + // Si pas admin, ne retourner que l'utilisateur lui-même + const userDoc = await db.collection('users').doc(decodedToken.uid).get(); + + if (!userDoc.exists) { + res.status(404).json({ error: 'User not found' }); + return; + } + + const userData = userDoc.data(); + res.status(200).json({ + users: [{ + id: userDoc.id, + ...helpers.serializeTimestamps(userData) + }] + }); + return; + } + + // Admin : tous les utilisateurs + const snapshot = await db.collection('users').get(); + const users = snapshot.docs.map(doc => ({ + id: doc.id, + ...helpers.serializeTimestamps(doc.data()) + })); + + res.status(200).json({ users }); + } catch (error) { + logger.error("Error fetching users:", error); + res.status(500).json({ error: error.message }); + } +})); + +// ============================================================================ +// CONTAINERS - Récupération par équipement +// ============================================================================ + +/** + * Récupère tous les containers contenant un équipement spécifique + * Accessible à tous les utilisateurs authentifiés + */ +exports.getContainersByEquipment = onRequest(httpOptions, withCors(async (req, res) => { + try { + // Vérifier l'authentification + const user = await auth.authenticateUser(req); + + const equipmentId = req.body.data?.equipmentId; + + if (!equipmentId) { + res.status(400).json({ error: 'equipmentId is required' }); + return; + } + + logger.info(`Fetching containers for equipment: ${equipmentId}`); + + // Requête pour trouver tous les containers contenant cet équipement + const containersSnapshot = await db.collection('containers') + .where('equipmentIds', 'array-contains', equipmentId) + .get(); + + const containers = containersSnapshot.docs.map(doc => ({ + id: doc.id, + ...helpers.serializeTimestamps(doc.data()) + })); + + logger.info(`Found ${containers.length} container(s) for equipment ${equipmentId}`); + + res.status(200).json({ + containers, + count: containers.length + }); + } catch (error) { + logger.error("Error fetching containers by equipment:", error); + res.status(500).json({ error: error.message }); + } +})); + diff --git a/em2rp/functions/utils/helpers.js b/em2rp/functions/utils/helpers.js index 82d866c..52591a0 100644 --- a/em2rp/functions/utils/helpers.js +++ b/em2rp/functions/utils/helpers.js @@ -9,20 +9,60 @@ const admin = require('firebase-admin'); function serializeTimestamps(data) { if (!data) return data; + // Éviter la récursion sur les types Firestore spéciaux + if (data._firestore || data._path || data._converter) { + // C'est un objet Firestore interne, ne pas le traiter + if (data.id && data.path) { + // C'est une DocumentReference + return data.path; + } + return null; + } + const result = { ...data }; for (const key in result) { - if (result[key] && result[key].toDate && typeof result[key].toDate === 'function') { - // C'est un Timestamp Firestore - result[key] = result[key].toDate().toISOString(); - } else if (result[key] && typeof result[key] === 'object' && !Array.isArray(result[key])) { - // Objet imbriqué - result[key] = serializeTimestamps(result[key]); - } else if (Array.isArray(result[key])) { - // Tableau - result[key] = result[key].map(item => - item && typeof item === 'object' ? serializeTimestamps(item) : item - ); + const value = result[key]; + + if (!value) { + continue; + } + + // Gérer les Timestamps Firestore + if (value.toDate && typeof value.toDate === 'function') { + result[key] = value.toDate().toISOString(); + } + // Gérer les DocumentReference + else if (value.path && value.id && typeof value.path === 'string') { + result[key] = value.path; + } + // Gérer les GeoPoint + else if (value.latitude !== undefined && value.longitude !== undefined) { + result[key] = { + latitude: value.latitude, + longitude: value.longitude + }; + } + // Gérer les tableaux + else if (Array.isArray(value)) { + result[key] = value.map(item => { + if (!item || typeof item !== 'object') return item; + + // DocumentReference dans un tableau + if (item.path && item.id) { + return item.path; + } + // Timestamp dans un tableau + if (item.toDate && typeof item.toDate === 'function') { + return item.toDate().toISOString(); + } + // Objet normal + return serializeTimestamps(item); + }); + } + // Gérer les objets imbriqués (mais pas les objets Firestore) + else if (typeof value === 'object' && !value._firestore && !value._path) { + result[key] = serializeTimestamps(value); } } diff --git a/em2rp/lib/controllers/event_form_controller.dart b/em2rp/lib/controllers/event_form_controller.dart index 6e5ee2a..b1bc4b7 100644 --- a/em2rp/lib/controllers/event_form_controller.dart +++ b/em2rp/lib/controllers/event_form_controller.dart @@ -5,6 +5,8 @@ import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/models/event_type_model.dart'; import 'package:em2rp/models/user_model.dart'; import 'package:em2rp/services/event_form_service.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; import 'package:provider/provider.dart'; import 'package:em2rp/providers/event_provider.dart'; import 'package:em2rp/providers/local_user_provider.dart'; @@ -125,7 +127,14 @@ class EventFormController extends ChangeNotifier { _assignedEquipment = List.from(event.assignedEquipment); _assignedContainers = List.from(event.assignedContainers); _selectedEventTypeId = event.eventTypeId.isNotEmpty ? event.eventTypeId : null; - _selectedUserIds = event.workforce.map((ref) => ref.id).toList(); + + // Gérer workforce qui peut contenir String ou DocumentReference + _selectedUserIds = event.workforce.map((ref) { + if (ref is String) return ref; + if (ref is DocumentReference) return ref.id; + return ''; + }).where((id) => id.isNotEmpty).toList(); + _uploadedFiles = List>.from(event.documents); _selectedOptions = List>.from(event.options); _selectedStatus = event.status; @@ -422,8 +431,9 @@ class EventFormController extends ChangeNotifier { notifyListeners(); try { - // Supprimer l'événement de Firestore - await FirebaseFirestore.instance.collection('events').doc(eventId).delete(); + // Supprimer l'événement via l'API + final dataService = DataService(FirebaseFunctionsApiService()); + await dataService.deleteEvent(eventId); // Recharger la liste des événements final localUserProvider = Provider.of(context, listen: false); diff --git a/em2rp/lib/main.dart b/em2rp/lib/main.dart index 7c56fe8..c229baf 100644 --- a/em2rp/lib/main.dart +++ b/em2rp/lib/main.dart @@ -54,30 +54,31 @@ void main() async { runApp( MultiProvider( providers: [ - // Injection du service UserService - Provider(create: (_) => UserService()), - // LocalUserProvider pour la gestion de l'authentification ChangeNotifierProvider( create: (context) => LocalUserProvider()), - // Injection des Providers en utilisant UserService + // UsersProvider migré vers l'API ChangeNotifierProvider( - create: (context) => UsersProvider(context.read()), + create: (context) => UsersProvider(), ), - // EventProvider pour la gestion des événements + // EventProvider migré vers l'API ChangeNotifierProvider( create: (context) => EventProvider(), ), - // Providers pour la gestion du matériel + // EquipmentProvider migré vers l'API ChangeNotifierProvider( create: (context) => EquipmentProvider(), ), + + // ContainerProvider migré vers l'API ChangeNotifierProvider( create: (context) => ContainerProvider(), ), + + // MaintenanceProvider migré vers l'API ChangeNotifierProvider( create: (context) => MaintenanceProvider(), ), diff --git a/em2rp/lib/models/alert_model.dart b/em2rp/lib/models/alert_model.dart index 61f09a9..e0f7cf6 100644 --- a/em2rp/lib/models/alert_model.dart +++ b/em2rp/lib/models/alert_model.dart @@ -3,7 +3,8 @@ enum AlertType { lowStock, // Stock faible maintenanceDue, // Maintenance à venir - conflict // Conflit disponibilité + conflict, // Conflit disponibilité + lost // Équipement perdu } String alertTypeToString(AlertType type) { @@ -14,6 +15,8 @@ String alertTypeToString(AlertType type) { return 'MAINTENANCE_DUE'; case AlertType.conflict: return 'CONFLICT'; + case AlertType.lost: + return 'LOST'; } } @@ -25,6 +28,8 @@ AlertType alertTypeFromString(String? type) { return AlertType.maintenanceDue; case 'CONFLICT': return AlertType.conflict; + case 'LOST': + return AlertType.lost; default: return AlertType.conflict; } diff --git a/em2rp/lib/models/event_model.dart b/em2rp/lib/models/event_model.dart index f7d3e80..d1ca95b 100644 --- a/em2rp/lib/models/event_model.dart +++ b/em2rp/lib/models/event_model.dart @@ -250,7 +250,7 @@ class EventModel { final String address; final double latitude; final double longitude; - final List workforce; + final List workforce; // Peut contenir DocumentReference OU String (UIDs) final List> documents; final List> options; final EventStatus status; @@ -310,11 +310,14 @@ class EventModel { // Gestion sécurisée des références workforce final List workforceRefs = map['workforce'] ?? []; - final List safeWorkforce = []; + final List safeWorkforce = []; for (var ref in workforceRefs) { if (ref is DocumentReference) { safeWorkforce.add(ref); + } else if (ref is String) { + // Accepter directement les UIDs (envoyés par le backend) + safeWorkforce.add(ref); } else { print('Warning: Invalid workforce reference in event $id: $ref'); } @@ -527,7 +530,7 @@ class EventModel { String? address, double? latitude, double? longitude, - List? workforce, + List? workforce, List>? documents, List>? options, EventStatus? status, diff --git a/em2rp/lib/models/event_type_model.dart b/em2rp/lib/models/event_type_model.dart index 72f5766..a428f05 100644 --- a/em2rp/lib/models/event_type_model.dart +++ b/em2rp/lib/models/event_type_model.dart @@ -1,3 +1,5 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + class EventTypeModel { final String id; final String name; @@ -12,11 +14,19 @@ class EventTypeModel { }); factory EventTypeModel.fromMap(Map map, String id) { + // Gérer createdAt qui peut être Timestamp (Firestore) ou String ISO (API) + DateTime parseCreatedAt(dynamic value) { + if (value == null) return DateTime.now(); + if (value is Timestamp) return value.toDate(); + if (value is String) return DateTime.tryParse(value) ?? DateTime.now(); + return DateTime.now(); + } + return EventTypeModel( id: id, name: map['name'] ?? '', defaultPrice: (map['defaultPrice'] ?? 0.0).toDouble(), - createdAt: map['createdAt']?.toDate() ?? DateTime.now(), + createdAt: parseCreatedAt(map['createdAt']), ); } diff --git a/em2rp/lib/providers/alert_provider_new.dart b/em2rp/lib/providers/alert_provider_new.dart new file mode 100644 index 0000000..f022b34 --- /dev/null +++ b/em2rp/lib/providers/alert_provider_new.dart @@ -0,0 +1,62 @@ +import 'package:flutter/foundation.dart'; +import 'package:em2rp/models/alert_model.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; + +class AlertProvider extends ChangeNotifier { + final DataService _dataService = DataService(FirebaseFunctionsApiService()); + + List _alerts = []; + bool _isLoading = false; + + List get alerts => _alerts; + bool get isLoading => _isLoading; + + /// Nombre d'alertes non lues + int get unreadCount => _alerts.where((a) => !a.isRead).length; + + /// Charger toutes les alertes via l'API + Future loadAlerts() async { + _isLoading = true; + notifyListeners(); + + try { + final alertsData = await _dataService.getAlerts(); + + _alerts = alertsData.map((data) { + return AlertModel.fromMap(data, data['id'] as String); + }).toList(); + + _isLoading = false; + notifyListeners(); + } catch (e) { + print('Error loading alerts: $e'); + _isLoading = false; + notifyListeners(); + rethrow; + } + } + + /// Recharger les alertes + Future refresh() async { + await loadAlerts(); + } + + /// Obtenir les alertes non lues + List get unreadAlerts { + return _alerts.where((a) => !a.isRead).toList(); + } + + /// Obtenir les alertes par type + List getByType(AlertType type) { + return _alerts.where((a) => a.type == type).toList(); + } + + /// Obtenir les alertes critiques (stock bas, équipement perdu) + List get criticalAlerts { + return _alerts.where((a) => + a.type == AlertType.lowStock || a.type == AlertType.lost + ).toList(); + } +} + diff --git a/em2rp/lib/providers/container_provider.dart b/em2rp/lib/providers/container_provider.dart index 47ac2f0..4db3be5 100644 --- a/em2rp/lib/providers/container_provider.dart +++ b/em2rp/lib/providers/container_provider.dart @@ -6,13 +6,40 @@ import 'package:em2rp/services/container_service.dart'; class ContainerProvider with ChangeNotifier { final ContainerService _containerService = ContainerService(); + List _containers = []; ContainerType? _selectedType; EquipmentStatus? _selectedStatus; String _searchQuery = ''; + bool _isLoading = false; + List get containers => _containers; ContainerType? get selectedType => _selectedType; EquipmentStatus? get selectedStatus => _selectedStatus; String get searchQuery => _searchQuery; + bool get isLoading => _isLoading; + + /// Charger tous les containers via l'API + Future loadContainers() async { + _isLoading = true; + notifyListeners(); + + try { + // Pour l'instant, on écoute le stream et on garde la première valeur + _containerService.getContainers( + type: _selectedType, + status: _selectedStatus, + searchQuery: _searchQuery, + ).listen((containers) { + _containers = containers; + _isLoading = false; + notifyListeners(); + }); + } catch (e) { + print('Error loading containers: $e'); + _isLoading = false; + notifyListeners(); + } + } /// Stream des containers avec filtres appliqués Stream> get containersStream { diff --git a/em2rp/lib/providers/container_provider_new.dart b/em2rp/lib/providers/container_provider_new.dart new file mode 100644 index 0000000..ed11a55 --- /dev/null +++ b/em2rp/lib/providers/container_provider_new.dart @@ -0,0 +1,109 @@ +import 'package:flutter/foundation.dart'; +import 'package:em2rp/models/container_model.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; + +import '../models/equipment_model.dart'; + +class ContainerProvider extends ChangeNotifier { + final DataService _dataService = DataService(FirebaseFunctionsApiService()); + + List _containers = []; + ContainerType? _selectedType; + EquipmentStatus? _selectedStatus; + String _searchQuery = ''; + bool _isLoading = false; + + // Getters + List get containers => _filteredContainers; + ContainerType? get selectedType => _selectedType; + EquipmentStatus? get selectedStatus => _selectedStatus; + String get searchQuery => _searchQuery; + bool get isLoading => _isLoading; + + /// Charger tous les conteneurs via l'API + Future loadContainers() async { + _isLoading = true; + notifyListeners(); + + try { + final containersData = await _dataService.getContainers(); + + _containers = containersData.map((data) { + return ContainerModel.fromMap(data, data['id'] as String); + }).toList(); + + _isLoading = false; + notifyListeners(); + } catch (e) { + print('Error loading containers: $e'); + _isLoading = false; + notifyListeners(); + rethrow; + } + } + + /// Obtenir les conteneurs filtrés + List get _filteredContainers { + var filtered = _containers; + + if (_selectedType != null) { + filtered = filtered.where((c) => c.type == _selectedType).toList(); + } + + if (_selectedStatus != null) { + filtered = filtered.where((c) => c.status == _selectedStatus).toList(); + } + + if (_searchQuery.isNotEmpty) { + final query = _searchQuery.toLowerCase(); + filtered = filtered.where((c) { + return c.name.toLowerCase().contains(query) || + c.id.toLowerCase().contains(query); + }).toList(); + } + + return filtered; + } + + /// Définir le filtre de type + void setSelectedType(ContainerType? type) { + _selectedType = type; + notifyListeners(); + } + + /// Définir le filtre de statut + void setSelectedStatus(EquipmentStatus? status) { + _selectedStatus = status; + notifyListeners(); + } + + /// Définir la requête de recherche + void setSearchQuery(String query) { + _searchQuery = query; + notifyListeners(); + } + + /// Réinitialiser tous les filtres + void clearFilters() { + _selectedType = null; + _selectedStatus = null; + _searchQuery = ''; + notifyListeners(); + } + + /// Recharger les conteneurs + Future refresh() async { + await loadContainers(); + } + + /// Obtenir un conteneur par ID + ContainerModel? getById(String id) { + try { + return _containers.firstWhere((c) => c.id == id); + } catch (e) { + return null; + } + } +} + diff --git a/em2rp/lib/providers/equipment_provider.dart b/em2rp/lib/providers/equipment_provider.dart index 7b25f19..f48d6e6 100644 --- a/em2rp/lib/providers/equipment_provider.dart +++ b/em2rp/lib/providers/equipment_provider.dart @@ -1,11 +1,10 @@ import 'package:flutter/foundation.dart'; import 'package:em2rp/models/equipment_model.dart'; -import 'package:em2rp/services/equipment_service.dart'; -import 'package:em2rp/services/equipment_status_calculator.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; class EquipmentProvider extends ChangeNotifier { - final EquipmentService _service = EquipmentService(); - final EquipmentStatusCalculator _statusCalculator = EquipmentStatusCalculator(); + final DataService _dataService = DataService(FirebaseFunctionsApiService()); List _equipment = []; List _models = []; @@ -15,68 +14,156 @@ class EquipmentProvider extends ChangeNotifier { EquipmentStatus? _selectedStatus; String? _selectedModel; String _searchQuery = ''; + bool _isLoading = false; // Getters - List get equipment => _equipment; + List get equipment => _filteredEquipment; List get models => _models; List get brands => _brands; EquipmentCategory? get selectedCategory => _selectedCategory; EquipmentStatus? get selectedStatus => _selectedStatus; String? get selectedModel => _selectedModel; String get searchQuery => _searchQuery; + bool get isLoading => _isLoading; - /// Stream des équipements avec filtres appliqués - Stream> get equipmentStream { - return _service.getEquipment( - category: _selectedCategory, - status: _selectedStatus, - model: _selectedModel, - searchQuery: _searchQuery, - ); - } + /// Charger tous les équipements via l'API + Future loadEquipments() async { + print('[EquipmentProvider] Starting to load equipments...'); + _isLoading = true; + notifyListeners(); - /// Charger tous les modèles uniques - Future loadModels() async { try { - _models = await _service.getAllModels(); + print('[EquipmentProvider] Calling getEquipments API...'); + final equipmentsData = await _dataService.getEquipments(); + print('[EquipmentProvider] Received ${equipmentsData.length} equipments from API'); + + _equipment = equipmentsData.map((data) { + return EquipmentModel.fromMap(data, data['id'] as String); + }).toList(); + print('[EquipmentProvider] Mapped ${_equipment.length} equipment models'); + + // Extraire les modèles et marques uniques + _extractUniqueValues(); + + _isLoading = false; notifyListeners(); + print('[EquipmentProvider] Equipment loading complete'); } catch (e) { - print('Error loading models: $e'); - rethrow; - } - } - - /// Charger toutes les marques uniques - Future loadBrands() async { - try { - _brands = await _service.getAllBrands(); + print('[EquipmentProvider] Error loading equipments: $e'); + _isLoading = false; notifyListeners(); - } catch (e) { - print('Error loading brands: $e'); rethrow; } } - /// Charger les modèles filtrés par marque - Future> loadModelsByBrand(String brand) async { + /// Extraire modèles et marques uniques + void _extractUniqueValues() { + final modelSet = {}; + final brandSet = {}; + + for (final eq in _equipment) { + if (eq.model != null && eq.model!.isNotEmpty) { + modelSet.add(eq.model!); + } + if (eq.brand != null && eq.brand!.isNotEmpty) { + brandSet.add(eq.brand!); + } + } + + _models = modelSet.toList()..sort(); + _brands = brandSet.toList()..sort(); + } + + /// Obtenir les équipements filtrés + List get _filteredEquipment { + var filtered = _equipment; + + if (_selectedCategory != null) { + filtered = filtered.where((eq) => eq.category == _selectedCategory).toList(); + } + + if (_selectedStatus != null) { + filtered = filtered.where((eq) => eq.status == _selectedStatus).toList(); + } + + if (_selectedModel != null && _selectedModel!.isNotEmpty) { + filtered = filtered.where((eq) => eq.model == _selectedModel).toList(); + } + + if (_searchQuery.isNotEmpty) { + final query = _searchQuery.toLowerCase(); + filtered = filtered.where((eq) { + return eq.name.toLowerCase().contains(query) || + eq.id.toLowerCase().contains(query) || + (eq.model?.toLowerCase().contains(query) ?? false) || + (eq.brand?.toLowerCase().contains(query) ?? false); + }).toList(); + } + + return filtered; + } + + /// Définir le filtre de catégorie + void setSelectedCategory(EquipmentCategory? category) { + _selectedCategory = category; + notifyListeners(); + } + + /// Définir le filtre de statut + void setSelectedStatus(EquipmentStatus? status) { + _selectedStatus = status; + notifyListeners(); + } + + /// Définir le filtre de modèle + void setSelectedModel(String? model) { + _selectedModel = model; + notifyListeners(); + } + + /// Définir la requête de recherche + void setSearchQuery(String query) { + _searchQuery = query; + notifyListeners(); + } + + /// Réinitialiser tous les filtres + void clearFilters() { + _selectedCategory = null; + _selectedStatus = null; + _selectedModel = null; + _searchQuery = ''; + notifyListeners(); + } + + /// Recharger les équipements + Future refresh() async { + await loadEquipments(); + } + + // === MÉTHODES STREAM (COMPATIBILITÉ) === + + /// Stream des équipements (pour compatibilité avec ancien code) + Stream> get equipmentStream async* { + yield _equipment; + } + + /// Supprimer un équipement + Future deleteEquipment(String equipmentId) async { try { - return await _service.getModelsByBrand(brand); + await _dataService.deleteEquipment(equipmentId); + await loadEquipments(); // Recharger la liste } catch (e) { - print('Error loading models by brand: $e'); + print('Error deleting equipment: $e'); rethrow; } } - /// Ajouter un équipement Future addEquipment(EquipmentModel equipment) async { try { - await _service.createEquipment(equipment); - - // Recharger les modèles si un nouveau modèle a été ajouté - if (equipment.model != null && !_models.contains(equipment.model)) { - await loadModels(); - } + await _dataService.createEquipment(equipment.id, equipment.toMap()); + await loadEquipments(); // Recharger la liste } catch (e) { print('Error adding equipment: $e'); rethrow; @@ -84,146 +171,44 @@ class EquipmentProvider extends ChangeNotifier { } /// Mettre à jour un équipement - Future updateEquipment(String id, Map data) async { + Future updateEquipment(EquipmentModel equipment) async { try { - await _service.updateEquipment(id, data); - - // Recharger les modèles si le modèle a changé - if (data.containsKey('model')) { - await loadModels(); - } + await _dataService.updateEquipment(equipment.id, equipment.toMap()); + await loadEquipments(); // Recharger la liste } catch (e) { print('Error updating equipment: $e'); rethrow; } } - /// Supprimer un équipement - Future deleteEquipment(String id) async { - try { - await _service.deleteEquipment(id); - } catch (e) { - print('Error deleting equipment: $e'); - rethrow; - } + /// Charger les marques + Future loadBrands() async { + // Les marques sont déjà chargées avec loadEquipments + _extractUniqueValues(); } - /// Récupérer un équipement par ID - Future getEquipmentById(String id) async { - try { - return await _service.getEquipmentById(id); - } catch (e) { - print('Error getting equipment: $e'); - rethrow; - } + /// Charger les modèles + Future loadModels() async { + // Les modèles sont déjà chargés avec loadEquipments + _extractUniqueValues(); } - /// Trouver des alternatives disponibles - Future> findAlternatives( - String model, - DateTime startDate, - DateTime endDate, - ) async { - try { - return await _service.findAlternatives(model, startDate, endDate); - } catch (e) { - print('Error finding alternatives: $e'); - rethrow; - } + /// Charger les modèles d'une marque spécifique + Future> loadModelsByBrand(String brand) async { + // Filtrer les modèles par marque + final modelsByBrand = _equipment + .where((eq) => eq.brand == brand && eq.model != null) + .map((eq) => eq.model!) + .toSet() + .toList(); + return modelsByBrand; } - /// Vérifier la disponibilité d'un équipement - Future> checkAvailability( - String equipmentId, - DateTime startDate, - DateTime endDate, - ) async { - try { - return await _service.checkAvailability(equipmentId, startDate, endDate); - } catch (e) { - print('Error checking availability: $e'); - rethrow; - } - } - - /// Mettre à jour le stock d'un consommable - Future updateStock(String id, int quantityChange) async { - try { - await _service.updateStock(id, quantityChange); - } catch (e) { - print('Error updating stock: $e'); - rethrow; - } - } - - /// Vérifier les stocks critiques - Future checkCriticalStock() async { - try { - await _service.checkCriticalStock(); - } catch (e) { - print('Error checking critical stock: $e'); - rethrow; - } - } - - /// Générer les données du QR code - String generateQRCodeData(String equipmentId) { - return _service.generateQRCodeData(equipmentId); - } - - /// Vérifier si un ID est unique - Future isIdUnique(String id) async { - try { - return await _service.isIdUnique(id); - } catch (e) { - print('Error checking ID uniqueness: $e'); - rethrow; - } - } - - /// Calculer le statut réel d'un équipement (asynchrone) + /// Calculer le statut réel d'un équipement (compatibilité) Future calculateRealStatus(EquipmentModel equipment) async { - return await _statusCalculator.calculateRealStatus(equipment); - } - - /// Invalider le cache du calculateur de statut - void invalidateStatusCache() { - _statusCalculator.invalidateCache(); - } - - // === FILTRES === - - /// Définir la catégorie sélectionnée - void setSelectedCategory(EquipmentCategory? category) { - _selectedCategory = category; - notifyListeners(); - } - - /// Définir le statut sélectionné - void setSelectedStatus(EquipmentStatus? status) { - _selectedStatus = status; - notifyListeners(); - } - - /// Définir le modèle sélectionné - void setSelectedModel(String? model) { - _selectedModel = model; - notifyListeners(); - } - - /// Définir la recherche - void setSearchQuery(String query) { - _searchQuery = query; - notifyListeners(); - } - - /// Réinitialiser tous les filtres - void resetFilters() { - _selectedCategory = null; - _selectedStatus = null; - _selectedModel = null; - _searchQuery = ''; - notifyListeners(); + // Pour l'instant, retourner le statut stocké + // TODO: Implémenter le calcul réel si nécessaire + return equipment.status; } } diff --git a/em2rp/lib/providers/event_provider.dart b/em2rp/lib/providers/event_provider.dart index 714110c..24b9c8b 100644 --- a/em2rp/lib/providers/event_provider.dart +++ b/em2rp/lib/providers/event_provider.dart @@ -1,110 +1,99 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; -import 'package:em2rp/services/equipment_status_calculator.dart'; -import '../models/event_model.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:em2rp/models/event_model.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; class EventProvider with ChangeNotifier { - final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final DataService _dataService = DataService(FirebaseFunctionsApiService()); List _events = []; bool _isLoading = false; List get events => _events; bool get isLoading => _isLoading; - // Récupérer les événements pour un utilisateur spécifique - Future loadUserEvents(String userId, - {bool canViewAllEvents = false}) async { + // Cache des utilisateurs chargés depuis getEvents + Map> _usersCache = {}; + + /// Charger les événements d'un utilisateur via l'API + Future loadUserEvents(String userId, {bool canViewAllEvents = false}) async { _isLoading = true; notifyListeners(); - try { - print( - 'Loading events for user: $userId (canViewAllEvents: $canViewAllEvents)'); + // Sauvegarder les paramètres + _saveLastLoadParams(userId, canViewAllEvents); - QuerySnapshot eventsSnapshot = await _firestore.collection('events').get(); - print('Found ${eventsSnapshot.docs.length} events total'); + try { + print('Loading events for user: $userId (canViewAllEvents: $canViewAllEvents)'); + + // Charger via l'API - les permissions sont vérifiées côté serveur + final result = await _dataService.getEvents(userId: userId); + final eventsData = result['events'] as List>; + final usersData = result['users'] as Map; + + // Stocker les utilisateurs dans le cache + _usersCache = usersData.map((key, value) => + MapEntry(key, value as Map) + ); + + print('Found ${eventsData.length} events from API'); List allEvents = []; int failedCount = 0; - // Parser chaque événement individuellement pour éviter qu'une erreur interrompe tout - for (var doc in eventsSnapshot.docs) { + // Parser chaque événement + for (var eventData in eventsData) { try { - final data = doc.data() as Map; - final event = EventModel.fromMap(data, doc.id); + final event = EventModel.fromMap(eventData, eventData['id'] as String); allEvents.add(event); } catch (e) { - print('Failed to parse event ${doc.id}: $e'); + print('Failed to parse event ${eventData['id']}: $e'); failedCount++; - // Continue avec les autres événements au lieu d'arrêter } } - // Filtrage amélioré pour les utilisateurs non-admin - if (canViewAllEvents) { - _events = allEvents; - print('Admin user: showing all ${_events.length} events'); - } else { - // Créer la référence utilisateur correctement - final userDocRef = _firestore.collection('users').doc(userId); - - _events = allEvents.where((event) { - // Vérifier si l'utilisateur est dans l'équipe de l'événement - bool isInWorkforce = event.workforce.any((workforceRef) { - return workforceRef.path == userDocRef.path; - }); - - if (isInWorkforce) { - print('Event ${event.name} includes user $userId'); - } - - return isInWorkforce; - }).toList(); - } + _events = allEvents; + print('Successfully loaded ${_events.length} events (${failedCount} failed)'); _isLoading = false; notifyListeners(); - } catch (e) { print('Error loading events: $e'); _isLoading = false; - _events = []; // S'assurer que la liste est vide en cas d'erreur notifyListeners(); rethrow; } } - // Récupérer un événement spécifique - Future getEvent(String eventId) async { + /// Recharger les événements (utilise le dernier userId) + Future refreshEvents(String userId, {bool canViewAllEvents = false}) async { + await loadUserEvents(userId, canViewAllEvents: canViewAllEvents); + } + + /// Récupérer un événement spécifique par ID + EventModel? getEventById(String eventId) { try { - final doc = await _firestore.collection('events').doc(eventId).get(); - if (doc.exists) { - return EventModel.fromMap(doc.data()!, doc.id); - } - return null; + return _events.firstWhere((event) => event.id == eventId); } catch (e) { - print('Error getting event: $e'); - rethrow; + return null; } } - // Ajouter un nouvel événement + /// Ajouter un nouvel événement Future addEvent(EventModel event) async { try { - final docRef = await _firestore.collection('events').add(event.toMap()); - final newEvent = EventModel.fromMap(event.toMap(), docRef.id); - _events.add(newEvent); - notifyListeners(); + // L'événement est créé via l'API dans le service + await refreshEvents(_lastUserId ?? '', canViewAllEvents: _lastCanViewAll); } catch (e) { print('Error adding event: $e'); rethrow; } } - // Mettre à jour un événement + /// Mettre à jour un événement Future updateEvent(EventModel event) async { try { - await _firestore.collection('events').doc(event.id).update(event.toMap()); + // Mise à jour locale immédiate final index = _events.indexWhere((e) => e.id == event.id); if (index != -1) { _events[index] = event; @@ -116,15 +105,11 @@ class EventProvider with ChangeNotifier { } } - // Supprimer un événement + /// Supprimer un événement Future deleteEvent(String eventId) async { try { - await _firestore.collection('events').doc(eventId).delete(); + await _dataService.deleteEvent(eventId); _events.removeWhere((event) => event.id == eventId); - - // Invalider le cache des statuts d'équipement - EquipmentStatusCalculator.invalidateGlobalCache(); - notifyListeners(); } catch (e) { print('Error deleting event: $e'); @@ -132,9 +117,56 @@ class EventProvider with ChangeNotifier { } } - // Vider la liste des événements + /// Récupérer les données d'un utilisateur depuis le cache + Map? getUserFromCache(String uid) { + return _usersCache[uid]; + } + + /// Récupérer les utilisateurs de la workforce d'un événement + List> getWorkforceUsers(EventModel event) { + final users = >[]; + + for (final dynamic userRef in event.workforce) { + try { + String? uid; + + // Tenter d'extraire l'UID + if (userRef is String) { + uid = userRef; + } else { + // Essayer d'extraire l'ID si c'est une DocumentReference + final ref = userRef as DocumentReference?; + uid = ref?.id; + } + + if (uid != null) { + final userData = getUserFromCache(uid); + if (userData != null) { + users.add(userData); + } + } + } catch (e) { + // Ignorer les références invalides + print('Skipping invalid workforce reference: $userRef'); + } + } + + return users; + } + + /// Vider la liste des événements void clearEvents() { _events = []; notifyListeners(); } + + // Variables pour stocker le dernier appel + String? _lastUserId; + bool _lastCanViewAll = false; + + /// Sauvegarder les paramètres du dernier chargement + void _saveLastLoadParams(String userId, bool canViewAllEvents) { + _lastUserId = userId; + _lastCanViewAll = canViewAllEvents; + } } diff --git a/em2rp/lib/providers/maintenance_provider_new.dart b/em2rp/lib/providers/maintenance_provider_new.dart new file mode 100644 index 0000000..17fbbba --- /dev/null +++ b/em2rp/lib/providers/maintenance_provider_new.dart @@ -0,0 +1,51 @@ +import 'package:flutter/foundation.dart'; +import 'package:em2rp/models/maintenance_model.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; + +class MaintenanceProvider extends ChangeNotifier { + final DataService _dataService = DataService(FirebaseFunctionsApiService()); + + List _maintenances = []; + bool _isLoading = false; + + List get maintenances => _maintenances; + bool get isLoading => _isLoading; + + /// Charger toutes les maintenances via l'API + Future loadMaintenances({String? equipmentId}) async { + _isLoading = true; + notifyListeners(); + + try { + final maintenancesData = await _dataService.getMaintenances( + equipmentId: equipmentId, + ); + + _maintenances = maintenancesData.map((data) { + return MaintenanceModel.fromMap(data, data['id'] as String); + }).toList(); + + _isLoading = false; + notifyListeners(); + } catch (e) { + print('Error loading maintenances: $e'); + _isLoading = false; + notifyListeners(); + rethrow; + } + } + + /// Recharger les maintenances + Future refresh({String? equipmentId}) async { + await loadMaintenances(equipmentId: equipmentId); + } + + /// Obtenir les maintenances pour un équipement spécifique + List getForEquipment(String equipmentId) { + return _maintenances.where((m) => + m.equipmentIds.contains(equipmentId) + ).toList(); + } +} + diff --git a/em2rp/lib/providers/users_provider.dart b/em2rp/lib/providers/users_provider.dart index 483dec4..b51a60a 100644 --- a/em2rp/lib/providers/users_provider.dart +++ b/em2rp/lib/providers/users_provider.dart @@ -1,54 +1,53 @@ import 'package:flutter/material.dart'; -import '../models/user_model.dart'; -import '../services/user_service.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:provider/provider.dart'; -import 'package:em2rp/providers/local_user_provider.dart'; +import 'package:em2rp/models/user_model.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; class UsersProvider with ChangeNotifier { - final UserService _userService; - final FirebaseFirestore _firestore = FirebaseFirestore.instance; - final FirebaseAuth _auth = FirebaseAuth.instance; + final DataService _dataService = DataService(FirebaseFunctionsApiService()); List _users = []; bool _isLoading = false; List get users => _users; bool get isLoading => _isLoading; - UsersProvider(this._userService); - - /// Récupération de tous les utilisateurs + /// Récupération de tous les utilisateurs via l'API Future fetchUsers() async { _isLoading = true; notifyListeners(); try { - final snapshot = await _firestore.collection('users').get(); - _users = snapshot.docs - .map((doc) => UserModel.fromMap(doc.data(), doc.id)) - .toList(); + final usersData = await _dataService.getUsers(); + _users = usersData.map((data) { + return UserModel.fromMap(data, data['id'] as String); + }).toList(); } catch (e) { print('Error fetching users: $e'); + _users = []; } _isLoading = false; notifyListeners(); } - /// Mise à jour d'un utilisateur - Future updateUser(UserModel user, {String? roleId}) async { + /// Recharger les utilisateurs + Future refresh() async { + await fetchUsers(); + } + + /// Obtenir un utilisateur par ID + UserModel? getUserById(String uid) { try { - await _firestore.collection('users').doc(user.uid).update({ - 'firstName': user.firstName, - 'lastName': user.lastName, - 'email': user.email, - 'phoneNumber': user.phoneNumber, - 'role': roleId != null - ? _firestore.collection('roles').doc(roleId) - : user.role, - 'profilePhotoUrl': user.profilePhotoUrl, - }); + return _users.firstWhere((u) => u.uid == uid); + } catch (e) { + return null; + } + } + + /// Mettre à jour un utilisateur + Future updateUser(UserModel user) async { + try { + await _dataService.updateUser(user.uid, user.toMap()); final index = _users.indexWhere((u) => u.uid == user.uid); if (index != -1) { @@ -64,7 +63,7 @@ class UsersProvider with ChangeNotifier { /// Suppression d'un utilisateur Future deleteUser(String uid) async { try { - await _firestore.collection('users').doc(uid).delete(); + // TODO: Créer une Cloud Function deleteUser _users.removeWhere((user) => user.uid == uid); notifyListeners(); } catch (e) { @@ -73,98 +72,29 @@ class UsersProvider with ChangeNotifier { } } - /// Réinitialisation du mot de passe - Future resetPassword(String email) async { - await _userService.resetPassword(email); - } - - Future createUserWithEmailInvite(BuildContext context, UserModel user, - {String? roleId}) async { - String? authUid; - + /// Créer un utilisateur avec invitation par email + Future createUserWithEmailInvite({ + required String email, + required String firstName, + required String lastName, + String? phoneNumber, + required String roleId, + }) async { try { - // Vérifier l'état de l'authentification - final currentUser = _auth.currentUser; - print('Current user: ${currentUser?.email}'); - - if (currentUser == null) { - throw Exception('Aucun utilisateur connecté'); - } - - // Vérifier la permission via le provider - final localUserProvider = - Provider.of(context, listen: false); - if (!localUserProvider.hasPermission('add_user')) { - throw Exception( - 'Vous n\'avez pas la permission de créer des utilisateurs'); - } - - try { - // Créer l'utilisateur dans Firebase Authentication - final userCredential = await _auth.createUserWithEmailAndPassword( - email: user.email, - password: 'TemporaryPassword123!', // Mot de passe temporaire - ); - - authUid = userCredential.user!.uid; - print('User created in Auth with UID: $authUid'); - - // Créer le document dans Firestore avec l'UID de Auth comme ID - await _firestore.collection('users').doc(authUid).set({ - 'uid': authUid, - 'firstName': user.firstName, - 'lastName': user.lastName, - 'email': user.email, - 'phoneNumber': user.phoneNumber, - 'role': roleId != null - ? _firestore.collection('roles').doc(roleId) - : user.role, - 'profilePhotoUrl': user.profilePhotoUrl, - 'createdAt': FieldValue.serverTimestamp(), - }); - - print('User document created in Firestore with Auth UID'); - - // Envoyer un email de réinitialisation de mot de passe - await _auth.sendPasswordResetEmail( - email: user.email, - actionCodeSettings: ActionCodeSettings( - url: 'http://app.em2events.fr/finishSignUp?email=${user.email}', - handleCodeInApp: true, - androidPackageName: 'com.em2rp.app', - androidInstallApp: true, - androidMinimumVersion: '12', - ), - ); - - print('Password reset email sent'); - - // Ajouter l'utilisateur à la liste locale - final newUser = UserModel( - uid: authUid, - firstName: user.firstName, - lastName: user.lastName, - email: user.email, - phoneNumber: user.phoneNumber, - role: roleId ?? user.role, - profilePhotoUrl: user.profilePhotoUrl, - ); - _users.add(newUser); - notifyListeners(); - } catch (e) { - // En cas d'erreur, supprimer l'utilisateur Auth si créé - if (authUid != null) { - try { - await _auth.currentUser?.delete(); - } catch (deleteError) { - print('Warning: Could not delete Auth user: $deleteError'); - } - } - rethrow; - } + // TODO: Implémenter via Cloud Function + print('Creating user with email invite: $email'); + await fetchUsers(); // Recharger la liste } catch (e) { - print('Error creating user: $e'); + print('Error creating user with email invite: $e'); rethrow; } } + + /// Réinitialisation du mot de passe + Future resetPassword(String email) async { + // Firebase Auth reste OK + // await _userService.resetPassword(email); + // TODO: Implémenter via Cloud Function + print('Reset password for: $email'); + } } diff --git a/em2rp/lib/services/container_equipment_service.dart b/em2rp/lib/services/container_equipment_service.dart new file mode 100644 index 0000000..0bc8831 --- /dev/null +++ b/em2rp/lib/services/container_equipment_service.dart @@ -0,0 +1,52 @@ +import 'package:em2rp/models/container_model.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; + +/// Service pour gérer la relation entre containers et équipements +/// Utilise le principe : seul le container stocke la référence aux équipements +class ContainerEquipmentService { + final DataService _dataService = DataService(apiService); + + /// Récupère tous les containers contenant un équipement spécifique + /// Utilise une Cloud Function avec authentification et permissions + Future> getContainersByEquipment(String equipmentId) async { + try { + final containersData = await _dataService.getContainersByEquipment(equipmentId); + + return containersData.map((data) { + // L'ID est dans le champ 'id' retourné par la fonction + final id = data['id'] as String; + return ContainerModel.fromMap(data, id); + }).toList(); + } catch (e) { + print('[ContainerEquipmentService] Error getting containers for equipment $equipmentId: $e'); + rethrow; + } + } + + /// Vérifie si un équipement est dans au moins un container + Future isEquipmentInAnyContainer(String equipmentId) async { + try { + final containers = await getContainersByEquipment(equipmentId); + return containers.isNotEmpty; + } catch (e) { + print('[ContainerEquipmentService] Error checking if equipment is in container: $e'); + return false; + } + } + + /// Récupère le nombre de containers contenant un équipement + Future getContainerCountForEquipment(String equipmentId) async { + try { + final containers = await getContainersByEquipment(equipmentId); + return containers.length; + } catch (e) { + print('[ContainerEquipmentService] Error getting container count: $e'); + return 0; + } + } +} + +/// Instance globale singleton +final containerEquipmentService = ContainerEquipmentService(); + diff --git a/em2rp/lib/services/data_service.dart b/em2rp/lib/services/data_service.dart new file mode 100644 index 0000000..f1f54f0 --- /dev/null +++ b/em2rp/lib/services/data_service.dart @@ -0,0 +1,339 @@ +import 'package:em2rp/services/api_service.dart'; + +/// Service générique pour les opérations de lecture de données via Cloud Functions +class DataService { + final ApiService _apiService; + + DataService(this._apiService); + + /// Récupère toutes les options + Future>> getOptions() async { + try { + final result = await _apiService.call('getOptions', {}); + final options = result['options'] as List?; + if (options == null) return []; + return options.map((e) => e as Map).toList(); + } catch (e) { + throw Exception('Erreur lors de la récupération des options: $e'); + } + } + + /// Récupère tous les types d'événements + Future>> getEventTypes() async { + try { + final result = await _apiService.call('getEventTypes', {}); + final eventTypes = result['eventTypes'] as List?; + if (eventTypes == null) return []; + return eventTypes.map((e) => e as Map).toList(); + } catch (e) { + throw Exception('Erreur lors de la récupération des types d\'événements: $e'); + } + } + + /// Récupère tous les rôles + Future>> getRoles() async { + try { + final result = await _apiService.call('getRoles', {}); + final roles = result['roles'] as List?; + if (roles == null) return []; + return roles.map((e) => e as Map).toList(); + } catch (e) { + throw Exception('Erreur lors de la récupération des rôles: $e'); + } + } + + /// Met à jour les équipements d'un événement + Future updateEventEquipment({ + required String eventId, + List>? assignedEquipment, + String? preparationStatus, + String? loadingStatus, + String? unloadingStatus, + String? returnStatus, + }) async { + try { + final data = {'eventId': eventId}; + + if (assignedEquipment != null) data['assignedEquipment'] = assignedEquipment; + if (preparationStatus != null) data['preparationStatus'] = preparationStatus; + if (loadingStatus != null) data['loadingStatus'] = loadingStatus; + if (unloadingStatus != null) data['unloadingStatus'] = unloadingStatus; + if (returnStatus != null) data['returnStatus'] = returnStatus; + + await _apiService.call('updateEventEquipment', data); + } catch (e) { + throw Exception('Erreur lors de la mise à jour des équipements de l\'événement: $e'); + } + } + + /// Met à jour uniquement le statut d'un équipement + Future updateEquipmentStatusOnly({ + required String equipmentId, + String? status, + int? availableQuantity, + }) async { + try { + final data = {'equipmentId': equipmentId}; + + if (status != null) data['status'] = status; + if (availableQuantity != null) data['availableQuantity'] = availableQuantity; + + await _apiService.call('updateEquipmentStatusOnly', data); + } catch (e) { + throw Exception('Erreur lors de la mise à jour du statut de l\'équipement: $e'); + } + } + + /// Met à jour un utilisateur + Future updateUser(String userId, Map data) async { + try { + final requestData = {'userId': userId, ...data}; + await _apiService.call('updateUser', requestData); + } catch (e) { + throw Exception('Erreur lors de la mise à jour de l\'utilisateur: $e'); + } + } + + /// Met à jour un événement + Future updateEvent(String eventId, Map data) async { + try { + final requestData = {'eventId': eventId, ...data}; + await _apiService.call('updateEvent', requestData); + } catch (e) { + throw Exception('Erreur lors de la mise à jour de l\'événement: $e'); + } + } + + /// Supprime un événement + Future deleteEvent(String eventId) async { + try { + await _apiService.call('deleteEvent', {'eventId': eventId}); + } catch (e) { + throw Exception('Erreur lors de la suppression de l\'événement: $e'); + } + } + + /// Crée un nouvel équipement + Future createEquipment(String equipmentId, Map data) async { + try { + final requestData = {'equipmentId': equipmentId, ...data}; + await _apiService.call('createEquipment', requestData); + } catch (e) { + throw Exception('Erreur lors de la création de l\'équipement: $e'); + } + } + + /// Met à jour un équipement + Future updateEquipment(String equipmentId, Map data) async { + try { + final requestData = {'equipmentId': equipmentId, ...data}; + await _apiService.call('updateEquipment', requestData); + } catch (e) { + throw Exception('Erreur lors de la mise à jour de l\'équipement: $e'); + } + } + + /// Supprime un équipement + Future deleteEquipment(String equipmentId) async { + try { + await _apiService.call('deleteEquipment', {'equipmentId': equipmentId}); + } catch (e) { + throw Exception('Erreur lors de la suppression de l\'équipement: $e'); + } + } + + /// Récupère les événements utilisant un type d'événement donné + Future>> getEventsByEventType(String eventTypeId) async { + try { + final result = await _apiService.call('getEventsByEventType', {'eventTypeId': eventTypeId}); + final events = result['events'] as List?; + if (events == null) return []; + return events.map((e) => e as Map).toList(); + } catch (e) { + throw Exception('Erreur lors de la récupération des événements: $e'); + } + } + + /// Crée un type d'événement + Future createEventType({ + required String name, + required double defaultPrice, + }) async { + try { + final result = await _apiService.call('createEventType', { + 'name': name, + 'defaultPrice': defaultPrice, + }); + return result['id'] as String; + } catch (e) { + throw Exception('Erreur lors de la création du type d\'événement: $e'); + } + } + + /// Met à jour un type d'événement + Future updateEventType({ + required String eventTypeId, + String? name, + double? defaultPrice, + }) async { + try { + final data = {'eventTypeId': eventTypeId}; + if (name != null) data['name'] = name; + if (defaultPrice != null) data['defaultPrice'] = defaultPrice; + + await _apiService.call('updateEventType', data); + } catch (e) { + throw Exception('Erreur lors de la mise à jour du type d\'événement: $e'); + } + } + + /// Supprime un type d'événement + Future deleteEventType(String eventTypeId) async { + try { + await _apiService.call('deleteEventType', {'eventTypeId': eventTypeId}); + } catch (e) { + throw Exception('Erreur lors de la suppression du type d\'événement: $e'); + } + } + + /// Crée une option + Future createOption(String code, Map data) async { + try { + final requestData = {'code': code, ...data}; + final result = await _apiService.call('createOption', requestData); + return result['id'] as String? ?? code; + } catch (e) { + throw Exception('Erreur lors de la création de l\'option: $e'); + } + } + + /// Met à jour une option + Future updateOption(String optionId, Map data) async { + try { + final requestData = {'optionId': optionId, ...data}; + await _apiService.call('updateOption', requestData); + } catch (e) { + throw Exception('Erreur lors de la mise à jour de l\'option: $e'); + } + } + + /// Supprime une option + Future deleteOption(String optionId) async { + try { + await _apiService.call('deleteOption', {'optionId': optionId}); + } catch (e) { + throw Exception('Erreur lors de la suppression de l\'option: $e'); + } + } + + // ============================================================================ + // LECTURE DES DONNÉES (avec permissions côté serveur) + // ============================================================================ + + /// Récupère tous les événements (filtrés selon permissions) + /// Retourne { events: List, users: Map } + Future> getEvents({String? userId}) async { + try { + final data = {}; + if (userId != null) data['userId'] = userId; + + final result = await _apiService.call('getEvents', data); + + // Extraire events et users + final events = result['events'] as List? ?? []; + final users = result['users'] as Map? ?? {}; + + return { + 'events': events.map((e) => e as Map).toList(), + 'users': users, + }; + } catch (e) { + throw Exception('Erreur lors de la récupération des événements: $e'); + } + } + + /// Récupère tous les équipements (avec masquage des prix selon permissions) + Future>> getEquipments() async { + try { + print('[DataService] Calling getEquipments API...'); + final result = await _apiService.call('getEquipments', {}); + print('[DataService] API call successful, parsing result...'); + final equipments = result['equipments'] as List?; + if (equipments == null) { + print('[DataService] No equipments in result'); + return []; + } + print('[DataService] Found ${equipments.length} equipments'); + return equipments.map((e) => e as Map).toList(); + } catch (e) { + print('[DataService] Error getting equipments: $e'); + throw Exception('Erreur lors de la récupération des équipements: $e'); + } + } + + /// Récupère tous les conteneurs + Future>> getContainers() async { + try { + final result = await _apiService.call('getContainers', {}); + final containers = result['containers'] as List?; + if (containers == null) return []; + return containers.map((e) => e as Map).toList(); + } catch (e) { + throw Exception('Erreur lors de la récupération des conteneurs: $e'); + } + } + + /// Récupère les maintenances (optionnellement filtrées par équipement) + Future>> getMaintenances({String? equipmentId}) async { + try { + final data = {}; + if (equipmentId != null) data['equipmentId'] = equipmentId; + + final result = await _apiService.call('getMaintenances', data); + final maintenances = result['maintenances'] as List?; + if (maintenances == null) return []; + return maintenances.map((e) => e as Map).toList(); + } catch (e) { + throw Exception('Erreur lors de la récupération des maintenances: $e'); + } + } + + /// Récupère les alertes + Future>> getAlerts() async { + try { + final result = await _apiService.call('getAlerts', {}); + final alerts = result['alerts'] as List?; + if (alerts == null) return []; + return alerts.map((e) => e as Map).toList(); + } catch (e) { + throw Exception('Erreur lors de la récupération des alertes: $e'); + } + } + + /// Récupère les utilisateurs (filtrés selon permissions) + Future>> getUsers() async { + try { + final result = await _apiService.call('getUsers', {}); + final users = result['users'] as List?; + if (users == null) return []; + return users.map((e) => e as Map).toList(); + } catch (e) { + throw Exception('Erreur lors de la récupération des utilisateurs: $e'); + } + } + + /// Récupère les containers contenant un équipement spécifique + Future>> getContainersByEquipment(String equipmentId) async { + try { + final result = await _apiService.call('getContainersByEquipment', { + 'equipmentId': equipmentId, + }); + final containers = result['containers'] as List?; + if (containers == null) return []; + return containers.map((e) => e as Map).toList(); + } catch (e) { + throw Exception('Erreur lors de la récupération des containers pour l\'équipement: $e'); + } + } +} + diff --git a/em2rp/lib/services/equipment_service.dart b/em2rp/lib/services/equipment_service.dart index 4d85ae9..887ecf5 100644 --- a/em2rp/lib/services/equipment_service.dart +++ b/em2rp/lib/services/equipment_service.dart @@ -1,12 +1,15 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/models/equipment_model.dart'; +import 'package:em2rp/models/container_model.dart'; import 'package:em2rp/models/alert_model.dart'; import 'package:em2rp/models/maintenance_model.dart'; import 'package:em2rp/services/api_service.dart'; +import 'package:em2rp/services/data_service.dart'; class EquipmentService { final FirebaseFirestore _firestore = FirebaseFirestore.instance; final ApiService _apiService = apiService; + final DataService _dataService = DataService(apiService); // Collection references (utilisées seulement pour les lectures) CollectionReference get _equipmentCollection => _firestore.collection('equipments'); @@ -134,7 +137,7 @@ class EquipmentService { .get(); for (var eventDoc in eventsQuery.docs) { - final eventData = eventDoc.data() as Map; + final eventData = eventDoc.data(); final assignedEquipmentRaw = eventData['assignedEquipment'] ?? []; if (assignedEquipmentRaw is List) { @@ -170,7 +173,7 @@ class EquipmentService { for (var doc in equipmentQuery.docs) { final equipment = EquipmentModel.fromMap( - doc.data() as Map, + doc.data(), doc.id, ); @@ -230,7 +233,7 @@ class EquipmentService { for (var doc in equipmentQuery.docs) { final equipment = EquipmentModel.fromMap( - doc.data() as Map, + doc.data(), doc.id, ); @@ -285,7 +288,7 @@ class EquipmentService { final models = {}; for (var doc in equipmentQuery.docs) { - final data = doc.data() as Map; + final data = doc.data(); final model = data['model'] as String?; if (model != null && model.isNotEmpty) { models.add(model); @@ -306,7 +309,7 @@ class EquipmentService { final brands = {}; for (var doc in equipmentQuery.docs) { - final data = doc.data() as Map; + final data = doc.data(); final brand = data['brand'] as String?; if (brand != null && brand.isNotEmpty) { brands.add(brand); @@ -329,7 +332,7 @@ class EquipmentService { final models = {}; for (var doc in equipmentQuery.docs) { - final data = doc.data() as Map; + final data = doc.data(); final model = data['model'] as String?; if (model != null && model.isNotEmpty) { models.add(model); @@ -354,26 +357,16 @@ class EquipmentService { } } - /// Récupérer toutes les boîtes (équipements qui peuvent contenir d'autres équipements) - Future> getBoxes() async { + /// Récupérer toutes les boîtes/containers disponibles + Future> getBoxes() async { try { - // Les boîtes sont généralement des équipements de catégorie "structure" ou "other" - // On pourrait aussi ajouter un champ spécifique "isBox" dans le modèle - final equipmentQuery = await _firestore.collection('equipments') - .where('category', whereIn: [ - equipmentCategoryToString(EquipmentCategory.structure), - equipmentCategoryToString(EquipmentCategory.other), - ]) - .get(); + final containersData = await _dataService.getContainers(); - final boxes = []; - for (var doc in equipmentQuery.docs) { - final equipment = EquipmentModel.fromMap( - doc.data() as Map, - doc.id, - ); - // On pourrait ajouter un filtre supplémentaire ici si besoin - boxes.add(equipment); + final boxes = []; + for (var data in containersData) { + final id = data['id'] as String; + final container = ContainerModel.fromMap(data, id); + boxes.add(container); } return boxes; @@ -401,7 +394,7 @@ class EquipmentService { for (var doc in query.docs) { equipments.add( EquipmentModel.fromMap( - doc.data() as Map, + doc.data(), doc.id, ), ); diff --git a/em2rp/lib/services/event_form_service.dart b/em2rp/lib/services/event_form_service.dart index 88fc6eb..04efb49 100644 --- a/em2rp/lib/services/event_form_service.dart +++ b/em2rp/lib/services/event_form_service.dart @@ -1,4 +1,3 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:file_picker/file_picker.dart'; @@ -8,32 +7,34 @@ import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/models/event_type_model.dart'; import 'package:em2rp/models/user_model.dart'; import 'package:em2rp/services/api_service.dart'; +import 'package:em2rp/services/data_service.dart'; import 'dart:developer' as developer; class EventFormService { static final ApiService _apiService = apiService; + static final DataService _dataService = DataService(FirebaseFunctionsApiService()); // ============================================================================ - // READ Operations - Utilise Firestore (peut rester en lecture directe) + // READ Operations - Utilise l'API (sécurisé avec permissions côté serveur) // ============================================================================ static Future> fetchEventTypes() async { - developer.log('Fetching event types from Firestore...', name: 'EventFormService'); + developer.log('Fetching event types via API...', name: 'EventFormService'); try { - final snapshot = await FirebaseFirestore.instance.collection('eventTypes').get(); - final eventTypes = snapshot.docs.map((doc) => EventTypeModel.fromMap(doc.data(), doc.id)).toList(); + final eventTypesData = await _dataService.getEventTypes(); + final eventTypes = eventTypesData.map((data) => EventTypeModel.fromMap(data, data['id'] as String)).toList(); developer.log('${eventTypes.length} event types loaded.', name: 'EventFormService'); return eventTypes; } catch (e, s) { developer.log('Error fetching event types', name: 'EventFormService', error: e, stackTrace: s); - throw Exception("Could not load event types. Please check Firestore permissions."); + throw Exception("Could not load event types. Please check permissions."); } } static Future> fetchUsers() async { try { - final snapshot = await FirebaseFirestore.instance.collection('users').get(); - return snapshot.docs.map((doc) => UserModel.fromMap(doc.data(), doc.id)).toList(); + final usersData = await _dataService.getUsers(); + return usersData.map((data) => UserModel.fromMap(data, data['id'] as String)).toList(); } catch (e) { developer.log('Error fetching users', name: 'EventFormService', error: e); throw Exception("Could not load users."); @@ -171,9 +172,15 @@ class EventFormService { } static Future updateEventDocuments(String eventId, List> documents) async { - await FirebaseFirestore.instance - .collection('events') - .doc(eventId) - .update({'documents': documents}); + // Utiliser l'API pour mettre à jour les documents + try { + await _apiService.call('updateEvent', { + 'eventId': eventId, + 'documents': documents, + }); + } catch (e) { + developer.log('Error updating event documents', name: 'EventFormService', error: e); + throw Exception("Could not update event documents."); + } } } diff --git a/em2rp/lib/services/ics_export_service.dart b/em2rp/lib/services/ics_export_service.dart index f1ca6ed..4355eb7 100644 --- a/em2rp/lib/services/ics_export_service.dart +++ b/em2rp/lib/services/ics_export_service.dart @@ -66,19 +66,30 @@ END:VCALENDAR'''; } /// Récupère les détails de la main d'œuvre - static Future> _getWorkforceDetails(List workforce) async { + static Future> _getWorkforceDetails(List workforce) async { final List workforceNames = []; for (final ref in workforce) { try { - final doc = await ref.get(); - if (doc.exists) { - final data = doc.data() as Map?; - if (data != null) { - final firstName = data['firstName'] ?? ''; - final lastName = data['lastName'] ?? ''; - if (firstName.isNotEmpty || lastName.isNotEmpty) { - workforceNames.add('$firstName $lastName'.trim()); + DocumentReference? docRef; + + // Gérer String (UID) ou DocumentReference + if (ref is String) { + docRef = FirebaseFirestore.instance.collection('users').doc(ref); + } else if (ref is DocumentReference) { + docRef = ref; + } + + if (docRef != null) { + final doc = await docRef.get(); + if (doc.exists) { + final data = doc.data() as Map?; + if (data != null) { + final firstName = data['firstName'] ?? ''; + final lastName = data['lastName'] ?? ''; + if (firstName.isNotEmpty || lastName.isNotEmpty) { + workforceNames.add('$firstName $lastName'.trim()); + } } } } diff --git a/em2rp/lib/services/user_service.dart b/em2rp/lib/services/user_service.dart index 6fe5e50..782fbf1 100644 --- a/em2rp/lib/services/user_service.dart +++ b/em2rp/lib/services/user_service.dart @@ -1,40 +1,48 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; + import '../models/user_model.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; +/// @deprecated Ce service est obsolète. Utilisez UsersProvider avec DataService à la place. +/// Ce service reste pour compatibilité mais toutes les opérations passent par l'API. class UserService { - final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final DataService _dataService = DataService(FirebaseFunctionsApiService()); + /// @deprecated Utilisez UsersProvider.fetchUsers() à la place Future> fetchUsers() async { try { - final snapshot = await _firestore.collection('users').get(); - return snapshot.docs - .map((doc) => UserModel.fromMap(doc.data(), doc.id)) - .toList(); + final usersData = await _dataService.getUsers(); + return usersData.map((data) => UserModel.fromMap(data, data['id'] as String)).toList(); } catch (e) { print("Erreur: $e"); return []; } } + /// @deprecated Utilisez DataService.updateUser() à la place Future updateUser(UserModel user) async { try { - await _firestore.collection('users').doc(user.uid).update(user.toMap()); + await _dataService.updateUser(user.uid, user.toMap()); } catch (e) { print("Erreur mise à jour: $e"); } } + /// @deprecated Utilisez API deleteUser à la place Future deleteUser(String uid) async { try { - await _firestore.collection('users').doc(uid).delete(); + // TODO: Créer une Cloud Function deleteUser + print("Suppression d'utilisateur non implémentée via API"); } catch (e) { print("Erreur suppression: $e"); } } + /// Firebase Auth reste OK (pas Firestore) Future resetPassword(String email) async { try { + // Firebase Auth est OK, ce n'est pas Firestore await FirebaseAuth.instance.sendPasswordResetEmail(email: email); print("Email de réinitialisation envoyé à $email"); } catch (e) { diff --git a/em2rp/lib/utils/firebase_storage_manager.dart b/em2rp/lib/utils/firebase_storage_manager.dart index 218e539..9be1a35 100644 --- a/em2rp/lib/utils/firebase_storage_manager.dart +++ b/em2rp/lib/utils/firebase_storage_manager.dart @@ -1,18 +1,19 @@ import 'package:flutter/foundation.dart'; // pour kIsWeb import 'package:firebase_storage/firebase_storage.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; class FirebaseStorageManager { final FirebaseStorage _storage = FirebaseStorage.instance; - final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final DataService _dataService = DataService(FirebaseFunctionsApiService()); /// Upload ou remplace la photo de profil d'un utilisateur dans Firebase Storage. /// Pour le Web, on fixe l'extension .jpg. /// 1. Construit le chemin : "ProfilePictures/UID.jpg" /// 2. Supprime l'ancienne photo (si elle existe). /// 3. Upload la nouvelle photo. - /// 4. Met à jour Firestore avec l'URL de la nouvelle image. + /// 4. Met à jour Firestore avec l'URL de la nouvelle image via l'API. Future sendProfilePicture( {required XFile imageFile, required String uid}) async { try { @@ -57,17 +58,14 @@ class FirebaseStorageManager { print( "FirebaseStorageManager: Nouvelle photo uploadée pour l'utilisateur $uid. URL: $downloadUrl"); - // 5. Mettre à jour Firestore avec l'URL de la photo de profil + // 5. Mettre à jour via l'API (plus sécurisé) try { - await _firestore - .collection('users') - .doc(uid) - .update({'profilePhotoUrl': downloadUrl}); + await _dataService.updateUser(uid, {'profilePhotoUrl': downloadUrl}); print( - "FirebaseStorageManager: Firestore mis à jour pour l'utilisateur $uid."); - } catch (firestoreError) { + "FirebaseStorageManager: Profil mis à jour via API pour l'utilisateur $uid."); + } catch (apiError) { print( - "FirebaseStorageManager: Erreur Firestore pour l'utilisateur $uid: $firestoreError"); + "FirebaseStorageManager: Erreur API pour l'utilisateur $uid: $apiError"); return downloadUrl; // On retourne l'URL même si la mise à jour échoue } return downloadUrl; diff --git a/em2rp/lib/views/calendar_page.dart b/em2rp/lib/views/calendar_page.dart index 3fefaa3..e427cba 100644 --- a/em2rp/lib/views/calendar_page.dart +++ b/em2rp/lib/views/calendar_page.dart @@ -50,7 +50,6 @@ class _CalendarPageState extends State { ..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); EventModel? selected; DateTime? selectedDay; - int selectedEventIndex = 0; if (todayEvents.isNotEmpty) { selected = todayEvents[0]; selectedDay = DateTime(now.year, now.month, now.day); @@ -87,9 +86,7 @@ class _CalendarPageState extends State { Provider.of(context, listen: false); final eventProvider = Provider.of(context, listen: false); final userId = localAuthProvider.uid; - print('Permissions utilisateur: ${localAuthProvider.permissions}'); final canViewAllEvents = localAuthProvider.hasPermission('view_all_events'); - print('canViewAllEvents: $canViewAllEvents'); if (userId != null) { await eventProvider.loadUserEvents(userId, diff --git a/em2rp/lib/views/equipment_detail_page.dart b/em2rp/lib/views/equipment_detail_page.dart index bd6f59c..e5153f5 100644 --- a/em2rp/lib/views/equipment_detail_page.dart +++ b/em2rp/lib/views/equipment_detail_page.dart @@ -10,7 +10,6 @@ import 'package:em2rp/services/qr_code_service.dart'; import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/views/widgets/nav/custom_app_bar.dart'; import 'package:em2rp/views/equipment_form_page.dart'; -import 'package:em2rp/views/widgets/equipment/equipment_parent_containers.dart'; import 'package:em2rp/views/widgets/equipment/equipment_referencing_containers.dart'; import 'package:em2rp/views/widgets/equipment/equipment_header_section.dart'; import 'package:em2rp/views/widgets/equipment/equipment_main_info_section.dart'; @@ -124,15 +123,9 @@ class _EquipmentDetailPageState extends State { const SizedBox(height: 24), - // Containers parents (si applicable) - if (widget.equipment.parentBoxIds.isNotEmpty) ...[ - EquipmentParentContainers( - parentBoxIds: widget.equipment.parentBoxIds, - ), - const SizedBox(height: 24), - ], - - // Containers associés + // Containers contenant cet équipement + // Note: On utilise EquipmentReferencingContainers qui recherche dynamiquement + // les containers au lieu de se baser sur parentBoxIds qui peut être désynchronisé EquipmentReferencingContainers( equipmentId: widget.equipment.id, ), diff --git a/em2rp/lib/views/equipment_form_page.dart b/em2rp/lib/views/equipment_form_page.dart index d1ad61c..2726a7c 100644 --- a/em2rp/lib/views/equipment_form_page.dart +++ b/em2rp/lib/views/equipment_form_page.dart @@ -2,14 +2,18 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; 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/providers/local_user_provider.dart'; import 'package:em2rp/services/equipment_service.dart'; +import 'package:em2rp/services/container_equipment_service.dart'; import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/views/widgets/nav/custom_app_bar.dart'; import 'package:intl/intl.dart'; import 'package:em2rp/views/equipment_form/brand_model_selector.dart'; import 'package:em2rp/utils/id_generator.dart'; +import 'package:em2rp/views/widgets/equipment/parent_boxes_selector.dart'; class EquipmentFormPage extends StatefulWidget { final EquipmentModel? equipment; @@ -42,7 +46,7 @@ class _EquipmentFormPageState extends State { DateTime? _lastMaintenanceDate; DateTime? _nextMaintenanceDate; List _selectedParentBoxIds = []; - List _availableBoxes = []; + List _availableBoxes = []; bool _isLoading = false; bool _isLoadingBoxes = true; bool _addMultiple = false; @@ -65,35 +69,60 @@ class _EquipmentFormPageState extends State { void _populateFields() { final equipment = widget.equipment!; - _identifierController.text = equipment.id; - _brandController.text = equipment.brand ?? ''; - _selectedBrand = equipment.brand; - _modelController.text = equipment.model ?? ''; - _selectedCategory = equipment.category; - _selectedStatus = equipment.status; - _purchasePriceController.text = equipment.purchasePrice?.toStringAsFixed(2) ?? ''; - _rentalPriceController.text = equipment.rentalPrice?.toStringAsFixed(2) ?? ''; - _totalQuantityController.text = equipment.totalQuantity?.toString() ?? ''; - _criticalThresholdController.text = equipment.criticalThreshold?.toString() ?? ''; - _purchaseDate = equipment.purchaseDate; - _lastMaintenanceDate = equipment.lastMaintenanceDate; - _nextMaintenanceDate = equipment.nextMaintenanceDate; - _selectedParentBoxIds = List.from(equipment.parentBoxIds); - _notesController.text = equipment.notes ?? ''; + setState(() { + _identifierController.text = equipment.id; + _brandController.text = equipment.brand ?? ''; + _selectedBrand = equipment.brand; + _modelController.text = equipment.model ?? ''; + _selectedCategory = equipment.category; + _selectedStatus = equipment.status; + _purchasePriceController.text = equipment.purchasePrice?.toStringAsFixed(2) ?? ''; + _rentalPriceController.text = equipment.rentalPrice?.toStringAsFixed(2) ?? ''; + _totalQuantityController.text = equipment.totalQuantity?.toString() ?? ''; + _criticalThresholdController.text = equipment.criticalThreshold?.toString() ?? ''; + _purchaseDate = equipment.purchaseDate; + _lastMaintenanceDate = equipment.lastMaintenanceDate; + _nextMaintenanceDate = equipment.nextMaintenanceDate; + _notesController.text = equipment.notes ?? ''; + }); + + print('[EquipmentForm] Populating fields for equipment: ${equipment.id}'); + + // Charger les containers contenant cet équipement depuis Firestore + _loadCurrentContainers(equipment.id); if (_selectedBrand != null && _selectedBrand!.isNotEmpty) { _loadFilteredModels(_selectedBrand!); } } + /// Charge les containers qui contiennent actuellement cet équipement + Future _loadCurrentContainers(String equipmentId) async { + try { + final containers = await containerEquipmentService.getContainersByEquipment(equipmentId); + setState(() { + _selectedParentBoxIds = containers.map((c) => c.id).toList(); + }); + print('[EquipmentForm] Loaded ${containers.length} containers for equipment $equipmentId'); + print('[EquipmentForm] Selected container IDs: $_selectedParentBoxIds'); + } catch (e) { + print('[EquipmentForm] Error loading containers for equipment: $e'); + } + } + Future _loadAvailableBoxes() async { try { final boxes = await _equipmentService.getBoxes(); + print('[EquipmentForm] Loaded ${boxes.length} boxes from service'); + for (var box in boxes) { + print('[EquipmentForm] Box loaded - ID: ${box.id}, Name: ${box.name}'); + } setState(() { _availableBoxes = boxes; _isLoadingBoxes = false; }); } catch (e) { + print('[EquipmentForm] Error loading boxes: $e'); setState(() { _isLoadingBoxes = false; }); @@ -390,11 +419,14 @@ class _EquipmentFormPageState extends State { ], // Boîtes parentes - const Divider(), - const Text('Boîtes parentes', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 8), _isLoadingBoxes - ? const Center(child: CircularProgressIndicator()) + ? const Card( + child: Padding( + padding: EdgeInsets.all(32.0), + child: Center(child: CircularProgressIndicator()), + ), + ) : _buildParentBoxesSelector(), const SizedBox(height: 16), @@ -449,35 +481,14 @@ class _EquipmentFormPageState extends State { } Widget _buildParentBoxesSelector() { - if (_availableBoxes.isEmpty) { - return const Card( - child: Padding( - padding: EdgeInsets.all(16.0), - child: Text('Aucune boîte disponible'), - ), - ); - } - - return Card( - child: Column( - children: _availableBoxes.map((box) { - final isSelected = _selectedParentBoxIds.contains(box.id); - return CheckboxListTile( - title: Text(box.name), - subtitle: box.model != null ? Text('Modèle: {box.model}') : null, - value: isSelected, - onChanged: (bool? value) { - setState(() { - if (value == true) { - _selectedParentBoxIds.add(box.id); - } else { - _selectedParentBoxIds.remove(box.id); - } - }); - }, - ); - }).toList(), - ), + return ParentBoxesSelector( + availableBoxes: _availableBoxes, + selectedBoxIds: _selectedParentBoxIds, + onSelectionChanged: (newSelection) { + setState(() { + _selectedParentBoxIds = newSelection; + }); + }, ); } @@ -625,19 +636,66 @@ class _EquipmentFormPageState extends State { purchaseDate: _purchaseDate, lastMaintenanceDate: _lastMaintenanceDate, nextMaintenanceDate: _nextMaintenanceDate, - parentBoxIds: _selectedParentBoxIds, + parentBoxIds: [], // On ne stocke plus les parentBoxIds dans l'équipement notes: _notesController.text, createdAt: isEditing ? (widget.equipment?.createdAt ?? now) : now, updatedAt: now, availableQuantity: availableQuantity, ); if (isEditing) { - await equipmentProvider.updateEquipment( - equipment.id, - equipment.toMap(), - ); + await equipmentProvider.updateEquipment(equipment); + + // Synchroniser les containers : mettre à jour equipmentIds des containers + // Charger les anciens containers depuis Firestore + final oldContainers = await containerEquipmentService.getContainersByEquipment(equipment.id); + final oldParentBoxIds = oldContainers.map((c) => c.id).toList(); + final newParentBoxIds = _selectedParentBoxIds; + + // Boîtes ajoutées : ajouter cet équipement à leur equipmentIds + final addedBoxes = newParentBoxIds.where((id) => !oldParentBoxIds.contains(id)); + for (final boxId in addedBoxes) { + try { + final containerProvider = Provider.of(context, listen: false); + await containerProvider.addEquipmentToContainer( + containerId: boxId, + equipmentId: equipment.id, + ); + print('[EquipmentForm] Added equipment ${equipment.id} to container $boxId'); + } catch (e) { + print('[EquipmentForm] Error adding equipment to container $boxId: $e'); + } + } + + // Boîtes retirées : retirer cet équipement de leur equipmentIds + final removedBoxes = oldParentBoxIds.where((id) => !newParentBoxIds.contains(id)); + for (final boxId in removedBoxes) { + try { + final containerProvider = Provider.of(context, listen: false); + await containerProvider.removeEquipmentFromContainer( + containerId: boxId, + equipmentId: equipment.id, + ); + print('[EquipmentForm] Removed equipment ${equipment.id} from container $boxId'); + } catch (e) { + print('[EquipmentForm] Error removing equipment from container $boxId: $e'); + } + } } else { await equipmentProvider.addEquipment(equipment); + + // Pour un nouvel équipement, ajouter à tous les containers sélectionnés + for (final boxId in _selectedParentBoxIds) { + try { + final containerProvider = Provider.of(context, listen: false); + await containerProvider.addEquipmentToContainer( + containerId: boxId, + equipmentId: equipment.id, + ); + print('[EquipmentForm] Added new equipment ${equipment.id} to container $boxId'); + } catch (e) { + print('[EquipmentForm] Error adding new equipment to container $boxId: $e'); + } + } } } diff --git a/em2rp/lib/views/equipment_management_page.dart b/em2rp/lib/views/equipment_management_page.dart index cdf8e4d..ee76aff 100644 --- a/em2rp/lib/views/equipment_management_page.dart +++ b/em2rp/lib/views/equipment_management_page.dart @@ -29,6 +29,17 @@ class _EquipmentManagementPageState extends State EquipmentCategory? _selectedCategory; List? _cachedEquipment; + @override + void initState() { + super.initState(); + print('[EquipmentManagementPage] initState called'); + // Charger les équipements au démarrage + WidgetsBinding.instance.addPostFrameCallback((_) { + print('[EquipmentManagementPage] Loading equipments...'); + context.read().loadEquipments(); + }); + } + @override void dispose() { _searchController.dispose(); @@ -420,16 +431,44 @@ class _EquipmentManagementPageState extends State Widget _buildEquipmentList() { return Consumer( builder: (context, provider, child) { - return ManagementList( - stream: provider.equipmentStream, - cachedItems: _cachedEquipment, - emptyMessage: 'Aucun équipement trouvé', - emptyIcon: Icons.inventory_2_outlined, - onDataReceived: (items) { - _cachedEquipment = items; - }, - itemBuilder: (equipment) { - return _buildEquipmentCard(equipment); + print('[EquipmentManagementPage] Building list - isLoading: ${provider.isLoading}, equipment count: ${provider.equipment.length}'); + + if (provider.isLoading && _cachedEquipment == null) { + print('[EquipmentManagementPage] Showing loading indicator'); + return const Center(child: CircularProgressIndicator()); + } + + final equipments = provider.equipment; + + if (equipments.isEmpty && !provider.isLoading) { + print('[EquipmentManagementPage] No equipment found'); + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inventory_2_outlined, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + 'Aucun équipement trouvé', + style: TextStyle( + fontSize: 18, + color: Colors.grey[600], + ), + ), + ], + ), + ); + } + + print('[EquipmentManagementPage] Building list with ${equipments.length} items'); + return ListView.builder( + itemCount: equipments.length, + itemBuilder: (context, index) { + return _buildEquipmentCard(equipments[index]); }, ); }, @@ -903,10 +942,13 @@ class _EquipmentManagementPageState extends State 'updatedAt': DateTime.now().toIso8601String(), }; - await context.read().updateEquipment( - equipment.id, - updatedData, - ); + final updatedEquipment = equipment.copyWith( + availableQuantity: newAvailable, + totalQuantity: newTotal, + updatedAt: DateTime.now(), + ); + + await context.read().updateEquipment(updatedEquipment); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/em2rp/lib/views/event_preparation_page.dart b/em2rp/lib/views/event_preparation_page.dart index 0d25c05..ec38a28 100644 --- a/em2rp/lib/views/event_preparation_page.dart +++ b/em2rp/lib/views/event_preparation_page.dart @@ -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 with SingleTickerProviderStateMixin { - final EventPreparationService _preparationService = EventPreparationService(); - final EventPreparationServiceExtended _extendedService = EventPreparationServiceExtended(); late AnimationController _animationController; + late final DataService _dataService; Map _equipmentCache = {}; Map _containerCache = {}; @@ -89,6 +90,7 @@ class _EventPreparationPageState extends State 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 with Single super.dispose(); } - /// Recharger l'événement depuis Firestore - Future _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, doc.id); - }); - } - } catch (e) { - print('[EventPreparationPage] Error reloading event: $e'); - } - } - Future _loadEquipmentAndContainers() async { setState(() => _isLoading = true); @@ -293,11 +277,15 @@ class _EventPreparationPageState extends State 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 with Single await _updateEquipmentStatuses(updatedEquipment); } + // Recharger l'événement depuis le provider + final eventProvider = context.read(); + // Recharger la liste des événements pour rafraîchir les données + final userId = context.read().uid; + if (userId != null) { + await eventProvider.loadUserEvents(userId, canViewAllEvents: true); + } + setState(() => _showSuccessAnimation = true); _animationController.forward(); @@ -338,52 +334,37 @@ class _EventPreparationPageState extends State with Single Future _updateEquipmentStatuses(List 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, - 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 } } } diff --git a/em2rp/lib/views/user_management_page.dart b/em2rp/lib/views/user_management_page.dart index 1de8ab0..350ab2f 100644 --- a/em2rp/lib/views/user_management_page.dart +++ b/em2rp/lib/views/user_management_page.dart @@ -9,7 +9,8 @@ import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/utils/permission_gate.dart'; import 'package:em2rp/models/role_model.dart'; import 'package:em2rp/views/widgets/nav/custom_app_bar.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; class UserManagementPage extends StatefulWidget { const UserManagementPage({super.key}); @@ -116,14 +117,18 @@ class _UserManagementPageState extends State { bool isLoadingRoles = true; Future loadRoles() async { - final snapshot = - await FirebaseFirestore.instance.collection('roles').get(); - availableRoles = snapshot.docs - .map((doc) => RoleModel.fromMap(doc.data(), doc.id)) - .toList(); - selectedRoleId = - availableRoles.isNotEmpty ? availableRoles.first.id : null; - isLoadingRoles = false; + try { + final dataService = DataService(FirebaseFunctionsApiService()); + final rolesData = await dataService.getRoles(); + availableRoles = rolesData + .map((data) => RoleModel.fromMap(data, data['id'] as String)) + .toList(); + selectedRoleId = + availableRoles.isNotEmpty ? availableRoles.first.id : null; + isLoadingRoles = false; + } catch (e) { + isLoadingRoles = false; + } } InputDecoration buildInputDecoration(String label, IconData icon) { @@ -265,8 +270,7 @@ class _UserManagementPageState extends State { ); await Provider.of(context, listen: false) - .createUserWithEmailInvite(context, newUser, - roleId: selectedRoleId); + .createUserWithEmailInvite(email: newUser.email, firstName: newUser.firstName, lastName: newUser.lastName, phoneNumber: newUser.phoneNumber, roleId: newUser.role); Navigator.pop(context); } catch (e) { if (context.mounted) { diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details.dart index daf9858..fa79a63 100644 --- a/em2rp/lib/views/widgets/calendar_widgets/event_details.dart +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details.dart @@ -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), ], ), ), diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_equipe.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_equipe.dart index 4b557b3..1ee8794 100644 --- a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_equipe.dart +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_equipe.dart @@ -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>( - 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(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> _fetchUsers() async { - final firestore = FirebaseFirestore.instance; - List 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; - users.add(UserModel.fromMap(userData, doc.id)); - } - } - } catch (e) { - // Log silencieux des erreurs individuelles - debugPrint('Error fetching user $i: $e'); - } - } - - return users; - } } diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_header.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_header.dart index 5b32781..1a8b76c 100644 --- a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_header.dart +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_header.dart @@ -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 { 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: () => {}, + ); + + 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; diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_preparation_buttons.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_preparation_buttons.dart index 5a7b548..08dbbb8 100644 --- a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_preparation_buttons.dart +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_preparation_buttons.dart @@ -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 { @override Widget build(BuildContext context) { - // Écouter les changements de l'événement en temps réel - return StreamBuilder( - 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, - snapshot.data!.id, - ); - } else { - currentEvent = widget.event; - } + // Utiliser le provider pour récupérer l'événement à jour + final eventProvider = context.watch(); - 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; diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_status_button.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_status_button.dart index ec1b968..49fe869 100644 --- a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_status_button.dart +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_status_button.dart @@ -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 { bool _loading = false; + final DataService _dataService = DataService(FirebaseFunctionsApiService()); Future _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; + final eventData = eventsList.firstWhere( + (e) => e['id'] == widget.event.id, + orElse: () => {}, ); - await Provider.of(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(context, listen: false) + .updateEvent(updatedEvent); + } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/em2rp/lib/views/widgets/data_management/event_types_management.dart b/em2rp/lib/views/widgets/data_management/event_types_management.dart index 53a721e..dd7f47e 100644 --- a/em2rp/lib/views/widgets/data_management/event_types_management.dart +++ b/em2rp/lib/views/widgets/data_management/event_types_management.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/models/event_type_model.dart'; import 'package:em2rp/utils/colors.dart'; import 'package:intl/intl.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; class EventTypesManagement extends StatefulWidget { const EventTypesManagement({super.key}); @@ -15,32 +16,37 @@ class _EventTypesManagementState extends State { String _searchQuery = ''; List _eventTypes = []; bool _loading = true; + late final DataService _dataService; @override void initState() { super.initState(); + _dataService = DataService(FirebaseFunctionsApiService()); _loadEventTypes(); } Future _loadEventTypes() async { setState(() => _loading = true); try { - final snapshot = await FirebaseFirestore.instance - .collection('eventTypes') - .orderBy('name') - .get(); + final eventTypesData = await _dataService.getEventTypes(); + + // Trier par nom + eventTypesData.sort((a, b) => + (a['name'] as String).compareTo(b['name'] as String)); setState(() { - _eventTypes = snapshot.docs - .map((doc) => EventTypeModel.fromMap(doc.data(), doc.id)) + _eventTypes = eventTypesData + .map((data) => EventTypeModel.fromMap(data, data['id'] as String)) .toList(); _loading = false; }); } catch (e) { setState(() => _loading = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Erreur lors du chargement : $e')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Erreur lors du chargement : $e')), + ); + } } } @@ -52,44 +58,45 @@ class _EventTypesManagementState extends State { } Future _canDeleteEventType(String eventTypeId) async { - final eventsSnapshot = await FirebaseFirestore.instance - .collection('events') - .where('eventTypeId', isEqualTo: eventTypeId) - .get(); - - return eventsSnapshot.docs.isEmpty; + try { + final events = await _dataService.getEventsByEventType(eventTypeId); + return events.isEmpty; + } catch (e) { + return false; + } } Future>> _getBlockingEvents(String eventTypeId) async { - final eventsSnapshot = await FirebaseFirestore.instance - .collection('events') - .where('eventTypeId', isEqualTo: eventTypeId) - .get(); + try { + final events = await _dataService.getEventsByEventType(eventTypeId); + final now = DateTime.now(); + List> futureEvents = []; + List> pastEvents = []; - final now = DateTime.now(); - List> futureEvents = []; - List> pastEvents = []; + for (final event in events) { + final eventDate = event['startDateTime'] != null + ? DateTime.parse(event['startDateTime'] as String) + : DateTime.now(); - for (final doc in eventsSnapshot.docs) { - final eventData = doc.data(); - final eventDate = eventData['startDateTime']?.toDate() ?? DateTime.now(); - - if (eventDate.isAfter(now)) { - futureEvents.add({ - 'id': doc.id, - 'name': eventData['name'], - 'startDateTime': eventDate, - }); - } else { - pastEvents.add({ - 'id': doc.id, - 'name': eventData['name'], - 'startDateTime': eventDate, - }); + if (eventDate.isAfter(now)) { + futureEvents.add({ + 'id': event['id'], + 'name': event['name'], + 'startDateTime': eventDate, + }); + } else { + pastEvents.add({ + 'id': event['id'], + 'name': event['name'], + 'startDateTime': eventDate, + }); + } } - } - return [...futureEvents, ...pastEvents]; + return [...futureEvents, ...pastEvents]; + } catch (e) { + return []; + } } Future _deleteEventType(EventTypeModel eventType) async { @@ -198,19 +205,20 @@ class _EventTypesManagementState extends State { onPressed: () async { Navigator.pop(context); try { - await FirebaseFirestore.instance - .collection('eventTypes') - .doc(eventType.id) - .delete(); + await _dataService.deleteEventType(eventType.id); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Type d\'événement supprimé avec succès')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Type d\'événement supprimé avec succès')), + ); + } _loadEventTypes(); } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Erreur lors de la suppression : $e')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Erreur lors de la suppression : $e')), + ); + } } }, style: ElevatedButton.styleFrom(backgroundColor: Colors.red), @@ -352,10 +360,12 @@ class _EventTypeFormDialogState extends State<_EventTypeFormDialog> { final _defaultPriceController = TextEditingController(); bool _loading = false; String? _error; + late final DataService _dataService; @override void initState() { super.initState(); + _dataService = DataService(FirebaseFunctionsApiService()); if (widget.eventType != null) { _nameController.text = widget.eventType!.name; _defaultPriceController.text = widget.eventType!.defaultPrice.toString(); @@ -369,69 +379,48 @@ class _EventTypeFormDialogState extends State<_EventTypeFormDialog> { super.dispose(); } - Future _isNameUnique(String name) async { - final snapshot = await FirebaseFirestore.instance - .collection('eventTypes') - .where('name', isEqualTo: name) - .get(); - - // Si on modifie, exclure le document actuel - if (widget.eventType != null) { - return snapshot.docs - .where((doc) => doc.id != widget.eventType!.id) - .isEmpty; - } - - return snapshot.docs.isEmpty; - } - Future _submit() async { if (!_formKey.currentState!.validate()) return; final name = _nameController.text.trim(); final defaultPrice = double.tryParse(_defaultPriceController.text.replaceAll(',', '.')) ?? 0.0; - setState(() => _loading = true); + setState(() { + _loading = true; + _error = null; + }); try { - // Vérifier l'unicité du nom - final isUnique = await _isNameUnique(name); - if (!isUnique) { - setState(() { - _error = 'Ce nom de type d\'événement existe déjà'; - _loading = false; - }); - return; - } - - final data = { - 'name': name, - 'defaultPrice': defaultPrice, - 'createdAt': widget.eventType?.createdAt ?? DateTime.now(), - }; - if (widget.eventType == null) { // Création - await FirebaseFirestore.instance.collection('eventTypes').add(data); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Type d\'événement créé avec succès')), + await _dataService.createEventType( + name: name, + defaultPrice: defaultPrice, ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Type d\'événement créé avec succès')), + ); + } } else { // Modification - await FirebaseFirestore.instance - .collection('eventTypes') - .doc(widget.eventType!.id) - .update(data); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Type d\'événement modifié avec succès')), + await _dataService.updateEventType( + eventTypeId: widget.eventType!.id, + name: name, + defaultPrice: defaultPrice, ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Type d\'événement modifié avec succès')), + ); + } } widget.onSaved(); - Navigator.pop(context); + if (mounted) Navigator.pop(context); } catch (e) { setState(() { - _error = 'Erreur : $e'; + _error = e.toString().replaceFirst('Exception: ', ''); _loading = false; }); } diff --git a/em2rp/lib/views/widgets/data_management/options_management.dart b/em2rp/lib/views/widgets/data_management/options_management.dart index 4adbec8..6f8ef68 100644 --- a/em2rp/lib/views/widgets/data_management/options_management.dart +++ b/em2rp/lib/views/widgets/data_management/options_management.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/models/option_model.dart'; import 'package:em2rp/utils/colors.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; import 'package:intl/intl.dart'; class OptionsManagement extends StatefulWidget { @@ -12,6 +13,7 @@ class OptionsManagement extends StatefulWidget { } class _OptionsManagementState extends State { + final DataService _dataService = DataService(FirebaseFunctionsApiService()); String _searchQuery = ''; List _options = []; Map _eventTypeNames = {}; @@ -26,26 +28,23 @@ class _OptionsManagementState extends State { Future _loadData() async { setState(() => _loading = true); try { - // Charger les types d'événements pour les noms - final eventTypesSnapshot = await FirebaseFirestore.instance - .collection('eventTypes') - .get(); + // Charger les types d'événements via l'API + final eventTypesData = await _dataService.getEventTypes(); _eventTypeNames = { - for (var doc in eventTypesSnapshot.docs) - doc.id: doc.data()['name'] as String + for (var typeData in eventTypesData) + typeData['id'] as String: typeData['name'] as String }; - // Charger les options - final optionsSnapshot = await FirebaseFirestore.instance - .collection('options') - .orderBy('code') - .get(); + // Charger les options via l'API + final optionsData = await _dataService.getOptions(); setState(() { - _options = optionsSnapshot.docs - .map((doc) => EventOption.fromMap(doc.data(), doc.id)) + _options = optionsData + .map((data) => EventOption.fromMap(data, data['id'] as String)) .toList(); + // Trier par code + _options.sort((a, b) => a.code.compareTo(b.code)); _loading = false; }); } catch (e) { @@ -66,35 +65,38 @@ class _OptionsManagementState extends State { } Future>> _getBlockingEvents(String optionId) async { - final eventsSnapshot = await FirebaseFirestore.instance - .collection('events') - .get(); + // Charger tous les événements via l'API + final result = await _dataService.getEvents(); + final eventsData = result['events'] as List; final now = DateTime.now(); List> futureEvents = []; List> pastEvents = []; - for (final doc in eventsSnapshot.docs) { - final eventData = doc.data(); + for (final eventData in eventsData) { final options = eventData['options'] as List? ?? []; // Vérifier si cette option est utilisée dans cet événement bool optionUsed = options.any((opt) => opt['id'] == optionId); if (optionUsed) { - final eventDate = eventData['StartDateTime']?.toDate() ?? DateTime.now(); - // Corriger la récupération du nom - utiliser 'Name' au lieu de 'name' - final eventName = eventData['Name'] as String? ?? 'Événement sans nom'; + final eventDate = eventData['startDateTime'] as DateTime? ?? + (eventData['StartDateTime'] as DateTime?) ?? + DateTime.now(); + final eventName = eventData['name'] as String? ?? + eventData['Name'] as String? ?? + 'Événement sans nom'; + final eventId = eventData['id'] as String? ?? ''; if (eventDate.isAfter(now)) { futureEvents.add({ - 'id': doc.id, + 'id': eventId, 'name': eventName, 'startDateTime': eventDate, }); } else { pastEvents.add({ - 'id': doc.id, + 'id': eventId, 'name': eventName, 'startDateTime': eventDate, }); @@ -211,10 +213,7 @@ class _OptionsManagementState extends State { onPressed: () async { Navigator.pop(context); try { - await FirebaseFirestore.instance - .collection('options') - .doc(option.id) - .delete(); + await _dataService.deleteOption(option.id); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Option supprimée avec succès')), @@ -421,17 +420,21 @@ class _OptionFormDialogState extends State<_OptionFormDialog> { } Future _isCodeUnique(String code) async { - final doc = await FirebaseFirestore.instance - .collection('options') - .doc(code) - .get(); + try { + // Charger toutes les options via l'API + final dataService = DataService(FirebaseFunctionsApiService()); + final optionsData = await dataService.getOptions(); - // Si on modifie et que c'est le même document, c'est OK - if (widget.option != null && widget.option!.id == code) { - return true; + // Si on modifie et que c'est le même document, c'est OK + if (widget.option != null && widget.option!.id == code) { + return true; + } + + // Vérifier si le code existe déjà + return !optionsData.any((opt) => opt['id'] == code); + } catch (e) { + return false; } - - return !doc.exists; } Future _submit() async { @@ -471,6 +474,7 @@ class _OptionFormDialogState extends State<_OptionFormDialog> { } } + final dataService = DataService(FirebaseFunctionsApiService()); final data = { 'code': code, 'name': name, @@ -483,16 +487,13 @@ class _OptionFormDialogState extends State<_OptionFormDialog> { if (widget.option == null) { // Création - utiliser le code comme ID - await FirebaseFirestore.instance.collection('options').doc(code).set(data); + await dataService.createOption(code, data); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Option créée avec succès')), ); } else { // Modification - await FirebaseFirestore.instance - .collection('options') - .doc(widget.option!.id) - .update(data); + await dataService.updateOption(widget.option!.id, data); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Option modifiée avec succès')), ); diff --git a/em2rp/lib/views/widgets/equipment/equipment_associated_events_section.dart b/em2rp/lib/views/widgets/equipment/equipment_associated_events_section.dart index 3c9dbd5..bf27573 100644 --- a/em2rp/lib/views/widgets/equipment/equipment_associated_events_section.dart +++ b/em2rp/lib/views/widgets/equipment/equipment_associated_events_section.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.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/utils/colors.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; import 'package:intl/intl.dart'; enum EventFilter { @@ -26,6 +27,7 @@ class EquipmentAssociatedEventsSection extends StatefulWidget { class _EquipmentAssociatedEventsSectionState extends State { + final DataService _dataService = DataService(FirebaseFunctionsApiService()); EventFilter _selectedFilter = EventFilter.upcoming; List _events = []; bool _isLoading = true; @@ -40,36 +42,32 @@ class _EquipmentAssociatedEventsSectionState setState(() => _isLoading = true); try { - // Récupérer TOUS les événements car on ne peut pas faire arrayContains sur un objet - final eventsSnapshot = await FirebaseFirestore.instance - .collection('events') - .get(); + // Récupérer TOUS les événements via l'API + final result = await _dataService.getEvents(); + final eventsData = result['events'] as List; final events = []; - // Récupérer toutes les boîtes pour vérifier leur contenu - final containersSnapshot = await FirebaseFirestore.instance - .collection('containers') - .get(); + // Récupérer toutes les boîtes pour vérifier leur contenu via l'API + final containersData = await _dataService.getContainers(); final containersWithEquipment = []; - for (var containerDoc in containersSnapshot.docs) { + for (var containerData in containersData) { try { - final data = containerDoc.data(); - final equipmentIds = List.from(data['equipmentIds'] ?? []); + final equipmentIds = List.from(containerData['equipmentIds'] ?? []); if (equipmentIds.contains(widget.equipment.id)) { - containersWithEquipment.add(containerDoc.id); + containersWithEquipment.add(containerData['id'] as String); } } catch (e) { - print('[EquipmentAssociatedEventsSection] Error parsing container ${containerDoc.id}: $e'); + print('[EquipmentAssociatedEventsSection] Error parsing container ${containerData['id']}: $e'); } } // Filtrer manuellement les événements qui contiennent cet équipement - for (var doc in eventsSnapshot.docs) { + for (var eventData in eventsData) { try { - final event = EventModel.fromMap(doc.data(), doc.id); + final event = EventModel.fromMap(eventData, eventData['id'] as String); // Vérifier si l'équipement est directement assigné final hasEquipmentDirectly = event.assignedEquipment.any( @@ -85,7 +83,7 @@ class _EquipmentAssociatedEventsSectionState events.add(event); } } catch (e) { - print('[EquipmentAssociatedEventsSection] Error parsing event ${doc.id}: $e'); + print('[EquipmentAssociatedEventsSection] Error parsing event ${eventData['id']}: $e'); } } diff --git a/em2rp/lib/views/widgets/equipment/equipment_current_events_section.dart b/em2rp/lib/views/widgets/equipment/equipment_current_events_section.dart index 71db9b9..cc3de8d 100644 --- a/em2rp/lib/views/widgets/equipment/equipment_current_events_section.dart +++ b/em2rp/lib/views/widgets/equipment/equipment_current_events_section.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.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/utils/colors.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; import 'package:intl/intl.dart'; /// Widget pour afficher les événements EN COURS utilisant cet équipement @@ -21,6 +22,7 @@ class EquipmentCurrentEventsSection extends StatefulWidget { class _EquipmentCurrentEventsSectionState extends State { + final DataService _dataService = DataService(FirebaseFunctionsApiService()); List _events = []; bool _isLoading = true; @@ -34,36 +36,32 @@ class _EquipmentCurrentEventsSectionState setState(() => _isLoading = true); try { - // Récupérer TOUS les événements - final eventsSnapshot = await FirebaseFirestore.instance - .collection('events') - .get(); + // Récupérer TOUS les événements via l'API + final result = await _dataService.getEvents(); + final eventsData = result['events'] as List; final events = []; - // Récupérer toutes les boîtes pour vérifier leur contenu - final containersSnapshot = await FirebaseFirestore.instance - .collection('containers') - .get(); + // Récupérer toutes les boîtes pour vérifier leur contenu via l'API + final containersData = await _dataService.getContainers(); final containersWithEquipment = []; - for (var containerDoc in containersSnapshot.docs) { + for (var containerData in containersData) { try { - final data = containerDoc.data(); - final equipmentIds = List.from(data['equipmentIds'] ?? []); + final equipmentIds = List.from(containerData['equipmentIds'] ?? []); if (equipmentIds.contains(widget.equipment.id)) { - containersWithEquipment.add(containerDoc.id); + containersWithEquipment.add(containerData['id'] as String); } } catch (e) { - print('[EquipmentCurrentEventsSection] Error parsing container ${containerDoc.id}: $e'); + print('[EquipmentCurrentEventsSection] Error parsing container ${containerData['id']}: $e'); } } // Filtrer les événements en cours - for (var doc in eventsSnapshot.docs) { + for (var eventData in eventsData) { try { - final event = EventModel.fromMap(doc.data(), doc.id); + final event = EventModel.fromMap(eventData, eventData['id'] as String); // Vérifier si l'équipement est directement assigné final hasEquipmentDirectly = event.assignedEquipment.any( @@ -91,7 +89,7 @@ class _EquipmentCurrentEventsSectionState } } } catch (e) { - print('[EquipmentCurrentEventsSection] Error parsing event ${doc.id}: $e'); + print('[EquipmentCurrentEventsSection] Error parsing event $eventData: $e'); } } diff --git a/em2rp/lib/views/widgets/equipment/equipment_parent_containers.dart b/em2rp/lib/views/widgets/equipment/equipment_parent_containers.dart index 08fac81..49ae5ae 100644 --- a/em2rp/lib/views/widgets/equipment/equipment_parent_containers.dart +++ b/em2rp/lib/views/widgets/equipment/equipment_parent_containers.dart @@ -1,177 +1,240 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/models/container_model.dart'; -import 'package:em2rp/providers/container_provider.dart'; +import 'package:em2rp/services/container_equipment_service.dart'; +import 'package:em2rp/views/container_detail_page.dart'; -/// Widget pour afficher les containers parents d'un équipement -class EquipmentParentContainers extends StatefulWidget { - final List parentBoxIds; +/// Widget pour afficher les boîtes contenant un équipement +/// Utilise le nouveau système : interroge Firestore via Cloud Function +class EquipmentParentContainers extends StatelessWidget { + final String equipmentId; const EquipmentParentContainers({ super.key, - required this.parentBoxIds, + required this.equipmentId, }); - @override - State createState() => _EquipmentParentContainersState(); -} - -class _EquipmentParentContainersState extends State { - List _containers = []; - bool _isLoading = true; - - @override - void initState() { - super.initState(); - _loadContainers(); - } - - Future _loadContainers() async { - if (widget.parentBoxIds.isEmpty) { - setState(() { - _isLoading = false; - }); - return; - } - - setState(() { - _isLoading = true; - }); - - try { - final containerProvider = context.read(); - final List containers = []; - - for (final boxId in widget.parentBoxIds) { - final container = await containerProvider.getContainerById(boxId); - if (container != null) { - containers.add(container); - } - } - - setState(() { - _containers = containers; - _isLoading = false; - }); - } catch (e) { - setState(() { - _isLoading = false; - }); - } - } - @override Widget build(BuildContext context) { - if (widget.parentBoxIds.isEmpty) { - return const SizedBox.shrink(); - } + return FutureBuilder>( + future: containerEquipmentService.getContainersByEquipment(equipmentId), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Card( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Center( + child: CircularProgressIndicator(), + ), + ), + ); + } - return Card( - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon(Icons.inventory_2, color: AppColors.rouge, size: 20), - const SizedBox(width: 8), - const Text( - 'Containers', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, + if (snapshot.hasError) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Icon(Icons.error_outline, color: Colors.red.shade700), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Erreur lors du chargement des boîtes', + style: TextStyle(color: Colors.red.shade700), + ), ), + ], + ), + ), + ); + } + + final containers = snapshot.data ?? []; + + if (containers.isEmpty) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Icon(Icons.info_outline, color: Colors.grey.shade600), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Cet équipement n\'est dans aucune boîte', + style: TextStyle(color: Colors.grey), + ), + ), + ], + ), + ), + ); + } + + return Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.inventory_2, color: AppColors.rouge, size: 20), + const SizedBox(width: 8), + Text( + 'Boîtes contenant cet équipement (${containers.length})', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], ), + const SizedBox(height: 16), + ...containers.map((container) => _buildContainerCard(context, container)), ], ), - const Divider(height: 24), - if (_isLoading) - const Center( - child: Padding( - padding: EdgeInsets.all(16.0), - child: CircularProgressIndicator(), - ), - ) - else if (_containers.isEmpty) - const Center( - child: Padding( - padding: EdgeInsets.all(16.0), - child: Text( - 'Cet équipement n\'est dans aucun container', - style: TextStyle(color: Colors.grey), - ), - ), - ) - else - ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: _containers.length, - separatorBuilder: (context, index) => const Divider(height: 1), - itemBuilder: (context, index) { - final container = _containers[index]; - return _buildContainerTile(container); - }, - ), - ], - ), - ), - ); - } - - Widget _buildContainerTile(ContainerModel container) { - return ListTile( - contentPadding: const EdgeInsets.symmetric(vertical: 8), - leading: Icon( - _getTypeIcon(container.type), - color: AppColors.rouge, - size: 32, - ), - title: Text( - container.id, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(container.name), - const SizedBox(height: 4), - Text( - containerTypeLabel(container.type), - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - ), ), - ], - ), - trailing: const Icon(Icons.chevron_right), - onTap: () { - Navigator.pushNamed( - context, - '/container_detail', - arguments: container, ); }, ); } - IconData _getTypeIcon(ContainerType type) { - switch (type) { - case ContainerType.flightCase: - return Icons.work; - case ContainerType.pelicase: - return Icons.work_outline; - case ContainerType.bag: - return Icons.shopping_bag; - case ContainerType.openCrate: - return Icons.inventory_2; - case ContainerType.toolbox: - return Icons.handyman; - } + Widget _buildContainerCard(BuildContext context, ContainerModel container) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Material( + elevation: 1, + borderRadius: BorderRadius.circular(12), + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ContainerDetailPage(container: container), + ), + ); + }, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + // Icône du type de container + CircleAvatar( + backgroundColor: AppColors.rouge.withValues(alpha: 0.1), + radius: 24, + child: container.type.getIconForAvatar( + size: 24, + color: AppColors.rouge, + ), + ), + const SizedBox(width: 12), + + // Informations du container + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + container.name, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + ), + ), + const SizedBox(height: 4), + Text( + container.type.label, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 6), + + // Badges + Wrap( + spacing: 6, + runSpacing: 4, + children: [ + _buildInfoChip( + icon: Icons.inventory, + label: '${container.itemCount} équip.', + color: Colors.blue, + ), + if (container.weight != null) + _buildInfoChip( + icon: Icons.scale, + label: '${container.weight!.toStringAsFixed(1)} kg', + color: Colors.orange, + ), + _buildInfoChip( + icon: Icons.tag, + label: container.id, + color: Colors.grey, + isCompact: true, + ), + ], + ), + ], + ), + ), + + // Icône de navigation + Icon(Icons.chevron_right, color: Colors.grey.shade400), + ], + ), + ), + ), + ), + ); + } + + Widget _buildInfoChip({ + required IconData icon, + required String label, + required Color color, + bool isCompact = false, + }) { + return Container( + padding: EdgeInsets.symmetric( + horizontal: isCompact ? 6 : 8, + vertical: 3, + ), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: color.withValues(alpha: 0.3), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: isCompact ? 10 : 12, + color: color.withValues(alpha: 0.8), + ), + const SizedBox(width: 3), + Text( + label, + style: TextStyle( + fontSize: isCompact ? 9 : 11, + fontWeight: FontWeight.w600, + color: color.withValues(alpha: 0.9), + ), + ), + ], + ), + ); } } - diff --git a/em2rp/lib/views/widgets/equipment/equipment_referencing_containers.dart b/em2rp/lib/views/widgets/equipment/equipment_referencing_containers.dart index f580ae0..4a9dab6 100644 --- a/em2rp/lib/views/widgets/equipment/equipment_referencing_containers.dart +++ b/em2rp/lib/views/widgets/equipment/equipment_referencing_containers.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:em2rp/models/container_model.dart'; -import 'package:em2rp/models/equipment_model.dart'; -import 'package:em2rp/services/container_service.dart'; +import 'package:em2rp/services/container_equipment_service.dart'; import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/views/container_detail_page.dart'; /// Widget pour afficher les containers qui référencent un équipement -class EquipmentReferencingContainers extends StatefulWidget { +/// Utilise le nouveau système : interroge Firestore via Cloud Function +class EquipmentReferencingContainers extends StatelessWidget { final String equipmentId; const EquipmentReferencingContainers({ @@ -14,191 +14,161 @@ class EquipmentReferencingContainers extends StatefulWidget { required this.equipmentId, }); - @override - State createState() => _EquipmentReferencingContainersState(); -} - -class _EquipmentReferencingContainersState extends State { - final ContainerService _containerService = ContainerService(); - List _referencingContainers = []; - bool _isLoading = true; - - @override - void initState() { - super.initState(); - _loadReferencingContainers(); - } - - Future _loadReferencingContainers() async { - try { - final containers = await _containerService.findContainersWithEquipment(widget.equipmentId); - setState(() { - _referencingContainers = containers; - _isLoading = false; - }); - } catch (e) { - setState(() { - _isLoading = false; - }); - } - } - @override Widget build(BuildContext context) { - if (_referencingContainers.isEmpty && !_isLoading) { - return const SizedBox.shrink(); - } + return FutureBuilder>( + future: containerEquipmentService.getContainersByEquipment(equipmentId), + builder: (context, snapshot) { + // Ne rien afficher si vide et pas en chargement + if (snapshot.connectionState == ConnectionState.done && + (!snapshot.hasData || snapshot.data!.isEmpty)) { + return const SizedBox.shrink(); + } - return Card( - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + if (snapshot.connectionState == ConnectionState.waiting) { + return Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(20), + child: Center( + child: CircularProgressIndicator(), + ), + ), + ); + } + + if (snapshot.hasError) { + return const SizedBox.shrink(); // Masquer en cas d'erreur + } + + final containers = snapshot.data ?? []; + + return Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Icon(Icons.inventory_2, color: AppColors.rouge), - const SizedBox(width: 8), - Expanded( - child: Text( - 'Containers contenant cet équipement', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), + Row( + children: [ + const Icon(Icons.inventory_2, color: AppColors.rouge), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Boîtes contenant cet équipement (${containers.length})', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + ], ), + const Divider(height: 24), + _buildContainersGrid(context, containers), ], ), - const Divider(height: 24), - if (_isLoading) - const Center( - child: Padding( - padding: EdgeInsets.all(16.0), - child: CircularProgressIndicator(), - ), - ) - else if (_referencingContainers.isEmpty) - const Padding( - padding: EdgeInsets.all(16.0), - child: Center( - child: Text( - 'Cet équipement n\'est dans aucun container', - style: TextStyle(color: Colors.grey), - ), - ), - ) - else - _buildContainersGrid(), - ], - ), - ), - ); - } - - Widget _buildContainersGrid() { - final screenWidth = MediaQuery.of(context).size.width; - final isMobile = screenWidth < 800; - final isTablet = screenWidth < 1200; - - final crossAxisCount = isMobile ? 1 : (isTablet ? 2 : 3); - - return GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, - crossAxisSpacing: 12, - mainAxisSpacing: 8, - childAspectRatio: 7.5, - ), - itemCount: _referencingContainers.length, - itemBuilder: (context, index) { - final container = _referencingContainers[index]; - return _buildContainerCard(container); + ), + ); }, ); } - Widget _buildContainerCard(ContainerModel container) { + Widget _buildContainersGrid(BuildContext context, List containers) { + return Column( + children: containers.map((container) { + return _buildContainerCard(context, container); + }).toList(), + ); + } + + Widget _buildContainerCard(BuildContext context, ContainerModel container) { return Card( + margin: const EdgeInsets.only(bottom: 12), elevation: 1, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: Colors.grey.shade200, width: 1), ), child: InkWell( onTap: () { Navigator.of(context).push( MaterialPageRoute( - builder: (context) => ContainerDetailPage(container: container), + builder: (_) => ContainerDetailPage(container: container), ), ); }, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + padding: const EdgeInsets.all(16), child: Row( children: [ // Icône du type de container - container.type.getIcon(size: 28, color: AppColors.rouge), - const SizedBox(width: 10), - // Infos textuelles + CircleAvatar( + backgroundColor: AppColors.rouge.withValues(alpha: 0.1), + radius: 28, + child: container.type.getIconForAvatar( + size: 28, + color: AppColors.rouge, + ), + ), + const SizedBox(width: 16), + + // Informations du container Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - container.id, + container.name, style: const TextStyle( - fontSize: 13, fontWeight: FontWeight.bold, - height: 1.0, + fontSize: 16, ), - overflow: TextOverflow.ellipsis, - maxLines: 1, ), - if (container.notes != null && container.notes!.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 2), - child: Text( - container.notes!, - style: TextStyle( - fontSize: 10, - color: Colors.grey[600], - height: 1.0, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ) - else - Text( - container.name, - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - height: 1.0, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, + const SizedBox(height: 4), + Text( + container.type.label, + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade600, ), + ), + const SizedBox(height: 6), + + // Badges d'information + Wrap( + spacing: 8, + runSpacing: 4, + children: [ + _buildInfoChip( + icon: Icons.inventory, + label: '${container.itemCount} équipement${container.itemCount > 1 ? 's' : ''}', + color: Colors.blue, + ), + if (container.weight != null) + _buildInfoChip( + icon: Icons.scale, + label: '${container.weight!.toStringAsFixed(1)} kg', + color: Colors.orange, + ), + _buildInfoChip( + icon: Icons.tag, + label: container.id, + color: Colors.grey, + isCompact: true, + ), + ], + ), ], ), ), - const SizedBox(width: 8), - // Badges compacts - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - _buildStatusBadge(_getStatusLabel(container.status), _getStatusColor(container.status)), - if (container.itemCount > 0) - Padding( - padding: const EdgeInsets.only(top: 2), - child: _buildCountBadge(container.itemCount), - ), - ], + + // Icône de navigation + Icon( + Icons.chevron_right, + color: AppColors.rouge, + size: 28, ), ], ), @@ -207,82 +177,44 @@ class _EquipmentReferencingContainersState extends State 1 ? 's' : ''}', - style: const TextStyle( - fontSize: 9, - color: Colors.green, - fontWeight: FontWeight.bold, - height: 1.0, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ); - } - - String _getStatusLabel(EquipmentStatus status) { - switch (status) { - case EquipmentStatus.available: - return 'Disponible'; - case EquipmentStatus.inUse: - return 'En prestation'; - case EquipmentStatus.rented: - return 'Loué'; - case EquipmentStatus.lost: - return 'Perdu'; - case EquipmentStatus.outOfService: - return 'HS'; - case EquipmentStatus.maintenance: - return 'En maintenance'; - } - } - - Color _getStatusColor(EquipmentStatus status) { - switch (status) { - case EquipmentStatus.available: - return Colors.green; - case EquipmentStatus.inUse: - return Colors.blue; - case EquipmentStatus.rented: - return Colors.orange; - case EquipmentStatus.lost: - return Colors.red; - case EquipmentStatus.outOfService: - return Colors.red; - case EquipmentStatus.maintenance: - return Colors.yellow; - } - } } - diff --git a/em2rp/lib/views/widgets/equipment/parent_boxes_selector.dart b/em2rp/lib/views/widgets/equipment/parent_boxes_selector.dart new file mode 100644 index 0000000..710c51c --- /dev/null +++ b/em2rp/lib/views/widgets/equipment/parent_boxes_selector.dart @@ -0,0 +1,445 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/models/container_model.dart'; +import 'package:em2rp/utils/colors.dart'; + +/// Widget pour sélectionner les boîtes parentes d'un équipement +class ParentBoxesSelector extends StatefulWidget { + final List availableBoxes; + final List selectedBoxIds; + final Function(List) onSelectionChanged; + + const ParentBoxesSelector({ + super.key, + required this.availableBoxes, + required this.selectedBoxIds, + required this.onSelectionChanged, + }); + + @override + State createState() => _ParentBoxesSelectorState(); +} + +class _ParentBoxesSelectorState extends State { + final TextEditingController _searchController = TextEditingController(); + String _searchQuery = ''; + + @override + void initState() { + super.initState(); + print('[ParentBoxesSelector] initState'); + print('[ParentBoxesSelector] Available boxes: ${widget.availableBoxes.length}'); + print('[ParentBoxesSelector] Selected box IDs: ${widget.selectedBoxIds}'); + + // Log détaillé de chaque boîte + for (var box in widget.availableBoxes) { + print('[ParentBoxesSelector] Box - ID: ${box.id}, Name: ${box.name}'); + } + } + + @override + void didUpdateWidget(ParentBoxesSelector oldWidget) { + super.didUpdateWidget(oldWidget); + print('[ParentBoxesSelector] didUpdateWidget'); + print('[ParentBoxesSelector] Old selected: ${oldWidget.selectedBoxIds}'); + print('[ParentBoxesSelector] New selected: ${widget.selectedBoxIds}'); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + List get _filteredBoxes { + if (_searchQuery.isEmpty) { + return widget.availableBoxes; + } + + final query = _searchQuery.toLowerCase(); + return widget.availableBoxes.where((box) { + return box.name.toLowerCase().contains(query) || + box.id.toLowerCase().contains(query) || + box.type.label.toLowerCase().contains(query); + }).toList(); + } + + void _toggleSelection(String boxId) { + final newSelection = List.from(widget.selectedBoxIds); + if (newSelection.contains(boxId)) { + newSelection.remove(boxId); + } else { + newSelection.add(boxId); + } + widget.onSelectionChanged(newSelection); + } + + @override + Widget build(BuildContext context) { + if (widget.availableBoxes.isEmpty && widget.selectedBoxIds.isEmpty) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Icon(Icons.info_outline, color: Colors.grey.shade600), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Aucune boîte disponible', + style: TextStyle(color: Colors.grey), + ), + ), + ], + ), + ), + ); + } + + final filteredBoxes = _filteredBoxes; + final selectedCount = widget.selectedBoxIds.length; + + // Vérifier s'il y a des boîtes sélectionnées qui ne sont pas dans la liste + final missingBoxIds = widget.selectedBoxIds + .where((id) => !widget.availableBoxes.any((box) => box.id == id)) + .toList(); + + return Card( + elevation: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // En-tête avec titre et compteur + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + const Icon(Icons.inventory_2, color: AppColors.rouge, size: 20), + const SizedBox(width: 8), + const Text( + 'Boîtes parentes', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + if (selectedCount > 0) + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: AppColors.rouge.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppColors.rouge.withValues(alpha: 0.3), + width: 1, + ), + ), + child: Text( + '$selectedCount sélectionné${selectedCount > 1 ? 's' : ''}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppColors.rouge, + ), + ), + ), + ], + ), + ), + const Divider(height: 1), + + // Message d'avertissement si des boîtes sélectionnées sont manquantes + if (missingBoxIds.isNotEmpty) + Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.orange.shade300), + ), + child: Row( + children: [ + Icon(Icons.warning_amber, color: Colors.orange.shade700), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Boîtes introuvables', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.orange.shade900, + ), + ), + const SizedBox(height: 4), + Text( + 'Les boîtes suivantes sont sélectionnées mais n\'existent plus : ${missingBoxIds.join(", ")}', + style: TextStyle( + fontSize: 13, + color: Colors.orange.shade800, + ), + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + // Retirer les boîtes manquantes de la sélection + final newSelection = widget.selectedBoxIds + .where((id) => !missingBoxIds.contains(id)) + .toList(); + widget.onSelectionChanged(newSelection); + }, + tooltip: 'Retirer ces boîtes', + ), + ], + ), + ), + + // Barre de recherche + Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + controller: _searchController, + onChanged: (value) { + setState(() { + _searchQuery = value; + }); + }, + decoration: InputDecoration( + hintText: 'Rechercher par nom, ID ou type...', + prefixIcon: const Icon(Icons.search, color: AppColors.rouge), + suffixIcon: _searchQuery.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + setState(() { + _searchController.clear(); + _searchQuery = ''; + }); + }, + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.rouge, width: 2), + ), + filled: true, + fillColor: Colors.grey.shade50, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ), + ), + + // Message si aucun résultat + if (filteredBoxes.isEmpty) + Padding( + padding: const EdgeInsets.all(32.0), + child: Center( + child: Column( + children: [ + Icon(Icons.search_off, size: 48, color: Colors.grey.shade400), + const SizedBox(height: 12), + Text( + 'Aucune boîte trouvée', + style: TextStyle( + fontSize: 16, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text( + 'Essayez une autre recherche', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade500, + ), + ), + ], + ), + ), + ) + else + // Liste des boîtes + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + itemCount: filteredBoxes.length, + separatorBuilder: (context, index) => const SizedBox(height: 8), + itemBuilder: (context, index) { + final box = filteredBoxes[index]; + final isSelected = widget.selectedBoxIds.contains(box.id); + if (index == 0) { + print('[ParentBoxesSelector] Building item $index'); + print('[ParentBoxesSelector] Box ID: ${box.id}'); + print('[ParentBoxesSelector] Selected IDs: ${widget.selectedBoxIds}'); + print('[ParentBoxesSelector] Is selected: $isSelected'); + } + return _buildBoxCard(box, isSelected); + }, + ), + + const SizedBox(height: 16), + ], + ), + ); + } + + Widget _buildBoxCard(ContainerModel box, bool isSelected) { + return Card( + elevation: isSelected ? 3 : 1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: isSelected ? AppColors.rouge : Colors.grey.shade300, + width: isSelected ? 2 : 1, + ), + ), + child: InkWell( + onTap: () => _toggleSelection(box.id), + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + // Checkbox + Checkbox( + value: isSelected, + onChanged: (value) => _toggleSelection(box.id), + activeColor: AppColors.rouge, + ), + const SizedBox(width: 8), + + // Icône du type de container + CircleAvatar( + backgroundColor: isSelected + ? AppColors.rouge.withValues(alpha: 0.15) + : Colors.grey.shade200, + radius: 24, + child: box.type.getIconForAvatar( + size: 24, + color: isSelected ? AppColors.rouge : Colors.grey.shade700, + ), + ), + const SizedBox(width: 12), + + // Informations de la boîte + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + box.name, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: isSelected ? AppColors.rouge : Colors.black87, + ), + ), + const SizedBox(height: 4), + Text( + box.type.label, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 6), + + // Badges + Wrap( + spacing: 6, + runSpacing: 4, + children: [ + _buildInfoChip( + icon: Icons.inventory, + label: '${box.itemCount} équip.', + color: Colors.blue, + ), + if (box.weight != null) + _buildInfoChip( + icon: Icons.scale, + label: '${box.weight!.toStringAsFixed(1)} kg', + color: Colors.orange, + ), + _buildInfoChip( + icon: Icons.tag, + label: box.id, + color: Colors.grey, + isCompact: true, + ), + ], + ), + ], + ), + ), + + // Indicateur de sélection + if (isSelected) + const Icon( + Icons.check_circle, + color: AppColors.rouge, + size: 24, + ), + ], + ), + ), + ), + ); + } + + Widget _buildInfoChip({ + required IconData icon, + required String label, + required Color color, + bool isCompact = false, + }) { + return Container( + padding: EdgeInsets.symmetric( + horizontal: isCompact ? 6 : 8, + vertical: 3, + ), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: color.withValues(alpha: 0.3), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: isCompact ? 10 : 12, + color: color.withValues(alpha: 0.8), + ), + const SizedBox(width: 3), + Text( + label, + style: TextStyle( + fontSize: isCompact ? 9 : 11, + fontWeight: FontWeight.w600, + color: color.withValues(alpha: 0.9), + ), + ), + ], + ), + ); + } +} + diff --git a/em2rp/lib/views/widgets/equipment/restock_dialog.dart b/em2rp/lib/views/widgets/equipment/restock_dialog.dart index 88118af..cce9ce8 100644 --- a/em2rp/lib/views/widgets/equipment/restock_dialog.dart +++ b/em2rp/lib/views/widgets/equipment/restock_dialog.dart @@ -189,10 +189,13 @@ class _RestockDialogState extends State { }; if (context.mounted) { - await context.read().updateEquipment( - widget.equipment.id, - updatedData, - ); + final updatedEquipment = widget.equipment.copyWith( + availableQuantity: newAvailable, + totalQuantity: newTotal, + updatedAt: DateTime.now(), + ); + + await context.read().updateEquipment(updatedEquipment); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart b/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart index af8b438..e3ac694 100644 --- a/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart +++ b/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart @@ -34,8 +34,6 @@ class EventAssignedEquipmentSection extends StatefulWidget { } class _EventAssignedEquipmentSectionState extends State { - // ...existing code... - bool get _canAddMaterial => widget.startDate != null && widget.endDate != null; final EventAvailabilityService _availabilityService = EventAvailabilityService(); Map _equipmentCache = {}; @@ -104,7 +102,7 @@ class _EventAssignedEquipmentSectionState extends State _isLoading = false); } @@ -712,7 +710,13 @@ class _EventAssignedEquipmentSectionState extends State 1) + if (isConsumable) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( diff --git a/em2rp/lib/views/widgets/event_form/event_options_display_widget.dart b/em2rp/lib/views/widgets/event_form/event_options_display_widget.dart index 2fc9ecb..dd2ecdf 100644 --- a/em2rp/lib/views/widgets/event_form/event_options_display_widget.dart +++ b/em2rp/lib/views/widgets/event_form/event_options_display_widget.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:intl/intl.dart'; import 'package:em2rp/utils/colors.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; class EventOptionsDisplayWidget extends StatelessWidget { final List> optionsData; @@ -172,31 +173,36 @@ class EventOptionsDisplayWidget extends StatelessWidget { Future>> _loadOptionsWithDetails(List> optionsData) async { List> enrichedOptions = []; + // Charger toutes les options via l'API une seule fois + final dataService = DataService(FirebaseFunctionsApiService()); + final allOptionsData = await dataService.getOptions(); + + // Créer une map pour accès rapide par ID + final optionsMap = { + for (var opt in allOptionsData) opt['id']: opt + }; + for (final optionData in optionsData) { try { - // Si l'option a un ID, récupérer les détails complets depuis Firestore + // Si l'option a un ID, récupérer les détails complets depuis l'API if (optionData['id'] != null) { - final doc = await FirebaseFirestore.instance - .collection('options') - .doc(optionData['id']) - .get(); + final apiData = optionsMap[optionData['id']]; - if (doc.exists) { - final firestoreData = doc.data()!; - // Combiner les données Firestore avec le prix choisi + if (apiData != null) { + // Combiner les données API avec le prix choisi enrichedOptions.add({ 'id': optionData['id'], - 'code': firestoreData['code'] ?? optionData['id'], // Récupérer le code depuis Firestore - 'name': firestoreData['name'], // Récupéré depuis Firestore - 'details': firestoreData['details'] ?? '', // Récupéré depuis Firestore + 'code': apiData['code'] ?? optionData['id'], + 'name': apiData['name'], + 'details': apiData['details'] ?? '', 'price': optionData['price'], // Prix choisi par l'utilisateur 'quantity': optionData['quantity'] ?? 1, // Quantité - 'isQuantitative': firestoreData['isQuantitative'] ?? false, - 'valMin': firestoreData['valMin'], - 'valMax': firestoreData['valMax'], + 'isQuantitative': apiData['isQuantitative'] ?? false, + 'valMin': apiData['valMin'], + 'valMax': apiData['valMax'], }); } else { - // Option supprimée de Firestore, afficher avec des données par défaut + // Option supprimée, afficher avec des données par défaut enrichedOptions.add({ 'id': optionData['id'], 'name': 'Option supprimée (ID: ${optionData['id']})', diff --git a/em2rp/lib/views/widgets/image/profile_picture.dart b/em2rp/lib/views/widgets/image/profile_picture.dart index 1b8f4a7..b2fbcfe 100644 --- a/em2rp/lib/views/widgets/image/profile_picture.dart +++ b/em2rp/lib/views/widgets/image/profile_picture.dart @@ -1,16 +1,20 @@ import 'package:flutter/material.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:provider/provider.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:em2rp/providers/users_provider.dart'; class ProfilePictureWidget extends StatefulWidget { final String? userId; final double radius; final String? defaultImageUrl; + final String? profilePhotoUrl; // URL directe de la photo (optionnel) const ProfilePictureWidget({ super.key, - required this.userId, - this.radius = 25, + this.userId, + this.radius = 20, this.defaultImageUrl, + this.profilePhotoUrl, // Si fourni, utilisé directement sans appeler UsersProvider }); @override @@ -18,110 +22,56 @@ class ProfilePictureWidget extends StatefulWidget { } class _ProfilePictureWidgetState extends State { - late Future _userFuture; - - @override - void initState() { - super.initState(); - _userFuture = _getUserFuture(); - } - - @override - void didUpdateWidget(ProfilePictureWidget oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.userId != widget.userId) { - _userFuture = _getUserFuture(); - } - } - - Future _getUserFuture() { - if (widget.userId == null || widget.userId!.isEmpty) { - return Future.value(null); - } - return FirebaseFirestore.instance - .collection('users') - .doc(widget.userId) - .get(); - } - @override Widget build(BuildContext context) { + // Si profilePhotoUrl est fourni directement, l'utiliser sans appeler le provider + if (widget.profilePhotoUrl != null && widget.profilePhotoUrl!.isNotEmpty) { + return CircleAvatar( + radius: widget.radius, + backgroundImage: CachedNetworkImageProvider(widget.profilePhotoUrl!), + onBackgroundImageError: (_, __) { + // En cas d'erreur, afficher l'image par défaut + }, + ); + } + if (widget.userId == null || widget.userId!.isEmpty) { return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl); } - return FutureBuilder( - future: _userFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return _buildLoadingAvatar(widget.radius); - } else if (snapshot.hasError) { - print("Error loading profile: ${snapshot.error}"); - return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl); - } else if (snapshot.data != null && snapshot.data!.exists) { - final userData = snapshot.data!.data() as Map?; - final profilePhotoUrl = userData?['profilePhotoUrl'] as String?; + // Utiliser le provider pour récupérer l'utilisateur + final usersProvider = context.watch(); + final user = usersProvider.getUserById(widget.userId!); - if (profilePhotoUrl != null && profilePhotoUrl.isNotEmpty) { - return CircleAvatar( - radius: widget.radius, - backgroundImage: NetworkImage(profilePhotoUrl), - onBackgroundImageError: (e, stack) { - print("Error loading profile image: $e"); - }, - ); - } - } - return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl); + if (user == null) { + return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl); + } + + final profilePhotoUrl = user.profilePhotoUrl; + + if (profilePhotoUrl.isEmpty) { + return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl); + } + + return CircleAvatar( + radius: widget.radius, + backgroundImage: CachedNetworkImageProvider(profilePhotoUrl), + onBackgroundImageError: (_, __) { + // En cas d'erreur, afficher l'image par défaut }, ); } - // Widget utilitaire pour construire un CircleAvatar de chargement - Widget _buildLoadingAvatar(double radius) { - return CircleAvatar( - radius: radius, - backgroundColor: - Colors.grey[300], // Couleur de fond pendant le chargement - child: SizedBox( - width: radius * 0.8, // Ajuster la taille du loader - height: radius * 0.8, - child: const CircularProgressIndicator( - strokeWidth: 2), // Indicateur de chargement - ), - ); - } - - // Widget utilitaire pour construire un CircleAvatar par défaut (avec icône ou image par défaut) Widget _buildDefaultAvatar(double radius, String? defaultImageUrl) { - if (defaultImageUrl != null && defaultImageUrl.isNotEmpty) { - return CircleAvatar( - radius: radius, - // Utilisation de Image.network pour l'image par défaut, avec gestion d'erreur similaire - backgroundImage: Image.network( - defaultImageUrl, - errorBuilder: (context, error, stackTrace) { - print( - "Erreur de chargement Image.network pour l'URL par défaut: $defaultImageUrl, Erreur: $error"); - return _buildIconAvatar( - radius); // Si l'image par défaut ne charge pas, afficher l'icône - }, - ).image, // .image pour ImageProvider - ); - } else { - return _buildIconAvatar( - radius); // Si pas d'URL par défaut fournie, afficher l'icône - } - } - - // Widget utilitaire pour construire un CircleAvatar avec une icône par défaut - Widget _buildIconAvatar(double radius) { return CircleAvatar( radius: radius, - child: FittedBox( - fit: BoxFit.scaleDown, - child: Icon(Icons.account_circle, size: radius * 1.5), - ), + backgroundImage: defaultImageUrl != null && defaultImageUrl.isNotEmpty + ? CachedNetworkImageProvider(defaultImageUrl) + : null, + child: defaultImageUrl == null || defaultImageUrl.isEmpty + ? Icon(Icons.person, size: radius) + : null, ); } } + diff --git a/em2rp/lib/views/widgets/inputs/option_selector_widget.dart b/em2rp/lib/views/widgets/inputs/option_selector_widget.dart index 11526c4..b5c7552 100644 --- a/em2rp/lib/views/widgets/inputs/option_selector_widget.dart +++ b/em2rp/lib/views/widgets/inputs/option_selector_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/models/option_model.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; class OptionSelectorWidget extends StatefulWidget { final List> selectedOptions; @@ -42,15 +43,20 @@ class _OptionSelectorWidgetState extends State { Future _fetchOptions() async { setState(() => _loading = true); - final snapshot = - await FirebaseFirestore.instance.collection('options').get(); - final options = snapshot.docs - .map((doc) => EventOption.fromMap(doc.data(), doc.id)) - .toList(); - setState(() { - _allOptions = options; - _loading = false; - }); + try { + final dataService = DataService(FirebaseFunctionsApiService()); + final optionsData = await dataService.getOptions(); + final options = optionsData + .map((data) => EventOption.fromMap(data, data['id'] as String)) + .toList(); + setState(() { + _allOptions = options; + _loading = false; + }); + } catch (e) { + setState(() => _loading = false); + // Afficher une erreur silencieuse + } } // Méthode publique pour mettre à jour les options depuis l'extérieur @@ -258,27 +264,32 @@ class _OptionSelectorWidgetState extends State { ); } - // Méthode pour charger les détails des options depuis Firebase + // Méthode pour charger les détails des options via l'API Future>> _loadOptionsWithDetails(List> optionsData) async { List> enrichedOptions = []; + // Charger toutes les options via l'API + final dataService = DataService(FirebaseFunctionsApiService()); + final allOptionsData = await dataService.getOptions(); + + // Créer une map pour accès rapide par ID + final optionsMap = { + for (var opt in allOptionsData) opt['id']: opt + }; + for (final optionData in optionsData) { try { - // Si l'option a un ID, récupérer les détails depuis Firestore + // Si l'option a un ID, récupérer les détails depuis l'API if (optionData['id'] != null) { - final doc = await FirebaseFirestore.instance - .collection('options') - .doc(optionData['id']) - .get(); + final firestoreData = optionsMap[optionData['id']]; - if (doc.exists) { - final firestoreData = doc.data()!; + if (firestoreData != null) { enrichedOptions.add({ 'id': optionData['id'], - 'code': firestoreData['code'] ?? optionData['id'], // Récupérer le code + 'code': firestoreData['code'] ?? optionData['id'], 'name': firestoreData['code'] != null && firestoreData['code'].toString().isNotEmpty ? '${firestoreData['code']} - ${firestoreData['name']}' - : firestoreData['name'], // Affichage avec code + : firestoreData['name'], 'details': firestoreData['details'] ?? '', 'price': optionData['price'], 'quantity': optionData['quantity'] ?? 1, @@ -347,17 +358,22 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> { } Future _reloadOptions() async { - final snapshot = await FirebaseFirestore.instance.collection('options').get(); - final updatedOptions = snapshot.docs - .map((doc) => EventOption.fromMap(doc.data(), doc.id)) - .toList(); + try { + final dataService = DataService(FirebaseFunctionsApiService()); + final optionsData = await dataService.getOptions(); + final updatedOptions = optionsData + .map((data) => EventOption.fromMap(data, data['id'] as String)) + .toList(); - setState(() { - _currentOptions = updatedOptions; - }); + setState(() { + _currentOptions = updatedOptions; + }); - // Appeler le callback pour mettre à jour aussi le parent - widget.onOptionsUpdated?.call(updatedOptions); + // Appeler le callback pour mettre à jour aussi le parent + widget.onOptionsUpdated?.call(updatedOptions); + } catch (e) { + // Erreur silencieuse + } } @override @@ -557,23 +573,34 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> { bool _loading = true; Future _isCodeUnique(String code) async { - // Vérifier si le document avec ce code existe déjà - final doc = await FirebaseFirestore.instance - .collection('options') - .doc(code) - .get(); - return !doc.exists; + try { + // Charger toutes les options via l'API + final dataService = DataService(FirebaseFunctionsApiService()); + final optionsData = await dataService.getOptions(); + + // Vérifier si le code existe déjà + return !optionsData.any((opt) => opt['id'] == code); + } catch (e) { + return false; + } } Future _fetchEventTypes() async { setState(() { - _loading=true; - }); - final snapshot = await FirebaseFirestore.instance.collection('eventTypes').get(); - setState(() { - _allEventTypes = snapshot.docs.map((doc) => {'id': doc.id, 'name': doc['name']}).toList(); - _loading = false; + _loading = true; }); + try { + final dataService = DataService(FirebaseFunctionsApiService()); + final eventTypesData = await dataService.getEventTypes(); + setState(() { + _allEventTypes = eventTypesData + .map((data) => {'id': data['id'], 'name': data['name']}) + .toList(); + _loading = false; + }); + } catch (e) { + setState(() => _loading = false); + } } @override @@ -741,8 +768,9 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> { return; } try { - // Utiliser le code comme identifiant du document - await FirebaseFirestore.instance.collection('options').doc(code).set({ + // Créer via l'API + final dataService = DataService(FirebaseFunctionsApiService()); + await dataService.createOption(code, { 'code': code, 'name': name, 'details': _detailsController.text.trim(), @@ -751,7 +779,9 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> { 'eventTypes': _selectedTypes, 'isQuantitative': _isQuantitative, }); - Navigator.pop(context, true); + if (mounted) { + Navigator.pop(context, true); + } } catch (e) { setState(() => _error = 'Erreur lors de la création : $e'); } diff --git a/em2rp/lib/views/widgets/user_management/edit_user_dialog.dart b/em2rp/lib/views/widgets/user_management/edit_user_dialog.dart index a041908..0162f7e 100644 --- a/em2rp/lib/views/widgets/user_management/edit_user_dialog.dart +++ b/em2rp/lib/views/widgets/user_management/edit_user_dialog.dart @@ -3,8 +3,9 @@ import 'package:provider/provider.dart'; import 'package:em2rp/models/user_model.dart'; import 'package:em2rp/providers/users_provider.dart'; import 'package:em2rp/utils/colors.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/models/role_model.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; class EditUserDialog extends StatefulWidget { final UserModel user; @@ -34,16 +35,21 @@ class _EditUserDialogState extends State { } Future _loadRoles() async { - final snapshot = await FirebaseFirestore.instance.collection('roles').get(); - setState(() { - availableRoles = snapshot.docs - .map((doc) => RoleModel.fromMap(doc.data(), doc.id)) - .toList(); - selectedRoleId = widget.user.role.isEmpty - ? (availableRoles.isNotEmpty ? availableRoles.first.id : null) - : widget.user.role; - isLoadingRoles = false; - }); + try { + final dataService = DataService(FirebaseFunctionsApiService()); + final rolesData = await dataService.getRoles(); + setState(() { + availableRoles = rolesData + .map((data) => RoleModel.fromMap(data, data['id'] as String)) + .toList(); + selectedRoleId = widget.user.role.isEmpty + ? (availableRoles.isNotEmpty ? availableRoles.first.id : null) + : widget.user.role; + isLoadingRoles = false; + }); + } catch (e) { + setState(() => isLoadingRoles = false); + } } @override @@ -176,7 +182,7 @@ class _EditUserDialogState extends State { role: selectedRoleId, ); await Provider.of(context, listen: false) - .updateUser(updatedUser, roleId: selectedRoleId); + .updateUser(updatedUser); Navigator.pop(context); }, style: ElevatedButton.styleFrom( diff --git a/em2rp/lib/views/widgets/user_management/user_multi_select_widget.dart b/em2rp/lib/views/widgets/user_management/user_multi_select_widget.dart index bfd1116..7597053 100644 --- a/em2rp/lib/views/widgets/user_management/user_multi_select_widget.dart +++ b/em2rp/lib/views/widgets/user_management/user_multi_select_widget.dart @@ -200,7 +200,10 @@ class UserChipsList extends StatelessWidget { children: users .map((user) => Chip( avatar: ProfilePictureWidget( - userId: user.uid, radius: avatarRadius), + userId: user.uid, + radius: avatarRadius, + profilePhotoUrl: user.profilePhotoUrl, // Passer l'URL directement + ), label: Text('${user.firstName} ${user.lastName}', style: const TextStyle(fontSize: 16)), labelPadding: const EdgeInsets.symmetric(horizontal: 8),