filter on imported
This commit is contained in:
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* gridFilter.js — Selezione righe + filtri colonna a scomparsa per imported.php
|
||||
* Dipende da window.gridRenderer (gridRenderer.js). Includere DOPO gridRenderer.js.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
let filtersActive = false;
|
||||
let restricted = false;
|
||||
const selected = new Set(); // iddatadb selezionati
|
||||
const colFilters = {}; // colKey -> testo filtro
|
||||
let restrictedIds = null; // Set di iddatadb visibili quando ristretto
|
||||
|
||||
function R() {
|
||||
return window.gridRenderer;
|
||||
}
|
||||
function data() {
|
||||
return R().getData();
|
||||
}
|
||||
function meta() {
|
||||
return R().getMeta();
|
||||
}
|
||||
|
||||
// ── Filtro: decide se una riga passa i filtri colonna ──
|
||||
function rowMatchesFilters(row) {
|
||||
for (const [key, term] of Object.entries(colFilters)) {
|
||||
if (!term) continue;
|
||||
const t = term.toLowerCase();
|
||||
const val = getRowColValue(row, key);
|
||||
if (!String(val).toLowerCase().includes(t)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getRowColValue(row, key) {
|
||||
const cols = meta().columns || [];
|
||||
const col = cols.find((c) => String(c.key) === String(key));
|
||||
if (!col) return "";
|
||||
switch (col.type) {
|
||||
case "detail":
|
||||
case "main_field":
|
||||
return row.details?.[String(key)] ?? "";
|
||||
case "fixed":
|
||||
return row.fixedFields?.[key] ?? "";
|
||||
case "idclient":
|
||||
return row.idclient ?? "";
|
||||
case "cliente_fornitore_id":
|
||||
return row.cliente_fornitore_id ?? "";
|
||||
case "tested_component":
|
||||
return row.tested_component ?? "";
|
||||
case "static":
|
||||
return row[key] ?? "";
|
||||
case "status":
|
||||
return row.status ?? "";
|
||||
default:
|
||||
return row[key] ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Applica visibilità righe (filtri + restrizione) ──
|
||||
function applyVisibility() {
|
||||
const rows = document.querySelectorAll("#gridRowContainer .grid-row");
|
||||
rows.forEach((rowEl) => {
|
||||
const id = parseInt(rowEl.dataset.id, 10);
|
||||
const row = data().find((r) => String(r.iddatadb) === String(id));
|
||||
let show = true;
|
||||
if (row && !rowMatchesFilters(row)) show = false;
|
||||
if (restricted && restrictedIds && !restrictedIds.has(id))
|
||||
show = false;
|
||||
rowEl.style.display = show ? "" : "none";
|
||||
});
|
||||
window.dispatchEvent(new Event("resize")); // ricalcola scrollbar top
|
||||
}
|
||||
|
||||
// ── Lista iddatadb attualmente visibili (per propagazione/export ecc.) ──
|
||||
window.getVisibleGridIds = function () {
|
||||
const ids = [];
|
||||
document
|
||||
.querySelectorAll("#gridRowContainer .grid-row")
|
||||
.forEach((el) => {
|
||||
if (el.style.display !== "none")
|
||||
ids.push(parseInt(el.dataset.id, 10));
|
||||
});
|
||||
return ids.filter(Boolean);
|
||||
};
|
||||
|
||||
// ── Righe target per operazioni bulk (propaga/export/clona/save) ──
|
||||
// Regola: filtri attivi + selezione → solo selezionate;
|
||||
// filtri attivi senza selezione → solo visibili;
|
||||
// filtri non attivi → null (= tutte, comportamento originale).
|
||||
window.getTargetGridIds = function () {
|
||||
if (!filtersActive) return null; // null = nessun vincolo
|
||||
if (selected.size > 0) return new Set([...selected]);
|
||||
return new Set(window.getVisibleGridIds());
|
||||
};
|
||||
|
||||
// Helper: true se l'id è tra i target (o se non c'è vincolo)
|
||||
window.isTargetGridId = function (id) {
|
||||
const t = window.getTargetGridIds();
|
||||
if (t === null) return true;
|
||||
return t.has(parseInt(id, 10));
|
||||
};
|
||||
|
||||
// ── Toggle selezione riga ──
|
||||
function toggleRow(id, on) {
|
||||
id = parseInt(id, 10);
|
||||
if (on) selected.add(id);
|
||||
else selected.delete(id);
|
||||
const rowEl = document.querySelector(
|
||||
`#gridRowContainer .grid-row[data-id="${id}"]`,
|
||||
);
|
||||
if (rowEl) {
|
||||
rowEl.classList.toggle("row-selected", selected.has(id));
|
||||
const cb = rowEl.querySelector(".filter-row-checkbox");
|
||||
if (cb) cb.checked = selected.has(id);
|
||||
}
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
// ── Inietta checkbox nelle righe visibili ──
|
||||
// ── Inietta checkbox dentro la cella Actions (già sticky) ──
|
||||
function injectCheckboxes() {
|
||||
document
|
||||
.querySelectorAll("#gridRowContainer .grid-row")
|
||||
.forEach((rowEl) => {
|
||||
const btnCell = rowEl.querySelector(".button-cell");
|
||||
if (!btnCell) return;
|
||||
if (btnCell.querySelector(".filter-row-checkbox")) return;
|
||||
const id = parseInt(rowEl.dataset.id, 10);
|
||||
const wrap = document.createElement("label");
|
||||
wrap.className = "filter-cb-wrap";
|
||||
wrap.innerHTML = `<input type="checkbox" class="filter-row-checkbox" ${selected.has(id) ? "checked" : ""}>`;
|
||||
btnCell.insertBefore(wrap, btnCell.firstChild);
|
||||
if (selected.has(id)) rowEl.classList.add("row-selected");
|
||||
});
|
||||
}
|
||||
|
||||
function removeCheckboxes() {
|
||||
document
|
||||
.querySelectorAll(".filter-cb-wrap")
|
||||
.forEach((el) => el.remove());
|
||||
document
|
||||
.querySelectorAll(".grid-row.row-selected")
|
||||
.forEach((el) => el.classList.remove("row-selected"));
|
||||
}
|
||||
|
||||
// ── Riga input filtri colonna (allineata alle larghezze reali) ──
|
||||
// ── Riga input filtri colonna (allineata alle larghezze reali) ──
|
||||
function injectFilterRow() {
|
||||
if (document.getElementById("gridFilterRow")) return;
|
||||
const top = document.getElementById("gridTopContainer");
|
||||
const header = document.getElementById("gridHeaderContainer");
|
||||
if (!top || !header) return;
|
||||
|
||||
const fr = document.createElement("div");
|
||||
fr.className = "grid-row grid-filter-row";
|
||||
fr.id = "gridFilterRow";
|
||||
|
||||
// cella allineata alla colonna Actions (sticky, legge larghezza reale)
|
||||
const actHeader = header.querySelector(".button-header");
|
||||
const actCell = document.createElement("div");
|
||||
actCell.className = "grid-cell button-cell filter-actions-cell";
|
||||
const aw = actHeader ? actHeader.offsetWidth : 220;
|
||||
actCell.style.flex = `0 0 ${aw}px`;
|
||||
actCell.style.minWidth = `${aw}px`;
|
||||
actCell.innerHTML = `<label class="filter-cb-wrap" style="display:inline-flex;" title="Seleziona tutti i filtrati"><input type="checkbox" id="filterSelectAll"></label>`;
|
||||
fr.appendChild(actCell);
|
||||
|
||||
// una cella per colonna, larghezza letta dall'header reale
|
||||
const headerCells = header.querySelectorAll(
|
||||
".grid-header:not(.button-header)",
|
||||
);
|
||||
(meta().columns || []).forEach((col, i) => {
|
||||
const hc = headerCells[i];
|
||||
const w = hc ? hc.offsetWidth : col.width || 150;
|
||||
const cell = document.createElement("div");
|
||||
cell.className = "grid-cell";
|
||||
cell.style.flex = `0 0 ${w}px`;
|
||||
if (col.type === "tracking" || col.type === "awb") {
|
||||
// niente filtro
|
||||
} else {
|
||||
cell.innerHTML = `<input type="text" class="filter-col-input" data-col-key="${col.key}" placeholder="🔍 ${col.label || ""}" value="${colFilters[col.key] || ""}" style="width:100%;padding:3px 6px;font-size:12px;border:1px solid #ced4da;border-radius:4px;">`;
|
||||
}
|
||||
fr.appendChild(cell);
|
||||
});
|
||||
|
||||
top.parentNode.insertBefore(fr, top);
|
||||
}
|
||||
|
||||
function removeFilterRow() {
|
||||
const fr = document.getElementById("gridFilterRow");
|
||||
if (fr) fr.remove();
|
||||
}
|
||||
|
||||
// ── Toolbar (bottoni contestuali) ──
|
||||
function updateToolbar() {
|
||||
const bar = document.getElementById("filterActionBar");
|
||||
if (!bar) return;
|
||||
const count = selected.size;
|
||||
bar.style.display = filtersActive ? "inline-flex" : "none";
|
||||
const cntEl = document.getElementById("filterSelCount");
|
||||
if (cntEl) cntEl.textContent = count;
|
||||
document.getElementById("filterDeleteBtn").disabled = count === 0;
|
||||
document.getElementById("filterRestrictBtn").disabled =
|
||||
count === 0 && !restricted;
|
||||
document.getElementById("filterRestrictBtn").innerHTML = restricted
|
||||
? '<i class="fas fa-eye"></i> Mostra tutti'
|
||||
: '<i class="fas fa-compress"></i> Restringi selezione';
|
||||
}
|
||||
|
||||
// ── Main toggle ──
|
||||
function toggleFilters() {
|
||||
filtersActive = !filtersActive;
|
||||
const grid = document.getElementById("gridContainer");
|
||||
const btn = document.getElementById("filtersToggleBtn");
|
||||
if (filtersActive) {
|
||||
grid.classList.add("filters-on");
|
||||
btn.classList.add("active");
|
||||
injectCheckboxes();
|
||||
injectFilterRow();
|
||||
} else {
|
||||
grid.classList.remove("filters-on");
|
||||
btn.classList.remove("active");
|
||||
removeCheckboxes();
|
||||
removeFilterRow();
|
||||
}
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
// ── Batch delete ──
|
||||
async function batchDelete() {
|
||||
const ids = [...selected];
|
||||
if (!ids.length) return;
|
||||
if (
|
||||
!confirm(
|
||||
`Eliminare ${ids.length} righe dal database? Operazione irreversibile.`,
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
const btn = document.getElementById("filterDeleteBtn");
|
||||
btn.disabled = true;
|
||||
const original = btn.innerHTML;
|
||||
|
||||
let ok = 0,
|
||||
fail = 0;
|
||||
for (const id of ids) {
|
||||
try {
|
||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${ok + fail + 1}/${ids.length}`;
|
||||
const resp = await fetch("delete_record.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id: id }),
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (json.success) {
|
||||
ok++;
|
||||
// rimuovi da gridData
|
||||
const arr = data();
|
||||
const idx = arr.findIndex(
|
||||
(r) => String(r.iddatadb) === String(id),
|
||||
);
|
||||
if (idx !== -1) arr.splice(idx, 1);
|
||||
selected.delete(id);
|
||||
if (restrictedIds) restrictedIds.delete(id);
|
||||
} else fail++;
|
||||
} catch (e) {
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
btn.innerHTML = original;
|
||||
R().renderVisibleRows();
|
||||
if (filtersActive) {
|
||||
injectCheckboxes();
|
||||
}
|
||||
applyVisibility();
|
||||
updateToolbar();
|
||||
alert(`Eliminate: ${ok}${fail ? " — Falliti: " + fail : ""}`);
|
||||
}
|
||||
|
||||
// ── Restringi / mostra tutti ──
|
||||
function toggleRestrict() {
|
||||
if (!restricted) {
|
||||
if (!selected.size) return;
|
||||
restrictedIds = new Set([...selected]);
|
||||
restricted = true;
|
||||
} else {
|
||||
restricted = false;
|
||||
restrictedIds = null;
|
||||
}
|
||||
applyVisibility();
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
// ── Costruzione UI toolbar (bottoni in cima) ──
|
||||
function buildUI() {
|
||||
const addBtn = document.getElementById("addRowBtn");
|
||||
if (!addBtn) return;
|
||||
|
||||
// bottone Filters
|
||||
const fBtn = document.createElement("button");
|
||||
fBtn.type = "button";
|
||||
fBtn.id = "filtersToggleBtn";
|
||||
fBtn.className = "btn btn-outline-info btn-sm";
|
||||
fBtn.style.flexShrink = "0";
|
||||
fBtn.innerHTML = '<i class="fas fa-filter"></i> Filters';
|
||||
addBtn.parentNode.insertBefore(fBtn, addBtn.nextSibling);
|
||||
|
||||
// barra azioni contestuali
|
||||
const bar = document.createElement("span");
|
||||
bar.id = "filterActionBar";
|
||||
bar.style.cssText =
|
||||
"display:none;align-items:center;gap:8px;flex-shrink:0;";
|
||||
bar.innerHTML = `
|
||||
<span style="font-size:12px;color:#555;">Selezionate: <strong id="filterSelCount">0</strong></span>
|
||||
<button type="button" id="filterRestrictBtn" class="btn btn-outline-primary btn-sm"><i class="fas fa-compress"></i> Restringi selezione</button>
|
||||
<button type="button" id="filterDeleteBtn" class="btn btn-outline-danger btn-sm"><i class="fas fa-trash"></i> Elimina selezionati</button>
|
||||
`;
|
||||
fBtn.parentNode.insertBefore(bar, fBtn.nextSibling);
|
||||
|
||||
fBtn.addEventListener("click", toggleFilters);
|
||||
document
|
||||
.getElementById("filterDeleteBtn")
|
||||
.addEventListener("click", batchDelete);
|
||||
document
|
||||
.getElementById("filterRestrictBtn")
|
||||
.addEventListener("click", toggleRestrict);
|
||||
}
|
||||
|
||||
// ── Event delegation ──
|
||||
function attachEvents() {
|
||||
// checkbox click
|
||||
document.addEventListener("change", function (e) {
|
||||
if (e.target.classList.contains("filter-row-checkbox")) {
|
||||
const rowEl = e.target.closest(".grid-row");
|
||||
toggleRow(rowEl.dataset.id, e.target.checked);
|
||||
}
|
||||
});
|
||||
|
||||
// Seleziona / deseleziona tutti i FILTRATI visibili
|
||||
document.addEventListener("change", function (e) {
|
||||
if (e.target.id !== "filterSelectAll") return;
|
||||
const on = e.target.checked;
|
||||
document
|
||||
.querySelectorAll("#gridRowContainer .grid-row")
|
||||
.forEach((rowEl) => {
|
||||
if (rowEl.style.display === "none") return;
|
||||
const id = parseInt(rowEl.dataset.id, 10);
|
||||
if (!id) return;
|
||||
if (on) selected.add(id);
|
||||
else selected.delete(id);
|
||||
rowEl.classList.toggle("row-selected", on);
|
||||
const cb = rowEl.querySelector(".filter-row-checkbox");
|
||||
if (cb) cb.checked = on;
|
||||
});
|
||||
updateToolbar();
|
||||
});
|
||||
|
||||
// click su QUALSIASI punto della riga (tranne campi interattivi) = seleziona
|
||||
document.addEventListener("click", function (e) {
|
||||
if (!filtersActive) return;
|
||||
const rowEl = e.target.closest("#gridRowContainer .grid-row");
|
||||
if (!rowEl) return;
|
||||
|
||||
// ignora click su elementi interattivi
|
||||
if (
|
||||
e.target.closest(
|
||||
"input, select, textarea, button, a, .select2-container, .action-btn, .propagate-btn, .add-part-btn",
|
||||
)
|
||||
) {
|
||||
// ma se ho cliccato proprio la checkbox, lascia fare al change
|
||||
return;
|
||||
}
|
||||
|
||||
const cb = rowEl.querySelector(".filter-row-checkbox");
|
||||
if (!cb) return;
|
||||
cb.checked = !cb.checked;
|
||||
toggleRow(rowEl.dataset.id, cb.checked);
|
||||
});
|
||||
|
||||
// filtri colonna live (debounce)
|
||||
let t = null;
|
||||
document.addEventListener("input", function (e) {
|
||||
if (!e.target.classList.contains("filter-col-input")) return;
|
||||
const key = e.target.dataset.colKey;
|
||||
colFilters[key] = e.target.value;
|
||||
clearTimeout(t);
|
||||
t = setTimeout(applyVisibility, 200);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Re-inietta dopo ogni re-render della griglia ──
|
||||
function hookRerender() {
|
||||
const orig = R().renderVisibleRows;
|
||||
R().renderVisibleRows = function () {
|
||||
orig.apply(this, arguments);
|
||||
if (filtersActive) {
|
||||
injectCheckboxes();
|
||||
applyVisibility();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (!window.gridRenderer) {
|
||||
console.error("[gridFilter] gridRenderer non trovato");
|
||||
return;
|
||||
}
|
||||
buildUI();
|
||||
attachEvents();
|
||||
hookRerender();
|
||||
}
|
||||
|
||||
// parte dopo che gridRenderer ha finito init
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () =>
|
||||
setTimeout(init, 300),
|
||||
);
|
||||
} else {
|
||||
setTimeout(init, 300);
|
||||
}
|
||||
})();
|
||||
@@ -1388,6 +1388,11 @@
|
||||
|
||||
if (column === "idclient") {
|
||||
data.forEach((row) => {
|
||||
if (
|
||||
window.isTargetGridId &&
|
||||
!window.isTargetGridId(row.iddatadb)
|
||||
)
|
||||
return;
|
||||
const oldClientId = row.idclient || "";
|
||||
|
||||
row.idclient = value;
|
||||
@@ -1421,6 +1426,11 @@
|
||||
|
||||
if (column === "cliente_fornitore_id") {
|
||||
data.forEach((row) => {
|
||||
if (
|
||||
window.isTargetGridId &&
|
||||
!window.isTargetGridId(row.iddatadb)
|
||||
)
|
||||
return;
|
||||
row.cliente_fornitore_id = value;
|
||||
row._dirty = true;
|
||||
});
|
||||
@@ -1434,6 +1444,11 @@
|
||||
const fixedKey = column.replace("fixed_", "");
|
||||
|
||||
data.forEach((row) => {
|
||||
if (
|
||||
window.isTargetGridId &&
|
||||
!window.isTargetGridId(row.iddatadb)
|
||||
)
|
||||
return;
|
||||
if (!row.fixedFields) row.fixedFields = {};
|
||||
row.fixedFields[fixedKey] = value;
|
||||
row._dirty = true;
|
||||
@@ -1446,6 +1461,11 @@
|
||||
|
||||
if (col && (col.type === "detail" || col.type === "main_field")) {
|
||||
data.forEach((row) => {
|
||||
if (
|
||||
window.isTargetGridId &&
|
||||
!window.isTargetGridId(row.iddatadb)
|
||||
)
|
||||
return;
|
||||
if (!row.details) row.details = {};
|
||||
row.details[col.key] = value;
|
||||
|
||||
@@ -1760,3 +1780,127 @@
|
||||
},
|
||||
};
|
||||
})();
|
||||
// ===================
|
||||
// NAVIGAZIONE GRIGLIA CON FRECCE TASTIERA
|
||||
// ===================
|
||||
(function () {
|
||||
// Naviga solo tra input di testo/numero/time e date-picker.
|
||||
// I Select2 (client, fornitore, SceltaMultipla) sono esclusi:
|
||||
// lì le frecce servono al dropdown.
|
||||
function isNavigable(el) {
|
||||
if (!el || !el.classList) return false;
|
||||
if (!el.classList.contains("cell-input")) return false;
|
||||
// Escludi i select (nativi o Select2)
|
||||
if (el.tagName === "SELECT") return false;
|
||||
if (el.classList.contains("select2-hidden-accessible")) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Tutte le celle navigabili della riga, ordinate per data-index
|
||||
function navCellsInRow($row) {
|
||||
return $row.find(".grid-cell[data-index]").filter(function () {
|
||||
const input = this.querySelector(".cell-input");
|
||||
return isNavigable(input);
|
||||
});
|
||||
}
|
||||
|
||||
function focusCellInput($cell) {
|
||||
if (!$cell || !$cell.length) return;
|
||||
const input = $cell.get(0).querySelector(".cell-input");
|
||||
if (!input) return;
|
||||
input.focus();
|
||||
if (input.type !== "date" && typeof input.select === "function") {
|
||||
try {
|
||||
input.select();
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
function atStart(el) {
|
||||
if (el.type === "date" || el.type === "number" || el.type === "time")
|
||||
return true;
|
||||
return el.selectionStart === 0 && el.selectionEnd === 0;
|
||||
}
|
||||
|
||||
function atEnd(el) {
|
||||
if (el.type === "date" || el.type === "number" || el.type === "time")
|
||||
return true;
|
||||
const len = (el.value || "").length;
|
||||
return el.selectionStart === len && el.selectionEnd === len;
|
||||
}
|
||||
|
||||
$(document).on("keydown", "#gridRowContainer .cell-input", function (e) {
|
||||
const el = this;
|
||||
if (!isNavigable(el)) return;
|
||||
|
||||
// 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 $cell = $(el).closest(".grid-cell");
|
||||
const $row = $cell.closest(".grid-row");
|
||||
if (!$cell.length || !$row.length) return;
|
||||
|
||||
const colIndex = $cell.attr("data-index");
|
||||
|
||||
// --- SU / GIÙ: stessa colonna (data-index), riga adiacente ---
|
||||
if (key === "ArrowUp" || key === "ArrowDown") {
|
||||
const $targetRow =
|
||||
key === "ArrowUp"
|
||||
? $row.prev(".grid-row")
|
||||
: $row.next(".grid-row");
|
||||
if (!$targetRow.length) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// Cella stessa colonna nella riga di destinazione
|
||||
let $target = $targetRow.find(
|
||||
`.grid-cell[data-index="${colIndex}"]`,
|
||||
);
|
||||
|
||||
// Se quella colonna non è navigabile (es. è un select),
|
||||
// cerca la più vicina navigabile nella riga
|
||||
if (
|
||||
!$target.length ||
|
||||
!isNavigable($target.get(0).querySelector(".cell-input"))
|
||||
) {
|
||||
const $cells = navCellsInRow($targetRow);
|
||||
if (!$cells.length) return;
|
||||
$target = $cells.first();
|
||||
}
|
||||
|
||||
focusCellInput($target);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- SINISTRA / DESTRA: cella navigabile precedente/successiva nella riga ---
|
||||
const $cells = navCellsInRow($row);
|
||||
const curPos = $cells.index($cell);
|
||||
if (curPos === -1) return;
|
||||
|
||||
if (key === "ArrowLeft") {
|
||||
if (!atStart(el)) return;
|
||||
if (curPos <= 0) return;
|
||||
e.preventDefault();
|
||||
focusCellInput($cells.eq(curPos - 1));
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "ArrowRight") {
|
||||
if (!atEnd(el)) return;
|
||||
if (curPos >= $cells.length - 1) return;
|
||||
e.preventDefault();
|
||||
focusCellInput($cells.eq(curPos + 1));
|
||||
return;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -1328,6 +1328,117 @@ $gridMeta = [
|
||||
outline: 2px solid #dc3545 !important;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* ── Filtri / selezione righe ── */
|
||||
#filtersToggleBtn.active {
|
||||
background-color: #0dcaf0 !important;
|
||||
color: #fff !important;
|
||||
border-color: #0dcaf0 !important;
|
||||
}
|
||||
|
||||
/* checkbox dentro la cella Actions */
|
||||
.filter-cb-wrap {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.grid-container.filters-on .filter-cb-wrap {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.filter-cb-wrap input {
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.grid-container.filters-on .button-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.grid-row.row-selected {
|
||||
background-color: #cfe9ff !important;
|
||||
}
|
||||
|
||||
.grid-row.row-selected .button-cell,
|
||||
.grid-row.row-selected .grid-cell:nth-child(2),
|
||||
.grid-row.row-selected .grid-cell:nth-child(3) {
|
||||
background-color: #cfe9ff !important;
|
||||
}
|
||||
|
||||
.grid-filter-row {
|
||||
background: #eef6f9 !important;
|
||||
border-bottom: 2px solid #0dcaf0;
|
||||
}
|
||||
|
||||
.grid-filter-row .grid-cell {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.grid-filter-row .filter-actions-cell {
|
||||
background: #eef6f9 !important;
|
||||
}
|
||||
|
||||
.grid-container.filters-on .grid-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.grid-container.filters-on .grid-row input,
|
||||
.grid-container.filters-on .grid-row select,
|
||||
.grid-container.filters-on .grid-row textarea,
|
||||
.grid-container.filters-on .grid-row button,
|
||||
.grid-container.filters-on .grid-row a {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
/* Sticky su riga propagazione e filtri (fix scroll orizzontale) */
|
||||
.grid-top .grid-cell.save-all-cell {
|
||||
position: sticky !important;
|
||||
left: 0;
|
||||
z-index: 9;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.grid-top .grid-cell:nth-child(2) {
|
||||
position: sticky !important;
|
||||
left: 210px;
|
||||
z-index: 8;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
<?php if (isset($mainFieldMappings) && count($mainFieldMappings) >= 2): ?>.grid-top .grid-cell:nth-child(3) {
|
||||
position: sticky !important;
|
||||
left: 360px;
|
||||
z-index: 7;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
<?php endif; ?>.grid-filter-row .filter-actions-cell {
|
||||
position: sticky !important;
|
||||
left: 0;
|
||||
z-index: 9;
|
||||
background: #eef6f9 !important;
|
||||
}
|
||||
|
||||
.grid-filter-row .grid-cell:nth-child(2) {
|
||||
position: sticky !important;
|
||||
left: 210px;
|
||||
z-index: 8;
|
||||
background: #eef6f9 !important;
|
||||
}
|
||||
|
||||
<?php if (isset($mainFieldMappings) && count($mainFieldMappings) >= 2): ?>.grid-filter-row .grid-cell:nth-child(3) {
|
||||
position: sticky !important;
|
||||
left: 360px;
|
||||
z-index: 7;
|
||||
background: #eef6f9 !important;
|
||||
}
|
||||
|
||||
<?php endif; ?>
|
||||
</style>
|
||||
<title>Edit Imported Data - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
</head>
|
||||
@@ -1456,6 +1567,7 @@ $gridMeta = [
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script src="tracking.js"></script>
|
||||
<script src="gridRenderer.js"></script>
|
||||
<script src="gridFilter.js"></script>
|
||||
<script src="saveAll.js"></script>
|
||||
<script src="exportLims_gridData.js"></script>
|
||||
<script src="modals_gridData.js"></script>
|
||||
|
||||
@@ -103,30 +103,31 @@
|
||||
</div>
|
||||
|
||||
<!-- ===== KPI ===== -->
|
||||
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-6 g-3 kpi-row">
|
||||
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-5 g-3 kpi-row">
|
||||
<div class="col">
|
||||
<div class="card radius-10 stat-card stat-card-extra h-100">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">Export mese <span id="kpiMonthLabel"></span></div>
|
||||
<div class="stat-value text-primary" id="kpiExportMonth">0</div>
|
||||
<small class="text-muted">Dati dal 16/07/2026</small>
|
||||
<div class="stat-label">Campioni mese <span id="kpiMonthLabel"></span></div>
|
||||
<div class="stat-value" id="kpiPartsMonth">0</div>
|
||||
<small class="text-muted">Commesse: <span id="kpiExportMonth">0</span> · dal 16/07/2026</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card radius-10 stat-card stat-card-extra h-100">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">Export anno <span id="kpiYearLabel"></span></div>
|
||||
<div class="stat-value text-dark" id="kpiExportYear">0</div>
|
||||
<small class="text-muted">Dati dal 16/07/2026</small>
|
||||
<div class="stat-label">Campioni anno <span id="kpiYearLabel"></span></div>
|
||||
<div class="stat-value" id="kpiPartsYear">0</div>
|
||||
<small class="text-muted">Commesse: <span id="kpiExportYear">0</span> · dal 16/07/2026</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card radius-10 stat-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">Export giorno (status L)</div>
|
||||
<div class="stat-value text-primary" id="kpiExport">0</div>
|
||||
<div class="stat-label">Campioni esportati (giorno)</div>
|
||||
<div class="stat-value text-primary" id="kpiParts">0</div>
|
||||
<small class="text-muted">Commesse: <span id="kpiExport">0</span></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,14 +147,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card radius-10 stat-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">Clienti distinti</div>
|
||||
<div class="stat-value text-warning" id="kpiClienti">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== DATO GIORNALIERO CAMPIONI LIMS ===== -->
|
||||
@@ -222,7 +215,8 @@
|
||||
<tr>
|
||||
<th>ID Cliente</th>
|
||||
<th>Cliente</th>
|
||||
<th>Export</th>
|
||||
<th>Campioni</th>
|
||||
<th>Commesse</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
@@ -243,7 +237,8 @@
|
||||
<tr>
|
||||
<th>ID Utente</th>
|
||||
<th>Utente</th>
|
||||
<th>Export</th>
|
||||
<th>Campioni</th>
|
||||
<th>Commesse</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
@@ -295,6 +290,10 @@
|
||||
data: 'clientname',
|
||||
defaultContent: '—'
|
||||
},
|
||||
{
|
||||
data: 'parts',
|
||||
className: 'text-end'
|
||||
},
|
||||
{
|
||||
data: 'tot',
|
||||
className: 'text-end'
|
||||
@@ -318,6 +317,10 @@
|
||||
data: 'username',
|
||||
defaultContent: '—'
|
||||
},
|
||||
{
|
||||
data: 'parts',
|
||||
className: 'text-end'
|
||||
},
|
||||
{
|
||||
data: 'tot',
|
||||
className: 'text-end'
|
||||
@@ -428,11 +431,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// KPI
|
||||
$('#kpiExport').text(res.totals.export);
|
||||
$('#kpiExportMonth').text(res.totalsMonth ? res.totalsMonth.export : 0);
|
||||
$('#kpiExportYear').text(res.totalsYear ? res.totalsYear.export : 0);
|
||||
$('#kpiClienti').text(res.byClient.length);
|
||||
// KPI — dato principale = parti/campioni, sotto le commesse
|
||||
$('#kpiParts').text(res.totals.parts ?? 0);
|
||||
$('#kpiExport').text(res.totals.export ?? 0);
|
||||
$('#kpiPartsMonth').text(res.totalsMonth ? (res.totalsMonth.parts ?? 0) : 0);
|
||||
$('#kpiExportMonth').text(res.totalsMonth ? (res.totalsMonth.export ?? 0) : 0);
|
||||
$('#kpiPartsYear').text(res.totalsYear ? (res.totalsYear.parts ?? 0) : 0);
|
||||
$('#kpiExportYear').text(res.totalsYear ? (res.totalsYear.export ?? 0) : 0);
|
||||
$('#kpiLimsSamples').text(res.limsSamples ?? 0);
|
||||
$('#limsSamplesInput').val(res.limsSamples ?? '');
|
||||
|
||||
@@ -452,25 +457,26 @@
|
||||
}
|
||||
|
||||
const limsTot = parseInt(res.limsSamples ?? 0, 10);
|
||||
const partsTot = parseInt(res.totals.parts ?? 0, 10);
|
||||
if (limsTot > 0) {
|
||||
const perc = (res.totals.export / limsTot) * 100;
|
||||
const perc = (partsTot / limsTot) * 100;
|
||||
$('#kpiPerc').text(perc.toFixed(1) + '%');
|
||||
} else {
|
||||
$('#kpiPerc').text('—');
|
||||
}
|
||||
|
||||
// Torta esportati vs totale
|
||||
renderPie(res.totals.export, limsTot);
|
||||
// Torta parti esportate vs totale inserito nel LIMS
|
||||
renderPie(partsTot, limsTot);
|
||||
|
||||
// Grafici
|
||||
// Grafici — valori = campioni/parti
|
||||
const clientTop = topN(res.byClient);
|
||||
const userTop = topN(res.byUser);
|
||||
chartClienti = renderBarChart(chartClienti, 'chartClienti',
|
||||
clientTop.map(r => r.clientname ? (r.clientname + ' (' + r.idclient + ')') : ('ID ' + r.idclient)),
|
||||
clientTop.map(r => r.tot), 'Export');
|
||||
clientTop.map(r => r.parts), 'Campioni');
|
||||
chartUtenti = renderBarChart(chartUtenti, 'chartUtenti',
|
||||
userTop.map(r => r.username ? (r.username + ' (' + r.user_id + ')') : ('ID ' + r.user_id)),
|
||||
userTop.map(r => r.tot), 'Export');
|
||||
userTop.map(r => r.parts), 'Campioni');
|
||||
|
||||
// Tabelle
|
||||
dtClienti.clear().rows.add(res.byClient).draw();
|
||||
|
||||
@@ -15,14 +15,17 @@ try {
|
||||
}
|
||||
|
||||
// --- Export per cliente (status = 'l', giornata su export_date) ---
|
||||
// tot = numero commesse, parts = numero parti/campioni
|
||||
$stmtC = $pdo->prepare("
|
||||
SELECT d.idclient AS idclient,
|
||||
COUNT(*) AS tot
|
||||
COUNT(DISTINCT d.iddatadb) AS tot,
|
||||
COUNT(ip.id) AS parts
|
||||
FROM datadb d
|
||||
LEFT JOIN identification_parts ip ON ip.iddatadb = d.iddatadb
|
||||
WHERE d.status = 'l'
|
||||
AND DATE(d.export_date) = :d
|
||||
GROUP BY d.idclient
|
||||
ORDER BY tot DESC
|
||||
ORDER BY parts DESC
|
||||
");
|
||||
$stmtC->execute(['d' => $date]);
|
||||
$byClient = $stmtC->fetchAll(PDO::FETCH_ASSOC);
|
||||
@@ -38,16 +41,19 @@ try {
|
||||
}
|
||||
|
||||
// --- Export per utente (status = 'l', giornata su export_date) ---
|
||||
// tot = numero commesse, parts = numero parti/campioni
|
||||
$stmtU = $pdo->prepare("
|
||||
SELECT d.user_id AS user_id,
|
||||
TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS username,
|
||||
COUNT(*) AS tot
|
||||
COUNT(DISTINCT d.iddatadb) AS tot,
|
||||
COUNT(ip.id) AS parts
|
||||
FROM datadb d
|
||||
LEFT JOIN auth_users u ON u.id = d.user_id
|
||||
LEFT JOIN identification_parts ip ON ip.iddatadb = d.iddatadb
|
||||
WHERE d.status = 'l'
|
||||
AND DATE(d.export_date) = :d
|
||||
GROUP BY d.user_id, username
|
||||
ORDER BY tot DESC
|
||||
ORDER BY parts DESC
|
||||
");
|
||||
$stmtU->execute(['d' => $date]);
|
||||
$byUser = $stmtU->fetchAll(PDO::FETCH_ASSOC);
|
||||
@@ -56,6 +62,7 @@ try {
|
||||
foreach ($byClient as &$r) {
|
||||
$r['idclient'] = $r['idclient'] !== null ? (int)$r['idclient'] : null;
|
||||
$r['tot'] = (int)$r['tot'];
|
||||
$r['parts'] = (int)$r['parts'];
|
||||
$nm = $clientNames[$r['idclient']] ?? '';
|
||||
$r['clientname'] = $nm !== '' ? $nm : null;
|
||||
}
|
||||
@@ -63,6 +70,7 @@ try {
|
||||
foreach ($byUser as &$r) {
|
||||
$r['user_id'] = $r['user_id'] !== null ? (int)$r['user_id'] : null;
|
||||
$r['tot'] = (int)$r['tot'];
|
||||
$r['parts'] = (int)$r['parts'];
|
||||
if (isset($r['username'])) $r['username'] = $r['username'] !== null && $r['username'] !== '' ? $r['username'] : null;
|
||||
}
|
||||
unset($r);
|
||||
@@ -70,7 +78,19 @@ try {
|
||||
// --- Totale export della giornata ---
|
||||
$totExport = array_sum(array_column($byClient, 'tot'));
|
||||
|
||||
// --- Totale export del MESE della data selezionata (status = 'l') ---
|
||||
// --- Totale PARTI/CAMPIONI della giornata (righe identification_parts legate
|
||||
// ai datadb esportati status='l' in questa giornata) ---
|
||||
$stmtP = $pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM identification_parts ip
|
||||
INNER JOIN datadb d ON d.iddatadb = ip.iddatadb
|
||||
WHERE d.status = 'l'
|
||||
AND DATE(d.export_date) = :d
|
||||
");
|
||||
$stmtP->execute(['d' => $date]);
|
||||
$totParts = (int)$stmtP->fetchColumn();
|
||||
|
||||
// --- Totale COMMESSE del MESE (status = 'l') ---
|
||||
$stmtM = $pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM datadb d
|
||||
@@ -80,7 +100,18 @@ try {
|
||||
$stmtM->execute(['m' => $d->format('Y-m')]);
|
||||
$totExportMonth = (int)$stmtM->fetchColumn();
|
||||
|
||||
// --- Totale export dell'ANNO della data selezionata (status = 'l') ---
|
||||
// --- Totale PARTI del MESE (status = 'l') ---
|
||||
$stmtMP = $pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM identification_parts ip
|
||||
INNER JOIN datadb d ON d.iddatadb = ip.iddatadb
|
||||
WHERE d.status = 'l'
|
||||
AND DATE_FORMAT(d.export_date, '%Y-%m') = :m
|
||||
");
|
||||
$stmtMP->execute(['m' => $d->format('Y-m')]);
|
||||
$totPartsMonth = (int)$stmtMP->fetchColumn();
|
||||
|
||||
// --- Totale COMMESSE dell'ANNO (status = 'l') ---
|
||||
$stmtY = $pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM datadb d
|
||||
@@ -90,6 +121,17 @@ try {
|
||||
$stmtY->execute(['y' => $d->format('Y')]);
|
||||
$totExportYear = (int)$stmtY->fetchColumn();
|
||||
|
||||
// --- Totale PARTI dell'ANNO (status = 'l') ---
|
||||
$stmtYP = $pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM identification_parts ip
|
||||
INNER JOIN datadb d ON d.iddatadb = ip.iddatadb
|
||||
WHERE d.status = 'l'
|
||||
AND YEAR(d.export_date) = :y
|
||||
");
|
||||
$stmtYP->execute(['y' => $d->format('Y')]);
|
||||
$totPartsYear = (int)$stmtYP->fetchColumn();
|
||||
|
||||
// --- Campioni totali inseriti nel LIMS per la giornata (tabella lims_daily_samples) ---
|
||||
$stmtL = $pdo->prepare("SELECT total_samples FROM lims_daily_samples WHERE sample_date = :d LIMIT 1");
|
||||
$stmtL->execute(['d' => $date]);
|
||||
@@ -101,9 +143,9 @@ try {
|
||||
'date' => $date,
|
||||
'month' => $d->format('Y-m'),
|
||||
'year' => (int)$d->format('Y'),
|
||||
'totals' => ['export' => (int)$totExport],
|
||||
'totalsMonth' => ['export' => $totExportMonth],
|
||||
'totalsYear' => ['export' => $totExportYear],
|
||||
'totals' => ['export' => (int)$totExport, 'parts' => $totParts],
|
||||
'totalsMonth' => ['export' => $totExportMonth, 'parts' => $totPartsMonth],
|
||||
'totalsYear' => ['export' => $totExportYear, 'parts' => $totPartsYear],
|
||||
'byClient' => $byClient,
|
||||
'byUser' => $byUser,
|
||||
'limsSamples' => $limsSamples,
|
||||
|
||||
Reference in New Issue
Block a user