/** * Jetons d'accès de terrain (`PTA.modules.accessTokens`), écran administration.php * — V7.27. * * Le Service Enfance équipe ses agents et ses responsables de lieu : création * d'un compte, régénération d'un jeton perdu, désactivation, suppression, et * génération en lot pour les identités qui n'en ont pas encore. * * PÉRIMÈTRE : agents et responsables de lieu, RIEN D'AUTRE. Les comptes Service * Enfance et DSI ne sont ni listés ni créables. Ce module ne les propose pas, et * le serveur les refuse en 403 (AccessAccountController) : l'écran ne fait que * refléter une frontière qu'il ne décide pas. * * RÈGLE CENTRALE, héritée de l'espace DSI : un jeton n'existe en clair que dans * la réponse qui vient de le produire. Il est affiché dans un panneau dédié, qui * reste ouvert tant qu'il n'a pas été refermé, et n'est jamais rechargé depuis * le serveur — la base n'en garde qu'une empreinte. */ (() => { 'use strict'; const PTA = window.PTA; // core.js absent : la page reste lisible, simplement non interactive. if (!PTA) return; const { readJsonResponse, apiErrorMessage, escapeHtml } = PTA.utils; const el = (id) => document.getElementById(id); const LIBELLES_ROLE = { RESPONSABLE_STRUCTURE: 'Responsable de structure', AGENT: 'Agent', }; /** * Ce qu'il faut renseigner change selon le type de compte : l'écran * l'explique plutôt que de laisser deviner pourquoi un champ change. */ const AIDES_ROLE = { AGENT: 'Compte en lecture seule sur son propre planning. Seuls les agents actifs qui n’ont pas encore de compte sont proposés.', RESPONSABLE_STRUCTURE: 'Compte d’édition des brouillons d’un seul lieu. Seuls les lieux actifs qui n’ont pas encore de compte sont proposés.', }; /** Message dans un des bandeaux de la section. */ function message(id, texte, type = 'info') { const boite = el(id); if (!boite) return; boite.className = `message message-${type}`; boite.textContent = texte; } /** Appel d'API mutualisé : toute réponse non-OK devient une exception. */ async function api(url, options = {}) { const reponse = await fetch(url, options); const donnees = await readJsonResponse(reponse); if (!reponse.ok) throw new Error(apiErrorMessage(donnees, 'Opération impossible.')); return donnees; } /** Raccourci des appels mutants : POST JSON avec le jeton CSRF dans le corps. */ function poster(url, charge) { return api(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: window.PTA_CONFIG.csrfToken, ...charge }), }); } /** « 2026-08-20 09:41:07 » → « 20/08/2026 09:41 ». */ function formatDateHeure(valeur) { if (!valeur) return '—'; const [date, heure = ''] = String(valeur).split(' '); const [annee, mois, jour] = date.split('-'); return `${jour}/${mois}/${annee} ${heure.slice(0, 5)}`; } // ------------------------------------------------------------------ jetons /** * Affiche les jetons produits par la dernière action. * * @param {Array<{libelle: string, role: string, jeton: string}>} jetons */ function afficherJetons(jetons) { const panneau = el('access-token-panel'); const liste = el('access-token-list'); if (!panneau || !liste || !jetons.length) return; liste.innerHTML = jetons.map((entree) => `
${escapeHtml(entree.libelle)} ${escapeHtml(LIBELLES_ROLE[entree.role] || entree.role)}
${escapeHtml(entree.jeton)}
`).join(''); panneau.hidden = false; message('access-token-message', `${jetons.length} jeton(s) à transmettre. Ils ne seront plus affichés après fermeture.`, 'warning'); panneau.scrollIntoView({ behavior: 'smooth', block: 'start' }); } /** * Copie les jetons affichés dans le presse-papiers. * * L'API Clipboard n'existe QUE dans un contexte sécurisé : sur une * installation en http:// sur le réseau de la collectivité, elle est * absente. D'où le repli par zone de texte masquée, puis, en dernier * ressort, l'invitation à sélectionner à la main. */ async function copierJetons() { const lignes = Array.from(document.querySelectorAll('#access-token-list .dsi-token-row')).map((ligne) => { const identite = ligne.querySelector('strong')?.textContent || ''; const jeton = ligne.querySelector('.dsi-token-value')?.textContent || ''; return `${identite}\t${jeton}`; }); if (!lignes.length) return; const texte = lignes.join('\n'); try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(texte); } else { const zone = document.createElement('textarea'); zone.value = texte; zone.setAttribute('readonly', 'readonly'); zone.className = 'dsi-clipboard-relay'; document.body.appendChild(zone); zone.select(); document.execCommand('copy'); zone.remove(); } message('access-token-message', `${lignes.length} jeton(s) copié(s) dans le presse-papiers.`, 'success'); } catch (erreur) { message('access-token-message', 'Copie impossible sur ce navigateur : sélectionnez les jetons à la main.', 'error'); } } /** Referme le panneau des jetons ; leur contenu est perdu pour de bon. */ function fermerJetons() { const panneau = el('access-token-panel'); if (!panneau) return; panneau.hidden = true; el('access-token-list').innerHTML = ''; message('access-token-message', ''); } // ----------------------------------------------------------------- comptes /** Une ligne du tableau des comptes. */ function ligneCompte(compte) { const identite = [ `${escapeHtml(compte.identite_libelle)}`, compte.matricule ? `
Matricule ${escapeHtml(compte.matricule)}` : '', compte.commentaire ? `
${escapeHtml(compte.commentaire)}` : '', ].join(''); const etats = []; if (!compte.actif) etats.push('Désactivé'); else if (!compte.identite_active) etats.push('Identité inactive'); else etats.push('Actif'); const connexions = compte.nombre_connexions > 0 ? `
${compte.nombre_connexions} connexion(s)` : ''; return ` ${identite} ${escapeHtml(LIBELLES_ROLE[compte.role] || compte.role)} ${escapeHtml(compte.jeton_prefixe)}… ${formatDateHeure(compte.date_derniere_connexion)}${connexions} ${etats.join(' ')} `; } /** * Remplit les listes de création à partir des identités SANS compte : * proposer un agent qui en a déjà un ne mènerait qu'à un refus du serveur * (un seul compte par identité). */ function remplirIdentites(manquants) { const agents = manquants.filter((identite) => identite.role === 'AGENT'); const lieux = manquants.filter((identite) => identite.role === 'RESPONSABLE_STRUCTURE'); const selectAgent = el('access-account-agent'); const selectLieu = el('access-account-structure'); if (selectAgent) { selectAgent.innerHTML = '' + agents.map((agent) => ``).join(''); } if (selectLieu) { selectLieu.innerHTML = '' + lieux.map((lieu) => ``).join(''); } const compteur = el('access-missing-count'); if (compteur) { compteur.textContent = String(manquants.length); compteur.hidden = manquants.length === 0; } const bouton = el('access-generate-missing'); if (bouton) bouton.disabled = manquants.length === 0; } /** Recharge la liste des comptes selon les filtres courants. */ async function chargerComptes() { const parametres = new URLSearchParams(); const role = el('access-filter-role')?.value || ''; const etat = el('access-filter-state')?.value || ''; const recherche = el('access-filter-search')?.value.trim() || ''; if (role) parametres.set('role', role); if (etat !== '') parametres.set('actif', etat); if (recherche) parametres.set('q', recherche); message('access-accounts-message', 'Chargement des comptes…'); try { const donnees = await api(`api/acces_comptes.php?${parametres.toString()}`); const comptes = donnees.comptes || []; el('access-accounts-body').innerHTML = comptes.length ? comptes.map(ligneCompte).join('') : 'Aucun compte ne correspond à ces filtres.'; remplirIdentites(donnees.manquants || []); message('access-accounts-message', `${comptes.length} compte(s) affiché(s).`, 'success'); } catch (erreur) { message('access-accounts-message', erreur.message, 'error'); } } /** Affiche les seuls champs d'identité utiles au type de compte choisi. */ function ajusterFormulaireCompte() { const role = el('access-account-role')?.value || ''; const champAgent = el('access-account-agent-field'); const champLieu = el('access-account-structure-field'); if (!champAgent || !champLieu) return; champAgent.hidden = role !== 'AGENT'; champLieu.hidden = role !== 'RESPONSABLE_STRUCTURE'; el('access-account-agent').required = role === 'AGENT'; el('access-account-structure').required = role === 'RESPONSABLE_STRUCTURE'; const aide = el('access-account-hint'); if (aide) aide.textContent = AIDES_ROLE[role] || ''; } /** Crée un compte, puis affiche son jeton — unique occasion de le lire. */ async function creerCompte(evenement) { evenement.preventDefault(); const role = el('access-account-role').value; try { const donnees = await poster('api/acces_compte_creer.php', { role, agent_id: role === 'AGENT' ? Number(el('access-account-agent').value) : null, structure_id: role === 'RESPONSABLE_STRUCTURE' ? Number(el('access-account-structure').value) : null, commentaire: el('access-account-comment').value, }); message('access-account-message', donnees.message, 'success'); el('access-account-comment').value = ''; afficherJetons([{ libelle: donnees.compte?.identite_libelle || '', role: donnees.compte?.role || role, jeton: donnees.jeton, }]); await chargerComptes(); } catch (erreur) { message('access-account-message', erreur.message, 'error'); } } /** Régénération, activation, suppression : délégation sur le tableau. */ async function agirSurCompte(evenement) { const ligne = evenement.target.closest('tr[data-compte-id]'); if (!ligne) return; const compteId = Number(ligne.dataset.compteId); const titulaire = ligne.querySelector('strong')?.textContent || 'ce compte'; try { if (evenement.target.closest('.access-regenerate')) { if (!window.confirm(`Régénérer le jeton de ${titulaire} ? L’ancien cessera immédiatement de fonctionner.`)) return; const donnees = await poster('api/acces_compte_jeton.php', { compte_id: compteId }); afficherJetons([{ libelle: donnees.compte?.identite_libelle || titulaire, role: donnees.compte?.role || '', jeton: donnees.jeton, }]); message('access-accounts-message', donnees.message, 'success'); } else if (evenement.target.closest('.access-toggle')) { const desactiver = evenement.target.textContent.trim().startsWith('Dés'); const donnees = await poster('api/acces_compte_statut.php', { compte_id: compteId, action: desactiver ? 'DESACTIVER' : 'ACTIVER', }); message('access-accounts-message', donnees.message, 'success'); } else if (evenement.target.closest('.access-delete')) { if (!window.confirm(`Supprimer définitivement le compte de ${titulaire} ? Son jeton cessera de fonctionner ; ses traces dans le journal sont conservées.`)) return; const donnees = await poster('api/acces_compte_statut.php', { compte_id: compteId, action: 'SUPPRIMER' }); message('access-accounts-message', donnees.message, 'success'); } else { return; } await chargerComptes(); } catch (erreur) { message('access-accounts-message', erreur.message, 'error'); } } /** Équipe d'un coup toutes les identités actives sans compte. */ async function genererManquants() { if (!window.confirm('Générer un jeton pour chaque agent et chaque lieu actif qui n’en a pas ? Ils seront affichés une seule fois.')) return; try { const donnees = await poster('api/acces_comptes_manquants.php', {}); message('access-accounts-message', donnees.message, 'success'); afficherJetons(donnees.jetons || []); await chargerComptes(); } catch (erreur) { message('access-accounts-message', erreur.message, 'error'); } } function init() { // La section n'existe que sur l'écran Administration : ailleurs, le // module se retire sans rien tenter. if (!el('access-accounts-body')) return; el('access-account-role')?.addEventListener('change', ajusterFormulaireCompte); el('access-account-form')?.addEventListener('submit', creerCompte); el('access-accounts-body')?.addEventListener('click', agirSurCompte); el('access-accounts-refresh')?.addEventListener('click', chargerComptes); el('access-filter-role')?.addEventListener('change', chargerComptes); el('access-filter-state')?.addEventListener('change', chargerComptes); el('access-generate-missing')?.addEventListener('click', genererManquants); el('access-token-copy')?.addEventListener('click', copierJetons); el('access-token-close')?.addEventListener('click', fermerJetons); ajusterFormulaireCompte(); chargerComptes(); } PTA.modules.accessTokens = { init, refresh: chargerComptes }; })();