(() => { '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 = ''; function contextReady() { return Boolean(agentSelect?.value && weekInput?.value); } function globalEntryCount() { return (state.entries?.length || 0) + (state.otherStructureEntries?.length || 0); } function refreshButtons() { const ready = contextReady(); if (saveButton) saveButton.disabled = !ready; if (applyButton) applyButton.disabled = !ready; if (periodButton) periodButton.disabled = !ready; } function setInlineMessage(element, text = '', type = 'info') { if (!element) return; element.className = text ? `message message-${type}` : 'message'; element.textContent = text; } 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'); } function agentLabel() { return agentSelect?.options[agentSelect.selectedIndex]?.textContent?.trim() || 'Agent sélectionné'; } 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; } 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); } 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; } } 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; } 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(); } 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' : ''}
`; } async function openApplyModal() { 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(); setInlineMessage(applyMessage); } catch (error) { setInlineMessage(applyMessage, error.message, 'error'); } } 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(); } } function periodMode() { return periodForm?.querySelector('input[name="week-template-period-mode"]:checked')?.value || 'merge'; } function periodFilter() { return periodForm?.querySelector('input[name="week-template-period-filter"]:checked')?.value || 'all'; } 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; } 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}`; } 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); } function invalidatePeriodPreview() { lastPeriodPreview = null; lastPeriodPreviewSignature = ''; if (periodConfirmButton) periodConfirmButton.disabled = true; if (periodPreviewPanel) { periodPreviewPanel.hidden = true; periodPreviewPanel.innerHTML = ''; } setInlineMessage(periodMessage); updatePeriodReplaceWarning(); } function currentPeriodSignature() { return JSON.stringify({ template_id: Number(periodTemplateSelect?.value || 0), start_date: periodStartInput?.value || '', end_date: periodEndInput?.value || '', mode: periodMode(), period_filter: periodFilter(), }); } function renderPeriodTemplateSelect() { if (!periodTemplateSelect) return; if (!templates.length) { periodTemplateSelect.innerHTML = ''; periodTemplateSelect.disabled = true; return; } periodTemplateSelect.disabled = false; periodTemplateSelect.innerHTML = '' + templates.map((template) => ( `` )).join(''); } 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); } } async function openPeriodModal() { 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(); setInlineMessage(periodMessage); } catch (error) { setInlineMessage(periodMessage, error.message, 'error'); } } function updatePeriodReplaceWarning() { if (periodReplaceWarning) periodReplaceWarning.hidden = periodMode() !== 'replace'; } 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; } 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; } } 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; } } 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(); } } 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, }; })();