MSIN-4464 — Enhance audit logging functionality for participant management. Introduce new functions to track last touch events and handle cancellation/restoration of participant registrations. Update admin interface to display recent logs and improve oversight of audit activities, ensuring better traceability of changes made to participant records.

This commit is contained in:
2026-07-15 13:04:09 -04:00
parent 1260270c7d
commit c8aa6394f5
6 changed files with 371 additions and 11 deletions

View File

@ -315,6 +315,139 @@ function fxFicheAuditRecordChange(
}
}
/**
* MSIN-4464 — Dernière intervention seule (édition fiche, etc.), sans old→new.
*/
function fxFicheAuditRecordLastTouchOnly($intParId, $strTriggerField, $strSource = '', $intEveId = 0)
{
global $objDatabase;
$intParId = intval($intParId);
$strTriggerField = preg_replace('/[^a-z0-9_]/i', '', (string)$strTriggerField);
if ($strTriggerField === '') {
$strTriggerField = 'fiche';
}
if ($intParId <= 0 || !isset($objDatabase) || !fxFicheAuditTablesReady()) {
return;
}
$arrSettings = fxFicheAuditGetSettings();
if (intval($arrSettings['track_last_touch']) !== 1) {
return;
}
if ($intEveId <= 0) {
$intEveId = intval($objDatabase->fxGetVar(
'SELECT eve_id FROM resultats_participants WHERE par_id = ' . $intParId . ' LIMIT 1'
));
}
$arrActor = fxFicheAuditResolveActor();
fxFicheAuditInsertLogRow(
$intParId,
$intEveId,
'last_touch',
'',
$strTriggerField,
$arrActor,
$strSource
);
fxFicheAuditTrimField($intParId, 'last_touch', $arrSettings);
}
/**
* Après annulation / rétablissement sur une commande (tous les par_id du pec).
*/
function fxFicheAuditRecordCancelRestoreForPec($intPecId, $strOld, $strNew, $strSource, $intEveId = 0)
{
global $objDatabase;
$intPecId = intval($intPecId);
if ($intPecId <= 0 || !isset($objDatabase) || !fxFicheAuditTablesReady()) {
return;
}
$sql = 'SELECT par_id, eve_id FROM resultats_participants WHERE pec_id = ' . $intPecId;
$arrRows = $objDatabase->fxGetResults($sql);
if (!is_array($arrRows)) {
return;
}
foreach ($arrRows as $row) {
if (!is_array($row) || empty($row['par_id'])) {
continue;
}
$intEveRow = intval($row['eve_id'] ?? 0);
if ($intEveRow <= 0) {
$intEveRow = intval($intEveId);
}
fxFicheAuditRecordChange(
intval($row['par_id']),
'is_cancelled',
$strOld,
$strNew,
$strSource,
$intEveRow
);
}
}
/**
* Événements pour le sélecteur admin (actifs récents + ceux déjà dans les logs).
*
* @return array[]
*/
function fxFicheAuditAdminListEvents()
{
global $objDatabase;
if (!isset($objDatabase) || !fxFicheAuditTablesReady()) {
return array();
}
$sql = 'SELECT e.eve_id, e.eve_nom_fr, e.eve_date_debut,'
. ' (SELECT COUNT(*) FROM inscriptions_fiche_audit_log l WHERE l.eve_id = e.eve_id) AS fal_nb'
. ' FROM inscriptions_evenements e'
. ' WHERE e.eve_actif = 1'
. ' OR EXISTS (SELECT 1 FROM inscriptions_fiche_audit_log l2 WHERE l2.eve_id = e.eve_id)'
. ' ORDER BY e.eve_date_debut DESC, e.eve_id DESC'
. ' LIMIT 300';
$arrRows = $objDatabase->fxGetResults($sql);
return is_array($arrRows) ? $arrRows : array();
}
/**
* Derniers logs d'un événement (contrôle admin).
*
* @return array[]
*/
function fxFicheAuditAdminListRecentByEve($intEveId, $intLimit = 100)
{
global $objDatabase;
$intEveId = intval($intEveId);
$intLimit = intval($intLimit);
if ($intLimit !== 200) {
$intLimit = 100;
}
if ($intEveId <= 0 || !isset($objDatabase) || !fxFicheAuditTablesReady()) {
return array();
}
$sql = 'SELECT l.fal_id, l.par_id, l.eve_id, l.field_key, l.old_value, l.new_value,'
. ' l.changed_by_com_id, l.changed_by_label, l.source, l.created_at,'
. ' p.par_prenom, p.par_nom, p.no_bib'
. ' FROM inscriptions_fiche_audit_log l'
. ' LEFT JOIN resultats_participants p ON p.par_id = l.par_id'
. ' WHERE l.eve_id = ' . $intEveId
. ' ORDER BY l.created_at DESC, l.fal_id DESC'
. ' LIMIT ' . $intLimit;
$arrRows = $objDatabase->fxGetResults($sql);
return is_array($arrRows) ? $arrRows : array();
}
/**
* @param array{com_id:int,label:string} $arrActor
*/

