/** * gridRenderer.js — Data-driven grid renderer for imported.php * * Reads window.gridData (array of row objects) and window.gridMeta (column defs, config). * Renders only visible rows into DOM. Propagate/edit operates on gridData array, * then re-renders visible rows. Save reads from gridData, not DOM. */ (function () { 'use strict'; const PAGE_SIZE = 20; let revealedCount = PAGE_SIZE; let dropdownOptionsCache = {}; // fieldId -> [{id, text}] let clientData = []; // loaded from get_clienti.php let fixedFieldCache = window.fixedFieldDataCache || {}; window.fixedFieldDataCache = fixedFieldCache; const data = window.gridData || []; const meta = window.gridMeta || {}; const columns = meta.columns || []; const totalRows = data.length; // ── DOM refs ──────────────────────────────────────────────────────────── let rowContainer = null; let headerContainer = null; let topContainer = null; let statusEl = null; // ── Helpers ───────────────────────────────────────────────────────────── function esc(str) { if (str === null || str === undefined) return ''; const d = document.createElement('div'); d.textContent = String(str); return d.innerHTML; } function getDetailValue(rowIndex, mappingId) { return data[rowIndex].details[String(mappingId)] ?? ''; } function setDetailValue(rowIndex, mappingId, value) { data[rowIndex].details[String(mappingId)] = value; data[rowIndex]._dirty = true; } function getFixedValue(rowIndex, key) { return data[rowIndex].fixedFields[key] ?? ''; } function setFixedValue(rowIndex, key, value) { data[rowIndex].fixedFields[key] = value; data[rowIndex]._dirty = true; } // ── Client data 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})`; } function buildClientOptionsHTML(selectedId) { let html = ''; clientData.forEach(c => { const id = c.IdCliente || ''; const sel = String(id) === String(selectedId) ? ' selected' : ''; 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); } } // ── Fixed field data loading ─────────────────────────────────────────── const fixedFieldApiConfig = { MoltiplicatorePrezzo: { endpoint: 'MoltiplicatorePrezzo', idKey: 'IdMoltiplicatorePrezzo', textKey: 'Descrizione' }, AnagraficaCertestObject: { endpoint: 'AnagraficaCertestObject', idKey: 'IdAnagrafica', textKey: 'NomeAnagrafica' }, AnagraficaCertestService: { endpoint: 'AnagraficaCertestService', idKey: 'IdAnagrafica', textKey: 'NomeAnagrafica' }, ClienteResponsabile: { endpoint: 'ClienteResponsabile', idKey: 'IdClienteResponsabile', textKey: 'Nominativo', dependsOn: 'idclient', getParams: (cid) => ({ id_cliente: cid }) }, ClienteFornitore: { source: 'clients' }, ClienteAnalisi: { source: 'clients' }, }; let _pendingFixed = {}; async function loadFixedFieldOptions(fieldKey, clientId) { const config = fixedFieldApiConfig[fieldKey]; if (!config) return []; // Client-sourced fields 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; } const cacheKey = fieldKey + (clientId ? '_' + clientId : ''); if (fixedFieldCache[cacheKey]) return fixedFieldCache[cacheKey]; if (_pendingFixed[cacheKey]) return _pendingFixed[cacheKey]; const params = { field: config.endpoint }; if (config.dependsOn && clientId) { Object.assign(params, config.getParams(clientId)); } _pendingFixed[cacheKey] = (async () => { try { const resp = await fetch('get_fixed_field_data.php?' + new URLSearchParams(params)); const json = await resp.json(); let items = (fieldKey === 'ClienteResponsabile') ? (json.Responsabili || []) : (json.value || json.d?.results || json || []); const results = items.map(item => ({ id: item[config.idKey], text: (item.Codice ? item.Codice + ' - ' : '') + (item[config.textKey] || '') })).sort((a, b) => String(a.text).localeCompare(String(b.text), 'it', { sensitivity: 'base' })); fixedFieldCache[cacheKey] = results; delete _pendingFixed[cacheKey]; return results; } catch (e) { delete _pendingFixed[cacheKey]; console.error('Failed to load fixed field ' + fieldKey, e); return []; } })(); return _pendingFixed[cacheKey]; } // ── 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); } } } // ── Preload all data ─────────────────────────────────────────────────── async function preloadAllData() { // 1. Clients await loadClientData(); // 2. Non-dependent fixed fields const nonDependent = Object.keys(fixedFieldApiConfig).filter(k => !fixedFieldApiConfig[k].dependsOn && !fixedFieldApiConfig[k].source); await Promise.all(nonDependent.map(k => loadFixedFieldOptions(k))); // Client-sourced fixed fields const clientSourced = Object.keys(fixedFieldApiConfig).filter(k => fixedFieldApiConfig[k].source === 'clients'); await Promise.all(clientSourced.map(k => loadFixedFieldOptions(k))); // 3. Dependent fixed fields — collect unique clientIds const clientIds = new Set(); data.forEach(row => { if (row.idclient) clientIds.add(String(row.idclient)); }); const dependent = Object.keys(fixedFieldApiConfig).filter(k => fixedFieldApiConfig[k].dependsOn); for (const fieldKey of dependent) { for (const cid of clientIds) { await loadFixedFieldOptions(fieldKey, cid); } } // 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); } console.log('[gridRenderer] preload done:', { clients: clientData.length, fixedFieldCache: Object.keys(fixedFieldCache), dropdownOptionsCache: Object.keys(dropdownOptionsCache), }); } // ── Cell rendering ───────────────────────────────────────────────────── function createCell(col, rowIndex, cellIndex) { const div = document.createElement('div'); div.className = 'grid-cell editable-cell'; div.dataset.col = col.key; div.dataset.colType = col.type; div.dataset.row = rowIndex; div.dataset.index = cellIndex; div.style.flex = `0 0 ${col.width}px`; const row = data[rowIndex]; switch (col.type) { case 'main_field': div.innerHTML = createInputHTML(col, row.mainFieldValue || '', rowIndex); break; case 'status': { const st = row.status || 'i'; const label = st === 'i' ? 'Imported' : (st === 'P' ? 'In Progress' : 'To LIMS'); let html = `${label}`; if (row.commessaweb) { html += `${esc(row.commessaweb)}`; } div.innerHTML = html; div.classList.remove('editable-cell'); break; } case 'idclient': { const sel = document.createElement('select'); sel.className = 'cell-input dropdown-select client-select searchable-client'; sel.dataset.currentValue = row.idclient || ''; sel.innerHTML = buildClientOptionsHTML(row.idclient); div.appendChild(sel); break; } case 'cliente_fornitore_id': { const sel = document.createElement('select'); sel.className = 'cell-input dropdown-select client-select searchable-client fornitore-select'; sel.dataset.currentValue = row.cliente_fornitore_id || ''; sel.innerHTML = buildClientOptionsHTML(row.cliente_fornitore_id); div.appendChild(sel); break; } case 'detail': { const val = getDetailValue(rowIndex, col.key); div.innerHTML = createInputHTML(col, val, rowIndex); break; } case 'tested_component': div.style.overflow = 'visible'; div.innerHTML = `
`; break; case 'awb': div.innerHTML = `` + `` + ``; break; case 'tracking': div.className = 'grid-cell tracking-info'; div.dataset.row = rowIndex; div.innerHTML = 'Shipment Info'; break; case 'fixed': { const val = getFixedValue(rowIndex, col.key); div.innerHTML = createFixedFieldHTML(col, val, rowIndex); break; } case 'static': { const val = row[col.key] || ''; div.classList.remove('editable-cell'); if (col.key === 'filename_import' && val) { div.innerHTML = `File`; } else { div.innerHTML = `${esc(val)}`; } break; } } return div; } function createInputHTML(col, value, rowIndex) { const cls = col.isManual ? 'manual-input' : 'auto-input'; const reqCls = col.isRequired ? ' required-input' : ''; const req = col.isRequired ? ' required' : ''; const v = esc(value); if (col.dataType === 'SceltaMultipla') { const options = buildDropdownOptionsHTML(col.fieldId, value); return ``; } if (col.dataType === 'Data') { return ``; } if (col.dataType === 'INT') { return ``; } if (col.autoValue === 'import_time' || (meta.timeLabels || []).includes(col.label)) { return ``; } return ``; } function createFixedFieldHTML(col, value, rowIndex) { if (col.dataType === 'DATE') { const reqCls = col.isRequired ? ' required-input' : ''; const req = col.isRequired ? ' required' : ''; return ``; } // Select — build from cache const isApiField = !!fixedFieldApiConfig[col.key]; const selectClass = isApiField ? 'api-fixed-select' : ''; let options = ''; const cacheKey = fixedFieldApiConfig[col.key]?.dependsOn ? col.key + '_' + (data[rowIndex].idclient || '') : col.key; const items = fixedFieldCache[cacheKey] || []; items.forEach(item => { const sel = String(item.id) === String(value) ? ' selected' : ''; options += ``; }); const reqCls = col.isRequired ? ' required-input' : ''; const req = col.isRequired ? ' required' : ''; return ``; } function buildDropdownOptionsHTML(fieldId, selectedValue) { let html = ''; const items = dropdownOptionsCache[fieldId] || []; items.forEach(item => { const sel = String(item.IdCustomFieldsValue) === String(selectedValue) ? ' selected' : ''; html += ``; }); return html; } // ── Row rendering ────────────────────────────────────────────────────── function renderActionButtons(rowIndex) { const row = data[rowIndex]; const isExported = row.status === 'l'; const div = document.createElement('div'); div.className = 'grid-cell button-cell'; div.style.flex = '0 0 auto'; div.style.position = 'relative'; let html = ''; if (meta.isAdmin) { html += ``; } html += ``; html += ``; html += ``; html += ``; div.innerHTML = html; return div; } function renderRow(rowIndex) { const row = data[rowIndex]; const rowDiv = document.createElement('div'); rowDiv.className = 'grid-row'; rowDiv.dataset.id = row.iddatadb; if (row._dirty) rowDiv.classList.add('row-dirty'); // Action buttons rowDiv.appendChild(renderActionButtons(rowIndex)); // All columns let cellIndex = 1; columns.forEach(col => { rowDiv.appendChild(createCell(col, rowIndex, cellIndex)); cellIndex++; }); // Restore validation errors if present if (row._validationErrors && row._validationErrors.length > 0) { rowDiv.classList.add('validation-row-error'); const messages = []; row._validationErrors.forEach(err => { messages.push(err.message); if (!err.field) return; let cell = null; if (err.field.startsWith('field_label:')) { const label = err.field.substring('field_label:'.length); const headers = document.querySelectorAll('.grid-header'); let targetIndex = null; headers.forEach(h => { if (h.textContent.trim() === label) targetIndex = h.getAttribute('data-index'); }); if (targetIndex) cell = rowDiv.querySelector(`.grid-cell[data-index="${targetIndex}"]`); } else { cell = rowDiv.querySelector(`.grid-cell[data-col="${err.field}"]`); } if (cell) { cell.classList.add('validation-error'); cell.querySelectorAll('input, select').forEach(el => el.classList.add('input-validation-error')); let tooltip = cell.querySelector('.validation-tooltip'); if (!tooltip) { tooltip = document.createElement('div'); tooltip.className = 'validation-tooltip'; cell.appendChild(tooltip); } tooltip.textContent = err.message; } }); // Show error msg on button cell const btnCell2 = rowDiv.querySelector('.button-cell'); if (btnCell2) { const errorEl = document.createElement('div'); errorEl.className = 'batch-error-msg'; errorEl.textContent = 'Warning — click for details'; errorEl.addEventListener('click', () => { document.getElementById('exportResponseMessage').innerHTML = messages.join('
'); document.getElementById('exportResponseModalLabel').textContent = 'Validation Error (id: ' + row.iddatadb + ')'; new bootstrap.Modal(document.getElementById('exportResponseModal'), { keyboard: false }).show(); }); btnCell2.appendChild(errorEl); } } // Restore export error indicator if present if (row._exportError) { rowDiv.classList.add('batch-row-error'); const btnCell = rowDiv.querySelector('.button-cell'); if (btnCell) { const errorEl = document.createElement('div'); errorEl.className = 'batch-error-msg'; errorEl.textContent = 'Warning — click for details'; errorEl.addEventListener('click', () => { document.getElementById('exportResponseMessage').innerHTML = row._exportError.replace(/\n/g, '
'); document.getElementById('exportResponseModalLabel').textContent = 'Error (id: ' + row.iddatadb + ')'; new bootstrap.Modal(document.getElementById('exportResponseModal'), { keyboard: false }).show(); }); btnCell.appendChild(errorEl); } } return rowDiv; } // ── Render visible rows ──────────────────────────────────────────────── function renderVisibleRows() { if (!rowContainer) return; // Destroy Select2 on existing rows $(rowContainer).find('.select2-hidden-accessible').select2('destroy'); rowContainer.innerHTML = ''; const end = Math.min(revealedCount, totalRows); for (let i = 0; i < end; i++) { rowContainer.appendChild(renderRow(i)); } // Init flatpickr on visible date pickers initFlatpickr(); updateStatus(); updateDirtyIndicator(); } function renderSingleRow(rowIndex) { const existing = rowContainer.querySelector(`.grid-row[data-id="${data[rowIndex].iddatadb}"]`); if (!existing) return; // Destroy Select2 before removing $(existing).find('.select2-hidden-accessible').select2('destroy'); const newRow = renderRow(rowIndex); existing.replaceWith(newRow); // Init flatpickr $(newRow).find('.date-picker').each(function () { flatpickr(this, { dateFormat: 'Y-m-d', allowInput: true }); }); } function revealNextBatch() { if (revealedCount >= totalRows) return; const start = revealedCount; revealedCount = Math.min(revealedCount + PAGE_SIZE, totalRows); for (let i = start; i < revealedCount; i++) { rowContainer.appendChild(renderRow(i)); } initFlatpickr(); updateStatus(); } function initFlatpickr() { $(rowContainer).find('.date-picker:not(.flatpickr-input)').each(function () { flatpickr(this, { dateFormat: 'Y-m-d', allowInput: true }); }); } // ── Headers & Propagate row ──────────────────────────────────────────── function renderHeaders() { if (!headerContainer) return; headerContainer.innerHTML = ''; // Actions header const actH = document.createElement('div'); actH.className = 'grid-header button-header'; actH.style.flex = '0 0 380px'; actH.textContent = 'Actions'; headerContainer.appendChild(actH); let idx = 1; columns.forEach(col => { const h = document.createElement('div'); h.className = 'grid-header'; h.dataset.index = idx; h.style.flex = `0 0 ${col.width}px`; h.style.position = 'relative'; h.innerHTML = esc(col.label) + '
'; headerContainer.appendChild(h); idx++; }); } function renderTopRow() { if (!topContainer) return; topContainer.innerHTML = ''; // Empty cell for actions column const empty = document.createElement('div'); empty.className = 'grid-cell save-all-cell'; topContainer.appendChild(empty); columns.forEach((col, colIdx) => { const cell = document.createElement('div'); cell.className = 'grid-cell grid-top-cell'; cell.style.flex = `0 0 ${col.width}px`; if (col.editable === false || col.type === 'static' || col.type === 'tracking') { // Empty top cell } else if (col.type === 'idclient') { cell.innerHTML = `` + ``; } else if (col.type === 'cliente_fornitore_id') { cell.innerHTML = `` + ``; } else if (col.type === 'fixed' && col.dataType === 'DATE') { cell.innerHTML = `` + ``; } else if (col.type === 'fixed') { const isApiField = !!fixedFieldApiConfig[col.key]; if (isApiField) { cell.innerHTML = `` + ``; } else { cell.innerHTML = `` + ``; } } else if (col.type === 'detail' || col.type === 'main_field') { if (col.dataType === 'SceltaMultipla') { cell.innerHTML = `` + ``; } else if (col.dataType === 'Data') { cell.innerHTML = `` + ``; } else { cell.innerHTML = `` + ``; } } topContainer.appendChild(cell); }); // Populate top row selects populateTopRowSelects(); } async function populateTopRowSelects() { // Client selects in top row const clientSel = document.getElementById('clientSelect'); if (clientSel) { clientSel.innerHTML = buildClientOptionsHTML(meta.defaultIdclient); $(clientSel).select2({ placeholder: 'Select a client...', allowClear: true, width: '100%', minimumInputLength: 1 }); } const fornitSel = document.getElementById('clienteFornitoreSelect'); if (fornitSel) { fornitSel.innerHTML = buildClientOptionsHTML(''); $(fornitSel).select2({ placeholder: 'Select a supplier...', allowClear: true, width: '100%', minimumInputLength: 1 }); } // Fixed field selects in top row topContainer.querySelectorAll('.api-fixed-select').forEach(sel => { const fieldKey = sel.dataset.fixedKey; const config = fixedFieldApiConfig[fieldKey]; if (config && config.dependsOn) { // For dependent fields: merge all cached values across all clientIds const allItems = new Map(); for (const [key, items] of Object.entries(fixedFieldCache)) { if (key.startsWith(fieldKey + '_')) { items.forEach(item => allItems.set(String(item.id), item)); } } sel.innerHTML = ''; [...allItems.values()] .sort((a, b) => String(a.text).localeCompare(String(b.text), 'it', { sensitivity: 'base' })) .forEach(item => sel.add(new Option(item.text, item.id))); } else { const items = fixedFieldCache[fieldKey] || []; sel.innerHTML = ''; items.forEach(item => sel.add(new Option(item.text, item.id))); } }); // Custom field dropdowns in top row 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)); }); }); // Flatpickr in top row topContainer.querySelectorAll('.date-picker').forEach(el => { flatpickr(el, { dateFormat: 'Y-m-d', allowInput: true }); }); } // ── Event delegation ─────────────────────────────────────────────────── function attachEvents() { if (!rowContainer) return; // Cell value changes → write to gridData rowContainer.addEventListener('change', function (e) { const cell = e.target.closest('.grid-cell'); if (!cell || !cell.dataset.row) return; const rowIndex = parseInt(cell.dataset.row); const colType = cell.dataset.colType; const colKey = cell.dataset.col; const value = e.target.value; if (colType === 'detail' || colType === 'main_field') { if (colType === 'main_field') { data[rowIndex].mainFieldValue = value; } setDetailValue(rowIndex, colKey, value); } else if (colType === 'fixed') { setFixedValue(rowIndex, colKey, value); } else if (colType === 'idclient') { data[rowIndex].idclient = value; data[rowIndex]._dirty = true; } else if (colType === 'cliente_fornitore_id') { data[rowIndex].cliente_fornitore_id = value; data[rowIndex]._dirty = true; console.log('[gridRenderer] cliente_fornitore_id changed:', rowIndex, value); } // Visual feedback cell.classList.add('cell-changed'); updateDirtyIndicator(); }); // Propagate buttons document.addEventListener('click', function (e) { const btn = e.target.closest('.propagate-btn'); if (!btn) return; const colIndex = parseInt(btn.dataset.colIndex); const column = btn.dataset.column; if (isNaN(colIndex) && !column) return; // Get value from the input/select in the same cell const cell = btn.closest('.grid-cell') || btn.closest('.grid-top-cell'); if (!cell) return; const input = cell.querySelector('select, input'); if (!input) return; const value = $(input).hasClass('select2-hidden-accessible') ? $(input).val() : input.value; const col = columns[colIndex] || null; if (column === 'idclient') { data.forEach(row => { row.idclient = value; row._dirty = true; }); } else if (column === 'cliente_fornitore_id') { data.forEach(row => { row.cliente_fornitore_id = value; row._dirty = true; }); } else if (column && column.startsWith('fixed_')) { const fixedKey = column.replace('fixed_', ''); data.forEach(row => { row.fixedFields[fixedKey] = value; row._dirty = true; }); } else if (col) { if (col.type === 'detail' || col.type === 'main_field') { data.forEach(row => { row.details[col.key] = value; if (col.type === 'main_field') row.mainFieldValue = value; row._dirty = true; }); } } renderVisibleRows(); }); // Select2 change events (don't bubble via native addEventListener) $(rowContainer).on('change', '.searchable-client', function () { const cell = this.closest('.grid-cell'); if (!cell || !cell.dataset.row) return; const rowIndex = parseInt(cell.dataset.row); const colType = cell.dataset.colType; const value = $(this).val() || ''; if (colType === 'idclient') { data[rowIndex].idclient = value; data[rowIndex]._dirty = true; } else if (colType === 'cliente_fornitore_id') { data[rowIndex].cliente_fornitore_id = value; data[rowIndex]._dirty = true; } cell.classList.add('cell-changed'); updateDirtyIndicator(); }); // Select2 change on fixed field selects $(rowContainer).on('change', '.api-fixed-select', function () { const cell = this.closest('.grid-cell'); if (!cell || !cell.dataset.row) return; const rowIndex = parseInt(cell.dataset.row); const key = this.dataset.fixedKey || cell.dataset.col; const value = $(this).val() || ''; if (key) { data[rowIndex].fixedFields[key] = value; data[rowIndex]._dirty = true; cell.classList.add('cell-changed'); updateDirtyIndicator(); } }); // Scroll function onScroll() { if (revealedCount >= totalRows) return; const docEl = document.documentElement; const scrollBottom = Math.max(docEl.scrollTop + docEl.clientHeight, document.body.scrollTop + window.innerHeight); if (scrollBottom >= docEl.scrollHeight - 300) { revealNextBatch(); } } window.addEventListener('scroll', onScroll, { passive: true }); document.querySelector('.page-wrapper')?.addEventListener('scroll', onScroll, { passive: true }); } // ── Save ─────────────────────────────────────────────────────────────── window.buildSavePayload = function (rowIndex) { const row = data[rowIndex]; const formData = new FormData(); formData.append('iddatadb', row.iddatadb); // Details for (const [mappingId, value] of Object.entries(row.details)) { formData.append(`details${mappingId}field_value`, value); } // Client if (row.idclient) formData.append('idclient', row.idclient); formData.append('cliente_fornitore_id', row.cliente_fornitore_id || ''); // Fixed fields → real column names const aliasMap = meta.fixedAliasMap || {}; for (const [logicalKey, value] of Object.entries(row.fixedFields)) { const realKey = aliasMap[logicalKey] || logicalKey; formData.append(realKey, value); } return formData; }; // ── Dirty indicator ──────────────────────────────────────────────────── function updateDirtyIndicator() { const dirtyCount = data.filter(r => r._dirty).length; const indicator = document.getElementById('unsavedChanges'); const changedEl = document.getElementById('changedRows'); if (indicator) { indicator.style.display = dirtyCount > 0 ? '' : 'none'; } if (changedEl) { changedEl.textContent = dirtyCount > 0 ? `(${dirtyCount} rows)` : ''; } } function updateStatus() { if (!statusEl) return; const shown = Math.min(revealedCount, totalRows); if (shown >= totalRows) { statusEl.textContent = `All ${totalRows} rows loaded`; setTimeout(() => statusEl.style.display = 'none', 2000); } else { statusEl.textContent = `Showing ${shown} of ${totalRows} rows — scroll down for more`; } } // ── Lazy Select2 for row selects ─────────────────────────────────────── 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).find('select[data-field-id]:not(.select2-hidden-accessible)').each(function () { if ((this.options || []).length > 12) { $(this).select2({ placeholder: 'Seleziona...', allowClear: true, width: '100%' }); } }); }); } // ── Init ─────────────────────────────────────────────────────────────── async function init() { rowContainer = document.getElementById('gridRowContainer'); headerContainer = document.getElementById('gridHeaderContainer'); topContainer = document.getElementById('gridTopContainer'); if (!rowContainer) { console.error('gridRenderer: #gridRowContainer not found'); return; } // Status bar statusEl = document.createElement('div'); statusEl.style.cssText = 'text-align:center; padding:8px; color:#666; font-size:12px;'; if (totalRows > PAGE_SIZE) { statusEl.textContent = `Loading data...`; rowContainer.parentElement.appendChild(statusEl); } // Preload all dropdown/field data await preloadAllData(); // Render renderHeaders(); renderTopRow(); renderVisibleRows(); // Events attachEvents(); initLazySelect2(); } // Start when DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // ── Public API ───────────────────────────────────────────────────────── window.gridRenderer = { renderVisibleRows, renderSingleRow, revealNextBatch, buildSavePayload: window.buildSavePayload, getData: () => data, getMeta: () => meta, getClientData: () => clientData, getDirtyRows: () => data.filter(r => r._dirty).map((r, i) => i), clearDirty: (rowIndex) => { data[rowIndex]._dirty = false; updateDirtyIndicator(); }, }; })();