update
This commit is contained in:
345
assets/js/access-tokens.js
Normal file
345
assets/js/access-tokens.js
Normal file
@@ -0,0 +1,345 @@
|
||||
/**
|
||||
* 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) => `
|
||||
<div class="dsi-token-row">
|
||||
<div class="dsi-token-identity">
|
||||
<strong>${escapeHtml(entree.libelle)}</strong>
|
||||
<small>${escapeHtml(LIBELLES_ROLE[entree.role] || entree.role)}</small>
|
||||
</div>
|
||||
<code class="dsi-token-value">${escapeHtml(entree.jeton)}</code>
|
||||
</div>
|
||||
`).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 = [
|
||||
`<strong>${escapeHtml(compte.identite_libelle)}</strong>`,
|
||||
compte.matricule ? `<br><small>Matricule ${escapeHtml(compte.matricule)}</small>` : '',
|
||||
compte.commentaire ? `<br><small>${escapeHtml(compte.commentaire)}</small>` : '',
|
||||
].join('');
|
||||
|
||||
const etats = [];
|
||||
if (!compte.actif) etats.push('<span class="dsi-state dsi-state-off">Désactivé</span>');
|
||||
else if (!compte.identite_active) etats.push('<span class="dsi-state dsi-state-off">Identité inactive</span>');
|
||||
else etats.push('<span class="dsi-state dsi-state-on">Actif</span>');
|
||||
|
||||
const connexions = compte.nombre_connexions > 0
|
||||
? `<br><small>${compte.nombre_connexions} connexion(s)</small>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<tr data-compte-id="${Number(compte.id_compte)}">
|
||||
<td>${identite}</td>
|
||||
<td>${escapeHtml(LIBELLES_ROLE[compte.role] || compte.role)}</td>
|
||||
<td><code>${escapeHtml(compte.jeton_prefixe)}…</code></td>
|
||||
<td>${formatDateHeure(compte.date_derniere_connexion)}${connexions}</td>
|
||||
<td>${etats.join(' ')}</td>
|
||||
<td class="dsi-actions-cell">
|
||||
<button type="button" class="button button-secondary access-regenerate">Régénérer</button>
|
||||
<button type="button" class="button button-secondary access-toggle">${compte.actif ? 'Désactiver' : 'Réactiver'}</button>
|
||||
<button type="button" class="button button-danger access-delete">Supprimer</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = '<option value="">Choisir un agent sans compte</option>'
|
||||
+ agents.map((agent) => `<option value="${Number(agent.id_agent)}">${escapeHtml(agent.libelle)}</option>`).join('');
|
||||
}
|
||||
if (selectLieu) {
|
||||
selectLieu.innerHTML = '<option value="">Choisir un lieu sans compte</option>'
|
||||
+ lieux.map((lieu) => `<option value="${Number(lieu.id_structure)}">${escapeHtml(lieu.libelle)}</option>`).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('')
|
||||
: '<tr><td colspan="6">Aucun compte ne correspond à ces filtres.</td></tr>';
|
||||
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 };
|
||||
})();
|
||||
@@ -252,7 +252,10 @@
|
||||
<td class="agent-row-phone">${escapeHtml(agent.telephone || 'Non renseigné')}</td>
|
||||
<td class="agent-row-contract-start">${escapeHtml(formatContractDate(agent.date_debut_contrat))}</td>
|
||||
<td class="agent-row-structure">${escapeHtml(structureLabel)}</td>
|
||||
<td><button type="button" class="button button-secondary edit-agent-profile">Modifier</button></td>
|
||||
<td class="agent-row-actions">
|
||||
<button type="button" class="button button-secondary edit-agent-profile">Modifier</button>
|
||||
<button type="button" class="button button-secondary agent-access-toggle" disabled>Compte d’accès…</button>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -301,6 +304,10 @@
|
||||
setAgentRowData(row, agent);
|
||||
row.innerHTML = agentRowHtml(agent);
|
||||
body.appendChild(row);
|
||||
// L'agent vient de naître : il n'a pas encore de compte d'accès. Le
|
||||
// module le dira sur la ligne, plutôt que d'y laisser un bouton
|
||||
// inerte au libellé provisoire.
|
||||
PTA.modules.agentAccess?.refresh?.();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
158
assets/js/agent-access.js
Normal file
158
assets/js/agent-access.js
Normal 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 };
|
||||
})();
|
||||
@@ -54,7 +54,18 @@
|
||||
}
|
||||
if (params.agentId && agentSelect) {
|
||||
agentSelect.value = String(params.agentId);
|
||||
if (weekInput) weekInput.disabled = false;
|
||||
// Depuis la V7.29, un agent dont le compte d'accès est coupé sort des
|
||||
// listes d'affectation. Un lien profond qui le désigne (file de
|
||||
// validation, tableau de bord, vue annuelle) ne trouve alors aucune
|
||||
// option : sans ce message, l'écran resterait vide sans rien dire.
|
||||
if (agentSelect.value !== String(params.agentId)) {
|
||||
PTA.utils.showMessage(
|
||||
'Cet agent n’est plus proposé à la saisie : son accès a été coupé. Rétablissez-le depuis l’onglet Agents pour reprendre son planning.',
|
||||
'warning'
|
||||
);
|
||||
} else if (weekInput) {
|
||||
weekInput.disabled = false;
|
||||
}
|
||||
}
|
||||
if (params.week && weekInput) {
|
||||
weekInput.disabled = !agentSelect?.value;
|
||||
|
||||
@@ -3394,3 +3394,17 @@ input.field-invalid {
|
||||
input.field-valid {
|
||||
border-color: #1b7f3b;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
V7.28 - Actions de ligne sur la fiche des agents
|
||||
============================================================ */
|
||||
.agent-row-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.agent-row-actions .button {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user