feat: add multiple skills addition and optimize evaluation handling
- Introduced `addMultipleSkillsToEvaluation` function in `EvaluationClientWrapper` for batch skill addition. - Updated `SkillEvaluation` and `SkillSelector` components to utilize the new multiple skills addition feature. - Implemented optimistic UI updates for skill level, mentor status, and learning status changes, enhancing user experience. - Refactored evaluation state management to improve performance and maintainability. - Added error handling and rollback mechanisms for better reliability during API interactions.
This commit is contained in:
@@ -1,9 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useUser } from "@/hooks/use-user-context";
|
import { useUser } from "@/hooks/use-user-context";
|
||||||
import { UserEvaluation, Team } from "@/lib/types";
|
import { UserEvaluation, Team, SkillLevel } from "@/lib/types";
|
||||||
import {
|
import {
|
||||||
updateSkillLevel as updateSkillLevelAction,
|
updateSkillLevel as updateSkillLevelAction,
|
||||||
updateSkillMentorStatus as updateSkillMentorStatusAction,
|
updateSkillMentorStatus as updateSkillMentorStatusAction,
|
||||||
@@ -24,16 +23,68 @@ export function EvaluationClientWrapper({
|
|||||||
children,
|
children,
|
||||||
}: EvaluationClientWrapperProps) {
|
}: EvaluationClientWrapperProps) {
|
||||||
const { setUserInfo } = useUser();
|
const { setUserInfo } = useUser();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// Wrapper functions that refresh the page after API calls
|
// État local pour l'UI optimiste - commence avec les données SSR
|
||||||
|
const [currentEvaluation, setCurrentEvaluation] =
|
||||||
|
useState<UserEvaluation | null>(userEvaluation);
|
||||||
|
|
||||||
|
// Met à jour l'état local quand les props changent (SSR)
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentEvaluation(userEvaluation);
|
||||||
|
}, [userEvaluation]);
|
||||||
|
|
||||||
|
// Fonctions avec UI optimiste
|
||||||
const updateSkillLevel = async (
|
const updateSkillLevel = async (
|
||||||
category: string,
|
category: string,
|
||||||
skillId: string,
|
skillId: string,
|
||||||
level: any
|
level: SkillLevel
|
||||||
) => {
|
) => {
|
||||||
await updateSkillLevelAction(category, skillId, level);
|
if (!currentEvaluation) return;
|
||||||
router.refresh();
|
|
||||||
|
// Sauvegarder l'état actuel pour le rollback
|
||||||
|
const previousEvaluation = currentEvaluation;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Optimistic UI update - mettre à jour immédiatement l'interface
|
||||||
|
const updatedEvaluations = currentEvaluation.evaluations.map(
|
||||||
|
(catEval) => {
|
||||||
|
if (catEval.category === category) {
|
||||||
|
const existingSkill = catEval.skills.find(
|
||||||
|
(s) => s.skillId === skillId
|
||||||
|
);
|
||||||
|
const updatedSkills = existingSkill
|
||||||
|
? catEval.skills.map((skill) =>
|
||||||
|
skill.skillId === skillId ? { ...skill, level } : skill
|
||||||
|
)
|
||||||
|
: [
|
||||||
|
...catEval.skills,
|
||||||
|
{ skillId, level, canMentor: false, wantsToLearn: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...catEval,
|
||||||
|
skills: updatedSkills,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return catEval;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const newEvaluation: UserEvaluation = {
|
||||||
|
...currentEvaluation,
|
||||||
|
evaluations: updatedEvaluations,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setCurrentEvaluation(newEvaluation);
|
||||||
|
|
||||||
|
// Appel API en arrière-plan
|
||||||
|
await updateSkillLevelAction(category, skillId, level);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update skill level:", error);
|
||||||
|
// Rollback optimiste en cas d'erreur
|
||||||
|
setCurrentEvaluation(previousEvaluation);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSkillMentorStatus = async (
|
const updateSkillMentorStatus = async (
|
||||||
@@ -41,8 +92,39 @@ export function EvaluationClientWrapper({
|
|||||||
skillId: string,
|
skillId: string,
|
||||||
canMentor: boolean
|
canMentor: boolean
|
||||||
) => {
|
) => {
|
||||||
await updateSkillMentorStatusAction(category, skillId, canMentor);
|
if (!currentEvaluation) return;
|
||||||
router.refresh();
|
|
||||||
|
const previousEvaluation = currentEvaluation;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedEvaluations = currentEvaluation.evaluations.map(
|
||||||
|
(catEval) => {
|
||||||
|
if (catEval.category === category) {
|
||||||
|
const updatedSkills = catEval.skills.map((skill) =>
|
||||||
|
skill.skillId === skillId ? { ...skill, canMentor } : skill
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...catEval,
|
||||||
|
skills: updatedSkills,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return catEval;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const newEvaluation: UserEvaluation = {
|
||||||
|
...currentEvaluation,
|
||||||
|
evaluations: updatedEvaluations,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setCurrentEvaluation(newEvaluation);
|
||||||
|
await updateSkillMentorStatusAction(category, skillId, canMentor);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update skill mentor status:", error);
|
||||||
|
setCurrentEvaluation(previousEvaluation);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSkillLearningStatus = async (
|
const updateSkillLearningStatus = async (
|
||||||
@@ -50,45 +132,204 @@ export function EvaluationClientWrapper({
|
|||||||
skillId: string,
|
skillId: string,
|
||||||
wantsToLearn: boolean
|
wantsToLearn: boolean
|
||||||
) => {
|
) => {
|
||||||
await updateSkillLearningStatusAction(category, skillId, wantsToLearn);
|
if (!currentEvaluation) return;
|
||||||
router.refresh();
|
|
||||||
|
const previousEvaluation = currentEvaluation;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedEvaluations = currentEvaluation.evaluations.map(
|
||||||
|
(catEval) => {
|
||||||
|
if (catEval.category === category) {
|
||||||
|
const updatedSkills = catEval.skills.map((skill) =>
|
||||||
|
skill.skillId === skillId ? { ...skill, wantsToLearn } : skill
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...catEval,
|
||||||
|
skills: updatedSkills,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return catEval;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const newEvaluation: UserEvaluation = {
|
||||||
|
...currentEvaluation,
|
||||||
|
evaluations: updatedEvaluations,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setCurrentEvaluation(newEvaluation);
|
||||||
|
await updateSkillLearningStatusAction(category, skillId, wantsToLearn);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update skill learning status:", error);
|
||||||
|
setCurrentEvaluation(previousEvaluation);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addSkillToEvaluation = async (category: string, skillId: string) => {
|
const addSkillToEvaluation = async (category: string, skillId: string) => {
|
||||||
await addSkillToEvaluationAction(category, skillId);
|
if (!currentEvaluation) return;
|
||||||
router.refresh();
|
|
||||||
|
const previousEvaluation = currentEvaluation;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedEvaluations = currentEvaluation.evaluations.map(
|
||||||
|
(catEval) => {
|
||||||
|
if (catEval.category === category) {
|
||||||
|
if (!catEval.selectedSkillIds.includes(skillId)) {
|
||||||
|
return {
|
||||||
|
...catEval,
|
||||||
|
selectedSkillIds: [...catEval.selectedSkillIds, skillId],
|
||||||
|
skills: [
|
||||||
|
...catEval.skills,
|
||||||
|
{
|
||||||
|
skillId,
|
||||||
|
level: null,
|
||||||
|
canMentor: false,
|
||||||
|
wantsToLearn: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return catEval;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const newEvaluation: UserEvaluation = {
|
||||||
|
...currentEvaluation,
|
||||||
|
evaluations: updatedEvaluations,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setCurrentEvaluation(newEvaluation);
|
||||||
|
await addSkillToEvaluationAction(category, skillId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to add skill to evaluation:", error);
|
||||||
|
setCurrentEvaluation(previousEvaluation);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMultipleSkillsToEvaluation = async (
|
||||||
|
category: string,
|
||||||
|
skillIds: string[]
|
||||||
|
) => {
|
||||||
|
if (!currentEvaluation || skillIds.length === 0) return;
|
||||||
|
|
||||||
|
const previousEvaluation = currentEvaluation;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedEvaluations = currentEvaluation.evaluations.map(
|
||||||
|
(catEval) => {
|
||||||
|
if (catEval.category === category) {
|
||||||
|
// Filtrer seulement les compétences qui ne sont pas déjà sélectionnées
|
||||||
|
const newSkillIds = skillIds.filter(
|
||||||
|
(skillId) => !catEval.selectedSkillIds.includes(skillId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newSkillIds.length > 0) {
|
||||||
|
return {
|
||||||
|
...catEval,
|
||||||
|
selectedSkillIds: [...catEval.selectedSkillIds, ...newSkillIds],
|
||||||
|
skills: [
|
||||||
|
...catEval.skills,
|
||||||
|
...newSkillIds.map((skillId) => ({
|
||||||
|
skillId,
|
||||||
|
level: null,
|
||||||
|
canMentor: false,
|
||||||
|
wantsToLearn: false,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return catEval;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const newEvaluation: UserEvaluation = {
|
||||||
|
...currentEvaluation,
|
||||||
|
evaluations: updatedEvaluations,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setCurrentEvaluation(newEvaluation);
|
||||||
|
|
||||||
|
// Ajouter toutes les compétences en parallèle côté API
|
||||||
|
await Promise.all(
|
||||||
|
skillIds.map((skillId) => addSkillToEvaluationAction(category, skillId))
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to add multiple skills to evaluation:", error);
|
||||||
|
setCurrentEvaluation(previousEvaluation);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeSkillFromEvaluation = async (
|
const removeSkillFromEvaluation = async (
|
||||||
category: string,
|
category: string,
|
||||||
skillId: string
|
skillId: string
|
||||||
) => {
|
) => {
|
||||||
await removeSkillFromEvaluationAction(category, skillId);
|
if (!currentEvaluation) return;
|
||||||
router.refresh();
|
|
||||||
|
const previousEvaluation = currentEvaluation;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedEvaluations = currentEvaluation.evaluations.map(
|
||||||
|
(catEval) => {
|
||||||
|
if (catEval.category === category) {
|
||||||
|
return {
|
||||||
|
...catEval,
|
||||||
|
selectedSkillIds: catEval.selectedSkillIds.filter(
|
||||||
|
(id) => id !== skillId
|
||||||
|
),
|
||||||
|
skills: catEval.skills.filter(
|
||||||
|
(skill) => skill.skillId !== skillId
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return catEval;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const newEvaluation: UserEvaluation = {
|
||||||
|
...currentEvaluation,
|
||||||
|
evaluations: updatedEvaluations,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setCurrentEvaluation(newEvaluation);
|
||||||
|
await removeSkillFromEvaluationAction(category, skillId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to remove skill from evaluation:", error);
|
||||||
|
setCurrentEvaluation(previousEvaluation);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update user info in navigation when user evaluation is loaded
|
// Update user info in navigation when user evaluation is loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userEvaluation) {
|
if (currentEvaluation) {
|
||||||
const teamName =
|
const teamName =
|
||||||
teams.find((t) => t.id === userEvaluation.profile.teamId)?.name || "";
|
teams.find((t) => t.id === currentEvaluation.profile.teamId)?.name ||
|
||||||
|
"";
|
||||||
setUserInfo({
|
setUserInfo({
|
||||||
firstName: userEvaluation.profile.firstName,
|
firstName: currentEvaluation.profile.firstName,
|
||||||
lastName: userEvaluation.profile.lastName,
|
lastName: currentEvaluation.profile.lastName,
|
||||||
teamName,
|
teamName,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setUserInfo(null);
|
setUserInfo(null);
|
||||||
}
|
}
|
||||||
}, [userEvaluation, teams, setUserInfo]);
|
}, [currentEvaluation, teams, setUserInfo]);
|
||||||
|
|
||||||
// Provide evaluation functions to children through React context or props
|
// Provide evaluation functions to children through React context or props
|
||||||
return (
|
return (
|
||||||
<EvaluationProvider
|
<EvaluationProvider
|
||||||
|
currentEvaluation={currentEvaluation}
|
||||||
updateSkillLevel={updateSkillLevel}
|
updateSkillLevel={updateSkillLevel}
|
||||||
updateSkillMentorStatus={updateSkillMentorStatus}
|
updateSkillMentorStatus={updateSkillMentorStatus}
|
||||||
updateSkillLearningStatus={updateSkillLearningStatus}
|
updateSkillLearningStatus={updateSkillLearningStatus}
|
||||||
addSkillToEvaluation={addSkillToEvaluation}
|
addSkillToEvaluation={addSkillToEvaluation}
|
||||||
|
addMultipleSkillsToEvaluation={addMultipleSkillsToEvaluation}
|
||||||
removeSkillFromEvaluation={removeSkillFromEvaluation}
|
removeSkillFromEvaluation={removeSkillFromEvaluation}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -100,6 +341,7 @@ export function EvaluationClientWrapper({
|
|||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
interface EvaluationContextType {
|
interface EvaluationContextType {
|
||||||
|
currentEvaluation: UserEvaluation | null;
|
||||||
updateSkillLevel: (categoryId: string, skillId: string, level: any) => void;
|
updateSkillLevel: (categoryId: string, skillId: string, level: any) => void;
|
||||||
updateSkillMentorStatus: (
|
updateSkillMentorStatus: (
|
||||||
categoryId: string,
|
categoryId: string,
|
||||||
@@ -112,6 +354,10 @@ interface EvaluationContextType {
|
|||||||
wantsToLearn: boolean
|
wantsToLearn: boolean
|
||||||
) => void;
|
) => void;
|
||||||
addSkillToEvaluation: (categoryId: string, skillId: string) => void;
|
addSkillToEvaluation: (categoryId: string, skillId: string) => void;
|
||||||
|
addMultipleSkillsToEvaluation: (
|
||||||
|
categoryId: string,
|
||||||
|
skillIds: string[]
|
||||||
|
) => void;
|
||||||
removeSkillFromEvaluation: (categoryId: string, skillId: string) => void;
|
removeSkillFromEvaluation: (categoryId: string, skillId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,10 +367,12 @@ const EvaluationContext = createContext<EvaluationContextType | undefined>(
|
|||||||
|
|
||||||
function EvaluationProvider({
|
function EvaluationProvider({
|
||||||
children,
|
children,
|
||||||
|
currentEvaluation,
|
||||||
updateSkillLevel,
|
updateSkillLevel,
|
||||||
updateSkillMentorStatus,
|
updateSkillMentorStatus,
|
||||||
updateSkillLearningStatus,
|
updateSkillLearningStatus,
|
||||||
addSkillToEvaluation,
|
addSkillToEvaluation,
|
||||||
|
addMultipleSkillsToEvaluation,
|
||||||
removeSkillFromEvaluation,
|
removeSkillFromEvaluation,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -132,10 +380,12 @@ function EvaluationProvider({
|
|||||||
return (
|
return (
|
||||||
<EvaluationContext.Provider
|
<EvaluationContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
currentEvaluation,
|
||||||
updateSkillLevel,
|
updateSkillLevel,
|
||||||
updateSkillMentorStatus,
|
updateSkillMentorStatus,
|
||||||
updateSkillLearningStatus,
|
updateSkillLearningStatus,
|
||||||
addSkillToEvaluation,
|
addSkillToEvaluation,
|
||||||
|
addMultipleSkillsToEvaluation,
|
||||||
removeSkillFromEvaluation,
|
removeSkillFromEvaluation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -23,12 +23,17 @@ export function SkillEvaluation({
|
|||||||
evaluations,
|
evaluations,
|
||||||
}: SkillEvaluationProps) {
|
}: SkillEvaluationProps) {
|
||||||
const {
|
const {
|
||||||
|
currentEvaluation: contextEvaluation,
|
||||||
updateSkillLevel,
|
updateSkillLevel,
|
||||||
updateSkillMentorStatus,
|
updateSkillMentorStatus,
|
||||||
updateSkillLearningStatus,
|
updateSkillLearningStatus,
|
||||||
addSkillToEvaluation,
|
addSkillToEvaluation,
|
||||||
|
addMultipleSkillsToEvaluation,
|
||||||
removeSkillFromEvaluation,
|
removeSkillFromEvaluation,
|
||||||
} = useEvaluationContext();
|
} = useEvaluationContext();
|
||||||
|
|
||||||
|
// Utiliser l'évaluation du contexte (avec état optimiste) ou celle des props (SSR)
|
||||||
|
const activeEvaluations = contextEvaluation?.evaluations || evaluations;
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const categoryParam = searchParams.get("category");
|
const categoryParam = searchParams.get("category");
|
||||||
@@ -57,7 +62,7 @@ export function SkillEvaluation({
|
|||||||
const currentCategory = categories.find(
|
const currentCategory = categories.find(
|
||||||
(cat) => cat.category === selectedCategory
|
(cat) => cat.category === selectedCategory
|
||||||
);
|
);
|
||||||
const currentEvaluation = evaluations.find(
|
const currentEvaluation = activeEvaluations.find(
|
||||||
(evaluation) => evaluation.category === selectedCategory
|
(evaluation) => evaluation.category === selectedCategory
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -85,9 +90,10 @@ export function SkillEvaluation({
|
|||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<SkillSelector
|
<SkillSelector
|
||||||
categories={categories}
|
categories={categories}
|
||||||
evaluations={evaluations}
|
evaluations={activeEvaluations}
|
||||||
selectedCategory={selectedCategory}
|
selectedCategory={selectedCategory}
|
||||||
onAddSkill={addSkillToEvaluation}
|
onAddSkill={addSkillToEvaluation}
|
||||||
|
onAddMultipleSkills={addMultipleSkillsToEvaluation}
|
||||||
onRemoveSkill={removeSkillFromEvaluation}
|
onRemoveSkill={removeSkillFromEvaluation}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface SkillSelectorProps {
|
|||||||
evaluations: CategoryEvaluation[];
|
evaluations: CategoryEvaluation[];
|
||||||
selectedCategory: string;
|
selectedCategory: string;
|
||||||
onAddSkill: (category: string, skillId: string) => void;
|
onAddSkill: (category: string, skillId: string) => void;
|
||||||
|
onAddMultipleSkills?: (category: string, skillIds: string[]) => void;
|
||||||
onRemoveSkill: (category: string, skillId: string) => void;
|
onRemoveSkill: (category: string, skillId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,11 +30,14 @@ export function SkillSelector({
|
|||||||
evaluations,
|
evaluations,
|
||||||
selectedCategory,
|
selectedCategory,
|
||||||
onAddSkill,
|
onAddSkill,
|
||||||
|
onAddMultipleSkills,
|
||||||
onRemoveSkill,
|
onRemoveSkill,
|
||||||
}: SkillSelectorProps) {
|
}: SkillSelectorProps) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [isAddingAll, setIsAddingAll] = useState(false);
|
||||||
|
const [addingSkillIds, setAddingSkillIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const currentCategory = categories.find(
|
const currentCategory = categories.find(
|
||||||
(cat) => cat.category === selectedCategory
|
(cat) => cat.category === selectedCategory
|
||||||
@@ -60,6 +64,47 @@ export function SkillSelector({
|
|||||||
selectedSkillIds.includes(skill.id)
|
selectedSkillIds.includes(skill.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleAddSkill = async (skillId: string) => {
|
||||||
|
setAddingSkillIds((prev) => new Set(prev).add(skillId));
|
||||||
|
try {
|
||||||
|
await onAddSkill(selectedCategory, skillId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to add skill:", error);
|
||||||
|
} finally {
|
||||||
|
setAddingSkillIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(skillId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddAllSkills = async () => {
|
||||||
|
setIsAddingAll(true);
|
||||||
|
try {
|
||||||
|
const skillIds = availableSkills.map((skill) => skill.id);
|
||||||
|
|
||||||
|
// Utiliser la fonction d'ajout multiple si disponible, sinon fallback sur l'ajout individuel
|
||||||
|
if (onAddMultipleSkills) {
|
||||||
|
await onAddMultipleSkills(selectedCategory, skillIds);
|
||||||
|
} else {
|
||||||
|
// Fallback: ajouter en séquentiel pour éviter les conflits d'état
|
||||||
|
for (const skill of availableSkills) {
|
||||||
|
await onAddSkill(selectedCategory, skill.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Petit délai pour permettre à l'UI de se mettre à jour avant de fermer
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}, 100);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to add all skills:", error);
|
||||||
|
} finally {
|
||||||
|
setIsAddingAll(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Selected Skills */}
|
{/* Selected Skills */}
|
||||||
@@ -88,15 +133,30 @@ export function SkillSelector({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Search */}
|
{/* Search and Add All */}
|
||||||
<div className="relative">
|
<div className="space-y-3">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<div className="relative">
|
||||||
<Input
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
placeholder="Rechercher une compétence..."
|
<Input
|
||||||
value={searchTerm}
|
placeholder="Rechercher une compétence..."
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
value={searchTerm}
|
||||||
className="pl-10"
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
/>
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{availableSkills.length > 1 && (
|
||||||
|
<Button
|
||||||
|
onClick={handleAddAllSkills}
|
||||||
|
disabled={isAddingAll}
|
||||||
|
className="w-full gap-2 bg-green-600 hover:bg-green-700 text-white disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
{isAddingAll
|
||||||
|
? "Ajout en cours..."
|
||||||
|
: `Ajouter toutes les compétences (${availableSkills.length})`}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Create New Skill Button */}
|
{/* Create New Skill Button */}
|
||||||
@@ -159,13 +219,16 @@ export function SkillSelector({
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => handleAddSkill(skill.id)}
|
||||||
onAddSkill(selectedCategory, skill.id);
|
disabled={
|
||||||
}}
|
addingSkillIds.has(skill.id) || isAddingAll
|
||||||
className="gap-2"
|
}
|
||||||
|
className="gap-2 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
Ajouter
|
{addingSkillIds.has(skill.id)
|
||||||
|
? "Ajout..."
|
||||||
|
: "Ajouter"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -296,14 +296,21 @@ export function useEvaluation() {
|
|||||||
const addSkillToEvaluation = async (category: string, skillId: string) => {
|
const addSkillToEvaluation = async (category: string, skillId: string) => {
|
||||||
if (!userEvaluation) return;
|
if (!userEvaluation) return;
|
||||||
|
|
||||||
|
// Sauvegarder l'état actuel pour le rollback
|
||||||
|
const previousEvaluation = userEvaluation;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Optimistic UI update - mettre à jour immédiatement l'interface
|
||||||
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
|
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
|
||||||
if (catEval.category === category) {
|
if (catEval.category === category) {
|
||||||
if (!catEval.selectedSkillIds.includes(skillId)) {
|
if (!catEval.selectedSkillIds.includes(skillId)) {
|
||||||
return {
|
return {
|
||||||
...catEval,
|
...catEval,
|
||||||
selectedSkillIds: [...catEval.selectedSkillIds, skillId],
|
selectedSkillIds: [...catEval.selectedSkillIds, skillId],
|
||||||
skills: [...catEval.skills, { skillId, level: null }],
|
skills: [
|
||||||
|
...catEval.skills,
|
||||||
|
{ skillId, level: null, canMentor: false, wantsToLearn: false },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,6 +324,8 @@ export function useEvaluation() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
setUserEvaluation(newEvaluation);
|
setUserEvaluation(newEvaluation);
|
||||||
|
|
||||||
|
// Appel API en arrière-plan
|
||||||
await apiClient.addSkillToEvaluation(
|
await apiClient.addSkillToEvaluation(
|
||||||
userEvaluation.profile,
|
userEvaluation.profile,
|
||||||
category,
|
category,
|
||||||
@@ -324,6 +333,9 @@ export function useEvaluation() {
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to add skill to evaluation:", error);
|
console.error("Failed to add skill to evaluation:", error);
|
||||||
|
// Rollback optimiste en cas d'erreur
|
||||||
|
setUserEvaluation(previousEvaluation);
|
||||||
|
// Optionnel: afficher une notification d'erreur à l'utilisateur
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -333,7 +345,11 @@ export function useEvaluation() {
|
|||||||
) => {
|
) => {
|
||||||
if (!userEvaluation) return;
|
if (!userEvaluation) return;
|
||||||
|
|
||||||
|
// Sauvegarder l'état actuel pour le rollback
|
||||||
|
const previousEvaluation = userEvaluation;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Optimistic UI update - mettre à jour immédiatement l'interface
|
||||||
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
|
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
|
||||||
if (catEval.category === category) {
|
if (catEval.category === category) {
|
||||||
return {
|
return {
|
||||||
@@ -354,6 +370,8 @@ export function useEvaluation() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
setUserEvaluation(newEvaluation);
|
setUserEvaluation(newEvaluation);
|
||||||
|
|
||||||
|
// Appel API en arrière-plan
|
||||||
await apiClient.removeSkillFromEvaluation(
|
await apiClient.removeSkillFromEvaluation(
|
||||||
userEvaluation.profile,
|
userEvaluation.profile,
|
||||||
category,
|
category,
|
||||||
@@ -361,6 +379,9 @@ export function useEvaluation() {
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to remove skill from evaluation:", error);
|
console.error("Failed to remove skill from evaluation:", error);
|
||||||
|
// Rollback optimiste en cas d'erreur
|
||||||
|
setUserEvaluation(previousEvaluation);
|
||||||
|
// Optionnel: afficher une notification d'erreur à l'utilisateur
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user