requireAuth(); $db = Database::getInstance(); // Récupérer tous les projets de l'utilisateur $stmt = $db->prepare(' SELECT p.*, GROUP_CONCAT(t.id) as tag_ids, GROUP_CONCAT(t.name) as tag_names FROM projects p LEFT JOIN project_tags pt ON p.id = pt.project_id LEFT JOIN tags t ON pt.tag_id = t.id WHERE p.user_id = :user_id GROUP BY p.id ORDER BY p.parent_id ASC, p.position ASC, p.name ASC '); $stmt->execute(['user_id' => $this->getUserId()]); $projects = $stmt->fetchAll(); // Construire l'arborescence $tree = $this->buildTree($projects); Response::success($tree); } /** * GET /projects/{id} */ public function show(): void { $this->requireAuth(); $id = (int) $this->request->getParam('id'); $project = $this->findOrFail($id); // Récupérer les tags $project['tags'] = $this->getProjectTags($id); // Récupérer les sous-projets $project['children'] = $this->getChildren($id); Response::success($project); } /** * POST /projects */ public function store(): void { $this->requireAuth(); $data = $this->validate([ 'name' => 'required|min:1|max:100', 'description' => 'max:65535', 'parent_id' => 'int', 'position' => 'int', ]); // Vérifier que le parent appartient à l'utilisateur if (!empty($data['parent_id'])) { $this->findOrFail((int) $data['parent_id']); } $db = Database::getInstance(); $stmt = $db->prepare(' INSERT INTO projects (user_id, parent_id, name, description, position) VALUES (:user_id, :parent_id, :name, :description, :position) '); $stmt->execute([ 'user_id' => $this->getUserId(), 'parent_id' => $data['parent_id'] ?: null, 'name' => $data['name'], 'description' => $data['description'] ?? null, 'position' => $data['position'] ?? 0, ]); $projectId = (int) $db->lastInsertId(); // Gérer les tags si fournis $tags = $this->request->get('tags'); if (is_array($tags)) { $this->syncTags($projectId, $tags); } $project = $this->findOrFail($projectId); $project['tags'] = $this->getProjectTags($projectId); Response::success($project, 'Projet créé', 201); } /** * PUT /projects/{id} */ public function update(): void { $this->requireAuth(); $id = (int) $this->request->getParam('id'); $this->findOrFail($id); $data = $this->validate([ 'name' => 'min:1|max:100', 'description' => 'max:65535', 'parent_id' => 'int', 'position' => 'int', ]); // Vérifier que le parent n'est pas le projet lui-même ou un de ses enfants if (!empty($data['parent_id'])) { $parentId = (int) $data['parent_id']; if ($parentId === $id) { Response::error('Un projet ne peut pas être son propre parent', 422); } $this->findOrFail($parentId); // Vérifier que le parent n'est pas un enfant du projet if ($this->isDescendant($parentId, $id)) { Response::error('Le parent ne peut pas être un sous-projet', 422); } } $db = Database::getInstance(); $fields = []; $params = ['id' => $id]; if (isset($data['name'])) { $fields[] = 'name = :name'; $params['name'] = $data['name']; } if (array_key_exists('description', $data)) { $fields[] = 'description = :description'; $params['description'] = $data['description']; } if (array_key_exists('parent_id', $data)) { $fields[] = 'parent_id = :parent_id'; $params['parent_id'] = $data['parent_id'] ?: null; } if (isset($data['position'])) { $fields[] = 'position = :position'; $params['position'] = $data['position']; } if (!empty($fields)) { $sql = 'UPDATE projects SET ' . implode(', ', $fields) . ' WHERE id = :id'; $stmt = $db->prepare($sql); $stmt->execute($params); } // Gérer les tags si fournis $tags = $this->request->get('tags'); if (is_array($tags)) { $this->syncTags($id, $tags); } $project = $this->findOrFail($id); $project['tags'] = $this->getProjectTags($id); Response::success($project, 'Projet mis à jour'); } /** * DELETE /projects/{id} */ public function destroy(): void { $this->requireAuth(); $id = (int) $this->request->getParam('id'); $this->findOrFail($id); $db = Database::getInstance(); // Les sous-projets et tâches seront supprimés en cascade (FK) $stmt = $db->prepare('DELETE FROM projects WHERE id = :id'); $stmt->execute(['id' => $id]); Response::success(null, 'Projet supprimé'); } /** * Trouver un projet ou retourner 404 */ private function findOrFail(int $id): array { $db = Database::getInstance(); $stmt = $db->prepare(' SELECT * FROM projects WHERE id = :id AND user_id = :user_id '); $stmt->execute([ 'id' => $id, 'user_id' => $this->getUserId(), ]); $project = $stmt->fetch(); if (!$project) { Response::notFound('Projet non trouvé'); } return $project; } /** * Construire l'arborescence des projets */ private function buildTree(array $projects, ?int $parentId = null): array { $tree = []; foreach ($projects as $project) { if ($project['parent_id'] == $parentId) { // Parser les tags $project['tags'] = []; if (!empty($project['tag_ids'])) { $ids = explode(',', $project['tag_ids']); $names = explode(',', $project['tag_names']); foreach ($ids as $i => $tagId) { $project['tags'][] = [ 'id' => (int) $tagId, 'name' => $names[$i] ?? '', ]; } } unset($project['tag_ids'], $project['tag_names']); // Récursion pour les enfants $project['children'] = $this->buildTree($projects, (int) $project['id']); $tree[] = $project; } } return $tree; } /** * Récupérer les tags d'un projet */ private function getProjectTags(int $projectId): array { $db = Database::getInstance(); $stmt = $db->prepare(' SELECT t.id, t.name, t.color FROM tags t JOIN project_tags pt ON t.id = pt.tag_id WHERE pt.project_id = :project_id '); $stmt->execute(['project_id' => $projectId]); return $stmt->fetchAll(); } /** * Récupérer les sous-projets directs */ private function getChildren(int $projectId): array { $db = Database::getInstance(); $stmt = $db->prepare(' SELECT * FROM projects WHERE parent_id = :parent_id AND user_id = :user_id ORDER BY position ASC, name ASC '); $stmt->execute([ 'parent_id' => $projectId, 'user_id' => $this->getUserId(), ]); return $stmt->fetchAll(); } /** * Vérifier si un projet est un descendant d'un autre */ private function isDescendant(int $projectId, int $ancestorId): bool { $db = Database::getInstance(); $stmt = $db->prepare(' SELECT parent_id FROM projects WHERE id = :id AND user_id = :user_id '); $stmt->execute([ 'id' => $projectId, 'user_id' => $this->getUserId(), ]); $project = $stmt->fetch(); if (!$project || $project['parent_id'] === null) { return false; } if ((int) $project['parent_id'] === $ancestorId) { return true; } return $this->isDescendant((int) $project['parent_id'], $ancestorId); } /** * Synchroniser les tags d'un projet */ private function syncTags(int $projectId, array $tagIds): void { $db = Database::getInstance(); // Supprimer les associations existantes $stmt = $db->prepare('DELETE FROM project_tags WHERE project_id = :project_id'); $stmt->execute(['project_id' => $projectId]); // Ajouter les nouvelles associations if (!empty($tagIds)) { $stmt = $db->prepare(' INSERT INTO project_tags (project_id, tag_id) SELECT :project_id, id FROM tags WHERE id = :tag_id AND user_id = :user_id '); foreach ($tagIds as $tagId) { $stmt->execute([ 'project_id' => $projectId, 'tag_id' => (int) $tagId, 'user_id' => $this->getUserId(), ]); } } } }