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.
This commit is contained in:
2026-07-07 14:49:57 -04:00
parent f35d651ffc
commit ec024e6fba
5 changed files with 397 additions and 36 deletions

View File

@ -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;

View File

@ -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');
});

View File

@ -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();
?>
<div class="bib-range-spec">
<input type="number"
class="form-control form-control-sm bib-qty"
min="1"
value="<?php echo $strQty; ?>"
placeholder="<?php echo fxBibEsc(fxBibTexte('bib_v4_range_qty_ph', 0)); ?>"
<?php echo $strReadonly; ?>
<?php echo fxBibAideTippyAttr('bib_v4_col_qty'); ?>>
<span class="bib-range-or" aria-hidden="true"><?php fxBibTexte('bib_v4_range_or', 1); ?></span>
<input type="number"
class="form-control form-control-sm bib-end"
min="1"
value="<?php echo $strEnd; ?>"
placeholder="<?php echo fxBibEsc(fxBibTexte('bib_v4_range_end_ph', 0)); ?>"
<?php echo $strReadonly; ?>
<?php echo fxBibAideTippyAttr('bib_v4_col_end'); ?>>
</div>
<?php
return ob_get_clean();
}
/** En-tête colonnes séquences — répété par épreuve, inclus dans renderBibRanges (y compris AJAX). */
function renderBibRangeHeader() {
ob_start();
@ -3633,7 +3690,11 @@ function renderBibRangeHeader() {
<div class="epr-col-lock"><?php echo fxBibColHeader('bib_v4_col_lock'); ?></div>
<div><?php echo fxBibColHeader('bib_v4_col_view'); ?></div>
<div><?php echo fxBibColHeader('bib_v4_col_start'); ?></div>
<div><?php echo fxBibColHeader('bib_v4_col_end'); ?></div>
<div class="epr-col-range-spec">
<div><?php echo fxBibColHeader('bib_v4_col_qty'); ?></div>
<div class="bib-range-or bib-range-or--header" aria-hidden="true"><?php fxBibTexte('bib_v4_range_or', 1); ?></div>
<div><?php echo fxBibColHeader('bib_v4_col_end'); ?></div>
</div>
<div><?php echo fxBibColHeader('bib_v4_col_ok'); ?></div>
<div><?php echo fxBibColHeader('bib_v4_col_del'); ?></div>
<div class="epr-col-stats">
@ -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);
?>
<div class="bib-range epr-line<?php echo $blnRangeLocked ? ' bib-range--locked' : ''; ?>"
data-id="<?php echo $range['epr_bib_id']; ?>"
data-locked="<?php echo $blnRangeLocked ? '1' : '0'; ?>">
data-locked="<?php echo $blnRangeLocked ? '1' : '0'; ?>"
data-has-values="<?php echo $tabSpec['has_values'] ? '1' : '0'; ?>">
<?php echo fxBibRenderRangeLockButton($range, $epr_id); ?>
@ -5166,13 +5229,13 @@ $blnRangeLocked = fxBibRangeIsLocked($range);
<input type="number"
class="form-control form-control-sm bib-start"
min="1"
value="<?php echo $range['epr_bib_start']; ?>"
<?php echo $blnRangeLocked ? 'readonly' : ''; ?>>
placeholder="<?php echo fxBibEsc(fxBibTexte('bib_v4_range_start_ph', 0)); ?>"
<?php echo $blnRangeLocked ? 'readonly' : ''; ?>
<?php echo fxBibAideTippyAttr('bib_v4_col_start'); ?>>
<input type="number"
class="form-control form-control-sm bib-end"
value="<?php echo $range['epr_bib_finish']; ?>"
<?php echo $blnRangeLocked ? 'readonly' : ''; ?>>
<?php echo fxBibRenderRangeQtyEndInputs($range, $blnRangeLocked); ?>
<?php echo fxBibBtnAideSegmentOpen(); ?>
<button type="button"
@ -5245,6 +5308,8 @@ if (!$blnRangeLocked && (
</div>
</div>
<div class="bib-range-preview text-muted small bib-ui-hidden" aria-live="polite"></div>
</div>
<div class="bib-error text-danger small mb-2" style="<?php echo (!empty($range['liste_trous']) || !empty($range['liste_doublons'])) ? 'display:block;' : 'display:none;'; ?>">

View File

@ -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}/)

View File

@ -0,0 +1 @@
MSIN-4429