View File

@ -310,6 +310,18 @@ function fxAnnulerRetablirInscription($tabEpreuve, $int_pec_id) {
if (!$qryInscription || !$qryParticipants) {
$tabRetour['state'] = 'error';
fxcreer_log("Erreur d'annulation d'inscription : " . $_SESSION['com_info']['com_courriel']);
} else {
// MSIN-4464 — audit annulation
if (!function_exists('fxFicheAuditRecordCancelRestoreForPec')) {
require_once dirname(__FILE__) . '/inc_fx_fiche_audit.php';
}
fxFicheAuditRecordCancelRestoreForPec(
intval($int_pec_id),
'0',
'1',
'annuler',
intval($tabEpreuve['eve_id'] ?? 0)
);
}
} else {
$tabRetour['action'] = 'retablir';
@ -327,6 +339,18 @@ function fxAnnulerRetablirInscription($tabEpreuve, $int_pec_id) {
if (!$qryInscription || !$qryParticipants) {
$tabRetour['state'] = 'error';
fxcreer_log("Erreur d'annulation d'inscription : " . $_SESSION['com_info']['com_courriel']);
} else {
// MSIN-4464 — audit rétablissement
if (!function_exists('fxFicheAuditRecordCancelRestoreForPec')) {
require_once dirname(__FILE__) . '/inc_fx_fiche_audit.php';
}
fxFicheAuditRecordCancelRestoreForPec(
intval($int_pec_id),
'1',
'0',
'retablir',
intval($tabEpreuve['eve_id'] ?? 0)
);
}
// ajuster qte epreuve

View File

@ -797,11 +797,26 @@ LEFT JOIN inscriptions_panier_epreuves ee on e.epr_id=ee.epr_id
$intDiff = ($tabAncienneEpreuve['epr_nb_participants'] - $tabNouvelleEpreuve['epr_nb_participants']);
for ($i = count($tabInscription); $i > $tabNouvelleEpreuve['epr_nb_participants']; $i--) {
$sqlUpdate = "UPDATE resultats_participants SET is_cancelled = 1, par_modification_promoteur = 1 WHERE par_id = " . intval($tabInscription[$i]['par_id']);
$intParCancel = intval($tabInscription[$i]['par_id']);
$sqlUpdate = "UPDATE resultats_participants SET is_cancelled = 1, par_modification_promoteur = 1 WHERE par_id = " . $intParCancel;
$qryUpdate = $objDatabase->fxQuery($sqlUpdate);
if (!$qryUpdate)
if (!$qryUpdate) {
fxcreer_log("Erreur lors du changemenet d'épreuve par le promoteur.");
} else {
// MSIN-4464 — participants surplus annulés au changement d'épreuve
if (!function_exists('fxFicheAuditRecordChange')) {
require_once dirname(__FILE__) . '/inc_fx_fiche_audit.php';
}
fxFicheAuditRecordChange(
$intParCancel,
'is_cancelled',
'0',
'1',
'changement_epreuve',
intval($tabInscription[$i]['eve_id'] ?? ($tabEvenement['general']['eve_id'] ?? 0))
);
}
}
$sqlUpdate = "UPDATE resultats_epreuves_commandees SET pec_equipe = " . $tabNouvelleEpreuve['epr_equipe'] . " WHERE pec_id_original = " . $intpec_id;
@ -921,6 +936,17 @@ LEFT JOIN inscriptions_panier_epreuves ee on e.epr_id=ee.epr_id
if (!$qryUpdate) {
fxcreer_log('Erreur dans la mise à jour des participants.(' . $tabInscription[1]['par_courriel'] . ')');
} elseif ($strTableSqlPart === 'resultats_participants' && $strModMarker !== '') {
// MSIN-4464 — édition fiche promoteur : dernière intervention (pas le détail de chaque champ).
if (!function_exists('fxFicheAuditRecordLastTouchOnly')) {
require_once dirname(__FILE__) . '/inc_fx_fiche_audit.php';
}
fxFicheAuditRecordLastTouchOnly(
intval($intParId),
'fiche',
'fiche_edit',
intval($tabEvenement['general']['eve_id'] ?? 0)
);
}
// catégorie
if ($tabEvenement['general']['eve_categorie_auto'] == 1) {
@ -4094,8 +4120,15 @@ function fxMajEpreuvesCommandees($str_ancien_no_panier, $pec_id_old, $pec_id_new
$sqlUpdate = "UPDATE resultats_participants SET is_cancelled = 1, par_modification_promoteur = 1, par_maj = '" . fxGetDateTime() . "' WHERE pec_id = " . intval($pec_id_old);
$qryUpdateParticipants = $objDatabase->fxQuery($sqlUpdate);
if (!$qryUpdateEpreuve || !$qryUpdateParticipants)
if (!$qryUpdateEpreuve || !$qryUpdateParticipants) {
fxcreer_log("Erreur de désactivation de l'ancienne épreuve lors d'un upgrade ou transfert.");
} else {
// MSIN-4464 — annulation suite upgrade / transfert
if (!function_exists('fxFicheAuditRecordCancelRestoreForPec')) {
require_once dirname(__FILE__) . '/inc_fx_fiche_audit.php';
}
fxFicheAuditRecordCancelRestoreForPec(intval($pec_id_old), '0', '1', 'transfert_upgrade', 0);
}
}
unset($_SESSION['ancien_no_panier']);

View File

@ -1789,6 +1789,11 @@ function fxShowBibNumbersTable($tab_epreuve, $strLangue, $intEveId = 0) {
function fxSetBibNumbers($tab_data, $int_epr_id, $int_start, $int_finish, $int_type, $int_par_equipe, $str_action = '', $int_mode,$int_finish_dispo=999999) {
global $strLangue, $objDatabase, $vDomaine;
// MSIN-4464 — audit dossards batch
if (!function_exists('fxFicheAuditRecordChange')) {
require_once dirname(__FILE__) . '/inc_fx_fiche_audit.php';
}
if ($strLangue == 'fr') {
$strPage = '/compte';
} else {
@ -1902,6 +1907,9 @@ function fxSetBibNumbers($tab_data, $int_epr_id, $int_start, $int_finish, $int_t
for ($i = 1; $i <= count($tabInscriptions); $i++) {
if ($tabInscriptions[$i]['rol_id'] != 3 && $tabInscriptions[$i]['rol_id'] != 4) {
$intParBatch = intval($tabInscriptions[$i]['par_id']);
$strOldBibBatch = (string)($tabInscriptions[$i]['no_bib'] ?? '');
$strNewBibBatch = ($str_action == 'reset') ? '' : (string)($intNoBib + $intCtr);
$sqlUpdate = "UPDATE resultats_participants ";
if ($str_action == 'reset') {
$sqlUpdate .= "SET no_bib = '', par_date_bib = null";
@ -1919,8 +1927,18 @@ function fxSetBibNumbers($tab_data, $int_epr_id, $int_start, $int_finish, $int_t
$sqlUpdate .= " WHERE par_id = " . $tabInscriptions[$i]['par_id'] . " AND is_cancelled = 0";
$qryUpdate = $objDatabase->fxQuery($sqlUpdate);
if (!$qryUpdate)
if (!$qryUpdate) {
fxcreer_log('Erreur lors de mise a jour du numero de dossard du participant ' . $tabInscriptions[$i]['par_id']);
} else {
fxFicheAuditRecordChange(
$intParBatch,
'no_bib',
$strOldBibBatch,
$strNewBibBatch,
($str_action == 'reset') ? 'bib_batch_reset' : 'bib_batch',
intval($tabInscriptions[$i]['eve_id'] ?? 0)
);
}
$sqlUpdate = "UPDATE resultats_epreuves_commandees SET no_equipe = '" . $objDatabase->fxEscape($intNoBib + $intCtr) . "' WHERE epr_id = " . intval($tabInscriptions[$i]['epr_id']) . " AND pec_id_original = " . $tabInscriptions[$i]['pec_id'];
$qryUpdate = $objDatabase->fxQuery($sqlUpdate);
@ -1967,16 +1985,29 @@ function fxSetBibNumbers($tab_data, $int_epr_id, $int_start, $int_finish, $int_t
} else{
$intNoBib = $strNumeroEquipe;
}
$intParEqBatch = intval($tabEquipe[$j]['par_id']);
$strOldBibEq = (string)($tabEquipe[$j]['no_bib'] ?? '');
$strNewBibEq = (string)$intNoBib;
if($tabInscriptions[$i]['par_date_bib'] == null){
$sqlUpdatePart = "UPDATE resultats_participants SET par_date_bib = '" . fxGetDateTime() . "', no_bib = '" . $objDatabase->fxEscape($intNoBib). "' WHERE par_id = " . intval($tabEquipe[$j]['par_id']);
$sqlUpdatePart = "UPDATE resultats_participants SET par_date_bib = '" . fxGetDateTime() . "', no_bib = '" . $objDatabase->fxEscape($intNoBib). "' WHERE par_id = " . $intParEqBatch;
} else{
$sqlUpdatePart = "UPDATE resultats_participants SET no_bib = '" . $objDatabase->fxEscape($intNoBib) . "' WHERE par_id = " . intval($tabEquipe[$j]['par_id']);
$sqlUpdatePart = "UPDATE resultats_participants SET no_bib = '" . $objDatabase->fxEscape($intNoBib) . "' WHERE par_id = " . $intParEqBatch;
// $sqlUpdatePart = "UPDATE resultats_participants SET par_maj = '" . fxGetDateTime() . "', no_bib = '" . $objDatabase->fxEscape($intNoBib) . "' WHERE par_id = " . intval($tabEquipe[$j]['par_id']);
// $sqlUpdatePart = "UPDATE resultats_participants SET par_maj = '" . fxGetDateTime() . "', par_modification_promoteur = 1, no_bib = '" . $objDatabase->fxEscape($intNoBib) . "' WHERE par_id = " . intval($tabEquipe[$j]['par_id']);
}
$qryUpdatePart = $objDatabase->fxQuery($sqlUpdatePart);
if (!$qryUpdatePart)
if (!$qryUpdatePart) {
fxcreer_log('Erreur lors de mise a jour du numero de dossard du participant ' . $tabInscriptions[$i]['par_id']);
} else {
fxFicheAuditRecordChange(
$intParEqBatch,
'no_bib',
$strOldBibEq,
$strNewBibEq,
'bib_batch',
intval($tabEquipe[$j]['eve_id'] ?? 0)
);
}
}
}
$intCtr++;

View File

@ -47,7 +47,10 @@ include('inc_header.php');
<div class="col-md-9 col-lg-10 order-first order-md-last py-3">
<?php
require_once dirname(__FILE__) . '/../php/inc_fx_superadm_v2_ui.php';
fxSuperadmV2PanelOpen('Traçabilité fiche — Champs & rétention', array(
$strV2Title = !empty($_GET['voir_logs'])
? 'Traçabilité fiche — Logs'
: 'Traçabilité fiche — Champs & rétention';
fxSuperadmV2PanelOpen($strV2Title, array(
'zone_class' => 'superadm-content-v2-zone superadm-content-v2-zone--flush',
));
fxFicheAuditAdminRenderPage($strFlash, $strError);

View File

@ -16,6 +16,11 @@ function fxFicheAuditAdminRenderPage($strFlash = '', $strError = '')
$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>';
@ -35,7 +40,7 @@ function fxFicheAuditAdminRenderPage($strFlash = '', $strError = '')
<?php if (!$blnCanEdit) { ?>
<div class="alert alert-info py-2">
Lecture seule : vous pouvez consulter la configuration. Modification réservée.
Lecture seule : vous pouvez consulter la configuration et les logs. Modification réservée.
</div>
<?php } ?>
@ -50,8 +55,9 @@ function fxFicheAuditAdminRenderPage($strFlash = '', $strError = '')
<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">
L'enregistrement est actif pour statut, dossard et dossard remis (selon whitelist / dernière intervention).
L'affichage « ? » sur la fiche et le droit promoteur associé viennent dans une étape suivante.
Points branchés : statut, dossard manuel, dossard remis, annulation/rétablissement, édition fiche,
batch dossards, surplus changement d'épreuve, transfert/upgrade.
L'affichage « ? » sur la fiche et le droit promoteur associé viennent ensuite.
</p>
</div>
</div>
@ -136,5 +142,135 @@ function fxFicheAuditAdminRenderPage($strFlash = '', $strError = '')
</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" class="form-row align-items-end">
<input type="hidden" name="voir_logs" value="1">
<div class="form-group col-md-6 mb-2">
<label for="eve_id_logs">Événement</label>
<select class="form-control" id="eve_id_logs" name="eve_id" required>
<option value="">— choisir —</option>
<?php foreach ($arrEvents as $arrEve) {
$intEveOpt = intval($arrEve['eve_id'] ?? 0);
if ($intEveOpt <= 0) {
continue;
}
$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)' : '');
?>
<option value="<?= $intEveOpt ?>"<?= $intEveLogs === $intEveOpt ? ' selected' : '' ?>>
<?= htmlspecialchars($strLabel, ENT_QUOTES, 'UTF-8') ?>
</option>
<?php } ?>
</select>
</div>
<div class="form-group col-md-6 mb-2">
<button type="submit" name="limit" value="100" class="btn btn-outline-primary mr-2">
100 derniers
</button>
<button type="submit" name="limit" value="200" class="btn btn-outline-primary">
200 derniers
</button>
</div>
</form>
<?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
}