feat: refactor de la gestion des utilisateurs et migration de la logique métier vers les Cloud Functions
Cette mise à jour majeure refactorise entièrement la gestion des utilisateurs pour la faire passer par des Cloud Functions sécurisées et migre une part importante de la logique métier (gestion des événements, maintenances, containers) du client vers le backend.
**Gestion des Utilisateurs (Backend & Frontend):**
- **Nouvelle fonction `createUserWithInvite` :**
- Crée l'utilisateur dans Firebase Auth avec un mot de passe temporaire.
- Crée le document utilisateur correspondant dans Firestore.
- Envoie automatiquement un e-mail de réinitialisation de mot de passe (via l'API REST de Firebase et `axios`) pour que l'utilisateur définisse son propre mot de passe, améliorant la sécurité et l'expérience d'intégration.
- **Refactorisation de `updateUser` et `deleteUser` :**
- Les anciennes fonctions `onCall` sont remplacées par des fonctions `onRequest` (HTTP) standards, alignées avec le reste de l'API.
- La logique de suppression gère désormais la suppression dans Auth et Firestore.
- **Réinitialisation de Mot de Passe (UI) :**
- Ajout d'un bouton "Réinitialiser le mot de passe" sur la carte utilisateur, permettant aux administrateurs d'envoyer un e-mail de réinitialisation à n'importe quel utilisateur.
- **Amélioration de l'UI :**
- Boîte de dialogue de confirmation améliorée pour la suppression d'un utilisateur.
- Notifications (Snackbars) pour les opérations de création, suppression et réinitialisation de mot de passe.
**Migration de la Logique Métier vers les Cloud Functions:**
- **Gestion de la Préparation d'Événements :**
- Migration complète de la logique de validation des étapes (préparation, chargement, déchargement, retour) du client vers de nouvelles Cloud Functions (`validateEquipmentPreparation`, `validateAllLoading`, etc.).
- Le backend gère désormais la mise à jour des statuts de l'événement (`inProgress`, `completed`) et des équipements (`inUse`, `available`).
- Le code frontend (`EventPreparationService`) a été simplifié pour appeler ces nouvelles fonctions au lieu d'effectuer des écritures directes sur Firestore.
- **Création de Maintenance :**
- La fonction `createMaintenance` gère maintenant la mise à jour des équipements associés (`maintenanceIds`) et la création d'alertes (`maintenanceDue`) si une maintenance est prévue prochainement. La logique client a été supprimée.
- **Suppression de Container :**
- La fonction `deleteContainer` a été améliorée pour nettoyer automatiquement les références (`parentBoxIds`) dans tous les équipements contenus avant de supprimer le container.
**Refactorisation et Corrections (Backend & Frontend) :**
- **Fiabilisation des Appels API (Frontend) :**
- Le `ApiService` a été renforcé pour convertir de manière plus robuste les données (notamment les `Map` de type `_JsonMap`) en JSON standard avant de les envoyer aux Cloud Functions, évitant ainsi des erreurs de sérialisation.
- **Correction des Références (Backend) :**
- La fonction `updateUser` convertit correctement les `roleId` (string) en `DocumentReference` Firestore.
- Sécurisation de la vérification de l'assignation d'un utilisateur à un événement (`workforce`) pour éviter les erreurs sur des références nulles.
- **Dépendance (Backend) :**
- Ajout de la librairie `axios` pour effectuer des appels à l'API REST de Firebase.
This commit is contained in:
@@ -393,6 +393,30 @@ exports.deleteContainer = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer le container pour obtenir les équipements
|
||||
const containerDoc = await db.collection('containers').doc(containerId).get();
|
||||
if (containerDoc.exists) {
|
||||
const containerData = containerDoc.data();
|
||||
const equipmentIds = containerData.equipmentIds || [];
|
||||
|
||||
// Retirer le container des parentBoxIds de chaque équipement
|
||||
for (const equipmentId of equipmentIds) {
|
||||
try {
|
||||
const equipmentDoc = await db.collection('equipments').doc(equipmentId).get();
|
||||
if (equipmentDoc.exists) {
|
||||
const equipmentData = equipmentDoc.data();
|
||||
const parentBoxIds = (equipmentData.parentBoxIds || []).filter(boxId => boxId !== containerId);
|
||||
await db.collection('equipments').doc(equipmentId).update({
|
||||
parentBoxIds: parentBoxIds,
|
||||
updatedAt: admin.firestore.Timestamp.now(),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Error updating equipment ${equipmentId} when deleting container:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await db.collection('containers').doc(containerId).delete();
|
||||
|
||||
res.status(200).json({ message: 'Container deleted successfully' });
|
||||
@@ -613,8 +637,73 @@ exports.createMaintenance = onRequest(httpOptions, withCors(async (req, res) =>
|
||||
]);
|
||||
|
||||
const docRef = await db.collection('maintenances').add(dataToSave);
|
||||
const maintenanceId = docRef.id;
|
||||
|
||||
res.status(201).json({ id: docRef.id, message: 'Maintenance created successfully' });
|
||||
// Mettre à jour les équipements concernés
|
||||
if (maintenanceData.equipmentIds && Array.isArray(maintenanceData.equipmentIds)) {
|
||||
for (const equipmentId of maintenanceData.equipmentIds) {
|
||||
try {
|
||||
const equipmentDoc = await db.collection('equipments').doc(equipmentId).get();
|
||||
if (equipmentDoc.exists) {
|
||||
const equipmentData = equipmentDoc.data();
|
||||
const maintenanceIds = equipmentData.maintenanceIds || [];
|
||||
if (!maintenanceIds.includes(maintenanceId)) {
|
||||
maintenanceIds.push(maintenanceId);
|
||||
await db.collection('equipments').doc(equipmentId).update({
|
||||
maintenanceIds: maintenanceIds,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Si la maintenance est planifiée dans les 7 prochains jours, créer une alerte
|
||||
if (maintenanceData.scheduledDate) {
|
||||
const scheduledDate = maintenanceData.scheduledDate.toDate ?
|
||||
maintenanceData.scheduledDate.toDate() :
|
||||
new Date(maintenanceData.scheduledDate);
|
||||
const sevenDaysFromNow = new Date();
|
||||
sevenDaysFromNow.setDate(sevenDaysFromNow.getDate() + 7);
|
||||
|
||||
if (scheduledDate <= sevenDaysFromNow) {
|
||||
// Vérifier si une alerte existe déjà
|
||||
const existingAlerts = await db.collection('alerts')
|
||||
.where('equipmentId', '==', equipmentId)
|
||||
.where('type', '==', 'maintenanceDue')
|
||||
.where('isRead', '==', false)
|
||||
.get();
|
||||
|
||||
let alertExists = false;
|
||||
for (const alertDoc of existingAlerts.docs) {
|
||||
const alertData = alertDoc.data();
|
||||
if (alertData.message && alertData.message.includes(maintenanceData.name || '')) {
|
||||
alertExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alertExists) {
|
||||
const equipmentName = equipmentDoc.exists ?
|
||||
(equipmentDoc.data().name || equipmentId) :
|
||||
equipmentId;
|
||||
|
||||
const daysUntil = Math.ceil((scheduledDate - new Date()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
await db.collection('alerts').add({
|
||||
type: 'maintenanceDue',
|
||||
message: `Maintenance "${maintenanceData.name || 'Sans nom'}" prévue dans ${daysUntil} jour(s) pour ${equipmentName}`,
|
||||
equipmentId: equipmentId,
|
||||
createdAt: admin.firestore.Timestamp.now(),
|
||||
isRead: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Error updating equipment ${equipmentId} for maintenance:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).json({ id: maintenanceId, message: 'Maintenance created successfully' });
|
||||
} catch (error) {
|
||||
logger.error("Error creating maintenance:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -775,6 +864,96 @@ exports.createUser = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
}
|
||||
}));
|
||||
|
||||
// Créer un utilisateur avec invitation par email
|
||||
exports.createUserWithInvite = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const isAdminUser = await auth.isAdmin(decodedToken.uid);
|
||||
|
||||
if (!isAdminUser) {
|
||||
res.status(403).json({ error: 'Forbidden: Admin access required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { email, firstName, lastName, phoneNumber, roleId } = req.body.data;
|
||||
|
||||
if (!email || !firstName || !lastName || !roleId) {
|
||||
res.status(400).json({ error: 'email, firstName, lastName, and roleId are required' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Générer un mot de passe temporaire aléatoire
|
||||
const tempPassword = Math.random().toString(36).slice(-12) + 'Aa1!';
|
||||
|
||||
// Créer l'utilisateur dans Firebase Auth
|
||||
let userRecord;
|
||||
try {
|
||||
userRecord = await admin.auth().createUser({
|
||||
email: email,
|
||||
password: tempPassword,
|
||||
emailVerified: false,
|
||||
displayName: `${firstName} ${lastName}`,
|
||||
});
|
||||
} catch (authError) {
|
||||
logger.error("Error creating user in Auth:", authError);
|
||||
res.status(500).json({ error: `Failed to create user in Auth: ${authError.message}` });
|
||||
return;
|
||||
}
|
||||
|
||||
// Créer le document utilisateur dans Firestore
|
||||
try {
|
||||
await db.collection('users').doc(userRecord.uid).set({
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
email: email,
|
||||
phoneNumber: phoneNumber || '',
|
||||
profilePhotoUrl: '',
|
||||
role: db.collection('roles').doc(roleId),
|
||||
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
createdBy: decodedToken.uid,
|
||||
});
|
||||
} catch (firestoreError) {
|
||||
// Si la création Firestore échoue, supprimer l'utilisateur Auth
|
||||
logger.error("Error creating user in Firestore:", firestoreError);
|
||||
try {
|
||||
await admin.auth().deleteUser(userRecord.uid);
|
||||
} catch (cleanupError) {
|
||||
logger.error("Error cleaning up Auth user:", cleanupError);
|
||||
}
|
||||
res.status(500).json({ error: `Failed to create user in Firestore: ${firestoreError.message}` });
|
||||
return;
|
||||
}
|
||||
|
||||
// Envoyer l'email de réinitialisation du mot de passe
|
||||
// Utilisation de l'API REST de Firebase Auth pour déclencher l'envoi automatique
|
||||
try {
|
||||
const axios = require('axios');
|
||||
const firebaseApiKey = 'AIzaSyARQL4P-t5l-cNjQNP9cMokQrLJ8BorF0U'; // Web API Key
|
||||
|
||||
await axios.post(
|
||||
`https://identitytoolkit.googleapis.com/v1/accounts:sendOobCode?key=${firebaseApiKey}`,
|
||||
{
|
||||
requestType: 'PASSWORD_RESET',
|
||||
email: email,
|
||||
}
|
||||
);
|
||||
logger.info(`Password reset email sent to ${email}`);
|
||||
} catch (emailError) {
|
||||
logger.warn(`Could not send password reset email to ${email}: ${emailError.message}`);
|
||||
// Ne pas faire échouer la requête si l'email ne peut pas être envoyé
|
||||
}
|
||||
|
||||
logger.info(`User ${userRecord.uid} created by ${decodedToken.uid}`);
|
||||
res.status(201).json({
|
||||
id: userRecord.uid,
|
||||
message: 'User created successfully. Password reset email sent.',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error in createUserWithInvite:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}));
|
||||
|
||||
// Mettre à jour un utilisateur
|
||||
exports.updateUser = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
try {
|
||||
@@ -810,6 +989,12 @@ exports.updateUser = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
await db.collection('users').doc(userId).update(filteredData);
|
||||
} else {
|
||||
delete data.uid;
|
||||
|
||||
// Convertir le role string en DocumentReference si présent
|
||||
if (data.role && typeof data.role === 'string') {
|
||||
data.role = db.collection('roles').doc(data.role);
|
||||
}
|
||||
|
||||
await db.collection('users').doc(userId).update(data);
|
||||
}
|
||||
|
||||
@@ -820,6 +1005,49 @@ exports.updateUser = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
}
|
||||
}));
|
||||
|
||||
// Supprimer un utilisateur
|
||||
exports.deleteUser = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const isAdminUser = await auth.isAdmin(decodedToken.uid);
|
||||
|
||||
if (!isAdminUser) {
|
||||
res.status(403).json({ error: 'Forbidden: Admin access required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { userId } = req.body.data;
|
||||
|
||||
if (!userId) {
|
||||
res.status(400).json({ error: 'User ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Empêcher un admin de se supprimer lui-même
|
||||
if (decodedToken.uid === userId) {
|
||||
res.status(400).json({ error: 'Cannot delete your own account' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Supprimer le document utilisateur dans Firestore
|
||||
await db.collection('users').doc(userId).delete();
|
||||
|
||||
// Optionnel: Supprimer l'utilisateur de Firebase Auth
|
||||
// Note: Cela nécessite le SDK Admin et des privilèges élevés
|
||||
try {
|
||||
await admin.auth().deleteUser(userId);
|
||||
} catch (authError) {
|
||||
logger.warn(`Could not delete user from Auth: ${authError.message}`);
|
||||
// On continue même si la suppression Auth échoue
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'User deleted successfully' });
|
||||
} catch (error) {
|
||||
logger.error("Error deleting user:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}));
|
||||
|
||||
// ============================================================================
|
||||
// EQUIPMENT STATUS - Batch Update
|
||||
// ============================================================================
|
||||
@@ -946,7 +1174,15 @@ exports.updateEventEquipment = onRequest(httpOptions, withCors(async (req, res)
|
||||
|
||||
const eventData = eventDoc.data();
|
||||
const isAdminUser = await auth.hasPermission(decodedToken.uid, 'edit_event');
|
||||
const isAssigned = eventData.workforce?.some(ref => ref.path.endsWith(decodedToken.uid));
|
||||
|
||||
// Vérifier si l'utilisateur est assigné en vérifiant workforce de manière sécurisée
|
||||
let isAssigned = false;
|
||||
if (eventData.workforce && Array.isArray(eventData.workforce)) {
|
||||
isAssigned = eventData.workforce.some(ref => {
|
||||
if (!ref || !ref.path) return false;
|
||||
return ref.path.endsWith(decodedToken.uid) || ref.path === `/users/${decodedToken.uid}`;
|
||||
});
|
||||
}
|
||||
|
||||
if (!isAssigned && !isAdminUser) {
|
||||
res.status(403).json({ error: 'Forbidden: Not assigned to this event' });
|
||||
@@ -1573,148 +1809,6 @@ exports.getUser = onCall(async (request) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// USER MANAGEMENT - Delete & Update
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Supprime un utilisateur (Auth + Firestore)
|
||||
* Permissions: 'delete_user' OU propriétaire
|
||||
*/
|
||||
exports.deleteUser = onCall(async (request) => {
|
||||
const { auth, data } = request;
|
||||
|
||||
if (!auth) {
|
||||
throw new Error("Unauthorized: Authentication required");
|
||||
}
|
||||
|
||||
const { userId } = data;
|
||||
if (!userId) {
|
||||
throw new Error("userId is required");
|
||||
}
|
||||
|
||||
try {
|
||||
// Vérifier les permissions
|
||||
const callerDoc = await db.collection("users").doc(auth.uid).get();
|
||||
const callerData = callerDoc.data();
|
||||
|
||||
if (!callerData) {
|
||||
throw new Error("Caller user not found");
|
||||
}
|
||||
|
||||
// Vérifier si l'utilisateur a la permission delete_user
|
||||
let canDelete = false;
|
||||
if (callerData.role) {
|
||||
const roleDoc = await callerData.role.get();
|
||||
const roleData = roleDoc.data();
|
||||
canDelete = roleData?.permissions?.includes("delete_user") || false;
|
||||
}
|
||||
|
||||
// Ou si c'est le propriétaire (mais on ne peut pas se supprimer soi-même)
|
||||
if (userId === auth.uid) {
|
||||
throw new Error("Cannot delete your own account");
|
||||
}
|
||||
|
||||
if (!canDelete) {
|
||||
throw new Error("Unauthorized: Missing delete_user permission");
|
||||
}
|
||||
|
||||
// Supprimer de Firebase Auth
|
||||
try {
|
||||
await admin.auth().deleteUser(userId);
|
||||
} catch (authError) {
|
||||
logger.warn(`Could not delete user from Auth: ${authError.message}`);
|
||||
// Continuer même si Auth échoue (l'utilisateur peut ne plus exister dans Auth)
|
||||
}
|
||||
|
||||
// Supprimer de Firestore
|
||||
await db.collection("users").doc(userId).delete();
|
||||
|
||||
logger.info(`User ${userId} deleted by ${auth.uid}`);
|
||||
return { success: true, message: "User deleted successfully" };
|
||||
} catch (error) {
|
||||
logger.error("Error deleting user:", error);
|
||||
throw new Error(error.message || "Failed to delete user");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Met à jour un utilisateur
|
||||
* Permissions: 'edit_user' OU propriétaire (modifications limitées)
|
||||
*/
|
||||
exports.updateUser = onCall(async (request) => {
|
||||
const { auth, data } = request;
|
||||
|
||||
if (!auth) {
|
||||
throw new Error("Unauthorized: Authentication required");
|
||||
}
|
||||
|
||||
const { userId, userData } = data;
|
||||
if (!userId || !userData) {
|
||||
throw new Error("userId and userData are required");
|
||||
}
|
||||
|
||||
try {
|
||||
// Vérifier les permissions
|
||||
const callerDoc = await db.collection("users").doc(auth.uid).get();
|
||||
const callerData = callerDoc.data();
|
||||
|
||||
if (!callerData) {
|
||||
throw new Error("Caller user not found");
|
||||
}
|
||||
|
||||
let canEditAll = false;
|
||||
if (callerData.role) {
|
||||
const roleDoc = await callerData.role.get();
|
||||
const roleData = roleDoc.data();
|
||||
canEditAll = roleData?.permissions?.includes("edit_user") || false;
|
||||
}
|
||||
|
||||
const isOwner = userId === auth.uid;
|
||||
|
||||
// Si pas de permission edit_user et pas propriétaire, refuser
|
||||
if (!canEditAll && !isOwner) {
|
||||
throw new Error("Unauthorized: Missing edit_user permission");
|
||||
}
|
||||
|
||||
// Préparer les données à mettre à jour
|
||||
const updateData = {
|
||||
firstName: userData.firstName,
|
||||
lastName: userData.lastName,
|
||||
email: userData.email,
|
||||
phoneNumber: userData.phoneNumber || "",
|
||||
};
|
||||
|
||||
// Seuls ceux avec edit_user peuvent changer le rôle
|
||||
if (userData.role) {
|
||||
if (!canEditAll) {
|
||||
throw new Error("Unauthorized: Cannot change role without edit_user permission");
|
||||
}
|
||||
// Créer la référence au rôle
|
||||
updateData.role = db.collection("roles").doc(userData.role);
|
||||
}
|
||||
|
||||
// Mettre à jour Firestore
|
||||
await db.collection("users").doc(userId).update(updateData);
|
||||
|
||||
// Mettre à jour Firebase Auth si email a changé (seulement avec edit_user)
|
||||
if (userData.email && canEditAll) {
|
||||
try {
|
||||
await admin.auth().updateUser(userId, {
|
||||
email: userData.email,
|
||||
});
|
||||
} catch (authError) {
|
||||
logger.warn(`Could not update email in Auth: ${authError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`User ${userId} updated by ${auth.uid}`);
|
||||
return { success: true, message: "User updated successfully" };
|
||||
} catch (error) {
|
||||
logger.error("Error updating user:", error);
|
||||
throw new Error(error.message || "Failed to update user");
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// EQUIPMENT AVAILABILITY - Vérification de disponibilité
|
||||
@@ -2264,3 +2358,438 @@ exports.deleteMaintenance = onRequest(httpOptions, withCors(async (req, res) =>
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}));
|
||||
|
||||
// ============================================================================
|
||||
// EVENT PREPARATION - Validation des étapes de préparation
|
||||
// ============================================================================
|
||||
|
||||
// Helper: Mettre à jour le statut d'un équipement
|
||||
async function updateEquipmentStatus(equipmentId, status) {
|
||||
try {
|
||||
const doc = await db.collection('equipments').doc(equipmentId).get();
|
||||
if (!doc.exists) {
|
||||
logger.warn(`Equipment ${equipmentId} does not exist, skipping status update`);
|
||||
return;
|
||||
}
|
||||
|
||||
await db.collection('equipments').doc(equipmentId).update({
|
||||
status: status,
|
||||
updatedAt: admin.firestore.Timestamp.now(),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error updating equipment status for ${equipmentId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Valider un équipement individuel en préparation
|
||||
exports.validateEquipmentPreparation = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const canManage = await auth.hasPermission(decodedToken.uid, 'manage_events');
|
||||
if (!canManage) {
|
||||
res.status(403).json({ error: 'Forbidden: Requires manage_events permission' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { eventId, equipmentId } = req.body.data;
|
||||
if (!eventId || !equipmentId) {
|
||||
res.status(400).json({ error: 'eventId and equipmentId are required' });
|
||||
return;
|
||||
}
|
||||
|
||||
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 assignedEquipment = eventData.assignedEquipment || [];
|
||||
|
||||
// Mettre à jour le statut de l'équipement
|
||||
const updatedEquipment = assignedEquipment.map(eq => {
|
||||
if (eq.equipmentId === equipmentId) {
|
||||
return { ...eq, isPrepared: true };
|
||||
}
|
||||
return eq;
|
||||
});
|
||||
|
||||
// Vérifier si tous sont préparés
|
||||
const allPrepared = updatedEquipment.every(eq => eq.isPrepared);
|
||||
|
||||
const updateData = {
|
||||
assignedEquipment: updatedEquipment,
|
||||
preparationStatus: allPrepared ? 'completed' : 'inProgress',
|
||||
};
|
||||
|
||||
await db.collection('events').doc(eventId).update(updateData);
|
||||
|
||||
res.status(200).json({ success: true, allPrepared });
|
||||
} catch (error) {
|
||||
logger.error("Error validating equipment preparation:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}));
|
||||
|
||||
// Valider tous les équipements en préparation
|
||||
exports.validateAllPreparation = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const canManage = await auth.hasPermission(decodedToken.uid, 'manage_events');
|
||||
if (!canManage) {
|
||||
res.status(403).json({ error: 'Forbidden: Requires manage_events permission' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { eventId } = req.body.data;
|
||||
if (!eventId) {
|
||||
res.status(400).json({ error: 'eventId is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
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 assignedEquipment = eventData.assignedEquipment || [];
|
||||
|
||||
// Marquer tous comme préparés
|
||||
const updatedEquipment = assignedEquipment.map(eq => ({
|
||||
...eq,
|
||||
isPrepared: true,
|
||||
}));
|
||||
|
||||
await db.collection('events').doc(eventId).update({
|
||||
assignedEquipment: updatedEquipment,
|
||||
preparationStatus: 'completed',
|
||||
});
|
||||
|
||||
// Mettre à jour le statut des équipements à "inUse"
|
||||
for (const equipment of assignedEquipment) {
|
||||
await updateEquipmentStatus(equipment.equipmentId, 'inUse');
|
||||
}
|
||||
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
logger.error("Error validating all preparation:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}));
|
||||
|
||||
// Valider un équipement individuel pour le chargement
|
||||
exports.validateEquipmentLoading = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const canManage = await auth.hasPermission(decodedToken.uid, 'manage_events');
|
||||
if (!canManage) {
|
||||
res.status(403).json({ error: 'Forbidden: Requires manage_events permission' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { eventId, equipmentId } = req.body.data;
|
||||
if (!eventId || !equipmentId) {
|
||||
res.status(400).json({ error: 'eventId and equipmentId are required' });
|
||||
return;
|
||||
}
|
||||
|
||||
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 assignedEquipment = eventData.assignedEquipment || [];
|
||||
|
||||
const updatedEquipment = assignedEquipment.map(eq => {
|
||||
if (eq.equipmentId === equipmentId) {
|
||||
return { ...eq, isLoaded: true };
|
||||
}
|
||||
return eq;
|
||||
});
|
||||
|
||||
const allLoaded = updatedEquipment.every(eq => eq.isLoaded);
|
||||
|
||||
const updateData = {
|
||||
assignedEquipment: updatedEquipment,
|
||||
loadingStatus: allLoaded ? 'completed' : 'inProgress',
|
||||
};
|
||||
|
||||
await db.collection('events').doc(eventId).update(updateData);
|
||||
|
||||
res.status(200).json({ success: true, allLoaded });
|
||||
} catch (error) {
|
||||
logger.error("Error validating equipment loading:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}));
|
||||
|
||||
// Valider tous les équipements pour le chargement
|
||||
exports.validateAllLoading = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const canManage = await auth.hasPermission(decodedToken.uid, 'manage_events');
|
||||
if (!canManage) {
|
||||
res.status(403).json({ error: 'Forbidden: Requires manage_events permission' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { eventId } = req.body.data;
|
||||
if (!eventId) {
|
||||
res.status(400).json({ error: 'eventId is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
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 assignedEquipment = eventData.assignedEquipment || [];
|
||||
|
||||
const updatedEquipment = assignedEquipment.map(eq => ({
|
||||
...eq,
|
||||
isLoaded: true,
|
||||
}));
|
||||
|
||||
await db.collection('events').doc(eventId).update({
|
||||
assignedEquipment: updatedEquipment,
|
||||
loadingStatus: 'completed',
|
||||
});
|
||||
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
logger.error("Error validating all loading:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}));
|
||||
|
||||
// Valider un équipement individuel pour le déchargement
|
||||
exports.validateEquipmentUnloading = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const canManage = await auth.hasPermission(decodedToken.uid, 'manage_events');
|
||||
if (!canManage) {
|
||||
res.status(403).json({ error: 'Forbidden: Requires manage_events permission' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { eventId, equipmentId } = req.body.data;
|
||||
if (!eventId || !equipmentId) {
|
||||
res.status(400).json({ error: 'eventId and equipmentId are required' });
|
||||
return;
|
||||
}
|
||||
|
||||
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 assignedEquipment = eventData.assignedEquipment || [];
|
||||
|
||||
const updatedEquipment = assignedEquipment.map(eq => {
|
||||
if (eq.equipmentId === equipmentId) {
|
||||
return { ...eq, isUnloaded: true };
|
||||
}
|
||||
return eq;
|
||||
});
|
||||
|
||||
const allUnloaded = updatedEquipment.every(eq => eq.isUnloaded);
|
||||
|
||||
const updateData = {
|
||||
assignedEquipment: updatedEquipment,
|
||||
unloadingStatus: allUnloaded ? 'completed' : 'inProgress',
|
||||
};
|
||||
|
||||
await db.collection('events').doc(eventId).update(updateData);
|
||||
|
||||
res.status(200).json({ success: true, allUnloaded });
|
||||
} catch (error) {
|
||||
logger.error("Error validating equipment unloading:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}));
|
||||
|
||||
// Valider tous les équipements pour le déchargement
|
||||
exports.validateAllUnloading = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const canManage = await auth.hasPermission(decodedToken.uid, 'manage_events');
|
||||
if (!canManage) {
|
||||
res.status(403).json({ error: 'Forbidden: Requires manage_events permission' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { eventId } = req.body.data;
|
||||
if (!eventId) {
|
||||
res.status(400).json({ error: 'eventId is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
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 assignedEquipment = eventData.assignedEquipment || [];
|
||||
|
||||
const updatedEquipment = assignedEquipment.map(eq => ({
|
||||
...eq,
|
||||
isUnloaded: true,
|
||||
}));
|
||||
|
||||
await db.collection('events').doc(eventId).update({
|
||||
assignedEquipment: updatedEquipment,
|
||||
unloadingStatus: 'completed',
|
||||
});
|
||||
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
logger.error("Error validating all unloading:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}));
|
||||
|
||||
// Valider un équipement individuel pour le retour
|
||||
exports.validateEquipmentReturn = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const canManage = await auth.hasPermission(decodedToken.uid, 'manage_events');
|
||||
if (!canManage) {
|
||||
res.status(403).json({ error: 'Forbidden: Requires manage_events permission' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { eventId, equipmentId, returnedQuantity } = req.body.data;
|
||||
if (!eventId || !equipmentId) {
|
||||
res.status(400).json({ error: 'eventId and equipmentId are required' });
|
||||
return;
|
||||
}
|
||||
|
||||
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 assignedEquipment = eventData.assignedEquipment || [];
|
||||
|
||||
const updatedEquipment = assignedEquipment.map(eq => {
|
||||
if (eq.equipmentId === equipmentId) {
|
||||
return {
|
||||
...eq,
|
||||
isReturned: true,
|
||||
returnedQuantity: returnedQuantity !== undefined ? returnedQuantity : eq.returnedQuantity,
|
||||
};
|
||||
}
|
||||
return eq;
|
||||
});
|
||||
|
||||
const allReturned = updatedEquipment.every(eq => eq.isReturned);
|
||||
|
||||
const updateData = {
|
||||
assignedEquipment: updatedEquipment,
|
||||
returnStatus: allReturned ? 'completed' : 'inProgress',
|
||||
};
|
||||
|
||||
await db.collection('events').doc(eventId).update(updateData);
|
||||
|
||||
// Mettre à jour le stock si c'est un consommable
|
||||
if (returnedQuantity !== undefined) {
|
||||
const equipmentDoc = await db.collection('equipments').doc(equipmentId).get();
|
||||
if (equipmentDoc.exists) {
|
||||
const equipmentData = equipmentDoc.data();
|
||||
if (equipmentData.hasQuantity) {
|
||||
const currentAvailable = equipmentData.availableQuantity || 0;
|
||||
await db.collection('equipments').doc(equipmentId).update({
|
||||
availableQuantity: currentAvailable + returnedQuantity,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({ success: true, allReturned });
|
||||
} catch (error) {
|
||||
logger.error("Error validating equipment return:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}));
|
||||
|
||||
// Valider tous les retours
|
||||
exports.validateAllReturn = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const canManage = await auth.hasPermission(decodedToken.uid, 'manage_events');
|
||||
if (!canManage) {
|
||||
res.status(403).json({ error: 'Forbidden: Requires manage_events permission' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { eventId, returnedQuantities } = req.body.data;
|
||||
if (!eventId) {
|
||||
res.status(400).json({ error: 'eventId is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
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 assignedEquipment = eventData.assignedEquipment || [];
|
||||
|
||||
const updatedEquipment = assignedEquipment.map(eq => {
|
||||
const returnedQty = returnedQuantities?.[eq.equipmentId] || eq.returnedQuantity || eq.quantity;
|
||||
return {
|
||||
...eq,
|
||||
isReturned: true,
|
||||
returnedQuantity: returnedQty,
|
||||
};
|
||||
});
|
||||
|
||||
await db.collection('events').doc(eventId).update({
|
||||
assignedEquipment: updatedEquipment,
|
||||
returnStatus: 'completed',
|
||||
});
|
||||
|
||||
// Mettre à jour le statut des équipements à "available" et gérer les stocks
|
||||
for (const equipment of updatedEquipment) {
|
||||
const equipmentDoc = await db.collection('equipments').doc(equipment.equipmentId).get();
|
||||
if (equipmentDoc.exists) {
|
||||
const equipmentData = equipmentDoc.data();
|
||||
|
||||
// Mettre à jour le statut uniquement pour les équipements non quantifiables
|
||||
if (!equipmentData.hasQuantity) {
|
||||
await updateEquipmentStatus(equipment.equipmentId, 'available');
|
||||
}
|
||||
|
||||
// Restaurer le stock pour les consommables
|
||||
if (equipmentData.hasQuantity && equipment.returnedQuantity) {
|
||||
const currentAvailable = equipmentData.availableQuantity || 0;
|
||||
await db.collection('equipments').doc(equipment.equipmentId).update({
|
||||
availableQuantity: currentAvailable + equipment.returnedQuantity,
|
||||
updatedAt: admin.firestore.Timestamp.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
logger.error("Error validating all return:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
59
em2rp/functions/package-lock.json
generated
59
em2rp/functions/package-lock.json
generated
@@ -6,6 +6,7 @@
|
||||
"": {
|
||||
"name": "functions",
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"firebase-admin": "^12.6.0",
|
||||
"firebase-functions": "^6.0.1"
|
||||
},
|
||||
@@ -1923,8 +1924,34 @@
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "29.7.0",
|
||||
@@ -2387,7 +2414,6 @@
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
@@ -2549,7 +2575,6 @@
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
@@ -2739,7 +2764,6 @@
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
@@ -3342,6 +3366,26 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz",
|
||||
@@ -3725,7 +3769,6 @@
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
@@ -5758,6 +5801,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
},
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"firebase-admin": "^12.6.0",
|
||||
"firebase-functions": "^6.0.1"
|
||||
},
|
||||
|
||||
@@ -65,7 +65,7 @@ class UserModel {
|
||||
return {
|
||||
'firstName': firstName,
|
||||
'lastName': lastName,
|
||||
'role': FirebaseFirestore.instance.collection('roles').doc(role),
|
||||
'role': role, // Envoyer directement le string roleId au lieu de créer une DocumentReference
|
||||
'profilePhotoUrl': profilePhotoUrl,
|
||||
'email': email,
|
||||
'phoneNumber': phoneNumber,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:em2rp/models/user_model.dart';
|
||||
import 'package:em2rp/services/data_service.dart';
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
@@ -81,9 +82,21 @@ class UsersProvider with ChangeNotifier {
|
||||
required String roleId,
|
||||
}) async {
|
||||
try {
|
||||
// TODO: Implémenter via Cloud Function
|
||||
print('Creating user with email invite: $email');
|
||||
await fetchUsers(); // Recharger la liste
|
||||
|
||||
// Appeler la Cloud Function pour créer l'utilisateur
|
||||
await _dataService.createUserWithInvite(
|
||||
email: email,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
phoneNumber: phoneNumber,
|
||||
roleId: roleId,
|
||||
);
|
||||
|
||||
// Recharger la liste des utilisateurs
|
||||
await fetchUsers();
|
||||
|
||||
print('User created successfully: $email');
|
||||
} catch (e) {
|
||||
print('Error creating user with email invite: $e');
|
||||
rethrow;
|
||||
@@ -92,9 +105,13 @@ class UsersProvider with ChangeNotifier {
|
||||
|
||||
/// Réinitialisation du mot de passe
|
||||
Future<void> resetPassword(String email) async {
|
||||
// Firebase Auth reste OK
|
||||
// await _userService.resetPassword(email);
|
||||
// TODO: Implémenter via Cloud Function
|
||||
print('Reset password for: $email');
|
||||
try {
|
||||
// Firebase Auth reste OK (ce n'est pas Firestore)
|
||||
await FirebaseAuth.instance.sendPasswordResetEmail(email: email);
|
||||
print('Email de réinitialisation envoyé à $email');
|
||||
} catch (e) {
|
||||
print('Error reset password: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,38 +35,110 @@ class FirebaseFunctionsApiService implements ApiService {
|
||||
};
|
||||
}
|
||||
|
||||
/// Convertit récursivement les Timestamps Firestore, DocumentReference et GeoPoint en formats encodables
|
||||
dynamic _convertTimestamps(dynamic value) {
|
||||
/// Convertit récursivement TOUT en types JSON standards (String, num, bool, List, Map)
|
||||
/// Garantit que toutes les Maps sont des Map<String, dynamic> littérales
|
||||
dynamic _toJsonSafe(dynamic value) {
|
||||
if (value == null) return null;
|
||||
|
||||
// Types primitifs JSON-safe
|
||||
if (value is String || value is num || value is bool) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Types Firestore
|
||||
if (value is Timestamp) {
|
||||
// Convertir Timestamp en ISO string
|
||||
return value.toDate().toIso8601String();
|
||||
} else if (value is DateTime) {
|
||||
// Convertir DateTime en ISO string
|
||||
}
|
||||
if (value is DateTime) {
|
||||
return value.toIso8601String();
|
||||
} else if (value is DocumentReference) {
|
||||
// Convertir DocumentReference en path string
|
||||
}
|
||||
if (value is DocumentReference) {
|
||||
return value.path;
|
||||
} else if (value is GeoPoint) {
|
||||
// Convertir GeoPoint en objet avec latitude et longitude
|
||||
return {
|
||||
}
|
||||
if (value is GeoPoint) {
|
||||
// Créer une Map littérale explicite
|
||||
return <String, dynamic>{
|
||||
'latitude': value.latitude,
|
||||
'longitude': value.longitude,
|
||||
};
|
||||
} else if (value is Map) {
|
||||
// Parcourir récursivement les Maps et créer une nouvelle Map typée
|
||||
final Map<String, dynamic> result = {};
|
||||
value.forEach((key, val) {
|
||||
result[key.toString()] = _convertTimestamps(val);
|
||||
});
|
||||
return result;
|
||||
} else if (value is List) {
|
||||
// Parcourir récursivement les Lists
|
||||
return value.map((item) => _convertTimestamps(item)).toList();
|
||||
}
|
||||
|
||||
return value;
|
||||
// Listes - créer une nouvelle List littérale
|
||||
if (value is List) {
|
||||
final result = <dynamic>[];
|
||||
for (final item in value) {
|
||||
result.add(_toJsonSafe(item));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Maps - créer une nouvelle Map littérale explicite
|
||||
if (value is Map) {
|
||||
final result = <String, dynamic>{};
|
||||
value.forEach((k, v) {
|
||||
final key = k.toString();
|
||||
final convertedValue = _toJsonSafe(v);
|
||||
result[key] = convertedValue;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Type non supporté - retourner en String
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
/// Prépare les données pour jsonEncode en faisant un double passage
|
||||
Map<String, dynamic> _prepareForJson(Map<String, dynamic> data) {
|
||||
try {
|
||||
// Premier passage : convertir tous les types Firestore
|
||||
final safeData = _toJsonSafe(data);
|
||||
|
||||
// Deuxième passage : encoder puis décoder pour forcer la normalisation
|
||||
// Cela garantit que tout est 100% compatible JSON et élimine tous les _JsonMap
|
||||
final jsonString = jsonEncode(safeData);
|
||||
final decoded = jsonDecode(jsonString);
|
||||
|
||||
// Force le type Map<String, dynamic>
|
||||
if (decoded is Map) {
|
||||
return Map<String, dynamic>.from(decoded);
|
||||
}
|
||||
|
||||
// Fallback - ne devrait jamais arriver
|
||||
return Map<String, dynamic>.from(safeData as Map);
|
||||
} catch (e) {
|
||||
// Si l'encodage échoue, essayer de créer une copie profonde manuelle
|
||||
print('[API] Error in _prepareForJson: $e');
|
||||
print('[API] Trying manual deep copy...');
|
||||
return _deepCopyMap(data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Copie profonde manuelle d'une Map pour éviter les _JsonMap
|
||||
Map<String, dynamic> _deepCopyMap(Map<String, dynamic> source) {
|
||||
final result = <String, dynamic>{};
|
||||
source.forEach((key, value) {
|
||||
if (value is Map) {
|
||||
result[key] = _deepCopyMap(Map<String, dynamic>.from(value));
|
||||
} else if (value is List) {
|
||||
result[key] = _deepCopyList(value);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Copie profonde manuelle d'une List
|
||||
List<dynamic> _deepCopyList(List<dynamic> source) {
|
||||
return source.map((item) {
|
||||
if (item is Map) {
|
||||
return _deepCopyMap(Map<String, dynamic>.from(item));
|
||||
} else if (item is List) {
|
||||
return _deepCopyList(item);
|
||||
} else {
|
||||
return item;
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -74,16 +146,20 @@ class FirebaseFunctionsApiService implements ApiService {
|
||||
final url = Uri.parse('$_baseUrl/$functionName');
|
||||
final headers = await _getHeaders();
|
||||
|
||||
// Convertir les Timestamps avant l'envoi
|
||||
final convertedData = _convertTimestamps(data) as Map<String, dynamic>;
|
||||
// Préparer les données avec double passage pour éviter les _JsonMap
|
||||
final preparedData = _prepareForJson(data);
|
||||
|
||||
// Log pour débogage
|
||||
print('[API] Calling $functionName with eventId: ${convertedData['eventId']}');
|
||||
print('[API] Calling $functionName with eventId: ${preparedData['eventId']}');
|
||||
|
||||
try {
|
||||
// Encoder directement avec jsonEncode standard
|
||||
final bodyJson = jsonEncode({'data': preparedData});
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: headers,
|
||||
body: jsonEncode({'data': convertedData}),
|
||||
body: bodyJson,
|
||||
);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
@@ -96,6 +172,14 @@ class FirebaseFunctionsApiService implements ApiService {
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print('[API] Error during request: $e');
|
||||
print('[API] Error type: ${e.runtimeType}');
|
||||
throw ApiException(
|
||||
message: 'Error calling $functionName: $e',
|
||||
statusCode: 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -124,13 +208,13 @@ class FirebaseFunctionsApiService implements ApiService {
|
||||
final url = Uri.parse('$_baseUrl/$endpoint');
|
||||
final headers = await _getHeaders();
|
||||
|
||||
// Convertir les Timestamps avant l'envoi
|
||||
final convertedData = _convertTimestamps(data) as Map<String, dynamic>;
|
||||
// Préparer les données avec double passage
|
||||
final preparedData = _prepareForJson(data);
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: headers,
|
||||
body: jsonEncode({'data': convertedData}),
|
||||
body: jsonEncode({'data': preparedData}),
|
||||
);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
@@ -150,13 +234,13 @@ class FirebaseFunctionsApiService implements ApiService {
|
||||
final url = Uri.parse('$_baseUrl/$endpoint');
|
||||
final headers = await _getHeaders();
|
||||
|
||||
// Convertir les Timestamps avant l'envoi
|
||||
final convertedData = _convertTimestamps(data) as Map<String, dynamic>;
|
||||
// Préparer les données avec double passage
|
||||
final preparedData = _prepareForJson(data);
|
||||
|
||||
final response = await http.put(
|
||||
url,
|
||||
headers: headers,
|
||||
body: jsonEncode({'data': convertedData}),
|
||||
body: jsonEncode({'data': preparedData}),
|
||||
);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
@@ -176,13 +260,13 @@ class FirebaseFunctionsApiService implements ApiService {
|
||||
final url = Uri.parse('$_baseUrl/$endpoint');
|
||||
final headers = await _getHeaders();
|
||||
|
||||
// Convertir les Timestamps avant l'envoi si data existe
|
||||
final convertedData = data != null ? _convertTimestamps(data) as Map<String, dynamic> : null;
|
||||
// Préparer les données avec double passage si data existe
|
||||
final preparedData = data != null ? _prepareForJson(data) : null;
|
||||
|
||||
final response = await http.delete(
|
||||
url,
|
||||
headers: headers,
|
||||
body: convertedData != null ? jsonEncode({'data': convertedData}) : null,
|
||||
body: preparedData != null ? jsonEncode({'data': preparedData}) : null,
|
||||
);
|
||||
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
|
||||
@@ -41,27 +41,8 @@ class ContainerService {
|
||||
/// Supprimer un container (via Cloud Function)
|
||||
Future<void> deleteContainer(String id) async {
|
||||
try {
|
||||
// Récupérer le container pour obtenir les équipements
|
||||
final container = await getContainerById(id);
|
||||
if (container != null && container.equipmentIds.isNotEmpty) {
|
||||
// Retirer le container des parentBoxIds de chaque équipement
|
||||
for (final equipmentId in container.equipmentIds) {
|
||||
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
|
||||
if (equipmentDoc.exists) {
|
||||
final equipment = EquipmentModel.fromMap(
|
||||
equipmentDoc.data() as Map<String, dynamic>,
|
||||
equipmentDoc.id,
|
||||
);
|
||||
final updatedParents = equipment.parentBoxIds.where((boxId) => boxId != id).toList();
|
||||
await _equipmentCollection.doc(equipmentId).update({
|
||||
'parentBoxIds': updatedParents,
|
||||
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _apiService.call('deleteContainer', {'containerId': id});
|
||||
// Note: La Cloud Function gère maintenant la mise à jour des équipements
|
||||
} catch (e) {
|
||||
print('Error deleting container: $e');
|
||||
rethrow;
|
||||
|
||||
@@ -496,11 +496,34 @@ class DataService {
|
||||
/// Met à jour un utilisateur
|
||||
Future<void> updateUser(String userId, Map<String, dynamic> data) async {
|
||||
try {
|
||||
final requestData = {'userId': userId, ...data};
|
||||
await _apiService.call('updateUser', requestData);
|
||||
await _apiService.call('updateUser', {
|
||||
'userId': userId,
|
||||
'data': data,
|
||||
});
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la mise à jour de l\'utilisateur: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée un utilisateur avec invitation par email
|
||||
Future<Map<String, dynamic>> createUserWithInvite({
|
||||
required String email,
|
||||
required String firstName,
|
||||
required String lastName,
|
||||
String? phoneNumber,
|
||||
required String roleId,
|
||||
}) async {
|
||||
try {
|
||||
final result = await _apiService.call('createUserWithInvite', {
|
||||
'email': email,
|
||||
'firstName': firstName,
|
||||
'lastName': lastName,
|
||||
'phoneNumber': phoneNumber ?? '',
|
||||
'roleId': roleId,
|
||||
});
|
||||
return result;
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la création de l\'utilisateur: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:em2rp/models/event_model.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/api_service.dart';
|
||||
|
||||
class EventPreparationService {
|
||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
final EquipmentService _equipmentService = EquipmentService();
|
||||
final ApiService _apiService = apiService;
|
||||
|
||||
// Collection references
|
||||
// Collection references (utilisées uniquement pour les lectures)
|
||||
CollectionReference get _eventsCollection => _firestore.collection('events');
|
||||
CollectionReference get _equipmentCollection => _firestore.collection('equipments');
|
||||
|
||||
@@ -17,34 +17,10 @@ class EventPreparationService {
|
||||
/// Valider un équipement individuel en préparation
|
||||
Future<void> validateEquipmentPreparation(String eventId, String equipmentId) async {
|
||||
try {
|
||||
final event = await _getEvent(eventId);
|
||||
if (event == null) {
|
||||
throw Exception('Event not found');
|
||||
}
|
||||
|
||||
// Mettre à jour le statut de l'équipement dans la liste
|
||||
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||
if (eq.equipmentId == equipmentId) {
|
||||
return eq.copyWith(isPrepared: true);
|
||||
}
|
||||
return eq;
|
||||
}).toList();
|
||||
|
||||
// Vérifier si tous les équipements sont préparés
|
||||
final allPrepared = updatedEquipment.every((eq) => eq.isPrepared);
|
||||
|
||||
final updateData = <String, dynamic>{
|
||||
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||
};
|
||||
|
||||
// Mettre à jour le statut selon la complétion
|
||||
if (allPrepared) {
|
||||
updateData['preparationStatus'] = preparationStatusToString(PreparationStatus.completed);
|
||||
} else {
|
||||
updateData['preparationStatus'] = preparationStatusToString(PreparationStatus.inProgress);
|
||||
}
|
||||
|
||||
await _eventsCollection.doc(eventId).update(updateData);
|
||||
await _apiService.call('validateEquipmentPreparation', {
|
||||
'eventId': eventId,
|
||||
'equipmentId': equipmentId,
|
||||
});
|
||||
} catch (e) {
|
||||
print('Error validating equipment preparation: $e');
|
||||
rethrow;
|
||||
@@ -54,32 +30,12 @@ class EventPreparationService {
|
||||
/// Valider tous les équipements en préparation
|
||||
Future<void> validateAllPreparation(String eventId) async {
|
||||
try {
|
||||
final event = await _getEvent(eventId);
|
||||
if (event == null) {
|
||||
throw Exception('Event not found');
|
||||
}
|
||||
|
||||
// Marquer tous les équipements comme préparés
|
||||
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||
return eq.copyWith(isPrepared: true);
|
||||
}).toList();
|
||||
|
||||
await _eventsCollection.doc(eventId).update({
|
||||
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||
'preparationStatus': preparationStatusToString(PreparationStatus.completed),
|
||||
await _apiService.call('validateAllPreparation', {
|
||||
'eventId': eventId,
|
||||
});
|
||||
|
||||
// Invalider le cache des statuts d'équipement
|
||||
EquipmentStatusCalculator.invalidateGlobalCache();
|
||||
|
||||
// Mettre à jour le statut des équipements à "inUse" (seulement pour les équipements qui existent)
|
||||
for (var equipment in event.assignedEquipment) {
|
||||
// Vérifier si l'équipement existe avant de mettre à jour son statut
|
||||
final doc = await _equipmentCollection.doc(equipment.equipmentId).get();
|
||||
if (doc.exists) {
|
||||
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.inUse);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error validating all preparation: $e');
|
||||
rethrow;
|
||||
@@ -128,55 +84,11 @@ class EventPreparationService {
|
||||
int? returnedQuantity,
|
||||
}) async {
|
||||
try {
|
||||
final event = await _getEvent(eventId);
|
||||
if (event == null) {
|
||||
throw Exception('Event not found');
|
||||
}
|
||||
|
||||
// Mettre à jour le statut de l'équipement dans la liste
|
||||
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||
if (eq.equipmentId == equipmentId) {
|
||||
return eq.copyWith(
|
||||
isReturned: true,
|
||||
returnedQuantity: returnedQuantity,
|
||||
);
|
||||
}
|
||||
return eq;
|
||||
}).toList();
|
||||
|
||||
// Vérifier si tous les équipements sont retournés
|
||||
final allReturned = updatedEquipment.every((eq) => eq.isReturned);
|
||||
|
||||
final updateData = <String, dynamic>{
|
||||
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||
};
|
||||
|
||||
// Mettre à jour le statut selon la complétion
|
||||
if (allReturned) {
|
||||
updateData['returnStatus'] = returnStatusToString(ReturnStatus.completed);
|
||||
} else {
|
||||
updateData['returnStatus'] = returnStatusToString(ReturnStatus.inProgress);
|
||||
}
|
||||
|
||||
await _eventsCollection.doc(eventId).update(updateData);
|
||||
|
||||
// Mettre à jour le stock si c'est un consommable
|
||||
if (returnedQuantity != null) {
|
||||
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
|
||||
if (equipmentDoc.exists) {
|
||||
final equipment = EquipmentModel.fromMap(
|
||||
equipmentDoc.data() as Map<String, dynamic>,
|
||||
equipmentDoc.id,
|
||||
);
|
||||
|
||||
if (equipment.hasQuantity) {
|
||||
final currentAvailable = equipment.availableQuantity ?? 0;
|
||||
await _equipmentCollection.doc(equipmentId).update({
|
||||
'availableQuantity': currentAvailable + returnedQuantity,
|
||||
await _apiService.call('validateEquipmentReturn', {
|
||||
'eventId': eventId,
|
||||
'equipmentId': equipmentId,
|
||||
if (returnedQuantity != null) 'returnedQuantity': returnedQuantity,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error validating equipment return: $e');
|
||||
rethrow;
|
||||
@@ -189,53 +101,11 @@ class EventPreparationService {
|
||||
Map<String, int>? returnedQuantities,
|
||||
]) async {
|
||||
try {
|
||||
final event = await _getEvent(eventId);
|
||||
if (event == null) {
|
||||
throw Exception('Event not found');
|
||||
}
|
||||
|
||||
// Marquer tous les équipements comme retournés
|
||||
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||
final returnedQty = returnedQuantities?[eq.equipmentId] ??
|
||||
eq.returnedQuantity ??
|
||||
eq.quantity;
|
||||
return eq.copyWith(
|
||||
isReturned: true,
|
||||
returnedQuantity: returnedQty,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
await _eventsCollection.doc(eventId).update({
|
||||
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||
'returnStatus': returnStatusToString(ReturnStatus.completed),
|
||||
await _apiService.call('validateAllReturn', {
|
||||
'eventId': eventId,
|
||||
if (returnedQuantities != null) 'returnedQuantities': returnedQuantities,
|
||||
});
|
||||
|
||||
// Mettre à jour le statut des équipements à "available" et gérer les stocks
|
||||
for (var equipment in updatedEquipment) {
|
||||
// Vérifier si le document existe
|
||||
final equipmentDoc = await _equipmentCollection.doc(equipment.equipmentId).get();
|
||||
if (equipmentDoc.exists) {
|
||||
final equipmentData = EquipmentModel.fromMap(
|
||||
equipmentDoc.data() as Map<String, dynamic>,
|
||||
equipmentDoc.id,
|
||||
);
|
||||
|
||||
// Mettre à jour le statut uniquement pour les équipements non quantifiables
|
||||
if (!equipmentData.hasQuantity) {
|
||||
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.available);
|
||||
}
|
||||
|
||||
// Restaurer le stock pour les consommables
|
||||
if (equipmentData.hasQuantity && equipment.returnedQuantity != null) {
|
||||
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
||||
await _equipmentCollection.doc(equipment.equipmentId).update({
|
||||
'availableQuantity': currentAvailable + equipment.returnedQuantity!,
|
||||
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Invalider le cache des statuts d'équipement
|
||||
EquipmentStatusCalculator.invalidateGlobalCache();
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,45 +1,21 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:em2rp/models/equipment_model.dart';
|
||||
import 'package:em2rp/services/equipment_status_calculator.dart';
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
|
||||
/// Service étendu pour gérer les 4 étapes : Préparation, Chargement, Déchargement, Retour
|
||||
class EventPreparationServiceExtended {
|
||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
final ApiService _apiService = apiService;
|
||||
|
||||
CollectionReference get _eventsCollection => _firestore.collection('events');
|
||||
CollectionReference get _equipmentCollection => _firestore.collection('equipments');
|
||||
|
||||
// === CHARGEMENT (LOADING) ===
|
||||
|
||||
/// Valider un équipement individuel pour le chargement
|
||||
Future<void> validateEquipmentLoading(String eventId, String equipmentId) async {
|
||||
try {
|
||||
final event = await _getEvent(eventId);
|
||||
if (event == null) throw Exception('Event not found');
|
||||
|
||||
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||
if (eq.equipmentId == equipmentId) {
|
||||
return eq.copyWith(isLoaded: true);
|
||||
}
|
||||
return eq;
|
||||
}).toList();
|
||||
|
||||
// Vérifier si tous les équipements sont chargés
|
||||
final allLoaded = updatedEquipment.every((eq) => eq.isLoaded);
|
||||
|
||||
final updateData = <String, dynamic>{
|
||||
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||
};
|
||||
|
||||
// Si tous sont chargés, mettre à jour le statut
|
||||
if (allLoaded) {
|
||||
updateData['loadingStatus'] = loadingStatusToString(LoadingStatus.completed);
|
||||
} else {
|
||||
updateData['loadingStatus'] = loadingStatusToString(LoadingStatus.inProgress);
|
||||
}
|
||||
|
||||
await _eventsCollection.doc(eventId).update(updateData);
|
||||
await _apiService.call('validateEquipmentLoading', {
|
||||
'eventId': eventId,
|
||||
'equipmentId': equipmentId,
|
||||
});
|
||||
} catch (e) {
|
||||
print('Error validating equipment loading: $e');
|
||||
rethrow;
|
||||
@@ -49,16 +25,8 @@ class EventPreparationServiceExtended {
|
||||
/// Valider tous les équipements pour le chargement
|
||||
Future<void> validateAllLoading(String eventId) async {
|
||||
try {
|
||||
final event = await _getEvent(eventId);
|
||||
if (event == null) throw Exception('Event not found');
|
||||
|
||||
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||
return eq.copyWith(isLoaded: true);
|
||||
}).toList();
|
||||
|
||||
await _eventsCollection.doc(eventId).update({
|
||||
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||
'loadingStatus': loadingStatusToString(LoadingStatus.completed),
|
||||
await _apiService.call('validateAllLoading', {
|
||||
'eventId': eventId,
|
||||
});
|
||||
|
||||
// Invalider le cache des statuts d'équipement
|
||||
@@ -74,31 +42,10 @@ class EventPreparationServiceExtended {
|
||||
/// Valider un équipement individuel pour le déchargement
|
||||
Future<void> validateEquipmentUnloading(String eventId, String equipmentId) async {
|
||||
try {
|
||||
final event = await _getEvent(eventId);
|
||||
if (event == null) throw Exception('Event not found');
|
||||
|
||||
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||
if (eq.equipmentId == equipmentId) {
|
||||
return eq.copyWith(isUnloaded: true);
|
||||
}
|
||||
return eq;
|
||||
}).toList();
|
||||
|
||||
// Vérifier si tous les équipements sont déchargés
|
||||
final allUnloaded = updatedEquipment.every((eq) => eq.isUnloaded);
|
||||
|
||||
final updateData = <String, dynamic>{
|
||||
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||
};
|
||||
|
||||
// Si tous sont déchargés, mettre à jour le statut
|
||||
if (allUnloaded) {
|
||||
updateData['unloadingStatus'] = unloadingStatusToString(UnloadingStatus.completed);
|
||||
} else {
|
||||
updateData['unloadingStatus'] = unloadingStatusToString(UnloadingStatus.inProgress);
|
||||
}
|
||||
|
||||
await _eventsCollection.doc(eventId).update(updateData);
|
||||
await _apiService.call('validateEquipmentUnloading', {
|
||||
'eventId': eventId,
|
||||
'equipmentId': equipmentId,
|
||||
});
|
||||
} catch (e) {
|
||||
print('Error validating equipment unloading: $e');
|
||||
rethrow;
|
||||
@@ -108,16 +55,8 @@ class EventPreparationServiceExtended {
|
||||
/// Valider tous les équipements pour le déchargement
|
||||
Future<void> validateAllUnloading(String eventId) async {
|
||||
try {
|
||||
final event = await _getEvent(eventId);
|
||||
if (event == null) throw Exception('Event not found');
|
||||
|
||||
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||
return eq.copyWith(isUnloaded: true);
|
||||
}).toList();
|
||||
|
||||
await _eventsCollection.doc(eventId).update({
|
||||
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||
'unloadingStatus': unloadingStatusToString(UnloadingStatus.completed),
|
||||
await _apiService.call('validateAllUnloading', {
|
||||
'eventId': eventId,
|
||||
});
|
||||
|
||||
// Invalider le cache des statuts d'équipement
|
||||
@@ -133,26 +72,13 @@ class EventPreparationServiceExtended {
|
||||
/// Valider préparation ET chargement en même temps
|
||||
Future<void> validateAllPreparationAndLoading(String eventId) async {
|
||||
try {
|
||||
final event = await _getEvent(eventId);
|
||||
if (event == null) throw Exception('Event not found');
|
||||
// Note: On pourrait créer une fonction cloud dédiée pour ça,
|
||||
// mais pour l'instant on appelle les deux séquentiellement
|
||||
await _apiService.call('validateAllPreparation', {'eventId': eventId});
|
||||
await _apiService.call('validateAllLoading', {'eventId': eventId});
|
||||
|
||||
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||
return eq.copyWith(isPrepared: true, isLoaded: true);
|
||||
}).toList();
|
||||
|
||||
await _eventsCollection.doc(eventId).update({
|
||||
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||
'preparationStatus': preparationStatusToString(PreparationStatus.completed),
|
||||
'loadingStatus': loadingStatusToString(LoadingStatus.completed),
|
||||
});
|
||||
|
||||
// Mettre à jour le statut des équipements
|
||||
for (var equipment in event.assignedEquipment) {
|
||||
final doc = await _equipmentCollection.doc(equipment.equipmentId).get();
|
||||
if (doc.exists) {
|
||||
await _updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.inUse);
|
||||
}
|
||||
}
|
||||
// Invalider le cache
|
||||
EquipmentStatusCalculator.invalidateGlobalCache();
|
||||
} catch (e) {
|
||||
print('Error validating all preparation and loading: $e');
|
||||
rethrow;
|
||||
@@ -167,81 +93,20 @@ class EventPreparationServiceExtended {
|
||||
Map<String, int>? returnedQuantities,
|
||||
) async {
|
||||
try {
|
||||
final event = await _getEvent(eventId);
|
||||
if (event == null) throw Exception('Event not found');
|
||||
|
||||
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||
final returnedQty = returnedQuantities?[eq.equipmentId] ??
|
||||
eq.returnedQuantity ??
|
||||
eq.quantity;
|
||||
return eq.copyWith(
|
||||
isUnloaded: true,
|
||||
isReturned: true,
|
||||
returnedQuantity: returnedQty,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
await _eventsCollection.doc(eventId).update({
|
||||
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||
'unloadingStatus': unloadingStatusToString(UnloadingStatus.completed),
|
||||
'returnStatus': returnStatusToString(ReturnStatus.completed),
|
||||
// Note: On pourrait créer une fonction cloud dédiée pour ça,
|
||||
// mais pour l'instant on appelle les deux séquentiellement
|
||||
await _apiService.call('validateAllUnloading', {'eventId': eventId});
|
||||
await _apiService.call('validateAllReturn', {
|
||||
'eventId': eventId,
|
||||
if (returnedQuantities != null) 'returnedQuantities': returnedQuantities,
|
||||
});
|
||||
|
||||
// Mettre à jour les statuts et stocks
|
||||
for (var equipment in updatedEquipment) {
|
||||
final equipmentDoc = await _equipmentCollection.doc(equipment.equipmentId).get();
|
||||
if (equipmentDoc.exists) {
|
||||
final equipmentData = EquipmentModel.fromMap(
|
||||
equipmentDoc.data() as Map<String, dynamic>,
|
||||
equipmentDoc.id,
|
||||
);
|
||||
|
||||
if (!equipmentData.hasQuantity) {
|
||||
await _updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.available);
|
||||
}
|
||||
|
||||
if (equipmentData.hasQuantity && equipment.returnedQuantity != null) {
|
||||
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
||||
await _equipmentCollection.doc(equipment.equipmentId).update({
|
||||
'availableQuantity': currentAvailable + equipment.returnedQuantity!,
|
||||
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Invalider le cache
|
||||
EquipmentStatusCalculator.invalidateGlobalCache();
|
||||
} catch (e) {
|
||||
print('Error validating all unloading and return: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// === HELPERS ===
|
||||
|
||||
Future<void> _updateEquipmentStatus(String equipmentId, EquipmentStatus status) async {
|
||||
try {
|
||||
final doc = await _equipmentCollection.doc(equipmentId).get();
|
||||
if (!doc.exists) return;
|
||||
|
||||
await _equipmentCollection.doc(equipmentId).update({
|
||||
'status': equipmentStatusToString(status),
|
||||
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
||||
});
|
||||
} catch (e) {
|
||||
print('Error updating equipment status: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<EventModel?> _getEvent(String eventId) async {
|
||||
try {
|
||||
final doc = await _eventsCollection.doc(eventId).get();
|
||||
if (doc.exists) {
|
||||
return EventModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
print('Error getting event: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:em2rp/models/maintenance_model.dart';
|
||||
import 'package:em2rp/models/alert_model.dart';
|
||||
import 'package:em2rp/models/equipment_model.dart';
|
||||
import 'package:em2rp/services/equipment_service.dart';
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
|
||||
class MaintenanceService {
|
||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
final EquipmentService _equipmentService = EquipmentService();
|
||||
final ApiService _apiService = apiService;
|
||||
|
||||
// Collection references
|
||||
@@ -23,16 +20,7 @@ class MaintenanceService {
|
||||
Future<void> createMaintenance(MaintenanceModel maintenance) async {
|
||||
try {
|
||||
await _apiService.call('createMaintenance', maintenance.toMap());
|
||||
|
||||
// Mettre à jour les équipements concernés (côté client pour l'instant)
|
||||
for (String equipmentId in maintenance.equipmentIds) {
|
||||
await _updateEquipmentMaintenanceList(equipmentId, maintenance.id);
|
||||
|
||||
// Si la maintenance est planifiée dans les 7 prochains jours, créer une alerte
|
||||
if (maintenance.scheduledDate.isBefore(DateTime.now().add(const Duration(days: 7)))) {
|
||||
await _createMaintenanceAlert(equipmentId, maintenance);
|
||||
}
|
||||
}
|
||||
// Note: La Cloud Function gère maintenant la mise à jour des équipements et la création des alertes
|
||||
} catch (e) {
|
||||
print('Error creating maintenance: $e');
|
||||
rethrow;
|
||||
@@ -55,21 +43,10 @@ class MaintenanceService {
|
||||
/// Supprimer une maintenance
|
||||
Future<void> deleteMaintenance(String id) async {
|
||||
try {
|
||||
// Récupérer la maintenance pour connaître les équipements
|
||||
final doc = await _maintenancesCollection.doc(id).get();
|
||||
if (doc.exists) {
|
||||
final maintenance = MaintenanceModel.fromMap(
|
||||
doc.data() as Map<String, dynamic>,
|
||||
doc.id,
|
||||
);
|
||||
|
||||
// Retirer la maintenance des équipements
|
||||
for (String equipmentId in maintenance.equipmentIds) {
|
||||
await _removeMaintenanceFromEquipment(equipmentId, id);
|
||||
}
|
||||
}
|
||||
|
||||
await _maintenancesCollection.doc(id).delete();
|
||||
await _apiService.call('deleteMaintenance', {
|
||||
'maintenanceId': id,
|
||||
});
|
||||
// Note: La Cloud Function gère la mise à jour des équipements
|
||||
} catch (e) {
|
||||
print('Error deleting maintenance: $e');
|
||||
rethrow;
|
||||
@@ -236,52 +213,4 @@ class MaintenanceService {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Mettre à jour la liste des maintenances d'un équipement
|
||||
Future<void> _updateEquipmentMaintenanceList(String equipmentId, String maintenanceId) async {
|
||||
try {
|
||||
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
|
||||
if (equipmentDoc.exists) {
|
||||
final equipment = EquipmentModel.fromMap(
|
||||
equipmentDoc.data() as Map<String, dynamic>,
|
||||
equipmentDoc.id,
|
||||
);
|
||||
|
||||
final updatedMaintenanceIds = List<String>.from(equipment.maintenanceIds);
|
||||
if (!updatedMaintenanceIds.contains(maintenanceId)) {
|
||||
updatedMaintenanceIds.add(maintenanceId);
|
||||
|
||||
await _equipmentCollection.doc(equipmentId).update({
|
||||
'maintenanceIds': updatedMaintenanceIds,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error updating equipment maintenance list: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Retirer une maintenance de la liste d'un équipement
|
||||
Future<void> _removeMaintenanceFromEquipment(String equipmentId, String maintenanceId) async {
|
||||
try {
|
||||
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
|
||||
if (equipmentDoc.exists) {
|
||||
final equipment = EquipmentModel.fromMap(
|
||||
equipmentDoc.data() as Map<String, dynamic>,
|
||||
equipmentDoc.id,
|
||||
);
|
||||
|
||||
final updatedMaintenanceIds = List<String>.from(equipment.maintenanceIds);
|
||||
updatedMaintenanceIds.remove(maintenanceId);
|
||||
|
||||
await _equipmentCollection.doc(equipmentId).update({
|
||||
'maintenanceIds': updatedMaintenanceIds,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error removing maintenance from equipment: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +32,11 @@ class UserService {
|
||||
/// @deprecated Utilisez API deleteUser à la place
|
||||
Future<void> deleteUser(String uid) async {
|
||||
try {
|
||||
// TODO: Créer une Cloud Function deleteUser
|
||||
print("Suppression d'utilisateur non implémentée via API");
|
||||
final apiService = FirebaseFunctionsApiService();
|
||||
await apiService.call('deleteUser', {'userId': uid});
|
||||
} catch (e) {
|
||||
print("Erreur suppression: $e");
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,8 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
||||
onEdit: () => showDialog(
|
||||
context: context,
|
||||
builder: (_) => EditUserDialog(user: user)),
|
||||
onDelete: () => usersProvider.deleteUser(user.uid),
|
||||
onResetPassword: () => _resetPassword(context, user),
|
||||
onDelete: () => _confirmDeleteUser(context, usersProvider, user),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -259,19 +260,27 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final newUser = UserModel(
|
||||
uid: '', // Sera généré par Firebase
|
||||
firstName: firstNameController.text,
|
||||
lastName: lastNameController.text,
|
||||
email: emailController.text,
|
||||
phoneNumber: phoneController.text,
|
||||
role: selectedRoleId!,
|
||||
profilePhotoUrl: '',
|
||||
);
|
||||
await Provider.of<UsersProvider>(context,
|
||||
listen: false)
|
||||
.createUserWithEmailInvite(email: newUser.email, firstName: newUser.firstName, lastName: newUser.lastName, phoneNumber: newUser.phoneNumber, roleId: newUser.role);
|
||||
.createUserWithEmailInvite(
|
||||
email: emailController.text,
|
||||
firstName: firstNameController.text,
|
||||
lastName: lastNameController.text,
|
||||
phoneNumber: phoneController.text,
|
||||
roleId: selectedRoleId!,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Utilisateur créé avec succès. Email de réinitialisation envoyé à ${emailController.text}',
|
||||
),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -307,4 +316,174 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Réinitialise le mot de passe d'un utilisateur
|
||||
Future<void> _resetPassword(BuildContext context, UserModel user) async {
|
||||
try {
|
||||
await Provider.of<UsersProvider>(context, listen: false)
|
||||
.resetPassword(user.email);
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Email de réinitialisation envoyé à ${user.email}',
|
||||
),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Erreur lors de l\'envoi: ${e.toString()}',
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Affiche une confirmation avant de supprimer un utilisateur
|
||||
Future<void> _confirmDeleteUser(
|
||||
BuildContext context,
|
||||
UsersProvider usersProvider,
|
||||
UserModel user,
|
||||
) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
const Icon(Icons.warning, color: Colors.orange),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Confirmer la suppression',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Êtes-vous sûr de vouloir supprimer cet utilisateur ?',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.person, size: 20, color: AppColors.noir),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${user.firstName} ${user.lastName}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.email, size: 20, color: AppColors.gris),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
user.email,
|
||||
style: TextStyle(color: Colors.grey[700]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Cette action est irréversible. L\'utilisateur sera supprimé et désattribué de tous les événements liés',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.red[700],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
child: const Text(
|
||||
'Annuler',
|
||||
style: TextStyle(color: AppColors.gris),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Supprimer',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true && context.mounted) {
|
||||
try {
|
||||
await usersProvider.deleteUser(user.uid);
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Utilisateur ${user.firstName} ${user.lastName} supprimé avec succès',
|
||||
),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Erreur lors de la suppression: ${e.toString()}',
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ class UserCard extends StatefulWidget {
|
||||
final UserModel user;
|
||||
final VoidCallback onEdit;
|
||||
final VoidCallback onDelete;
|
||||
final VoidCallback onResetPassword;
|
||||
|
||||
static const double _desktopMaxWidth = 280;
|
||||
|
||||
@@ -14,6 +15,7 @@ class UserCard extends StatefulWidget {
|
||||
required this.user,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
required this.onResetPassword,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -128,6 +130,17 @@ class _UserCardState extends State<UserCard> {
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.lock_reset, size: 20),
|
||||
onPressed: widget.onResetPassword,
|
||||
color: AppColors.noir,
|
||||
padding: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
tooltip: 'Réinitialiser le mot de passe',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, size: 20),
|
||||
onPressed: widget.onEdit,
|
||||
@@ -205,6 +218,14 @@ class _UserCardState extends State<UserCard> {
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildButton(
|
||||
icon: Icons.lock_reset,
|
||||
label: "Réinit. MDP",
|
||||
onPressed: widget.onResetPassword,
|
||||
color: AppColors.noir,
|
||||
isNarrow: true,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
_buildButton(
|
||||
icon: Icons.edit,
|
||||
label: "Modifier",
|
||||
@@ -225,21 +246,35 @@ class _UserCardState extends State<UserCard> {
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildButton(
|
||||
Expanded(
|
||||
child: _buildButton(
|
||||
icon: Icons.lock_reset,
|
||||
label: "Réinit.",
|
||||
onPressed: widget.onResetPassword,
|
||||
color: AppColors.noir,
|
||||
isNarrow: false,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: _buildButton(
|
||||
icon: Icons.edit,
|
||||
label: "Modifier",
|
||||
label: "Edit",
|
||||
onPressed: widget.onEdit,
|
||||
color: AppColors.rouge,
|
||||
isNarrow: false,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildButton(
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: _buildButton(
|
||||
icon: Icons.delete,
|
||||
label: "Supprimer",
|
||||
label: "Suppr",
|
||||
onPressed: widget.onDelete,
|
||||
color: AppColors.gris,
|
||||
isNarrow: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user