From 1a47d5649a5273256398713377f7c7cf959b7ea2 Mon Sep 17 00:00:00 2001 From: solocla Date: Fri, 17 Jul 2026 09:17:44 +0200 Subject: [PATCH] filter on imported --- public/userarea/gridFilter.js | 423 +++++++++++++++++++++ public/userarea/gridRenderer.js | 144 +++++++ public/userarea/imported.php | 112 ++++++ public/userarea/stats_export_lims.php | 66 ++-- public/userarea/stats_export_lims_data.php | 60 ++- 5 files changed, 766 insertions(+), 39 deletions(-) create mode 100644 public/userarea/gridFilter.js diff --git a/public/userarea/gridFilter.js b/public/userarea/gridFilter.js new file mode 100644 index 0000000..b6774a9 --- /dev/null +++ b/public/userarea/gridFilter.js @@ -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 = ``; + 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 = ``; + 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 = ``; + } + 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 + ? ' Mostra tutti' + : ' 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 = ` ${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 = ' 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 = ` + Selezionate: 0 + + + `; + 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); + } +})(); diff --git a/public/userarea/gridRenderer.js b/public/userarea/gridRenderer.js index 4048a94..24463da 100644 --- a/public/userarea/gridRenderer.js +++ b/public/userarea/gridRenderer.js @@ -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; + } + }); +})(); diff --git a/public/userarea/imported.php b/public/userarea/imported.php index c728f3b..39ae9f8 100644 --- a/public/userarea/imported.php +++ b/public/userarea/imported.php @@ -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; + } + + = 2): ?>.grid-top .grid-cell:nth-child(3) { + position: sticky !important; + left: 360px; + z-index: 7; + background: #fff; + } + + .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; + } + + = 2): ?>.grid-filter-row .grid-cell:nth-child(3) { + position: sticky !important; + left: 360px; + z-index: 7; + background: #eef6f9 !important; + } + + Edit Imported Data - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> @@ -1456,6 +1567,7 @@ $gridMeta = [ + diff --git a/public/userarea/stats_export_lims.php b/public/userarea/stats_export_lims.php index 797d13f..a48c10c 100644 --- a/public/userarea/stats_export_lims.php +++ b/public/userarea/stats_export_lims.php @@ -103,30 +103,31 @@ -
+
-
Export mese
-
0
- Dati dal 16/07/2026 +
Campioni mese
+
0
+ Commesse: 0 · dal 16/07/2026
-
Export anno
-
0
- Dati dal 16/07/2026 +
Campioni anno
+
0
+ Commesse: 0 · dal 16/07/2026
-
Export giorno (status L)
-
0
+
Campioni esportati (giorno)
+
0
+ Commesse: 0
@@ -146,14 +147,6 @@
-
-
-
-
Clienti distinti
-
0
-
-
-
@@ -222,7 +215,8 @@ ID Cliente Cliente - Export + Campioni + Commesse @@ -243,7 +237,8 @@ ID Utente Utente - Export + Campioni + Commesse @@ -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(); diff --git a/public/userarea/stats_export_lims_data.php b/public/userarea/stats_export_lims_data.php index a918633..9191332 100644 --- a/public/userarea/stats_export_lims_data.php +++ b/public/userarea/stats_export_lims_data.php @@ -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,