From 4f0dbc7e912f31001b2866318edfb09a2a668fc9 Mon Sep 17 00:00:00 2001 From: "r.mubarakzyanov" Date: Mon, 30 Mar 2026 16:28:20 +0300 Subject: [PATCH] cache and view improvements --- public/userarea/add_record.php | 23 +- public/userarea/get_clienti.php | 13 +- public/userarea/get_customfield_values.php | 17 +- public/userarea/gridRenderer.js | 243 ++++++++++++------ public/userarea/import_insert.php | 11 +- public/userarea/search_clienti.php | 58 +++++ public/userarea/search_customfield_values.php | 62 +++++ public/userarea/tolims.php | 187 ++++++++++---- public/userarea/warm_cache.php | 110 ++++++++ 9 files changed, 588 insertions(+), 136 deletions(-) create mode 100644 public/userarea/search_clienti.php create mode 100644 public/userarea/search_customfield_values.php create mode 100644 public/userarea/warm_cache.php diff --git a/public/userarea/add_record.php b/public/userarea/add_record.php index fa711f7..4eafe9b 100644 --- a/public/userarea/add_record.php +++ b/public/userarea/add_record.php @@ -33,15 +33,26 @@ try { $stmt->execute([$templateId, $userId, $idclient, $importReferenceCode]); $iddatadb = (int)$pdo->lastInsertId(); - // Create empty import_data_details for all mappings - $mappingStmt = $pdo->prepare("SELECT id FROM template_mapping WHERE template_id = ?"); + // Create import_data_details for all mappings (with auto_value support) + $mappingStmt = $pdo->prepare("SELECT id, auto_value, manual_default, data_type FROM template_mapping WHERE template_id = ?"); $mappingStmt->execute([$templateId]); - $mappings = $mappingStmt->fetchAll(PDO::FETCH_COLUMN); + $mappings = $mappingStmt->fetchAll(PDO::FETCH_ASSOC); if (!empty($mappings)) { - $insertStmt = $pdo->prepare("INSERT INTO import_data_details (id, mapping_id, field_value) VALUES (?, ?, '')"); - foreach ($mappings as $mappingId) { - $insertStmt->execute([$iddatadb, $mappingId]); + $insertStmt = $pdo->prepare("INSERT INTO import_data_details (id, mapping_id, field_value) VALUES (?, ?, ?)"); + foreach ($mappings as $m) { + $val = ''; + $auto = $m['auto_value'] ?? 'none'; + if ($auto === 'import_date') { + $val = date('Y-m-d'); + } elseif ($auto === 'import_time') { + $val = date('H:i'); + } elseif ($m['data_type'] === 'DATE' && ($m['manual_default'] ?? '') === 'today') { + $val = date('Y-m-d'); + } elseif (!empty($m['manual_default'])) { + $val = $m['manual_default']; + } + $insertStmt->execute([$iddatadb, $m['id'], $val]); } } diff --git a/public/userarea/get_clienti.php b/public/userarea/get_clienti.php index 0e6dd83..7996167 100644 --- a/public/userarea/get_clienti.php +++ b/public/userarea/get_clienti.php @@ -74,10 +74,21 @@ try { throw new Exception("Massimo numero di tentativi raggiunto per $endpoint"); } + // Cache file (1 hour TTL) + $cacheFile = __DIR__ . '/cache/clienti.json'; + if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) { + readfile($cacheFile); + exit; + } + // Esegui la chiamata con retry $data = makeApiRequest($api, $endpoint); - echo json_encode($data); + $json = json_encode($data); + if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true); + file_put_contents($cacheFile, $json); + + echo $json; } catch (Exception $e) { http_response_code(500); $errorResponse = [ diff --git a/public/userarea/get_customfield_values.php b/public/userarea/get_customfield_values.php index 5770073..71ea7a4 100644 --- a/public/userarea/get_customfield_values.php +++ b/public/userarea/get_customfield_values.php @@ -21,17 +21,26 @@ try { } $results = []; + $cacheDir = __DIR__ . '/cache'; + if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true); foreach ($fieldIds as $customFieldId) { + $cacheFile = $cacheDir . '/customfield_' . $customFieldId . '.json'; + + // Use cache if fresh (1 hour) + if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) { + $results[$customFieldId] = json_decode(file_get_contents($cacheFile), true); + continue; + } + $endpoint = "CustomField($customFieldId)?\$expand=CustomFieldsValues"; $data = $api->get($endpoint); + $values = $data['CustomFieldsValues'] ?? []; + $results[$customFieldId] = $values; - $results[$customFieldId] = $data['CustomFieldsValues'] ?? []; + file_put_contents($cacheFile, json_encode($values)); } - // Debug ფაილი - file_put_contents(__DIR__ . '/customfield_values_response.json', json_encode($results)); - echo json_encode($results); } catch (Exception $e) { http_response_code(500); diff --git a/public/userarea/gridRenderer.js b/public/userarea/gridRenderer.js index 63c2c96..502c21d 100644 --- a/public/userarea/gridRenderer.js +++ b/public/userarea/gridRenderer.js @@ -10,7 +10,8 @@ const PAGE_SIZE = 20; let revealedCount = PAGE_SIZE; - let dropdownOptionsCache = {}; // fieldId -> [{id, text}] + let dropdownOptionsCache = {}; // fieldId -> [{id, text}] — used only for small lists + const dropdownNameCache = {}; // "fieldId_valueId" -> label let clientData = []; // loaded from get_clienti.php let fixedFieldCache = window.fixedFieldDataCache || {}; window.fixedFieldDataCache = fixedFieldCache; @@ -53,36 +54,75 @@ data[rowIndex]._dirty = true; } - // ── Client data loading ──────────────────────────────────────────────── + // ── Client data (AJAX Select2, no bulk loading) ────────────────────── function formatClientLabel(client) { - const nome = client.Nominativo || ''; - const id = client.IdCliente || ''; - const code = (client.CodiceCliente || '').toString().trim(); - const suffix = (code.split('_')[1] || '').trim(); - const short = suffix || (code ? code.charAt(0) : '--'); - return `${nome.trim()} - ${short} (ID: ${id})`; + return (client.Nominativo || '').trim(); } + // Cache of resolved client names: id → name + const clientNameCache = {}; + + // Build select with only the selected option function buildClientOptionsHTML(selectedId) { let html = ''; - clientData.forEach(c => { - const id = c.IdCliente || ''; - const sel = String(id) === String(selectedId) ? ' selected' : ''; - html += ``; - }); + if (selectedId) { + const label = clientNameCache[selectedId] || selectedId; + html += ``; + } return html; } - async function loadClientData() { - if (clientData.length > 0) return; - try { - const resp = await fetch('get_clienti.php'); - const json = await resp.json(); - clientData = json.value || []; - } catch (e) { - console.error('Failed to load clients:', e); + // Pre-resolve all unique client IDs used in data rows, then re-render + async function resolveClientNames() { + const ids = new Set(); + data.forEach(row => { + if (row.idclient) ids.add(String(row.idclient)); + if (row.cliente_fornitore_id) ids.add(String(row.cliente_fornitore_id)); + // Fixed fields that are client-sourced + if (row.fixedFields) { + for (const [key, val] of Object.entries(row.fixedFields)) { + const cfg = fixedFieldApiConfig[key]; + if (cfg && cfg.source === 'clients' && val) ids.add(String(val)); + } + } + }); + + // Batch resolve via single request per ID + const unresolvedIds = [...ids].filter(id => !clientNameCache[id]); + if (unresolvedIds.length === 0) return; + + await Promise.all(unresolvedIds.map(async (id) => { + try { + const resp = await fetch('search_clienti.php?id=' + encodeURIComponent(id)); + const json = await resp.json(); + const item = (json.results || [])[0]; + if (item) clientNameCache[id] = item.text; + } catch (e) { /* ignore */ } + })); + } + + // Select2 AJAX config for client selects + const clientSelect2Config = { + placeholder: 'Search client...', + allowClear: true, + width: '100%', + minimumInputLength: 0, + dropdownCssClass: 'select2-dropdown-smaller', + ajax: { + url: 'search_clienti.php', + dataType: 'json', + delay: 150, + data: function(params) { return { q: params.term || '', limit: 20 }; }, + processResults: function(data) { return { results: data.results || [] }; }, + cache: true } + }; + + async function loadClientData() { + // No longer loads all clients — AJAX Select2 handles search + // Just resolve names for pre-selected values + await resolveClientNames(); } // ── Fixed field data loading ─────────────────────────────────────────── @@ -102,13 +142,10 @@ const config = fixedFieldApiConfig[fieldKey]; if (!config) return []; - // Client-sourced fields + // Client-sourced fields — handled by AJAX Select2, skip preloading if (config.source === 'clients') { - await loadClientData(); - const results = clientData.map(c => ({ id: c.IdCliente, text: formatClientLabel(c) })); - results.sort((a, b) => String(a.text).localeCompare(String(b.text), 'it', { sensitivity: 'base' })); - fixedFieldCache[fieldKey] = results; - return results; + fixedFieldCache[fieldKey] = []; + return []; } const cacheKey = fieldKey + (clientId ? '_' + clientId : ''); @@ -144,26 +181,23 @@ // ── Custom field dropdown data loading ───────────────────────────────── - async function loadDropdownOptions(fieldIds) { - const missing = fieldIds.filter(id => !dropdownOptionsCache[id]); - if (missing.length > 0) { - try { - const resp = await fetch('get_customfield_values.php?field_ids=' + missing.join(',')); - const json = await resp.json(); - // API returns { fieldId: [values] } directly (no success/data wrapper) - const entries = json.data ? json.data : json; - for (const [fid, values] of Object.entries(entries)) { - if (Array.isArray(values)) { - const sorted = values.sort((a, b) => - String(a.Valore || '').localeCompare(String(b.Valore || ''), 'it', { sensitivity: 'base' }) - ); - dropdownOptionsCache[fid] = sorted; - } - } - } catch (e) { - console.error('Failed to load dropdown options:', e); + + // Select2 AJAX config factory for SceltaMultipla + function sceltaSelect2Config(fieldId) { + return { + placeholder: 'Search...', + allowClear: true, + width: '100%', + minimumInputLength: 0, + ajax: { + url: 'search_customfield_values.php', + dataType: 'json', + delay: 150, + data: function(params) { return { field_id: fieldId, q: params.term || '', limit: 10 }; }, + processResults: function(data) { return { results: data.results || [] }; }, + cache: true } - } + }; } // ── Preload all data ─────────────────────────────────────────────────── @@ -192,13 +226,26 @@ } } - // 4. Custom field dropdowns - const fieldIds = columns - .filter(c => c.type === 'detail' && c.dataType === 'SceltaMultipla' && c.fieldId) - .map(c => String(c.fieldId)); - const uniqueIds = [...new Set(fieldIds)]; - if (uniqueIds.length > 0) { - await loadDropdownOptions(uniqueIds); + // 4. Warm server cache + resolve SceltaMultipla names in one request + const allFieldIds = [...new Set( + columns.filter(c => (c.type === 'detail' || c.type === 'main_field') && c.dataType === 'SceltaMultipla' && c.fieldId) + .map(c => String(c.fieldId)) + )]; + if (allFieldIds.length > 0) { + try { + const resp = await fetch('get_customfield_values.php?field_ids=' + allFieldIds.join(',')); + const json = await resp.json(); + const entries = json.data ? json.data : json; + for (const [fid, values] of Object.entries(entries)) { + if (Array.isArray(values)) { + values.forEach(v => { + dropdownNameCache[fid + '_' + v.IdCustomFieldsValue] = v.Valore || ''; + }); + } + } + } catch (e) { + console.error('Failed to preload dropdown data:', e); + } } console.log('[gridRenderer] preload done:', { @@ -308,7 +355,7 @@ if (col.dataType === 'SceltaMultipla') { const options = buildDropdownOptionsHTML(col.fieldId, value); - return ``; + return ``; } if (col.dataType === 'Data') { return ``; @@ -329,11 +376,24 @@ return ``; } + // Client-sourced fields → AJAX Select2 (like idclient) + const config = fixedFieldApiConfig[col.key]; + if (config && config.source === 'clients') { + const reqCls = col.isRequired ? ' required-input' : ''; + const req = col.isRequired ? ' required' : ''; + let opts = ''; + if (value) { + const label = clientNameCache[value] || value; + opts += ``; + } + return ``; + } + // Select — build from cache - const isApiField = !!fixedFieldApiConfig[col.key]; + const isApiField = !!config; const selectClass = isApiField ? 'api-fixed-select' : ''; let options = ''; - const cacheKey = fixedFieldApiConfig[col.key]?.dependsOn + const cacheKey = config?.dependsOn ? col.key + '_' + (data[rowIndex].idclient || '') : col.key; const items = fixedFieldCache[cacheKey] || []; @@ -349,11 +409,10 @@ function buildDropdownOptionsHTML(fieldId, selectedValue) { let html = ''; - const items = dropdownOptionsCache[fieldId] || []; - items.forEach(item => { - const sel = String(item.IdCustomFieldsValue) === String(selectedValue) ? ' selected' : ''; - html += ``; - }); + if (selectedValue) { + const label = dropdownNameCache[fieldId + '_' + selectedValue] || selectedValue; + html += ``; + } return html; } @@ -597,16 +656,16 @@ } async function populateTopRowSelects() { - // Client selects in top row + // Client selects in top row — AJAX mode const clientSel = document.getElementById('clientSelect'); if (clientSel) { clientSel.innerHTML = buildClientOptionsHTML(meta.defaultIdclient); - $(clientSel).select2({ placeholder: 'Select a client...', allowClear: true, width: '100%', minimumInputLength: 1 }); + $(clientSel).select2(clientSelect2Config); } const fornitSel = document.getElementById('clienteFornitoreSelect'); if (fornitSel) { fornitSel.innerHTML = buildClientOptionsHTML(''); - $(fornitSel).select2({ placeholder: 'Select a supplier...', allowClear: true, width: '100%', minimumInputLength: 1 }); + $(fornitSel).select2(clientSelect2Config); } // Fixed field selects in top row @@ -614,6 +673,12 @@ const fieldKey = sel.dataset.fixedKey; const config = fixedFieldApiConfig[fieldKey]; + // Client-sourced → init as AJAX Select2 + if (config && config.source === 'clients') { + $(sel).select2(clientSelect2Config); + return; + } + if (config && config.dependsOn) { // For dependent fields: merge all cached values across all clientIds const allItems = new Map(); @@ -633,14 +698,10 @@ } }); - // Custom field dropdowns in top row + // Custom field dropdowns in top row — AJAX Select2 topContainer.querySelectorAll('.dropdown-select[data-field-id]').forEach(sel => { const fieldId = sel.dataset.fieldId; - const items = dropdownOptionsCache[fieldId] || []; - sel.innerHTML = ''; - items.forEach(item => { - sel.add(new Option(item.Valore, item.IdCustomFieldsValue)); - }); + if (fieldId) $(sel).select2(sceltaSelect2Config(fieldId)); }); // Flatpickr in top row @@ -700,6 +761,17 @@ if (!input) return; const value = $(input).hasClass('select2-hidden-accessible') ? $(input).val() : input.value; + // Cache Select2 label so re-render shows name not ID + if (value && $(input).hasClass('select2-hidden-accessible')) { + const label = $(input).find('option:selected').text(); + if (label && label !== value) { + clientNameCache[value] = label; + // Also cache for SceltaMultipla + const fieldId = input.dataset?.fieldId; + if (fieldId) dropdownNameCache[fieldId + '_' + value] = label; + } + } + const col = columns[colIndex] || null; if (column === 'idclient') { @@ -730,6 +802,12 @@ const colType = cell.dataset.colType; const value = $(this).val() || ''; + // Cache selected label + if (value) { + const label = $(this).find('option:selected').text(); + if (label && label !== value) clientNameCache[value] = label; + } + if (colType === 'idclient') { data[rowIndex].idclient = value; data[rowIndex]._dirty = true; @@ -741,6 +819,16 @@ updateDirtyIndicator(); }); + // Cache labels on SceltaMultipla change + $(rowContainer).on('change', '.searchable-dropdown', function () { + const val = $(this).val(); + const fieldId = this.dataset.fieldId; + if (val && fieldId) { + const label = $(this).find('option:selected').text(); + if (label && label !== val) dropdownNameCache[fieldId + '_' + val] = label; + } + }); + // Select2 change on fixed field selects $(rowContainer).on('change', '.api-fixed-select', function () { const cell = this.closest('.grid-cell'); @@ -826,18 +914,11 @@ function initLazySelect2() { $(document).on('mouseenter', '.grid-row .grid-cell', function () { $(this).find('.searchable-client:not(.select2-hidden-accessible)').each(function () { - $(this).select2({ - placeholder: 'Select a client...', - allowClear: true, - width: '100%', - dropdownCssClass: 'select2-dropdown-smaller', - minimumInputLength: 1 - }); + $(this).select2(clientSelect2Config); }); - $(this).find('select[data-field-id]:not(.select2-hidden-accessible)').each(function () { - if ((this.options || []).length > 12) { - $(this).select2({ placeholder: 'Seleziona...', allowClear: true, width: '100%' }); - } + $(this).find('.searchable-dropdown:not(.select2-hidden-accessible)').each(function () { + const fieldId = this.dataset.fieldId; + if (fieldId) $(this).select2(sceltaSelect2Config(fieldId)); }); }); } diff --git a/public/userarea/import_insert.php b/public/userarea/import_insert.php index 8ddd231..1d1561d 100644 --- a/public/userarea/import_insert.php +++ b/public/userarea/import_insert.php @@ -47,7 +47,7 @@ $pdo = $db->getConnection(); $importReferenceCode = date('YmdHis') . '-' . uniqid(); // Recupera tutti i mapping dal template -$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field FROM template_mapping WHERE template_id = ?"); +$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?"); $stmt->execute([$template_id]); $allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -135,6 +135,15 @@ foreach ($selected_rows as $rowIndex) { $fieldValue = date('Y-m-d'); } } + // Apply auto_value if field is still empty + if (($fieldValue === null || $fieldValue === '') && !empty($mapping['auto_value']) && $mapping['auto_value'] !== 'none') { + if ($mapping['auto_value'] === 'import_date') { + $fieldValue = date('Y-m-d'); + } elseif ($mapping['auto_value'] === 'import_time') { + $fieldValue = date('H:i'); + } + } + if ($mapping['is_required'] && (is_null($fieldValue) || $fieldValue === '')) { error_log("Required field missing for mapping ID: " . $mapping['id'] . ", field: " . $mapping['field_label']); } diff --git a/public/userarea/search_clienti.php b/public/userarea/search_clienti.php new file mode 100644 index 0000000..1d87d91 --- /dev/null +++ b/public/userarea/search_clienti.php @@ -0,0 +1,58 @@ + 'IdCliente,Nominativo,CodiceCliente', + '$orderby' => 'Nominativo asc' + ]; + $data = $api->get("Cliente?" . http_build_query($params)); + if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true); + file_put_contents($cacheFile, json_encode($data)); + } + + $clients = $data['value'] ?? []; + + // If requesting by specific ID (for loading selected value) + if ($id !== null) { + foreach ($clients as $c) { + if ((int)$c['IdCliente'] === $id) { + echo json_encode(['results' => [['id' => $c['IdCliente'], 'text' => trim($c['Nominativo'] ?? '')]]]); + exit; + } + } + echo json_encode(['results' => []]); + exit; + } + + // Search by query + $results = []; + foreach ($clients as $c) { + $name = trim($c['Nominativo'] ?? ''); + $code = trim($c['CodiceCliente'] ?? ''); + if ($q === '' || mb_strpos(mb_strtolower($name), $q) !== false || mb_strpos(mb_strtolower($code), $q) !== false) { + $results[] = ['id' => $c['IdCliente'], 'text' => $name]; + if (count($results) >= $limit) break; + } + } + + echo json_encode(['results' => $results]); +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); +} diff --git a/public/userarea/search_customfield_values.php b/public/userarea/search_customfield_values.php new file mode 100644 index 0000000..65fb77e --- /dev/null +++ b/public/userarea/search_customfield_values.php @@ -0,0 +1,62 @@ + []]); + exit; +} + +try { + $cacheDir = __DIR__ . '/cache'; + $cacheFile = $cacheDir . '/customfield_' . $fieldId . '.json'; + + if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) { + $values = json_decode(file_get_contents($cacheFile), true); + } else { + $api = VisualLimsApiClient::getInstance(); + $data = $api->get("CustomField($fieldId)?\$expand=CustomFieldsValues"); + $values = $data['CustomFieldsValues'] ?? []; + if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true); + file_put_contents($cacheFile, json_encode($values)); + } + + // Lookup by ID + if ($id !== null) { + foreach ($values as $v) { + if ((int)($v['IdCustomFieldsValue'] ?? 0) === $id) { + echo json_encode(['results' => [['id' => $v['IdCustomFieldsValue'], 'text' => $v['Valore'] ?? '']]]); + exit; + } + } + echo json_encode(['results' => []]); + exit; + } + + // Search by query + $results = []; + foreach ($values as $v) { + $text = $v['Valore'] ?? ''; + if ($q === '' || mb_strpos(mb_strtolower($text), $q) !== false) { + $results[] = ['id' => $v['IdCustomFieldsValue'], 'text' => $text]; + if (count($results) >= $limit) break; + } + } + + // Sort alphabetically + usort($results, fn($a, $b) => strcasecmp($a['text'], $b['text'])); + + echo json_encode(['results' => $results]); +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); +} diff --git a/public/userarea/tolims.php b/public/userarea/tolims.php index a803a96..2bd16a0 100644 --- a/public/userarea/tolims.php +++ b/public/userarea/tolims.php @@ -1007,6 +1007,48 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string { padding-right: 18px !important; appearance: auto; } + /* Select2 inside filter row */ + .grid-filter-row .select2-container { + width: 100% !important; + min-width: 0; + flex: 1; + } + .grid-filter-row .select2-container .select2-selection--single { + height: 26px !important; + min-height: 26px !important; + display: flex !important; + align-items: center !important; + padding: 0 24px 0 6px; + font-size: 11px; + border: 1px solid #ced4da; + border-radius: 3px; + position: relative; + } + .grid-filter-row .select2-container .select2-selection__rendered { + line-height: 1 !important; + font-size: 11px; + padding: 0 !important; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .grid-filter-row .select2-container .select2-selection__arrow { + height: 26px !important; + position: absolute; + right: 2px; + top: 0; + } + .grid-filter-row .filter-wrap { + overflow: visible; + } + /* Hide our clear button when Select2 is active — Select2 has its own allowClear */ + .filter-wrap:has(.select2-container) .filter-clear-btn { + display: none; + } + .grid-filter-row .grid-cell { + overflow: visible !important; + } .filter-wrap { display: flex; align-items: center; @@ -1747,15 +1789,14 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string { +