imported && tolims

This commit is contained in:
r.mubarakzyanov 2026-03-26 16:05:17 +03:00
parent 223688c372
commit 2a7b1fae17
11 changed files with 4480 additions and 23 deletions

View File

@ -0,0 +1,61 @@
<?php
include('include/headscript.php');
header('Content-Type: application/json');
try {
$input = json_decode(file_get_contents('php://input'), true);
$templateId = intval($input['template_id'] ?? 0);
if (!$templateId) {
throw new Exception('Template ID missing');
}
$userId = $iduserlogin ?? 1;
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Get default idclient from template
$stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
$stmt->execute([$templateId]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
$idclient = $template['idclient'] ?? null;
// Generate import reference code
$importReferenceCode = date('YmdHis') . '-' . uniqid();
// Insert empty record
$stmt = $pdo->prepare("
INSERT INTO datadb (templateid, user_id, status, idclient, importreferencecode, importdate)
VALUES (?, ?, 'i', ?, ?, NOW())
");
$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 = ?");
$mappingStmt->execute([$templateId]);
$mappings = $mappingStmt->fetchAll(PDO::FETCH_COLUMN);
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]);
}
}
// Get user name
$userStmt = $pdo->prepare("SELECT CONCAT(first_name, ' ', last_name) AS user_name FROM auth_users WHERE id = ?");
$userStmt->execute([$userId]);
$userName = $userStmt->fetchColumn() ?: '';
echo json_encode([
'success' => true,
'iddatadb' => $iddatadb,
'importreferencecode' => $importReferenceCode,
'user_name' => $userName,
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}

View File

@ -0,0 +1,555 @@
/**
* exportLims_gridData.js Export to LIMS using gridData (for imported.php)
*
* Replaces export_to_lims.js for pages that use gridData instead of DOM-rendered rows.
* Single export + batch export (Export All) with validation.
*/
(function () {
'use strict';
let pendingConfirmHandler = null;
let batchRunning = false;
Object.defineProperty(window, "batchRunning", { get: () => batchRunning });
let batchCancelled = false;
let pendingBatchConfirmHandler = null;
// ── Helpers ──────────────────────────────────────────────────────────
function cleanupBackdrop() {
document.querySelectorAll(".modal-backdrop").forEach(b => b.remove());
document.body.classList.remove("modal-open");
document.body.style.paddingRight = "";
const overlay = document.querySelector(".overlay.toggle-icon");
if (overlay) overlay.style.display = "none";
}
function getGridRow(iddatadb) {
return document.querySelector(`.grid-row[data-id="${iddatadb}"]`);
}
function getRowIndexByIddatadb(iddatadb) {
return (window.gridData || []).findIndex(r => String(r.iddatadb) === String(iddatadb));
}
// ── Validation ──────────────────────────────────────────────────────
async function validateRows(rowsToValidate) {
const response = await fetch("validate_export.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rows: rowsToValidate }),
});
if (!response.ok) throw new Error(`Validation HTTP error: ${response.status}`);
return response.json();
}
function clearValidationErrors() {
// Clear from gridData
(window.gridData || []).forEach(row => { delete row._validationErrors; delete row._exportError; });
document.querySelectorAll(".grid-cell.validation-error").forEach(cell => {
cell.classList.remove("validation-error");
cell.querySelectorAll(".input-validation-error").forEach(el => el.classList.remove("input-validation-error"));
const tooltip = cell.querySelector(".validation-tooltip");
if (tooltip) tooltip.remove();
});
document.querySelectorAll(".grid-row.validation-row-error").forEach(row => row.classList.remove("validation-row-error"));
clearAllRowErrors();
}
function showValidationErrors(gridRow, iddatadb, errors) {
// Store in gridData for re-render persistence
const idx = getRowIndexByIddatadb(iddatadb);
if (idx >= 0) window.gridData[idx]._validationErrors = errors;
if (!gridRow) return;
gridRow.classList.add("validation-row-error");
const messages = [];
errors.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 = gridRow.querySelector(`.grid-cell[data-index="${targetIndex}"]`);
}
} else {
cell = gridRow.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;
}
});
showRowError(gridRow, iddatadb, messages.join("\n"));
}
// ── Send export ─────────────────────────────────────────────────────
async function sendExport(iddatadb, batchUuid) {
const formData = new FormData();
formData.append("iddatadb", iddatadb);
if (batchUuid) formData.append("batch_uuid", batchUuid);
const response = await fetch("export_to_lims.php", { method: "POST", body: formData });
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
if (data.success) {
// Update gridData
const idx = getRowIndexByIddatadb(iddatadb);
if (idx >= 0) {
window.gridData[idx].status = 'l';
window.gridData[idx].commessaweb = data.commessaweb;
}
// Update visible DOM row
const gridRow = getGridRow(iddatadb);
if (gridRow) {
const statusBadge = gridRow.querySelector('.grid-cell[data-col="status"] .status-badge');
if (statusBadge) {
statusBadge.classList.remove("status-i", "status-P");
statusBadge.classList.add("status-l");
statusBadge.textContent = "To LIMS";
}
const statusCell = gridRow.querySelector('.grid-cell[data-col="status"]');
if (statusCell && data.commessaweb) {
let cwSpan = statusCell.querySelector(".commessaweb-code");
if (!cwSpan) {
cwSpan = document.createElement("span");
cwSpan.className = "commessaweb-code";
cwSpan.style.cssText = "display:block; font-size:0.75em; color:#555; margin-top:2px;";
statusCell.appendChild(cwSpan);
}
cwSpan.textContent = data.commessaweb;
}
const exportBtn = gridRow.querySelector(".export-lims-btn");
if (exportBtn) {
exportBtn.disabled = true;
exportBtn.style.background = "#ccc";
exportBtn.style.cursor = "not-allowed";
exportBtn.style.opacity = "0.5";
}
}
}
return data;
}
// ── Show result modal ───────────────────────────────────────────────
function showExportResult(data) {
const el = document.getElementById("exportResponseModal");
if (!el) return;
const modal = new bootstrap.Modal(el, { keyboard: false });
const msg = document.getElementById("exportResponseMessage");
const label = document.getElementById("exportResponseModalLabel");
if (data.success) {
msg.innerHTML = `${data.message.replace(/\n/g, "<br>")}` +
`<br>ID CommessaWeb: ${data.idcommessaweb}` +
`<br>Codice CommessaWeb: ${data.commessaweb}` +
(data.totalPhotos > 0 ? `<br>Foto: ${data.totalPhotos}` : "");
label.textContent = "Esportazione Completata";
} else {
msg.textContent = `Errore: ${data.message}`;
label.textContent = "Errore Esportazione";
}
modal.show();
el.addEventListener("hidden.bs.modal", cleanupBackdrop, { once: true });
}
// ── Row UI helpers ──────────────────────────────────────────────────
function setRowExporting(row, active) {
if (!row) return;
const btnCell = row.querySelector(".button-cell");
if (active) {
row.classList.remove("batch-disabled");
row.classList.add("batch-exporting");
if (btnCell) {
btnCell.querySelectorAll(".action-btn").forEach(b => { b.dataset.prevDisplay = b.style.display; b.style.display = "none"; });
const spinner = document.createElement("span");
spinner.className = "batch-row-spinner";
spinner.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Exporting...';
btnCell.appendChild(spinner);
}
} else {
row.classList.remove("batch-exporting");
if (btnCell) {
const spinner = btnCell.querySelector(".batch-row-spinner");
if (spinner) spinner.remove();
btnCell.querySelectorAll(".action-btn").forEach(b => { b.style.display = b.dataset.prevDisplay || ""; delete b.dataset.prevDisplay; });
}
}
}
function showRowError(row, iddatadb, message) {
if (!row) return;
row.classList.add("batch-row-error");
const btnCell = row.querySelector(".button-cell");
if (btnCell) {
const old = btnCell.querySelector(".batch-error-msg");
if (old) old.remove();
const errorEl = document.createElement("div");
errorEl.className = "batch-error-msg";
errorEl.textContent = "Warning — click for details";
errorEl.addEventListener("click", () => {
document.getElementById("exportResponseMessage").innerHTML = message.replace(/\n/g, "<br>");
document.getElementById("exportResponseModalLabel").textContent = "Error (id: " + iddatadb + ")";
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
});
btnCell.appendChild(errorEl);
}
}
function clearAllRowErrors() {
document.querySelectorAll(".grid-row.batch-row-error").forEach(row => {
row.classList.remove("batch-row-error");
const msg = row.querySelector(".batch-error-msg");
if (msg) msg.remove();
});
}
function disableAllRowButtons() {
document.querySelectorAll(".grid-row[data-id]").forEach(row => row.classList.add("batch-disabled"));
const toggle = document.querySelector(".actions-dropdown .dropdown-toggle");
if (toggle) { toggle.disabled = true; toggle.style.opacity = "0.5"; toggle.style.pointerEvents = "none"; }
}
function enableAllRowButtons() {
document.querySelectorAll(".grid-row[data-id]").forEach(row => row.classList.remove("batch-disabled"));
const toggle = document.querySelector(".actions-dropdown .dropdown-toggle");
if (toggle) { toggle.disabled = false; toggle.style.opacity = ""; toggle.style.pointerEvents = ""; }
}
// ── Single row export: validate → confirm → send ────────────────────
function startExportConfirmFlow(iddatadb, rowIndex) {
const gridRow = getGridRow(iddatadb);
clearValidationErrors();
if (gridRow) {
setRowExporting(gridRow, true);
const spinner = gridRow.querySelector(".batch-row-spinner");
if (spinner) spinner.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Validating...';
}
validateRows([{ iddatadb: parseInt(iddatadb), index: rowIndex }])
.then(validationData => {
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
if (!validationData.success) {
showExportResult({ success: false, message: validationData.message || "Validation error" });
return;
}
const result = validationData.results[rowIndex];
if (result && !result.valid) {
if (gridRow) showValidationErrors(gridRow, iddatadb, result.errors);
return;
}
showConfirmAndExport(iddatadb, rowIndex);
})
.catch(error => {
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
showExportResult({ success: false, message: "Validation error: " + error.message });
});
}
function showConfirmAndExport(iddatadb, rowIndex) {
const confirmModalElement = document.getElementById("exportConfirmModal");
if (!confirmModalElement) return;
const confirmModal = new bootstrap.Modal(confirmModalElement, { keyboard: false });
document.getElementById("exportIddatadb").textContent = iddatadb;
confirmModal.show();
const confirmBtn = document.getElementById("exportConfirmBtn");
if (!confirmBtn) { confirmModal.hide(); return; }
const confirmHandler = async () => {
pendingConfirmHandler = null;
confirmModal.hide();
const gridRow = getGridRow(iddatadb);
if (gridRow) setRowExporting(gridRow, true);
try {
const data = await sendExport(iddatadb);
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
if (!data.success) showRowError(gridRow, iddatadb, data.message || "Unknown error");
showExportResult(data);
} catch (error) {
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
showRowError(gridRow, iddatadb, error.message);
showExportResult({ success: false, message: error.message });
}
};
if (pendingConfirmHandler) confirmBtn.removeEventListener("click", pendingConfirmHandler);
pendingConfirmHandler = confirmHandler;
confirmBtn.addEventListener("click", confirmHandler, { once: true });
}
// ── Single row click (event delegation) ─────────────────────────────
$(document).on('click', '.export-lims-btn', function (e) {
e.preventDefault();
if (batchRunning) return;
const iddatadb = this.dataset.iddatadb;
const rowIndex = parseInt(this.dataset.row);
const gridRow = getGridRow(iddatadb);
// Check unsaved changes for this row
const dataRow = window.gridData?.[rowIndex];
if (dataRow && dataRow._dirty) {
const unsavedModal = new bootstrap.Modal(document.getElementById("exportUnsavedModal"), { keyboard: false });
unsavedModal.show();
document.getElementById("saveAndExportBtn")?.addEventListener("click", () => {
unsavedModal.hide();
// Save first, then export
const formData = window.buildSavePayload(rowIndex);
fetch('save_edited_row.php', { method: 'POST', body: formData })
.then(r => r.json())
.then(result => {
if (result.success) {
dataRow._dirty = false;
startExportConfirmFlow(iddatadb, rowIndex);
} else {
alert('Save failed: ' + result.message);
}
});
}, { once: true });
return;
}
startExportConfirmFlow(iddatadb, rowIndex);
});
// ── Batch export (Export All) ───────────────────────────────────────
function collectEligibleRows() {
// Read from gridData, not DOM
const eligible = [];
(window.gridData || []).forEach((row, index) => {
if (row.status !== 'l') {
eligible.push({ iddatadb: row.iddatadb, index, row: getGridRow(row.iddatadb) });
}
});
return eligible;
}
function hasUnsavedChanges() {
return (window.gridData || []).some(r => r._dirty);
}
async function validateAndFilter(eligibleRows) {
const rowsToValidate = eligibleRows.map(({ iddatadb, index }) => ({
iddatadb: parseInt(iddatadb),
index,
}));
const validationData = await validateRows(rowsToValidate);
if (!validationData.success) throw new Error(validationData.message || "Validation error");
const validRows = [];
let invalidCount = 0;
for (const item of eligibleRows) {
const result = validationData.results[item.index];
if (result && !result.valid) {
if (item.row) showValidationErrors(item.row, item.iddatadb, result.errors);
invalidCount++;
} else {
validRows.push(item);
}
}
return { validRows, invalidCount };
}
function showValidationSpinner(show) {
const bar = document.getElementById("batchExportBar");
const statusEl = document.getElementById("batchExportStatus");
const cancelBtn = document.getElementById("exportBatchCancelBtn");
if (show) {
if (bar) bar.style.display = "";
if (statusEl) statusEl.textContent = "Validating...";
if (cancelBtn) cancelBtn.style.display = "none";
} else {
if (bar) bar.style.display = "none";
if (cancelBtn) cancelBtn.style.display = "";
}
}
function showBatchConfirm(eligibleRows) {
clearValidationErrors();
showValidationSpinner(true);
validateAndFilter(eligibleRows)
.then(({ validRows, invalidCount }) => {
showValidationSpinner(false);
if (validRows.length === 0) {
document.getElementById("exportResponseMessage").innerHTML =
`No valid rows for export.<br><strong>${invalidCount}</strong> rows with validation errors.`;
document.getElementById("exportResponseModalLabel").textContent = "Validation Failed";
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
return;
}
const confirmModal = new bootstrap.Modal(document.getElementById("exportBatchConfirmModal"), { keyboard: false });
let countText = String(validRows.length);
if (invalidCount > 0) countText += ` (${invalidCount} excluded due to errors)`;
document.getElementById("exportBatchCount").textContent = countText;
confirmModal.show();
const confirmBtn = document.getElementById("exportBatchConfirmBtn");
if (pendingBatchConfirmHandler) confirmBtn.removeEventListener("click", pendingBatchConfirmHandler);
pendingBatchConfirmHandler = () => {
pendingBatchConfirmHandler = null;
confirmModal.hide();
startBatchExport(validRows);
};
confirmBtn.addEventListener("click", pendingBatchConfirmHandler, { once: true });
})
.catch(error => {
showValidationSpinner(false);
document.getElementById("exportResponseMessage").textContent = "Validation error: " + error.message;
document.getElementById("exportResponseModalLabel").textContent = "Validation Error";
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
});
}
$(document).on('click', '.export-all-lims-btn', function (e) {
e.preventDefault();
if (batchRunning) return;
if (hasUnsavedChanges()) {
const unsavedModal = new bootstrap.Modal(document.getElementById("exportBatchUnsavedModal"), { keyboard: false });
unsavedModal.show();
document.getElementById("batchSaveAndExportBtn")?.addEventListener("click", () => {
unsavedModal.hide();
// Trigger save all first — listen for completion
alert("Please Save All first, then Export All.");
}, { once: true });
return;
}
const eligibleRows = collectEligibleRows();
if (eligibleRows.length === 0) {
document.getElementById("exportResponseMessage").textContent = "All rows already exported to LIMS.";
document.getElementById("exportResponseModalLabel").textContent = "Export All";
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
return;
}
showBatchConfirm(eligibleRows);
});
function startBatchExport(eligibleRows) {
batchCancelled = false;
batchRunning = true;
const batchUuid = crypto.randomUUID();
const total = eligibleRows.length;
let processed = 0, succeeded = 0, failed = 0;
disableAllRowButtons();
const bar = document.getElementById("batchExportBar");
const statusEl = document.getElementById("batchExportStatus");
const cancelBtn = document.getElementById("exportBatchCancelBtn");
if (bar) bar.style.display = "";
if (cancelBtn) cancelBtn.disabled = false;
if (statusEl) statusEl.textContent = `Exporting 0 / ${total}...`;
cancelBtn?.addEventListener("click", () => {
batchCancelled = true;
if (statusEl) statusEl.textContent = "Cancelling... (waiting for current row)";
if (cancelBtn) cancelBtn.disabled = true;
}, { once: true });
(async () => {
for (let i = 0; i < eligibleRows.length; i++) {
if (batchCancelled) break;
const { iddatadb, row } = eligibleRows[i];
if (statusEl) statusEl.textContent = `Exporting ${processed + 1} / ${total} (id: ${iddatadb})...`;
const gridRow = row || getGridRow(iddatadb);
if (gridRow) setRowExporting(gridRow, true);
try {
const data = await sendExport(iddatadb, batchUuid);
processed++;
if (data.success) {
succeeded++;
} else {
failed++;
const errIdx = getRowIndexByIddatadb(iddatadb);
if (errIdx >= 0) window.gridData[errIdx]._exportError = data.message || "Unknown error";
if (gridRow) showRowError(gridRow, iddatadb, data.message || "Unknown error");
}
} catch (error) {
processed++;
failed++;
const errIdx = getRowIndexByIddatadb(iddatadb);
if (errIdx >= 0) window.gridData[errIdx]._exportError = error.message;
if (gridRow) showRowError(gridRow, iddatadb, error.message);
}
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
}
batchRunning = false;
enableAllRowButtons();
if (bar) bar.style.display = "none";
// Re-render to reflect status changes, then restore error indicators
const gr = window.gridRenderer;
if (gr) gr.renderVisibleRows();
// Restore error state from gridData._exportError
(window.gridData || []).forEach((row, idx) => {
if (row._exportError) {
const gridRow = getGridRow(row.iddatadb);
if (gridRow) showRowError(gridRow, row.iddatadb, row._exportError);
}
});
const msgEl = document.getElementById("exportResponseMessage");
const labelEl = document.getElementById("exportResponseModalLabel");
if (batchCancelled) {
labelEl.textContent = "Export All — Cancelled";
msgEl.innerHTML = `Exported: <strong>${succeeded}</strong><br>Errors: <strong>${failed}</strong><br>Not processed: <strong>${total - processed}</strong>`;
} else if (failed === 0) {
labelEl.textContent = "Export All — Complete";
msgEl.innerHTML = `All <strong>${succeeded}</strong> rows exported successfully.`;
} else {
labelEl.textContent = "Export All — Completed with errors";
msgEl.innerHTML = `Exported: <strong>${succeeded}</strong><br>Errors: <strong>${failed}</strong>`;
}
const modalEl = document.getElementById("exportResponseModal");
new bootstrap.Modal(modalEl, { keyboard: false }).show();
modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, { once: true });
})();
}
})();

