(() => { 'use strict'; const PTA = window.PTA; if (!PTA) throw new Error('PTA core doit être chargé avant coverage.js'); const { elements, utils } = PTA; const { readJsonResponse, apiErrorMessage, escapeHtml, formatDuration, formatDate } = utils; const badge = document.querySelector('#coverage-tab-badge'); const weekInput = document.querySelector('#coverage-week'); const structureFilter = document.querySelector('#coverage-structure-filter'); const refreshButton = document.querySelector('#refresh-coverage-alerts'); const loadingElement = document.querySelector('#coverage-loading'); const emptyElement = document.querySelector('#coverage-empty'); const contentElement = document.querySelector('#coverage-content'); const bodyElement = document.querySelector('#coverage-alerts-body'); const messageElement = document.querySelector('#coverage-message'); const unconfiguredElement = document.querySelector('#coverage-unconfigured'); const ruleStructureSelect = document.querySelector('#coverage-rules-structure'); const ruleEditor = document.querySelector('#coverage-rules-editor'); const ruleMessage = document.querySelector('#coverage-rules-message'); const loadRulesButton = document.querySelector('#load-coverage-rules'); const saveRulesButton = document.querySelector('#save-coverage-rules'); const dayNames = { 1: 'Lundi', 2: 'Mardi', 3: 'Mercredi', 4: 'Jeudi', 5: 'Vendredi', }; let alerts = []; let loading = false; function setBadge(count) { if (!badge) return; badge.textContent = String(count); badge.hidden = count === 0; badge.setAttribute('aria-label', `${count} plage${count > 1 ? 's' : ''} avec couverture insuffisante`); } function setMessage(text = '', type = 'info') { if (!messageElement) return; messageElement.className = text ? `message message-${type}` : 'message'; messageElement.textContent = text; } function renderAlerts(data) { alerts = Array.isArray(data.alerts) ? data.alerts : []; const summary = data.summary || {}; const unconfigured = Array.isArray(data.unconfigured_structures) ? data.unconfigured_structures : []; setBadge(alerts.length); document.querySelector('#coverage-gap-count').textContent = String(Number(summary.gap_count || 0)); document.querySelector('#coverage-structure-count').textContent = String(Number(summary.affected_structure_count || 0)); document.querySelector('#coverage-gap-hours').textContent = formatDuration(Number(summary.gap_minutes || 0)); document.querySelector('#coverage-unconfigured-count').textContent = String(Number(summary.unconfigured_structure_count || 0)); if (loadingElement) loadingElement.hidden = true; if (emptyElement) emptyElement.hidden = alerts.length !== 0; if (contentElement) contentElement.hidden = alerts.length === 0; if (bodyElement) { bodyElement.innerHTML = alerts.map((alert, index) => { const missing = Number(alert.agents_manquants || 0); return ` ${escapeHtml(alert.structure_nom)} ${escapeHtml(alert.structure_code || '')} ${escapeHtml(alert.jour_libelle)} ${formatDate(alert.date)} ${escapeHtml(alert.heure_debut)}–${escapeHtml(alert.heure_fin)} ${Number(alert.minimum_agents)} ${Number(alert.agents_planifies)} ${missing} agent${missing > 1 ? 's' : ''} `; }).join(''); } if (unconfiguredElement) { unconfiguredElement.hidden = unconfigured.length === 0; unconfiguredElement.innerHTML = unconfigured.length ? ` Couverture non configurée pour ${unconfigured.length} lieu${unconfigured.length > 1 ? 'x' : ''}

${unconfigured.map((item) => escapeHtml(item.nom)).join(', ')}. Configurez les plages attendues dans la page Administration pour pouvoir détecter les créneaux vides.

` : ''; } } async function refresh(options = {}) { if (loading) return; const week = weekInput?.value; if (!week) return; loading = true; if (!options.silent) setMessage(''); if (loadingElement) loadingElement.hidden = false; if (refreshButton) refreshButton.disabled = true; try { const params = new URLSearchParams({ week }); if (structureFilter?.value) params.set('structure_id', structureFilter.value); const response = await fetch(`api/coverage_alerts.php?${params.toString()}`, { headers: { Accept: 'application/json' } }); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible d’analyser la couverture.')); renderAlerts(data); } catch (error) { if (loadingElement) loadingElement.hidden = true; setMessage(error.message, 'error'); } finally { loading = false; if (refreshButton) refreshButton.disabled = false; } } function openGap(alert) { if (!alert) return; const params = new URLSearchParams({ structure_id: String(alert.id_structure), week: weekInput?.value || '', notice: `Couverture insuffisante le ${alert.jour_libelle.toLowerCase()} ${formatDate(alert.date)} de ${alert.heure_debut} à ${alert.heure_fin} sur ${alert.structure_nom}. Choisissez un agent à affecter.`, }); window.location.href = `planning.php?${params.toString()}`; } function coverageRuleRow(rule = {}) { return `
`; } function renderRuleEditor(rules = []) { if (!ruleEditor) return; const grouped = new Map(); for (let day = 1; day <= 5; day += 1) grouped.set(day, []); rules.forEach((rule) => grouped.get(Number(rule.jour_semaine))?.push(rule)); ruleEditor.innerHTML = Array.from(grouped.entries()).map(([day, dayRules]) => `
${dayNames[day]}
${dayRules.length ? dayRules.map(coverageRuleRow).join('') : '

Aucune couverture obligatoire configurée.

'}
`).join(''); if (saveRulesButton) saveRulesButton.disabled = false; } async function loadRules() { const structureId = ruleStructureSelect?.value; if (!structureId || !ruleEditor) { if (ruleEditor) ruleEditor.innerHTML = '
Choisissez un lieu d’affectation.
'; if (saveRulesButton) saveRulesButton.disabled = true; return; } if (ruleMessage) { ruleMessage.className = 'message'; ruleMessage.textContent = ''; } ruleEditor.innerHTML = '
Chargement des règles de couverture…
'; try { const response = await fetch(`api/structure_coverage_rules.php?structure_id=${encodeURIComponent(structureId)}`); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de charger les règles de couverture.')); renderRuleEditor(data.rules || []); } catch (error) { ruleEditor.innerHTML = ''; if (ruleMessage) { ruleMessage.className = 'message message-error'; ruleMessage.textContent = error.message; } } } function collectRules() { const rules = []; ruleEditor?.querySelectorAll('.coverage-rule-day').forEach((daySection) => { const day = Number(daySection.dataset.day); daySection.querySelectorAll('.coverage-rule-row').forEach((row) => { const start = row.querySelector('.coverage-rule-start')?.value || ''; const end = row.querySelector('.coverage-rule-end')?.value || ''; const minimum = Number(row.querySelector('.coverage-rule-minimum')?.value || 1); if (!start && !end) return; rules.push({ jour_semaine: day, heure_debut: start, heure_fin: end, minimum_agents: minimum }); }); }); return rules; } async function saveRules() { const structureId = ruleStructureSelect?.value; if (!structureId || !saveRulesButton || !ruleMessage) return; saveRulesButton.disabled = true; ruleMessage.className = 'message'; ruleMessage.textContent = ''; try { const response = await fetch('api/save_structure_coverage_rules.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, structure_id: Number(structureId), rules: collectRules(), }), }); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible d’enregistrer les règles de couverture.')); renderRuleEditor(data.rules || []); ruleMessage.className = 'message message-success'; ruleMessage.textContent = data.message; await refresh({ silent: true }); if (document.querySelector('#overview-structure')?.value === String(structureId)) { await PTA.modules.structureOverview?.load?.(); } } catch (error) { ruleMessage.className = 'message message-error'; ruleMessage.textContent = error.message; } finally { saveRulesButton.disabled = false; } } function init() { refreshButton?.addEventListener('click', () => refresh()); weekInput?.addEventListener('change', () => refresh()); structureFilter?.addEventListener('change', () => refresh()); bodyElement?.addEventListener('click', (event) => { const button = event.target.closest('.open-coverage-gap'); if (!button) return; openGap(alerts[Number(button.dataset.alertIndex)]); }); ruleStructureSelect?.addEventListener('change', loadRules); loadRulesButton?.addEventListener('click', loadRules); saveRulesButton?.addEventListener('click', saveRules); ruleEditor?.addEventListener('click', (event) => { const addButton = event.target.closest('.add-coverage-rule'); if (addButton) { const day = addButton.closest('.coverage-rule-day'); const ranges = day?.querySelector('.coverage-rule-day-ranges'); if (!ranges) return; ranges.querySelector('.coverage-rule-empty')?.remove(); ranges.insertAdjacentHTML('beforeend', coverageRuleRow()); return; } const removeButton = event.target.closest('.remove-coverage-rule'); if (removeButton) { const ranges = removeButton.closest('.coverage-rule-day-ranges'); removeButton.closest('.coverage-rule-row')?.remove(); if (ranges && !ranges.querySelector('.coverage-rule-row')) { ranges.innerHTML = '

Aucune couverture obligatoire configurée.

'; } } }); refresh({ silent: true }); } PTA.modules.coverage = { init, refresh, loadRules }; })();