diff --git a/bun.lock b/bun.lock index 048b6f7..b99a740 100644 --- a/bun.lock +++ b/bun.lock @@ -7,19 +7,23 @@ "@fastify/cors": "^11.0.1", "bcryptjs": "^3.0.2", "cors": "^2.8.5", + "date-fns": "^4.1.0", "dotenv": "^17.2.0", "fastify": "^5.4.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.16.4", "socket.io": "^4.8.1", "uuid": "^11.1.0", + "validator": "^13.15.15", }, "devDependencies": { + "@eslint/js": "^9.33.0", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^11.0.3", "commitizen": "^4.3.1", "cz-conventional-changelog": "^3.3.0", + "globals": "^16.3.0", "nodemon": "^3.1.10", "semantic-release": "^24.2.7", }, @@ -42,6 +46,8 @@ "@commitlint/types": ["@commitlint/types@19.8.1", "", { "dependencies": { "@types/conventional-commits-parser": "^5.0.0", "chalk": "^5.3.0" } }, "sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw=="], + "@eslint/js": ["@eslint/js@9.35.0", "", {}, "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw=="], + "@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.2", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ=="], "@fastify/cors": ["@fastify/cors@11.0.1", "", { "dependencies": { "fastify-plugin": "^5.0.0", "toad-cache": "^3.7.0" } }, "sha512-dmZaE7M1f4SM8ZZuk5RhSsDJ+ezTgI7v3HHRj8Ow9CneczsPLZV6+2j2uwdaSLn8zhTv6QV0F4ZRcqdalGx1pQ=="], @@ -254,6 +260,8 @@ "cz-conventional-changelog": ["cz-conventional-changelog@3.3.0", "", { "dependencies": { "chalk": "^2.4.1", "commitizen": "^4.0.3", "conventional-commit-types": "^3.0.0", "lodash.map": "^4.5.1", "longest": "^2.0.1", "word-wrap": "^1.0.3" }, "optionalDependencies": { "@commitlint/load": ">6.1.1" } }, "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw=="], + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], "dedent": ["dedent@0.7.0", "", {}, "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA=="], @@ -370,6 +378,8 @@ "global-prefix": ["global-prefix@1.0.2", "", { "dependencies": { "expand-tilde": "^2.0.2", "homedir-polyfill": "^1.0.1", "ini": "^1.3.4", "is-windows": "^1.0.1", "which": "^1.2.14" } }, "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg=="], + "globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="], + "globby": ["globby@14.1.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^2.1.0", "fast-glob": "^3.3.3", "ignore": "^7.0.3", "path-type": "^6.0.0", "slash": "^5.1.0", "unicorn-magic": "^0.3.0" } }, "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], @@ -842,6 +852,8 @@ "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], + "validator": ["validator@13.15.15", "", {}, "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], diff --git a/frontend/src/context/RoomProvider.tsx b/frontend/src/context/RoomProvider.tsx index 34afd43..09aa520 100644 --- a/frontend/src/context/RoomProvider.tsx +++ b/frontend/src/context/RoomProvider.tsx @@ -35,9 +35,14 @@ export const RoomProvider = ({ children }: RoomProviderProps) => { const { adminRooms = [], memberRooms = [] } = res.data; setRoom([...adminRooms, ...memberRooms]); - } catch (error: any) { - // Error handling - setErrors(error) + } catch (error) { + const apiError = error as ApiError; + const errorMessage = + apiError.response?.data?.error || + apiError.response?.data?.message || + apiError.message || + "Erreur lors de la recuperation des membres"; + setErrors(`Error: ${errorMessage}`); } finally { setLoading(false); } @@ -97,11 +102,7 @@ export const RoomProvider = ({ children }: RoomProviderProps) => { } if (response.data.room) { - setRoom((prevRooms) => - prevRooms.map((room) => - room._id === roomId ? response.data.room : room - ) - ); + setErrors(null); return response.data; } diff --git a/frontend/src/pages/ProfilePage.tsx b/frontend/src/pages/ProfilePage.tsx index b8e61e1..acd2578 100644 --- a/frontend/src/pages/ProfilePage.tsx +++ b/frontend/src/pages/ProfilePage.tsx @@ -1,36 +1,27 @@ import { useAuth } from "../hook" -import Profile from "../ui/profile/Profile" -import { Skeleton } from "../components/ui/skeleton" -import { useToast } from "../components/ui/use-toast" +import Profiles from "../ui/profile/Profile" +import Loader from "../components/Loader" +import Errors from "../components/Error" const ProfilePage = () => { const { currentUser, loading, error } = useAuth() - const { toast } = useToast() if (loading && !currentUser) { return (
Veuillez vous connecter pour accéder à votre profil
diff --git a/frontend/src/ui/profile/Profile.tsx b/frontend/src/ui/profile/Profile.tsx index 6f3173c..5fd6d41 100644 --- a/frontend/src/ui/profile/Profile.tsx +++ b/frontend/src/ui/profile/Profile.tsx @@ -7,7 +7,7 @@ interface ProfileProps { currentUser: User | null | undefined } -const Profile = ({ currentUser }: ProfileProps) => { +const Profiles = ({ currentUser }: ProfileProps) => { const { updatePreferences, updateProfile } = useAuth() // useState pour gérer l'affichage des onglets de profil et de préférence @@ -271,4 +271,4 @@ const [loading, setLoading] = useState(false); ) } -export default Profile +export default Profiles diff --git a/src/controllers/room.controller.js b/src/controllers/room.controller.js index 67fbbfd..022e375 100644 --- a/src/controllers/room.controller.js +++ b/src/controllers/room.controller.js @@ -4,6 +4,7 @@ import Comment from "../models/comment.model.js"; import { handleError } from "../helpers/handleError.js"; import { ERROR_MESSAGES } from "../constants/roomErrorMessage.js"; import Room from "../models/room.model.js"; +import mongoose from "mongoose"; export class RoomController { async getRoom(request, reply) { @@ -624,10 +625,10 @@ export class RoomController { const { roomId } = request.params; const currentUserId = request.user.userId; const session = await mongoose.startSession(); - + try { session.startTransaction(); - + // Vérifier que la salle existe const room = await Room.findById(roomId).session(session); if (!room) { diff --git a/src/controllers/task.controller.js b/src/controllers/task.controller.js index 4096c38..84b9025 100644 --- a/src/controllers/task.controller.js +++ b/src/controllers/task.controller.js @@ -1,481 +1,5 @@ -import Room from "../models/room.model.js"; -import mongoose from "mongoose"; -import Task from "../models/tasks.model.js"; -import { v4 as uuidv4 } from "uuid"; -import { isValidObjectId } from "../helpers/validateId.js"; -import User from "../models/user.model.js"; - export class TaskController { - emitTaskNotification = (events, data, userId = null) => { - try { - const notification = { - ...data, - timestamp: new Date(), - id: uuidv4(), - }; - - if (userId) { - // Si un utilisateur est ciblé, on envoie la notification à son socket - fastify.io.to(`user_${userId}`).emit(events, notification); - } else { - // Sinon, on envoie la notification à tous les utilisateurs connectés - fastify.io.emit(events, notification); - } - } catch (error) { - console.error("Erreur pour émettre une notification", error); - } - }; - /** * Get all Tasks for the authenticated user */ - async getTask(request) { - try { - const tasks = await Task.find({ author: request.user.userId }) - .populate("author", "userName email") - .sort({ createdAt: -1 }); - - return { - success: true, - count: tasks.length, - tasks, - }; - } catch (error) { - console.error("Erreur pour récupérer les tâches", error); - return { - success: false, - error: "Impossible de récupérer les tâches", - }; - } - } - - async addTask(request, reply) { - try { - const { - title, - description, - dueDate, - estimatedHours, - assignees = [], - label, - roomId, - priority = "low", - status = "pending", - } = request.body; - - const userId = request.user.userId; - - // Valider que les utilisateurs assignés existent - if (assignees && assignees.length > 0) { - const existingUsers = await User.find({ _id: { $in: assignees } }); - if (existingUsers.length !== assignees.length) { - return reply.code(400).send({ - success: false, - error: "Un ou plusieurs utilisateurs assignés n'existent pas", - }); - } - } - - // Vérifier que la salle existe et que l'utilisateur y a accès - let room = null; - if (roomId) { - room = await Room.findOne({ - _id: roomId, - $or: [ - { admin: request.user.userId }, - { members: request.user.userId }, - ], - }); - - if (!room) { - return reply.code(403).send({ - success: false, - error: "Salle non trouvée ou accès non autorisé", - }); - } - } - - if (!title || typeof title !== "string" || title.trim() === "") { - return reply.code(400).send({ - success: false, - error: "Le titre de la tâche est obligatoire", - }); - } - - if ( - !description || - typeof description !== "string" || - description.trim() === "" - ) { - return reply.code(400).send({ - success: false, - error: "La description est obligatoire", - }); - } - - // Convertir la date si elle est fournie comme chaîne - let dueDateObj; - if (dueDate) { - dueDateObj = new Date(dueDate); - if (isNaN(dueDateObj.getTime())) { - return reply.code(400).send({ - success: false, - error: "Format de date d'échéance invalide", - }); - } - room; - - // Vérifier que la date est dans le futur - if (dueDateObj <= new Date()) { - return reply.code(400).send({ - success: false, - error: "La date d'échéance doit être une date future", - }); - } - } - - if (!estimatedHours || typeof estimatedHours !== "number") { - return reply.code(400).send({ - success: false, - error: "Vous devez préciser l'heure d'estimation", - }); - } - - // Chercher les utilisateurs assignés existants (sans doublon) - const assigneesId = [...new Set(assignees)]; - // Chercher les utilisateurs assignés existants (sans doublon) - const existingAssignees = await User.find({ - $and: [{ _id: { $in: assigneesId } }, { _id: { $ne: userId } }], - }).select("_id"); - - // Créer la tâche avec les données validées - const taskData = { - title: title.trim(), - description: description ? description.trim() : "", - status, - priority, - dueDate: dueDateObj, - estimatedHours: parseFloat(estimatedHours) || 0, - assignees: [...existingAssignees], - label, - author: request.user.userId, - startDate: new Date(), - room: roomId, - }; - - const task = new Task(taskData); - - const session = await mongoose.startSession(); - session.startTransaction(); - - try { - const savedTask = await task.save({ session }); - - // Mettre à jour l'auteur avec la nouvelle tâche - await User.findByIdAndUpdate( - request.user.userId, - { - $addToSet: { myTasks: savedTask._id }, - $inc: { "stats.tasksCreated": 1 }, - }, - { session } - ); - - // Mettre à jour les utilisateurs assignés - await User.updateMany( - { _id: { $in: assignees } }, - { $addToSet: { collaborators: savedTask._id } }, - { session } - ); - - // Mettre à jour la salle avec la nouvelle tâche si nécessaire - if (roomId) { - await Room.findByIdAndUpdate( - roomId, - { $addToSet: { tasks: savedTask._id } }, - { session } - ); - } - - await session.commitTransaction(); - session.endSession(); - - // Notifications - emitTaskNotification("taskCreated", { - task: savedTask, - message: `Nouvelle tâche créée: ${savedTask.title}`, - type: "success", - authorId: request.user.userId, - }); - - // Notifier les utilisateurs assignés - assignees.forEach((userId) => { - if (userId.toString() !== request.user.userId.toString()) { - emitTaskNotification( - "taskAssigned", - { - task: savedTask, - message: `Vous avez été assigné à la tâche: ${savedTask.title}`, - type: "info", - }, - userId - ); - } - }); - - return savedTask; - } catch (error) { - await session.abortTransaction(); - session.endSession(); - throw error; // Laisser le bloc catch global gérer l'erreur - } - } catch (error) { - console.error("Erreur lors de la création de la tâche:", error); - reply.code(500).send({ - success: false, - error: `Une erreur est survenue lors de la création de la tâche: ${error.message}`, - details: - process.env.NODE_ENV === "development" ? error.message : undefined, - }); - } - } - - async updateTask(request, reply) { - try { - const { id } = request.params; - const userId = request.user.userId; - - // Validation de l'ID - if (!isValidObjectId(id)) { - return reply.code(400).send({ - success: false, - error: "ID de tâche invalide", - }); - } - - // Vérifier que la tâche existe et appartient à l'utilisateur - const existingTask = await Task.findOne({ _id: id, author: userId }); - if (!existingTask) { - return reply.code(404).send({ - success: false, - error: "Tâche non trouvée ou accès non autorisé", - }); - } - - // Valider les données de la requête - const { title, description, dueDate, estimatedHours } = request.body; - if (title && (typeof title !== "string" || title.trim() === "")) { - return reply.code(400).send({ - success: false, - error: "Le titre de la tâche est invalide", - }); - } - - if ( - description && - (typeof description !== "string" || description.trim() === "") - ) { - return reply.code(400).send({ - success: false, - error: "La description est invalide", - }); - } - - // Convertir la date si elle est fournie comme chaîne - let dueDateObj; - if (dueDate) { - dueDateObj = new Date(dueDate); - if (isNaN(dueDateObj.getTime())) { - return reply.code(400).send({ - success: false, - error: "Format de date d'échéance invalide", - }); - } - - // Vérifier que la date est dans le futur - if (dueDateObj <= new Date()) { - return reply.code(400).send({ - success: false, - error: "La date d'échéance doit être une date future", - }); - } - } - - if (estimatedHours && typeof estimatedHours !== "number") { - return reply.code(400).send({ - success: false, - error: "L'heure d'estimation doit etre un nombre", - }); - } - - // Mettre à jour la tâche - const updateData = { ...request.body }; - // Ne pas permettre la modification de l'auteur - if (updateData.author) delete updateData.author; - if (updateData.room) delete updateData.room; - - const updatedTask = await Task.findByIdAndUpdate(id, updateData, { - new: true, - runValidators: true, - }).populate("author", "userName email"); - - if (!updatedTask) { - return reply.code(500).send({ - success: false, - error: "Échec de la mise à jour de la tâche", - }); - } - - const statusDone = updateData.status === "done"; - if (statusDone) { - await User.findByIdAndUpdate( - userId, - { $inc: { "stats.tasksCompleted": 1 } }, - { new: true } - ); - } - - //Détecter les changements siginficative - const statusChange = existingTask.status !== updatedTask.status; - const priorityChange = existingTask.priority !== updatedTask.priority; - - //Notification en temps réel avec context - let notificationMessage = `Tache "${updatedTask.title}" mise à jour`; - - if (statusChange) { - notificationMessage = `Tache "${updatedTask.title}" - Status: ${updatedTask.status}`; - } else if (priorityChange) { - notificationMessage = `Tache "${updatedTask.title}" - Priority: ${updatedTask.priority}`; - } - - // Notification en temps réel avec context - // - previousTask pour stocker l'ancienne version de la tâche - // - change pour stocker les changements (status, priority) - emitTaskNotification("taskUpdated", { - task: updatedTask, - previousTask: existingTask, - message: notificationMessage, - type: statusChange ? "warning" : "info", - change: { - status: statusChange, - priority: priorityChange, - }, - }); - - //Notification spécifique à la tache (si d'autre utilisateur collabore) - fastify.io.to(`task_${id}`).emit("taskRoomUpdate", { - taskId: id, - task: updatedTask, - updatedBy: userId, - timestamp: new Date(), - }); - - return updatedTask; - } catch (error) { - reply.code(500).send({ - error: `Erreur lors de la mise à jour de la tâche ${error.message}`, - }); - } - } - - async getById(request, reply) { - try { - const { id } = request.params; - // Validation de l'ID - if (!isValidObjectId(id)) { - return reply.code(400).send({ - success: false, - error: "ID de tâche invalide", - }); - } - - const task = await Task.findOne({ - _id: id, - author: request.user.userId, - }).populate("author", "userName email"); - - if (!task) { - return reply - .code(404) - .send({ error: "Tâche non trouvée ou accès non autorisé" }); - } - - // Envoie une notification en temps réel pour signaler que la tâche a été vue - // par l'utilisateur connecté - emitTaskNotification( - "taskViewed", - { - taskId: id, - viewedBy: request.user.userId, - taskTitle: task.title, - }, - request.user.userId - ); - - return task; - } catch (error) { - reply - .code(500) - .send({ error: "Erreur lors de la récupération de la tâche" }); - } - } - - async delete(request, reply) { - try { - const { id } = request.params; - const userId = request.user.userId; - - // Validation de l'ID - if (!isValidObjectId(id)) { - return reply.code(400).send({ - success: false, - error: "ID de tâche invalide", - }); - } - - // Vérifier que la tâche existe et appartient à l'utilisateur - const task = await Task.findOne({ _id: id, author: userId }); - if (!task) { - return reply.code(404).send({ - success: false, - error: "Tâche non trouvée ou accès non autorisé", - }); - } - - // Supprimer la tâche - await Task.findByIdAndDelete(id); - - // Supprimer la référence de la tâche dans le tableau myTasks de l'utilisateur - await User.findByIdAndUpdate( - userId, - { $pull: { myTasks: id } }, - { new: true } - ); - - // Emettre une notification de suppression de tâche - emitTaskNotification("taskDeleted", { - taskId: id, - taskTitle: task.title, - message: `tache: ${task.title} supprimé`, - deletedBy: userId, - }); - - // Informer les utilisateurs de la tâche qu'elle a été supprimée - fastify.io.to(`task_${id}`).emit("taskRoomDeleted", { - taskId: id, - message: "Cette tache a été suprimé", - timestamp: new Date(), - }); - - return { - success: true, - message: "Tâche supprimée avec succès", - DeletedTaskId: id, - }; - } catch (error) { - reply.code(500).send({ - error: `Erreur lors de la suppression de la tâche ${error.message}`, - }); - } - } } diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js index ff2d92f..101bce9 100644 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.js @@ -5,14 +5,6 @@ export class UserController { async getUser(request, reply) { const users = await User.find({}) .populate("userName email stats") - .populate({ - path: "notification", - select: "message type", - populate: { - path: "user", - select: "userName email", - }, - }) .sort({ updatedAt: -1 }); return users; diff --git a/src/routes/task.route.js b/src/routes/task.route.js index c472fe5..a2b73e4 100644 --- a/src/routes/task.route.js +++ b/src/routes/task.route.js @@ -1,12 +1,53 @@ -import { TaskController } from "../controllers/task.controller.js"; - -const taskController = new TaskController(); +import mongoose from "mongoose"; +import Room from "../models/room.model.js"; +import Task from "../models/tasks.model.js"; +import { v4 as uuidv4 } from "uuid"; +import { isValidObjectId } from "../helpers/validateId.js"; +import User from "../models/user.model.js"; export const taskRoutes = async (fastify, options) => { + const emitTaskNotification = (events, data, userId = null) => { + try { + const notification = { + ...data, + timestamp: new Date(), + id: uuidv4(), + }; + + if (userId) { + // Si un utilisateur est ciblé, on envoie la notification à son socket + fastify.io.to(`user_${userId}`).emit(events, notification); + } else { + // Sinon, on envoie la notification à tous les utilisateurs connectés + fastify.io.emit(events, notification); + } + } catch (error) { + console.error("Erreur pour émettre une notification", error); + } + }; + fastify.get( "/get/tasks", { preHandler: fastify.authenticate }, - taskController.getTask + async (request) => { + try { + const tasks = await Task.find({ author: request.user.userId }) + .populate("author", "userName email") + .sort({ createdAt: -1 }); + + return { + success: true, + count: tasks.length, + tasks, + }; + } catch (error) { + console.error("Erreur pour récupérer les tâches", error); + return { + success: false, + error: "Impossible de récupérer les tâches", + }; + } + } ); /** @@ -27,126 +68,197 @@ export const taskRoutes = async (fastify, options) => { fastify.post( "/add/tasks", { preHandler: fastify.authenticate }, - taskController.addTask - ); + async (request, reply) => { + try { + const { + title, + description, + dueDate, + estimatedHours, + assignees = [], + label, + roomId, + priority = "low", + status = "pending", + } = request.body; + + const userId = request.user.userId; + + // Valider que les utilisateurs assignés existent + if (assignees && assignees.length > 0) { + const existingUsers = await User.find({ _id: { $in: assignees } }); + if (existingUsers.length !== assignees.length) { + return reply.code(400).send({ + success: false, + error: "Un ou plusieurs utilisateurs assignés n'existent pas", + }); + } + } + + // Vérifier que la salle existe et que l'utilisateur y a accès + let room = null; + if (roomId) { + room = await Room.findOne({ + _id: roomId, + $or: [ + { admin: request.user.userId }, + { members: request.user.userId }, + ], + }); + + if (!room) { + return reply.code(403).send({ + success: false, + error: "Salle non trouvée ou accès non autorisé", + }); + } + } + + if (!title || typeof title !== "string" || title.trim() === "") { + return reply.code(400).send({ + success: false, + error: "Le titre de la tâche est obligatoire", + }); + } + + if ( + !description || + typeof description !== "string" || + description.trim() === "" + ) { + return reply.code(400).send({ + success: false, + error: "La description est obligatoire", + }); + } + + // Convertir la date si elle est fournie comme chaîne + let dueDateObj; + if (dueDate) { + dueDateObj = new Date(dueDate); + if (isNaN(dueDateObj.getTime())) { + return reply.code(400).send({ + success: false, + error: "Format de date d'échéance invalide", + }); + } + room; + + // Vérifier que la date est dans le futur + if (dueDateObj <= new Date()) { + return reply.code(400).send({ + success: false, + error: "La date d'échéance doit être une date future", + }); + } + } + + if (!estimatedHours || typeof estimatedHours !== "number") { + return reply.code(400).send({ + success: false, + error: "Vous devez préciser l'heure d'estimation", + }); + } - //Add task to room - // fastify.post( - // "add/room/task/:roomId", - // { preHandler: fastify.authenticate }, - // async (request, reply) => { - // const { roomId } = request.params; - // const { - // title, - // description, - // dueDate, - // estimatedHours, - // // assignees, - // // roomId, - // priority = "low", - // status = "pending", - // } = request.body; - - // if (!title || typeof title !== "string" || title.trim() === "") { - // return reply.code(400).send({ - // success: false, - // error: "Le titre de la tâche est obligatoire", - // }); - // } - - // if ( - // !description || - // typeof description !== "string" || - // description.trim() === "" - // ) { - // return reply.code(400).send({ - // success: false, - // error: "La description est obligatoire", - // }); - // } - - // // Convertir la date si elle est fournie comme chaîne - // let dueDateObj; - // if (dueDate) { - // dueDateObj = new Date(dueDate); - // if (isNaN(dueDateObj.getTime())) { - // return reply.code(400).send({ - // success: false, - // error: "Format de date d'échéance invalide", - // }); - // } - - // // Vérifier que la date est dans le futur - // if (dueDateObj <= new Date()) { - // return reply.code(400).send({ - // success: false, - // error: "La date d'échéance doit être une date future", - // }); - // } - // } - - // if (!estimatedHours || typeof estimatedHours !== "number") { - // return reply.code(400).send({ - // success: false, - // error: "Vous devez préciser l'heure d'estimation", - // }); - // } - - // const roomExist = await Room.findById(roomId); - // if (!roomExist) { - // return reply.code(401).send({ - // success: false, - // message: "Room Invalid ou ID room inexistante", - // }); - // } - - // // Créer la tâche avec les données validées - // const taskData = { - // title: title.trim(), - // description: description ? description.trim() : "", - // status, - // priority, - // dueDate: dueDateObj, - // estimatedHours: parseFloat(estimatedHours) || 0, - // // assignees, - // author: request.user.userId, - // startDate: new Date(), - // // room: roomId, - // }; - - // const task = new Task(taskData); - - // const session = await mongoose.startSession(); - // session.startTransaction(); - - // try { - // const savedTask = await task.save({ session }); - - // // Mettre à jour l'auteur avec la nouvelle tâche - // await User.findByIdAndUpdate( - // request.user.userId, - // { - // $addToSet: { myTasks: savedTask._id }, - // $inc: { "stats.tasksCreated": 1 }, - // }, - // { session } - // ); - - // await Room.findByIdAndUpdate( - // roomId, - // { $addToSet: { tasks: savedTask._id } }, - // { session } - // ); - - // await session.commitTransaction(); - // session.endSession(); - // } catch (error) { - // await session.abortTransaction(); - // session.endSession(); - // throw error; // Laisser le bloc catch global gérer l'erreur - // } - // } - // ); + // Chercher les utilisateurs assignés existants (sans doublon) + const assigneesId = [...new Set(assignees)]; + // Chercher les utilisateurs assignés existants (sans doublon) + const existingAssignees = await User.find({ + $and: [{ _id: { $in: assigneesId } }, { _id: { $ne: userId } }], + }).select("_id"); + + // Créer la tâche avec les données validées + const taskData = { + title: title.trim(), + description: description ? description.trim() : "", + status, + priority, + dueDate: dueDateObj, + estimatedHours: parseFloat(estimatedHours) || 0, + assignees: [...existingAssignees], + label, + author: request.user.userId, + startDate: new Date(), + room: roomId, + }; + + const task = new Task(taskData); + + const session = await mongoose.startSession(); + session.startTransaction(); + + try { + const savedTask = await task.save({ session }); + + // Mettre à jour l'auteur avec la nouvelle tâche + await User.findByIdAndUpdate( + request.user.userId, + { + $addToSet: { myTasks: savedTask._id }, + $inc: { "stats.tasksCreated": 1 }, + }, + { session } + ); + + // Mettre à jour les utilisateurs assignés + await User.updateMany( + { _id: { $in: assignees } }, + { $addToSet: { collaborators: savedTask._id } }, + { session } + ); + + // Mettre à jour la salle avec la nouvelle tâche si nécessaire + if (roomId) { + await Room.findByIdAndUpdate( + roomId, + { $addToSet: { tasks: savedTask._id } }, + { session } + ); + } + + await session.commitTransaction(); + session.endSession(); + + // Notifications + emitTaskNotification("taskCreated", { + task: savedTask, + message: `Nouvelle tâche créée: ${savedTask.title}`, + type: "success", + authorId: request.user.userId, + }); + + // Notifier les utilisateurs assignés + assignees.forEach((userId) => { + if (userId.toString() !== request.user.userId.toString()) { + emitTaskNotification( + "taskAssigned", + { + task: savedTask, + message: `Vous avez été assigné à la tâche: ${savedTask.title}`, + type: "info", + }, + userId + ); + } + }); + + return savedTask; + } catch (error) { + await session.abortTransaction(); + session.endSession(); + throw error; // Laisser le bloc catch global gérer l'erreur + } + } catch (error) { + console.error("Erreur lors de la création de la tâche:", error); + reply.code(500).send({ + success: false, + error: `Une erreur est survenue lors de la création de la tâche: ${error.message}`, + details: + process.env.NODE_ENV === "development" ? error.message : undefined, + }); + } + } + ); /** * Update Task by Id @@ -154,7 +266,143 @@ export const taskRoutes = async (fastify, options) => { fastify.put( "/update/tasks/:id", { preHandler: fastify.authenticate }, - taskController.updateTask + async (request, reply) => { + try { + const { id } = request.params; + const userId = request.user.userId; + + // Validation de l'ID + if (!isValidObjectId(id)) { + return reply.code(400).send({ + success: false, + error: "ID de tâche invalide", + }); + } + + // Vérifier que la tâche existe et appartient à l'utilisateur + const existingTask = await Task.findOne({ _id: id, author: userId }); + if (!existingTask) { + return reply.code(404).send({ + success: false, + error: "Tâche non trouvée ou accès non autorisé", + }); + } + + // Valider les données de la requête + const { title, description, dueDate, estimatedHours } = request.body; + if (title && (typeof title !== "string" || title.trim() === "")) { + return reply.code(400).send({ + success: false, + error: "Le titre de la tâche est invalide", + }); + } + + if ( + description && + (typeof description !== "string" || description.trim() === "") + ) { + return reply.code(400).send({ + success: false, + error: "La description est invalide", + }); + } + + // Convertir la date si elle est fournie comme chaîne + let dueDateObj; + if (dueDate) { + dueDateObj = new Date(dueDate); + if (isNaN(dueDateObj.getTime())) { + return reply.code(400).send({ + success: false, + error: "Format de date d'échéance invalide", + }); + } + + // Vérifier que la date est dans le futur + if (dueDateObj <= new Date()) { + return reply.code(400).send({ + success: false, + error: "La date d'échéance doit être une date future", + }); + } + } + + if (estimatedHours && typeof estimatedHours !== "number") { + return reply.code(400).send({ + success: false, + error: "L'heure d'estimation doit etre un nombre", + }); + } + + // Mettre à jour la tâche + const updateData = { ...request.body }; + // Ne pas permettre la modification de l'auteur + if (updateData.author) delete updateData.author; + if (updateData.room) delete updateData.room; + + const updatedTask = await Task.findByIdAndUpdate(id, updateData, { + new: true, + runValidators: true, + }).populate("author", "userName email"); + + if (!updatedTask) { + return reply.code(500).send({ + success: false, + error: "Échec de la mise à jour de la tâche", + }); + } + + const statusDone = updateData.status === "done"; + if (statusDone) { + await User.findByIdAndUpdate( + userId, + { $inc: { "stats.tasksCompleted": 1 } }, + { new: true } + ); + } + + //Détecter les changements siginficative + const statusChange = existingTask.status !== updatedTask.status; + const priorityChange = existingTask.priority !== updatedTask.priority; + + //Notification en temps réel avec context + let notificationMessage = `Tache "${updatedTask.title}" mise à jour`; + + if (statusChange) { + notificationMessage = `Tache "${updatedTask.title}" - Status: ${updatedTask.status}`; + } else if (priorityChange) { + notificationMessage = `Tache "${updatedTask.title}" - Priority: ${updatedTask.priority}`; + } + + // Notification en temps réel avec context + // - previousTask pour stocker l'ancienne version de la tâche + // - change pour stocker les changements (status, priority) + emitTaskNotification("taskUpdated", { + task: updatedTask, + previousTask: existingTask, + message: notificationMessage, + type: statusChange ? "warning" : "info", + change: { + status: statusChange, + priority: priorityChange, + }, + }); + + //Notification spécifique à la tache (si d'autre utilisateur collabore) + fastify.io.to(`task_${id}`).emit("taskRoomUpdate", { + taskId: id, + task: updatedTask, + updatedBy: userId, + timestamp: new Date(), + }); + + return updatedTask; + } catch (error) { + reply.code(500).send({ + error: `Erreur lors de la mise à jour de la tâche ${error.message}`, + }); + } + } ); /** @@ -163,7 +411,47 @@ export const taskRoutes = async (fastify, options) => { fastify.get( "/get/task/:id", { preHandler: fastify.authenticate }, - taskController.getById + async (request, reply) => { + try { + const { id } = request.params; + // Validation de l'ID + if (!isValidObjectId(id)) { + return reply.code(400).send({ + success: false, + error: "ID de tâche invalide", + }); + } + + const task = await Task.findOne({ + _id: id, + author: request.user.userId, + }).populate("author", "userName email"); + + if (!task) { + return reply + .code(404) + .send({ error: "Tâche non trouvée ou accès non autorisé" }); + } + + // Envoie une notification en temps réel pour signaler que la tâche a été vue + // par l'utilisateur connecté + emitTaskNotification( + "taskViewed", + { + taskId: id, + viewedBy: request.user.userId, + taskTitle: task.title, + }, + request.user.userId + ); + + return task; + } catch (error) { + reply + .code(500) + .send({ error: "Erreur lors de la récupération de la tâche" }); + } + } ); /** @@ -172,6 +460,63 @@ export const taskRoutes = async (fastify, options) => { fastify.delete( "/delete/task/:id", { preHandler: fastify.authenticate }, - taskController.delete + async (request, reply) => { + try { + const { id } = request.params; + const userId = request.user.userId; + + // Validation de l'ID + if (!isValidObjectId(id)) { + return reply.code(400).send({ + success: false, + error: "ID de tâche invalide", + }); + } + + // Vérifier que la tâche existe et appartient à l'utilisateur + const task = await Task.findOne({ _id: id, author: userId }); + if (!task) { + return reply.code(404).send({ + success: false, + error: "Tâche non trouvée ou accès non autorisé", + }); + } + + // Supprimer la tâche + await Task.findByIdAndDelete(id); + + // Supprimer la référence de la tâche dans le tableau myTasks de l'utilisateur + await User.findByIdAndUpdate( + userId, + { $pull: { myTasks: id } }, + { new: true } + ); + + // Emettre une notification de suppression de tâche + emitTaskNotification("taskDeleted", { + taskId: id, + taskTitle: task.title, + message: `tache: ${task.title} supprimé`, + deletedBy: userId, + }); + + // Informer les utilisateurs de la tâche qu'elle a été supprimée + fastify.io.to(`task_${id}`).emit("taskRoomDeleted", { + taskId: id, + message: "Cette tache a été suprimé", + timestamp: new Date(), + }); + + return { + success: true, + message: "Tâche supprimée avec succès", + DeletedTaskId: id, + }; + } catch (error) { + reply.code(500).send({ + error: `Erreur lors de la suppression de la tâche ${error.message}`, + }); + } + } ); };