Compare commits
14 Commits
39a821357e
...
b812563023
| Author | SHA1 | Date | |
|---|---|---|---|
| b812563023 | |||
| 53c223ea5f | |||
| 9775a12d4a | |||
| abb4200215 | |||
| 0be7109df4 | |||
| d24836e2b1 | |||
| d983659000 | |||
| 4f0dbc7e91 | |||
| 7463fc6726 | |||
| 0bc2ff7e9d | |||
| aa1c32b7ed | |||
| c573a46318 | |||
| 3e66d67dc5 | |||
| 2a7b1fae17 |
73
public/userarea/add_record.php
Normal file
73
public/userarea/add_record.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?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);
|
||||
$importRefFromClient = trim($input['importreferencecode'] ?? '');
|
||||
|
||||
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;
|
||||
|
||||
// Use provided importreferencecode (from filtered page) or generate new
|
||||
$importReferenceCode = $importRefFromClient !== '' ? $importRefFromClient : 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 import_data_details for all mappings (with auto_value support)
|
||||
$mappingStmt = $pdo->prepare("SELECT id, auto_value, manual_default, data_type FROM template_mapping WHERE template_id = ?");
|
||||
$mappingStmt->execute([$templateId]);
|
||||
$mappings = $mappingStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($mappings)) {
|
||||
$insertStmt = $pdo->prepare("INSERT INTO import_data_details (id, mapping_id, field_value) VALUES (?, ?, ?)");
|
||||
foreach ($mappings as $m) {
|
||||
$val = '';
|
||||
$auto = $m['auto_value'] ?? 'none';
|
||||
if ($auto === 'import_date') {
|
||||
$val = date('Y-m-d');
|
||||
} elseif ($auto === 'import_time') {
|
||||
$val = date('H:i');
|
||||
} elseif ($m['data_type'] === 'DATE' && ($m['manual_default'] ?? '') === 'today') {
|
||||
$val = date('Y-m-d');
|
||||
} elseif (!empty($m['manual_default'])) {
|
||||
$val = $m['manual_default'];
|
||||
}
|
||||
$insertStmt->execute([$iddatadb, $m['id'], $val]);
|
||||
}
|
||||
}
|
||||
|
||||
// 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()]);
|
||||
}
|
||||
555
public/userarea/exportLims_gridData.js
Normal file
555
public/userarea/exportLims_gridData.js
Normal 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 });
|
||||
})();
|
||||
}
|
||||
})();
|
||||
@ -74,10 +74,21 @@ try {
|
||||
throw new Exception("Massimo numero di tentativi raggiunto per $endpoint");
|
||||
}
|
||||
|
||||
// Cache file (1 hour TTL)
|
||||
$cacheFile = __DIR__ . '/cache/clienti.json';
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
|
||||
readfile($cacheFile);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Esegui la chiamata con retry
|
||||
$data = makeApiRequest($api, $endpoint);
|
||||
|
||||
echo json_encode($data);
|
||||
$json = json_encode($data);
|
||||
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
|
||||
file_put_contents($cacheFile, $json);
|
||||
|
||||
echo $json;
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
$errorResponse = [
|
||||
|
||||
@ -21,17 +21,26 @@ try {
|
||||
}
|
||||
|
||||
$results = [];
|
||||
$cacheDir = __DIR__ . '/cache';
|
||||
if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true);
|
||||
|
||||
foreach ($fieldIds as $customFieldId) {
|
||||
$cacheFile = $cacheDir . '/customfield_' . $customFieldId . '.json';
|
||||
|
||||
// Use cache if fresh (1 hour)
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
|
||||
$results[$customFieldId] = json_decode(file_get_contents($cacheFile), true);
|
||||
continue;
|
||||
}
|
||||
|
||||
$endpoint = "CustomField($customFieldId)?\$expand=CustomFieldsValues";
|
||||
$data = $api->get($endpoint);
|
||||
$values = $data['CustomFieldsValues'] ?? [];
|
||||
$results[$customFieldId] = $values;
|
||||
|
||||
$results[$customFieldId] = $data['CustomFieldsValues'] ?? [];
|
||||
file_put_contents($cacheFile, json_encode($values));
|
||||
}
|
||||
|
||||
// Debug ფაილი
|
||||
file_put_contents(__DIR__ . '/customfield_values_response.json', json_encode($results));
|
||||
|
||||
echo json_encode($results);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
|
||||
@ -1,59 +1,46 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php'; // Risale a root/vendor/
|
||||
use Dotenv\Dotenv;
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
include dirname(__DIR__) . '/../extra/auth.php';
|
||||
if (!Auth::check()) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; }
|
||||
|
||||
// Set JSON header
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Debug: Log the path where we expect the .env file
|
||||
$envPath = dirname(__DIR__, 2);
|
||||
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . ' - Expected .env path: ' . $envPath . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Carica il file .env dalla root del progetto
|
||||
try {
|
||||
$dotenv = Dotenv::createImmutable($envPath);
|
||||
$dotenv->load();
|
||||
} catch (Exception $e) {
|
||||
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - Errore caricamento .env: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
|
||||
echo json_encode(['success' => false, 'message' => 'Errore caricamento configurazione: ' . $e->getMessage()]);
|
||||
exit(1);
|
||||
}
|
||||
// Read from matrici cache (populated by get_matrici.php / warm_cache.php)
|
||||
$cacheFile = __DIR__ . '/cache/matrici.json';
|
||||
if (file_exists($cacheFile)) {
|
||||
$data = json_decode(file_get_contents($cacheFile), true);
|
||||
$matrici = $data['value'] ?? [];
|
||||
} else {
|
||||
// Fallback: trigger cache creation
|
||||
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
$apiData = $api->get('Matrice');
|
||||
$matrici = [];
|
||||
foreach (($apiData['value'] ?? []) as $item) {
|
||||
$nome = $item['NomeMatrice'] ?? '';
|
||||
if ($nome !== '' && substr($nome, 0, 1) !== '*') {
|
||||
$matrici[] = ['IdMatrice' => $item['IdMatrice'], 'NomeMatrice' => $nome, 'MacroMatrice' => $item['MacroMatrice'] ?? null];
|
||||
}
|
||||
}
|
||||
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
|
||||
file_put_contents($cacheFile, json_encode(['success' => true, 'value' => $matrici]));
|
||||
}
|
||||
|
||||
// Recupera le variabili d'ambiente
|
||||
$dbHost = $_ENV['DB_HOST'];
|
||||
$dbName = $_ENV['DB_DATABASE'];
|
||||
$dbUser = $_ENV['DB_USERNAME'];
|
||||
$dbPass = $_ENV['DB_PASSWORD'];
|
||||
$dbPrefix = $_ENV['DB_PREFIX'];
|
||||
// Extract unique MacroMatrice values
|
||||
$macroValues = [];
|
||||
foreach ($matrici as $m) {
|
||||
$macro = $m['MacroMatrice'] ?? null;
|
||||
if ($macro !== null && $macro !== '' && substr($macro, 0, 1) !== '*') {
|
||||
$macroValues[$macro] = true;
|
||||
}
|
||||
}
|
||||
$macroMatrici = array_keys($macroValues);
|
||||
sort($macroMatrici);
|
||||
|
||||
// Debug: Log database connection details (excluding password)
|
||||
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . " - DB Connection: host=$dbHost, dbname=$dbName, user=$dbUser, prefix=$dbPrefix" . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Connessione al database MySQL
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8mb4", $dbUser, $dbPass);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
} catch (PDOException $e) {
|
||||
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - Errore connessione DB: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
|
||||
echo json_encode(['success' => false, 'message' => 'Errore connessione al database: ' . $e->getMessage()]);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
// Query per recuperare i valori distinti di MacroMatrice, escludendo quelli che iniziano con '*' e ordinandoli
|
||||
$query = "SELECT DISTINCT MacroMatrice FROM {$dbPrefix}matrici WHERE MacroMatrice IS NOT NULL AND MacroMatrice NOT LIKE '*%' ORDER BY MacroMatrice ASC";
|
||||
$stmt = $pdo->prepare($query);
|
||||
$stmt->execute();
|
||||
$macroMatrici = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
// Debug: Log del numero di MacroMatrice recuperate
|
||||
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . ' - Retrieved ' . count($macroMatrici) . ' MacroMatrice from database' . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Restituisci risposta JSON
|
||||
echo json_encode(['success' => true, 'value' => $macroMatrici]);
|
||||
} catch (PDOException $e) {
|
||||
// Log errore e restituisci risposta di errore
|
||||
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - Errore nel recupero delle MacroMatrice: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
|
||||
echo json_encode(['success' => false, 'message' => 'Errore nel recupero delle MacroMatrice: ' . $e->getMessage()]);
|
||||
exit(1);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
47
public/userarea/get_matrici.php
Normal file
47
public/userarea/get_matrici.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
include dirname(__DIR__) . '/../extra/auth.php';
|
||||
if (!Auth::check()) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; }
|
||||
|
||||
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
// Cache file (1 hour TTL)
|
||||
$cacheFile = __DIR__ . '/cache/matrici.json';
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
|
||||
readfile($cacheFile);
|
||||
exit;
|
||||
}
|
||||
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
$data = $api->get('Matrice');
|
||||
|
||||
$matrici = [];
|
||||
if (isset($data['value']) && is_array($data['value'])) {
|
||||
foreach ($data['value'] as $item) {
|
||||
$nome = $item['NomeMatrice'] ?? '';
|
||||
if ($nome !== '' && substr($nome, 0, 1) !== '*') {
|
||||
$matrici[] = [
|
||||
'IdMatrice' => $item['IdMatrice'],
|
||||
'NomeMatrice' => $nome,
|
||||
'MacroMatrice' => $item['MacroMatrice'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
usort($matrici, fn($a, $b) => strcasecmp($a['NomeMatrice'], $b['NomeMatrice']));
|
||||
}
|
||||
|
||||
$json = json_encode(['success' => true, 'value' => $matrici]);
|
||||
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
|
||||
file_put_contents($cacheFile, $json);
|
||||
|
||||
echo $json;
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
979
public/userarea/gridRenderer.js
Normal file
979
public/userarea/gridRenderer.js
Normal file
@ -0,0 +1,979 @@
|
||||
/**
|
||||
* 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}] — used only for small lists
|
||||
const dropdownNameCache = {}; // "fieldId_valueId" -> label
|
||||
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 || [];
|
||||
let 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 (AJAX Select2, no bulk loading) ──────────────────────
|
||||
|
||||
function formatClientLabel(client) {
|
||||
return (client.Nominativo || '').trim();
|
||||
}
|
||||
|
||||
// Cache of resolved client names: id → name
|
||||
const clientNameCache = {};
|
||||
|
||||
// Build select with only the selected option
|
||||
function buildClientOptionsHTML(selectedId) {
|
||||
let html = '<option value="">Select a client...</option>';
|
||||
if (selectedId) {
|
||||
const label = clientNameCache[selectedId] || selectedId;
|
||||
html += `<option value="${esc(String(selectedId))}" selected>${esc(String(label))}</option>`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
// Pre-resolve all unique client IDs used in data rows, then re-render
|
||||
async function resolveClientNames() {
|
||||
const ids = new Set();
|
||||
data.forEach(row => {
|
||||
if (row.idclient) ids.add(String(row.idclient));
|
||||
if (row.cliente_fornitore_id) ids.add(String(row.cliente_fornitore_id));
|
||||
// Fixed fields that are client-sourced
|
||||
if (row.fixedFields) {
|
||||
for (const [key, val] of Object.entries(row.fixedFields)) {
|
||||
const cfg = fixedFieldApiConfig[key];
|
||||
if (cfg && cfg.source === 'clients' && val) ids.add(String(val));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Batch resolve via single request per ID
|
||||
const unresolvedIds = [...ids].filter(id => !clientNameCache[id]);
|
||||
if (unresolvedIds.length === 0) return;
|
||||
|
||||
await Promise.all(unresolvedIds.map(async (id) => {
|
||||
try {
|
||||
const resp = await fetch('search_clienti.php?id=' + encodeURIComponent(id));
|
||||
const json = await resp.json();
|
||||
const item = (json.results || [])[0];
|
||||
if (item) clientNameCache[id] = item.text;
|
||||
} catch (e) { /* ignore */ }
|
||||
}));
|
||||
}
|
||||
|
||||
// Select2 AJAX config for client selects
|
||||
const clientSelect2Config = {
|
||||
placeholder: 'Search client...',
|
||||
allowClear: true,
|
||||
width: '100%',
|
||||
minimumInputLength: 0,
|
||||
dropdownCssClass: 'select2-dropdown-smaller',
|
||||
ajax: {
|
||||
url: 'search_clienti.php',
|
||||
dataType: 'json',
|
||||
delay: 150,
|
||||
data: function(params) { return { q: params.term || '', limit: 20 }; },
|
||||
processResults: function(data) { return { results: data.results || [] }; },
|
||||
cache: true
|
||||
}
|
||||
};
|
||||
|
||||
async function loadClientData() {
|
||||
// No longer loads all clients — AJAX Select2 handles search
|
||||
// Just resolve names for pre-selected values
|
||||
await resolveClientNames();
|
||||
}
|
||||
|
||||
// ── Fixed field data loading ───────────────────────────────────────────
|
||||
|
||||
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 — handled by AJAX Select2, skip preloading
|
||||
if (config.source === 'clients') {
|
||||
fixedFieldCache[fieldKey] = [];
|
||||
return [];
|
||||
}
|
||||
|
||||
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 ─────────────────────────────────
|
||||
|
||||
|
||||
// Select2 AJAX config factory for SceltaMultipla
|
||||
function sceltaSelect2Config(fieldId) {
|
||||
return {
|
||||
placeholder: 'Search...',
|
||||
allowClear: true,
|
||||
width: '100%',
|
||||
minimumInputLength: 0,
|
||||
ajax: {
|
||||
url: 'search_customfield_values.php',
|
||||
dataType: 'json',
|
||||
delay: 150,
|
||||
data: function(params) { return { field_id: fieldId, q: params.term || '', limit: 10 }; },
|
||||
processResults: function(data) { return { results: data.results || [] }; },
|
||||
cache: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ── Preload all data ───────────────────────────────────────────────────
|
||||
|
||||
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. Warm server cache + resolve SceltaMultipla names in one request
|
||||
const allFieldIds = [...new Set(
|
||||
columns.filter(c => (c.type === 'detail' || c.type === 'main_field') && c.dataType === 'SceltaMultipla' && c.fieldId)
|
||||
.map(c => String(c.fieldId))
|
||||
)];
|
||||
if (allFieldIds.length > 0) {
|
||||
try {
|
||||
const resp = await fetch('get_customfield_values.php?field_ids=' + allFieldIds.join(','));
|
||||
const json = await resp.json();
|
||||
const entries = json.data ? json.data : json;
|
||||
for (const [fid, values] of Object.entries(entries)) {
|
||||
if (Array.isArray(values)) {
|
||||
values.forEach(v => {
|
||||
dropdownNameCache[fid + '_' + v.IdCustomFieldsValue] = v.Valore || '';
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to preload dropdown data:', e);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[gridRenderer] preload done:', {
|
||||
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 searchable-dropdown ${cls}${reqCls}" data-field-id="${col.fieldId}"${req}>${options}</select>`;
|
||||
}
|
||||
if (col.dataType === 'Data') {
|
||||
return `<input type="text" class="cell-input date-picker ${cls}${reqCls}" value="${v}"${req}>`;
|
||||
}
|
||||
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}>`;
|
||||
}
|
||||
|
||||
// Client-sourced fields → AJAX Select2 (like idclient)
|
||||
const config = fixedFieldApiConfig[col.key];
|
||||
if (config && config.source === 'clients') {
|
||||
const reqCls = col.isRequired ? ' required-input' : '';
|
||||
const req = col.isRequired ? ' required' : '';
|
||||
let opts = '<option value="">Select...</option>';
|
||||
if (value) {
|
||||
const label = clientNameCache[value] || value;
|
||||
opts += `<option value="${esc(String(value))}" selected>${esc(String(label))}</option>`;
|
||||
}
|
||||
return `<select class="cell-input manual-input fixed-input searchable-client api-fixed-select${reqCls}" data-fixed-key="${col.key}" data-current-value="${esc(value)}"${req}>${opts}</select>`;
|
||||
}
|
||||
|
||||
// Select — build from cache
|
||||
const isApiField = !!config;
|
||||
const selectClass = isApiField ? 'api-fixed-select' : '';
|
||||
let options = '<option value="">Seleziona...</option>';
|
||||
const cacheKey = config?.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>';
|
||||
if (selectedValue) {
|
||||
const label = dropdownNameCache[fieldId + '_' + selectedValue] || selectedValue;
|
||||
html += `<option value="${esc(String(selectedValue))}" selected>${esc(String(label))}</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;
|
||||
|
||||
// Refresh totalRows in case data was modified externally
|
||||
totalRows = data.length;
|
||||
if (revealedCount < totalRows) revealedCount = totalRows;
|
||||
|
||||
// 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 — AJAX mode
|
||||
const clientSel = document.getElementById('clientSelect');
|
||||
if (clientSel) {
|
||||
clientSel.innerHTML = buildClientOptionsHTML(meta.defaultIdclient);
|
||||
$(clientSel).select2(clientSelect2Config);
|
||||
}
|
||||
const fornitSel = document.getElementById('clienteFornitoreSelect');
|
||||
if (fornitSel) {
|
||||
fornitSel.innerHTML = buildClientOptionsHTML('');
|
||||
$(fornitSel).select2(clientSelect2Config);
|
||||
}
|
||||
|
||||
// Fixed field selects in top row
|
||||
topContainer.querySelectorAll('.api-fixed-select').forEach(sel => {
|
||||
const fieldKey = sel.dataset.fixedKey;
|
||||
const config = fixedFieldApiConfig[fieldKey];
|
||||
|
||||
// Client-sourced → init as AJAX Select2
|
||||
if (config && config.source === 'clients') {
|
||||
$(sel).select2(clientSelect2Config);
|
||||
return;
|
||||
}
|
||||
|
||||
if (config && config.dependsOn) {
|
||||
// For dependent fields: merge all cached values across all clientIds
|
||||
const allItems = new Map();
|
||||
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 — AJAX Select2
|
||||
topContainer.querySelectorAll('.dropdown-select[data-field-id]').forEach(sel => {
|
||||
const fieldId = sel.dataset.fieldId;
|
||||
if (fieldId) $(sel).select2(sceltaSelect2Config(fieldId));
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
// Cache Select2 label so re-render shows name not ID
|
||||
if (value && $(input).hasClass('select2-hidden-accessible')) {
|
||||
const label = $(input).find('option:selected').text();
|
||||
if (label && label !== value) {
|
||||
clientNameCache[value] = label;
|
||||
// Also cache for SceltaMultipla
|
||||
const fieldId = input.dataset?.fieldId;
|
||||
if (fieldId) dropdownNameCache[fieldId + '_' + value] = label;
|
||||
}
|
||||
}
|
||||
|
||||
const col = columns[colIndex] || null;
|
||||
|
||||
if (column === 'idclient') {
|
||||
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() || '';
|
||||
|
||||
// Cache selected label
|
||||
if (value) {
|
||||
const label = $(this).find('option:selected').text();
|
||||
if (label && label !== value) clientNameCache[value] = label;
|
||||
}
|
||||
|
||||
if (colType === 'idclient') {
|
||||
data[rowIndex].idclient = value;
|
||||
data[rowIndex]._dirty = true;
|
||||
} else if (colType === 'cliente_fornitore_id') {
|
||||
data[rowIndex].cliente_fornitore_id = value;
|
||||
data[rowIndex]._dirty = true;
|
||||
}
|
||||
cell.classList.add('cell-changed');
|
||||
updateDirtyIndicator();
|
||||
});
|
||||
|
||||
// Cache labels on SceltaMultipla change
|
||||
$(rowContainer).on('change', '.searchable-dropdown', function () {
|
||||
const val = $(this).val();
|
||||
const fieldId = this.dataset.fieldId;
|
||||
if (val && fieldId) {
|
||||
const label = $(this).find('option:selected').text();
|
||||
if (label && label !== val) dropdownNameCache[fieldId + '_' + val] = label;
|
||||
}
|
||||
});
|
||||
|
||||
// Select2 change on fixed field selects
|
||||
$(rowContainer).on('change', '.api-fixed-select', function () {
|
||||
const cell = this.closest('.grid-cell');
|
||||
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(clientSelect2Config);
|
||||
});
|
||||
$(this).find('.searchable-dropdown:not(.select2-hidden-accessible)').each(function () {
|
||||
const fieldId = this.dataset.fieldId;
|
||||
if (fieldId) $(this).select2(sceltaSelect2Config(fieldId));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── 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(); },
|
||||
};
|
||||
|
||||
})();
|
||||
@ -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">
|
||||
@ -2324,7 +2324,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 => {
|
||||
@ -2767,7 +2767,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...'
|
||||
@ -2778,9 +2779,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 => {
|
||||
|
||||
@ -24,7 +24,7 @@ $columns = json_decode(urldecode($_POST['columns'] ?? '[]'), true);
|
||||
$rows = json_decode(urldecode($_POST['rows'] ?? '[]'), true);
|
||||
$excelrows = json_decode(urldecode($_POST['excelrows'] ?? '[]'), true);
|
||||
|
||||
$newFilename = htmlspecialchars($_POST['filename']);
|
||||
$newFilename = $_POST['filename'];
|
||||
|
||||
$_SESSION['template_id'] = $template_id;
|
||||
$_SESSION['selected_rows'] = $selected_rows;
|
||||
@ -47,7 +47,7 @@ $pdo = $db->getConnection();
|
||||
$importReferenceCode = date('YmdHis') . '-' . uniqid();
|
||||
|
||||
// Recupera tutti i mapping dal template
|
||||
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field FROM template_mapping WHERE template_id = ?");
|
||||
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
|
||||
$stmt->execute([$template_id]);
|
||||
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
@ -135,6 +135,15 @@ foreach ($selected_rows as $rowIndex) {
|
||||
$fieldValue = date('Y-m-d');
|
||||
}
|
||||
}
|
||||
// Apply auto_value if field is still empty
|
||||
if (($fieldValue === null || $fieldValue === '') && !empty($mapping['auto_value']) && $mapping['auto_value'] !== 'none') {
|
||||
if ($mapping['auto_value'] === 'import_date') {
|
||||
$fieldValue = date('Y-m-d');
|
||||
} elseif ($mapping['auto_value'] === 'import_time') {
|
||||
$fieldValue = date('H:i');
|
||||
}
|
||||
}
|
||||
|
||||
if ($mapping['is_required'] && (is_null($fieldValue) || $fieldValue === '')) {
|
||||
error_log("Required field missing for mapping ID: " . $mapping['id'] . ", field: " . $mapping['field_label']);
|
||||
}
|
||||
@ -147,28 +156,6 @@ foreach ($selected_rows as $rowIndex) {
|
||||
|
||||
$_SESSION['inserted_ids'] = $insertedIds;
|
||||
|
||||
$params = [
|
||||
'template_id' => $template_id,
|
||||
'filename' => $newFilename,
|
||||
];
|
||||
|
||||
?>
|
||||
<form id="redirectForm" action="import_edit2.php" method="post" style="display: none;">
|
||||
<input type="hidden" name="template_id" value="<?= htmlspecialchars($template_id) ?>">
|
||||
<input type="hidden" name="filename" value="<?= htmlspecialchars($newFilename) ?>">
|
||||
<?php foreach ($selected_rows as $row): ?>
|
||||
<input type="hidden" name="selected_rows[]" value="<?= htmlspecialchars($row) ?>">
|
||||
<?php endforeach; ?>
|
||||
<?php foreach ($insertedIds as $id): ?>
|
||||
<input type="hidden" name="inserted_ids[]" value="<?= htmlspecialchars($id) ?>">
|
||||
<?php endforeach; ?>
|
||||
<input type="hidden" name="columns" value='<?= json_encode($columns) ?>'>
|
||||
<input type="hidden" name="rows" value='<?= json_encode($rows) ?>'>
|
||||
<input type="hidden" name="excelrows" value='<?= json_encode($excelrows) ?>'>
|
||||
</form>
|
||||
<script>
|
||||
document.getElementById('redirectForm').submit();
|
||||
</script>
|
||||
<?php
|
||||
header("Location: imported.php?id=" . urlencode($template_id) . "&importref=" . urlencode($importReferenceCode));
|
||||
exit;
|
||||
?>
|
||||
|
||||
@ -143,9 +143,8 @@ error_log("Loaded template: " . print_r($template, true));
|
||||
<div class="page-content">
|
||||
<?php include('top_stat_widget.php'); ?>
|
||||
<div class="mb-3 text">
|
||||
<a href="historical_trf.php?id=<?= $id ?>&status=i" class="btn btn-warning me-2">Imported (i)</a>
|
||||
<a href="historical_trf.php?id=<?= $id ?>&status=P" class="btn btn-primary me-2">In Progress (P)</a>
|
||||
<a href="historical_trf.php?id=<?= $id ?>&status=l" class="btn btn-success">To LIMS (l)</a>
|
||||
<a href="imported.php?id=<?= $id ?>" class="btn btn-warning me-2">Imported (i)</a>
|
||||
<a href="tolims.php?id=<?= $id ?>" class="btn btn-success">To LIMS (l)</a>
|
||||
</div>
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
@ -330,7 +329,10 @@ error_log("Loaded template: " . print_r($template, true));
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="selectAll"> Seleziona</th>
|
||||
${data.columns.map(col => `<th>${col || 'Colonna senza nome'}<div class="resize-handle"></div></th>`).join('')}
|
||||
${data.columns.map(col => {
|
||||
const label = !col ? 'Colonna senza nome' : (col.match(/^__empty_\d+__$/) ? 'Colonna senza nome' : col);
|
||||
return `<th>${label}<div class="resize-handle"></div></th>`;
|
||||
}).join('')}
|
||||
</tr>
|
||||
<tr class="column-filters">
|
||||
<th></th>
|
||||
@ -491,4 +493,4 @@ error_log("Loaded template: " . print_r($template, true));
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
1397
public/userarea/imported.php
Normal file
1397
public/userarea/imported.php
Normal file
File diff suppressed because it is too large
Load Diff
@ -204,7 +204,13 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
|
||||
<div class="card-body">
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Upload XLS Example:</label>
|
||||
<input type="file" id="xlsUpload" class="form-control">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<input type="file" id="xlsUpload" class="form-control" style="flex:1;">
|
||||
<label class="form-check-label" style="white-space:nowrap; cursor:pointer;">
|
||||
<input type="checkbox" class="form-check-input" id="autoDetectHeader" checked>
|
||||
Auto-detect header row
|
||||
</label>
|
||||
</div>
|
||||
<small id="uploadStatus">
|
||||
<?php if (!empty($template['sample_xlsx'])): ?>
|
||||
✅ Current file: <a href="xlstemplates/<?php echo htmlspecialchars($template['sample_xlsx']); ?>" target="_blank"><?php echo htmlspecialchars($template['sample_xlsx']); ?></a>
|
||||
@ -514,8 +520,10 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
|
||||
type: 'array'
|
||||
});
|
||||
let sheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||
let rowIndex = parseInt(document.getElementById('headerRow').textContent) || 1;
|
||||
let startColumn = parseInt(document.getElementById('startColumn').textContent) || 1;
|
||||
|
||||
// Read sheet range to determine column offset
|
||||
const sheetRange = XLSX.utils.decode_range(sheet['!ref'] || 'A1');
|
||||
const colOffset = sheetRange.s.c; // first column index in sheet (0-based)
|
||||
|
||||
let sheetData = XLSX.utils.sheet_to_json(sheet, {
|
||||
header: 1,
|
||||
@ -523,37 +531,224 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
|
||||
raw: false,
|
||||
range: 0
|
||||
});
|
||||
console.log("Dati della riga " + rowIndex + ":", sheetData[rowIndex - 1]);
|
||||
if (!sheetData[rowIndex - 1]) {
|
||||
|
||||
// Track merged cell ranges — adjust for column offset
|
||||
const merges = (sheet['!merges'] || []).map(m => ({
|
||||
s: { r: m.s.r, c: m.s.c - colOffset },
|
||||
e: { r: m.e.r, c: m.e.c - colOffset }
|
||||
}));
|
||||
console.log('Sheet column offset:', colOffset, '(first col:', String.fromCharCode(65 + colOffset) + ')');
|
||||
|
||||
const useAutoDetect = document.getElementById('autoDetectHeader').checked;
|
||||
|
||||
if (!useAutoDetect) {
|
||||
// Use values from template settings
|
||||
let rowIndex = parseInt(document.getElementById('headerRow').textContent) || 1;
|
||||
let startColumn = parseInt(document.getElementById('startColumn').textContent) || 1;
|
||||
console.log(`Manual mode: row ${rowIndex}, startCol ${startColumn}`);
|
||||
|
||||
if (!sheetData[rowIndex - 1]) {
|
||||
document.getElementById('schemaFieldsBody').querySelectorAll('select.xls-columns').forEach(select => {
|
||||
select.innerHTML = '<option value="">Nessuna intestazione trovata</option>';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Build logical headers, collapsing merged cells
|
||||
const mergeStartMapManual = {};
|
||||
merges.forEach(m => {
|
||||
if ((rowIndex - 1) >= m.s.r && (rowIndex - 1) <= m.e.r) {
|
||||
for (let c = m.s.c; c <= m.e.c; c++) {
|
||||
mergeStartMapManual[c] = m.s.c;
|
||||
}
|
||||
}
|
||||
});
|
||||
let headers = [];
|
||||
const rawRowManual = sheetData[rowIndex - 1] || [];
|
||||
const seenManual = new Set();
|
||||
for (let c = startColumn - 1; c < rawRowManual.length; c++) {
|
||||
const ms = mergeStartMapManual[c];
|
||||
if (ms !== undefined) {
|
||||
if (seenManual.has(ms)) continue;
|
||||
seenManual.add(ms);
|
||||
const v = rawRowManual[ms];
|
||||
headers.push(v === undefined ? "" : String(v).replace(/[\r\n\t]+/g, ' ').trim());
|
||||
} else {
|
||||
const v = rawRowManual[c];
|
||||
headers.push(v === undefined ? "" : String(v).replace(/[\r\n\t]+/g, ' ').trim());
|
||||
}
|
||||
}
|
||||
while (headers.length > 0 && headers[headers.length - 1] === '') {
|
||||
headers.pop();
|
||||
}
|
||||
headers = headers.map((h, i) => {
|
||||
h = h.replace(/[\r\n\t]+/g, ' ').trim();
|
||||
return h === '' ? `__empty_${i + 1}__` : h;
|
||||
});
|
||||
console.log("Intestazioni estratte (manual):", headers);
|
||||
availableXlsColumns = [...headers];
|
||||
usedColumnsFromDB = [];
|
||||
saveXlsHeaders(headers, rowIndex, startColumn);
|
||||
updateXlsDropdowns();
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect field_label titles from the schema table
|
||||
const knownLabels = [];
|
||||
document.querySelectorAll('.title-col').forEach(td => {
|
||||
const v = (td.textContent || '').trim().toLowerCase().replace(/\s*required\s*$/i, '').trim();
|
||||
if (v && v !== 'n/a') knownLabels.push(v);
|
||||
});
|
||||
const uniqueLabels = [...new Set(knownLabels)];
|
||||
|
||||
console.group('🔍 Auto-detect header row');
|
||||
console.log('Sheet name:', workbook.SheetNames[0]);
|
||||
console.log('Total rows in sheet:', sheetData.length);
|
||||
console.log('Labels from schema field titles:', knownLabels);
|
||||
console.log('Unique labels to match against:', uniqueLabels);
|
||||
|
||||
// Auto-detect: find the row with most matches, fallback to most non-empty unique text cells
|
||||
let bestRow = 0;
|
||||
let bestScore = 0;
|
||||
let bestStartCol = 0;
|
||||
let fallbackRow = 0;
|
||||
let fallbackScore = 0;
|
||||
let fallbackStartCol = 0;
|
||||
const scanLimit = Math.min(sheetData.length, 50);
|
||||
const rowScores = [];
|
||||
|
||||
for (let r = 0; r < scanLimit; r++) {
|
||||
const row = sheetData[r] || [];
|
||||
let score = 0;
|
||||
let firstNonEmpty = -1;
|
||||
let nonEmptyCount = 0;
|
||||
const matched = [];
|
||||
const cellValues = [];
|
||||
const seen = new Set();
|
||||
for (let c = 0; c < row.length; c++) {
|
||||
const cellVal = String(row[c] || '').trim();
|
||||
const cellLower = cellVal.toLowerCase();
|
||||
if (cellVal) {
|
||||
cellValues.push(`[${c}]="${cellVal}"`);
|
||||
if (firstNonEmpty < 0) firstNonEmpty = c;
|
||||
// Count unique short text cells (likely headers, not data/descriptions)
|
||||
if (cellVal.length < 80 && !seen.has(cellLower)) {
|
||||
nonEmptyCount++;
|
||||
seen.add(cellLower);
|
||||
}
|
||||
}
|
||||
if (cellLower && uniqueLabels.includes(cellLower)) {
|
||||
score++;
|
||||
matched.push(cellVal);
|
||||
}
|
||||
}
|
||||
rowScores.push({ row: r + 1, score, nonEmpty: nonEmptyCount, matched, firstNonEmpty: firstNonEmpty + 1, cells: cellValues });
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestRow = r;
|
||||
bestStartCol = firstNonEmpty >= 0 ? firstNonEmpty : 0;
|
||||
}
|
||||
// Fallback: row with most unique short text cells (likely header row)
|
||||
if (nonEmptyCount > fallbackScore) {
|
||||
fallbackScore = nonEmptyCount;
|
||||
fallbackRow = r;
|
||||
fallbackStartCol = firstNonEmpty >= 0 ? firstNonEmpty : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// If no label matches, use fallback (most populated row)
|
||||
if (bestScore === 0 && fallbackScore > 0) {
|
||||
bestRow = fallbackRow;
|
||||
bestStartCol = fallbackStartCol;
|
||||
console.log(`⚠️ No label matches found. Using fallback: row with most headers (${fallbackScore} unique cells)`);
|
||||
}
|
||||
|
||||
console.log('All rows scanned:');
|
||||
console.table(rowScores.filter(r => r.nonEmpty > 0).map(r => ({
|
||||
row: r.row,
|
||||
labelMatches: r.score,
|
||||
uniqueCells: r.nonEmpty,
|
||||
matched: r.matched.join(', '),
|
||||
firstCol: r.firstNonEmpty
|
||||
})));
|
||||
console.log(`✅ Result: row ${bestRow + 1}, startCol ${bestStartCol + 1}, labelScore ${bestScore}/${uniqueLabels.length}, fallbackScore ${fallbackScore}`);
|
||||
console.log('Selected row raw data:', sheetData[bestRow]);
|
||||
console.groupEnd();
|
||||
|
||||
// Update display
|
||||
document.getElementById('headerRow').textContent = bestRow + 1;
|
||||
document.getElementById('startColumn').textContent = bestStartCol + 1;
|
||||
|
||||
if (!sheetData[bestRow]) {
|
||||
document.getElementById('schemaFieldsBody').querySelectorAll('select.xls-columns').forEach(select => {
|
||||
select.innerHTML = '<option value="">Nessuna intestazione trovata</option>';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let headers = sheetData[rowIndex - 1].slice(startColumn - 1).map(header => header === undefined ? "" : header);
|
||||
console.log("Intestazioni estratte:", headers);
|
||||
// Build logical columns: each merge = one column, each non-merged cell = one column
|
||||
let headers = [];
|
||||
const rawRow = sheetData[bestRow] || [];
|
||||
|
||||
// Map each physical column to its merge start (or itself if not merged)
|
||||
const mergeStartMap = {}; // physCol -> startCol of its merge
|
||||
merges.forEach(m => {
|
||||
if (bestRow >= m.s.r && bestRow <= m.e.r) {
|
||||
for (let c = m.s.c; c <= m.e.c; c++) {
|
||||
mergeStartMap[c] = m.s.c;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const seen = new Set();
|
||||
for (let c = bestStartCol; c < rawRow.length; c++) {
|
||||
const mergeStart = mergeStartMap[c];
|
||||
const cleanVal = (v) => (v === undefined ? "" : String(v).replace(/[\r\n\t]+/g, ' ').trim());
|
||||
if (mergeStart !== undefined) {
|
||||
// Part of a merge — only take the first occurrence
|
||||
if (seen.has(mergeStart)) continue;
|
||||
seen.add(mergeStart);
|
||||
headers.push(cleanVal(rawRow[mergeStart]));
|
||||
} else {
|
||||
headers.push(cleanVal(rawRow[c]));
|
||||
}
|
||||
}
|
||||
// Trim trailing empty columns
|
||||
while (headers.length > 0 && headers[headers.length - 1] === '') {
|
||||
headers.pop();
|
||||
}
|
||||
// Final clean + name empty columns to match __empty_N__ convention
|
||||
headers = headers.map((h, i) => {
|
||||
h = h.replace(/[\r\n\t]+/g, ' ').trim();
|
||||
return h === '' ? `__empty_${i + 1}__` : h;
|
||||
});
|
||||
console.log("Logical headers:", headers, `(${headers.length} columns from ${rawRow.length} physical)`);
|
||||
availableXlsColumns = [...headers];
|
||||
usedColumnsFromDB = [];
|
||||
saveXlsHeaders(headers);
|
||||
saveXlsHeaders(headers, bestRow + 1, bestStartCol + 1);
|
||||
updateXlsDropdowns();
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
|
||||
function saveXlsHeaders(headers) {
|
||||
function saveXlsHeaders(headers, headerRow, startColumn) {
|
||||
const payload = {
|
||||
template_id: <?php echo $id; ?>,
|
||||
xls_headers: JSON.stringify(headers)
|
||||
};
|
||||
if (headerRow !== undefined) payload.header_row = headerRow;
|
||||
if (startColumn !== undefined) payload.start_column = startColumn;
|
||||
|
||||
fetch('update_xls_headers.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
template_id: <?php echo $id; ?>,
|
||||
xls_headers: JSON.stringify(headers)
|
||||
})
|
||||
body: JSON.stringify(payload)
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
if (!data.success) console.error("❌ Error saving XLS headers:", data.message);
|
||||
else console.log("✅ Saved headers, header_row:", headerRow, "start_column:", startColumn);
|
||||
})
|
||||
.catch(error => console.error("❌ Fetch error:", error));
|
||||
}
|
||||
@ -570,10 +765,18 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
|
||||
let currentValue = select.value || select.dataset.currentXls || '';
|
||||
|
||||
let options = availableXlsColumns
|
||||
.map(col => {
|
||||
.map((col, origIdx) => ({ col, origIdx }))
|
||||
.map(({ col, origIdx }) => {
|
||||
const clean = col.replace(/[\r\n\t]+/g, ' ').trim();
|
||||
const isEmpty = clean === '';
|
||||
const colNum = origIdx + 1;
|
||||
const val = isEmpty ? `__empty_${colNum}__` : clean;
|
||||
const isUsed = uniqueUsedColumns.includes(col) && col !== currentValue;
|
||||
const label = isUsed ? `⚠ ${col} (already used)` : col;
|
||||
return `<option value="${col}" class="${isUsed ? 'used-option' : ''}" ${col === currentValue ? 'selected' : ''}>${label}</option>`;
|
||||
const label = isEmpty
|
||||
? `(empty column ${colNum})`
|
||||
: (isUsed ? `⚠ ${clean} (already used)` : clean);
|
||||
const isSelected = (isEmpty ? val === currentValue : col === currentValue) ? 'selected' : '';
|
||||
return `<option value="${val}" class="${isUsed ? 'used-option' : ''}" ${isSelected}>${label}</option>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
@ -1565,4 +1768,4 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@ -19,8 +19,6 @@
|
||||
<i class="fas fa-flask"></i> Analysis
|
||||
</button>
|
||||
|
||||
<select id="global-matrice" class="ms-2" style="width: 250px !important; min-width: 250px !important;">
|
||||
</select>
|
||||
|
||||
<input type="checkbox" id="showMixParts" name="showMixParts" style="margin-right: 5px; margin-left: 10px;">
|
||||
<label for="showMixParts" style="font-size: 0.9rem; margin-right: 10px;">Mix</label>
|
||||
@ -36,13 +34,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>
|
||||
@ -68,7 +66,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>
|
||||
@ -76,9 +74,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>
|
||||
@ -201,6 +199,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
|
||||
|
||||
218
public/userarea/modals_gridData.js
Normal file
218
public/userarea/modals_gridData.js
Normal file
@ -0,0 +1,218 @@
|
||||
/**
|
||||
* 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 urlParams = new URLSearchParams(window.location.search);
|
||||
const importref = urlParams.get('importref') || '';
|
||||
const resp = await fetch('add_record.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ template_id: templateId, importreferencecode: importref })
|
||||
});
|
||||
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: false,
|
||||
};
|
||||
|
||||
// Add to beginning of gridData
|
||||
window.gridData.unshift(newRow);
|
||||
|
||||
// Re-render
|
||||
const gr = window.gridRenderer;
|
||||
if (gr) gr.renderVisibleRows();
|
||||
|
||||
// Highlight new row briefly
|
||||
const newGridRow = document.querySelector(`.grid-row[data-id="${result.iddatadb}"]`);
|
||||
if (newGridRow) {
|
||||
newGridRow.classList.add('row-just-created');
|
||||
newGridRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
setTimeout(() => newGridRow.classList.remove('row-just-created'), 4000);
|
||||
}
|
||||
} else {
|
||||
alert('Error: ' + (result.message || 'Unknown error'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
@ -143,7 +143,7 @@ $(document).ready(function () {
|
||||
if (iddatadb) {
|
||||
if (matrici.length === 0) {
|
||||
$.ajax({
|
||||
url: "get_matrici_db.php",
|
||||
url: "get_matrici.php",
|
||||
method: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
|
||||
@ -342,51 +342,12 @@ $(document).ready(function () {
|
||||
// ===================
|
||||
function loadParts(iddatadb, idquotations, callback = null) {
|
||||
if (iddatadb) {
|
||||
if (matrici.length === 0) {
|
||||
$.ajax({
|
||||
url: "get_matrici_db.php",
|
||||
method: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
matrici = data.value || [];
|
||||
loadMacroMatrici();
|
||||
initializeGlobalSelect2();
|
||||
loadPartsExtraField(iddatadb, function () {
|
||||
loadPhoto(iddatadb, idquotations);
|
||||
loadExistingParts(iddatadb, idquotations, callback);
|
||||
});
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
matrici = [];
|
||||
loadMacroMatrici();
|
||||
initializeGlobalSelect2();
|
||||
loadPartsExtraField(iddatadb, function () {
|
||||
loadPhoto(iddatadb, idquotations);
|
||||
loadExistingParts(iddatadb, idquotations, callback);
|
||||
});
|
||||
const errorMsg = $(
|
||||
'<div class="alert alert-danger temp-alert" role="alert">Errore nel caricamento delle matrici: ' +
|
||||
error +
|
||||
" (" +
|
||||
xhr.status +
|
||||
")</div>",
|
||||
);
|
||||
$("#partsModal .modal-body").prepend(errorMsg);
|
||||
setTimeout(function () {
|
||||
errorMsg.fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
loadMacroMatrici();
|
||||
initializeGlobalSelect2();
|
||||
loadPartsExtraField(iddatadb, function () {
|
||||
loadPhoto(iddatadb, idquotations);
|
||||
loadExistingParts(iddatadb, idquotations, callback);
|
||||
});
|
||||
}
|
||||
loadMacroMatrici();
|
||||
initializeGlobalSelect2();
|
||||
loadPartsExtraField(iddatadb, function () {
|
||||
loadPhoto(iddatadb, idquotations);
|
||||
loadExistingParts(iddatadb, idquotations, callback);
|
||||
});
|
||||
} else {
|
||||
loadPartsExtraField(iddatadb, function () {
|
||||
loadPhoto(iddatadb, idquotations);
|
||||
@ -941,7 +902,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 +911,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 +1399,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 +1408,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);
|
||||
@ -1518,7 +1479,7 @@ $(document).ready(function () {
|
||||
|
||||
function loadMacroMatrici() {
|
||||
$.ajax({
|
||||
url: "get_macro_matrici.php",
|
||||
url: "search_matrici.php?macro_list=1",
|
||||
method: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
@ -1615,18 +1576,6 @@ $(document).ready(function () {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filtra le matrici in base alla MacroMatrice selezionata
|
||||
const filteredMatrici = selectedMacro
|
||||
? matrici.filter(
|
||||
(matrice) => matrice.MacroMatrice === selectedMacro,
|
||||
)
|
||||
: matrici;
|
||||
|
||||
// Crea opzioni per Select2
|
||||
const options = filteredMatrici.map(function (matrice) {
|
||||
return { id: matrice.IdMatrice, text: matrice.NomeMatrice };
|
||||
});
|
||||
|
||||
// Memorizza il valore corrente
|
||||
const currentValue = $select.val();
|
||||
|
||||
@ -1635,39 +1584,39 @@ $(document).ready(function () {
|
||||
$select.select2("destroy");
|
||||
}
|
||||
|
||||
// Inizializza Select2
|
||||
// Inizializza Select2 con AJAX
|
||||
$select.empty().select2({
|
||||
placeholder: filteredMatrici.length
|
||||
? "Seleziona matrice globale"
|
||||
: "Nessuna matrice disponibile",
|
||||
placeholder: "Seleziona matrice globale",
|
||||
allowClear: true,
|
||||
data: options,
|
||||
dropdownParent: $("#partsModal"),
|
||||
minimumResultsForSearch: 0, // Abilita ricerca senza limite minimo di caratteri
|
||||
matcher: function (params, data) {
|
||||
if (!params.term) return data; // Mostra tutte le opzioni se non c'è termine di ricerca
|
||||
const term = params.term.toUpperCase();
|
||||
if (data.text.toUpperCase().indexOf(term) >= 0) return data;
|
||||
return null;
|
||||
minimumInputLength: 0,
|
||||
ajax: {
|
||||
url: "search_matrici.php",
|
||||
dataType: "json",
|
||||
delay: 150,
|
||||
data: function (params) {
|
||||
return { q: params.term || "", limit: 20, macro: selectedMacro || "" };
|
||||
},
|
||||
processResults: function (data) {
|
||||
return { results: data.results || [] };
|
||||
},
|
||||
cache: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Ripristina il valore corrente se valido
|
||||
if (
|
||||
currentValue &&
|
||||
filteredMatrici.some((m) => m.IdMatrice == currentValue)
|
||||
) {
|
||||
$select.val(currentValue).trigger("change");
|
||||
} else if (filteredMatrici.length === 0) {
|
||||
const errorMsg = $(
|
||||
'<div class="alert alert-warning temp-alert" role="alert">Nessuna matrice disponibile per la MacroMatrice selezionata.</div>',
|
||||
);
|
||||
$("#partsModal .modal-body").prepend(errorMsg);
|
||||
setTimeout(function () {
|
||||
errorMsg.fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
// Ripristina il valore corrente
|
||||
if (currentValue) {
|
||||
$.ajax({
|
||||
url: "search_matrici.php",
|
||||
data: { id: currentValue },
|
||||
dataType: "json",
|
||||
}).then(function (data) {
|
||||
const item = (data.results || [])[0];
|
||||
if (item) {
|
||||
const option = new Option(item.text, item.id, true, true);
|
||||
$select.append(option).trigger("change");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1686,32 +1635,6 @@ $(document).ready(function () {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filtra le matrici in base alla MacroMatrice selezionata
|
||||
const filteredMatrici = selectedMacro
|
||||
? matrici.filter((m) => m.MacroMatrice === selectedMacro)
|
||||
: matrici;
|
||||
|
||||
// Crea opzioni per Select2, includendo il valore pre-selezionato se non è nel filtro
|
||||
let options = filteredMatrici.map(function (matrice) {
|
||||
return { id: matrice.IdMatrice, text: matrice.NomeMatrice };
|
||||
});
|
||||
|
||||
// Se idmatrice esiste ed è fuori dal filtro, aggiungilo come opzione
|
||||
if (
|
||||
idmatrice &&
|
||||
!filteredMatrici.some((m) => m.IdMatrice == idmatrice)
|
||||
) {
|
||||
const selectedMatrice = matrici.find(
|
||||
(m) => m.IdMatrice == idmatrice,
|
||||
);
|
||||
if (selectedMatrice) {
|
||||
options.push({
|
||||
id: selectedMatrice.IdMatrice,
|
||||
text: selectedMatrice.NomeMatrice,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Memorizza il valore corrente
|
||||
const currentValue = idmatrice || $select.val();
|
||||
|
||||
@ -1720,44 +1643,44 @@ $(document).ready(function () {
|
||||
$select.select2("destroy");
|
||||
}
|
||||
|
||||
// Inizializza Select2
|
||||
// Inizializza Select2 con AJAX
|
||||
$select.empty().select2({
|
||||
placeholder: filteredMatrici.length
|
||||
? "Seleziona matrice"
|
||||
: "Nessuna matrice disponibile",
|
||||
placeholder: "Seleziona matrice",
|
||||
allowClear: true,
|
||||
data: options,
|
||||
dropdownParent: $("#partsModal"),
|
||||
minimumResultsForSearch: 0, // Abilita ricerca senza limite minimo di caratteri
|
||||
matcher: function (params, data) {
|
||||
if (!params.term) return data; // Mostra tutte le opzioni se non c'è termine di ricerca
|
||||
const term = params.term.toUpperCase();
|
||||
if (data.text.toUpperCase().indexOf(term) >= 0) return data;
|
||||
return null;
|
||||
minimumInputLength: 0,
|
||||
ajax: {
|
||||
url: "search_matrici.php",
|
||||
dataType: "json",
|
||||
delay: 150,
|
||||
data: function (params) {
|
||||
return { q: params.term || "", limit: 20, macro: selectedMacro || "" };
|
||||
},
|
||||
processResults: function (data) {
|
||||
return { results: data.results || [] };
|
||||
},
|
||||
cache: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Ripristina il valore se valido
|
||||
if (partId && partId !== "new" && currentValue) {
|
||||
const matrice = matrici.find((m) => m.IdMatrice == currentValue);
|
||||
if (matrice) {
|
||||
const option = new Option(
|
||||
matrice.NomeMatrice,
|
||||
matrice.IdMatrice,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
if (!fromFilter) $select.append(option).trigger("change");
|
||||
else $select.append(option);
|
||||
|
||||
partMatrice[partNumber] = matrice.IdMatrice;
|
||||
} else {
|
||||
// Aggiusta valore non valido
|
||||
if (!fromFilter) $select.val(null).trigger("change");
|
||||
|
||||
partMatrice[partNumber] = null;
|
||||
}
|
||||
$.ajax({
|
||||
url: "search_matrici.php",
|
||||
data: { id: currentValue },
|
||||
dataType: "json",
|
||||
}).then(function (data) {
|
||||
const item = (data.results || [])[0];
|
||||
if (item) {
|
||||
const option = new Option(item.text, item.id, true, true);
|
||||
if (!fromFilter) $select.append(option).trigger("change");
|
||||
else $select.append(option);
|
||||
partMatrice[partNumber] = item.id;
|
||||
} else {
|
||||
if (!fromFilter) $select.val(null).trigger("change");
|
||||
partMatrice[partNumber] = null;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$select.val(null).trigger("change", [{ skipHandler: true }]);
|
||||
}
|
||||
@ -1834,18 +1757,7 @@ $(document).ready(function () {
|
||||
}
|
||||
});
|
||||
|
||||
// Mostra messaggio se non ci sono matrici
|
||||
if (filteredMatrici.length === 0 && selectedMacro) {
|
||||
const errorMsg = $(
|
||||
'<div class="alert alert-warning temp-alert" role="alert">Nessuna matrice disponibile per la MacroMatrice selezionata.</div>',
|
||||
);
|
||||
$("#partsModal .modal-body").prepend(errorMsg);
|
||||
setTimeout(function () {
|
||||
errorMsg.fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
// Messaggio se macro selezionata ma nessun risultato sarà gestito dal placeholder Select2
|
||||
|
||||
// Debug per verificare inizializzazione
|
||||
console.log(
|
||||
@ -1861,8 +1773,13 @@ $(document).ready(function () {
|
||||
$(document).on("click", ".propagate-matrice-btn", function () {
|
||||
const $row = $(this).closest("tr");
|
||||
const globalVal = $("#global-matrice").val();
|
||||
const globalText = $("#global-matrice").find("option:selected").text();
|
||||
if (globalVal) {
|
||||
$row.find(".part-matrice").val(globalVal).trigger("change");
|
||||
const $target = $row.find(".part-matrice");
|
||||
if (!$target.find(`option[value="${globalVal}"]`).length) {
|
||||
$target.append(new Option(globalText, globalVal, true, true));
|
||||
}
|
||||
$target.val(globalVal).trigger("change");
|
||||
} else {
|
||||
const errorMsg = $(
|
||||
'<div class="alert alert-danger temp-alert" role="alert">Seleziona una matrice globale prima di propagare.</div>',
|
||||
@ -1878,8 +1795,15 @@ $(document).ready(function () {
|
||||
|
||||
$(document).on("click", ".propagate-all-btn", function () {
|
||||
const globalVal = $("#global-matrice").val();
|
||||
const globalText = $("#global-matrice").find("option:selected").text();
|
||||
if (globalVal) {
|
||||
$("#partsTableBody .part-matrice").val(globalVal).trigger("change");
|
||||
$("#partsTableBody .part-matrice").each(function () {
|
||||
const $target = $(this);
|
||||
if (!$target.find(`option[value="${globalVal}"]`).length) {
|
||||
$target.append(new Option(globalText, globalVal, true, true));
|
||||
}
|
||||
$target.val(globalVal).trigger("change");
|
||||
});
|
||||
} else {
|
||||
const errorMsg = $(
|
||||
'<div class="alert alert-danger temp-alert" role="alert">Seleziona una matrice globale prima di propagare.</div>',
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -11,36 +11,6 @@ session_start();
|
||||
require_once '../../vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
|
||||
function findHeaderRow(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, array $expectedHeaders, int $startCol, int $highestColIndex, int $maxRowsToScan = 20): ?int
|
||||
{
|
||||
$normalizedExpected = array_filter(array_map(function ($h) {
|
||||
return strtolower(trim(str_replace(['\\r\\n', '\r\n', "\r\n", "\n", "\r"], ' ', $h)));
|
||||
}, $expectedHeaders));
|
||||
$normalizedExpected = array_values($normalizedExpected);
|
||||
sort($normalizedExpected);
|
||||
|
||||
for ($row = 1; $row <= $maxRowsToScan; $row++) {
|
||||
$rowHeaders = [];
|
||||
for ($col = $startCol; $col <= $highestColIndex; $col++) {
|
||||
$colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col);
|
||||
$cell = $worksheet->getCell($colLetter . $row);
|
||||
$val = $cell ? trim((string)$cell->getCalculatedValue()) : '';
|
||||
if ($val !== '') {
|
||||
$rowHeaders[] = strtolower(trim(str_replace(["\r\n", "\n", "\r"], ' ', $val)));
|
||||
}
|
||||
}
|
||||
$normalizedRow = $rowHeaders;
|
||||
sort($normalizedRow);
|
||||
|
||||
$matches = count(array_intersect($normalizedExpected, $normalizedRow));
|
||||
$threshold = (int) ceil(count($normalizedExpected) * 0.6);
|
||||
if ($matches >= $threshold) {
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$response = ['error' => '', 'rows' => [], 'columns' => [], 'template_id' => 0, 'filename' => '', 'apply_routine' => false];
|
||||
|
||||
try {
|
||||
@ -101,34 +71,18 @@ try {
|
||||
$highestColumn = $worksheet->getHighestColumn();
|
||||
$highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn);
|
||||
|
||||
$startRow = max(1, $header_row);
|
||||
$startColumn = max(1, $start_column);
|
||||
|
||||
// Recupera routine e headers dal template — DEVE essere prima dell'auto-detect
|
||||
$stmt = $pdo->prepare("SELECT idroutine, idclient, xls_headers FROM excel_templates WHERE id = ?");
|
||||
$stmt->execute([$template_id]);
|
||||
$template = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
error_log("=== DEBUG TEMPLATE ===");
|
||||
error_log("template raw: " . print_r($template, true));
|
||||
error_log("xls_headers value: " . var_export($template['xls_headers'] ?? 'KEY NON ESISTE', true));
|
||||
error_log("xls_headers empty check: " . var_export(empty($template['xls_headers']), true));
|
||||
// Auto-detect della riga header se xls_headers è disponibile
|
||||
$detectedHeaderRow = $header_row;
|
||||
if (!empty($template['xls_headers'])) {
|
||||
$expectedHeaders = json_decode($template['xls_headers'], true);
|
||||
if (is_array($expectedHeaders) && !empty($expectedHeaders)) {
|
||||
error_log("Expected headers from DB: " . print_r($expectedHeaders, true));
|
||||
$found = findHeaderRow($worksheet, $expectedHeaders, $startColumn, $highestColumnIndex);
|
||||
error_log("findHeaderRow result: " . var_export($found, true));
|
||||
if ($found !== null) {
|
||||
$detectedHeaderRow = $found;
|
||||
error_log("Header row auto-detected at row: $detectedHeaderRow (was: $header_row)");
|
||||
} else {
|
||||
error_log("Header row auto-detection failed, using provided header_row: $header_row");
|
||||
}
|
||||
// Advance startColumn to first non-empty cell in header row (match JS behavior)
|
||||
for ($sc = $startColumn; $sc <= $highestColumnIndex; $sc++) {
|
||||
$cl = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($sc);
|
||||
$cv = trim((string)($worksheet->getCell($cl . $header_row)->getCalculatedValue() ?? ''));
|
||||
if ($cv !== '') {
|
||||
$startColumn = $sc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$startRow = max(1, $detectedHeaderRow);
|
||||
$header_row = $detectedHeaderRow;
|
||||
|
||||
// Debug dei parametri
|
||||
error_log("Processing - template_id: $template_id, startRow: $startRow, startColumn: $startColumn, highestRow: $highestRow, highestColumn: $highestColumn, highestColumnIndex: $highestColumnIndex");
|
||||
@ -140,30 +94,86 @@ try {
|
||||
$response['error'] = "La colonna di partenza ($startColumn) supera il numero totale di colonne ($highestColumnIndex).";
|
||||
} else {
|
||||
$excelData = [];
|
||||
// Estrai la riga degli header
|
||||
$headerRowData = [];
|
||||
for ($col = $startColumn; $col <= $highestColumnIndex; $col++) {
|
||||
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col);
|
||||
$cell = $worksheet->getCell($columnLetter . $header_row);
|
||||
$cellValue = $cell ? $cell->getCalculatedValue() : '';
|
||||
$headerRowData[] = $cellValue ?: '';
|
||||
|
||||
// Build merge map for header row: physCol -> mergeStartCol
|
||||
$mergeStartMap = [];
|
||||
foreach ($worksheet->getMergeCells() as $range) {
|
||||
[$startCell, $endCell] = explode(':', $range);
|
||||
$mStartCol = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString(preg_replace('/\d+/', '', $startCell));
|
||||
$mEndCol = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString(preg_replace('/\d+/', '', $endCell));
|
||||
$mStartRow = (int)preg_replace('/[A-Z]+/i', '', $startCell);
|
||||
$mEndRow = (int)preg_replace('/[A-Z]+/i', '', $endCell);
|
||||
if ($header_row >= $mStartRow && $header_row <= $mEndRow) {
|
||||
for ($c = $mStartCol; $c <= $mEndCol; $c++) {
|
||||
$mergeStartMap[$c] = $mStartCol;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Estrai i dati a partire dalla riga successiva, includendo excelrow
|
||||
for ($row = $header_row + 1; $row <= $highestRow; $row++) {
|
||||
// Build logical columns: each merge = one column
|
||||
$logicalCols = []; // array of physical column indices (one per logical column)
|
||||
$seen = [];
|
||||
for ($col = $startColumn; $col <= $highestColumnIndex; $col++) {
|
||||
if (isset($mergeStartMap[$col])) {
|
||||
$ms = $mergeStartMap[$col];
|
||||
if (in_array($ms, $seen, true)) continue;
|
||||
$seen[] = $ms;
|
||||
$logicalCols[] = $ms;
|
||||
} else {
|
||||
$logicalCols[] = $col;
|
||||
}
|
||||
}
|
||||
|
||||
// Build header row using logical columns
|
||||
$headerRowData = [];
|
||||
$logicalNum = 0;
|
||||
foreach ($logicalCols as $physCol) {
|
||||
$logicalNum++;
|
||||
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($physCol);
|
||||
$cell = $worksheet->getCell($columnLetter . $header_row);
|
||||
$cellValue = trim((string)($cell ? $cell->getCalculatedValue() : ''));
|
||||
$cellValue = preg_replace('/[\r\n\t]+/', ' ', $cellValue);
|
||||
// Empty headers get __empty_N__ to match mapping page
|
||||
$headerRowData[] = ($cellValue !== '') ? $cellValue : '__empty_' . $logicalNum . '__';
|
||||
}
|
||||
|
||||
error_log("Logical headers: " . json_encode($headerRowData));
|
||||
error_log("Logical cols (physical indices): " . json_encode($logicalCols));
|
||||
|
||||
// Find which logical columns have real headers
|
||||
$headerFilledIndices = [];
|
||||
foreach ($headerRowData as $idx => $hVal) {
|
||||
if (!str_starts_with($hVal, '__empty_')) $headerFilledIndices[] = $idx;
|
||||
}
|
||||
$minFilled = max(1, min(2, count($headerFilledIndices)));
|
||||
|
||||
// Extract data rows using logical columns
|
||||
for ($row = $startRow + 1; $row <= $highestRow; $row++) {
|
||||
$rowData = [];
|
||||
for ($col = $startColumn; $col <= $highestColumnIndex; $col++) {
|
||||
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col);
|
||||
foreach ($logicalCols as $physCol) {
|
||||
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($physCol);
|
||||
$cell = $worksheet->getCell($columnLetter . $row);
|
||||
$cellValue = $cell ? $cell->getCalculatedValue() : '';
|
||||
$rowData[] = $cellValue ?: '';
|
||||
}
|
||||
if (!empty(array_filter($rowData))) {
|
||||
|
||||
// Count how many header columns have data in this row
|
||||
$filledCount = 0;
|
||||
foreach ($headerFilledIndices as $idx) {
|
||||
if (isset($rowData[$idx]) && trim((string)$rowData[$idx]) !== '') {
|
||||
$filledCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($filledCount >= $minFilled) {
|
||||
$excelData[] = ['data' => $rowData, 'excelrow' => $row];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Recupera routine dal template
|
||||
$stmt = $pdo->prepare("SELECT idroutine, idclient FROM excel_templates WHERE id = ?");
|
||||
$stmt->execute([$template_id]);
|
||||
$template = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($template && $template['idroutine']) {
|
||||
$stmtRoutine = $pdo->prepare("SELECT idroutine, name, filename, headerrow, instruction FROM routine WHERE idroutine = ?");
|
||||
|
||||
130
public/userarea/saveAll.js
Normal file
130
public/userarea/saveAll.js
Normal file
@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 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;
|
||||
if (window.gridRenderer?.clearDirty) window.gridRenderer.clearDirty(rowIndex);
|
||||
// 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;
|
||||
if (window.gridRenderer?.clearDirty) window.gridRenderer.clearDirty(idx);
|
||||
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;
|
||||
})();
|
||||
62
public/userarea/search_clienti.php
Normal file
62
public/userarea/search_clienti.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
include dirname(__DIR__) . '/../extra/auth.php';
|
||||
if (!Auth::check()) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; }
|
||||
|
||||
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
$q = mb_strtolower(trim($_GET['q'] ?? ''));
|
||||
$limit = max(1, min(50, intval($_GET['limit'] ?? 20)));
|
||||
$id = isset($_GET['id']) ? intval($_GET['id']) : null;
|
||||
|
||||
try {
|
||||
// Load from cache or API
|
||||
$cacheFile = __DIR__ . '/cache/clienti.json';
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
|
||||
$data = json_decode(file_get_contents($cacheFile), true);
|
||||
} else {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
$params = [
|
||||
'$select' => 'IdCliente,Nominativo,CodiceCliente',
|
||||
'$orderby' => 'Nominativo asc'
|
||||
];
|
||||
$data = $api->get("Cliente?" . http_build_query($params));
|
||||
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
|
||||
file_put_contents($cacheFile, json_encode($data));
|
||||
}
|
||||
|
||||
$clients = $data['value'] ?? [];
|
||||
|
||||
// If requesting by specific ID (for loading selected value)
|
||||
if ($id !== null) {
|
||||
foreach ($clients as $c) {
|
||||
if ((int)$c['IdCliente'] === $id) {
|
||||
echo json_encode(['results' => [['id' => $c['IdCliente'], 'text' => trim($c['Nominativo'] ?? '')]]]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
echo json_encode(['results' => []]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Search by query
|
||||
$results = [];
|
||||
foreach ($clients as $c) {
|
||||
$name = trim($c['Nominativo'] ?? '');
|
||||
$code = trim($c['CodiceCliente'] ?? '');
|
||||
if ($q === '' || mb_strpos(mb_strtolower($name), $q) !== false || mb_strpos(mb_strtolower($code), $q) !== false) {
|
||||
$results[] = ['id' => $c['IdCliente'], 'text' => $name];
|
||||
if (count($results) >= $limit) break;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['results' => $results]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
66
public/userarea/search_customfield_values.php
Normal file
66
public/userarea/search_customfield_values.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
include dirname(__DIR__) . '/../extra/auth.php';
|
||||
if (!Auth::check()) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; }
|
||||
|
||||
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
$fieldId = intval($_GET['field_id'] ?? 0);
|
||||
$q = mb_strtolower(trim($_GET['q'] ?? ''));
|
||||
$id = isset($_GET['id']) ? intval($_GET['id']) : null;
|
||||
$limit = max(1, min(50, intval($_GET['limit'] ?? 20)));
|
||||
|
||||
if (!$fieldId) {
|
||||
echo json_encode(['results' => []]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$cacheDir = __DIR__ . '/cache';
|
||||
$cacheFile = $cacheDir . '/customfield_' . $fieldId . '.json';
|
||||
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
|
||||
$values = json_decode(file_get_contents($cacheFile), true);
|
||||
} else {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
$data = $api->get("CustomField($fieldId)?\$expand=CustomFieldsValues");
|
||||
$values = $data['CustomFieldsValues'] ?? [];
|
||||
if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true);
|
||||
file_put_contents($cacheFile, json_encode($values));
|
||||
}
|
||||
|
||||
// Lookup by ID
|
||||
if ($id !== null) {
|
||||
foreach ($values as $v) {
|
||||
if ((int)($v['IdCustomFieldsValue'] ?? 0) === $id) {
|
||||
echo json_encode(['results' => [['id' => $v['IdCustomFieldsValue'], 'text' => $v['Valore'] ?? '']]]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
echo json_encode(['results' => []]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Search by query
|
||||
$results = [];
|
||||
foreach ($values as $v) {
|
||||
$text = $v['Valore'] ?? '';
|
||||
if ($q === '' || mb_strpos(mb_strtolower($text), $q) !== false) {
|
||||
$results[] = ['id' => $v['IdCustomFieldsValue'], 'text' => $text];
|
||||
if (count($results) >= $limit) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort alphabetically
|
||||
usort($results, fn($a, $b) => strcasecmp($a['text'], $b['text']));
|
||||
|
||||
echo json_encode(['results' => $results]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
73
public/userarea/search_matrici.php
Normal file
73
public/userarea/search_matrici.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
include dirname(__DIR__) . '/../extra/auth.php';
|
||||
if (!Auth::check()) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; }
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
|
||||
$q = mb_strtolower(trim($_GET['q'] ?? ''));
|
||||
$id = isset($_GET['id']) ? intval($_GET['id']) : null;
|
||||
$limit = max(1, min(50, intval($_GET['limit'] ?? 20)));
|
||||
$macro = trim($_GET['macro'] ?? '');
|
||||
|
||||
$cacheFile = __DIR__ . '/cache/matrici.json';
|
||||
|
||||
if (!file_exists($cacheFile)) {
|
||||
// Trigger cache creation
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
$data = $api->get('Matrice');
|
||||
$matrici = [];
|
||||
foreach (($data['value'] ?? []) as $item) {
|
||||
$nome = $item['NomeMatrice'] ?? '';
|
||||
if ($nome !== '' && substr($nome, 0, 1) !== '*') {
|
||||
$matrici[] = ['IdMatrice' => $item['IdMatrice'], 'NomeMatrice' => $nome, 'MacroMatrice' => $item['MacroMatrice'] ?? null];
|
||||
}
|
||||
}
|
||||
usort($matrici, fn($a, $b) => strcasecmp($a['NomeMatrice'], $b['NomeMatrice']));
|
||||
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
|
||||
file_put_contents($cacheFile, json_encode(['success' => true, 'value' => $matrici]));
|
||||
} else {
|
||||
$cached = json_decode(file_get_contents($cacheFile), true);
|
||||
$matrici = $cached['value'] ?? [];
|
||||
}
|
||||
|
||||
// Lookup by ID
|
||||
if ($id !== null) {
|
||||
foreach ($matrici as $m) {
|
||||
if ((int)$m['IdMatrice'] === $id) {
|
||||
echo json_encode(['results' => [['id' => $m['IdMatrice'], 'text' => $m['NomeMatrice']]]]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
echo json_encode(['results' => []]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Return unique MacroMatrice list
|
||||
if (isset($_GET['macro_list'])) {
|
||||
$macros = [];
|
||||
foreach ($matrici as $m) {
|
||||
$mv = $m['MacroMatrice'] ?? '';
|
||||
if ($mv !== '' && !in_array($mv, $macros, true)) $macros[] = $mv;
|
||||
}
|
||||
sort($macros);
|
||||
echo json_encode(['success' => true, 'value' => $macros]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Search (with optional MacroMatrice filter)
|
||||
$results = [];
|
||||
foreach ($matrici as $m) {
|
||||
$nome = $m['NomeMatrice'] ?? '';
|
||||
if ($macro !== '' && ($m['MacroMatrice'] ?? '') !== $macro) continue;
|
||||
if ($q === '' || mb_strpos(mb_strtolower($nome), $q) !== false) {
|
||||
$results[] = ['id' => $m['IdMatrice'], 'text' => $nome];
|
||||
if (count($results) >= $limit) break;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['results' => $results]);
|
||||
2021
public/userarea/tolims.php
Normal file
2021
public/userarea/tolims.php
Normal file
File diff suppressed because it is too large
Load Diff
@ -18,10 +18,27 @@ if (!$data || !isset($data['template_id'], $data['xls_headers'])) {
|
||||
|
||||
$templateId = $data['template_id'];
|
||||
$xlsHeaders = $data['xls_headers'];
|
||||
$headerRow = isset($data['header_row']) ? (int)$data['header_row'] : null;
|
||||
$startColumn = isset($data['start_column']) ? (int)$data['start_column'] : null;
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("UPDATE excel_templates SET xls_headers = ? WHERE id = ?");
|
||||
$result = $stmt->execute([$xlsHeaders, $templateId]);
|
||||
$sql = "UPDATE excel_templates SET xls_headers = ?";
|
||||
$params = [$xlsHeaders];
|
||||
|
||||
if ($headerRow !== null) {
|
||||
$sql .= ", header_row = ?";
|
||||
$params[] = $headerRow;
|
||||
}
|
||||
if ($startColumn !== null) {
|
||||
$sql .= ", start_column = ?";
|
||||
$params[] = $startColumn;
|
||||
}
|
||||
|
||||
$sql .= " WHERE id = ?";
|
||||
$params[] = $templateId;
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$result = $stmt->execute($params);
|
||||
|
||||
if (!$result) {
|
||||
echo json_encode(["success" => false, "message" => "Database update failed"]);
|
||||
|
||||
124
public/userarea/warm_cache.php
Normal file
124
public/userarea/warm_cache.php
Normal file
@ -0,0 +1,124 @@
|
||||
<?php
|
||||
// Cache warm-up script.
|
||||
// Run via CLI: php warm_cache.php
|
||||
// Run via cron: every 30 min: php /path/to/warm_cache.php
|
||||
// Run via HTTP: http://localhost:8000/public/userarea/warm_cache.php (requires auth)
|
||||
|
||||
$startTime = microtime(true);
|
||||
$isCli = (php_sapi_name() === 'cli');
|
||||
|
||||
if (!$isCli) {
|
||||
// When called via HTTP, require auth
|
||||
include('include/headscript.php');
|
||||
if (!Auth::check()) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
header('Content-Type: application/json');
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
|
||||
$cacheDir = __DIR__ . '/cache';
|
||||
if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true);
|
||||
|
||||
$log = [];
|
||||
function warmLog(string $msg, bool $isCli) {
|
||||
global $log;
|
||||
$line = date('H:i:s') . " $msg";
|
||||
$log[] = $line;
|
||||
if ($isCli) echo $line . PHP_EOL;
|
||||
}
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
// 1. Clients
|
||||
warmLog('[clients] Fetching...', $isCli);
|
||||
$params = ['$select' => 'IdCliente,Nominativo,CodiceCliente', '$orderby' => 'Nominativo asc'];
|
||||
$clientData = $api->get("Cliente?" . http_build_query($params));
|
||||
file_put_contents($cacheDir . '/clienti.json', json_encode($clientData));
|
||||
$clientCount = count($clientData['value'] ?? []);
|
||||
warmLog("[clients] Cached $clientCount clients", $isCli);
|
||||
|
||||
// 2. Fixed fields: MoltiplicatorePrezzo, AnagraficaCertestObject, AnagraficaCertestService
|
||||
$fixedFields = [
|
||||
'MoltiplicatorePrezzo' => ['endpoint' => 'MoltiplicatorePrezzi', 'file' => 'moltiplicatori_prezzo.json'],
|
||||
'AnagraficaCertestObject' => ['endpoint' => 'AnagraficaCertestObject', 'file' => 'anagrafica_certest_object.json'],
|
||||
'AnagraficaCertestService' => ['endpoint' => 'AnagraficaCertestService', 'file' => 'anagrafica_certest_service.json'],
|
||||
];
|
||||
|
||||
foreach ($fixedFields as $name => $cfg) {
|
||||
warmLog("[$name] Fetching...", $isCli);
|
||||
$data = $api->get($cfg['endpoint']);
|
||||
file_put_contents($cacheDir . '/' . $cfg['file'], json_encode($data, JSON_PRETTY_PRINT));
|
||||
$count = count($data['value'] ?? $data ?? []);
|
||||
warmLog("[$name] Cached $count items", $isCli);
|
||||
}
|
||||
|
||||
// 3. ClienteResponsabile — for each unique client in datadb
|
||||
$stmt = $pdo->query("SELECT DISTINCT idclient FROM datadb WHERE idclient IS NOT NULL AND idclient > 0 ORDER BY idclient ASC");
|
||||
$clientIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
warmLog("[ClienteResponsabile] Fetching for " . count($clientIds) . " clients...", $isCli);
|
||||
|
||||
foreach ($clientIds as $cid) {
|
||||
$cid = (int)$cid;
|
||||
$endpoint = "Cliente($cid)?\$expand=Responsabili";
|
||||
$cacheFile = $cacheDir . '/cliente_responsabili_' . $cid . '.json';
|
||||
try {
|
||||
$data = $api->get($endpoint);
|
||||
file_put_contents($cacheFile, json_encode($data, JSON_PRETTY_PRINT));
|
||||
$count = count($data['Responsabili'] ?? []);
|
||||
warmLog("[ClienteResponsabile] Client $cid: $count responsabili", $isCli);
|
||||
} catch (Exception $e) {
|
||||
warmLog("[ClienteResponsabile] Client $cid: ERROR " . $e->getMessage(), $isCli);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. CustomField values — all SceltaMultipla field_ids from template_mapping
|
||||
$stmt = $pdo->query("SELECT DISTINCT field_id FROM template_mapping WHERE data_type = 'SceltaMultipla' AND is_visible_import = 1 AND field_id IS NOT NULL ORDER BY field_id");
|
||||
$fieldIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
warmLog("[CustomFields] Fetching " . count($fieldIds) . " fields...", $isCli);
|
||||
|
||||
foreach ($fieldIds as $fid) {
|
||||
$fid = (int)$fid;
|
||||
$cacheFile = $cacheDir . '/customfield_' . $fid . '.json';
|
||||
try {
|
||||
$endpoint = "CustomField($fid)?\$expand=CustomFieldsValues";
|
||||
$data = $api->get($endpoint);
|
||||
$values = $data['CustomFieldsValues'] ?? [];
|
||||
file_put_contents($cacheFile, json_encode($values));
|
||||
warmLog("[CustomField $fid] Cached " . count($values) . " values", $isCli);
|
||||
} catch (Exception $e) {
|
||||
warmLog("[CustomField $fid] ERROR " . $e->getMessage(), $isCli);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Matrici (from LIMS API)
|
||||
warmLog('[matrici] Fetching from API...', $isCli);
|
||||
$matriciData = $api->get('Matrice');
|
||||
$matrici = [];
|
||||
foreach (($matriciData['value'] ?? []) as $item) {
|
||||
$nome = $item['NomeMatrice'] ?? '';
|
||||
if ($nome !== '' && substr($nome, 0, 1) !== '*') {
|
||||
$matrici[] = ['IdMatrice' => $item['IdMatrice'], 'NomeMatrice' => $nome, 'MacroMatrice' => $item['MacroMatrice'] ?? null];
|
||||
}
|
||||
}
|
||||
usort($matrici, fn($a, $b) => strcasecmp($a['NomeMatrice'], $b['NomeMatrice']));
|
||||
file_put_contents($cacheDir . '/matrici.json', json_encode(['success' => true, 'value' => $matrici]));
|
||||
warmLog("[matrici] Cached " . count($matrici) . " items", $isCli);
|
||||
|
||||
$elapsed = round(microtime(true) - $startTime, 1);
|
||||
warmLog("Done in {$elapsed}s", $isCli);
|
||||
|
||||
} catch (Exception $e) {
|
||||
warmLog("FATAL: " . $e->getMessage(), $isCli);
|
||||
}
|
||||
|
||||
if (!$isCli) {
|
||||
echo json_encode(['success' => true, 'log' => $log]);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user