cache and view improvements

This commit is contained in:
r.mubarakzyanov 2026-03-30 16:28:20 +03:00
parent 7463fc6726
commit 4f0dbc7e91
9 changed files with 588 additions and 136 deletions

View File

@ -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]);
}
}

View File

@ -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 = [

View File

@ -21,16 +21,25 @@ try {
}
$results = [];
$cacheDir = __DIR__ . '/cache';
if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true);
foreach ($fieldIds as $customFieldId) {
$endpoint = "CustomField($customFieldId)?\$expand=CustomFieldsValues";
$data = $api->get($endpoint);
$cacheFile = $cacheDir . '/customfield_' . $customFieldId . '.json';
$results[$customFieldId] = $data['CustomFieldsValues'] ?? [];
// Use cache if fresh (1 hour)
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
$results[$customFieldId] = json_decode(file_get_contents($cacheFile), true);
continue;
}
// Debug ფაილი
file_put_contents(__DIR__ . '/customfield_values_response.json', json_encode($results));
$endpoint = "CustomField($customFieldId)?\$expand=CustomFieldsValues";
$data = $api->get($endpoint);
$values = $data['CustomFieldsValues'] ?? [];
$results[$customFieldId] = $values;
file_put_contents($cacheFile, json_encode($values));
}
echo json_encode($results);
} catch (Exception $e) {

View File

@ -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,37 +54,76 @@
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 = '<option value="">Select a client...</option>';
clientData.forEach(c => {
const id = c.IdCliente || '';
const sel = String(id) === String(selectedId) ? ' selected' : '';
html += `<option value="${id}"${sel}>${esc(formatClientLabel(c))}</option>`;
});
if (selectedId) {
const label = clientNameCache[selectedId] || selectedId;
html += `<option value="${esc(String(selectedId))}" selected>${esc(String(label))}</option>`;
}
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 `<select class="cell-input dropdown-select ${cls}${reqCls}" data-field-id="${col.fieldId}"${req}>${options}</select>`;
return `<select class="cell-input dropdown-select searchable-dropdown ${cls}${reqCls}" data-field-id="${col.fieldId}"${req}>${options}</select>`;
}
if (col.dataType === 'Data') {
return `<input type="text" class="cell-input date-picker ${cls}${reqCls}" value="${v}"${req}>`;
@ -329,11 +376,24 @@
return `<input type="text" class="cell-input date-picker manual-input${reqCls} fixed-input" data-fixed-key="${col.key}" value="${esc(value)}"${req}>`;
}
// 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 = '<option value="">Select...</option>';
if (value) {
const label = clientNameCache[value] || value;
opts += `<option value="${esc(String(value))}" selected>${esc(String(label))}</option>`;
}
return `<select class="cell-input manual-input fixed-input searchable-client api-fixed-select${reqCls}" data-fixed-key="${col.key}" data-current-value="${esc(value)}"${req}>${opts}</select>`;
}
// Select — build from cache
const isApiField = !!fixedFieldApiConfig[col.key];
const isApiField = !!config;
const selectClass = isApiField ? 'api-fixed-select' : '';
let options = '<option value="">Seleziona...</option>';
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 = '<option value="">Seleziona...</option>';
const items = dropdownOptionsCache[fieldId] || [];
items.forEach(item => {
const sel = String(item.IdCustomFieldsValue) === String(selectedValue) ? ' selected' : '';
html += `<option value="${item.IdCustomFieldsValue}"${sel}>${esc(item.Valore)}</option>`;
});
if (selectedValue) {
const label = dropdownNameCache[fieldId + '_' + selectedValue] || selectedValue;
html += `<option value="${esc(String(selectedValue))}" selected>${esc(String(label))}</option>`;
}
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 = '<option value="">Seleziona...</option>';
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));
});
});
}

View File

@ -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']);
}

View File

@ -0,0 +1,58 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
$q = mb_strtolower(trim($_GET['q'] ?? ''));
$limit = max(1, min(50, intval($_GET['limit'] ?? 20)));
$id = isset($_GET['id']) ? intval($_GET['id']) : null;
try {
// Load from cache or API
$cacheFile = __DIR__ . '/cache/clienti.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
$data = json_decode(file_get_contents($cacheFile), true);
} else {
$api = VisualLimsApiClient::getInstance();
$params = [
'$select' => '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()]);
}

View File

@ -0,0 +1,62 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
$fieldId = intval($_GET['field_id'] ?? 0);
$q = mb_strtolower(trim($_GET['q'] ?? ''));
$id = isset($_GET['id']) ? intval($_GET['id']) : null;
$limit = max(1, min(50, intval($_GET['limit'] ?? 20)));
if (!$fieldId) {
echo json_encode(['results' => []]);
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()]);
}

View File

