This commit is contained in:
PTA
2026-08-25 15:12:11 +02:00
parent 44730eefca
commit 1a67080268
25 changed files with 2646 additions and 231 deletions

158
assets/js/agent-access.js Normal file
View File

@@ -0,0 +1,158 @@
/**
* Compte d'accès depuis la fiche des agents (`PTA.modules.agentAccess`),
* écran manage_agents.php — V7.28.
*
* Pose sur chaque ligne du tableau des agents un bouton qui COUPE ou RÉTABLIT
* l'accès de l'agent à l'application. C'est le geste courant — un départ, un
* arrêt long, un retour — là où l'écran Administration porte la distribution des
* jetons elle-même.
*
* CE QUE LE BOUTON NE FAIT PAS : il ne touche ni au jeton (désactiver puis
* réactiver rend le MÊME jeton valable), ni à la fiche de l'agent, qui reste
* planifiable. Il ne crée pas non plus de compte : un agent qui n'en a pas
* encore l'obtient depuis Administration, seul écran qui sache afficher un jeton
* fraîchement produit.
*
* Il réutilise les endpoints de la délégation V7.27 (api/acces_comptes.php et
* api/acces_compte_statut.php) : mêmes gardes serveur, même liste blanche de
* rôles, même journalisation. Rien de neuf côté droits.
*
* L'état des comptes N'EST PAS rendu par PHP mais chargé par ce module : le HTML
* servi ne porte ainsi aucune information sur les comptes, comme sur l'écran
* DSI.
*/
(() => {
'use strict';
const PTA = window.PTA;
if (!PTA) return;
const { readJsonResponse, apiErrorMessage } = PTA.utils;
const corps = () => document.querySelector('#agent-assignment-body');
/** Comptes d'accès des agents, indexés par identifiant d'agent. */
let comptesParAgent = new Map();
let chargement = false;
function message(texte, type = 'info') {
const boite = document.querySelector('#assignment-message');
if (!boite) return;
boite.className = texte ? `message message-${type}` : 'message';
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;
}
/**
* Met un bouton de ligne à l'état du compte correspondant.
*
* Trois états, et un seul est actionnable :
* - compte actif → « Couper l'accès » ;
* - compte coupé → « Rétablir l'accès » ;
* - aucun compte → bouton inerte, qui dit où en créer un.
*/
function decorerLigne(ligne) {
const bouton = ligne.querySelector('.agent-access-toggle');
if (!bouton) return;
const agentId = Number(ligne.dataset.agentId);
const compte = comptesParAgent.get(agentId);
if (!compte) {
bouton.disabled = true;
bouton.textContent = 'Sans compte d’accès';
bouton.title = 'Cet agent n’a pas encore de jeton. Créez son compte depuis l’onglet Administration, section « Jetons d’accès ».';
delete bouton.dataset.compteId;
delete bouton.dataset.actif;
return;
}
bouton.disabled = false;
bouton.dataset.compteId = String(compte.id_compte);
bouton.dataset.actif = compte.actif ? '1' : '0';
bouton.textContent = compte.actif ? 'Couper l’accès' : 'Rétablir l’accès';
bouton.title = compte.actif
? 'Désactive le compte : l’agent ne pourra plus se connecter. Son jeton reste valable et resservira au rétablissement.'
: 'Réactive le compte : l’agent pourra de nouveau se connecter avec le jeton qu’il détient déjà.';
bouton.classList.toggle('button-danger', compte.actif);
bouton.classList.toggle('button-secondary', !compte.actif);
}
/** Applique l'état chargé à toutes les lignes présentes. */
function decorerToutesLesLignes() {
corps()?.querySelectorAll('tr[data-agent-id]').forEach(decorerLigne);
}
/**
* Recharge l'état des comptes agents.
*
* L'échec est SILENCIEUX sur le tableau : les boutons restent inertes avec
* leur libellé d'attente, et la page des agents continue de fonctionner. Une
* commodité ne doit pas empêcher de consulter ou de modifier une fiche.
*/
async function refresh() {
if (!corps() || chargement) return;
chargement = true;
try {
const donnees = await api('api/acces_comptes.php?role=AGENT');
comptesParAgent = new Map(
(donnees.comptes || [])
.filter((compte) => compte.id_agent !== null)
.map((compte) => [Number(compte.id_agent), compte])
);
decorerToutesLesLignes();
} catch (erreur) {
message(`État des comptes d’accès indisponible : ${erreur.message}`, 'warning');
} finally {
chargement = false;
}
}
/** Coupe ou rétablit l'accès de l'agent d'une ligne. */
async function basculer(evenement) {
const bouton = evenement.target.closest('.agent-access-toggle');
if (!bouton || bouton.disabled) return;
const ligne = bouton.closest('tr[data-agent-id]');
const compteId = Number(bouton.dataset.compteId);
const actif = bouton.dataset.actif === '1';
const titulaire = ligne?.querySelector('.agent-row-name')?.textContent?.trim() || 'cet agent';
if (actif && !window.confirm(`Couper l’accès de ${titulaire} ? Il ne pourra plus se connecter, et sa session en cours se refermera à sa prochaine action. Son jeton reste valable pour un rétablissement.`)) {
return;
}
bouton.disabled = true;
try {
const donnees = await api('api/acces_compte_statut.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken,
compte_id: compteId,
action: actif ? 'DESACTIVER' : 'ACTIVER',
}),
});
message(donnees.message, 'success');
await refresh();
} catch (erreur) {
message(erreur.message, 'error');
bouton.disabled = false;
}
}
function init() {
// Le module ne sert que sur l'écran des agents : ailleurs, il se retire.
if (!corps()) return;
corps().addEventListener('click', basculer);
refresh();
}
PTA.modules.agentAccess = { init, refresh };
})();