feat: importance in db and mentorcard home colors

This commit is contained in:
Julien Froidefond
2025-08-27 11:51:43 +02:00
parent df1fd24e84
commit aee5d74445
13 changed files with 388 additions and 94 deletions

View File

@@ -0,0 +1,37 @@
import { NextRequest, NextResponse } from "next/server";
import { SkillsService } from "@/services/skills-service";
export async function PUT(
request: NextRequest,
{ params }: { params: { skillId: string } }
) {
try {
const { importance } = await request.json();
const { skillId } = params;
// Validation
if (
!importance ||
!["incontournable", "majeure", "standard"].includes(importance)
) {
return NextResponse.json(
{
error:
"Importance invalide. Doit être 'incontournable', 'majeure' ou 'standard'",
},
{ status: 400 }
);
}
// Mettre à jour l'importance
await SkillsService.updateSkillImportance(skillId, importance);
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error updating skill importance:", error);
return NextResponse.json(
{ error: "Erreur lors de la mise à jour de l'importance" },
{ status: 500 }
);
}
}

View File

@@ -8,6 +8,7 @@ export interface Skill {
categoryId: string; categoryId: string;
category: string; category: string;
usageCount: number; usageCount: number;
importance: string;
} }
export interface Team { export interface Team {
@@ -59,6 +60,13 @@ export class AdminClient extends BaseHttpClient {
await this.delete(`/admin/skills/categories/${categoryId}`); await this.delete(`/admin/skills/categories/${categoryId}`);
} }
async updateSkillImportance(
skillId: string,
importance: "incontournable" | "majeure" | "standard"
): Promise<void> {
await this.put(`/admin/skills/${skillId}/importance`, { importance });
}
// Teams Management // Teams Management
async getTeams(): Promise<Team[]> { async getTeams(): Promise<Team[]> {
return await this.get<Team[]>(`/admin/teams`); return await this.get<Team[]>(`/admin/teams`);

View File

@@ -0,0 +1,89 @@
"use client";
import { useState } from "react";
import { ChevronDown, Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { getImportanceColors } from "@/lib/tech-colors";
interface ImportanceBadgeProps {
importance: string;
onImportanceChange: (newImportance: string) => void;
disabled?: boolean;
}
export function ImportanceBadge({
importance,
onImportanceChange,
disabled = false,
}: ImportanceBadgeProps) {
const [isOpen, setIsOpen] = useState(false);
const colors = getImportanceColors(importance);
const handleImportanceChange = (newImportance: string) => {
onImportanceChange(newImportance);
setIsOpen(false);
};
const IMPORTANCE_LABELS = {
incontournable: "Incontournable",
majeure: "Majeure",
standard: "Standard",
};
return (
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className={`h-6 px-2 text-xs font-medium border ${colors.bg} ${
colors.text
} ${colors.border} hover:opacity-80 transition-all ${
disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
}`}
disabled={disabled}
>
<span
className={`w-2 h-2 rounded-full ${colors.text.replace(
"text-",
"bg-"
)}`}
/>
{IMPORTANCE_LABELS[importance as keyof typeof IMPORTANCE_LABELS] ||
importance}
{!disabled && <ChevronDown className="w-3 h-3 ml-1" />}
</Button>
</DropdownMenuTrigger>
{!disabled && (
<DropdownMenuContent align="end" className="w-40">
{Object.entries(IMPORTANCE_LABELS).map(([key, label]) => {
const itemColors = getImportanceColors(key);
return (
<DropdownMenuItem
key={key}
onClick={() => handleImportanceChange(key)}
className="flex items-center gap-2"
>
<span
className={`w-2 h-2 rounded-full ${itemColors.text.replace(
"text-",
"bg-"
)}`}
/>
{label}
{importance === key && <Check className="w-3 h-3 ml-auto" />}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
)}
</DropdownMenu>
);
}

View File

@@ -12,6 +12,7 @@ import {
import { TreeCategoryHeader, TreeItemRow } from "@/components/admin"; import { TreeCategoryHeader, TreeItemRow } from "@/components/admin";
import { TechIcon } from "@/components/icons/tech-icon"; import { TechIcon } from "@/components/icons/tech-icon";
import { Skill } from "@/clients/domains/admin-client"; import { Skill } from "@/clients/domains/admin-client";
import { ImportanceBadge } from "./importance-badge";
interface SkillsListProps { interface SkillsListProps {
filteredSkillsByCategory: Record<string, Skill[]>; filteredSkillsByCategory: Record<string, Skill[]>;
@@ -20,6 +21,7 @@ interface SkillsListProps {
onEditSkill: (skill: Skill) => void; onEditSkill: (skill: Skill) => void;
onDeleteSkill: (skillId: string) => void; onDeleteSkill: (skillId: string) => void;
onDeleteCategory?: (categoryName: string) => void; // Nouvelle prop onDeleteCategory?: (categoryName: string) => void; // Nouvelle prop
onImportanceChange?: (skillId: string, newImportance: string) => void;
} }
export function SkillsList({ export function SkillsList({
@@ -29,6 +31,7 @@ export function SkillsList({
onEditSkill, onEditSkill,
onDeleteSkill, onDeleteSkill,
onDeleteCategory, onDeleteCategory,
onImportanceChange,
}: SkillsListProps) { }: SkillsListProps) {
// Fonction pour obtenir l'icône de la catégorie // Fonction pour obtenir l'icône de la catégorie
const getCategoryIcon = (category: string) => { const getCategoryIcon = (category: string) => {
@@ -108,6 +111,14 @@ export function SkillsList({
onDelete={() => onDeleteSkill(skill.id)} onDelete={() => onDeleteSkill(skill.id)}
canDelete={skill.usageCount === 0} canDelete={skill.usageCount === 0}
showSeparator={skillIndex > 0} showSeparator={skillIndex > 0}
additionalInfo={
<ImportanceBadge
importance={skill.importance || "standard"}
onImportanceChange={(newImportance: string) =>
onImportanceChange?.(skill.id, newImportance)
}
/>
}
/> />
))} ))}
</div> </div>

View File

@@ -51,6 +51,7 @@ export function SkillsManagementPage({
handleEditSkill, handleEditSkill,
handleUpdateSkill, handleUpdateSkill,
handleDeleteSkill, handleDeleteSkill,
handleImportanceChange,
} = useSkillsManagement(skillCategories, initialSkills); } = useSkillsManagement(skillCategories, initialSkills);
// Utilisation du hook factorisé pour la vue arborescente // Utilisation du hook factorisé pour la vue arborescente
@@ -93,6 +94,13 @@ export function SkillsManagementPage({
openEditDialog(); openEditDialog();
}; };
const handleImportanceChangeWrapper = async (
skillId: string,
newImportance: string
) => {
await handleImportanceChange(skillId, newImportance);
};
const headerActions = ( const headerActions = (
<Button onClick={handleOpenCreateDialog}> <Button onClick={handleOpenCreateDialog}>
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
@@ -137,6 +145,7 @@ export function SkillsManagementPage({
onEditSkill={handleOpenEditDialog} onEditSkill={handleOpenEditDialog}
onDeleteSkill={handleDeleteSkill} onDeleteSkill={handleDeleteSkill}
onDeleteCategory={handleDeleteCategory} onDeleteCategory={handleDeleteCategory}
onImportanceChange={handleImportanceChangeWrapper}
/> />
</TreeViewPage> </TreeViewPage>

View File

@@ -1,5 +1,5 @@
import { TechIcon } from "@/components/icons/tech-icon"; import { TechIcon } from "@/components/icons/tech-icon";
import { getTechColors } from "@/lib/tech-colors"; import { getImportanceColors } from "@/lib/tech-colors";
import { UserEvaluation, SkillCategory } from "@/lib/types"; import { UserEvaluation, SkillCategory } from "@/lib/types";
interface MentorSectionProps { interface MentorSectionProps {
@@ -13,26 +13,6 @@ export function MentorSection({
userEvaluation, userEvaluation,
skillCategories, skillCategories,
}: MentorSectionProps) { }: MentorSectionProps) {
// Récupérer les compétences où l'utilisateur peut être mentor
const mentorSkills = userEvaluation.evaluations.flatMap((cat) => {
const skillCategory = skillCategories.find(
(sc) => sc.category === cat.category
);
return cat.skills
.filter((skill) => skill.canMentor)
.map((skill) => {
const skillInfo = skillCategory?.skills.find(
(s) => s.id === skill.skillId
);
return {
id: skill.skillId,
name: skillInfo?.name || skill.skillId,
icon: skillInfo?.icon || "fas-code",
level: skill.level,
};
});
});
// Récupérer les compétences maîtrisées (expert uniquement) // Récupérer les compétences maîtrisées (expert uniquement)
const masteredSkills = userEvaluation.evaluations.flatMap((cat) => { const masteredSkills = userEvaluation.evaluations.flatMap((cat) => {
const skillCategory = skillCategories.find( const skillCategory = skillCategories.find(
@@ -49,6 +29,28 @@ export function MentorSection({
name: skillInfo?.name || skill.skillId, name: skillInfo?.name || skill.skillId,
icon: skillInfo?.icon || "fas-code", icon: skillInfo?.icon || "fas-code",
level: skill.level, level: skill.level,
importance: skillInfo?.importance || "standard", // Récupérer l'importance depuis la base
};
});
});
// Récupérer les compétences où l'utilisateur peut être mentor
const mentorSkills = userEvaluation.evaluations.flatMap((cat) => {
const skillCategory = skillCategories.find(
(sc) => sc.category === cat.category
);
return cat.skills
.filter((skill) => skill.canMentor)
.map((skill) => {
const skillInfo = skillCategory?.skills.find(
(s) => s.id === skill.skillId
);
return {
id: skill.skillId,
name: skillInfo?.name || skill.skillId,
icon: skillInfo?.icon || "fas-code",
level: skill.level,
importance: skillInfo?.importance || "standard", // Récupérer l'importance depuis la base
}; };
}); });
}); });
@@ -69,26 +71,11 @@ export function MentorSection({
name: skillInfo?.name || skill.skillId, name: skillInfo?.name || skill.skillId,
icon: skillInfo?.icon || "fas-code", icon: skillInfo?.icon || "fas-code",
level: skill.level, level: skill.level,
importance: skillInfo?.importance || "standard", // Récupérer l'importance depuis la base
}; };
}); });
}); });
// Fonction pour déterminer la couleur d'une technologie
const getTechColor = (techName: string) => {
const lowerName = techName.toLowerCase();
if (lowerName.includes("react") || lowerName.includes("next"))
return "react";
if (lowerName.includes("typescript") || lowerName.includes("javascript"))
return "typescript";
if (lowerName.includes("node")) return "nodejs";
if (lowerName.includes("python")) return "python";
if (lowerName.includes("docker")) return "docker";
if (lowerName.includes("aws")) return "aws";
if (lowerName.includes("kubernetes")) return "kubernetes";
if (lowerName.includes("git")) return "default";
return "default";
};
return ( return (
<div <div
className={`bg-white/5 border border-white/10 rounded-xl p-6 backdrop-blur-sm ${className}`} className={`bg-white/5 border border-white/10 rounded-xl p-6 backdrop-blur-sm ${className}`}
@@ -103,7 +90,7 @@ export function MentorSection({
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{masteredSkills.length > 0 ? ( {masteredSkills.length > 0 ? (
masteredSkills.map((tech) => { masteredSkills.map((tech) => {
const colors = getTechColors(getTechColor(tech.name)); const colors = getImportanceColors(tech.importance);
return ( return (
<div <div
key={tech.id} key={tech.id}
@@ -138,7 +125,7 @@ export function MentorSection({
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{mentorSkills.length > 0 ? ( {mentorSkills.length > 0 ? (
mentorSkills.map((tech) => { mentorSkills.map((tech) => {
const colors = getTechColors(getTechColor(tech.name)); const colors = getImportanceColors(tech.importance);
return ( return (
<div <div
key={tech.id} key={tech.id}
@@ -172,7 +159,7 @@ export function MentorSection({
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{learningSkills.length > 0 ? ( {learningSkills.length > 0 ? (
learningSkills.map((tech) => { learningSkills.map((tech) => {
const colors = getTechColors(getTechColor(tech.name)); const colors = getImportanceColors(tech.importance);
return ( return (
<div <div
key={tech.id} key={tech.id}

View File

@@ -96,10 +96,12 @@ export function useSkillsManagement(
...skillFormData, ...skillFormData,
category: category.category, category: category.category,
categoryId: category.id, categoryId: category.id,
importance: "standard", // Valeur par défaut
}; };
const newSkill = await adminClient.createSkill(skillData); const newSkill = await adminClient.createSkill(skillData);
setSkills([...skills, newSkill]);
setSkills((prevSkills) => [...prevSkills, newSkill]);
resetForm(); resetForm();
toast({ toast({
@@ -152,6 +154,7 @@ export function useSkillsManagement(
...skillFormData, ...skillFormData,
category: category.category, category: category.category,
usageCount: editingSkill.usageCount, usageCount: editingSkill.usageCount,
importance: editingSkill.importance || "standard", // Garder l'importance existante
}; };
const updatedSkill = await adminClient.updateSkill(skillData); const updatedSkill = await adminClient.updateSkill(skillData);
@@ -207,6 +210,36 @@ export function useSkillsManagement(
} }
}; };
const handleImportanceChange = async (
skillId: string,
newImportance: string
) => {
try {
await adminClient.updateSkillImportance(skillId, newImportance as any);
// Mettre à jour l'état local
setSkills((prevSkills) =>
prevSkills.map((skill) =>
skill.id === skillId ? { ...skill, importance: newImportance } : skill
)
);
toast({
title: "Succès",
description: "Importance mise à jour avec succès",
});
return true;
} catch (error: any) {
toast({
title: "Erreur",
description:
error.message || "Erreur lors de la mise à jour de l'importance",
variant: "destructive",
});
return false;
}
};
return { return {
skills, skills,
isLoading, isLoading,
@@ -219,5 +252,6 @@ export function useSkillsManagement(
handleEditSkill, handleEditSkill,
handleUpdateSkill, handleUpdateSkill,
handleDeleteSkill, handleDeleteSkill,
handleImportanceChange,
}; };
} }

View File

@@ -1,61 +1,36 @@
// Couleurs officielle des technologies // Couleurs basées sur l'importance des skills
export const TECH_COLORS: Record< export const IMPORTANCE_COLORS: Record<
string, string,
{ bg: string; text: string; border: string } { bg: string; text: string; border: string }
> = { > = {
react: { incontournable: {
bg: "bg-blue-500/10", bg: "bg-red-500/20",
text: "text-blue-600 dark:text-blue-400", text: "text-red-400 dark:text-red-300",
border: "border-blue-500/20", border: "border-red-500/30",
}, },
vue: { majeure: {
bg: "bg-green-500/10", bg: "bg-blue-500/20",
text: "text-green-600 dark:text-green-400", text: "text-blue-400 dark:text-blue-300",
border: "border-green-500/20", border: "border-blue-500/30",
}, },
typescript: { standard: {
bg: "bg-blue-500/10", bg: "bg-slate-500/20",
text: "text-blue-600 dark:text-blue-400", text: "text-slate-400 dark:text-slate-300",
border: "border-blue-500/20", border: "border-slate-500/30",
},
nextjs: {
bg: "bg-gray-500/10",
text: "text-gray-900 dark:text-gray-100",
border: "border-gray-500/20",
},
nodejs: {
bg: "bg-green-500/10",
text: "text-green-600 dark:text-green-400",
border: "border-green-500/20",
},
python: {
bg: "bg-yellow-500/10",
text: "text-yellow-600 dark:text-yellow-400",
border: "border-yellow-500/20",
},
docker: {
bg: "bg-blue-500/10",
text: "text-blue-600 dark:text-blue-400",
border: "border-blue-500/20",
},
kubernetes: {
bg: "bg-indigo-500/10",
text: "text-indigo-600 dark:text-indigo-400",
border: "border-indigo-500/20",
},
aws: {
bg: "bg-orange-500/10",
text: "text-orange-600 dark:text-orange-400",
border: "border-orange-500/20",
},
// Couleur par défaut
default: {
bg: "bg-muted",
text: "text-foreground",
border: "border-muted",
}, },
}; };
export function getTechColors(techId: string) { // Fonction principale pour récupérer les couleurs basées sur l'importance
return TECH_COLORS[techId] || TECH_COLORS.default; export function getImportanceColors(importance: string) {
return IMPORTANCE_COLORS[importance] || IMPORTANCE_COLORS.standard;
}
// Fonction legacy pour la compatibilité (utilise seulement l'importance)
export function getTechColors(importance: string) {
return getImportanceColors(importance);
}
// Fonction pour obtenir les couleurs d'importance uniquement
export function getSmartColors(importance: string) {
return getImportanceColors(importance);
} }

View File

@@ -25,6 +25,7 @@ export interface Skill {
description: string; description: string;
icon?: string; icon?: string;
links: string[]; links: string[];
importance?: string;
} }
export interface SkillCategory { export interface SkillCategory {

View File

@@ -29,6 +29,7 @@ CREATE TABLE skills (
description TEXT, description TEXT,
icon VARCHAR(100), icon VARCHAR(100),
category_id VARCHAR(50) REFERENCES skill_categories(id), category_id VARCHAR(50) REFERENCES skill_categories(id),
importance VARCHAR(20) DEFAULT 'standard' CHECK (importance IN ('incontournable', 'majeure', 'standard')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
@@ -88,6 +89,7 @@ CREATE TABLE skill_evaluations (
-- Indexes for performance -- Indexes for performance
CREATE INDEX idx_teams_direction ON teams(direction); CREATE INDEX idx_teams_direction ON teams(direction);
CREATE INDEX idx_skills_category_id ON skills(category_id); CREATE INDEX idx_skills_category_id ON skills(category_id);
CREATE INDEX idx_skills_importance ON skills(importance);
CREATE INDEX idx_skill_links_skill_id ON skill_links(skill_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_team_id ON users(team_id);
CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_email ON users(email);

View File

@@ -0,0 +1,72 @@
-- Migration pour ajouter la colonne importance aux skills
-- Compatible avec les bases existantes
-- Étape 1: Vérifier si la colonne existe déjà
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'skills' AND column_name = 'importance'
) THEN
-- Ajouter la colonne importance
ALTER TABLE skills
ADD COLUMN importance VARCHAR(20) DEFAULT 'standard';
-- Ajouter la contrainte CHECK
ALTER TABLE skills
ADD CONSTRAINT skills_importance_check
CHECK (importance IN ('incontournable', 'majeure', 'standard'));
RAISE NOTICE 'Colonne importance ajoutée avec succès';
ELSE
RAISE NOTICE 'Colonne importance existe déjà';
END IF;
END $$;
-- Étape 2: Mettre à jour les technologies incontournables
UPDATE skills
SET importance = 'incontournable'
WHERE name IN (
'React', 'TypeScript', 'JavaScript', 'Git', 'HTML', 'CSS',
'Node.js', 'Python', 'SQL', 'HTTP', 'REST API', 'JSON',
'GitHub', 'npm', 'yarn', 'package.json'
);
-- Étape 3: Mettre à jour les technologies majeures
UPDATE skills
SET importance = 'majeure'
WHERE name IN (
'Next.js', 'Vue.js', 'Angular', 'Docker', 'Kubernetes',
'AWS', 'Azure', 'Google Cloud', 'MongoDB', 'PostgreSQL',
'Redis', 'GraphQL', 'Webpack', 'Vite', 'Jest', 'Cypress',
'Tailwind CSS', 'Sass', 'Less', 'Redux', 'Zustand',
'Prisma', 'TypeORM', 'Sequelize', 'Express.js', 'Fastify'
);
-- Étape 4: Créer l'index s'il n'existe pas
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = 'skills' AND indexname = 'idx_skills_importance'
) THEN
CREATE INDEX idx_skills_importance ON skills(importance);
RAISE NOTICE 'Index idx_skills_importance créé avec succès';
ELSE
RAISE NOTICE 'Index idx_skills_importance existe déjà';
END IF;
END $$;
-- Étape 5: Vérification finale
SELECT
importance,
COUNT(*) as count,
STRING_AGG(name, ', ' ORDER BY name) as examples
FROM skills
GROUP BY importance
ORDER BY
CASE importance
WHEN 'incontournable' THEN 1
WHEN 'majeure' THEN 2
WHEN 'standard' THEN 3
END;

View File

@@ -0,0 +1,39 @@
-- Script pour peupler la colonne importance des skills
-- À exécuter après avoir ajouté la colonne importance
-- Technologies incontournables (fondamentales)
UPDATE skills
SET importance = 'incontournable'
WHERE name IN (
'React', 'TypeScript', 'JavaScript', 'Git', 'HTML', 'CSS',
'Node.js', 'Python', 'SQL', 'HTTP', 'REST API', 'JSON',
'GitHub', 'npm', 'yarn', 'package.json'
);
-- Technologies majeures (importantes mais pas fondamentales)
UPDATE skills
SET importance = 'majeure'
WHERE name IN (
'Next.js', 'Vue.js', 'Angular', 'Docker', 'Kubernetes',
'AWS', 'Azure', 'Google Cloud', 'MongoDB', 'PostgreSQL',
'Redis', 'GraphQL', 'Webpack', 'Vite', 'Jest', 'Cypress',
'Tailwind CSS', 'Sass', 'Less', 'Redux', 'Zustand',
'Prisma', 'TypeORM', 'Sequelize', 'Express.js', 'Fastify'
);
-- Le reste reste en 'standard' (valeur par défaut)
-- Ces technologies sont importantes dans leur domaine mais pas critiques globalement
-- Vérification des mises à jour
SELECT
importance,
COUNT(*) as count,
STRING_AGG(name, ', ' ORDER BY name) as examples
FROM skills
GROUP BY importance
ORDER BY
CASE importance
WHEN 'incontournable' THEN 1
WHEN 'majeure' THEN 2
WHEN 'standard' THEN 3
END;

View File

@@ -20,6 +20,7 @@ export class SkillsService {
'name', s.name, 'name', s.name,
'description', s.description, 'description', s.description,
'icon', s.icon, 'icon', s.icon,
'importance', s.importance,
'links', COALESCE( 'links', COALESCE(
(SELECT json_agg(sl.url ORDER BY sl.id) (SELECT json_agg(sl.url ORDER BY sl.id)
FROM skill_links sl FROM skill_links sl
@@ -249,6 +250,7 @@ export class SkillsService {
categoryId: string; categoryId: string;
category: string; category: string;
usageCount: number; usageCount: number;
importance: string;
}> }>
> { > {
const pool = getPool(); const pool = getPool();
@@ -260,11 +262,12 @@ export class SkillsService {
s.icon, s.icon,
sc.id as category_id, sc.id as category_id,
sc.name as category_name, sc.name as category_name,
s.importance,
COUNT(DISTINCT se.id) as usage_count COUNT(DISTINCT se.id) as usage_count
FROM skills s FROM skills s
LEFT JOIN skill_categories sc ON s.category_id = sc.id 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 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 GROUP BY s.id, s.name, s.description, s.icon, sc.id, sc.name, s.importance
ORDER BY s.name ORDER BY s.name
`; `;
@@ -279,6 +282,7 @@ export class SkillsService {
categoryId: row.category_id, categoryId: row.category_id,
category: row.category_name, category: row.category_name,
usageCount: parseInt(row.usage_count) || 0, usageCount: parseInt(row.usage_count) || 0,
importance: row.importance || "standard",
})); }));
} catch (error) { } catch (error) {
console.error("Error fetching skills with usage:", error); console.error("Error fetching skills with usage:", error);
@@ -286,6 +290,32 @@ export class SkillsService {
} }
} }
/**
* Update skill importance
*/
static async updateSkillImportance(
skillId: string,
importance: "incontournable" | "majeure" | "standard"
): Promise<void> {
const pool = getPool();
const query = `
UPDATE skills
SET importance = $1, updated_at = CURRENT_TIMESTAMP
WHERE id = $2
`;
try {
const result = await pool.query(query, [importance, skillId]);
if (result.rowCount === 0) {
throw new Error("Skill not found");
}
} catch (error) {
console.error("Error updating skill importance:", error);
throw new Error("Failed to update skill importance");
}
}
/** /**
* Create a new skill for admin * Create a new skill for admin
*/ */