(() => { 'use strict'; const PTA = window.PTA; if (!PTA) throw new Error('PTA core doit être chargé avant dashboard.js'); const { utils } = PTA; const { readJsonResponse, apiErrorMessage, escapeHtml, formatDuration, formatDate } = utils; const elements = { week: document.querySelector('#dashboard-week'), weekLabel: document.querySelector('#dashboard-week-label'), weekContext: document.querySelector('#dashboard-week-context'), refresh: document.querySelector('#dashboard-refresh'), message: document.querySelector('#dashboard-message'), loading: document.querySelector('#dashboard-loading'), pendingCount: document.querySelector('#dashboard-pending-count'), pendingDetail: document.querySelector('#dashboard-pending-detail'), pendingTile: document.querySelector('#dashboard-tile-pending'), pendingList: document.querySelector('#dashboard-pending-list'), pendingEmpty: document.querySelector('#dashboard-pending-empty'), coverageCount: document.querySelector('#dashboard-coverage-count'), coverageDetail: document.querySelector('#dashboard-coverage-detail'), coverageTile: document.querySelector('#dashboard-tile-coverage'), coverageList: document.querySelector('#dashboard-coverage-list'), coverageEmpty: document.querySelector('#dashboard-coverage-empty'), coverageLink: document.querySelector('#dashboard-coverage-link'), coverageUnconfigured: document.querySelector('#dashboard-coverage-unconfigured'), quotaCount: document.querySelector('#dashboard-quota-count'), quotaDetail: document.querySelector('#dashboard-quota-detail'), quotaTile: document.querySelector('#dashboard-tile-quota'), quotaSubtitle: document.querySelector('#dashboard-quota-subtitle'), quotaAgents: document.querySelector('#dashboard-quota-agents'), quotaRate: document.querySelector('#dashboard-quota-rate'), quotaRateDetail: document.querySelector('#dashboard-quota-rate-detail'), quotaRemaining: document.querySelector('#dashboard-quota-remaining'), quotaEmptyAgents: document.querySelector('#dashboard-quota-empty'), quotaBody: document.querySelector('#dashboard-quota-body'), quotaEmptyState: document.querySelector('#dashboard-quota-empty-state'), setupCount: document.querySelector('#dashboard-setup-count'), setupDetail: document.querySelector('#dashboard-setup-detail'), setupTile: document.querySelector('#dashboard-tile-setup'), navBadge: document.querySelector('#pending-validation-tab-badge'), }; let pendingItems = []; let loading = false; function setMessage(text = '', type = 'info') { if (!elements.message) return; elements.message.className = text ? `message message-${type}` : 'message'; elements.message.textContent = text; } /** * Une tuile est neutre quand il n'y a rien à faire, alertante dès qu'un * compteur remonte. La couleur ne dit rien de plus que le chiffre : elle * évite juste d'avoir à lire les quatre tuiles pour savoir où agir. */ function setTileTone(tile, count) { if (!tile) return; tile.classList.toggle('is-alert', Number(count) > 0); tile.classList.toggle('is-clear', Number(count) === 0); } function plural(count, singular, pluralForm) { return Number(count) > 1 ? pluralForm : singular; } function weekParam() { return elements.week?.value || ''; } function weekValueOf(item) { return `${item.annee}-W${String(item.numero_semaine).padStart(2, '0')}`; } function setNavBadge(count) { if (!elements.navBadge) return; elements.navBadge.textContent = String(count); elements.navBadge.hidden = Number(count) === 0; elements.navBadge.setAttribute( 'aria-label', `${count} planning${Number(count) > 1 ? 's' : ''} en attente de validation` ); } function renderWeek(week) { if (!week) return; const contexteLabel = { PERISCOLAIRE: 'Semaine scolaire — contexte périscolaire', EXTRASCOLAIRE: 'Semaine de vacances — contexte extrascolaire', MIXTE: 'Semaine à cheval entre période scolaire et vacances', }[week.contexte] || ''; if (elements.weekLabel) { elements.weekLabel.textContent = `Semaine ${week.numero} · du ${formatDate(week.date_debut)} au ${formatDate(week.date_fin)}`; } if (elements.weekContext) { const vacances = Array.isArray(week.vacances_libelles) ? week.vacances_libelles : []; const suffix = vacances.length ? ` (${vacances.join(', ')})` : ''; elements.weekContext.textContent = contexteLabel ? `${contexteLabel}${suffix}` : ''; elements.weekContext.hidden = !contexteLabel; } if (elements.coverageLink) { elements.coverageLink.href = `coverage.php?week=${encodeURIComponent(week.valeur)}`; } if (elements.coverageTile) { elements.coverageTile.href = `coverage.php?week=${encodeURIComponent(week.valeur)}`; } } function renderPending(validation) { const count = Number(validation.count || 0); pendingItems = Array.isArray(validation.apercu) ? validation.apercu : []; setNavBadge(count); setTileTone(elements.pendingTile, count); if (elements.pendingCount) elements.pendingCount.textContent = String(count); if (elements.pendingDetail) { elements.pendingDetail.textContent = count === 0 ? 'Aucun brouillon en attente' : `${formatDuration(Number(validation.minutes_decomptees || 0))} décomptées · ` + `${validation.agent_count} ${plural(validation.agent_count, 'agent', 'agents')} · ` + `${validation.structure_count} ${plural(validation.structure_count, 'lieu', 'lieux')}`; } if (elements.pendingEmpty) elements.pendingEmpty.hidden = count !== 0; if (!elements.pendingList) return; elements.pendingList.innerHTML = pendingItems.map((item) => `
  • ${escapeHtml(item.agent_prenom)} ${escapeHtml(item.agent_nom)} ${escapeHtml(item.structure_nom)} · semaine ${Number(item.numero_semaine)} du ${formatDate(item.date_debut)}
    ${Number(item.nombre_creneaux)} ${plural(item.nombre_creneaux, 'créneau', 'créneaux')} ${formatDuration(Number(item.total_minutes || 0))}
  • `).join(''); } function renderCoverage(coverage, week, perimetre) { const gapCount = Number(coverage.gap_count || 0); const unconfigured = Number(coverage.unconfigured_count || 0); // Sans aucun lieu enregistré, « tous les minimums sont atteints » serait // vrai mais trompeur : il n'y a simplement rien à contrôler. const sansLieu = Number(perimetre?.structures || 0) === 0; setTileTone(elements.coverageTile, gapCount); setTileTone(elements.setupTile, unconfigured); if (elements.coverageCount) elements.coverageCount.textContent = String(gapCount); if (elements.coverageDetail) { elements.coverageDetail.textContent = gapCount === 0 ? (sansLieu ? 'Aucun lieu d’affectation enregistré' : 'Tous les minimums sont atteints') : `${formatDuration(Number(coverage.gap_minutes || 0))} non couvertes · ` + `${coverage.structure_count} ${plural(coverage.structure_count, 'lieu concerné', 'lieux concernés')}`; } if (elements.setupCount) elements.setupCount.textContent = String(unconfigured); if (elements.setupDetail) { elements.setupDetail.textContent = unconfigured === 0 ? (sansLieu ? 'Aucun lieu à paramétrer pour l’instant' : 'Tous les lieux ont des horaires attendus') : 'Aucun minimum d’agents n’y est contrôlé'; } const items = Array.isArray(coverage.apercu) ? coverage.apercu : []; if (elements.coverageEmpty) elements.coverageEmpty.hidden = items.length !== 0; if (elements.coverageList) { const weekValue = week?.valeur || ''; elements.coverageList.innerHTML = items.map((gap) => { const href = `coverage.php?week=${encodeURIComponent(weekValue)}&structure_id=${Number(gap.id_structure)}`; return `
  • ${escapeHtml(gap.structure_nom)} ${escapeHtml(gap.jour_libelle)} ${formatDate(gap.date)} · ${escapeHtml(gap.heure_debut)} → ${escapeHtml(gap.heure_fin)}
    ${Number(gap.agents_planifies)} / ${Number(gap.minimum_agents)} agents −${Number(gap.agents_manquants)}
    Analyser
  • `; }).join(''); } if (elements.coverageUnconfigured) { const list = Array.isArray(coverage.unconfigured) ? coverage.unconfigured : []; if (list.length === 0) { elements.coverageUnconfigured.hidden = true; elements.coverageUnconfigured.textContent = ''; } else { const names = list.map((structure) => escapeHtml(structure.nom)).join(', '); const reste = unconfigured - list.length; elements.coverageUnconfigured.innerHTML = `Non contrôlés : ${names}${reste > 0 ? ` et ${reste} autre${reste > 1 ? 's' : ''}` : ''}. ` + 'Définissez leurs horaires de couverture dans l’administration.'; elements.coverageUnconfigured.hidden = false; } } } function renderQuota(quota) { const overCount = Number(quota.depassement_count || 0); setTileTone(elements.quotaTile, overCount); if (elements.quotaCount) elements.quotaCount.textContent = String(overCount); if (elements.quotaDetail) { elements.quotaDetail.textContent = overCount === 0 ? (Number(quota.agent_count || 0) === 0 ? 'Aucun agent actif à suivre' : `Aucun dépassement sur ${quota.annee}`) : `Sur ${quota.agent_count} ${plural(quota.agent_count, 'agent suivi', 'agents suivis')} en ${quota.annee}`; } if (elements.quotaTile) { elements.quotaTile.href = `annual.php?year=${Number(quota.annee)}`; } if (elements.quotaSubtitle) { elements.quotaSubtitle.textContent = `Année civile ${quota.annee} · heures validées et brouillon comprises, motifs décomptés uniquement.`; } if (elements.quotaAgents) elements.quotaAgents.textContent = String(quota.agent_count || 0); if (elements.quotaRate) elements.quotaRate.textContent = `${Number(quota.taux_global || 0)} %`; if (elements.quotaRateDetail) { elements.quotaRateDetail.textContent = `${formatDuration(Number(quota.minutes_affectees || 0))} sur ${formatDuration(Number(quota.minutes_cible || 0))}`; } if (elements.quotaRemaining) elements.quotaRemaining.textContent = formatDuration(Number(quota.minutes_restantes || 0)); if (elements.quotaEmptyAgents) elements.quotaEmptyAgents.textContent = String(quota.sans_heure_count || 0); const items = Array.isArray(quota.apercu) ? quota.apercu : []; if (elements.quotaEmptyState) elements.quotaEmptyState.hidden = items.length !== 0; if (!elements.quotaBody) return; elements.quotaBody.innerHTML = items.map((agent) => { const over = Number(agent.minutes_restantes) < 0; const rate = Number(agent.taux_consommation || 0); const href = `annual.php?agent_id=${Number(agent.id_agent)}&year=${Number(quota.annee)}`; return ` ${escapeHtml(agent.prenom)} ${escapeHtml(agent.nom)} ${escapeHtml(agent.poste_libelle || 'Poste non renseigné')} ${escapeHtml(agent.structure_nom || 'Non rattaché')} ${formatDuration(Number(agent.quota_cible_minutes || 0))} quotité ${Number(agent.quotite_travail)} % ${formatDuration(Number(agent.minutes_affectees || 0))} ${formatDuration(Number(agent.minutes_brouillon || 0))} en brouillon ${rate} % Ouvrir `; }).join(''); } function openPendingPlanning(item) { if (!item) return; const params = new URLSearchParams({ structure_id: String(item.id_structure), agent_id: String(item.id_agent), week: weekValueOf(item), focus: 'validation', notice: `Planning de ${item.agent_prenom} ${item.agent_nom} ouvert depuis le tableau de bord.`, }); window.location.href = `planning.php?${params.toString()}`; } async function load(options = {}) { if (loading) return; loading = true; if (!options.silent) setMessage(''); if (elements.loading) elements.loading.hidden = false; if (elements.refresh) elements.refresh.disabled = true; try { const query = new URLSearchParams(); const week = weekParam(); if (week) query.set('week', week); const response = await fetch(`api/dashboard_summary.php?${query.toString()}`, { headers: { Accept: 'application/json' }, }); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de charger le tableau de bord.')); renderWeek(data.semaine); renderPending(data.validation || {}); renderCoverage(data.couverture || {}, data.semaine, data.perimetre); renderQuota(data.quota || {}); } catch (error) { setMessage(error.message, 'error'); } finally { loading = false; if (elements.loading) elements.loading.hidden = true; if (elements.refresh) elements.refresh.disabled = false; } } function init() { elements.refresh?.addEventListener('click', () => load()); elements.week?.addEventListener('change', () => load()); elements.pendingList?.addEventListener('click', (event) => { const trigger = event.target.closest('[data-planning-id]'); if (!trigger) return; const id = Number(trigger.dataset.planningId); openPendingPlanning(pendingItems.find((item) => Number(item.id_planning) === id)); }); load({ silent: true }); } PTA.modules.dashboard = { init, load, refresh: load }; })();