cache and view improvements
This commit is contained in:
+162
-81
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user