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,24 @@
/**
* Application des semaines types depuis l'écran de saisie
* (`PTA.modules.weekTemplates`).
*
* À distinguer de `week-template-editor.js`, qui sert à COMPOSER un modèle ; ce
* module-ci sert à en ENREGISTRER un depuis la semaine affichée, et surtout à
* l'APPLIQUER. Les deux cohabitent sur planning.php et ne sont chargés que si la
* permission `can_use_templates` est accordée.
*
* Trois modales, de complexité croissante :
* 1. ENREGISTRER la semaine affichée comme modèle ;
* 2. APPLIQUER un modèle à la semaine affichée ;
* 3. APPLIQUER un modèle sur une PÉRIODE — la seule qui impose un aperçu.
*
* Le parcours d'aperçu est le point le plus délicat : une application sur
* période touche des dizaines de semaines et peut rouvrir des plannings validés.
* Le bouton de confirmation reste donc désactivé tant qu'un aperçu VALIDE ET À
* JOUR n'a pas été obtenu. C'est le rôle de `lastPeriodPreviewSignature` :
* toute modification d'un paramètre invalide l'aperçu, empêchant d'appliquer un
* réglage différent de celui qui a été montré.
*/
(() => {
'use strict';
@@ -41,14 +62,17 @@
let lastPeriodPreview = null;
let lastPeriodPreviewSignature = '';
/** Un agent et une semaine suffisent : le modèle porte ses propres lieux. */
function contextReady() {
return Boolean(agentSelect?.value && weekInput?.value);
}
/** Nombre de créneaux de la semaine, tous lieux confondus. */
function globalEntryCount() {
return (state.entries?.length || 0) + (state.otherStructureEntries?.length || 0);
}
/** Réactive les trois boutons de semaine type selon le contexte courant. */
function refreshButtons() {
const ready = contextReady();
if (saveButton) saveButton.disabled = !ready;
@@ -56,12 +80,17 @@
if (periodButton) periodButton.disabled = !ready;
}
/**
* Message DANS une modale, distinct du bandeau global de la page : une
* modale ouverte masque le bandeau, l'erreur y passerait inaperçue.
*/
function setInlineMessage(element, text = '', type = 'info') {
if (!element) return;
element.className = text ? `message message-${type}` : 'message';
element.textContent = text;
}
/** Bloque le défilement de la page tant qu'une modale est ouverte. */
function updateBodyModalState() {
const anyOpen = Array.from(document.querySelectorAll('.modal-backdrop'))
.some((modal) => !modal.hidden);
@@ -80,10 +109,16 @@
document.body.classList.add('modal-open');
}
/** Libellé de l'agent sélectionné, pour les intitulés de modale. */
function agentLabel() {
return agentSelect?.options[agentSelect.selectedIndex]?.textContent?.trim() || 'Agent sélectionné';
}
/**
* Enregistre le brouillon en cours avant d'en faire un modèle.
* Le modèle est construit côté serveur à partir de ce qui est EN BASE : une
* saisie non enregistrée n'y figurerait pas.
*/
async function persistCurrentEditorIfNeeded() {
const planning = PTA.modules.planning;
if (!planning || state.planningStatus === 'VALIDE') return true;
@@ -93,6 +128,7 @@
return planningId !== null && planningId !== undefined;
}
/** Ouvre la modale d'enregistrement, après avoir sécurisé le brouillon. */
async function openSaveModal() {
if (!contextReady()) {
showMessage('Sélectionnez un agent et une semaine avant d’enregistrer une semaine type.', 'warning');
@@ -120,6 +156,13 @@
setTimeout(() => saveNameInput?.focus(), 0);
}
/**
* Enregistre la semaine affichée comme modèle.
*
* Un 409 signifie qu'un modèle porte déjà ce nom : l'utilisateur se voit
* proposer le remplacement, ce qui rappelle cette fonction avec
* `replace = true`. Jamais d'écrasement sans confirmation.
*/
async function saveTemplate(replace = false) {
const name = saveNameInput?.value?.trim() || '';
if (!name) {
@@ -173,6 +216,7 @@
}
}
/** Recharge la liste des modèles de l'agent courant. */
async function loadTemplates() {
if (!agentSelect?.value) return [];
@@ -183,6 +227,7 @@
return templates;
}
/** Remplit le sélecteur de la modale d'application. */
function renderTemplateSelect() {
if (!templateSelect) return;
if (!templates.length) {
@@ -200,6 +245,7 @@
renderSelectedTemplateDetails();
}
/** Affiche le contenu du modèle choisi, pour vérification avant application. */
function renderSelectedTemplateDetails() {
const selectedId = Number(templateSelect?.value || 0);
const template = templates.find((item) => Number(item.id) === selectedId);
@@ -223,6 +269,7 @@
* `preselectedId` permet à l'éditeur de semaine type d'ouvrir la modale
* directement sur le modèle en cours d'édition, sans le rechercher.
*/
/** Ouvre la modale d'application à la semaine affichée. */
async function openApplyModal(preselectedId = null) {
if (!contextReady()) {
showMessage('Sélectionnez un agent et la semaine cible avant d’importer une semaine type.', 'warning');
@@ -249,6 +296,10 @@
}
}
/**
* Applique un modèle à la semaine affichée, puis recharge le planning : les
* créneaux créés doivent apparaître immédiatement.
*/
async function applyTemplate() {
const templateId = Number(templateSelect?.value || 0);
const mode = applyForm?.querySelector('input[name="week-template-mode"]:checked')?.value || 'merge';
@@ -307,14 +358,23 @@
}
}
/** Mode d'application sur période : « merge » (défaut) ou « replace ». */
function periodMode() {
return periodForm?.querySelector('input[name="week-template-period-mode"]:checked')?.value || 'merge';
}
/** Filtre de calendrier : « all », « school » ou « extra ». */
function periodFilter() {
return periodForm?.querySelector('input[name="week-template-period-filter"]:checked')?.value || 'all';
}
/**
* « 2026-W08 » → l'objet Date du lundi de cette semaine ISO.
*
* Calcul entièrement en UTC (Date.UTC, getUTC*) : ces dates ne servent qu'à
* préremplir des champs, jamais à afficher une heure, et l'UTC évite tout
* décalage de fuseau ou d'heure d'été.
*/
function weekMonday(weekValue) {
const match = /^(\d{4})-W(\d{2})$/.exec(weekValue || '');
if (!match) return null;
@@ -327,6 +387,7 @@
return monday;
}
/** Date → « AAAA-MM-JJ » en UTC, pour un champ `input type="date"`. */
function dateInputValue(date) {
if (!(date instanceof Date) || Number.isNaN(date.getTime())) return '';
const year = date.getUTCFullYear();
@@ -335,12 +396,23 @@
return `${year}-${month}-${day}`;
}
/**
* « 2026-02-17 » → « 17/02/2026 ».
* Le « T12:00:00 » ancre la date à midi : à minuit, un décalage de fuseau
* ferait basculer l'affichage sur la veille.
*/
function formatDateFr(value) {
if (!value) return '';
const date = new Date(`${value}T12:00:00`);
return new Intl.DateTimeFormat('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' }).format(date);
}
/**
* Invalide l'aperçu et REDÉSACTIVE le bouton d'application.
*
* Appelée dès qu'un paramètre change. C'est la garantie qu'on n'applique
* jamais un réglage différent de celui qui a été montré à l'utilisateur.
*/
function invalidatePeriodPreview() {
lastPeriodPreview = null;
lastPeriodPreviewSignature = '';
@@ -353,6 +425,10 @@
updatePeriodReplaceWarning();
}
/**
* Empreinte des paramètres courants, comparée à celle de l'aperçu obtenu
* pour détecter qu'il est devenu obsolète.
*/
function currentPeriodSignature() {
return JSON.stringify({
template_id: Number(periodTemplateSelect?.value || 0),
@@ -363,6 +439,7 @@
});
}
/** Remplit le sélecteur de la modale « période ». */
function renderPeriodTemplateSelect() {
if (!periodTemplateSelect) return;
if (!templates.length) {
@@ -376,6 +453,10 @@
)).join('');
}
/**
* Préremplit la période avec la semaine affichée (lundi → vendredi), sans
* jamais écraser une saisie de l'utilisateur.
*/
function setDefaultPeriodDates() {
const monday = weekMonday(weekInput?.value);
if (!monday) return;
@@ -387,6 +468,7 @@
}
}
/** Ouvre la modale « période », aperçu remis à zéro. */
async function openPeriodModal(preselectedId = null) {
if (!contextReady()) {
showMessage('Sélectionnez un agent et une semaine avant d’appliquer une semaine type sur une période.', 'warning');
@@ -415,10 +497,23 @@
}
}
/** Affiche l'avertissement propre au mode « remplacer ». */
function updatePeriodReplaceWarning() {
if (periodReplaceWarning) periodReplaceWarning.hidden = periodMode() !== 'replace';
}
/**
* Rend l'aperçu d'application sur période : compteurs de synthèse, vacances
* prises en compte, conflits, puis le détail jour par jour.
*
* Le tableau est PLAFONNÉ à 80 journées, avec la mention du total réel :
* deux ans de période produiraient plusieurs centaines de lignes, illisibles
* et coûteuses à rendre.
*
* L'application n'est autorisée que si le plan crée au moins un créneau ET,
* en mode « compléter », qu'aucun conflit ne subsiste — le serveur refuserait
* de toute façon.
*/
function renderPeriodPreview(preview) {
if (!periodPreviewPanel) return;
const summary = preview.summary || {};
@@ -484,6 +579,7 @@
if (periodConfirmButton) periodConfirmButton.disabled = !canApply;
}
/** Demande l'aperçu au serveur et mémorise sa signature. Aucune écriture. */
async function previewPeriodApplication() {
const templateId = Number(periodTemplateSelect?.value || 0);
const startDate = periodStartInput?.value || '';
@@ -532,6 +628,11 @@
}
}
/**
* Applique le modèle sur la période, une fois l'aperçu confirmé.
* Le message de retour du serveur est affiché tel quel : il détaille les
* créneaux créés, ignorés, remplacés et les plannings rouverts.
*/
async function applyTemplatePeriod() {
if (!lastPeriodPreview || lastPeriodPreviewSignature !== currentPeriodSignature()) {
setInlineMessage(periodMessage, 'La période a changé. Relancez la prévisualisation avant d’appliquer le modèle.', 'warning');
@@ -591,6 +692,10 @@
}
}
/**
* Supprime le modèle sélectionné, après confirmation.
* Les créneaux déjà appliqués ne sont pas touchés.
*/
async function deleteSelectedTemplate() {
const templateId = Number(templateSelect?.value || 0);
const template = templates.find((item) => Number(item.id) === templateId);
@@ -622,6 +727,7 @@
}
}
/** Affiche l'avertissement du mode « remplacer » de la modale semaine. */
function updateReplaceWarning() {
const mode = applyForm?.querySelector('input[name="week-template-mode"]:checked')?.value || 'merge';
if (replaceWarning) replaceWarning.hidden = mode !== 'replace';