feat: Configuration des compétences par cycle [#16]

This commit is contained in:
N3WT DE COMPET
2025-05-18 00:45:49 +02:00
parent 2888f8dcce
commit 4e5aab6db7
29 changed files with 1001 additions and 82 deletions

View File

@ -0,0 +1,310 @@
import React, { useState, useRef, useCallback } from 'react';
import TreeView from '@/components/Structure/Competencies/TreeView';
import SectionHeader from '@/components/SectionHeader';
import { Award, CheckCircle } from 'lucide-react';
import SelectChoice from '@/components/SelectChoice';
import CheckBox from '@/components/CheckBox';
import Button from '@/components/Button';
import { useEstablishment } from '@/context/EstablishmentContext';
import {
fetchEstablishmentCompetencies,
createEstablishmentCompetencies,
deleteEstablishmentCompetencies,
} from '@/app/actions/schoolAction';
import { useCsrfToken } from '@/context/CsrfContext';
import { useNotification } from '@/context/NotificationContext';
const cycles = [
{ id: 1, label: 'Cycle 1' },
{ id: 2, label: 'Cycle 2' },
{ id: 3, label: 'Cycle 3' },
{ id: 4, label: 'Cycle 4' },
];
export default function CompetenciesList({
establishmentCompetencies,
onChangeCycle,
}) {
const [selectedCycle, setSelectedCycle] = useState(cycles[0].id);
const [showSelectedOnlyByCycle, setShowSelectedOnlyByCycle] = useState({
1: true,
2: true,
3: true,
4: true,
});
const [expandAllByCycle, setExpandAllByCycle] = useState({
1: false,
2: false,
3: false,
4: false,
});
const [hasSelectionByCycle, setHasSelectionByCycle] = useState({
1: false,
2: false,
3: false,
4: false,
});
const { selectedEstablishmentId } = useEstablishment();
const csrfToken = useCsrfToken();
const { showNotification } = useNotification();
// Référence vers le composant TreeView pour récupérer les compétences sélectionnées
const treeViewRef = useRef();
// Met à jour l'état de sélection à chaque changement dans TreeView
const handleSelectionChange = useCallback(
(selectedCompetencies) => {
setHasSelectionByCycle((prev) => ({
...prev,
[selectedCycle]: selectedCompetencies.length > 0,
}));
},
[selectedCycle]
);
// Filtrage : si showSelectedOnly, on affiche uniquement les compétences de l'établissement (state !== "none")
// sinon, on affiche toutes les compétences du cycle
const filteredData = (establishmentCompetencies.data || []).map(
(domaine) => ({
...domaine,
categories: domaine.categories.map((cat) => ({
...cat,
competences: showSelectedOnlyByCycle[selectedCycle]
? cat.competences.filter((c) => c.state !== 'none')
: cat.competences,
})),
})
);
const showSelectedOnly = showSelectedOnlyByCycle[selectedCycle];
const expandAll = expandAllByCycle[selectedCycle];
const handleShowSelectedOnlyChange = () => {
setShowSelectedOnlyByCycle((prev) => ({
...prev,
[selectedCycle]: !prev[selectedCycle],
}));
};
const handleExpandAllChange = () => {
setExpandAllByCycle((prev) => ({
...prev,
[selectedCycle]: !prev[selectedCycle],
}));
};
const handleCycleChange = (e) => {
const value = Number(e.target.value);
setSelectedCycle(value);
setHasSelectionByCycle((prev) => ({
...prev,
[value]: false,
}));
// Réinitialise la sélection visuelle dans le TreeView
if (treeViewRef.current && treeViewRef.current.clearSelection) {
treeViewRef.current.clearSelection();
}
onChangeCycle(value);
};
const handleSubmit = () => {
if (!treeViewRef.current || !treeViewRef.current.getSelectedCompetencies)
return;
const selectedIds = treeViewRef.current.getSelectedCompetencies();
const toCreate = [];
const toDelete = [];
const selectedCustomKeys = new Set(
(establishmentCompetencies.data || []).flatMap((domaine) =>
domaine.categories.flatMap((cat) =>
cat.competences
.filter(
(c) =>
c.state === 'custom' &&
(selectedIds.includes(String(c.competence_id)) ||
selectedIds.includes(Number(c.competence_id)))
)
.map((c) => `${cat.categorie_id}__${c.nom.trim().toLowerCase()}`)
)
)
);
(establishmentCompetencies.data || []).forEach((domaine) => {
domaine.categories.forEach((cat) => {
cat.competences.forEach((competence) => {
const isSelected =
selectedIds.includes(String(competence.competence_id)) ||
selectedIds.includes(Number(competence.competence_id));
const key = `${cat.categorie_id}__${competence.nom.trim().toLowerCase()}`;
// "none" sélectionné => à créer, sauf si une custom du même nom/catégorie est déjà sélectionnée
if (
competence.state === 'none' &&
isSelected &&
!selectedCustomKeys.has(key)
) {
toCreate.push({
category_id: cat.categorie_id,
establishment_id: selectedEstablishmentId,
nom: competence.nom,
});
} else if (competence.state === 'custom' && isSelected) {
// Suppression d'une compétence custom
toDelete.push({
competence_id: competence.competence_id, // id de EstablishmentCompetency
nom: competence.nom,
category_id: cat.categorie_id,
establishment_id: selectedEstablishmentId,
});
}
});
});
});
const afterSuccess = () => {
if (treeViewRef.current && treeViewRef.current.clearSelection) {
treeViewRef.current.clearSelection();
}
setHasSelectionByCycle((prev) => ({
...prev,
[selectedCycle]: false,
}));
onChangeCycle(selectedCycle);
showNotification('Opération effectuée avec succès', 'success', 'Succès');
};
if (toCreate.length > 0 && toDelete.length > 0) {
Promise.all([
createEstablishmentCompetencies(toCreate, csrfToken),
deleteEstablishmentCompetencies(
toDelete.map((item) => item.competence_id),
csrfToken
),
])
.then(afterSuccess)
.catch((error) => {
showNotification(
error.message ||
'Erreur apparue lors de la mise à jour des compétences',
'error',
'Erreur'
);
});
} else if (toCreate.length > 0) {
createEstablishmentCompetencies(toCreate, csrfToken)
.then(afterSuccess)
.catch((error) => {
showNotification(
error.message ||
'Erreur apparue lors de la mise à jour des compétences',
'error',
'Erreur'
);
});
} else if (toDelete.length > 0) {
deleteEstablishmentCompetencies(
toDelete.map((item) => item.competence_id),
csrfToken
)
.then(afterSuccess)
.catch((error) => {
showNotification(
error.message ||
'Erreur apparue lors de la mise à jour des compétences',
'error',
'Erreur'
);
});
}
};
const hasSelection = hasSelectionByCycle[selectedCycle];
return (
<div className="h-full flex flex-col">
<SectionHeader
icon={Award}
title="Liste des compétences"
description="Gérez les compétences par cycle"
/>
{/* Zone filtres centrée et plus large */}
<div className="mb-6 flex justify-center">
<div className="w-full max-w-3xl flex flex-col gap-4 p-6 rounded-lg border border-emerald-200 shadow-sm bg-white/80 backdrop-blur-sm">
<div className="flex flex-col sm:flex-row sm:items-start gap-8">
{/* Select cycle */}
<div className="flex-1 min-w-[220px]">
<SelectChoice
name="cycle"
label="Cycle"
placeHolder="Sélectionnez un cycle"
choices={cycles.map((cycle) => ({
value: cycle.id,
label: cycle.label,
}))}
selected={selectedCycle}
callback={handleCycleChange}
/>
</div>
{/* Cases à cocher l'une sous l'autre */}
<div className="flex flex-col gap-4 min-w-[220px]">
<CheckBox
item={{ id: 'showSelectedOnly' }}
formData={{ showSelectedOnly }}
handleChange={handleShowSelectedOnlyChange}
fieldName="showSelectedOnly"
itemLabelFunc={() => 'Uniquement les compétences sélectionnées'}
horizontal={false}
/>
<CheckBox
item={{ id: 'expandAll' }}
formData={{ expandAll }}
handleChange={handleExpandAllChange}
fieldName="expandAll"
itemLabelFunc={() => 'Tout dérouler'}
horizontal={false}
/>
</div>
</div>
</div>
</div>
{/* Zone scrollable pour le TreeView */}
<div className="flex-1 min-h-0 overflow-y-auto">
<TreeView
ref={treeViewRef}
data={filteredData}
expandAll={expandAll}
onSelectionChange={handleSelectionChange}
/>
</div>
{/* Bouton submit centré en bas */}
<div className="flex justify-center mb-2 mt-6">
<Button
text="Sauvegarder"
className={`px-6 py-2 rounded-md shadow ${
!hasSelection
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
: 'bg-emerald-500 text-white hover:bg-emerald-600'
}`}
onClick={handleSubmit}
primary
disabled={!hasSelection}
/>
</div>
{/* Légende en dessous du bouton, alignée à gauche */}
<div className="flex flex-row items-center gap-4 mb-4">
<span className="flex items-center gap-2 text-emerald-700 font-bold">
<CheckCircle className="w-4 h-4 text-emerald-500" />
Compétence requise
</span>
<span className="flex items-center gap-2 text-emerald-600 font-semibold">
Compétence sélectionnée ou créée
</span>
<span className="flex items-center gap-2 text-gray-500">
Compétence ignorée
</span>
</div>
</div>
);
}