/** * Contrôle de couverture (`PTA.modules.coverage`). * * Chargé par DEUX écrans, avec des rôles différents : * - coverage.php : consultation des alertes de la semaine ; * - administration.php : édition des règles de couverture des lieux. * Les sélecteurs des deux moitiés sont donc absents l'un chez l'autre, d'où les * `if (!element) return;` systématiques. * * Une « alerte » est une plage où le nombre d'agents planifiés passe sous le * minimum attendu du lieu. Le serveur découpe au quart d'heure puis refusionne * les créneaux consécutifs identiques ; ce module se contente d'afficher les * intervalles reçus. * * L'ÉDITEUR DE RÈGLES fonctionne en REMPLACEMENT INTÉGRAL : `collectRules()` lit * la totalité de l'écran et l'envoie d'un bloc. Un jour vidé de ses plages * supprime les règles correspondantes — c'est le comportement voulu. */ (() => { '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; /** Pastille d'onglet « Couverture », masquée quand il n'y a rien à signaler. */ 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; } /** * Rend le tableau des alertes. * Les lieux NON CONFIGURÉS sont affichés séparément : sans règle, ils ne * peuvent produire aucune alerte et passeraient à tort pour conformes. */ 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 `
${unconfigured.map((item) => escapeHtml(item.nom)).join(', ')}. Configurez les plages attendues dans la page Administration pour pouvoir détecter les créneaux vides.
` : ''; } } /** * Recharge les alertes de la semaine et du lieu sélectionnés. * Appelée aussi par l'éditeur de planning après un enregistrement, en mode * silencieux : la couverture dépend de ce qui vient d'être saisi. * * @param {{silent?: boolean}} options */ 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; } } /** * Ouvre l'écran de saisie sur le lieu et la semaine du manque, avec un * `notice` qui rappelle la plage à couvrir : sans lui, l'utilisateur * arriverait sur une semaine complète sans savoir où agir. */ 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()}`; } /** * Balisage d'une plage de couverture : début, fin, effectif minimal. * `step="900"` contraint la saisie au quart d'heure, comme partout ailleurs. */ function coverageRuleRow(rule = {}) { return `Aucune couverture obligatoire configurée.
'}Aucune couverture obligatoire configurée.
'; } } }); refresh({ silent: true }); } PTA.modules.coverage = { init, refresh, loadRules }; })();