mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-05 12:41:27 +00:00
312 lines
10 KiB
JavaScript
312 lines
10 KiB
JavaScript
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/Form/SelectChoice';
|
|
import CheckBox from '@/components/Form/CheckBox';
|
|
import Button from '@/components/Form/Button';
|
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
|
import {
|
|
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, profileRole } = 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-primary/20 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">
|
|
{profileRole !== 0 && (
|
|
<Button
|
|
text="Sauvegarder"
|
|
className={`px-6 py-2 rounded-md shadow ${
|
|
!hasSelection
|
|
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
|
: 'bg-primary text-white hover:bg-primary'
|
|
}`}
|
|
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-secondary font-bold">
|
|
<CheckCircle className="w-4 h-4 text-primary" />
|
|
Compétence requise
|
|
</span>
|
|
<span className="flex items-center gap-2 text-primary 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>
|
|
);
|
|
}
|