From a5e5a75561fc5b512b46cb56502769d1a589f29b Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Mon, 25 Aug 2025 08:38:56 +0200 Subject: [PATCH] refactor: use services in routes plz --- app/api/admin/skills/route.ts | 185 +++------- app/api/admin/teams/[teamId]/members/route.ts | 57 +--- app/api/admin/teams/route.ts | 200 +++-------- app/api/admin/users/[userId]/route.ts | 48 +-- app/api/admin/users/route.ts | 32 +- services/skills-service.ts | 239 +++++++++++++ services/teams-service.ts | 315 ++++++++++++++++++ services/user-service.ts | 93 ++++++ 8 files changed, 757 insertions(+), 412 deletions(-) diff --git a/app/api/admin/skills/route.ts b/app/api/admin/skills/route.ts index 663bb55..1fd647e 100644 --- a/app/api/admin/skills/route.ts +++ b/app/api/admin/skills/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { getPool } from "@/services/database"; +import { SkillsService } from "@/services/skills-service"; // Configuration pour éviter la génération statique export const dynamic = "force-dynamic"; @@ -7,35 +7,7 @@ export const dynamic = "force-dynamic"; // GET - Récupérer toutes les skills export async function GET() { try { - const pool = getPool(); - const query = ` - SELECT - s.id, - s.name, - s.description, - s.icon, - sc.id as category_id, - sc.name as category_name, - COUNT(DISTINCT se.id) as usage_count - FROM skills s - LEFT JOIN skill_categories sc ON s.category_id = sc.id - LEFT JOIN skill_evaluations se ON s.id = se.skill_id AND se.is_selected = true - GROUP BY s.id, s.name, s.description, s.icon, sc.id, sc.name - ORDER BY s.name - `; - - const result = await pool.query(query); - - const skills = result.rows.map((row) => ({ - id: row.id, - name: row.name, - description: row.description || "", - icon: row.icon || "", - categoryId: row.category_id, - category: row.category_name, - usageCount: parseInt(row.usage_count) || 0, - })); - + const skills = await SkillsService.getAllSkillsWithUsage(); return NextResponse.json(skills); } catch (error) { console.error("Error fetching skills:", error); @@ -58,48 +30,20 @@ export async function POST(request: NextRequest) { ); } - const pool = getPool(); - - // Vérifier si la skill existe déjà - const existingSkill = await pool.query( - "SELECT id FROM skills WHERE LOWER(name) = LOWER($1)", - [name] - ); - - if (existingSkill.rows.length > 0) { - return NextResponse.json( - { error: "Une skill avec ce nom existe déjà" }, - { status: 409 } - ); + try { + const skill = await SkillsService.createSkillForAdmin({ + name, + categoryId, + description, + icon, + }); + return NextResponse.json(skill, { status: 201 }); + } catch (error: any) { + if (error.message === "Une skill avec ce nom existe déjà") { + return NextResponse.json({ error: error.message }, { status: 409 }); + } + throw error; } - - // Créer la nouvelle skill - const result = await pool.query( - `INSERT INTO skills (id, name, category_id, description, icon) - VALUES (gen_random_uuid(), $1, $2, $3, $4) - RETURNING id, name, description, icon, category_id`, - [name, categoryId, description || "", icon || ""] - ); - - const newSkill = result.rows[0]; - - // Récupérer le nom de la catégorie - const categoryResult = await pool.query( - "SELECT name FROM skill_categories WHERE id = $1", - [newSkill.category_id] - ); - - const skill = { - id: newSkill.id, - name: newSkill.name, - description: newSkill.description, - icon: newSkill.icon, - categoryId: newSkill.category_id, - category: categoryResult.rows[0]?.name || "Inconnue", - usageCount: 0, - }; - - return NextResponse.json(skill, { status: 201 }); } catch (error) { console.error("Error creating skill:", error); return NextResponse.json( @@ -121,58 +65,24 @@ export async function PUT(request: NextRequest) { ); } - const pool = getPool(); - - // Vérifier si la skill existe - const existingSkill = await pool.query( - "SELECT id FROM skills WHERE id = $1", - [id] - ); - - if (existingSkill.rows.length === 0) { - return NextResponse.json({ error: "Skill non trouvée" }, { status: 404 }); + try { + const skill = await SkillsService.updateSkillForAdmin({ + id, + name, + categoryId, + description, + icon, + }); + return NextResponse.json(skill); + } catch (error: any) { + if (error.message === "Skill non trouvée") { + return NextResponse.json({ error: error.message }, { status: 404 }); + } + if (error.message === "Une skill avec ce nom existe déjà") { + return NextResponse.json({ error: error.message }, { status: 409 }); + } + throw error; } - - // Vérifier si le nom existe déjà (sauf pour cette skill) - const duplicateName = await pool.query( - "SELECT id FROM skills WHERE LOWER(name) = LOWER($1) AND id != $2", - [name, id] - ); - - if (duplicateName.rows.length > 0) { - return NextResponse.json( - { error: "Une skill avec ce nom existe déjà" }, - { status: 409 } - ); - } - - // Mettre à jour la skill - await pool.query( - `UPDATE skills - SET name = $1, category_id = $2, description = $3, icon = $4 - WHERE id = $5`, - [name, categoryId, description || "", icon || "", id] - ); - - // Récupérer la skill mise à jour - const result = await pool.query( - `SELECT s.id, s.name, s.description, s.icon, s.category_id, sc.name as category_name - FROM skills s - LEFT JOIN skill_categories sc ON s.category_id = sc.id - WHERE s.id = $1`, - [id] - ); - - const skill = result.rows[0]; - - return NextResponse.json({ - id: skill.id, - name: skill.name, - description: skill.description, - icon: skill.icon, - categoryId: skill.category_id, - category: skill.category_name, - }); } catch (error) { console.error("Error updating skill:", error); return NextResponse.json( @@ -195,28 +105,17 @@ export async function DELETE(request: NextRequest) { ); } - const pool = getPool(); - - // Vérifier si la skill est utilisée - const usageCheck = await pool.query( - `SELECT COUNT(*) as count - FROM skill_evaluations se - WHERE se.skill_id = $1 AND se.is_selected = true`, - [id] - ); - - const usageCount = parseInt(usageCheck.rows[0].count); - if (usageCount > 0) { - return NextResponse.json( - { error: "Impossible de supprimer une skill qui est utilisée" }, - { status: 409 } - ); + try { + await SkillsService.deleteSkillForAdmin(id); + return NextResponse.json({ message: "Skill supprimée avec succès" }); + } catch (error: any) { + if ( + error.message === "Impossible de supprimer une skill qui est utilisée" + ) { + return NextResponse.json({ error: error.message }, { status: 409 }); + } + throw error; } - - // Supprimer la skill - await pool.query("DELETE FROM skills WHERE id = $1", [id]); - - return NextResponse.json({ message: "Skill supprimée avec succès" }); } catch (error) { console.error("Error deleting skill:", error); return NextResponse.json( diff --git a/app/api/admin/teams/[teamId]/members/route.ts b/app/api/admin/teams/[teamId]/members/route.ts index 70b26ec..2eefa37 100644 --- a/app/api/admin/teams/[teamId]/members/route.ts +++ b/app/api/admin/teams/[teamId]/members/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { getPool } from "@/services/database"; +import { TeamsService } from "@/services/teams-service"; // GET - Récupérer les membres d'une équipe export async function GET( @@ -16,28 +16,7 @@ export async function GET( ); } - const pool = getPool(); - const query = ` - SELECT - u.uuid_id, - u.first_name, - u.last_name, - u.created_at - FROM users u - WHERE u.team_id = $1 - ORDER BY u.last_name, u.first_name - `; - - const result = await pool.query(query, [teamId]); - - const members = result.rows.map((row) => ({ - id: row.uuid_id, - firstName: row.first_name, - lastName: row.last_name, - fullName: `${row.first_name} ${row.last_name}`, - joinedAt: row.created_at, - })); - + const members = await TeamsService.getTeamMembersForAdmin(teamId); return NextResponse.json(members); } catch (error) { console.error("Error fetching team members:", error); @@ -64,29 +43,17 @@ export async function DELETE( ); } - const pool = getPool(); - - // Vérifier que le membre appartient bien à cette équipe - const memberCheck = await pool.query( - "SELECT uuid_id FROM users WHERE uuid_id = $1 AND team_id = $2", - [memberId, teamId] - ); - - if (memberCheck.rows.length === 0) { - return NextResponse.json( - { error: "Membre non trouvé dans cette équipe" }, - { status: 404 } - ); + try { + await TeamsService.removeMemberFromTeamForAdmin({ teamId, memberId }); + return NextResponse.json({ + message: "Membre supprimé de l'équipe avec succès", + }); + } catch (error: any) { + if (error.message === "Membre non trouvé dans cette équipe") { + return NextResponse.json({ error: error.message }, { status: 404 }); + } + throw error; } - - // Supprimer le membre (mettre team_id à NULL au lieu de supprimer l'utilisateur) - await pool.query("UPDATE users SET team_id = NULL WHERE uuid_id = $1", [ - memberId, - ]); - - return NextResponse.json({ - message: "Membre supprimé de l'équipe avec succès", - }); } catch (error) { console.error("Error removing team member:", error); return NextResponse.json( diff --git a/app/api/admin/teams/route.ts b/app/api/admin/teams/route.ts index 3c444cc..fd60ee8 100644 --- a/app/api/admin/teams/route.ts +++ b/app/api/admin/teams/route.ts @@ -1,31 +1,10 @@ import { NextRequest, NextResponse } from "next/server"; -import { getPool } from "@/services/database"; +import { TeamsService } from "@/services/teams-service"; // GET - Récupérer toutes les teams export async function GET() { try { - const pool = getPool(); - const query = ` - SELECT - t.id, - t.name, - t.direction, - COUNT(DISTINCT u.uuid_id) as member_count - FROM teams t - LEFT JOIN users u ON t.id = u.team_id - GROUP BY t.id, t.name, t.direction - ORDER BY t.direction, t.name - `; - - const result = await pool.query(query); - - const teams = result.rows.map((row) => ({ - id: row.id, - name: row.name, - direction: row.direction, - memberCount: parseInt(row.member_count) || 0, - })); - + const teams = await TeamsService.getAllTeamsWithMemberCount(); return NextResponse.json(teams); } catch (error) { console.error("Error fetching teams:", error); @@ -48,39 +27,18 @@ export async function POST(request: NextRequest) { ); } - const pool = getPool(); - - // Vérifier si la team existe déjà - const existingTeam = await pool.query( - "SELECT id FROM teams WHERE LOWER(name) = LOWER($1)", - [name] - ); - - if (existingTeam.rows.length > 0) { - return NextResponse.json( - { error: "Une équipe avec ce nom existe déjà" }, - { status: 409 } - ); + try { + const team = await TeamsService.createTeamForAdmin({ + name, + direction, + }); + return NextResponse.json(team, { status: 201 }); + } catch (error: any) { + if (error.message === "Une équipe avec ce nom existe déjà") { + return NextResponse.json({ error: error.message }, { status: 409 }); + } + throw error; } - - // Créer la nouvelle team - const result = await pool.query( - `INSERT INTO teams (name, direction) - VALUES ($1, $2) - RETURNING id, name, direction`, - [name, direction] - ); - - const newTeam = result.rows[0]; - - const team = { - id: newTeam.id, - name: newTeam.name, - direction: newTeam.direction, - memberCount: 0, - }; - - return NextResponse.json(team, { status: 201 }); } catch (error) { console.error("Error creating team:", error); return NextResponse.json( @@ -102,60 +60,22 @@ export async function PUT(request: NextRequest) { ); } - const pool = getPool(); - - // Vérifier si la team existe - const existingTeam = await pool.query( - "SELECT id FROM teams WHERE id = $1", - [id] - ); - - if (existingTeam.rows.length === 0) { - return NextResponse.json( - { error: "Équipe non trouvée" }, - { status: 404 } - ); + try { + const team = await TeamsService.updateTeamForAdmin({ + id, + name, + direction, + }); + return NextResponse.json(team); + } catch (error: any) { + if (error.message === "Équipe non trouvée") { + return NextResponse.json({ error: error.message }, { status: 404 }); + } + if (error.message === "Une équipe avec ce nom existe déjà") { + return NextResponse.json({ error: error.message }, { status: 409 }); + } + throw error; } - - // Vérifier si le nom existe déjà (sauf pour cette team) - const duplicateName = await pool.query( - "SELECT id FROM teams WHERE LOWER(name) = LOWER($1) AND id != $2", - [name, id] - ); - - if (duplicateName.rows.length > 0) { - return NextResponse.json( - { error: "Une équipe avec ce nom existe déjà" }, - { status: 409 } - ); - } - - // Mettre à jour la team - await pool.query( - `UPDATE teams - SET name = $1, direction = $2 - WHERE id = $3`, - [name, direction, id] - ); - - // Récupérer la team mise à jour - const result = await pool.query( - `SELECT t.id, t.name, t.direction, COUNT(DISTINCT u.uuid_id) as member_count - FROM teams t - LEFT JOIN users u ON t.id = u.team_id - WHERE t.id = $1 - GROUP BY t.id, t.name, t.direction`, - [id] - ); - - const team = result.rows[0]; - - return NextResponse.json({ - id: team.id, - name: team.name, - direction: team.direction, - memberCount: parseInt(team.member_count) || 0, - }); } catch (error) { console.error("Error updating team:", error); return NextResponse.json( @@ -179,60 +99,20 @@ export async function DELETE(request: NextRequest) { ); } - const pool = getPool(); - - if (direction) { - // Supprimer une direction entière - // Vérifier d'abord si des équipes ont des membres - const memberCheck = await pool.query( - `SELECT COUNT(*) as count - FROM users u - JOIN teams t ON u.team_id = t.id - WHERE t.direction = $1`, - [direction] - ); - - const memberCount = parseInt(memberCheck.rows[0].count); - if (memberCount > 0) { - return NextResponse.json( - { - error: `Impossible de supprimer la direction "${direction}" car certaines équipes ont des membres`, - }, - { status: 409 } - ); - } - - // Supprimer toutes les équipes de la direction - await pool.query("DELETE FROM teams WHERE direction = $1", [direction]); - - return NextResponse.json({ - message: `Direction "${direction}" et toutes ses équipes supprimées avec succès`, + try { + const result = await TeamsService.deleteTeamOrDirectionForAdmin({ + id: id || undefined, + direction: direction || undefined, }); - } else { - // Supprimer une équipe spécifique - // Vérifier si la team a des membres - const memberCheck = await pool.query( - `SELECT COUNT(*) as count - FROM users - WHERE team_id = $1`, - [id] - ); - - const memberCount = parseInt(memberCheck.rows[0].count); - if (memberCount > 0) { - return NextResponse.json( - { - error: - "Impossible de supprimer une équipe qui contient des membres", - }, - { status: 409 } - ); + return NextResponse.json(result); + } catch (error: any) { + if ( + error.message.includes("Impossible de supprimer") || + error.message.includes("contient des membres") + ) { + return NextResponse.json({ error: error.message }, { status: 409 }); } - - // Supprimer la team - await pool.query("DELETE FROM teams WHERE id = $1", [id]); - - return NextResponse.json({ message: "Équipe supprimée avec succès" }); + throw error; } } catch (error) { console.error("Error deleting team/direction:", error); diff --git a/app/api/admin/users/[userId]/route.ts b/app/api/admin/users/[userId]/route.ts index d5be15a..038772f 100644 --- a/app/api/admin/users/[userId]/route.ts +++ b/app/api/admin/users/[userId]/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { getPool } from "@/services/database"; +import { UserService } from "@/services/user-service"; // DELETE - Supprimer complètement un utilisateur export async function DELETE( @@ -16,40 +16,20 @@ export async function DELETE( ); } - const pool = getPool(); - - // Vérifier que l'utilisateur existe - const userCheck = await pool.query( - "SELECT uuid_id, first_name, last_name FROM users WHERE uuid_id = $1", - [userId] - ); - - if (userCheck.rows.length === 0) { - return NextResponse.json( - { error: "Utilisateur non trouvé" }, - { status: 404 } - ); + try { + const user = await UserService.deleteUserForAdmin(userId); + return NextResponse.json({ + message: `Utilisateur ${user.firstName} ${user.lastName} supprimé avec succès`, + }); + } catch (error: any) { + if (error.message === "Utilisateur non trouvé") { + return NextResponse.json({ error: error.message }, { status: 404 }); + } + if (error.message.includes("appartient à une équipe")) { + return NextResponse.json({ error: error.message }, { status: 409 }); + } + throw error; } - - const user = userCheck.rows[0]; - - // Vérifier que l'utilisateur n'est pas dans une équipe - if (user.team_id) { - return NextResponse.json( - { - error: - "Impossible de supprimer un utilisateur qui appartient à une équipe. Retirez-le d'abord de son équipe.", - }, - { status: 409 } - ); - } - - // Supprimer l'utilisateur (les évaluations par skills seront supprimées automatiquement grâce aux contraintes CASCADE) - await pool.query("DELETE FROM users WHERE uuid_id = $1", [userId]); - - return NextResponse.json({ - message: `Utilisateur ${user.first_name} ${user.last_name} supprimé avec succès`, - }); } catch (error) { console.error("Error deleting user:", error); return NextResponse.json( diff --git a/app/api/admin/users/route.ts b/app/api/admin/users/route.ts index dab0f08..ff432db 100644 --- a/app/api/admin/users/route.ts +++ b/app/api/admin/users/route.ts @@ -1,38 +1,10 @@ import { NextRequest, NextResponse } from "next/server"; -import { getPool } from "@/services/database"; +import { UserService } from "@/services/user-service"; // GET - Récupérer la liste des utilisateurs export async function GET(request: NextRequest) { try { - const pool = getPool(); - - // Récupérer tous les utilisateurs avec leurs informations d'équipe et d'évaluations - const query = ` - SELECT - u.uuid_id, - u.first_name, - u.last_name, - t.name as team_name, - CASE - WHEN ue.id IS NOT NULL THEN true - ELSE false - END as has_evaluations - FROM users u - LEFT JOIN teams t ON u.team_id = t.id - LEFT JOIN user_evaluations ue ON u.uuid_id = ue.user_uuid - ORDER BY u.first_name, u.last_name - `; - - const result = await pool.query(query); - - const users = result.rows.map((row) => ({ - uuid: row.uuid_id, - firstName: row.first_name, - lastName: row.last_name, - teamName: row.team_name, - hasEvaluations: row.has_evaluations, - })); - + const users = await UserService.getAllUsersForAdmin(); return NextResponse.json(users); } catch (error) { console.error("Error fetching users:", error); diff --git a/services/skills-service.ts b/services/skills-service.ts index 4a83fe0..a241d7f 100644 --- a/services/skills-service.ts +++ b/services/skills-service.ts @@ -40,6 +40,8 @@ export class SkillsService { if (!categoriesMap.has(categoryId)) { categoriesMap.set(categoryId, { + id: categoryId, + name: row.category_name, category: row.category_name, icon: row.category_icon, skills: [], @@ -250,4 +252,241 @@ export class SkillsService { client.release(); } } + + /** + * Get all skills with usage count for admin + */ + static async getAllSkillsWithUsage(): Promise< + Array<{ + id: string; + name: string; + description: string; + icon: string; + categoryId: string; + category: string; + usageCount: number; + }> + > { + const pool = getPool(); + const query = ` + SELECT + s.id, + s.name, + s.description, + s.icon, + sc.id as category_id, + sc.name as category_name, + COUNT(DISTINCT se.id) as usage_count + FROM skills s + LEFT JOIN skill_categories sc ON s.category_id = sc.id + LEFT JOIN skill_evaluations se ON s.id = se.skill_id AND se.is_selected = true + GROUP BY s.id, s.name, s.description, s.icon, sc.id, sc.name + ORDER BY s.name + `; + + try { + const result = await pool.query(query); + + return result.rows.map((row) => ({ + id: row.id, + name: row.name, + description: row.description || "", + icon: row.icon || "", + categoryId: row.category_id, + category: row.category_name, + usageCount: parseInt(row.usage_count) || 0, + })); + } catch (error) { + console.error("Error fetching skills with usage:", error); + throw new Error("Failed to fetch skills with usage"); + } + } + + /** + * Create a new skill for admin + */ + static async createSkillForAdmin(data: { + name: string; + categoryId: string; + description?: string; + icon?: string; + }): Promise<{ + id: string; + name: string; + description: string; + icon: string; + categoryId: string; + category: string; + usageCount: number; + }> { + const pool = getPool(); + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + + // Vérifier si la skill existe déjà + const existingSkill = await client.query( + "SELECT id FROM skills WHERE LOWER(name) = LOWER($1)", + [data.name] + ); + + if (existingSkill.rows.length > 0) { + throw new Error("Une skill avec ce nom existe déjà"); + } + + // Créer la nouvelle skill + const result = await client.query( + `INSERT INTO skills (id, name, category_id, description, icon) + VALUES (gen_random_uuid(), $1, $2, $3, $4) + RETURNING id, name, description, icon, category_id`, + [data.name, data.categoryId, data.description || "", data.icon || ""] + ); + + const newSkill = result.rows[0]; + + // Récupérer le nom de la catégorie + const categoryResult = await client.query( + "SELECT name FROM skill_categories WHERE id = $1", + [newSkill.category_id] + ); + + await client.query("COMMIT"); + + return { + id: newSkill.id, + name: newSkill.name, + description: newSkill.description, + icon: newSkill.icon, + categoryId: newSkill.category_id, + category: categoryResult.rows[0]?.name || "Inconnue", + usageCount: 0, + }; + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } finally { + client.release(); + } + } + + /** + * Update a skill for admin + */ + static async updateSkillForAdmin(data: { + id: string; + name: string; + categoryId: string; + description?: string; + icon?: string; + }): Promise<{ + id: string; + name: string; + description: string; + icon: string; + categoryId: string; + category: string; + }> { + const pool = getPool(); + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + + // Vérifier si la skill existe + const existingSkill = await client.query( + "SELECT id FROM skills WHERE id = $1", + [data.id] + ); + + if (existingSkill.rows.length === 0) { + throw new Error("Skill non trouvée"); + } + + // Vérifier si le nom existe déjà (sauf pour cette skill) + const duplicateName = await client.query( + "SELECT id FROM skills WHERE LOWER(name) = LOWER($1) AND id != $2", + [data.name, data.id] + ); + + if (duplicateName.rows.length > 0) { + throw new Error("Une skill avec ce nom existe déjà"); + } + + // Mettre à jour la skill + await client.query( + `UPDATE skills + SET name = $1, category_id = $2, description = $3, icon = $4 + WHERE id = $5`, + [ + data.name, + data.categoryId, + data.description || "", + data.icon || "", + data.id, + ] + ); + + // Récupérer la skill mise à jour + const result = await client.query( + `SELECT s.id, s.name, s.description, s.icon, s.category_id, sc.name as category_name + FROM skills s + LEFT JOIN skill_categories sc ON s.category_id = sc.id + WHERE s.id = $1`, + [data.id] + ); + + await client.query("COMMIT"); + + const skill = result.rows[0]; + return { + id: skill.id, + name: skill.name, + description: skill.description, + icon: skill.icon, + categoryId: skill.category_id, + category: skill.category_name, + }; + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } finally { + client.release(); + } + } + + /** + * Delete a skill for admin + */ + static async deleteSkillForAdmin(id: string): Promise { + const pool = getPool(); + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + + // Vérifier si la skill est utilisée + const usageCheck = await client.query( + `SELECT COUNT(*) as count + FROM skill_evaluations se + WHERE se.skill_id = $1 AND se.is_selected = true`, + [id] + ); + + const usageCount = parseInt(usageCheck.rows[0].count); + if (usageCount > 0) { + throw new Error("Impossible de supprimer une skill qui est utilisée"); + } + + // Supprimer la skill + await client.query("DELETE FROM skills WHERE id = $1", [id]); + + await client.query("COMMIT"); + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } finally { + client.release(); + } + } } diff --git a/services/teams-service.ts b/services/teams-service.ts index 5ca0740..1ab4198 100644 --- a/services/teams-service.ts +++ b/services/teams-service.ts @@ -168,4 +168,319 @@ export class TeamsService { throw new Error("Failed to fetch directions"); } } + + /** + * Get all teams with member count for admin + */ + static async getAllTeamsWithMemberCount(): Promise< + Array<{ + id: string; + name: string; + direction: string; + memberCount: number; + }> + > { + const pool = getPool(); + const query = ` + SELECT + t.id, + t.name, + t.direction, + COUNT(DISTINCT u.uuid_id) as member_count + FROM teams t + LEFT JOIN users u ON t.id = u.team_id + GROUP BY t.id, t.name, t.direction + ORDER BY t.direction, t.name + `; + + try { + const result = await pool.query(query); + return result.rows.map((row) => ({ + id: row.id, + name: row.name, + direction: row.direction, + memberCount: parseInt(row.member_count) || 0, + })); + } catch (error) { + console.error("Error fetching teams with member count:", error); + throw new Error("Failed to fetch teams with member count"); + } + } + + /** + * Create a new team for admin + */ + static async createTeamForAdmin(data: { + name: string; + direction: string; + }): Promise<{ + id: string; + name: string; + direction: string; + memberCount: number; + }> { + const pool = getPool(); + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + + // Vérifier si la team existe déjà + const existingTeam = await client.query( + "SELECT id FROM teams WHERE LOWER(name) = LOWER($1)", + [data.name] + ); + + if (existingTeam.rows.length > 0) { + throw new Error("Une équipe avec ce nom existe déjà"); + } + + // Créer la nouvelle team + const result = await client.query( + `INSERT INTO teams (name, direction) + VALUES ($1, $2) + RETURNING id, name, direction`, + [data.name, data.direction] + ); + + await client.query("COMMIT"); + + const newTeam = result.rows[0]; + return { + id: newTeam.id, + name: newTeam.name, + direction: newTeam.direction, + memberCount: 0, + }; + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } finally { + client.release(); + } + } + + /** + * Update a team for admin + */ + static async updateTeamForAdmin(data: { + id: string; + name: string; + direction: string; + }): Promise<{ + id: string; + name: string; + direction: string; + memberCount: number; + }> { + const pool = getPool(); + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + + // Vérifier si la team existe + const existingTeam = await client.query( + "SELECT id FROM teams WHERE id = $1", + [data.id] + ); + + if (existingTeam.rows.length === 0) { + throw new Error("Équipe non trouvée"); + } + + // Vérifier si le nom existe déjà (sauf pour cette team) + const duplicateName = await client.query( + "SELECT id FROM teams WHERE LOWER(name) = LOWER($1) AND id != $2", + [data.name, data.id] + ); + + if (duplicateName.rows.length > 0) { + throw new Error("Une équipe avec ce nom existe déjà"); + } + + // Mettre à jour la team + await client.query( + `UPDATE teams + SET name = $1, direction = $2 + WHERE id = $3`, + [data.name, data.direction, data.id] + ); + + // Récupérer la team mise à jour + const result = await client.query( + `SELECT t.id, t.name, t.direction, COUNT(DISTINCT u.uuid_id) as member_count + FROM teams t + LEFT JOIN users u ON t.id = u.team_id + WHERE t.id = $1 + GROUP BY t.id, t.name, t.direction`, + [data.id] + ); + + await client.query("COMMIT"); + + const team = result.rows[0]; + return { + id: team.id, + name: team.name, + direction: team.direction, + memberCount: parseInt(team.member_count) || 0, + }; + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } finally { + client.release(); + } + } + + /** + * Delete a team or direction for admin + */ + static async deleteTeamOrDirectionForAdmin(params: { + id?: string; + direction?: string; + }): Promise<{ message: string }> { + const pool = getPool(); + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + + if (params.direction) { + // Supprimer une direction entière + // Vérifier d'abord si des équipes ont des membres + const memberCheck = await client.query( + `SELECT COUNT(*) as count + FROM users u + JOIN teams t ON u.team_id = t.id + WHERE t.direction = $1`, + [params.direction] + ); + + const memberCount = parseInt(memberCheck.rows[0].count); + if (memberCount > 0) { + throw new Error( + `Impossible de supprimer la direction "${params.direction}" car certaines équipes ont des membres` + ); + } + + // Supprimer toutes les équipes de la direction + await client.query("DELETE FROM teams WHERE direction = $1", [ + params.direction, + ]); + + await client.query("COMMIT"); + return { + message: `Direction "${params.direction}" et toutes ses équipes supprimées avec succès`, + }; + } else if (params.id) { + // Supprimer une équipe spécifique + // Vérifier si la team a des membres + const memberCheck = await client.query( + `SELECT COUNT(*) as count + FROM users + WHERE team_id = $1`, + [params.id] + ); + + const memberCount = parseInt(memberCheck.rows[0].count); + if (memberCount > 0) { + throw new Error( + "Impossible de supprimer une équipe qui contient des membres" + ); + } + + // Supprimer la team + await client.query("DELETE FROM teams WHERE id = $1", [params.id]); + + await client.query("COMMIT"); + return { message: "Équipe supprimée avec succès" }; + } else { + throw new Error("L'ID de l'équipe ou la direction est requis"); + } + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } finally { + client.release(); + } + } + + /** + * Get team members for admin + */ + static async getTeamMembersForAdmin(teamId: string): Promise< + Array<{ + id: string; + firstName: string; + lastName: string; + fullName: string; + joinedAt: Date; + }> + > { + const pool = getPool(); + const query = ` + SELECT + u.uuid_id, + u.first_name, + u.last_name, + u.created_at + FROM users u + WHERE u.team_id = $1 + ORDER BY u.last_name, u.first_name + `; + + try { + const result = await pool.query(query, [teamId]); + + return result.rows.map((row) => ({ + id: row.uuid_id, + firstName: row.first_name, + lastName: row.last_name, + fullName: `${row.first_name} ${row.last_name}`, + joinedAt: row.created_at, + })); + } catch (error) { + console.error("Error fetching team members:", error); + throw new Error("Failed to fetch team members"); + } + } + + /** + * Remove member from team for admin + */ + static async removeMemberFromTeamForAdmin(params: { + teamId: string; + memberId: string; + }): Promise { + const pool = getPool(); + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + + // Vérifier que le membre appartient bien à cette équipe + const memberCheck = await client.query( + "SELECT uuid_id FROM users WHERE uuid_id = $1 AND team_id = $2", + [params.memberId, params.teamId] + ); + + if (memberCheck.rows.length === 0) { + throw new Error("Membre non trouvé dans cette équipe"); + } + + // Supprimer le membre (mettre team_id à NULL au lieu de supprimer l'utilisateur) + await client.query("UPDATE users SET team_id = NULL WHERE uuid_id = $1", [ + params.memberId, + ]); + + await client.query("COMMIT"); + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } finally { + client.release(); + } + } } diff --git a/services/user-service.ts b/services/user-service.ts index 28ad1e4..e061045 100644 --- a/services/user-service.ts +++ b/services/user-service.ts @@ -186,6 +186,99 @@ export class UserService { client.release(); } } + + /** + * Delete user for admin + */ + static async deleteUserForAdmin( + userId: string + ): Promise<{ firstName: string; lastName: string }> { + const pool = getPool(); + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + + // Vérifier que l'utilisateur existe + const userCheck = await client.query( + "SELECT uuid_id, first_name, last_name, team_id FROM users WHERE uuid_id = $1", + [userId] + ); + + if (userCheck.rows.length === 0) { + throw new Error("Utilisateur non trouvé"); + } + + const user = userCheck.rows[0]; + + // Vérifier que l'utilisateur n'est pas dans une équipe + if (user.team_id) { + throw new Error( + "Impossible de supprimer un utilisateur qui appartient à une équipe. Retirez-le d'abord de son équipe." + ); + } + + // Supprimer l'utilisateur (les évaluations par skills seront supprimées automatiquement grâce aux contraintes CASCADE) + await client.query("DELETE FROM users WHERE uuid_id = $1", [userId]); + + await client.query("COMMIT"); + + return { + firstName: user.first_name, + lastName: user.last_name, + }; + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } finally { + client.release(); + } + } + + /** + * Get all users with team info and evaluations status for admin + */ + static async getAllUsersForAdmin(): Promise< + Array<{ + uuid: string; + firstName: string; + lastName: string; + teamName: string | null; + hasEvaluations: boolean; + }> + > { + const pool = getPool(); + const query = ` + SELECT + u.uuid_id, + u.first_name, + u.last_name, + t.name as team_name, + CASE + WHEN ue.id IS NOT NULL THEN true + ELSE false + END as has_evaluations + FROM users u + LEFT JOIN teams t ON u.team_id = t.id + LEFT JOIN user_evaluations ue ON u.uuid_id = ue.user_uuid + ORDER BY u.first_name, u.last_name + `; + + try { + const result = await pool.query(query); + + return result.rows.map((row) => ({ + uuid: row.uuid_id, + firstName: row.first_name, + lastName: row.last_name, + teamName: row.team_name, + hasEvaluations: row.has_evaluations, + })); + } catch (error) { + console.error("Error fetching users:", error); + throw new Error("Failed to fetch users"); + } + } } // Instance singleton