Compare commits

...

12 Commits

18 changed files with 2134 additions and 3085 deletions

View File

@ -25,8 +25,8 @@
#
##############################################################################
"""Tableau de bord module
"""
"""Tableau de bord module"""
import math
import time
import datetime
@ -329,8 +329,6 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
>Saisie Absences journée</a></span>
"""
)
year, week, day = datetime.date.today().isocalendar()
semaine: str = f"{year}-W{week}"
H.append(
f"""
<span class="moduleimpl_abs_link"><a class="stdlink" href="{
@ -338,11 +336,10 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
"assiduites.signal_assiduites_diff",
scodoc_dept=g.scodoc_dept,
group_ids=group_id,
semaine=semaine,
formsemestre_id=formsemestre.id,
moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
)}"
>Saisie Absences hebdo</a></span>
>Saisie Absences Différée</a></span>
"""
)

View File

@ -6,7 +6,9 @@
--color-justi: #29b990;
--color-justi-clair: #48f6ff;
--color-justi-attente: yellow;
--color-justi-attente-stripe: #29b990; /* pink #fa25cb; */ /* #789dbb;*/
--color-justi-attente-stripe: #29b990;
/* pink #fa25cb; */
/* #789dbb;*/
--color-justi-modifie: rgb(255, 230, 0);
--color-justi-invalide: #a84476;
--color-nonwork: #badfff;
@ -28,27 +30,23 @@
--color-defaut-dark: #444;
--color-default-text: #1f1f1f;
--motif-justi: repeating-linear-gradient(
135deg,
transparent,
transparent 4px,
var(--color-justi) 4px,
var(--color-justi) 8px
);
--motif-justi-invalide: repeating-linear-gradient(
-135deg,
transparent,
transparent 4px,
var(--color-justi-invalide) 4px,
var(--color-justi-invalide) 8px
);
--motif-justi: repeating-linear-gradient(135deg,
transparent,
transparent 4px,
var(--color-justi) 4px,
var(--color-justi) 8px);
--motif-justi-invalide: repeating-linear-gradient(-135deg,
transparent,
transparent 4px,
var(--color-justi-invalide) 4px,
var(--color-justi-invalide) 8px);
}
* {
box-sizing: border-box;
}
.selectors > * {
.selectors>* {
margin: 10px 0;
}
@ -339,6 +337,11 @@
background-image: url(../icons/retard.svg);
}
.rbtn.conflit::before {
background-color: var(--color-absent);
background-image: url(../icons/solveur_conflits.svg);
}
.rbtn:checked:before {
outline: 5px solid var(--color-primary);
border-radius: 50%;
@ -405,29 +408,11 @@
.assiduite {
position: absolute;
top: 20px;
cursor: pointer;
border-radius: 4px;
z-index: 10;
height: 100px;
padding: 4px;
}
.assiduite-info {
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-between;
}
.assiduite-id,
.assiduite-period,
.assiduite-state,
.assiduite-user_id {
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
border: 1px solid #444;
}
.assiduites-container {
@ -438,7 +423,7 @@
margin-bottom: 10px;
}
.action-buttons {
.modal-buttons {
position: absolute;
text-align: center;
display: flex;
@ -449,48 +434,38 @@
bottom: 5%;
}
/* Ajout de la classe CSS pour la bordure en pointillés */
.assiduite.selected {
border: 2px dashed black;
}
.assiduite-special {
height: 120px;
position: absolute;
z-index: 5;
border: 2px solid #000;
background-color: rgba(36, 36, 36, 0.25);
background-image: repeating-linear-gradient(
135deg,
transparent,
transparent 5px,
rgba(81, 81, 81, 0.61) 5px,
rgba(81, 81, 81, 0.61) 10px
);
border: 5px solid var(--color-primary);
border-radius: 5px;
}
/*<== Info sur l'assiduité sélectionnée ==>*/
.modal-assiduite-content {
background-color: #fefefe;
margin: 5% auto;
padding: 20px;
border: 1px solid #888;
width: max-content;
position: relative;
border-radius: 10px;
display: none;
.assiduite .assiduite-bubble {
top: 5px;
left: 50%;
transform: translateX(-50%);
}
.modal-assiduite-content.show {
display: block;
.assiduite-infos {
position: absolute;
right: 0;
margin: 5px;
top: 0;
font-size: 16px;
cursor: pointer;
}
.modal-assiduite-content .infos {
.action-buttons {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: flex-start;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 2px;
height: 100%;
}
/*<=== Mass Action ==>*/
@ -500,57 +475,16 @@
justify-content: flex-start;
align-items: center;
width: 100%;
margin: 2% 0;
gap: 4px;
}
.mass-selection span {
margin: 0 1%;
}
.mass-selection .rbtn {
background-color: transparent;
cursor: pointer;
}
/*<== Loader ==> */
.loader-container {
display: none;
/* Cacher le loader par défaut */
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
/* Fond semi-transparent pour bloquer les clics */
z-index: 9999;
/* Placer le loader au-dessus de tout le contenu */
}
.loader {
border: 6px solid #f3f3f3;
border-radius: 50%;
border-top: 6px solid var(--color-primary);
width: 60px;
height: 60px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
.fieldsplit {
display: flex;
justify-content: flex-start;
@ -569,7 +503,7 @@
flex-direction: column;
}
#page-assiduite-content > * {
#page-assiduite-content>* {
margin: 1.5% 0;
}
@ -649,6 +583,7 @@
margin-right: 24px;
padding: 12px;
}
#options-tableau label {
font-weight: normal;
margin-right: 12px;
@ -657,15 +592,20 @@
section.assi-form {
margin-bottom: 12px;
}
table.liste_assi td.date {
width: 140px;
}
table.liste_assi.dataTable tbody td.date-debut {
padding-left: 12px;
}
table.liste_assi td.actions {
white-space: nowrap; /* boutons horizontalement */
white-space: nowrap;
/* boutons horizontalement */
}
table.liste_assi td.actions a:last-child {
padding-right: 12px;
}
@ -673,31 +613,154 @@ table.liste_assi td.actions a:last-child {
tr.row-assiduite td {
border-bottom: 1px solid grey;
}
table.liste_assi tbody tr td.assi-type {
padding-left: 8px;
padding-right: 4px;
}
tr.row-assiduite.absent td.assi-type {
background-color: var(--color-absent-clair);
}
tr.row-assiduite.absent.justifiee td.assi-type {
background-color: var(--color-absent-justi);
}
tr.row-assiduite.retard td.assi-type {
background-color: var(--color-retard);
}
tr.row-assiduite.present td.assi-type {
background-color: var(--color-present);
}
tr.row-justificatif.valide td.assi-type {
background-color: var(--color-justi);
}
tr.row-justificatif.attente td.assi-type {
background-color: var(--color-justi-attente);
}
tr.row-justificatif.modifie td.assi-type {
background-color: var(--color-justi-modifie);
}
tr.row-justificatif.non_valide td.assi-type {
background-color: var(--color-justi-invalide);
}
/*
<== Loader ==>
*/
/* HTML: <div class="loader"></div> */
.loader {
width: 80px;
height: 70px;
border: 5px solid #000;
padding: 0 8px;
box-sizing: border-box;
background:
linear-gradient(#fff 0 0) 0 0/8px 20px,
linear-gradient(#fff 0 0) 100% 0/8px 20px,
radial-gradient(farthest-side, #fff 90%, #0000) 0 5px/8px 8px content-box,
#000;
background-repeat: no-repeat;
animation: l3 2s infinite linear;
}
@keyframes l3 {
25% {
background-position: 0 0, 100% 100%, 100% calc(100% - 5px)
}
50% {
background-position: 0 100%, 100% 100%, 0 calc(100% - 5px)
}
75% {
background-position: 0 100%, 100% 0, 100% 5px
}
}
#loader {
width: 100%;
height: 100%;
position: fixed;
top: 50%;
left: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transform: translate(-50%, -50%);
z-index: 1000;
background-color: rgba(255, 255, 255, 0.8);
}
/**
* <== Couleurs ==>
*/
.color.present {
background-color: var(--color-present) !important;
}
.color.absent {
background-color: var(--color-absent) !important;
}
.color.absent.est_just {
background-color: var(--color-absent-justi) !important;
}
.color.retard {
background-color: var(--color-retard) !important;
}
.color.retard.est_just {
background-color: var(--color-retard-justi) !important;
}
.color.nonwork {
background-color: var(--color-nonwork) !important;
}
.color {
background-color: var(--color-defaut) !important;
}
.color.est_just.sans_etat::before {
content: "";
position: absolute;
width: 25%;
height: 100%;
background-color: var(--color-justi) !important;
right: 0;
}
.color.invalide::before {
content: "";
position: absolute;
width: 25%;
height: 100%;
right: 0;
background-color: var(--color-justi-invalide) !important;
}
.color.attente::before,
.color.modifie::before {
content: "";
position: absolute;
width: 25%;
height: 100%;
right: 0;
background: repeating-linear-gradient(to bottom,
var(--color-justi-attente-stripe) 0px,
var(--color-justi-attente-stripe) 4px,
var(--color-justi-attente) 4px,
var(--color-justi-attente) 7px) !important;
}

View File

@ -94,7 +94,9 @@
top: 200%;
}
.mini-timeline-block:hover .assiduite-bubble {
.mini-timeline-block:hover .assiduite-bubble,
#prevDateAssi:hover .assiduite-bubble,
.assiduites-container .assiduite:hover .assiduite-bubble {
display: flex;
justify-content: center;
align-items: center;
@ -103,6 +105,11 @@
max-height: 150px;
}
#prevDateAssi:hover .assiduite-bubble {
transform: translateY(55%);
top: 0;
}
.assiduite-bubble::before {
content: "";
position: absolute;
@ -189,24 +196,4 @@
.mini-timeline-block.creneau {
outline: 3px solid var(--color-primary);
pointer-events: none;
}
.mini-timeline-block.absent {
background-color: var(--color-absent) !important;
}
.mini-timeline-block.present {
background-color: var(--color-present) !important;
}
.mini-timeline-block.retard {
background-color: var(--color-retard) !important;
}
.mini-timeline-block.justified {
background-image: var(--motif-justi);
}
.mini-timeline-block.invalid_justified {
background-image: var(--motif-justi-invalide);
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="85" width="85" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 482.568 482.568" xml:space="preserve">
<g>
<g opacity="0.7">
<path d="M116.993,203.218c13.4-1.8,26.8,2.8,36.3,12.3l24,24l22.7-22.6l-32.8-32.7c-5.1-5.1-5.1-13.4,0-18.5s13.4-5.1,18.5,0
l32.8,32.8l22.7-22.6l-24.1-24.1c-9.5-9.5-14.1-23-12.3-36.3c4-30.4-5.7-62.2-29-85.6c-23.8-23.8-56.4-33.4-87.3-28.8
c-4.9,0.7-6.9,6.8-3.4,10.3l30.9,30.9c14.7,14.7,14.7,38.5,0,53.1l-19,19c-14.7,14.7-38.5,14.7-53.1,0l-31-30.9
c-3.5-3.5-9.5-1.5-10.3,3.4c-4.6,30.9,5,63.5,28.8,87.3C54.793,197.518,86.593,207.218,116.993,203.218z"/>
<path d="M309.193,243.918l-22.7,22.6l134.8,134.8c5.1,5.1,5.1,13.4,0,18.5s-13.4,5.1-18.5,0l-134.8-134.8l-22.7,22.6l138.9,138.9
c17.6,17.6,46.1,17.5,63.7-0.1s17.6-46.1,0.1-63.7L309.193,243.918z"/>
<path d="M361.293,153.918h59.9l59.9-119.7l-29.9-29.9l-119.8,59.8v59.9l-162.8,162.3l-29.3-29.2l-118,118
c-24.6,24.6-24.6,64.4,0,89s64.4,24.6,89,0l118-118l-29.9-29.9L361.293,153.918z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because it is too large Load Diff

View File

@ -255,6 +255,13 @@ Object.defineProperty(Date.prototype, "format", {
value: function (formatString) {
let iso = this.toIsoUtcString();
switch (formatString) {
case "DD/MM/YYYY":
return this.toLocaleString("fr-FR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
timeZone: SCO_TIMEZONE,
});
case "DD/MM/Y HH:mm":
return this.toLocaleString("fr-FR", {
day: "2-digit",
@ -275,6 +282,8 @@ Object.defineProperty(Date.prototype, "format", {
hour12: false,
timeZone: SCO_TIMEZONE,
});
case "HH:mm":
return iso.slice(11, 16);
case "YYYY-MM-DDTHH:mm":
// slice : YYYY-MM-DDTHH
@ -407,196 +416,17 @@ class Duration {
}
}
class ScoDocDateTimePicker extends HTMLElement {
constructor() {
super();
// Définir si le champ est requis
this.required = this.hasAttribute("required");
// Initialiser le shadow DOM
const shadow = this.attachShadow({ mode: "open" });
// Créer l'input pour la date
const dateInput = document.createElement("input");
dateInput.type = "date";
dateInput.id = "date";
// Créer l'input pour l'heure
const timeInput = document.createElement("input");
timeInput.type = "time";
timeInput.id = "time";
timeInput.step = 60;
// Ajouter les inputs dans le shadow DOM
shadow.appendChild(dateInput);
shadow.appendChild(timeInput);
// Gestionnaires d'événements pour la mise à jour de la valeur
dateInput.addEventListener("change", () => this.updateValue());
timeInput.addEventListener("change", () => this.updateValue());
// Style CSS pour les inputs
const style = document.createElement("style");
style.textContent = `
input {
display: inline-block;
}
input:invalid {
border: 1px solid red;
}
`;
// Ajouter le style au shadow DOM
shadow.appendChild(style);
//Si une value est donnée
let value = this.getAttribute("value");
if (value != null) {
this.value = value;
}
}
static get observedAttributes() {
return ["show"]; // Ajoute 'show' à la liste des attributs observés
}
connectedCallback() {
// Récupérer l'attribut 'name'
this.name = this.getAttribute("name");
// Créer un input caché pour la valeur datetime
this.hiddenInput = document.createElement("input");
this.hiddenInput.type = "hidden";
this.hiddenInput.name = this.name;
this.appendChild(this.hiddenInput);
// Gérer la soumission du formulaire
this.closest("form")?.addEventListener("submit", (e) => {
if (!this.validate()) {
e.preventDefault(); // Empêcher la soumission si non valide
this.dispatchEvent(
new Event("invalid", { bubbles: true, cancelable: true })
);
} else {
// Mettre à jour la valeur de l'input caché avant la soumission
this.hiddenInput.value = this.isValid()
? this.valueAsDate.toFakeIso()
: "";
}
});
this.updateDisplay();
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === "show") {
this.updateDisplay(); // Met à jour l'affichage si l'attribut 'show' change
}
}
updateDisplay() {
const mode = this.getAttribute("show") || "both";
const dateInput = this.shadowRoot.querySelector("#date");
const timeInput = this.shadowRoot.querySelector("#time");
switch (mode) {
case "date":
dateInput.style.display = "inline-block";
timeInput.style.display = "none";
break;
case "time":
dateInput.style.display = "none";
timeInput.style.display = "inline-block";
break;
case "both":
default:
dateInput.style.display = "inline-block";
timeInput.style.display = "inline-block";
}
}
// Vérifier si la valeur forme une date valide
isValid() {
return !Number.isNaN(this.valueAsDate.getTime());
}
// Valider l'élément
validate() {
if (this.required && !this.isValid()) {
return false;
}
return true;
}
// Mettre à jour la valeur interne
updateValue() {
const dateInput = this.shadowRoot.querySelector("#date");
const timeInput = this.shadowRoot.querySelector("#time");
this._value = `${dateInput.value}T${timeInput.value}`;
this.dispatchEvent(new Event("change", { bubbles: true }));
// Appliquer le style 'invalid' si nécessaire
dateInput.classList.toggle("invalid", this.required && !this.isValid());
timeInput.classList.toggle("invalid", this.required && !this.isValid());
}
// Getter pour obtenir la valeur actuelle.
get value() {
return this._value;
}
get valueAsObject() {
const dateInput = this.shadowRoot.querySelector("#date");
const timeInput = this.shadowRoot.querySelector("#time");
return {
date: dateInput.value,
time: timeInput.value,
};
}
// Getter pour obtenir la valeur en tant qu'objet Date.
get valueAsDate() {
return new Date(this._value);
}
// Setter pour définir la valeur. Sépare la valeur en date et heure et les définit individuellement.
set value(val) {
let [date, time] = val.split("T");
this.shadowRoot.querySelector("#date").value = date;
time = time.substring(0, 5);
this.shadowRoot.querySelector("#time").value = time;
this._value = val;
}
// Setter pour définir la valeur à partir d'un objet avec les propriétés 'date' et 'time'.
set valueAsObject(obj) {
const dateInput = this.shadowRoot.querySelector("#date");
const timeInput = this.shadowRoot.querySelector("#time");
if (obj.hasOwnProperty("date")) {
dateInput.value = obj.date || ""; // Définit la valeur de l'input de date si elle est fournie
}
if (obj.hasOwnProperty("time")) {
timeInput.value = obj.time.substring(0, 5) || ""; // Définit la valeur de l'input d'heure si elle est fournie
}
// Met à jour la valeur interne en fonction des nouvelles valeurs des inputs
this.updateValue();
}
// Setter pour définir la valeur à partir d'un objet Date.
set valueAsDate(dateVal) {
// Formatage de l'objet Date en string et mise à jour de la valeur.
this.value = `${dateVal.getFullYear()}-${String(
dateVal.getMonth() + 1
).padStart(2, "0")}-${String(dateVal.getDate()).padStart(2, "0")}T${String(
dateVal.getHours()
).padStart(2, "0")}:${String(dateVal.getMinutes()).padStart(2, "0")}`;
}
/**
* Fonction qui vérifie si une période est dans un interval
* Objet période / interval
* {
* deb: Date,
* fin: Date,
* }
* @param {object} period
* @param {object} interval
* @returns {boolean} Vrai si la période est dans l'interval
*/
function hasTimeConflict(period, interval) {
return period.deb.isBefore(interval.fin) && period.fin.isAfter(interval.deb);
}
// Définition du nouvel élément personnalisé 'scodoc-datetime'.
customElements.define("scodoc-datetime", ScoDocDateTimePicker);

View File

@ -120,7 +120,6 @@ div.submit > input {
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
{% include "sco_timepicker.j2" %}
{% endblock scripts %}

View File

@ -149,7 +149,6 @@ div.submit > input {
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
{% include "sco_timepicker.j2" %}
<script>

View File

@ -89,17 +89,13 @@ Bilan assiduité de {{sco.etud.nomprenom}}
</div>
</section>
<br>
<section class="nonvalide">
<div>Le tableau n'affiche que les assiduités non justifiées et les justificatifs soumis / modifiés</div>
{{tableau | safe }}
</section>
<section class="suppr">
<h4>Boutons de suppresions (toute suppression est définitive) </h4>
<button type="button" onclick="removeAllAssiduites()">Suppression des assiduités</button>
<button type="button" onclick="removeAllJustificatifs()">Suppression des justificatifs</button>
</section>
<div class="legende">
<h3>Statistiques</h3>
<p>Un message d'alerte apparait si le nombre d'absence dépasse le seuil (indiqué dans les préférences du
@ -140,8 +136,7 @@ Bilan assiduité de {{sco.etud.nomprenom}}
}
function getAssiduitesCount(dateDeb, dateFin, action) {
const url_api = getUrl() + `/api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&etat=absent,retard,present&split`;
//Utiliser async_get au lieu de Jquery
const url_api = `../../api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&etat=absent,retard,present&split`;
async_get(
url_api,
action,
@ -211,79 +206,6 @@ Bilan assiduité de {{sco.etud.nomprenom}}
getAssiduitesCount(dateDeb, dateFin, showStats);
}
function removeAllAssiduites() {
openPromptModal(
"Suppression de l'assiduité",
document.createTextNode(
'Souhaitez vous réellement supprimer toutes les informations sur l\'assiduité de cet étudiant ? Cette suppression est irréversible.')
,
() => {
getAllAssiduitesFromEtud(etudid, (data) => {
const toRemove = data.map((a) => a.assiduite_id);
console.log(toRemove)
deleteAssiduites(toRemove);
})
})
}
function removeAllJustificatifs() {
openPromptModal(
"Suppression des justificatifs",
document.createTextNode(
'Souhaitez vous réelement supprimer tous les justificatifs de cet étudiant ? Cette supression est irréversible.')
,
() => {
getAllJustificatifsFromEtud(etudid, (data) => {
const toRemove = data.map((a) => a.justif_id);
deleteJustificatifs(toRemove);
})
})
}
/**
* Suppression des assiduties
*/
function deleteAssiduites(assi) {
const path = getUrl() + `/api/assiduite/delete`;
async_post(
path,
assi,
(data, status) => {
//success
if (data.success.length > 0) {
}
location.reload();
},
(data, status) => {
//error
console.error(data, status);
errorAlert();
}
);
}
/**
* Suppression des justificatifs
*/
function deleteJustificatifs(justis) {
const path = getUrl() + `/api/justificatif/delete`;
async_post(
path,
justis,
(data, status) => {
//success
location.reload();
},
(data, status) => {
//error
console.error(data, status);
errorAlert();
}
);
}
const metriques = {
"heure": "H.",
"demi": "1/2 J.",

View File

@ -149,33 +149,7 @@ Calendrier de l'assiduité
list-style-type: none;
}
.color.present {
background-color: var(--color-present) !important;
}
.color.absent {
background-color: var(--color-absent) !important;
}
.color.absent.est_just {
background-color: var(--color-absent-justi) !important;
}
.color.retard {
background-color: var(--color-retard) !important;
}
.color.retard.est_just {
background-color: var(--color-retard-justi) !important;
}
.color.nonwork {
background-color: var(--color-nonwork) !important;
}
.color {
background-color: var(--color-defaut) !important;
}
.pageContent {
margin-top: 1vh;
@ -208,38 +182,6 @@ Calendrier de l'assiduité
justify-content: start;
}
.color.est_just.sans_etat::before {
content: "";
position: absolute;
width: 25%;
height: 100%;
background-color: var(--color-justi) !important;
right: 0;
}
.color.invalide::before {
content: "";
position: absolute;
width: 25%;
height: 100%;
right: 0;
background-color: var(--color-justi-invalide) !important;
}
.color.attente::before,
.color.modifie::before {
content: "";
position: absolute;
width: 25%;
height: 100%;
right: 0;
background: repeating-linear-gradient(to bottom,
var(--color-justi-attente-stripe) 0px,
var(--color-justi-attente-stripe) 4px,
var(--color-justi-attente) 4px,
var(--color-justi-attente) 7px) !important;
}
.demo.invalide {
background-color: var(--color-justi-invalide) !important;
}

View File

@ -11,7 +11,6 @@ Assiduité de {{etud.nomprenom}}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
{% endblock %}

View File

@ -1,101 +1,637 @@
{#
- TODO : revoir le fonctionnement de cette page (trop lente / complexe)
- Utiliser majoritairement du python
#}
{% extends "sco_page.j2" %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
{{ super() }}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
<style>
.ui-timepicker-container,#ui-datepicker-div{
z-index: 5 !important;
}
#new_periode,
#actions {
display: flex;
flex-direction: column;
width: fit-content;
gap: 0.5em;
}
#actions {
flex-direction: row;
align-items: center;
margin-bottom: 5px;
}
#actions label{
margin: 0;
}
#fix {
display: flex;
flex-direction: row;
gap: 1em;
justify-content: space-between;
width: fit-content;
}
#fix>.box {
border: 1px solid #444;
border-radius: 0.5em;
padding: 1em;
}
.timepicker {
width: 5em;
text-align: center;
}
#moduleimpl_select {
width: 10em;
}
#gtrcontent .pdp {
display: none;
}
#gtrcontent[data-pdp="true"] .pdp {
display: block;
}
#tableau-periode {
display: flex;
flex-direction: column;
overflow-x: scroll;
max-width: var(--sco-content-max-width);
}
#tableau-periode .pdp {
width: 5em;
border-radius: 8px;
}
.header {
background-color: #f9f9f9;
padding: 10px;
text-align: center;
border: 1px solid #ddd;
}
.cell, .header {
border: 1px solid #ddd;
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
background-color: #f9f9f9;
}
.header{
justify-content: space-between;
}
.cell {
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #ddd;
padding: 10px;
text-align: center;
width: 256px;
}
.cell p{
text-align: center;
}
.sticky {
position: sticky;
top: 0;
background-color: #f9f9f9;
z-index: 2;
}
.cell .assiduite-bubble {
display: block;
top: 0;
z-index: 0;
width: 100% !important;
min-width: inherit !important;
}
.assi-btns {
display: flex;
gap: 4px;
}
.pointer{
cursor: pointer;
}
.ligne{
display: flex;
gap: 1px;
}
</style>
{% endblock styles %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
{% include "sco_timepicker.j2" %}
<script>
/**
* Permet d'afficher ou non les photos des étudiants
* @param {boolean} checked
*/
function afficherPDP(checked) {
if (checked) {
gtrcontent.setAttribute("data-pdp", "true");
} else {
gtrcontent.removeAttribute("data-pdp");
}
// On sauvegarde le choix dans le localStorage
localStorage.setItem("scodoc-signal_assiduites_diff-pdp", `${checked}`);
pdp.checked = checked;
}
/**
* Permet d'ajouter une nouvelle période au tableau
* Par défaut la période est générèe avec les valeurs des inputs
* Si une période est passée en paramètre, alors on utilise ses valeurs
* @param {Object} period - La période à ajouter
*/
async function nouvellePeriode(period = null) {
// On récupère l'id de la période
let periodId;
if (period) {
periodId = period.periodId;
} else {
periodId = currentPeriodId++;
}
// On récupère les valeurs des inputs
let date = document.getElementById("date").value;
let debut = document.getElementById("debut").value;
let fin = document.getElementById("fin").value;
let moduleimpl_id = document.getElementById("moduleimpl_select").value;
const moduleimpl = await getModuleImpl({ moduleimpl_id: moduleimpl_id });
// Si une période est passée en paramètre, on utilise ses valeurs
if (period) {
date = period.date_debut.format("DD/MM/YYYY");
debut = period.date_debut.format("HH:mm");
fin = period.date_fin.format("HH:mm");
moduleimpl_id = period.moduleimpl_id;
}else{
//Sinon on vérifie qu'on a bien des valeurs
const text = document.createTextNode("Veuillez remplir tous les champs pour ajouter une plage.")
if (date == "" || debut == "" || fin == "" || moduleimpl_id == "") {
openAlertModal(
"Erreur",
text
);
return;
}
}
// On ajoute la nouvelle période au tableau
let periodeDiv = document.createElement("div");
periodeDiv.classList.add("cell", "header");
periodeDiv.id = `periode-${periodId}`;
const periodP = document.createElement("p");
periodP.textContent = `Plage du ${date} de ${debut} à ${fin}`;
// On ajoute le moduleimpl
const modP = document.createElement("p");
modP.textContent = moduleimpl;
// On ajoute le bouton pour supprimer la période
const close = document.createElement("button");
close.textContent = "❌";
close.addEventListener("click", () => {
// On supprime toutes les cases du tableau correspondant à cette période
document
.querySelectorAll(
`[data-periodeid="${periodeDiv.getAttribute("data-periodeid")}"]`
)
.forEach((e) => e.remove());
// On supprime la période de la Map periodes
periodes.delete(Number(periodeDiv.getAttribute("data-periodeid")));
});
//On ajoute les éléments au DOM
periodeDiv.appendChild(periodP);
periodeDiv.appendChild(modP);
periodeDiv.appendChild(close);
periodeDiv.setAttribute("data-periodeid", periodId);
document.getElementById("tete-table").appendChild(periodeDiv);
// On récupère les étudiants (etudids)
let etudids = [
...document.querySelectorAll(".ligne[data-etudid]"),
].map((e) => e.getAttribute("data-etudid"));
// On génère une date de début et de fin de la période
const date_debut = new Date(
$("#date").datepicker("getDate").format("YYYY-MM-DD") + "T" + debut
);
const date_fin = new Date(
$("#date").datepicker("getDate").format("YYYY-MM-DD") + "T" + fin
);
date_debut.add(1, "seconds");
// Préparation de la requête
const url =
`../../api/assiduites/group/query?date_debut=${date_debut.toFakeIso()}` +
`&date_fin=${date_fin.toFakeIso()}&etudids=${etudids.join(
","
)}&with_justifs`;
//Si la période n'existait pas, alors on l'ajoute à la Map
if (!period) {
periodes.set(periodId, {
date_debut: date_debut.clone().add(-1, "seconds"),
date_fin: date_fin,
moduleimpl_id: moduleimpl_id,
periodId: periodId,
});
}
// On récupère les incriptions au module
const inscriptions = await getInscriptionModule(moduleimpl_id);
// On récupère les assiduités
await fetch(url)
// On convertit la réponse en JSON
.then((res) => {
if (!res.ok) {
throw new Error("Network response was not ok");
}
return res.json();
})
// On traite les données
.then((data) => {
for (let etudid of etudids) {
// On crée une case pour chaque étudiant
let cell = document.createElement("div");
cell.classList.add("cell");
cell.setAttribute("data-etudid", etudid);
cell.setAttribute("data-periodeid", periodId);
cell.id = `cell-${etudid}-${periodId}`;
document.querySelector(`.ligne[data-etudid="${etudid}"]`).appendChild(cell);
//Vérification inscription au module
// Si l'étudiant n'est pas inscrit, on le notifie et on passe à l'étudiant suivant
const inscrit =
inscriptions == null ? true : inscriptions.find((e) => e == etudid);
if (!inscrit) {
cell.textContent = "Non inscrit";
cell.classList.add("non-inscrit");
continue;
}
//Gestion des assiduités déjà existantes
const assiduites = data[etudid];
// Si l'étudiant n'a pas d'assiduité, on crée les boutons assiduité
if (assiduites.length == 0) {
const assi_btns = document.createElement('div');
assi_btns.classList.add('assi-btns');
["present", "retard", "absent"].forEach((value) => {
const cbox = document.createElement("input");
cbox.type = "checkbox";
cbox.value = value;
cbox.name = `rbtn_${etudid}_${periodId}`;
cbox.classList.add("rbtn", value);
// Event pour être sur qu'un seul bouton est coché à la fois
cbox.addEventListener("click", (event) => {
const parent = event.target.parentElement;
parent.querySelectorAll(".rbtn").forEach((ele) => {
if (ele.value != value) {
ele.checked = false;
}
});
});
// Si une valeur par défaut est donnée alors on l'applique
cbox.checked = etatDef.value == value;
assi_btns.appendChild(cbox);
});
cell.appendChild(assi_btns);
} else {
// Si une (ou plus) assiduité sont trouvée pour la période
// alors on affiche les informations de la première assiduité
setupAssiduiteBubble(cell, assiduites[0]);
}
}
})
//Si jamais la requête échoue, on affiche un message d'erreur dans la console
.catch((error) => {
console.error("Error:", error);
});
document.getElementById("tableau-periode").classList.remove("hidden");
}
/**
* Permet de récupérer la saisie puis créer les assiduités grâce à l'api
*/
function sauvegarderAssiduites() {
// Initialisation de la liste des assiduités à créer
let assiduitesData = [];
// Pour chaque période, on récupère les assiduités saisies
for (let [periodeId, periode] of periodes.entries()) {
// On prend chaque cellule correspondant à la période
const cells = document.querySelectorAll(
`.cell[data-periodeid="${periodeId}"][data-etudid]`
);
// Pour chaque cellule, on récupère l'état de l'assiduité
cells.forEach((cell) => {
const etudid = cell.getAttribute("data-etudid");
const etat = cell.querySelector(".rbtn:checked")?.value;
// Il est possible que l'état soit null
// - Cas où l'étudiant n'est pas inscrit
// - Cas où l'étudiant avait déjà une assiduité
if (etat) {
// On génère un objet "assiduité"
/*
{
etudid: <int>,
etat: <string>,
date_debut: <string>,
date_fin: <string>,
moduleimpl_id: <int>,
periodId: <int>
}
*/
assiduitesData.push({
etudid: etudid,
etat: etat,
...periode,
});
}
});
}
// Une fois les assiduités générées, on les envoie à l'api
async_post(
"../../api/assiduites/create",
assiduitesData,
// Si la requête passe
async (data) => {
// On supprime toutes les cases du tableau pour le mettre à jour
document.querySelectorAll("[data-periodeid]").forEach((e)=>e.remove())
// On recrée les périodes
// (cela permet de redemander les assiduités, donc mettre à jour les cases)
for (let periode of periodes.values()) {
await nouvellePeriode(periode);
}
// Si il y n'a pas d'erreur, on affiche un message de succès
if (data.errors.length == 0) {
const span = document.createElement("span");
span.textContent = "Les assiduités ont bien été sauvegardées.";
openAlertModal(
"Sauvegarde des assiduités",
span,
null,
"var(--color-present)"
);
return;
}
// Si il y a des erreurs, on les affiche
if (data.errors.length > 0) {
// On crée une map pour regrouper les erreurs par période
const erreurs = new Map();
data.errors.forEach((err) => {
// Pour chaque période on créer une liste d'erreurs
// format : [message, etudid]
const assi = assiduitesData[err.indice];
const msg = err.message;
const periodErrors = erreurs.get(assi.periodId) || [];
// Récupération du nom de l'étudiant
const etud = document.querySelector(
`#head-${assi.etudid} span`
).textContent;
periodErrors.push([`Erreur pour ${etud} : ${msg}`, assi.etudid]);
erreurs.set(assi.periodId, periodErrors);
});
// Création du DOM
/*
<ul>
<li>
Période du ... de ... à ...
<ul>
<li>Erreur pour ...</li>
<li>Erreur pour ...</li>
</ul>
/li>
</ul>
*/
const ul = document.createElement("ul");
//Pour chaque période on créer un titre "periode du ... de ... à ..."
for (let [periodeId, periodErrors] of erreurs.entries()) {
const period = periodes.get(periodeId);
const li = document.createElement("li");
// On affiche la période
li.textContent = `Plage du ${period.date_debut.format(
"DD/MM/YYYY HH:mm"
)} à ${period.date_fin.format("HH:mm")}`;
// Nous emmène à la période lorsqu'on clique dessus
li.addEventListener("click", () => {
location.href = `#periode-${periodeId}`;
});
li.classList.add("pointer");
// Pour chaque erreur, on créer un élément de liste
const ul2 = document.createElement("ul");
periodErrors.forEach((err) => {
const li2 = document.createElement("li");
li2.textContent = err[0];
li2.classList.add("pointer");
// Nous emmène à la case de l'étudiant lorsqu'on clique dessus
li2.addEventListener("click", () => {
location.href = `#cell-${err[1]}-${periodeId}`;
});
ul2.appendChild(li2);
});
li.appendChild(ul2);
ul.appendChild(li);
}
openAlertModal(
"Erreurs lors de la sauvegarde des assiduités",
ul,
"Les autres assiduités ont bien été sauvegardées."
);
}
},
(e) => {
console.error("Erreur lors de la création des assiduités", e);
}
);
}
// Mis en place des variables globales
let currentPeriodId = 0;
const periodes = new Map();
const moduleimpls = new Map();
const inscriptionsModules = new Map();
const nonWorkDays = [{{ nonworkdays| safe }}];
// Vérification du forçage de module
window.forceModule = "{{ forcer_module }}" == "True";
if (window.forceModule) {
if (moduleimpl_select.value == "") {
document.getElementById("forcemodule").style.display = "block";
add_periode.disabled = true;
}
// Désactivation du bouton d'ajout de période si aucun module n'est sélectionné
// et affichage du message de forçage de module
moduleimpl_select?.addEventListener("change", (e) => {
if (e.target.value != "") {
document.getElementById("forcemodule").style.display = "none";
add_periode.disabled = false;
} else {
document.getElementById("forcemodule").style.display = "block";
add_periode.disabled = true;
}
});
}
/**
* Fonction exécutée au lancement de la page
* - On affiche ou non les photos des étudiants
* - On vérifie si la date est un jour travaillé
*/
async function main() {
const checked = localStorage.getItem("scodoc-signal_assiduites_diff-pdp") == "true";
afficherPDP(checked);
$("#date").on("change", async function (d) {
// On vérifie si la date est un jour travaillé
dateCouranteEstTravaillee();
});
}
main();
</script>
{% endblock scripts %}
{% block title %}
{{title}}
{{title}}
{% endblock title %}
{% block app_content %}
{% include "assiduites/widgets/alert.j2" %}
{% include "assiduites/widgets/prompt.j2" %}
{% include "assiduites/widgets/conflict.j2" %}
{% include "assiduites/widgets/toast.j2" %}
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
<h2>Signalement différé de l'assiduité {{gr |safe}}</h2>
<div class="ue_warning">Attention, cette page utilise des couleurs et conventions différentes
de celles des autres pages ScoDoc: elle sera prochainement modifée, merci de votre patience.
</div>
<h3>{{sem | safe }}</h3>
<div id="fix">
<!-- Nouvelle Plage
Permet de créer une nouvelle ligne pour une nouvelle Plage
(
Jour, -> datepicker
Heure de début, -> timepicker
Heure de fin -> timepicker
ModuleImplId -> select (liste des modules tout semestre confondu)
)
--->
{{diff | safe}}
<div id="new_periode" class="box">
<label for="date">
Date :
<input type="text" name="date" id="date" class="datepicker">
</label>
<label for="debut">
Heure de début :
<input type="text" name="debut" id="debut" class="timepicker">
</label>
<label for="fin">
Heure de fin :
<input type="text" name="fin" id="fin" class="timepicker">
</label>
<div class="help">
<h3>Explication de la saisie différée</h3>
<p>Si la colonne n'est pas valide elle sera affichée en rouge, passez le curseur sur la colonne pour afficher
le message d'erreur</p>
<p>Sélectionner la date de début de la colonne mettra automatiquement la date de fin à la durée d'une séance
(préférence de département)</p>
<p>Modifier le module alors que des informations d'assiduité sont déjà enregistrées pour la période changera leur
module.</p>
<p>Il y a 4 boutons sur la colonne permettant d'enregistrer l'information pour tous les étudiants</p>
<p>Le dernier des boutons retire l'information présente.</p>
<p>Vous pouvez ajouter des colonnes en appuyant sur le bouton + </p>
<p>Vous pouvez supprimer une colonne en appuyant sur la croix qui se situe dans le coin haut droit de la colonne.
</p>
<label for="moduleimpl_select">
<div id="forcemodule" style="display: none; margin:10px 0px;">
Vous devez spécifier le module ! (voir réglage préférence du semestre)
</div>
Module :
{{moduleimpl_select | safe}}
</label>
<button id="add_periode" onclick="nouvellePeriode()">Ajouter une plage</button>
</div>
</div>
<!-- Boutons d'actions
- Sauvegarder
- Afficher la photo de profil
- Assiduité par défaut (aucune, present, retard, absent)
--->
<br>
<div id="actions" class="flex">
<button id="save" onclick="sauvegarderAssiduites()">ENREGISTRER</button>
<label for="pdp">
Photo de profil :
<input type="checkbox" name="pdp" id="pdp" checked onclick="afficherPDP(this.checked)">
</label>
<label for="etatDef">
Intialiser les étudiants comme :
<select name="etatDef" id="etatDef">
<option value="">-</option>
<option value="present">présents</option>
<option value="retard">en retard</option>
<option value="absent">absents</option>
</select>
</label>
</div>
<!-- Tableau à double entrée
Colonne : Etudiants (Header = Nom, Prénom, Photo (si actif))
Ligne : Période (Header = Jour, Heure de début, Heure de fin, ModuleImplId)
Contenu :
- bouton assiduité (présent, retard, absent)
- Bouton conflit si conflit de période
--->
<div id="tableau-periode" class="grid-table">
<!-- Première ligne : Plages -->
<div class="ligne" id="tete-table">
<div class="cell header sticky">Étudiants</div>
{# <div class="cell header" periode-id="X">Plage X</div> #}
</div>
{# ... #}
<hr class="hidden" id="separator">
{% for etud in etudiants %}
<div class="ligne" data-etudid="{{etud.etudid}}">
<div class="cell etudinfo sticky" id="head-{{etud.etudid}}">
<img src="../../api/etudiant/etudid/{{etud.etudid}}/photo?size=small" alt="{{etud.nomprenom}}" class="pdp">
<span>{{ etud.nomprenom }}</span>
</div>
{# <div class="cell" periode-id="X">Assiduité Plage 1</div> #}
</div>
{% endfor %}
</div>
{% include "assiduites/widgets/alert.j2" %}
{% endblock app_content %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
<script>
const etudsDefDem = {{ defdem | safe }}
const timeMorning = "{{ timeMorning | safe}}";
const timeNoon = "{{ timeNoon | safe}}";
const timeEvening = "{{ timeEvening | safe}}";
const defaultDates = {{ defaultDates | safe }}
const nonWorkDays = [{{ nonworkdays| safe }}];
window.addEventListener('load', () => {
[...document.querySelectorAll('.tr[etudid]')].forEach((a) => {
try {
if (a.getAttribute("etudid") in etudsDefDem) {
defdem = etudsDefDem[a.getAttribute("etudid")] == "D" ? "dem" : "def";
a.classList.add(defdem);
}
} catch (_) { }
});
if (defaultDates != null) {
defaultDates.forEach((dateString) => {
d = new Date(dateString);
if (isNonWorkDay(d, nonWorkDays)) return;
matin = `${dateString}T${timeMorning}`;
midi = `${dateString}T${timeNoon}`;
soir = `${dateString}T${timeEvening}`;
console.log(matin, midi, soir)
createColumn(matin, midi);
createColumn(midi, soir);
});
updateAllCol();
} else {
createColumn();
}
})
</script>
{% endblock scripts %}

View File

@ -13,6 +13,7 @@
<script src="{{scu.STATIC_DIR}}/js/groups_view.js"></script>
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
{% include "sco_timepicker.j2" %}
<script>
@ -20,33 +21,21 @@
function getPeriodValues() {
return [0, 23]
}
{% else %}
setupTimeLine(()=>{creerTousLesEtudiants(etuds)})
{% endif %}
const nonWorkDays = [{{ nonworkdays| safe }}];
const readOnly = {{ readonly }};
setupDate();
updateDate();
if (!readOnly) {
setupTimeLine(() => {
generateAllEtudRow();
});
}
window.forceModule = "{{ forcer_module }}"
window.forceModule = window.forceModule == "True" ? true : false
window.forceModule = "{{ forcer_module }}" == "True"
const etudsDefDem = {{ defdem | safe }}
const select = document.getElementById("moduleimpl_select");
select?.addEventListener('change', (e) => {
generateAllEtudRow();
});
if (window.forceModule) {
const btn = document.getElementById("validate_selectors");
@ -63,12 +52,51 @@
}
});
}
document.getElementById("pdp").addEventListener("change", (e) => {
creerTousLesEtudiants(etuds);
});
$('#date').on('change', async function(d) {
// On vérifie si la date est un jour travaillé
dateCouranteEstTravaillee();
await recupAssiduites(etuds, $("#date").datepicker("getDate"));
creerTousLesEtudiants(etuds);
});
$("#moduleimpl_select").on("change", ()=>{
creerTousLesEtudiants(etuds);
});
$("#group_ids_sel").on("change", ()=>{
main();
})
const moduleimpls = {};
const inscriptionsModules = new Map();
let etuds = new Map();
async function main(){
dateCouranteEstTravaillee();
etuds = await recupEtuds($('#group_ids_sel').val());
if (etuds.size != 0){
await recupAssiduites(etuds, $("#date").datepicker("getDate"));
}
creerTousLesEtudiants(etuds);
}
setTimeout(main, 0);
</script>
{% endblock scripts %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.css">
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
@ -77,14 +105,8 @@
{% block app_content %}
{% include "assiduites/widgets/toast.j2" %}
{{ minitimeline|safe }}
<style>
#moduleimpl_select {
max-width: 200px;
}
</style>
<section id="content">
<div class="no-display">
@ -104,15 +126,16 @@
<fieldset class="selectors">
<div class="infos">
<div class="infos-button">Groupes&nbsp;: {{grp|safe}}</div>
<div class="infos-button" style="margin-left: 24px;">Date&nbsp;: <span style="margin-left: 8px;"
id="datestr"></span>
<input type="text" name="tl_date" id="tl_date" value="{{ date }}" onchange="updateDate()">
<div>
<input type="text" name="date" id="date" class="datepicker" value="{{date}}">
</div>
</div>
</fieldset>
<div style="display: {{'none' if readonly == 'true' else 'block'}};">
{{timeline|safe}}
</div>
{% if readonly == "false" %}
{{timeline|safe}}
<div style="margin: 1vh 0;">
<div id="forcemodule" style="display: none; margin:10px 0px;">
@ -123,46 +146,40 @@
{% else %}
{% endif %}
{% if readonly == "true" %}
<button id="validate_selectors" onclick="validateSelectors(this)">
Voir l'assiduité
</button>
{% else %}
<button id="validate_selectors" onclick="validateSelectors(this)">
Faire la saisie
</button>
<div>
<label for="pdp">
<span>Afficher les photos</span>
<input type="checkbox" name="pdp" id="pdp">
</label>
</div>
{% if readonly == "false" %}
<div class="mass-selection">
<span>Mettre tout le monde :</span>
<fieldset class="btns_field mass">
<input type="checkbox" value="present" name="mass_btn_assiduites" id="mass_rbtn_present"
class="rbtn present" onclick="mettreToutLeMonde('present', this)" title="Present">
<input type="checkbox" value="retard" name="mass_btn_assiduites" id="mass_rbtn_retard"
class="rbtn retard" onclick="mettreToutLeMonde('retard', this)" title="Retard">
<input type="checkbox" value="absent" name="mass_btn_assiduites" id="mass_rbtn_absent"
class="rbtn absent" onclick="mettreToutLeMonde('absent', this)" title="Absent">
<input type="checkbox" value="remove" name="mass_btn_assiduites" id="mass_rbtn_aucun"
class="rbtn aucun" onclick="mettreToutLeMonde('vide', this)" title="Supprimer">
</fieldset>
</div>
{% endif %}
<div class="etud_holder">
<p class="placeholder">
</p>
</div>
<div class="legende">
<h3>Explication diverses</h3>
<p>
Si la période indiquée par la timeline provoque un conflit d'assiduité pour un étudiant sa ligne deviendra
rouge.
<br>
Dans ce cas il faut résoudre manuellement le conflit : cliquez sur un des boutons d'assiduités pour ouvrir
le
résolveur de conflit.
<br>
Correspondance des couleurs :
</p>
<ul>
{% include "assiduites/widgets/legende_couleur.j2" %}
</ul>
</div>
<!-- Ajout d'un conteneur pour le loader -->
<div class="loader-container" id="loaderContainer">
<div class="loader"></div>
</div>
{% include "assiduites/widgets/toast.j2" %}
{% include "assiduites/widgets/alert.j2" %}
{% include "assiduites/widgets/prompt.j2" %}
{% include "assiduites/widgets/conflict.j2" %}
</section>
{% endblock app_content %}
{% endblock app_content %}

View File

@ -1,118 +1,100 @@
<script>
/**
* Transformation d'une date de début en position sur la timeline
* @param {String} start
* @returns {String} un déplacement par rapport à la gauche en %
*/
function getLeftPosition(start) {
const startTime = new Date(start);
const startMins = (startTime.getHours() - t_start) * 60 + startTime.getMinutes();
return (startMins / (t_end * 60 - t_start * 60)) * 100 + "%";
}
/**
* Ajustement de l'espacement vertical entre les assiduités superposées
* @param {HTMLElement} container le conteneur des assiduités
* @param {String} start la date début de l'assiduité à placer
* @param {String} end la date de fin de l'assiduité à placer
* @returns {String} La position en px
*/
function getTopPosition(container, start, end) {
const overlaps = (a, b) => {
return a.start < b.end && a.end > b.start;
};
/**
* Transformation d'une date de début en position sur la timeline
* @param {String} start
* @returns {String} un déplacement par rapport à la gauche en %
*/
function getLeftPosition(start) {
const startTime = new Date(start);
const startMins =
(startTime.getHours() - t_start) * 60 + startTime.getMinutes();
return (startMins / (t_end * 60 - t_start * 60)) * 100 + "%";
}
/**
* Ajustement de l'espacement vertical entre les assiduités superposées
* @param {HTMLElement} container le conteneur des assiduités
* @param {String} start la date début de l'assiduité à placer
* @param {String} end la date de fin de l'assiduité à placer
* @returns {String} La position en px
*/
function getTopPosition(container, start, end) {
const overlaps = (a, b) => {
return a.start < b.end && a.end > b.start;
};
const startTime = new Date(start);
const endTime = new Date(end);
const assiduiteDuration = { start: startTime, end: endTime };
const startTime = new Date(start);
const endTime = new Date(end);
const assiduiteDuration = { start: startTime, end: endTime };
let position = 0;
let hasOverlap = true;
let position = 0;
let hasOverlap = true;
while (hasOverlap) {
hasOverlap = false;
Array.from(container.children).some((el) => {
const elStart = new Date(el.getAttribute("data-start"));
const elEnd = new Date(el.getAttribute("data-end"));
const elDuration = { start: elStart, end: elEnd };
while (hasOverlap) {
hasOverlap = false;
Array.from(container.children).some((el) => {
const elStart = new Date(el.getAttribute("data-start"));
const elEnd = new Date(el.getAttribute("data-end"));
const elDuration = { start: elStart, end: elEnd };
if (overlaps(assiduiteDuration, elDuration)) {
position += 25; // Pour ajuster l'espacement vertical entre les assiduités superposées
hasOverlap = true;
return true;
}
return false;
});
}
return position + "px";
if (overlaps(assiduiteDuration, elDuration)) {
position += 25; // Pour ajuster l'espacement vertical entre les assiduités superposées
hasOverlap = true;
return true;
}
return false;
});
}
return position + "px";
}
/**
* Calcule de la largeur de l'assiduité sur la timeline
* @param {String} start date iso de début
* @param {String} end date iso de fin
* @returns {String} la taille en %
*/
function getWidth(start, end) {
const startTime = new Date(start);
const endTime = new Date(end);
const duration = (endTime - startTime) / 1000 / 60;
const percent = (duration / (t_end * 60 - t_start * 60)) * 100;
return percent + "%";
}
function formatDateModal(date) {
return new Date(Date.removeUTC(date)).format("DD/MM/Y HH:mm");
}
class ConflitResolver {
constructor(assiduitesList, conflictPeriod, interval) {
this.list = assiduitesList;
this.conflictPeriod = conflictPeriod;
this.interval = interval;
this.selectedAssiduite = null;
this.element = undefined;
this.callbacks = {
delete: () => {},
split: () => {},
edit: () => {},
};
}
refresh(assiduitesList, periode) {
this.list = assiduitesList;
if (periode) {
this.conflictPeriod = periode;
}
/**
* Transformation d'un état en couleur
* @param {String} state l'état
* @returns {String} la couleur correspondant à l'état
*/
function getColor(state) {
switch (state) {
case "PRESENT":
return "var(--color-present)";
case "ABSENT":
return "var(--color-absent)";
case "RETARD":
return "var(--color-retard)";
default:
return "var(--color-defaut-dark)";
}
}
this.render();
}
/**
* Calcule de la largeur de l'assiduité sur la timeline
* @param {String} start date iso de début
* @param {String} end date iso de fin
* @returns {String} la taille en %
*/
function getWidth(start, end) {
const startTime = new Date(start);
const endTime = new Date(end);
const duration = (endTime - startTime) / 1000 / 60;
const percent = (duration / (t_end * 60 - t_start * 60)) * 100
return percent + "%";
}
class ConflitResolver {
constructor(assiduitesList, conflictPeriod, interval) {
this.list = assiduitesList;
this.conflictPeriod = conflictPeriod;
this.interval = interval;
this.selectedAssiduite = null;
this.element = undefined;
this.callbacks = {
delete: () => { },
split: () => { },
edit: () => { },
}
}
refresh(assiduitesList, periode) {
this.list = assiduitesList;
if (periode) {
this.conflictPeriod = periode;
}
this.render()
}
selectAssiduite() {
}
open() {
const html = `
<div id="myModal" class="modal">
open() {
const html = `
<div id="myModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Veuillez régler le conflit pour poursuivre</h2>
@ -122,201 +104,264 @@
<div class="assiduites-container"></div>
</div>
<div class="action-buttons">
<div class="modal-buttons">
<button id="finish" class="btnPrompt">Quitter</button>
<button id="delete" class="btnPrompt" disabled>Supprimer</button>
<button id="split" class="btnPrompt" disabled>Séparer</button>
<button id="edit" class="btnPrompt" disabled>Modifier l'état</button>
</div>
</div>
<div class="modal-assiduite-content">
<h2>Information de l'assiduité sélectionnée</h2>
<div class="infos">
<p>Assiduite id : <span id="modal-assiduite-id">A</span></p>
<p>Etat : <span id="modal-assiduite-etat">B</span></p>
<p>Date de début : <span id="modal-assiduite-deb">C</span></p>
<p>Date de fin: <span id="modal-assiduite-fin">D</span></p>
<p>Module : <span id="modal-assiduite-module">E</span></p>
<p><span id="modal-assiduite-user">F</span></p>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML("afterbegin", html);
this.element = document.getElementById('myModal');
this.deleteBtn = document.querySelector('#myModal #delete');
this.editBtn = document.querySelector('#myModal #edit');
this.splitBtn = document.querySelector('#myModal #split');
this.deleteBtn.addEventListener('click', () => { this.deleteAssiduiteModal() });
this.editBtn.addEventListener('click', () => { this.editAssiduiteModal() });
this.splitBtn.addEventListener('click', () => { this.splitAssiduiteModal() });
document.querySelector("#myModal #finish").addEventListener('click', () => { this.close() })
document.body.insertAdjacentHTML("afterbegin", html);
this.element = document.getElementById("myModal");
document.querySelector("#myModal #finish").addEventListener("click", () => {
this.close();
});
document.querySelector('#myModal .close').addEventListener('click', () => { this.close() })
document.querySelector("#myModal .close").addEventListener("click", () => {
this.close();
});
// fermeture du modal en appuyant sur echap
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.close()
}
}, { once: true })
this.render()
// fermeture du modal en appuyant sur echap
document.addEventListener(
"keydown",
(e) => {
if (e.key === "Escape") {
this.close();
}
},
{ once: true }
);
this.render();
}
close() {
if (this.element) {
this.element.remove();
}
}
close() {
if (this.element) {
this.element.remove()
/**
* Génération du modal
*/
render() {
const timeLabels = document.querySelector(".time-labels");
const assiduitesContainer = document.querySelector(".assiduites-container");
timeLabels.innerHTML = "";
assiduitesContainer.innerHTML = '<div class="assiduite-special"></div>';
// Ajout des labels d'heure sur la frise chronologique
for (let i = t_start; i <= t_end; i++) {
const timeLabel = document.createElement("div");
timeLabel.className = "time-label";
timeLabel.textContent = numberToTime(i);
timeLabels.appendChild(timeLabel);
}
//Placement de la période conflictuelle sur la timeline
const specialAssiduiteEl = document.querySelector(".assiduite-special");
specialAssiduiteEl.style.width = getWidth(
this.conflictPeriod.deb,
this.conflictPeriod.fin
);
specialAssiduiteEl.style.left = getLeftPosition(this.conflictPeriod.deb);
specialAssiduiteEl.style.top = "0";
specialAssiduiteEl.style.zIndex = "0"; // Place l'assiduité spéciale en arrière-plan
assiduitesContainer.appendChild(specialAssiduiteEl);
//Placement des assiduités sur la timeline
this.list.forEach((assiduite) => {
const period = {
deb: new Date(assiduite.date_debut),
fin: new Date(assiduite.date_fin),
};
if (!hasTimeConflict(period, this.interval)) {
return;
}
const el = document.createElement("div");
el.classList.add("assiduite", "color", assiduite.etat.toLowerCase());
el.style.width = getWidth(assiduite.date_debut, assiduite.date_fin);
el.style.left = getLeftPosition(assiduite.date_debut);
el.style.top = "10px";
el.setAttribute("data-id", assiduite.assiduite_id);
el.addEventListener("click", () => {});
// Génération des boutons d'action (supprimer, éditer, diviser)
const actionButtons = document.createElement("div");
actionButtons.className = "action-buttons";
const deleteButton = document.createElement("button");
deleteButton.textContent = "🗑️";
deleteButton.addEventListener("click", () => {
this.supprimerAssiduite(assiduite);
});
const editButton = document.createElement("button");
editButton.textContent = "📝";
editButton.addEventListener("click", () => {
this.editerAssiduite(assiduite);
});
const splitButton = document.createElement("button");
splitButton.textContent = "✂️";
splitButton.addEventListener("click", () => {
this.spliterAssiduite(assiduite);
});
actionButtons.appendChild(editButton);
actionButtons.appendChild(splitButton);
actionButtons.appendChild(deleteButton);
el.appendChild(actionButtons);
setupAssiduiteBubble(el, assiduite);
assiduitesContainer.appendChild(el);
});
}
supprimerAssiduite(assiduite) {
const html = `
<p>Êtes-vous sûr de vouloir supprimer cette assiduité ?</p>
`;
const div = document.createElement("div");
div.innerHTML = html;
openPromptModal(
"Suppression de l'assiduité",
div,
async (closePromptModal) => {
await async_post(
`../../api/assiduite/delete`,
[assiduite.assiduite_id],
async (data) => {
if (data.success.length > 0) {
const etud = etuds.get(Number(assiduite.etudid));
await MiseAJourLigneEtud(etud);
this.refresh(etud.assiduites, this.conflictPeriod);
closePromptModal();
} else {
console.error(data.errors["0"].message);
}
}
},
(error) => {
console.error(
"Erreur lors de la suppression de l'assiduité",
error
);
}
);
},
() => {},
"var(--color-error)"
);
}
/**
* Sélection d'une assiduité sur la timeline
* @param {Assiduité} assiduite l'assiduité sélectionnée
*/
selectAssiduite(assiduite) {
// Désélectionner l'assiduité précédemment sélectionnée
if (this.selectedAssiduite) {
const prevSelectedEl = document.querySelector(
`.assiduite[data-id="${this.selectedAssiduite.assiduite_id}"]`
);
if (prevSelectedEl) {
prevSelectedEl.classList.remove("selected");
}
editerAssiduite(assiduite) {
// Select pour choisir l'état de l'assiduité
const html = `
<select id="etat" name="etat">
<option disabled>Choisir un état</option>
<option value="present">Présent</option>
<option value="absent">Absent</option>
<option value="retard">Retard</option>
</select>
`;
const div = document.createElement("div");
div.innerHTML = html;
div.style.display = "flex";
div.style.justifyContent = "center";
openPromptModal(
"Modifier l'état de l'assiduité",
div,
async (closePromptModal) => {
const etatAssi = etat.value;
if (!etat) return true;
await async_post(
`../../api/assiduite/${assiduite.assiduite_id}/edit`,
{
etat: etatAssi,
},
async (data) => {
const etud = etuds.get(Number(assiduite.etudid));
await MiseAJourLigneEtud(etud);
this.refresh(etud.assiduites, this.conflictPeriod);
closePromptModal();
},
(error) => {
console.error("Erreur lors de la modification de l'assiduité", error);
}
);
// Sélectionner la nouvelle assiduité
this.selectedAssiduite = assiduite;
const selectedEl = document.querySelector(
`.assiduite[data-id="${assiduite.assiduite_id}"]`
);
if (selectedEl) {
selectedEl.classList.add("selected");
}
},
() => {},
"var(--color-present)"
);
}
//Mise à jour de la partie information du modal
const selectedModal = document.querySelector(".modal-assiduite-content");
spliterAssiduite(assiduite) {
// Select pour choisir l'état de l'assiduité
const creneau = getPeriodAsDate()
creneau.deb = creneau.deb.format().substring(11,16)
creneau.fin = creneau.fin.format().substring(11,16)
const html = `
<p>La période conflictuelle s'étend de ${creneau.deb} à ${creneau.fin}</p>
<br>
<input type="text" id="promptTime" name="promptTime" class="timepicker"
placeholder="Cliquez pour choisir un horaire" required>
`;
selectedModal.classList.add("show");
const div = document.createElement("div");
div.innerHTML = html;
div.style.display = "flex";
div.style.justifyContent = "center";
div.style.flexDirection = "column";
document.getElementById("modal-assiduite-id").textContent =
assiduite.assiduite_id;
document.getElementById(
"modal-assiduite-user"
).textContent = `saisie le ${formatDateModal(
assiduite.entry_date,
"à"
)} \npar ${assiduite.user_id}`;
document.getElementById("modal-assiduite-module").textContent =
assiduite.moduleimpl_id;
document.getElementById("modal-assiduite-deb").textContent = formatDateModal(
assiduite.date_debut
);
document.getElementById("modal-assiduite-fin").textContent = formatDateModal(
assiduite.date_fin
);
document.getElementById("modal-assiduite-etat").textContent =
assiduite.etat.capitalize();
openPromptModal(
"Séparer l'assiduité",
div,
async (closePromptModal) => {
const separateur = promptTime.value;
if (separateur === "") return true;
//Activation des boutons d'actions de conflit
this.deleteBtn.disabled = false;
this.splitBtn.disabled = false;
this.editBtn.disabled = false;
}
/**
* Suppression de l'assiduité sélectionnée
*/
deleteAssiduiteModal() {
if (!this.selectedAssiduite) return;
deleteAssiduite(this.selectedAssiduite.assiduite_id);
const assiduiteAvant = {...assiduite};
const assiduiteAprès = {...assiduite};
this.callbacks.delete(this.selectedAssiduite)
assiduiteAvant.date_fin = assiduite.date_fin.substring(0,11) + separateur;
assiduiteAprès.date_debut = assiduite.date_debut.substring(0,11) + separateur;
this.refresh(assiduites[this.selectedAssiduite.etudid]);
// On supprime l'assiduité actuelle
await async_post(
"../../api/assiduite/delete",
[assiduite.assiduite_id],
(data)=>{console.log(data)},
()=>{},
)
// Désélection de l'assiduité
this.resetSelection();
}
// On ajoute les deux nouvelles assiduités
await async_post(
"../../api/assiduites/create",
[assiduiteAvant, assiduiteAprès],
async (data)=>{
console.log(data);
const etud = etuds.get(Number(assiduite.etudid));
await MiseAJourLigneEtud(etud);
this.refresh(etud.assiduites, this.conflictPeriod);
closePromptModal();
},
()=>{},
)
/**
* Division d'une assiduité
*/
splitAssiduiteModal() {
//Préparation du prompt
const htmlPrompt = `<legend>Entrez l'heure de séparation</legend>
<input type="text" id="promptTime" name="appt"required style="position: relative; z-index: 100000;">`;
const fieldSet = document.createElement("fieldset");
fieldSet.classList.add("fieldsplit", "timepicker");
fieldSet.innerHTML = htmlPrompt;
//Callback de division
const success = () => {
const separatorTime = document.getElementById("promptTime").value;
const dateString =
getDate().format("YYYY-MM-DD") + `T${separatorTime}`;
const separtorDate = new Date(dateString);
const assiduite_debut = new Date(this.selectedAssiduite.date_debut);
const assiduite_fin = new Date(this.selectedAssiduite.date_fin);
if (
separtorDate.isAfter(assiduite_debut) &&
separtorDate.isBefore(assiduite_fin)
) {
const assiduite_avant = {
etat: this.selectedAssiduite.etat,
date_debut: assiduite_debut.toFakeIso(),
date_fin: separtorDate.toFakeIso(),
};
const assiduite_apres = {
etat: this.selectedAssiduite.etat,
date_debut: separtorDate.toFakeIso(),
date_fin: assiduite_fin.toFakeIso(),
};
if (this.selectedAssiduite.moduleimpl_id) {
assiduite_apres["moduleimpl_id"] = this.selectedAssiduite.moduleimpl_id;
assiduite_avant["moduleimpl_id"] = this.selectedAssiduite.moduleimpl_id;
}
deleteAssiduite(this.selectedAssiduite.assiduite_id);
const path = getUrl() + `/api/assiduite/${this.selectedAssiduite.etudid}/create`;
sync_post(
path,
[assiduite_avant, assiduite_apres],
(data, status) => {
//success
},
(data, status) => {
//error
console.error(data, status);
errorAlert();
}
);
this.callbacks.split(this.selectedAssiduite)
this.refresh(assiduites[this.selectedAssiduite.etudid]);
this.resetSelection();
} else {
const att = document.createTextNode(
"L'heure de séparation doit être compris dans la période de l'assiduité sélectionnée."
);
openAlertModal("Attention", att, "", "var(--color-warning)");
}
};
openPromptModal("Séparation de l'assiduité sélectionnée", fieldSet, success, () => { }, "var(--color-present)");
// Initialisation du timepicker
const deb = this.selectedAssiduite.date_debut.substring(11,16);
const fin = this.selectedAssiduite.date_fin.substring(11,16);
},
() => {},
"var(--color-retard)"
);
// Initialisation du timepicker
const deb = assiduite.date_debut.substring(11,16);
const fin = assiduite.date_fin.substring(11,16);
setTimeout(()=>{
$('#promptTime').timepicker({
timeFormat: 'HH:mm',
@ -331,150 +376,15 @@
});
}, 100
);
}
}
/**
* Modification d'une assiduité conflictuelle
*/
editAssiduiteModal() {
if (!this.selectedAssiduite) return;
//Préparation du modal d'édition
const htmlPrompt = `<legend>Entrez l'état de l'assiduité :</legend>
<select name="promptSelect" id="promptSelect" required>
<option value="">Choissez l'état</option>
<option value="present">Présent</option>
<option value="retard">En Retard</option>
<option value="absent">Absent</option>
</select>`;
const fieldSet = document.createElement("fieldset");
fieldSet.classList.add("fieldsplit");
fieldSet.innerHTML = htmlPrompt;
//Callback d'action d'édition
const success = () => {
const newState = document.getElementById("promptSelect").value;
if (!["present", "absent", "retard"].includes(newState.toLowerCase())) {
const att = document.createTextNode(
"L'état doit être 'present', 'absent' ou 'retard'."
);
openAlertModal("Attention", att, "", "var(--color-warning)");
return;
}
// Actualiser l'affichage
editAssiduite(this.selectedAssiduite.assiduite_id, newState, [this.selectedAssiduite]);
this.callbacks.edit(this.selectedAssiduite)
this.refresh(assiduites[this.selectedAssiduite.etudid]);
// Désélection de l'assiduité
this.resetSelection();
};
//Affichage du prompt
openPromptModal("Modification de l'état de l'assiduité sélectionnée", fieldSet, success, () => { }, "var(--color-present)");
}
/**
* Génération du modal
*/
render() {
const timeLabels = document.querySelector(".time-labels");
const assiduitesContainer = document.querySelector(".assiduites-container");
timeLabels.innerHTML = "";
assiduitesContainer.innerHTML = '<div class="assiduite-special"></div>';
// Ajout des labels d'heure sur la frise chronologique
for (let i = t_start; i <= t_end; i++) {
const timeLabel = document.createElement("div");
timeLabel.className = "time-label";
timeLabel.textContent = i < 10 ? `0${i}:00` : `${i}:00`;
timeLabels.appendChild(timeLabel);
}
//Placement de la période conflictuelle sur la timeline
const specialAssiduiteEl = document.querySelector(".assiduite-special");
specialAssiduiteEl.style.width = getWidth(
this.conflictPeriod.deb,
this.conflictPeriod.fin
);
specialAssiduiteEl.style.left = getLeftPosition(this.conflictPeriod.deb);
specialAssiduiteEl.style.top = "0";
specialAssiduiteEl.style.zIndex = "0"; // Place l'assiduité spéciale en arrière-plan
assiduitesContainer.appendChild(specialAssiduiteEl);
}
//Placement des assiduités sur la timeline
this.list.forEach((assiduite) => {
const period = {
deb: new Date(assiduite.date_debut),
fin: new Date(assiduite.date_fin),
};
if (!hasTimeConflict(period, this.interval)) {
return;
}
const el = document.createElement("div");
el.className = "assiduite";
el.style.backgroundColor = getColor(assiduite.etat);
el.style.width = getWidth(assiduite.date_debut, assiduite.date_fin);
el.style.left = getLeftPosition(assiduite.date_debut);
el.style.top = "10px";
el.setAttribute("data-id", assiduite.assiduite_id);
el.addEventListener("click", () => this.selectAssiduite(assiduite));
// Ajout des informations dans la visualisation d'une assiduité
const infoContainer = document.createElement("div");
infoContainer.className = "assiduite-info";
const idDiv = document.createElement("div");
idDiv.className = "assiduite-id";
idDiv.textContent = `ID: ${assiduite.assiduite_id}`;
infoContainer.appendChild(idDiv);
const periodDivDeb = document.createElement("div");
periodDivDeb.className = "assiduite-period";
periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`;
infoContainer.appendChild(periodDivDeb);
const periodDivFin = document.createElement("div");
periodDivFin.className = "assiduite-period";
periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`;
infoContainer.appendChild(periodDivFin);
const stateDiv = document.createElement("div");
stateDiv.className = "assiduite-state";
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
infoContainer.appendChild(stateDiv);
const userIdDiv = document.createElement("div");
userIdDiv.className = "assiduite-user_id";
userIdDiv.textContent = `saisie le ${formatDateModal(
assiduite.entry_date,
"à"
)} \npar ${assiduite.user_id}`;
infoContainer.appendChild(userIdDiv);
el.appendChild(infoContainer);
assiduitesContainer.appendChild(el);
});
}
/**
* Remise à zéro de la sélection
* Désactivation des boutons d'actions de conflit
*/
resetSelection() {
this.selectedAssiduite = null;
this.deleteBtn.disabled = true;
this.splitBtn.disabled = true;
this.editBtn.disabled = true;
}
}
</script>
<style>
.ui-timepicker-container {

View File

@ -13,23 +13,18 @@
*/
function createMiniTimeline(assiduitesArray, day = null) {
const array = [...assiduitesArray];
const date = day == null ? getDate() : new Date(day);
const date = day == null ? $("#date").datepicker("getDate") : new Date(day);
const timeline = document.createElement("div");
timeline.className = "mini-timeline";
if (isSingleEtud()) {
timeline.classList.add("single");
}
const timelineDate = date.startOf("day");
const dayStart = timelineDate.clone().add(mt_start, "hours");
const dayEnd = timelineDate.clone().add(mt_end, "hours");
const dayStart = new Date(`${timelineDate.format("YYYY-MM-DD").substring(0,10)}T${numberToTime(mt_start)}`);
const dayEnd = new Date(`${timelineDate.format("YYYY-MM-DD").substring(0,10)}T${numberToTime(mt_end)}`);
const dayDuration = new Duration(dayStart, dayEnd).minutes;
timeline.appendChild(setMiniTick(timelineDate, dayStart, dayDuration));
if (day == null) {
const tlTimes = getTimeLineTimes();
const tlTimes = getPeriodAsDate();
array.push({
date_debut: tlTimes.deb.format(),
date_fin: tlTimes.fin.format(),
@ -69,56 +64,20 @@
fin = Math.min(mt_end, fin);
if (day == null) setPeriodValues(deb, fin);
if (isSingleEtud()) {
updateSelectedSelect(getCurrentAssiduiteModuleImplId());
updateJustifyBtn();
}
$("#moduleimpl_select").val(getModuleImplId(assiduité))
setTimeout(()=>{
$("#moduleimpl_select").trigger("change");
}, 0)
});
//ajouter affichage assiduites on over
setupAssiduiteBuble(block, assiduité);
setupAssiduiteBubble(block, assiduité);
}
const action = (justificatifs) => {
if (justificatifs.length > 0) {
let j = "invalid_justified";
// TODO: ajout couleur justificatif
justificatifs.forEach((ju) => {
if (ju.etat == "VALIDE") {
j = "justified";
}
});
block.classList.add(j);
}
};
if (assiduité.etudid) {
getJustificatifFromPeriod(
{
deb: new Date(assiduité.date_debut),
fin: new Date(assiduité.date_fin),
},
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";
}
block.classList.add(assiduité.etat.toLowerCase());
if(assiduité.etat != "CRENEAU") block.classList.add("color");
timeline.appendChild(block);
});
@ -126,57 +85,6 @@
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;
const bubble = document.createElement('div');
bubble.className = "assiduite-bubble";
bubble.classList.add(assiduite.etat.toLowerCase());
const idDiv = document.createElement("div");
idDiv.className = "assiduite-id";
idDiv.textContent = `${getModuleImpl(assiduite)}`;
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 motifDiv = document.createElement("div");
stateDiv.className = "assiduite-why";
stateDiv.textContent = `Motif: ${assiduite.desc?.capitalize()}`;
bubble.appendChild(motifDiv);
const userIdDiv = document.createElement("div");
userIdDiv.className = "assiduite-user_id";
userIdDiv.textContent = `saisie le ${formatDateModal(
assiduite.entry_date,
" à "
)}`;
if (assiduite.user_id != null) {
userIdDiv.textContent += `\npar ${assiduite.user_nom_complet}`
}
bubble.appendChild(userIdDiv);
el.appendChild(bubble);
}
function setMiniTick(timelineDate, dayStart, dayDuration) {
const endDate = timelineDate.clone().startOf("day");
endDate.setHours(13, 0);

View File

@ -182,6 +182,9 @@
promptModal.removeEventListener('click', this)
}
})
document.body.style.overflow = "hidden";
}
function promptModalButtonAction(success, cancel) {
@ -189,7 +192,7 @@
succBtn.classList.add("btnPrompt")
succBtn.textContent = "Valider"
succBtn.addEventListener('click', () => {
const retour = success();
const retour = success(closePromptModal);
if (retour == null || retour == false || retour == undefined) {
closePromptModal();
}
@ -207,6 +210,7 @@
function closePromptModal() {
promptModal.classList.remove("is-active")
document.body.style.overflow = "auto";
}
const promptClose = document.querySelector(".promptModal-close");
promptClose.onclick = function () {

View File

@ -1,8 +1,14 @@
<div class="timeline-container">
<div class="period" style="left: 0%; width: 20%">
<div class="period-handle left"></div>
<div class="period-handle right"></div>
<div class="period-time">Time</div>
<div id="timeline">
<div class="inputs">
<input type="text" name="deb" id="deb" class="timepicker">
<input type="text" name="fin" id="fin" class="timepicker">
</div>
<div class="timeline-container">
<div class="period" style="left: 0%; width: 20%">
<div class="period-handle left"></div>
<div class="period-handle right"></div>
<div class="period-time">Time</div>
</div>
</div>
</div>
<script>
@ -87,6 +93,12 @@
const text = `${deb} - ${fin}`
periodTimeLine.querySelector('.period-time').textContent = text;
//Mise à jour des inputs
try{
$('#deb').val(deb);
$('#fin').val(fin);
}catch{}
}
function timelineMainEvent(event) {
@ -176,6 +188,25 @@
func_call = callback;
timelineContainer.addEventListener("mousedown", (e) => { timelineMainEvent(e) });
timelineContainer.addEventListener("touchstart", (e) => { timelineMainEvent(e) });
const updateFromInputs = ()=>{
let deb = $('#deb').val();
let fin = $('#fin').val();
if (deb != '' && fin != '') {
deb = fromTime(deb);
fin = fromTime(fin);
try {
setPeriodValues(deb, fin);
} catch {
setPeriodValues(...getPeriodValues());
}
}
}
$('#deb').data('TimePicker').options.change = updateFromInputs;
$('#fin').data('TimePicker').options.change = updateFromInputs;
updatePeriodTimeLabel();
}
function adjustPeriodPosition(newLeft, newWidth) {
@ -204,7 +235,7 @@
const computedValues = [Math.max(startValue, t_start), Math.min(t_end, endValue)];
if (computedValues[0] > t_end || computedValues[1] < t_start) {
return [t_start, min(t_end, t_start + period_default)];
return [t_start, Math.min(t_end, t_start + period_default)];
}
if (computedValues[1] - computedValues[0] <= tick_delay && computedValues[1] < t_end - tick_delay) {
@ -262,6 +293,22 @@
return hours + minutes / 60
}
function getPeriodAsDate(){
let [deb, fin] = getPeriodValues();
deb = numberToTime(deb);
fin = numberToTime(fin);
const dateStr = $("#date")
.datepicker("getDate")
.format("yyyy-mm-dd")
.substring(0, 10);
return {
deb: new Date(`${dateStr}T${deb}`),
fin: new Date(`${dateStr}T${fin}`)
}
}
createTicks();
setPeriodValues(t_start, t_start + period_default);
@ -277,6 +324,21 @@
</script>
<style>
#timeline {
display: flex;
justify-content: start;
}
.inputs {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 5px;
margin-bottom: 10px;
width: 5em;
}
.timeline-container {
width: 75%;
margin-left: 25px;

View File

@ -1813,33 +1813,8 @@ def signal_assiduites_diff():
formsemestre_id: int = request.args.get("formsemestre_id", -1)
formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
date: str = request.args.get("jour", datetime.date.today().isoformat())
date_deb: str = request.args.get("date_deb")
date_fin: str = request.args.get("date_fin")
semaine: str = request.args.get("semaine")
# Dans le cas où on donne une semaine plutot qu'un jour
if semaine is not None:
# On génère la semaine iso à partir de l'anne scolaire.
semaine = (
f"{scu.annee_scolaire()}-W{semaine}" if "W" not in semaine else semaine
)
# On met à jour les dates avec le date de debut et fin de semaine
date_deb: datetime.date = datetime.datetime.strptime(
semaine + "-1", "%Y-W%W-%w"
)
date_fin: datetime.date = date_deb + datetime.timedelta(days=6)
etudiants: list[Identite] = []
# --- Vérification de la date ---
real_date = scu.is_iso_formated(date, True).date()
if real_date < formsemestre.date_debut:
date = formsemestre.date_debut.isoformat()
elif real_date > formsemestre.date_fin:
date = formsemestre.date_fin.isoformat()
# Vérification des groupes
if group_ids is None:
group_ids = []
@ -1873,28 +1848,26 @@ def signal_assiduites_diff():
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
)
moduleimpl_id = request.args.get("moduleimpl_id", -1)
try:
moduleimpl_id = int(moduleimpl_id)
except ValueError:
moduleimpl_id = -1
return render_template(
"assiduites/pages/signal_assiduites_diff.j2",
defaultDates=_get_days_between_dates(date_deb, date_fin),
defdem=_get_etuds_dem_def(formsemestre),
diff=_differee(
etudiants=etudiants,
moduleimpl_select=_module_selector(
formsemestre, request.args.get("moduleimpl_id", None)
),
date=date,
periode={
"deb": formsemestre.date_debut.isoformat(),
"fin": formsemestre.date_fin.isoformat(),
},
etudiants=etudiants,
moduleimpl_select=_module_selector(
formsemestre=formsemestre, moduleimpl_id=moduleimpl_id
),
gr=gr_tit,
nonworkdays=_non_work_days(),
sco=ScoData(formsemestre=formsemestre),
sem=formsemestre.titre_num(),
timeEvening=ScoDocSiteConfig.get("assi_afternoon_time", "18:00:00"),
timeMorning=ScoDocSiteConfig.get("assi_morning_time", "08:00:00"),
timeNoon=ScoDocSiteConfig.get("assi_lunch_time", "13:00:00"),
forcer_module=sco_preferences.get_preference(
"forcer_module",
formsemestre_id=formsemestre_id,
dept_id=g.scodoc_dept_id,
),
)