Ajout des commentaires dans le code

This commit is contained in:
PTA
2026-08-20 13:22:49 +02:00
parent 4201894e28
commit 5d8c0993a6
154 changed files with 5071 additions and 47 deletions

View File

@@ -1,3 +1,20 @@
/**
* Vue mensuelle d'un agent (`PTA.modules.agentOverview`), écran agents.php.
*
* Affiche le planning d'un agent mois par mois, TOUS LIEUX CONFONDUS, et permet
* de modifier ses créneaux sans passer par l'écran de saisie.
*
* DIFFÉRENCE ESSENTIELLE AVEC L'ÉDITEUR DE PLANNING : ici, toute modification
* part IMMÉDIATEMENT au serveur. Il n'y a pas de brouillon local, pas de bouton
* « Enregistrer » : la page ne travaille pas sur une semaine mais sur des
* créneaux déjà en base, éventuellement répartis sur plusieurs lieux et
* plusieurs plannings. Chaque geste est donc suivi d'un rechargement complet.
*
* Trois niveaux de droits se combinent : `can_edit_draft` pour modifier,
* `can_edit_cross_structure` pour toucher un créneau d'un autre lieu que le
* sien, `can_copy_full_week` pour dupliquer une semaine entière. L'interface
* masque en conséquence ; le serveur retranche de toute façon.
*/
(() => {
'use strict';
@@ -26,18 +43,34 @@
const typeLabel = (type) => type === 'EXTRASCOLAIRE' ? 'Extrascolaire' : 'Périscolaire';
/** Message dans un conteneur donné : la page en a plusieurs (bandeau, modales). */
function setMessage(element, text = '', type = 'info') {
if (!element) return;
element.className = text ? `message message-${type}` : 'message';
element.textContent = text;
}
/**
* Un créneau n'est modifiable que si l'utilisateur peut éditer les
* brouillons ET que le créneau relève de son périmètre : hors
* `can_edit_cross_structure`, seul son propre lieu.
*/
function canEditEntry(entry) {
if (!canEditDraft) return false;
if (canEditCrossStructure) return true;
return Number(entry.id_structure) === Number(accessProfile.structure_id || 0);
}
/**
* Balisage d'un créneau dans la grille mensuelle.
*
* Un créneau non modifiable est rendu SANS attribut de glisser-déposer et
* porte la mention « Lecture seule » : le droit se voit, plutôt que de se
* découvrir par un refus au clic.
*
* Toutes les valeurs issues de la base passent par escapeHtml(), y compris
* `motif_code` qui alimente un nom de classe CSS.
*/
function entryBadge(entry) {
const editable = canEditEntry(entry);
const interactionAttributes = editable
@@ -62,6 +95,7 @@
`;
}
/** Total des minutes s'imputant sur le quota, motifs hors quota exclus. */
function countedMinutes(entries) {
return entries.reduce((sum, entry) => {
if (!Number(entry.compte_dans_quota)) return sum;
@@ -69,6 +103,13 @@
}, 0);
}
/**
* Rend la grille mensuelle : une carte par jour, groupée par semaine ISO.
*
* La grille déborde du mois pour rester alignée sur les semaines complètes
* (voir AgentController::monthOverview) ; les jours hors mois sont grisés
* mais bien comptés dans les totaux hebdomadaires.
*/
function render(data) {
const container = document.querySelector('#agent-overview-content');
const empty = document.querySelector('#agent-overview-empty');
@@ -138,10 +179,19 @@
bindMonthInteractions();
}
/**
* Retrouve un créneau dans les données du dernier chargement.
* `shared.lastAgentOverviewData` sert de cache : le glisser-déposer a besoin
* du créneau complet à partir du seul identifiant porté par le DOM.
*/
function findEntry(entryId) {
return shared.lastAgentOverviewData?.entries?.find((item) => Number(item.id_creneau) === Number(entryId)) || null;
}
/**
* Envoie un déplacement ou une duplication au serveur.
* Laisse remonter l'exception : l'appelant décide de l'affichage.
*/
async function persistEntryMove(entryId, targetDate, duplicate) {
const response = await fetch('api/move_or_duplicate_entry.php', {
method: 'POST',
@@ -158,12 +208,22 @@
return data;
}
/** Retire les marques visuelles de dépôt sur toutes les cartes de jour. */
function clearDropTargets() {
document.querySelectorAll('#agent-overview-content .agent-month-day-card').forEach((card) => {
card.classList.remove('is-drop-target', 'is-copy-target');
});
}
/**
* Attache le glisser-déposer aux créneaux et aux jours après chaque rendu.
*
* À rappeler à CHAQUE `render()` : la grille est reconstruite entièrement et
* les écouteurs précédents disparaissent avec les anciens nœuds.
*
* Ctrl/⌘ enfoncé pendant le glisser transforme le déplacement en
* duplication, convention reprise des gestionnaires de fichiers.
*/
function bindMonthInteractions() {
const container = document.querySelector('#agent-overview-content');
if (!container) return;
@@ -225,6 +285,7 @@
});
}
/** Ouvre le contrat horaire annuel de l'agent dans un nouvel onglet. */
function openHoursContractPdf() {
const data = shared.lastAgentOverviewData;
const agentId = data?.agent?.id_agent || document.querySelector('#overview-agent')?.value;
@@ -234,6 +295,7 @@
window.open(`api/agent_hours_contract_pdf.php?${params.toString()}`, '_blank', 'noopener');
}
/** Ouvre le bilan d'heures annuel de l'agent dans un nouvel onglet. */
function openTimeSummaryPdf() {
const data = shared.lastAgentOverviewData;
const agentId = data?.agent?.id_agent || document.querySelector('#overview-agent')?.value;
@@ -260,6 +322,7 @@
* droit à l'enveloppe de formation. Le motif déjà porté par le créneau reste
* proposé, sans quoi le modal changerait silencieusement de motif.
*/
/** Options du sélecteur de motif de la modale d'édition. */
function renderMotifOptions(selectedMotifId) {
const select = document.querySelector('#edit-agent-entry-motif');
const motifs = window.PTA_CONFIG?.motifs || [];
@@ -276,6 +339,7 @@
.join('');
}
/** Ouvre la modale d'édition d'un créneau, préremplie de ses valeurs. */
function openModal(entryId) {
const entry = findEntry(entryId);
if (!entry || !canEditEntry(entry)) return;
@@ -295,6 +359,11 @@
document.querySelector('#edit-agent-entry-motif')?.focus();
}
/**
* Supprime un créneau après confirmation, puis recharge la vue.
* Si le planning porteur était validé, le serveur le repasse en brouillon et
* le signale dans son message.
*/
async function deleteEntry(entryId) {
const entry = findEntry(entryId);
if (!entry || !canEditEntry(entry)) return;
@@ -334,6 +403,7 @@
}
}
/** Enregistre les modifications du créneau, puis recharge la vue. */
async function saveEntry(event) {
event.preventDefault();
if (!canEditDraft) return;
@@ -371,6 +441,14 @@
}
}
/**
* Date → « 2026-W08 », numérotation ISO 8601.
*
* L'algorithme décale la date au JEUDI de sa semaine (`+ 4 - jour`), car
* c'est le jeudi qui détermine à quelle année appartient une semaine ISO.
* Sans ce décalage, une semaine à cheval sur deux années serait mal
* numérotée. Calcul en UTC pour éviter tout décalage de fuseau.
*/
function isoWeekValueFromDate(date) {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const day = d.getUTCDay() || 7;
@@ -380,6 +458,7 @@
return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;
}
/** « 2026-W08 » → la Date du lundi correspondant (semaine 1 = celle du 4 janvier). */
function weekMonday(weekValue) {
const match = /^(\d{4})-W(\d{2})$/.exec(weekValue);
if (!match) return null;
@@ -392,6 +471,11 @@
return monday;
}
/**
* Ouvre la modale de duplication d'une semaine entière.
* Réservée à `can_copy_full_week` : la copie porte sur TOUS les lieux de
* l'agent, elle déborde du périmètre d'un responsable de structure.
*/
function openWeekCopyModal(sourceWeek) {
if (!canCopyFullWeek) return;
const modal = document.querySelector('#agent-week-copy-modal');
@@ -414,6 +498,7 @@
targetInput.focus();
}
/** Ferme la modale de duplication de semaine. */
function closeWeekCopyModal() {
const modal = document.querySelector('#agent-week-copy-modal');
if (!modal) return;
@@ -424,6 +509,12 @@
setMessage(document.querySelector('#agent-week-copy-message'));
}
/**
* Duplique la semaine source vers la semaine cible.
* Deux modes : « merge » complète l'existant, « replace » l'écrase. En mode
* merge, le moindre chevauchement fait échouer toute l'opération côté
* serveur — il n'y a pas de copie partielle.
*/
async function copyWeek(event) {
event.preventDefault();
if (!canCopyFullWeek) return;
@@ -471,6 +562,11 @@
}
}
/**
* Charge et rend la vue mensuelle de l'agent sélectionné.
* Sans agent ni mois, affiche une invite plutôt qu'une erreur : c'est
* l'état normal à l'ouverture de la page.
*/
async function load() {
const agentId = document.querySelector('#overview-agent')?.value;
const month = document.querySelector('#agent-overview-month')?.value;
@@ -502,6 +598,12 @@
}
}
/**
* Point d'entrée appelé par page-bootstrap.js.
* Les clics dans la grille sont traités par DÉLÉGATION sur le conteneur :
* elle est redessinée à chaque chargement, des écouteurs individuels
* devraient être reposés à chaque fois.
*/
function init() {
document.querySelector('#load-agent-overview')?.addEventListener('click', load);
document.querySelector('#overview-agent')?.addEventListener('change', load);