mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
feat: Configuration des compétences par cycle [#16]
This commit is contained in:
@ -557,13 +557,13 @@ export default function Page() {
|
||||
/>
|
||||
{/* Popups */}
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
isOpen={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => setPopupVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
/>
|
||||
<Popup
|
||||
visible={confirmPopupVisible}
|
||||
isOpen={confirmPopupVisible}
|
||||
message={confirmPopupMessage}
|
||||
onConfirm={confirmPopupOnConfirm}
|
||||
onCancel={() => setConfirmPopupVisible(false)}
|
||||
|
||||
@ -182,7 +182,7 @@ export default function Layout({ children }) {
|
||||
<Footer softwareName={softwareName} softwareVersion={softwareVersion} />
|
||||
|
||||
<Popup
|
||||
visible={isPopupVisible}
|
||||
isOpen={isPopupVisible}
|
||||
message="Êtes-vous sûr(e) de vouloir vous déconnecter ?"
|
||||
onConfirm={confirmDisconnect}
|
||||
onCancel={() => setIsPopupVisible(false)}
|
||||
|
||||
@ -548,7 +548,7 @@ export default function Page() {
|
||||
|
||||
{/* Popup */}
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
isOpen={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => setPopupVisible(false)}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
fetchTuitionPaymentPlans,
|
||||
fetchRegistrationPaymentModes,
|
||||
fetchTuitionPaymentModes,
|
||||
fetchEstablishmentCompetencies,
|
||||
} from '@/app/actions/schoolAction';
|
||||
import { fetchProfiles } from '@/app/actions/authAction';
|
||||
import SidebarTabs from '@/components/SidebarTabs';
|
||||
@ -30,6 +31,7 @@ import { fetchRegistrationSchoolFileMasters } from '@/app/actions/registerFileGr
|
||||
import logger from '@/utils/logger';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { PlanningProvider, PlanningModes } from '@/context/PlanningContext';
|
||||
import CompetenciesList from '@/components/Structure/Competencies/CompetenciesList';
|
||||
|
||||
export default function Page() {
|
||||
const [specialities, setSpecialities] = useState([]);
|
||||
@ -45,6 +47,9 @@ export default function Page() {
|
||||
const [registrationPaymentModes, setRegistrationPaymentModes] = useState([]);
|
||||
const [tuitionPaymentModes, setTuitionPaymentModes] = useState([]);
|
||||
const [profiles, setProfiles] = useState([]);
|
||||
const [establishmentCompetencies, setEstablishmentCompetencies] = useState(
|
||||
[]
|
||||
);
|
||||
|
||||
const csrfToken = useCsrfToken();
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
@ -98,9 +103,22 @@ export default function Page() {
|
||||
.catch((error) => {
|
||||
logger.error('Error fetching profileRoles:', error);
|
||||
});
|
||||
|
||||
// Fetch data for establishment competencies
|
||||
handleEstablishmentCompetencies();
|
||||
}
|
||||
}, [selectedEstablishmentId]);
|
||||
|
||||
const handleEstablishmentCompetencies = (cycle = 1) => {
|
||||
fetchEstablishmentCompetencies(selectedEstablishmentId, cycle)
|
||||
.then((data) => {
|
||||
setEstablishmentCompetencies(data);
|
||||
})
|
||||
.catch((error) =>
|
||||
logger.error('Error fetching setEstablishmentCompetencies:', error)
|
||||
);
|
||||
};
|
||||
|
||||
const handleSpecialities = () => {
|
||||
fetchSpecialities(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
@ -339,6 +357,18 @@ export default function Page() {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'Competencies',
|
||||
label: 'Compétences',
|
||||
content: (
|
||||
<div className="h-full overflow-y-auto p-4">
|
||||
<CompetenciesList
|
||||
establishmentCompetencies={establishmentCompetencies}
|
||||
onChangeCycle={handleEstablishmentCompetencies}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@ -837,13 +837,13 @@ export default function Page({ params: { locale } }) {
|
||||
) : null}
|
||||
</div>
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
isOpen={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => setPopupVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
/>
|
||||
<Popup
|
||||
visible={confirmPopupVisible}
|
||||
isOpen={confirmPopupVisible}
|
||||
message={confirmPopupMessage}
|
||||
onConfirm={confirmPopupOnConfirm}
|
||||
onCancel={() => setConfirmPopupVisible(false)}
|
||||
|
||||
@ -113,7 +113,7 @@ export default function Page() {
|
||||
</div>
|
||||
</div>
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
isOpen={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={popupConfirmAction}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
|
||||
@ -8,9 +8,46 @@ import {
|
||||
BE_SCHOOL_PAYMENT_PLANS_URL,
|
||||
BE_SCHOOL_PAYMENT_MODES_URL,
|
||||
BE_SCHOOL_ESTABLISHMENT_URL,
|
||||
BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL,
|
||||
} from '@/utils/Url';
|
||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
||||
|
||||
export const deleteEstablishmentCompetencies = (ids, csrfToken) => {
|
||||
return fetch(BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: JSON.stringify({ ids }),
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const createEstablishmentCompetencies = (newData, csrfToken) => {
|
||||
return fetch(BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(newData),
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchEstablishmentCompetencies = (establishment, cycle = 1) => {
|
||||
return fetch(
|
||||
`${BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL}?establishment_id=${establishment}&cycle=${cycle}`
|
||||
)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchSpecialities = (establishment) => {
|
||||
return fetch(
|
||||
`${BE_SCHOOL_SPECIALITIES_URL}?establishment_id=${establishment}`
|
||||
|
||||
@ -9,8 +9,6 @@ const CheckBox = ({
|
||||
itemLabelFunc = () => null,
|
||||
horizontal,
|
||||
}) => {
|
||||
logger.debug(formData);
|
||||
|
||||
// Vérifier si formData[fieldName] est un tableau ou une valeur booléenne
|
||||
const isChecked = Array.isArray(formData[fieldName])
|
||||
? formData[fieldName].includes(parseInt(item.id)) // Si c'est un tableau, vérifier si l'élément est inclus
|
||||
|
||||
@ -84,7 +84,7 @@ const DateTab = ({
|
||||
</div>
|
||||
{popupVisible && (
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
isOpen={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => setPopupVisible(false)}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
|
||||
@ -248,14 +248,14 @@ export default function FilesToUpload({
|
||||
</div>
|
||||
)}
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
isOpen={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => setPopupVisible(false)}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
/>
|
||||
<Popup
|
||||
visible={removePopupVisible}
|
||||
isOpen={removePopupVisible}
|
||||
message={removePopupMessage}
|
||||
onConfirm={removePopupOnConfirm}
|
||||
onCancel={() => setRemovePopupVisible(false)}
|
||||
|
||||
@ -1,12 +1,6 @@
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
|
||||
const Modal = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
title,
|
||||
children,
|
||||
modalClassName,
|
||||
}) => {
|
||||
const Modal = ({ isOpen, setIsOpen, title, children, modalClassName }) => {
|
||||
return (
|
||||
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
||||
<Dialog.Portal>
|
||||
|
||||
@ -330,7 +330,7 @@ const PaymentPlanSelector = ({
|
||||
)}
|
||||
</div>
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
isOpen={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => setPopupVisible(false)}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
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;
|
||||
@ -469,7 +469,7 @@ const ClassesSection = ({
|
||||
onClick={() => {
|
||||
setRemovePopupVisible(true);
|
||||
setRemovePopupMessage(
|
||||
'Attentions ! \nVous êtes sur le point de supprimer la classe ' +
|
||||
'Attention ! \nVous êtes sur le point de supprimer la classe ' +
|
||||
classe.atmosphere_name +
|
||||
".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?"
|
||||
);
|
||||
@ -554,14 +554,14 @@ const ClassesSection = ({
|
||||
}
|
||||
/>
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
isOpen={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => setPopupVisible(false)}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
/>
|
||||
<Popup
|
||||
visible={removePopupVisible}
|
||||
isOpen={removePopupVisible}
|
||||
message={removePopupMessage}
|
||||
onConfirm={removePopupOnConfirm}
|
||||
onCancel={() => setRemovePopupVisible(false)}
|
||||
|
||||
@ -197,7 +197,7 @@ const SpecialitiesSection = ({
|
||||
onClick={() => {
|
||||
setRemovePopupVisible(true);
|
||||
setRemovePopupMessage(
|
||||
'Attentions ! \nVous êtes sur le point de supprimer la spécialité ' +
|
||||
'Attention ! \nVous êtes sur le point de supprimer la spécialité ' +
|
||||
speciality.name +
|
||||
".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?"
|
||||
);
|
||||
@ -265,14 +265,14 @@ const SpecialitiesSection = ({
|
||||
}
|
||||
/>
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
isOpen={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => setPopupVisible(false)}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
/>
|
||||
<Popup
|
||||
visible={removePopupVisible}
|
||||
isOpen={removePopupVisible}
|
||||
message={removePopupMessage}
|
||||
onConfirm={removePopupOnConfirm}
|
||||
onCancel={() => setRemovePopupVisible(false)}
|
||||
|
||||
@ -512,7 +512,7 @@ const TeachersSection = ({
|
||||
onClick={() => {
|
||||
setRemovePopupVisible(true);
|
||||
setRemovePopupMessage(
|
||||
"Attentions ! \nVous êtes sur le point de supprimer l'enseignant " +
|
||||
"Attention ! \nVous êtes sur le point de supprimer l'enseignant " +
|
||||
teacher.last_name +
|
||||
' ' +
|
||||
teacher.first_name +
|
||||
@ -589,14 +589,14 @@ const TeachersSection = ({
|
||||
}
|
||||
/>
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
isOpen={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => setPopupVisible(false)}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
/>
|
||||
<Popup
|
||||
visible={removePopupVisible}
|
||||
isOpen={removePopupVisible}
|
||||
message={removePopupMessage}
|
||||
onConfirm={removePopupOnConfirm}
|
||||
onCancel={() => setRemovePopupVisible(false)}
|
||||
|
||||
@ -155,7 +155,7 @@ export default function FileUploadDocuSeal({
|
||||
return (
|
||||
<div className="h-full flex flex-col mt-4 space-y-6">
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
isOpen={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => setPopupVisible(false)}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
|
||||
@ -592,7 +592,7 @@ export default function FilesGroupsManagement({
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
<Popup
|
||||
visible={removePopupVisible}
|
||||
isOpen={removePopupVisible}
|
||||
message={removePopupMessage}
|
||||
onConfirm={removePopupOnConfirm}
|
||||
onCancel={() => setRemovePopupVisible(false)}
|
||||
|
||||
@ -348,7 +348,7 @@ export default function ParentFilesSection({
|
||||
}
|
||||
/>
|
||||
<Popup
|
||||
visible={removePopupVisible}
|
||||
isOpen={removePopupVisible}
|
||||
message={removePopupMessage}
|
||||
onConfirm={removePopupOnConfirm}
|
||||
onCancel={() => setRemovePopupVisible(false)}
|
||||
|
||||
@ -384,14 +384,14 @@ const DiscountsSection = ({
|
||||
emptyMessage={emptyMessage}
|
||||
/>
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
isOpen={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => setPopupVisible(false)}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
/>
|
||||
<Popup
|
||||
visible={removePopupVisible}
|
||||
isOpen={removePopupVisible}
|
||||
message={removePopupMessage}
|
||||
onConfirm={removePopupOnConfirm}
|
||||
onCancel={() => setRemovePopupVisible(false)}
|
||||
|
||||
@ -42,6 +42,7 @@ export const BE_SCHOOL_FEES_URL = `${BASE_URL}/School/fees`;
|
||||
export const BE_SCHOOL_DISCOUNTS_URL = `${BASE_URL}/School/discounts`;
|
||||
export const BE_SCHOOL_PAYMENT_PLANS_URL = `${BASE_URL}/School/paymentPlans`;
|
||||
export const BE_SCHOOL_PAYMENT_MODES_URL = `${BASE_URL}/School/paymentModes`;
|
||||
export const BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL = `${BASE_URL}/School/establishmentCompetencies`;
|
||||
|
||||
// ESTABLISHMENT
|
||||
export const BE_SCHOOL_ESTABLISHMENT_URL = `${BASE_URL}/Establishment/establishments`;
|
||||
|
||||
Reference in New Issue
Block a user