/** * Périodes de vacances scolaires (`PTA.modules.vacationCalendar`), écran * vacations.php. * * Ces périodes déterminent le contexte périscolaire ou extrascolaire de chaque * journée : c'est un référentiel structurant, pas un simple calendrier * d'affichage. * * L'IMPORT SE FAIT EN DEUX TEMPS, volontairement séparés : * 1. APERÇU — `preview()` interroge l'API publique data.education.gouv.fr via * le serveur, SANS RIEN ÉCRIRE. Les périodes proposées sont affichées dans * un tableau où chaque ligne est éditable et cochable ; * 2. ENREGISTREMENT — seules les lignes cochées sont envoyées, avec les * libellés et dates éventuellement corrigés à l'écran. * Aucune donnée externe n'entre en base sans relecture humaine : les libellés et * découpages publiés varient d'une académie et d'une année à l'autre. * * Une saisie entièrement manuelle reste possible, pour les cas que l'API ne * couvre pas. */ (() => { 'use strict'; const PTA = window.PTA; if (!PTA) throw new Error('PTA core doit être chargé avant vacation-calendar.js'); const { escapeHtml, readJsonResponse, apiErrorMessage, formatDate } = PTA.utils; let previewPeriods = []; /** * Sélecteurs résolus À CHAQUE APPEL, et non mis en cache : les tableaux de * cette page sont reconstruits, et des références figées pointeraient vers * des nœuds détachés du document. */ const elements = () => ({ academy: document.querySelector('#vacation-academy'), calendarYear: document.querySelector('#vacation-calendar-year'), previewButton: document.querySelector('#preview-vacations'), previewMessage: document.querySelector('#vacation-preview-message'), previewPanel: document.querySelector('#vacation-preview-panel'), previewBody: document.querySelector('#vacation-preview-body'), selectAll: document.querySelector('#vacation-select-all'), saveSelected: document.querySelector('#save-selected-vacations'), savedBody: document.querySelector('#vacation-saved-body'), savedMessage: document.querySelector('#vacation-saved-message'), refresh: document.querySelector('#refresh-vacations'), manualForm: document.querySelector('#manual-vacation-form'), manualMessage: document.querySelector('#manual-vacation-message'), }); function setMessage(element, text = '', type = 'info') { if (!element) return; element.className = text ? `message message-${type}` : 'message'; element.textContent = text; } /** Origine d'une période : import officiel ou saisie manuelle. */ function sourceLabel(source) { return source === 'DATA_GOUV' ? 'API officielle' : 'Saisie manuelle'; } /** * Rend le tableau d'aperçu : une ligne par période proposée, avec une case à * cocher et des champs éditables. L'utilisateur peut corriger un libellé ou * une date avant d'enregistrer. */ function renderPreview() { const el = elements(); if (!el.previewBody || !el.previewPanel) return; if (!previewPeriods.length) { el.previewBody.innerHTML = '
Aucune période proposée.
'; el.previewPanel.hidden = false; return; } el.previewBody.innerHTML = previewPeriods.map((period, index) => ` ${escapeHtml(period.annee_scolaire || '')} ${period.date_reprise_api ? `Reprise API : ${escapeHtml(formatDate(period.date_reprise_api))}` : ''} ${escapeHtml([period.zone, period.academie].filter(Boolean).join(' · ') || 'Non précisé')} `).join(''); el.previewPanel.hidden = false; if (el.selectAll) el.selectAll.checked = false; } /** * Interroge l'API officielle par l'intermédiaire du serveur. AUCUNE * écriture. Un échec ressort en 502 : c'est le service distant qui n'a pas * répondu, pas PTA qui a échoué. Les `warnings` signalent les périodes que * le serveur n'a pas su interpréter. */ async function preview() { const el = elements(); const academy = el.academy?.value.trim() || ''; const calendarYear = String(el.calendarYear?.value || '').trim(); if (!academy || !/^\d{4}$/.test(calendarYear)) { setMessage(el.previewMessage, 'Renseignez une académie et une année civile au format 2026.', 'warning'); return; } el.previewButton.disabled = true; setMessage(el.previewMessage, 'Consultation de l’API officielle en cours…', 'info'); if (el.previewPanel) el.previewPanel.hidden = true; try { const params = new URLSearchParams({ academy, year: calendarYear }); const response = await fetch(`api/preview_school_holidays.php?${params.toString()}`); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de consulter le calendrier scolaire.')); previewPeriods = Array.isArray(data.periods) ? data.periods : []; renderPreview(); const warnings = Array.isArray(data.warnings) ? data.warnings.filter(Boolean) : []; const baseMessage = data.message || `${previewPeriods.length} période(s) proposée(s). Aucune donnée n’a encore été enregistrée.`; const fullMessage = warnings.length ? `${baseMessage} Attention : ${warnings.join(' ')}` : baseMessage; setMessage(el.previewMessage, fullMessage, warnings.length ? 'warning' : (previewPeriods.length ? 'success' : 'warning')); } catch (error) { previewPeriods = []; setMessage(el.previewMessage, error.message, 'error'); } finally { el.previewButton.disabled = false; } } /** * Construit la charge utile à partir des seules lignes cochées. * * Les valeurs affichées (libellé, dates) priment, puisqu'elles ont pu être * corrigées ; les métadonnées non éditables (année scolaire, académie, zone) * sont reprises de la proposition d'origine, retrouvée par l'index de ligne. */ function selectedPreviewPeriods() { const el = elements(); return Array.from(el.previewBody?.querySelectorAll('.vacation-preview-row') || []) .filter((row) => row.querySelector('.vacation-import-check')?.checked) .map((row) => { const source = previewPeriods[Number(row.dataset.index)] || {}; return { libelle: row.querySelector('.vacation-preview-label')?.value.trim() || '', date_debut: row.querySelector('.vacation-preview-start')?.value || '', date_fin: row.querySelector('.vacation-preview-end')?.value || '', annee_scolaire: source.annee_scolaire || '', academie: source.academie || elements().academy?.value.trim() || '', zone: source.zone || '', source: 'DATA_GOUV', }; }); } /** * Enregistre un lot de périodes puis recharge la liste enregistrée. * Mutualisé entre l'import et la saisie manuelle. */ async function savePeriods(periods, messageElement) { const response = await fetch('api/save_vacation_periods.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, periods, }), }); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible d’enregistrer les périodes.')); setMessage(messageElement, data.message, 'success'); await loadSaved(); return data; } /** Enregistre les périodes cochées dans l'aperçu. */ async function saveSelected() { const el = elements(); const periods = selectedPreviewPeriods(); if (!periods.length) { setMessage(el.previewMessage, 'Cochez au moins une période avant de l’enregistrer.', 'warning'); return; } el.saveSelected.disabled = true; try { await savePeriods(periods, el.previewMessage); el.previewBody?.querySelectorAll('.vacation-import-check').forEach((checkbox) => { checkbox.checked = false; }); if (el.selectAll) el.selectAll.checked = false; } catch (error) { setMessage(el.previewMessage, error.message, 'error'); } finally { el.saveSelected.disabled = false; } } /** Enregistre une période saisie à la main, pour les cas hors API. */ async function saveManual(event) { event.preventDefault(); const el = elements(); const submit = el.manualForm?.querySelector('button[type="submit"]'); const period = { libelle: document.querySelector('#manual-vacation-label')?.value.trim() || '', date_debut: document.querySelector('#manual-vacation-start')?.value || '', date_fin: document.querySelector('#manual-vacation-end')?.value || '', annee_scolaire: document.querySelector('#manual-vacation-school-year')?.value.trim() || '', academie: document.querySelector('#manual-vacation-academy')?.value.trim() || '', zone: document.querySelector('#manual-vacation-zone')?.value.trim() || '', source: 'MANUEL', }; if (period.date_debut && period.date_fin && period.date_fin < period.date_debut) { setMessage(el.manualMessage, 'La date de fin doit être postérieure ou égale à la date de début.', 'warning'); return; } if (submit) submit.disabled = true; try { await savePeriods([period], el.manualMessage); document.querySelector('#manual-vacation-label').value = ''; document.querySelector('#manual-vacation-start').value = ''; document.querySelector('#manual-vacation-end').value = ''; document.querySelector('#manual-vacation-zone').value = ''; } catch (error) { setMessage(el.manualMessage, error.message, 'error'); } finally { if (submit) submit.disabled = false; } } /** Rend le tableau des périodes déjà enregistrées, avec leur origine. */ function renderSaved(periods) { const el = elements(); if (!el.savedBody) return; if (!periods.length) { el.savedBody.innerHTML = '
Aucune période de vacances n’est encore enregistrée.
'; return; } el.savedBody.innerHTML = periods.map((period) => ` ${escapeHtml(period.libelle)} ${escapeHtml(formatDate(period.date_debut))} → ${escapeHtml(formatDate(period.date_fin))} ${escapeHtml(period.annee_scolaire || '')} ${escapeHtml([period.academie, period.zone].filter(Boolean).join(' · ') || 'Non précisé')} ${escapeHtml(sourceLabel(period.source))} `).join(''); } /** Recharge les périodes enregistrées en base. */ async function loadSaved() { const el = elements(); try { const response = await fetch('api/vacation_periods.php'); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de charger les périodes enregistrées.')); renderSaved(Array.isArray(data.periods) ? data.periods : []); setMessage(el.savedMessage, '', 'info'); } catch (error) { setMessage(el.savedMessage, error.message, 'error'); } } /** * Supprime une période après confirmation. * Les créneaux déjà planifiés ne sont pas touchés, mais le contexte * scolaire/extrascolaire des journées concernées change. */ async function deletePeriod(button) { const row = button.closest('tr[data-period-id]'); const id = Number(row?.dataset.periodId || 0); if (!id || !confirm('Supprimer cette période de vacances ? Les journées concernées seront à nouveau considérées comme périscolaires.')) return; button.disabled = true; try { const response = await fetch('api/delete_vacation_period.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, period_id: id, }), }); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Suppression impossible.')); setMessage(elements().savedMessage, data.message, 'success'); await loadSaved(); } catch (error) { setMessage(elements().savedMessage, error.message, 'error'); button.disabled = false; } } function init() { const el = elements(); el.previewButton?.addEventListener('click', preview); el.saveSelected?.addEventListener('click', saveSelected); el.refresh?.addEventListener('click', loadSaved); el.manualForm?.addEventListener('submit', saveManual); el.selectAll?.addEventListener('change', () => { el.previewBody?.querySelectorAll('.vacation-import-check').forEach((checkbox) => { checkbox.checked = el.selectAll.checked; }); }); el.savedBody?.addEventListener('click', (event) => { const button = event.target.closest('.delete-vacation-period'); if (button) deletePeriod(button); }); loadSaved(); } PTA.modules.vacationCalendar = { init, loadSaved, preview }; })();