From 97ac1807b02a6010a66202090f2d428441538de1 Mon Sep 17 00:00:00 2001 From: stephan Date: Wed, 15 Jul 2026 12:57:18 -0400 Subject: [PATCH] =?UTF-8?q?MSIN-4464=20=E2=80=94=20Add=20configuration=20f?= =?UTF-8?q?or=20fiche=20audit=20traceability=20in=20admin=20interface.=20I?= =?UTF-8?q?ntroduce=20a=20new=20sidebar=20section=20for=20audited=20fields?= =?UTF-8?q?=20and=20retention,=20enhancing=20the=20user=20experience=20by?= =?UTF-8?q?=20providing=20easy=20access=20to=20audit-related=20features.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- php/inc_fx_fiche_audit.php | 260 ++++++++++++++++++++++ php/inc_settings.php | 1 + sql/MSIN-4464-fiche-audit-config.sql | 80 +++++++ superadm/fiche_audit.php | 61 +++++ superadm/inc_droite.php | 13 ++ superadm/php/inc_fx_fiche_audit_admin.php | 140 ++++++++++++ 6 files changed, 555 insertions(+) create mode 100644 php/inc_fx_fiche_audit.php create mode 100644 sql/MSIN-4464-fiche-audit-config.sql create mode 100644 superadm/fiche_audit.php create mode 100644 superadm/php/inc_fx_fiche_audit_admin.php diff --git a/php/inc_fx_fiche_audit.php b/php/inc_fx_fiche_audit.php new file mode 100644 index 0000000..2b0d33b --- /dev/null +++ b/php/inc_fx_fiche_audit.php @@ -0,0 +1,260 @@ +fxGetResults("SHOW TABLES LIKE 'inscriptions_fiche_audit_settings'"); + $blnReady = ($tab != null && count($tab) > 0); + + return $blnReady; +} + +/** + * Catalogue connu (ordre d'affichage admin). Les clés absentes en BD sont + * proposées à l'écran et créées au premier enregistrement. + * + * @return array[] + */ +function fxFicheAuditFieldCatalog() +{ + return array( + array( + 'field_key' => 'par_statut_course', + 'field_label_fr' => 'Statut de course', + 'field_label_en' => 'Race status', + 'field_tri' => 10, + ), + array( + 'field_key' => 'no_bib', + 'field_label_fr' => 'Numéro de dossard', + 'field_label_en' => 'Bib number', + 'field_tri' => 20, + ), + array( + 'field_key' => 'no_bib_remis', + 'field_label_fr' => 'Dossard remis', + 'field_label_en' => 'Bib handed out', + 'field_tri' => 30, + ), + array( + 'field_key' => 'is_cancelled', + 'field_label_fr' => 'Annulation', + 'field_label_en' => 'Cancellation', + 'field_tri' => 40, + ), + ); +} + +/** + * @return array{retention_days:int,retention_max_per_field:int,track_last_touch:int} + */ +function fxFicheAuditGetSettings() +{ + global $objDatabase; + + $arrDefault = array( + 'retention_days' => 90, + 'retention_max_per_field' => 10, + 'track_last_touch' => 1, + ); + + if (!isset($objDatabase) || !fxFicheAuditTablesReady()) { + return $arrDefault; + } + + $sql = 'SELECT retention_days, retention_max_per_field, track_last_touch' + . ' FROM inscriptions_fiche_audit_settings WHERE setting_id = 1 LIMIT 1'; + $row = $objDatabase->fxGetRow($sql); + if ($row === null || !is_array($row)) { + return $arrDefault; + } + + return array( + 'retention_days' => max(1, intval($row['retention_days'])), + 'retention_max_per_field' => max(1, intval($row['retention_max_per_field'])), + 'track_last_touch' => (intval($row['track_last_touch']) === 1) ? 1 : 0, + ); +} + +/** + * Champs whitelist fusionnés avec le catalogue (actif = 0 si absent en BD). + * + * @return array[] + */ +function fxFicheAuditGetFieldsForAdmin() +{ + global $objDatabase; + + $arrByKey = array(); + if (isset($objDatabase) && fxFicheAuditTablesReady()) { + $sql = 'SELECT field_key, field_label_fr, field_label_en, field_actif, field_tri' + . ' FROM inscriptions_fiche_audit_fields ORDER BY field_tri, field_key'; + $arrRows = $objDatabase->fxGetResults($sql); + if (is_array($arrRows)) { + foreach ($arrRows as $row) { + if (!is_array($row) || empty($row['field_key'])) { + continue; + } + $arrByKey[$row['field_key']] = array( + 'field_key' => (string)$row['field_key'], + 'field_label_fr' => (string)$row['field_label_fr'], + 'field_label_en' => (string)$row['field_label_en'], + 'field_actif' => (intval($row['field_actif']) === 1) ? 1 : 0, + 'field_tri' => intval($row['field_tri']), + ); + } + } + } + + $arrOut = array(); + foreach (fxFicheAuditFieldCatalog() as $arrCat) { + $strKey = $arrCat['field_key']; + if (isset($arrByKey[$strKey])) { + $arrOut[] = $arrByKey[$strKey]; + unset($arrByKey[$strKey]); + } else { + $arrOut[] = array( + 'field_key' => $strKey, + 'field_label_fr' => $arrCat['field_label_fr'], + 'field_label_en' => $arrCat['field_label_en'], + 'field_actif' => 0, + 'field_tri' => intval($arrCat['field_tri']), + ); + } + } + + // Champs éventuels hors catalogue (déjà en BD) + foreach ($arrByKey as $row) { + $arrOut[] = $row; + } + + return $arrOut; +} + +/** + * Clés actives (pour usage futur des writers). + * + * @return string[] + */ +function fxFicheAuditGetActiveFieldKeys() +{ + global $objDatabase; + + if (!isset($objDatabase)) { + return array(); + } + + $sql = 'SELECT field_key FROM inscriptions_fiche_audit_fields' + . ' WHERE field_actif = 1 ORDER BY field_tri, field_key'; + $arrRows = $objDatabase->fxGetResults($sql); + $arrKeys = array(); + if (is_array($arrRows)) { + foreach ($arrRows as $row) { + if (!empty($row['field_key'])) { + $arrKeys[] = (string)$row['field_key']; + } + } + } + + return $arrKeys; +} + +/** + * @param array $arrPost + * @return array{state:string,message:string} + */ +function fxFicheAuditSaveConfig($arrPost) +{ + global $objDatabase; + + if (!isset($objDatabase)) { + return array('state' => 'error', 'message' => 'Base de données indisponible.'); + } + if (!fxFicheAuditTablesReady()) { + return array( + 'state' => 'error', + 'message' => 'Tables absentes : exécuter sql/MSIN-4464-fiche-audit-config.sql', + ); + } + + $intDays = isset($arrPost['retention_days']) ? intval($arrPost['retention_days']) : 90; + $intMax = isset($arrPost['retention_max_per_field']) ? intval($arrPost['retention_max_per_field']) : 10; + $intLastTouch = !empty($arrPost['track_last_touch']) ? 1 : 0; + + if ($intDays < 1 || $intDays > 3650) { + return array('state' => 'error', 'message' => 'Rétention (jours) : entre 1 et 3650.'); + } + if ($intMax < 1 || $intMax > 100) { + return array('state' => 'error', 'message' => 'Max par champ : entre 1 et 100.'); + } + + $arrActif = array(); + if (!empty($arrPost['field_actif']) && is_array($arrPost['field_actif'])) { + foreach ($arrPost['field_actif'] as $strKey) { + $strKey = preg_replace('/[^a-z0-9_]/i', '', (string)$strKey); + if ($strKey !== '') { + $arrActif[$strKey] = true; + } + } + } + + $sqlSettings = 'INSERT INTO inscriptions_fiche_audit_settings' + . ' (setting_id, retention_days, retention_max_per_field, track_last_touch)' + . ' VALUES (1, ' . $intDays . ', ' . $intMax . ', ' . $intLastTouch . ')' + . ' ON DUPLICATE KEY UPDATE' + . ' retention_days = VALUES(retention_days),' + . ' retention_max_per_field = VALUES(retention_max_per_field),' + . ' track_last_touch = VALUES(track_last_touch)'; + if (!$objDatabase->fxQuery($sqlSettings)) { + return array('state' => 'error', 'message' => 'Erreur à l\'enregistrement des réglages.'); + } + + foreach (fxFicheAuditGetFieldsForAdmin() as $arrField) { + $strKey = $arrField['field_key']; + $intActif = isset($arrActif[$strKey]) ? 1 : 0; + $strLabelFr = $objDatabase->fxEscape($arrField['field_label_fr']); + $strLabelEn = $objDatabase->fxEscape($arrField['field_label_en']); + $intTri = intval($arrField['field_tri']); + + $sqlField = 'INSERT INTO inscriptions_fiche_audit_fields' + . ' (field_key, field_label_fr, field_label_en, field_actif, field_tri)' + . " VALUES ('" . $objDatabase->fxEscape($strKey) . "', '" . $strLabelFr . "', '" + . $strLabelEn . "', " . $intActif . ', ' . $intTri . ')' + . ' ON DUPLICATE KEY UPDATE' + . ' field_label_fr = VALUES(field_label_fr),' + . ' field_label_en = VALUES(field_label_en),' + . ' field_actif = VALUES(field_actif),' + . ' field_tri = VALUES(field_tri)'; + if (!$objDatabase->fxQuery($sqlField)) { + return array('state' => 'error', 'message' => 'Erreur à l\'enregistrement du champ ' . $strKey . '.'); + } + } + + return array('state' => 'ok', 'message' => 'Configuration enregistrée.'); +} diff --git a/php/inc_settings.php b/php/inc_settings.php index 539f061..6f8a2f8 100644 --- a/php/inc_settings.php +++ b/php/inc_settings.php @@ -27,6 +27,7 @@ define('KEYCHAIN_HTTP_EDIT_SHIFT', 100); define('KEYCHAIN_HTTP_EDIT_ALLOWED_COM_IDS', ''); // MSIN-4401 — Qui peut créer / modifier / supprimer les définitions de kits (pas l'assignation). +// MSIN-4464 — Même liste pour la config traçabilité fiche (whitelist + rétention). // Valeurs = com_courriel ou com_login (session superadm usa_info). $arrEveAccesKitEditors = array( 'stephan@progiweb.ca', diff --git a/sql/MSIN-4464-fiche-audit-config.sql b/sql/MSIN-4464-fiche-audit-config.sql new file mode 100644 index 0000000..776e838 --- /dev/null +++ b/sql/MSIN-4464-fiche-audit-config.sql @@ -0,0 +1,80 @@ +-- MSIN-4464 — Traçabilité fiches : config whitelist + rétention +-- Prérequis : aucun +-- Notes prod : idempotent (CREATE IF NOT EXISTS + seeds via NOT EXISTS) + +SET NAMES utf8mb4; + +/* Réglages globaux (1 seule ligne) */ +CREATE TABLE IF NOT EXISTS `inscriptions_fiche_audit_settings` ( + `setting_id` tinyint(3) unsigned NOT NULL DEFAULT 1, + `retention_days` smallint(5) unsigned NOT NULL DEFAULT 90 + COMMENT 'MSIN-4464 — durée de conservation des entrées détaillées', + `retention_max_per_field` smallint(5) unsigned NOT NULL DEFAULT 10 + COMMENT 'MSIN-4464 — max d''entrées gardées par champ × participant', + `track_last_touch` tinyint(1) unsigned NOT NULL DEFAULT 1 + COMMENT 'MSIN-4464 — noter aussi qui a touché la fiche (sans détail de champ)', + `setting_updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`setting_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='MSIN-4464 — réglages traçabilité fiche'; + +INSERT INTO `inscriptions_fiche_audit_settings` + (`setting_id`, `retention_days`, `retention_max_per_field`, `track_last_touch`) +SELECT 1, 90, 10, 1 +WHERE NOT EXISTS ( + SELECT 1 FROM `inscriptions_fiche_audit_settings` WHERE `setting_id` = 1 +); + +/* Whitelist des champs à tracer en détail (old → new + auteur) */ +CREATE TABLE IF NOT EXISTS `inscriptions_fiche_audit_fields` ( + `field_key` varchar(64) NOT NULL, + `field_label_fr` varchar(100) NOT NULL, + `field_label_en` varchar(100) NOT NULL, + `field_actif` tinyint(1) unsigned NOT NULL DEFAULT 0, + `field_tri` smallint(5) unsigned NOT NULL DEFAULT 0, + `field_updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`field_key`), + KEY `idx_faf_actif_tri` (`field_actif`, `field_tri`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='MSIN-4464 — whitelist champs audités fiche'; + +INSERT INTO `inscriptions_fiche_audit_fields` + (`field_key`, `field_label_fr`, `field_label_en`, `field_actif`, `field_tri`) +SELECT 'par_statut_course', 'Statut de course', 'Race status', 1, 10 +WHERE NOT EXISTS (SELECT 1 FROM `inscriptions_fiche_audit_fields` WHERE `field_key` = 'par_statut_course'); + +INSERT INTO `inscriptions_fiche_audit_fields` + (`field_key`, `field_label_fr`, `field_label_en`, `field_actif`, `field_tri`) +SELECT 'no_bib', 'Numéro de dossard', 'Bib number', 1, 20 +WHERE NOT EXISTS (SELECT 1 FROM `inscriptions_fiche_audit_fields` WHERE `field_key` = 'no_bib'); + +INSERT INTO `inscriptions_fiche_audit_fields` + (`field_key`, `field_label_fr`, `field_label_en`, `field_actif`, `field_tri`) +SELECT 'no_bib_remis', 'Dossard remis', 'Bib handed out', 0, 30 +WHERE NOT EXISTS (SELECT 1 FROM `inscriptions_fiche_audit_fields` WHERE `field_key` = 'no_bib_remis'); + +INSERT INTO `inscriptions_fiche_audit_fields` + (`field_key`, `field_label_fr`, `field_label_en`, `field_actif`, `field_tri`) +SELECT 'is_cancelled', 'Annulation', 'Cancellation', 0, 40 +WHERE NOT EXISTS (SELECT 1 FROM `inscriptions_fiche_audit_fields` WHERE `field_key` = 'is_cancelled'); + +/* Journal détaillé — writers + UI « ? » dans une phase suivante */ +CREATE TABLE IF NOT EXISTS `inscriptions_fiche_audit_log` ( + `fal_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `par_id` int(10) unsigned NOT NULL, + `eve_id` int(10) unsigned DEFAULT NULL, + `field_key` varchar(64) NOT NULL + COMMENT 'champ whitelisté, ou last_touch pour intervention globale', + `old_value` varchar(255) DEFAULT NULL, + `new_value` varchar(255) DEFAULT NULL, + `changed_by_com_id` int(10) unsigned DEFAULT NULL, + `changed_by_label` varchar(120) DEFAULT NULL, + `source` varchar(32) DEFAULT NULL + COMMENT 'fiche, liste, mobile, system…', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`fal_id`), + KEY `idx_fal_par_field_at` (`par_id`, `field_key`, `created_at`), + KEY `idx_fal_eve_at` (`eve_id`, `created_at`), + KEY `idx_fal_created` (`created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='MSIN-4464 — historique changements fiche (détail + last_touch)'; diff --git a/superadm/fiche_audit.php b/superadm/fiche_audit.php new file mode 100644 index 0000000..b0acec1 --- /dev/null +++ b/superadm/fiche_audit.php @@ -0,0 +1,61 @@ + +
+
+
+ 'superadm-content-v2-zone superadm-content-v2-zone--flush', + )); + fxFicheAuditAdminRenderPage($strFlash, $strError); + fxSuperadmV2PanelClose(); + ?> +

 

