Assiduites : préférences - granularité

This commit is contained in:
iziram 2023-05-30 10:17:49 +02:00
parent 54db0d70d5
commit cfa209a24b
7 changed files with 221 additions and 185 deletions

View File

@ -30,7 +30,7 @@ Formulaire configuration Module Assiduités
"""
from flask_wtf import FlaskForm
from wtforms import SubmitField
from wtforms import SubmitField, DecimalField
from wtforms.fields.simple import StringField
from wtforms.widgets import TimeInput
import datetime
@ -82,5 +82,7 @@ class ConfigAssiduitesForm(FlaskForm):
lunch_time = TimeField("Heure de midi (date pivot entre Matin et Après Midi)")
afternoon_time = TimeField("Fin de la journée")
tick_time = DecimalField("Granularité de la Time Line (temps en minutes)", places=0)
submit = SubmitField("Valider")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

View File

@ -439,9 +439,9 @@ function hideLoader() {
*/
function toTime(time) {
let heure = Math.floor(time);
let minutes = (time - heure) * 60;
if (minutes < 1) {
minutes = "00";
let minutes = Math.round((time - heure) * 60);
if (minutes < 10) {
minutes = `0${minutes}`;
}
if (heure < 10) {
heure = `0${heure}`;
@ -997,7 +997,6 @@ function generateEtudRow(
</div>
<div class="assiduites_bar">
<div id="prevDateAssi" class="${assiduite.prevAssiduites?.etat?.toLowerCase()}">
<span class="mini_tick">13h</span>
</div>
</div>
<fieldset class="btns_field single" etudid="${etud.id}" assiduite_id="${
@ -1065,172 +1064,6 @@ function insertEtudRow(etud, index, output = false) {
}
}
/**
* Création de la minitiline d'un étudiant
* @param {Array[Assiduité]} assiduitesArray
* @returns {HTMLElement} l'élément correspondant à la mini timeline
*/
function createMiniTimeline(assiduitesArray) {
const array = [...assiduitesArray];
const dateiso = document.getElementById("tl_date").value;
const timeline = document.createElement("div");
timeline.className = "mini-timeline";
if (isSingleEtud()) {
timeline.classList.add("single");
}
const timelineDate = moment(dateiso).startOf("day");
const dayStart = timelineDate.clone().add(8, "hours");
const dayEnd = timelineDate.clone().add(18, "hours");
const dayDuration = moment.duration(dayEnd.diff(dayStart)).asMinutes();
const tlTimes = getTimeLineTimes();
const period_assi = {
date_debut: tlTimes.deb.format(),
date_fin: tlTimes.fin.format(),
etat: "CRENEAU",
};
array.push(period_assi);
array.forEach((assiduité) => {
const startDate = moment(assiduité.date_debut);
const endDate = moment(assiduité.date_fin);
if (startDate.isBefore(dayStart)) {
startDate.startOf("day").add(8, "hours");
}
if (endDate.isAfter(dayEnd)) {
endDate.startOf("day").add(18, "hours");
}
const block = document.createElement("div");
block.className = "mini-timeline-block";
const startOffset = moment.duration(startDate.diff(dayStart)).asMinutes();
const duration = moment.duration(endDate.diff(startDate)).asMinutes();
const leftPercentage = (startOffset / dayDuration) * 100;
const widthPercentage = (duration / dayDuration) * 100;
block.style.left = `${leftPercentage}%`;
block.style.width = `${widthPercentage}%`;
if (assiduité.etat != "CRENEAU") {
if (isSingleEtud()) {
block.addEventListener("click", () => {
let deb = startDate.hours() + startDate.minutes() / 60;
let fin = endDate.hours() + endDate.minutes() / 60;
deb = Math.max(8, deb);
fin = Math.min(18, fin);
setPeriodValues(deb, fin);
updateSelectedSelect(getCurrentAssiduiteModuleImplId());
updateJustifyBtn();
});
}
//ajouter affichage assiduites on over
setupAssiduiteBuble(block, assiduité);
}
const action = (justificatifs) => {
if (justificatifs.length > 0) {
let j = "invalid_justified";
justificatifs.forEach((ju) => {
if (ju.etat == "VALIDE") {
j = "justified";
}
});
block.classList.add(j);
}
};
if (assiduité.etudid) {
getJustificatifFromPeriod(
{
deb: new moment.tz(assiduité.date_debut, TIMEZONE),
fin: new moment.tz(assiduité.date_fin, TIMEZONE),
},
assiduité.etudid,
action
);
}
switch (assiduité.etat) {
case "PRESENT":
block.classList.add("present");
break;
case "RETARD":
block.classList.add("retard");
break;
case "ABSENT":
block.classList.add("absent");
break;
case "CRENEAU":
block.classList.add("creneau");
break;
default:
block.style.backgroundColor = "white";
}
timeline.appendChild(block);
});
return timeline;
}
/**
* Ajout de la visualisation des assiduités de la mini timeline
* @param {HTMLElement} el l'élément survollé
* @param {Assiduité} assiduite l'assiduité représentée par l'élément
*/
function setupAssiduiteBuble(el, assiduite) {
if (!assiduite) return;
el.addEventListener("mouseenter", (event) => {
const bubble = document.querySelector(".assiduite-bubble");
bubble.className = "assiduite-bubble";
bubble.classList.add("is-active", assiduite.etat.toLowerCase());
bubble.innerHTML = "";
const idDiv = document.createElement("div");
idDiv.className = "assiduite-id";
idDiv.textContent = `ID: ${assiduite.assiduite_id}`;
bubble.appendChild(idDiv);
const periodDivDeb = document.createElement("div");
periodDivDeb.className = "assiduite-period";
periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`;
bubble.appendChild(periodDivDeb);
const periodDivFin = document.createElement("div");
periodDivFin.className = "assiduite-period";
periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`;
bubble.appendChild(periodDivFin);
const stateDiv = document.createElement("div");
stateDiv.className = "assiduite-state";
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
bubble.appendChild(stateDiv);
const userIdDiv = document.createElement("div");
userIdDiv.className = "assiduite-user_id";
userIdDiv.textContent = `saisi le ${formatDateModal(
assiduite.entry_date,
"à"
)} \npar ${getUserFromId(assiduite.user_id)}`;
bubble.appendChild(userIdDiv);
bubble.style.left = `${event.clientX - bubble.offsetWidth / 2}px`;
bubble.style.top = `${event.clientY + 20}px`;
});
el.addEventListener("mouseout", () => {
const bubble = document.querySelector(".assiduite-bubble");
bubble.classList.remove("is-active");
});
}
/**
* Mise à jour d'une ligne étudiant
* @param {String | Number} etudid l'identifiant de l'étudiant

View File

@ -14,6 +14,7 @@
{{ wtf.form_field(form.morning_time) }}
{{ wtf.form_field(form.lunch_time) }}
{{ wtf.form_field(form.afternoon_time) }}
{{ wtf.form_field(form.tick_time) }}
<div class="form-group">
{{ wtf.form_field(form.submit) }}
{{ wtf.form_field(form.cancel) }}

View File

@ -3,6 +3,189 @@
</div>
<script>
const mt_start = {{ t_start }};
const mt_end = {{ t_end }};
/**
* Création de la minitiline d'un étudiant
* @param {Array[Assiduité]} assiduitesArray
* @returns {HTMLElement} l'élément correspondant à la mini timeline
*/
function createMiniTimeline(assiduitesArray) {
const array = [...assiduitesArray];
const dateiso = document.getElementById("tl_date").value;
const timeline = document.createElement("div");
timeline.className = "mini-timeline";
if (isSingleEtud()) {
timeline.classList.add("single");
}
const timelineDate = moment(dateiso).startOf("day");
const dayStart = timelineDate.clone().add(mt_start, "hours");
const dayEnd = timelineDate.clone().add(mt_end, "hours");
const dayDuration = moment.duration(dayEnd.diff(dayStart)).asMinutes();
timeline.appendChild(setMiniTick(timelineDate, dayStart, dayDuration));
const tlTimes = getTimeLineTimes();
const period_assi = {
date_debut: tlTimes.deb.format(),
date_fin: tlTimes.fin.format(),
etat: "CRENEAU",
};
array.push(period_assi);
array.forEach((assiduité) => {
const startDate = moment(assiduité.date_debut);
const endDate = moment(assiduité.date_fin);
if (startDate.isBefore(dayStart)) {
startDate.startOf("day").add(mt_start, "hours");
}
if (endDate.isAfter(dayEnd)) {
endDate.startOf("day").add(mt_end, "hours");
}
const block = document.createElement("div");
block.className = "mini-timeline-block";
const duration = moment.duration(endDate.diff(startDate)).asMinutes();
const startOffset = moment.duration(startDate.diff(dayStart)).asMinutes();
const leftPercentage = (startOffset / dayDuration) * 100;
const widthPercentage = (duration / dayDuration) * 100;
block.style.left = `${leftPercentage}%`;
block.style.width = `${widthPercentage}%`;
if (assiduité.etat != "CRENEAU") {
if (isSingleEtud()) {
block.addEventListener("click", () => {
let deb = startDate.hours() + startDate.minutes() / 60;
let fin = endDate.hours() + endDate.minutes() / 60;
deb = Math.max(mt_start, deb);
fin = Math.min(mt_end, fin);
setPeriodValues(deb, fin);
updateSelectedSelect(getCurrentAssiduiteModuleImplId());
updateJustifyBtn();
});
}
//ajouter affichage assiduites on over
setupAssiduiteBuble(block, assiduité);
}
const action = (justificatifs) => {
if (justificatifs.length > 0) {
let j = "invalid_justified";
justificatifs.forEach((ju) => {
if (ju.etat == "VALIDE") {
j = "justified";
}
});
block.classList.add(j);
}
};
if (assiduité.etudid) {
getJustificatifFromPeriod(
{
deb: new moment.tz(assiduité.date_debut, TIMEZONE),
fin: new moment.tz(assiduité.date_fin, TIMEZONE),
},
assiduité.etudid,
action
);
}
switch (assiduité.etat) {
case "PRESENT":
block.classList.add("present");
break;
case "RETARD":
block.classList.add("retard");
break;
case "ABSENT":
block.classList.add("absent");
break;
case "CRENEAU":
block.classList.add("creneau");
break;
default:
block.style.backgroundColor = "white";
}
timeline.appendChild(block);
});
return timeline;
}
/**
* Ajout de la visualisation des assiduités de la mini timeline
* @param {HTMLElement} el l'élément survollé
* @param {Assiduité} assiduite l'assiduité représentée par l'élément
*/
function setupAssiduiteBuble(el, assiduite) {
if (!assiduite) return;
el.addEventListener("mouseenter", (event) => {
const bubble = document.querySelector(".assiduite-bubble");
bubble.className = "assiduite-bubble";
bubble.classList.add("is-active", assiduite.etat.toLowerCase());
bubble.innerHTML = "";
const idDiv = document.createElement("div");
idDiv.className = "assiduite-id";
idDiv.textContent = `ID: ${assiduite.assiduite_id}`;
bubble.appendChild(idDiv);
const periodDivDeb = document.createElement("div");
periodDivDeb.className = "assiduite-period";
periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`;
bubble.appendChild(periodDivDeb);
const periodDivFin = document.createElement("div");
periodDivFin.className = "assiduite-period";
periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`;
bubble.appendChild(periodDivFin);
const stateDiv = document.createElement("div");
stateDiv.className = "assiduite-state";
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
bubble.appendChild(stateDiv);
const userIdDiv = document.createElement("div");
userIdDiv.className = "assiduite-user_id";
userIdDiv.textContent = `saisi le ${formatDateModal(
assiduite.entry_date,
"à"
)} \npar ${getUserFromId(assiduite.user_id)}`;
bubble.appendChild(userIdDiv);
bubble.style.left = `${event.clientX - bubble.offsetWidth / 2}px`;
bubble.style.top = `${event.clientY + 20}px`;
});
el.addEventListener("mouseout", () => {
const bubble = document.querySelector(".assiduite-bubble");
bubble.classList.remove("is-active");
});
}
function setMiniTick(timelineDate, dayStart, dayDuration) {
const endDate = timelineDate.clone().set({ 'hour': 13, 'minute': 0 });
const duration = moment.duration(endDate.diff(dayStart)).asMinutes();
const widthPercentage = (duration / dayDuration) * 100;
console.log(endDate, duration, widthPercentage)
const tick = document.createElement('span');
tick.className = "mini_tick"
tick.textContent = "13h"
tick.style.left = `${widthPercentage}%`
return tick
}
</script>
<style>
@ -87,10 +270,9 @@
.mini_tick {
position: absolute;
text-align: center;
top: -16px;
left: 50%;
text-align: start;
top: -40px;
transform: translateX(-50%)
}
.mini_tick::after {

View File

@ -12,6 +12,9 @@
const t_start = {{ t_start }};
const t_end = {{ t_end }};
const tick_time = 60 / {{ tick_time }}
const tick_delay = 1 / tick_time
const period_default = {{ periode_defaut }};
function createTicks() {
@ -33,7 +36,7 @@
let j = Math.floor(i + 1)
while (i < j) {
i += 0.25;
i += tick_delay;
if (i <= t_end) {
const quarterTick = document.createElement("div");
@ -43,6 +46,7 @@
}
}
i = j
}
}
}
@ -66,7 +70,9 @@
}
function snapToQuarter(value) {
return Math.round(value * 4) / 4;
return Math.round(value * tick_time) / tick_time;
}
function setupTimeLine(callback) {
@ -153,8 +159,8 @@
const startHour = (leftPercentage / 100) * (t_end - t_start) + t_start;
const endHour = ((leftPercentage + widthPercentage) / 100) * (t_end - t_start) + t_start;
const startValue = Math.round(startHour * 4) / 4;
const endValue = Math.round(endHour * 4) / 4;
const startValue = snapToQuarter(startHour);
const endValue = snapToQuarter(endHour);
const computedValues = [Math.max(startValue, t_start), Math.min(t_end, endValue)]
@ -162,8 +168,8 @@
return [t_start, min(t_end, t_start + period_default)]
}
if (computedValues[1] - computedValues[0] <= 0.25 && computedValues[1] < t_end - 0.25) {
computedValues[1] += 0.25;
if (computedValues[1] - computedValues[0] <= tick_delay && computedValues[1] < t_end - tick_delay) {
computedValues[1] += tick_delay;
}
return computedValues
@ -183,13 +189,13 @@
function snapHandlesToQuarters() {
const periodValues = getPeriodValues();
let lef = Math.min(computePercentage(periodValues[0], t_start), computePercentage(t_end, 0.25))
let lef = Math.min(computePercentage(periodValues[0], t_start), computePercentage(t_end, tick_delay))
if (lef < 0) {
lef = 0;
}
const left = `${lef}%`
let wid = Math.max(computePercentage(periodValues[1], periodValues[0]), computePercentage(0.25, 0))
let wid = Math.max(computePercentage(periodValues[1], periodValues[0]), computePercentage(tick_delay, 0))
if (wid > 100) {
wid = 100;
}

View File

@ -198,7 +198,7 @@ def signal_assiduites_etud():
return HTMLBuilder(
header,
render_template("assiduites/minitimeline.j2"),
_mini_timeline(),
render_template(
"assiduites/signal_assiduites_etud.j2",
sco=ScoData(etud),
@ -385,7 +385,7 @@ def signal_assiduites_group():
return HTMLBuilder(
header,
render_template("assiduites/minitimeline.j2"),
_mini_timeline(),
render_template(
"assiduites/signal_assiduites_group.j2",
gr_tit=gr_tit,
@ -449,7 +449,16 @@ def _timeline(formsemestre_id=None) -> HTMLElement:
"assiduites/timeline.j2",
t_start=get_time("assi_morning_time", "08:00:00"),
t_end=get_time("assi_afternoon_time", "18:00:00"),
tick_time=ScoDocSiteConfig.get("assi_tick_time", 0.25),
periode_defaut=sco_preferences.get_preference(
"periode_defaut", formsemestre_id
),
)
def _mini_timeline() -> HTMLElement:
return render_template(
"assiduites/minitimeline.j2",
t_start=get_time("assi_morning_time", "08:00:00"),
t_end=get_time("assi_afternoon_time", "18:00:00"),
)

View File

@ -203,6 +203,8 @@ def config_assiduites():
flash("Heure de midi enregistrée")
if ScoDocSiteConfig.set("assi_afternoon_time", form.data["afternoon_time"]):
flash("Heure de fin de la journée enregistrée")
if ScoDocSiteConfig.set("assi_tick_time", form.data["tick_time"]):
flash("Granularité de la timeline enregistrée")
return redirect(url_for("scodoc.configuration"))
elif request.method == "GET":
@ -215,6 +217,7 @@ def config_assiduites():
form.afternoon_time.data = ScoDocSiteConfig.get(
"assi_afternoon_time", datetime.time(18, 0, 0)
)
form.tick_time.data = float(ScoDocSiteConfig.get("assi_tick_time", 0.25))
return render_template(
"assiduites/config_assiduites.j2",
form=form,