From ec024e6fbab9659c304de7999f54071285a9de34 Mon Sep 17 00:00:00 2001 From: stephan Date: Tue, 7 Jul 2026 14:49:57 -0400 Subject: [PATCH] MSIN-4429 Implement range input functionality for quantity and end values in bib management This commit introduces new JavaScript functions and PHP methods to enhance the handling of range inputs for quantity and end values in the bib management system. It includes the addition of input fields for quantity and end values, along with logic to manage their states based on user interaction. The CSS is updated to support the new layout and visual states for these fields. Additionally, data attributes are added to improve the handling of range validation and preview functionality. These changes aim to improve user experience and data integrity in the bib input process. --- css/style.css | 91 +++++++++-- inc_footer_scripts.php | 258 +++++++++++++++++++++++++++++--- php/inc_fx_promoteur.php | 79 +++++++++- php/inc_settings.php | 4 +- sql/MSIN-4429-bib-range-qty.sql | 1 + 5 files changed, 397 insertions(+), 36 deletions(-) create mode 100644 sql/MSIN-4429-bib-range-qty.sql diff --git a/css/style.css b/css/style.css index 5f2d3ca..6e44c49 100644 --- a/css/style.css +++ b/css/style.css @@ -1961,13 +1961,14 @@ a.ms1-trad-link.btn-aide-trad{ } /* MSIN-4379 — Cadenas + view/OK/stats ; badge auto à droite. */ +/* MSIN-4429 — colonne Début + bloc Qté/ou/Fin. */ .epr-line{ display:grid; grid-template-columns: 40px 40px 58px - 58px + minmax(148px, 1fr) 44px 48px auto @@ -1983,7 +1984,7 @@ a.ms1-trad-link.btn-aide-trad{ 40px 40px 58px - 58px + minmax(148px, 1fr) 44px 48px auto @@ -2039,10 +2040,86 @@ a.ms1-trad-link.btn-aide-trad{ } .bib-range--locked .bib-start, +.bib-range--locked .bib-qty, .bib-range--locked .bib-end{ background-color:#f1f3f5; } +/* MSIN-4429 — Qté / ou / Fin : pilote actif vs champ calculé / choix. */ +.epr-col-range-spec, +.bib-range-spec{ + display:grid; + grid-template-columns:minmax(0, 1fr) auto minmax(0, 1fr); + gap:6px; + align-items:center; + min-width:0; +} + +.epr-line-header .epr-col-range-spec{ + min-width:0; +} + +.epr-line-header .epr-col-range-spec > div{ + display:flex; + align-items:center; + justify-content:center; + min-width:0; +} + +.bib-range-or{ + color:#6c757d; + font-size:11px; + font-weight:600; + text-transform:lowercase; + white-space:nowrap; + user-select:none; +} + +.bib-range-or--header{ + font-size:12px; +} + +.bib-range-preview{ + grid-column:1 / -1; + margin:-2px 0 2px; + font-size:12px; + line-height:1.3; +} + +.epr-line .bib-start, +.epr-line .bib-qty, +.epr-line .bib-end{ + width:100%; + min-width:0; + padding-left:4px; + padding-right:4px; +} + +.bib-field--idle, +.bib-field--pick, +.bib-field--calc{ + background-color:#eef1f4; + color:#6c757d; + cursor:pointer; +} + +.bib-field--idle{ + cursor:not-allowed; + opacity:.85; +} + +.bib-field--active{ + background-color:#fff; + color:inherit; + cursor:text; +} + +.bib-field--pick:focus, +.bib-field--calc:focus{ + outline:none; + box-shadow:none; +} + .epr-col-stats{ display:grid; grid-template-columns:56px 48px 48px; @@ -2067,14 +2144,6 @@ a.ms1-trad-link.btn-aide-trad{ min-width:0; } -.epr-line .bib-start, -.epr-line .bib-end{ - width:100%; - min-width:0; - padding-left:4px; - padding-right:4px; -} - .epr-add-range-row{ margin-top:6px; } @@ -2217,6 +2286,7 @@ a.ms1-trad-link.btn-aide-trad{ } .epr-row.auto-mode .bib-start, +.epr-row.auto-mode .bib-qty, .epr-row.auto-mode .bib-end { pointer-events: none; background: #eee; @@ -2232,6 +2302,7 @@ a.ms1-trad-link.btn-aide-trad{ } .epr-row.batch-mode .bib-start, +.epr-row.batch-mode .bib-qty, .epr-row.batch-mode .bib-end { pointer-events: none; background: #eee; diff --git a/inc_footer_scripts.php b/inc_footer_scripts.php index 89ca42d..f9aef3e 100644 --- a/inc_footer_scripts.php +++ b/inc_footer_scripts.php @@ -2613,17 +2613,227 @@ if ($strLangue == 'fr') { } if (container) { initModuleBibTippy(row); + initBibRangeRows(container); } syncBibUiState(row); refreshBibAnomaliesPanel(); return container; } + // MSIN-4429 — Séquence : Début puis Qté ou Fin (pilote / calculé). + function bibRangeParseInt(val) { + var n = parseInt(String(val || '').trim(), 10); + return isNaN(n) ? 0 : n; + } + + function bibRangeGetFields(row) { + if (!row) return null; + return { + start: row.querySelector('.bib-start'), + qty: row.querySelector('.bib-qty'), + end: row.querySelector('.bib-end'), + preview: row.querySelector('.bib-range-preview') + }; + } + + function bibRangeClearFieldClasses(row) { + var fields = bibRangeGetFields(row); + if (!fields) return; + [fields.start, fields.qty, fields.end].forEach(function (el) { + if (!el) return; + el.classList.remove('bib-field--idle', 'bib-field--pick', 'bib-field--calc', 'bib-field--active'); + }); + } + + function bibRangeUpdatePreview(row) { + var fields = bibRangeGetFields(row); + if (!fields || !fields.preview) return; + + var start = bibRangeParseInt(fields.start && fields.start.value); + var end = bibRangeParseInt(fields.end && fields.end.value); + var qty = bibRangeParseInt(fields.qty && fields.qty.value); + + if (start > 0 && end >= start && qty > 0) { + fields.preview.textContent = bibJsFmt('jsRangePreview', start, end, qty); + fields.preview.classList.remove('bib-ui-hidden'); + } else if (!row.dataset.rangePilot && bibJs('jsRangePickHint')) { + var startVal = bibRangeParseInt(fields.start && fields.start.value); + if (startVal > 0) { + fields.preview.textContent = bibJs('jsRangePickHint'); + fields.preview.classList.remove('bib-ui-hidden'); + } else { + fields.preview.textContent = ''; + fields.preview.classList.add('bib-ui-hidden'); + } + } else { + fields.preview.textContent = ''; + fields.preview.classList.add('bib-ui-hidden'); + } + } + + function applyBibRangeFieldState(row) { + var fields = bibRangeGetFields(row); + if (!fields || !fields.start || !fields.qty || !fields.end) return; + + if (row.dataset.locked === '1') { + bibRangeClearFieldClasses(row); + bibRangeUpdatePreview(row); + return; + } + + var start = bibRangeParseInt(fields.start.value); + var pilot = row.dataset.rangePilot || ''; + + bibRangeClearFieldClasses(row); + + if (start <= 0) { + fields.qty.classList.add('bib-field--idle'); + fields.end.classList.add('bib-field--idle'); + fields.qty.readOnly = true; + fields.end.readOnly = true; + fields.qty.value = ''; + fields.end.value = ''; + row.dataset.rangePilot = ''; + bibRangeUpdatePreview(row); + return; + } + + if (!pilot) { + fields.qty.classList.add('bib-field--pick'); + fields.end.classList.add('bib-field--pick'); + fields.qty.readOnly = true; + fields.end.readOnly = true; + bibRangeUpdatePreview(row); + return; + } + + if (pilot === 'qty') { + var qty = bibRangeParseInt(fields.qty.value); + fields.qty.classList.add('bib-field--active'); + fields.end.classList.add('bib-field--calc'); + fields.qty.readOnly = false; + fields.end.readOnly = true; + if (qty > 0) { + fields.end.value = String(start + qty - 1); + } else { + fields.end.value = ''; + } + } else if (pilot === 'end') { + var end = bibRangeParseInt(fields.end.value); + fields.end.classList.add('bib-field--active'); + fields.qty.classList.add('bib-field--calc'); + fields.end.readOnly = false; + fields.qty.readOnly = true; + if (end >= start) { + fields.qty.value = String(end - start + 1); + } else { + fields.qty.value = ''; + } + } + + bibRangeUpdatePreview(row); + } + + function onBibRangeFieldPick(row, pilot) { + if (!row || row.dataset.locked === '1') return; + + var fields = bibRangeGetFields(row); + if (!fields || !fields.start) return; + + if (bibRangeParseInt(fields.start.value) <= 0) { + fields.start.focus(); + return; + } + + row.dataset.rangePilot = pilot; + applyBibRangeFieldState(row); + + var target = pilot === 'qty' ? fields.qty : fields.end; + if (target) { + target.focus(); + target.select(); + } + } + + function onBibRangeStartChange(row) { + var fields = bibRangeGetFields(row); + if (!fields || !fields.start) return; + + var start = bibRangeParseInt(fields.start.value); + var lastStart = bibRangeParseInt(row.dataset.rangeLastStart || '0'); + + if (start !== lastStart && !row.dataset.rangePilot) { + fields.qty.value = ''; + fields.end.value = ''; + row.dataset.hasValues = '0'; + } + + row.dataset.rangeLastStart = String(start); + + if (start <= 0) { + row.dataset.rangePilot = ''; + } + + applyBibRangeFieldState(row); + } + + function bindBibRangeRow(row) { + if (!row || row.dataset.bibRangeBound === '1') return; + row.dataset.bibRangeBound = '1'; + + var fields = bibRangeGetFields(row); + if (!fields || !fields.start || !fields.qty || !fields.end) return; + + row.dataset.rangeLastStart = String(bibRangeParseInt(fields.start.value)); + + fields.start.addEventListener('input', function () { + onBibRangeStartChange(row); + }); + + fields.qty.addEventListener('input', function () { + row.dataset.rangePilot = 'qty'; + applyBibRangeFieldState(row); + }); + + fields.end.addEventListener('input', function () { + row.dataset.rangePilot = 'end'; + applyBibRangeFieldState(row); + }); + + fields.qty.addEventListener('click', function () { + onBibRangeFieldPick(row, 'qty'); + }); + + fields.end.addEventListener('click', function () { + onBibRangeFieldPick(row, 'end'); + }); + + fields.qty.addEventListener('focus', function () { + onBibRangeFieldPick(row, 'qty'); + }); + + fields.end.addEventListener('focus', function () { + onBibRangeFieldPick(row, 'end'); + }); + + applyBibRangeFieldState(row); + + if (row.dataset.id === '0' && !fields.start.value) { + fields.start.focus(); + } + } + + function initBibRangeRows(root) { + if (!root) return; + root.querySelectorAll('.bib-range').forEach(bindBibRangeRow); + } + function bibCsrfToken() { return moduleBib ? (moduleBib.dataset.csrfToken || '') : ''; } initModuleBibTippy(moduleBib); + initBibRangeRows(moduleBib); // ============================================================ // MSIN-4379 — Étape 2 : assignation automatique (interface JS) @@ -2676,7 +2886,7 @@ if ($strLangue == 'fr') { container.classList.remove('batch-mode'); } - row.querySelectorAll('.bib-start, .bib-end').forEach(input => { + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { input.removeAttribute('readonly'); }); @@ -2720,7 +2930,7 @@ if ($strLangue == 'fr') { container.classList.add('batch-mode'); } - row.querySelectorAll('.bib-start, .bib-end').forEach(input => { + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { input.setAttribute('readonly', true); }); @@ -2747,7 +2957,7 @@ if ($strLangue == 'fr') { bibContainer.classList.add('batch-mode'); - row.querySelectorAll('.bib-start, .bib-end').forEach(input => { + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { input.setAttribute('readonly', true); }); @@ -2990,7 +3200,7 @@ if ($strLangue == 'fr') { container.classList.add('batch-mode'); // 5. remettre readonly - container.querySelectorAll('.bib-start, .bib-end').forEach(input => { + container.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { input.setAttribute('readonly', true); }); @@ -3187,20 +3397,34 @@ if ($strLangue == 'fr') { errorDiv.innerText = ''; } - let start = row.querySelector('.bib-start').value; - let end = row.querySelector('.bib-end').value; + applyBibRangeFieldState(row); + + let start = bibRangeParseInt(row.querySelector('.bib-start').value); + let end = bibRangeParseInt(row.querySelector('.bib-end').value); let id = btnSave.dataset.id; // ligne temporaire vide = on la retire - if (id == 0 && (!start || !end)) { + if (id == 0 && start <= 0) { row.remove(); - if (errorDiv) errorDiv.remove(); + if (errorDiv && errorDiv.classList.contains('bib-error')) { + errorDiv.remove(); + } return; } - if (!start || !end) { - errorDiv.innerText = bibJs('jsFieldsRequired'); - errorDiv.style.display = 'block'; + if (start <= 0 || end <= 0) { + if (errorDiv) { + errorDiv.innerText = bibJs('jsRangeIncomplete') || bibJs('jsFieldsRequired'); + errorDiv.style.display = 'block'; + } + return; + } + + if (end < start) { + if (errorDiv) { + errorDiv.innerText = bibJs('jsRangeInvalid') || bibJs('jsFieldsRequired'); + errorDiv.style.display = 'block'; + } return; } @@ -3367,7 +3591,7 @@ if ($strLangue == 'fr') { }); container.classList.remove('batch-mode'); - row.querySelectorAll('.bib-start, .bib-end').forEach(input => { + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { input.removeAttribute('readonly'); }); @@ -3390,7 +3614,7 @@ if ($strLangue == 'fr') { row.classList.add('batch-mode'); container.classList.add('batch-mode'); - row.querySelectorAll('.bib-start, .bib-end').forEach(input => { + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { input.setAttribute('readonly', true); }); @@ -3491,7 +3715,7 @@ if ($strLangue == 'fr') { btnReset.textContent = row.dataset.resetLabel || btnReset.textContent; container.classList.remove('batch-mode'); - row.querySelectorAll('.bib-start, .bib-end').forEach(input => { + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { input.removeAttribute('readonly'); }); @@ -3521,7 +3745,7 @@ if ($strLangue == 'fr') { row.dataset.mode = 'reset'; container.classList.add('batch-mode'); - row.querySelectorAll('.bib-start, .bib-end').forEach(input => { + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { input.setAttribute('readonly', true); }); @@ -3553,7 +3777,7 @@ if ($strLangue == 'fr') { container.classList.add('batch-mode'); - row.querySelectorAll('.bib-start, .bib-end').forEach(input => { + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { input.setAttribute('readonly', true); }); @@ -3642,7 +3866,7 @@ if ($strLangue == 'fr') { btnBatch.textContent = row.dataset.batchLabel || btnBatch.textContent; } - row.querySelectorAll('.bib-start, .bib-end').forEach(input => { + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { input.removeAttribute('readonly'); }); diff --git a/php/inc_fx_promoteur.php b/php/inc_fx_promoteur.php index b4e1509..4cce9c3 100644 --- a/php/inc_fx_promoteur.php +++ b/php/inc_fx_promoteur.php @@ -3102,6 +3102,10 @@ function fxBibModuleJsDataAttrs() { 'lock-confirm' => 'bib_v4_js_lock_confirm', 'unlock-confirm' => 'bib_v4_js_unlock_confirm', 'auto-pending-bibs' => 'bib_v4_js_auto_pending_bibs', + 'range-incomplete' => 'bib_v4_js_range_incomplete', + 'range-invalid' => 'bib_v4_js_range_invalid', + 'range-preview' => 'bib_v4_js_range_preview', + 'range-pick-hint' => 'bib_v4_js_range_pick_hint', ]; $html = ''; foreach ($map as $attr => $clef) { @@ -3625,6 +3629,59 @@ function renderBibAddRangeButton($epr_id) { return ob_get_clean(); } +/** + * MSIN-4429 — Bloc Qté / ou / Fin (saisie pilote + champ calculé). + * + * @return array{qty:int|string,start:int,end:int,has_values:bool} + */ +function fxBibRangeSpecValues(array $range) { + $intStart = (int)($range['epr_bib_start'] ?? 0); + $intEnd = (int)($range['epr_bib_finish'] ?? 0); + $intId = (int)($range['epr_bib_id'] ?? 0); + $intQty = ''; + + if ($intStart > 0 && $intEnd >= $intStart) { + $intQty = $intEnd - $intStart + 1; + } + + return [ + 'qty' => $intQty, + 'start' => $intStart, + 'end' => $intEnd, + 'has_values' => ($intId > 0 && $intStart > 0 && $intEnd >= $intStart), + ]; +} + +/** MSIN-4429 — Inputs quantité + fin avec séparateur « ou ». */ +function fxBibRenderRangeQtyEndInputs(array $range, $blnRangeLocked) { + $tabSpec = fxBibRangeSpecValues($range); + $strQty = ($tabSpec['qty'] !== '') ? (int)$tabSpec['qty'] : ''; + $strEnd = ($tabSpec['end'] > 0) ? (int)$tabSpec['end'] : ''; + $strReadonly = $blnRangeLocked ? 'readonly' : ''; + + ob_start(); + ?> +
+ + > + + + > +
+
-
+
+
+ +
+
@@ -5143,13 +5204,15 @@ $nb_utilises = (int)($range['nb_utilises'] ?? 0); $mem_dispo = max(0, $nb_total - $nb_utilises); $blnRangeLocked = fxBibRangeIsLocked($range); +$tabSpec = fxBibRangeSpecValues($range); ?>
+ data-locked="" + data-has-values=""> @@ -5166,13 +5229,13 @@ $blnRangeLocked = fxBibRangeIsLocked($range); > + placeholder="" + + > - > +
+
+
diff --git a/php/inc_settings.php b/php/inc_settings.php index fe85ef0..378a69b 100644 --- a/php/inc_settings.php +++ b/php/inc_settings.php @@ -7,8 +7,8 @@ * Constantes * * **************/ -define('_VERSION_CODE', '4.72.727'); -define('_DATE_CODE', '2026-07-03'); +define('_VERSION_CODE', '4.72.728'); +define('_DATE_CODE', '2026-07-07'); //MSIN-4290 define('QR_SECRET_KEY', 'ms1_qr_2026_cle_secrete_longue_et_fixe'); // Outil temporaire ms1_kc_set.php — modifier key_chain_http (lien /kc/{pk}/) diff --git a/sql/MSIN-4429-bib-range-qty.sql b/sql/MSIN-4429-bib-range-qty.sql new file mode 100644 index 0000000..a103643 --- /dev/null +++ b/sql/MSIN-4429-bib-range-qty.sql @@ -0,0 +1 @@ +MSIN-4429 \ No newline at end of file