import { getPool } from "./database"; import { UserEvaluation, UserProfile, CategoryEvaluation, SkillEvaluation, SkillLevel, } from "../lib/types"; export class EvaluationService { /** * Crée ou met à jour un utilisateur et retourne son UUID */ async upsertUserUuid(profile: UserProfile): Promise { const pool = getPool(); const client = await pool.connect(); try { // Vérifier si l'utilisateur existe déjà (par firstName + lastName + teamId) const existingUserQuery = ` SELECT uuid_id FROM users WHERE first_name = $1 AND last_name = $2 AND team_id = $3 `; const existingUser = await client.query(existingUserQuery, [ profile.firstName, profile.lastName, profile.teamId, ]); if (existingUser.rows.length > 0) { // Retourner l'UUID de l'utilisateur existant return existingUser.rows[0].uuid_id; } else { // Créer un nouvel utilisateur avec UUID auto-généré const insertQuery = ` INSERT INTO users (first_name, last_name, team_id, uuid_id) VALUES ($1, $2, $3, uuid_generate_v4()) RETURNING uuid_id `; const result = await client.query(insertQuery, [ profile.firstName, profile.lastName, profile.teamId, ]); return result.rows[0].uuid_id; } } finally { client.release(); } } /** * Récupère un utilisateur par son UUID */ async getUserByUuid(userUuid: string): Promise { const pool = getPool(); const client = await pool.connect(); try { const query = ` SELECT u.first_name, u.last_name, u.team_id FROM users u WHERE u.uuid_id = $1 `; const result = await client.query(query, [userUuid]); if (result.rows.length === 0) { return null; } const user = result.rows[0]; return { firstName: user.first_name, lastName: user.last_name, teamId: user.team_id, }; } finally { client.release(); } } /** * Crée ou met à jour un utilisateur (legacy - retourne l'ID numérique) */ async upsertUser(profile: UserProfile): Promise { const pool = getPool(); const client = await pool.connect(); try { // Vérifier si l'utilisateur existe déjà (par firstName + lastName + teamId) const existingUserQuery = ` SELECT id FROM users WHERE first_name = $1 AND last_name = $2 AND team_id = $3 `; const existingUser = await client.query(existingUserQuery, [ profile.firstName, profile.lastName, profile.teamId, ]); if (existingUser.rows.length > 0) { // Mettre à jour l'utilisateur existant const updateQuery = ` UPDATE users SET first_name = $1, last_name = $2, team_id = $3, updated_at = CURRENT_TIMESTAMP WHERE id = $4 RETURNING id `; const result = await client.query(updateQuery, [ profile.firstName, profile.lastName, profile.teamId, existingUser.rows[0].id, ]); return result.rows[0].id; } else { // Créer un nouvel utilisateur const insertQuery = ` INSERT INTO users (first_name, last_name, team_id) VALUES ($1, $2, $3) RETURNING id `; const result = await client.query(insertQuery, [ profile.firstName, profile.lastName, profile.teamId, ]); return result.rows[0].id; } } finally { client.release(); } } /** * Récupère un utilisateur par son ID */ async getUserById(userId: number): Promise { const pool = getPool(); const client = await pool.connect(); try { const query = ` SELECT u.first_name, u.last_name, u.team_id FROM users u WHERE u.id = $1 `; const result = await client.query(query, [userId]); if (result.rows.length === 0) { return null; } const user = result.rows[0]; return { firstName: user.first_name, lastName: user.last_name, teamId: user.team_id, }; } finally { client.release(); } } /** * Sauvegarde une évaluation utilisateur complète (version UUID) */ async saveUserEvaluationUuid(evaluation: UserEvaluation): Promise { const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); // 1. Upsert user avec UUID const userUuid = await this.upsertUserUuid(evaluation.profile); // 2. Upsert user_evaluation avec user_uuid const userEvalQuery = ` INSERT INTO user_evaluations (user_uuid, last_updated) VALUES ($1, $2) ON CONFLICT (user_uuid) DO UPDATE SET last_updated = $2 RETURNING id `; const userEvalResult = await client.query(userEvalQuery, [ userUuid, new Date(evaluation.lastUpdated), ]); const userEvaluationId = userEvalResult.rows[0].id; // 3. Sauvegarde chaque catégorie d'évaluation for (const catEval of evaluation.evaluations) { await this.saveSkillEvaluations(client, userEvaluationId, catEval); } await client.query("COMMIT"); } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } } /** * Sauvegarde une évaluation utilisateur complète (legacy) */ async saveUserEvaluation(evaluation: UserEvaluation): Promise { const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); // 1. Upsert user const userId = await this.upsertUser(evaluation.profile); // 2. Upsert user_evaluation const userEvalQuery = ` INSERT INTO user_evaluations (user_id, last_updated) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET last_updated = $2 RETURNING id `; const userEvalResult = await client.query(userEvalQuery, [ userId, new Date(evaluation.lastUpdated), ]); const userEvaluationId = userEvalResult.rows[0].id; // 3. Supprimer les anciennes évaluations de skills await client.query( "DELETE FROM skill_evaluations WHERE user_evaluation_id = $1", [userEvaluationId] ); // 4. Sauvegarder les nouvelles évaluations directement for (const catEval of evaluation.evaluations) { await this.saveSkillEvaluations(client, userEvaluationId, catEval); } await client.query("COMMIT"); } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } } /** * Sauvegarde les évaluations de skills directement */ private async saveSkillEvaluations( client: any, userEvaluationId: number, catEval: CategoryEvaluation ): Promise { // Insérer les skills sélectionnées (sans évaluation) for (const skillId of catEval.selectedSkillIds) { const isEvaluated = catEval.skills.some( (se) => se.skillId === skillId && se.level !== null ); if (!isEvaluated) { // Skill sélectionnée mais pas encore évaluée await client.query( ` INSERT INTO skill_evaluations (user_evaluation_id, skill_id, level, is_selected, can_mentor, wants_to_learn) VALUES ($1, $2, 'never'::skill_level_enum, true, false, false) `, [userEvaluationId, skillId] ); } } // Insérer les évaluations de skills for (const skillEval of catEval.skills) { if (skillEval.level !== null) { await client.query( ` INSERT INTO skill_evaluations (user_evaluation_id, skill_id, level, is_selected, can_mentor, wants_to_learn) VALUES ($1, $2, $3, true, $4, $5) `, [ userEvaluationId, skillEval.skillId, skillEval.level, skillEval.canMentor || false, skillEval.wantsToLearn || false, ] ); } } } /** * Charge une évaluation utilisateur */ async loadUserEvaluation( profile: UserProfile ): Promise { const pool = getPool(); const client = await pool.connect(); try { // Trouver l'utilisateur const userQuery = ` SELECT u.*, ue.id as user_evaluation_id, ue.last_updated FROM users u LEFT JOIN user_evaluations ue ON u.uuid_id = ue.user_uuid WHERE u.first_name = $1 AND u.last_name = $2 AND u.team_id = $3 `; const userResult = await client.query(userQuery, [ profile.firstName, profile.lastName, profile.teamId, ]); if ( userResult.rows.length === 0 || !userResult.rows[0].user_evaluation_id ) { return null; } const userData = userResult.rows[0]; const userEvaluationId = userData.user_evaluation_id; // Charger directement les skills évaluées avec leurs catégories const skillsQuery = ` SELECT sc.name as category_name, se.skill_id, se.level, se.can_mentor, se.wants_to_learn, se.is_selected FROM skill_evaluations se JOIN skills s ON se.skill_id = s.id JOIN skill_categories sc ON s.category_id = sc.id WHERE se.user_evaluation_id = $1 ORDER BY sc.name, s.name `; const skillsResult = await client.query(skillsQuery, [userEvaluationId]); // Grouper par catégorie const categoriesMap = new Map(); for (const row of skillsResult.rows) { const categoryName = row.category_name; if (!categoriesMap.has(categoryName)) { categoriesMap.set(categoryName, { category: categoryName, selectedSkillIds: [], skills: [], }); } const catEval = categoriesMap.get(categoryName)!; // Ajouter aux skills sélectionnées si is_selected = true if (row.is_selected) { catEval.selectedSkillIds.push(row.skill_id); } // Ajouter aux évaluations si elle est sélectionnée (donc évaluée) if (row.is_selected) { catEval.skills.push({ skillId: row.skill_id, level: row.level, canMentor: row.can_mentor, wantsToLearn: row.wants_to_learn, }); } } const evaluations: CategoryEvaluation[] = Array.from( categoriesMap.values() ); return { profile, evaluations, lastUpdated: userData.last_updated.toISOString(), }; } finally { client.release(); } } /** * Met à jour le niveau d'une skill (version UUID) */ async updateSkillLevelUuid( profile: UserProfile, category: string, skillId: string, level: SkillLevel ): Promise { await this.updateSkillPropertyUuid( profile, category, skillId, "level", level ); } /** * Met à jour le statut mentor d'une skill (version UUID) */ async updateSkillMentorStatusUuid( profile: UserProfile, category: string, skillId: string, canMentor: boolean ): Promise { await this.updateSkillPropertyUuid( profile, category, skillId, "can_mentor", canMentor ); } /** * Met à jour le statut apprentissage d'une skill (version UUID) */ async updateSkillLearningStatusUuid( profile: UserProfile, category: string, skillId: string, wantsToLearn: boolean ): Promise { await this.updateSkillPropertyUuid( profile, category, skillId, "wants_to_learn", wantsToLearn ); } /** * Méthode utilitaire pour mettre à jour une propriété de skill (version UUID) */ private async updateSkillPropertyUuid( profile: UserProfile, category: string, skillId: string, property: string, value: any ): Promise { const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); const userUuid = await this.upsertUserUuid(profile); // Upsert user_evaluation avec user_uuid const userEvalResult = await client.query( ` INSERT INTO user_evaluations (user_uuid, last_updated) VALUES ($1, CURRENT_TIMESTAMP) ON CONFLICT (user_uuid) DO UPDATE SET last_updated = CURRENT_TIMESTAMP RETURNING id `, [userUuid] ); const userEvaluationId = userEvalResult.rows[0].id; // Upsert skill evaluation avec gestion conditionnelle du level let updateQuery: string; let queryParams: any[]; if (property === "level") { // Si on met à jour le level, utiliser la valeur fournie updateQuery = ` INSERT INTO skill_evaluations (user_evaluation_id, skill_id, level, is_selected) VALUES ($1, $2, $3, true) ON CONFLICT (user_evaluation_id, skill_id) DO UPDATE SET level = $3, is_selected = true `; queryParams = [userEvaluationId, skillId, value]; } else { // Si on met à jour une autre propriété, level par défaut = 'never' updateQuery = ` INSERT INTO skill_evaluations (user_evaluation_id, skill_id, level, ${property}, is_selected) VALUES ($1, $2, 'never', $3, true) ON CONFLICT (user_evaluation_id, skill_id) DO UPDATE SET ${property} = $3, is_selected = true `; queryParams = [userEvaluationId, skillId, value]; } await client.query(updateQuery, queryParams); await client.query("COMMIT"); } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } } /** * Met à jour le niveau d'une skill (legacy) */ async updateSkillLevel( profile: UserProfile, category: string, skillId: string, level: SkillLevel ): Promise { await this.upsertSkillEvaluation(profile, skillId, { level }); } /** * Met à jour le statut de mentorat d'une skill */ async updateSkillMentorStatus( profile: UserProfile, category: string, skillId: string, canMentor: boolean ): Promise { await this.upsertSkillEvaluation(profile, skillId, { canMentor }); } /** * Met à jour le statut d'apprentissage d'une skill */ async updateSkillLearningStatus( profile: UserProfile, category: string, skillId: string, wantsToLearn: boolean ): Promise { await this.upsertSkillEvaluation(profile, skillId, { wantsToLearn }); } /** * Méthode utilitaire pour mettre à jour ou créer une évaluation de skill */ private async upsertSkillEvaluation( profile: UserProfile, skillId: string, updates: { level?: SkillLevel; canMentor?: boolean; wantsToLearn?: boolean; isSelected?: boolean; } ): Promise { const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); const userId = await this.upsertUser(profile); // Upsert user_evaluation const userEvalResult = await client.query( ` INSERT INTO user_evaluations (user_id, last_updated) VALUES ($1, CURRENT_TIMESTAMP) ON CONFLICT (user_id) DO UPDATE SET last_updated = CURRENT_TIMESTAMP RETURNING id `, [userId] ); const userEvaluationId = userEvalResult.rows[0].id; // Upsert skill evaluation avec valeurs par défaut const query = ` INSERT INTO skill_evaluations ( user_evaluation_id, skill_id, level, can_mentor, wants_to_learn, is_selected ) VALUES ($1, $2, COALESCE($3::skill_level_enum, 'never'::skill_level_enum), COALESCE($4, false), COALESCE($5, false), COALESCE($6, true) ) ON CONFLICT (user_evaluation_id, skill_id) DO UPDATE SET level = CASE WHEN $3 IS NOT NULL THEN $3::skill_level_enum ELSE skill_evaluations.level END, can_mentor = CASE WHEN $4 IS NOT NULL THEN $4 ELSE skill_evaluations.can_mentor END, wants_to_learn = CASE WHEN $5 IS NOT NULL THEN $5 ELSE skill_evaluations.wants_to_learn END, is_selected = CASE WHEN $6 IS NOT NULL THEN $6 ELSE skill_evaluations.is_selected END, updated_at = CURRENT_TIMESTAMP `; await client.query(query, [ userEvaluationId, skillId, updates.level || null, updates.canMentor !== undefined ? updates.canMentor : null, updates.wantsToLearn !== undefined ? updates.wantsToLearn : null, updates.isSelected !== undefined ? updates.isSelected : null, ]); await client.query("COMMIT"); } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } } /** * Ajoute une skill à l'évaluation (version UUID) */ async addSkillToEvaluationUuid( profile: UserProfile, category: string, skillId: string ): Promise { await this.updateSkillPropertyUuid( profile, category, skillId, "level", "never" // Valeur par défaut pour une skill nouvellement sélectionnée ); } /** * Supprime une skill de l'évaluation (version UUID) */ async removeSkillFromEvaluationUuid( profile: UserProfile, category: string, skillId: string ): Promise { const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); const userUuid = await this.upsertUserUuid(profile); // Supprimer directement la skill evaluation const deleteQuery = ` DELETE FROM skill_evaluations WHERE user_evaluation_id = ( SELECT id FROM user_evaluations WHERE user_uuid = $1 ) AND skill_id = $2 `; await client.query(deleteQuery, [userUuid, skillId]); await client.query("COMMIT"); } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } } /** * Ajoute une skill à l'évaluation (legacy) */ async addSkillToEvaluation( profile: UserProfile, category: string, skillId: string ): Promise { await this.upsertSkillEvaluation(profile, skillId, { isSelected: true, level: "never" as SkillLevel, // Valeur par défaut pour une skill nouvellement sélectionnée }); } /** * Supprime une skill de l'évaluation */ async removeSkillFromEvaluation( profile: UserProfile, category: string, skillId: string ): Promise { const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); const userId = await this.upsertUser(profile); // Supprimer directement la skill evaluation const deleteQuery = ` DELETE FROM skill_evaluations WHERE user_evaluation_id = ( SELECT id FROM user_evaluations WHERE user_id = $1 ) AND skill_id = $2 `; await client.query(deleteQuery, [userId, skillId]); await client.query("COMMIT"); } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } } } // Instance singleton export const evaluationService = new EvaluationService();