From ac1b35b1d9fc5727d38f3e5a5c45edeff8656c4c Mon Sep 17 00:00:00 2001 From: TopheC Date: Mon, 25 May 2026 01:34:18 +0200 Subject: [PATCH] Ajout de visualisations pratiques de la matrice --- src/components/matrix/SkillMatrixTable.jsx | 171 +++++++++++++++------ src/pages/History.jsx | 8 +- src/pages/SkillMatrix.jsx | 71 +++++++-- 3 files changed, 187 insertions(+), 63 deletions(-) diff --git a/src/components/matrix/SkillMatrixTable.jsx b/src/components/matrix/SkillMatrixTable.jsx index 9e1f0c5..4af904c 100644 --- a/src/components/matrix/SkillMatrixTable.jsx +++ b/src/components/matrix/SkillMatrixTable.jsx @@ -1,9 +1,37 @@ +import { useState } from 'react' +import { ChevronDown, ChevronRight, ListCollapse } from 'lucide-react' import { SkillLevelBadge, SkillLevelSelect } from '@/components/SkillLevelBadge' -export function SkillMatrixTable({ skills, members, levels, isAdmin, editing, onEdit, onUpdate, onCancel }) { - const visibleMembers = skills.length === 0 ? [] : members +export function SkillMatrixTable({ categories, skills, members, levels, isAdmin, currentUserId, editing, onEdit, onUpdate, onCancel }) { + const [collapsed, setCollapsed] = useState(new Set()) - if (visibleMembers.length === 0) { + const grouped = categories + .map((cat) => ({ + ...cat, + catSkills: skills.filter((s) => s.category_id === cat.id), + })) + .filter((g) => g.catSkills.length > 0) + + const allCollapsed = grouped.every((g) => collapsed.has(g.id)) + + function toggleCategory(catId) { + setCollapsed((prev) => { + const next = new Set(prev) + if (next.has(catId)) next.delete(catId) + else next.add(catId) + return next + }) + } + + function toggleAll() { + if (allCollapsed) { + setCollapsed(new Set()) + } else { + setCollapsed(new Set(grouped.map((g) => g.id))) + } + } + + if (members.length === 0) { return (
Aucun résultat @@ -16,57 +44,102 @@ export function SkillMatrixTable({ skills, members, levels, isAdmin, editing, on - - {skills.map((s) => ( - + + {members.map((m) => ( + ))} - {visibleMembers.map((m) => ( - - - {skills.map((s) => { - const key = `${m.id}-${s.id}` - const level = levels[key] - const isEditing = editing === key - return ( - + - ) - })} - - ))} + {members.map((m) => { + const dotColors = ['bg-gray-200', 'bg-blue-200', 'bg-amber-200', 'bg-green-200'] + const counts = [0, 0, 0, 0] + g.catSkills.forEach((s) => { + const lvl = levels[`${m.id}-${s.id}`]?.level + if (lvl) counts[lvl - 1]++ + }) + return ( + + ) + })} + + {!isCollapsed && g.catSkills.map((s) => ( + + + {members.map((m) => { + const key = `${m.id}-${s.id}` + const level = levels[key] + const canEditCell = isAdmin || currentUserId === m.id + const isEditing = editing === key + return ( + + ) + })} + + ))} + + ) + })}
Membre{s.name} + + Compétence + + {m.full_name || m.email} +
- {m.full_name || m.email} - - {isEditing && isAdmin ? ( - onUpdate(m.id, s.id, v)} - /> - ) : ( - level ? ( - isAdmin && onEdit(key)} - /> - ) : ( - isAdmin && onEdit(key)} - > - — - - ) - )} - {isEditing && isAdmin && ( - - )} + {grouped.map((g) => { + const isCollapsed = collapsed.has(g.id) + return ( + <> +
toggleCategory(g.id)}> + + {isCollapsed ? : } + + {g.name} +
+ + {counts.map((c, i) => + c > 0 ? ( + + + {c} + + ) : null + )} + +
+ {s.name} + + {isEditing && canEditCell ? ( + onUpdate(m.id, s.id, v)} + /> + ) : ( + level ? ( + canEditCell && onEdit(key)} + /> + ) : ( + canEditCell && onEdit(key)} + > + — + + ) + )} + {isEditing && canEditCell && ( + + )} +
diff --git a/src/pages/History.jsx b/src/pages/History.jsx index 2cff147..6e092af 100644 --- a/src/pages/History.jsx +++ b/src/pages/History.jsx @@ -50,8 +50,12 @@ export function History() {
  • - {h.member?.full_name} - {' → '}{h.skill?.name} + Membre : + {' '}{h.member?.full_name || h.member_id?.slice(0, 8)} +

    +

    + Compétence : + {' '}{h.skill?.name}

    Par {h.changer?.full_name} — {new Date(h.created_at).toLocaleString()} diff --git a/src/pages/SkillMatrix.jsx b/src/pages/SkillMatrix.jsx index dc6943b..6c08200 100644 --- a/src/pages/SkillMatrix.jsx +++ b/src/pages/SkillMatrix.jsx @@ -6,11 +6,15 @@ import { useMembers } from '@/hooks/useMembers' import { useSkillLevels } from '@/hooks/useSkillLevels' import { SkillMatrixFilters } from '@/components/matrix/SkillMatrixFilters' import { SkillMatrixTable } from '@/components/matrix/SkillMatrixTable' +import { SkillMemberForm } from '@/components/matrix/SkillMemberForm' +import { Download } from 'lucide-react' +import { Button } from '@/components/ui/button' import { toast } from 'sonner' export function SkillMatrix() { const { profile } = useAuth() const isAdmin = profile?.role === 'admin' + const currentUserId = profile?.id const { categories } = useCategories() const { skills } = useSkills() const { members } = useMembers() @@ -50,9 +54,34 @@ export function SkillMatrix() { toast.success('Niveau mis à jour') } + function exportCSV() { + const header = ['Compétence', ...visibleMembers.map((m) => m.full_name || m.email)].join(',') + const rows = filteredSkills.map((s) => { + const levelsRow = visibleMembers.map((m) => { + const key = `${m.id}-${s.id}` + return levels[key]?.level || '' + }) + return [`"${s.name}"`, ...levelsRow].join(',') + }) + const blob = new Blob(['\uFEFF' + header + '\n' + rows.join('\n')], { type: 'text/csv;charset=utf-8;' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'matrice-competences.csv' + a.click() + URL.revokeObjectURL(url) + toast.success('Fichier CSV téléchargé') + } + return (

    -

    Matrice des compétences

    +
    +

    Matrice des compétences

    + +
    - setEditing(null)} - /> -
    Débutant Intermédiaire Avancé Expert
    + + {filterMember !== 'all' && visibleMembers.length === 1 ? ( + { + Promise.all(changes.map((c) => updateLevel(memberId, c.skillId, c.newLevel, profile.id))) + .then(() => toast.success('Compétences mises à jour')) + .catch(() => toast.error('Erreur lors de la mise à jour')) + }} + /> + ) : ( + setEditing(null)} + /> + )}
    ) }