diff --git a/ajax_inscr_gestion.php b/ajax_inscr_gestion.php
index 561ca1b..d000b34 100644
--- a/ajax_inscr_gestion.php
+++ b/ajax_inscr_gestion.php
@@ -280,6 +280,43 @@ switch ($strAction) {
$tabRetour = array('state' => 'ok', 'html' => $strHtml);
break;
+ case 'fiche_audit_history':
+ // MSIN-4464 — derniers changements d'un champ whitelist (popup « ! » fiche).
+ require_once 'php/inc_fx_fiche_audit.php';
+ $intParId = intval($_REQUEST['par_id'] ?? 0);
+ $strField = preg_replace('/[^a-z0-9_]/i', '', (string)($_REQUEST['field'] ?? ''));
+ $intEveId = fxEveAccesResolveEveIdFromParId($intParId);
+
+ if ($intParId <= 0 || $strField === '' || $intEveId <= 0
+ || !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.audit_view')) {
+ $tabRetour = array('state' => 'error', 'message' => 'unauthorized');
+ break;
+ }
+
+ $arrItems = fxFicheAuditGetRecentForField($intParId, $strField);
+ $arrOut = array();
+ if (is_array($arrItems)) {
+ foreach ($arrItems as $row) {
+ if (!is_array($row)) {
+ continue;
+ }
+ $arrOut[] = array(
+ 'when' => (string)($row['created_at'] ?? ''),
+ 'who' => (string)($row['changed_by_label'] ?? ''),
+ 'old' => (string)($row['old_value'] ?? ''),
+ 'new' => (string)($row['new_value'] ?? ''),
+ 'source' => (string)($row['source'] ?? ''),
+ );
+ }
+ }
+
+ $tabRetour = array(
+ 'state' => 'success',
+ 'field' => $strField,
+ 'items' => $arrOut,
+ );
+ break;
+
default:
$tabRetour = array('state' => 'error', 'message' => 'invalid_action');
break;
diff --git a/css/style.css b/css/style.css
index 4366d6d..f99ea58 100644
--- a/css/style.css
+++ b/css/style.css
@@ -4468,6 +4468,36 @@ button.inscr-gestion-list-remis:hover{
margin-bottom:8px;
}
+/* MSIN-4464 — historique champ ( ! ) sur la fiche */
+.inscr-gestion-audit-bang{
+ display:inline-block;
+ margin-left:6px;
+ padding:0 7px;
+ min-width:22px;
+ height:22px;
+ line-height:20px;
+ border:1px solid #c0392b;
+ border-radius:4px;
+ background:#fff5f5;
+ color:#c0392b;
+ font-weight:800;
+ font-size:14px;
+ vertical-align:middle;
+ cursor:pointer;
+}
+.inscr-gestion-audit-bang:hover,
+.inscr-gestion-audit-bang:focus{
+ background:#c0392b;
+ color:#fff;
+ outline:none;
+}
+.inscr-gestion-audit-inline{
+ display:inline-flex;
+ align-items:center;
+ font-size:13px;
+ color:#495057;
+}
+
.inscr-gestion-order-row{
display:flex;
flex-wrap:wrap;
diff --git a/js/v2/inscr-gestion.js b/js/v2/inscr-gestion.js
index c501c7c..bb394a9 100644
--- a/js/v2/inscr-gestion.js
+++ b/js/v2/inscr-gestion.js
@@ -255,6 +255,103 @@
});
})();
+ // MSIN-4464 — « ! » historique champ (fiche seulement).
+ (function () {
+ function auditFieldLabel(fieldKey) {
+ var map = {
+ par_statut_course: t('auditFieldStatut'),
+ no_bib: t('auditFieldBib'),
+ no_bib_remis: t('auditFieldRemis'),
+ is_cancelled: t('auditFieldCancel')
+ };
+ return map[fieldKey] || fieldKey;
+ }
+
+ function escHtml(str) {
+ return String(str == null ? '' : str)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+ }
+
+ function renderAuditTable(items) {
+ if (!items || !items.length) {
+ return '
' + escHtml(t('auditPopupEmpty') || '') + '
';
+ }
+ var html = '';
+ html += ''
+ + '| ' + escHtml(t('auditColWhen') || '') + ' | '
+ + '' + escHtml(t('auditColWho') || '') + ' | '
+ + '' + escHtml(t('auditColFrom') || '') + ' | '
+ + '' + escHtml(t('auditColTo') || '') + ' | '
+ + '
';
+ for (var i = 0; i < items.length; i++) {
+ var row = items[i] || {};
+ html += ''
+ + '| ' + escHtml(row.when || '') + ' | '
+ + '' + escHtml(row.who || '') + ' | '
+ + '' + escHtml(row.old || '') + ' | '
+ + '' + escHtml(row.new || '') + ' | '
+ + '
';
+ }
+ html += '
';
+ return html;
+ }
+
+ $body.on('click', '.inscr-gestion-audit-bang', function (e) {
+ e.preventDefault();
+ var $btn = $(this);
+ var intParId = parseInt($btn.data('par_id'), 10) || 0;
+ var strField = String($btn.data('field') || '');
+ if (!intParId || !strField) {
+ return;
+ }
+
+ var strTitle = (t('auditPopupTitle') || '') + ' — ' + auditFieldLabel(strField);
+
+ function showDialog(htmlBody) {
+ if (typeof window.bootbox !== 'undefined') {
+ window.bootbox.dialog({
+ title: strTitle,
+ message: htmlBody,
+ size: 'large',
+ buttons: {
+ close: {
+ label: t('auditPopupClose') || 'OK',
+ className: 'btn btn-secondary rounded-0'
+ }
+ }
+ });
+ return;
+ }
+ window.alert(strTitle);
+ }
+
+ $preloader_text.html(cfg.preloaderWait || '');
+ $preloader.show();
+ $.post((cfg.domain || '') + '/ajax_inscr_gestion.php', {
+ a: 'fiche_audit_history',
+ par_id: intParId,
+ field: strField,
+ lang: cfg.lang || 'fr'
+ }, function (result) {
+ $preloader.fadeOut('slow');
+ if (fxInscrRedirectIfSessionExpired(result)) {
+ return;
+ }
+ if (!result || result.state !== 'success') {
+ showDialog('' + escHtml(t('errLoad') || '') + '
');
+ return;
+ }
+ showDialog(renderAuditTable(result.items || []));
+ }, 'json').fail(function () {
+ $preloader.fadeOut('slow');
+ showDialog('' + escHtml(t('errNetwork') || '') + '
');
+ });
+ });
+ })();
+
(function () {
var pendingStatutXhr = null;
var lastStatutParId = null;
diff --git a/php/inc_fx_fiche_audit.php b/php/inc_fx_fiche_audit.php
index 791baaf..73bb003 100644
--- a/php/inc_fx_fiche_audit.php
+++ b/php/inc_fx_fiche_audit.php
@@ -419,6 +419,70 @@ function fxFicheAuditAdminListEvents()
return is_array($arrRows) ? $arrRows : array();
}
+/**
+ * Champs (hors last_touch) qui ont au moins 1 log pour ce participant.
+ *
+ * @return array
+ */
+function fxFicheAuditFieldsWithLogsForPar($intParId)
+{
+ global $objDatabase;
+
+ $intParId = intval($intParId);
+ $arrOut = array();
+ if ($intParId <= 0 || !isset($objDatabase) || !fxFicheAuditTablesReady()) {
+ return $arrOut;
+ }
+
+ $sql = 'SELECT field_key FROM inscriptions_fiche_audit_log'
+ . ' WHERE par_id = ' . $intParId
+ . " AND field_key <> 'last_touch'"
+ . ' GROUP BY field_key';
+ $arrRows = $objDatabase->fxGetResults($sql);
+ if (is_array($arrRows)) {
+ foreach ($arrRows as $row) {
+ if (!empty($row['field_key'])) {
+ $arrOut[(string)$row['field_key']] = true;
+ }
+ }
+ }
+
+ return $arrOut;
+}
+
+/**
+ * Historique détail d'un champ (fiche / AJAX), limité par la config.
+ *
+ * @return array[]
+ */
+function fxFicheAuditGetRecentForField($intParId, $strFieldKey, $intLimit = 0)
+{
+ global $objDatabase;
+
+ $intParId = intval($intParId);
+ $strFieldKey = preg_replace('/[^a-z0-9_]/i', '', (string)$strFieldKey);
+ if ($intParId <= 0 || $strFieldKey === '' || $strFieldKey === 'last_touch'
+ || !isset($objDatabase) || !fxFicheAuditTablesReady()) {
+ return array();
+ }
+
+ if ($intLimit <= 0) {
+ $arrSettings = fxFicheAuditGetSettings();
+ $intLimit = max(1, intval($arrSettings['retention_max_per_field']));
+ }
+ $intLimit = min(100, max(1, intval($intLimit)));
+
+ $sql = 'SELECT fal_id, field_key, old_value, new_value, changed_by_label, source, created_at'
+ . ' FROM inscriptions_fiche_audit_log'
+ . ' WHERE par_id = ' . $intParId
+ . " AND field_key = '" . $objDatabase->fxEscape($strFieldKey) . "'"
+ . ' ORDER BY created_at DESC, fal_id DESC'
+ . ' LIMIT ' . $intLimit;
+ $arrRows = $objDatabase->fxGetResults($sql);
+
+ return is_array($arrRows) ? $arrRows : array();
+}
+
/**
* Derniers logs d'un événement (contrôle admin).
*
diff --git a/php/inc_fx_inscriptions_gestion.php b/php/inc_fx_inscriptions_gestion.php
index 0e81f74..063a9cb 100644
--- a/php/inc_fx_inscriptions_gestion.php
+++ b/php/inc_fx_inscriptions_gestion.php
@@ -121,6 +121,28 @@ function fxInscrGestionPermInspectEchoHidden($strPermKey, $strLabel) {
}
/** MSIN-4401 — Enveloppe action + cadenas (toujours a droite du controle). */
+/**
+ * MSIN-4464 — Bouton « ! » historique (seulement s'il y a au moins 1 log pour le champ).
+ *
+ * @param array $arrFieldsWithLogs
+ */
+function fxInscrGestionRenderAuditBang($intParId, $strFieldKey, $blnCanAudit, $arrFieldsWithLogs)
+{
+ $intParId = intval($intParId);
+ $strFieldKey = preg_replace('/[^a-z0-9_]/i', '', (string)$strFieldKey);
+ if (!$blnCanAudit || $intParId <= 0 || $strFieldKey === '' || empty($arrFieldsWithLogs[$strFieldKey])) {
+ return;
+ }
+
+ $strTitle = fxInscrGestionT('inscr_gestion_audit_bang_title');
+ $strAria = fxInscrGestionT('inscr_gestion_audit_bang_aria');
+ echo ' ';
+}
+
function fxInscrGestionActionItemOpen($strExtraClass = '') {
$strExtra = trim((string)$strExtraClass);
echo ' $blnFr ? 'Retirer' : 'Remove',
'inscr_gestion_remis_confirm_cancel' => $blnFr ? 'Annuler' : 'Cancel',
+ // MSIN-4464 — historique fiche ( ! )
+ 'inscr_gestion_audit_bang_title' => $blnFr ? 'Voir les derniers changements' : 'View recent changes',
+ 'inscr_gestion_audit_bang_aria' => $blnFr ? 'Historique des modifications' : 'Change history',
+ 'inscr_gestion_audit_popup_title' => $blnFr ? 'Derniers changements' : 'Recent changes',
+ 'inscr_gestion_audit_popup_empty' => $blnFr ? 'Aucun historique pour ce champ.' : 'No history for this field.',
+ 'inscr_gestion_audit_popup_close' => $blnFr ? 'Fermer' : 'Close',
+ 'inscr_gestion_audit_col_when' => $blnFr ? 'Quand' : 'When',
+ 'inscr_gestion_audit_col_who' => $blnFr ? 'Par' : 'By',
+ 'inscr_gestion_audit_col_from' => $blnFr ? 'Avant' : 'From',
+ 'inscr_gestion_audit_col_to' => $blnFr ? 'Après' : 'To',
+ 'inscr_gestion_audit_field_par_statut_course' => $blnFr ? 'Statut de course' : 'Race status',
+ 'inscr_gestion_audit_field_no_bib' => $blnFr ? 'Numéro de dossard' : 'Bib number',
+ 'inscr_gestion_audit_field_no_bib_remis' => $blnFr ? 'Dossard remis' : 'Bib handed out',
+ 'inscr_gestion_audit_field_is_cancelled' => $blnFr ? 'Annulation' : 'Cancellation',
// MSIN-4328 — bouton superadmin vers fiche ChronoTrack
'inscr_gestion_btn_chronotrack' => $blnFr ? 'Voir dans ChronoTrack' : 'View in ChronoTrack',
'inscr_gestion_list_count_one' => $blnFr ? '%d inscription' : '%d registration',
@@ -2243,9 +2279,24 @@ function fxInscrGestionRenderFiche($intEveId, $strLangue, $strBaseUrl, $arrReq,
$blnCanInvoice = fxInscrGestionCanDo($intComId, $intRowEveId, 'inscriptions_gestion.invoice');
$blnCanTransaction = fxInscrGestionCanDo($intComId, $intRowEveId, 'inscriptions_gestion.transaction');
$blnCanRenvoi = fxInscrGestionCanDo($intComId, $intRowEveId, 'inscriptions_gestion.renvoi');
- $blnHasEditable = ($blnCanStatut || $blnCanBib || $blnCanRemis);
+ // MSIN-4464 — historique ( ! ) sur champs whitelist avec au moins 1 log.
+ $blnCanAudit = fxInscrGestionCanDo($intComId, $intRowEveId, 'inscriptions_gestion.audit_view');
+ $arrAuditFieldsWithLogs = array();
+ if ($blnCanAudit) {
+ if (!function_exists('fxFicheAuditFieldsWithLogsForPar')) {
+ require_once dirname(__FILE__) . '/inc_fx_fiche_audit.php';
+ }
+ $arrAuditFieldsWithLogs = fxFicheAuditFieldsWithLogsForPar(intval($arrRow['par_id']));
+ }
+ $blnHasAuditEditable = $blnCanAudit && (
+ !empty($arrAuditFieldsWithLogs['no_bib'])
+ || !empty($arrAuditFieldsWithLogs['par_statut_course'])
+ || !empty($arrAuditFieldsWithLogs['no_bib_remis'])
+ );
+ $blnHasEditable = ($blnCanStatut || $blnCanBib || $blnCanRemis || $blnHasAuditEditable);
$blnHasGestion = ($blnCanEdit || $blnCanCancel || $blnCanRestore || $blnCanRefund
- || $blnCanInvoice || $blnCanTransaction || $blnCanRenvoi);
+ || $blnCanInvoice || $blnCanTransaction || $blnCanRenvoi
+ || ($blnCanAudit && !empty($arrAuditFieldsWithLogs['is_cancelled'])));
$blnInspect = function_exists('fxAdminPermInspectModeActive') && fxAdminPermInspectModeActive();
$strBackUrl = fxInscrGestionResolveBackUrl($strBaseUrl, $intEveId, $arrReq);
@@ -2317,10 +2368,16 @@ function fxInscrGestionRenderFiche($intEveId, $strLangue, $strBaseUrl, $arrReq,
if ($blnCancelled) {
// MSIN-4405 — Detail de l'annulation (transferee / annulee) derive en lecture seule.
$strCause = fxGetAnnulationCause($arrRow['pec_id_original'], $arrRow['pec_actif']);
+ $strCancelVal = ''
+ . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_statut_' . $strCause)) . '';
+ if ($blnCanAudit && !empty($arrAuditFieldsWithLogs['is_cancelled'])) {
+ ob_start();
+ fxInscrGestionRenderAuditBang($intParId, 'is_cancelled', $blnCanAudit, $arrAuditFieldsWithLogs);
+ $strCancelVal .= ob_get_clean();
+ }
fxInscrGestionRenderKvRow(
fxInscrGestionT('inscr_gestion_annulation_titre'),
- ''
- . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_statut_' . $strCause)) . '',
+ $strCancelVal,
true
);
}
@@ -2429,6 +2486,18 @@ function fxInscrGestionRenderFiche($intEveId, $strLangue, $strBaseUrl, $arrReq,
fxInscrGestionPermInspectEchoHidden('inscriptions_gestion.restore', fxInscrGestionT('btn_retablirinscriptions'));
fxInscrGestionActionItemClose();
}
+ // MSIN-4464 — historique annulation si pas deja affiche a cote du badge (fiche active).
+ if ($blnCanAudit && !empty($arrAuditFieldsWithLogs['is_cancelled']) && !$blnCancelled) {
+ fxInscrGestionActionItemOpen();
+ echo ''
+ . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_audit_field_is_cancelled'));
+ fxInscrGestionRenderAuditBang($intParId, 'is_cancelled', $blnCanAudit, $arrAuditFieldsWithLogs);
+ echo '';
+ fxInscrGestionActionItemClose();
+ }
+ if ($blnInspect) {
+ fxInscrGestionPermInspectEcho('inscriptions_gestion.audit_view');
+ }
if ($blnCanRenvoi) {
fxInscrGestionActionItemOpen();
echo '