mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
feat: Configuration des compétences par cycle [#16]
This commit is contained in:
144
Front-End/src/components/Structure/Competencies/TreeView.js
Normal file
144
Front-End/src/components/Structure/Competencies/TreeView.js
Normal file
@ -0,0 +1,144 @@
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
import { CheckCircle, Circle } from 'lucide-react';
|
||||
|
||||
const TreeView = forwardRef(function TreeView(
|
||||
{ data, expandAll, onSelectionChange },
|
||||
ref
|
||||
) {
|
||||
const [openDomains, setOpenDomains] = useState({});
|
||||
const [openCategories, setOpenCategories] = useState({});
|
||||
const [selectedCompetencies, setSelectedCompetencies] = useState({}); // { [competence_id]: true }
|
||||
|
||||
// N'ouvre ou ne ferme tout que si expandAll change explicitement
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
const allDomains = {};
|
||||
const allCategories = {};
|
||||
if (expandAll) {
|
||||
data.forEach((domaine) => {
|
||||
allDomains[domaine.domaine_id] = true;
|
||||
domaine.categories.forEach((cat) => {
|
||||
allCategories[cat.categorie_id] = true;
|
||||
});
|
||||
});
|
||||
setOpenDomains(allDomains);
|
||||
setOpenCategories(allCategories);
|
||||
} else {
|
||||
// On ne ferme tout que si l'utilisateur décoche explicitement "Tout dérouler"
|
||||
setOpenDomains({});
|
||||
setOpenCategories({});
|
||||
}
|
||||
}, [expandAll]); // <-- uniquement expandAll
|
||||
|
||||
// Appelle le callback à chaque changement de sélection
|
||||
useEffect(() => {
|
||||
if (onSelectionChange) {
|
||||
const selected = Object.entries(selectedCompetencies)
|
||||
.filter(([_, selected]) => selected)
|
||||
.map(([id]) => id);
|
||||
onSelectionChange(selected);
|
||||
}
|
||||
}, [selectedCompetencies, onSelectionChange]);
|
||||
|
||||
const toggleDomain = (id) =>
|
||||
setOpenDomains((prev) => ({ ...prev, [id]: !prev[id] }));
|
||||
const toggleCategory = (id) =>
|
||||
setOpenCategories((prev) => ({ ...prev, [id]: !prev[id] }));
|
||||
|
||||
const handleCompetenceClick = (competence) => {
|
||||
if (competence.state === 'required') return;
|
||||
setSelectedCompetencies((prev) => {
|
||||
const next = {
|
||||
...prev,
|
||||
[competence.competence_id]: !prev[competence.competence_id],
|
||||
};
|
||||
console.log(competence);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// Pour exposer la sélection au parent
|
||||
useImperativeHandle(ref, () => ({
|
||||
getSelectedCompetencies: () => {
|
||||
const selected = Object.entries(selectedCompetencies)
|
||||
.filter(([_, selected]) => selected)
|
||||
.map(([id]) => id);
|
||||
return selected;
|
||||
},
|
||||
clearSelection: () => setSelectedCompetencies({}),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div>
|
||||
{data.map((domaine) => (
|
||||
<div key={domaine.domaine_id} className="mb-4">
|
||||
<button
|
||||
className="w-full text-left px-3 py-2 bg-emerald-100 hover:bg-emerald-200 rounded font-semibold text-emerald-800"
|
||||
onClick={() => toggleDomain(domaine.domaine_id)}
|
||||
>
|
||||
{openDomains[domaine.domaine_id] ? '▼' : '►'} {domaine.domaine_nom}
|
||||
</button>
|
||||
{openDomains[domaine.domaine_id] && (
|
||||
<div className="ml-4">
|
||||
{domaine.categories.map((categorie) => (
|
||||
<div key={categorie.categorie_id} className="mb-2">
|
||||
<button
|
||||
className="w-full text-left px-2 py-1 bg-emerald-50 hover:bg-emerald-100 rounded text-emerald-700"
|
||||
onClick={() => toggleCategory(categorie.categorie_id)}
|
||||
>
|
||||
{openCategories[categorie.categorie_id] ? '▼' : '►'}
|
||||
{categorie.categorie_nom}
|
||||
</button>
|
||||
{openCategories[categorie.categorie_id] && (
|
||||
<ul className="ml-4">
|
||||
{categorie.competences.map((competence) => {
|
||||
const isSelected =
|
||||
selectedCompetencies[competence.competence_id];
|
||||
return (
|
||||
<li
|
||||
key={competence.competence_id}
|
||||
className={`py-1 flex items-center gap-2 ${
|
||||
competence.state === 'required'
|
||||
? 'text-emerald-700 font-bold'
|
||||
: competence.state === 'custom'
|
||||
? isSelected
|
||||
? 'text-gray-500 cursor-pointer hover:text-emerald-600'
|
||||
: 'text-emerald-600 font-semibold cursor-pointer'
|
||||
: isSelected
|
||||
? 'text-emerald-600 font-semibold cursor-pointer'
|
||||
: 'text-gray-500 cursor-pointer hover:text-emerald-600'
|
||||
}`}
|
||||
onClick={() => handleCompetenceClick(competence)}
|
||||
style={{
|
||||
cursor:
|
||||
competence.state === 'required'
|
||||
? 'default'
|
||||
: 'pointer',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
{competence.state === 'required' && (
|
||||
<CheckCircle className="w-4 h-4 text-emerald-500" />
|
||||
)}
|
||||
{competence.nom}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default TreeView;
|
||||
Reference in New Issue
Block a user