/** * Écran d'administration des référentiels (`PTA.modules.administration`) : * agents, lieux d'affectation, postes et horaires de couverture. * * PRINCIPE DE MISE À JOUR — la page ne se recharge JAMAIS après une création ou * une modification. Les nouveaux éléments sont insérés à la main dans TOUS les * sélecteurs et tableaux concernés, d'où les fonctions `add*Everywhere()`. Un * lieu créé ici doit apparaître immédiatement dans les listes de l'écran de * saisie, de la vue par lieu, du formulaire agent et des règles de couverture. * * CONSÉQUENCE POUR LA MAINTENANCE : ajouter un sélecteur de lieu ou d'agent * ailleurs dans l'application impose de l'inscrire dans `addStructureEverywhere()` * ou `addAgentEverywhere()`, faute de quoi il restera périmé jusqu'au prochain * rechargement complet de la page. * * Cet écran charge aussi le module `coverage`, les règles de couverture étant * un référentiel de lieu. */ (() => { 'use strict'; const PTA = window.PTA; if (!PTA) throw new Error('PTA core doit être chargé avant administration.js'); const { structureSelect } = PTA.elements; const { escapeHtml, readJsonResponse, apiErrorMessage, formatDuration, } = PTA.utils; const typeLabel = (type) => type === 'EXTRASCOLAIRE' ? 'Extrascolaire' : 'Périscolaire'; /** * Enregistre le lieu de rattachement principal d'un agent depuis sa ligne de * tableau. Un lieu vide est valide : il détache l'agent, qui reste * planifiable ponctuellement ailleurs. */ async function saveAgentStructure(button) { const row = button.closest('tr[data-agent-id]'); const select = row?.querySelector('.agent-structure-select'); const message = document.querySelector('#assignment-message'); if (!row || !select || !message) return; button.disabled = true; message.className = 'message'; message.textContent = ''; try { const response = await fetch('api/agent_assignments.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, agent_id: Number(row.dataset.agentId), structure_id: select.value ? Number(select.value) : null, }), }); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de modifier le lieu principal de l’agent.')); message.className = 'message message-success'; message.textContent = data.message; if (structureSelect?.value) await PTA.modules.planning?.loadAgents?.(); if (document.querySelector('#overview-structure')?.value) await PTA.modules.structureOverview?.load?.(); if (document.querySelector('#overview-agent')?.value) await PTA.modules.agentOverview?.load?.(); } catch (error) { message.className = 'message message-error'; message.textContent = error.message; } finally { button.disabled = false; } } /** * Enregistre le type d'un lieu, puis répercute le nouveau libellé dans TOUS * les sélecteurs de la page — le type fait partie du texte affiché. */ async function saveStructureType(button) { const row = button.closest('tr[data-structure-id]'); const select = row?.querySelector('.structure-type-select'); const message = document.querySelector('#structure-settings-message'); if (!row || !select || !message) return; button.disabled = true; message.className = 'message'; message.textContent = ''; try { const response = await fetch('api/update_structure.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, structure_id: Number(row.dataset.structureId), type_affectation: select.value, }), }); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de modifier le type du lieu.')); updateStructureLabels(Number(row.dataset.structureId), select.value); message.className = 'message message-success'; message.textContent = data.message; if (structureSelect?.value) await PTA.modules.planning?.loadAgents?.(); if (document.querySelector('#overview-structure')?.value === String(row.dataset.structureId)) { await PTA.modules.structureOverview?.load?.(); } if (document.querySelector('#overview-agent')?.value) await PTA.modules.agentOverview?.load?.(); } catch (error) { message.className = 'message message-error'; message.textContent = error.message; } finally { button.disabled = false; } } /** * Ajoute une option à un `` de la page après * un changement de type. Parcourt l'ensemble des sélecteurs plutôt qu'une * liste nommée : le libellé peut apparaître n'importe où. */ function updateStructureLabels(structureId, type) { const source = document.querySelector(`#structure-settings-body tr[data-structure-id="${Number(structureId)}"]`); const structureName = source?.querySelector('td strong')?.textContent?.trim(); if (!structureName) return; const label = `${structureName} — ${typeLabel(type)}`; document.querySelectorAll('select').forEach((select) => { const option = Array.from(select.options).find((item) => item.value === String(structureId)); if (option && ( select.matches('#structure, #overview-structure, #new-agent-structure, #edit-agent-entry-structure, #edit-agent-profile-structure, #coverage-structure-filter, #coverage-rules-structure, .agent-structure-select') )) { option.textContent = label; } }); } /** Options de lieu, reconstruites depuis les lieux actuellement affichés. */ function structureOptionsHtml(selectedStructureId = null) { const source = document.querySelector('#new-agent-structure'); if (!source) return ''; const options = ['']; Array.from(source.options).forEach((option) => { if (!option.value) return; const selected = Number(option.value) === Number(selectedStructureId) ? ' selected' : ''; options.push(``); }); return options.join(''); } /** Ajoute la ligne d'un lieu au tableau de paramétrage, sans doublon. */ function addStructureSettingsRow(structure) { const body = document.querySelector('#structure-settings-body'); if (!body || body.querySelector(`tr[data-structure-id="${Number(structure.id_structure)}"]`)) return; const row = document.createElement('tr'); row.dataset.structureId = String(Number(structure.id_structure)); row.innerHTML = ` ${escapeHtml(structure.nom)} ${escapeHtml(structure.code)} `; body.appendChild(row); } /** « Diplômé — Libellé », « Diplômé » ou « Non diplômé ». */ function qualificationLabel(agent) { if (!Number(agent.est_diplome)) return 'Non diplômé'; return agent.diplome_libelle ? `Diplômé — ${agent.diplome_libelle}` : 'Diplômé'; } /** * « 2026-02-17 » → « 17/02/2026 », « Non renseignée » si vide. * Découpage de chaîne plutôt que `new Date()`, qui décalerait la date. */ function formatContractDate(value) { if (!value) return 'Non renseignée'; const [year, month, day] = String(value).split('-'); return year && month && day ? `${day}/${month}/${year}` : value; } /** * Cellules d'une ligne d'agent. * Toutes les valeurs passent par escapeHtml() : elles viennent de la base et * sont insérées en HTML brut. */ function agentRowHtml(agent) { const structureLabel = agent.structure_nom || agent.structure_principale_nom || 'Sans lieu principal'; return ` ${escapeHtml(agent.prenom)} ${escapeHtml(agent.nom)} ${escapeHtml(agent.matricule)} ${escapeHtml(agent.poste_libelle || 'Non renseigné')} ${escapeHtml(qualificationLabel(agent))} ${escapeHtml(agent.telephone || 'Non renseigné')} ${escapeHtml(formatContractDate(agent.date_debut_contrat))} ${escapeHtml(structureLabel)} `; } /** * Recopie la fiche de l'agent dans les `data-*` de sa ligne. * * Ces attributs sont la SOURCE de la modale d'édition : elle s'ouvre * uniquement à partir d'eux, sans nouvel appel réseau. Ajouter un champ * modifiable impose donc de l'ajouter ici ET dans * `openAgentProfileModal()`, sinon il s'ouvrira vide et sera écrasé à * l'enregistrement. * * Les noms sont en camelCase, conversion automatique des `data-*` par le DOM * (`data-date-debut-contrat` → `dataset.dateDebutContrat`). */ function setAgentRowData(row, agent) { row.dataset.agentId = String(Number(agent.id_agent)); row.dataset.email = agent.email || ''; row.dataset.telephone = agent.telephone || ''; row.dataset.adresse = agent.adresse || ''; row.dataset.estDiplome = Number(agent.est_diplome) ? '1' : '0'; row.dataset.diplomeLibelle = agent.diplome_libelle || ''; row.dataset.dateDebutContrat = agent.date_debut_contrat || ''; row.dataset.dateFinContrat = agent.date_fin_contrat || ''; row.dataset.idPoste = agent.id_poste ? String(Number(agent.id_poste)) : ''; row.dataset.typeContrat = agent.type_contrat || 'PERMANENT'; row.dataset.droitEnveloppeFormation = Number(agent.droit_enveloppe_formation ?? 1) ? '1' : '0'; row.dataset.commentairePta = agent.commentaire_pta || ''; row.dataset.structureId = agent.id_structure ? String(Number(agent.id_structure)) : ''; } /** Insère un agent nouvellement créé dans les sélecteurs et le tableau. */ function addAgentEverywhere(agent) { const overviewAgent = document.querySelector('#overview-agent'); addOptionIfMissing( overviewAgent, Number(agent.id_agent), `${agent.prenom} ${agent.nom} (${agent.matricule})` ); const body = document.querySelector('#agent-assignment-body'); if (body && !body.querySelector(`tr[data-agent-id="${Number(agent.id_agent)}"]`)) { const row = document.createElement('tr'); setAgentRowData(row, agent); row.innerHTML = agentRowHtml(agent); body.appendChild(row); } } /** * Le libellé du diplôme n'a de sens que si l'agent est diplômé : le champ * est désactivé et VIDÉ sinon, pour ne pas enregistrer une valeur résiduelle. */ function toggleDiplomaField(select, input) { if (!select || !input) return; const qualified = select.value === '1'; input.disabled = !qualified; if (!qualified) input.value = ''; } /** * Ouvre la modale d'édition d'un agent, préremplie depuis les `data-*` de sa * ligne (voir `setAgentRowData()`). */ function openAgentProfileModal(row) { const modal = document.querySelector('#agent-profile-modal'); if (!modal || !row) return; document.querySelector('#edit-agent-profile-id').value = row.dataset.agentId || ''; document.querySelector('#agent-profile-modal-subtitle').textContent = row.querySelector('.agent-row-name')?.textContent?.trim() || ''; document.querySelector('#edit-agent-profile-email').value = row.dataset.email || ''; document.querySelector('#edit-agent-profile-phone').value = row.dataset.telephone || ''; document.querySelector('#edit-agent-profile-address').value = row.dataset.adresse || ''; document.querySelector('#edit-agent-profile-qualified').value = row.dataset.estDiplome === '1' ? '1' : '0'; document.querySelector('#edit-agent-profile-diploma').value = row.dataset.diplomeLibelle || ''; document.querySelector('#edit-agent-profile-contract-start').value = row.dataset.dateDebutContrat || ''; document.querySelector('#edit-agent-profile-contract-end').value = row.dataset.dateFinContrat || ''; document.querySelector('#edit-agent-profile-post').value = row.dataset.idPoste || ''; document.querySelector('#edit-agent-profile-contract-type').value = row.dataset.typeContrat || 'PERMANENT'; document.querySelector('#edit-agent-profile-training-right').checked = row.dataset.droitEnveloppeFormation !== '0'; document.querySelector('#edit-agent-profile-pta-comment').value = row.dataset.commentairePta || ''; document.querySelector('#edit-agent-profile-structure').value = row.dataset.structureId || ''; toggleDiplomaField(document.querySelector('#edit-agent-profile-qualified'), document.querySelector('#edit-agent-profile-diploma')); const message = document.querySelector('#agent-profile-message'); if (message) { message.className = 'message'; message.textContent = ''; } modal.hidden = false; } /** Ferme la modale d'édition d'un agent. */ function closeAgentProfileModal() { const modal = document.querySelector('#agent-profile-modal'); if (modal) modal.hidden = true; } /** * Enregistre la fiche d'un agent, puis met à jour sa ligne et ses `data-*` * avec la fiche renvoyée par le serveur — et non avec les valeurs saisies : * le serveur peut avoir normalisé ou refusé certains champs. */ async function saveAgentProfile(event) { event.preventDefault(); const form = event.currentTarget; const message = document.querySelector('#agent-profile-message'); const submit = form.querySelector('button[type="submit"]'); const agentId = Number(document.querySelector('#edit-agent-profile-id').value); if (!agentId || !message || !submit) return; submit.disabled = true; message.className = 'message'; message.textContent = ''; try { const structureValue = document.querySelector('#edit-agent-profile-structure').value; const response = await fetch('api/update_agent.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, agent_id: agentId, email: document.querySelector('#edit-agent-profile-email').value, telephone: document.querySelector('#edit-agent-profile-phone').value, adresse: document.querySelector('#edit-agent-profile-address').value, est_diplome: document.querySelector('#edit-agent-profile-qualified').value === '1', diplome_libelle: document.querySelector('#edit-agent-profile-diploma').value, date_debut_contrat: document.querySelector('#edit-agent-profile-contract-start').value, date_fin_contrat: document.querySelector('#edit-agent-profile-contract-end').value, id_poste: Number(document.querySelector('#edit-agent-profile-post').value), type_contrat: document.querySelector('#edit-agent-profile-contract-type').value, droit_enveloppe_formation: document.querySelector('#edit-agent-profile-training-right').checked, commentaire_pta: document.querySelector('#edit-agent-profile-pta-comment').value, structure_id: structureValue ? Number(structureValue) : null, }), }); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de modifier les informations de l’agent.')); const row = document.querySelector(`#agent-assignment-body tr[data-agent-id="${agentId}"]`); if (row) { setAgentRowData(row, data.agent); row.innerHTML = agentRowHtml({ ...data.agent, structure_nom: data.agent.structure_principale_nom, }); } message.className = 'message message-success'; message.textContent = data.message; if (structureSelect?.value) await PTA.modules.planning?.loadAgents?.(); if (document.querySelector('#overview-structure')?.value) await PTA.modules.structureOverview?.load?.(); if (document.querySelector('#overview-agent')?.value === String(agentId)) await PTA.modules.agentOverview?.load?.(); setTimeout(closeAgentProfileModal, 500); } catch (error) { message.className = 'message message-error'; message.textContent = error.message; } finally { submit.disabled = false; } } /** * Crée un lieu d'affectation, puis le diffuse dans toute la page. * Un code déjà pris ressort en 409 et le message le dit explicitement. */ async function createStructure(event) { event.preventDefault(); const form = event.currentTarget; const message = document.querySelector('#create-structure-message'); const submit = form.querySelector('button[type="submit"]'); if (!message || !submit) return; message.className = 'message'; message.textContent = ''; submit.disabled = true; try { const response = await fetch('api/create_structure.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, code: document.querySelector('#new-structure-code').value, nom: document.querySelector('#new-structure-name').value, adresse: document.querySelector('#new-structure-address').value, type_affectation: document.querySelector('#new-structure-type').value, }), }); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de créer le lieu d’affectation.')); addStructureEverywhere(data.structure); addStructureSettingsRow(data.structure); form.reset(); document.querySelector('#new-structure-type').value = 'PERISCOLAIRE'; message.className = 'message message-success'; message.textContent = data.message; PTA.modules.coverage?.refresh?.({ silent: true }); } catch (error) { message.className = 'message message-error'; message.textContent = error.message; } finally { submit.disabled = false; } } /** * Crée un agent, puis le diffuse dans toute la page. * Le serveur ouvre au passage son quota annuel et son PTA ; un matricule * déjà pris ressort en 409. */ async function createAgent(event) { event.preventDefault(); const form = event.currentTarget; const message = document.querySelector('#create-agent-message'); const submit = form.querySelector('button[type="submit"]'); if (!message || !submit) return; message.className = 'message'; message.textContent = ''; submit.disabled = true; try { const response = await fetch('api/create_agent.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, matricule: document.querySelector('#new-agent-matricule').value, nom: document.querySelector('#new-agent-name').value, prenom: document.querySelector('#new-agent-firstname').value, email: document.querySelector('#new-agent-email').value, telephone: document.querySelector('#new-agent-phone').value, adresse: document.querySelector('#new-agent-address').value, est_diplome: document.querySelector('#new-agent-qualified').value === '1', diplome_libelle: document.querySelector('#new-agent-diploma').value, structure_id: Number(document.querySelector('#new-agent-structure').value), quotite_travail: Number(document.querySelector('#new-agent-rate').value), date_debut_contrat: document.querySelector('#new-agent-contract-start').value, date_fin_contrat: document.querySelector('#new-agent-contract-end').value, id_poste: Number(document.querySelector('#new-agent-post').value), type_contrat: document.querySelector('#new-agent-contract-type').value, droit_enveloppe_formation: document.querySelector('#new-agent-training-right').checked, commentaire_pta: document.querySelector('#new-agent-pta-comment').value, }), }); const data = await readJsonResponse(response); if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de créer l’agent.')); addAgentEverywhere(data.agent); const createdStructureId = String(data.agent.id_structure); form.reset(); document.querySelector('#new-agent-rate').value = '100'; document.querySelector('#new-agent-contract-type').value = 'PERMANENT'; document.querySelector('#new-agent-qualified').value = '0'; toggleDiplomaField(document.querySelector('#new-agent-qualified'), document.querySelector('#new-agent-diploma')); message.className = 'message message-success'; message.textContent = `${data.message} Quota annuel : ${formatDuration(Number(data.agent.quota_cible_minutes))}, suivi du 1er janvier au 31 décembre.`; if (structureSelect?.value === createdStructureId) { await PTA.modules.planning?.loadAgents?.(); } if (document.querySelector('#overview-structure')?.value === createdStructureId) { await PTA.modules.structureOverview?.load?.(); } } catch (error) { message.className = 'message message-error'; message.textContent = error.message; } finally { submit.disabled = false; } } /** * Point d'entrée appelé par page-bootstrap.js. * Les tableaux sont écoutés par DÉLÉGATION : leurs lignes sont ajoutées * dynamiquement et n'existent pas au moment où les écouteurs sont posés. */ function init() { document.querySelector('#agent-assignment-body')?.addEventListener('click', (event) => { const button = event.target.closest('.edit-agent-profile'); if (button) openAgentProfileModal(button.closest('tr[data-agent-id]')); }); document.querySelector('#structure-settings-body')?.addEventListener('click', (event) => { const button = event.target.closest('.save-structure-type'); if (button) saveStructureType(button); }); document.querySelector('#new-agent-qualified')?.addEventListener('change', (event) => { toggleDiplomaField(event.currentTarget, document.querySelector('#new-agent-diploma')); }); document.querySelector('#edit-agent-profile-qualified')?.addEventListener('change', (event) => { toggleDiplomaField(event.currentTarget, document.querySelector('#edit-agent-profile-diploma')); }); document.querySelector('#close-agent-profile-modal')?.addEventListener('click', closeAgentProfileModal); document.querySelector('#cancel-agent-profile')?.addEventListener('click', closeAgentProfileModal); document.querySelector('#agent-profile-modal')?.addEventListener('click', (event) => { if (event.target.id === 'agent-profile-modal') closeAgentProfileModal(); }); document.querySelector('#agent-profile-form')?.addEventListener('submit', saveAgentProfile); document.querySelector('#create-structure-form')?.addEventListener('submit', createStructure); document.querySelector('#create-agent-form')?.addEventListener('submit', createAgent); toggleDiplomaField(document.querySelector('#new-agent-qualified'), document.querySelector('#new-agent-diploma')); } PTA.modules.administration = { init, saveAgentStructure, saveStructureType, addStructureEverywhere, addAgentEverywhere, openAgentProfileModal, }; })();