From c3f3e824dc15974b6c48b3fedfe4be57ea969161 Mon Sep 17 00:00:00 2001 From: "r.mubarakzyanov" Date: Sun, 19 Jul 2026 13:05:00 +0300 Subject: [PATCH] server side filtering and ui-fixes --- public/userarea/filter_records.php | 340 ++++++++++++++++++ public/userarea/gridFilter.js | 309 +++++++++++++++- public/userarea/imported.php | 3 +- public/userarea/include/grid_data_builder.php | 213 +++++++++++ 4 files changed, 860 insertions(+), 5 deletions(-) create mode 100644 public/userarea/filter_records.php create mode 100644 public/userarea/include/grid_data_builder.php diff --git a/public/userarea/filter_records.php b/public/userarea/filter_records.php new file mode 100644 index 0000000..2ed5565 --- /dev/null +++ b/public/userarea/filter_records.php @@ -0,0 +1,340 @@ +getConnection(); + +const FILTER_ALLOWED_PER_PAGE = [20, 40, 60, 100]; +const FILTER_ALLIDS_CAP = 5000; + +$body = json_decode(file_get_contents('php://input'), true) ?: []; + +$templateId = (int)($body['template_id'] ?? 0); +if ($templateId <= 0) { + echo json_encode(['success' => false, 'message' => 'template_id mancante']); + exit; +} + +$status = in_array($body['status'] ?? 'i', ['i', 'P', 'l'], true) ? $body['status'] : 'i'; +$showAll = !empty($body['all_users']); +$importref = trim((string)($body['importref'] ?? '')); // scope su un singolo import +$filters = is_array($body['filters'] ?? null) ? $body['filters'] : []; +$userId = (int)($iduserlogin ?? 0); +$perPage = in_array((int)($body['per_page'] ?? 20), FILTER_ALLOWED_PER_PAGE, true) ? (int)$body['per_page'] : 20; +$page = max(1, (int)($body['page'] ?? 1)); +$wantAllIds = !empty($body['want_all_ids']); + +$cacheDir = __DIR__ . '/cache'; + +$fixedAliasMap = [ + 'ClienteResponsabile' => 'cliente_responsabile_id', + 'ClienteFornitore' => 'cliente_fornitore_id', + 'ClienteAnalisi' => 'clienteAnalisi', + 'ClienteFatturazione' => 'ClienteFatturazione', + 'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id', + 'AnagraficaCertestObject' => 'anagrafica_certest_object_id', + 'AnagraficaCertestService' => 'anagrafica_certest_service_id', + 'ConsegnaRichiesta' => 'consegna_richiesta', +]; +$staticCols = ['importreferencecode', 'filename_import', 'importdate', 'commessaweb']; + +// ── Risolutori "etichetta → lista di ID" via cache ──────────────────────── +function cacheItems(string $file): array +{ + if (!is_file($file)) return []; + $data = json_decode(file_get_contents($file), true); + if (!is_array($data)) return []; + return $data['value'] ?? $data ?? []; +} + +/** Clienti: label "Nome - X - id" (come search_clienti.php). */ +function resolveClientIdsByTerm(string $cacheDir, string $term): array +{ + $term = mb_strtolower(trim($term)); + if ($term === '') return []; + $ids = []; + foreach (cacheItems($cacheDir . '/clienti.json') as $c) { + $name = trim($c['Nominativo'] ?? ''); + $id = trim((string)($c['IdCliente'] ?? '')); + $code = trim((string)($c['CodiceCliente'] ?? '')); + $parts = explode('_', $code); + $suffix = trim($parts[1] ?? ''); + if ($suffix === '' && $code !== '') $suffix = substr($code, 0, 1); + if ($suffix === '') $suffix = '--'; + $label = mb_strtolower($name . ' - ' . $suffix . ' - ' . $id); + if (mb_strpos($label, $term) !== false) $ids[] = (int)$c['IdCliente']; + } + return array_values(array_unique($ids)); +} + +/** Dropdown generico: matcha il termine su qualsiasi label field, ritorna gli id. */ +function resolveIdsFromCache(string $file, string $idField, array $labelFields, string $term): array +{ + $term = mb_strtolower(trim($term)); + if ($term === '') return []; + $ids = []; + foreach (cacheItems($file) as $it) { + $hay = ''; + foreach ($labelFields as $lf) $hay .= ' ' . ($it[$lf] ?? ''); + if (mb_strpos(mb_strtolower($hay), $term) !== false && isset($it[$idField])) { + $ids[] = (int)$it[$idField]; + } + } + return array_values(array_unique($ids)); +} + +/** + * Per un fixed field che memorizza un ID, risolve il termine in lista di id. + * Ritorna: array di id (IN), [] se il termine non matcha nulla, null se la colonna + * non è risolvibile (→ il chiamante la salta invece di fare LIKE sbagliato). + */ +function resolveFixedIds(string $cacheDir, string $key, string $term): ?array +{ + switch ($key) { + case 'ClienteAnalisi': + case 'ClienteFatturazione': + return resolveClientIdsByTerm($cacheDir, $term); + case 'MoltiplicatorePrezzo': + return resolveIdsFromCache($cacheDir . '/moltiplicatori_prezzo.json', 'IdMoltiplicatorePrezzo', ['Codice', 'Descrizione'], $term); + case 'AnagraficaCertestObject': + return resolveIdsFromCache($cacheDir . '/anagrafica_certest_object.json', 'IdAnagrafica', ['Codice', 'NomeAnagrafica'], $term); + case 'AnagraficaCertestService': + return resolveIdsFromCache($cacheDir . '/anagrafica_certest_service.json', 'IdAnagrafica', ['Codice', 'NomeAnagrafica'], $term); + default: + // ClienteResponsabile (per-cliente) e altri: non risolvibili globalmente + return null; + } +} + +/** + * @return array lista di [idclient, idResponsabile] + */ +function resolveResponsabilePairs(string $cacheDir, string $term): array +{ + $term = mb_strtolower(trim($term)); + if ($term === '') return []; + $pairs = []; + foreach (glob($cacheDir . '/cliente_responsabili_*.json') ?: [] as $file) { + if (!preg_match('/cliente_responsabili_(\d+)\.json$/', $file, $m)) continue; + $cid = (int)$m[1]; + $data = json_decode(file_get_contents($file), true); + foreach (($data['Responsabili'] ?? []) as $r) { + $name = mb_strtolower((string)($r['Nominativo'] ?? '')); + if ($name !== '' && mb_strpos($name, $term) !== false && isset($r['IdClienteResponsabile'])) { + $pairs[] = [$cid, (int)$r['IdClienteResponsabile']]; + } + } + } + return $pairs; +} + +/** Meta di un mapping (data_type, field_id), con cache statica. */ +function mappingMeta(PDO $pdo, int $mappingId): array +{ + static $cache = []; + if (!array_key_exists($mappingId, $cache)) { + $st = $pdo->prepare("SELECT data_type, field_id FROM template_mapping WHERE id = ?"); + $st->execute([$mappingId]); + $cache[$mappingId] = $st->fetch(PDO::FETCH_ASSOC) ?: ['data_type' => 'Testo', 'field_id' => null]; + } + return $cache[$mappingId]; +} + +function resolveCustomFieldValueIds(string $cacheDir, $fieldId, string $term): array +{ + $fieldId = (int)$fieldId; + $term = mb_strtolower(trim($term)); + if ($fieldId <= 0 || $term === '') return []; + $ids = []; + foreach (cacheItems($cacheDir . "/customfield_{$fieldId}.json") as $it) { + $label = mb_strtolower((string)($it['Valore'] ?? '')); + if (mb_strpos($label, $term) !== false && isset($it['IdCustomFieldsValue'])) { + $ids[] = (int)$it['IdCustomFieldsValue']; + } + } + return array_values(array_unique($ids)); +} + +// ── Costruzione WHERE ────────────────────────────────────────────────────── +$conds = ['d.templateid = ?', 'd.status = ?']; +$params = [$templateId, $status]; +$needUserJoin = false; + +if (!$showAll) { + $conds[] = 'd.user_id = ?'; + $params[] = $userId; +} + +// Scope su un singolo import (come imported.php ?importref=) +if ($importref !== '') { + $conds[] = 'd.importreferencecode = ?'; + $params[] = $importref; +} + +/** Aggiunge una condizione "colonna id IN (lista risolta)". */ +function addIdInCondition(array &$conds, array &$params, string $col, array $ids): void +{ + if (empty($ids)) { + $conds[] = '1 = 0'; // termine dato ma nessun match → zero risultati + return; + } + $ph = implode(',', array_fill(0, count($ids), '?')); + $conds[] = "d.`{$col}` IN ($ph)"; + foreach ($ids as $id) $params[] = (int)$id; +} + +foreach ($filters as $f) { + $term = trim((string)($f['term'] ?? '')); + if ($term === '') continue; + $type = (string)($f['type'] ?? ''); + $key = (string)($f['key'] ?? ''); + $like = '%' . $term . '%'; + + switch ($type) { + case 'detail': + case 'main_field': + $mappingId = (int)$key; + if ($mappingId <= 0) break; + $mm = mappingMeta($pdo, $mappingId); + if (($mm['data_type'] ?? '') === 'SceltaMultipla') { + // Dropdown: field_value = ID valore → risolvi label→id, match IN (= non LIKE) + $vids = resolveCustomFieldValueIds($cacheDir, $mm['field_id'], $term); + if (empty($vids)) { + $conds[] = '1 = 0'; + break; + } + $inph = implode(',', array_fill(0, count($vids), '?')); + $conds[] = "EXISTS (SELECT 1 FROM import_data_details x + WHERE x.id = d.iddatadb AND x.mapping_id = ? AND x.field_value IN ($inph))"; + $params[] = $mappingId; + foreach ($vids as $vid) $params[] = $vid; + } else { + // Testo / Data: match testuale + $conds[] = "EXISTS (SELECT 1 FROM import_data_details x + WHERE x.id = d.iddatadb AND x.mapping_id = ? AND x.field_value LIKE ?)"; + $params[] = $mappingId; + $params[] = $like; + } + break; + + case 'idclient': + addIdInCondition($conds, $params, 'idclient', resolveClientIdsByTerm($cacheDir, $term)); + break; + + case 'cliente_fornitore_id': + addIdInCondition($conds, $params, 'cliente_fornitore_id', resolveClientIdsByTerm($cacheDir, $term)); + break; + + case 'tested_component': + $conds[] = 'd.tested_component LIKE ?'; + $params[] = $like; + break; + + case 'status': + // Il dropdown invia direttamente il codice ('i'/'P'/'l'). + if (in_array($term, ['i', 'P', 'l'], true)) { + $conds[] = 'd.status = ?'; + $params[] = $term; + } else { + $conds[] = '1 = 0'; + } + break; + + case 'fixed': + $col = $fixedAliasMap[$key] ?? null; + if ($col === null) break; + if ($key === 'ConsegnaRichiesta') { + // Data: match testuale sul valore data (es. "2026-03"). + $conds[] = "CAST(d.`{$col}` AS CHAR) LIKE ?"; + $params[] = $like; + } elseif ($key === 'ClienteResponsabile') { + // Match sulla coppia (idclient, id responsabile). + $pairs = resolveResponsabilePairs($cacheDir, $term); + if (empty($pairs)) { + $conds[] = '1 = 0'; + break; + } + $rowvals = implode(',', array_fill(0, count($pairs), '(?,?)')); + $conds[] = "(d.idclient, d.`{$col}`) IN ($rowvals)"; + foreach ($pairs as $pr) { + $params[] = $pr[0]; + $params[] = $pr[1]; + } + } else { + $ids = resolveFixedIds($cacheDir, $key, $term); + if ($ids === null) break; // non risolvibile → non filtrare (niente LIKE su id) + addIdInCondition($conds, $params, $col, $ids); + } + break; + + case 'static': + if ($key === 'user_name') { + $needUserJoin = true; + $conds[] = "CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,'')) LIKE ?"; + $params[] = $like; + } elseif (in_array($key, $staticCols, true)) { + $conds[] = "d.`{$key}` LIKE ?"; + $params[] = $like; + } + break; + + default: + break; // tracking/awb ecc. → non filtrabili + } +} + +$whereSql = implode(' AND ', $conds); +$joinSql = $needUserJoin ? 'LEFT JOIN auth_users u ON d.user_id = u.id' : ''; + +try { + // Totale + pagine + $countStmt = $pdo->prepare("SELECT COUNT(*) FROM datadb d {$joinSql} WHERE {$whereSql}"); + $countStmt->execute($params); + $total = (int)$countStmt->fetchColumn(); + $totalPages = max(1, (int)ceil($total / $perPage)); + if ($page > $totalPages) $page = $totalPages; + $offset = ($page - 1) * $perPage; + + // Pagina corrente + $pageStmt = $pdo->prepare(" + SELECT d.iddatadb + FROM datadb d {$joinSql} + WHERE {$whereSql} + ORDER BY d.excelrow ASC, d.iddatadb ASC + LIMIT {$perPage} OFFSET {$offset} + "); + $pageStmt->execute($params); + $pageIds = array_map('intval', $pageStmt->fetchAll(PDO::FETCH_COLUMN)); + + $config = buildGridConfig($pdo, $templateId); + $rows = buildGridRows($pdo, $pageIds, $config); + + $out = [ + 'success' => true, + 'total' => $total, + 'page' => $page, + 'per_page' => $perPage, + 'total_pages' => $totalPages, + 'rows' => $rows, + ]; + + // Tutti gli id del match (per "seleziona tutti"), solo su richiesta + if ($wantAllIds) { + $allStmt = $pdo->prepare(" + SELECT d.iddatadb FROM datadb d {$joinSql} + WHERE {$whereSql} + ORDER BY d.excelrow ASC, d.iddatadb ASC + LIMIT " . FILTER_ALLIDS_CAP . " + "); + $allStmt->execute($params); + $out['all_ids'] = array_map('intval', $allStmt->fetchAll(PDO::FETCH_COLUMN)); + $out['all_ids_capped'] = $total > FILTER_ALLIDS_CAP; + } + + echo json_encode($out); +} catch (Exception $e) { + error_log('filter_records error: ' . $e->getMessage()); + echo json_encode(['success' => false, 'message' => $e->getMessage()]); +} diff --git a/public/userarea/gridFilter.js b/public/userarea/gridFilter.js index 201c61d..1f9f056 100644 --- a/public/userarea/gridFilter.js +++ b/public/userarea/gridFilter.js @@ -21,8 +21,255 @@ return R().getMeta(); } - // ── Filtro: decide se una riga passa i filtri colonna ── + // ── Filtro SERVER-SIDE (paginato, come la lista normale ma filtrata) ────── + // Il filtro gira su TUTTO il template; il server restituisce UNA PAGINA di righe + // filtrate + il totale, e navighiamo le pagine del set filtrato via AJAX. + const TEMPLATE_ID = + new URLSearchParams(location.search).get("id") || (meta().templateId ?? ""); + const SHOW_ALL = + new URLSearchParams(location.search).get("all_users") === "1"; + const IMPORTREF = + new URLSearchParams(location.search).get("importref") || ""; + const FILTER_PER_PAGE = + parseInt(new URLSearchParams(location.search).get("limit"), 10) || 20; + + let serverFiltered = false; + let filterPage = 1; + let filterTotalPages = 1; + let filterTotal = 0; + let originalData = null; // snapshot della pagina originale (per ripristino) + let filterReqSeq = 0; + + function collectActiveFilters() { + const cols = meta().columns || []; + const out = []; + for (const [key, term] of Object.entries(colFilters)) { + if (!term || !String(term).trim()) continue; + const col = cols.find((c) => String(c.key) === String(key)); + if (!col) continue; + out.push({ key: String(key), type: col.type, term: String(term).trim() }); + } + return out; + } + + // Sostituisce il dataset del renderer in-place e ridisegna. + function swapData(rows) { + const arr = data(); + arr.length = 0; + (rows || []).forEach((r) => arr.push(r)); + R().renderVisibleRows(); + } + + function hidePagination(hide) { + // Durante il filtro la paginazione server (per pagina, via URL) è fuorviante. + document.querySelectorAll(".pager-bar").forEach((el) => { + el.style.display = hide ? "none" : ""; + }); + } + + // Pager del set filtrato (Prec / pag X di Y / Succ) nella toolbar. + function updateFilterPager() { + const el = document.getElementById("filterPager"); + if (!el) return; + if (!serverFiltered) { + el.style.display = "none"; + el.innerHTML = ""; + return; + } + el.style.display = "inline-flex"; + el.innerHTML = + `` + + `${filterTotal} trovati · pag ${filterPage}/${filterTotalPages}` + + ``; + const prev = document.getElementById("filterPrevBtn"); + const next = document.getElementById("filterNextBtn"); + if (prev) prev.addEventListener("click", () => applyServerFilter(filterPage - 1)); + if (next) next.addEventListener("click", () => applyServerFilter(filterPage + 1)); + } + + function restoreOriginal() { + serverFiltered = false; + filterPage = 1; + filterTotalPages = 1; + filterTotal = 0; + if (originalData) swapData(originalData); + updateFilterPager(); + hidePagination(false); + updateToolbar(); + } + + async function applyServerFilter(page = 1) { + const active = collectActiveFilters(); + if (active.length === 0) { + if (serverFiltered) restoreOriginal(); + return; + } + if (!originalData) originalData = [...data()]; // snapshot pagina 1 originale + + const seq = ++filterReqSeq; + try { + const resp = await fetch("filter_records.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + template_id: parseInt(TEMPLATE_ID, 10), + all_users: SHOW_ALL, + importref: IMPORTREF, + status: "i", + filters: active, + page: Math.max(1, page), + per_page: FILTER_PER_PAGE, + }), + }); + const json = await resp.json(); + if (seq !== filterReqSeq) return; // risposta obsoleta: ignora + if (!json.success) { + console.error("[gridFilter] filtro:", json.message); + return; + } + serverFiltered = true; + filterPage = json.page; + filterTotalPages = json.total_pages; + filterTotal = json.total; + swapData(json.rows || []); + updateFilterPager(); + hidePagination(true); + updateToolbar(); + } catch (e) { + console.error("[gridFilter] fetch filtro fallita", e); + } + } + + // "Seleziona tutti i filtrati": chiede al server TUTTI gli id del match e li seleziona. + async function selectAllMatching(on) { + if (!serverFiltered) return; + const active = collectActiveFilters(); + try { + const resp = await fetch("filter_records.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + template_id: parseInt(TEMPLATE_ID, 10), + all_users: SHOW_ALL, + importref: IMPORTREF, + status: "i", + filters: active, + page: filterPage, + per_page: FILTER_PER_PAGE, + want_all_ids: true, + }), + }); + const json = await resp.json(); + if (!json.success) return; + (json.all_ids || []).forEach((id) => (on ? selected.add(id) : selected.delete(id))); + document + .querySelectorAll("#gridRowContainer .grid-row") + .forEach((rowEl) => { + rowEl.classList.toggle("row-selected", on); + const cb = rowEl.querySelector(".filter-row-checkbox"); + if (cb) cb.checked = on; + }); + updateToolbar(); + if (json.all_ids_capped) { + alert("Selezione limitata a " + (json.all_ids || []).length + " record."); + } + } catch (e) { + console.error("[gridFilter] select-all fallita", e); + } + } + + function syncFilterRow() { + const fr = document.getElementById("gridFilterRow"); + const header = document.getElementById("gridHeaderContainer"); + if (!fr || !header) return; + + const actHeader = header.querySelector(".button-header"); + const actCell = fr.querySelector(".filter-actions-cell"); + if (actHeader && actCell) { + const aw = actHeader.offsetWidth; + actCell.style.flex = `0 0 ${aw}px`; + actCell.style.minWidth = `${aw}px`; + } + + const headerCells = header.querySelectorAll( + ".grid-header:not(.button-header)", + ); + const filterCells = fr.querySelectorAll( + ".grid-cell:not(.filter-actions-cell)", + ); + headerCells.forEach((hc, i) => { + const fc = filterCells[i]; + if (fc) fc.style.flex = `0 0 ${hc.offsetWidth}px`; + }); + } + + function syncStickyLeft() { + const btnHeader = document.querySelector( + "#gridHeaderContainer .button-header", + ); + if (!btnHeader) return; + const w1 = btnHeader.offsetWidth; // larghezza reale colonna Actions + + const fixRow = (rowEl) => { + if (!rowEl) return; + const cells = rowEl.querySelectorAll( + ":scope > .grid-cell, :scope > .grid-header", + ); + const c2 = cells[1]; + const c3 = cells[2]; + if (c2 && getComputedStyle(c2).position === "sticky") { + c2.style.left = `${w1}px`; + } + if (c3 && getComputedStyle(c3).position === "sticky") { + c3.style.left = `${w1 + (c2 ? c2.offsetWidth : 0)}px`; + } + }; + + fixRow(document.getElementById("gridHeaderContainer")); + document.querySelectorAll(".grid-top").forEach(fixRow); + document + .querySelectorAll("#gridRowContainer .grid-row") + .forEach(fixRow); + fixRow(document.getElementById("gridFilterRow")); + } + + let _headerObs = null; + let _rowObs = null; + let _syncRaf = null; + + function reconnectObservers() { + const header = document.getElementById("gridHeaderContainer"); + const rows = document.getElementById("gridRowContainer"); + if (_headerObs && header) + _headerObs.observe(header, { + attributes: true, + attributeFilter: ["style"], + subtree: true, + }); + if (_rowObs && rows) _rowObs.observe(rows, { childList: true }); + } + + function scheduleSync() { + if (_syncRaf) return; + _syncRaf = requestAnimationFrame(() => { + _syncRaf = null; + if (_headerObs) _headerObs.disconnect(); + if (_rowObs) _rowObs.disconnect(); + syncStickyLeft(); + syncFilterRow(); + reconnectObservers(); + }); + } + + function hookGridSync() { + _headerObs = new MutationObserver(scheduleSync); + _rowObs = new MutationObserver(scheduleSync); + reconnectObservers(); + scheduleSync(); // primo allineamento + } + function rowMatchesFilters(row) { + if (serverFiltered) return true; for (const [key, term] of Object.entries(colFilters)) { if (!term) continue; const t = term.toLowerCase(); @@ -74,6 +321,11 @@ // ── Lista iddatadb attualmente visibili (per propagazione/export ecc.) ── window.getVisibleGridIds = function () { + if (serverFiltered) { + return data() + .map((r) => parseInt(r.iddatadb, 10)) + .filter(Boolean); + } const ids = []; document .querySelectorAll("#gridRowContainer .grid-row") @@ -178,6 +430,18 @@ cell.style.flex = `0 0 ${w}px`; if (col.type === "tracking" || col.type === "awb") { // niente filtro + } else if (col.type === "status") { + // Status: dropdown (3 valori) invece del testo libero. + const cur = colFilters[col.key] || ""; + const opt = (v, lbl) => + ``; + cell.innerHTML = + ``; } else { cell.innerHTML = ``; } @@ -185,6 +449,7 @@ }); top.parentNode.insertBefore(fr, top); + scheduleSync(); // allinea larghezze filtri + offset sticky } function removeFilterRow() { @@ -223,6 +488,9 @@ btn.classList.remove("active"); removeCheckboxes(); removeFilterRow(); + // reset filtri colonna + ripristina la pagina originale + Object.keys(colFilters).forEach((k) => delete colFilters[k]); + if (serverFiltered) restoreOriginal(); } updateToolbar(); } @@ -313,6 +581,8 @@ bar.style.cssText = "display:none;align-items:center;gap:8px;flex-shrink:0;"; bar.innerHTML = ` + + Selezionate: 0 @@ -320,6 +590,9 @@ fBtn.parentNode.insertBefore(bar, fBtn.nextSibling); fBtn.addEventListener("click", toggleFilters); + document + .getElementById("filterClearBtn") + .addEventListener("click", clearFilters); document .getElementById("filterDeleteBtn") .addEventListener("click", batchDelete); @@ -328,6 +601,17 @@ .addEventListener("click", toggleRestrict); } + // Svuota tutti i filtri di colonna (input + select) e torna alla pagina originale. + function clearFilters() { + Object.keys(colFilters).forEach((k) => delete colFilters[k]); + document + .querySelectorAll("#gridFilterRow .filter-col-input") + .forEach((el) => { + el.value = ""; + }); + if (serverFiltered) restoreOriginal(); + } + // ── Event delegation ── function attachEvents() { // checkbox click @@ -338,10 +622,17 @@ } }); - // Seleziona / deseleziona tutti i FILTRATI visibili + // Seleziona / deseleziona tutti i FILTRATI document.addEventListener("change", function (e) { if (e.target.id !== "filterSelectAll") return; const on = e.target.checked; + + // Con filtro server-side: seleziona TUTTI i match (anche altre pagine). + if (serverFiltered) { + selectAllMatching(on); + return; + } + document .querySelectorAll("#gridRowContainer .grid-row") .forEach((rowEl) => { @@ -379,14 +670,23 @@ toggleRow(rowEl.dataset.id, cb.checked); }); - // filtri colonna live (debounce) + // filtri colonna testo (debounce) → filtro server-side su tutto il template let t = null; document.addEventListener("input", function (e) { if (!e.target.classList.contains("filter-col-input")) return; + if (e.target.tagName === "SELECT") return; // i select li gestisce 'change' const key = e.target.dataset.colKey; colFilters[key] = e.target.value; clearTimeout(t); - t = setTimeout(applyVisibility, 200); + t = setTimeout(applyServerFilter, 250); + }); + + // dropdown (es. Status): applica subito, senza debounce + document.addEventListener("change", function (e) { + if (!e.target.classList.contains("filter-col-input")) return; + if (e.target.tagName !== "SELECT") return; + colFilters[e.target.dataset.colKey] = e.target.value; + applyServerFilter(); }); } @@ -431,6 +731,7 @@ buildUI(); attachEvents(); hookRerender(); + hookGridSync(); } // parte dopo che gridRenderer ha finito init diff --git a/public/userarea/imported.php b/public/userarea/imported.php index 39ae9f8..7f242fe 100644 --- a/public/userarea/imported.php +++ b/public/userarea/imported.php @@ -787,6 +787,7 @@ $gridMeta = [ padding: 10px 0; min-height: 0; flex-wrap: nowrap; + min-width: fit-content; } .grid-top .grid-cell { @@ -1806,4 +1807,4 @@ $gridMeta = [ - \ No newline at end of file + diff --git a/public/userarea/include/grid_data_builder.php b/public/userarea/include/grid_data_builder.php new file mode 100644 index 0000000..5463f0c --- /dev/null +++ b/public/userarea/include/grid_data_builder.php @@ -0,0 +1,213 @@ + 'cliente_responsabile_id', + 'ClienteFornitore' => 'cliente_fornitore_id', + 'ClienteAnalisi' => 'clienteAnalisi', + 'ClienteFatturazione' => 'ClienteFatturazione', + 'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id', + 'AnagraficaCertestObject' => 'anagrafica_certest_object_id', + 'AnagraficaCertestService' => 'anagrafica_certest_service_id', + 'ConsegnaRichiesta' => 'consegna_richiesta', + ]; + + // Fixed fields visibili, ordinati come imported.php + $fixedStmt = $pdo->prepare(" + SELECT id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import + FROM template_fixed_mapping + WHERE template_id = ? AND is_visible_import = 1 + ORDER BY id + "); + $fixedStmt->execute([$templateId]); + $fixedFieldsRaw = $fixedStmt->fetchAll(PDO::FETCH_ASSOC); + + $desiredOrder = [ + 'ClienteResponsabile', 'ClienteFornitore', 'ClienteAnalisi', 'ClienteFatturazione', + 'AnagraficaCertestObject', 'AnagraficaCertestService', 'MoltiplicatorePrezzo', 'ConsegnaRichiesta', + ]; + $excludeFromFixed = ['ClienteFornitore']; // reso come colonna a sé + + $fixedFields = []; + $tempMap = []; + foreach ($fixedFieldsRaw as $f) { + if (in_array($f['fixed_field_key'], $excludeFromFixed, true)) continue; + $tempMap[$f['fixed_field_key']] = $f; + } + foreach ($desiredOrder as $key) { + if (isset($tempMap[$key])) { + $fixedFields[] = $tempMap[$key]; + unset($tempMap[$key]); + } + } + foreach ($tempMap as $f) { + $fixedFields[] = $f; + } + + // Main field mappings (max 2), come imported.php + $mapStmt = $pdo->prepare(" + SELECT id, field_label, data_type, is_required, field_id, field_order, + main_field, is_visible_import, manual_default + FROM template_mapping + WHERE template_id = ? + ORDER BY field_order ASC, id ASC + "); + $mapStmt->execute([$templateId]); + $allMappings = $mapStmt->fetchAll(PDO::FETCH_ASSOC); + + $mainFieldMappings = []; + foreach ($allMappings as $mapping) { + if ((string)$mapping['main_field'] === '1' && (int)$mapping['is_visible_import'] === 1) { + $mainFieldMappings[] = $mapping; + } + if (count($mainFieldMappings) >= 2) break; + } + + // Default idclient dal template + $tplStmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?"); + $tplStmt->execute([$templateId]); + $default_idclient = $tplStmt->fetchColumn(); + $default_idclient = $default_idclient !== false ? $default_idclient : null; + + return [ + 'fixedFields' => $fixedFields, + 'fixedAliasMap' => $fixedAliasMap, + 'mainFieldMappings' => $mainFieldMappings, + 'default_idclient' => $default_idclient, + ]; + } +} + +if (!function_exists('buildGridRows')) { + /** + * Costruisce le righe gridData per la lista di iddatadb data, nell'ordine passato. + * + * @param int[] $iddatadbList + * @param array $config output di buildGridConfig() + * @return array righe nella stessa forma di imported.php $gridDataArray + */ + function buildGridRows(PDO $pdo, array $iddatadbList, array $config): array + { + $iddatadbList = array_values(array_filter(array_map('intval', $iddatadbList), fn($v) => $v > 0)); + if (empty($iddatadbList)) { + return []; + } + + $fixedFields = $config['fixedFields']; + $fixedAliasMap = $config['fixedAliasMap']; + $mainFieldMappings = $config['mainFieldMappings']; + $default_idclient = $config['default_idclient']; + + $ph = implode(',', array_fill(0, count($iddatadbList), '?')); + + // Righe datadb + user_name + $stmt = $pdo->prepare(" + SELECT d.*, CONCAT(u.first_name, ' ', u.last_name) AS user_name + FROM datadb d + LEFT JOIN auth_users u ON d.user_id = u.id + WHERE d.iddatadb IN ($ph) + "); + $stmt->execute($iddatadbList); + $byId = []; + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) { + $byId[(int)$r['iddatadb']] = $r; + } + + // Dettagli (import_data_details) + $detStmt = $pdo->prepare(" + SELECT d.id AS datadb_id, d.mapping_id, d.field_value, + m.field_id, m.field_label, m.data_type, m.is_required, m.manual_default + FROM import_data_details d + JOIN template_mapping m ON d.mapping_id = m.id + WHERE d.id IN ($ph) + "); + $detStmt->execute($iddatadbList); + $detailsByRow = []; + foreach ($detStmt->fetchAll(PDO::FETCH_ASSOC) as $d) { + $detailsByRow[(int)$d['datadb_id']][] = $d; + } + + // Costruzione righe nell'ordine della lista passata + $rows = []; + foreach ($iddatadbList as $id) { + $row = $byId[$id] ?? null; + if ($row === null) continue; + + $rowObj = [ + 'iddatadb' => (int)$row['iddatadb'], + 'status' => $row['status'] ?? 'i', + 'idclient' => $row['idclient'] ?? $default_idclient, + 'cliente_fornitore_id' => $row['cliente_fornitore_id'] ?? null, + 'tested_component' => $row['tested_component'] ?? '', + 'commessaweb' => $row['commessaweb'] ?? null, + 'user_name' => $row['user_name'] ?? '', + 'importreferencecode' => $row['importreferencecode'] ?? '', + 'filename_import' => $row['filename_import'] ?? '', + 'importdate' => $row['importdate'] ?? '', + ]; + + // Fixed fields + $rowObj['fixedFields'] = []; + foreach ($fixedFields as $f) { + $key = $f['fixed_field_key']; + $dbCol = $fixedAliasMap[$key] ?? $key; + $val = $row[$dbCol] ?? ''; + if ($val === '' || $val === null) { + $val = gdbFixedDefaultValue($f); + } + $rowObj['fixedFields'][$key] = (string)$val; + } + + // Details + $rowObj['details'] = []; + $rowDetails = $detailsByRow[$id] ?? []; + foreach ($rowDetails as $d) { + $rowObj['details'][(string)$d['mapping_id']] = $d['field_value'] ?? ''; + } + + // Main field values + foreach ($mainFieldMappings as $mainMapping) { + $found = null; + foreach ($rowDetails as $d) { + if ($d['mapping_id'] == $mainMapping['id']) { + $found = $d; + break; + } + } + $rowObj['details'][(string)$mainMapping['id']] = + ($found['field_value'] ?? null) ?? ($mainMapping['manual_default'] ?? ''); + } + + if (!empty($mainFieldMappings)) { + $firstMain = $mainFieldMappings[0]; + $rowObj['mainFieldValue'] = $rowObj['details'][(string)$firstMain['id']] ?? ''; + } + + $rowObj['_dirty'] = false; + $rows[] = $rowObj; + } + + return $rows; + } +}