server side filtering and ui-fixes

This commit is contained in:
2026-07-19 13:05:00 +03:00
parent 05356fc98b
commit c3f3e824dc
4 changed files with 860 additions and 5 deletions
+340
View File
@@ -0,0 +1,340 @@
<?php
include('include/headscript.php');
require_once __DIR__ . '/include/grid_data_builder.php';
header('Content-Type: application/json');
$pdo = DBHandlerSelect::getInstance()->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<array{0:int,1:int}> 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()]);
}
+305 -4
View File
@@ -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 =
`<button type="button" class="btn btn-outline-secondary btn-sm" id="filterPrevBtn" ${filterPage <= 1 ? "disabled" : ""}></button>` +
`<span style="font-size:12px;color:#333;">${filterTotal} trovati · pag ${filterPage}/${filterTotalPages}</span>` +
`<button type="button" class="btn btn-outline-secondary btn-sm" id="filterNextBtn" ${filterPage >= filterTotalPages ? "disabled" : ""}></button>`;
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) =>
`<option value="${v}" ${cur === v ? "selected" : ""}>${lbl}</option>`;
cell.innerHTML =
`<select class="filter-col-input" data-col-key="${col.key}" style="width:100%;padding:3px 6px;font-size:12px;border:1px solid #ced4da;border-radius:4px;">` +
opt("", "— tutti —") +
opt("i", "Imported") +
opt("P", "In Progress") +
opt("l", "To LIMS") +
`</select>`;
} 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;">`;
}
@@ -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 = `
<span id="filterPager" style="display:none;align-items:center;gap:6px;"></span>
<button type="button" id="filterClearBtn" class="btn btn-outline-secondary btn-sm"><i class="fas fa-times"></i> Pulisci filtri</button>
<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>
@@ -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
+1
View File
@@ -787,6 +787,7 @@ $gridMeta = [
padding: 10px 0;
min-height: 0;
flex-wrap: nowrap;
min-width: fit-content;
}
.grid-top .grid-cell {
@@ -0,0 +1,213 @@
<?php
if (!function_exists('gdbFixedDefaultValue')) {
/** Default di un fixed field (DATE 'today' → data odierna). */
function gdbFixedDefaultValue(array $f): string
{
$v = $f['default_value'] ?? '';
if (($f['data_type'] ?? '') === 'DATE' && $v === 'today') {
return date('Y-m-d');
}
return (string)$v;
}
}
if (!function_exists('buildGridConfig')) {
/**
* Config del template necessaria per costruire le righe: fixed fields (ordinati
* come in imported.php), main field mappings, alias map, default idclient.
*
* @return array{fixedFields:array, fixedAliasMap:array, mainFieldMappings:array, default_idclient:mixed}
*/
function buildGridConfig(PDO $pdo, int $templateId): array
{
// Mappa logica fixed_field_key → colonna reale su datadb (come imported.php)
$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',
];
// 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;
}
}