View File

@ -0,0 +1,894 @@
/**
* 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 = '<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>`;
});
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 = `<span class="status-badge status-${esc(st)}">${label}</span>`;
if (row.commessaweb) {
html += `<span class="commessaweb-code" style="display:block; font-size:0.75em; color:#555; margin-top:2px;">${esc(row.commessaweb)}</span>`;
}
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 = `<div style="display:flex; align-items:center; gap:4px; width:100%; height:100%;"><input type="text" class="cell-input manual-input" style="flex:1; min-width:0; height:28px;"><button type="button" class="add-part-btn btn btn-sm btn-primary" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" style="display:inline-flex; align-items:center; justify-content:center; min-width:28px; width:28px; height:28px; padding:0; font-size:12px; flex-shrink:0; text-align:center;"><i class="fas fa-plus" style="margin:0; padding:0;"></i></button></div>`;
break;
case 'awb':
div.innerHTML = `<select class="carrier-select"><option value="tnt-it">TNT Italy</option><option value="dhl">DHL</option><option value="gls">GLS</option><option value="sda">SDA</option><option value="ups">UPS</option></select>` +
`<input type="text" class="cell-input awb-input" placeholder="AWB Number">` +
`<button type="button" class="go-btn" data-row="${rowIndex}"><i class="fas fa-play"></i></button>`;
break;
case 'tracking':
div.className = 'grid-cell tracking-info';
div.dataset.row = rowIndex;
div.innerHTML = '<span class="tracking-result">Shipment Info</span>';
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 = `<a href="imported_trf/${esc(val)}" target="_blank">File</a>`;
} else {
div.innerHTML = `<span>${esc(val)}</span>`;
}
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 `<select class="cell-input dropdown-select ${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}>`;
}
if (col.dataType === 'INT') {
return `<input type="number" class="cell-input ${cls}${reqCls}" value="${v}"${req}>`;
}
if (col.autoValue === 'import_time' || (meta.timeLabels || []).includes(col.label)) {
return `<input type="time" class="cell-input ${cls}${reqCls}" value="${v}"${req}>`;
}
return `<input type="text" class="cell-input ${cls}${reqCls}" value="${v}"${req}>`;
}
function createFixedFieldHTML(col, value, rowIndex) {
if (col.dataType === 'DATE') {
const reqCls = col.isRequired ? ' required-input' : '';
const req = col.isRequired ? ' required' : '';
return `<input type="text" class="cell-input date-picker manual-input${reqCls} fixed-input" data-fixed-key="${col.key}" value="${esc(value)}"${req}>`;
}
// Select — build from cache
const isApiField = !!fixedFieldApiConfig[col.key];
const selectClass = isApiField ? 'api-fixed-select' : '';
let options = '<option value="">Seleziona...</option>';
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 += `<option value="${item.id}"${sel}>${esc(item.text)}</option>`;
});
const reqCls = col.isRequired ? ' required-input' : '';
const req = col.isRequired ? ' required' : '';
return `<select class="cell-input manual-input fixed-input ${selectClass}${reqCls}" data-fixed-key="${col.key}" data-current-value="${esc(value)}"${req}>${options}</select>`;
}
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>`;
});
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 += `<button type="button" class="export-lims-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="${isExported ? 'Already exported' : 'Export to LIMS'}" style="background:${isExported ? '#ccc' : '#eb0b0b'}; color:white; border:none; border-radius:5px; cursor:${isExported ? 'not-allowed' : 'pointer'}; ${isExported ? 'opacity:0.5;' : ''}" ${isExported ? 'disabled' : ''}><i class="fas fa-upload"></i></button>`;
}
html += `<button type="button" class="save-btn action-btn" data-row="${rowIndex}" title="Save" style="background:#28a745; color:white; border:none; border-radius:5px; cursor:pointer;"><i class="fas fa-save"></i></button>`;
html += `<button type="button" class="photos-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="Photos" style="background:#007bff; color:white; border:none; border-radius:5px; cursor:pointer;"><i class="fas fa-camera"></i></button>`;
html += `<button type="button" class="parts-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="Parts" style="background:#ffc107; color:white; border:none; border-radius:5px; cursor:pointer;"><i class="fas fa-puzzle-piece"></i></button>`;
html += `<button type="button" class="delete-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="Delete" style="background:#dc3545; color:white; border:none; border-radius:5px; cursor:pointer;"><i class="fas fa-trash"></i></button>`;
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('<br>');
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, '<br>');
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) + '<div class="resizer"></div>';
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 = `<select class="custom-field dropdown-select client-select searchable-client" data-column="idclient" id="clientSelect"><option value="">Select a client...</option></select>` +
`<button type="button" class="propagate-btn" data-column="idclient" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
} else if (col.type === 'cliente_fornitore_id') {
cell.innerHTML = `<select class="custom-field dropdown-select client-select searchable-client" data-column="cliente_fornitore_id" id="clienteFornitoreSelect"><option value="">Select a supplier...</option></select>` +
`<button type="button" class="propagate-btn" data-column="cliente_fornitore_id" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
} else if (col.type === 'fixed' && col.dataType === 'DATE') {
cell.innerHTML = `<input type="text" class="custom-field date-picker manual-input fixed-top" data-column="fixed_${col.key}" data-fixed-key="${col.key}" value="">` +
`<button type="button" class="propagate-btn" data-column="fixed_${col.key}" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
} else if (col.type === 'fixed') {
const isApiField = !!fixedFieldApiConfig[col.key];
if (isApiField) {
cell.innerHTML = `<select class="custom-field dropdown-select api-fixed-select fixed-top" data-column="fixed_${col.key}" data-fixed-key="${col.key}"><option value="">Seleziona...</option></select>` +
`<button type="button" class="propagate-btn" data-column="fixed_${col.key}" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
} else {
cell.innerHTML = `<input type="number" class="custom-field manual-input fixed-top" data-column="fixed_${col.key}" data-fixed-key="${col.key}" value="">` +
`<button type="button" class="propagate-btn" data-column="fixed_${col.key}" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
}
} else if (col.type === 'detail' || col.type === 'main_field') {
if (col.dataType === 'SceltaMultipla') {
cell.innerHTML = `<select class="custom-field dropdown-select" data-column="col_${colIdx}" data-field-id="${col.fieldId}"><option value="">Seleziona...</option></select>` +
`<button type="button" class="propagate-btn" data-column="col_${colIdx}" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
} else if (col.dataType === 'Data') {
cell.innerHTML = `<input type="text" class="custom-field date-picker" data-column="col_${colIdx}" value="">` +
`<button type="button" class="propagate-btn" data-column="col_${colIdx}" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
} else {
cell.innerHTML = `<input type="text" class="custom-field" data-column="col_${colIdx}" value="">` +
`<button type="button" class="propagate-btn" data-column="col_${colIdx}" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
}
}
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 = '<option value="">Seleziona...</option>';
[...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 = '<option value="">Seleziona...</option>';
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 = '<option value="">Seleziona...</option>';
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(); },
};
})();

View File

@ -340,6 +340,7 @@ function fixedDefaultValue(array $f): string
align-items: center;
padding: 0;
border-bottom: 1px solid #dee2e6;
min-width: fit-content;
}
.grid-row:last-child {
@ -890,9 +891,8 @@ function fixedDefaultValue(array $f): string
<div class="page-wrapper">
<div class="page-content">
<div class="mb-3 text">
<a href="historical_trf.php?id=<?= $template_id ?>&status=i" class="btn btn-warning me-2">Imported (i)</a>
<a href="historical_trf.php?id=<?= $template_id ?>&status=P" class="btn btn-primary me-2">In Progress (P)</a>
<a href="historical_trf.php?id=<?= $template_id ?>&status=l" class="btn btn-success">To LIMS (l)</a>
<a href="imported.php?id=<?= $template_id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success">To LIMS (l)</a>
</div>
<div class="card radius-10">
<div class="card-header">
@ -2312,7 +2312,7 @@ function fixedDefaultValue(array $f): string
} else {
// Select nativa
try {
$(dropdown).select2('destroy');
if ($(dropdown).hasClass('select2-hidden-accessible')) $(dropdown).select2('destroy');
} catch (e) {}
dropdown.innerHTML = '<option value="">Seleziona...</option>';
items.forEach(value => {
@ -2755,7 +2755,8 @@ function fixedDefaultValue(array $f): string
if (results.length > 12) {
// Select2 con ricerca
$select.select2('destroy').empty().select2({
if ($select.hasClass('select2-hidden-accessible')) $select.select2('destroy');
$select.empty().select2({
data: [{
id: '',
text: 'Seleziona...'
@ -2766,9 +2767,7 @@ function fixedDefaultValue(array $f): string
});
} else {
// Select nativa senza Select2
try {
$select.select2('destroy');
} catch (e) {}
if ($select.hasClass('select2-hidden-accessible')) $select.select2('destroy');
$select.empty();
$select.append(new Option('Seleziona...', '', true, false));
results.forEach(r => {

1238
public/userarea/imported.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -23,13 +23,13 @@
</div>
<!-- Seconda riga: +, M, MacroMatrice, Matrice globale, Propaga -->
<div style="display: flex; align-items: center; margin-bottom: 10px;">
<button type="button" class="btn btn-success btn-sm add-row-global" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; margin-right: 5px;"><i class="fas fa-plus fa-xs"></i></button>
<button type="button" class="btn btn-success btn-sm add-row-global" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; margin-right: 5px;"><i class="fas fa-plus"></i></button>
<button type="button" class="btn btn-primary btn-sm add-mix-global" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; margin-right: 10px;">M</button>
<select id="macro-matrice-filter" class="form-control form-control-sm ms-2" style="width: 200px !important; min-width: 200px !important; margin-right: 10px;">
<option value="">Tutte le MacroMatrici</option>
</select>
<select id="global-matrice" class="form-control form-control-sm" style="width: 350px !important; margin-right: 10px;"></select>
<button type="button" class="btn btn-primary btn-sm propagate-all-btn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;"><i class="fas fa-arrow-right fa-xs"></i> Propaga a tutte</button>
<button type="button" class="btn btn-primary btn-sm propagate-all-btn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;"><i class="fas fa-arrow-right"></i> Propaga a tutte</button>
</div>
<table class="table table-striped table-sm" id="partsTable">
<thead>
@ -55,7 +55,7 @@
<td><input type="text" class="form-control form-control-sm part-description" placeholder="Inserisci descrizione"></td>
<td>
<div style="display: flex; align-items: center;">
<button type="button" class="btn btn-primary btn-sm propagate-matrice-btn" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; margin-right: 3px;"><i class="fas fa-arrow-right fa-xs"></i></button>
<button type="button" class="btn btn-primary btn-sm propagate-matrice-btn" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; margin-right: 3px;"><i class="fas fa-arrow-right"></i></button>
<select class="part-matrice form-control form-control-sm" style="width: 150px;"></select>
</div>
</td>
@ -63,9 +63,9 @@
<td>
<button type="button" class="btn btn-light btn-sm note-btn" style="padding: 0.2rem 0.4rem; font-size: 0.9rem;" title="Aggiungi/Modifica nota"><i class="fas fa-sticky-note"></i></button>
<button type="button" class="btn btn-warning btn-sm add-mix-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;">M+</button>
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; display: none;"><i class="fas fa-trash fa-xs"></i></button>
<span class="save-status text-success" style="display: none; margin-left: 5px;"><i class="fas fa-check fa-xs"></i></span>
<span class="save-loading text-warning" style="display: none; margin-left: 5px;"><i class="fas fa-spinner fa-spin fa-xs"></i></span>
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; display: none;"><i class="fas fa-trash"></i></button>
<span class="save-status text-success" style="display: none; margin-left: 5px;"><i class="fas fa-check"></i></span>
<span class="save-loading text-warning" style="display: none; margin-left: 5px;"><i class="fas fa-spinner fa-spin"></i></span>
</td>
</tr>
</tbody>
@ -188,6 +188,14 @@
z-index: 1055 !important
}
/* Fix: app.css sets .btn i { margin-top:-1em; margin-bottom:-1em; font-size:1.3rem } which collapses small icon buttons */
#partsModal .btn i {
margin-top: 0 !important;
margin-bottom: 0 !important;
font-size: 0.8rem !important;
margin-right: 2px !important;
}
/* Tabelle */
#partsTable tr {
display: table-row !important

