/** * 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'; const PTA = window.PTA; if (!PTA) throw new Error('PTA core doit être chargé avant week-templates.js'); const { state } = PTA; const { agentSelect, weekInput } = PTA.elements; const { showMessage, readJsonResponse, apiErrorMessage, escapeHtml } = PTA.utils; const saveButton = document.querySelector('#save-week-template'); const applyButton = document.querySelector('#apply-week-template'); const saveModal = document.querySelector('#save-week-template-modal'); const saveForm = document.querySelector('#save-week-template-form'); const saveNameInput = document.querySelector('#week-template-name'); const saveContext = document.querySelector('#save-week-template-context'); const saveMessage = document.querySelector('#save-week-template-message'); const applyModal = document.querySelector('#apply-week-template-modal'); const applyForm = document.querySelector('#apply-week-template-form'); const templateSelect = document.querySelector('#week-template-select'); const templateDetails = document.querySelector('#week-template-details'); const replaceWarning = document.querySelector('#week-template-replace-warning'); const deleteButton = document.querySelector('#delete-week-template'); const applyMessage = document.querySelector('#apply-week-template-message'); const periodButton = document.querySelector('#apply-week-template-period'); const periodModal = document.querySelector('#apply-week-template-period-modal'); const periodForm = document.querySelector('#apply-week-template-period-form'); const periodTemplateSelect = document.querySelector('#week-template-period-select'); const periodStartInput = document.querySelector('#week-template-period-start'); const periodEndInput = document.querySelector('#week-template-period-end'); const periodReplaceWarning = document.querySelector('#week-template-period-replace-warning'); const periodPreviewButton = document.querySelector('#preview-week-template-period'); const periodPreviewPanel = document.querySelector('#week-template-period-preview'); const periodConfirmButton = document.querySelector('#confirm-apply-week-template-period'); const periodMessage = document.querySelector('#apply-week-template-period-message'); let templates = []; 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; if (applyButton) applyButton.disabled = !ready; 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); document.body.classList.toggle('modal-open', anyOpen); } function closeModal(modal) { if (!modal) return; modal.hidden = true; updateBodyModalState(); } function openModal(modal) { if (!modal) return; modal.hidden = false; 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; if (!planning.isPlanningContextReady?.()) return true; const planningId = await planning.saveDraft?.({ silent: true }); 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'); return; } if (state.planningStatus !== 'VALIDE') { const synced = PTA.modules.planning?.syncEntriesFromWeeklyForm?.({ silent: true }); if (synced === false) return; } if (globalEntryCount() === 0) { showMessage('La semaine ne contient aucune affectation à enregistrer comme modèle.', 'warning'); return; } if (saveNameInput) saveNameInput.value = ''; if (saveContext) { saveContext.innerHTML = ` ${escapeHtml(agentLabel())} Semaine ${escapeHtml(weekInput.value)} · ${globalEntryCount()} créneau${globalEntryCount() > 1 ? 'x' : ''} actuellement affiché${globalEntryCount() > 1 ? 's' : ''} `; } setInlineMessage(saveMessage); openModal(saveModal); 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) { setInlineMessage(saveMessage, 'Saisissez un nom pour la semaine type.', 'warning'); saveNameInput?.focus(); return; } const submitButton = saveForm?.querySelector('button[type="submit"]'); if (submitButton) submitButton.disabled = true; setInlineMessage(saveMessage, 'Enregistrement du modèle...', 'info'); try { const persisted = await persistCurrentEditorIfNeeded(); if (!persisted) { throw new Error('Le planning courant n’a pas pu être enregistré avant la création du modèle.'); } const response = await fetch('api/save_week_template.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, agent_id: Number(agentSelect.value), week: weekInput.value, name, replace, }), }); const data = await readJsonResponse(response); if (response.status === 409 && data.code === 'TEMPLATE_EXISTS' && !replace) { const confirmed = window.confirm(`Une semaine type nommée « ${name} » existe déjà pour cet agent. Voulez-vous la remplacer par la semaine actuelle ?`); if (confirmed) { await saveTemplate(true); } else { setInlineMessage(saveMessage, 'Enregistrement annulé.', 'info'); } return; } if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible d’enregistrer la semaine type.')); setInlineMessage(saveMessage, data.message, 'success'); showMessage(data.message, 'success'); setTimeout(() => closeModal(saveModal), 450); } catch (error) { setInlineMessage(saveMessage, error.message, 'error'); } finally { if (submitButton) submitButton.disabled = false; } } /** Recharge la liste des modèles de l'agent courant. */ async function loadTemplates() { if (!agentSelect?.value) return []; const response = await fetch(`api/week_templates.php?agent_id=${encodeURIComponent(agentSelect.value)}`); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de charger les semaines types.')); templates = Array.isArray(data.templates) ? data.templates : []; return templates; } /** Remplit le sélecteur de la modale d'application. */ function renderTemplateSelect() { if (!templateSelect) return; if (!templates.length) { templateSelect.innerHTML = ''; templateSelect.disabled = true; if (deleteButton) deleteButton.disabled = true; if (templateDetails) templateDetails.innerHTML = '

Créez d’abord une semaine type depuis une semaine déjà planifiée.

'; return; } templateSelect.disabled = false; templateSelect.innerHTML = '' + templates.map((template) => ( `` )).join(''); 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); if (deleteButton) deleteButton.disabled = !template; if (!templateDetails) return; if (!template) { templateDetails.innerHTML = '

Sélectionnez un modèle pour l’appliquer à la semaine affichée.

'; return; } templateDetails.innerHTML = `
${escapeHtml(template.name)} ${Number(template.entry_count)} créneau${Number(template.entry_count) > 1 ? 'x' : ''} · ${Number(template.location_count)} lieu${Number(template.location_count) > 1 ? 'x' : ''}
`; } /** * `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'); return; } setInlineMessage(applyMessage, 'Chargement des modèles...', 'info'); if (templateSelect) { templateSelect.disabled = true; templateSelect.innerHTML = ''; } openModal(applyModal); try { await loadTemplates(); renderTemplateSelect(); if (preselectedId && templateSelect) { templateSelect.value = String(Number(preselectedId)); renderSelectedTemplateDetails(); } setInlineMessage(applyMessage); } catch (error) { setInlineMessage(applyMessage, error.message, 'error'); } } /** * 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'; const template = templates.find((item) => Number(item.id) === templateId); if (!templateId || !template) { setInlineMessage(applyMessage, 'Sélectionnez une semaine type.', 'warning'); return; } if (mode === 'replace') { const confirmed = window.confirm( `Le mode « Remplacer » va supprimer toutes les affectations actuelles de ${agentLabel()} pour ${weekInput.value}, tous lieux confondus, puis appliquer « ${template.name} ».\n\nContinuer ?` ); if (!confirmed) return; } const submitButton = applyForm?.querySelector('button[type="submit"]'); if (submitButton) submitButton.disabled = true; if (deleteButton) deleteButton.disabled = true; setInlineMessage(applyMessage, 'Import de la semaine type...', 'info'); try { // En mode compléter, persister d'abord les éventuelles modifications locales non encore enregistrées. if (mode === 'merge') { const persisted = await persistCurrentEditorIfNeeded(); if (!persisted) { throw new Error('Le planning courant n’a pas pu être enregistré avant l’import.'); } } const response = await fetch('api/apply_week_template.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, template_id: templateId, agent_id: Number(agentSelect.value), week: weekInput.value, mode, }), }); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible d’importer la semaine type.')); if (data.quota) state.quota = data.quota; await PTA.modules.planning?.loadPlanning?.(); setInlineMessage(applyMessage, data.message, 'success'); showMessage(data.message, 'success'); setTimeout(() => closeModal(applyModal), 550); } catch (error) { setInlineMessage(applyMessage, error.message, 'error'); } finally { if (submitButton) submitButton.disabled = false; renderSelectedTemplateDetails(); } } /** 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; const year = Number(match[1]); const week = Number(match[2]); const jan4 = new Date(Date.UTC(year, 0, 4)); const jan4Day = jan4.getUTCDay() || 7; const monday = new Date(jan4); monday.setUTCDate(jan4.getUTCDate() - jan4Day + 1 + (week - 1) * 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(); const month = String(date.getUTCMonth() + 1).padStart(2, '0'); const day = String(date.getUTCDate()).padStart(2, '0'); 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 = ''; if (periodConfirmButton) periodConfirmButton.disabled = true; if (periodPreviewPanel) { periodPreviewPanel.hidden = true; periodPreviewPanel.innerHTML = ''; } setInlineMessage(periodMessage); 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), start_date: periodStartInput?.value || '', end_date: periodEndInput?.value || '', mode: periodMode(), period_filter: periodFilter(), }); } /** Remplit le sélecteur de la modale « période ». */ function renderPeriodTemplateSelect() { if (!periodTemplateSelect) return; if (!templates.length) { periodTemplateSelect.innerHTML = ''; periodTemplateSelect.disabled = true; return; } periodTemplateSelect.disabled = false; periodTemplateSelect.innerHTML = '' + templates.map((template) => ( `` )).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; if (periodStartInput && !periodStartInput.value) periodStartInput.value = dateInputValue(monday); if (periodEndInput && !periodEndInput.value) { const friday = new Date(monday); friday.setUTCDate(monday.getUTCDate() + 4); periodEndInput.value = dateInputValue(friday); } } /** 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'); return; } invalidatePeriodPreview(); setDefaultPeriodDates(); if (periodTemplateSelect) { periodTemplateSelect.disabled = true; periodTemplateSelect.innerHTML = ''; } openModal(periodModal); setInlineMessage(periodMessage, 'Chargement des modèles...', 'info'); try { await loadTemplates(); renderPeriodTemplateSelect(); if (preselectedId && periodTemplateSelect) { periodTemplateSelect.value = String(Number(preselectedId)); invalidatePeriodPreview(); } setInlineMessage(periodMessage); } catch (error) { setInlineMessage(periodMessage, error.message, 'error'); } } /** 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 || {}; const conflicts = Array.isArray(preview.conflicts) ? preview.conflicts : []; const days = Array.isArray(preview.days) ? preview.days : []; const vacations = Array.isArray(preview.vacation_periods) ? preview.vacation_periods : []; const mode = preview.mode || periodMode(); const filter = preview.period_filter || periodFilter(); const filterLabel = filter === 'school' ? 'Périodes scolaires uniquement' : filter === 'extra' ? 'Périodes extrascolaires uniquement' : 'Toutes les semaines'; const vacationHtml = vacations.length ? `
Vacances prises en compte
` : filter === 'extra' ? '
Aucune période de vacances enregistrée sur cette plage. Aucun jour ne pourra être considéré comme extrascolaire tant que le calendrier n’est pas renseigné.
' : '
Aucune période de vacances enregistrée sur cette plage. Les dates seront considérées comme scolaires.
'; const relevantDays = days.filter((day) => Number(day.eligible_count) > 0 || Number(day.skipped_count) > 0 || Number(day.conflict_count) > 0); const dayRows = relevantDays.slice(0, 80).map((day) => { const periodLabel = day.period_type === 'EXTRASCOLAIRE' ? `Extrascolaire${day.vacation_label ? ` · ${escapeHtml(day.vacation_label)}` : ''}` : 'Périscolaire'; return ` ${formatDateFr(day.date)} ${periodLabel} ${Number(day.eligible_count)} ${Number(day.skipped_count)} ${Number(day.conflict_count) || '—'} `; }).join(''); const conflictHtml = conflicts.length ? `
${conflicts.length} conflit${conflicts.length > 1 ? 's' : ''} détecté${conflicts.length > 1 ? 's' : ''}. ${mode === 'merge' ? 'Le mode « Compléter » ne peut pas être appliqué tant que ces conflits existent. Vous pouvez choisir « Remplacer les jours concernés » après vérification.' : 'Ces créneaux existants seront remplacés sur les jours concernés.'}
` : '
Aucun chevauchement détecté.
'; periodPreviewPanel.innerHTML = `
Périmètre${escapeHtml(filterLabel)}
Créneaux à créer${Number(summary.entry_count || 0)}
Jours concernés${Number(summary.target_day_count || 0)}
Semaines touchées${Number(summary.week_count || 0)}
Ignorés (vacances)${Number(summary.skipped_vacation_count || 0)}
Ignorés (hors vacances)${Number(summary.skipped_school_count || 0)}
${mode === 'replace' ? 'Créneaux existants remplacés' : 'Conflits'}${mode === 'replace' ? Number(summary.existing_entry_count_on_target_dates || 0) : Number(summary.conflict_count || 0)}
${vacationHtml} ${conflictHtml}
${dayRows || ''}
DateContexteCréésIgnorésConflits
Aucune journée du modèle ne correspond à cette période.
${relevantDays.length > 80 ? `

Aperçu limité aux 80 premières journées concernées sur ${relevantDays.length}.

` : ''} `; periodPreviewPanel.hidden = false; const canApply = Number(summary.entry_count || 0) > 0 && (mode !== 'merge' || Number(summary.conflict_count || 0) === 0); 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 || ''; const endDate = periodEndInput?.value || ''; if (!templateId || !startDate || !endDate) { setInlineMessage(periodMessage, 'Sélectionnez une semaine type et renseignez les deux dates.', 'warning'); return; } if (endDate < startDate) { setInlineMessage(periodMessage, 'La date de fin doit être postérieure ou égale à la date de début.', 'warning'); return; } if (periodPreviewButton) periodPreviewButton.disabled = true; if (periodConfirmButton) periodConfirmButton.disabled = true; setInlineMessage(periodMessage, 'Analyse de la période et des vacances...', 'info'); try { const response = await fetch('api/preview_week_template_period.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, template_id: templateId, agent_id: Number(agentSelect.value), start_date: startDate, end_date: endDate, mode: periodMode(), period_filter: periodFilter(), }), }); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de prévisualiser l’application sur la période.')); lastPeriodPreview = data.preview; lastPeriodPreviewSignature = currentPeriodSignature(); renderPeriodPreview(lastPeriodPreview); setInlineMessage(periodMessage, 'Aperçu calculé. Vérifiez les dates avant de confirmer.', 'success'); } catch (error) { lastPeriodPreview = null; lastPeriodPreviewSignature = ''; if (periodPreviewPanel) periodPreviewPanel.hidden = true; setInlineMessage(periodMessage, error.message, 'error'); } finally { if (periodPreviewButton) periodPreviewButton.disabled = false; } } /** * 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'); if (periodConfirmButton) periodConfirmButton.disabled = true; return; } const templateId = Number(periodTemplateSelect?.value || 0); const template = templates.find((item) => Number(item.id) === templateId); const mode = periodMode(); const summary = lastPeriodPreview.summary || {}; if (mode === 'replace' && Number(summary.existing_entry_count_on_target_dates || 0) > 0) { const confirmed = window.confirm( `${Number(summary.existing_entry_count_on_target_dates)} créneau(x) existant(s) seront supprimé(s) sur les jours réellement concernés avant d’appliquer « ${template?.name || 'la semaine type'} ».\n\nLes dates exclues par le filtre de calendrier ne seront pas modifiées. Continuer ?` ); if (!confirmed) return; } if (periodConfirmButton) periodConfirmButton.disabled = true; if (periodPreviewButton) periodPreviewButton.disabled = true; setInlineMessage(periodMessage, 'Application de la semaine type sur la période...', 'info'); try { const response = await fetch('api/apply_week_template_period.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, template_id: templateId, agent_id: Number(agentSelect.value), start_date: periodStartInput.value, end_date: periodEndInput.value, mode, period_filter: periodFilter(), }), }); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible d’appliquer la semaine type sur la période.')); await PTA.modules.planning?.loadPlanning?.(); PTA.modules.pendingValidation?.refresh?.({ silent: true }); PTA.modules.coverage?.refresh?.({ silent: true }); showMessage(data.message, 'success'); setInlineMessage(periodMessage, data.message, 'success'); lastPeriodPreview = null; lastPeriodPreviewSignature = ''; setTimeout(() => closeModal(periodModal), 700); } catch (error) { setInlineMessage(periodMessage, error.message, 'error'); // Le planning peut avoir changé depuis l'aperçu : imposer une nouvelle prévisualisation. lastPeriodPreview = null; lastPeriodPreviewSignature = ''; if (periodConfirmButton) periodConfirmButton.disabled = true; } finally { if (periodPreviewButton) periodPreviewButton.disabled = false; } } /** * 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); if (!template) return; const confirmed = window.confirm(`Supprimer définitivement la semaine type « ${template.name} » ?`); if (!confirmed) return; if (deleteButton) deleteButton.disabled = true; try { const response = await fetch('api/delete_week_template.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, template_id: templateId, agent_id: Number(agentSelect.value), }), }); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de supprimer la semaine type.')); setInlineMessage(applyMessage, data.message, 'success'); await loadTemplates(); renderTemplateSelect(); } catch (error) { setInlineMessage(applyMessage, error.message, 'error'); renderSelectedTemplateDetails(); } } /** 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'; } function init() { saveButton?.addEventListener('click', openSaveModal); applyButton?.addEventListener('click', openApplyModal); periodButton?.addEventListener('click', openPeriodModal); saveForm?.addEventListener('submit', (event) => { event.preventDefault(); saveTemplate(false); }); applyForm?.addEventListener('submit', (event) => { event.preventDefault(); applyTemplate(); }); periodForm?.addEventListener('submit', (event) => { event.preventDefault(); applyTemplatePeriod(); }); periodPreviewButton?.addEventListener('click', previewPeriodApplication); document.querySelector('#close-save-week-template-modal')?.addEventListener('click', () => closeModal(saveModal)); document.querySelector('#cancel-save-week-template')?.addEventListener('click', () => closeModal(saveModal)); document.querySelector('#close-apply-week-template-modal')?.addEventListener('click', () => closeModal(applyModal)); document.querySelector('#cancel-apply-week-template')?.addEventListener('click', () => closeModal(applyModal)); document.querySelector('#close-apply-week-template-period-modal')?.addEventListener('click', () => closeModal(periodModal)); document.querySelector('#cancel-apply-week-template-period')?.addEventListener('click', () => closeModal(periodModal)); saveModal?.addEventListener('click', (event) => { if (event.target === saveModal) closeModal(saveModal); }); applyModal?.addEventListener('click', (event) => { if (event.target === applyModal) closeModal(applyModal); }); periodModal?.addEventListener('click', (event) => { if (event.target === periodModal) closeModal(periodModal); }); templateSelect?.addEventListener('change', renderSelectedTemplateDetails); applyForm?.querySelectorAll('input[name="week-template-mode"]').forEach((input) => { input.addEventListener('change', updateReplaceWarning); }); periodForm?.querySelectorAll('input[name="week-template-period-mode"]').forEach((input) => { input.addEventListener('change', invalidatePeriodPreview); }); periodForm?.querySelectorAll('input[name="week-template-period-filter"]').forEach((input) => { input.addEventListener('change', invalidatePeriodPreview); }); [periodTemplateSelect, periodStartInput, periodEndInput].forEach((input) => { input?.addEventListener('change', invalidatePeriodPreview); input?.addEventListener('input', invalidatePeriodPreview); }); deleteButton?.addEventListener('click', deleteSelectedTemplate); agentSelect?.addEventListener('change', refreshButtons); weekInput?.addEventListener('input', refreshButtons); weekInput?.addEventListener('change', refreshButtons); document.addEventListener('keydown', (event) => { if (event.key !== 'Escape') return; if (saveModal && !saveModal.hidden) closeModal(saveModal); if (applyModal && !applyModal.hidden) closeModal(applyModal); if (periodModal && !periodModal.hidden) closeModal(periodModal); }); updateReplaceWarning(); updatePeriodReplaceWarning(); refreshButtons(); } PTA.modules.weekTemplates = { init, refreshButtons, loadTemplates, // Ouverts aussi depuis l'éditeur de semaine type. openApplyModal, openPeriodModal, }; })();