cache and view improvements

This commit is contained in:
2026-03-30 16:28:20 +03:00
parent 7463fc6726
commit 4f0dbc7e91
9 changed files with 588 additions and 136 deletions
+162 -81
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,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 = '<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));
});
});
}