Ajout des commentaires dans le code
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user