diff --git a/components/evaluation/client-wrapper.tsx b/components/evaluation/client-wrapper.tsx index dab4a52..b2bb5fb 100644 --- a/components/evaluation/client-wrapper.tsx +++ b/components/evaluation/client-wrapper.tsx @@ -1,9 +1,8 @@ "use client"; -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; import { useUser } from "@/hooks/use-user-context"; -import { UserEvaluation, Team } from "@/lib/types"; +import { UserEvaluation, Team, SkillLevel } from "@/lib/types"; import { updateSkillLevel as updateSkillLevelAction, updateSkillMentorStatus as updateSkillMentorStatusAction, @@ -24,16 +23,68 @@ export function EvaluationClientWrapper({ children, }: EvaluationClientWrapperProps) { 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); + + // Met à jour l'état local quand les props changent (SSR) + useEffect(() => { + setCurrentEvaluation(userEvaluation); + }, [userEvaluation]); + + // Fonctions avec UI optimiste const updateSkillLevel = async ( category: string, skillId: string, - level: any + level: SkillLevel ) => { - await updateSkillLevelAction(category, skillId, level); - router.refresh(); + if (!currentEvaluation) return; + + // 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 ( @@ -41,8 +92,39 @@ export function EvaluationClientWrapper({ skillId: string, canMentor: boolean ) => { - await updateSkillMentorStatusAction(category, skillId, canMentor); - router.refresh(); + if (!currentEvaluation) return; + + 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 ( @@ -50,45 +132,204 @@ export function EvaluationClientWrapper({ skillId: string, wantsToLearn: boolean ) => { - await updateSkillLearningStatusAction(category, skillId, wantsToLearn); - router.refresh(); + if (!currentEvaluation) return; + + 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) => { - await addSkillToEvaluationAction(category, skillId); - router.refresh(); + if (!currentEvaluation) return; + + 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 ( category: string, skillId: string ) => { - await removeSkillFromEvaluationAction(category, skillId); - router.refresh(); + if (!currentEvaluation) return; + + 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 useEffect(() => { - if (userEvaluation) { + if (currentEvaluation) { const teamName = - teams.find((t) => t.id === userEvaluation.profile.teamId)?.name || ""; + teams.find((t) => t.id === currentEvaluation.profile.teamId)?.name || + ""; setUserInfo({ - firstName: userEvaluation.profile.firstName, - lastName: userEvaluation.profile.lastName, + firstName: currentEvaluation.profile.firstName, + lastName: currentEvaluation.profile.lastName, teamName, }); } else { setUserInfo(null); } - }, [userEvaluation, teams, setUserInfo]); + }, [currentEvaluation, teams, setUserInfo]); // Provide evaluation functions to children through React context or props return ( {children} @@ -100,6 +341,7 @@ export function EvaluationClientWrapper({ import { createContext, useContext } from "react"; interface EvaluationContextType { + currentEvaluation: UserEvaluation | null; updateSkillLevel: (categoryId: string, skillId: string, level: any) => void; updateSkillMentorStatus: ( categoryId: string, @@ -112,6 +354,10 @@ interface EvaluationContextType { wantsToLearn: boolean ) => void; addSkillToEvaluation: (categoryId: string, skillId: string) => void; + addMultipleSkillsToEvaluation: ( + categoryId: string, + skillIds: string[] + ) => void; removeSkillFromEvaluation: (categoryId: string, skillId: string) => void; } @@ -121,10 +367,12 @@ const EvaluationContext = createContext( function EvaluationProvider({ children, + currentEvaluation, updateSkillLevel, updateSkillMentorStatus, updateSkillLearningStatus, addSkillToEvaluation, + addMultipleSkillsToEvaluation, removeSkillFromEvaluation, }: { children: React.ReactNode; @@ -132,10 +380,12 @@ function EvaluationProvider({ return ( diff --git a/components/skill-evaluation.tsx b/components/skill-evaluation.tsx index dab8307..725ce0a 100644 --- a/components/skill-evaluation.tsx +++ b/components/skill-evaluation.tsx @@ -23,12 +23,17 @@ export function SkillEvaluation({ evaluations, }: SkillEvaluationProps) { const { + currentEvaluation: contextEvaluation, updateSkillLevel, updateSkillMentorStatus, updateSkillLearningStatus, addSkillToEvaluation, + addMultipleSkillsToEvaluation, removeSkillFromEvaluation, } = useEvaluationContext(); + + // Utiliser l'évaluation du contexte (avec état optimiste) ou celle des props (SSR) + const activeEvaluations = contextEvaluation?.evaluations || evaluations; const searchParams = useSearchParams(); const router = useRouter(); const categoryParam = searchParams.get("category"); @@ -57,7 +62,7 @@ export function SkillEvaluation({ const currentCategory = categories.find( (cat) => cat.category === selectedCategory ); - const currentEvaluation = evaluations.find( + const currentEvaluation = activeEvaluations.find( (evaluation) => evaluation.category === selectedCategory ); @@ -85,9 +90,10 @@ export function SkillEvaluation({
diff --git a/components/skill-selector.tsx b/components/skill-selector.tsx index 3eef341..44e228e 100644 --- a/components/skill-selector.tsx +++ b/components/skill-selector.tsx @@ -21,6 +21,7 @@ interface SkillSelectorProps { evaluations: CategoryEvaluation[]; selectedCategory: string; onAddSkill: (category: string, skillId: string) => void; + onAddMultipleSkills?: (category: string, skillIds: string[]) => void; onRemoveSkill: (category: string, skillId: string) => void; } @@ -29,11 +30,14 @@ export function SkillSelector({ evaluations, selectedCategory, onAddSkill, + onAddMultipleSkills, onRemoveSkill, }: SkillSelectorProps) { const [isOpen, setIsOpen] = useState(false); const [isCreateOpen, setIsCreateOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(""); + const [isAddingAll, setIsAddingAll] = useState(false); + const [addingSkillIds, setAddingSkillIds] = useState>(new Set()); const currentCategory = categories.find( (cat) => cat.category === selectedCategory @@ -60,6 +64,47 @@ export function SkillSelector({ 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 (
{/* Selected Skills */} @@ -88,15 +133,30 @@ export function SkillSelector({
- {/* Search */} -
- - setSearchTerm(e.target.value)} - className="pl-10" - /> + {/* Search and Add All */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+ + {availableSkills.length > 1 && ( + + )}
{/* Create New Skill Button */} @@ -159,13 +219,16 @@ export function SkillSelector({
)) diff --git a/hooks/use-evaluation.ts b/hooks/use-evaluation.ts index 4226831..0fbe617 100644 --- a/hooks/use-evaluation.ts +++ b/hooks/use-evaluation.ts @@ -296,14 +296,21 @@ export function useEvaluation() { const addSkillToEvaluation = async (category: string, skillId: string) => { if (!userEvaluation) return; + // Sauvegarder l'état actuel pour le rollback + const previousEvaluation = userEvaluation; + try { + // Optimistic UI update - mettre à jour immédiatement l'interface const updatedEvaluations = userEvaluation.evaluations.map((catEval) => { if (catEval.category === category) { if (!catEval.selectedSkillIds.includes(skillId)) { return { ...catEval, 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); + + // Appel API en arrière-plan await apiClient.addSkillToEvaluation( userEvaluation.profile, category, @@ -324,6 +333,9 @@ export function useEvaluation() { ); } catch (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; + // Sauvegarder l'état actuel pour le rollback + const previousEvaluation = userEvaluation; + try { + // Optimistic UI update - mettre à jour immédiatement l'interface const updatedEvaluations = userEvaluation.evaluations.map((catEval) => { if (catEval.category === category) { return { @@ -354,6 +370,8 @@ export function useEvaluation() { }; setUserEvaluation(newEvaluation); + + // Appel API en arrière-plan await apiClient.removeSkillFromEvaluation( userEvaluation.profile, category, @@ -361,6 +379,9 @@ export function useEvaluation() { ); } catch (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 } };