View File

@ -0,0 +1,208 @@
/**
* modals_gridData.js Photos, Parts, Tested Component handlers for gridData pages
*/
(function () {
'use strict';
// ── Photos — use photos.js loadPopupContent (exported to window) ──
$(document).on('click', '.photos-btn', function () {
const iddatadb = $(this).data('iddatadb') || null;
const idquotations = $(this).data('idquotations') || null;
const modal = document.getElementById('photosModal');
if (!modal) return;
modal.style.display = 'block';
if (typeof window.loadPopupContent === 'function') {
window.loadPopupContent(iddatadb, idquotations);
}
});
// Close photos modal
$(document).on('click', '.close-btn', function () {
const modal = document.getElementById('photosModal');
if (modal) modal.style.display = 'none';
});
// Close on backdrop click
$(document).on('click', '#photosModal', function (e) {
if (e.target === this) {
this.style.display = 'none';
}
});
// ── Parts (matching import_edit2.php behavior) ─────────────────────
$(document).on('click', '.parts-btn', function () {
const iddatadb = $(this).data('iddatadb') || null;
const idquotations = $(this).data('idquotations') || null;
$.ajax({
url: 'modal_partsTable.php',
method: 'GET',
data: { iddatadb: iddatadb },
success: function (response) {
$('#partsModalContainer').html(response);
const modalElement = document.getElementById('partsModal');
if (!modalElement) return;
$("#trfHeader").text(iddatadb || idquotations || '');
$("#partsModal").data("iddatadb", iddatadb).data("idquotations", idquotations);
let modal = bootstrap.Modal.getInstance(modalElement);
if (!modal) modal = new bootstrap.Modal(modalElement, { backdrop: true, keyboard: true, focus: true });
modal.show();
if (typeof window.loadParts === 'function') {
window.loadParts(iddatadb, idquotations);
}
},
error: function (xhr, status, error) {
console.error('Error loading parts:', error);
}
});
});
$(document).on('hidden.bs.modal', '#partsModal', function () {
const modalElement = document.getElementById('partsModal');
if (modalElement) {
const modal = bootstrap.Modal.getInstance(modalElement);
if (modal) modal.dispose();
}
$('#partsModalContainer').empty();
$('.modal-backdrop').remove();
$('body').removeClass('modal-open').css('padding-right', '');
});
// ── Tested Component quick add ───────────────────────────────────────
$(document).on('click', '.add-part-btn', async function () {
const iddatadb = $(this).data('iddatadb') || null;
const rowIndex = parseInt($(this).data('row'));
const row = window.gridData?.[rowIndex];
const id = iddatadb || (row ? row.iddatadb : null);
if (!id) return;
const $cell = $(this).closest('.grid-cell, div');
const $input = $cell.find('input');
const raw = ($input.val() || '').trim();
const parts = raw.split('|').map(s => s.trim()).filter(s => s.length > 0);
const uniqueParts = [...new Set(parts)];
if (!uniqueParts.length) {
alert('Insert a description first.');
$input.focus();
return;
}
try {
for (const p of uniqueParts) {
const formData = new FormData();
formData.append('iddatadb', id);
formData.append('part_description', p);
await fetch('add_part_quick.php', { method: 'POST', body: formData });
}
alert(`Added ${uniqueParts.length} part(s).`);
$input.val('');
} catch (e) {
alert('Error: ' + e.message);
}
});
// ── Delete row ───────────────────────────────────────────────────────
let deleteIddatadb = null;
let deleteRowIndex = null;
$(document).on('click', '.delete-btn', function () {
deleteIddatadb = $(this).data('iddatadb');
deleteRowIndex = parseInt($(this).data('row'));
const modalEl = document.getElementById('deleteConfirmModal');
if (modalEl) {
document.getElementById('deleteIddatadbText').textContent = deleteIddatadb;
new bootstrap.Modal(modalEl).show();
}
});
$(document).on('click', '#deleteConfirmBtn', async function () {
const modalEl = document.getElementById('deleteConfirmModal');
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) modal.hide();
if (!deleteIddatadb) return;
try {
const resp = await fetch('delete_record.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: deleteIddatadb })
});
const result = await resp.json();
if (result.success) {
// Remove from gridData
const idx = window.gridData.findIndex(r => r.iddatadb === deleteIddatadb);
if (idx >= 0) window.gridData.splice(idx, 1);
// Re-render
const gr = window.gridRenderer;
if (gr) gr.renderVisibleRows();
} else {
alert('Error: ' + result.message);
}
} catch (e) {
alert('Error: ' + e.message);
}
deleteIddatadb = null;
deleteRowIndex = null;
});
// ── Add new row ──────────────────────────────────────────────────────
$(document).on('click', '#addRowBtn', async function () {
const btn = this;
btn.disabled = true;
try {
const templateId = window.gridMeta?.templateId;
if (!templateId) { alert('Template ID missing'); return; }
const resp = await fetch('add_record.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ template_id: templateId })
});
const result = await resp.json();
if (result.success && result.iddatadb) {
// Build new row object
const newRow = {
iddatadb: result.iddatadb,
status: 'i',
idclient: window.gridMeta?.defaultIdclient || '',
cliente_fornitore_id: null,
commessaweb: null,
user_name: result.user_name || '',
importreferencecode: result.importreferencecode || '',
filename_import: '',
importdate: new Date().toISOString().slice(0, 19).replace('T', ' '),
fixedFields: {},
details: {},
mainFieldValue: '',
_dirty: true,
};
// Add to beginning of gridData
window.gridData.unshift(newRow);
// Re-render
const gr = window.gridRenderer;
if (gr) gr.renderVisibleRows();
} else {
alert('Error: ' + (result.message || 'Unknown error'));
}
} catch (e) {
alert('Error: ' + e.message);
} finally {
btn.disabled = false;
}
});
})();

