Merge branch 'salvataggio-manuale-parts' into filter

This commit is contained in:
2026-07-17 09:20:09 +02:00
2 changed files with 311 additions and 477 deletions
+3
View File
@@ -29,6 +29,9 @@
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; min-width: 0;">
<h6 style="margin: 0; white-space: nowrap;">Elenco Parti</h6>
<div style="display: flex; align-items: center; min-width: 0; gap: 8px;">
<button type="button" id="savePartsBtn" class="btn btn-success btn-sm">
<i class="fas fa-save"></i> Salva
</button>
<button type="button"
class="btn btn-dark btn-sm open-analysis-modal-btn"
id="openAnalysisModalBtn"
+308 -477
View File
@@ -245,7 +245,7 @@ $(document).ready(function () {
$row.data("extra-value-id", valId);
$(this).closest("td").find(".part-extra-value-id").val(valId);
saveRow($row);
markUnsaved();
});
$(document).on("click", ".extra-propagate-btn", function (e) {
@@ -316,9 +316,9 @@ $(document).ready(function () {
$row.data("extra-value-text", propagateValueText);
}
}
saveRow($row);
});
markUnsaved();
});
function applyExtraFieldColumn() {
@@ -530,6 +530,7 @@ $(document).ready(function () {
const finishLoading = function () {
unsavedChanges = false;
isLoadingPartsRecord = false;
updateSaveBtnState();
if (callback) callback();
};
@@ -809,7 +810,11 @@ $(document).ready(function () {
}
}
$("#partsModal").on("hide.bs.modal", function (e) {
$(document).on("hide.bs.modal", "#partsModal", function (e) {
console.log(
"[partsModal] hide.bs.modal — unsavedChanges =",
unsavedChanges,
);
if (
unsavedChanges &&
!confirm("Hai modifiche non salvate. Vuoi davvero uscire?")
@@ -987,15 +992,9 @@ $(document).ready(function () {
.get();
const maxPartNumber = partNumbers.length ? Math.max(...partNumbers) : 0;
// Crea la riga Mix
// Crea la riga Mix (verrà salvata col pulsante Salva)
addNewRow(maxPartNumber + 1, true);
const $mixRow = $("#partsTableBody tr:last");
// Consenti SOLO ora la creazione (INSERT) della riga Mix
$mixRow.data("allowCreateMix", true);
// esegue SUBITO l'INSERT così ottieni part-id
saveRow($mixRow);
markUnsaved();
});
function extractPartId(response) {
@@ -1183,7 +1182,7 @@ $(document).ready(function () {
})
.last();
// Se non esiste una riga Mix, ne creo una e la INSERISCO SUBITO (come fa il bottone in header)
// Se non esiste una riga Mix, ne creo una nuova (salvata col pulsante Salva)
if ($mixRow.length === 0) {
const maxPartNumber = Math.max(
...$("#partsTableBody tr")
@@ -1198,12 +1197,8 @@ $(document).ready(function () {
addNewRow(maxPartNumber + 1, true);
$mixRow = $("#partsTableBody tr:last");
$mixRow.find(".part-description").val(`Mix ${partDescription}`);
// Consenti la creazione (INSERT) della riga Mix e salvala subito
$mixRow.data("allowCreateMix", true);
saveRow($mixRow); // -> INSERT
return; // la descrizione include già l'elemento appena aggiunto
markUnsaved();
return;
}
// Aggiorna la descrizione del Mix esistente
@@ -1214,22 +1209,7 @@ $(document).ready(function () {
newDesc = currentMix + " + " + partDescription;
$mixRow.find(".part-description").val(newDesc);
// Se il Mix è già in salvataggio (INSERT o UPDATE in corso), accodiamo un solo UPDATE
if ($mixRow.data("saving") === true) {
// evita più code accumulate
if (!$mixRow.data("pendingUpdate")) {
$mixRow.data("pendingUpdate", true);
$mixRow.one("row:saved", function () {
$mixRow.removeData("pendingUpdate");
// ora che saving è false, salviamo l'ultima descrizione impostata
saveRow($mixRow);
});
}
} else {
// libero: salva subito
saveRow($mixRow);
}
markUnsaved();
});
function addNewRow(nextPartNumber, isMix = false) {
@@ -1291,186 +1271,21 @@ $(document).ready(function () {
$(document).on("click", ".save-note-btn", function () {
const $noteModal = $("#noteModal");
const $row = $noteModal.data("row");
const partId = $noteModal.data("part-id");
const note = $noteModal.find(".part-note").val().trim();
const iddatadb = $("#partsModal").data("iddatadb");
const idquotations = $("#partsModal").data("idquotations");
const endpoint = idquotations
? "save_parts_quotation.php"
: "save_parts.php";
const data = idquotations
? { idquotations: idquotations }
: { iddatadb: iddatadb };
// Raccogli tutti i dati della riga per evitare sovrascritture
const partNumber = $row.find(".part-number").val();
const partDescription = $row.find(".part-description").val().trim();
const mix = getRowMix($row);
const idmatrice = $row.find(".part-matrice").val() || null;
const dateexpiry = $row.find(".part-dateexpiry").val() || null;
if (partId && partId !== "new") {
const $saveStatus = $row.find(".save-status");
const $saveLoading = $row.find(".save-loading");
$saveLoading.show();
$saveStatus.hide();
$.ajax({
url: endpoint,
method: "POST",
data: JSON.stringify({
...data,
parts: [
{
id: partId,
part_number: partNumber,
part_description: partDescription,
mix: mix,
idmatrice: idmatrice,
note: note || null,
dateexpiry: dateexpiry,
},
],
}),
contentType: "application/json",
success: function (response) {
$saveLoading.hide();
if (response.success) {
$row.data("note", note);
$row.find(".note-btn").toggleClass("has-note", !!note);
$saveStatus.show();
setTimeout(() => $saveStatus.hide(), 2000);
bootstrap.Modal.getInstance(
document.getElementById("noteModal"),
).hide();
} else {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio della nota: ' +
response.message +
"</div>",
);
$("#partsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
}
},
error: function (xhr, status, error) {
$saveLoading.hide();
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio della nota: ' +
error +
" (" +
xhr.status +
")</div>",
);
$("#partsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
},
});
} else {
$row.data("note", note);
$row.find(".note-btn").toggleClass("has-note", !!note);
bootstrap.Modal.getInstance(
document.getElementById("noteModal"),
).hide();
markUnsaved();
}
$row.data("note", note);
$row.find(".note-btn").toggleClass("has-note", !!note);
bootstrap.Modal.getInstance(
document.getElementById("noteModal"),
).hide();
markUnsaved();
});
// ===================
// DATEEXPIRY HANDLING
// ===================
$(document).on("change", ".part-dateexpiry", function () {
const $input = $(this);
const $row = $input.closest("tr");
const partId = getPartId($row);
const dateexpiry = $input.val();
const iddatadb = $("#partsModal").data("iddatadb");
const idquotations = $("#partsModal").data("idquotations");
const endpoint = idquotations
? "save_parts_quotation.php"
: "save_parts.php";
const data = idquotations
? { idquotations: idquotations }
: { iddatadb: iddatadb };
// Raccogli tutti i dati della riga per evitare sovrascritture
const partNumber = $row.find(".part-number").val();
const partDescription = $row.find(".part-description").val().trim();
const mix = getRowMix($row);
const idmatrice = $row.find(".part-matrice").val() || null;
const note = $row.data("note") || null;
if (partId && partId !== "new") {
const $saveStatus = $row.find(".save-status");
const $saveLoading = $row.find(".save-loading");
$saveLoading.show();
$saveStatus.hide();
$.ajax({
url: endpoint,
method: "POST",
data: JSON.stringify({
...data,
parts: [
{
id: partId,
part_number: partNumber,
part_description: partDescription,
mix: mix,
idmatrice: idmatrice,
note: note,
dateexpiry: dateexpiry || null,
},
],
}),
contentType: "application/json",
success: function (response) {
$saveLoading.hide();
if (response.success) {
$saveStatus.show();
setTimeout(() => $saveStatus.hide(), 2000);
} else {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio della data: ' +
response.message +
"</div>",
);
$("#partsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
}
},
error: function (xhr, status, error) {
$saveLoading.hide();
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio della data: ' +
error +
" (" +
xhr.status +
")</div>",
);
$("#partsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
},
});
} else {
markUnsaved();
}
markUnsaved();
});
// ===================
@@ -1688,7 +1503,7 @@ $(document).ready(function () {
"blur",
".part-description, .part-number, .part-extra-field",
function () {
saveRow($(this).closest("tr"));
markUnsaved();
},
);
@@ -2033,11 +1848,17 @@ $(document).ready(function () {
const item = (data.results || [])[0];
if (item) {
const option = new Option(item.text, item.id, true, true);
if (!fromFilter) $select.append(option).trigger("change");
if (!fromFilter)
$select
.append(option)
.trigger("change", [{ skipHandler: true }]);
else $select.append(option);
partMatrice[partNumber] = item.id;
} else {
if (!fromFilter) $select.val(null).trigger("change");
if (!fromFilter)
$select
.val(null)
.trigger("change", [{ skipHandler: true }]);
partMatrice[partNumber] = null;
}
});
@@ -2050,71 +1871,10 @@ $(document).ready(function () {
const idmatrice = $(this).val();
const $row = $(this).closest("tr");
const partId = $row.data("part-id");
const partNumber = $row.find(".part-number").val();
const $saveStatus = $row.find(".save-status");
const $saveLoading = $row.find(".save-loading");
partMatrice[partNumber] = idmatrice || null;
if (partId && partId !== "new") {
$saveLoading.show();
$saveStatus.hide();
const iddatadb = $("#partsModal").data("iddatadb");
const idquotations = $("#partsModal").data("idquotations");
const endpoint = idquotations
? "save_matrice_quotation.php"
: "save_matrice.php";
const data = idquotations
? { idquotations: idquotations }
: { iddatadb: iddatadb };
$.ajax({
url: endpoint,
method: "POST",
data: JSON.stringify({
...data,
parts: [{ id: partId, idmatrice: idmatrice || null }],
}),
contentType: "application/json",
success: function (response) {
if (response.success) {
$saveLoading.hide();
$saveStatus.show();
setTimeout(() => $saveStatus.hide(), 2000);
} else {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio della matrice: ' +
response.message +
"</div>",
);
$("#partsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
$saveLoading.hide();
}
},
error: function (xhr, status, error) {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio della matrice: ' +
error +
" (" +
xhr.status +
")</div>",
);
$("#partsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
$saveLoading.hide();
},
});
}
markUnsaved();
});
// Messaggio se macro selezionata ma nessun risultato sarà gestito dal placeholder Select2
@@ -2178,7 +1938,153 @@ $(document).ready(function () {
}, 5000);
}
});
function saveAllParts(done = null) {
const $rows = $("#partsTableBody tr");
const iddatadb = $("#partsModal").data("iddatadb");
const idquotations = $("#partsModal").data("idquotations");
const endpoint = idquotations
? "save_parts_quotation.php"
: "save_parts.php";
const ctx = idquotations ? { idquotations } : { iddatadb };
if (!iddatadb && !idquotations) {
if (done) done(false);
return;
}
// Stesso criterio del PHP: una riga nuova viene salvata solo se ha
// descrizione, nota o data. Filtriamo qui per tenere l'allineamento
// per indice tra righe inviate e results[] restituiti.
const rowHasContent = ($row) => {
const hasId = !!getPartId($row);
const desc = $row.find(".part-description").val().trim();
const note = $row.data("note") || "";
const date = $row.find(".part-dateexpiry").val() || "";
return hasId || desc || note || date;
};
const $rowsToSave = $rows.filter(function () {
return rowHasContent($(this));
});
const partsToSave = $rowsToSave
.map(function () {
const $row = $(this);
return {
id: getPartId($row), // null se "new"/""
part_number: $row.find(".part-number").val(),
part_description: $row
.find(".part-description")
.val()
.trim(),
mix: getRowMix($row),
idmatrice: $row.find(".part-matrice").val() || null,
dateexpiry: $row.find(".part-dateexpiry").val() || null,
note: $row.data("note") || null,
extra_field_id:
$row.find(".part-extra-field-id").val() || null,
extra_value_id:
$row.find(".part-extra-value-id").val() || null,
extra_value_text: $row.find(".part-extra-field").length
? ($row.find(".part-extra-field").val() || "").trim()
: null,
};
})
.get();
if (partsToSave.length === 0) {
unsavedChanges = false;
if (done) done(true);
return;
}
const $btn = $("#savePartsBtn");
const originalHtml = $btn.length ? $btn.html() : "";
if ($btn.length) {
$btn.prop("disabled", true).html(
'<i class="fas fa-spinner fa-spin"></i> Salvataggio...',
);
}
$rowsToSave.find(".save-loading").show();
$rowsToSave.find(".save-status").hide();
$.ajax({
url: endpoint,
method: "POST",
data: JSON.stringify({ ...ctx, parts: partsToSave }),
contentType: "application/json",
success: function (response) {
if ($btn.length)
$btn.prop("disabled", false).html(originalHtml);
$rowsToSave.find(".save-loading").hide();
if (response.success) {
// results[] è nell'ordine delle righe inviate
const results = response.results || [];
$rowsToSave.each(function (i) {
const r = results[i];
const newId = r ? r.part_id || r.id : null;
if (newId) setPartId($(this), newId);
});
$rowsToSave.find(".save-status").show();
setTimeout(
() => $rowsToSave.find(".save-status").hide(),
2000,
);
unsavedChanges = false;
updateSaveBtnState();
if (!$("#quotationeBtn").hasClass("d-none"))
$("#quotationeBtn").addClass("d-none");
if (done) done(true);
} else {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio: ' +
(response.message || "Errore sconosciuto") +
"</div>",
);
$("#partsModal .modal-body").prepend(errorMsg);
setTimeout(() => {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
if (done) done(false);
}
},
error: function (xhr, status, error) {
if ($btn.length)
$btn.prop("disabled", false).html(originalHtml);
$rowsToSave.find(".save-loading").hide();
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio: ' +
error +
" (" +
xhr.status +
")</div>",
);
$("#partsModal .modal-body").prepend(errorMsg);
setTimeout(() => {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
if (done) done(false);
},
});
}
$(document).on("click", "#savePartsBtn", function (e) {
e.preventDefault();
saveAllParts();
});
window.saveAllParts = saveAllParts;
function renumberParts() {
console.log(
"Inizio rinumera parts, numero righe:",
@@ -2318,16 +2224,37 @@ $(document).ready(function () {
function markUnsaved() {
if (isLoadingPartsRecord) return;
if (!unsavedChanges) {
unsavedChanges = true;
}
unsavedChanges = true;
updateSaveBtnState();
}
// Aggiorna aspetto del tasto Salva in base a unsavedChanges
function updateSaveBtnState() {
const $btn = $("#savePartsBtn");
if (!$btn.length) return;
if (unsavedChanges) {
$btn.removeClass("btn-success").addClass("btn-danger");
} else {
$btn.removeClass("btn-danger").addClass("btn-success");
}
}
// Catch-all: modifiche manuali negli input della tabella parti segnano "non salvato".
// I <select> (matrici Select2) sono esclusi: le loro modifiche reali sono già
// gestite in initializeSelect2, mentre l'inizializzazione async genererebbe falsi positivi.
$(document).on(
"input change",
"#partsTableBody input, #partsTableBody select",
markUnsaved,
"input keyup",
"#partsTableBody input:not(.part-matrice), #partsTableBody textarea",
function () {
markUnsaved();
},
);
$(window).on("beforeunload", function () {
if (unsavedChanges && $("#partsModal").hasClass("show")) {
return "Hai modifiche non salvate.";
}
});
$(document).on(
"click",
".add-row-global, .add-mix-global, .add-mix-row, .remove-row, .propagate-matrice-btn, .propagate-all-btn, .note-btn",
@@ -2612,102 +2539,8 @@ $(document).ready(function () {
$(document).on("change", ".propagate-date-input", function () {
const dateexpiry = $(this).val();
const iddatadb = $("#partsModal").data("iddatadb");
const idquotations = $("#partsModal").data("idquotations");
const endpoint = idquotations
? "save_parts_quotation.php"
: "save_parts.php";
const data = idquotations
? { idquotations: idquotations }
: { iddatadb: iddatadb };
const partsToSave = [];
$("#partsTableBody tr").each(function () {
const $row = $(this);
const partId = $row.data("part-id");
const partNumber = $row.find(".part-number").val();
const partDescription = $row.find(".part-description").val().trim();
const mix = $row.attr("data-is-mix") === "Y" ? "Y" : "N";
const idmatrice = $row.find(".part-matrice").val() || null;
const note = $row.data("note") || null;
partsToSave.push({
id: partId && partId !== "new" ? partId : null,
part_number: partNumber,
part_description: partDescription,
mix: mix,
idmatrice: idmatrice,
note: note,
dateexpiry: dateexpiry || null,
});
if (partId && partId !== "new") {
$row.find(".save-loading").show();
$row.find(".save-status").hide();
}
});
if (partsToSave.length > 0) {
$.ajax({
url: endpoint,
method: "POST",
data: JSON.stringify({
...data,
parts: partsToSave,
}),
contentType: "application/json",
success: function (response) {
$("#partsTableBody tr").each(function () {
const $row = $(this);
const partId = $row.data("part-id");
if (partId && partId !== "new") {
$row.find(".save-loading").hide();
$row.find(".save-status").show();
setTimeout(
() => $row.find(".save-status").hide(),
2000,
);
}
$row.find(".part-dateexpiry").val(dateexpiry);
});
if (!response.success) {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio della data comune: ' +
response.message +
"</div>",
);
$("#partsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
}
},
error: function (xhr, status, error) {
$("#partsTableBody tr").each(function () {
const $row = $(this);
$row.find(".save-loading").hide();
});
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio della data comune: ' +
error +
" (" +
xhr.status +
")</div>",
);
$("#partsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
},
});
} else {
$("#partsTableBody tr").find(".part-dateexpiry").val(dateexpiry);
markUnsaved();
}
$("#partsTableBody tr").find(".part-dateexpiry").val(dateexpiry);
markUnsaved();
});
$(document).on("click", ".propagate-note-btn", function () {
@@ -2723,113 +2556,16 @@ $(document).on("click", ".propagate-note-btn", function () {
$(document).on("click", ".save-common-note-btn", function () {
const $commonNoteModal = $("#commonNoteModal");
const note = $commonNoteModal.find(".part-note").val().trim();
const iddatadb = $("#partsModal").data("iddatadb");
const idquotations = $("#partsModal").data("idquotations");
const endpoint = idquotations
? "save_parts_quotation.php"
: "save_parts.php";
const data = idquotations
? { idquotations: idquotations }
: { iddatadb: iddatadb };
const partsToSave = [];
$("#partsTableBody tr").each(function () {
const $row = $(this);
const partId = $row.data("part-id");
const partNumber = $row.find(".part-number").val();
const partDescription = $row.find(".part-description").val().trim();
const mix = $row.attr("data-is-mix") === "Y" ? "Y" : "N";
const idmatrice = $row.find(".part-matrice").val() || null;
const dateexpiry = $row.find(".part-dateexpiry").val() || null;
partsToSave.push({
id: partId && partId !== "new" ? partId : null,
part_number: partNumber,
part_description: partDescription,
mix: mix,
idmatrice: idmatrice,
note: note || null,
dateexpiry: dateexpiry,
});
if (partId && partId !== "new") {
$row.find(".save-loading").show();
$row.find(".save-status").hide();
}
$row.data("note", note);
$row.find(".note-btn").toggleClass("has-note", !!note);
});
if (partsToSave.length > 0) {
$.ajax({
url: endpoint,
method: "POST",
data: JSON.stringify({
...data,
parts: partsToSave,
}),
contentType: "application/json",
success: function (response) {
$("#partsTableBody tr").each(function () {
const $row = $(this);
const partId = $row.data("part-id");
if (partId && partId !== "new") {
$row.find(".save-loading").hide();
$row.find(".save-status").show();
setTimeout(
() => $row.find(".save-status").hide(),
2000,
);
}
$row.data("note", note);
$row.find(".note-btn").toggleClass("has-note", !!note);
});
bootstrap.Modal.getInstance(
document.getElementById("commonNoteModal"),
).hide();
if (!response.success) {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio della nota comune: ' +
response.message +
"</div>",
);
$("#partsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
}
},
error: function (xhr, status, error) {
$("#partsTableBody tr").each(function () {
const $row = $(this);
$row.find(".save-loading").hide();
});
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio della nota comune: ' +
error +
" (" +
xhr.status +
")</div>",
);
$("#partsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
},
});
} else {
$("#partsTableBody tr").each(function () {
const $row = $(this);
$row.data("note", note);
$row.find(".note-btn").toggleClass("has-note", !!note);
});
bootstrap.Modal.getInstance(
document.getElementById("commonNoteModal"),
).hide();
markUnsaved();
}
bootstrap.Modal.getInstance(
document.getElementById("commonNoteModal"),
).hide();
markUnsaved();
});
$(document).on("click", "#showHideImageBtn", function () {
@@ -2991,3 +2727,98 @@ $(document).on("click", "#showHideImageBtn", function () {
window.initPartsResizableColumns = init;
window.applyPartsColumnWidths = syncColgroupToHeaders;
})();
// ===================
// NAVIGAZIONE TABELLA PARTI CON FRECCE TASTIERA
// ===================
(function () {
// Selettore dei campi navigabili in ogni riga, in ordine di colonna.
// I select Matrice (Select2) sono esclusi di proposito.
const NAV_SELECTOR =
".part-number, .part-description, .part-dateexpiry, .part-extra-field";
// Ritorna la lista ordinata dei campi navigabili della riga
function navFields($row) {
return $row.find(NAV_SELECTOR).filter(":visible");
}
// Il cursore è all'inizio del testo dell'input?
function atStart(el) {
if (el.type === "date" || el.type === "number") return true;
return el.selectionStart === 0 && el.selectionEnd === 0;
}
// Il cursore è alla fine del testo dell'input?
function atEnd(el) {
if (el.type === "date" || el.type === "number") return true;
const len = (el.value || "").length;
return el.selectionStart === len && el.selectionEnd === len;
}
function focusField($field) {
if (!$field || !$field.length) return;
const el = $field.get(0);
el.focus();
// Seleziona il testo per comodità (non sui date input)
if (el.type !== "date" && typeof el.select === "function") {
try {
el.select();
} catch (e) {}
}
}
$(document).on("keydown", "#partsTableBody " + NAV_SELECTOR, function (e) {
// Ignora se un dropdown Select2 è aperto
if ($(".select2-container--open").length) return;
const key = e.key;
if (
key !== "ArrowUp" &&
key !== "ArrowDown" &&
key !== "ArrowLeft" &&
key !== "ArrowRight"
) {
return;
}
const el = this;
const $cur = $(el);
const $row = $cur.closest("tr");
const fields = navFields($row);
const colIndex = fields.index(el);
if (colIndex === -1) return;
// --- SU / GIÙ: stessa colonna, riga adiacente ---
if (key === "ArrowUp" || key === "ArrowDown") {
const $targetRow =
key === "ArrowUp" ? $row.prev("tr") : $row.next("tr");
if (!$targetRow.length) return;
e.preventDefault();
const targetFields = navFields($targetRow);
// Stessa colonna se esiste, altrimenti l'ultima disponibile
const $target = targetFields.eq(colIndex).length
? targetFields.eq(colIndex)
: targetFields.last();
focusField($target);
return;
}
// --- SINISTRA: campo precedente, solo se cursore a inizio testo ---
if (key === "ArrowLeft") {
if (!atStart(el)) return; // lascia muovere il cursore nel testo
if (colIndex <= 0) return;
e.preventDefault();
focusField(fields.eq(colIndex - 1));
return;
}
// --- DESTRA: campo successivo, solo se cursore a fine testo ---
if (key === "ArrowRight") {
if (!atEnd(el)) return;
if (colIndex >= fields.length - 1) return;
e.preventDefault();
focusField(fields.eq(colIndex + 1));
return;
}
});
})();