diff --git a/ajax_bib_range.php b/ajax_bib_range.php index 31125d1..8466045 100644 --- a/ajax_bib_range.php +++ b/ajax_bib_range.php @@ -73,6 +73,20 @@ if ($action == 'delete') { if ($id > 0) { + $rowLock = $objDatabase->fxGetRow(" + SELECT epr_bib_locked + FROM inscriptions_epreuves_bib + WHERE epr_bib_id = $id + "); + + if ($rowLock && (int)($rowLock['epr_bib_locked'] ?? 0) === 1) { + echo json_encode([ + 'success' => false, + 'message' => fxBibMsg('bib_v4_ajax_range_locked') + ]); + exit; + } + $range = fxInfosBibRange(0,$id); $epr_id = $range[1]["epr_id"]; @@ -169,8 +183,20 @@ if ($action == 'save') { // ===================== if (!$is_new) { - $sqlGet = "SELECT epr_id FROM inscriptions_epreuves_bib WHERE epr_bib_id = $id"; - $epr_id = (int)$objDatabase->fxGetVar($sqlGet); + $sqlGet = "SELECT epr_id, epr_bib_locked FROM inscriptions_epreuves_bib WHERE epr_bib_id = $id"; + $rowRange = $objDatabase->fxGetRow($sqlGet); + + if (!$rowRange || (int)($rowRange['epr_id'] ?? 0) <= 0) { + echo json_encode(['success' => false, 'message' => fxBibMsg('bib_v4_ajax_range_not_found')]); + exit; + } + + if ((int)($rowRange['epr_bib_locked'] ?? 0) === 1) { + echo json_encode(['success' => false, 'message' => fxBibMsg('bib_v4_ajax_range_locked')]); + exit; + } + + $epr_id = (int)$rowRange['epr_id']; if ($epr_id <= 0) { echo json_encode(['success' => false, 'message' => fxBibMsg('bib_v4_ajax_range_not_found')]); @@ -373,6 +399,15 @@ if ($action == 'batch_go') { // Liste des ranges sélectionnés (JSON → array PHP) $ranges = json_decode($_POST['ranges'] ?? '[]', true); + $lockCheck = fxBibAssertRangesUnlocked($epr_id, is_array($ranges) ? $ranges : []); + if (!$lockCheck['success']) { + echo json_encode([ + 'success' => false, + 'message' => $lockCheck['message'] ?? fxBibMsg('bib_v4_ajax_range_locked') + ]); + exit; + } + // Ordre choisi (table bib_assignements) $ba_id = intval($_POST['ba_id'] ?? 0); @@ -444,6 +479,15 @@ if ($action == 'batch_apply') { $ranges = json_decode($_POST['ranges'] ?? '[]', true); $ba_id = intval($_POST['ba_id'] ?? 0); + $lockCheck = fxBibAssertRangesUnlocked($epr_id, is_array($ranges) ? $ranges : []); + if (!$lockCheck['success']) { + echo json_encode([ + 'success' => false, + 'message' => $lockCheck['message'] ?? fxBibMsg('bib_v4_ajax_range_locked') + ]); + exit; + } + $participants = fxGetParticipantsForBatchAssign($epr_id, $ba_id); $tabRanges = fxGetRangesForBatchAssign($epr_id, $ranges); @@ -506,6 +550,15 @@ if ($action == 'reset_preview') { $ranges = json_decode($_POST['ranges'] ?? '[]', true); + $lockCheck = fxBibAssertRangesUnlocked($epr_id, is_array($ranges) ? $ranges : []); + if (!$lockCheck['success']) { + echo json_encode([ + 'success' => false, + 'message' => $lockCheck['message'] ?? fxBibMsg('bib_v4_ajax_range_locked') + ]); + exit; + } + $participants = fxGetParticipantsForBatchReset($epr_id, $ranges); $html = renderBibView( @@ -535,6 +588,15 @@ if ($action == 'reset_apply') { $ranges = json_decode($_POST['ranges'] ?? '[]', true); + $lockCheck = fxBibAssertRangesUnlocked($epr_id, is_array($ranges) ? $ranges : []); + if (!$lockCheck['success']) { + echo json_encode([ + 'success' => false, + 'message' => $lockCheck['message'] ?? fxBibMsg('bib_v4_ajax_range_locked') + ]); + exit; + } + $participants = fxGetParticipantsForBatchReset($epr_id, $ranges); $count = 0; @@ -611,6 +673,51 @@ if ($action == 'save_team_mode') { exit; } +// MSIN-4379 — Verrouillage séquence (cadenas ouvert/fermé) +if ($action == 'toggle_lock') { + + if (!fxBibRequirePromoteurSession()) { + echo json_encode([ + 'success' => false, + 'message' => fxBibMsg('bib_v4_ajax_session_invalid') + ]); + exit; + } + + if (!fxBibValidateCsrf($_POST['csrf_token'] ?? '')) { + echo json_encode([ + 'success' => false, + 'message' => fxBibMsg('bib_v4_ajax_unauthorized') + ]); + exit; + } + + $epr_bib_id = intval($_POST['epr_bib_id'] ?? 0); + $epr_id = intval($_POST['epr_id'] ?? 0); + $locked = intval($_POST['locked'] ?? 0) === 1; + + $result = fxBibSetRangeLocked($epr_bib_id, $locked, $epr_id); + + if (!$result['success']) { + echo json_encode([ + 'success' => false, + 'message' => $result['message'] ?? fxBibMsg('bib_v4_ajax_error') + ]); + exit; + } + + $epr_id = (int)$result['epr_id']; + $tabBibStats = fxInfosBibRange($epr_id); + + echo json_encode([ + 'success' => true, + 'locked' => !empty($result['locked']), + 'auto_active' => fxIsBibAutoActive($epr_id), + 'html' => renderBibRanges($tabBibStats, $epr_id, 'assign') + ]); + exit; +} + // MSIN-4379 — Étape 2 // Endpoint AJAX : sauvegarde ou désactivation de la config auto (epr_bib_auto). // Paramètres : epr_id, active (0|1), ranges (JSON des epr_bib_id), csrf_token. diff --git a/css/style.css b/css/style.css index 8c59e08..6a93cb2 100644 --- a/css/style.css +++ b/css/style.css @@ -1813,10 +1813,11 @@ a.ms1-trad-link.btn-aide-trad{ width:100%; } -/* MSIN-4379 — View/OK/Suppr icônes ; stats regroupées ; badge auto à droite. */ +/* MSIN-4379 — Cadenas + view/OK/stats ; badge auto à droite. */ .epr-line{ display:grid; grid-template-columns: + 32px 40px 58px 58px @@ -1832,6 +1833,7 @@ a.ms1-trad-link.btn-aide-trad{ .bib-container.batch-mode .epr-line{ grid-template-columns: + 32px 40px 58px 58px @@ -1841,6 +1843,52 @@ a.ms1-trad-link.btn-aide-trad{ 28px; } +.epr-col-lock{ + display:flex; + align-items:center; + justify-content:center; + min-width:0; +} + +.btn-bib-lock{ + width:100%; + padding-left:0; + padding-right:0; + border-width:1px; + border-style:solid; +} + +.btn-bib-lock--open{ + background-color:#28a745; + border-color:#218838; + color:#fff; +} + +.btn-bib-lock--open:hover, +.btn-bib-lock--open:focus{ + background-color:#218838; + border-color:#1e7e34; + color:#fff; +} + +.btn-bib-lock--closed{ + background-color:#dc3545; + border-color:#c82333; + color:#fff; +} + +.btn-bib-lock--closed:hover, +.btn-bib-lock--closed:focus{ + background-color:#c82333; + border-color:#bd2130; + color:#fff; +} + +.bib-range--locked .bib-start, +.bib-range--locked .bib-end{ + background-color:#f1f3f5; +} + .epr-col-stats{ display:grid; grid-template-columns:56px 48px 48px; diff --git a/inc_footer_scripts.php b/inc_footer_scripts.php index 8b55227..24aa872 100644 --- a/inc_footer_scripts.php +++ b/inc_footer_scripts.php @@ -1975,6 +1975,10 @@ if ($strLangue == 'fr') { return moduleBib.dataset[attr] || ''; } + function bibJsConfirm(attr) { + return bibJs(attr).replace(/\\n/g, '\n'); + } + function bibJsFmt(attr) { var tpl = bibJs(attr); var args = Array.prototype.slice.call(arguments, 1); @@ -2546,6 +2550,59 @@ if ($strLangue == 'fr') { return; } + // ===================== + // LOCK / UNLOCK SEQUENCE + // ===================== + let btnLock = e.target.closest('.btn-bib-lock'); + if (btnLock) { + e.preventDefault(); + + let row = btnLock.closest('.epr-row'); + if (!row) return; + + let isLocked = btnLock.dataset.locked === '1'; + let confirmMsg = isLocked + ? bibJsConfirm('jsUnlockConfirm') + : bibJsConfirm('jsLockConfirm'); + + if (!confirm(confirmMsg)) { + return; + } + + fetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: + 'action=toggle_lock' + + '&epr_bib_id=' + encodeURIComponent(btnLock.dataset.rangeId) + + '&epr_id=' + encodeURIComponent(btnLock.dataset.eprId || row.dataset.eprId) + + '&locked=' + encodeURIComponent(isLocked ? '0' : '1') + + '&csrf_token=' + encodeURIComponent(bibCsrfToken()) + + bibAjaxLangSuffix() + }) + .then(res => res.json()) + .then(data => { + + if (!data.success) { + alert(data.message || bibJs('jsErrGeneric')); + return; + } + + if (typeof data.auto_active !== 'undefined') { + row.dataset.autoActive = data.auto_active ? '1' : '0'; + syncAutoUiState(row, data.auto_active); + } + + if (data.html) { + setBibContainerHtml(row, data.html); + } + }); + + return; + } + // ===================== // DELETE // ===================== diff --git a/php/inc_fx_promoteur.php b/php/inc_fx_promoteur.php index 223325f..f0a2cc1 100644 --- a/php/inc_fx_promoteur.php +++ b/php/inc_fx_promoteur.php @@ -2917,6 +2917,8 @@ function fxBibModuleJsDataAttrs() { 'batch-summary-short' => 'bib_v4_ajax_sim_summary', 'batch-reset-count' => 'bib_v4_ajax_reset_preview_count', 'auto-select-hint' => 'bib_v4_auto_select_hint', + 'lock-confirm' => 'bib_v4_js_lock_confirm', + 'unlock-confirm' => 'bib_v4_js_unlock_confirm', ]; $html = ''; foreach ($map as $attr => $clef) { @@ -3073,9 +3075,122 @@ function fxBibIsRangeAvailableForAuto($epr_id, $epr_bib_id) { return false; } + if (fxBibRangeIsLocked($rows[1])) { + return false; + } + return fxBibRangeDispoFromRow($rows[1]) > 0; } +/** Séquence verrouillée (epr_bib_locked = 1) — plus de modification ni assignation. */ +function fxBibRangeIsLocked(array $range) { + return (int)($range['epr_bib_locked'] ?? 0) === 1; +} + +/** Vérifie qu'aucune des séquences listées n'est verrouillée. */ +function fxBibAssertRangesUnlocked($epr_id, $rangeIds) { + global $objDatabase; + + $epr_id = (int)$epr_id; + + if ($epr_id <= 0 || empty($rangeIds) || !is_array($rangeIds)) { + return ['success' => true]; + } + + $ids = array_values(array_filter(array_map('intval', $rangeIds))); + + if (empty($ids)) { + return ['success' => true]; + } + + $strIds = implode(',', $ids); + + $sql = " + SELECT COUNT(*) + FROM inscriptions_epreuves_bib + WHERE epr_id = $epr_id + AND epr_bib_id IN ($strIds) + AND epr_bib_locked = 1 + "; + + if ((int)$objDatabase->fxGetVar($sql) > 0) { + return ['success' => false, 'message' => fxBibMsg('bib_v4_ajax_range_locked')]; + } + + return ['success' => true]; +} + +/** Bascule le verrou d'une séquence (0 = ouvert, 1 = fermé). */ +function fxBibSetRangeLocked($epr_bib_id, $locked, $epr_id = 0) { + global $objDatabase; + + $epr_bib_id = (int)$epr_bib_id; + $epr_id = (int)$epr_id; + $locked = $locked ? 1 : 0; + + if ($epr_bib_id <= 0) { + return ['success' => false, 'message' => fxBibMsg('bib_v4_ajax_id_invalid')]; + } + + $sqlGet = " + SELECT epr_id + FROM inscriptions_epreuves_bib + WHERE epr_bib_id = $epr_bib_id + "; + $rowEprId = (int)$objDatabase->fxGetVar($sqlGet); + + if ($rowEprId <= 0) { + return ['success' => false, 'message' => fxBibMsg('bib_v4_ajax_range_not_found')]; + } + + if ($epr_id > 0 && $rowEprId !== $epr_id) { + return ['success' => false, 'message' => fxBibMsg('bib_v4_ajax_seq_invalid')]; + } + + $objDatabase->fxQuery(" + UPDATE inscriptions_epreuves_bib + SET epr_bib_locked = $locked + WHERE epr_bib_id = $epr_bib_id + "); + + if ($locked === 1) { + $objDatabase->fxQuery(" + UPDATE inscriptions_epreuves_bib + SET epr_bib_auto = 0 + WHERE epr_bib_id = $epr_bib_id + "); + } + + return [ + 'success' => true, + 'epr_id' => $rowEprId, + 'locked' => $locked === 1, + ]; +} + +/** Bouton cadenas (vert ouvert / rouge fermé) devant une séquence enregistrée. */ +function fxBibRenderRangeLockButton(array $range, $epr_id = 0) { + if ((int)($range['epr_bib_id'] ?? 0) <= 0) { + return '
'; + } + + $locked = fxBibRangeIsLocked($range); + $strClass = $locked ? 'btn-bib-lock--closed' : 'btn-bib-lock--open'; + $strIcon = $locked ? 'fa-lock' : 'fa-unlock'; + $strTip = $locked ? 'bib_v4_tip_lock_closed' : 'bib_v4_tip_lock_open'; + + return ''; +} + /** Badge « Assignation automatique » sur une ligne de séquence (epr_bib_auto = 1). */ function fxBibRenderRangeAutoBadge(array $range, $mode = 'assign') { if ($mode !== 'assign') { @@ -3087,6 +3202,9 @@ function fxBibRenderRangeAutoBadge(array $range, $mode = 'assign') { if ((int)($range['epr_bib_auto'] ?? 0) !== 1) { return ''; } + if (fxBibRangeIsLocked($range)) { + return ''; + } if (fxBibRangeDispoFromRow($range) <= 0) { return ''; } @@ -3302,6 +3420,7 @@ function renderBibRangeHeader() { ob_start(); ?>