View File

@ -941,7 +941,7 @@ $(document).ready(function () {
<td><input type="text" class="form-control form-control-sm part-description" value="${description}" placeholder="Inserisci descrizione"></td>
<td>
<div style="display: flex; align-items: center;">
<button type="button" class="btn btn-primary btn-sm propagate-matrice-btn" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; margin-right: 3px;"><i class="fas fa-arrow-right fa-xs"></i></button>
<button type="button" class="btn btn-primary btn-sm propagate-matrice-btn" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; margin-right: 3px;"><i class="fas fa-arrow-right"></i></button>
<select class="part-matrice form-control form-control-sm" style="width: 150px;"></select>
</div>
</td>
@ -950,9 +950,9 @@ $(document).ready(function () {
<td>
<button type="button" class="btn btn-light btn-sm note-btn" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;" title="Aggiungi/Modifica nota"><i class="fas fa-file-alt"></i></button>
<button type="button" class="btn btn-warning btn-sm add-mix-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;">M+</button>
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; display: none;"><i class="fas fa-trash fa-xs"></i></button>
<span class="save-status text-success" style="display: none; margin-left: 5px;"><i class="fas fa-check fa-xs"></i></span>
<span class="save-loading text-warning" style="display: none; margin-left: 5px;"><i class="fas fa-spinner fa-spin fa-xs"></i></span>
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; display: none;"><i class="fas fa-trash"></i></button>
<span class="save-status text-success" style="display: none; margin-left: 5px;"><i class="fas fa-check"></i></span>
<span class="save-loading text-warning" style="display: none; margin-left: 5px;"><i class="fas fa-spinner fa-spin"></i></span>
</td>
</tr>`;
$("#partsTableBody").append(newRow);
@ -1438,7 +1438,7 @@ $(document).ready(function () {
<td><input type="text" class="form-control form-control-sm part-description" value="${escapedDescription}" placeholder="Inserisci descrizione"></td>
<td>
<div style="display: flex; align-items: center;">
<button type="button" class="btn btn-primary btn-sm propagate-matrice-btn" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; margin-right: 3px;"><i class="fas fa-arrow-right fa-xs"></i></button>
<button type="button" class="btn btn-primary btn-sm propagate-matrice-btn" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; margin-right: 3px;"><i class="fas fa-arrow-right"></i></button>
<select class="part-matrice form-control form-control-sm" style="width: 150px;" ${idquotations && !part.idmatrice ? "disabled" : ""}></select>
</div>
</td>
@ -1447,9 +1447,9 @@ $(document).ready(function () {
<td>
<button type="button" class="btn btn-light btn-sm note-btn ${part.note ? "has-note" : ""}" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;" title="Aggiungi/Modifica nota"><i class="fas fa-file-alt"></i></button>
<button type="button" class="btn btn-warning btn-sm add-mix-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;">M+</button>
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;"><i class="fas fa-trash fa-xs"></i></button>
<span class="save-status text-success" style="display: none; margin-left: 5px;"><i class="fas fa-check fa-xs"></i></span>
<span class="save-loading text-warning" style="display: none; margin-left: 5px;"><i class="fas fa-spinner fa-spin fa-xs"></i></span>
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;"><i class="fas fa-trash"></i></button>
<span class="save-status text-success" style="display: none; margin-left: 5px;"><i class="fas fa-check"></i></span>
<span class="save-loading text-warning" style="display: none; margin-left: 5px;"><i class="fas fa-spinner fa-spin"></i></span>
</td>
</tr>`;
$("#partsTableBody").append(newRow);

View File

@ -1,5 +1,5 @@
document.addEventListener("DOMContentLoaded", function () {
// Funzione per caricare il contenuto del popup
// Funzione per caricare il contenuto del popup (exported for external use)
async function loadPopupContent(iddatadb, idquotations) {
const popupContent = document.getElementById("popupContent");
if (!popupContent) {
@ -1218,4 +1218,7 @@ document.addEventListener("DOMContentLoaded", function () {
closeBtn,
});
}
// Export for external use (gridData pages)
window.loadPopupContent = loadPopupContent;
});

128
public/userarea/saveAll.js Normal file
View File

@ -0,0 +1,128 @@
/**
* saveAll.js Save All functionality using gridData
*/
(function() {
'use strict';
let saveAllRunning = false;
function isBusy() {
return saveAllRunning || window.batchRunning;
}
// ── Save single row ──────────────────────────────────────────────────
$(document).on('click', '.save-btn', async function() {
const btn = this;
const rowIndex = parseInt(btn.dataset.row);
const row = window.gridData?.[rowIndex];
if (!row) return;
const origHtml = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
btn.disabled = true;
try {
const formData = window.buildSavePayload(rowIndex);
const resp = await fetch('save_edited_row.php', { method: 'POST', body: formData });
const result = await resp.json();
if (result.success) {
row._dirty = false;
// Flash success on row without re-rendering (preserves Select2 state)
const gridRow = document.querySelector(`.grid-row[data-id="${row.iddatadb}"]`);
if (gridRow) {
gridRow.classList.remove('row-dirty');
gridRow.querySelectorAll('.grid-cell').forEach(cell => {
cell.classList.remove('cell-changed');
cell.classList.add('flash-success');
});
setTimeout(() => gridRow.querySelectorAll('.flash-success').forEach(c => c.classList.remove('flash-success')), 500);
}
const toastEl = document.getElementById('saveSuccessToast');
if (toastEl) bootstrap.Toast.getOrCreateInstance(toastEl).show();
} else {
alert('Errore: ' + result.message);
}
} catch (e) {
alert('Errore: ' + e.message);
} finally {
btn.innerHTML = origHtml;
btn.disabled = false;
}
});
// ── Save All ─────────────────────────────────────────────────────────
$(document).on('click', '.save-all-btn', function(e) {
e.preventDefault();
if (isBusy()) return;
const modalEl = document.getElementById('saveAllConfirmModal');
if (!modalEl) return;
new bootstrap.Modal(modalEl, { keyboard: false }).show();
});
$(document).on('click', '#saveAllConfirmBtn', async function() {
const confirmModal = bootstrap.Modal.getInstance(document.getElementById('saveAllConfirmModal'));
if (confirmModal) confirmModal.hide();
saveAllRunning = true;
const bar = document.getElementById('batchExportBar');
const statusEl = document.getElementById('batchExportStatus');
const cancelBtn = document.getElementById('exportBatchCancelBtn');
if (bar) bar.style.display = '';
if (cancelBtn) cancelBtn.style.display = 'none';
if (statusEl) statusEl.textContent = 'Saving...';
const data = window.gridData || [];
const dirtyRows = data.map((r, i) => r._dirty ? i : -1).filter(i => i >= 0);
if (dirtyRows.length === 0) {
saveAllRunning = false;
if (bar) bar.style.display = 'none';
const msgEl = document.getElementById('saveAllResultMessage');
if (msgEl) msgEl.textContent = 'No changes to save.';
new bootstrap.Modal(document.getElementById('saveAllResultModal')).show();
return;
}
let success = 0, fail = 0;
for (const idx of dirtyRows) {
if (statusEl) statusEl.textContent = `Saving ${success + fail + 1} / ${dirtyRows.length}...`;
try {
const formData = window.buildSavePayload(idx);
const resp = await fetch('save_edited_row.php', { method: 'POST', body: formData });
const result = await resp.json();
if (result.success) {
data[idx]._dirty = false;
success++;
} else {
fail++;
}
} catch (e) {
fail++;
}
}
saveAllRunning = false;
if (bar) bar.style.display = 'none';
const gr = window.gridRenderer;
if (gr) gr.renderVisibleRows();
const msg = `Saved: ${success}` + (fail > 0 ? `, Errors: ${fail}` : '');
const msgEl = document.getElementById('saveAllResultMessage');
if (msgEl) msgEl.textContent = msg;
new bootstrap.Modal(document.getElementById('saveAllResultModal')).show();
});
// ── beforeunload ─────────────────────────────────────────────────────
window.addEventListener('beforeunload', function(e) {
if (window.gridData && window.gridData.some(r => r._dirty)) {
e.preventDefault();
e.returnValue = '';
}
});
// Public
window.saveAllRunning = () => saveAllRunning;
})();

1363
public/userarea/tolims.php Normal file

File diff suppressed because it is too large Load Diff