ScoDoc-PE/app/templates/assiduites/differee.j2

862 lines
26 KiB
Plaintext
Raw Normal View History

<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">
<span>{{etud.nomprenom}}</span>
<img class="pdp-hover" src="/ScoDoc/GEII/api/etudiant/etudid/{{etud.etudid}}/photo?size=small">
</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;
}
.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 {
padding: 20px;
text-align: center;
width: 225px;
border: 1px solid #ddd;
display: inline-block;
}
.tr {
display: flex;
justify-content: flex-start;
align-items: center;
width: max-content;
}
.sticky {
position: sticky;
left: 0;
background-color: #fafafa;
border-right: 1px solid #ddd;
}
.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: #007BFF;
color: white;
border: none;
outline: none;
cursor: pointer;
transition: background-color 0.3s;
}
#addColumn:hover {
background-color: #0056b3;
}
.th {
background-color: #007BFF;
color: white;
position: relative;
}
.th.error {
background-color: crimson;
}
.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;
}
</style>
<script>
const etatDef = "{{etat_def}}";
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");
}
function createColumn() {
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="mini-form">
<div class="btngroup">
<span class="num">${col_id}</span>
<button class="closeCol" onclick="removeColumn(this)">x</button>
</div>
<input type="datetime-local" id="dateStart">
<input type="datetime-local" id="dateEnd">
{{moduleimpl_select|safe}}
<select name="mass_action_${col_id}" id="mass_action_${col_id}" onchange="setEtatCol(${col_id},this.value)" disabled>
<option value="">Sélectionner une assiduité</option>
<option value="0">Présent</option>
<option value="1">Retard</option>
<option value="2">Absent</option>
</select>
</div>
`;
table
.querySelector(".thead .tr")
.insertBefore(th, document.querySelector("#addColumn"));
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);
})
})
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">
<input type="radio" name="etat_${col_id}_${etudid}" value="present" disabled onchange="updateEtudAssiduite(this)">
<input type="radio" name="etat_${col_id}_${etudid}" value="retard" disabled onchange="updateEtudAssiduite(this)">
<input type="radio" name="etat_${col_id}_${etudid}" value="absent" disabled onchange="updateEtudAssiduite(this)">
</div>
<br/>
<div class="assi_lbl">
<span>Present</span>
<span>Retard</span>
<span>Absent</span>
</div>
`;
rows[i].appendChild(td);
if (etatDef != "" && etatDef != "aucun") {
const inp = td.querySelector(`[value='${etatDef}']`).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;
}
}
function getEtatCol(colId) {
const etats = {};
const tds = [...document.querySelectorAll(`.td[colid='${colId}']`)]
tds.forEach((td) => {
const tr = td.parentElement
const etudid = tr.getAttribute("etudid");
let inputs = [...td.querySelectorAll("input")]
etatInput = inputs.filter((e) => e.checked).pop()
if (etatInput == undefined) {
etats[etudid] = "";
} else {
etats[etudid] = etatInput.value;
}
})
return etats;
}
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 getAndVerify() {
const assiduites = [];
const cols = [...document.querySelectorAll("[col]")];
const errors = [];
cols.forEach((col) => {
const col_id = col.getAttribute("col");
const etats = getEtatCol(col_id);
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);
const d_fin = moment(inputFin);
if (inputDeb == "" || inputFin == "" || d_debut > d_fin) {
errors.push(`La période de la colonne n°${col_id} n'est pas valide`);
return;
}
{% if periode %}
const testPeriode = [
d_debut.isBefore("{{periode.deb}}"),
d_debut.isAfter("{{periode.fin}}"),
d_fin.isBefore("{{periode.deb}}"),
d_fin.isAfter("{{periode.fin}}"),
]
if (testPeriode.some((e) => e)) {
errors.push(`La période de la colonne n°${col_id} n'est pas dans le semestre`);
return;
}
{% endif %}
if (window.forceModule && moduleSelect == "") {
errors.push(`Le module de la colonne n°${col_id} n'a pas été entré. (Préférence de semestre)`);
return;
}
// TODO Mettre une erreur lorsque assiduité forcé (pref)
Object.keys(etats).forEach((key) => {
const etat = etats[key];
if (etat != "") {
assiduites.push(_createAssiduites(inputDeb, inputFin, moduleSelect, key, etat, col_id))
}
})
});
if (errors.length > 0) {
const texte = document.createElement("div");
errors.map((err) => document.createTextNode(err)).forEach((err) => {
texte.appendChild(err);
texte.appendChild(document.createElement('br'));
})
openAlertModal("Erreur(s) détéctée(s)", texte)
} else {
if (assiduites.length > 0) {
createAllAssiduites(assiduites);
}
}
}
function createAllAssiduites(createQueue) {
if (createQueue.length < 0)
return;
const path = getUrl() + `/api/assiduites/create`;
sync_post(
path,
createQueue,
(data, status) => {
const { success, errors } = data;
const indexes = [...Object.keys(errors)];
if (indexes.length > 0) {
const incriminated = indexes.map((i) => {
return createQueue[Number.parseInt(i)];
})
const error_message = document.createElement('div');
for (let i = 0; i < incriminated.length; i++) {
const err = errors[indexes[i]];
const crimi = incriminated[i];
const nom = document.querySelector(`[etudid='${crimi.etudid}']`).firstElementChild.textContent.trim();
const col = crimi.colid;
const div = document.createElement('div');
div.classList.add("err-assi")
const span = document.createElement("span");
span.setAttribute("title", err);
span.textContent = ""
const span2 = document.createElement("span");
span2.textContent = `L'assiduité (Colonne n°${col}) de ${nom} n'a pas pu être enregistrée`;
div.appendChild(span2);
div.appendChild(span);
error_message.appendChild(div);
}
openAlertModal("Certaines assiduités non pas été enregistrées", error_message)
} else {
openAlertModal("Tous les assiduités ont bien été enregistrée", document.createTextNode(""), null, "#09AD2A")
}
},
(data, status) => {
//error
console.error(data, status);
}
);
}
function getAssiduitesCol(col_id, get = true) {
const col = document.querySelector(`[col='${col_id}']`);
const etats = getEtatCol(col_id);
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) {
//errors.push(`La période de la colonne n°${col_id} 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)) {
//errors.push(`La période de la colonne n°${col_id} n'est pas dans le semestre`);
return 0x3;
}
{% endif %}
if (window.forceModule && moduleSelect == "") {
//errors.push(`Le module de la colonne n°${col_id} n'a pas été entré. (Préférence de semestre)`);
return 0x4;
}
if (get) {
getAssiduitesFromEtuds(false, 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 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 conflits = getAssiduitesConflict(etu, periode);
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;
i.addEventListener('click', () => { /*updateEtudAssiduite(i)*/ });
})
}
})
}
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) {
if (getAssiduitesCol(colid) == 0) {
document.querySelector(`[col='${colid}']`).classList.remove('error');
document.getElementById(`mass_action_${colid}`).removeAttribute('disabled')
updateAssiduitesCol(colid)
} else {
const column = document.querySelector(`[col='${colid}']`)
document.getElementById(`mass_action_${colid}`).setAttribute('disabled', 'true')
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) => {
updateAssiduitesCol(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 = `${etuds[etudid].nom.toUpperCase()} ${etuds[
etudid
].prenom.capitalize()}`;
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);
asyncCreateAssiduite(assiduite, (data) => {
if (Object.keys(data.success).length > 0) {
const assi_id = data.success['0'].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();
break;
default:
// Une assiduité est déjà connue -> modifier assiduité
const edit = {
moduleimpl_id: moduleimpl == "" ? null : moduleimpl,
etat: etat,
assiduite_id: Number.parseInt(assi),
}
asyncEditAssiduite(edit, (data) => {
const obj = getAssiduite(etudid, assi);
obj.moduleimpl = edit.moduleimpl_id;
obj.etat = edit.etat;
launchToast(etudid, etat);
})
break;
}
}
function getAssiduite(etudid, id) {
return assiduites[etudid].filter((a) => a.assiduite_id == id)
}
function asyncCreateAssiduite(assi, callback = () => { }) {
const path = getUrl() + `/api/assiduite/${assi.etudid}/create`;
async_post(
path,
[assi],
callback,
(data, status) => {
//error
console.error(data, status);
}
);
}
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);
}
);
}
window.addEventListener('load', () => {
document.getElementById("addColumn").addEventListener("click", () => {
createColumn();
});
createColumn();
setEtuds();
}, { once: true });
</script>