diff --git a/scripts/init.sql b/scripts/init.sql index a6d921e..e1274a0 100644 --- a/scripts/init.sql +++ b/scripts/init.sql @@ -45,10 +45,15 @@ CREATE TABLE users ( last_name VARCHAR(100) NOT NULL, team_id VARCHAR(50) REFERENCES teams(id), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(first_name, last_name, team_id) ); --- User evaluations table +-- ======================================== +-- SIMPLIFIED EVALUATION SCHEMA +-- ======================================== + +-- User evaluations - metadata about user's evaluation session CREATE TABLE user_evaluations ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, @@ -57,35 +62,18 @@ CREATE TABLE user_evaluations ( UNIQUE(user_id) ); --- Category evaluations table -CREATE TABLE category_evaluations ( - id SERIAL PRIMARY KEY, - user_evaluation_id INTEGER REFERENCES user_evaluations(id) ON DELETE CASCADE, - category VARCHAR(100) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(user_evaluation_id, category) -); - --- Selected skills table (skills the user wants to evaluate) -CREATE TABLE selected_skills ( - id SERIAL PRIMARY KEY, - category_evaluation_id INTEGER REFERENCES category_evaluations(id) ON DELETE CASCADE, - skill_id VARCHAR(100) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(category_evaluation_id, skill_id) -); - --- Skill evaluations table +-- Skill evaluations - direct relationship between user and skill CREATE TABLE skill_evaluations ( id SERIAL PRIMARY KEY, - category_evaluation_id INTEGER REFERENCES category_evaluations(id) ON DELETE CASCADE, - skill_id VARCHAR(100) NOT NULL, + user_evaluation_id INTEGER REFERENCES user_evaluations(id) ON DELETE CASCADE, + skill_id VARCHAR(100) REFERENCES skills(id) ON DELETE CASCADE, level skill_level_enum NOT NULL, can_mentor BOOLEAN DEFAULT FALSE, wants_to_learn BOOLEAN DEFAULT FALSE, + is_selected BOOLEAN DEFAULT TRUE, -- Replaces selected_skills table created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(category_evaluation_id, skill_id) + UNIQUE(user_evaluation_id, skill_id) ); -- Insert initial teams data @@ -104,11 +92,11 @@ CREATE INDEX idx_teams_direction ON teams(direction); CREATE INDEX idx_skills_category_id ON skills(category_id); CREATE INDEX idx_skill_links_skill_id ON skill_links(skill_id); CREATE INDEX idx_users_team_id ON users(team_id); +CREATE INDEX idx_users_unique_person ON users(first_name, last_name, team_id); CREATE INDEX idx_user_evaluations_user_id ON user_evaluations(user_id); -CREATE INDEX idx_category_evaluations_user_evaluation_id ON category_evaluations(user_evaluation_id); -CREATE INDEX idx_selected_skills_category_evaluation_id ON selected_skills(category_evaluation_id); -CREATE INDEX idx_skill_evaluations_category_evaluation_id ON skill_evaluations(category_evaluation_id); +CREATE INDEX idx_skill_evaluations_user_evaluation_id ON skill_evaluations(user_evaluation_id); CREATE INDEX idx_skill_evaluations_skill_id ON skill_evaluations(skill_id); +CREATE INDEX idx_skill_evaluations_is_selected ON skill_evaluations(is_selected); -- Update trigger for users table CREATE OR REPLACE FUNCTION update_updated_at_column() @@ -133,3 +121,33 @@ CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users CREATE TRIGGER update_skill_evaluations_updated_at BEFORE UPDATE ON skill_evaluations FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +-- Views for easy querying +CREATE VIEW user_skills_summary AS +SELECT + u.first_name, + u.last_name, + t.name as team_name, + t.direction, + sc.name as category_name, + s.name as skill_name, + se.level, + se.can_mentor, + se.wants_to_learn, + se.is_selected, + se.updated_at +FROM users u +JOIN teams t ON u.team_id = t.id +JOIN user_evaluations ue ON u.id = ue.user_id +JOIN skill_evaluations se ON ue.id = se.user_evaluation_id +JOIN skills s ON se.skill_id = s.id +JOIN skill_categories sc ON s.category_id = sc.id; + +CREATE VIEW category_skills_count AS +SELECT + sc.id as category_id, + sc.name as category_name, + COUNT(s.id) as total_skills +FROM skill_categories sc +LEFT JOIN skills s ON sc.id = s.category_id +GROUP BY sc.id, sc.name; diff --git a/services/evaluation-service.ts b/services/evaluation-service.ts index 21cab40..bda260c 100644 --- a/services/evaluation-service.ts +++ b/services/evaluation-service.ts @@ -95,15 +95,15 @@ export class EvaluationService { const userEvaluationId = userEvalResult.rows[0].id; - // 3. Supprimer les anciennes évaluations de catégories + // 3. Supprimer les anciennes évaluations de skills await client.query( - "DELETE FROM category_evaluations WHERE user_evaluation_id = $1", + "DELETE FROM skill_evaluations WHERE user_evaluation_id = $1", [userEvaluationId] ); - // 4. Sauvegarder les nouvelles évaluations + // 4. Sauvegarder les nouvelles évaluations directement for (const catEval of evaluation.evaluations) { - await this.saveCategoryEvaluation(client, userEvaluationId, catEval); + await this.saveSkillEvaluations(client, userEvaluationId, catEval); } await client.query("COMMIT"); @@ -116,33 +116,30 @@ export class EvaluationService { } /** - * Sauvegarde une évaluation de catégorie + * Sauvegarde les évaluations de skills directement */ - private async saveCategoryEvaluation( + private async saveSkillEvaluations( client: any, userEvaluationId: number, catEval: CategoryEvaluation ): Promise { - // Insérer la catégorie d'évaluation - const catEvalQuery = ` - INSERT INTO category_evaluations (user_evaluation_id, category) - VALUES ($1, $2) - RETURNING id - `; - - const catEvalResult = await client.query(catEvalQuery, [ - userEvaluationId, - catEval.category, - ]); - - const categoryEvaluationId = catEvalResult.rows[0].id; - - // Insérer les skills sélectionnées + // Insérer les skills sélectionnées (sans évaluation) for (const skillId of catEval.selectedSkillIds) { - await client.query( - "INSERT INTO selected_skills (category_evaluation_id, skill_id) VALUES ($1, $2)", - [categoryEvaluationId, skillId] + 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 @@ -151,11 +148,11 @@ export class EvaluationService { await client.query( ` INSERT INTO skill_evaluations - (category_evaluation_id, skill_id, level, can_mentor, wants_to_learn) - VALUES ($1, $2, $3, $4, $5) + (user_evaluation_id, skill_id, level, is_selected, can_mentor, wants_to_learn) + VALUES ($1, $2, $3, true, $4, $5) `, [ - categoryEvaluationId, + userEvaluationId, skillEval.skillId, skillEval.level, skillEval.canMentor || false, @@ -200,38 +197,58 @@ export class EvaluationService { const userData = userResult.rows[0]; const userEvaluationId = userData.user_evaluation_id; - // Charger les évaluations de catégories - const categoriesQuery = ` - SELECT ce.*, - array_agg(DISTINCT ss.skill_id) FILTER (WHERE ss.skill_id IS NOT NULL) as selected_skill_ids, - array_agg( - json_build_object( - 'skillId', se.skill_id, - 'level', se.level, - 'canMentor', se.can_mentor, - 'wantsToLearn', se.wants_to_learn - ) - ) FILTER (WHERE se.skill_id IS NOT NULL) as skill_evaluations - FROM category_evaluations ce - LEFT JOIN selected_skills ss ON ce.id = ss.category_evaluation_id - LEFT JOIN skill_evaluations se ON ce.id = se.category_evaluation_id - WHERE ce.user_evaluation_id = $1 - GROUP BY ce.id, ce.category - ORDER BY ce.category + // 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 categoriesResult = await client.query(categoriesQuery, [ - userEvaluationId, - ]); + const skillsResult = await client.query(skillsQuery, [userEvaluationId]); - const evaluations: CategoryEvaluation[] = categoriesResult.rows.map( - (row) => ({ - category: row.category, - selectedSkillIds: row.selected_skill_ids || [], - skills: (row.skill_evaluations || []).filter( - (se: any) => se.skillId !== null - ), - }) + // 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 { @@ -253,60 +270,7 @@ export class EvaluationService { skillId: string, level: SkillLevel ): Promise { - const pool = getPool(); - const client = await pool.connect(); - - try { - await client.query("BEGIN"); - - const userId = await this.upsertUser(profile); - - // Obtenir ou créer 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; - - // Obtenir ou créer category_evaluation - const catEvalResult = await client.query( - ` - INSERT INTO category_evaluations (user_evaluation_id, category) - VALUES ($1, $2) - ON CONFLICT (user_evaluation_id, category) - DO UPDATE SET category = $2 - RETURNING id - `, - [userEvaluationId, category] - ); - - const categoryEvaluationId = catEvalResult.rows[0].id; - - // Upsert skill evaluation - await client.query( - ` - INSERT INTO skill_evaluations (category_evaluation_id, skill_id, level) - VALUES ($1, $2, $3) - ON CONFLICT (category_evaluation_id, skill_id) - DO UPDATE SET level = $3, updated_at = CURRENT_TIMESTAMP - `, - [categoryEvaluationId, skillId, level] - ); - - await client.query("COMMIT"); - } catch (error) { - await client.query("ROLLBACK"); - throw error; - } finally { - client.release(); - } + await this.upsertSkillEvaluation(profile, skillId, { level }); } /** @@ -318,13 +282,7 @@ export class EvaluationService { skillId: string, canMentor: boolean ): Promise { - await this.updateSkillProperty( - profile, - category, - skillId, - "can_mentor", - canMentor - ); + await this.upsertSkillEvaluation(profile, skillId, { canMentor }); } /** @@ -336,24 +294,21 @@ export class EvaluationService { skillId: string, wantsToLearn: boolean ): Promise { - await this.updateSkillProperty( - profile, - category, - skillId, - "wants_to_learn", - wantsToLearn - ); + await this.upsertSkillEvaluation(profile, skillId, { wantsToLearn }); } /** - * Méthode utilitaire pour mettre à jour une propriété de skill + * Méthode utilitaire pour mettre à jour ou créer une évaluation de skill */ - private async updateSkillProperty( + private async upsertSkillEvaluation( profile: UserProfile, - category: string, skillId: string, - property: "can_mentor" | "wants_to_learn", - value: boolean + updates: { + level?: SkillLevel; + canMentor?: boolean; + wantsToLearn?: boolean; + isSelected?: boolean; + } ): Promise { const pool = getPool(); const client = await pool.connect(); @@ -363,6 +318,7 @@ export class EvaluationService { const userId = await this.upsertUser(profile); + // Upsert user_evaluation const userEvalResult = await client.query( ` INSERT INTO user_evaluations (user_id, last_updated) @@ -376,27 +332,39 @@ export class EvaluationService { const userEvaluationId = userEvalResult.rows[0].id; - const catEvalResult = await client.query( - ` - INSERT INTO category_evaluations (user_evaluation_id, category) - VALUES ($1, $2) - ON CONFLICT (user_evaluation_id, category) - DO UPDATE SET category = $2 - RETURNING id - `, - [userEvaluationId, category] - ); - - const categoryEvaluationId = catEvalResult.rows[0].id; - - // Update the specific property - const updateQuery = ` - UPDATE skill_evaluations - SET ${property} = $3, updated_at = CURRENT_TIMESTAMP - WHERE category_evaluation_id = $1 AND skill_id = $2 + // 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(updateQuery, [categoryEvaluationId, skillId, value]); + 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) { @@ -415,57 +383,10 @@ export class EvaluationService { category: string, skillId: string ): Promise { - const pool = getPool(); - const client = await pool.connect(); - - try { - await client.query("BEGIN"); - - const userId = await this.upsertUser(profile); - - 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; - - const catEvalResult = await client.query( - ` - INSERT INTO category_evaluations (user_evaluation_id, category) - VALUES ($1, $2) - ON CONFLICT (user_evaluation_id, category) - DO UPDATE SET category = $2 - RETURNING id - `, - [userEvaluationId, category] - ); - - const categoryEvaluationId = catEvalResult.rows[0].id; - - // Ajouter à selected_skills - await client.query( - ` - INSERT INTO selected_skills (category_evaluation_id, skill_id) - VALUES ($1, $2) - ON CONFLICT DO NOTHING - `, - [categoryEvaluationId, skillId] - ); - - await client.query("COMMIT"); - } catch (error) { - await client.query("ROLLBACK"); - throw error; - } finally { - client.release(); - } + await this.upsertSkillEvaluation(profile, skillId, { + isSelected: true, + level: "never" as SkillLevel, // Valeur par défaut pour une skill nouvellement sélectionnée + }); } /** @@ -482,36 +403,18 @@ export class EvaluationService { try { await client.query("BEGIN"); - // Trouver les IDs nécessaires - const findQuery = ` - SELECT ce.id as category_evaluation_id - FROM users u - JOIN user_evaluations ue ON u.id = ue.user_id - JOIN category_evaluations ce ON ue.id = ce.user_evaluation_id - WHERE u.first_name = $1 AND u.last_name = $2 AND u.team_id = $3 AND ce.category = $4 + 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 `; - const result = await client.query(findQuery, [ - profile.firstName, - profile.lastName, - profile.teamId, - category, - ]); - - if (result.rows.length > 0) { - const categoryEvaluationId = result.rows[0].category_evaluation_id; - - // Supprimer de selected_skills et skill_evaluations - await client.query( - "DELETE FROM selected_skills WHERE category_evaluation_id = $1 AND skill_id = $2", - [categoryEvaluationId, skillId] - ); - - await client.query( - "DELETE FROM skill_evaluations WHERE category_evaluation_id = $1 AND skill_id = $2", - [categoryEvaluationId, skillId] - ); - } + await client.query(deleteQuery, [userId, skillId]); await client.query("COMMIT"); } catch (error) {