Compare commits

...

48 Commits

Author SHA1 Message Date
RMubarakzyanov b812563023 Merge branch 'main' into feature/milestone3 2026-04-08 09:24:30 +03:00
solocla 39a821357e analysis complete 2026-04-03 11:15:33 +02:00
RMubarakzyanov 53c223ea5f merge 2026-04-02 17:01:22 +03:00
RMubarakzyanov 9775a12d4a Merge branch 'main' into feature/milestone3
# Conflicts:
#	public/userarea/import_insert.php
#	public/userarea/mapping_template_xls_scheme2.php
#	public/userarea/process_import_xls2.php
2026-04-02 12:29:17 +03:00
solocla 7dfb935e33 new mocnler routine (3 steps) 2026-04-02 09:49:09 +02:00
RMubarakzyanov abb4200215 Fix import 2026-03-31 17:15:43 +03:00
RMubarakzyanov 0be7109df4 Fix header finder 2026-03-31 16:33:08 +03:00
solocla 578671e013 added analysis by client and matrici 2026-03-31 14:08:51 +02:00
RMubarakzyanov d24836e2b1 matrici cache 2026-03-31 14:25:10 +03:00
RMubarakzyanov d983659000 transfer auto-detect header 2026-03-31 13:41:31 +03:00
RMubarakzyanov 7c5aa7734f hide form 2026-03-30 18:19:16 +03:00
RMubarakzyanov 4f0dbc7e91 cache and view improvements 2026-03-30 16:28:20 +03:00
RMubarakzyanov fb09f033ae removed htmlspecialchars on import && savings 2026-03-30 13:24:18 +03:00
solocla 8bae2d7008 analysis 2026-03-30 11:31:25 +02:00
RMubarakzyanov 7463fc6726 change links 2026-03-30 08:53:14 +03:00
RMubarakzyanov 0bc2ff7e9d fix autodetect 2026-03-29 21:47:23 +03:00
RMubarakzyanov aa1c32b7ed autodetect mapping 2026-03-29 21:07:16 +03:00
RMubarakzyanov c573a46318 tolims filter, pagination 2026-03-29 17:13:54 +03:00
solocla bf18a904bd update API template 2026-03-28 10:33:28 +01:00
solocla b3ce489348 api change dahsboard 2026-03-28 10:28:25 +01:00
solocla 08b89e01cc Stop tracking customfield_values_response.json 2026-03-28 10:28:08 +01:00
RMubarakzyanov 3e66d67dc5 remove htmlspecialchars from saving 2026-03-27 20:55:54 +03:00
solocla 490786731a moncler supplier fabric routine 2026-03-27 15:54:22 +01:00
solocla e96f538a47 added multiple column option template with color 2026-03-27 15:36:57 +01:00
solocla cc96ecb67f order parts by part_number 2026-03-27 15:18:44 +01:00
solocla 4cf03ae742 fixed routine 2026-03-27 09:36:06 +01:00
solocla 7a486e9dcf update moncler routine 2026-03-27 09:24:46 +01:00
RMubarakzyanov 2a7b1fae17 imported && tolims 2026-03-26 16:05:17 +03:00
solocla a05d9fed2b scheme alphabetical order 2026-03-26 11:47:02 +01:00
solocla 65170a0a7c fixed 285 export to LIMS and header import 2026-03-26 11:42:52 +01:00
solocla 223688c372 added select 2 on capitolato 2026-03-25 14:23:44 +01:00
solocla b562eb4033 fixed ordine alfabetico capitolato 2026-03-25 14:20:45 +01:00
solocla 0645a0c675 update profile 2026-03-25 11:02:27 +01:00
solocla a02a6b2c4c added clienteAnalisi on query export 2026-03-22 15:11:00 +01:00
RMubarakzyanov 45dd8d6907 Link photos to .01 Campioni 2026-03-21 16:18:27 +03:00
RMubarakzyanov 755f6812d4 Campione custom fields (Tested Component + parts), per-part ConsegnaRichiesta with validation 2026-03-20 23:31:31 +03:00
RMubarakzyanov 5e59ae2162 Tested component 2026-03-20 23:15:50 +03:00
solocla 71a19144c8 gitignore fixed 2026-03-19 13:48:51 +01:00
RMubarakzyanov 381a05341b PrimaPagina, StampaNelRapporto flags handling 2026-03-19 13:02:13 +03:00
solocla 5a58decd40 added clienti in template, export 2026-03-19 10:08:29 +01:00
RMubarakzyanov eb21910ef3 per-row buttons locking, progress 2026-03-18 19:58:57 +03:00
RMubarakzyanov 5dedc779df format date to d/m/Y 2026-03-18 19:24:21 +03:00
solocla f4e0074a73 fixed tested components 2026-03-18 16:11:03 +01:00
solocla 9c850e4ea6 fixed filter import 2026-03-18 16:07:25 +01:00
solocla f300811341 photos fixed background 2026-03-18 15:53:46 +01:00
RMubarakzyanov 48387a9945 cliente fornitore 2026-03-17 15:43:12 +03:00
RMubarakzyanov 0e90db8219 import date format 2026-03-17 12:44:37 +03:00
RMubarakzyanov eaf70d5a46 export date format 2026-03-16 20:57:05 +03:00
90 changed files with 50362 additions and 880 deletions
+3 -1
View File
@@ -33,11 +33,13 @@ yarn-error.log
/public/userarea/*.log /public/userarea/*.log
/public/userarea/*.txt /public/userarea/*.txt
/public/userarea/*_response.json /public/userarea/*_response.json
/public/userarea/customfield_values_response.json
/public/userarea/error_log.txt /public/userarea/error_log.txt
/public/userarea/import_debug.log /public/userarea/import_debug.log
/public/userarea/last_url.txt /public/userarea/last_url.txt
/public/userarea/logaspi/ /public/userarea/logaspi/
/public/userarea/logsapi/ /public/userarea/logs/api/
/public/userarea/logs/api/**
/public/userarea/photostrf/ /public/userarea/photostrf/
/public/userarea/class/*.log /public/userarea/class/*.log
/public/userarea/class/curl_auth_debug.log /public/userarea/class/curl_auth_debug.log
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
# DB LOCALE (Windows 11)
url=jdbc:mysql://localhost:3306/trfcertest
username=solocla
password=!Massarosa2
driver=com.mysql.cj.jdbc.Driver
changeLogFile=liquibase/changelog/db.changelog-master.yaml
+73
View 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()]);
}
+849
View File
@@ -0,0 +1,849 @@
(function () {
"use strict";
let analysisMatriciMap = {};
let analysisLoadedCache = {};
let analysisAssignedState = {};
let analysisSelectedState = {};
function loadAnalysisMatrixNames() {
return $.ajax({
url: "get_matrici_db.php",
method: "GET",
dataType: "json",
})
.done(function (data) {
analysisMatriciMap = {};
(data.value || []).forEach(function (matrice) {
analysisMatriciMap[String(matrice.IdMatrice)] =
matrice.NomeMatrice || "#" + matrice.IdMatrice;
});
applyAnalysisMatrixNames();
})
.fail(function () {
analysisMatriciMap = {};
applyAnalysisMatrixNames();
});
}
function applyAnalysisMatrixNames() {
const modal = document.getElementById("analysisModal");
if (!modal) return;
modal.querySelectorAll(".analysis-matrix-item").forEach((item) => {
const matrixId = item.getAttribute("data-matrix-id");
const nameEl = item.querySelector(".analysis-matrix-name");
let matrixName = "No Matrix";
if (matrixId && matrixId !== "NO_MATRIX") {
matrixName =
analysisMatriciMap[String(matrixId)] || "#" + matrixId;
}
item.setAttribute("data-matrix-name", matrixName);
if (nameEl) {
nameEl.textContent = matrixName;
}
});
modal.querySelectorAll(".analysis-part-matrix-name").forEach((el) => {
const matrixId = el.getAttribute("data-matrix-id");
if (!matrixId || matrixId === "NO_MATRIX") {
el.textContent = "No Matrix";
return;
}
el.textContent =
analysisMatriciMap[String(matrixId)] || "#" + matrixId;
});
}
function readInitialAssignedAnalyses() {
const jsonEl = document.getElementById("analysisAssignedInitialData");
if (!jsonEl) {
analysisAssignedState = {};
return;
}
try {
analysisAssignedState =
JSON.parse(jsonEl.textContent || "{}") || {};
} catch (e) {
analysisAssignedState = {};
}
}
function getSelectedPartIds() {
const modal = document.getElementById("analysisModal");
if (!modal) return [];
return Array.from(
modal.querySelectorAll(".analysis-part-checkbox:checked"),
).map((el) => String(el.value));
}
function getCurrentSelectedAnalysisRecordKeys() {
return Object.keys(analysisSelectedState).filter(function (key) {
return analysisSelectedState[key] === true;
});
}
function renderAssignedAnalysesForPart(partId) {
const container = document.getElementById(
"analysisAssignedListPart" + partId,
);
if (!container) return;
const items = Array.isArray(analysisAssignedState[String(partId)])
? analysisAssignedState[String(partId)]
: [];
if (!items.length) {
container.innerHTML = "";
return;
}
container.innerHTML = items
.map(function (item) {
const recordKey = item.analysis_recordkey || "";
const title = item.analysis_name || "Unnamed analysis";
const method = item.analysis_method || "";
return `
<div class="analysis-assigned-chip" data-recordkey="${escapeHtml(recordKey)}">
<div class="analysis-assigned-chip-text">
<div class="analysis-assigned-chip-title">${escapeHtml(title)}</div>
${method ? `<div class="analysis-assigned-chip-method">${escapeHtml(method)}</div>` : ""}
</div>
<button type="button"
class="analysis-remove-btn"
data-part-id="${escapeHtml(partId)}"
data-recordkey="${escapeHtml(recordKey)}"
title="Remove analysis">×</button>
</div>
`;
})
.join("");
}
function renderAssignedAnalysesForSelectedParts() {
const modal = document.getElementById("analysisModal");
if (!modal) return;
modal.querySelectorAll(".analysis-part-item").forEach(function (item) {
const partId = item.getAttribute("data-part-id");
renderAssignedAnalysesForPart(partId);
});
}
function syncSelectedAnalysisRows() {
const modal = document.getElementById("analysisModal");
if (!modal) return;
modal
.querySelectorAll(".analysis-analysis-item")
.forEach(function (item) {
const recordKey = item.getAttribute("data-recordkey") || "";
if (recordKey && analysisSelectedState[recordKey]) {
item.classList.add("analysis-selected");
} else {
item.classList.remove("analysis-selected");
}
});
}
function buildAnalysisPayloadFromRow(rowEl) {
return {
analysis_recordkey: rowEl.getAttribute("data-recordkey") || "",
analysis_name: rowEl.getAttribute("data-analysis-name") || "",
analysis_method: rowEl.getAttribute("data-analysis-method") || "",
analysis_level: rowEl.getAttribute("data-analysis-level") || "",
is_web_selectable: rowEl.getAttribute("data-web") === "1" ? 1 : 0,
is_accredited:
rowEl.getAttribute("data-accredited") === "1" ? 1 : 0,
};
}
function addAnalysisToLocalState(partId, payload, iddatadb, idmatrice) {
const key = String(partId);
if (!Array.isArray(analysisAssignedState[key])) {
analysisAssignedState[key] = [];
}
const exists = analysisAssignedState[key].some(function (item) {
return item.analysis_recordkey === payload.analysis_recordkey;
});
if (!exists) {
analysisAssignedState[key].push({
id: null,
part_id: parseInt(partId, 10),
iddatadb: iddatadb || null,
idmatrice: idmatrice || null,
analysis_recordkey: payload.analysis_recordkey,
analysis_name: payload.analysis_name,
analysis_method: payload.analysis_method,
analysis_level:
payload.analysis_level !== ""
? parseInt(payload.analysis_level, 10)
: null,
is_web_selectable: payload.is_web_selectable,
is_accredited: payload.is_accredited,
});
}
}
function removeAnalysisFromLocalState(partId, recordKey) {
const key = String(partId);
if (!Array.isArray(analysisAssignedState[key])) {
return;
}
analysisAssignedState[key] = analysisAssignedState[key].filter(
function (item) {
return item.analysis_recordkey !== recordKey;
},
);
}
function saveAnalysisAssociation(partId, payload, callback) {
const modal = document.getElementById("analysisModal");
if (!modal) return;
const iddatadb =
modal.querySelector("#analysisModalIddatadb")?.value || "";
const partItem = modal.querySelector(
'.analysis-part-item[data-part-id="' + partId + '"]',
);
const idmatrice = partItem
? partItem.getAttribute("data-matrix-id") || ""
: "";
$.ajax({
url: "save_part_analysis.php",
method: "POST",
dataType: "json",
data: {
part_id: partId,
iddatadb: iddatadb,
idmatrice: idmatrice !== "NO_MATRIX" ? idmatrice : "",
analysis_recordkey: payload.analysis_recordkey,
analysis_name: payload.analysis_name,
analysis_method: payload.analysis_method,
analysis_level: payload.analysis_level,
is_web_selectable: payload.is_web_selectable,
is_accredited: payload.is_accredited,
},
})
.done(function () {
addAnalysisToLocalState(
partId,
payload,
iddatadb,
idmatrice !== "NO_MATRIX" ? idmatrice : null,
);
renderAssignedAnalysesForPart(partId);
if (typeof callback === "function") callback(true);
})
.fail(function (xhr) {
let message = "Error saving analysis association";
if (xhr && xhr.responseJSON && xhr.responseJSON.message) {
message = xhr.responseJSON.message;
}
alert(message);
if (typeof callback === "function") callback(false);
});
}
function deleteAnalysisAssociation(partId, recordKey, callback) {
$.ajax({
url: "delete_part_analysis.php",
method: "POST",
dataType: "json",
data: {
part_id: partId,
analysis_recordkey: recordKey,
},
})
.done(function () {
removeAnalysisFromLocalState(partId, recordKey);
renderAssignedAnalysesForPart(partId);
if (typeof callback === "function") callback(true);
})
.fail(function (xhr) {
let message = "Error deleting analysis association";
if (xhr && xhr.responseJSON && xhr.responseJSON.message) {
message = xhr.responseJSON.message;
}
alert(message);
if (typeof callback === "function") callback(false);
});
}
function setAnalysisLoadingState(isLoading) {
const modal = document.getElementById("analysisModal");
if (!modal) return;
const loadingEl = modal.querySelector("#analysisLoadingBox");
if (!loadingEl) return;
if (isLoading) {
loadingEl.classList.remove("d-none");
} else {
loadingEl.classList.add("d-none");
}
}
function showAnalysisError(message) {
const modal = document.getElementById("analysisModal");
if (!modal) return;
const errorEl = modal.querySelector("#analysisErrorBox");
const emptyEl = modal.querySelector("#analysisEmptyBox");
const listEl = modal.querySelector("#analysisList");
if (listEl) listEl.innerHTML = "";
if (emptyEl) emptyEl.classList.add("d-none");
if (errorEl) {
errorEl.textContent = message || "Error loading analyses";
errorEl.classList.remove("d-none");
}
}
function showAnalysisEmpty(message) {
const modal = document.getElementById("analysisModal");
if (!modal) return;
const errorEl = modal.querySelector("#analysisErrorBox");
const emptyEl = modal.querySelector("#analysisEmptyBox");
const listEl = modal.querySelector("#analysisList");
if (listEl) listEl.innerHTML = "";
if (errorEl) errorEl.classList.add("d-none");
if (emptyEl) {
emptyEl.textContent =
message || "No analyses found for this matrix";
emptyEl.classList.remove("d-none");
}
}
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function renderAnalysesList(analyses) {
const modal = document.getElementById("analysisModal");
if (!modal) return;
const errorEl = modal.querySelector("#analysisErrorBox");
const emptyEl = modal.querySelector("#analysisEmptyBox");
const listEl = modal.querySelector("#analysisList");
if (!listEl) return;
if (errorEl) errorEl.classList.add("d-none");
if (!Array.isArray(analyses) || analyses.length === 0) {
showAnalysisEmpty("No analyses found for this matrix");
return;
}
if (emptyEl) emptyEl.classList.add("d-none");
listEl.innerHTML = analyses
.map(function (item) {
const recordKey = item.RecordKey || "";
const analysisName =
item.NomeAnalisiTraduzione ||
item.NomeAnalisi ||
"Unnamed analysis";
const methodName = item.MetodoNome || "";
const selectable = item.SelezionabileSuWeb === true;
const accredited = item.Accreditato === true;
const level = item.Livello ?? "";
const searchText = (
analysisName +
" " +
methodName
).toLowerCase();
let badges = "";
if (selectable) {
badges += '<span class="badge bg-success">Web</span>';
} else {
badges += '<span class="badge bg-secondary">Not web</span>';
}
if (accredited) {
badges +=
'<span class="badge bg-info text-dark">Accredited</span>';
}
if (level !== "") {
badges +=
'<span class="badge bg-light text-dark border">Level ' +
escapeHtml(level) +
"</span>";
}
return `
<div
class="list-group-item analysis-analysis-item"
data-recordkey="${escapeHtml(recordKey)}"
data-analysis-name="${escapeHtml(analysisName)}"
data-analysis-method="${escapeHtml(methodName)}"
data-analysis-level="${escapeHtml(level)}"
data-web="${selectable ? "1" : "0"}"
data-accredited="${accredited ? "1" : "0"}"
data-search="${escapeHtml(searchText)}"
>
<div class="fw-semibold">${escapeHtml(analysisName)}</div>
${methodName ? `<div class="analysis-analysis-meta mt-1">${escapeHtml(methodName)}</div>` : ""}
<div class="analysis-analysis-badges mt-2">
${badges}
</div>
</div>
`;
})
.join("");
filterAnalysisList();
syncSelectedAnalysisRows();
}
function filterAnalysisList() {
const modal = document.getElementById("analysisModal");
if (!modal) return;
const webOnlyEl = modal.querySelector("#analysisWebOnly");
const searchEl = modal.querySelector("#analysisSearchInput");
const items = modal.querySelectorAll(".analysis-analysis-item");
const emptyEl = modal.querySelector("#analysisEmptyBox");
const errorEl = modal.querySelector("#analysisErrorBox");
const webOnly = webOnlyEl ? webOnlyEl.checked : false;
const searchValue = searchEl ? searchEl.value.trim().toLowerCase() : "";
let visibleCount = 0;
items.forEach((item) => {
const isWeb = item.getAttribute("data-web") === "1";
const searchText = (
item.getAttribute("data-search") || ""
).toLowerCase();
let visible = true;
if (webOnly && !isWeb) {
visible = false;
}
if (visible && searchValue && !searchText.includes(searchValue)) {
visible = false;
}
item.style.display = visible ? "" : "none";
if (visible) {
visibleCount++;
}
});
if (errorEl) {
errorEl.classList.add("d-none");
}
if (emptyEl) {
if (items.length === 0) {
emptyEl.textContent = "No analyses found for this matrix";
emptyEl.classList.remove("d-none");
} else if (visibleCount === 0) {
emptyEl.textContent = "No analyses match the current filters";
emptyEl.classList.remove("d-none");
} else {
emptyEl.classList.add("d-none");
}
}
}
function loadAnalysesByMatrix(matrixId) {
const modal = document.getElementById("analysisModal");
if (!modal) return;
if (!matrixId || matrixId === "NO_MATRIX") {
showAnalysisEmpty("No matrix selected");
return;
}
const listEl = modal.querySelector("#analysisList");
const errorEl = modal.querySelector("#analysisErrorBox");
const emptyEl = modal.querySelector("#analysisEmptyBox");
if (listEl) listEl.innerHTML = "";
if (errorEl) errorEl.classList.add("d-none");
if (emptyEl) {
emptyEl.textContent = "";
emptyEl.classList.add("d-none");
}
if (analysisLoadedCache[String(matrixId)]) {
renderAnalysesList(analysisLoadedCache[String(matrixId)]);
return;
}
setAnalysisLoadingState(true);
$.ajax({
url: "get_analisi_matrice_filter.php",
method: "GET",
dataType: "json",
data: {
id_matrice: matrixId,
},
})
.done(function (response) {
const analyses = Array.isArray(response.value)
? response.value
: [];
analysisLoadedCache[String(matrixId)] = analyses;
renderAnalysesList(analyses);
})
.fail(function (xhr) {
let message = "Error loading analyses";
if (xhr && xhr.responseJSON && xhr.responseJSON.error) {
message = xhr.responseJSON.error;
}
showAnalysisError(message);
})
.always(function () {
setAnalysisLoadingState(false);
});
}
function updateSelectedPartsInfo() {
const modal = document.getElementById("analysisModal");
if (!modal) return;
const checked = modal.querySelectorAll(
".analysis-part-checkbox:checked",
);
const ids = Array.from(checked).map((el) => el.value);
const countEl = modal.querySelector("#analysisSelectedPartsCount");
const idsEl = modal.querySelector("#analysisSelectedPartsIds");
if (countEl) {
countEl.textContent = `${ids.length} selected`;
}
if (idsEl) {
idsEl.textContent = ids.length ? ids.join(", ") : "-";
}
modal.querySelectorAll(".analysis-part-item").forEach((item) => {
const checkbox = item.querySelector(".analysis-part-checkbox");
if (checkbox && checkbox.checked) {
item.classList.add("part-checked");
} else {
item.classList.remove("part-checked");
}
});
}
function selectPartsByMatrix(matrixId, matrixLabel) {
const modal = document.getElementById("analysisModal");
if (!modal) return;
const partItems = modal.querySelectorAll(".analysis-part-item");
const matrixLabelEl = modal.querySelector("#analysisCurrentMatrix");
partItems.forEach((item) => {
const itemMatrixId = item.getAttribute("data-matrix-id");
const checkbox = item.querySelector(".analysis-part-checkbox");
item.classList.remove("matrix-active");
if (itemMatrixId === String(matrixId)) {
item.classList.add("matrix-active");
if (checkbox) checkbox.checked = true;
} else {
if (checkbox) checkbox.checked = false;
}
});
if (matrixLabelEl) {
matrixLabelEl.textContent = matrixLabel || "-";
}
analysisSelectedState = {};
const selectedPartIds = getSelectedPartIds();
selectedPartIds.forEach(function (partId) {
const assigned = Array.isArray(
analysisAssignedState[String(partId)],
)
? analysisAssignedState[String(partId)]
: [];
assigned.forEach(function (item) {
if (item.analysis_recordkey) {
analysisSelectedState[item.analysis_recordkey] = true;
}
});
});
updateSelectedPartsInfo();
loadAnalysesByMatrix(matrixId);
}
function clearAnalysisSelection() {
const modal = document.getElementById("analysisModal");
if (!modal) return;
modal.querySelectorAll(".analysis-matrix-item").forEach((item) => {
item.classList.remove("active");
});
modal.querySelectorAll(".analysis-part-item").forEach((item) => {
item.classList.remove("matrix-active", "part-checked");
});
modal.querySelectorAll(".analysis-part-checkbox").forEach((cb) => {
cb.checked = false;
});
const matrixLabelEl = modal.querySelector("#analysisCurrentMatrix");
if (matrixLabelEl) matrixLabelEl.textContent = "-";
const webOnlyEl = modal.querySelector("#analysisWebOnly");
if (webOnlyEl) webOnlyEl.checked = false;
const searchEl = modal.querySelector("#analysisSearchInput");
if (searchEl) searchEl.value = "";
const listEl = modal.querySelector("#analysisList");
if (listEl) listEl.innerHTML = "";
showAnalysisEmpty("Select a matrix to load analyses");
updateSelectedPartsInfo();
}
function initAnalysisModal() {
const modal = document.getElementById("analysisModal");
if (!modal) return;
analysisLoadedCache = {};
analysisSelectedState = {};
readInitialAssignedAnalyses();
renderAssignedAnalysesForSelectedParts();
modal.querySelectorAll(".analysis-matrix-item").forEach((btn) => {
btn.addEventListener("click", function () {
modal
.querySelectorAll(".analysis-matrix-item")
.forEach((x) => x.classList.remove("active"));
this.classList.add("active");
const matrixId = this.getAttribute("data-matrix-id");
const matrixLabel =
this.getAttribute("data-matrix-name") ||
this.textContent.trim();
selectPartsByMatrix(matrixId, matrixLabel);
});
});
modal.querySelectorAll(".analysis-part-checkbox").forEach((cb) => {
cb.addEventListener("change", function () {
updateSelectedPartsInfo();
});
});
const clearBtn = modal.querySelector("#analysisClearSelectionBtn");
if (clearBtn) {
clearBtn.addEventListener("click", function () {
clearAnalysisSelection();
});
}
const webOnlyEl = modal.querySelector("#analysisWebOnly");
if (webOnlyEl) {
webOnlyEl.addEventListener("change", function () {
filterAnalysisList();
});
}
const searchEl = modal.querySelector("#analysisSearchInput");
if (searchEl) {
searchEl.addEventListener("input", function () {
filterAnalysisList();
});
}
modal.addEventListener("click", function (e) {
const removeBtn = e.target.closest(".analysis-remove-btn");
if (removeBtn) {
e.preventDefault();
e.stopPropagation();
const partId = removeBtn.getAttribute("data-part-id");
const recordKey = removeBtn.getAttribute("data-recordkey");
deleteAnalysisAssociation(partId, recordKey, function () {
const stillUsed = Object.keys(analysisAssignedState).some(
function (pid) {
return (
Array.isArray(analysisAssignedState[pid]) &&
analysisAssignedState[pid].some(
function (item) {
return (
item.analysis_recordkey ===
recordKey
);
},
)
);
},
);
if (!stillUsed) {
delete analysisSelectedState[recordKey];
}
syncSelectedAnalysisRows();
});
return;
}
const row = e.target.closest(".analysis-analysis-item");
if (!row) return;
const selectedPartIds = getSelectedPartIds();
if (!selectedPartIds.length) {
alert("Select at least one part first");
return;
}
const payload = buildAnalysisPayloadFromRow(row);
if (!payload.analysis_recordkey) {
alert("Invalid analysis record key");
return;
}
const recordKey = payload.analysis_recordkey;
const alreadySelected = !!analysisSelectedState[recordKey];
if (alreadySelected) {
selectedPartIds.forEach(function (partId) {
deleteAnalysisAssociation(partId, recordKey);
});
delete analysisSelectedState[recordKey];
syncSelectedAnalysisRows();
return;
}
let pending = selectedPartIds.length;
let atLeastOneSaved = false;
selectedPartIds.forEach(function (partId) {
saveAnalysisAssociation(partId, payload, function (ok) {
if (ok) atLeastOneSaved = true;
pending--;
if (pending <= 0 && atLeastOneSaved) {
analysisSelectedState[recordKey] = true;
syncSelectedAnalysisRows();
}
});
});
});
const firstActiveMatrix = modal.querySelector(
".analysis-matrix-item.active",
);
if (firstActiveMatrix) {
const matrixId = firstActiveMatrix.getAttribute("data-matrix-id");
const matrixLabel =
firstActiveMatrix.getAttribute("data-matrix-name") ||
firstActiveMatrix.textContent.trim();
selectPartsByMatrix(matrixId, matrixLabel);
} else {
updateSelectedPartsInfo();
}
}
// OPEN ANALYSIS MODAL FROM PARTS MODAL BUTTON
$(document).on("click", ".open-analysis-modal-btn", function () {
const partsModal = document.getElementById("partsModal");
const iddatadb = $("#partsModal").data("iddatadb");
if (!iddatadb) {
console.error("iddatadb not found on #partsModal");
alert("iddatadb not found");
return;
}
$.ajax({
url: "modal_analysis.php",
method: "GET",
data: { iddatadb: iddatadb },
success: function (response) {
$("#analysisModalContainer").html(response);
const modalElement = document.getElementById("analysisModal");
if (!modalElement) {
console.error("Analysis modal not found: #analysisModal");
return;
}
let modal = bootstrap.Modal.getInstance(modalElement);
if (!modal) {
modal = new bootstrap.Modal(modalElement, {
backdrop: true,
keyboard: true,
focus: true,
});
}
loadAnalysisMatrixNames().always(function () {
initAnalysisModal();
modal.show();
});
},
error: function (xhr, status, error) {
console.error("Error loading analysis modal:", error);
alert("Error loading analysis modal: " + error);
},
});
});
// CLEANUP ON CLOSE
$(document).on("hidden.bs.modal", "#analysisModal", function () {
const modalElement = document.getElementById("analysisModal");
if (modalElement) {
const modal = bootstrap.Modal.getInstance(modalElement);
if (modal) modal.dispose();
}
$("#analysisModalContainer").empty();
// keep parts modal alive, but remove extra backdrop leftovers
$(".modal-backdrop").last().remove();
if ($("#partsModal").hasClass("show")) {
$("body").addClass("modal-open");
} else {
$("body").removeClass("modal-open").css("padding-right", "");
}
});
})();
+2
View File
@@ -30,6 +30,8 @@ if ((int)$stmt->fetchColumn() > 0) {
$fixedFields = [ $fixedFields = [
// fixed_field_key, data_type // fixed_field_key, data_type
['ClienteResponsabile', 'INT'], ['ClienteResponsabile', 'INT'],
['ClienteFornitore', 'INT'],
['ClienteAnalisi', 'INT'],
['MoltiplicatorePrezzo', 'INT'], ['MoltiplicatorePrezzo', 'INT'],
['AnagraficaCertestObject', 'INT'], ['AnagraficaCertestObject', 'INT'],
['AnagraficaCertestService', 'INT'], ['AnagraficaCertestService', 'INT'],
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
https://93.43.5.102/limsapi/api/odata/Matrice
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
2026-04-03 10:07:01 - Aggiornamento completato: 3198 record inseriti.
File diff suppressed because one or more lines are too long
+290
View File
@@ -0,0 +1,290 @@
<?php
require_once "class/VisualLimsApiClient.class.php";
include('include/headscript.php');
// Force HTML response
header('Content-Type: text/html; charset=UTF-8');
// Initialize variables
$error = null;
$result = null;
$entityType = $_GET['entity_type'] ?? 'CommessaWeb';
$entityId = isset($_GET['entity_id']) ? (int)$_GET['entity_id'] : 0;
$customExpand = trim($_GET['expand'] ?? '');
$rawEndpoint = null;
function h($value)
{
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}
function renderArrayAsTable(array $items)
{
if (empty($items)) {
echo '<div class="alert alert-secondary">No records found</div>';
return;
}
$firstRow = reset($items);
if (!is_array($firstRow)) {
echo '<pre class="bg-light p-3 border rounded">' . h(json_encode($items, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) . '</pre>';
return;
}
$headers = [];
foreach ($items as $row) {
if (is_array($row)) {
$headers = array_unique(array_merge($headers, array_keys($row)));
}
}
echo '<div class="table-responsive">';
echo '<table class="table table-striped table-bordered table-sm align-middle">';
echo '<thead class="table-dark"><tr>';
foreach ($headers as $header) {
echo '<th>' . h($header) . '</th>';
}
echo '</tr></thead><tbody>';
foreach ($items as $row) {
echo '<tr>';
foreach ($headers as $header) {
$value = $row[$header] ?? '';
if (is_array($value) || is_object($value)) {
$value = json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo '<td><pre style="white-space:pre-wrap; margin:0;">' . h($value) . '</pre></td>';
} else {
echo '<td>' . h($value) . '</td>';
}
}
echo '</tr>';
}
echo '</tbody></table>';
echo '</div>';
}
try {
if ($entityId > 0) {
$api = VisualLimsApiClient::getInstance();
// Default expands for better debugging
if ($customExpand !== '') {
$expand = $customExpand;
} else {
if ($entityType === 'CommessaWeb') {
$expand = implode(',', [
'CommesseCustomFields($expand=CustomField)',
'Campioni'
]);
} else {
$expand = implode(',', [
'CommesseCustomFields($expand=CustomField)',
'Campioni'
]);
}
}
$rawEndpoint = $entityType . '(' . $entityId . ')?$expand=' . $expand;
$result = $api->get($rawEndpoint);
}
} catch (Exception $e) {
$error = $e->getMessage();
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Debug Commessa API</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
background: #f5f7fb;
font-family: Arial, sans-serif;
}
.debug-card {
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.08);
padding: 20px;
margin-bottom: 20px;
}
.section-title {
font-size: 18px;
font-weight: 700;
margin-bottom: 15px;
}
pre {
background: #f8f9fa;
border: 1px solid #ddd;
border-radius: 8px;
padding: 12px;
white-space: pre-wrap;
word-break: break-word;
}
.label-box {
display: inline-block;
padding: 6px 10px;
border-radius: 8px;
background: #eef3ff;
color: #2446a8;
font-weight: 600;
margin-right: 8px;
margin-bottom: 8px;
}
.mini-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.mini-box {
background: #f8fafc;
border: 1px solid #e4e7eb;
border-radius: 10px;
padding: 12px;
}
.mini-box strong {
display: block;
margin-bottom: 6px;
color: #1f2937;
}
</style>
</head>
<body>
<div class="container py-4">
<div class="debug-card">
<h2 class="mb-3">Commessa / CommessaWeb API Inspector</h2>
<form method="GET" class="row g-3">
<div class="col-md-3">
<label class="form-label">Entity Type</label>
<select name="entity_type" class="form-select">
<option value="CommessaWeb" <?= $entityType === 'CommessaWeb' ? 'selected' : '' ?>>CommessaWeb</option>
<option value="Commessa" <?= $entityType === 'Commessa' ? 'selected' : '' ?>>Commessa</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Entity ID</label>
<input type="number" name="entity_id" class="form-control" value="<?= h($entityId) ?>" required>
</div>
<div class="col-md-5">
<label class="form-label">Expand</label>
<input type="text" name="expand" class="form-control" value="<?= h($customExpand) ?>" placeholder="CommesseCustomFields($expand=CustomField),Campioni">
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">Load Data</button>
</div>
</form>
</div>
<?php if ($rawEndpoint): ?>
<div class="debug-card">
<div class="section-title">Requested Endpoint</div>
<pre><?= h($rawEndpoint) ?></pre>
</div>
<?php endif; ?>
<?php if ($error): ?>
<div class="debug-card">
<div class="alert alert-danger mb-0">
<strong>API Error:</strong><br>
<?= h($error) ?>
</div>
</div>
<?php endif; ?>
<?php if ($result && is_array($result)): ?>
<div class="debug-card">
<div class="section-title">Main Information</div>
<div class="mini-grid">
<div class="mini-box">
<strong>ID</strong>
<?= h($result['IdCommessa'] ?? $result['IdCommessaWeb'] ?? '') ?>
</div>
<div class="mini-box">
<strong>Code</strong>
<?= h($result['CodiceCommessa'] ?? '') ?>
</div>
<div class="mini-box">
<strong>Cliente</strong>
<?= h($result['Cliente'] ?? '') ?>
</div>
<div class="mini-box">
<strong>SchemaCustomField</strong>
<?= h($result['SchemaCustomField'] ?? '') ?>
</div>
<div class="mini-box">
<strong>Richiedente</strong>
<?= h($result['Richiedente'] ?? '') ?>
</div>
<div class="mini-box">
<strong>Descrizione</strong>
<?= h($result['Descrizione'] ?? '') ?>
</div>
</div>
</div>
<div class="debug-card">
<div class="section-title">Direct Properties</div>
<pre><?= h(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) ?></pre>
</div>
<?php if (!empty($result['CommesseCustomFields']) && is_array($result['CommesseCustomFields'])): ?>
<div class="debug-card">
<div class="section-title">CommesseCustomFields</div>
<?php
$fieldsRows = [];
foreach ($result['CommesseCustomFields'] as $field) {
$fieldsRows[] = [
'IdCommesseCustomFields' => $field['IdCommesseCustomFields'] ?? '',
'Valore' => $field['Valore'] ?? '',
'CustomFieldId' => $field['CustomField']['IdCustomField'] ?? '',
'Label' => $field['CustomField']['Descrizione'] ?? ($field['CustomField']['Name'] ?? ''),
'Tipo' => $field['CustomField']['Tipo'] ?? '',
];
}
renderArrayAsTable($fieldsRows);
?>
</div>
<?php endif; ?>
<?php if (!empty($result['Campioni']) && is_array($result['Campioni'])): ?>
<div class="debug-card">
<div class="section-title">Campioni</div>
<?php renderArrayAsTable($result['Campioni']); ?>
</div>
<?php endif; ?>
<div class="debug-card">
<div class="section-title">Raw JSON</div>
<pre><?= h(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) ?></pre>
</div>
<?php elseif ($entityId > 0 && !$error): ?>
<div class="debug-card">
<div class="alert alert-warning mb-0">No data returned from API</div>
</div>
<?php endif; ?>
</div>
</body>
</html>
<!-- Example of use
debug_commessa_api.php?entity_type=CommessaWeb&entity_id=564779
debug_commessa_api.php?entity_type=Commessa&entity_id=12345
debug_commessa_api.php?entity_type=CommessaWeb&entity_id=564779&expand=CommesseCustomFields($expand=CustomField),Campioni
-->
+48
View File
@@ -0,0 +1,48 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
exit;
}
$partId = isset($_POST['part_id']) ? (int)$_POST['part_id'] : 0;
$analysisRecordkey = trim($_POST['analysis_recordkey'] ?? '');
if ($partId <= 0 || $analysisRecordkey === '') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Missing required data']);
exit;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("
DELETE FROM identification_parts_analyses
WHERE part_id = :part_id
AND analysis_recordkey = :analysis_recordkey
");
$stmt->execute([
':part_id' => $partId,
':analysis_recordkey' => $analysisRecordkey,
]);
echo json_encode([
'success' => true,
'message' => 'Association deleted'
]);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
+145 -71
View File
@@ -1,26 +1,27 @@
<?php include('include/headscript.php'); <?php include('include/headscript.php');
// Controlla se è stato passato un ID valido // Check if a valid ID was provided
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) { if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Invalid ID")); header("Location: templates_dashboard.php?status=error&message=" . urlencode("Invalid ID"));
exit; exit;
} }
$id = intval($_GET['id']); // Sanifica l'ID $id = intval($_GET['id']);
// Recupera il template dal database // Retrieve template from database
$db = DBHandlerSelect::getInstance(); $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection(); $pdo = $db->getConnection();
$stmt = $pdo->prepare("SELECT * FROM excel_templates WHERE id = ?"); $stmt = $pdo->prepare("SELECT * FROM excel_templates WHERE id = ?");
$stmt->execute([$id]); $stmt->execute([$id]);
$template = $stmt->fetch(PDO::FETCH_ASSOC); $template = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$template) { if (!$template) {
header("Location: template_dashboard.php?status=error&message=" . urlencode("Template not found")); header("Location: templates_dashboard.php?status=error&message=" . urlencode("Template not found"));
exit; exit;
} }
// Recupera tutte le routine dal database // Retrieve all routines
$stmt = $pdo->prepare("SELECT * FROM routine"); $stmt = $pdo->prepare("SELECT * FROM routine");
$stmt->execute(); $stmt->execute();
$routines = $stmt->fetchAll(PDO::FETCH_ASSOC); $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -34,7 +35,6 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" /> <link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" /> <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<?php include('cssinclude.php'); ?> <?php include('cssinclude.php'); ?>
<!-- Include jQuery prima di Select2 -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<title>Edit Template <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title> <title>Edit Template <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
@@ -44,19 +44,22 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<div class="wrapper"> <div class="wrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
<div class="page-wrapper"> <div class="page-wrapper">
<div class="page-content"> <div class="page-content">
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <div class="card-header">
<h5 class="mb-0">Update XLS Template</h5> <h5 class="mb-0">Update Template</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<p class="mb-2">Edit the following form in order to update the selected import XLS template</p> <p class="mb-2">Edit the following form in order to update the selected import template</p>
<p class="mb-2">Mandatory Fields</p> <p class="mb-2">Mandatory Fields</p>
<ul class="mb-0"> <ul class="mb-0">
<li>Template Name</li> <li>Template Name</li>
<li>Row Header and Column Header: where the title of the excel starts</li> <li>Source Type</li>
<li>Schema</li> <li>Schema and Client</li>
<li>Row Header and Column Header only for XLS templates</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -69,34 +72,44 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
</div> </div>
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="col-12"> <div class="col-12">
<form id="editTemplateForm" method="POST"> <form id="editTemplateForm" method="POST">
<input type="hidden" name="id" value="<?php echo $template['id']; ?>"> <input type="hidden" name="id" value="<?php echo (int)$template['id']; ?>">
<div class="mb-3"> <div class="mb-3">
<label class="form-label"><?= htmlspecialchars($templatename, ENT_QUOTES, 'UTF-8'); ?> *</label> <label class="form-label"><?= htmlspecialchars($templatename, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="text" name="name" class="form-control" value="<?php echo htmlspecialchars($template['name']); ?>" required> <input type="text" name="name" class="form-control" value="<?php echo htmlspecialchars($template['name'] ?? ''); ?>" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Source Type *</label>
<select name="source_type" id="sourceType" class="form-control" required>
<option value="XLS" <?php echo (($template['source_type'] ?? 'XLS') === 'XLS') ? 'selected' : ''; ?>>XLS</option>
<option value="API" <?php echo (($template['source_type'] ?? 'XLS') === 'API') ? 'selected' : ''; ?>>API</option>
</select>
<small class="text-muted">Choose the source used by this template</small>
</div>
<div class="mb-3" id="headerRowWrapper">
<label class="form-label"><?= htmlspecialchars($rowheader, ENT_QUOTES, 'UTF-8'); ?> *</label> <label class="form-label"><?= htmlspecialchars($rowheader, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="number" name="header_row" class="form-control" value="<?php echo $template['header_row']; ?>" required> <input type="number" name="header_row" id="headerRow" class="form-control" value="<?php echo htmlspecialchars($template['header_row'] ?? ''); ?>">
</div> </div>
<div class="mb-3"> <div class="mb-3" id="startColumnWrapper">
<label class="form-label"><?= htmlspecialchars($columnheader, ENT_QUOTES, 'UTF-8'); ?> *</label> <label class="form-label"><?= htmlspecialchars($columnheader, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="text" name="start_column" class="form-control" value="<?php echo htmlspecialchars($template['start_column']); ?>" required> <input type="text" name="start_column" id="startColumn" class="form-control" value="<?php echo htmlspecialchars($template['start_column'] ?? ''); ?>">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label"><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></label> <label class="form-label"><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></label>
<textarea name="description" class="form-control"><?php echo htmlspecialchars($template['description']); ?></textarea> <textarea name="description" class="form-control"><?php echo htmlspecialchars($template['description'] ?? ''); ?></textarea>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label"><?= htmlspecialchars($desttable, ENT_QUOTES, 'UTF-8'); ?> *</label> <label class="form-label"><?= htmlspecialchars($desttable, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="text" name="target_table" class="form-control" value="<?php echo htmlspecialchars($template['target_table']); ?>" readonly required> <input type="text" name="target_table" class="form-control" value="<?php echo htmlspecialchars($template['target_table'] ?? 'datadb'); ?>" readonly required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -110,12 +123,12 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Button Background Color</label> <label class="form-label">Button Background Color</label>
<input type="color" name="button_bg_color" class="form-control" value="<?php echo htmlspecialchars($template['button_bg_color'] ?? '#007bff'); ?>"> <input type="color" name="button_bg_color" class="form-control form-control-color" value="<?php echo htmlspecialchars($template['button_bg_color'] ?? '#007bff'); ?>">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Button Text Color</label> <label class="form-label">Button Text Color</label>
<input type="color" name="button_text_color" class="form-control" value="<?php echo htmlspecialchars($template['button_text_color'] ?? '#ffffff'); ?>"> <input type="color" name="button_text_color" class="form-control form-control-color" value="<?php echo htmlspecialchars($template['button_text_color'] ?? '#ffffff'); ?>">
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -128,7 +141,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<select name="client_id" id="clientSelect" class="form-control" required> <select name="client_id" id="clientSelect" class="form-control" required>
<option value="">Select a client...</option> <option value="">Select a client...</option>
</select> </select>
<span id="clientLoadingStatus" class="text-muted" style="margin-left: 10px; display: none;">Recupero clienti in corso...</span> <span id="clientLoadingStatus" class="text-muted" style="margin-left: 10px; display: none;">Loading clients...</span>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -136,7 +149,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<select name="schema_id" id="schemaSelect" class="form-control" required> <select name="schema_id" id="schemaSelect" class="form-control" required>
<option value="">Select a schema...</option> <option value="">Select a schema...</option>
</select> </select>
<span id="schemaLoadingStatus" class="text-muted" style="margin-left: 10px; display: none;">Caricamento schemi in corso...</span> <span id="schemaLoadingStatus" class="text-muted" style="margin-left: 10px; display: none;">Loading schemas...</span>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -144,11 +157,12 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<select name="idroutine" id="routineSelect" class="form-control"> <select name="idroutine" id="routineSelect" class="form-control">
<option value="">Select a routine...</option> <option value="">Select a routine...</option>
<?php foreach ($routines as $routine): ?> <?php foreach ($routines as $routine): ?>
<option value="<?php echo $routine['idroutine']; ?>" <?php echo ($template['idroutine'] ?? '') == $routine['idroutine'] ? 'selected' : ''; ?>> <option value="<?php echo $routine['idroutine']; ?>" <?php echo (($template['idroutine'] ?? '') == $routine['idroutine']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($routine['name']); ?> <?php echo htmlspecialchars($routine['name']); ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<div id="routineDetails" class="mt-2" style="display: none;"> <div id="routineDetails" class="mt-2" style="display: none;">
<h6>Routine Details</h6> <h6>Routine Details</h6>
<p><strong>Name:</strong> <span id="routineName"></span></p> <p><strong>Name:</strong> <span id="routineName"></span></p>
@@ -166,8 +180,10 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="overlay toggle-icon"></div> <div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a> <a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?> <?php include('include/footer.php'); ?>
@@ -175,9 +191,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<script> <script>
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
// Verifica che jQuery sia caricato
if (typeof jQuery === 'undefined') { if (typeof jQuery === 'undefined') {
alert("Errore: jQuery non è caricato. Contatta l'amministratore."); alert("Error: jQuery is not loaded.");
return; return;
} }
@@ -192,12 +207,15 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const routineAction2 = document.getElementById("routineAction2"); const routineAction2 = document.getElementById("routineAction2");
const routineAction3 = document.getElementById("routineAction3"); const routineAction3 = document.getElementById("routineAction3");
if (!form || !clientLoadingStatus || !schemaLoadingStatus || !routineSelect || !routineDetails) { const sourceType = document.getElementById("sourceType");
alert("Errore: Uno o più elementi della pagina non sono stati trovati. Contatta l'amministratore."); const headerRowWrapper = document.getElementById("headerRowWrapper");
return; const startColumnWrapper = document.getElementById("startColumnWrapper");
} const headerRow = document.getElementById("headerRow");
const startColumn = document.getElementById("startColumn");
const selectedClientId = <?php echo json_encode((int)($template['idclient'] ?? 0)); ?>;
const selectedSchemaId = <?php echo json_encode((int)($template['idschema'] ?? 0)); ?>;
// Inizializza Select2
$('#clientSelect').select2({ $('#clientSelect').select2({
placeholder: "Search for a client...", placeholder: "Search for a client...",
allowClear: true allowClear: true
@@ -213,108 +231,164 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
allowClear: true allowClear: true
}); });
// Carica i clienti function updateSourceFields() {
const selectedSource = sourceType.value;
if (selectedSource === 'API') {
headerRowWrapper.style.opacity = '0.6';
startColumnWrapper.style.opacity = '0.6';
headerRow.required = false;
startColumn.required = false;
headerRow.disabled = true;
startColumn.disabled = true;
} else {
headerRowWrapper.style.opacity = '1';
startColumnWrapper.style.opacity = '1';
headerRow.required = true;
startColumn.required = true;
headerRow.disabled = false;
startColumn.disabled = false;
}
}
sourceType.addEventListener('change', updateSourceFields);
updateSourceFields();
async function loadClients() { async function loadClients() {
try { try {
clientLoadingStatus.style.display = 'inline'; clientLoadingStatus.style.display = 'inline';
clientLoadingStatus.textContent = 'Recupero clienti in corso...'; clientLoadingStatus.textContent = 'Loading clients...';
const response = await fetch("get_clienti.php", { const response = await fetch("get_clienti.php", {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
} }
}); });
const data = await response.json(); const data = await response.json();
if (!response.ok) throw new Error(data.error || `Errore HTTP: ${response.status}`);
if (!response.ok) {
throw new Error(data.error || `HTTP error: ${response.status}`);
}
const select = document.getElementById("clientSelect"); const select = document.getElementById("clientSelect");
select.innerHTML = '<option value="">Select a client...</option>'; select.innerHTML = '<option value="">Select a client...</option>';
data.value.forEach(client => { data.value.forEach(client => {
const nome = client.Nominativo || "Nome non disponibile"; const nome = client.Nominativo || "Name not available";
const id = client.IdCliente || "ID non disponibile"; const id = client.IdCliente || "";
const option = new Option(`${nome.trim()} (ID: ${id})`, id); const option = new Option(`${nome.trim()} (ID: ${id})`, id);
if (parseInt(id) === parseInt(<?php echo json_encode($template['idclient'] ?? 0); ?>)) {
if (parseInt(id) === parseInt(selectedClientId)) {
option.selected = true; option.selected = true;
} }
select.add(option); select.add(option);
}); });
$(select).trigger('change'); $(select).trigger('change');
clientLoadingStatus.textContent = "Clienti caricati."; clientLoadingStatus.textContent = "Clients loaded.";
} catch (error) { } catch (error) {
clientLoadingStatus.textContent = "Errore nel caricamento."; clientLoadingStatus.textContent = "Loading error.";
Swal.fire({ Swal.fire({
title: "Errore!", title: "Error!",
text: "Impossibile caricare i clienti: " + error.message, text: "Unable to load clients: " + error.message,
icon: "error", icon: "error",
confirmButtonText: "OK" confirmButtonText: "OK"
}); });
} finally { } finally {
setTimeout(() => clientLoadingStatus.style.display = 'none', 2000); setTimeout(() => clientLoadingStatus.style.display = 'none', 1500);
} }
} }
// Carica gli schemi
async function loadSchemas() { async function loadSchemas() {
try { try {
schemaLoadingStatus.style.display = 'inline'; schemaLoadingStatus.style.display = 'inline';
schemaLoadingStatus.textContent = 'Caricamento schemi in corso...'; schemaLoadingStatus.textContent = 'Loading schemas...';
const response = await fetch("get_schemi.php", { const response = await fetch("get_schemi.php", {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
} }
}); });
const data = await response.json(); const data = await response.json();
if (!response.ok) throw new Error(data.error || `Errore HTTP: ${response.status}`);
if (!response.ok) {
throw new Error(data.error || `HTTP error: ${response.status}`);
}
const select = document.getElementById("schemaSelect"); const select = document.getElementById("schemaSelect");
select.innerHTML = '<option value="">Select a schema...</option>'; select.innerHTML = '<option value="">Select a schema...</option>';
data.value.forEach(schema => {
const nome = schema.Nome || "Nome non disponibile"; const sortedSchemas = [...data.value].sort((a, b) => {
const id = schema.IdSchemaCustomFields || "ID non disponibile"; const nomeA = (a.Nome || "").trim().toLowerCase();
const nomeB = (b.Nome || "").trim().toLowerCase();
return nomeA.localeCompare(nomeB, 'it', {
sensitivity: 'base'
});
});
sortedSchemas.forEach(schema => {
const nome = schema.Nome || "Name not available";
const id = schema.IdSchemaCustomFields || "";
const option = new Option(`${nome.trim()} (ID: ${id})`, id); const option = new Option(`${nome.trim()} (ID: ${id})`, id);
if (parseInt(id) === parseInt(<?php echo json_encode($template['idschema'] ?? 0); ?>)) {
if (parseInt(id) === parseInt(selectedSchemaId)) {
option.selected = true; option.selected = true;
} }
select.add(option); select.add(option);
}); });
$(select).trigger('change'); $(select).trigger('change');
schemaLoadingStatus.textContent = "Schemi caricati."; schemaLoadingStatus.textContent = "Schemas loaded.";
} catch (error) { } catch (error) {
schemaLoadingStatus.textContent = "Errore nel caricamento."; schemaLoadingStatus.textContent = "Loading error.";
Swal.fire({ Swal.fire({
title: "Errore!", title: "Error!",
text: "Impossibile caricare gli schemi: " + error.message, text: "Unable to load schemas: " + error.message,
icon: "error", icon: "error",
confirmButtonText: "OK" confirmButtonText: "OK"
}); });
} finally { } finally {
setTimeout(() => schemaLoadingStatus.style.display = 'none', 2000); setTimeout(() => schemaLoadingStatus.style.display = 'none', 1500);
} }
} }
// Carica i dati
async function loadData() { async function loadData() {
try { try {
await loadClients(); await loadClients();
await loadSchemas(); await loadSchemas();
} catch (error) { } catch (error) {
Swal.fire({ Swal.fire({
title: "Errore!", title: "Error!",
text: "Errore nel caricamento dei dati: " + error.message, text: "Error while loading data: " + error.message,
icon: "error", icon: "error",
confirmButtonText: "OK" confirmButtonText: "OK"
}); });
} }
} }
loadData(); loadData();
// Routine dettagli
const routines = <?php echo json_encode($routines); ?>; const routines = <?php echo json_encode($routines); ?>;
function updateRoutineDetails() { function updateRoutineDetails() {
const selectedId = routineSelect.value; const selectedId = routineSelect.value;
routineDetails.style.display = selectedId ? 'block' : 'none'; routineDetails.style.display = selectedId ? 'block' : 'none';
if (selectedId) { if (selectedId) {
const routine = routines.find(r => r.idroutine == selectedId); const routine = routines.find(r => r.idroutine == selectedId);
if (routine) { if (routine) {
routineName.textContent = routine.name || 'N/A'; routineName.textContent = routine.name || 'N/A';
routineDescription.textContent = routine.description || 'N/A'; routineDescription.textContent = routine.description || 'N/A';
@@ -336,10 +410,10 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
routineAction3.textContent = ''; routineAction3.textContent = '';
} }
} }
routineSelect.addEventListener('change', updateRoutineDetails);
updateRoutineDetails(); // Inizializza dettagli se una routine è preselezionata
// Submit del form routineSelect.addEventListener('change', updateRoutineDetails);
updateRoutineDetails();
form.addEventListener("submit", function(e) { form.addEventListener("submit", function(e) {
e.preventDefault(); e.preventDefault();
@@ -351,8 +425,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!clientId) { if (!clientId) {
Swal.fire({ Swal.fire({
title: "Errore!", title: "Error!",
text: "Per favore seleziona un cliente.", text: "Please select a client.",
icon: "error", icon: "error",
confirmButtonText: "OK" confirmButtonText: "OK"
}); });
@@ -373,8 +447,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!schemaId) { if (!schemaId) {
Swal.fire({ Swal.fire({
title: "Errore!", title: "Error!",
text: "Per favore seleziona uno schema.", text: "Please select a schema.",
icon: "error", icon: "error",
confirmButtonText: "OK" confirmButtonText: "OK"
}); });
@@ -387,10 +461,10 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const nameMatch = optionText.match(/^(.+?)(?:\s*\(ID:\s*\d+\))?$/); const nameMatch = optionText.match(/^(.+?)(?:\s*\(ID:\s*\d+\))?$/);
schemaName = nameMatch ? nameMatch[1].trim() : optionText; schemaName = nameMatch ? nameMatch[1].trim() : optionText;
} }
formData.append("idschema", schemaId); formData.append("idschema", schemaId);
formData.append("schemaname", schemaName); formData.append("schemaname", schemaName);
// Aggiungi idroutine
const routineId = routineSelect.value; const routineId = routineSelect.value;
formData.append("idroutine", routineId); formData.append("idroutine", routineId);
@@ -402,8 +476,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
.then(data => { .then(data => {
if (data.success) { if (data.success) {
Swal.fire({ Swal.fire({
title: "Successo!", title: "Success!",
text: "Template aggiornato con successo!", text: "Template updated successfully!",
icon: "success", icon: "success",
confirmButtonText: "OK" confirmButtonText: "OK"
}).then(() => { }).then(() => {
@@ -411,17 +485,17 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
}); });
} else { } else {
Swal.fire({ Swal.fire({
title: "Errore!", title: "Error!",
text: data.message, text: data.message,
icon: "error", icon: "error",
confirmButtonText: "OK" confirmButtonText: "OK"
}); });
} }
}) })
.catch(error => { .catch(() => {
Swal.fire({ Swal.fire({
title: "Errore!", title: "Error!",
text: "Si è verificato un errore imprevisto.", text: "An unexpected error occurred.",
icon: "error", icon: "error",
confirmButtonText: "OK" confirmButtonText: "OK"
}); });
+14
View File
@@ -314,3 +314,17 @@
2026-02-26 15:01:32 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"} 2026-02-26 15:01:32 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-28 21:01:27 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"} 2026-02-28 21:01:27 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-03-01 19:11:58 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"} 2026-03-01 19:11:58 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-03-18 15:51:45 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-03-19 09:50:34 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-03-25 14:13:34 - Autenticazione fallita: HTTP 503, Errore cURL: , Risposta: The service is unavailable.
2026-03-25 14:13:34 - Autenticazione fallita: HTTP 503, Errore cURL: , Risposta: The service is unavailable.
2026-03-26 10:57:18 [AnagraficaCertestObject] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:57:21 [AnagraficaCertestObject] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:57:21 [AnagraficaCertestService] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:57:21 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:57:36 [AnagraficaCertestObject] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:57:36 [AnagraficaCertestService] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:57:36 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:57:41 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:57:56 [AnagraficaCertestObject] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:58:11 [AnagraficaCertestService] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
+555
View File
@@ -0,0 +1,555 @@
/**
* exportLims_gridData.js — Export to LIMS using gridData (for imported.php)
*
* Replaces export_to_lims.js for pages that use gridData instead of DOM-rendered rows.
* Single export + batch export (Export All) with validation.
*/
(function () {
'use strict';
let pendingConfirmHandler = null;
let batchRunning = false;
Object.defineProperty(window, "batchRunning", { get: () => batchRunning });
let batchCancelled = false;
let pendingBatchConfirmHandler = null;
// ── Helpers ──────────────────────────────────────────────────────────
function cleanupBackdrop() {
document.querySelectorAll(".modal-backdrop").forEach(b => b.remove());
document.body.classList.remove("modal-open");
document.body.style.paddingRight = "";
const overlay = document.querySelector(".overlay.toggle-icon");
if (overlay) overlay.style.display = "none";
}
function getGridRow(iddatadb) {
return document.querySelector(`.grid-row[data-id="${iddatadb}"]`);
}
function getRowIndexByIddatadb(iddatadb) {
return (window.gridData || []).findIndex(r => String(r.iddatadb) === String(iddatadb));
}
// ── Validation ──────────────────────────────────────────────────────
async function validateRows(rowsToValidate) {
const response = await fetch("validate_export.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rows: rowsToValidate }),
});
if (!response.ok) throw new Error(`Validation HTTP error: ${response.status}`);
return response.json();
}
function clearValidationErrors() {
// Clear from gridData
(window.gridData || []).forEach(row => { delete row._validationErrors; delete row._exportError; });
document.querySelectorAll(".grid-cell.validation-error").forEach(cell => {
cell.classList.remove("validation-error");
cell.querySelectorAll(".input-validation-error").forEach(el => el.classList.remove("input-validation-error"));
const tooltip = cell.querySelector(".validation-tooltip");
if (tooltip) tooltip.remove();
});
document.querySelectorAll(".grid-row.validation-row-error").forEach(row => row.classList.remove("validation-row-error"));
clearAllRowErrors();
}
function showValidationErrors(gridRow, iddatadb, errors) {
// Store in gridData for re-render persistence
const idx = getRowIndexByIddatadb(iddatadb);
if (idx >= 0) window.gridData[idx]._validationErrors = errors;
if (!gridRow) return;
gridRow.classList.add("validation-row-error");
const messages = [];
errors.forEach(err => {
messages.push(err.message);
if (!err.field) return;
let cell = null;
if (err.field.startsWith("field_label:")) {
const label = err.field.substring("field_label:".length);
const headers = document.querySelectorAll(".grid-header");
let targetIndex = null;
headers.forEach(h => {
if (h.textContent.trim() === label) targetIndex = h.getAttribute("data-index");
});
if (targetIndex) {
cell = gridRow.querySelector(`.grid-cell[data-index="${targetIndex}"]`);
}
} else {
cell = gridRow.querySelector(`.grid-cell[data-col="${err.field}"]`);
}
if (cell) {
cell.classList.add("validation-error");
cell.querySelectorAll("input, select").forEach(el => el.classList.add("input-validation-error"));
let tooltip = cell.querySelector(".validation-tooltip");
if (!tooltip) {
tooltip = document.createElement("div");
tooltip.className = "validation-tooltip";
cell.appendChild(tooltip);
}
tooltip.textContent = err.message;
}
});
showRowError(gridRow, iddatadb, messages.join("\n"));
}
// ── Send export ─────────────────────────────────────────────────────
async function sendExport(iddatadb, batchUuid) {
const formData = new FormData();
formData.append("iddatadb", iddatadb);
if (batchUuid) formData.append("batch_uuid", batchUuid);
const response = await fetch("export_to_lims.php", { method: "POST", body: formData });
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
if (data.success) {
// Update gridData
const idx = getRowIndexByIddatadb(iddatadb);
if (idx >= 0) {
window.gridData[idx].status = 'l';
window.gridData[idx].commessaweb = data.commessaweb;
}
// Update visible DOM row
const gridRow = getGridRow(iddatadb);
if (gridRow) {
const statusBadge = gridRow.querySelector('.grid-cell[data-col="status"] .status-badge');
if (statusBadge) {
statusBadge.classList.remove("status-i", "status-P");
statusBadge.classList.add("status-l");
statusBadge.textContent = "To LIMS";
}
const statusCell = gridRow.querySelector('.grid-cell[data-col="status"]');
if (statusCell && data.commessaweb) {
let cwSpan = statusCell.querySelector(".commessaweb-code");
if (!cwSpan) {
cwSpan = document.createElement("span");
cwSpan.className = "commessaweb-code";
cwSpan.style.cssText = "display:block; font-size:0.75em; color:#555; margin-top:2px;";
statusCell.appendChild(cwSpan);
}
cwSpan.textContent = data.commessaweb;
}
const exportBtn = gridRow.querySelector(".export-lims-btn");
if (exportBtn) {
exportBtn.disabled = true;
exportBtn.style.background = "#ccc";
exportBtn.style.cursor = "not-allowed";
exportBtn.style.opacity = "0.5";
}
}
}
return data;
}
// ── Show result modal ───────────────────────────────────────────────
function showExportResult(data) {
const el = document.getElementById("exportResponseModal");
if (!el) return;
const modal = new bootstrap.Modal(el, { keyboard: false });
const msg = document.getElementById("exportResponseMessage");
const label = document.getElementById("exportResponseModalLabel");
if (data.success) {
msg.innerHTML = `${data.message.replace(/\n/g, "<br>")}` +
`<br>ID CommessaWeb: ${data.idcommessaweb}` +
`<br>Codice CommessaWeb: ${data.commessaweb}` +
(data.totalPhotos > 0 ? `<br>Foto: ${data.totalPhotos}` : "");
label.textContent = "Esportazione Completata";
} else {
msg.textContent = `Errore: ${data.message}`;
label.textContent = "Errore Esportazione";
}
modal.show();
el.addEventListener("hidden.bs.modal", cleanupBackdrop, { once: true });
}
// ── Row UI helpers ──────────────────────────────────────────────────
function setRowExporting(row, active) {
if (!row) return;
const btnCell = row.querySelector(".button-cell");
if (active) {
row.classList.remove("batch-disabled");
row.classList.add("batch-exporting");
if (btnCell) {
btnCell.querySelectorAll(".action-btn").forEach(b => { b.dataset.prevDisplay = b.style.display; b.style.display = "none"; });
const spinner = document.createElement("span");
spinner.className = "batch-row-spinner";
spinner.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Exporting...';
btnCell.appendChild(spinner);
}
} else {
row.classList.remove("batch-exporting");
if (btnCell) {
const spinner = btnCell.querySelector(".batch-row-spinner");
if (spinner) spinner.remove();
btnCell.querySelectorAll(".action-btn").forEach(b => { b.style.display = b.dataset.prevDisplay || ""; delete b.dataset.prevDisplay; });
}
}
}
function showRowError(row, iddatadb, message) {
if (!row) return;
row.classList.add("batch-row-error");
const btnCell = row.querySelector(".button-cell");
if (btnCell) {
const old = btnCell.querySelector(".batch-error-msg");
if (old) old.remove();
const errorEl = document.createElement("div");
errorEl.className = "batch-error-msg";
errorEl.textContent = "Warning — click for details";
errorEl.addEventListener("click", () => {
document.getElementById("exportResponseMessage").innerHTML = message.replace(/\n/g, "<br>");
document.getElementById("exportResponseModalLabel").textContent = "Error (id: " + iddatadb + ")";
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
});
btnCell.appendChild(errorEl);
}
}
function clearAllRowErrors() {
document.querySelectorAll(".grid-row.batch-row-error").forEach(row => {
row.classList.remove("batch-row-error");
const msg = row.querySelector(".batch-error-msg");
if (msg) msg.remove();
});
}
function disableAllRowButtons() {
document.querySelectorAll(".grid-row[data-id]").forEach(row => row.classList.add("batch-disabled"));
const toggle = document.querySelector(".actions-dropdown .dropdown-toggle");
if (toggle) { toggle.disabled = true; toggle.style.opacity = "0.5"; toggle.style.pointerEvents = "none"; }
}
function enableAllRowButtons() {
document.querySelectorAll(".grid-row[data-id]").forEach(row => row.classList.remove("batch-disabled"));
const toggle = document.querySelector(".actions-dropdown .dropdown-toggle");
if (toggle) { toggle.disabled = false; toggle.style.opacity = ""; toggle.style.pointerEvents = ""; }
}
// ── Single row export: validate → confirm → send ────────────────────
function startExportConfirmFlow(iddatadb, rowIndex) {
const gridRow = getGridRow(iddatadb);
clearValidationErrors();
if (gridRow) {
setRowExporting(gridRow, true);
const spinner = gridRow.querySelector(".batch-row-spinner");
if (spinner) spinner.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Validating...';
}
validateRows([{ iddatadb: parseInt(iddatadb), index: rowIndex }])
.then(validationData => {
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
if (!validationData.success) {
showExportResult({ success: false, message: validationData.message || "Validation error" });
return;
}
const result = validationData.results[rowIndex];
if (result && !result.valid) {
if (gridRow) showValidationErrors(gridRow, iddatadb, result.errors);
return;
}
showConfirmAndExport(iddatadb, rowIndex);
})
.catch(error => {
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
showExportResult({ success: false, message: "Validation error: " + error.message });
});
}
function showConfirmAndExport(iddatadb, rowIndex) {
const confirmModalElement = document.getElementById("exportConfirmModal");
if (!confirmModalElement) return;
const confirmModal = new bootstrap.Modal(confirmModalElement, { keyboard: false });
document.getElementById("exportIddatadb").textContent = iddatadb;
confirmModal.show();
const confirmBtn = document.getElementById("exportConfirmBtn");
if (!confirmBtn) { confirmModal.hide(); return; }
const confirmHandler = async () => {
pendingConfirmHandler = null;
confirmModal.hide();
const gridRow = getGridRow(iddatadb);
if (gridRow) setRowExporting(gridRow, true);
try {
const data = await sendExport(iddatadb);
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
if (!data.success) showRowError(gridRow, iddatadb, data.message || "Unknown error");
showExportResult(data);
} catch (error) {
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
showRowError(gridRow, iddatadb, error.message);
showExportResult({ success: false, message: error.message });
}
};
if (pendingConfirmHandler) confirmBtn.removeEventListener("click", pendingConfirmHandler);
pendingConfirmHandler = confirmHandler;
confirmBtn.addEventListener("click", confirmHandler, { once: true });
}
// ── Single row click (event delegation) ─────────────────────────────
$(document).on('click', '.export-lims-btn', function (e) {
e.preventDefault();
if (batchRunning) return;
const iddatadb = this.dataset.iddatadb;
const rowIndex = parseInt(this.dataset.row);
const gridRow = getGridRow(iddatadb);
// Check unsaved changes for this row
const dataRow = window.gridData?.[rowIndex];
if (dataRow && dataRow._dirty) {
const unsavedModal = new bootstrap.Modal(document.getElementById("exportUnsavedModal"), { keyboard: false });
unsavedModal.show();
document.getElementById("saveAndExportBtn")?.addEventListener("click", () => {
unsavedModal.hide();
// Save first, then export
const formData = window.buildSavePayload(rowIndex);
fetch('save_edited_row.php', { method: 'POST', body: formData })
.then(r => r.json())
.then(result => {
if (result.success) {
dataRow._dirty = false;
startExportConfirmFlow(iddatadb, rowIndex);
} else {
alert('Save failed: ' + result.message);
}
});
}, { once: true });
return;
}
startExportConfirmFlow(iddatadb, rowIndex);
});
// ── Batch export (Export All) ───────────────────────────────────────
function collectEligibleRows() {
// Read from gridData, not DOM
const eligible = [];
(window.gridData || []).forEach((row, index) => {
if (row.status !== 'l') {
eligible.push({ iddatadb: row.iddatadb, index, row: getGridRow(row.iddatadb) });
}
});
return eligible;
}
function hasUnsavedChanges() {
return (window.gridData || []).some(r => r._dirty);
}
async function validateAndFilter(eligibleRows) {
const rowsToValidate = eligibleRows.map(({ iddatadb, index }) => ({
iddatadb: parseInt(iddatadb),
index,
}));
const validationData = await validateRows(rowsToValidate);
if (!validationData.success) throw new Error(validationData.message || "Validation error");
const validRows = [];
let invalidCount = 0;
for (const item of eligibleRows) {
const result = validationData.results[item.index];
if (result && !result.valid) {
if (item.row) showValidationErrors(item.row, item.iddatadb, result.errors);
invalidCount++;
} else {
validRows.push(item);
}
}
return { validRows, invalidCount };
}
function showValidationSpinner(show) {
const bar = document.getElementById("batchExportBar");
const statusEl = document.getElementById("batchExportStatus");
const cancelBtn = document.getElementById("exportBatchCancelBtn");
if (show) {
if (bar) bar.style.display = "";
if (statusEl) statusEl.textContent = "Validating...";
if (cancelBtn) cancelBtn.style.display = "none";
} else {
if (bar) bar.style.display = "none";
if (cancelBtn) cancelBtn.style.display = "";
}
}
function showBatchConfirm(eligibleRows) {
clearValidationErrors();
showValidationSpinner(true);
validateAndFilter(eligibleRows)
.then(({ validRows, invalidCount }) => {
showValidationSpinner(false);
if (validRows.length === 0) {
document.getElementById("exportResponseMessage").innerHTML =
`No valid rows for export.<br><strong>${invalidCount}</strong> rows with validation errors.`;
document.getElementById("exportResponseModalLabel").textContent = "Validation Failed";
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
return;
}
const confirmModal = new bootstrap.Modal(document.getElementById("exportBatchConfirmModal"), { keyboard: false });
let countText = String(validRows.length);
if (invalidCount > 0) countText += ` (${invalidCount} excluded due to errors)`;
document.getElementById("exportBatchCount").textContent = countText;
confirmModal.show();
const confirmBtn = document.getElementById("exportBatchConfirmBtn");
if (pendingBatchConfirmHandler) confirmBtn.removeEventListener("click", pendingBatchConfirmHandler);
pendingBatchConfirmHandler = () => {
pendingBatchConfirmHandler = null;
confirmModal.hide();
startBatchExport(validRows);
};
confirmBtn.addEventListener("click", pendingBatchConfirmHandler, { once: true });
})
.catch(error => {
showValidationSpinner(false);
document.getElementById("exportResponseMessage").textContent = "Validation error: " + error.message;
document.getElementById("exportResponseModalLabel").textContent = "Validation Error";
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
});
}
$(document).on('click', '.export-all-lims-btn', function (e) {
e.preventDefault();
if (batchRunning) return;
if (hasUnsavedChanges()) {
const unsavedModal = new bootstrap.Modal(document.getElementById("exportBatchUnsavedModal"), { keyboard: false });
unsavedModal.show();
document.getElementById("batchSaveAndExportBtn")?.addEventListener("click", () => {
unsavedModal.hide();
// Trigger save all first — listen for completion
alert("Please Save All first, then Export All.");
}, { once: true });
return;
}
const eligibleRows = collectEligibleRows();
if (eligibleRows.length === 0) {
document.getElementById("exportResponseMessage").textContent = "All rows already exported to LIMS.";
document.getElementById("exportResponseModalLabel").textContent = "Export All";
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
return;
}
showBatchConfirm(eligibleRows);
});
function startBatchExport(eligibleRows) {
batchCancelled = false;
batchRunning = true;
const batchUuid = crypto.randomUUID();
const total = eligibleRows.length;
let processed = 0, succeeded = 0, failed = 0;
disableAllRowButtons();
const bar = document.getElementById("batchExportBar");
const statusEl = document.getElementById("batchExportStatus");
const cancelBtn = document.getElementById("exportBatchCancelBtn");
if (bar) bar.style.display = "";
if (cancelBtn) cancelBtn.disabled = false;
if (statusEl) statusEl.textContent = `Exporting 0 / ${total}...`;
cancelBtn?.addEventListener("click", () => {
batchCancelled = true;
if (statusEl) statusEl.textContent = "Cancelling... (waiting for current row)";
if (cancelBtn) cancelBtn.disabled = true;
}, { once: true });
(async () => {
for (let i = 0; i < eligibleRows.length; i++) {
if (batchCancelled) break;
const { iddatadb, row } = eligibleRows[i];
if (statusEl) statusEl.textContent = `Exporting ${processed + 1} / ${total} (id: ${iddatadb})...`;
const gridRow = row || getGridRow(iddatadb);
if (gridRow) setRowExporting(gridRow, true);
try {
const data = await sendExport(iddatadb, batchUuid);
processed++;
if (data.success) {
succeeded++;
} else {
failed++;
const errIdx = getRowIndexByIddatadb(iddatadb);
if (errIdx >= 0) window.gridData[errIdx]._exportError = data.message || "Unknown error";
if (gridRow) showRowError(gridRow, iddatadb, data.message || "Unknown error");
}
} catch (error) {
processed++;
failed++;
const errIdx = getRowIndexByIddatadb(iddatadb);
if (errIdx >= 0) window.gridData[errIdx]._exportError = error.message;
if (gridRow) showRowError(gridRow, iddatadb, error.message);
}
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
}
batchRunning = false;
enableAllRowButtons();
if (bar) bar.style.display = "none";
// Re-render to reflect status changes, then restore error indicators
const gr = window.gridRenderer;
if (gr) gr.renderVisibleRows();
// Restore error state from gridData._exportError
(window.gridData || []).forEach((row, idx) => {
if (row._exportError) {
const gridRow = getGridRow(row.iddatadb);
if (gridRow) showRowError(gridRow, row.iddatadb, row._exportError);
}
});
const msgEl = document.getElementById("exportResponseMessage");
const labelEl = document.getElementById("exportResponseModalLabel");
if (batchCancelled) {
labelEl.textContent = "Export All — Cancelled";
msgEl.innerHTML = `Exported: <strong>${succeeded}</strong><br>Errors: <strong>${failed}</strong><br>Not processed: <strong>${total - processed}</strong>`;
} else if (failed === 0) {
labelEl.textContent = "Export All — Complete";
msgEl.innerHTML = `All <strong>${succeeded}</strong> rows exported successfully.`;
} else {
labelEl.textContent = "Export All — Completed with errors";
msgEl.innerHTML = `Exported: <strong>${succeeded}</strong><br>Errors: <strong>${failed}</strong>`;
}
const modalEl = document.getElementById("exportResponseModal");
new bootstrap.Modal(modalEl, { keyboard: false }).show();
modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, { once: true });
})();
}
})();
+28 -1
View File
@@ -330,8 +330,16 @@ document.addEventListener("DOMContentLoaded", () => {
// Validate first // Validate first
clearValidationErrors(); clearValidationErrors();
// Show validating state on the row
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: parseInt(rowIndex) }]) validateRows([{ iddatadb: parseInt(iddatadb), index: parseInt(rowIndex) }])
.then((validationData) => { .then((validationData) => {
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
if (!validationData.success) { if (!validationData.success) {
showExportResult({ success: false, message: validationData.message || "Errore di validazione" }); showExportResult({ success: false, message: validationData.message || "Errore di validazione" });
return; return;
@@ -348,6 +356,9 @@ document.addEventListener("DOMContentLoaded", () => {
showConfirmAndExport(iddatadb, btn); showConfirmAndExport(iddatadb, btn);
}) })
.catch((error) => { .catch((error) => {
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
console.error("Validation error:", error); console.error("Validation error:", error);
showExportResult({ success: false, message: "Errore di validazione: " + error.message }); showExportResult({ success: false, message: "Errore di validazione: " + error.message });
}); });
@@ -379,13 +390,29 @@ document.addEventListener("DOMContentLoaded", () => {
console.log(`Confirmed export for iddatadb: ${iddatadb}`); console.log(`Confirmed export for iddatadb: ${iddatadb}`);
confirmModal.hide(); confirmModal.hide();
try {
const gridRow = btn.closest(".grid-row"); const gridRow = btn.closest(".grid-row");
setRowExporting(gridRow, true);
try {
const data = await sendExport(iddatadb, gridRow); const data = await sendExport(iddatadb, gridRow);
console.log("Export response:", data); console.log("Export response:", data);
// Stop spinner, fully restore row
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
if (!data.success) {
showRowError(gridRow, iddatadb, data.message || "Errore sconosciuto");
}
showExportResult(data); showExportResult(data);
} catch (error) { } catch (error) {
console.error("Export error:", error); console.error("Export error:", error);
// Stop spinner, fully restore row so user can retry
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
showRowError(gridRow, iddatadb, error.message);
showExportResult({ showExportResult({
success: false, success: false,
message: error.message, message: error.message,
+166 -19
View File
@@ -49,6 +49,16 @@ function validateDate($value)
return null; // Imposta null se non è una data valida return null; // Imposta null se non è una data valida
} }
// 🔹 Funzione per validare e convertire date
function formatDateToExport($value)
{
$date = DateTime::createFromFormat('Y-m-d', $value) ?: DateTime::createFromFormat('Y-m-d H:i:s', $value);
if ($date) {
return $date->format('d/m/Y');
}
return null; // Imposta null se non è una data valida
}
try { try {
$iddatadb = $_POST['iddatadb'] ?? null; $iddatadb = $_POST['iddatadb'] ?? null;
if (!$iddatadb) { if (!$iddatadb) {
@@ -69,6 +79,7 @@ try {
d.anagrafica_certest_object_id, d.anagrafica_certest_object_id,
d.anagrafica_certest_service_id, d.anagrafica_certest_service_id,
d.cliente_fornitore_id, d.cliente_fornitore_id,
d.clienteAnalisi,
d.consegna_richiesta d.consegna_richiesta
FROM datadb as d FROM datadb as d
INNER JOIN excel_templates as et ON d.templateid = et.id INNER JOIN excel_templates as et ON d.templateid = et.id
@@ -91,17 +102,35 @@ try {
$anagraficaObject = !empty($result['anagrafica_certest_object_id']) ? (int) $result['anagrafica_certest_object_id'] : null; $anagraficaObject = !empty($result['anagrafica_certest_object_id']) ? (int) $result['anagrafica_certest_object_id'] : null;
$anagraficaService = !empty($result['anagrafica_certest_service_id']) ? (int) $result['anagrafica_certest_service_id'] : null; $anagraficaService = !empty($result['anagrafica_certest_service_id']) ? (int) $result['anagrafica_certest_service_id'] : null;
$clienteFornitore = !empty($result['cliente_fornitore_id']) ? (int) $result['cliente_fornitore_id'] : null; $clienteFornitore = !empty($result['cliente_fornitore_id']) ? (int) $result['cliente_fornitore_id'] : null;
$clienteAnalisi = !empty($result['clienteAnalisi']) ? (int) $result['clienteAnalisi'] : null;
$consegnaRichiesta = !empty($result['consegna_richiesta']) ? $result['consegna_richiesta'] : null; $consegnaRichiesta = !empty($result['consegna_richiesta']) ? $result['consegna_richiesta'] : null;
// 🔹 STEP 3: Fetch Parts (including idmatrice) // 🔹 STEP 3: Fetch Parts (including idmatrice and part id for custom fields)
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
SELECT part_number, part_description, material, color, mix, idmatrice SELECT id AS part_id, part_number, part_description, material, color, mix, idmatrice, dateexpiry
FROM identification_parts FROM identification_parts
WHERE iddatadb = :iddatadb WHERE iddatadb = :iddatadb
ORDER BY CAST(part_number AS UNSIGNED) ASC, part_number ASC
"); ");
$stmt->execute(['iddatadb' => $iddatadb]); $stmt->execute(['iddatadb' => $iddatadb]);
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC); $parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 🔹 STEP 3.1: Fetch custom field values per part from identification_parts_customfields
$partIds = array_column($parts, 'part_id');
$partsCustomFields = []; // part_id => [ { field_id, value_id, value_text }, ... ]
if (!empty($partIds)) {
$placeholders = implode(',', array_fill(0, count($partIds), '?'));
$cfStmt = $pdo->prepare("
SELECT part_id, field_id, value_id, value_text
FROM identification_parts_customfields
WHERE part_id IN ({$placeholders})
");
$cfStmt->execute($partIds);
foreach ($cfStmt->fetchAll(PDO::FETCH_ASSOC) as $cfRow) {
$partsCustomFields[(int)$cfRow['part_id']][] = $cfRow;
}
}
// 🔹 STEP 4a: Auto-populate export_date / export_time fields // 🔹 STEP 4a: Auto-populate export_date / export_time fields
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
UPDATE import_data_details idd UPDATE import_data_details idd
@@ -165,6 +194,7 @@ try {
"AnagraficaCertestObject" => $anagraficaObject, "AnagraficaCertestObject" => $anagraficaObject,
"AnagraficaCertestService" => $anagraficaService, "AnagraficaCertestService" => $anagraficaService,
"ClienteFornitore" => $clienteFornitore, // PLACEHOLDER — to be implemented "ClienteFornitore" => $clienteFornitore, // PLACEHOLDER — to be implemented
"ClienteAnalisi" => $clienteAnalisi, // PLACEHOLDER — to be implemented
// DeliveryRequest goes to Campione, not CommessaWeb // DeliveryRequest goes to Campione, not CommessaWeb
]; ];
@@ -202,9 +232,9 @@ try {
"Matrice" => $matriceId, "Matrice" => $matriceId,
"SottoMatrice" => null, "SottoMatrice" => null,
"SchemaCustomField" => $schemaId, "SchemaCustomField" => $schemaId,
"Riferimento" => $part["part_description"] ?? "", // "Riferimento" => $part["part_description"] ?? "",
"NoteWeb" => $part["part_description"] ?? "", "NoteWeb" => $part["part_description"] ?? "",
"ConsegnaRichiesta" => $consegnaRichiesta, "ConsegnaRichiesta" => !empty($part["dateexpiry"]) ? $part["dateexpiry"] : $consegnaRichiesta,
]; ];
// Costruisci curl-like per questo campione // Costruisci curl-like per questo campione
@@ -231,6 +261,83 @@ try {
$logFileStep6 = $logDir . "commessa_{$commessaId}_campioni_step6_" . time() . ".txt"; $logFileStep6 = $logDir . "commessa_{$commessaId}_campioni_step6_" . time() . ".txt";
$writeLog($logFileStep6, $logContentStep6, "STEP 6 - Campioni (commessa={$commessaId})"); $writeLog($logFileStep6, $logContentStep6, "STEP 6 - Campioni (commessa={$commessaId})");
// 🔹 STEP 6.0: PATCH each Campione custom fields:
// - field 189 (Tested Component) = part_description
// - additional fields from identification_parts_customfields (field_id → value_id/value_text)
$logContentStep63 = "";
foreach ($campioni as $index => $campione) {
$campioneId = (int)($campione['IdCampione'] ?? 0);
$partDescription = $parts[$index]['part_description'] ?? '';
$partId = (int)($parts[$index]['part_id'] ?? 0);
if ($campioneId <= 0) {
continue;
}
// Build a map of overrides: IdCustomField => value
$overrides = [];
// Override 1: Tested Component (field 189) = part_description
if ($partDescription !== '') {
$overrides[189] = $partDescription;
}
// Override 2: values from identification_parts_customfields
$partCFs = $partsCustomFields[$partId] ?? [];
foreach ($partCFs as $pcf) {
$cfFieldId = (int)$pcf['field_id'];
$cfValue = $pcf['value_text'] ?? $pcf['value_id'] ?? null;
if ($cfFieldId > 0 && $cfValue !== null && $cfValue !== '') {
$overrides[$cfFieldId] = (string)$cfValue;
}
}
// Skip if nothing to override
if (empty($overrides)) {
continue;
}
// GET campione custom fields
$campioneWithFields = $api->get("Campione({$campioneId})?\$expand=CampioniCustomFields(\$expand=CustomField)");
$logContentStep63 .= "GET Campione({$campioneId}) CustomFields:\n" .
json_encode($campioneWithFields['CampioniCustomFields'] ?? [], JSON_PRETTY_PRINT) . "\n\n";
$logContentStep63 .= "Overrides for part {$partId}: " . json_encode($overrides) . "\n\n";
// Build PATCH payload — apply overrides where IdCustomField matches
$campioniCustomFields = [];
foreach ($campioneWithFields["CampioniCustomFields"] ?? [] as $cf) {
$definitionId = (int)($cf["CustomField"]["IdCustomField"] ?? 0);
$fieldInstanceId = (int)$cf["IdCampioniCustomFields"];
$currentValue = $cf["Valore"] ?? '';
$newValue = $overrides[$definitionId] ?? $currentValue;
$campioniCustomFields[] = [
"IdCampioniCustomFields" => $fieldInstanceId,
"Valore" => $newValue
];
}
if (!empty($campioniCustomFields)) {
$patchPayload = ["CampioniCustomFields" => $campioniCustomFields];
$patchJson = json_encode($patchPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$logContentStep63 .= "PATCH Campione({$campioneId}):\n" .
"curl --location --request PATCH '{$apiBaseUrl}Campione({$campioneId})' \\\n" .
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{$patchJson}'\n\n";
$patchResult = $api->patch("Campione({$campioneId})", $patchPayload);
$logContentStep63 .= "RESPONSE:\n" . json_encode($patchResult, JSON_PRETTY_PRINT) . "\n\n---\n";
}
}
$logFileStep63 = $logDir . "commessa_{$commessaId}_campioni_customfields_step60_" . time() . ".txt";
$writeLog($logFileStep63, $logContentStep63, "STEP 6.0 - Campioni CustomFields (commessa={$commessaId})");
// 🔹 STEP 6.1: Fetch photos linked to this iddatadb // 🔹 STEP 6.1: Fetch photos linked to this iddatadb
$stmtPhotos = $pdo->prepare(" $stmtPhotos = $pdo->prepare("
SELECT id, file_path, file_name, StampaNelRapporto, PrimaPagina SELECT id, file_path, file_name, StampaNelRapporto, PrimaPagina
@@ -242,15 +349,28 @@ try {
$photos = $stmtPhotos->fetchAll(PDO::FETCH_ASSOC); $photos = $stmtPhotos->fetchAll(PDO::FETCH_ASSOC);
// 🔹 STEP 6.2: Upload photos to the first (main) Campione only // 🔹 STEP 6.2: Upload photos to Campione .01 (fetched from API)
$photosUploaded = 0; $photosUploaded = 0;
$logContentPhotos = "Photos for CommessaWeb {$commessaId} (iddatadb={$iddatadb}):\n"; $logContentPhotos = "Photos for CommessaWeb {$commessaId} (iddatadb={$iddatadb}):\n";
$logContentPhotos .= "Total photos found: " . count($photos) . ", campioni: " . count($campioni) . "\n\n"; $logContentPhotos .= "Total photos found: " . count($photos) . ", campioni: " . count($campioni) . "\n\n";
if (!empty($campioni) && !empty($photos)) { if (!empty($campioni) && !empty($photos)) {
$mainCampione = $campioni[0]; // Fetch campioni list from API to find the .01 campione
$commessaCampioni = $api->get("CommessaWeb({$commessaId})?\$expand=Campioni");
$apiCampioni = $commessaCampioni['Campioni'] ?? [];
// Sort by CodiceCampione to find .01
usort($apiCampioni, fn($a, $b) => strcmp($a['CodiceCampione'] ?? '', $b['CodiceCampione'] ?? ''));
$mainCampione = $apiCampioni[0] ?? null;
$campioneId = (int)($mainCampione['IdCampione'] ?? 0); $campioneId = (int)($mainCampione['IdCampione'] ?? 0);
$logContentPhotos .= "API Campioni order:\n";
foreach ($apiCampioni as $ac) {
$logContentPhotos .= " - {$ac['CodiceCampione']} (IdCampione: {$ac['IdCampione']})\n";
}
$logContentPhotos .= "Selected .01 campione: IdCampione={$campioneId}\n\n";
if ($campioneId > 0) { if ($campioneId > 0) {
$logContentPhotos .= "=== Campione {$campioneId} (main) ===\n"; $logContentPhotos .= "=== Campione {$campioneId} (main) ===\n";
@@ -264,21 +384,42 @@ try {
} }
$photoEndpoint = "Campione({$campioneId})/UploadCampioneFile"; $photoEndpoint = "Campione({$campioneId})/UploadCampioneFile";
$stampaNelRapporto = !empty($photo['StampaNelRapporto']) ? 'true' : 'false'; $stampaNelRapporto = !empty($photo['StampaNelRapporto']);
$primaPagina = !empty($photo['PrimaPagina']) ? 'true' : 'false'; $primaPagina = !empty($photo['PrimaPagina']);
$logContentPhotos .= "curl --location --request POST '{$apiBaseUrl}{$photoEndpoint}' \\\n" . $logContentPhotos .= "curl --location --request POST '{$apiBaseUrl}{$photoEndpoint}' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" . "--header 'Authorization: Bearer ••••••' \\\n" .
"--form 'file=@{$fullPath}' \\\n" . "--form 'file=@{$fullPath}'\n\n";
"--form 'StampaNelRapporto={$stampaNelRapporto}' \\\n" .
"--form 'PrimaPagina={$primaPagina}'\n\n";
$extraFields = [ // Step 1: Upload file (flags are ignored by API during upload)
'StampaNelRapporto' => $stampaNelRapporto, $photoResult = $api->postMultipart($photoEndpoint, $fullPath, $photo['file_name']);
'PrimaPagina' => $primaPagina, $logContentPhotos .= "UPLOAD RESPONSE:\n" . json_encode($photoResult, JSON_PRETTY_PRINT) . "\n\n";
];
$photoResult = $api->postMultipart($photoEndpoint, $fullPath, $photo['file_name'], $extraFields); // Step 2: PATCH CampioneFile to set flags (StampaNelRapporto, PrimaPagina)
$logContentPhotos .= "RESPONSE:\n" . json_encode($photoResult, JSON_PRETTY_PRINT) . "\n\n---\n"; $campioneFileId = (int)($photoResult['IdCampioneFile'] ?? 0);
if ($campioneFileId > 0 && ($stampaNelRapporto || $primaPagina)) {
$patchPayload = [];
if ($stampaNelRapporto) {
$patchPayload['StampaNelRapporto'] = true;
}
if ($primaPagina) {
$patchPayload['PrimaPagina'] = true;
}
$patchEndpoint = "CampioneFile({$campioneFileId})";
$patchJsonLog = json_encode($patchPayload, JSON_PRETTY_PRINT);
$logContentPhotos .= "curl --location --request PATCH '{$apiBaseUrl}{$patchEndpoint}' \\\n" .
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{$patchJsonLog}'\n\n";
$patchResult = $api->patch($patchEndpoint, $patchPayload);
$logContentPhotos .= "PATCH RESPONSE:\n" . json_encode($patchResult, JSON_PRETTY_PRINT) . "\n\n";
}
$logContentPhotos .= "---\n";
$photosUploaded++; $photosUploaded++;
} }
} else { } else {
@@ -315,7 +456,7 @@ try {
// Valida se il campo è di tipo Data // Valida se il campo è di tipo Data
if ($fieldType === 'Data' && $newValue !== $currentValue) { if ($fieldType === 'Data' && $newValue !== $currentValue) {
$newValue = validateDate($newValue); $newValue = formatDateToExport($newValue);
} }
$commessaCustomFields[] = [ $commessaCustomFields[] = [
@@ -372,7 +513,13 @@ try {
// 🔹 STEP 9.5: Importazione da CommessaWeb a Commessa (commentato come richiesto) // 🔹 STEP 9.5: Importazione da CommessaWeb a Commessa (commentato come richiesto)
// Supplier call: POST api/odata/CommessaWeb(XXX)/ImportaCommessa // Supplier call: POST api/odata/CommessaWeb(XXX)/ImportaCommessa
$importPayload = ["IdUtente" => 285]; // user-id $importUserId = (!empty($lims_global_user_id) && is_numeric($lims_global_user_id))
? (int) $lims_global_user_id
: 285;
$importPayload = [
"IdUtente" => $importUserId
];
$importResult = $api->post("CommessaWeb({$commessaId})/ImportaCommessa", $importPayload); $importResult = $api->post("CommessaWeb({$commessaId})/ImportaCommessa", $importPayload);
$importPayloadLog = json_encode($importPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); $importPayloadLog = json_encode($importPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+50
View File
@@ -0,0 +1,50 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
// Endpoint per recuperare le Analisi
$endpoint = 'Analisi';
// Opzioni OData
$options = [
// Restituisce solo i campi principali utili
'$select' => 'IdAnalisi,Codice,NomeAnalisi,ClientiAbilitati,MatriciAbilitate,IsGenerico,Tipo,ParentKey,SelezionabileSuWeb',
// Solo analisi effettivamente selezionabili sul web
'$filter' => 'SelezionabileSuWeb eq true',
// Ordinamento alfabetico per nome analisi
'$orderby' => 'NomeAnalisi asc'
];
// Debug: salva URL usato
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$query = http_build_query($options);
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
file_put_contents(__DIR__ . '/last_analisi_url.txt', $full_url . PHP_EOL, FILE_APPEND);
// Chiamata API
$data = $api->get($endpoint, $options);
// Salva il JSON in locale
file_put_contents(__DIR__ . '/analisi_response.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo json_encode($data, JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/analisi_error_log.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
}
@@ -0,0 +1,68 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$idCliente = isset($_GET['id_cliente']) ? (int)$_GET['id_cliente'] : 0;
$idMatrice = isset($_GET['id_matrice']) ? (int)$_GET['id_matrice'] : 0;
$debug = isset($_GET['debug']) ? (int)$_GET['debug'] : 0;
if ($idCliente <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Missing or invalid id_cliente']);
exit;
}
$api = VisualLimsApiClient::getInstance();
if ($idMatrice > 0) {
$expandExpr = "AnalisiAbilitate(\$filter=Matrice/IdMatrice eq $idMatrice)";
} else {
$expandExpr = "AnalisiAbilitate";
}
// Encode only the $expand expression because it contains spaces, slash and parentheses
$expandEncoded = rawurlencode($expandExpr);
$endpoint = "Cliente($idCliente)?\$expand={$expandEncoded}";
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$full_url = $base_url . $endpoint;
file_put_contents(__DIR__ . '/last_url_analisi.txt', $full_url . PHP_EOL, FILE_APPEND);
$data = $api->get($endpoint, []);
file_put_contents(
__DIR__ . '/analisi_by_cliente_matrice_response.json',
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
if ($debug === 1) {
echo json_encode([
'endpoint' => $endpoint,
'full_url' => $full_url,
'raw_response' => $data
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
$analisi = $data['AnalisiAbilitate'] ?? [];
echo json_encode([
'value' => $analisi,
'count' => is_array($analisi) ? count($analisi) : 0
], JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log_analisi.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
@@ -0,0 +1,56 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$idMatrice = isset($_GET['id_matrice']) ? (int)$_GET['id_matrice'] : 0;
if ($idMatrice <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Missing or invalid id_matrice']);
exit;
}
$api = VisualLimsApiClient::getInstance();
/**
* OData hypothesis:
* - Expand enabled matrices
* - Return only selectable analyses
* - Include generic analyses OR analyses enabled for the given matrix
*
* This endpoint must be verified against the real VisualLims API metadata.
*/
$filter = rawurlencode("SelezionabileSuWeb eq true and (IsGenerico eq true or MatriciAbilitate/any(m:m/IdMatrice eq $idMatrice))");
$expand = rawurlencode("MatriciAbilitate");
$endpoint = "Analisi?\$top=10";
// Debug URL
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$full_url = $base_url . $endpoint;
file_put_contents(__DIR__ . '/last_url_analisi.txt', $full_url . PHP_EOL, FILE_APPEND);
$data = $api->get($endpoint, []);
file_put_contents(
__DIR__ . '/analisi_by_matrice_response.json',
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
$analisi = $data['value'] ?? [];
echo json_encode(['value' => $analisi], JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log_analisi.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
@@ -0,0 +1,60 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$idMatrice = isset($_GET['id_matrice']) ? (int)$_GET['id_matrice'] : 0;
$debug = isset($_GET['debug']) ? (int)$_GET['debug'] : 0;
if ($idMatrice <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Missing or invalid id_matrice']);
exit;
}
$api = VisualLimsApiClient::getInstance();
$filter = rawurlencode("Matrice/IdMatrice eq $idMatrice");
$endpoint = "Analisi?\$filter={$filter}";
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$full_url = $base_url . $endpoint;
file_put_contents(__DIR__ . '/last_url_analisi.txt', $full_url . PHP_EOL, FILE_APPEND);
$data = $api->get($endpoint, []);
file_put_contents(
__DIR__ . '/analisi_by_matrice_response.json',
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
if ($debug === 1) {
echo json_encode([
'endpoint' => $endpoint,
'full_url' => $full_url,
'raw_response' => $data
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
$analisi = $data['value'] ?? [];
echo json_encode([
'value' => $analisi,
'count' => is_array($analisi) ? count($analisi) : 0
], JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log_analisi.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
+12 -1
View File
@@ -74,10 +74,21 @@ try {
throw new Exception("Massimo numero di tentativi raggiunto per $endpoint"); 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 // Esegui la chiamata con retry
$data = makeApiRequest($api, $endpoint); $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) { } catch (Exception $e) {
http_response_code(500); http_response_code(500);
$errorResponse = [ $errorResponse = [
+103
View File
@@ -0,0 +1,103 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
// Disable PHP error display
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance(); // also loads dotenv
// In simulate mode: return fake clients built from idclient values already in datadb.
if (($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true') {
require_once __DIR__ . '/class/db-functions.php';
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->query("
SELECT DISTINCT idclient
FROM datadb
WHERE idclient IS NOT NULL
AND idclient > 0
ORDER BY idclient ASC
");
$ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
$fakeClients = array_map(fn($id) => [
'IdCliente' => (int) $id,
'Nominativo' => "Cliente Simulato {$id}",
'CodiceCliente' => "SIM_{$id}",
], $ids);
echo json_encode(['value' => $fakeClients], JSON_UNESCAPED_UNICODE);
exit;
}
// Endpoint senza filtri: recupera tutto
$endpoint = 'Cliente?$top=50';
// Funzione per eseguire la chiamata con retry
function makeApiRequest($api, $endpoint, $maxRetries = 3)
{
for ($retry = 0; $retry < $maxRetries; $retry++) {
try {
$data = $api->get($endpoint);
// Save response for debug
file_put_contents(
__DIR__ . '/clienti_response.json',
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
return $data;
} catch (Exception $e) {
$errorMessage = $e->getMessage();
// Retry only for specific auth/token related issue
if (
strpos($errorMessage, 'HTTP 400') !== false &&
strpos($errorMessage, 'Cannot persist the object') !== false
) {
try {
if (method_exists($api, 'refreshToken')) {
$api->refreshToken();
error_log("Tentativo {$retry}: refresh token eseguito per endpoint {$endpoint}");
} else {
throw new Exception('Il metodo refreshToken() non esiste in VisualLimsApiClient');
}
} catch (Exception $refreshEx) {
error_log("Errore durante il refresh del token: " . $refreshEx->getMessage());
throw new Exception("Impossibile eseguire il refresh del token: " . $refreshEx->getMessage());
}
usleep(500000); // 500ms
continue;
}
throw $e;
}
}
throw new Exception("Massimo numero di tentativi raggiunto per {$endpoint}");
}
// Esegui la chiamata
$data = makeApiRequest($api, $endpoint);
echo json_encode($data, JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
http_response_code(500);
$errorResponse = [
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
];
error_log("Errore in get_clienti.php: " . json_encode($errorResponse, JSON_UNESCAPED_UNICODE));
echo json_encode($errorResponse, JSON_UNESCAPED_UNICODE);
}
+14 -5
View File
@@ -21,16 +21,25 @@ try {
} }
$results = []; $results = [];
$cacheDir = __DIR__ . '/cache';
if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true);
foreach ($fieldIds as $customFieldId) { foreach ($fieldIds as $customFieldId) {
$endpoint = "CustomField($customFieldId)?\$expand=CustomFieldsValues"; $cacheFile = $cacheDir . '/customfield_' . $customFieldId . '.json';
$data = $api->get($endpoint);
$results[$customFieldId] = $data['CustomFieldsValues'] ?? []; // Use cache if fresh (1 hour)
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
$results[$customFieldId] = json_decode(file_get_contents($cacheFile), true);
continue;
} }
// Debug ფაილი $endpoint = "CustomField($customFieldId)?\$expand=CustomFieldsValues";
file_put_contents(__DIR__ . '/customfield_values_response.json', json_encode($results)); $data = $api->get($endpoint);
$values = $data['CustomFieldsValues'] ?? [];
$results[$customFieldId] = $values;
file_put_contents($cacheFile, json_encode($values));
}
echo json_encode($results); echo json_encode($results);
} catch (Exception $e) { } catch (Exception $e) {
+35 -48
View File
@@ -1,59 +1,46 @@
<?php <?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php'; // Risale a root/vendor/ require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
use Dotenv\Dotenv; 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'); 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 { try {
$dotenv = Dotenv::createImmutable($envPath); // Read from matrici cache (populated by get_matrici.php / warm_cache.php)
$dotenv->load(); $cacheFile = __DIR__ . '/cache/matrici.json';
} catch (Exception $e) { if (file_exists($cacheFile)) {
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - Errore caricamento .env: ' . $e->getMessage() . PHP_EOL, FILE_APPEND); $data = json_decode(file_get_contents($cacheFile), true);
echo json_encode(['success' => false, 'message' => 'Errore caricamento configurazione: ' . $e->getMessage()]); $matrici = $data['value'] ?? [];
exit(1); } 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 // Extract unique MacroMatrice values
$dbHost = $_ENV['DB_HOST']; $macroValues = [];
$dbName = $_ENV['DB_DATABASE']; foreach ($matrici as $m) {
$dbUser = $_ENV['DB_USERNAME']; $macro = $m['MacroMatrice'] ?? null;
$dbPass = $_ENV['DB_PASSWORD']; if ($macro !== null && $macro !== '' && substr($macro, 0, 1) !== '*') {
$dbPrefix = $_ENV['DB_PREFIX']; $macroValues[$macro] = true;
// 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);
} }
}
$macroMatrici = array_keys($macroValues);
sort($macroMatrici);
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]); echo json_encode(['success' => true, 'value' => $macroMatrici]);
} catch (PDOException $e) { } catch (Exception $e) {
// Log errore e restituisci risposta di errore http_response_code(500);
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' => $e->getMessage()]);
echo json_encode(['success' => false, 'message' => 'Errore nel recupero delle MacroMatrice: ' . $e->getMessage()]);
exit(1);
} }
+47
View 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
View 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(); },
};
})();
+196 -181
View File
@@ -167,6 +167,8 @@ $fixedFieldsRaw = $fixedStmt->fetchAll(PDO::FETCH_ASSOC);
// Ordine desiderato: Cliente Responsabile prima, poi gli altri, ConsegnaRichiesta prima delle 3 speciali // Ordine desiderato: Cliente Responsabile prima, poi gli altri, ConsegnaRichiesta prima delle 3 speciali
$desiredOrder = [ $desiredOrder = [
'ClienteResponsabile', 'ClienteResponsabile',
'ClienteFornitore',
'ClienteAnalisi',
'AnagraficaCertestObject', 'AnagraficaCertestObject',
'AnagraficaCertestService', 'AnagraficaCertestService',
'MoltiplicatorePrezzo', 'MoltiplicatorePrezzo',
@@ -174,9 +176,13 @@ $desiredOrder = [
// se ci sono altri campi fixed li mettiamo dopo // se ci sono altri campi fixed li mettiamo dopo
]; ];
// No exclusions: fixed fields will be rendered together at the end
$excludeFromFixed = [];
$fixedFields = []; $fixedFields = [];
$tempMap = []; $tempMap = [];
foreach ($fixedFieldsRaw as $f) { foreach ($fixedFieldsRaw as $f) {
if (in_array($f['fixed_field_key'], $excludeFromFixed, true)) continue;
$tempMap[$f['fixed_field_key']] = $f; $tempMap[$f['fixed_field_key']] = $f;
} }
@@ -195,10 +201,11 @@ foreach ($tempMap as $f) {
// Maps logical fixed_field_key → real datadb column name (mirrors JS fixedAliasMap) // Maps logical fixed_field_key → real datadb column name (mirrors JS fixedAliasMap)
$fixedAliasMap = [ $fixedAliasMap = [
'ClienteResponsabile' => 'cliente_responsabile_id', 'ClienteResponsabile' => 'cliente_responsabile_id',
'ClienteFornitore' => 'cliente_fornitore_id',
'ClienteAnalisi' => 'clienteAnalisi',
'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id', 'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id',
'AnagraficaCertestObject' => 'anagrafica_certest_object_id', 'AnagraficaCertestObject' => 'anagrafica_certest_object_id',
'AnagraficaCertestService' => 'anagrafica_certest_service_id', 'AnagraficaCertestService' => 'anagrafica_certest_service_id',
'ClienteFornitore' => 'cliente_fornitore_id',
'ConsegnaRichiesta' => 'consegna_richiesta', 'ConsegnaRichiesta' => 'consegna_richiesta',
]; ];
@@ -333,6 +340,7 @@ function fixedDefaultValue(array $f): string
align-items: center; align-items: center;
padding: 0; padding: 0;
border-bottom: 1px solid #dee2e6; border-bottom: 1px solid #dee2e6;
min-width: fit-content;
} }
.grid-row:last-child { .grid-row:last-child {
@@ -360,8 +368,15 @@ function fixedDefaultValue(array $f): string
} }
@keyframes batch-pulse { @keyframes batch-pulse {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; } 0%,
100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
} }
.batch-row-spinner { .batch-row-spinner {
@@ -876,9 +891,8 @@ function fixedDefaultValue(array $f): string
<div class="page-wrapper"> <div class="page-wrapper">
<div class="page-content"> <div class="page-content">
<div class="mb-3 text"> <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="imported.php?id=<?= $template_id ?>" 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="tolims.php?id=<?= $template_id ?>" class="btn btn-success">To LIMS (l)</a>
<a href="historical_trf.php?id=<?= $template_id ?>&status=l" class="btn btn-success">To LIMS (l)</a>
</div> </div>
<div class="card radius-10"> <div class="card radius-10">
<div class="card-header"> <div class="card-header">
@@ -946,14 +960,15 @@ function fixedDefaultValue(array $f): string
$topColIndex = $mainFieldMapping ? 2 : 1; ?> $topColIndex = $mainFieldMapping ? 2 : 1; ?>
<div class="grid-cell grid-top-cell" style="flex: 0 0 300px;" data-index="<?= $mainFieldMapping ? 2 : 1 ?>"> <div class="grid-cell grid-top-cell" style="flex: 0 0 150px;" data-index="<?= $mainFieldMapping ? 2 : 1 ?>"></div>
<div class="grid-cell grid-top-cell" style="flex: 0 0 300px;" data-index="<?= $mainFieldMapping ? 3 : 2 ?>">
<select class="custom-field dropdown-select client-select searchable-client" data-column="idclient" id="clientSelect" name="idclient"> <select class="custom-field dropdown-select client-select searchable-client" data-column="idclient" id="clientSelect" name="idclient">
<option value="">Select a client...</option> <option value="">Select a client...</option>
</select> </select>
<button type="button" class="propagate-btn" data-column="idclient"><i class="fas fa-arrow-down"></i></button> <button type="button" class="propagate-btn" data-column="idclient"><i class="fas fa-arrow-down"></i></button>
<span id="clientLoadingStatus" class="text-muted" style="margin-left: 10px; display: none;">Recupero clienti in corso...</span> <span id="clientLoadingStatus" class="text-muted" style="margin-left: 10px; display: none;">Recupero clienti in corso...</span>
</div> </div>
<div class="grid-cell grid-top-cell" style="flex: 0 0 150px;" data-index="<?= $mainFieldMapping ? 3 : 2 ?>"></div>
<?php <?php
$topColIndex = $mainFieldMapping ? 4 : 3; $topColIndex = $mainFieldMapping ? 4 : 3;
@@ -1065,6 +1080,8 @@ function fixedDefaultValue(array $f): string
$isApiField = in_array($key, [ $isApiField = in_array($key, [
'MoltiplicatorePrezzo', 'MoltiplicatorePrezzo',
'ClienteResponsabile', 'ClienteResponsabile',
'ClienteFornitore',
'ClienteAnalisi',
'AnagraficaCertestObject', 'AnagraficaCertestObject',
'AnagraficaCertestService' 'AnagraficaCertestService'
], true); ], true);
@@ -1118,9 +1135,9 @@ function fixedDefaultValue(array $f): string
<div class="resizer"></div> <div class="resizer"></div>
</div> </div>
<?php endif; ?> <?php endif; ?>
<div class="grid-header" data-index="<?= $mainFieldMapping ? 2 : 1 ?>" style="flex: 0 0 300px; position: relative;">Client<div class="resizer"></div> <div class="grid-header" data-index="<?= $mainFieldMapping ? 2 : 1 ?>" style="flex: 0 0 150px; position: relative;">Status<div class="resizer"></div>
</div> </div>
<div class="grid-header" data-index="<?= $mainFieldMapping ? 3 : 2 ?>" style="flex: 0 0 150px; position: relative;">Status<div class="resizer"></div> <div class="grid-header" data-index="<?= $mainFieldMapping ? 3 : 2 ?>" style="flex: 0 0 300px; position: relative;">Client<div class="resizer"></div>
</div> </div>
<?php <?php
$headerIndex = $mainFieldMapping ? 4 : 3; $headerIndex = $mainFieldMapping ? 4 : 3;
@@ -1151,6 +1168,16 @@ function fixedDefaultValue(array $f): string
$key = $f['fixed_field_key']; $key = $f['fixed_field_key'];
$label = $slugMapping[$key] ?? $key; $label = $slugMapping[$key] ?? $key;
if ($key === 'ClienteFornitore') {
$label = 'Fornitore';
}
if ($key === 'ClienteAnalisi') {
$label = 'Buyer';
}
echo "<div class='grid-header' data-index='$headerIndex' style='flex: 0 0 180px; position: relative;'>" echo "<div class='grid-header' data-index='$headerIndex' style='flex: 0 0 180px; position: relative;'>"
. htmlspecialchars($label) . . htmlspecialchars($label) .
"<div class='resizer'></div></div>"; "<div class='resizer'></div></div>";
@@ -1211,12 +1238,7 @@ function fixedDefaultValue(array $f): string
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<div class="grid-cell editable-cell" data-col="idclient" data-row="<?= $index ?>" data-index="<?= $mainFieldMapping ? 2 : 1 ?>" style="flex: 0 0 300px;"> <div class="grid-cell editable-cell" data-col="status" data-row="<?= $index ?>" data-index="<?= $mainFieldMapping ? 2 : 1 ?>" style="flex: 0 0 150px;">
<select name="rows[<?= $index ?>][idclient]" class="cell-input dropdown-select client-select searchable-client" data-current-value="<?= htmlspecialchars($row['idclient'] ?? $default_idclient) ?>">
<option value="">Select a client...</option>
</select>
</div>
<div class="grid-cell editable-cell" data-col="status" data-row="<?= $index ?>" data-index="<?= $mainFieldMapping ? 3 : 2 ?>" style="flex: 0 0 150px;">
<span class="status-badge status-<?= htmlspecialchars($row['status'] ?? 'i') ?>"> <span class="status-badge status-<?= htmlspecialchars($row['status'] ?? 'i') ?>">
<?= htmlspecialchars($row['status'] === 'i' ? 'Imported' : ($row['status'] === 'P' ? 'In Progress' : 'To LIMS')) ?> <?= htmlspecialchars($row['status'] === 'i' ? 'Imported' : ($row['status'] === 'P' ? 'In Progress' : 'To LIMS')) ?>
</span> </span>
@@ -1225,6 +1247,12 @@ function fixedDefaultValue(array $f): string
<?php endif; ?> <?php endif; ?>
<input type="hidden" name="rows[<?= $index ?>][status]" value="<?= htmlspecialchars($row['status'] ?? 'i') ?>"> <input type="hidden" name="rows[<?= $index ?>][status]" value="<?= htmlspecialchars($row['status'] ?? 'i') ?>">
</div> </div>
<div class="grid-cell editable-cell" data-col="idclient" data-row="<?= $index ?>" data-index="<?= $mainFieldMapping ? 3 : 2 ?>" style="flex: 0 0 300px;">
<select name="rows[<?= $index ?>][idclient]" class="cell-input dropdown-select client-select searchable-client" data-current-value="<?= htmlspecialchars($row['idclient'] ?? $default_idclient) ?>">
<option value="">Select a client...</option>
</select>
</div>
<?php <?php
$cellIndex = $mainFieldMapping ? 4 : 3; $cellIndex = $mainFieldMapping ? 4 : 3;
$rowDetails = array_filter($manualDetails, fn($d) => $d['datadb_id'] == $row['iddatadb']); $rowDetails = array_filter($manualDetails, fn($d) => $d['datadb_id'] == $row['iddatadb']);
@@ -1351,7 +1379,7 @@ function fixedDefaultValue(array $f): string
. (((int)$f['is_required'] === 1) ? "required" : "") . (((int)$f['is_required'] === 1) ? "required" : "")
. ">"; . ">";
} else { } else {
$isApiField = in_array($key, ['MoltiplicatorePrezzo', 'ClienteResponsabile', 'AnagraficaCertestObject', 'AnagraficaCertestService'], true); $isApiField = in_array($key, ['MoltiplicatorePrezzo', 'ClienteResponsabile', 'ClienteFornitore', 'ClienteAnalisi', 'AnagraficaCertestObject', 'AnagraficaCertestService'], true);
$selectClass = $isApiField ? 'api-fixed-select' : ''; $selectClass = $isApiField ? 'api-fixed-select' : '';
echo "<select echo "<select
name='rows[$index][$key]' name='rows[$index][$key]'
@@ -1398,6 +1426,7 @@ function fixedDefaultValue(array $f): string
</div> </div>
</form> </form>
<div id="partsModalContainer"></div> <div id="partsModalContainer"></div>
<div id="analysisModalContainer"></div>
<div id="annotationsModalContainer"></div> <div id="annotationsModalContainer"></div>
<?php include 'photos_functions.php'; ?> <?php include 'photos_functions.php'; ?>
</div> </div>
@@ -1416,8 +1445,63 @@ function fixedDefaultValue(array $f): string
<script src="photos.js"></script> <script src="photos.js"></script>
<script src="annotationsModal.js"></script> <script src="annotationsModal.js"></script>
<script src="partsTable.js"></script> <script src="partsTable.js"></script>
<script src="analysisModal.js"></script>
<script src="tracking.js"></script> <script src="tracking.js"></script>
<script src="export_to_lims.js"></script> <script src="export_to_lims.js"></script>
<script>
let globalClientData = [];
let globalClientPromise = null;
function formatClientLabel(client) {
const nome = client.Nominativo || "Nome non disponibile";
const id = client.IdCliente || "";
const codiceCliente = (client.CodiceCliente ?? client.codiceCliente ?? '').toString().trim();
const suffix = (codiceCliente.split('_')[1] || '').trim();
const shortCode = suffix || (codiceCliente ? codiceCliente.charAt(0) : '--');
return `${nome.trim()} - ${shortCode} (ID: ${id})`;
}
async function ensureClientsLoaded(retryCount = 0, maxRetries = 3) {
if (globalClientData.length > 0) {
return globalClientData;
}
if (globalClientPromise) {
return globalClientPromise;
}
globalClientPromise = (async () => {
const response = await fetch("get_clienti.php", {
method: "GET",
headers: {
"Content-Type": "application/json"
}
});
const data = await response.json();
if (!response.ok) {
if (
response.status === 500 &&
data.error &&
data.error.includes('Cannot persist the object') &&
retryCount < maxRetries
) {
await new Promise(resolve => setTimeout(resolve, 1000));
globalClientPromise = null;
return ensureClientsLoaded(retryCount + 1, maxRetries);
}
throw new Error(data.error || `Errore HTTP: ${response.status}`);
}
globalClientData = data.value || [];
return globalClientData;
})();
return globalClientPromise;
}
</script>
<script> <script>
function expandColumn(cell, expand) { function expandColumn(cell, expand) {
const columnIndex = cell.getAttribute('data-index'); const columnIndex = cell.getAttribute('data-index');
@@ -1525,6 +1609,14 @@ function fixedDefaultValue(array $f): string
const rowIndex = btn.dataset.row; const rowIndex = btn.dataset.row;
const row = btn.closest('.grid-row'); const row = btn.closest('.grid-row');
const iddatadb = row.getAttribute('data-id'); const iddatadb = row.getAttribute('data-id');
// Show spinner on save button
const origHtml = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
btn.disabled = true;
row.style.opacity = '0.6';
row.style.pointerEvents = 'none';
const formData = new FormData(); const formData = new FormData();
const inputs = row.querySelectorAll(`input[name^="rows[${rowIndex}][details]"], select[name^="rows[${rowIndex}][details]"]`); const inputs = row.querySelectorAll(`input[name^="rows[${rowIndex}][details]"], select[name^="rows[${rowIndex}][details]"]`);
@@ -1555,6 +1647,8 @@ function fixedDefaultValue(array $f): string
// Map: fixed key (logical) -> datadb real column // Map: fixed key (logical) -> datadb real column
const fixedAliasMap = { const fixedAliasMap = {
ClienteResponsabile: 'cliente_responsabile_id', ClienteResponsabile: 'cliente_responsabile_id',
ClienteFornitore: 'cliente_fornitore_id',
ClienteAnalisi: 'clienteAnalisi',
MoltiplicatorePrezzo: 'moltiplicatore_prezzo_id', MoltiplicatorePrezzo: 'moltiplicatore_prezzo_id',
AnagraficaCertestObject: 'anagrafica_certest_object_id', AnagraficaCertestObject: 'anagrafica_certest_object_id',
AnagraficaCertestService: 'anagrafica_certest_service_id', AnagraficaCertestService: 'anagrafica_certest_service_id',
@@ -1608,6 +1702,12 @@ function fixedDefaultValue(array $f): string
}) })
.catch(error => { .catch(error => {
alert('Errore durante il salvataggio: ' + error.message); alert('Errore durante il salvataggio: ' + error.message);
})
.finally(() => {
btn.innerHTML = origHtml;
btn.disabled = false;
row.style.opacity = '';
row.style.pointerEvents = '';
}); });
}); });
}); });
@@ -1616,7 +1716,9 @@ function fixedDefaultValue(array $f): string
document.querySelector('.save-all-btn').addEventListener('click', (e) => { document.querySelector('.save-all-btn').addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
if (saveAllRunning || window.batchRunning) return; if (saveAllRunning || window.batchRunning) return;
const confirmModal = new bootstrap.Modal(document.getElementById('saveAllConfirmModal'), { keyboard: false }); const confirmModal = new bootstrap.Modal(document.getElementById('saveAllConfirmModal'), {
keyboard: false
});
confirmModal.show(); confirmModal.show();
}); });
@@ -1634,7 +1736,11 @@ function fixedDefaultValue(array $f): string
// Disable all row buttons // Disable all row buttons
document.querySelectorAll('.grid-row[data-id]').forEach(r => r.classList.add('batch-disabled')); document.querySelectorAll('.grid-row[data-id]').forEach(r => r.classList.add('batch-disabled'));
const toggle = document.querySelector('.actions-dropdown .dropdown-toggle'); const toggle = document.querySelector('.actions-dropdown .dropdown-toggle');
if (toggle) { toggle.disabled = true; toggle.style.opacity = '0.5'; toggle.style.pointerEvents = 'none'; } if (toggle) {
toggle.disabled = true;
toggle.style.opacity = '0.5';
toggle.style.pointerEvents = 'none';
}
// Show status bar // Show status bar
const bar = document.getElementById('batchExportBar'); const bar = document.getElementById('batchExportBar');
@@ -1651,10 +1757,14 @@ function fixedDefaultValue(array $f): string
for (const row of rows) { for (const row of rows) {
const saveBtn = row.querySelector('.save-btn'); const saveBtn = row.querySelector('.save-btn');
if (!saveBtn) { continue; } if (!saveBtn) {
continue;
}
const rowIndex = saveBtn.dataset.row; const rowIndex = saveBtn.dataset.row;
const iddatadb = row.getAttribute('data-id'); const iddatadb = row.getAttribute('data-id');
if (!rowIndex || !iddatadb) { continue; } if (!rowIndex || !iddatadb) {
continue;
}
processed++; processed++;
statusEl.textContent = `Salvataggio ${processed} / ${total} (id: ${iddatadb})...`; statusEl.textContent = `Salvataggio ${processed} / ${total} (id: ${iddatadb})...`;
@@ -1664,7 +1774,10 @@ function fixedDefaultValue(array $f): string
row.classList.add('batch-exporting'); row.classList.add('batch-exporting');
const btnCell = row.querySelector('.button-cell'); const btnCell = row.querySelector('.button-cell');
if (btnCell) { if (btnCell) {
btnCell.querySelectorAll('.action-btn').forEach(b => { b.dataset.prevDisplay = b.style.display; b.style.display = 'none'; }); btnCell.querySelectorAll('.action-btn').forEach(b => {
b.dataset.prevDisplay = b.style.display;
b.style.display = 'none';
});
const spinner = document.createElement('span'); const spinner = document.createElement('span');
spinner.className = 'batch-row-spinner'; spinner.className = 'batch-row-spinner';
spinner.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Saving...'; spinner.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Saving...';
@@ -1696,6 +1809,8 @@ function fixedDefaultValue(array $f): string
const m = inp.name.match(/rows\[\d+\]\[([^\]]+)\]/); const m = inp.name.match(/rows\[\d+\]\[([^\]]+)\]/);
const fixedAliasMap = { const fixedAliasMap = {
ClienteResponsabile: 'cliente_responsabile_id', ClienteResponsabile: 'cliente_responsabile_id',
ClienteFornitore: 'cliente_fornitore_id',
ClienteAnalisi: 'clienteAnalisi',
MoltiplicatorePrezzo: 'moltiplicatore_prezzo_id', MoltiplicatorePrezzo: 'moltiplicatore_prezzo_id',
AnagraficaCertestObject: 'anagrafica_certest_object_id', AnagraficaCertestObject: 'anagrafica_certest_object_id',
AnagraficaCertestService: 'anagrafica_certest_service_id', AnagraficaCertestService: 'anagrafica_certest_service_id',
@@ -1758,7 +1873,10 @@ function fixedDefaultValue(array $f): string
if (btnCell) { if (btnCell) {
const sp = btnCell.querySelector('.batch-row-spinner'); const sp = btnCell.querySelector('.batch-row-spinner');
if (sp) sp.remove(); if (sp) sp.remove();
btnCell.querySelectorAll('.action-btn').forEach(b => { b.style.display = b.dataset.prevDisplay || ''; delete b.dataset.prevDisplay; }); btnCell.querySelectorAll('.action-btn').forEach(b => {
b.style.display = b.dataset.prevDisplay || '';
delete b.dataset.prevDisplay;
});
} }
} }
@@ -1771,7 +1889,11 @@ function fixedDefaultValue(array $f): string
// Re-enable all row buttons // Re-enable all row buttons
document.querySelectorAll('.grid-row[data-id]').forEach(r => r.classList.remove('batch-disabled')); document.querySelectorAll('.grid-row[data-id]').forEach(r => r.classList.remove('batch-disabled'));
if (toggle) { toggle.disabled = false; toggle.style.opacity = ''; toggle.style.pointerEvents = ''; } if (toggle) {
toggle.disabled = false;
toggle.style.opacity = '';
toggle.style.pointerEvents = '';
}
const resultEl = document.getElementById('saveAllResultMessage'); const resultEl = document.getElementById('saveAllResultMessage');
const resultLabel = document.getElementById('saveAllResultModalLabel'); const resultLabel = document.getElementById('saveAllResultModalLabel');
@@ -1782,7 +1904,9 @@ function fixedDefaultValue(array $f): string
resultLabel.textContent = 'Salvataggio con Errori'; resultLabel.textContent = 'Salvataggio con Errori';
resultEl.innerHTML = `<i class="fas fa-exclamation-triangle" style="color:#ffc107;"></i> Salvate <strong>${successCount}</strong> righe con successo.<br><br><strong>Errori:</strong><br>` + errorMessages.join('<br>'); resultEl.innerHTML = `<i class="fas fa-exclamation-triangle" style="color:#ffc107;"></i> Salvate <strong>${successCount}</strong> righe con successo.<br><br><strong>Errori:</strong><br>` + errorMessages.join('<br>');
} }
new bootstrap.Modal(document.getElementById('saveAllResultModal'), { keyboard: false }).show(); new bootstrap.Modal(document.getElementById('saveAllResultModal'), {
keyboard: false
}).show();
}); });
window.addEventListener("beforeunload", function(e) { window.addEventListener("beforeunload", function(e) {
@@ -1814,50 +1938,31 @@ function fixedDefaultValue(array $f): string
<script> <script>
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
const inputs = document.querySelectorAll('.cell-input'); const inputs = document.querySelectorAll('.cell-input');
let clientData = []; // Dichiarazione di clientData qui let clientData = globalClientData;
// Funzione per caricare i client // Funzione per caricare i client
async function loadClients(retryCount = 0, maxRetries = 3) { async function loadClients() {
const clientLoadingStatus = document.getElementById("clientLoadingStatus"); const clientLoadingStatus = document.getElementById("clientLoadingStatus");
try { try {
clientLoadingStatus.style.display = 'inline'; clientLoadingStatus.style.display = 'inline';
clientLoadingStatus.textContent = 'Recupero clienti in corso...'; clientLoadingStatus.textContent = 'Recupero clienti in corso...';
const response = await fetch("get_clienti.php", {
method: "GET",
headers: {
"Content-Type": "application/json"
}
});
const data = await response.json();
if (!response.ok) {
if (response.status === 500 && data.error.includes('Cannot persist the object') && retryCount < maxRetries) {
console.log(`Tentativo ${retryCount + 1}/${maxRetries}: Riprovo a caricare i clienti...`);
await new Promise(resolve => setTimeout(resolve, 1000));
return loadClients(retryCount + 1, maxRetries);
}
throw new Error(data.error || `Errore HTTP: ${response.status}`);
}
clientData = data.value || []; clientData = await ensureClientsLoaded();
const select = document.getElementById("clientSelect"); const select = document.getElementById("clientSelect");
select.innerHTML = '<option value="">Select a client...</option>'; select.innerHTML = '<option value="">Select a client...</option>';
clientData.forEach(client => {
const nome = client.Nominativo || "Nome non disponibile";
const id = client.IdCliente || "ID non disponibile";
const codiceCliente = (client.CodiceCliente || '').toString().trim();
const suffix = (codiceCliente.split('_')[1] || '').trim();
const shortCode = suffix || (codiceCliente ? codiceCliente.charAt(0) : '--');
const option = new Option(`${nome.trim()} - ${shortCode} (ID: ${id})`, id); clientData.forEach(client => {
if (parseInt(id) === parseInt(<?php echo json_encode($default_idclient ?? 0); ?>)) { const option = new Option(formatClientLabel(client), client.IdCliente);
if (parseInt(client.IdCliente) === parseInt(<?php echo json_encode($default_idclient ?? 0); ?>)) {
option.selected = true; option.selected = true;
} }
select.add(option); select.add(option);
}); });
populateClientDropdowns(); populateClientDropdowns();
clientLoadingStatus.textContent = "Clienti caricati."; clientLoadingStatus.textContent = "Clienti caricati.";
// ✅ force refresh of header dependent fixed fields
$('#clientSelect').trigger('change'); $('#clientSelect').trigger('change');
$('.grid-top .api-fixed-select[data-fixed-key="ClienteResponsabile"]').trigger('fixed:reload'); $('.grid-top .api-fixed-select[data-fixed-key="ClienteResponsabile"]').trigger('fixed:reload');
@@ -2219,7 +2324,7 @@ function fixedDefaultValue(array $f): string
} else { } else {
// Select nativa // Select nativa
try { try {
$(dropdown).select2('destroy'); if ($(dropdown).hasClass('select2-hidden-accessible')) $(dropdown).select2('destroy');
} catch (e) {} } catch (e) {}
dropdown.innerHTML = '<option value="">Seleziona...</option>'; dropdown.innerHTML = '<option value="">Seleziona...</option>';
items.forEach(value => { items.forEach(value => {
@@ -2245,121 +2350,6 @@ function fixedDefaultValue(array $f): string
populateDropdowns(); populateDropdowns();
}); });
document.addEventListener("DOMContentLoaded", function() {
let clientData = [];
async function loadClients(retryCount = 0, maxRetries = 3) {
const clientLoadingStatus = document.getElementById("clientLoadingStatus");
try {
clientLoadingStatus.style.display = 'inline';
clientLoadingStatus.textContent = 'Recupero clienti in corso...';
const response = await fetch("get_clienti.php", {
method: "GET",
headers: {
"Content-Type": "application/json"
}
});
const data = await response.json();
if (!response.ok) {
if (response.status === 500 && data.error.includes('Cannot persist the object') && retryCount < maxRetries) {
console.log(`Tentativo ${retryCount + 1}/${maxRetries}: Riprovo a caricare i clienti...`);
await new Promise(resolve => setTimeout(resolve, 1000));
return loadClients(retryCount + 1, maxRetries);
}
throw new Error(data.error || `Errore HTTP: ${response.status}`);
}
clientData = data.value || [];
const select = document.getElementById("clientSelect");
select.innerHTML = '<option value="">Select a client...</option>';
clientData.forEach(client => {
const nome = client.Nominativo || "Nome non disponibile";
const id = client.IdCliente || "ID non disponibile";
// CodiceCliente es: "Bl01858_E" -> vogliamo "E"
const codiceCliente = (client.CodiceCliente ?? client.codiceCliente ?? '').toString().trim();
const suffix = (codiceCliente.split('_')[1] || '').trim(); // parte dopo "_"
const shortCode = suffix || '--';
const option = new Option(`${nome.trim()} - ${shortCode} (ID: ${id})`, id);
if (parseInt(id) === parseInt(<?php echo json_encode($default_idclient ?? 0); ?>)) {
option.selected = true;
}
select.add(option);
});
populateClientDropdowns();
clientLoadingStatus.textContent = "Clienti caricati.";
} catch (error) {
clientLoadingStatus.textContent = "Errore nel caricamento.";
Swal.fire({
title: "Errore!",
text: "Impossibile caricare i clienti: " + error.message,
icon: "error",
confirmButtonText: "OK"
});
} finally {
setTimeout(() => clientLoadingStatus.style.display = 'none', 2000);
}
}
function populateClientDropdowns() {
const clientDropdowns = document.querySelectorAll('select[name^="rows"][name$="[idclient]"]');
clientDropdowns.forEach(dropdown => {
const currentValue = dropdown.getAttribute('data-current-value') || '';
dropdown.innerHTML = '<option value="">Select a client...</option>';
clientData.forEach(client => {
const nome = client.Nominativo || "Nome non disponibile";
const id = client.IdCliente || "ID non disponibile";
// CodiceCliente es: "Bl01858_E" -> vogliamo "E"
const codiceCliente = (client.CodiceCliente || '').toString().trim();
const suffix = (codiceCliente.split('_')[1] || '').trim(); // parte dopo "_"
const shortCode = suffix || '--';
const option = new Option(`${nome.trim()} - ${shortCode} (ID: ${id})`, id);
if (String(id) === String(currentValue)) {
option.selected = true;
}
dropdown.add(option);
});
// Ripristina il valore corrente
if (currentValue) {
dropdown.value = currentValue;
dropdown.setAttribute('data-restoring', '');
const event = new Event('change', {
bubbles: true
});
dropdown.dispatchEvent(event);
dropdown.removeAttribute('data-restoring');
}
});
}
loadClients();
document.getElementById('clientSelect').addEventListener('change', function() {
const gridCell = this.closest('.grid-cell');
const event = new Event('change', {
bubbles: true
});
gridCell.dispatchEvent(event);
});
document.addEventListener('change', function(e) {
if (e.target.matches('select[name^="rows"][name$="[idclient]"]')) {
const gridCell = e.target.closest('.grid-cell');
const event = new Event('change', {
bubbles: true
});
gridCell.dispatchEvent(event);
}
});
});
</script> </script>
<script> <script>
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
@@ -2381,6 +2371,13 @@ function fixedDefaultValue(array $f): string
}); });
gridCell.dispatchEvent(event); gridCell.dispatchEvent(event);
} }
if (e.target.matches('select[name^="rows"][name$="[cliente_fornitore_id]"]')) {
const gridCell = e.target.closest('.grid-cell');
const event = new Event('change', {
bubbles: true
});
gridCell.dispatchEvent(event);
}
}); });
}); });
</script> </script>
@@ -2397,11 +2394,12 @@ function fixedDefaultValue(array $f): string
// Ensure Select2 dropdowns trigger change events for unsaved changes tracking // Ensure Select2 dropdowns trigger change events for unsaved changes tracking
$('.searchable-client').on('select2:select select2:clear', function(e) { $('.searchable-client').on('select2:select select2:clear', function(e) {
const gridCell = this.closest('.grid-cell'); // Trigger native change on the real <select> element so
const event = new Event('change', { // addEventListener('change') handlers can detect modifications.
const nativeEvent = new Event('change', {
bubbles: true bubbles: true
}); });
gridCell.dispatchEvent(event); this.dispatchEvent(nativeEvent);
}); });
// Update propagate functionality for client dropdown // Update propagate functionality for client dropdown
@@ -2415,7 +2413,7 @@ function fixedDefaultValue(array $f): string
const event = new Event('change', { const event = new Event('change', {
bubbles: true bubbles: true
}); });
clientSelect.closest('.grid-cell').dispatchEvent(event); clientSelect.dispatchEvent(event);
} }
}); });
}); });
@@ -2438,10 +2436,7 @@ function fixedDefaultValue(array $f): string
.map(s => s.trim()) .map(s => s.trim())
.filter(s => s.length > 0); .filter(s => s.length > 0);
// (opzionale) dedup per evitare doppioni tipo "Cap | Cap" if (!parts.length) {
const uniqueParts = [...new Set(parts)];
if (!uniqueParts.length) {
alert('⚠️ Inserisci prima una descrizione nel campo Tested Component.'); alert('⚠️ Inserisci prima una descrizione nel campo Tested Component.');
$input.focus(); $input.focus();
return; return;
@@ -2452,7 +2447,7 @@ function fixedDefaultValue(array $f): string
const errors = []; const errors = [];
// ✅ esegue una chiamata per ogni parte (compatibile con lattuale add_part_quick.php) // ✅ esegue una chiamata per ogni parte (compatibile con lattuale add_part_quick.php)
for (const p of uniqueParts) { for (const p of parts) {
const formData = new FormData(); const formData = new FormData();
formData.append('iddatadb', iddatadb); formData.append('iddatadb', iddatadb);
formData.append('part_description', p); formData.append('part_description', p);
@@ -2626,6 +2621,12 @@ function fixedDefaultValue(array $f): string
getParams: (clientId) => ({ getParams: (clientId) => ({
id_cliente: clientId id_cliente: clientId
}) })
},
'ClienteFornitore': {
source: 'clients'
},
'ClienteAnalisi': {
source: 'clients'
} }
}; };
@@ -2649,6 +2650,21 @@ function fixedDefaultValue(array $f): string
const config = apiFields[fieldKey]; const config = apiFields[fieldKey];
if (!config) return []; if (!config) return [];
if (config.source === 'clients') {
const clients = await ensureClientsLoaded();
const results = clients.map(client => ({
id: client.IdCliente,
text: formatClientLabel(client)
}));
results.sort((a, b) => String(a.text || '').localeCompare(String(b.text || ''), 'it', {
sensitivity: 'base'
}));
fixedFieldDataCache[fieldKey] = results;
return results;
}
const cacheKey = fieldKey + (clientId ? '_' + clientId : ''); const cacheKey = fieldKey + (clientId ? '_' + clientId : '');
if (fixedFieldDataCache[cacheKey]) { if (fixedFieldDataCache[cacheKey]) {
return fixedFieldDataCache[cacheKey]; return fixedFieldDataCache[cacheKey];
@@ -2751,7 +2767,8 @@ function fixedDefaultValue(array $f): string
if (results.length > 12) { if (results.length > 12) {
// Select2 con ricerca // Select2 con ricerca
$select.select2('destroy').empty().select2({ if ($select.hasClass('select2-hidden-accessible')) $select.select2('destroy');
$select.empty().select2({
data: [{ data: [{
id: '', id: '',
text: 'Seleziona...' text: 'Seleziona...'
@@ -2762,9 +2779,7 @@ function fixedDefaultValue(array $f): string
}); });
} else { } else {
// Select nativa senza Select2 // Select nativa senza Select2
try { if ($select.hasClass('select2-hidden-accessible')) $select.select2('destroy');
$select.select2('destroy');
} catch (e) {}
$select.empty(); $select.empty();
$select.append(new Option('Seleziona...', '', true, false)); $select.append(new Option('Seleziona...', '', true, false));
results.forEach(r => { results.forEach(r => {
+13 -26
View File
@@ -24,7 +24,7 @@ $columns = json_decode(urldecode($_POST['columns'] ?? '[]'), true);
$rows = json_decode(urldecode($_POST['rows'] ?? '[]'), true); $rows = json_decode(urldecode($_POST['rows'] ?? '[]'), true);
$excelrows = json_decode(urldecode($_POST['excelrows'] ?? '[]'), true); $excelrows = json_decode(urldecode($_POST['excelrows'] ?? '[]'), true);
$newFilename = htmlspecialchars($_POST['filename']); $newFilename = $_POST['filename'];
$_SESSION['template_id'] = $template_id; $_SESSION['template_id'] = $template_id;
$_SESSION['selected_rows'] = $selected_rows; $_SESSION['selected_rows'] = $selected_rows;
@@ -47,7 +47,7 @@ $pdo = $db->getConnection();
$importReferenceCode = date('YmdHis') . '-' . uniqid(); $importReferenceCode = date('YmdHis') . '-' . uniqid();
// Recupera tutti i mapping dal template // 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]); $stmt->execute([$template_id]);
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC); $allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -126,7 +126,7 @@ foreach ($selected_rows as $rowIndex) {
case 'Testo': case 'Testo':
case 'VARCHAR': case 'VARCHAR':
default: default:
$fieldValue = !empty($fieldValue) ? htmlspecialchars((string)$fieldValue) : ($mapping['manual_default'] ?? ''); $fieldValue = !empty($fieldValue) ? (string)$fieldValue : ($mapping['manual_default'] ?? '');
break; break;
} }
} else { } else {
@@ -135,6 +135,15 @@ foreach ($selected_rows as $rowIndex) {
$fieldValue = date('Y-m-d'); $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 === '')) { if ($mapping['is_required'] && (is_null($fieldValue) || $fieldValue === '')) {
error_log("Required field missing for mapping ID: " . $mapping['id'] . ", field: " . $mapping['field_label']); 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; $_SESSION['inserted_ids'] = $insertedIds;
$params = [ header("Location: imported.php?id=" . urlencode($template_id) . "&importref=" . urlencode($importReferenceCode));
'template_id' => $template_id,
'filename' => $newFilename,
];
?>
<form id="redirectForm" action="import_edit2.php" method="post">
<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
exit; exit;
?> ?>
+37 -9
View File
@@ -143,9 +143,8 @@ error_log("Loaded template: " . print_r($template, true));
<div class="page-content"> <div class="page-content">
<?php include('top_stat_widget.php'); ?> <?php include('top_stat_widget.php'); ?>
<div class="mb-3 text"> <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="imported.php?id=<?= $id ?>" 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="tolims.php?id=<?= $id ?>" class="btn btn-success">To LIMS (l)</a>
<a href="historical_trf.php?id=<?= $id ?>&status=l" class="btn btn-success">To LIMS (l)</a>
</div> </div>
<div class="card radius-10"> <div class="card radius-10">
<div class="card-header"> <div class="card-header">
@@ -330,7 +329,10 @@ error_log("Loaded template: " . print_r($template, true));
<thead> <thead>
<tr> <tr>
<th><input type="checkbox" id="selectAll"> Seleziona</th> <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>
<tr class="column-filters"> <tr class="column-filters">
<th></th> <th></th>
@@ -374,16 +376,32 @@ error_log("Loaded template: " . print_r($template, true));
} }
selectAllCheckbox.addEventListener('change', function() { selectAllCheckbox.addEventListener('change', function() {
checkboxes.forEach(checkbox => { const visibleRows = Array.from(document.querySelectorAll('.table tbody tr'))
.filter(row => row.style.display !== 'none');
visibleRows.forEach(row => {
const checkbox = row.querySelector('.row-checkbox');
if (checkbox) {
checkbox.checked = this.checked; checkbox.checked = this.checked;
}
}); });
updateProceedButton(); updateProceedButton();
}); });
checkboxes.forEach(checkbox => { checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() { checkbox.addEventListener('change', function() {
console.log('Checkbox changed, checked:', this.checked, 'excelrow:', this.dataset.excelrow); console.log('Checkbox changed, checked:', this.checked, 'excelrow:', this.dataset.excelrow);
selectAllCheckbox.checked = Array.from(checkboxes).every(cb => cb.checked);
const visibleCheckboxes = Array.from(document.querySelectorAll('.table tbody tr'))
.filter(row => row.style.display !== 'none')
.map(row => row.querySelector('.row-checkbox'))
.filter(cb => cb !== null);
selectAllCheckbox.checked =
visibleCheckboxes.length > 0 &&
visibleCheckboxes.every(cb => cb.checked);
updateProceedButton(); updateProceedButton();
}); });
}); });
@@ -431,12 +449,11 @@ error_log("Loaded template: " . print_r($template, true));
function applyColumnFilters() { function applyColumnFilters() {
rows.forEach(row => { rows.forEach(row => {
// Le celle di data partono da index 1 (perché index 0 è checkbox)
let visible = true; let visible = true;
for (const [colIndexStr, filterValue] of Object.entries(activeFilters)) { for (const [colIndexStr, filterValue] of Object.entries(activeFilters)) {
const colIndex = parseInt(colIndexStr, 10); // 0..N-1 sulle colonne dati const colIndex = parseInt(colIndexStr, 10);
const cell = row.cells[colIndex + 1]; // +1 per saltare la colonna checkbox const cell = row.cells[colIndex + 1];
const cellText = (cell?.textContent || '').toLowerCase(); const cellText = (cell?.textContent || '').toLowerCase();
const searchText = (filterValue || '').toLowerCase().trim(); const searchText = (filterValue || '').toLowerCase().trim();
@@ -449,6 +466,17 @@ error_log("Loaded template: " . print_r($template, true));
row.style.display = visible ? '' : 'none'; row.style.display = visible ? '' : 'none';
}); });
const visibleCheckboxes = Array.from(document.querySelectorAll('.table tbody tr'))
.filter(row => row.style.display !== 'none')
.map(row => row.querySelector('.row-checkbox'))
.filter(cb => cb !== null);
selectAllCheckbox.checked =
visibleCheckboxes.length > 0 &&
visibleCheckboxes.every(cb => cb.checked);
updateProceedButton();
} }
filterInputs.forEach(input => { filterInputs.forEach(input => {
File diff suppressed because it is too large Load Diff
+2
View File
@@ -26,6 +26,8 @@ $nameuser = $user->present()->first_name;
$surnameuser = $user->present()->last_name; $surnameuser = $user->present()->last_name;
$emailuser = $user->present()->email; $emailuser = $user->present()->email;
$avatar = $user->present()->avatar; $avatar = $user->present()->avatar;
$lims_user_id = $user->lims_user_id ?? '';
$lims_global_user_id = $user->lims_global_user_id ?? '';
$kindofrole = $user->present()->role_id; $kindofrole = $user->present()->role_id;
+96 -15
View File
@@ -1,6 +1,6 @@
<?php include('include/headscript.php'); <?php include('include/headscript.php');
// Recupera tutte le routine dal database // Retrieve all routines from database
$db = DBHandlerSelect::getInstance(); $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection(); $pdo = $db->getConnection();
$stmt = $pdo->prepare("SELECT * FROM routine"); $stmt = $pdo->prepare("SELECT * FROM routine");
@@ -18,26 +18,29 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<?php include('cssinclude.php'); ?> <?php include('cssinclude.php'); ?>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<title>Insert XLS Template <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title> <title>Insert Template <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
</head> </head>
<body> <body>
<div class="wrapper"> <div class="wrapper">
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
<div class="page-wrapper"> <div class="page-wrapper">
<div class="page-content"> <div class="page-content">
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <div class="card-header">
<h5 class="mb-0">Insert new XLS Template</h5> <h5 class="mb-0">Insert New Template</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<p class="mb-2">Fill the following form in order to create a new import XLS template</p> <p class="mb-2">Fill the following form in order to create a new import template</p>
<p class="mb-2">Mandatory Fields</p> <p class="mb-2">Mandatory Fields</p>
<ul class="mb-0"> <ul class="mb-0">
<li>Template Name</li> <li>Template Name</li>
<li>Row Header and Column Header: where the title of the excel starts</li> <li>Source Type</li>
<li>Schema and client</li> <li>Schema and Client</li>
<li>Row Header and Column Header only for XLS templates</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -50,22 +53,33 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
</div> </div>
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="col-12"> <div class="col-12">
<form id="insertTemplateForm" method="POST"> <form id="insertTemplateForm" method="POST">
<div class="mb-3"> <div class="mb-3">
<label class="form-label"><?= htmlspecialchars($templatename, ENT_QUOTES, 'UTF-8'); ?> *</label> <label class="form-label"><?= htmlspecialchars($templatename, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="text" name="name" class="form-control" required> <input type="text" name="name" class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label"><?= htmlspecialchars($rowheader, ENT_QUOTES, 'UTF-8'); ?> *</label> <label class="form-label">Source Type *</label>
<input type="number" name="header_row" class="form-control" required> <select name="source_type" id="sourceType" class="form-control" required>
<option value="XLS" selected>XLS</option>
<option value="API">API</option>
</select>
<small class="text-muted">Choose the source used by this template</small>
</div> </div>
<div class="mb-3"> <div class="mb-3" id="headerRowWrapper">
<label class="form-label"><?= htmlspecialchars($rowheader, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="number" name="header_row" id="headerRow" class="form-control" value="1" required>
</div>
<div class="mb-3" id="startColumnWrapper">
<label class="form-label"><?= htmlspecialchars($columnheader, ENT_QUOTES, 'UTF-8'); ?> *</label> <label class="form-label"><?= htmlspecialchars($columnheader, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="text" name="start_column" class="form-control" required> <input type="text" name="start_column" id="startColumn" class="form-control" value="A" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -86,12 +100,12 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Button Background Color</label> <label class="form-label">Button Background Color</label>
<input type="color" name="button_bg_color" class="form-control" value="#007bff"> <input type="color" name="button_bg_color" class="form-control form-control-color" value="#007bff">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Button Text Color</label> <label class="form-label">Button Text Color</label>
<input type="color" name="button_text_color" class="form-control" value="#ffffff"> <input type="color" name="button_text_color" class="form-control form-control-color" value="#ffffff">
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -125,6 +139,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<div id="routineDetails" class="mt-2" style="display: none;"> <div id="routineDetails" class="mt-2" style="display: none;">
<h6>Routine Details</h6> <h6>Routine Details</h6>
<p><strong>Name:</strong> <span id="routineName"></span></p> <p><strong>Name:</strong> <span id="routineName"></span></p>
@@ -142,8 +157,10 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="overlay toggle-icon"></div> <div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a> <a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?> <?php include('include/footer.php'); ?>
@@ -167,6 +184,12 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const routineAction2 = document.getElementById("routineAction2"); const routineAction2 = document.getElementById("routineAction2");
const routineAction3 = document.getElementById("routineAction3"); const routineAction3 = document.getElementById("routineAction3");
const sourceType = document.getElementById("sourceType");
const headerRowWrapper = document.getElementById("headerRowWrapper");
const startColumnWrapper = document.getElementById("startColumnWrapper");
const headerRow = document.getElementById("headerRow");
const startColumn = document.getElementById("startColumn");
if (!form || !clientLoadingStatus || !schemaLoadingStatus || !routineSelect || !routineDetails) { if (!form || !clientLoadingStatus || !schemaLoadingStatus || !routineSelect || !routineDetails) {
alert("Errore: Uno o più elementi della pagina non sono stati trovati. Contatta l'amministratore."); alert("Errore: Uno o più elementi della pagina non sono stati trovati. Contatta l'amministratore.");
return; return;
@@ -187,30 +210,66 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
allowClear: true allowClear: true
}); });
function updateSourceFields() {
const selectedSource = sourceType.value;
if (selectedSource === 'API') {
headerRowWrapper.style.opacity = '0.6';
startColumnWrapper.style.opacity = '0.6';
headerRow.required = false;
startColumn.required = false;
headerRow.disabled = true;
startColumn.disabled = true;
} else {
headerRowWrapper.style.opacity = '1';
startColumnWrapper.style.opacity = '1';
headerRow.required = true;
startColumn.required = true;
headerRow.disabled = false;
startColumn.disabled = false;
}
}
sourceType.addEventListener('change', updateSourceFields);
updateSourceFields();
async function loadClients() { async function loadClients() {
try { try {
clientLoadingStatus.style.display = 'inline'; clientLoadingStatus.style.display = 'inline';
clientLoadingStatus.textContent = 'Recupero clienti in corso...'; clientLoadingStatus.textContent = 'Recupero clienti in corso...';
const response = await fetch("get_clienti.php", { const response = await fetch("get_clienti.php", {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
} }
}); });
const data = await response.json(); const data = await response.json();
if (!response.ok) throw new Error(data.error || `Errore HTTP: ${response.status}`);
if (!response.ok) {
throw new Error(data.error || `Errore HTTP: ${response.status}`);
}
const select = document.getElementById("clientSelect"); const select = document.getElementById("clientSelect");
select.innerHTML = '<option value="">Select a client...</option>'; select.innerHTML = '<option value="">Select a client...</option>';
data.value.forEach(client => { data.value.forEach(client => {
const nome = client.Nominativo || "Nome non disponibile"; const nome = client.Nominativo || "Nome non disponibile";
const id = client.IdCliente || "ID non disponibile"; const id = client.IdCliente || "ID non disponibile";
const option = new Option(`${nome.trim()} (ID: ${id})`, id); const option = new Option(`${nome.trim()} (ID: ${id})`, id);
select.add(option); select.add(option);
}); });
$(select).trigger('change'); $(select).trigger('change');
clientLoadingStatus.textContent = "Clienti caricati."; clientLoadingStatus.textContent = "Clienti caricati.";
} catch (error) { } catch (error) {
clientLoadingStatus.textContent = "Errore nel caricamento."; clientLoadingStatus.textContent = "Errore nel caricamento.";
Swal.fire({ Swal.fire({
title: "Errore!", title: "Errore!",
text: "Impossibile caricare i clienti: " + error.message, text: "Impossibile caricare i clienti: " + error.message,
@@ -226,26 +285,43 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
try { try {
schemaLoadingStatus.style.display = 'inline'; schemaLoadingStatus.style.display = 'inline';
schemaLoadingStatus.textContent = 'Caricamento schemi in corso...'; schemaLoadingStatus.textContent = 'Caricamento schemi in corso...';
const response = await fetch("get_schemi.php", { const response = await fetch("get_schemi.php", {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
} }
}); });
const data = await response.json(); const data = await response.json();
if (!response.ok) throw new Error(data.error || `Errore HTTP: ${response.status}`);
if (!response.ok) {
throw new Error(data.error || `Errore HTTP: ${response.status}`);
}
const select = document.getElementById("schemaSelect"); const select = document.getElementById("schemaSelect");
select.innerHTML = '<option value="">Select a schema...</option>'; select.innerHTML = '<option value="">Select a schema...</option>';
data.value.forEach(schema => {
const sortedSchemas = [...data.value].sort((a, b) => {
const nomeA = (a.Nome || "").trim().toLowerCase();
const nomeB = (b.Nome || "").trim().toLowerCase();
return nomeA.localeCompare(nomeB, 'it', {
sensitivity: 'base'
});
});
sortedSchemas.forEach(schema => {
const nome = schema.Nome || "Nome non disponibile"; const nome = schema.Nome || "Nome non disponibile";
const id = schema.IdSchemaCustomFields || "ID non disponibile"; const id = schema.IdSchemaCustomFields || "ID non disponibile";
const option = new Option(`${nome.trim()} (ID: ${id})`, id); const option = new Option(`${nome.trim()} (ID: ${id})`, id);
select.add(option); select.add(option);
}); });
$(select).trigger('change'); $(select).trigger('change');
schemaLoadingStatus.textContent = "Schemi caricati."; schemaLoadingStatus.textContent = "Schemi caricati.";
} catch (error) { } catch (error) {
schemaLoadingStatus.textContent = "Errore nel caricamento."; schemaLoadingStatus.textContent = "Errore nel caricamento.";
Swal.fire({ Swal.fire({
title: "Errore!", title: "Errore!",
text: "Impossibile caricare gli schemi: " + error.message, text: "Impossibile caricare gli schemi: " + error.message,
@@ -270,6 +346,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
}); });
} }
} }
loadData(); loadData();
const routines = <?php echo json_encode($routines); ?>; const routines = <?php echo json_encode($routines); ?>;
@@ -277,8 +354,10 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
function updateRoutineDetails() { function updateRoutineDetails() {
const selectedId = routineSelect.value; const selectedId = routineSelect.value;
routineDetails.style.display = selectedId ? 'block' : 'none'; routineDetails.style.display = selectedId ? 'block' : 'none';
if (selectedId) { if (selectedId) {
const routine = routines.find(r => r.idroutine == selectedId); const routine = routines.find(r => r.idroutine == selectedId);
if (routine) { if (routine) {
routineName.textContent = routine.name || 'N/A'; routineName.textContent = routine.name || 'N/A';
routineDescription.textContent = routine.description || 'N/A'; routineDescription.textContent = routine.description || 'N/A';
@@ -300,6 +379,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
routineAction3.textContent = ''; routineAction3.textContent = '';
} }
} }
routineSelect.addEventListener('change', updateRoutineDetails); routineSelect.addEventListener('change', updateRoutineDetails);
updateRoutineDetails(); updateRoutineDetails();
@@ -350,6 +430,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const nameMatch = optionText.match(/^(.+?)(?:\s*\(ID:\s*\d+\))?$/); const nameMatch = optionText.match(/^(.+?)(?:\s*\(ID:\s*\d+\))?$/);
schemaName = nameMatch ? nameMatch[1].trim() : optionText; schemaName = nameMatch ? nameMatch[1].trim() : optionText;
} }
formData.append("idschema", schemaId); formData.append("idschema", schemaId);
formData.append("schemaname", schemaName); formData.append("schemaname", schemaName);
@@ -0,0 +1,60 @@
CAMPIONE #0
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 564663,
"Matrice": 3028,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "Parte 1",
"ConsegnaRichiesta": "2026-03-31"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 736244,
"CodiceCampione": "11004",
"CodiceCampioneWeb": "11004",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-17T14:52:30.3143366+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-03-31T00:00:00+02:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "Parte 1",
"GruppiRicercati": null
}
---
CAMPIONE #1
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 564663,
"Matrice": 3028,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "Parte 2",
"ConsegnaRichiesta": "2026-03-31"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 736245,
"CodiceCampione": "11005",
"CodiceCampioneWeb": "11005",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-17T14:52:31.9631757+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-03-31T00:00:00+02:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "Parte 2",
"GruppiRicercati": null
}
---
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(564663)/ImportaCommessa' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"IdUtente": 285
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 564663,
"CodiceCommessa": "26C0059",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Elaborata",
"DataCreazioneWeb": "2026-03-17T14:52:29.347+01:00",
"CodiceCommessaWeb": "26C0059",
"DataInviatoWeb": "2026-03-17T14:52:38.99+01:00",
"Richiedente": "From TRFSmart Application",
"Descrizione": "From TRFSmart Application"
}
@@ -0,0 +1,48 @@
Photos for CommessaWeb 564663 (iddatadb=1268):
Total photos found: 2, campioni: 2
=== Campione 736244 (main) ===
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione(736244)/UploadCampioneFile' \
--header 'Authorization: Bearer ••••••' \
--form 'file=@C:\xampp\htdocs\trf_certest\public\photostrf\1268-20260317134828-1e672dd9-5420-4432-b422-02d8d271c178.jpg' \
--form 'StampaNelRapporto=true' \
--form 'PrimaPagina=true'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CampioneFiles\/$entity",
"IdCampioneFile": 1784768,
"FileName": "1268-20260317134828-1e672dd9-5420-4432-b422-02d8d271c178.jpg",
"Web": false,
"AllegaAlRapporto": false,
"StampaNelRapporto": false,
"ReportGermania": false,
"PrimaPagina": false,
"Duplica": false,
"LastUpdate": "2026-03-17T14:52:33.9816658+01:00",
"Titolo": null
}
---
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione(736244)/UploadCampioneFile' \
--header 'Authorization: Bearer ••••••' \
--form 'file=@C:\xampp\htdocs\trf_certest\public\photostrf\1268-20260317134828-104a2ca5-8a4c-4df8-b2ca-a28f518b7b25.jpg' \
--form 'StampaNelRapporto=false' \
--form 'PrimaPagina=false'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CampioneFiles\/$entity",
"IdCampioneFile": 1784769,
"FileName": "1268-20260317134828-104a2ca5-8a4c-4df8-b2ca-a28f518b7b25.jpg",
"Web": false,
"AllegaAlRapporto": false,
"StampaNelRapporto": false,
"ReportGermania": false,
"PrimaPagina": false,
"Duplica": false,
"LastUpdate": "2026-03-17T14:52:35.2759882+01:00",
"Titolo": null
}
---
@@ -0,0 +1,18 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(564663)/InviaCommessa' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 564663,
"CodiceCommessa": "26C0059",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Nuova",
"DataCreazioneWeb": "2026-03-17T14:52:29.347+01:00",
"CodiceCommessaWeb": "26C0059",
"DataInviatoWeb": "2026-03-17T14:52:38.9894528+01:00",
"Richiedente": "From TRFSmart Application",
"Descrizione": "From TRFSmart Application"
}
@@ -0,0 +1,226 @@
curl --location --request PATCH 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(564663)' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"CommesseCustomFields": [
{
"IdCommesseCustomFields": 23308665,
"Valore": "2026-03-17T13:52:36+00:00"
},
{
"IdCommesseCustomFields": 23308666,
"Valore": "13:52"
},
{
"IdCommesseCustomFields": 23308667,
"Valore": "9299"
},
{
"IdCommesseCustomFields": 23308668,
"Valore": "2026-03-17T13:52:36+00:00"
},
{
"IdCommesseCustomFields": 23308669,
"Valore": "aaa"
},
{
"IdCommesseCustomFields": 23308670,
"Valore": "1"
},
{
"IdCommesseCustomFields": 23308676,
"Valore": "673"
},
{
"IdCommesseCustomFields": 23308677,
"Valore": "674"
},
{
"IdCommesseCustomFields": 23308678,
"Valore": "4410"
},
{
"IdCommesseCustomFields": 23308680,
"Valore": "1468"
},
{
"IdCommesseCustomFields": 23308681,
"Valore": "1604"
},
{
"IdCommesseCustomFields": 23308682,
"Valore": "12875"
},
{
"IdCommesseCustomFields": 23308683,
"Valore": "8755"
},
{
"IdCommesseCustomFields": 23308684,
"Valore": ""
},
{
"IdCommesseCustomFields": 23308644,
"Valore": "13497"
},
{
"IdCommesseCustomFields": 23308645,
"Valore": "AAA"
},
{
"IdCommesseCustomFields": 23308646,
"Valore": "Example Description"
},
{
"IdCommesseCustomFields": 23308685,
"Valore": "1892"
},
{
"IdCommesseCustomFields": 23308686,
"Valore": "1111"
},
{
"IdCommesseCustomFields": 23308687,
"Valore": "2026-03-02T13:52:36+00:00"
},
{
"IdCommesseCustomFields": 23308688,
"Valore": "2026-03-17T13:52:36+00:00"
},
{
"IdCommesseCustomFields": 23308689,
"Valore": "2026-03-17T13:52:36+00:00"
},
{
"IdCommesseCustomFields": 23308690,
"Valore": "1978"
},
{
"IdCommesseCustomFields": 23308691,
"Valore": "13:52"
},
{
"IdCommesseCustomFields": 23308692,
"Valore": "14:44"
},
{
"IdCommesseCustomFields": 23308693,
"Valore": "14:00"
},
{
"IdCommesseCustomFields": 23308694,
"Valore": "2026-03-17T13:52:36+00:00"
},
{
"IdCommesseCustomFields": 23308695,
"Valore": "8596"
},
{
"IdCommesseCustomFields": 23308696,
"Valore": "8600"
},
{
"IdCommesseCustomFields": 23308697,
"Valore": "8605"
},
{
"IdCommesseCustomFields": 23308647,
"Valore": "L209A4M00130M7280"
},
{
"IdCommesseCustomFields": 23308648,
"Valore": "1"
},
{
"IdCommesseCustomFields": 23308649,
"Valore": "1"
},
{
"IdCommesseCustomFields": 23308650,
"Valore": "264"
},
{
"IdCommesseCustomFields": 23308651,
"Valore": "1589"
},
{
"IdCommesseCustomFields": 23308652,
"Valore": "AAA"
},
{
"IdCommesseCustomFields": 23308653,
"Valore": "3055"
},
{
"IdCommesseCustomFields": 23308654,
"Valore": "aaa"
},
{
"IdCommesseCustomFields": 23308655,
"Valore": "qqq"
},
{
"IdCommesseCustomFields": 23308656,
"Valore": "1"
},
{
"IdCommesseCustomFields": 23308657,
"Valore": "1"
},
{
"IdCommesseCustomFields": 23308658,
"Valore": "qqq"
},
{
"IdCommesseCustomFields": 23308659,
"Valore": "236"
},
{
"IdCommesseCustomFields": 23308660,
"Valore": "1208"
},
{
"IdCommesseCustomFields": 23308661,
"Valore": "ciao"
},
{
"IdCommesseCustomFields": 23308662,
"Valore": "bbb"
},
{
"IdCommesseCustomFields": 23308663,
"Valore": "12234"
},
{
"IdCommesseCustomFields": 23308664,
"Valore": "MONCLER"
},
{
"IdCommesseCustomFields": 23308671,
"Valore": "L209A4M00130M7280 - PACEY2"
},
{
"IdCommesseCustomFields": 23308672,
"Valore": "4678"
},
{
"IdCommesseCustomFields": 23308673,
"Valore": "1"
},
{
"IdCommesseCustomFields": 23308674,
"Valore": ""
},
{
"IdCommesseCustomFields": 23308675,
"Valore": "2450"
},
{
"IdCommesseCustomFields": 23308679,
"Valore": "744"
}
]
}'
RESPONSE:
null
@@ -0,0 +1,120 @@
CAMPIONE #0
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 564817,
"Matrice": 3028,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "AAA",
"ConsegnaRichiesta": "2026-01-29"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 736487,
"CodiceCampione": "11016",
"CodiceCampioneWeb": "11016",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-18T10:54:03.4537462+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-01-29T00:00:00+01:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "AAA",
"GruppiRicercati": null
}
---
CAMPIONE #1
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 564817,
"Matrice": 3028,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "CCC",
"ConsegnaRichiesta": "2026-01-29"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 736488,
"CodiceCampione": "11017",
"CodiceCampioneWeb": "11017",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-18T10:54:04.8623753+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-01-29T00:00:00+01:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "CCC",
"GruppiRicercati": null
}
---
CAMPIONE #2
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 564817,
"Matrice": 3028,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "DDD",
"ConsegnaRichiesta": "2026-01-29"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 736489,
"CodiceCampione": "11018",
"CodiceCampioneWeb": "11018",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-18T10:54:06.2840533+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-01-29T00:00:00+01:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "DDD",
"GruppiRicercati": null
}
---
CAMPIONE #3
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 564817,
"Matrice": 3028,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "EEE",
"ConsegnaRichiesta": "2026-01-29"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 736490,
"CodiceCampione": "11019",
"CodiceCampioneWeb": "11019",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-18T10:54:07.8853452+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-01-29T00:00:00+01:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "EEE",
"GruppiRicercati": null
}
---
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(564817)/ImportaCommessa' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"IdUtente": 285
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 564817,
"CodiceCommessa": "26C0071",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Elaborata",
"DataCreazioneWeb": "2026-03-18T10:54:02.187+01:00",
"CodiceCommessaWeb": "26C0071",
"DataInviatoWeb": "2026-03-18T10:54:14.727+01:00",
"Richiedente": "From TRFSmart Application",
"Descrizione": "From TRFSmart Application"
}
@@ -0,0 +1,48 @@
Photos for CommessaWeb 564817 (iddatadb=1270):
Total photos found: 2, campioni: 4
=== Campione 736487 (main) ===
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione(736487)/UploadCampioneFile' \
--header 'Authorization: Bearer ••••••' \
--form 'file=@C:\xampp\htdocs\trf_certest\public\photostrf\1270-20260318095305-1e672dd9-5420-4432-b422-02d8d271c178.jpg' \
--form 'StampaNelRapporto=true' \
--form 'PrimaPagina=true'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CampioneFiles\/$entity",
"IdCampioneFile": 1785492,
"FileName": "1270-20260318095305-1e672dd9-5420-4432-b422-02d8d271c178.jpg",
"Web": false,
"AllegaAlRapporto": false,
"StampaNelRapporto": false,
"ReportGermania": false,
"PrimaPagina": false,
"Duplica": false,
"LastUpdate": "2026-03-18T10:54:09.5438326+01:00",
"Titolo": null
}
---
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione(736487)/UploadCampioneFile' \
--header 'Authorization: Bearer ••••••' \
--form 'file=@C:\xampp\htdocs\trf_certest\public\photostrf\1270-20260318095305-104a2ca5-8a4c-4df8-b2ca-a28f518b7b25.jpg' \
--form 'StampaNelRapporto=false' \
--form 'PrimaPagina=false'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CampioneFiles\/$entity",
"IdCampioneFile": 1785493,
"FileName": "1270-20260318095305-104a2ca5-8a4c-4df8-b2ca-a28f518b7b25.jpg",
"Web": false,
"AllegaAlRapporto": false,
"StampaNelRapporto": false,
"ReportGermania": false,
"PrimaPagina": false,
"Duplica": false,
"LastUpdate": "2026-03-18T10:54:10.8237002+01:00",
"Titolo": null
}
---
@@ -0,0 +1,18 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(564817)/InviaCommessa' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 564817,
"CodiceCommessa": "26C0071",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Nuova",
"DataCreazioneWeb": "2026-03-18T10:54:02.187+01:00",
"CodiceCommessaWeb": "26C0071",
"DataInviatoWeb": "2026-03-18T10:54:14.7252132+01:00",
"Richiedente": "From TRFSmart Application",
"Descrizione": "From TRFSmart Application"
}
@@ -0,0 +1,226 @@
curl --location --request PATCH 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(564817)' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"CommesseCustomFields": [
{
"IdCommesseCustomFields": 23315543,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315544,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315545,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315546,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315547,
"Valore": "/"
},
{
"IdCommesseCustomFields": 23315548,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315554,
"Valore": "673"
},
{
"IdCommesseCustomFields": 23315555,
"Valore": "674"
},
{
"IdCommesseCustomFields": 23315556,
"Valore": "669"
},
{
"IdCommesseCustomFields": 23315558,
"Valore": "1469"
},
{
"IdCommesseCustomFields": 23315559,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315560,
"Valore": "12875"
},
{
"IdCommesseCustomFields": 23315561,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315562,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315522,
"Valore": "13497"
},
{
"IdCommesseCustomFields": 23315523,
"Valore": "20262"
},
{
"IdCommesseCustomFields": 23315524,
"Valore": "ART. PEACH"
},
{
"IdCommesseCustomFields": 23315563,
"Valore": "1892"
},
{
"IdCommesseCustomFields": 23315564,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315565,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315566,
"Valore": "2026-03-18T09:54:11+00:00"
},
{
"IdCommesseCustomFields": 23315567,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315568,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315569,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315570,
"Valore": "09:53"
},
{
"IdCommesseCustomFields": 23315571,
"Valore": "10:37"
},
{
"IdCommesseCustomFields": 23315572,
"Valore": "2026-03-18T09:54:11+00:00"
},
{
"IdCommesseCustomFields": 23315573,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315574,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315575,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315525,
"Valore": "L209A4M00130M7280"
},
{
"IdCommesseCustomFields": 23315526,
"Valore": "BLACK"
},
{
"IdCommesseCustomFields": 23315527,
"Valore": "PE007J"
},
{
"IdCommesseCustomFields": 23315528,
"Valore": "264"
},
{
"IdCommesseCustomFields": 23315529,
"Valore": "420"
},
{
"IdCommesseCustomFields": 23315530,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315531,
"Valore": "15357"
},
{
"IdCommesseCustomFields": 23315532,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315533,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315534,
"Valore": "P"
},
{
"IdCommesseCustomFields": 23315535,
"Valore": "RM-ACC"
},
{
"IdCommesseCustomFields": 23315536,
"Valore": " CONCERIA M2 SRL"
},
{
"IdCommesseCustomFields": 23315537,
"Valore": "344"
},
{
"IdCommesseCustomFields": 23315538,
"Valore": "520"
},
{
"IdCommesseCustomFields": 23315539,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315540,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315541,
"Valore": "Not provided"
},
{
"IdCommesseCustomFields": 23315542,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315549,
"Valore": ""
},
{
"IdCommesseCustomFields": 23315550,
"Valore": "4678"
},
{
"IdCommesseCustomFields": 23315551,
"Valore": "Leather&amp;amp;Fur"
},
{
"IdCommesseCustomFields": 23315552,
"Valore": "278"
},
{
"IdCommesseCustomFields": 23315553,
"Valore": "2089"
},
{
"IdCommesseCustomFields": 23315557,
"Valore": "745"
}
]
}'
RESPONSE:
null
@@ -0,0 +1,90 @@
CAMPIONE #0
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 565085,
"Matrice": 7714,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "Parte AAA",
"ConsegnaRichiesta": "2026-04-08"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 736899,
"CodiceCampione": "11038",
"CodiceCampioneWeb": "11038",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-19T10:03:19.8547279+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-04-08T00:00:00+02:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "Parte AAA",
"GruppiRicercati": null
}
---
CAMPIONE #1
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 565085,
"Matrice": 7714,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "Parte BBB",
"ConsegnaRichiesta": "2026-04-08"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 736900,
"CodiceCampione": "11039",
"CodiceCampioneWeb": "11039",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-19T10:03:22.0431135+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-04-08T00:00:00+02:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "Parte BBB",
"GruppiRicercati": null
}
---
CAMPIONE #2
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 565085,
"Matrice": 7714,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "Parte CCC",
"ConsegnaRichiesta": "2026-04-08"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 736901,
"CodiceCampione": "11040",
"CodiceCampioneWeb": "11040",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-19T10:03:23.6170597+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-04-08T00:00:00+02:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "Parte CCC",
"GruppiRicercati": null
}
---
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(565085)/ImportaCommessa' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"IdUtente": 285
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 565085,
"CodiceCommessa": "26C0089",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Elaborata",
"DataCreazioneWeb": "2026-03-19T10:03:18.653+01:00",
"CodiceCommessaWeb": "26C0089",
"DataInviatoWeb": "2026-03-19T10:03:30.603+01:00",
"Richiedente": "From TRFSmart Application",
"Descrizione": "From TRFSmart Application"
}
@@ -0,0 +1,48 @@
Photos for CommessaWeb 565085 (iddatadb=1279):
Total photos found: 2, campioni: 3
=== Campione 736899 (main) ===
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione(736899)/UploadCampioneFile' \
--header 'Authorization: Bearer ••••••' \
--form 'file=@C:\xampp\htdocs\trf_certest\public\photostrf\1279-20260319090037-1e672dd9-5420-4432-b422-02d8d271c178.jpg' \
--form 'StampaNelRapporto=true' \
--form 'PrimaPagina=true'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CampioneFiles\/$entity",
"IdCampioneFile": 1786602,
"FileName": "1279-20260319090037-1e672dd9-5420-4432-b422-02d8d271c178.jpg",
"Web": false,
"AllegaAlRapporto": false,
"StampaNelRapporto": false,
"ReportGermania": false,
"PrimaPagina": false,
"Duplica": false,
"LastUpdate": "2026-03-19T10:03:25.2757049+01:00",
"Titolo": null
}
---
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione(736899)/UploadCampioneFile' \
--header 'Authorization: Bearer ••••••' \
--form 'file=@C:\xampp\htdocs\trf_certest\public\photostrf\1279-20260319090037-104a2ca5-8a4c-4df8-b2ca-a28f518b7b25.jpg' \
--form 'StampaNelRapporto=false' \
--form 'PrimaPagina=false'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CampioneFiles\/$entity",
"IdCampioneFile": 1786603,
"FileName": "1279-20260319090037-104a2ca5-8a4c-4df8-b2ca-a28f518b7b25.jpg",
"Web": false,
"AllegaAlRapporto": false,
"StampaNelRapporto": false,
"ReportGermania": false,
"PrimaPagina": false,
"Duplica": false,
"LastUpdate": "2026-03-19T10:03:26.7566613+01:00",
"Titolo": null
}
---
@@ -0,0 +1,18 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(565085)/InviaCommessa' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 565085,
"CodiceCommessa": "26C0089",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Nuova",
"DataCreazioneWeb": "2026-03-19T10:03:18.653+01:00",
"CodiceCommessaWeb": "26C0089",
"DataInviatoWeb": "2026-03-19T10:03:30.6022436+01:00",
"Richiedente": "From TRFSmart Application",
"Descrizione": "From TRFSmart Application"
}
@@ -0,0 +1,226 @@
curl --location --request PATCH 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(565085)' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"CommesseCustomFields": [
{
"IdCommesseCustomFields": 23328248,
"Valore": "18/03/2026"
},
{
"IdCommesseCustomFields": 23328249,
"Valore": "09:00"
},
{
"IdCommesseCustomFields": 23328250,
"Valore": "9299"
},
{
"IdCommesseCustomFields": 23328251,
"Valore": "17/03/2026"
},
{
"IdCommesseCustomFields": 23328252,
"Valore": "/"
},
{
"IdCommesseCustomFields": 23328253,
"Valore": "5"
},
{
"IdCommesseCustomFields": 23328259,
"Valore": "673"
},
{
"IdCommesseCustomFields": 23328260,
"Valore": "674"
},
{
"IdCommesseCustomFields": 23328261,
"Valore": "669"
},
{
"IdCommesseCustomFields": 23328263,
"Valore": "1469"
},
{
"IdCommesseCustomFields": 23328264,
"Valore": "1604"
},
{
"IdCommesseCustomFields": 23328265,
"Valore": "12875"
},
{
"IdCommesseCustomFields": 23328266,
"Valore": "8755"
},
{
"IdCommesseCustomFields": 23328267,
"Valore": ""
},
{
"IdCommesseCustomFields": 23328227,
"Valore": "13497"
},
{
"IdCommesseCustomFields": 23328228,
"Valore": "20262"
},
{
"IdCommesseCustomFields": 23328229,
"Valore": "ART. PEACH"
},
{
"IdCommesseCustomFields": 23328268,
"Valore": "1892"
},
{
"IdCommesseCustomFields": 23328269,
"Valore": ""
},
{
"IdCommesseCustomFields": 23328270,
"Valore": ""
},
{
"IdCommesseCustomFields": 23328271,
"Valore": "19/03/2026"
},
{
"IdCommesseCustomFields": 23328272,
"Valore": "18/03/2026"
},
{
"IdCommesseCustomFields": 23328273,
"Valore": ""
},
{
"IdCommesseCustomFields": 23328274,
"Valore": "09:00"
},
{
"IdCommesseCustomFields": 23328275,
"Valore": "09:03"
},
{
"IdCommesseCustomFields": 23328276,
"Valore": "09:58"
},
{
"IdCommesseCustomFields": 23328277,
"Valore": "19/03/2026"
},
{
"IdCommesseCustomFields": 23328278,
"Valore": ""
},
{
"IdCommesseCustomFields": 23328279,
"Valore": ""
},
{
"IdCommesseCustomFields": 23328280,
"Valore": ""
},
{
"IdCommesseCustomFields": 23328230,
"Valore": "L209B4M00110M7280"
},
{
"IdCommesseCustomFields": 23328231,
"Valore": "BLACK"
},
{
"IdCommesseCustomFields": 23328232,
"Valore": "PE007J"
},
{
"IdCommesseCustomFields": 23328233,
"Valore": "264"
},
{
"IdCommesseCustomFields": 23328234,
"Valore": "420"
},
{
"IdCommesseCustomFields": 23328235,
"Valore": "AAA"
},
{
"IdCommesseCustomFields": 23328236,
"Valore": "3055"
},
{
"IdCommesseCustomFields": 23328237,
"Valore": "AAA"
},
{
"IdCommesseCustomFields": 23328238,
"Valore": "AAA"
},
{
"IdCommesseCustomFields": 23328239,
"Valore": "P"
},
{
"IdCommesseCustomFields": 23328240,
"Valore": "RM-ACC"
},
{
"IdCommesseCustomFields": 23328241,
"Valore": "\u00a0CONCERIA M2 SRL"
},
{
"IdCommesseCustomFields": 23328242,
"Valore": "344"
},
{
"IdCommesseCustomFields": 23328243,
"Valore": "1208"
},
{
"IdCommesseCustomFields": 23328244,
"Valore": "AAA"
},
{
"IdCommesseCustomFields": 23328245,
"Valore": "AAA"
},
{
"IdCommesseCustomFields": 23328246,
"Valore": "Not provided"
},
{
"IdCommesseCustomFields": 23328247,
"Valore": "AAA"
},
{
"IdCommesseCustomFields": 23328254,
"Valore": "aaa"
},
{
"IdCommesseCustomFields": 23328255,
"Valore": "4678"
},
{
"IdCommesseCustomFields": 23328256,
"Valore": "Leather&amp;amp;Fur"
},
{
"IdCommesseCustomFields": 23328257,
"Valore": "278"
},
{
"IdCommesseCustomFields": 23328258,
"Valore": "2089"
},
{
"IdCommesseCustomFields": 23328262,
"Valore": "745"
}
]
}'
RESPONSE:
null
@@ -0,0 +1,28 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Cliente": 3378,
"SchemaCustomField": 82,
"Richiedente": "From TRFSmart Application",
"Descrizione": "From TRFSmart Application",
"ClienteResponsabile": 7598,
"MoltiplicatorePrezzo": 2,
"AnagraficaCertestObject": 8964,
"AnagraficaCertestService": 9012,
"ClienteFornitore": 11208
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 564662,
"CodiceCommessa": "26C0058",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Nuova",
"DataCreazioneWeb": "2026-03-17T14:49:39.4933462+01:00",
"CodiceCommessaWeb": "26C0058",
"DataInviatoWeb": null,
"Richiedente": "From TRFSmart Application",
"Descrizione": "From TRFSmart Application"
}
@@ -0,0 +1,28 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Cliente": 3378,
"SchemaCustomField": 82,
"Richiedente": "From TRFSmart Application",
"Descrizione": "From TRFSmart Application",
"ClienteResponsabile": 7598,
"MoltiplicatorePrezzo": 2,
"AnagraficaCertestObject": 8964,
"AnagraficaCertestService": 9012,
"ClienteFornitore": 11208
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 564663,
"CodiceCommessa": "26C0059",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Nuova",
"DataCreazioneWeb": "2026-03-17T14:52:29.3463415+01:00",
"CodiceCommessaWeb": "26C0059",
"DataInviatoWeb": null,
"Richiedente": "From TRFSmart Application",
"Descrizione": "From TRFSmart Application"
}
@@ -0,0 +1,28 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Cliente": 3378,
"SchemaCustomField": 82,
"Richiedente": "From TRFSmart Application",
"Descrizione": "From TRFSmart Application",
"ClienteResponsabile": 7586,
"MoltiplicatorePrezzo": null,
"AnagraficaCertestObject": 8973,
"AnagraficaCertestService": 9007,
"ClienteFornitore": 11208
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 564817,
"CodiceCommessa": "26C0071",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Nuova",
"DataCreazioneWeb": "2026-03-18T10:54:02.1867227+01:00",
"CodiceCommessaWeb": "26C0071",
"DataInviatoWeb": null,
"Richiedente": "From TRFSmart Application",
"Descrizione": "From TRFSmart Application"
}
@@ -0,0 +1,29 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Cliente": 3378,
"SchemaCustomField": 82,
"Richiedente": "From TRFSmart Application",
"Descrizione": "From TRFSmart Application",
"ClienteResponsabile": 7586,
"MoltiplicatorePrezzo": 2,
"AnagraficaCertestObject": 8973,
"AnagraficaCertestService": 9007,
"ClienteFornitore": 4505,
"ClienteAnalisi": null
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 565085,
"CodiceCommessa": "26C0089",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Nuova",
"DataCreazioneWeb": "2026-03-19T10:03:18.6527414+01:00",
"CodiceCommessaWeb": "26C0089",
"DataInviatoWeb": null,
"Richiedente": "From TRFSmart Application",
"Descrizione": "From TRFSmart Application"
}
+268 -48
View File
@@ -163,6 +163,12 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
width: 100% !important; width: 100% !important;
} }
.xls-columns option.used-option {
background-color: #fff3cd;
color: #856404;
font-weight: 600;
}
/* Make Title column narrower + ellipsis */ /* Make Title column narrower + ellipsis */
#schemaFieldsTable td.title-col { #schemaFieldsTable td.title-col {
max-width: 320px; max-width: 320px;
@@ -198,7 +204,13 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
<div class="card-body"> <div class="card-body">
<div class="mb-4"> <div class="mb-4">
<label class="form-label">Upload XLS Example:</label> <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"> <small id="uploadStatus">
<?php if (!empty($template['sample_xlsx'])): ?> <?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> Current file: <a href="xlstemplates/<?php echo htmlspecialchars($template['sample_xlsx']); ?>" target="_blank"><?php echo htmlspecialchars($template['sample_xlsx']); ?></a>
@@ -414,6 +426,8 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
<?php elseif (in_array($fm['fixed_field_key'], [ <?php elseif (in_array($fm['fixed_field_key'], [
'ClienteResponsabile', 'ClienteResponsabile',
'ClienteFornitore',
'ClienteAnalisi',
'MoltiplicatorePrezzo', 'MoltiplicatorePrezzo',
'AnagraficaCertestObject', 'AnagraficaCertestObject',
'AnagraficaCertestService' 'AnagraficaCertestService'
@@ -506,8 +520,10 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
type: 'array' type: 'array'
}); });
let sheet = workbook.Sheets[workbook.SheetNames[0]]; 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, { let sheetData = XLSX.utils.sheet_to_json(sheet, {
header: 1, header: 1,
@@ -515,7 +531,22 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
raw: false, raw: false,
range: 0 range: 0
}); });
console.log("Dati della riga " + rowIndex + ":", 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]) { if (!sheetData[rowIndex - 1]) {
document.getElementById('schemaFieldsBody').querySelectorAll('select.xls-columns').forEach(select => { document.getElementById('schemaFieldsBody').querySelectorAll('select.xls-columns').forEach(select => {
select.innerHTML = '<option value="">Nessuna intestazione trovata</option>'; select.innerHTML = '<option value="">Nessuna intestazione trovata</option>';
@@ -523,50 +554,234 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
return; return;
} }
let headers = sheetData[rowIndex - 1].slice(startColumn - 1).map(header => header === undefined ? "" : header); // Build logical headers, collapsing merged cells
console.log("Intestazioni estratte:", headers); 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]; availableXlsColumns = [...headers];
usedColumnsFromDB = []; usedColumnsFromDB = [];
saveXlsHeaders(headers); 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;
}
// 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, bestRow + 1, bestStartCol + 1);
updateXlsDropdowns(); updateXlsDropdowns();
}; };
reader.readAsArrayBuffer(file); 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', { fetch('update_xls_headers.php', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify(payload)
template_id: <?php echo $id; ?>,
xls_headers: JSON.stringify(headers)
})
}).then(response => response.json()) }).then(response => response.json())
.then(data => { .then(data => {
if (!data.success) console.error("❌ Error saving XLS headers:", data.message); 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)); .catch(error => console.error("❌ Fetch error:", error));
} }
function updateXlsDropdowns() { function updateXlsDropdowns() {
let usedColumns = Array.from(document.querySelectorAll('select.xls-columns')) let usedColumns = Array.from(document.querySelectorAll('select.xls-columns'))
.filter(select => select.style.display === 'block' && select.value) .map(select => select.value || select.dataset.currentXls || '')
.map(select => select.value) .filter(Boolean)
.concat(usedColumnsFromDB); .concat(usedColumnsFromDB);
let uniqueUsedColumns = [...new Set(usedColumns)];
document.querySelectorAll('select.xls-columns').forEach(select => { document.querySelectorAll('select.xls-columns').forEach(select => {
let currentValue = select.value || select.dataset.currentXls || ''; let currentValue = select.value || select.dataset.currentXls || '';
let options = availableXlsColumns let options = availableXlsColumns
.filter(col => !usedColumns.includes(col) || col === currentValue) .map((col, origIdx) => ({ col, origIdx }))
.map(col => `<option value="${col}" ${col === currentValue ? 'selected' : ''}>${col}</option>`) .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 = 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(''); .join('');
select.innerHTML = '<option value="">Select XLS Column</option>' + options; select.innerHTML = '<option value="">Select XLS Column</option>' + options;
select.dataset.currentXls = currentValue; select.dataset.currentXls = currentValue;
if (currentValue && !options.includes(currentValue)) {
select.value = '';
}
}); });
} }
@@ -1013,33 +1228,6 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
checkbox.checked = !prevChecked; checkbox.checked = !prevChecked;
location.reload(); location.reload();
}); });
} else if (event.target.classList.contains('visible-parts-checkbox')) {
const checkbox = event.target;
const mappingId = checkbox.dataset.mappingId;
const value = checkbox.checked ? 1 : 0;
fetch('update_visible_parts.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
template_id: <?php echo $id; ?>,
mapping_id: mappingId,
value: value
})
})
.then(response => response.json())
.then(data => {
if (!data.success) {
console.error("❌ Error updating is_visible_parts:", data.message);
checkbox.checked = !checkbox.checked;
}
})
.catch(error => {
console.error("❌ Fetch error:", error);
checkbox.checked = !checkbox.checked;
});
} }
}); });
@@ -1079,7 +1267,7 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
mappedColumn = document.createElement('span'); mappedColumn = document.createElement('span');
mappedColumn.className = 'mapped-column'; mappedColumn.className = 'mapped-column';
mappedColumn.style.marginLeft = '5px'; mappedColumn.style.marginLeft = '5px';
tr.querySelector('td:nth-child(5)').appendChild(mappedColumn); tr.querySelector('td:nth-child(6)').appendChild(mappedColumn);
} }
if (!removeBtn) { if (!removeBtn) {
removeBtn = document.createElement('button'); removeBtn = document.createElement('button');
@@ -1087,7 +1275,7 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
removeBtn.textContent = 'X'; removeBtn.textContent = 'X';
removeBtn.style.marginLeft = '5px'; removeBtn.style.marginLeft = '5px';
removeBtn.setAttribute('data-id', mappingId); removeBtn.setAttribute('data-id', mappingId);
tr.querySelector('td:nth-child(5)').appendChild(removeBtn); tr.querySelector('td:nth-child(6)').appendChild(removeBtn);
removeBtn.addEventListener('click', function(e) { removeBtn.addEventListener('click', function(e) {
let tr = e.target.closest('tr'); let tr = e.target.closest('tr');
@@ -1230,6 +1418,8 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
const keysWithDropdown = [ const keysWithDropdown = [
'ClienteResponsabile', 'ClienteResponsabile',
'ClienteFornitore',
'ClienteAnalisi',
'MoltiplicatorePrezzo', 'MoltiplicatorePrezzo',
'AnagraficaCertestObject', 'AnagraficaCertestObject',
'AnagraficaCertestService' 'AnagraficaCertestService'
@@ -1443,7 +1633,14 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
const clientId = <?php echo (int)($template['idclient'] ?? 0); ?>; const clientId = <?php echo (int)($template['idclient'] ?? 0); ?>;
const requestCache = {};
async function fetchJson(url) { async function fetchJson(url) {
if (requestCache[url]) {
return requestCache[url];
}
requestCache[url] = (async () => {
const r = await fetch(url); const r = await fetch(url);
const text = await r.text(); const text = await r.text();
if (!r.ok) throw new Error(`HTTP ${r.status} on ${url}: ${text.slice(0, 200)}`); if (!r.ok) throw new Error(`HTTP ${r.status} on ${url}: ${text.slice(0, 200)}`);
@@ -1452,6 +1649,9 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
} catch (e) { } catch (e) {
throw new Error(`Invalid JSON from ${url}: ${text.slice(0, 200)}`); throw new Error(`Invalid JSON from ${url}: ${text.slice(0, 200)}`);
} }
})();
return requestCache[url];
} }
function normalizeList(j, key) { function normalizeList(j, key) {
@@ -1465,6 +1665,16 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
return []; return [];
} }
function formatClientLabel(x) {
const nome = x.Nominativo ?? x.Nome ?? x.name ?? 'Nome non disponibile';
const id = x.IdCliente ?? x.Id ?? x.id ?? '';
const codiceCliente = (x.CodiceCliente ?? x.codiceCliente ?? '').toString().trim();
const suffix = (codiceCliente.split('_')[1] || '').trim();
const shortCode = suffix || (codiceCliente ? codiceCliente.charAt(0) : '--');
return `${nome.trim()} - ${shortCode} (ID: ${id})`;
}
async function loadData(key) { async function loadData(key) {
if (key === 'MoltiplicatorePrezzo') { if (key === 'MoltiplicatorePrezzo') {
const j = await fetchJson('get_moltiplicatoreprezzo.php'); const j = await fetchJson('get_moltiplicatoreprezzo.php');
@@ -1507,6 +1717,16 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
})).filter(x => x.id != null && x.label); })).filter(x => x.id != null && x.label);
} }
if (key === 'ClienteFornitore' || key === 'ClienteAnalisi') {
const j = await fetchJson('get_clienti.php');
const list = normalizeList(j, key);
return list.map(x => ({
id: x.IdCliente ?? x.Id ?? x.id,
label: formatClientLabel(x)
})).filter(x => x.id != null && x.label);
}
return []; return [];
} }
+425
View File
@@ -0,0 +1,425 @@
<?php
include('include/headscript.php');
$iddatadb = isset($_GET['iddatadb']) ? (int)$_GET['iddatadb'] : 0;
if ($iddatadb <= 0) {
?>
<div class="modal fade" id="analysisModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-fullscreen">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Analysis</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-danger mb-0">Invalid iddatadb</div>
</div>
</div>
</div>
</div>
<?php
exit;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("
SELECT
p.id,
p.iddatadb,
p.part_number,
p.part_description,
p.material,
p.color,
p.mix,
p.idmatrice,
p.note,
p.dateexpiry,
a.NomeMatriceTraduzione
FROM identification_parts p
LEFT JOIN auth_matrici a ON a.IdMatrice = p.idmatrice
WHERE p.iddatadb = ?
ORDER BY
CASE WHEN p.part_number IS NULL THEN 999999 ELSE p.part_number END ASC,
p.id ASC
");
$stmt->execute([$iddatadb]);
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
$partIds = array_column($parts, 'id');
$assignedAnalysesByPart = [];
if (!empty($partIds)) {
$placeholders = implode(',', array_fill(0, count($partIds), '?'));
$stmtAssigned = $pdo->prepare("
SELECT
ipa.id,
ipa.part_id,
ipa.analysis_recordkey,
ipa.analysis_name,
ipa.analysis_method,
ipa.analysis_level,
ipa.is_web_selectable,
ipa.is_accredited
FROM identification_parts_analyses ipa
WHERE ipa.part_id IN ($placeholders)
ORDER BY ipa.analysis_name ASC, ipa.id ASC
");
$stmtAssigned->execute($partIds);
$assignedRows = $stmtAssigned->fetchAll(PDO::FETCH_ASSOC);
foreach ($assignedRows as $row) {
$partId = (int)$row['part_id'];
if (!isset($assignedAnalysesByPart[$partId])) {
$assignedAnalysesByPart[$partId] = [];
}
$assignedAnalysesByPart[$partId][] = $row;
}
}
/**
* Build matrix groups from parts.
* No join for now: we use idmatrice only.
*/
$matrixGroups = [];
foreach ($parts as $part) {
$matrixKey = (!empty($part['idmatrice'])) ? (string)$part['idmatrice'] : 'NO_MATRIX';
if (!isset($matrixGroups[$matrixKey])) {
$matrixGroups[$matrixKey] = [
'idmatrice' => $part['idmatrice'],
'NomeMatriceTraduzione' => $part['NomeMatriceTraduzione'] ?? '',
'parts_count' => 0,
'parts' => [],
];
}
$matrixGroups[$matrixKey]['parts_count']++;
$matrixGroups[$matrixKey]['parts'][] = $part['id'];
}
$matrixGroups = array_values($matrixGroups);
?>
<div class="modal fade" id="analysisModal" tabindex="-1" aria-labelledby="analysisModalLabel" aria-hidden="true">
<div class="modal-dialog" style="max-width: 96vw; width: 96vw; margin: 1.5vh auto;">
<div class="modal-content analysis-modal-content">
<div class="modal-header">
<div>
<h5 class="modal-title mb-0" id="analysisModalLabel">Analysis - TRF <?= htmlspecialchars((string)$iddatadb, ENT_QUOTES, 'UTF-8') ?></h5>
<small class="text-muted">
Parts: <?= count($parts) ?> |
Matrices: <?= count($matrixGroups) ?>
</small>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body analysis-modal-body">
<div class="row g-3 h-100">
<!-- MATRICES -->
<div class="col-lg-3 col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-header py-2 d-flex justify-content-between align-items-center">
<strong>Matrices</strong>
<span class="badge bg-secondary"><?= count($matrixGroups) ?></span>
</div>
<div class="card-body p-2 analysis-scroll-area">
<div class="list-group analysis-matrix-list" id="analysisMatrixList">
<?php if (empty($matrixGroups)): ?>
<div class="text-muted small p-2">No matrices found</div>
<?php else: ?>
<?php foreach ($matrixGroups as $index => $group): ?>
<button type="button"
class="list-group-item list-group-item-action analysis-matrix-item <?= $index === 0 ? 'active' : '' ?>"
data-matrix-id="<?= htmlspecialchars((string)($group['idmatrice'] ?? 'NO_MATRIX'), ENT_QUOTES, 'UTF-8') ?>">
<div class="fw-semibold">
<?= htmlspecialchars(
!empty($group['NomeMatriceTraduzione'])
? $group['NomeMatriceTraduzione']
: (!empty($group['idmatrice']) ? ('Matrix without translation') : 'No Matrix'),
ENT_QUOTES,
'UTF-8'
) ?>
</div>
<div class="small text-muted">
ID: <?= !empty($group['idmatrice']) ? (int)$group['idmatrice'] : '-' ?>
</div>
<div class="small text-muted">
Parts linked: <?= (int)$group['parts_count'] ?>
</div>
</button>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
</div>
<!-- PARTS -->
<div class="col-lg-4 col-md-8">
<div class="card h-100 shadow-sm">
<div class="card-header py-2 d-flex justify-content-between align-items-center">
<strong>Parts</strong>
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm" id="analysisClearSelectionBtn">Clear</button>
<span class="badge bg-primary" id="analysisSelectedPartsCount">0 selected</span>
</div>
</div>
<div class="card-body p-2 analysis-scroll-area">
<div class="list-group analysis-parts-list" id="analysisPartsList">
<?php if (empty($parts)): ?>
<div class="text-muted small p-2">No parts found</div>
<?php else: ?>
<?php foreach ($parts as $part): ?>
<?php
$matrixId = !empty($part['idmatrice']) ? (string)$part['idmatrice'] : 'NO_MATRIX';
$partLabel = trim(($part['part_number'] !== null ? ('Part ' . $part['part_number']) : 'Part') . ' - ' . ($part['part_description'] ?? ''));
?>
<div class="list-group-item analysis-part-item"
data-part-id="<?= (int)$part['id'] ?>"
data-matrix-id="<?= htmlspecialchars($matrixId, ENT_QUOTES, 'UTF-8') ?>">
<div class="d-flex align-items-start gap-2">
<input type="checkbox"
class="form-check-input mt-1 analysis-part-checkbox"
value="<?= (int)$part['id'] ?>">
<div class="flex-grow-1">
<div class="fw-semibold">
<?= htmlspecialchars($partLabel, ENT_QUOTES, 'UTF-8') ?>
</div>
<div class="small text-muted mt-1">
Matrix:
<strong>
<?= htmlspecialchars(
!empty($part['NomeMatriceTraduzione'])
? $part['NomeMatriceTraduzione']
: (!empty($part['idmatrice']) ? 'Matrix without translation' : 'No Matrix'),
ENT_QUOTES,
'UTF-8'
) ?>
</strong>
<?php if (!empty($part['idmatrice'])): ?>
<span class="small text-muted">(ID: <?= (int)$part['idmatrice'] ?>)</span>
<?php endif; ?>
<?php if (!empty($part['mix']) && strtoupper($part['mix']) === 'Y'): ?>
| <span class="badge bg-warning text-dark">Mix</span>
<?php endif; ?>
</div>
<?php if (!empty($part['material']) || !empty($part['color'])): ?>
<div class="small text-muted">
<?php if (!empty($part['material'])): ?>
Material: <?= htmlspecialchars($part['material'], ENT_QUOTES, 'UTF-8') ?>
<?php endif; ?>
<?php if (!empty($part['material']) && !empty($part['color'])): ?>
|
<?php endif; ?>
<?php if (!empty($part['color'])): ?>
Color: <?= htmlspecialchars($part['color'], ENT_QUOTES, 'UTF-8') ?>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (!empty($part['note'])): ?>
<div class="small text-muted mt-1">
Note: <?= htmlspecialchars($part['note'], ENT_QUOTES, 'UTF-8') ?>
</div>
<?php endif; ?>
</div>
</div>
<div class="analysis-part-assigned-list mt-2" id="analysisAssignedListPart<?= (int)$part['id'] ?>"></div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
</div>
<!-- ANALYSES PLACEHOLDER -->
<div class="col-lg-5">
<div class="card h-100 shadow-sm">
<div class="card-header py-2">
<strong>Analyses</strong>
</div>
<div class="card-body analysis-scroll-area" id="analysisRightPanel">
<div class="mt-1 mb-3">
<div class="small text-muted">Selected matrix:</div>
<div id="analysisCurrentMatrix" class="fw-semibold">-</div>
</div>
<div class="mb-3">
<div class="small text-muted">Selected parts IDs:</div>
<div id="analysisSelectedPartsIds" class="fw-semibold">-</div>
</div>
<div class="d-flex flex-wrap align-items-center gap-2 mb-3">
<div class="form-check m-0">
<input class="form-check-input" type="checkbox" id="analysisWebOnly">
<label class="form-check-label small" for="analysisWebOnly">
Web only
</label>
</div>
<div class="flex-grow-1" style="min-width: 220px;">
<input type="text" class="form-control form-control-sm" id="analysisSearchInput" placeholder="Search analyses or method">
</div>
</div>
<div id="analysisLoadingBox" class="alert alert-info mb-3 d-none">
Loading analyses...
</div>
<div id="analysisErrorBox" class="alert alert-danger mb-3 d-none"></div>
<div id="analysisEmptyBox" class="alert alert-secondary mb-3">
Select a matrix to load analyses
</div>
<div class="list-group" id="analysisList"></div>
</div>
</div>
</div>
</div>
<input type="hidden" id="analysisModalIddatadb" value="<?= (int)$iddatadb ?>">
<script type="application/json" id="analysisAssignedInitialData">
<?= json_encode($assignedAnalysesByPart, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>
</script>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<style>
#analysisModal {
z-index: 1080 !important;
}
#analysisModal .modal-content {
width: 100% !important;
max-width: 100% !important;
min-height: 95vh;
border-radius: 14px;
}
.analysis-modal-content {
overflow: hidden;
}
.analysis-modal-body {
height: calc(95vh - 120px);
}
.analysis-scroll-area {
overflow-y: auto;
max-height: calc(95vh - 180px);
}
.analysis-matrix-item {
text-align: left;
border-radius: 8px !important;
margin-bottom: 8px;
}
.analysis-part-item {
border-radius: 8px !important;
margin-bottom: 8px;
transition: all 0.2s ease-in-out;
}
.analysis-part-item.matrix-active {
background-color: #e8f4ff !important;
border-color: #86b7fe !important;
}
.analysis-part-item.part-checked {
background-color: #eaf7ea !important;
border-color: #7ac77a !important;
}
#analysisSelectedPartsIds {
word-break: break-word;
}
.analysis-analysis-item {
border-radius: 8px !important;
margin-bottom: 6px;
background-color: #ffffff;
}
#analysisList .analysis-analysis-item:nth-child(even) {
background-color: #eef4ff;
}
.analysis-analysis-meta {
font-size: 0.84rem;
color: #6c757d;
}
.analysis-analysis-badges .badge {
margin-right: 4px;
margin-bottom: 4px;
}
.analysis-assigned-chip {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 4px 8px;
border: 1px solid #dbe7f3;
background: #f7fbff;
border-radius: 8px;
margin-bottom: 6px;
font-size: 0.84rem;
}
.analysis-assigned-chip-text {
min-width: 0;
flex: 1;
}
.analysis-assigned-chip-title {
font-weight: 600;
line-height: 1.2;
}
.analysis-assigned-chip-method {
color: #6c757d;
font-size: 0.78rem;
line-height: 1.2;
margin-top: 2px;
}
.analysis-remove-btn {
border: 0;
background: transparent;
color: #dc3545;
font-weight: 700;
font-size: 1rem;
line-height: 1;
padding: 0 2px;
cursor: pointer;
}
.analysis-analysis-item.analysis-selected {
border-color: #198754 !important;
background-color: #eaf7ea !important;
}
.analysis-part-assigned-list {
border-top: 1px dashed #dee2e6;
padding-top: 8px;
}
</style>
+19
View File
@@ -307,4 +307,23 @@
width: 250px !important; width: 250px !important;
min-width: 250px !important; min-width: 250px !important;
} }
#partsTableBody .extra-field-td .select2-container {
width: 100% !important;
min-width: 180px !important;
}
#partsTableBody .extra-field-td .select2-selection--single {
height: 31px !important;
padding: 0.15rem 0.35rem !important;
border: 1px solid #ced4da !important;
}
#partsTableBody .extra-field-td .select2-selection__rendered {
line-height: 28px !important;
}
#partsTableBody .extra-field-td .select2-selection__arrow {
height: 29px !important;
}
</style> </style>
+34 -15
View File
@@ -9,27 +9,38 @@
<div class="row parts-row"> <div class="row parts-row">
<div class="col-md-9"> <div class="col-md-9">
<!-- Prima riga: Elenco Parti, Rinumera, Voce --> <!-- Prima riga: Elenco Parti, Rinumera, Voce -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; min-width: 0;">
<h6 style="margin: 0;">Elenco Parti</h6> <h6 style="margin: 0; white-space: nowrap;">Elenco Parti</h6>
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center; min-width: 0; gap: 8px;">
<button type="button" class="btn btn-info btn-sm" id="renumberPartsBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Rinumera Parti</button> <button type="button"
<button type="button" class="btn btn-secondary btn-sm ms-2" id="toggleVoiceBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;"><i class="fas fa-microphone"></i> Voce</button> class="btn btn-dark btn-sm open-analysis-modal-btn"
<button type="button" class="btn btn-info btn-sm ms-2 d-none" id="quotationeBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Add Quotation</button> id="openAnalysisModalBtn"
<button type="button" class="btn btn-primary btn-sm ms-2" id="showHideImageBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;"> style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
<i class="fas fa-eye-slash" style="font-size: 0.8rem;"></i> <i class="fas fa-flask"></i> Analysis
<i class="fas fa-image ms-1" style="font-size: 0.8rem;"></i> </button>
<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>
<button type="button" class="btn btn-info btn-sm" id="renumberPartsBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
Rinumera Parti
</button>
<button type="button" class="btn btn-secondary btn-sm ms-2" id="toggleVoiceBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
<i class="fas fa-microphone"></i> Voce
</button> </button>
</div> </div>
</div> </div>
<!-- Seconda riga: +, M, MacroMatrice, Matrice globale, Propaga --> <!-- Seconda riga: +, M, MacroMatrice, Matrice globale, Propaga -->
<div style="display: flex; align-items: center; margin-bottom: 10px;"> <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> <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;"> <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> <option value="">Tutte le MacroMatrici</option>
</select> </select>
<select id="global-matrice" class="form-control form-control-sm" style="width: 350px !important; margin-right: 10px;"></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> </div>
<table class="table table-striped table-sm" id="partsTable"> <table class="table table-striped table-sm" id="partsTable">
<thead> <thead>
@@ -55,7 +66,7 @@
<td><input type="text" class="form-control form-control-sm part-description" placeholder="Inserisci descrizione"></td> <td><input type="text" class="form-control form-control-sm part-description" placeholder="Inserisci descrizione"></td>
<td> <td>
<div style="display: flex; align-items: center;"> <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> <select class="part-matrice form-control form-control-sm" style="width: 150px;"></select>
</div> </div>
</td> </td>
@@ -63,9 +74,9 @@
<td> <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-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-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> <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 fa-xs"></i></span> <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 fa-xs"></i></span> <span class="save-loading text-warning" style="display: none; margin-left: 5px;"><i class="fas fa-spinner fa-spin"></i></span>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -188,6 +199,14 @@
z-index: 1055 !important 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 */ /* Tabelle */
#partsTable tr { #partsTable tr {
display: table-row !important display: table-row !important
+218
View 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;
}
});
})();
+1 -1
View File
@@ -143,7 +143,7 @@ $(document).ready(function () {
if (iddatadb) { if (iddatadb) {
if (matrici.length === 0) { if (matrici.length === 0) {
$.ajax({ $.ajax({
url: "get_matrici_db.php", url: "get_matrici.php",
method: "GET", method: "GET",
dataType: "json", dataType: "json",
success: function (data) { success: function (data) {
+145 -157
View File
@@ -81,10 +81,19 @@ $(document).ready(function () {
data: { field_ids: fieldId }, data: { field_ids: fieldId },
success: function (res) { success: function (res) {
const arr = res && res[fieldId] ? res[fieldId] : []; const arr = res && res[fieldId] ? res[fieldId] : [];
extraFieldOptions = (arr || []).map((x) => ({
extraFieldOptions = (arr || [])
.map((x) => ({
id: x.IdCustomFieldsValue, id: x.IdCustomFieldsValue,
label: x.Valore, label: x.Valore || "",
})); }))
.sort((a, b) =>
a.label.localeCompare(b.label, "it", {
sensitivity: "base",
numeric: true,
}),
);
done(); done();
}, },
error: function () { error: function () {
@@ -127,6 +136,50 @@ $(document).ready(function () {
</td>`; </td>`;
} }
function initializeExtraFieldSelect2($context) {
if (typeof $.fn.select2 === "undefined") return;
$context.find(".part-extra-select").each(function () {
const $select = $(this);
if ($select.hasClass("select2-hidden-accessible")) {
$select.select2("destroy");
}
$select.select2({
placeholder: "Seleziona...",
allowClear: true,
width: "100%",
dropdownParent: $("#partsModal"),
minimumResultsForSearch: 0,
sorter: function (data) {
return data.sort(function (a, b) {
const textA = (a.text || "").toLowerCase();
const textB = (b.text || "").toLowerCase();
return textA.localeCompare(textB, "it", {
sensitivity: "base",
numeric: true,
});
});
},
matcher: function (params, data) {
if ($.trim(params.term) === "") return data;
if (typeof data.text === "undefined") return null;
const term = params.term.toLowerCase();
const text = data.text.toLowerCase();
return text.indexOf(term) > -1 ? data : null;
},
});
const selected = ($select.attr("data-selected") || "").toString();
if (selected) {
$select.val(selected).trigger("change.select2");
}
});
}
$(document).on("change", ".part-extra-select", function () { $(document).on("change", ".part-extra-select", function () {
const valId = $(this).val() || ""; const valId = $(this).val() || "";
const $row = $(this).closest("tr"); const $row = $(this).closest("tr");
@@ -164,13 +217,14 @@ $(document).ready(function () {
const $row = $(this); const $row = $(this);
$row.find("td:last").before(buildExtraFieldCellHtml($row)); $row.find("td:last").before(buildExtraFieldCellHtml($row));
// ripristina selezione se già presente nello stato riga
const selected = ($row.data("extra-value-id") || "").toString(); const selected = ($row.data("extra-value-id") || "").toString();
if (selected) { if (selected) {
$row.find(".part-extra-value-id").val(selected); $row.find(".part-extra-value-id").val(selected);
$row.find(".part-extra-select").val(selected); $row.find(".part-extra-select").attr("data-selected", selected);
} }
}); });
initializeExtraFieldSelect2($("#partsTableBody"));
} }
function setPartId($row, id) { function setPartId($row, id) {
@@ -288,51 +342,12 @@ $(document).ready(function () {
// =================== // ===================
function loadParts(iddatadb, idquotations, callback = null) { function loadParts(iddatadb, idquotations, callback = null) {
if (iddatadb) { if (iddatadb) {
if (matrici.length === 0) {
$.ajax({
url: "get_matrici_db.php",
method: "GET",
dataType: "json",
success: function (data) {
matrici = data.value || [];
loadMacroMatrici(); loadMacroMatrici();
initializeGlobalSelect2(); initializeGlobalSelect2();
loadPartsExtraField(iddatadb, function () { loadPartsExtraField(iddatadb, function () {
loadPhoto(iddatadb, idquotations); loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback); 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);
});
}
} else { } else {
loadPartsExtraField(iddatadb, function () { loadPartsExtraField(iddatadb, function () {
loadPhoto(iddatadb, idquotations); loadPhoto(iddatadb, idquotations);
@@ -887,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><input type="text" class="form-control form-control-sm part-description" value="${description}" placeholder="Inserisci descrizione"></td>
<td> <td>
<div style="display: flex; align-items: center;"> <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> <select class="part-matrice form-control form-control-sm" style="width: 150px;"></select>
</div> </div>
</td> </td>
@@ -896,15 +911,19 @@ $(document).ready(function () {
<td> <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-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-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> <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 fa-xs"></i></span> <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 fa-xs"></i></span> <span class="save-loading text-warning" style="display: none; margin-left: 5px;"><i class="fas fa-spinner fa-spin"></i></span>
</td> </td>
</tr>`; </tr>`;
$("#partsTableBody").append(newRow); $("#partsTableBody").append(newRow);
const $select = $("#partsTableBody tr:last .part-matrice"); const $newRow = $("#partsTableBody tr:last");
const $select = $newRow.find(".part-matrice");
const selectedMacro = $("#macro-matrice-filter").val() || ""; const selectedMacro = $("#macro-matrice-filter").val() || "";
initializeSelect2($select, nextPartNumber, "", null, selectedMacro); initializeSelect2($select, nextPartNumber, "", null, selectedMacro);
initializeExtraFieldSelect2($newRow);
updateRowButtons(); updateRowButtons();
markUnsaved(); markUnsaved();
} }
@@ -1380,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><input type="text" class="form-control form-control-sm part-description" value="${escapedDescription}" placeholder="Inserisci descrizione"></td>
<td> <td>
<div style="display: flex; align-items: center;"> <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> <select class="part-matrice form-control form-control-sm" style="width: 150px;" ${idquotations && !part.idmatrice ? "disabled" : ""}></select>
</div> </div>
</td> </td>
@@ -1389,9 +1408,9 @@ $(document).ready(function () {
<td> <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-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-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> <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 fa-xs"></i></span> <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 fa-xs"></i></span> <span class="save-loading text-warning" style="display: none; margin-left: 5px;"><i class="fas fa-spinner fa-spin"></i></span>
</td> </td>
</tr>`; </tr>`;
$("#partsTableBody").append(newRow); $("#partsTableBody").append(newRow);
@@ -1407,8 +1426,14 @@ $(document).ready(function () {
const vid = String(part.extra_value_id); const vid = String(part.extra_value_id);
$row.data("extra-value-id", vid); $row.data("extra-value-id", vid);
$row.find(".part-extra-value-id").val(vid); $row.find(".part-extra-value-id").val(vid);
$row.find(".part-extra-select").val(vid); $row.find(".part-extra-select").attr(
"data-selected",
vid,
);
} }
initializeExtraFieldSelect2($row);
const $select = $("#partsTableBody").find( const $select = $("#partsTableBody").find(
`tr[data-part-id="${part.id}"] .part-matrice`, `tr[data-part-id="${part.id}"] .part-matrice`,
); );
@@ -1454,7 +1479,7 @@ $(document).ready(function () {
function loadMacroMatrici() { function loadMacroMatrici() {
$.ajax({ $.ajax({
url: "get_macro_matrici.php", url: "search_matrici.php?macro_list=1",
method: "GET", method: "GET",
dataType: "json", dataType: "json",
success: function (data) { success: function (data) {
@@ -1551,18 +1576,6 @@ $(document).ready(function () {
return; 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 // Memorizza il valore corrente
const currentValue = $select.val(); const currentValue = $select.val();
@@ -1571,39 +1584,39 @@ $(document).ready(function () {
$select.select2("destroy"); $select.select2("destroy");
} }
// Inizializza Select2 // Inizializza Select2 con AJAX
$select.empty().select2({ $select.empty().select2({
placeholder: filteredMatrici.length placeholder: "Seleziona matrice globale",
? "Seleziona matrice globale"
: "Nessuna matrice disponibile",
allowClear: true, allowClear: true,
data: options,
dropdownParent: $("#partsModal"), dropdownParent: $("#partsModal"),
minimumResultsForSearch: 0, // Abilita ricerca senza limite minimo di caratteri minimumInputLength: 0,
matcher: function (params, data) { ajax: {
if (!params.term) return data; // Mostra tutte le opzioni se non c'è termine di ricerca url: "search_matrici.php",
const term = params.term.toUpperCase(); dataType: "json",
if (data.text.toUpperCase().indexOf(term) >= 0) return data; delay: 150,
return null; 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 // Ripristina il valore corrente
if ( if (currentValue) {
currentValue && $.ajax({
filteredMatrici.some((m) => m.IdMatrice == currentValue) url: "search_matrici.php",
) { data: { id: currentValue },
$select.val(currentValue).trigger("change"); dataType: "json",
} else if (filteredMatrici.length === 0) { }).then(function (data) {
const errorMsg = $( const item = (data.results || [])[0];
'<div class="alert alert-warning temp-alert" role="alert">Nessuna matrice disponibile per la MacroMatrice selezionata.</div>', if (item) {
); const option = new Option(item.text, item.id, true, true);
$("#partsModal .modal-body").prepend(errorMsg); $select.append(option).trigger("change");
setTimeout(function () { }
errorMsg.fadeOut(500, function () {
$(this).remove();
}); });
}, 5000);
} }
} }
@@ -1622,32 +1635,6 @@ $(document).ready(function () {
return; 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 // Memorizza il valore corrente
const currentValue = idmatrice || $select.val(); const currentValue = idmatrice || $select.val();
@@ -1656,44 +1643,44 @@ $(document).ready(function () {
$select.select2("destroy"); $select.select2("destroy");
} }
// Inizializza Select2 // Inizializza Select2 con AJAX
$select.empty().select2({ $select.empty().select2({
placeholder: filteredMatrici.length placeholder: "Seleziona matrice",
? "Seleziona matrice"
: "Nessuna matrice disponibile",
allowClear: true, allowClear: true,
data: options,
dropdownParent: $("#partsModal"), dropdownParent: $("#partsModal"),
minimumResultsForSearch: 0, // Abilita ricerca senza limite minimo di caratteri minimumInputLength: 0,
matcher: function (params, data) { ajax: {
if (!params.term) return data; // Mostra tutte le opzioni se non c'è termine di ricerca url: "search_matrici.php",
const term = params.term.toUpperCase(); dataType: "json",
if (data.text.toUpperCase().indexOf(term) >= 0) return data; delay: 150,
return null; 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 // Ripristina il valore se valido
if (partId && partId !== "new" && currentValue) { if (partId && partId !== "new" && currentValue) {
const matrice = matrici.find((m) => m.IdMatrice == currentValue); $.ajax({
if (matrice) { url: "search_matrici.php",
const option = new Option( data: { id: currentValue },
matrice.NomeMatrice, dataType: "json",
matrice.IdMatrice, }).then(function (data) {
true, const item = (data.results || [])[0];
true, if (item) {
); const option = new Option(item.text, item.id, true, true);
if (!fromFilter) $select.append(option).trigger("change"); if (!fromFilter) $select.append(option).trigger("change");
else $select.append(option); else $select.append(option);
partMatrice[partNumber] = item.id;
partMatrice[partNumber] = matrice.IdMatrice;
} else { } else {
// Aggiusta valore non valido
if (!fromFilter) $select.val(null).trigger("change"); if (!fromFilter) $select.val(null).trigger("change");
partMatrice[partNumber] = null; partMatrice[partNumber] = null;
} }
});
} else { } else {
$select.val(null).trigger("change", [{ skipHandler: true }]); $select.val(null).trigger("change", [{ skipHandler: true }]);
} }
@@ -1770,18 +1757,7 @@ $(document).ready(function () {
} }
}); });
// Mostra messaggio se non ci sono matrici // Messaggio se macro selezionata ma nessun risultato sarà gestito dal placeholder Select2
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);
}
// Debug per verificare inizializzazione // Debug per verificare inizializzazione
console.log( console.log(
@@ -1797,8 +1773,13 @@ $(document).ready(function () {
$(document).on("click", ".propagate-matrice-btn", function () { $(document).on("click", ".propagate-matrice-btn", function () {
const $row = $(this).closest("tr"); const $row = $(this).closest("tr");
const globalVal = $("#global-matrice").val(); const globalVal = $("#global-matrice").val();
const globalText = $("#global-matrice").find("option:selected").text();
if (globalVal) { 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 { } else {
const errorMsg = $( const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Seleziona una matrice globale prima di propagare.</div>', '<div class="alert alert-danger temp-alert" role="alert">Seleziona una matrice globale prima di propagare.</div>',
@@ -1814,8 +1795,15 @@ $(document).ready(function () {
$(document).on("click", ".propagate-all-btn", function () { $(document).on("click", ".propagate-all-btn", function () {
const globalVal = $("#global-matrice").val(); const globalVal = $("#global-matrice").val();
const globalText = $("#global-matrice").find("option:selected").text();
if (globalVal) { 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 { } else {
const errorMsg = $( const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Seleziona una matrice globale prima di propagare.</div>', '<div class="alert alert-danger temp-alert" role="alert">Seleziona una matrice globale prima di propagare.</div>',
+102 -4
View File
@@ -1,5 +1,5 @@
document.addEventListener("DOMContentLoaded", function () { 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) { async function loadPopupContent(iddatadb, idquotations) {
const popupContent = document.getElementById("popupContent"); const popupContent = document.getElementById("popupContent");
if (!popupContent) { if (!popupContent) {
@@ -475,6 +475,11 @@ document.addEventListener("DOMContentLoaded", function () {
); );
return; return;
} }
if (canvas) {
canvas.dispose();
}
canvas = new fabric.Canvas("collageCanvas", { canvas = new fabric.Canvas("collageCanvas", {
backgroundColor: "#fff", backgroundColor: "#fff",
selection: true, selection: true,
@@ -583,6 +588,7 @@ document.addEventListener("DOMContentLoaded", function () {
if (isCropping || isRemovingBackground) { if (isCropping || isRemovingBackground) {
return; return;
} }
const state = JSON.stringify( const state = JSON.stringify(
canvas.toJSON([ canvas.toJSON([
"cornerColor", "cornerColor",
@@ -590,12 +596,16 @@ document.addEventListener("DOMContentLoaded", function () {
"cornerSize", "cornerSize",
"borderColor", "borderColor",
"transparentCorners", "transparentCorners",
"backgroundImage",
]), ]),
); );
history.push(state); history.push(state);
if (history.length > maxHistory) { if (history.length > maxHistory) {
history.shift(); history.shift();
} }
updateButtons(); updateButtons();
} }
@@ -603,10 +613,11 @@ document.addEventListener("DOMContentLoaded", function () {
if (history.length <= 1) { if (history.length <= 1) {
return; return;
} }
history.pop(); history.pop();
const previousState = history[history.length - 1]; const previousState = history[history.length - 1];
if (previousState) { if (previousState) {
canvas.clear();
canvas.loadFromJSON(previousState, () => { canvas.loadFromJSON(previousState, () => {
canvas.renderAll(); canvas.renderAll();
updateLayersPanel(); updateLayersPanel();
@@ -615,8 +626,11 @@ document.addEventListener("DOMContentLoaded", function () {
} else { } else {
console.warn("Nessuno stato precedente disponibile"); console.warn("Nessuno stato precedente disponibile");
canvas.clear(); canvas.clear();
canvas.setBackgroundColor("#fff"); canvas.backgroundImage = null;
canvas.renderAll(); canvas.setBackgroundColor(
"#fff",
canvas.renderAll.bind(canvas),
);
updateLayersPanel(); updateLayersPanel();
updateButtons(); updateButtons();
} }
@@ -909,6 +923,61 @@ document.addEventListener("DOMContentLoaded", function () {
canvas.renderAll(); canvas.renderAll();
} }
function setCanvasBackground(imgPath) {
console.log("setCanvasBackground chiamata con:", imgPath);
if (!canvas) {
console.error("Canvas non inizializzato");
return;
}
fabric.Image.fromURL(
imgPath,
(img) => {
const canvasWidth = canvas.getWidth();
const canvasHeight = canvas.getHeight();
const imgWidth = img.width;
const imgHeight = img.height;
if (!imgWidth || !imgHeight) {
alert(
"Impossibile leggere le dimensioni dell'immagine.",
);
return;
}
// Scale to cover: l'immagine copre tutto il canvas mantenendo il ratio
const scale = Math.max(
canvasWidth / imgWidth,
canvasHeight / imgHeight,
);
img.set({
originX: "left",
originY: "top",
scaleX: scale,
scaleY: scale,
left: (canvasWidth - imgWidth * scale) / 2,
top: (canvasHeight - imgHeight * scale) / 2,
selectable: false,
evented: false,
hasControls: false,
hasBorders: false,
excludeFromExport: false,
});
canvas.setBackgroundImage(img, () => {
canvas.requestRenderAll();
saveCanvasState();
updateLayersPanel();
updateButtons();
});
},
{ crossOrigin: "anonymous" },
);
}
const addToCanvasBtn = document.getElementById("addToCanvasBtn"); const addToCanvasBtn = document.getElementById("addToCanvasBtn");
if (addToCanvasBtn) { if (addToCanvasBtn) {
addToCanvasBtn.addEventListener("click", () => { addToCanvasBtn.addEventListener("click", () => {
@@ -948,6 +1017,32 @@ document.addEventListener("DOMContentLoaded", function () {
}); });
} }
const setBackgroundBtn = document.getElementById("setBackgroundBtn");
if (setBackgroundBtn) {
setBackgroundBtn.addEventListener("click", () => {
const checkboxes = document.querySelectorAll(
".photo-checkbox:checked",
);
if (checkboxes.length === 0) {
alert("Seleziona una foto da usare come sfondo!");
return;
}
if (checkboxes.length > 1) {
alert("Seleziona una sola foto per lo sfondo!");
return;
}
const imgPath = checkboxes[0].getAttribute("data-path");
setCanvasBackground(imgPath);
checkboxes.forEach((cb) => {
cb.checked = false;
});
});
}
const saveCollageBtn = document.getElementById("saveCollageBtn"); const saveCollageBtn = document.getElementById("saveCollageBtn");
if (saveCollageBtn) { if (saveCollageBtn) {
saveCollageBtn.addEventListener("click", async () => { saveCollageBtn.addEventListener("click", async () => {
@@ -1123,4 +1218,7 @@ document.addEventListener("DOMContentLoaded", function () {
closeBtn, closeBtn,
}); });
} }
// Export for external use (gridData pages)
window.loadPopupContent = loadPopupContent;
}); });
+4 -1
View File
@@ -298,8 +298,11 @@ $result->saveToFile($qrCodeFile);
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<!-- Bottone per aggiungere selezionate al canvas --> <!-- Bottoni azioni collage -->
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 10px;">
<button id="addToCanvasBtn">Aggiungi Selezionate al Canvas</button> <button id="addToCanvasBtn">Aggiungi Selezionate al Canvas</button>
<button id="setBackgroundBtn" type="button">Imposta Selezionata come Sfondo</button>
</div>
<!-- Canvas per editing --> <!-- Canvas per editing -->
<canvas id="collageCanvas" width="960" height="720" style="border: 1px solid #ccc; margin-top: 20px;"></canvas> <canvas id="collageCanvas" width="960" height="720" style="border: 1px solid #ccc; margin-top: 20px;"></canvas>
+58 -30
View File
@@ -9,46 +9,76 @@ try {
throw new Exception("Invalid request method."); throw new Exception("Invalid request method.");
} }
// Recupera e sanifica i dati // Retrieve and sanitize form data
$id = intval($_POST['id']); $id = intval($_POST['id'] ?? 0);
$name = trim($_POST['name']); $name = trim($_POST['name'] ?? '');
$header_row = intval($_POST['header_row']); $source_type = strtoupper(trim($_POST['source_type'] ?? 'XLS'));
$start_column = trim($_POST['start_column']); $header_row = isset($_POST['header_row']) && $_POST['header_row'] !== '' ? intval($_POST['header_row']) : null;
$start_column = trim($_POST['start_column'] ?? '');
$description = trim($_POST['description'] ?? ''); $description = trim($_POST['description'] ?? '');
$target_table = trim($_POST['target_table']); $target_table = trim($_POST['target_table'] ?? 'datadb');
$idclient = intval($_POST['client_id'] ?? 0); // Usa client_id dal form $idclient = intval($_POST['client_id'] ?? 0);
$clientname = trim($_POST['client_name'] ?? ''); // Usa client_name dal form $clientname = trim($_POST['client_name'] ?? '');
$idschema = intval($_POST['idschema'] ?? 0); // Nuovo campo $idschema = intval($_POST['idschema'] ?? 0);
$schemaname = trim($_POST['schemaname'] ?? ''); // Corretto da schemamaname $schemaname = trim($_POST['schemaname'] ?? '');
$idroutine = isset($_POST['idroutine']) && $_POST['idroutine'] !== '' ? intval($_POST['idroutine']) : null; // Aggiunto idroutine $idroutine = isset($_POST['idroutine']) && $_POST['idroutine'] !== '' ? intval($_POST['idroutine']) : null;
$button_size = trim($_POST['button_size'] ?? 'medium'); // Nuovo campo $button_size = trim($_POST['button_size'] ?? 'medium');
$button_bg_color = trim($_POST['button_bg_color'] ?? '#007bff'); // Nuovo campo $button_bg_color = trim($_POST['button_bg_color'] ?? '#007bff');
$button_text_color = trim($_POST['button_text_color'] ?? '#ffffff'); // Nuovo campo $button_text_color = trim($_POST['button_text_color'] ?? '#ffffff');
$button_label = trim($_POST['button_label'] ?? 'Click Me'); // Nuovo campo $button_label = trim($_POST['button_label'] ?? 'Click Me');
// Controllo sui campi obbligatori if (!in_array($source_type, ['XLS', 'API'], true)) {
if (empty($id) || empty($name) || empty($header_row) || empty($start_column) || empty($target_table) || $idschema <= 0) { $source_type = 'XLS';
throw new Exception("All fields marked with * are required, including schema.");
} }
// Validazione del idclient // Required fields validation
if ($idclient <= 0) { if ($id <= 0 || $name === '' || $target_table === '' || $idclient <= 0 || $idschema <= 0) {
throw new Exception("Please select a valid client."); throw new Exception("All fields marked with * are required, including client and schema.");
} }
// Connessione al database // XLS-only validation
if ($source_type === 'XLS') {
if ($header_row === null || $header_row <= 0 || $start_column === '') {
throw new Exception("Header Row and Start Column are required for XLS templates.");
}
}
// API templates do not require XLS coordinates
if ($source_type === 'API') {
$header_row = null;
$start_column = null;
}
// Database connection
$db = DBHandlerSelect::getInstance(); $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection(); $pdo = $db->getConnection();
// Aggiorna il database, includendo i nuovi campi // Update template
$stmt = $pdo->prepare("UPDATE excel_templates $stmt = $pdo->prepare("
SET name = ?, header_row = ?, start_column = ?, description = ?, target_table = ?, UPDATE excel_templates
idclient = ?, clientname = ?, schemaname = ?, idschema = ?, idroutine = ?, SET
button_size = ?, button_bg_color = ?, button_text_color = ?, button_label = ?, name = ?,
source_type = ?,
header_row = ?,
start_column = ?,
description = ?,
target_table = ?,
idclient = ?,
clientname = ?,
schemaname = ?,
idschema = ?,
idroutine = ?,
button_size = ?,
button_bg_color = ?,
button_text_color = ?,
button_label = ?,
updated_at = NOW() updated_at = NOW()
WHERE id = ?"); WHERE id = ?
");
$stmt->execute([ $stmt->execute([
$name, $name,
$source_type,
$header_row, $header_row,
$start_column, $start_column,
$description, $description,
@@ -65,12 +95,10 @@ try {
$id $id
]); ]);
// rowCount potrebbe essere 0 se non ci sono modifiche, quindi consideriamo comunque un successo
$response["success"] = true; $response["success"] = true;
$response["message"] = "Template updated successfully!"; $response["message"] = "Template updated successfully!";
} catch (Exception $e) { } catch (Exception $e) {
$response["message"] = $e->getMessage(); $response["message"] = $e->getMessage();
} }
// Restituisce un JSON per il fetch
echo json_encode($response); echo json_encode($response);
+2 -2
View File
@@ -70,7 +70,7 @@ try {
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col); $columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col);
$cell = $worksheet->getCell($columnLetter . $header_row); $cell = $worksheet->getCell($columnLetter . $header_row);
$cellValue = $cell ? $cell->getCalculatedValue() : ''; // Usa getCalculatedValue per le formule $cellValue = $cell ? $cell->getCalculatedValue() : ''; // Usa getCalculatedValue per le formule
$headerRowData[] = htmlspecialchars($cellValue ?: ''); $headerRowData[] = $cellValue ?: '';
} }
// Estrai i dati a partire dalla riga successiva // Estrai i dati a partire dalla riga successiva
@@ -80,7 +80,7 @@ try {
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col); $columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col);
$cell = $worksheet->getCell($columnLetter . $row); $cell = $worksheet->getCell($columnLetter . $row);
$cellValue = $cell ? $cell->getCalculatedValue() : ''; // Usa getCalculatedValue per le formule $cellValue = $cell ? $cell->getCalculatedValue() : ''; // Usa getCalculatedValue per le formule
$rowData[] = htmlspecialchars($cellValue ?: ''); $rowData[] = $cellValue ?: '';
} }
if (!empty(array_filter($rowData))) { if (!empty(array_filter($rowData))) {
$excelData[] = $rowData; $excelData[] = $rowData;
+75 -12
View File
@@ -74,6 +74,16 @@ try {
$startRow = max(1, $header_row); $startRow = max(1, $header_row);
$startColumn = max(1, $start_column); $startColumn = max(1, $start_column);
// 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;
}
}
// Debug dei parametri // Debug dei parametri
error_log("Processing - template_id: $template_id, startRow: $startRow, startColumn: $startColumn, highestRow: $highestRow, highestColumn: $highestColumn, highestColumnIndex: $highestColumnIndex"); error_log("Processing - template_id: $template_id, startRow: $startRow, startColumn: $startColumn, highestRow: $highestRow, highestColumn: $highestColumn, highestColumnIndex: $highestColumnIndex");
@@ -84,25 +94,78 @@ try {
$response['error'] = "La colonna di partenza ($startColumn) supera il numero totale di colonne ($highestColumnIndex)."; $response['error'] = "La colonna di partenza ($startColumn) supera il numero totale di colonne ($highestColumnIndex).";
} else { } else {
$excelData = []; $excelData = [];
// Estrai la riga degli header
$headerRowData = []; // Build merge map for header row: physCol -> mergeStartCol
for ($col = $startColumn; $col <= $highestColumnIndex; $col++) { $mergeStartMap = [];
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col); foreach ($worksheet->getMergeCells() as $range) {
$cell = $worksheet->getCell($columnLetter . $header_row); [$startCell, $endCell] = explode(':', $range);
$cellValue = $cell ? $cell->getCalculatedValue() : ''; $mStartCol = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString(preg_replace('/\d+/', '', $startCell));
$headerRowData[] = htmlspecialchars($cellValue ?: ''); $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 // 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++) { for ($row = $startRow + 1; $row <= $highestRow; $row++) {
$rowData = []; $rowData = [];
for ($col = $startColumn; $col <= $highestColumnIndex; $col++) { foreach ($logicalCols as $physCol) {
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col); $columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($physCol);
$cell = $worksheet->getCell($columnLetter . $row); $cell = $worksheet->getCell($columnLetter . $row);
$cellValue = $cell ? $cell->getCalculatedValue() : ''; $cellValue = $cell ? $cell->getCalculatedValue() : '';
$rowData[] = htmlspecialchars($cellValue ?: ''); $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]; $excelData[] = ['data' => $rowData, 'excelrow' => $row];
} }
} }
+50 -12
View File
@@ -9,12 +9,13 @@ try {
throw new Exception("Invalid request method."); throw new Exception("Invalid request method.");
} }
// Recupera e sanifica i dati // Retrieve and sanitize form data
$name = trim($_POST['name']); $name = trim($_POST['name'] ?? '');
$header_row = intval($_POST['header_row']); $source_type = strtoupper(trim($_POST['source_type'] ?? 'XLS'));
$start_column = trim($_POST['start_column']); $header_row = isset($_POST['header_row']) && $_POST['header_row'] !== '' ? intval($_POST['header_row']) : null;
$start_column = trim($_POST['start_column'] ?? '');
$description = trim($_POST['description'] ?? ''); $description = trim($_POST['description'] ?? '');
$target_table = trim($_POST['target_table']); $target_table = trim($_POST['target_table'] ?? 'datadb');
$idclient = intval($_POST['client_id'] ?? 0); $idclient = intval($_POST['client_id'] ?? 0);
$clientname = trim($_POST['client_name'] ?? ''); $clientname = trim($_POST['client_name'] ?? '');
$idschema = intval($_POST['idschema'] ?? 0); $idschema = intval($_POST['idschema'] ?? 0);
@@ -25,24 +26,61 @@ try {
$button_text_color = trim($_POST['button_text_color'] ?? '#ffffff'); $button_text_color = trim($_POST['button_text_color'] ?? '#ffffff');
$button_label = trim($_POST['button_label'] ?? 'Click Me'); $button_label = trim($_POST['button_label'] ?? 'Click Me');
// Controllo sui campi obbligatori // Normalize source type
if (empty($name) || empty($header_row) || empty($start_column) || empty($target_table) || $idclient <= 0 || $idschema <= 0) { if (!in_array($source_type, ['XLS', 'API'], true)) {
$source_type = 'XLS';
}
// Required fields validation
if ($name === '' || $target_table === '' || $idclient <= 0 || $idschema <= 0) {
throw new Exception("All fields marked with * are required, including client and schema."); throw new Exception("All fields marked with * are required, including client and schema.");
} }
// Connessione al database // XLS-only validation
if ($source_type === 'XLS') {
if ($header_row === null || $header_row <= 0 || $start_column === '') {
throw new Exception("Header Row and Start Column are required for XLS templates.");
}
}
// API templates do not require XLS coordinates
if ($source_type === 'API') {
$header_row = null;
$start_column = null;
}
// Database connection
$db = DBHandlerSelect::getInstance(); $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection(); $pdo = $db->getConnection();
// Inserisci il nuovo template // Insert the new template
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO excel_templates INSERT INTO excel_templates
(name, header_row, start_column, description, target_table, idclient, clientname, idschema, schemaname, idroutine, (
button_size, button_bg_color, button_text_color, button_label, created_at, updated_at) name,
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) source_type,
header_row,
start_column,
description,
target_table,
idclient,
clientname,
idschema,
schemaname,
idroutine,
button_size,
button_bg_color,
button_text_color,
button_label,
created_at,
updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
"); ");
$stmt->execute([ $stmt->execute([
$name, $name,
$source_type,
$header_row, $header_row,
$start_column, $start_column,
$description, $description,
+144 -35
View File
@@ -5,102 +5,211 @@ ini_set('error_log', __DIR__ . '/routine_debug.log');
function applyRoutine(&$excelData, $routineData) function applyRoutine(&$excelData, $routineData)
{ {
try { try {
// Log iniziale // Initial log
error_log("Inizio esecuzione routine Moncler: " . date('Y-m-d H:i:s')); error_log("Inizio esecuzione routine Moncler: " . date('Y-m-d H:i:s'));
error_log("Dati routine: " . print_r($routineData, true)); error_log("Dati routine: " . print_r($routineData, true));
error_log("Dati excel_data: " . print_r($excelData, true)); error_log("Dati excel_data: " . print_r($excelData, true));
// Verifica se excelData è vuoto // Check if excelData is empty
if (empty($excelData)) { if (empty($excelData)) {
throw new Exception("excelData è vuoto o non valido."); throw new Exception("excelData è vuoto o non valido.");
} }
// Estrai informazioni dalla routine con valori predefiniti // Extract routine settings with default values
$action1 = trim($routineData['action1'] ?? 'K'); $action1 = trim($routineData['action1'] ?? 'K');
$action2 = trim($routineData['action2'] ?? 'STYLE CODE + STYLE DESCRIPTION'); $action2 = trim($routineData['action2'] ?? 'STYLE CODE + STYLE DESCRIPTION');
$action3 = trim($routineData['action3'] ?? 'STYLE CODE'); $action3 = trim($routineData['action3'] ?? 'STYLE CODE');
$action4 = trim($routineData['action4'] ?? 'STYLE DESCRIPTION'); $action4 = trim($routineData['action4'] ?? 'STYLE DESCRIPTION');
$headers = $routineData['xls_headers'] ?? []; $headers = $routineData['xls_headers'] ?? [];
// Package-related headers
$package_header = 'PACKAGE';
$other_test_header = 'Other test from Control Matrix';
$only_colorfastness_header = 'Only Colorfastness package';
$only_chemical_header = 'Only Chemical package';
if (empty($headers)) { if (empty($headers)) {
throw new Exception("Nessun header trovato per la routine Moncler."); throw new Exception("Nessun header trovato per la routine Moncler.");
} }
// If headers arrive as JSON string, decode them
if (!is_array($headers) && is_string($headers)) {
$decoded_headers = json_decode($headers, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded_headers)) {
$headers = $decoded_headers;
}
}
if (!is_array($headers) || empty($headers)) {
throw new Exception("Gli header della routine non sono validi.");
}
error_log("Header ricevuti: " . print_r($headers, true)); error_log("Header ricevuti: " . print_r($headers, true));
// Normalizza gli header (solo trim) // Normalize headers
$normalized_headers = array_map('trim', $headers); $normalized_headers = array_map(function ($header) {
return trim((string)$header);
}, $headers);
error_log("Header normalizzati: " . print_r($normalized_headers, true)); error_log("Header normalizzati: " . print_r($normalized_headers, true));
error_log("Action values - action1: '$action1', action2: '$action2', action3: '$action3', action4: '$action4'"); error_log("Action values - action1: '$action1', action2: '$action2', action3: '$action3', action4: '$action4'");
// Trova gli indici delle colonne // Find package-related column indexes
$action1_index = array_search($action1, $normalized_headers); $package_index = array_search($package_header, $normalized_headers, true);
$action2_index = array_search($action2, $normalized_headers); $other_test_index = array_search($other_test_header, $normalized_headers, true);
$action3_index = array_search($action3, $normalized_headers); $only_colorfastness_index = array_search($only_colorfastness_header, $normalized_headers, true);
$action4_index = array_search($action4, $normalized_headers); $only_chemical_index = array_search($only_chemical_header, $normalized_headers, true);
if ($package_index === false) {
throw new Exception("Colonna PACKAGE non trovata.");
}
error_log(
"Indici package - PACKAGE: " . var_export($package_index, true) .
", Other test from Control Matrix: " . var_export($other_test_index, true) .
", Only Colorfastness package: " . var_export($only_colorfastness_index, true) .
", Only Chemical package: " . var_export($only_chemical_index, true)
);
/**
* PART 1
* Aggregate rows by action1 and split action2 into action3 + action4
* If it fails, continue with PART 2 only
*/
$part1_applied = false;
$action1_index = array_search($action1, $normalized_headers, true);
$action2_index = array_search($action2, $normalized_headers, true);
$action3_index = array_search($action3, $normalized_headers, true);
$action4_index = array_search($action4, $normalized_headers, true);
if ($action1_index === false || $action2_index === false || $action3_index === false || $action4_index === false) { if ($action1_index === false || $action2_index === false || $action3_index === false || $action4_index === false) {
throw new Exception("Colonne non trovate - action1: '$action1' (index: " . var_export($action1_index, true) . "), action2: '$action2' (index: " . var_export($action2_index, true) . "), action3: '$action3' (index: " . var_export($action3_index, true) . "), action4: '$action4' (index: " . var_export($action4_index, true) . ")"); error_log(
} "Unable to apply routine part 1. Package merge logic has been applied only. " .
"Missing columns - action1: '$action1' (index: " . var_export($action1_index, true) . ")" .
", action2: '$action2' (index: " . var_export($action2_index, true) . ")" .
", action3: '$action3' (index: " . var_export($action3_index, true) . ")" .
", action4: '$action4' (index: " . var_export($action4_index, true) . ")"
);
} else {
error_log("Indici colonne part 1 - action1: $action1_index, action2: $action2_index, action3: $action3_index, action4: $action4_index");
error_log("Indici colonne - action1: $action1_index, action2: $action2_index, action3: $action3_index, action4: $action4_index");
// Raggruppa le righe per il valore in action1 (colonna K)
$grouped_data = []; $grouped_data = [];
foreach ($excelData as $row) { foreach ($excelData as $row) {
if (!isset($row['data']) || !is_array($row['data'])) { if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Riga non valida, manca 'data' per excelrow {$row['excelrow']}"); error_log("Riga non valida, manca 'data' per excelrow " . ($row['excelrow'] ?? 'N/A'));
continue; continue;
} }
$key = $row['data'][$action1_index] ?? ''; $key = $row['data'][$action1_index] ?? '';
$key = empty($key) ? '_empty_' : $key; $key = trim((string)$key);
$key = $key === '' ? '_empty_' : $key;
if (!isset($grouped_data[$key])) { if (!isset($grouped_data[$key])) {
$grouped_data[$key] = [ $grouped_data[$key] = [
'data' => $row['data'], 'data' => $row['data'],
'excelrow' => [$row['excelrow']], 'excelrow' => [($row['excelrow'] ?? '')],
'style_codes' => [], 'style_codes' => [],
'style_descriptions' => [] 'style_descriptions' => []
]; ];
} else { } else {
$grouped_data[$key]['excelrow'][] = $row['excelrow']; $grouped_data[$key]['excelrow'][] = ($row['excelrow'] ?? '');
} }
// Separa il valore in action2 (STYLE CODE + STYLE DESCRIPTION) // Split STYLE CODE + STYLE DESCRIPTION
$action2_value = $row['data'][$action2_index] ?? ''; $action2_value = trim((string)($row['data'][$action2_index] ?? ''));
if (!empty($action2_value)) {
$parts = explode(' - ', trim($action2_value)); if ($action2_value !== '') {
$style_code = $parts[0] ?? ''; $parts = explode(' - ', $action2_value, 2);
$style_description = $parts[1] ?? ''; $style_code = trim((string)($parts[0] ?? ''));
if (!empty($style_code)) { $style_description = trim((string)($parts[1] ?? ''));
if ($style_code !== '') {
$grouped_data[$key]['style_codes'][] = $style_code; $grouped_data[$key]['style_codes'][] = $style_code;
} }
if (!empty($style_description)) { if ($style_description !== '') {
$grouped_data[$key]['style_descriptions'][] = $style_description; $grouped_data[$key]['style_descriptions'][] = $style_description;
} }
} else { } else {
error_log("Valore vuoto in action2 per excelrow {$row['excelrow']}"); error_log("Valore vuoto in action2 per excelrow " . ($row['excelrow'] ?? 'N/A'));
} }
} }
// Crea il nuovo array excel_data aggregato
$new_excel_data = []; $new_excel_data = [];
foreach ($grouped_data as $key => $group) { foreach ($grouped_data as $key => $group) {
$row_data = $group['data']; $row_data = $group['data'];
// Aggiorna action3 (STYLE CODE) e action4 (STYLE DESCRIPTION) con valori aggregati
// Update STYLE CODE and STYLE DESCRIPTION with aggregated unique values
$row_data[$action3_index] = implode(' - ', array_unique($group['style_codes'])); $row_data[$action3_index] = implode(' - ', array_unique($group['style_codes']));
$row_data[$action4_index] = implode(' - ', array_unique($group['style_descriptions'])); $row_data[$action4_index] = implode(' - ', array_unique($group['style_descriptions']));
// Concatena gli excelrow con '+' per le righe aggregate
$excelrow_value = count($group['excelrow']) > 1 ? implode('+', $group['excelrow']) : $group['excelrow'][0]; // Concatenate excelrow values with "+"
$excelrow_clean = array_filter($group['excelrow'], function ($value) {
return $value !== null && $value !== '';
});
$excelrow_value = count($excelrow_clean) > 1
? implode('+', $excelrow_clean)
: (reset($excelrow_clean) ?: '');
$new_excel_data[] = [ $new_excel_data[] = [
'data' => $row_data, 'data' => $row_data,
'excelrow' => $excelrow_value 'excelrow' => $excelrow_value
]; ];
} }
// Modifica excelData in-place
$excelData = $new_excel_data; $excelData = $new_excel_data;
$part1_applied = true;
error_log("Routine Moncler completata - Righe aggregate: " . count($new_excel_data)); error_log("Routine part 1 completata - Righe aggregate: " . count($new_excel_data));
error_log("Excelrow aggregati: " . print_r(array_column($new_excel_data, 'excelrow'), true)); error_log("Excelrow aggregati part 1: " . print_r(array_column($new_excel_data, 'excelrow'), true));
}
/**
* PART 2
* Merge package-related columns into PACKAGE
* Always applied if PACKAGE exists
*/
foreach ($excelData as $index => $row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Riga non valida nella part 2, manca 'data' all'indice $index");
continue;
}
$package_values = [];
$value_package = trim((string)($row['data'][$package_index] ?? ''));
$value_other_test = $other_test_index !== false ? trim((string)($row['data'][$other_test_index] ?? '')) : '';
$value_only_colorfastness = $only_colorfastness_index !== false ? trim((string)($row['data'][$only_colorfastness_index] ?? '')) : '';
$value_only_chemical = $only_chemical_index !== false ? trim((string)($row['data'][$only_chemical_index] ?? '')) : '';
if ($value_package !== '') {
$package_values[] = $value_package;
}
if ($value_other_test !== '') {
$package_values[] = $value_other_test;
}
if ($value_only_colorfastness !== '') {
$package_values[] = $value_only_colorfastness;
}
if ($value_only_chemical !== '') {
$package_values[] = $value_only_chemical;
}
$package_values = array_unique($package_values);
$excelData[$index]['data'][$package_index] = implode(' - ', $package_values);
}
error_log("Routine part 2 completata - Merge package applicato su " . count($excelData) . " righe");
if (!$part1_applied) {
error_log("Warning finale: routine part 1 non applicata, routine part 2 applicata correttamente");
}
error_log("Routine Moncler completata con successo");
} catch (Exception $e) { } catch (Exception $e) {
error_log("Eccezione nella routine Moncler: " . $e->getMessage()); error_log("Eccezione nella routine Moncler: " . $e->getMessage());
throw $e; throw $e;
@@ -0,0 +1,327 @@
<?php
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/routine_debug.log');
function applyRoutine(&$excelData, $routineData)
{
try {
// Initial log
error_log("Inizio esecuzione routine Moncler: " . date('Y-m-d H:i:s'));
error_log("Dati routine: " . print_r($routineData, true));
error_log("Dati excel_data: " . print_r($excelData, true));
// Check if excelData is empty
if (empty($excelData)) {
throw new Exception("excelData è vuoto o non valido.");
}
// Extract routine settings with default values
$action1 = trim($routineData['action1'] ?? 'K');
$action2 = trim($routineData['action2'] ?? 'STYLE CODE + STYLE DESCRIPTION');
$action3 = trim($routineData['action3'] ?? 'STYLE CODE');
$action4 = trim($routineData['action4'] ?? 'STYLE DESCRIPTION');
$headers = $routineData['xls_headers'] ?? [];
// Package-related headers
$package_header = 'PACKAGE';
$other_test_header = 'Other test from Control Matrix';
$only_colorfastness_header = 'Only Colorfastness package';
$only_chemical_header = 'Only Chemical package';
if (empty($headers)) {
throw new Exception("Nessun header trovato per la routine Moncler.");
}
// If headers arrive as JSON string, decode them
if (!is_array($headers) && is_string($headers)) {
$decoded_headers = json_decode($headers, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded_headers)) {
$headers = $decoded_headers;
}
}
if (!is_array($headers) || empty($headers)) {
throw new Exception("Gli header della routine non sono validi.");
}
error_log("Header ricevuti: " . print_r($headers, true));
// Normalize headers
$normalized_headers = array_map(function ($header) {
return trim((string)$header);
}, $headers);
error_log("Header normalizzati: " . print_r($normalized_headers, true));
error_log("Action values - action1: '$action1', action2: '$action2', action3: '$action3', action4: '$action4'");
// Find main indexes
$action1_index = array_search($action1, $normalized_headers, true);
$action2_index = array_search($action2, $normalized_headers, true);
$action3_index = array_search($action3, $normalized_headers, true);
$action4_index = array_search($action4, $normalized_headers, true);
// Find package-related column indexes
$package_index = array_search($package_header, $normalized_headers, true);
$other_test_index = array_search($other_test_header, $normalized_headers, true);
$only_colorfastness_index = array_search($only_colorfastness_header, $normalized_headers, true);
$only_chemical_index = array_search($only_chemical_header, $normalized_headers, true);
if ($package_index === false) {
throw new Exception("Colonna PACKAGE non trovata.");
}
error_log(
"Indici colonne - action1: " . var_export($action1_index, true) .
", action2: " . var_export($action2_index, true) .
", action3: " . var_export($action3_index, true) .
", action4: " . var_export($action4_index, true) .
", PACKAGE: " . var_export($package_index, true) .
", Other test from Control Matrix: " . var_export($other_test_index, true) .
", Only Colorfastness package: " . var_export($only_colorfastness_index, true) .
", Only Chemical package: " . var_export($only_chemical_index, true)
);
// Helper: split a value already joined by " - "
$splitJoinedValues = function ($value) {
$value = trim((string)$value);
if ($value === '') {
return [];
}
$parts = preg_split('/\s*-\s*/', $value);
$parts = array_map('trim', $parts);
$parts = array_filter($parts, function ($item) {
return $item !== '';
});
return array_values(array_unique($parts));
};
// Helper: join unique values with " - "
$joinUniqueValues = function (array $values) {
$clean = [];
foreach ($values as $value) {
$value = trim((string)$value);
if ($value !== '' && !in_array($value, $clean, true)) {
$clean[] = $value;
}
}
return implode(' - ', $clean);
};
// Helper: split STYLE CODE + STYLE DESCRIPTION into code + description
$splitStyleCombined = function ($value) {
$value = trim((string)$value);
if ($value === '') {
return ['', ''];
}
// Accept both "CODE - DESC" and "CODE-DESC"
$parts = preg_split('/\s*-\s*/', $value, 2);
$styleCode = trim((string)($parts[0] ?? ''));
$styleDescription = trim((string)($parts[1] ?? ''));
return [$styleCode, $styleDescription];
};
/**
* STEP 0
* Normalize STYLE CODE and STYLE DESCRIPTION before grouping by K
*
* Rules:
* - Read STYLE CODE + STYLE DESCRIPTION
* - If it contains code/description, fill missing values
* - If target field already contains a different value, add it instead of replacing it
* - Avoid duplicates
*/
$step0_applied = false;
if ($action2_index === false || $action3_index === false || $action4_index === false) {
error_log(
"Unable to apply routine step 0. Missing columns - action2: '$action2' (index: " . var_export($action2_index, true) . ")" .
", action3: '$action3' (index: " . var_export($action3_index, true) . ")" .
", action4: '$action4' (index: " . var_export($action4_index, true) . ")"
);
} else {
foreach ($excelData as $index => $row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Riga non valida nello step 0, manca 'data' all'indice $index");
continue;
}
$combinedValue = trim((string)($row['data'][$action2_index] ?? ''));
$currentStyleCode = trim((string)($row['data'][$action3_index] ?? ''));
$currentStyleDescription = trim((string)($row['data'][$action4_index] ?? ''));
if ($combinedValue === '') {
continue;
}
[$parsedStyleCode, $parsedStyleDescription] = $splitStyleCombined($combinedValue);
$styleCodeValues = $splitJoinedValues($currentStyleCode);
$styleDescriptionValues = $splitJoinedValues($currentStyleDescription);
if ($parsedStyleCode !== '' && !in_array($parsedStyleCode, $styleCodeValues, true)) {
$styleCodeValues[] = $parsedStyleCode;
}
if ($parsedStyleDescription !== '' && !in_array($parsedStyleDescription, $styleDescriptionValues, true)) {
$styleDescriptionValues[] = $parsedStyleDescription;
}
$excelData[$index]['data'][$action3_index] = $joinUniqueValues($styleCodeValues);
$excelData[$index]['data'][$action4_index] = $joinUniqueValues($styleDescriptionValues);
error_log(
"Step 0 - excelrow " . ($row['excelrow'] ?? 'N/A') .
" | combined: '" . $combinedValue . "'" .
" | style_code_result: '" . $excelData[$index]['data'][$action3_index] . "'" .
" | style_description_result: '" . $excelData[$index]['data'][$action4_index] . "'"
);
}
$step0_applied = true;
error_log("Routine step 0 completato - Normalizzazione style applicata su " . count($excelData) . " righe");
}
/**
* STEP 1
* Aggregate rows by action1 using normalized STYLE CODE and STYLE DESCRIPTION
* If it fails, continue with STEP 2 only
*/
$step1_applied = false;
if ($action1_index === false || $action3_index === false || $action4_index === false) {
error_log(
"Unable to apply routine step 1. Package merge logic has been applied only. " .
"Missing columns - action1: '$action1' (index: " . var_export($action1_index, true) . ")" .
", action3: '$action3' (index: " . var_export($action3_index, true) . ")" .
", action4: '$action4' (index: " . var_export($action4_index, true) . ")"
);
} else {
$grouped_data = [];
foreach ($excelData as $row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Riga non valida, manca 'data' per excelrow " . ($row['excelrow'] ?? 'N/A'));
continue;
}
$key = $row['data'][$action1_index] ?? '';
$key = trim((string)$key);
$key = $key === '' ? '_empty_' : $key;
if (!isset($grouped_data[$key])) {
$grouped_data[$key] = [
'data' => $row['data'],
'excelrow' => [($row['excelrow'] ?? '')],
'style_codes' => [],
'style_descriptions' => []
];
} else {
$grouped_data[$key]['excelrow'][] = ($row['excelrow'] ?? '');
}
// Collect normalized STYLE CODE values
$styleCodeValues = $splitJoinedValues($row['data'][$action3_index] ?? '');
foreach ($styleCodeValues as $styleCodeValue) {
if (!in_array($styleCodeValue, $grouped_data[$key]['style_codes'], true)) {
$grouped_data[$key]['style_codes'][] = $styleCodeValue;
}
}
// Collect normalized STYLE DESCRIPTION values
$styleDescriptionValues = $splitJoinedValues($row['data'][$action4_index] ?? '');
foreach ($styleDescriptionValues as $styleDescriptionValue) {
if (!in_array($styleDescriptionValue, $grouped_data[$key]['style_descriptions'], true)) {
$grouped_data[$key]['style_descriptions'][] = $styleDescriptionValue;
}
}
}
$new_excel_data = [];
foreach ($grouped_data as $key => $group) {
$row_data = $group['data'];
// Update STYLE CODE and STYLE DESCRIPTION with aggregated unique values
$row_data[$action3_index] = $joinUniqueValues($group['style_codes']);
$row_data[$action4_index] = $joinUniqueValues($group['style_descriptions']);
// Concatenate excelrow values with "+"
$excelrow_clean = array_filter($group['excelrow'], function ($value) {
return $value !== null && $value !== '';
});
$excelrow_value = count($excelrow_clean) > 1
? implode('+', $excelrow_clean)
: (reset($excelrow_clean) ?: '');
$new_excel_data[] = [
'data' => $row_data,
'excelrow' => $excelrow_value
];
}
$excelData = $new_excel_data;
$step1_applied = true;
error_log("Routine step 1 completato - Righe aggregate: " . count($new_excel_data));
error_log("Excelrow aggregati step 1: " . print_r(array_column($new_excel_data, 'excelrow'), true));
}
/**
* STEP 2
* Merge package-related columns into PACKAGE
* Always applied if PACKAGE exists
*/
foreach ($excelData as $index => $row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Riga non valida nello step 2, manca 'data' all'indice $index");
continue;
}
$package_values = [];
$value_package = trim((string)($row['data'][$package_index] ?? ''));
$value_other_test = $other_test_index !== false ? trim((string)($row['data'][$other_test_index] ?? '')) : '';
$value_only_colorfastness = $only_colorfastness_index !== false ? trim((string)($row['data'][$only_colorfastness_index] ?? '')) : '';
$value_only_chemical = $only_chemical_index !== false ? trim((string)($row['data'][$only_chemical_index] ?? '')) : '';
if ($value_package !== '') {
$package_values[] = $value_package;
}
if ($value_other_test !== '') {
$package_values[] = $value_other_test;
}
if ($value_only_colorfastness !== '') {
$package_values[] = $value_only_colorfastness;
}
if ($value_only_chemical !== '') {
$package_values[] = $value_only_chemical;
}
$package_values = array_unique($package_values);
$excelData[$index]['data'][$package_index] = implode(' - ', $package_values);
}
error_log("Routine step 2 completato - Merge package applicato su " . count($excelData) . " righe");
if (!$step0_applied) {
error_log("Warning finale: routine step 0 non applicata");
}
if (!$step1_applied) {
error_log("Warning finale: routine step 1 non applicata, routine step 2 applicata correttamente");
}
error_log("Routine Moncler completata con successo");
} catch (Exception $e) {
error_log("Eccezione nella routine Moncler: " . $e->getMessage());
throw $e;
}
}
@@ -0,0 +1,212 @@
<?php
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/routine_debug.log');
function applyRoutine(&$excelData, $routineData)
{
try {
// Initial log
error_log("Inizio esecuzione nuova routine: " . date('Y-m-d H:i:s'));
error_log("Dati routine: " . print_r($routineData, true));
error_log("Dati excel_data: " . print_r($excelData, true));
// Check if excelData is empty
if (empty($excelData)) {
throw new Exception("excelData è vuoto o non valido.");
}
// Get headers from routine data
$headers = $routineData['xls_headers'] ?? [];
if (empty($headers)) {
throw new Exception("Nessun header trovato per la routine.");
}
// If headers arrive as JSON string, decode them
if (!is_array($headers) && is_string($headers)) {
$decoded_headers = json_decode($headers, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded_headers)) {
$headers = $decoded_headers;
}
}
if (!is_array($headers) || empty($headers)) {
throw new Exception("Gli header della routine non sono validi.");
}
error_log("Header ricevuti: " . print_r($headers, true));
// Normalize headers
$normalized_headers = array_map(function ($header) {
return trim((string)$header);
}, $headers);
error_log("Header normalizzati: " . print_r($normalized_headers, true));
/**
* Define source and target headers
*/
$parte_moncler_header = 'PARTE MONCLER';
$parte_fornitore_header = 'PARTE FORNITORE';
$descr_parte_moncler_header = 'DESCRIZIONE PARTE MONCLER';
$descr_parte_fornitore_header = 'DESCRIZIONE PARTE FORNITORE';
$colore_moncler_header = 'COLORE MONCLER';
$colore_fornitore_header = 'COLORE FORNITORE';
$composizione_tessile_header = 'COMPOSIZIONE TESSILE';
$composizione_totale_header = 'COMPOSIZIONE TOTALE';
$pacchetti_test_header = 'PACCHETTI TEST';
$test_addizionali_header = 'TEST ADDIZIONALI (A5 solo se richiesti dal cliente)';
/**
* Find indexes
*/
$parte_moncler_index = array_search($parte_moncler_header, $normalized_headers, true);
$parte_fornitore_index = array_search($parte_fornitore_header, $normalized_headers, true);
$descr_parte_moncler_index = array_search($descr_parte_moncler_header, $normalized_headers, true);
$descr_parte_fornitore_index = array_search($descr_parte_fornitore_header, $normalized_headers, true);
$colore_moncler_index = array_search($colore_moncler_header, $normalized_headers, true);
$colore_fornitore_index = array_search($colore_fornitore_header, $normalized_headers, true);
$composizione_tessile_index = array_search($composizione_tessile_header, $normalized_headers, true);
$composizione_totale_index = array_search($composizione_totale_header, $normalized_headers, true);
$pacchetti_test_index = array_search($pacchetti_test_header, $normalized_headers, true);
$test_addizionali_index = array_search($test_addizionali_header, $normalized_headers, true);
error_log(
"Indici trovati - " .
"PARTE MONCLER: " . var_export($parte_moncler_index, true) .
", PARTE FORNITORE: " . var_export($parte_fornitore_index, true) .
", DESCRIZIONE PARTE MONCLER: " . var_export($descr_parte_moncler_index, true) .
", DESCRIZIONE PARTE FORNITORE: " . var_export($descr_parte_fornitore_index, true) .
", COLORE MONCLER: " . var_export($colore_moncler_index, true) .
", COLORE FORNITORE: " . var_export($colore_fornitore_index, true) .
", COMPOSIZIONE TESSILE: " . var_export($composizione_tessile_index, true) .
", COMPOSIZIONE TOTALE: " . var_export($composizione_totale_index, true) .
", PACCHETTI TEST: " . var_export($pacchetti_test_index, true) .
", TEST ADDIZIONALI: " . var_export($test_addizionali_index, true)
);
// Required target columns must exist
if ($parte_moncler_index === false) {
throw new Exception("Colonna PARTE MONCLER non trovata.");
}
if ($descr_parte_moncler_index === false) {
throw new Exception("Colonna DESCRIZIONE PARTE MONCLER non trovata.");
}
if ($colore_moncler_index === false) {
throw new Exception("Colonna COLORE MONCLER non trovata.");
}
if ($composizione_tessile_index === false) {
throw new Exception("Colonna COMPOSIZIONE TESSILE non trovata.");
}
if ($pacchetti_test_index === false) {
throw new Exception("Colonna PACCHETTI TEST non trovata.");
}
/**
* Apply merge row by row
*/
foreach ($excelData as $index => $row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Riga non valida, manca 'data' all'indice $index");
continue;
}
// 1. PARTE MONCLER + PARTE FORNITORE -> PARTE MONCLER
$parte_values = [];
$value_parte_moncler = trim((string)($row['data'][$parte_moncler_index] ?? ''));
$value_parte_fornitore = $parte_fornitore_index !== false ? trim((string)($row['data'][$parte_fornitore_index] ?? '')) : '';
if ($value_parte_moncler !== '') {
$parte_values[] = $value_parte_moncler;
}
if ($value_parte_fornitore !== '') {
$parte_values[] = $value_parte_fornitore;
}
$parte_values = array_unique($parte_values);
$excelData[$index]['data'][$parte_moncler_index] = implode(' - ', $parte_values);
// 2. DESCRIZIONE PARTE MONCLER + DESCRIZIONE PARTE FORNITORE -> DESCRIZIONE PARTE MONCLER
$descr_values = [];
$value_descr_moncler = trim((string)($row['data'][$descr_parte_moncler_index] ?? ''));
$value_descr_fornitore = $descr_parte_fornitore_index !== false ? trim((string)($row['data'][$descr_parte_fornitore_index] ?? '')) : '';
if ($value_descr_moncler !== '') {
$descr_values[] = $value_descr_moncler;
}
if ($value_descr_fornitore !== '') {
$descr_values[] = $value_descr_fornitore;
}
$descr_values = array_unique($descr_values);
$excelData[$index]['data'][$descr_parte_moncler_index] = implode(' - ', $descr_values);
// 3. COLORE MONCLER + COLORE FORNITORE -> COLORE MONCLER
$colore_values = [];
$value_colore_moncler = trim((string)($row['data'][$colore_moncler_index] ?? ''));
$value_colore_fornitore = $colore_fornitore_index !== false ? trim((string)($row['data'][$colore_fornitore_index] ?? '')) : '';
if ($value_colore_moncler !== '') {
$colore_values[] = $value_colore_moncler;
}
if ($value_colore_fornitore !== '') {
$colore_values[] = $value_colore_fornitore;
}
$colore_values = array_unique($colore_values);
$excelData[$index]['data'][$colore_moncler_index] = implode(' - ', $colore_values);
// 4. COMPOSIZIONE TESSILE + COMPOSIZIONE TOTALE -> COMPOSIZIONE TESSILE
$composizione_values = [];
$value_composizione_tessile = trim((string)($row['data'][$composizione_tessile_index] ?? ''));
$value_composizione_totale = $composizione_totale_index !== false ? trim((string)($row['data'][$composizione_totale_index] ?? '')) : '';
if ($value_composizione_tessile !== '') {
$composizione_values[] = $value_composizione_tessile;
}
if ($value_composizione_totale !== '') {
$composizione_values[] = $value_composizione_totale;
}
$composizione_values = array_unique($composizione_values);
$excelData[$index]['data'][$composizione_tessile_index] = implode(' - ', $composizione_values);
// 5. PACCHETTI TEST + TEST ADDIZIONALI -> PACCHETTI TEST
$pacchetti_values = [];
$value_pacchetti_test = trim((string)($row['data'][$pacchetti_test_index] ?? ''));
$value_test_addizionali = $test_addizionali_index !== false ? trim((string)($row['data'][$test_addizionali_index] ?? '')) : '';
if ($value_pacchetti_test !== '') {
$pacchetti_values[] = $value_pacchetti_test;
}
if ($value_test_addizionali !== '') {
$pacchetti_values[] = $value_test_addizionali;
}
$pacchetti_values = array_unique($pacchetti_values);
$excelData[$index]['data'][$pacchetti_test_index] = implode(' - ', $pacchetti_values);
}
error_log("Nuova routine completata con successo. Righe elaborate: " . count($excelData));
} catch (Exception $e) {
error_log("Eccezione nella nuova routine: " . $e->getMessage());
throw $e;
}
}
+130
View 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;
})();
+1 -1
View File
@@ -23,7 +23,7 @@ try {
foreach ($_POST as $key => $value) { foreach ($_POST as $key => $value) {
if ($key !== 'iddatadb' && !in_array($key, $excludeFields)) { if ($key !== 'iddatadb' && !in_array($key, $excludeFields)) {
$updates[] = "$key = ?"; $updates[] = "$key = ?";
$values[] = htmlspecialchars($value); $values[] = $value;
} }
} }
$values[] = $iddatadb; $values[] = $iddatadb;
+9 -1
View File
@@ -12,6 +12,7 @@ try {
$iddatadb = intval($_POST['iddatadb']); $iddatadb = intval($_POST['iddatadb']);
$idclient = isset($_POST['idclient']) ? (is_numeric($_POST['idclient']) ? intval($_POST['idclient']) : null) : null; $idclient = isset($_POST['idclient']) ? (is_numeric($_POST['idclient']) ? intval($_POST['idclient']) : null) : null;
$clienteFornitoreId = isset($_POST['cliente_fornitore_id']) ? (is_numeric($_POST['cliente_fornitore_id']) ? intval($_POST['cliente_fornitore_id']) : null) : null;
$db = DBHandlerSelect::getInstance(); $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection(); $pdo = $db->getConnection();
@@ -22,10 +23,11 @@ try {
// (NON tocchiamo MySQL, gestiamo solo qui la traduzione) // (NON tocchiamo MySQL, gestiamo solo qui la traduzione)
$fixedAliasMap = [ $fixedAliasMap = [
'ClienteResponsabile' => 'cliente_responsabile_id', 'ClienteResponsabile' => 'cliente_responsabile_id',
'ClienteFornitore' => 'cliente_fornitore_id',
'ClienteAnalisi' => 'clienteAnalisi',
'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id', 'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id',
'AnagraficaCertestObject' => 'anagrafica_certest_object_id', 'AnagraficaCertestObject' => 'anagrafica_certest_object_id',
'AnagraficaCertestService' => 'anagrafica_certest_service_id', 'AnagraficaCertestService' => 'anagrafica_certest_service_id',
'ClienteFornitore' => 'cliente_fornitore_id',
'ConsegnaRichiesta' => 'consegna_richiesta', 'ConsegnaRichiesta' => 'consegna_richiesta',
]; ];
@@ -137,6 +139,12 @@ try {
$params[] = $idclient; $params[] = $idclient;
} }
// 5a2) cliente_fornitore_id (se presente)
if (isset($clienteFornitoreId)) {
$setParts[] = "cliente_fornitore_id = ?";
$params[] = $clienteFornitoreId;
}
// 5b) fixed fields dal POST // 5b) fixed fields dal POST
// QUI è il punto chiave: accettiamo SOLO colonne reali (realWhitelist), // QUI è il punto chiave: accettiamo SOLO colonne reali (realWhitelist),
// ma se dal JS arrivassero ancora key logiche, funzionerebbe uguale // ma se dal JS arrivassero ancora key logiche, funzionerebbe uguale
+90
View File
@@ -0,0 +1,90 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
exit;
}
$partId = isset($_POST['part_id']) ? (int)$_POST['part_id'] : 0;
$iddatadb = isset($_POST['iddatadb']) ? (int)$_POST['iddatadb'] : 0;
$idmatrice = isset($_POST['idmatrice']) ? (int)$_POST['idmatrice'] : 0;
$analysisRecordkey = trim($_POST['analysis_recordkey'] ?? '');
$analysisName = trim($_POST['analysis_name'] ?? '');
$analysisMethod = trim($_POST['analysis_method'] ?? '');
$analysisLevel = isset($_POST['analysis_level']) && $_POST['analysis_level'] !== '' ? (int)$_POST['analysis_level'] : null;
$isWebSelectable = isset($_POST['is_web_selectable']) ? (int)$_POST['is_web_selectable'] : 0;
$isAccredited = isset($_POST['is_accredited']) ? (int)$_POST['is_accredited'] : 0;
if ($partId <= 0 || $analysisRecordkey === '') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Missing required data']);
exit;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("
INSERT INTO identification_parts_analyses (
part_id,
iddatadb,
idmatrice,
analysis_recordkey,
analysis_name,
analysis_method,
analysis_level,
is_web_selectable,
is_accredited
) VALUES (
:part_id,
:iddatadb,
:idmatrice,
:analysis_recordkey,
:analysis_name,
:analysis_method,
:analysis_level,
:is_web_selectable,
:is_accredited
)
ON DUPLICATE KEY UPDATE
analysis_name = VALUES(analysis_name),
analysis_method = VALUES(analysis_method),
analysis_level = VALUES(analysis_level),
is_web_selectable = VALUES(is_web_selectable),
is_accredited = VALUES(is_accredited),
iddatadb = VALUES(iddatadb),
idmatrice = VALUES(idmatrice),
updated_at = CURRENT_TIMESTAMP
");
$stmt->execute([
':part_id' => $partId,
':iddatadb' => $iddatadb ?: null,
':idmatrice' => $idmatrice ?: null,
':analysis_recordkey' => $analysisRecordkey,
':analysis_name' => $analysisName ?: null,
':analysis_method' => $analysisMethod ?: null,
':analysis_level' => $analysisLevel,
':is_web_selectable' => $isWebSelectable,
':is_accredited' => $isAccredited,
]);
echo json_encode([
'success' => true,
'message' => 'Association saved'
]);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
+1 -1
View File
@@ -761,7 +761,7 @@
"IdSchemaCustomFields": 182, "IdSchemaCustomFields": 182,
"ConteggioClienti": 0, "ConteggioClienti": 0,
"Nome": "Ralph Lauren - All testing V.6", "Nome": "Ralph Lauren - All testing V.6",
"Descrizione": null "Descrizione": "AGGIORNAMENTO AL 16\/03\/2026 per estrazione fatturazione"
}, },
{ {
"IdSchemaCustomFields": 183, "IdSchemaCustomFields": 183,
+62
View 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()]);
}
@@ -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
View 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]);
+150 -76
View File
@@ -56,6 +56,56 @@
input:checked+.slider:before { input:checked+.slider:before {
transform: translateX(14px); transform: translateX(14px);
} }
.badge-source {
font-size: 11px;
padding: 0.30rem 0.55rem;
border-radius: 999px;
font-weight: 600;
display: inline-block;
min-width: 50px;
text-align: center;
line-height: 1.2;
}
.badge-source-xls {
background-color: #e7f1ff;
color: #0d6efd;
}
.badge-source-api {
background-color: #e8fff1;
color: #198754;
}
#xlsTemplatesTable {
font-size: 13px;
}
#xlsTemplatesTable th,
#xlsTemplatesTable td {
vertical-align: middle;
white-space: nowrap;
}
#xlsTemplatesTable td.description-cell,
#xlsTemplatesTable td.client-cell,
#xlsTemplatesTable td.name-cell,
#xlsTemplatesTable td.button-cell {
white-space: normal;
}
.table-actions {
min-width: 120px;
}
.table-actions .btn {
padding: 0.25rem 0.45rem;
}
.compact-card .card-body {
padding: 1rem;
}
</style> </style>
</head> </head>
@@ -65,18 +115,20 @@
<!--sidebar wrapper --> <!--sidebar wrapper -->
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<!--end sidebar wrapper --> <!--end sidebar wrapper -->
<!--start header --> <!--start header -->
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
<!--end header --> <!--end header -->
<!--start page wrapper --> <!--start page wrapper -->
<div class="page-wrapper"> <div class="page-wrapper">
<div class="page-content"> <div class="page-content">
<?php include('top_stat_widget.php'); ?> <?php include('top_stat_widget.php'); ?>
<div class="card radius-10"> <div class="card radius-10 compact-card">
<div class="card-header"> <div class="card-header">
<div class="d-flex align-items-center justify-content-between"> <div class="d-flex align-items-center justify-content-between">
<h6 class="mb-0">XLS Templates Dashboard</h6> <h6 class="mb-0">Templates Dashboard</h6>
<a href="insert_template_xls.php" class="btn btn-success ms-auto"> <a href="insert_template_xls.php" class="btn btn-success ms-auto">
<i class="fas fa-plus"></i> New Template <i class="fas fa-plus"></i> New Template
</a> </a>
@@ -85,22 +137,22 @@
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <div class="table-responsive">
<table id="xlsTemplatesTable" class="table table-striped table-bordered"> <table id="xlsTemplatesTable" class="table table-striped table-bordered table-sm w-100">
<thead> <thead>
<tr> <tr>
<th>ID</th>
<th><?= htmlspecialchars($nametemplate, ENT_QUOTES, 'UTF-8'); ?></th>
<th><?= htmlspecialchars($rowheader, ENT_QUOTES, 'UTF-8'); ?></th>
<th><?= htmlspecialchars($columnheader, ENT_QUOTES, 'UTF-8'); ?></th>
<th><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></th>
<th>Client Name</th>
<th>Button Label</th>
<th>Status</th> <!-- Aggiunta colonna Status -->
<th><?= htmlspecialchars($action, ENT_QUOTES, 'UTF-8'); ?></th> <th><?= htmlspecialchars($action, ENT_QUOTES, 'UTF-8'); ?></th>
<th><?= htmlspecialchars($nametemplate, ENT_QUOTES, 'UTF-8'); ?></th>
<th>Type</th>
<th>Row</th>
<th>Col</th>
<th><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></th>
<th>Client</th>
<th>Button</th>
<th>Status</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<!-- DataTables riempirà questa sezione automaticamente --> <!-- DataTables will populate this section automatically -->
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -110,25 +162,19 @@
</div> </div>
</div> </div>
<!--end page wrapper --> <!--end page wrapper -->
<!--start overlay--> <!--start overlay-->
<div class="overlay toggle-icon"></div> <div class="overlay toggle-icon"></div>
<!--end overlay--> <!--end overlay-->
<!--Start Back To Top Button--> <!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a> <a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<!--End Back To Top Button--> <!--End Back To Top Button-->
<?php include('include/footer.php'); ?> <?php include('include/footer.php'); ?>
</div> </div>
<!--end wrapper--> <!--end wrapper-->
<!-- search modal -->
<?php //include('include/searchmodal.php');
?>
<!-- end search modal -->
<!--start switcher-->
<?php //include('include/themeswitcher.php');
?>
<!--end switcher-->
<?php include('jsinclude.php'); ?> <?php include('jsinclude.php'); ?>
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script> <script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script> <script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
@@ -139,47 +185,92 @@
processing: true, processing: true,
serverSide: false, serverSide: false,
ajax: 'load_templates.php', ajax: 'load_templates.php',
pageLength: 50,
autoWidth: false,
columns: [{ columns: [{
data: 'id', // ID del template data: 'id',
title: "ID" orderable: false,
}, searchable: false,
{ title: "Actions",
data: 'name', // Nome del template className: "table-actions text-center",
title: "Template Name"
},
{
data: 'header_row', // Riga degli header
title: "Header Row"
},
{
data: 'start_column', // Colonna di partenza
title: "Start Column"
},
{
data: 'description', // Descrizione del template
title: "Description",
defaultContent: 'No description'
},
{
data: null, // Nuova colonna per Client Name e ID
title: "Client Name",
render: function(data, type, row) { render: function(data, type, row) {
const clientName = row.clientname || "No client"; return `
const clientId = row.idclient || "N/A"; <div class="d-flex justify-content-center gap-1">
return `${clientName} (ID: ${clientId})`; <a href="edit_template_xls.php?id=${data}" class="btn btn-sm btn-primary" title="Edit">
<i class="bx bx-edit-alt"></i>
</a>
<a href="mapping_template_xls_scheme2.php?id=${data}" class="btn btn-sm btn-success" title="Mapping">
<i class="bx bx-link-alt"></i>
</a>
<button class="btn btn-sm btn-danger" onclick="confirmDelete(${data})" title="Delete">
<i class="bx bx-trash"></i>
</button>
</div>
`;
} }
}, },
{ {
data: 'button_label', // Nuova colonna per Button Label data: 'name',
title: "Button Label", title: "Template Name",
className: "name-cell"
},
{
data: 'source_type',
title: "Type",
className: "text-center",
render: function(data, type, row) {
const sourceType = (data || 'XLS').toUpperCase();
if (type === 'display') {
if (sourceType === 'API') {
return '<span class="badge-source badge-source-api">API</span>';
}
return '<span class="badge-source badge-source-xls">XLS</span>';
}
return sourceType;
}
},
{
data: 'header_row',
title: "Row",
className: "text-center",
defaultContent: ''
},
{
data: 'start_column',
title: "Col",
className: "text-center",
defaultContent: ''
},
{
data: 'description',
title: "Description",
className: "description-cell",
defaultContent: 'No description'
},
{
data: null,
title: "Client",
className: "client-cell",
render: function(data, type, row) {
const clientName = row.clientname || "No client";
const clientId = row.idclient || "N/A";
return `${clientName} <small class="text-muted">(ID: ${clientId})</small>`;
}
},
{
data: 'button_label',
title: "Button",
className: "button-cell",
defaultContent: 'Click Me' defaultContent: 'Click Me'
}, },
{ {
data: 'status', // Stato con Toggle Switch data: 'status',
title: "Status", title: "Status",
orderable: false, orderable: false,
searchable: false, searchable: false,
className: "text-center",
render: function(status, type, row) { render: function(status, type, row) {
let checked = (status === "active") ? "checked" : ""; let checked = (status === "active") ? "checked" : "";
return ` return `
@@ -189,31 +280,13 @@
</label> </label>
`; `;
} }
},
{
data: 'id', // Azioni: Modifica, Mappatura e Eliminazione
orderable: false,
searchable: false,
title: "Actions",
render: function(data) {
return `
<div class="d-flex">
<a href="edit_template_xls.php?id=${data}" class="btn btn-sm btn-primary me-1">
<i class="bx bx-edit-alt"></i>
</a>
<a href="mapping_template_xls_scheme2.php?id=${data}" class="btn btn-sm btn-success me-1">
<i class="bx bx-link-alt"></i>
</a>
<button class="btn btn-sm btn-danger" onclick="confirmDelete(${data})">
<i class="bx bx-trash"></i>
</button>
</div>
`;
}
} }
], ],
dom: '<"card-header border-bottom p-3"<"d-flex align-items-center"<"card-title mb-0 flex-grow-1"f>>>rt<"card-footer border-top p-3"<"d-flex align-items-center"<"me-auto"l><"d-flex gap-2"ip>>>', dom: '<"card-header border-bottom p-3"<"d-flex align-items-center"<"card-title mb-0 flex-grow-1"f>>>rt<"card-footer border-top p-3"<"d-flex align-items-center"<"me-auto"l><"d-flex gap-2"ip>>>',
lengthMenu: [10, 25, 50, 100], lengthMenu: [10, 25, 50, 100],
order: [
[1, 'asc']
],
language: { language: {
search: "Cerca:", search: "Cerca:",
lengthMenu: "Mostra _MENU_ elementi", lengthMenu: "Mostra _MENU_ elementi",
@@ -252,20 +325,21 @@
$.ajax({ $.ajax({
url: "update_template_status.php", url: "update_template_status.php",
type: "POST", type: "POST",
dataType: "json",
data: { data: {
id: templateId, id: templateId,
status: newStatus status: newStatus
}, },
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
console.log("Status updated successfully."); console.log("Status updated successfully.");
} else { } else {
console.error("Error updating status:", response.message); console.error("Error updating status:", response.message);
alert("Error updating status: " + response.message); alert("Error updating status: " + response.message);
} }
}, },
error: function() { error: function(xhr) {
console.error("AJAX error."); console.error("AJAX error:", xhr.responseText);
} }
}); });
}); });
File diff suppressed because it is too large Load Diff
+57 -16
View File
@@ -6,42 +6,84 @@ if (!Auth::check()) {
redirectTo('../public/login'); redirectTo('../public/login');
} }
$user = Auth::user(); if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$id = $_POST['iduserlogin']; header('Location: user-profile.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') { $user = Auth::user();
$first_name = $_POST['first_name']; $id = isset($_POST['iduserlogin']) ? (int)$_POST['iduserlogin'] : 0;
$last_name = $_POST['last_name'];
$email = $_POST['email']; $first_name = trim($_POST['first_name'] ?? '');
$password = $_POST['password'] ?: null; $last_name = trim($_POST['last_name'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = trim($_POST['password'] ?? '');
$lims_user_id = isset($_POST['lims_user_id']) && $_POST['lims_user_id'] !== ''
? (int)$_POST['lims_user_id']
: null;
$lims_global_user_id = isset($_POST['lims_global_user_id']) && $_POST['lims_global_user_id'] !== ''
? (int)$_POST['lims_global_user_id']
: null;
if ($id <= 0) {
die('Invalid user ID');
}
$db = DBHandlerSelect::getInstance()->getConnection(); $db = DBHandlerSelect::getInstance()->getConnection();
// Gestione avatar // Current avatar
$avatar = $user->present()->avatar; $avatar = $user->present()->avatar;
// Avatar upload
if (isset($_FILES['avatar']) && $_FILES['avatar']['error'] === UPLOAD_ERR_OK) { if (isset($_FILES['avatar']) && $_FILES['avatar']['error'] === UPLOAD_ERR_OK) {
$avatar = time() . '_' . basename($_FILES['avatar']['name']); $originalName = basename($_FILES['avatar']['name']);
$safeName = preg_replace('/[^A-Za-z0-9_\.-]/', '_', $originalName);
$avatar = time() . '_' . $safeName;
$uploadDir = __DIR__ . '/../../public/upload/users/'; $uploadDir = __DIR__ . '/../../public/upload/users/';
if (!is_dir($uploadDir)) { if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true); mkdir($uploadDir, 0755, true);
} }
move_uploaded_file($_FILES['avatar']['tmp_name'], $uploadDir . $avatar); move_uploaded_file($_FILES['avatar']['tmp_name'], $uploadDir . $avatar);
} }
// Aggiornamento dati // Update main user data
$sql = "UPDATE auth_users SET first_name = ?, last_name = ?, email = ?, avatar = ? WHERE id = ?"; $sql = "UPDATE auth_users
SET first_name = ?,
last_name = ?,
email = ?,
avatar = ?,
lims_user_id = ?,
lims_global_user_id = ?
WHERE id = ?";
$stmt = $db->prepare($sql); $stmt = $db->prepare($sql);
$stmt->execute([$first_name, $last_name, $email, $avatar, $id]); $stmt->execute([
$first_name,
$last_name,
$email,
$avatar,
$lims_user_id,
$lims_global_user_id,
$id
]);
// Aggiornamento password se fornita // Update password only if provided
if ($password) { if ($password !== '') {
$hashedPassword = password_hash($password, PASSWORD_BCRYPT); $hashedPassword = password_hash($password, PASSWORD_BCRYPT);
$sql = "UPDATE auth_users SET password = ? WHERE id = ?"; $sql = "UPDATE auth_users SET password = ? WHERE id = ?";
$stmt = $db->prepare($sql); $stmt = $db->prepare($sql);
$stmt->execute([$hashedPassword, $id]); $stmt->execute([$hashedPassword, $id]);
} }
// Aggiorna la sessione con i nuovi dati // Update session
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$_SESSION["nameuser"] = $first_name; $_SESSION["nameuser"] = $first_name;
$_SESSION["surnameuser"] = $last_name; $_SESSION["surnameuser"] = $last_name;
$_SESSION["emailuser"] = $email; $_SESSION["emailuser"] = $email;
@@ -49,4 +91,3 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Location: user-profile.php'); header('Location: user-profile.php');
exit; exit;
}
+19 -2
View File
@@ -18,10 +18,27 @@ if (!$data || !isset($data['template_id'], $data['xls_headers'])) {
$templateId = $data['template_id']; $templateId = $data['template_id'];
$xlsHeaders = $data['xls_headers']; $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 { try {
$stmt = $pdo->prepare("UPDATE excel_templates SET xls_headers = ? WHERE id = ?"); $sql = "UPDATE excel_templates SET xls_headers = ?";
$result = $stmt->execute([$xlsHeaders, $templateId]); $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) { if (!$result) {
echo json_encode(["success" => false, "message" => "Database update failed"]); echo json_encode(["success" => false, "message" => "Database update failed"]);
+11
View File
@@ -48,6 +48,17 @@
<label for="email" class="form-label">Email</label> <label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" value="<?= htmlspecialchars($emailuser); ?>" required> <input type="email" class="form-control" id="email" name="email" value="<?= htmlspecialchars($emailuser); ?>" required>
</div> </div>
<div class="mb-3">
<label for="lims_user_id" class="form-label">Accettatore</label>
<input type="number" class="form-control" id="lims_user_id" name="lims_user_id" value="<?= htmlspecialchars($lims_user_id ?? ''); ?>">
</div>
<div class="mb-3">
<label for="lims_global_user_id" class="form-label">LIMS Global</label>
<input type="number" class="form-control" id="lims_global_user_id" name="lims_global_user_id" value="<?= htmlspecialchars($lims_global_user_id ?? ''); ?>">
</div>
<div class="mb-3"> <div class="mb-3">
<label for="avatar" class="form-label">Profile Picture</label> <label for="avatar" class="form-label">Profile Picture</label>
<input type="file" class="form-control" id="avatar" name="avatar"> <input type="file" class="form-control" id="avatar" name="avatar">
+42
View File
@@ -59,6 +59,35 @@ $validators[] = function (int $iddatadb, array $ctx): array {
return []; return [];
}; };
// 2. ConsegnaRichiesta must be filled and >= today
$validators[] = function (int $iddatadb, array $ctx): array {
$record = $ctx['record'] ?? null;
if (!$record) {
return [];
}
$consegna = $record['consegna_richiesta'] ?? null;
if (empty($consegna)) {
return [[
'field' => 'ConsegnaRichiesta',
'message' => 'ConsegnaRichiesta è obbligatorio.',
]];
}
$consegnaDate = DateTime::createFromFormat('Y-m-d', $consegna) ?: DateTime::createFromFormat('Y-m-d H:i:s', $consegna);
$today = new DateTime('today');
if ($consegnaDate && $consegnaDate < $today) {
return [[
'field' => 'ConsegnaRichiesta',
'message' => 'ConsegnaRichiesta deve essere uguale o superiore alla data odierna (' . $today->format('Y-m-d') . ').',
]];
}
return [];
};
// ── Main ──────────────────────────────────────────────────────────────────── // ── Main ────────────────────────────────────────────────────────────────────
try { try {
@@ -75,6 +104,18 @@ try {
$iddatadbList = array_column($rows, 'iddatadb'); $iddatadbList = array_column($rows, 'iddatadb');
$placeholders = implode(',', array_fill(0, count($iddatadbList), '?')); $placeholders = implode(',', array_fill(0, count($iddatadbList), '?'));
// Records (datadb) for fixed field validation
$stmt = $pdo->prepare("
SELECT iddatadb, consegna_richiesta
FROM datadb
WHERE iddatadb IN ($placeholders)
");
$stmt->execute($iddatadbList);
$recordsInfo = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$recordsInfo[(int)$r['iddatadb']] = $r;
}
// Parts (full rows, so validators can report which parts have issues) // Parts (full rows, so validators can report which parts have issues)
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
SELECT iddatadb, part_number, part_description, idmatrice SELECT iddatadb, part_number, part_description, idmatrice
@@ -97,6 +138,7 @@ try {
// Build context for validators // Build context for validators
$ctx = [ $ctx = [
'record' => $recordsInfo[$iddatadb] ?? null,
'parts' => $partsInfo[$iddatadb] ?? [], 'parts' => $partsInfo[$iddatadb] ?? [],
]; ];
+124
View 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]);
}