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