Files
ms1inscription-v5/superadm/php/inc_fx_fiche_audit_admin.php

387 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* MSIN-4464 — Super admin : gestion whitelist + rétention traçabilité fiche.
*/
require_once dirname(__DIR__, 2) . '/php/inc_fx_fiche_audit.php';
/**
* @param string $strFlash
* @param string $strError
*/
function fxFicheAuditAdminRenderPage($strFlash = '', $strError = '')
{
$blnCanEdit = fxAdminCanEditFicheAudit();
$blnTablesOk = fxFicheAuditTablesReady();
$arrSettings = fxFicheAuditGetSettings();
$arrFields = fxFicheAuditGetFieldsForAdmin();
$strDisabled = ($blnCanEdit && $blnTablesOk) ? '' : ' disabled';
$intEveLogs = isset($_GET['eve_id']) ? intval($_GET['eve_id']) : 0;
$intLimitLogs = (isset($_GET['limit']) && intval($_GET['limit']) === 200) ? 200 : 100;
$blnShowLogs = ($blnTablesOk && $intEveLogs > 0 && isset($_GET['voir_logs']));
$arrEvents = $blnTablesOk ? fxFicheAuditAdminListEvents() : array();
$arrLogs = $blnShowLogs ? fxFicheAuditAdminListRecentByEve($intEveLogs, $intLimitLogs) : array();
if ($strFlash !== '') {
echo '<div class="alert alert-success">' . htmlspecialchars($strFlash, ENT_QUOTES, 'UTF-8') . '</div>';
}
if ($strError !== '') {
echo '<div class="alert alert-danger">' . htmlspecialchars($strError, ENT_QUOTES, 'UTF-8') . '</div>';
}
if (!$blnTablesOk) {
echo '<div class="alert alert-warning">'
. 'Tables absentes. Exécuter <code>sql/MSIN-4464-fiche-audit-config.sql</code> puis recharger.'
. '</div>';
}
?>
<div class="d-flex justify-content-between align-items-center mb-3">
<h1 class="mb-0">Traçabilité fiche</h1>
</div>
<?php if (!$blnCanEdit) { ?>
<div class="alert alert-info py-2">
Lecture seule : vous pouvez consulter la configuration et les logs. Modification réservée.
</div>
<?php } ?>
<div class="card mb-3 border-info">
<div class="card-header py-2 bg-light">
<strong><i class="fa fa-info-circle text-info"></i> Comment ça marche ?</strong>
</div>
<div class="card-body py-3 small">
<ul class="mb-2 pl-3">
<li><strong>Whitelist</strong> — seuls les champs cochés seront historisés en détail (ancienne → nouvelle valeur + personne).</li>
<li><strong>Dernière intervention</strong> — si activée, on note aussi qui a touché la fiche, même pour un champ hors whitelist.</li>
<li><strong>Rétention</strong> — on ne garde pas un log sans fin : durée max + nombre max d'entrées par champ × participant.</li>
</ul>
<p class="mb-0 text-muted">
Points branchés : statut, dossard manuel, dossard remis, annulation/rétablissement, édition fiche,
batch dossards, surplus changement d'épreuve, transfert/upgrade.
Sur la fiche : droit kit <code>inscriptions_gestion.audit_view</code> → « ! » si au moins 1 log
(pop-up limité au N de la rétention).
</p>
</div>
</div>
<form method="post" action="fiche_audit.php"<?= ($blnCanEdit && $blnTablesOk) ? '' : ' onsubmit="return false;"' ?>>
<input type="hidden" name="action" value="save_fiche_audit">
<div class="card mb-3">
<div class="card-header py-2">
<strong>Rétention</strong>
</div>
<div class="card-body">
<div class="form-row">
<div class="form-group col-md-4">
<label for="retention_days">Conserver pendant (jours)</label>
<input type="number" class="form-control" id="retention_days" name="retention_days"
min="1" max="3650" value="<?= intval($arrSettings['retention_days']) ?>"<?= $strDisabled ?>>
</div>
<div class="form-group col-md-4">
<label for="retention_max_per_field">Max d'entrées / champ / participant</label>
<input type="number" class="form-control" id="retention_max_per_field" name="retention_max_per_field"
min="1" max="100" value="<?= intval($arrSettings['retention_max_per_field']) ?>"<?= $strDisabled ?>>
</div>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="track_last_touch" name="track_last_touch" value="1"
<?= intval($arrSettings['track_last_touch']) === 1 ? ' checked' : '' ?><?= $strDisabled ?>>
<label class="custom-control-label" for="track_last_touch">
Noter aussi la dernière personne qui a touché la fiche (sans détail de champ)
</label>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header py-2">
<strong>Whitelist — champs à tracer en détail</strong>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<thead class="thead-light">
<tr>
<th style="width:4rem;">Actif</th>
<th>Champ</th>
<th>Clé technique</th>
</tr>
</thead>
<tbody>
<?php foreach ($arrFields as $arrField) {
$strKey = (string)$arrField['field_key'];
$strId = 'field_' . preg_replace('/[^a-z0-9_]/i', '_', $strKey);
$blnActif = (intval($arrField['field_actif']) === 1);
?>
<tr>
<td class="text-center align-middle">
<input type="checkbox" id="<?= htmlspecialchars($strId, ENT_QUOTES, 'UTF-8') ?>"
name="field_actif[]"
value="<?= htmlspecialchars($strKey, ENT_QUOTES, 'UTF-8') ?>"
<?= $blnActif ? ' checked' : '' ?><?= $strDisabled ?>>
</td>
<td>
<label class="mb-0" for="<?= htmlspecialchars($strId, ENT_QUOTES, 'UTF-8') ?>">
<?= htmlspecialchars($arrField['field_label_fr'], ENT_QUOTES, 'UTF-8') ?>
</label>
<div class="text-muted small">
<?= htmlspecialchars($arrField['field_label_en'], ENT_QUOTES, 'UTF-8') ?>
</div>
</td>
<td class="align-middle"><code><?= htmlspecialchars($strKey, ENT_QUOTES, 'UTF-8') ?></code></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
<?php if ($blnCanEdit && $blnTablesOk) { ?>
<button type="submit" class="btn btn-primary">
<i class="fa fa-save" aria-hidden="true"></i> Enregistrer
</button>
<?php } ?>
</form>
<hr class="my-4">
<div class="card mb-3">
<div class="card-header py-2">
<strong><i class="fa fa-list" aria-hidden="true"></i> Contrôle des logs (admin)</strong>
</div>
<div class="card-body">
<p class="small text-muted mb-3">
Affiche les dernières entrées enregistrées pour un événement — utile pour valider que ça logue bien.
</p>
<?php if (!$blnTablesOk) { ?>
<p class="text-muted mb-0">Tables absentes — impossible d'afficher les logs.</p>
<?php } else { ?>
<form method="get" action="fiche_audit.php" id="form-fiche-audit-logs">
<input type="hidden" name="voir_logs" value="1">
<input type="hidden" name="eve_id" id="eve_id_hidden" value="<?= $intEveLogs > 0 ? intval($intEveLogs) : '' ?>">
<div class="form-group mb-2">
<label for="eve_search">1) Rechercher / choisir un événement</label>
<div class="form-row">
<div class="col-md-8 mb-2">
<input type="search" class="form-control" id="eve_search" autocomplete="off"
placeholder="Nom ou #id… (ex. marathon, 1234)">
</div>
<div class="col-md-4 mb-2">
<input type="number" class="form-control" id="eve_id_direct"
min="1" step="1" placeholder="ou # eve_id direct"
value="<?= $intEveLogs > 0 ? intval($intEveLogs) : '' ?>">
</div>
</div>
<select class="form-control" id="eve_id_logs" size="8" style="min-height:10rem;">
<?php
$intEveCount = 0;
foreach ($arrEvents as $arrEve) {
$intEveOpt = intval($arrEve['eve_id'] ?? 0);
if ($intEveOpt <= 0) {
continue;
}
$intEveCount++;
$strNomEve = trim((string)($arrEve['eve_nom_fr'] ?? ''));
if ($strNomEve === '') {
$strNomEve = 'Événement #' . $intEveOpt;
}
$intNb = intval($arrEve['fal_nb'] ?? 0);
$strDate = trim((string)($arrEve['eve_date_debut'] ?? ''));
$strLabel = $strNomEve
. ($strDate !== '' && $strDate !== '0000-00-00' ? ' (' . $strDate . ')' : '')
. ' — #' . $intEveOpt
. ($intNb > 0 ? ' · ' . $intNb . ' log(s)' : '');
$strHay = mb_strtolower($strNomEve . ' ' . $intEveOpt, 'UTF-8');
?>
<option value="<?= $intEveOpt ?>"
data-search="<?= htmlspecialchars($strHay, ENT_QUOTES, 'UTF-8') ?>"
<?= $intEveLogs === $intEveOpt ? ' selected' : '' ?>>
<?= htmlspecialchars($strLabel, ENT_QUOTES, 'UTF-8') ?>
</option>
<?php } ?>
</select>
<small class="form-text text-muted">
<?= intval($intEveCount) ?> événement(s) — filtre, clique une ligne, puis bouton ci-dessous.
</small>
</div>
<div class="form-group mb-0 p-3 border rounded bg-light">
<label class="d-block font-weight-bold mb-2">2) Afficher les logs</label>
<div class="d-flex flex-wrap align-items-center">
<button type="submit" name="limit" value="100" class="btn btn-primary btn-lg mr-2 mb-2"
id="btn-voir-logs-100">
<i class="fa fa-eye" aria-hidden="true"></i> Voir les logs (100)
</button>
<button type="submit" name="limit" value="200" class="btn btn-outline-primary btn-lg mb-2"
id="btn-voir-logs-200">
Voir 200
</button>
</div>
</div>
</form>
<script>
(function () {
function boot() {
var sel = document.getElementById('eve_id_logs');
var search = document.getElementById('eve_search');
var direct = document.getElementById('eve_id_direct');
var hidden = document.getElementById('eve_id_hidden');
var form = document.getElementById('form-fiche-audit-logs');
if (!sel || !search || !direct || !hidden || !form) { return; }
function setEveId(n) {
n = parseInt(n, 10) || 0;
hidden.value = n > 0 ? String(n) : '';
if (n > 0) {
direct.value = String(n);
sel.value = String(n);
}
}
function filterList() {
var q = (search.value || '').toLowerCase().trim();
var opts = sel.options;
var firstVisible = null;
for (var i = 0; i < opts.length; i++) {
var hay = (opts[i].getAttribute('data-search') || opts[i].text || '').toLowerCase();
var show = (q === '' || hay.indexOf(q) !== -1);
opts[i].hidden = !show;
opts[i].disabled = !show;
if (show && firstVisible === null) {
firstVisible = opts[i];
}
}
if (q !== '' && firstVisible && (!sel.value || sel.options[sel.selectedIndex].hidden)) {
firstVisible.selected = true;
setEveId(firstVisible.value);
}
}
search.addEventListener('input', filterList);
search.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
filterList();
if (sel.value) {
setEveId(sel.value);
}
}
});
sel.addEventListener('change', function () {
setEveId(sel.value);
});
sel.addEventListener('dblclick', function () {
if (sel.value) {
setEveId(sel.value);
var btn = document.getElementById('btn-voir-logs-100');
if (btn) { btn.click(); }
}
});
direct.addEventListener('input', function () {
setEveId(direct.value);
});
form.addEventListener('submit', function (e) {
var n = parseInt(direct.value, 10) || parseInt(sel.value, 10) || 0;
setEveId(n);
if (n <= 0) {
e.preventDefault();
search.focus();
alert('Choisis un événement (liste ou # eve_id).');
}
});
if (hidden.value) {
setEveId(hidden.value);
}
filterList();
search.focus();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
})();
</script>
<?php } ?>
</div>
</div>
<?php if ($blnShowLogs) {
fxFicheAuditAdminRenderLogsTable($intEveLogs, $intLimitLogs, $arrLogs);
}
}
/**
* @param array[] $arrLogs
*/
function fxFicheAuditAdminRenderLogsTable($intEveId, $intLimit, $arrLogs)
{
$intEveId = intval($intEveId);
$intLimit = intval($intLimit);
$intCount = is_array($arrLogs) ? count($arrLogs) : 0;
?>
<div class="card mb-3" id="fiche-audit-logs">
<div class="card-header py-2 d-flex justify-content-between align-items-center">
<strong>
Logs — événement #<?= $intEveId ?>
<span class="text-muted font-weight-normal">(<?= $intCount ?> / <?= $intLimit ?> max)</span>
</strong>
<a class="btn btn-sm btn-outline-secondary" href="fiche_audit.php">Fermer</a>
</div>
<div class="card-body p-0">
<?php if ($intCount === 0) { ?>
<p class="p-3 mb-0 text-muted">Aucun log pour cet événement.</p>
<?php } else { ?>
<div class="table-responsive">
<table class="table table-sm table-striped table-hover mb-0 small">
<thead class="thead-light">
<tr>
<th>Quand</th>
<th>Participant</th>
<th>Champ</th>
<th>Avant</th>
<th>Après</th>
<th>Par</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<?php foreach ($arrLogs as $row) {
if (!is_array($row)) {
continue;
}
$strNom = trim(
(string)($row['par_prenom'] ?? '') . ' ' . (string)($row['par_nom'] ?? '')
);
if ($strNom === '') {
$strNom = 'par#' . intval($row['par_id'] ?? 0);
}
$strBib = trim((string)($row['no_bib'] ?? ''));
if ($strBib !== '') {
$strNom .= ' · #' . $strBib;
}
$strField = (string)($row['field_key'] ?? '');
if ($strField === 'last_touch') {
$strField = 'dernière intervention';
}
?>
<tr>
<td class="text-nowrap"><?= htmlspecialchars((string)($row['created_at'] ?? ''), ENT_QUOTES, 'UTF-8') ?></td>
<td><?= htmlspecialchars($strNom, ENT_QUOTES, 'UTF-8') ?></td>
<td><code><?= htmlspecialchars($strField, ENT_QUOTES, 'UTF-8') ?></code></td>
<td><?= htmlspecialchars((string)($row['old_value'] ?? ''), ENT_QUOTES, 'UTF-8') ?></td>
<td><?= htmlspecialchars((string)($row['new_value'] ?? ''), ENT_QUOTES, 'UTF-8') ?></td>
<td><?= htmlspecialchars((string)($row['changed_by_label'] ?? ''), ENT_QUOTES, 'UTF-8') ?></td>
<td><code><?= htmlspecialchars((string)($row['source'] ?? ''), ENT_QUOTES, 'UTF-8') ?></code></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } ?>
</div>
</div>
<?php
}