feat: importance in db and mentorcard home colors
This commit is contained in:
37
app/api/admin/skills/[skillId]/importance/route.ts
Normal file
37
app/api/admin/skills/[skillId]/importance/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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`);
|
||||||
|
|||||||
89
components/admin/skills/importance-badge.tsx
Normal file
89
components/admin/skills/importance-badge.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
72
scripts/migrations/add_skill_importance.sql
Normal file
72
scripts/migrations/add_skill_importance.sql
Normal 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;
|
||||||
39
scripts/populate_skill_importance.sql
Normal file
39
scripts/populate_skill_importance.sql
Normal 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;
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user