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 += '' + + '' + + '' + + '' + + '' + + ''; + for (var i = 0; i < items.length; i++) { + var row = items[i] || {}; + html += '' + + '' + + '' + + '' + + '' + + ''; + } + html += '
' + escHtml(t('auditColWhen') || '') + '' + escHtml(t('auditColWho') || '') + '' + escHtml(t('auditColFrom') || '') + '' + escHtml(t('auditColTo') || '') + '
' + escHtml(row.when || '') + '' + escHtml(row.who || '') + '' + escHtml(row.old || '') + '' + escHtml(row.new || '') + '
'; + 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 '