/** * PTA détaillé d'un agent (`PTA.modules.pta`), écran pta.php. * * Affiche la répartition annuelle des temps d'un agent, ses contraintes * prioritaires (médicales, temps partiel thérapeutique) et ses souhaits de * lieux, accompagnés des contrôles réglementaires calculés par PtaModel. * * Les contrôles suivent la forme commune {niveau, code, message}. Aucun n'est * bloquant ici : le PTA est un écran d'analyse, l'arbitrage revient à l'agent de * planification. * * Attention à `quotaLabel()` ci-dessous : les durées arrivent sous forme * d'OBJET et non de chaîne, piège déjà rencontré. */ (() => { 'use strict'; const PTA = window.PTA; // core.js absent : la page reste lisible, simplement non interactive. if (!PTA) return; const { readJsonResponse, apiErrorMessage, escapeHtml, formatDuration, formatDate } = PTA.utils; let current = null; const el = (id) => document.getElementById(id); const selectedAgentId = () => Number(el('pta-agent')?.value || 0); /** Message dans le bandeau de la page. */ function show(text, type = 'info') { const box = el('pta-message'); if (!box) return; box.className = `message message-${type}`; box.textContent = text; } /** Vignette de synthèse : libellé, valeur, précision facultative. */ function card(label, value, detail = '') { return `
${escapeHtml(label)}${escapeHtml(value)}${detail ? `${escapeHtml(detail)}` : ''}
`; } /** * QuotaModel::enrich() renvoie les durées sous forme d'objet * { minutes, heures_decimales, libelle } et non de chaîne. Passer l'objet * tel quel à une interpolation affiche « [object Object] » — et un objet * étant toujours truthy, un `||` ne rattrape jamais le coup. On lit donc * explicitement le libellé, en gardant le nombre de minutes comme repli. */ function quotaLabel(enriched, fallbackMinutes) { return enriched?.libelle || formatDuration(Number(fallbackMinutes || 0)); } /** Rend la synthèse : catégories de temps, compteurs et contrôles. */ function renderSummary(data) { current = data; const agent = data.agent || {}; const quota = data.quota || {}; const counters = data.counters || {}; el('pta-content').hidden = false; el('pta-agent-name').textContent = `${agent.prenom || ''} ${agent.nom || ''}`.trim(); el('pta-agent-meta').textContent = [ agent.matricule, agent.poste_libelle || 'Poste non renseigné', agent.structure_principale_nom || 'Sans lieu principal', agent.type_contrat === 'PERMANENT' ? 'Contrat permanent' : 'Contrat non permanent', ].filter(Boolean).join(' — '); el('pta-summary-cards').innerHTML = [ card('Année civile PTA', `${formatDate(quota.date_debut_periode)} → ${formatDate(quota.date_fin_periode)}`), card('Quotité', `${Number(quota.quotite_travail || 0).toLocaleString('fr-FR')} %`, `Référence : ${formatDuration(Number(quota.quota_reference_minutes || 96420))}`), card('Quota à réaliser', quotaLabel(quota.quota_cible, quota.quota_cible_minutes)), card('Déjà affecté', quotaLabel(quota.affectees, quota.minutes_affectees)), card('Reste à affecter', quotaLabel(quota.restantes, quota.minutes_restantes)), card('Formation', `${counters.formation_duree || '0 h 00'} / ${counters.formation_cible_duree || '0 h 00'}`), card('Congés annuels', `${Number(counters.conges_annuels_jours || 0)} / ${Number(counters.conges_annuels_cible || 25)} jours`), card('Fractionnement', `${Number(counters.jours_fractionnement || 0)} / ${Number(counters.jours_fractionnement_cible || 2)} jours`), ].join(''); el('pta-category-body').innerHTML = (data.categories || []).map((category) => ` ${escapeHtml(category.libelle)}${escapeHtml(category.duree)}${Number(category.jours || 0)} `).join('') || 'Aucune affectation sur la période.'; el('pta-controls').innerHTML = (data.controls || []).map((control) => { const level = String(control.niveau || 'INFO').toLowerCase(); return `
${escapeHtml(control.niveau || 'INFO')}${escapeHtml(control.message || '')}
`; }).join('') || '
Aucun contrôle disponible.
'; renderConstraints(data.constraints || []); renderWishes(data.wishes || []); const refDate = el('pta-reference-date').value; el('pta-open-agent-view').href = `agents.php?agent_id=${Number(agent.id_agent)}&month=${encodeURIComponent(refDate.slice(0, 7))}`; el('pta-open-planning').href = `planning.php?agent_id=${Number(agent.id_agent)}`; el('pta-constraint-start').value ||= quota.date_debut_periode || ''; el('pta-constraint-end').value ||= quota.date_fin_periode || ''; } /** Liste les contraintes prioritaires ; sans date de fin, elles courent indéfiniment. */ function renderConstraints(items) { el('pta-constraint-list').innerHTML = items.map((item) => `
${item.type_contrainte === 'TEMPS_PARTIEL_THERAPEUTIQUE' ? 'Temps partiel thérapeutique' : 'Préconisation médicale'}

${escapeHtml(formatDate(item.date_debut))} → ${item.date_fin ? escapeHtml(formatDate(item.date_fin)) : 'sans fin'} — ${escapeHtml(item.commentaire)}

`).join('') || '
Aucune contrainte active sur cette période.
'; } /** Liste les souhaits de lieux ; les interdictions sont présentées en premier. */ function renderWishes(items) { el('pta-wish-list').innerHTML = items.map((item) => `
${item.type_souhait === 'INTERDICTION' ? 'Interdiction' : 'Préférence'} — ${escapeHtml(item.structure_nom)}

Priorité ${Number(item.priorite)}${item.distance_km !== null ? ` — ${Number(item.distance_km).toLocaleString('fr-FR')} km` : ''}${item.commentaire ? ` — ${escapeHtml(item.commentaire)}` : ''}

`).join('') || '
Aucune préférence ou interdiction enregistrée.
'; } /** Charge le PTA de l'agent et de la date de référence sélectionnés. */ async function load() { const agentId = selectedAgentId(); const date = el('pta-reference-date')?.value; if (!agentId || !date) { show('Choisissez un agent et une date de référence.', 'error'); return; } show('Calcul du PTA en cours…'); try { const response = await fetch(`api/pta_summary.php?agent_id=${agentId}&date=${encodeURIComponent(date)}`); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de charger le PTA.')); renderSummary(data); show('PTA actualisé.', 'success'); } catch (error) { show(error.message, 'error'); } } /** * POST JSON mutualisé, jeton CSRF inclus d'office : c'est ce qui garantit * qu'aucune écriture de cet écran ne l'oublie. */ async function post(url, payload) { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, ...payload }), }); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Opération impossible.')); return data; } /** * Enregistre une contrainte prioritaire, puis recharge le PTA — les * contrôles en dépendent. */ async function saveConstraint(event) { event.preventDefault(); if (!selectedAgentId()) return; try { await post('api/pta_constraint.php', { agent_id: selectedAgentId(), type_contrainte: el('pta-constraint-type').value, date_debut: el('pta-constraint-start').value, date_fin: el('pta-constraint-end').value, quotite_temporaire: el('pta-constraint-rate').value, maximum_heures_jour: el('pta-constraint-day-max').value, maximum_heures_semaine: el('pta-constraint-week-max').value, interdit_matin: el('pta-forbid-morning').checked, interdit_midi: el('pta-forbid-midday').checked, interdit_soir: el('pta-forbid-evening').checked, commentaire: el('pta-constraint-comment').value, }); event.currentTarget.reset(); await load(); } catch (error) { show(error.message, 'error'); } } /** Enregistre un souhait de lieu, puis recharge le PTA. */ async function saveWish(event) { event.preventDefault(); if (!selectedAgentId()) return; try { await post('api/pta_wish.php', { agent_id: selectedAgentId(), structure_id: Number(el('pta-wish-structure').value), type_souhait: el('pta-wish-type').value, priorite: Number(el('pta-wish-priority').value), distance_km: el('pta-wish-distance').value, commentaire: el('pta-wish-comment').value, }); event.currentTarget.reset(); el('pta-wish-priority').value = '1'; await load(); } catch (error) { show(error.message, 'error'); } } /** * Supprime une contrainte ou un souhait, par délégation. L'action est portée * par les `data-*` du bouton, les deux listes étant redessinées à chaque * chargement. */ async function handleDelete(event) { const constraint = event.target.closest('.pta-delete-constraint'); const wish = event.target.closest('.pta-delete-wish'); if (!constraint && !wish) return; if (!window.confirm('Confirmer la suppression ?')) return; try { if (constraint) { await post('api/pta_constraint.php', { action: 'DELETE', agent_id: selectedAgentId(), constraint_id: Number(constraint.dataset.id) }); } else { await post('api/pta_wish.php', { action: 'DELETE', agent_id: selectedAgentId(), wish_id: Number(wish.dataset.id) }); } await load(); } catch (error) { show(error.message, 'error'); } } function init() { el('load-pta')?.addEventListener('click', load); el('pta-constraint-form')?.addEventListener('submit', saveConstraint); el('pta-wish-form')?.addEventListener('submit', saveWish); el('pta-content')?.addEventListener('click', handleDelete); const params = window.PTA_PAGE_CONFIG?.params || {}; if (params.agentId) el('pta-agent').value = String(params.agentId); if (params.date) el('pta-reference-date').value = params.date; if (params.agentId) load(); } PTA.modules.pta = { init, load }; })();