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

View File

@@ -221,7 +221,37 @@ final class AgentModel extends BaseModel
SQL;
/**
* Tous les agents actifs.
* Retire des listes D'AFFECTATION les agents dont le COMPTE D'ACCÈS est
* désactivé (V7.29).
*
* Trois précisions, toutes portées par le `NOT EXISTS` :
*
* - un agent SANS compte reste proposé. Il n'a simplement pas encore reçu
* son jeton, ce qui est l'état normal avant distribution : le filtrer
* viderait les listes d'une installation neuve ;
* - le filtre porte sur `compte_acces.actif`, PAS sur `agent.actif`, qui
* est déjà exigé par ailleurs. Ce sont deux décisions distinctes :
* fermer une fiche d'agent et couper un accès ;
* - il ne s'applique QU'AUX listes où l'on affecte. Les écrans de
* consultation (planning par agent, vue annuelle, PTA détaillé) et le
* référentiel « Agents » continuent de montrer l'agent — sans quoi on ne
* pourrait plus ni lire son année passée, ni lui rétablir son accès.
*
* La table compte_acces est garantie présente : SchemaModel::assertCurrent()
* refuse de démarrer si les vues de comptes manquent.
*/
private const SANS_COMPTE_COUPE = <<<'SQL'
AND NOT EXISTS (
SELECT 1 FROM compte_acces ca
WHERE ca.id_agent = a.id_agent AND ca.actif = 0
)
SQL;
/**
* Tous les agents actifs, y compris ceux dont l'accès est coupé.
*
* Liste de RÉFÉRENTIEL et de CONSULTATION. Pour une liste où l'on affecte,
* voir allSelectable().
*
* @return array<int, array<string, mixed>>
*/
@@ -232,6 +262,19 @@ final class AgentModel extends BaseModel
)->fetchAll();
}
/**
* Agents proposables dans une liste d'affectation : actifs ET dont le compte
* d'accès n'a pas été coupé.
*
* @return array<int, array<string, mixed>>
*/
public function allSelectable(): array
{
return $this->pdo->query(
self::AGENT_SELECT . " WHERE a.actif = TRUE" . self::SANS_COMPTE_COUPE . " ORDER BY a.nom, a.prenom"
)->fetchAll();
}
/**
* Agents visibles depuis un lieu : ceux qui y sont RATTACHÉS, plus ceux qui
* y ont un planning, quel que soit leur rattachement.
@@ -328,7 +371,7 @@ final class AgentModel extends BaseModel
FROM agent a
LEFT JOIN poste_agent po ON po.id_poste = a.id_poste
LEFT JOIN structure s ON s.id_structure = a.id_structure
WHERE a.actif = TRUE
WHERE a.actif = TRUE" . self::SANS_COMPTE_COUPE . "
ORDER BY lieu_principal_selectionne DESC, a.nom, a.prenom"
);
$stmt->execute(['selected_location' => $selectedLocation]);

View File

@@ -125,9 +125,16 @@ final class CompteModel extends BaseModel
}
/**
* Liste des comptes pour la page DSI.
* Liste des comptes pour la page DSI et pour la page « Jetons d'accès » du
* Service Enfance.
*
* @param array<string, mixed> $filtres role, actif (0|1), q (texte libre).
* `roles` BORNE la liste à un ensemble de rôles. C'est ce qui permet à la
* délégation V7.27 de ne jamais laisser voir un compte privilégié : le
* contrôleur passe sa liste blanche, et aucun filtre d'interface ne peut
* l'élargir — `role` ne fait que restreindre à l'intérieur de `roles`.
*
* @param array<string, mixed> $filtres role, actif (0|1), q (texte libre),
* roles (array<int, string> facultatif).
*
* @return array<int, array<string, mixed>>
*/
@@ -136,10 +143,27 @@ final class CompteModel extends BaseModel
$conditions = [];
$params = [];
$rolesAutorises = array_values(array_filter(array_map(
static fn (mixed $role): string => strtoupper(trim((string) $role)),
(array) ($filtres['roles'] ?? [])
)));
if ($rolesAutorises !== []) {
$marqueurs = [];
foreach ($rolesAutorises as $index => $roleAutorise) {
$marqueurs[] = ':role_autorise_' . $index;
$params['role_autorise_' . $index] = $roleAutorise;
}
$conditions[] = 'role IN (' . implode(', ', $marqueurs) . ')';
}
$role = (string) ($filtres['role'] ?? '');
if ($role !== '') {
// Le filtre d'interface ne peut que restreindre : un rôle hors de la
// liste blanche ne rend rien, il ne la contourne pas.
if ($role !== '' && ($rolesAutorises === [] || in_array(strtoupper($role), $rolesAutorises, true))) {
$conditions[] = 'role = :role';
$params['role'] = $role;
} elseif ($role !== '') {
$conditions[] = '1 = 0';
}
if (isset($filtres['actif']) && $filtres['actif'] !== '') {
@@ -149,8 +173,18 @@ final class CompteModel extends BaseModel
$recherche = trim((string) ($filtres['q'] ?? ''));
if ($recherche !== '') {
$conditions[] = '(identite_libelle LIKE :q OR libelle LIKE :q OR matricule LIKE :q OR jeton_prefixe LIKE :q)';
$params['q'] = '%' . $recherche . '%';
// UN MARQUEUR PAR COLONNE, et non un seul répété quatre fois : PDO
// tourne ici sans émulation de requêtes préparées (ATTR_EMULATE_
// PREPARES = 0), et MariaDB refuse alors un même paramètre nommé
// employé plusieurs fois — « Invalid parameter number ». Répéter
// « :q » faisait échouer toute recherche en 500.
$colonnes = ['identite_libelle', 'libelle', 'matricule', 'jeton_prefixe'];
$comparaisons = [];
foreach ($colonnes as $index => $colonne) {
$comparaisons[] = $colonne . ' LIKE :q_' . $index;
$params['q_' . $index] = '%' . $recherche . '%';
}
$conditions[] = '(' . implode(' OR ', $comparaisons) . ')';
}
$where = $conditions === [] ? '' : ' WHERE ' . implode(' AND ', $conditions);