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