filter on imported

This commit is contained in:
2026-07-17 09:17:44 +02:00
parent 05f5722ebd
commit 1a47d5649a
5 changed files with 766 additions and 39 deletions
+423
View File
@@ -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);
}
})();
+144
View File
@@ -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;
}
});
})();
+112
View File
@@ -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>
+36 -30
View File
@@ -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();
+51 -9
View File
@@ -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,