387 lines
19 KiB
PHP
387 lines
19 KiB
PHP
<?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
|
||
}
|