@ -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 {
</div>
<?php include('jsinclude.php'); ?>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(document).ready(function() {
// Resolve SceltaMultipla IDs → Valore (cells + filter selects)
// Resolve SceltaMultipla IDs → Valore in cells
(function() {
const sceltaEls = document.querySelectorAll('.scelta-value');
const sceltaFilters = document.querySelectorAll('select.scelta-filter');
const fieldIds = new Set();
sceltaEls.forEach(el => { if (el.dataset.fieldId) fieldIds.add(el.dataset.fieldId); });
sceltaFilters.forEach(el => { if (el.dataset.fieldId) fieldIds.add(el.dataset.fieldId); });
if (!fieldIds.size) return;
$.getJSON('get_customfield_values.php', { field_ids: [...fieldIds].join(',') }, function(data) {
@ -1766,7 +1807,6 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
lookup[fid][v.IdCustomFieldsValue] = v.Valore;
});
}
// Resolve cell values
sceltaEls.forEach(el => {
const fid = el.dataset.fieldId;
const raw = (el.textContent || '').trim();
@ -1774,55 +1814,116 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string {
el.textContent = lookup[fid][raw];
}
});
// Populate filter selects
sceltaFilters.forEach(sel => {
const fid = sel.dataset.fieldId;
if (!fid || !lookup[fid]) return;
const currentVal = sel.value;
// Sort by Valore
const sorted = Object.entries(lookup[fid]).sort((a, b) => a[1].localeCompare(b[1]));
sel.innerHTML = '<option value="">All</option>';
sorted.forEach(([id, label]) => {
const opt = document.createElement('option');
opt.value = id;
opt.textContent = label;
if (id === currentVal) opt.selected = true;
sel.appendChild(opt);
});
});
});
})();
// Load client names → resolve cells + populate filter selects
$.getJSON('get_clienti.php', function(data) {
const clients = data.value || [];
const map = {};
const sorted = [];
clients.forEach(c => {
const name = (c.Nominativo || '').trim();
map[c.IdCliente] = name;
sorted.push({ id: c.IdCliente, name: name });
// SceltaMultipla filter selects → AJAX Select2
document.querySelectorAll('select.scelta-filter').forEach(sel => {
const fieldId = sel.dataset.fieldId;
if (!fieldId) return;
const currentVal = sel.value;
if (currentVal) {
fetch('search_customfield_values.php?field_id=' + fieldId + '&id=' + encodeURIComponent(currentVal))
.then(r => r.json())
.then(json => {
const item = (json.results || [])[0];
if (item) sel.innerHTML = '<option value="">All</option><option value="' + item.id + '" selected>' + item.text + '</option>';
})
.catch(() => {});
}
$(sel).select2({
placeholder: 'All',
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: 20 }; },
processResults: function(data) { return { results: data.results || [] }; },
cache: true
}
});
});
sorted.sort((a, b) => a.name.localeCompare(b.name));
// Resolve cell values
// SceltaMultipla filter → submit on change
$(document).on('change', 'select.scelta-filter', function() {
submitFilters();
});
// Resolve client names by unique IDs (lightweight)
(function() {
const ids = new Set();
document.querySelectorAll('.client-text').forEach(el => {
const id = el.getAttribute('data-client-id');
if (id && map[id]) el.textContent = map[id];
if (id) ids.add(id);
});
if (!ids.size) return;
// Populate client filter selects
const promises = [...ids].map(id =>
fetch('search_clienti.php?id=' + encodeURIComponent(id))
.then(r => r.json())
.then(json => {
const item = (json.results || [])[0];
if (item) {
document.querySelectorAll(`.client-text[data-client-id="${id}"]`).forEach(el => {
el.textContent = item.text;
});
}
})
.catch(() => {})
);
Promise.all(promises);
})();
// Client filter selects → AJAX Select2
document.querySelectorAll('select.client-filter').forEach(sel => {
const currentVal = sel.value;
sel.innerHTML = '<option value="">All</option>';
sorted.forEach(c => {
const opt = document.createElement('option');
opt.value = c.id;
opt.textContent = c.name;
if (String(c.id) === currentVal) opt.selected = true;
sel.appendChild(opt);
if (currentVal) {
fetch('search_clienti.php?id=' + encodeURIComponent(currentVal))
.then(r => r.json())
.then(json => {
const item = (json.results || [])[0];
if (item) {
sel.innerHTML = '<option value="">All</option><option value="' + item.id + '" selected>' + item.text + '</option>';
}
$(sel).select2({
placeholder: 'All',
allowClear: true,
width: '100%',
minimumInputLength: 0,
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
}
});
});
} else {
$(sel).select2({
placeholder: 'All',
allowClear: true,
width: '100%',
minimumInputLength: 0,
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
}
});
}
});
// Client filter select2 → submit on change
$(document).on('change', 'select.client-filter', function() {
submitFilters();
});
// Modal close handlers

View File

@ -0,0 +1,110 @@
<?php
// Cache warm-up script.
// Run via CLI: php warm_cache.php
// Run via cron: every 30 min: php /path/to/warm_cache.php
// Run via HTTP: http://localhost:8000/public/userarea/warm_cache.php (requires auth)
$startTime = microtime(true);
$isCli = (php_sapi_name() === 'cli');
if (!$isCli) {
// When called via HTTP, require auth
include('include/headscript.php');
if (!Auth::check()) {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
header('Content-Type: application/json');
}
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
require_once __DIR__ . '/class/db-functions.php';
$cacheDir = __DIR__ . '/cache';
if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true);
$log = [];
function warmLog(string $msg, bool $isCli) {
global $log;
$line = date('H:i:s') . " $msg";
$log[] = $line;
if ($isCli) echo $line . PHP_EOL;
}
try {
$api = VisualLimsApiClient::getInstance();
$pdo = DBHandlerSelect::getInstance()->getConnection();
// 1. Clients
warmLog('[clients] Fetching...', $isCli);
$params = ['$select' => 'IdCliente,Nominativo,CodiceCliente', '$orderby' => 'Nominativo asc'];
$clientData = $api->get("Cliente?" . http_build_query($params));
file_put_contents($cacheDir . '/clienti.json', json_encode($clientData));
$clientCount = count($clientData['value'] ?? []);
warmLog("[clients] Cached $clientCount clients", $isCli);
// 2. Fixed fields: MoltiplicatorePrezzo, AnagraficaCertestObject, AnagraficaCertestService
$fixedFields = [
'MoltiplicatorePrezzo' => ['endpoint' => 'MoltiplicatorePrezzi', 'file' => 'moltiplicatori_prezzo.json'],
'AnagraficaCertestObject' => ['endpoint' => 'AnagraficaCertestObject', 'file' => 'anagrafica_certest_object.json'],
'AnagraficaCertestService' => ['endpoint' => 'AnagraficaCertestService', 'file' => 'anagrafica_certest_service.json'],
];
foreach ($fixedFields as $name => $cfg) {
warmLog("[$name] Fetching...", $isCli);
$data = $api->get($cfg['endpoint']);
file_put_contents($cacheDir . '/' . $cfg['file'], json_encode($data, JSON_PRETTY_PRINT));
$count = count($data['value'] ?? $data ?? []);
warmLog("[$name] Cached $count items", $isCli);
}
// 3. ClienteResponsabile — for each unique client in datadb
$stmt = $pdo->query("SELECT DISTINCT idclient FROM datadb WHERE idclient IS NOT NULL AND idclient > 0 ORDER BY idclient ASC");
$clientIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
warmLog("[ClienteResponsabile] Fetching for " . count($clientIds) . " clients...", $isCli);
foreach ($clientIds as $cid) {
$cid = (int)$cid;
$endpoint = "Cliente($cid)?\$expand=Responsabili";
$cacheFile = $cacheDir . '/cliente_responsabili_' . $cid . '.json';
try {
$data = $api->get($endpoint);
file_put_contents($cacheFile, json_encode($data, JSON_PRETTY_PRINT));
$count = count($data['Responsabili'] ?? []);
warmLog("[ClienteResponsabile] Client $cid: $count responsabili", $isCli);
} catch (Exception $e) {
warmLog("[ClienteResponsabile] Client $cid: ERROR " . $e->getMessage(), $isCli);
}
}
// 4. CustomField values — all SceltaMultipla field_ids from template_mapping
$stmt = $pdo->query("SELECT DISTINCT field_id FROM template_mapping WHERE data_type = 'SceltaMultipla' AND is_visible_import = 1 AND field_id IS NOT NULL ORDER BY field_id");
$fieldIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
warmLog("[CustomFields] Fetching " . count($fieldIds) . " fields...", $isCli);
foreach ($fieldIds as $fid) {
$fid = (int)$fid;
$cacheFile = $cacheDir . '/customfield_' . $fid . '.json';
try {
$endpoint = "CustomField($fid)?\$expand=CustomFieldsValues";
$data = $api->get($endpoint);
$values = $data['CustomFieldsValues'] ?? [];
file_put_contents($cacheFile, json_encode($values));
warmLog("[CustomField $fid] Cached " . count($values) . " values", $isCli);
} catch (Exception $e) {
warmLog("[CustomField $fid] ERROR " . $e->getMessage(), $isCli);
}
}
$elapsed = round(microtime(true) - $startTime, 1);
warmLog("Done in {$elapsed}s", $isCli);
} catch (Exception $e) {
warmLog("FATAL: " . $e->getMessage(), $isCli);
}
if (!$isCli) {
echo json_encode(['success' => true, 'log' => $log]);
}