<div id="studentTable"> <div class="thead"> <div class="tr"> <div class="th sticky" order="ASC" onclick="reOrderEtudiants()">Noms</div> <button id="addColumn" class="floating-button">+</button> </div> </div> <div class="tbody"> {% for etud in etudiants %} <div class="tr" etudid="{{etud.etudid}}"> <div class="td sticky etudinfo" id="row-{{etud.etudid}}"> <span>{{etud.nomprenom}}</span> </div> </div> {% endfor %} </div> </div> <style> .td.sticky .pdp-hover { display: none; } .td.sticky:hover .pdp-hover { position: absolute; display: block; width: 90px; height: 90px; border-radius: 15px; transition: all 0.5s; left: calc(80% - 45px); top: -25%; } .err-assi { display: flex; justify-content: flex-start; align-items: center; gap: 15px; } .tr[etudid] { height: 50px; } .table-container { overflow: auto; position: relative; max-width: 100%; margin: 0 auto; box-shadow: 0 0 1rem 0 rgba(0, 0, 0, .2); } .table { border-collapse: collapse; } .thead .tr { display: flex; align-items: center; } .thead .tr .th { height: 200px; display: flex; justify-content: center; align-items: center; font-size: 16px; } .th.sticky { z-index: 5; cursor: pointer; } .th, .td { text-align: center; width: 225px; border: 1px solid #ddd; display: flex; justify-content: center; align-content: center; min-height: 40px; } .tr { display: flex; justify-content: flex-start; align-items: center; width: max-content; } .td span { align-items: center; display: flex; } .td[assiduite_id='insc'] * { display: none; } .td[assiduite_id='insc']::after { content: "non inscrit au module"; font-style: italic; } .sticky { position: sticky; left: 0; background-color: #fafafa; border-right: 1px solid #ddd; z-index: 100; } .mini-form { display: flex; justify-content: center; align-items: center; flex-direction: column; width: 100%; } .mini-form input, .mini-form select { display: block; margin: 5px; padding: 5px; border-radius: 5px; border: 1px solid #ddd; max-width: 195px; } #addColumn { font-size: 24px; width: 50px; height: 50px; border-radius: 50%; right: -60px; top: calc(50% - 50px /2); background-color: #09c; color: white; border: none; outline: none; cursor: pointer; transition: background-color 0.3s; } #addColumn:hover { background-color: #0056b3; } .th { background-color: #09c; color: white; position: relative; } .th.error { background-color: #d71111; } .tbody .tr:nth-child(even) { background-color: #f2f2f2; } .tbody .tr:hover { background-color: #ddd; } .td .etat { display: grid; grid-template-columns: 33% 33% 33%; grid-template-rows: 50% 50%; font-size: small; } .btngroup { width: 100%; display: flex; justify-content: space-between; align-items: center; flex-direction: row; } .btngroup>button { background-color: transparent; outline: none; border: none; cursor: pointer; color: whitesmoke; } .th .closeCol { font-size: 16px; } .activate { font-size: 14px; } .th[activated] { transition: all 0.5s; } .th[activated='false'] { opacity: 0.5; } .num { border: 1px whitesmoke solid; border-radius: 15px; padding: 2px 4px; width: 24px; } .assi_rbtn, .assi_lbl { width: 100%; display: flex; justify-content: space-evenly; align-items: center; } .td[assiduite_id='conflit'] { background-color: #ff0000c2; } input[type='radio']:disabled { cursor: not-allowed; } .th.error:hover .col-error { display: block; z-index: 2000; background-color: #d71111; width: 100%; min-height: 25%; bottom: -25%; transition: all 1s; } .col-error { position: absolute; display: none; } .mass { display: flex; justify-content: space-evenly; align-items: center; } .mass input { outline: none; border: none; } .rbtn:disabled { opacity: 0.7; } .td.etat { box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; border: 10px solid white; } </style> <script> const assi_etat_defaut = "{{assi_etat_defaut}}"; window.forceModule = "{{ forcer_module }}" window.forceModule = window.forceModule == "True" ? true : false let colCount = 1; let currentDate = "{{date}}"; if (currentDate == "") { currentDate = moment().tz(TIMEZONE).format("YYYY-MM-DDTHH:mm"); } else { currentDate = moment(currentDate).tz(TIMEZONE).format("YYYY-MM-DDTHH:mm"); } const inscriptionsModule = {}; function createColumn(dateStart = "", dateEnd = "", moduleimpl_id = "") { let table = document.getElementById("studentTable"); let th = document.createElement("div"); th.classList.add("th", "error"); const col_id = `${colCount++}`; th.setAttribute("col", col_id); th.setAttribute("activated", "true"); th.innerHTML = ` <div class="col-error">La période n'est pas valide</div> <div class="mini-form"> <div class="btngroup" style="justify-content: flex-end;"> <button class="closeCol" onclick="removeColumn(this)">x</button> </div> <input type="datetime-local" id="dateStart" value="${dateStart}"> <input type="datetime-local" id="dateEnd" value="${dateEnd}"> {{moduleimpl_select|safe}} <div id="mass_action_${col_id}" class="mass"> <input disabled="" type="radio" class="rbtn present" name="mass_action_${col_id}" value="present" onclick="massCol(this)"> <input disabled="" type="radio" class="rbtn retard" name="mass_action_${col_id}" value="retard" onclick="massCol(this)"> <input disabled="" type="radio" class="rbtn absent" name="mass_action_${col_id}" value="absent" onclick="massCol(this)"> <input disabled="" type="radio" class="rbtn aucun" name="mass_action_${col_id}" value="remove" onclick="massCol(this)"> </div> </div> `; table .querySelector(".thead .tr") .insertBefore(th, document.querySelector("#addColumn")); if (dateStart == "") { const last = [...document.querySelectorAll("#dateStart")].pop(); defaultDate(last); } try { const sl = th.querySelector('.dynaSelect'); sl.id = `dynaSelect_${col_id}`; const miniform = sl.parentElement; const dateIso = miniform.querySelector("#dateStart").value; if (dateIso != "") { updateSelect("", `#${sl.id}`, dateIso) } miniform.querySelector("#dateStart").addEventListener('focusout', (el) => { updateSelect("", `#${sl.id}`, el.target.value); }) } catch { } th.querySelectorAll('input, select.dynaSelect, select#moduleimpl_select').forEach((el) => { el.addEventListener("change", () => { getAndUpdateCol(col_id); }) }) const sl = th.querySelector('select'); sl.addEventListener('change', () => { editModuleImpl(sl); }) if (moduleimpl_id != "") { sl.value = moduleimpl_id; } let rows = table.querySelector(".tbody").querySelectorAll(".tr"); for (let i = 0; i < rows.length; i++) { let td = document.createElement("div"); td.setAttribute("colid", col_id) td.classList.add("td", "etat"); const etudid = rows[i].getAttribute("etudid"); td.innerHTML = ` <div class="assi_rbtn" etat=""> <input class="rbtn present" type="radio" name="etat_${col_id}_${etudid}" value="present" disabled onclick="updateEtudAssiduite(this)"> <input class="rbtn retard" type="radio" name="etat_${col_id}_${etudid}" value="retard" disabled onclick="updateEtudAssiduite(this)"> <input class="rbtn absent" type="radio" name="etat_${col_id}_${etudid}" value="absent" disabled onclick="updateEtudAssiduite(this)"> </div> <br/> `; rows[i].appendChild(td); if (assi_etat_defaut != "" && assi_etat_defaut != "aucun") { const inp = td.querySelector(`[value='${assi_etat_defaut}']`).checked = true; } } } function reOrderEtudiants() { const th = document.querySelector(".th.sticky"); let lines = [...document.querySelectorAll('.tr[etudid]')]; const tbody = document.querySelector('.tbody') let order = (a, b) => { return a > b } if (th.getAttribute("order") == "ASC") { order = (a, b) => { return b > a } th.setAttribute("order", "DESC") } else { th.setAttribute("order", "ASC") } lines = lines.sort((a, b) => { const nameA = a.querySelector(".td.sticky").textContent.split(" ").pop(); const nameB = b.querySelector(".td.sticky").textContent.split(" ").pop(); return order(nameA, nameB) }) tbody.innerHTML = ""; tbody.append(...lines) } function previousCol(column) { const cols = [...document.querySelectorAll("[col]")] let previousCol = null; let i = 1; while (i < cols.length) { previousCol = cols[i - 1]; if (cols[i] == column) break; i++; } return previousCol } function removeColumn(element) { const col = element.parentElement.parentElement.parentElement; const col_id = col.getAttribute("col"); document.querySelectorAll(`[col='${col_id}'],[colid='${col_id}']`).forEach((el) => { el.remove() }) } function defaultDate(element) { const num = previousCol(element.parentElement.parentElement)?.getAttribute("col"); const last = document.querySelector(`[col='${num}'] #dateEnd`); let date = undefined; if (last == undefined) { date = currentDate; } else { date = last.value; } element.value = date; element.addEventListener( "focusout", () => { const el = element.parentElement.querySelector("#dateEnd"); const el2 = element.parentElement.querySelector("#dateStart"); el.value = moment(el2.valueAsDate).tz(TIMEZONE).utc() .add(2, "hours") .format("YYYY-MM-DDTHH:mm"); const colid = element.parentElement.parentElement.getAttribute('col'); getAndUpdateCol(colid); }, { once: true } ); } function setEtatCol(colId, etatId) { const tds = [...document.querySelectorAll(`.td[colid='${colId}']`)] tds.forEach((td) => { setEtatLine(td, etatId); }) } function setEtatLine(line, etatId) { let inputs = [...line.querySelectorAll("input")] inputs.forEach((el) => { el.checked = false }) if (etatId !== "") { inputs[Number.parseInt(etatId)].checked = true; inputs[Number.parseInt(etatId)].parentElement.setAttribute('etat', inputs[Number.parseInt(etatId)].value) } let color; switch (etatId) { case 0: color = "#9CF1AF"; break case 1: color = "#F1D99C"; break case 2: color = "#F1A69C"; break default: color = "white"; break; } line.style.borderColor = color; } function _createAssiduites(inputDeb, inputFin, moduleSelect, etudid, etat, colId) { if (moduleSelect == "") { return { "date_debut": inputDeb, "date_fin": inputFin, "etudid": etudid, "etat": etat, "colid": colId, } } else { return { "date_debut": inputDeb, "date_fin": inputFin, "etudid": etudid, "moduleimpl_id": moduleSelect, "etat": etat, "colid": colId, } } } function getAssiduitesCol(col_id, get = true) { const col = document.querySelector(`[col='${col_id}']`); const colErr = col.querySelector('.col-error') const inputDeb = col.querySelector("#dateStart").value; const inputFin = col.querySelector("#dateEnd").value; const moduleSelect = col.querySelector("#moduleimpl_select,.dynaSelect").value; const d_debut = moment(inputDeb).tz(TIMEZONE); const d_fin = moment(inputFin).tz(TIMEZONE); if (inputDeb == "" || inputFin == "" || d_debut >= d_fin) { colErr.textContent = `La période de la colonne n'est pas valide`; return 0x2; } {% if periode %} const t_before = "{{periode.deb}}T00:00"; const t_after = "{{periode.fin}}T23:59"; const testPeriode = [ d_debut.isBefore(t_before), d_debut.isAfter(t_after), d_fin.isBefore(t_before), d_fin.isAfter(t_after), ] if (testPeriode.some((e) => e)) { colErr.textContent = `La période de la colonne n'est pas dans le semestre`; return 0x3; } {% endif %} if (window.forceModule && moduleSelect == "") { colErr.textContent = `Le module de la colonne n'a pas été entré. (Préférence de semestre)`; return 0x4; } if (get) { getAssiduitesFromEtuds(false, d_debut.format(), d_fin.format()) return 0x0; } return { moduleimpl: moduleSelect, deb: d_debut.format(), fin: d_fin.format(), } } function updateAssiduitesCol(col_id) { const col = document.querySelector(`[col='${col_id}']`); const tds = [...document.querySelectorAll(`.td[colid='${col_id}']`)] const inputDeb = col.querySelector("#dateStart").value; const inputFin = col.querySelector("#dateEnd").value; const d_debut = moment(inputDeb).tz(TIMEZONE); const d_fin = moment(inputFin).tz(TIMEZONE); const moduleimpl_id = col.querySelector("#moduleimpl_select").value; const periode = { deb: d_debut, fin: d_fin, } tds.forEach((td) => { tds.forEach((el) => { const inputs = [...el.querySelectorAll('input')]; inputs.forEach((i) => { i.disabled = false }) }); setEtatLine(td, "") const etu = td.parentElement.getAttribute('etudid'); const inscriptionModule = ["", "autre"].indexOf(moduleimpl_id) !== -1 ? true : checkInscriptionModule(moduleimpl_id, etu); const conflits = getAssiduitesConflict(etu, periode); if (!inscriptionModule) { td.setAttribute('assiduite_id', "insc"); } else if (conflits.length == 0) { td.setAttribute('assiduite_id', "-1"); } else if (conflits.length == 1 && isConflictSameAsPeriod(conflits[0], periode)) { const assi = conflits[0]; td.setAttribute('assiduite_id', assi.assiduite_id); const ind = ["PRESENT", "RETARD", "ABSENT"].indexOf(assi.etat.toUpperCase()); setEtatLine(td, ind) } else { td.setAttribute('assiduite_id', "conflit"); const inputs = [...td.querySelectorAll('input')]; inputs.forEach((i) => { i.disabled = true; }) } }) } function setEtuds() { if (!isSingleEtud()) { const etudids = [...document.querySelectorAll("[etudid]")] .map((el) => Number.parseInt(el.getAttribute("etudid"))) .filter((value, index, array) => array.indexOf(value) === index); etudids.forEach((etu) => { etuds[etu] = { etudid: etu } }) } } function getAndUpdateCol(colid) { const column = document.querySelector(`[col='${colid}']`); if (getAssiduitesCol(colid) == 0) { column.classList.remove('error'); document.getElementById(`mass_action_${colid}`).querySelectorAll('.rbtn').forEach((el) => { el.removeAttribute('disabled'); }) updateAssiduitesCol(colid) } else { document.getElementById(`mass_action_${colid}`).querySelectorAll('.rbtn').forEach((el) => { el.setAttribute('disabled', ''); }) column.classList.add('error'); const tds = [...document.querySelectorAll(`.td[colid='${colid}']`)] tds.forEach((el) => { const inputs = [...el.querySelectorAll('input')]; inputs.forEach((i) => { i.disabled = true }) }); } } function updateAllCol() { const colIds = [...document.querySelectorAll("[col]")].map((col) => { return col.getAttribute('col') }); colIds.forEach((colid) => { getAndUpdateCol(colid) }) } function launchToast(etudid, etat) { let etatAffiche; switch (etat.toUpperCase()) { case "PRESENT": etatAffiche = "%etud% a été noté(e) <u><strong>présent(e)</strong></u>"; break; case "RETARD": etatAffiche = "%etud% a été noté(e) <u><strong>en retard</strong></u>"; break; case "ABSENT": etatAffiche = "%etud% a été noté(e) <u><strong>absent(e)</strong></u>"; break; case "REMOVE": etatAffiche = "L'assiduité de %etud% a été retirée."; } let color; switch (etat.toUpperCase()) { case "PRESENT": color = "#6bdb83"; break; case "ABSENT": color = "#F1A69C"; break; case "RETARD": color = "#f0c865"; break; default: color = "#AAA"; break; } const nom_prenom = document.querySelector(`.tr[etudid="${etudid}"] .td span`).textContent const span = document.createElement("span"); span.innerHTML = etatAffiche.replace("%etud%", nom_prenom); pushToast(generateToast(span, color, 5)); } function updateEtudAssiduite(rbtn) { const [_, colid, etudid] = rbtn.name.split("_"); const etat = rbtn.value; const etudLine = rbtn.parentElement.parentElement; const assi = etudLine.getAttribute('assiduite_id'); const { moduleimpl, deb, fin } = getAssiduitesCol(colid, false); switch (assi) { case "-1": // création d'une nouvelle assiduité const assiduite = _createAssiduites(deb, fin, moduleimpl, etudid, etat, colid); rbtn.parentElement.setAttribute('etat', etat); asyncCreateAssiduite(assiduite, (data) => { if (Object.keys(data.success).length > 0) { const assi_id = data.success['0'].message.assiduite_id; etudLine.setAttribute('assiduite_id', assi_id); assiduite["assiduite_id"] = assi_id; assiduites[etudid].push(assiduite); updateAllCol() launchToast(etudid, etat); } }) break; case "conflit": // Conflit, afficher résolveur const assiduitesList = assiduites[etudid]; const d_debut = new moment.tz(deb, TIMEZONE); const d_fin = new moment.tz(fin, TIMEZONE); const period = { deb: deb, fin: fin, } const conflitResolver = new ConflitResolver( assiduites[etudid], period, { deb: new moment.tz(d_debut.startOf('day'), TIMEZONE), fin: new moment.tz(d_fin.endOf('day'), TIMEZONE), } ); const update = () => { assiduites = {} const cols = [...document.querySelectorAll("[col]")].map((col) => col.getAttribute('col')); cols.forEach((c) => getAndUpdateCol(c)); }; conflitResolver.callbacks = { delete: update, edit: update, split: update, }; conflitResolver.open(); rbtn.checked = false; break; default: // Une assiduité est déjà connue -> modifier assiduité if (etat == rbtn.parentElement.getAttribute('etat')) { asyncRemoveAssiduite(assi, () => { etudLine.setAttribute('assiduite_id', "-1"); rbtn.parentElement.setAttribute('etat', "") assiduites[etudid] = assiduites[etudid].filter((el) => { return el.assiduite_id != assi }) updateAllCol() launchToast(etudid, "remove"); }) } else { const edit = { moduleimpl_id: moduleimpl == "" ? null : moduleimpl, etat: etat, assiduite_id: Number.parseInt(assi), } asyncEditAssiduite(edit, (data) => { const obj = structuredClone(getAssiduite(etudid, assi)[0]) obj.moduleimpl = edit.moduleimpl_id; obj.etat = edit.etat; replaceAssiduite(etudid, assi, obj) launchToast(etudid, etat); updateAllCol() }) } break; } } function getAssiduite(etudid, id) { return assiduites[etudid].filter((a) => a.assiduite_id == id) } function replaceAssiduite(etudid, id, obj) { assiduites[etudid] = assiduites[etudid].filter((a) => a.assiduite_id != id); assiduites[etudid].push(obj) } function asyncCreateAssiduite(assi, callback = () => { }) { const path = getUrl() + `/api/assiduite/${assi.etudid}/create`; async_post( path, [assi], callback, (data, status) => { //error console.error(data, status); errorAlert(); } ); } function asyncRemoveAssiduite(assi, callback = () => { }) { const path = getUrl() + `/api/assiduite/delete`; async_post( path, [assi], callback, (data, status) => { //error console.error(data, status); errorAlert(); } ); } function asyncEditAssiduite(assi, callback = () => { }) { const path = getUrl() + `/api/assiduite/${assi.assiduite_id}/edit`; async_post( path, assi, callback, (data, status) => { //error console.error(data, status); errorAlert(); } ); } function asyncCreateAssiduiteGroup(assis, callback = () => { }) { const path = getUrl() + `/api/assiduites/create`; return async_post( path, assis, callback, (data, status) => { //error console.error(data, status); errorAlert(); } ); } function asyncEditAssiduiteGroup(assis, callback = () => { }) { const path = getUrl() + `/api/assiduites/edit`; return async_post( path, assis, callback, (data, status) => { console.error(data, status); errorAlert(); } ); } function asyncDeleteAssiduiteGroup(assis, callback = () => { }) { const path = getUrl() + `/api/assiduite/delete`; async_post( path, assis, callback, (data, status) => { //error console.error(data, status); errorAlert(); } ); } function massCol(rbtn) { const colid = rbtn.name.replace("mass_action_", ""); const col = document.querySelector(`[col="${colid}"]`); const etat = rbtn.value; const { moduleimpl, deb, fin } = getAssiduitesCol(colid, false); const lines = [...document.querySelectorAll(`[assiduite_id][colid='${colid}']`)].filter((el) => { return ["conflit", "insc"].indexOf(el.getAttribute('assiduite_id')) == -1; }) const toCreate = lines.filter((el) => { return el.getAttribute('assiduite_id') == '-1' }) const toEdit = lines.filter((el) => { return el.getAttribute('assiduite_id') != '-1' }) if (etat == "remove") { const removeList = [] toEdit.forEach((el) => { removeList.push(Number.parseInt(el.getAttribute('assiduite_id'))) }) asyncDeleteAssiduiteGroup(removeList, () => { const span = document.createElement("span"); if (removeList.length > 0) { span.innerHTML = `${removeList.length} assiduités ont été supprimées.`; } else { span.innerHTML = `Aucune assiduité n'a été supprimée.`; } toEdit.forEach((el) => { const assi_id = Number.parseInt(el.getAttribute("assiduite_id")); const etudid = Number.parseInt(el.parentElement.getAttribute('etudid')); assiduites[etudid] = assiduites[etudid].filter((a) => { return a.assiduite_id != assi_id }); }) updateAllCol() pushToast(generateToast(span, getToastColorFromEtat("remove"), 5)); }) } else { const createList = [] const editList = [] toCreate.forEach((el) => { const etudid = Number.parseInt(el.parentElement.getAttribute('etudid')); createList.push(_createAssiduites(deb, fin, moduleimpl, etudid, etat, colid)) }) toEdit.forEach((el) => { const edit = { moduleimpl_id: moduleimpl == "" ? null : moduleimpl, etat: etat, assiduite_id: Number.parseInt(el.getAttribute('assiduite_id')), etudid: Number.parseInt(el.parentElement.getAttribute('etudid')), } editList.push(edit); }) $.when( asyncCreateAssiduiteGroup(createList, (data) => { }), asyncEditAssiduiteGroup(editList, (data) => { }) ).done((c, e) => { Object.keys(c[0].success).forEach((k) => { const assiduite = createList[Number.parseInt(k)]; assiduite["assiduite_id"] = c[0].success[k].message.assiduite_id; assiduites[assiduite.etudid].push(assiduite); }) Object.keys(e[0].success).forEach((k) => { const { etudid, assiduite_id, moduleimpl_id, etat } = editList[Number.parseInt(k)] assiduites[etudid].map((a) => { if (a.assiduite_id == assiduite_id) { a.moduleimpl_id = moduleimpl_id a.etat = etat.toUpperCase(); } else { return a } }) }) updateAllCol(); const count = toCreate.length + toEdit.length; const etatAffiche = etat.toUpperCase() == "RETARD" ? "En retard" : etat; const span = document.createElement("span"); if (count > 0) { span.innerHTML = `${count} étudiants ont été mis <u><strong>${etatAffiche .capitalize() .trim()}</strong></u>`; } else { span.innerHTML = `Aucun étudiant n'a été mis <u><strong>${etatAffiche .capitalize() .trim()}</strong></u>`; } pushToast( generateToast( span, getToastColorFromEtat(etat.toUpperCase()), 5 ) ); }) } rbtn.checked = false; } function editModuleImpl(select) { const col = select.parentElement.parentElement const colid = col.getAttribute('col') const value = select.value; const lines = [...document.querySelectorAll(`[assiduite_id][colid='${colid}']`)].filter((el) => { return el.getAttribute('assiduite_id') != "conflit"; }) const toEdit = lines.filter((el) => { return el.getAttribute('assiduite_id') != '-1' }) let editList = toEdit.map((e) => { return { assiduite_id: Number.parseInt(e.getAttribute('assiduite_id')), etudid: Number.parseInt(e.parentElement.getAttribute('etudid')), moduleimpl_id: value, } }); asyncEditAssiduiteGroup(editList, (data) => { Object.keys(data.success).forEach((k) => { const { etudid, assiduite_id, moduleimpl_id } = editList[Number.parseInt(k)] assiduites[etudid].map((a) => { if (a.assiduite_id == assiduite_id) { a.moduleimpl_id = moduleimpl_id } else { return a } }) }) if (Object.keys(data.success).length > 0) { const span = document.createElement("span"); span.innerHTML = `Le module a bien été changé.`; pushToast(generateToast(span, getToastColorFromEtat("remove"), 5)); } }) } function checkInscriptionModule(moduleimpl_id, etudid) { if (!inscriptionsModule.hasOwnProperty(moduleimpl_id)) { const path = getUrl() + `/api/moduleimpl/${moduleimpl_id}/inscriptions`; sync_get( path, (data, status) => { inscriptionsModule[moduleimpl_id] = data; }, (data, status) => { //error console.error(data, status); errorAlert(); } ); } const etudsInscrits = inscriptionsModule[moduleimpl_id].map((i) => i.etudid); return etudsInscrits.indexOf(Number(etudid)) !== -1; } window.addEventListener('load', () => { document.getElementById("addColumn").addEventListener("click", () => { createColumn(); }); setEtuds(); document.querySelectorAll('.pdp-hover').forEach((el) => { el.src = getUrl() + `/api/etudiant/etudid/${el.getAttribute('etudid')}/photo?size=small`; }) }, { once: true }); </script>