(() => { 'use strict'; const PTA = window.PTA; if (!PTA) throw new Error('PTA core doit être chargé avant planning-editor.js'); const { structureSelect, agentSelect, weekInput, weeklyEntryBody, addButton, saveButton, validateButton, modifyButton, planningBoard, emptyState, weekLabel, workTotal, statusBadge, openAgentPdfButton, quotaPanel, quotaRate, quotaTarget, quotaPeriod, quotaUsed, quotaUsedDetail, quotaRemaining, } = PTA.elements; const permissions = window.PTA_CONFIG?.permissions || {}; const accessProfile = window.PTA_CONFIG?.access || {}; const canEditDraft = Boolean(permissions.can_edit_draft); const canValidate = Boolean(permissions.can_validate); const canEditCrossStructure = Boolean(permissions.can_edit_cross_structure); const state = PTA.state; const { escapeHtml, showMessage, clearMessage, readJsonResponse, apiErrorMessage, parseIsoWeek, timeToMinutes, formatDuration, motifInfo, } = PTA.utils; function isPlanningContextReady() { return Boolean(structureSelect?.value && agentSelect?.value && weekInput?.value); } function updateAgentPdfButton() { if (openAgentPdfButton) { openAgentPdfButton.disabled = !(agentSelect?.value && weekInput?.value); } } function openAgentPlanningPdf() { if (!agentSelect?.value || !weekInput?.value) { showMessage('Sélectionnez un agent et une semaine avant d’ouvrir le PDF.', 'warning'); return; } const params = new URLSearchParams({ agent_id: agentSelect.value, week: weekInput.value, weeks: '7', }); window.open(`api/agent_planning_pdf.php?${params.toString()}`, '_blank', 'noopener'); } function setStatus(status) { state.planningStatus = status; if (!statusBadge) return; statusBadge.className = 'status-badge'; if (status === 'VALIDE') { statusBadge.classList.add('status-valid'); statusBadge.textContent = 'Planning validé'; } else if (status === 'BROUILLON') { statusBadge.classList.add('status-draft'); statusBadge.textContent = 'Brouillon'; } else { statusBadge.classList.add('status-neutral'); statusBadge.textContent = 'Nouveau planning'; } const locked = status === 'VALIDE'; if (modifyButton) { modifyButton.hidden = !canEditDraft || !locked; modifyButton.disabled = !canEditDraft || !locked || !state.planningId; } if (saveButton) { saveButton.hidden = !canEditDraft || locked; saveButton.disabled = !canEditDraft || locked || !isPlanningContextReady(); } if (validateButton) { validateButton.hidden = !canValidate || locked; validateButton.disabled = !canValidate || locked || !isPlanningContextReady() || state.entries.length === 0; } if (addButton) { addButton.hidden = !canEditDraft; addButton.disabled = !canEditDraft || locked || !isPlanningContextReady() || state.weekDays.length === 0; } updateEntryControlsState(); updateAgentPdfButton(); } function motifOptionsHtml(selectedMotifId = null) { return (window.PTA_CONFIG?.motifs || []) .map((motif) => { const selected = Number(motif.id) === Number(selectedMotifId) ? ' selected' : ''; const quotaLabel = motif.quota ? ' · décompte quota' : ' · hors quota'; return ``; }) .join(''); } function timeRangeHtml(dayLabel, index = 0, selectedMotifId = null) { const suffix = `${dayLabel} plage ${index + 1}`; return `
`; } function renderWeeklyEntryRows() { if (!weeklyEntryBody) return; weeklyEntryBody.innerHTML = ''; if (!state.weekDays.length) { weeklyEntryBody.innerHTML = 'Sélectionnez d’abord un lieu d’affectation, un agent et une semaine.'; return; } state.weekDays.forEach((day) => { const row = document.createElement('tr'); row.className = 'weekly-entry-row'; row.dataset.date = day.date; row.innerHTML = ` ${escapeHtml(day.label)} ${escapeHtml(day.display)} ${day.type_affectation ? `${day.type_affectation === 'EXTRASCOLAIRE' ? 'Extrascolaire' : 'Périscolaire'}` : ''} ${day.vacances?.libelle ? `${escapeHtml(day.vacances.libelle)}` : ''}
${timeRangeHtml(day.label, 0)}
`; weeklyEntryBody.appendChild(row); }); updateEntryControlsState(); } function fillWeeklyEntryRowsFromEntries() { if (!state.weekDays.length || !weeklyEntryBody) return; renderWeeklyEntryRows(); state.weekDays.forEach((day) => { const row = weeklyEntryBody.querySelector(`.weekly-entry-row[data-date="${day.date}"]`); if (!row) return; const entries = state.entries .filter((entry) => entry.date === day.date) .sort((a, b) => a.start.localeCompare(b.start)); if (!entries.length) return; const rangesContainer = row.querySelector('.weekly-time-ranges'); if (!rangesContainer) return; rangesContainer.innerHTML = ''; entries.forEach((entry, index) => { rangesContainer.insertAdjacentHTML('beforeend', timeRangeHtml(day.label, index, entry.motif_id)); const range = rangesContainer.lastElementChild; range.querySelector('.weekly-range-start').value = entry.start; range.querySelector('.weekly-range-end').value = entry.end; }); }); updateEntryControlsState(); } function updateEntryControlsState() { if (!weeklyEntryBody) return; const locked = state.planningStatus === 'VALIDE'; const enabled = canEditDraft && !locked && isPlanningContextReady() && state.weekDays.length > 0; weeklyEntryBody.querySelectorAll('.weekly-time, .weekly-range-motif, .add-time-range, .remove-time-range').forEach((control) => { control.disabled = !enabled; }); } function populateDays() { state.weekDays = parseIsoWeek(weekInput?.value || ''); renderWeeklyEntryRows(); if (state.weekDays.length) { if (weekLabel) { weekLabel.textContent = `Du ${state.weekDays[0].display} au ${state.weekDays[state.weekDays.length - 1].display} · tous lieux d’affectation`; } return true; } if (weekLabel) weekLabel.textContent = 'Sélectionnez une semaine valide.'; return false; } function countedMinutes(entries = state.entries) { return entries.reduce((total, entry) => { const motif = motifInfo(entry.motif_id) ?? { quota: Boolean(entry.compte_dans_quota) }; if (!motif.quota) return total; return total + (timeToMinutes(entry.end) - timeToMinutes(entry.start)); }, 0); } function updateQuotaPanel() { if (!quotaPanel) return; if (!state.quota) { quotaPanel.hidden = true; return; } const currentMinutes = countedMinutes(); const baseAllocated = Number(state.quota.minutes_affectees) - Number(state.savedCurrentCountedMinutes || 0); const projectedAllocated = baseAllocated + currentMinutes; const projectedRemaining = Number(state.quota.quota_cible_minutes) - projectedAllocated; quotaPanel.hidden = false; if (quotaRate) quotaRate.textContent = `${Number(state.quota.quotite_travail).toLocaleString('fr-FR')} %`; if (quotaTarget) quotaTarget.textContent = formatDuration(Number(state.quota.quota_cible_minutes)); if (quotaPeriod) { quotaPeriod.textContent = state.quota.periode_libelle || 'Année civile'; } if (quotaUsed) quotaUsed.textContent = formatDuration(projectedAllocated); if (quotaUsedDetail) { quotaUsedDetail.textContent = `${formatDuration(Number(state.quota.minutes_valides))} validées · ${formatDuration(Math.max(0, projectedAllocated - Number(state.quota.minutes_valides)))} en brouillon`; } if (quotaRemaining) { quotaRemaining.textContent = formatDuration(projectedRemaining); quotaRemaining.classList.toggle('quota-negative', projectedRemaining < 0); } } let draggedEntry = null; let activeEntryAction = null; let suppressEntryClickUntil = 0; function sameEntry(a, b) { if (!a || !b) return false; if (a.id_creneau && b.id_creneau) return Number(a.id_creneau) === Number(b.id_creneau); return String(a.local_id || '') === String(b.local_id || ''); } function allGlobalEntries() { const currentStructureName = state.currentStructure?.nom || structureSelect?.options[structureSelect.selectedIndex]?.textContent?.trim() || 'Lieu d’affectation sélectionné'; return [ ...state.entries.map((entry) => ({ ...entry, is_external: false, structure_name: currentStructureName, structure_id: Number(structureSelect?.value || 0), })), ...state.otherStructureEntries.map((entry) => ({ ...entry, is_external: true })), ]; } function findEntryConflict(entry, targetDate, duplicate) { const startMinutes = timeToMinutes(entry.start); const endMinutes = timeToMinutes(entry.end); return allGlobalEntries().find((candidate) => { if (!duplicate && sameEntry(candidate, entry)) return false; return candidate.date === targetDate && startMinutes < timeToMinutes(candidate.end) && endMinutes > timeToMinutes(candidate.start); }) || null; } function entryActionSummary(entry) { const motif = motifInfo(entry.motif_id) ?? { label: entry.motif_label || 'Affectation' }; const location = entry.structure_name || state.currentStructure?.nom || structureSelect?.options[structureSelect.selectedIndex]?.textContent?.trim() || 'Lieu non précisé'; return `${motif.label} · ${entry.start}–${entry.end} · ${location}`; } function closeEntryActionModal() { const modal = document.querySelector('#planning-entry-action-modal'); const message = document.querySelector('#planning-entry-action-message'); if (modal) modal.hidden = true; if (message) { message.className = 'message'; message.textContent = ''; } activeEntryAction = null; if (!document.querySelector('.modal-backdrop:not([hidden])')) { document.body.classList.remove('modal-open'); } } function openEntryActionModal(entry) { const modal = document.querySelector('#planning-entry-action-modal'); const summary = document.querySelector('#planning-entry-action-summary'); const targetSelect = document.querySelector('#planning-entry-target-date'); const moveRadio = document.querySelector('input[name="planning-entry-action"][value="move"]'); if (!modal || !targetSelect) return; activeEntryAction = entry; if (summary) summary.textContent = entryActionSummary(entry); targetSelect.innerHTML = state.weekDays.map((day) => ( `` )).join(''); if (moveRadio) moveRadio.checked = true; const message = document.querySelector('#planning-entry-action-message'); if (message) { message.className = 'message'; message.textContent = ''; } modal.hidden = false; document.body.classList.add('modal-open'); targetSelect.focus(); } function applyLocalEntryDayAction(entry, targetDate, duplicate) { const conflict = findEntryConflict(entry, targetDate, duplicate); if (conflict) { const location = conflict.structure_name || 'un autre lieu'; showMessage(`Action impossible : un créneau ${conflict.start}–${conflict.end} existe déjà à ${location} ce jour-là.`, 'warning'); return false; } if (duplicate) { const clone = { ...entry, local_id: crypto.randomUUID(), id_creneau: null, date: targetDate, }; delete clone.is_external; delete clone.structure_name; delete clone.structure_id; delete clone.structure_type_affectation; delete clone.status; state.entries.push(clone); } else { const source = state.entries.find((candidate) => sameEntry(candidate, entry)); if (!source) { showMessage('Le créneau à déplacer n’est plus présent dans le formulaire. Rechargez le planning.', 'warning'); return false; } source.date = targetDate; } fillWeeklyEntryRowsFromEntries(); render(); setStatus(state.planningStatus ?? 'BROUILLON'); showMessage( duplicate ? 'Le créneau a été dupliqué dans le formulaire. Enregistrez le brouillon pour confirmer la duplication.' : 'Le créneau a été déplacé dans le formulaire. Enregistrez le brouillon pour confirmer le changement de jour.', 'success' ); return true; } async function persistEntryDayAction(entry, targetDate, duplicate) { if (!entry.id_creneau) { return applyLocalEntryDayAction(entry, targetDate, duplicate); } if (entry.is_external && isPlanningContextReady() && state.planningStatus !== 'VALIDE') { if (!syncEntriesFromWeeklyForm({ silent: true })) return false; if (state.entries.length > 0 || state.planningId) { const saved = await saveDraft({ silent: true }); if (!saved) return false; } } try { const response = await fetch('api/move_or_duplicate_entry.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, entry_id: Number(entry.id_creneau), target_date: targetDate, action: duplicate ? 'duplicate' : 'move', }), }); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de modifier ce créneau.')); await loadPlanning(); showMessage(data.message || (duplicate ? 'Créneau dupliqué.' : 'Créneau déplacé.'), 'success'); return true; } catch (error) { showMessage(error.message, 'error'); return false; } } async function applyEntryDayAction(entry, targetDate, duplicate = false) { if (!entry || !targetDate) return false; if (!duplicate && entry.date === targetDate) { showMessage('Le créneau se trouve déjà sur ce jour.', 'info'); return true; } const conflict = findEntryConflict(entry, targetDate, duplicate); if (conflict) { const location = conflict.structure_name || 'un autre lieu'; showMessage(`Action impossible : un créneau ${conflict.start}–${conflict.end} existe déjà à ${location} ce jour-là.`, 'warning'); return false; } const directServerAction = Boolean(entry.id_creneau) && (entry.is_external || state.planningStatus === 'VALIDE'); return directServerAction ? persistEntryDayAction(entry, targetDate, duplicate) : applyLocalEntryDayAction(entry, targetDate, duplicate); } async function submitEntryActionModal(event) { event.preventDefault(); if (!activeEntryAction) return; const targetDate = document.querySelector('#planning-entry-target-date')?.value || ''; const action = document.querySelector('input[name="planning-entry-action"]:checked')?.value || 'move'; const button = document.querySelector('#confirm-planning-entry-action'); const message = document.querySelector('#planning-entry-action-message'); if (button) button.disabled = true; try { const success = await applyEntryDayAction(activeEntryAction, targetDate, action === 'duplicate'); if (success) closeEntryActionModal(); else if (message) { message.className = 'message message-warning'; message.textContent = 'La modification n’a pas été appliquée. Vérifiez les messages affichés dans la page.'; } } finally { if (button) button.disabled = false; } } function clearDropTargets() { planningBoard?.querySelectorAll('.day-column').forEach((column) => { column.classList.remove('is-drop-target', 'is-copy-target'); }); } function makeEntryInteractive(item, entry) { if (!canEditDraft || (entry.is_external && !canEditCrossStructure)) return; item.classList.add('planning-entry-interactive'); item.draggable = true; item.tabIndex = 0; item.setAttribute('role', 'button'); item.setAttribute('aria-label', `${entryActionSummary(entry)}. Cliquez pour déplacer ou dupliquer ce créneau.`); item.title = 'Cliquez pour déplacer ou dupliquer. Glissez vers un autre jour ; Ctrl/⌘ + glisser pour dupliquer.'; item.addEventListener('dragstart', (event) => { draggedEntry = entry; item.classList.add('is-dragging'); event.dataTransfer.effectAllowed = 'copyMove'; event.dataTransfer.setData('text/plain', String(entry.id_creneau || entry.local_id || 'entry')); }); item.addEventListener('dragend', () => { item.classList.remove('is-dragging'); draggedEntry = null; suppressEntryClickUntil = Date.now() + 250; clearDropTargets(); }); item.addEventListener('click', (event) => { if (Date.now() < suppressEntryClickUntil || event.target.closest('button')) return; openEntryActionModal(entry); }); item.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openEntryActionModal(entry); } }); } function makeDayDropTarget(column, day) { column.dataset.date = day.date; if (!canEditDraft) return; column.addEventListener('dragover', (event) => { if (!draggedEntry) return; event.preventDefault(); const copy = event.ctrlKey || event.metaKey; event.dataTransfer.dropEffect = copy ? 'copy' : 'move'; clearDropTargets(); column.classList.add('is-drop-target'); if (copy) column.classList.add('is-copy-target'); }); column.addEventListener('dragleave', (event) => { if (!column.contains(event.relatedTarget)) { column.classList.remove('is-drop-target', 'is-copy-target'); } }); column.addEventListener('drop', async (event) => { if (!draggedEntry) return; event.preventDefault(); const entry = draggedEntry; const duplicate = event.ctrlKey || event.metaKey; clearDropTargets(); await applyEntryDayAction(entry, day.date, duplicate); }); } function render() { if (!planningBoard || !emptyState) return; planningBoard.innerHTML = ''; const visibleDates = new Set(state.weekDays.map((day) => day.date)); const currentStructureName = state.currentStructure?.nom || structureSelect?.options[structureSelect.selectedIndex]?.textContent?.trim() || 'Lieu d’affectation sélectionné'; const localEntries = state.entries.map((entry) => ({ ...entry, is_external: false, structure_name: currentStructureName, structure_id: Number(structureSelect?.value || 0), structure_type_affectation: state.currentStructure?.type_affectation || '', })); const externalEntries = state.otherStructureEntries.map((entry) => ({ ...entry, is_external: true, })); const globalEntries = [...localEntries, ...externalEntries] .filter((entry) => visibleDates.has(entry.date)); const hasEntries = globalEntries.length > 0; emptyState.hidden = hasEntries; planningBoard.hidden = !hasEntries; let totalCountedMinutes = 0; state.weekDays.forEach((day) => { const entries = globalEntries .filter((entry) => entry.date === day.date) .sort((a, b) => a.start.localeCompare(b.start)); const column = document.createElement('section'); column.className = 'day-column'; column.innerHTML = `
${day.label} ${day.display}
`; makeDayDropTarget(column, day); const entriesContainer = column.querySelector('.day-entries'); if (entries.length === 0) { entriesContainer.innerHTML = '

Aucune affectation

'; } else { entries.forEach((entry) => { const motif = motifInfo(entry.motif_id) ?? { label: entry.motif_label || 'Motif', quota: Boolean(entry.compte_dans_quota), }; const duration = timeToMinutes(entry.end) - timeToMinutes(entry.start); if (motif.quota) totalCountedMinutes += duration; const item = document.createElement('article'); item.className = `planning-entry motif-${String(entry.motif_code || motif.code || 'autre').toLowerCase()}${entry.is_external ? ' planning-entry-external' : ''}`; item.innerHTML = `
${escapeHtml(motif.label)} ${escapeHtml(entry.start)} – ${escapeHtml(entry.end)} 📍 ${escapeHtml(entry.structure_name || 'Lieu non précisé')}${entry.structure_type_affectation ? ` · ${escapeHtml(structureTypeLabel(entry.structure_type_affectation))}` : ''} ${formatDuration(duration)}${motif.quota ? ' · quota' : ''}${entry.is_external ? ` · ${escapeHtml(String(entry.status || '').toLowerCase())}` : ''}
${entry.is_external ? 'Autre lieu' : state.planningStatus === 'VALIDE' ? 'Validé' : canEditDraft ? '' : 'Lecture seule'} `; makeEntryInteractive(item, entry); const removeButton = item.querySelector('.remove-entry'); if (removeButton) { removeButton.addEventListener('click', (event) => { event.stopPropagation(); if (state.planningStatus === 'VALIDE') return; state.entries = state.entries.filter((candidate) => candidate.local_id !== entry.local_id); fillWeeklyEntryRowsFromEntries(); render(); setStatus(state.planningStatus); }); } entriesContainer.appendChild(item); }); } planningBoard.appendChild(column); }); if (workTotal) workTotal.textContent = formatDuration(totalCountedMinutes); if (validateButton) { validateButton.disabled = state.planningStatus === 'VALIDE' || !isPlanningContextReady() || state.entries.length === 0; } updateQuotaPanel(); PTA.modules.weekTemplates?.refreshButtons?.(); } function resetPlanningState() { state.entries = []; state.otherStructureEntries = []; state.currentStructure = null; state.planningId = null; state.quota = null; state.savedCurrentCountedMinutes = 0; state.weekDays = []; renderWeeklyEntryRows(); setStatus(null); render(); clearMessage(); } const structureTypeLabel = (type) => type === 'EXTRASCOLAIRE' ? 'Extrascolaire' : type === 'PERISCOLAIRE' ? 'Périscolaire' : ''; async function loadAgents() { resetPlanningState(); if (!agentSelect || !weekInput || !structureSelect) return; agentSelect.disabled = true; weekInput.disabled = true; agentSelect.innerHTML = ''; if (!structureSelect.value) { agentSelect.innerHTML = ''; return; } try { const response = await fetch(`api/agents.php?structure_id=${encodeURIComponent(structureSelect.value)}`); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Erreur de chargement des agents.')); agentSelect.innerHTML = ''; data.agents.forEach((agent) => { const option = document.createElement('option'); option.value = agent.id_agent; const defaultLabel = agent.structure_nom ? ` · principal : ${agent.structure_nom}` : ' · sans lieu principal'; const principalType = agent.structure_type_affectation === 'EXTRASCOLAIRE' ? 'Extrascolaire' : 'Périscolaire'; const typeSuffix = agent.structure_nom ? ` · ${principalType}` : ''; option.textContent = `${agent.prenom} ${agent.nom} (${agent.matricule})${defaultLabel}${typeSuffix}`; agentSelect.appendChild(option); }); agentSelect.disabled = false; if (data.agents.length === 0) { showMessage('Aucun agent actif n’est disponible.', 'warning'); } } catch (error) { showMessage(error.message, 'error'); agentSelect.innerHTML = ''; } } async function loadPlanning() { if (!isPlanningContextReady()) return; if (!populateDays()) { setStatus(null); return; } state.entries = []; state.otherStructureEntries = []; state.currentStructure = null; state.planningId = null; state.savedCurrentCountedMinutes = 0; setStatus(null); render(); clearMessage(); try { const params = new URLSearchParams({ structure_id: structureSelect.value, agent_id: agentSelect.value, week: weekInput.value, }); const response = await fetch(`api/planning.php?${params.toString()}`); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de charger le planning.')); state.quota = data.quota || null; state.currentStructure = data.current_structure || null; const calendarByDate = new Map((data.calendar_days || []).map((day) => [day.date, day])); state.weekDays = state.weekDays.map((day) => ({ ...day, ...(calendarByDate.get(day.date) || {}) })); state.otherStructureEntries = (data.other_entries || []).map((entry) => ({ local_id: `external-${entry.id_creneau}`, id_creneau: Number(entry.id_creneau), date: entry.date_jour, motif_id: Number(entry.id_motif), motif_code: entry.motif_code, motif_label: entry.motif_libelle, compte_dans_quota: Number(entry.compte_dans_quota) === 1, start: entry.heure_debut, end: entry.heure_fin, structure_id: Number(entry.id_structure), structure_name: entry.structure_nom, structure_type_affectation: entry.structure_type_affectation, status: entry.statut, })); if (data.planning) { state.planningId = Number(data.planning.id_planning); state.entries = data.entries.map((entry) => ({ local_id: crypto.randomUUID(), id_creneau: Number(entry.id_creneau), date: entry.date_jour, motif_id: Number(entry.id_motif), motif_code: entry.motif_code, motif_label: entry.motif_libelle, compte_dans_quota: Number(entry.compte_dans_quota) === 1, start: entry.heure_debut, end: entry.heure_fin, })); state.savedCurrentCountedMinutes = countedMinutes(state.entries); setStatus(data.planning.statut); } else { setStatus(null); } fillWeeklyEntryRowsFromEntries(); render(); if (state.otherStructureEntries.length > 0) { const count = state.otherStructureEntries.length; showMessage(`${count} affectation${count > 1 ? 's' : ''} déjà saisie${count > 1 ? 's' : ''} sur d’autres lieux ${count > 1 ? 'sont' : 'est'} affichée${count > 1 ? 's' : ''} en lecture seule.`, 'info'); } } catch (error) { showMessage(error.message, 'error'); } } function hasLocalOverlap(date, start, end, additionalEntries = [], includeCurrentEntries = true) { const startMinutes = timeToMinutes(start); const endMinutes = timeToMinutes(end); const candidates = [ ...(includeCurrentEntries ? state.entries : []), ...state.otherStructureEntries, ...additionalEntries, ]; return candidates.find((entry) => ( entry.date === date && startMinutes < timeToMinutes(entry.end) && endMinutes > timeToMinutes(entry.start) )) || null; } function syncEntriesFromWeeklyForm({ silent = false } = {}) { clearMessage(); const newEntries = []; const errors = []; weeklyEntryBody?.querySelectorAll('.weekly-entry-row').forEach((row) => { const day = state.weekDays.find((item) => item.date === row.dataset.date); const ranges = Array.from(row.querySelectorAll('.weekly-time-range')); const filledRanges = ranges.map((range, index) => ({ index, motifId: Number(range.querySelector('.weekly-range-motif')?.value || 0), start: range.querySelector('.weekly-range-start')?.value || '', end: range.querySelector('.weekly-range-end')?.value || '', })).filter((range) => range.start || range.end); filledRanges.forEach((range) => { const label = `plage ${range.index + 1}`; const { motifId, start, end } = range; const motif = motifInfo(motifId); if (!motifId || !motif) { errors.push(`${day?.label || row.dataset.date} ${label} : sélectionnez un motif.`); return; } if (!start || !end) { errors.push(`${day?.label || row.dataset.date} ${label} : renseignez l’heure de début et l’heure de fin.`); return; } if (timeToMinutes(end) <= timeToMinutes(start)) { errors.push(`${day?.label || row.dataset.date} ${label} : l’heure de fin doit être postérieure à l’heure de début.`); return; } const conflict = hasLocalOverlap(row.dataset.date, start, end, newEntries, false); if (conflict) { const location = conflict.structure_name || state.currentStructure?.nom || structureSelect?.options[structureSelect.selectedIndex]?.textContent?.trim() || 'le lieu sélectionné'; errors.push(`${day?.label || row.dataset.date} ${label} : chevauchement avec ${conflict.start}-${conflict.end} à ${location}.`); return; } newEntries.push({ local_id: crypto.randomUUID(), date: row.dataset.date, motif_id: motifId, motif_code: motif.code, motif_label: motif.label, compte_dans_quota: motif.quota, start, end, }); }); }); if (errors.length) { showMessage(errors.join('\n'), 'warning'); return false; } state.entries = newEntries; render(); setStatus(state.planningStatus ?? 'BROUILLON'); if (!silent) { if (newEntries.length === 0) { showMessage('Toutes les plages ont été supprimées du formulaire. Enregistrez le brouillon pour confirmer la suppression.', 'info'); } else { showMessage(`${newEntries.length} affectation${newEntries.length > 1 ? 's' : ''} mise${newEntries.length > 1 ? 's' : ''} à jour dans le planning. Pensez à enregistrer le brouillon.`, 'success'); } } return true; } function addEntry() { syncEntriesFromWeeklyForm(); } async function saveDraft({ silent = false } = {}) { if (!canEditDraft) { showMessage('Votre rôle ne permet pas de modifier ce planning.', 'warning'); return null; } if (!isPlanningContextReady()) { showMessage('Sélectionnez un lieu d’affectation, un agent et une semaine.', 'warning'); return null; } if (!syncEntriesFromWeeklyForm({ silent: true })) { return null; } if (saveButton) saveButton.disabled = true; if (validateButton) validateButton.disabled = true; try { const response = await fetch('save_planning.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, structure_id: Number(structureSelect.value), agent_id: Number(agentSelect.value), week: weekInput.value, entries: state.entries.map((entry) => ({ date: entry.date, motif_id: entry.motif_id, start: entry.start, end: entry.end, })), }), }); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Enregistrement impossible.')); state.planningId = Number(data.planning_id); if (data.quota) state.quota = data.quota; state.savedCurrentCountedMinutes = countedMinutes(); setStatus('BROUILLON'); render(); if (!silent) showMessage(data.message, 'success'); PTA.modules.pendingValidation?.refresh?.({ silent: true }); PTA.modules.coverage?.refresh?.({ silent: true }); return state.planningId; } catch (error) { showMessage(error.message, 'error'); setStatus(state.planningStatus); return null; } } function controlsToText(controls) { return controls .filter((control) => control.niveau !== 'INFO') .map((control) => `• ${control.message}`) .join('\n'); } async function validatePlanning(forceQuota = false) { if (!canValidate) { showMessage('Seul le Service Enfance peut valider un planning.', 'warning'); return; } clearMessage(); const planningId = await saveDraft({ silent: true }); if (!planningId) return; try { const response = await fetch('validate_planning.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, planning_id: planningId, force_quota: forceQuota, }), }); const data = await readJsonResponse(response); if (response.status === 409 && data.status === 'warning') { const detail = controlsToText(data.controls || []); const confirmed = window.confirm(`${data.message}\n\n${detail}\n\nVoulez-vous valider malgré cette alerte ?`); if (confirmed) await validatePlanning(true); return; } if (!response.ok) { const detail = controlsToText(data.controls || []); throw new Error(detail || apiErrorMessage(data, 'Validation impossible.')); } if (data.quota) state.quota = data.quota; state.savedCurrentCountedMinutes = countedMinutes(); setStatus('VALIDE'); showMessage(data.message, 'success'); render(); PTA.modules.pendingValidation?.refresh?.({ silent: true }); PTA.modules.coverage?.refresh?.({ silent: true }); } catch (error) { showMessage(error.message, 'error'); setStatus('BROUILLON'); } } async function reopenPlanningForEdit() { if (!canEditDraft) return; clearMessage(); if (!state.planningId || state.planningStatus !== 'VALIDE') { showMessage('Aucun planning validé à modifier.', 'warning'); return; } const confirmed = window.confirm( 'Le planning va repasser en brouillon afin de pouvoir être corrigé.\n\n' + 'Il devra être validé à nouveau après les modifications. Continuer ?' ); if (!confirmed) return; if (modifyButton) modifyButton.disabled = true; try { const response = await fetch('reopen_planning.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, planning_id: state.planningId, }), }); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de modifier ce planning.')); if (data.quota) state.quota = data.quota; setStatus('BROUILLON'); render(); showMessage(data.message || 'Le planning est à nouveau modifiable.', 'success'); PTA.modules.pendingValidation?.refresh?.({ silent: true }); PTA.modules.coverage?.refresh?.({ silent: true }); } catch (error) { showMessage(error.message, 'error'); setStatus('VALIDE'); } } async function handleWeekSelection() { clearMessage(); updateAgentPdfButton(); const hasValidWeek = populateDays(); setStatus(state.planningStatus); render(); if (!hasValidWeek || !isPlanningContextReady()) return; await loadPlanning(); } function handleAgentChange() { if (!weekInput) return; weekInput.disabled = !agentSelect?.value; state.entries = []; state.otherStructureEntries = []; state.currentStructure = null; state.planningId = null; state.weekDays = []; state.quota = null; state.savedCurrentCountedMinutes = 0; renderWeeklyEntryRows(); setStatus(null); render(); clearMessage(); updateAgentPdfButton(); if (agentSelect?.value && weekInput.value) { handleWeekSelection(); } } function handleWeeklyEntryClick(event) { const addRangeButton = event.target.closest('.add-time-range'); if (addRangeButton) { const row = addRangeButton.closest('.weekly-entry-row'); const container = row?.querySelector('.weekly-time-ranges'); const day = state.weekDays.find((item) => item.date === row?.dataset.date); if (!row || !container || !day || state.planningStatus === 'VALIDE') return; const index = container.querySelectorAll('.weekly-time-range').length; container.insertAdjacentHTML('beforeend', timeRangeHtml(day.label, index)); updateEntryControlsState(); return; } const removeRangeButton = event.target.closest('.remove-time-range'); if (removeRangeButton) { const row = removeRangeButton.closest('.weekly-entry-row'); const ranges = row?.querySelectorAll('.weekly-time-range'); if (!row || !ranges || state.planningStatus === 'VALIDE') return; if (ranges.length === 1) { ranges[0].querySelectorAll('input, select').forEach((control) => { control.value = ''; }); } else { removeRangeButton.closest('.weekly-time-range')?.remove(); } } } function init() { structureSelect?.addEventListener('change', loadAgents); agentSelect?.addEventListener('change', handleAgentChange); weekInput?.addEventListener('input', handleWeekSelection); weekInput?.addEventListener('change', handleWeekSelection); if (canEditDraft) { weeklyEntryBody?.addEventListener('click', handleWeeklyEntryClick); addButton?.addEventListener('click', addEntry); saveButton?.addEventListener('click', () => saveDraft()); modifyButton?.addEventListener('click', reopenPlanningForEdit); } if (canValidate) validateButton?.addEventListener('click', () => validatePlanning(false)); openAgentPdfButton?.addEventListener('click', openAgentPlanningPdf); document.querySelector('#planning-entry-action-form')?.addEventListener('submit', submitEntryActionModal); document.querySelector('#close-planning-entry-action-modal')?.addEventListener('click', closeEntryActionModal); document.querySelector('#cancel-planning-entry-action')?.addEventListener('click', closeEntryActionModal); document.querySelector('#planning-entry-action-modal')?.addEventListener('click', (event) => { if (event.target.id === 'planning-entry-action-modal') closeEntryActionModal(); }); document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && !document.querySelector('#planning-entry-action-modal')?.hidden) { closeEntryActionModal(); } }); updateAgentPdfButton(); renderWeeklyEntryRows(); setStatus(null); render(); } PTA.modules.planning = { init, loadAgents, loadPlanning, render, setStatus, renderWeeklyEntryRows, handleWeekSelection, updateAgentPdfButton, applyEntryDayAction, openEntryActionModal, saveDraft, syncEntriesFromWeeklyForm, isPlanningContextReady, countedMinutes, }; })();