+
+ +
+
+ diff --git a/superadm/inc_droite.php b/superadm/inc_droite.php index 3d18d71..3dd12c3 100644 --- a/superadm/inc_droite.php +++ b/superadm/inc_droite.php @@ -5,6 +5,7 @@ $strSidebarScript = basename($_SERVER['SCRIPT_NAME'] ?? ''); $strEveAccesP = $_GET['p'] ?? 'kits'; $blnEveAccesActive = ($strSidebarScript === 'eve_acces.php'); $blnEveAccesKits = $blnEveAccesActive && in_array($strEveAccesP, array('kits', 'kit', 'assignations'), true); +$blnFicheAuditActive = ($strSidebarScript === 'fiche_audit.php'); $blnCtApiActive = ($strSidebarScript === 'chronotrack_api.php'); $blnDocActive = ($strSidebarScript === 'documentation.php'); // Sync BD — HTTP_HOST direct (évite échec des global $vbln… selon le contexte d'inclusion) @@ -245,6 +246,18 @@ $blnSidebarStaticSync = !empty($_SESSION['usa_id']) +
+
Traçabilité fiche
+ +
+
ChronoTrack
'; + } + if ($strError !== '') { + echo '
' . htmlspecialchars($strError, ENT_QUOTES, 'UTF-8') . '
'; + } + if (!$blnTablesOk) { + echo '
' + . 'Tables absentes. Exécuter sql/MSIN-4464-fiche-audit-config.sql puis recharger.' + . '
'; + } + ?> +
+

Traçabilité fiche

+
+ + +
+ Lecture seule : vous pouvez consulter la configuration. Modification réservée. +
+ + +
+
+ Comment ça marche ? +
+
+
    +
  • Whitelist — seuls les champs cochés seront historisés en détail (ancienne → nouvelle valeur + personne).
  • +
  • Dernière intervention — si activée, on note aussi qui a touché la fiche, même pour un champ hors whitelist.
  • +
  • Rétention — on ne garde pas un log sans fin : durée max + nombre max d'entrées par champ × participant.
  • +
+

+ L'affichage « ? » sur la fiche (qui a touché ce champ) et le droit promoteur associé viendront dans une étape suivante. + Pour l'instant, cette page configure uniquement ce qui pourra être tracé. +

+
+
+ +
> + + +
+
+ Rétention +
+
+
+
+ + > +
+
+ + > +
+
+
+ > + +
+
+
+ +
+
+ Whitelist — champs à tracer en détail +
+
+
+ + + + + + + + + + + + + + + + + +
ActifChampClé technique
+ > + + +
+ +
+
+
+
+
+ + + + +
+