Compare commits

...

82 Commits

Author SHA1 Message Date
RMubarakzyanov 9eb257d237 json bindings 2026-06-13 20:19:36 +03:00
RMubarakzyanov d623ee797c Merge branch 'main' into features/jsonintegration 2026-06-11 15:33:30 +03:00
RMubarakzyanov 4dd7b89c22 added mandatory field checking 2026-06-11 13:41:31 +03:00
RMubarakzyanov dec42b4442 Merge branch 'main' into feature/analysis 2026-06-11 09:37:01 +03:00
solocla d088364a0d report search 2026-06-10 11:02:43 +02:00
solocla 6e43a178af added fixed field in json 2026-06-09 09:46:21 +02:00
solocla 25bd916221 import insert json 2026-06-08 12:45:37 +02:00
solocla dab8d9aebf fixed mapping for json 2026-06-08 08:47:17 +02:00
solocla 375a10a678 filter analysis web 2026-06-08 07:40:05 +02:00
solocla 15990be884 fxied column order LIMS 2026-06-04 16:48:22 +02:00
RMubarakzyanov c3a6dd73b6 import backoff 2026-05-28 23:59:15 +03:00
solocla 44ed1186e0 added richmento pelletteria routine 2026-05-28 13:50:52 +02:00
solocla 9050cb1006 routine burberry 2026-05-26 12:15:45 +02:00
solocla e6820fdb62 added order column 2026-05-25 10:59:58 +02:00
solocla 5da37a7836 paulshark routine 2026-05-22 12:27:53 +02:00
solocla c5f27cb69a routine fendi 2026-05-21 10:11:53 +02:00
solocla 1d81d6c996 fixed import update 2026-05-20 18:47:41 +02:00
solocla 0c72dbf5ae sort first with exactly phrase 2026-05-20 17:01:07 +02:00
solocla 8455be04e1 remove hidden from xls import to avoid big file dimension 2026-05-20 16:47:18 +02:00
solocla e42d1b9c51 fix limit dropdown 2026-05-19 16:20:55 +02:00
solocla 3e69e3c322 fixed routine valentino 2026-05-19 11:23:26 +02:00
solocla 0eb4f7a2ad valentino routine 2026-05-19 11:12:18 +02:00
solocla 4f2cfc1930 fix render gird for doppi apici 2026-05-18 15:38:43 +02:00
solocla df075dd76a main field show in imported 2026-05-14 10:14:59 +02:00
solocla 6460454201 fixed main field 2026-05-14 10:00:47 +02:00
solocla 574ddbbd32 fixed propagation 2026-05-13 15:12:13 +02:00
solocla 41f414db5c 2 main 2026-05-13 14:58:13 +02:00
solocla 4a863e8c16 fixed double scroll bar parts dropdown 2026-05-13 14:26:11 +02:00
solocla b431f1d4e9 added top scrollbar imported 2026-05-13 14:22:43 +02:00
solocla f97b52f158 skip empty parts during clone and LIMS export 2026-05-13 11:54:28 +02:00
solocla 836fc055ec cliente reponsabile change based on client 2026-05-12 15:48:35 +02:00
solocla e8dd585df4 added arrows to jump parts 2026-05-12 09:37:52 +02:00
solocla 198b8c08ad Revert "update scheme different obbligatorioweb"
This reverts commit a3eb0f0a57.
2026-05-11 14:47:42 +02:00
solocla 28c467d55e fixed order import 2026-05-11 14:24:35 +02:00
solocla 56eee99a67 change template and import o use also sheet number of XLS 2026-05-09 15:39:43 +02:00
solocla f514b3d2c7 added sheet and config API in insert edit template 2026-05-09 09:44:52 +02:00
solocla a3eb0f0a57 update scheme different obbligatorioweb 2026-05-09 09:30:59 +02:00
solocla aa355905d7 update schema 2026-05-08 11:10:21 +02:00
solocla b38f3e1240 Stop tracking generated schema response file 2026-05-06 16:52:53 +02:00
solocla cbd0c5b68a annotation color and clone templates 2026-05-06 16:44:42 +02:00
solocla f3e5cb4ffd fixed capitolato riferimento 2026-05-05 09:37:18 +02:00
solocla 0f0c6a04b7 fixed Mix 2026-05-05 09:07:14 +02:00
solocla 813bd66f96 scroll table parts 2026-05-04 12:10:12 +02:00
solocla ce00247d1c edit templtae lingue e colori 2026-05-04 11:21:30 +02:00
solocla 3a7dd266c8 fixed quotation 2026-04-29 12:16:44 +02:00
solocla ba8dc4c721 added get 2026-04-29 08:38:16 +02:00
RMubarakzyanov cfbbc36116 Fix TZ Issue 2026-04-28 12:53:21 +03:00
solocla 50d578eea1 commesssa on commessaweb 2026-04-27 16:02:50 +02:00
solocla 6b0d2aa9b9 fixed color and import dahboard 2026-04-27 14:34:45 +02:00
RMubarakzyanov 67bbd9bbbb export analyses 2026-04-21 00:09:59 +03:00
solocla fa7997c980 tested component fixed 2026-04-19 17:38:35 +02:00
solocla 66be442eb6 scrollbar import page 2026-04-17 15:00:17 +02:00
solocla 19a2d6e3f7 scrollbar also top 2026-04-17 10:47:41 +02:00
solocla a15ab08576 Merge branch 'main' of http://192.168.1.93:8418/solocla/trf_certest 2026-04-17 08:43:32 +02:00
solocla f71e8a56b5 cloneparts 2026-04-17 08:43:05 +02:00
RMubarakzyanov cb38bfb75a fix resize on tolims page 2026-04-16 11:39:00 +03:00
solocla 28a708dad3 removed limit admin 2026-04-15 16:19:39 +02:00
solocla 6b9cf20ab9 fixed resize and analysis 2026-04-15 14:37:49 +02:00
solocla d40fc7d177 update gitignore 2026-04-15 08:49:06 +02:00
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
RMubarakzyanov 2a7b1fae17 imported && tolims 2026-03-26 16:05:17 +03:00
101 changed files with 49074 additions and 1498 deletions
+7 -1
View File
@@ -40,10 +40,14 @@ yarn-error.log
/public/userarea/logaspi/
/public/userarea/logs/api/
/public/userarea/logs/api/**
/public/userarea/logs/
/public/userarea/logs/**
/public/userarea/photostrf/
/public/userarea/class/*.log
/public/userarea/class/curl_auth_debug.log
/public/userarea/class/curl_request_debug.log
/public/userarea/schema_dettagli_response.json
public/userarea/schemi_base_response.json
# File XLSX temporanei importati
/public/userarea/imported_trf/*.xlsx
@@ -54,4 +58,6 @@ yarn-error.log
/public/photostrf/qrcodes/
# Ignora tutti i log ovunque
*.log
*.log
public/userarea/cache/
+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()]);
}
+854
View File
@@ -0,0 +1,854 @@
(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 = true;
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");
}
const cacheKey = String(matrixId) + "_WEB_ONLY";
if (analysisLoadedCache[cacheKey]) {
renderAnalysesList(analysisLoadedCache[cacheKey]);
return;
}
setAnalysisLoadingState(true);
$.ajax({
url: "get_analisi_matrice_filter.php",
method: "GET",
dataType: "json",
data: {
id_matrice: matrixId,
web_only: 1,
},
})
.done(function (response) {
const analyses = Array.isArray(response.value)
? response.value.filter(function (item) {
return (
item.SelezionabileSuWeb === true ||
item.SelezionabileSuWeb === 1 ||
item.SelezionabileSuWeb === "1"
);
})
: [];
analysisLoadedCache[cacheKey] = 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();
});
}
// WEB only is now fixed by default
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", "");
}
});
})();
+38 -26
View File
@@ -22,6 +22,8 @@ $(document).ready(function () {
let partsListData = [];
// DIMENSIONE GLOBALE MARKER
let globalMarkerSize = 16;
// COLORE TESTO LISTA DESCRIZIONI
let globalDescriptionColor = "#000000";
// ===================
// MODAL INITIALIZATION
@@ -138,6 +140,7 @@ $(document).ready(function () {
descriptionTextbox = null;
markerObjects = {};
globalMarkerSize = 16;
globalDescriptionColor = "#000000";
$("#photoSelectorContainerAnnotations").empty().hide();
$("#samplePhotoAnnotations").attr("src", "");
$("#partsListAnnotations").empty();
@@ -177,7 +180,25 @@ $(document).ready(function () {
updateMarkers();
markUnsaved();
});
// ===================
// COLORE LISTA DESCRIZIONI
// ===================
$(document)
.off("input.descriptionColor", "#descriptionColorPickerAnnotations")
.on(
"input.descriptionColor",
"#descriptionColorPickerAnnotations",
function () {
globalDescriptionColor = $(this).val();
if (descriptionTextbox && fabricCanvas) {
descriptionTextbox.set("fill", globalDescriptionColor);
fabricCanvas.renderAll();
}
markUnsaved();
},
);
// ===================
// PHOTO LOADERS
// ===================
@@ -614,17 +635,11 @@ $(document).ready(function () {
partsListData.forEach((part) => {
const partNumber = part.part_number;
const partDescription = part.part_description;
const isMixPart = String(part.mix || "N").toUpperCase() === "Y";
const partColor =
partColors[partNumber] ||
(partDescription.toLowerCase().startsWith("mix")
? "#0000ff"
: "#ff0000");
if (
partNumber &&
partDescription &&
(showMixParts ||
!partDescription.toLowerCase().startsWith("mix"))
) {
partColors[partNumber] || (isMixPart ? "#0000ff" : "#ff0000");
if (partNumber && partDescription && (showMixParts || !isMixPart)) {
const colorOptions = predefinedColors
.map(
(color) =>
@@ -922,11 +937,10 @@ $(document).ready(function () {
) {
partsListData = response.parts;
response.parts.forEach((part) => {
const defaultColor = part.part_description
.toLowerCase()
.startsWith("mix")
? "#0000ff"
: "#ff0000";
const isMixPart =
String(part.mix || "N").toUpperCase() === "Y";
const defaultColor = isMixPart ? "#0000ff" : "#ff0000";
partColors[part.part_number] = defaultColor;
});
updatePartsList();
@@ -1003,11 +1017,10 @@ $(document).ready(function () {
(p) => p.part_number == marker.partNumber,
);
const partDescription = part ? part.part_description : "";
if (
!showMixParts &&
partDescription &&
partDescription.toLowerCase().startsWith("mix")
) {
const isMixPart =
part && String(part.mix || "N").toUpperCase() === "Y";
if (!showMixParts && isMixPart) {
console.log("Ignoro marker per parte Mix:", marker.partNumber);
return;
}
@@ -1115,11 +1128,10 @@ $(document).ready(function () {
}
const partsList = partsListData
.filter(
(part) =>
showMixParts ||
!part.part_description.toLowerCase().startsWith("mix"),
)
.filter((part) => {
const isMixPart = String(part.mix || "N").toUpperCase() === "Y";
return showMixParts || !isMixPart;
})
.map((part) => `${part.part_number} ${part.part_description}`);
const text = partsList.join("\n");
@@ -1139,7 +1151,7 @@ $(document).ready(function () {
backgroundColor: "transparent",
fontFamily: "Arial",
fontSize: Math.max(16, Math.round(globalMarkerSize * 0.8)),
fill: "#000000",
fill: globalDescriptionColor,
padding: 10,
editable: false,
selectable: true,
+3
View File
@@ -3,6 +3,9 @@ ob_start();
session_start();
require_once '../../vendor/autoload.php';
Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2))->safeLoad();
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
$response = ['error' => '', 'rows' => [], 'columns' => [], 'template_id' => 0, 'filename' => '', 'excel_data' => []];
try {
+246
View File
@@ -0,0 +1,246 @@
<?php
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Filtro opzionale per template.
$templateFilter = isset($_GET['template_id']) ? intval($_GET['template_id']) : 0;
$templates = $pdo->query("SELECT id, name FROM excel_templates ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
$where = '';
$params = [];
if ($templateFilter > 0) {
$where = 'WHERE b.template_id = ?';
$params[] = $templateFilter;
}
$sql = "SELECT b.id, b.template_id, b.mapping_id, b.field_id, b.json_value,
b.lims_value_id, b.lims_value, b.updated_at,
t.name AS template_name,
m.field_label
FROM json_lims_binding b
LEFT JOIN excel_templates t ON t.id = b.template_id
LEFT JOIN template_mapping m ON m.id = b.mapping_id
$where
ORDER BY t.name, m.field_label, b.json_value";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$bindings = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<style>
.json-value {
font-family: Consolas, Monaco, monospace;
font-weight: 600;
}
td .select2-container {
min-width: 220px;
}
.row-status {
font-size: 12px;
}
</style>
<title>Gestione Binding JSON -> LIMS - <?= htmlspecialchars($titlewebsite ?? '', ENT_QUOTES, 'UTF-8'); ?></title>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card radius-10">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<h6 class="mb-0">Gestione Binding JSON -> LIMS</h6>
<form method="GET" class="d-flex align-items-center gap-2">
<label class="small text-muted mb-0">Template</label>
<select name="template_id" class="form-select form-select-sm" onchange="this.form.submit()">
<option value="0">Tutti</option>
<?php foreach ($templates as $t): ?>
<option value="<?= (int) $t['id'] ?>" <?= $templateFilter === (int) $t['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($t['name']) ?>
</option>
<?php endforeach; ?>
</select>
</form>
</div>
</div>
<div class="card-body">
<div id="globalError" class="alert alert-danger" style="display:none;"></div>
<div class="alert alert-secondary small">
Le modifiche ai binding si applicano alle <strong>importazioni future</strong>.
I record gia' importati non vengono ricalcolati.
</div>
<div class="table-responsive">
<table class="table table-striped table-bordered align-middle">
<thead>
<tr>
<th>Template</th>
<th>Campo (template_mapping)</th>
<th>Valore JSON</th>
<th>Valore LIMS</th>
<th style="width:170px;">Azioni</th>
</tr>
</thead>
<tbody>
<?php if (empty($bindings)): ?>
<tr>
<td colspan="5" class="text-center text-muted">Nessun binding presente.</td>
</tr>
<?php else: ?>
<?php foreach ($bindings as $b): ?>
<tr data-id="<?= (int) $b['id'] ?>"
data-mapping-id="<?= (int) $b['mapping_id'] ?>"
data-field-id="<?= (int) $b['field_id'] ?>"
data-template-id="<?= (int) $b['template_id'] ?>"
data-json-value="<?= htmlspecialchars($b['json_value'], ENT_QUOTES) ?>">
<td><?= htmlspecialchars($b['template_name'] ?? ('#' . $b['template_id'])) ?></td>
<td><?= htmlspecialchars($b['field_label'] ?? ('mapping ' . $b['mapping_id'])) ?></td>
<td class="json-value"><?= htmlspecialchars($b['json_value']) ?></td>
<td>
<select class="form-select binding-select" data-field-id="<?= (int) $b['field_id'] ?>">
<?php if ($b['lims_value_id']): ?>
<option value="<?= (int) $b['lims_value_id'] ?>" selected>
<?= htmlspecialchars($b['lims_value']) ?>
</option>
<?php endif; ?>
</select>
<span class="row-status text-muted"></span>
</td>
<td>
<button type="button" class="btn btn-sm btn-success save-binding-btn" disabled>
<i class="fas fa-save"></i> Salva
</button>
<button type="button" class="btn btn-sm btn-outline-danger delete-binding-btn">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(function() {
const $globalError = $('#globalError');
$('.binding-select').each(function() {
const fieldId = $(this).data('field-id');
const initialVal = $(this).val();
$(this).select2({
width: '220px',
ajax: {
url: 'search_customfield_values.php',
dataType: 'json',
delay: 200,
data: params => ({
field_id: fieldId,
q: params.term || '',
limit: 50
}),
processResults: data => ({
results: data.results || []
})
},
minimumInputLength: 0
});
// Abilito Salva solo quando il valore cambia davvero.
$(this).data('original-val', initialVal);
});
$('.binding-select').on('change', function() {
const $row = $(this).closest('tr');
const changed = String($(this).val()) !== String($(this).data('original-val'));
$row.find('.save-binding-btn').prop('disabled', !changed);
});
$('.save-binding-btn').on('click', function() {
const $row = $(this).closest('tr');
const $select = $row.find('.binding-select');
const selectedData = $select.select2('data')[0] || {};
const $status = $row.find('.row-status');
const $btn = $(this);
$globalError.hide();
$btn.prop('disabled', true);
$status.text('Salvataggio...').removeClass('text-success text-danger').addClass('text-muted');
$.post('save_binding.php', {
mapping_id: $row.data('mapping-id'),
field_id: $row.data('field-id'),
template_id: $row.data('template-id'),
json_value: String($row.data('json-value')),
lims_value_id: $select.val(),
lims_value: selectedData.text || ''
}).then(resp => {
if (resp && resp.success) {
$status.text('Salvato').removeClass('text-muted').addClass('text-success');
$select.data('original-val', $select.val());
} else {
throw new Error((resp && resp.error) || 'Errore salvataggio');
}
}).catch(err => {
$status.text('Errore').removeClass('text-muted').addClass('text-danger');
$globalError.text(err.message || 'Errore durante il salvataggio.').show();
$btn.prop('disabled', false);
});
});
$('.delete-binding-btn').on('click', function() {
if (!confirm('Eliminare questo binding?')) return;
const $row = $(this).closest('tr');
const $btn = $(this);
$globalError.hide();
$btn.prop('disabled', true);
$.post('delete_binding.php', {
id: $row.data('id')
}).then(resp => {
if (resp && resp.success) {
$row.fadeOut(200, () => $row.remove());
} else {
throw new Error((resp && resp.error) || 'Errore eliminazione');
}
}).catch(err => {
$globalError.text(err.message || 'Errore durante l\'eliminazione.').show();
$btn.prop('disabled', false);
});
});
});
</script>
</body>
</html>
@@ -255,4 +255,60 @@ class VisualLimsApiClient
{
return $this->baseUrl;
}
/**
* Get raw/binary content from VisualLims API.
* Used for PDF downloads from MediaFile/DownloadStream.
*/
public function getRaw($endpoint)
{
$token = $this->getToken();
/*
* Normal JSON OData calls use:
* {$this->baseUrl}/api/odata/...
*
* Media file downloads use:
* {$this->baseUrl}/api/MediaFile/DownloadStream...
*/
$url = rtrim($this->baseUrl, '/') . '/api/' . ltrim($endpoint, '/');
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer {$token}",
"Accept: application/pdf,*/*"
],
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 60
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new Exception("Errore cURL download raw: " . $curlError);
}
if ($httpCode < 200 || $httpCode >= 300) {
throw new Exception(
"Errore HTTP {$httpCode} durante download raw. Content-Type: {$contentType}. Response: " .
substr($response, 0, 500)
);
}
if (empty($response)) {
throw new Exception("Risposta vuota dal download raw.");
}
return $response;
}
}
+128
View File
@@ -0,0 +1,128 @@
<?php
// Helpers for JSON -> LIMS value bindings (table json_lims_binding).
function binding_is_list_field(array $mapping): bool
{
$hasList = (int) ($mapping['has_list'] ?? 0) === 1;
$isMultiChoice = strcasecmp(trim((string) ($mapping['data_type'] ?? '')), 'SceltaMultipla') === 0;
return $hasList || $isMultiChoice;
}
function binding_lookup(PDO $pdo, int $mappingId, string $jsonValue): ?array
{
$stmt = $pdo->prepare(
"SELECT id, lims_value_id, lims_value
FROM json_lims_binding
WHERE mapping_id = ? AND json_value = ?
LIMIT 1"
);
$stmt->execute([$mappingId, $jsonValue]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
function binding_upsert(
PDO $pdo,
int $templateId,
int $mappingId,
int $fieldId,
string $jsonValue,
int $limsValueId,
string $limsValue,
?int $createdBy
): void {
$stmt = $pdo->prepare(
"INSERT INTO json_lims_binding
(template_id, mapping_id, field_id, json_value, lims_value_id, lims_value, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
lims_value_id = VALUES(lims_value_id),
lims_value = VALUES(lims_value),
field_id = VALUES(field_id),
template_id = VALUES(template_id)"
);
$stmt->execute([$templateId, $mappingId, $fieldId, $jsonValue, $limsValueId, $limsValue, $createdBy]);
}
// LIMS list values for a field, reusing the cache/customfield_{id}.json cache (1h).
function binding_get_lims_values(int $fieldId): array
{
static $memo = [];
if ($fieldId <= 0) {
return [];
}
if (array_key_exists($fieldId, $memo)) {
return $memo[$fieldId];
}
$cacheDir = dirname(__DIR__) . '/cache';
$cacheFile = $cacheDir . '/customfield_' . $fieldId . '.json';
try {
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
$values = json_decode(file_get_contents($cacheFile), true);
} else {
require_once __DIR__ . '/VisualLimsApiClient.class.php';
$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));
}
} catch (Throwable $e) {
error_log("binding_get_lims_values failed for field $fieldId: " . $e->getMessage());
$values = [];
}
if (!is_array($values)) {
$values = [];
}
return $memo[$fieldId] = $values;
}
// Exactly one case-insensitive match by Valore -> that value, otherwise null.
function binding_auto_match(array $limsValues, string $jsonValue): ?array
{
$needle = mb_strtolower(trim($jsonValue));
if ($needle === '') {
return null;
}
$matches = [];
foreach ($limsValues as $v) {
$valore = (string) ($v['Valore'] ?? '');
if (mb_strtolower(trim($valore)) === $needle) {
$matches[] = $v;
}
}
return count($matches) === 1 ? $matches[0] : null;
}
// Scrive il valore risolto sui record indicati (datadb_ids gia' delimita l'import corrente).
function binding_apply_to_details(
PDO $pdo,
int $mappingId,
string $limsValue,
array $datadbIds
): int {
$datadbIds = array_values(array_filter(array_map('intval', $datadbIds)));
if (empty($datadbIds)) {
return 0;
}
$placeholders = implode(',', array_fill(0, count($datadbIds), '?'));
$sql = "UPDATE import_data_details
SET field_value = ?
WHERE mapping_id = ?
AND id IN ($placeholders)";
$params = array_merge([$limsValue, $mappingId], $datadbIds);
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->rowCount();
}
+3
View File
@@ -3,6 +3,9 @@ require_once dirname(__DIR__, 3) . '/vendor/autoload.php';
use Dotenv\Dotenv;
Dotenv::createImmutable(dirname(__DIR__, 3))->safeLoad();
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
class DBHandlerSelect
{
private static $instance = null;
+133
View File
@@ -0,0 +1,133 @@
<?php
header('Content-Type: application/json');
include('include/headscript.php');
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$data = json_decode(file_get_contents('php://input'), true);
$sourceIddatadb = isset($data['source_iddatadb']) ? (int)$data['source_iddatadb'] : 0;
$targetList = $data['target_iddatadb_list'] ?? [];
$targetIds = array_values(array_unique(array_filter(array_map('intval', (array)$targetList), function ($v) use ($sourceIddatadb) {
return $v > 0 && $v !== $sourceIddatadb;
})));
if ($sourceIddatadb <= 0 || empty($targetIds)) {
echo json_encode([
'success' => false,
'message' => 'Missing source or target records'
]);
exit;
}
try {
$pdo->beginTransaction();
// 1. Load source parts
$stmtParts = $pdo->prepare("
SELECT id, part_number, part_description, mix, idmatrice, note, dateexpiry
FROM identification_parts
WHERE iddatadb = ?
AND part_description IS NOT NULL
AND TRIM(part_description) <> ''
ORDER BY part_number ASC, id ASC
");
$stmtParts->execute([$sourceIddatadb]);
$sourceParts = $stmtParts->fetchAll(PDO::FETCH_ASSOC);
if (empty($sourceParts)) {
$pdo->rollBack();
echo json_encode([
'success' => false,
'message' => 'No parts found for source record'
]);
exit;
}
// 2. Prepare statements
$stmtInsertPart = $pdo->prepare("
INSERT INTO identification_parts
(iddatadb, part_number, part_description, mix, idmatrice, note, dateexpiry, created_at, updated_at)
VALUES
(:iddatadb, :part_number, :part_description, :mix, :idmatrice, :note, :dateexpiry, NOW(), NOW())
");
$stmtLoadCF = $pdo->prepare("
SELECT field_id, value_id, value_text
FROM identification_parts_customfields
WHERE part_id = ?
ORDER BY id ASC
");
$stmtInsertCF = $pdo->prepare("
INSERT INTO identification_parts_customfields
(part_id, field_id, value_id, value_text, created_at, updated_at)
VALUES
(:part_id, :field_id, :value_id, :value_text, NOW(), NOW())
");
$details = [];
$totalClonedParts = 0;
// 3. Clone source parts to each target record
foreach ($targetIds as $targetIddatadb) {
$clonedCountForTarget = 0;
foreach ($sourceParts as $part) {
$stmtInsertPart->execute([
':iddatadb' => $targetIddatadb,
':part_number' => $part['part_number'],
':part_description' => $part['part_description'],
':mix' => $part['mix'] ?? 'N',
':idmatrice' => $part['idmatrice'] !== '' ? $part['idmatrice'] : null,
':note' => $part['note'] ?? null,
':dateexpiry' => $part['dateexpiry'] ?? null,
]);
$newPartId = (int)$pdo->lastInsertId();
// Load source custom fields for this part
$stmtLoadCF->execute([(int)$part['id']]);
$customFields = $stmtLoadCF->fetchAll(PDO::FETCH_ASSOC);
foreach ($customFields as $cf) {
$stmtInsertCF->execute([
':part_id' => $newPartId,
':field_id' => (int)$cf['field_id'],
':value_id' => ($cf['value_id'] !== null && $cf['value_id'] !== '') ? (int)$cf['value_id'] : null,
':value_text' => $cf['value_text'] !== '' ? $cf['value_text'] : null,
]);
}
$clonedCountForTarget++;
$totalClonedParts++;
}
$details[] = [
'target_iddatadb' => $targetIddatadb,
'cloned_parts' => $clonedCountForTarget
];
}
$pdo->commit();
echo json_encode([
'success' => true,
'source_iddatadb' => $sourceIddatadb,
'cloned_targets' => count($targetIds),
'total_cloned_parts' => $totalClonedParts,
'details' => $details
]);
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
echo json_encode([
'success' => false,
'message' => 'Clone failed: ' . $e->getMessage()
]);
}
+228
View File
@@ -0,0 +1,228 @@
<?php
require_once 'class/db-functions.php';
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
die("Invalid template ID");
}
$sourceTemplateId = (int)$_GET['id'];
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$pdo->beginTransaction();
// 1. Get source template
$stmt = $pdo->prepare("
SELECT
name,
source_type,
header_row,
start_column,
description,
target_table,
sample_xlsx,
button_size,
button_bg_color,
button_text_color,
button_label,
status,
client_specific_fields,
idclient,
clientname,
schemaname,
idschema,
schemajson,
xls_headers,
api_sample_json,
json_nodes,
idroutine
FROM excel_templates
WHERE id = ?
LIMIT 1
");
$stmt->execute([$sourceTemplateId]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$template) {
throw new Exception("Template not found.");
}
// 2. Generate unique clone name
$baseName = 'Copia di ' . $template['name'];
$newName = $baseName;
$checkStmt = $pdo->prepare("SELECT COUNT(*) FROM excel_templates WHERE name = ?");
$checkStmt->execute([$newName]);
if ((int)$checkStmt->fetchColumn() > 0) {
$counter = 2;
do {
$newName = $baseName . ' (' . $counter . ')';
$checkStmt->execute([$newName]);
$exists = (int)$checkStmt->fetchColumn() > 0;
$counter++;
} while ($exists);
}
// 3. Insert cloned template
$insertTemplate = $pdo->prepare("
INSERT INTO excel_templates (
name,
source_type,
created_at,
updated_at,
header_row,
start_column,
description,
target_table,
sample_xlsx,
button_size,
button_bg_color,
button_text_color,
button_label,
status,
client_specific_fields,
idclient,
clientname,
schemaname,
idschema,
schemajson,
xls_headers,
api_sample_json,
json_nodes,
idroutine
) VALUES (
:name,
:source_type,
NOW(),
NOW(),
:header_row,
:start_column,
:description,
:target_table,
:sample_xlsx,
:button_size,
:button_bg_color,
:button_text_color,
:button_label,
:status,
:client_specific_fields,
:idclient,
:clientname,
:schemaname,
:idschema,
:schemajson,
:xls_headers,
:api_sample_json,
:json_nodes,
:idroutine
)
");
$insertTemplate->execute([
':name' => $newName,
':source_type' => $template['source_type'],
':header_row' => $template['header_row'],
':start_column' => $template['start_column'],
':description' => $template['description'],
':target_table' => $template['target_table'],
':sample_xlsx' => $template['sample_xlsx'],
':button_size' => $template['button_size'],
':button_bg_color' => $template['button_bg_color'],
':button_text_color' => $template['button_text_color'],
':button_label' => $template['button_label'],
':status' => $template['status'],
':client_specific_fields' => $template['client_specific_fields'],
':idclient' => $template['idclient'],
':clientname' => $template['clientname'],
':schemaname' => $template['schemaname'],
':idschema' => $template['idschema'],
':schemajson' => $template['schemajson'],
':xls_headers' => $template['xls_headers'],
':api_sample_json' => $template['api_sample_json'],
':json_nodes' => $template['json_nodes'],
':idroutine' => $template['idroutine']
]);
$newTemplateId = (int)$pdo->lastInsertId();
if ($newTemplateId <= 0) {
throw new Exception("Unable to create cloned template.");
}
// 4. Clone template_mapping rows
$cloneMappings = $pdo->prepare("
INSERT INTO template_mapping (
template_id,
schema_id,
field_id,
excel_column,
json_node,
is_manual,
manual_default,
auto_value,
data_type,
is_required,
default_value,
has_list,
length,
decimals,
min_value,
max_value,
default_curr_date,
tablename,
field_label,
main_field,
is_visible_import,
is_visible_parts
)
SELECT
:new_template_id,
schema_id,
field_id,
excel_column,
json_node,
is_manual,
manual_default,
auto_value,
data_type,
is_required,
default_value,
has_list,
length,
decimals,
min_value,
max_value,
default_curr_date,
tablename,
field_label,
main_field,
is_visible_import,
is_visible_parts
FROM template_mapping
WHERE template_id = :source_template_id
");
$cloneMappings->execute([
':new_template_id' => $newTemplateId,
':source_template_id' => $sourceTemplateId
]);
$pdo->commit();
header("Location: templates_dashboard.php?cloned=1&new_id=" . $newTemplateId);
exit;
} catch (Exception $e) {
if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollBack();
}
die("Clone error: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'));
}
File diff suppressed because it is too large Load Diff
@@ -17,6 +17,8 @@ try {
exit(1);
}
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
// Recupera le variabili d'ambiente
$dbHost = $_ENV['DB_HOST'];
$dbName = $_ENV['DB_DATABASE'];
+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
+40
View File
@@ -0,0 +1,40 @@
<?php
// Elimina un binding JSON -> LIMS. Ritorna JSON.
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
exit;
}
$id = intval($_POST['id'] ?? 0);
if ($id <= 0) {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Missing id']);
exit;
}
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->prepare("DELETE FROM json_lims_binding WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(['success' => true, 'deleted' => $stmt->rowCount()]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
+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()
]);
}
+112
View File
@@ -0,0 +1,112 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
ini_set('display_errors', '0');
error_reporting(E_ALL);
/**
* Uso:
* download_rapporto_file.php?id=123
*/
try {
$api = VisualLimsApiClient::getInstance();
$id = isset($_GET['id']) ? intval($_GET['id']) : 0;
if (!$id) {
throw new Exception("ID RapportoFile mancante");
}
$debugDir = __DIR__ . '/logs/lims_get_commessa/';
if (!is_dir($debugDir)) {
mkdir($debugDir, 0755, true);
}
/**
* Recupero il record RapportoFile.
*/
$data = $api->get("RapportoFile({$id})", []);
file_put_contents(
$debugDir . "rapporto_file_{$id}.json",
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
/**
* Provo vari nomi campo possibili per il contenuto PDF.
* Dopo il primo test potremo adattarlo al nome esatto.
*/
$possibleContentFields = [
'File',
'file',
'Content',
'content',
'FileContent',
'fileContent',
'Contenuto',
'contenuto',
'Bytes',
'bytes'
];
$base64 = null;
foreach ($possibleContentFields as $field) {
if (!empty($data[$field])) {
$base64 = $data[$field];
break;
}
}
if (!$base64) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => false,
'message' => 'Record RapportoFile recuperato, ma non ho trovato un campo base64 del PDF.',
'hint' => 'Apri logs/lims_get_commessa/rapporto_file_' . $id . '.json e controlla il nome reale del campo file.',
'data' => $data
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
/**
* Se il contenuto ha prefisso data URI, lo pulisco.
*/
if (strpos($base64, 'base64,') !== false) {
$parts = explode('base64,', $base64);
$base64 = end($parts);
}
$pdfBinary = base64_decode($base64, true);
if ($pdfBinary === false) {
throw new Exception("Il contenuto trovato non è un base64 valido");
}
$filename = $data['NomeFile']
?? $data['nomeFile']
?? $data['FileName']
?? $data['fileName']
?? "rapporto_{$id}.pdf";
if (!str_ends_with(strtolower($filename), '.pdf')) {
$filename .= '.pdf';
}
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="' . basename($filename) . '"');
header('Content-Length: ' . strlen($pdfBinary));
echo $pdfBinary;
} catch (Exception $e) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+46
View File
@@ -0,0 +1,46 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$idRapportoFile = isset($_GET['id_rapporto_file']) ? (int)$_GET['id_rapporto_file'] : 0;
if ($idRapportoFile <= 0) {
throw new Exception("Parametro id_rapporto_file mancante o non valido.");
}
/*
* This endpoint returns the PDF binary stream.
* Do not call this with the normal get() method because get() expects JSON.
*/
$endpoint = "MediaFile/DownloadStream?objectType=RapportoFile&propertyName=FileContent&objectKey={$idRapportoFile}";
$api = VisualLimsApiClient::getInstance();
$pdfContent = $api->getRaw($endpoint);
if (empty($pdfContent)) {
throw new Exception("PDF vuoto o non ricevuto dal server.");
}
$fileName = "rapporto_{$idRapportoFile}.pdf";
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="' . $fileName . '"');
header('Content-Length: ' . strlen($pdfContent));
header('Cache-Control: private, max-age=0, must-revalidate');
header('Pragma: public');
echo $pdfContent;
exit;
} catch (Exception $e) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+406 -74
View File
@@ -1,29 +1,75 @@
<?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'])) {
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Invalid ID"));
header("Location: templates_dashboard.php?status=error&message=" . urlencode("Invalid ID"));
exit;
}
$id = intval($_GET['id']); // Sanifica l'ID
$id = intval($_GET['id']);
// Recupera il template dal database
// Retrieve template from database
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("SELECT * FROM excel_templates WHERE id = ?");
$stmt->execute([$id]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
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;
}
// Recupera tutte le routine dal database
// Retrieve all routines
$stmt = $pdo->prepare("SELECT * FROM routine");
$stmt->execute();
$routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Retrieve active API/JSON configurations
$stmt = $pdo->prepare("
SELECT id, name, provider_code, api_type, php_class_name
FROM api_configurations
WHERE is_active = 1
ORDER BY name ASC
");
$stmt->execute();
$apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
$buttonBgPalette = [
'#0d6efd' => 'Blue',
'#6610f2' => 'Indigo',
'#6f42c1' => 'Purple',
'#d63384' => 'Pink',
'#dc3545' => 'Red',
'#fd7e14' => 'Orange',
'#ffc107' => 'Yellow',
'#198754' => 'Green',
'#20c997' => 'Teal',
'#0dcaf0' => 'Cyan',
'#212529' => 'Dark',
'#6c757d' => 'Gray',
'#495057' => 'Slate',
'#795548' => 'Brown',
'#2f5d50' => 'Sage Green',
];
$buttonTextPalette = [
'#ffffff' => 'White',
'#6c757d' => 'Gray',
'#000000' => 'Black',
];
$currentButtonBgColor = strtolower($template['button_bg_color'] ?? '#007bff');
$currentButtonTextColor = strtolower($template['button_text_color'] ?? '#ffffff');
if (!array_key_exists($currentButtonBgColor, array_change_key_case($buttonBgPalette, CASE_LOWER))) {
$currentButtonBgColor = '#0d6efd';
}
if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonTextPalette, CASE_LOWER))) {
$currentButtonTextColor = '#ffffff';
}
?>
<!doctype html>
<html lang="en">
@@ -34,7 +80,64 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<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" />
<?php include('cssinclude.php'); ?>
<!-- Include jQuery prima di Select2 -->
<style>
.color-palette {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 8px;
}
.color-option {
position: relative;
cursor: pointer;
}
.color-option input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.color-swatch {
width: 42px;
height: 42px;
border-radius: 10px;
border: 2px solid #d6dbe0;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
transition: all 0.18s ease;
}
.color-option input:checked+.color-swatch {
border: 3px solid #111827;
transform: scale(1.08);
box-shadow: 0 0 0 4px rgba(13, 110, 253, 0.18);
}
.color-swatch .check-mark {
display: none;
font-size: 18px;
font-weight: 700;
color: #ffffff;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
}
.color-option input:checked+.color-swatch .check-mark {
display: inline-block;
}
.color-name {
display: block;
font-size: 11px;
text-align: center;
margin-top: 4px;
color: #555;
max-width: 55px;
}
</style>
<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>
<title>Edit Template <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
@@ -44,19 +147,22 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Update XLS Template</h5>
<h5 class="mb-0">Update Template</h5>
</div>
<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>
<ul class="mb-0">
<li>Template Name</li>
<li>Row Header and Column Header: where the title of the excel starts</li>
<li>Schema</li>
<li>Source Type</li>
<li>Schema and Client</li>
<li>Row Header and Column Header only for XLS templates</li>
</ul>
</div>
</div>
@@ -69,34 +175,100 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
</div>
</div>
</div>
<div class="card-body">
<div class="col-12">
<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">
<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 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>
<option value="JSON" <?php echo (($template['source_type'] ?? 'XLS') === 'JSON') ? 'selected' : ''; ?>>JSON</option>
<option value="PDF" <?php echo (($template['source_type'] ?? 'XLS') === 'PDF') ? 'selected' : ''; ?>>PDF</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>
<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 class="mb-3">
<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>
<div class="mb-3" id="startColumnWrapper">
<label class="form-label"><?= htmlspecialchars($columnheader, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="text" name="start_column" id="startColumn" class="form-control" value="<?php echo htmlspecialchars($template['start_column'] ?? ''); ?>">
</div>
<div class="mb-3" id="xlsSheetNumberWrapper">
<label class="form-label">XLS Sheet Number</label>
<input
type="number"
name="xls_sheet_index"
id="xlsSheetIndex"
class="form-control"
min="0"
value="<?php echo htmlspecialchars($template['xls_sheet_index'] ?? 0); ?>">
<small class="text-muted">
Use 0 for the first sheet, 1 for the second sheet, 2 for the third sheet, and so on.
</small>
</div>
<div class="mb-3" id="apiConfigWrapper" style="display: none;">
<label class="form-label">API / JSON Configuration *</label>
<select name="api_config_id" id="apiConfigSelect" class="form-control">
<option value="">Select an API configuration...</option>
<?php foreach ($apiConfigurations as $apiConfig): ?>
<?php
$apiLabelParts = [];
if (!empty($apiConfig['name'])) {
$apiLabelParts[] = $apiConfig['name'];
}
if (!empty($apiConfig['provider_code'])) {
$apiLabelParts[] = '[' . $apiConfig['provider_code'] . ']';
}
if (!empty($apiConfig['api_type'])) {
$apiLabelParts[] = '(' . $apiConfig['api_type'] . ')';
}
if (!empty($apiConfig['php_class_name'])) {
$apiLabelParts[] = '- ' . $apiConfig['php_class_name'];
}
$apiLabel = implode(' ', $apiLabelParts);
?>
<option
value="<?php echo (int)$apiConfig['id']; ?>"
<?php echo ((int)($template['api_config_id'] ?? 0) === (int)$apiConfig['id']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($apiLabel, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
<small class="text-muted">
Select the API/JSON configuration linked to this template.
</small>
</div>
<div class="mb-3">
<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 class="mb-3">
<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>
<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'] ?? 'datadb'); ?>" readonly required>
</div>
<div class="mb-3">
@@ -110,12 +282,46 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<div class="mb-3">
<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'); ?>">
<div class="color-palette">
<?php foreach ($buttonBgPalette as $colorValue => $colorLabel): ?>
<?php $isChecked = strtolower($colorValue) === $currentButtonBgColor; ?>
<label class="color-option" title="<?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?>">
<input
type="radio"
name="button_bg_color"
value="<?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>"
<?= $isChecked ? 'checked' : ''; ?>>
<span class="color-swatch" style="background-color: <?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>;">
<span class="check-mark"></span>
</span>
<span class="color-name"><?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="mb-3">
<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'); ?>">
<div class="color-palette">
<?php foreach ($buttonTextPalette as $colorValue => $colorLabel): ?>
<?php $isChecked = strtolower($colorValue) === $currentButtonTextColor; ?>
<label class="color-option" title="<?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?>">
<input
type="radio"
name="button_text_color"
value="<?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>"
<?= $isChecked ? 'checked' : ''; ?>>
<span class="color-swatch" style="background-color: <?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>;">
<span class="check-mark"></span>
</span>
<span class="color-name"><?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="mb-3">
@@ -128,7 +334,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<select name="client_id" id="clientSelect" class="form-control" required>
<option value="">Select a client...</option>
</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 class="mb-3">
@@ -136,7 +342,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<select name="schema_id" id="schemaSelect" class="form-control" required>
<option value="">Select a schema...</option>
</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 class="mb-3">
@@ -144,11 +350,12 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<select name="idroutine" id="routineSelect" class="form-control">
<option value="">Select a routine...</option>
<?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']); ?>
</option>
<?php endforeach; ?>
</select>
<div id="routineDetails" class="mt-2" style="display: none;">
<h6>Routine Details</h6>
<p><strong>Name:</strong> <span id="routineName"></span></p>
@@ -166,8 +373,10 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
</div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
@@ -175,9 +384,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<script>
document.addEventListener("DOMContentLoaded", function() {
// Verifica che jQuery sia caricato
if (typeof jQuery === 'undefined') {
alert("Errore: jQuery non è caricato. Contatta l'amministratore.");
alert("Error: jQuery is not loaded.");
return;
}
@@ -192,12 +400,21 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const routineAction2 = document.getElementById("routineAction2");
const routineAction3 = document.getElementById("routineAction3");
if (!form || !clientLoadingStatus || !schemaLoadingStatus || !routineSelect || !routineDetails) {
alert("Errore: Uno o più elementi della pagina non sono stati trovati. Contatta l'amministratore.");
return;
}
const sourceType = document.getElementById("sourceType");
const headerRowWrapper = document.getElementById("headerRowWrapper");
const startColumnWrapper = document.getElementById("startColumnWrapper");
const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper");
const apiConfigWrapper = document.getElementById("apiConfigWrapper");
const headerRow = document.getElementById("headerRow");
const startColumn = document.getElementById("startColumn");
const xlsSheetIndex = document.getElementById("xlsSheetIndex");
const apiConfigSelect = document.getElementById("apiConfigSelect");
const selectedClientId = <?php echo json_encode((int)($template['idclient'] ?? 0)); ?>;
const selectedSchemaId = <?php echo json_encode((int)($template['idschema'] ?? 0)); ?>;
// Inizializza Select2
$('#clientSelect').select2({
placeholder: "Search for a client...",
allowClear: true
@@ -213,108 +430,201 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
allowClear: true
});
// Carica i clienti
$('#apiConfigSelect').select2({
placeholder: "Select an API configuration...",
allowClear: true
});
function updateSourceFields() {
const selectedSource = sourceType.value;
const isXls = selectedSource === 'XLS';
const isApiOrJson = selectedSource === 'API' || selectedSource === 'JSON';
if (isXls) {
headerRowWrapper.style.display = 'block';
startColumnWrapper.style.display = 'block';
xlsSheetNumberWrapper.style.display = 'block';
headerRow.required = true;
startColumn.required = true;
headerRow.disabled = false;
startColumn.disabled = false;
xlsSheetIndex.disabled = false;
apiConfigWrapper.style.display = 'none';
apiConfigSelect.required = false;
apiConfigSelect.disabled = true;
$('#apiConfigSelect').val(null).trigger('change');
} else {
headerRowWrapper.style.display = 'none';
startColumnWrapper.style.display = 'none';
xlsSheetNumberWrapper.style.display = 'none';
headerRow.required = false;
startColumn.required = false;
headerRow.disabled = true;
startColumn.disabled = true;
xlsSheetIndex.disabled = true;
if (isApiOrJson) {
apiConfigWrapper.style.display = 'block';
apiConfigSelect.required = true;
apiConfigSelect.disabled = false;
} else {
apiConfigWrapper.style.display = 'none';
apiConfigSelect.required = false;
apiConfigSelect.disabled = true;
$('#apiConfigSelect').val(null).trigger('change');
}
}
}
sourceType.addEventListener('change', updateSourceFields);
updateSourceFields();
function formatClientLabel(client) {
const nome = client.Nominativo || "Name not available";
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 loadClients() {
try {
clientLoadingStatus.style.display = 'inline';
clientLoadingStatus.textContent = 'Recupero clienti in corso...';
clientLoadingStatus.textContent = 'Loading clients...';
const response = await fetch("get_clienti.php", {
method: "GET",
headers: {
"Content-Type": "application/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");
select.innerHTML = '<option value="">Select a client...</option>';
data.value.forEach(client => {
const nome = client.Nominativo || "Nome non disponibile";
const id = client.IdCliente || "ID non disponibile";
const option = new Option(`${nome.trim()} (ID: ${id})`, id);
if (parseInt(id) === parseInt(<?php echo json_encode($template['idclient'] ?? 0); ?>)) {
const id = client.IdCliente || "";
const option = new Option(formatClientLabel(client), id);
if (parseInt(id) === parseInt(selectedClientId)) {
option.selected = true;
}
select.add(option);
});
$(select).trigger('change');
clientLoadingStatus.textContent = "Clienti caricati.";
clientLoadingStatus.textContent = "Clients loaded.";
} catch (error) {
clientLoadingStatus.textContent = "Errore nel caricamento.";
clientLoadingStatus.textContent = "Loading error.";
Swal.fire({
title: "Errore!",
text: "Impossibile caricare i clienti: " + error.message,
title: "Error!",
text: "Unable to load clients: " + error.message,
icon: "error",
confirmButtonText: "OK"
});
} finally {
setTimeout(() => clientLoadingStatus.style.display = 'none', 2000);
setTimeout(() => clientLoadingStatus.style.display = 'none', 1500);
}
}
// Carica gli schemi
async function loadSchemas() {
try {
schemaLoadingStatus.style.display = 'inline';
schemaLoadingStatus.textContent = 'Caricamento schemi in corso...';
schemaLoadingStatus.textContent = 'Loading schemas...';
const response = await fetch("get_schemi.php", {
method: "GET",
headers: {
"Content-Type": "application/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");
select.innerHTML = '<option value="">Select a schema...</option>';
data.value.forEach(schema => {
const nome = schema.Nome || "Nome non disponibile";
const id = schema.IdSchemaCustomFields || "ID non disponibile";
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 || "Name not available";
const id = schema.IdSchemaCustomFields || "";
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;
}
select.add(option);
});
$(select).trigger('change');
schemaLoadingStatus.textContent = "Schemi caricati.";
schemaLoadingStatus.textContent = "Schemas loaded.";
} catch (error) {
schemaLoadingStatus.textContent = "Errore nel caricamento.";
schemaLoadingStatus.textContent = "Loading error.";
Swal.fire({
title: "Errore!",
text: "Impossibile caricare gli schemi: " + error.message,
title: "Error!",
text: "Unable to load schemas: " + error.message,
icon: "error",
confirmButtonText: "OK"
});
} finally {
setTimeout(() => schemaLoadingStatus.style.display = 'none', 2000);
setTimeout(() => schemaLoadingStatus.style.display = 'none', 1500);
}
}
// Carica i dati
async function loadData() {
try {
await loadClients();
await loadSchemas();
} catch (error) {
Swal.fire({
title: "Errore!",
text: "Errore nel caricamento dei dati: " + error.message,
title: "Error!",
text: "Error while loading data: " + error.message,
icon: "error",
confirmButtonText: "OK"
});
}
}
loadData();
// Routine dettagli
const routines = <?php echo json_encode($routines); ?>;
function updateRoutineDetails() {
const selectedId = routineSelect.value;
routineDetails.style.display = selectedId ? 'block' : 'none';
if (selectedId) {
const routine = routines.find(r => r.idroutine == selectedId);
if (routine) {
routineName.textContent = routine.name || 'N/A';
routineDescription.textContent = routine.description || 'N/A';
@@ -336,10 +646,10 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
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) {
e.preventDefault();
@@ -351,8 +661,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!clientId) {
Swal.fire({
title: "Errore!",
text: "Per favore seleziona un cliente.",
title: "Error!",
text: "Please select a client.",
icon: "error",
confirmButtonText: "OK"
});
@@ -373,8 +683,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!schemaId) {
Swal.fire({
title: "Errore!",
text: "Per favore seleziona uno schema.",
title: "Error!",
text: "Please select a schema.",
icon: "error",
confirmButtonText: "OK"
});
@@ -387,13 +697,35 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const nameMatch = optionText.match(/^(.+?)(?:\s*\(ID:\s*\d+\))?$/);
schemaName = nameMatch ? nameMatch[1].trim() : optionText;
}
formData.append("idschema", schemaId);
formData.append("schemaname", schemaName);
// Aggiungi idroutine
const routineId = routineSelect.value;
formData.append("idroutine", routineId);
const selectedSource = sourceType.value;
if ((selectedSource === 'API' || selectedSource === 'JSON') && !apiConfigSelect.value) {
Swal.fire({
title: "Error!",
text: "Please select an API/JSON configuration.",
icon: "error",
confirmButtonText: "OK"
});
return;
}
if (selectedSource === 'XLS' && xlsSheetIndex.value === '') {
Swal.fire({
title: "Error!",
text: "Please enter the XLS sheet number.",
icon: "error",
confirmButtonText: "OK"
});
return;
}
fetch("process_edit_template_xls.php", {
method: "POST",
body: formData
@@ -402,8 +734,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
.then(data => {
if (data.success) {
Swal.fire({
title: "Successo!",
text: "Template aggiornato con successo!",
title: "Success!",
text: "Template updated successfully!",
icon: "success",
confirmButtonText: "OK"
}).then(() => {
@@ -411,17 +743,17 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
});
} else {
Swal.fire({
title: "Errore!",
title: "Error!",
text: data.message,
icon: "error",
confirmButtonText: "OK"
});
}
})
.catch(error => {
.catch(() => {
Swal.fire({
title: "Errore!",
text: "Si è verificato un errore imprevisto.",
title: "Error!",
text: "An unexpected error occurred.",
icon: "error",
confirmButtonText: "OK"
});
+3
View File
@@ -328,3 +328,6 @@
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"}
2026-04-30 15:01:25 - Errore nel recupero dati: HTTP 404, Risposta: {"title":"Not Found","status":404,"detail":"Not Found","instance":"GET /api/odata/Rapporto(2621521)","errorCode":"d25cbd678"}
2026-04-30 15:02:04 - Errore nel recupero dati: HTTP 404, Risposta:
2026-04-30 15:03:19 - Errore nella richiesta: URL rejected: Malformed input to a URL function
+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 });
})();
}
})();
+149 -13
View File
@@ -59,6 +59,49 @@ function formatDateToExport($value)
return null; // Imposta null se non è una data valida
}
// ImportaCommessa con retry: la chiamata è asincrona lato LIMS e a volte
// risponde 200 senza importare (StatoCommessaWeb resta "Inviata"/"Nuova").
// Riprova con backoff esponenziale finché non passa a "Elaborata".
function importaCommessaWithRetry($api, $commessaId, array $payload, $maxRetries = 3, $initialBackoff = 1)
{
$result = null;
$stato = null;
$succeeded = false;
$log = "";
$backoff = $initialBackoff;
set_time_limit(120); // i backoff non devono far scadere il timeout della richiesta
for ($attempt = 1; $attempt <= $maxRetries + 1; $attempt++) {
try {
$result = $api->post("CommessaWeb({$commessaId})/ImportaCommessa", $payload);
$stato = $result['StatoCommessaWeb'] ?? null;
$log .= "Attempt {$attempt}: HTTP 200, StatoCommessaWeb=" . ($stato ?? 'null') . "\n";
} catch (Exception $e) {
$stato = null;
$log .= "Attempt {$attempt}: EXCEPTION " . $e->getMessage() . "\n";
}
if ($stato === 'Elaborata') {
$succeeded = true;
break;
}
if ($attempt <= $maxRetries) {
$log .= " -> not Elaborata, waiting {$backoff}s before retry\n";
sleep($backoff);
$backoff *= 2;
}
}
return [
'succeeded' => $succeeded,
'stato' => $stato,
'result' => $result,
'log' => $log,
];
}
try {
$iddatadb = $_POST['iddatadb'] ?? null;
if (!$iddatadb) {
@@ -107,11 +150,13 @@ try {
// 🔹 STEP 3: Fetch Parts (including idmatrice and part id for custom fields)
$stmt = $pdo->prepare("
SELECT id AS part_id, part_number, part_description, material, color, mix, idmatrice, dateexpiry
FROM identification_parts
WHERE iddatadb = :iddatadb
ORDER BY CAST(part_number AS UNSIGNED) ASC, part_number ASC
");
SELECT id AS part_id, part_number, part_description, material, color, mix, idmatrice, dateexpiry
FROM identification_parts
WHERE iddatadb = :iddatadb
AND part_description IS NOT NULL
AND TRIM(part_description) <> ''
ORDER BY CAST(part_number AS UNSIGNED) ASC, part_number ASC
");
$stmt->execute(['iddatadb' => $iddatadb]);
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -432,6 +477,71 @@ try {
$logFilePhotos = $logDir . "commessa_{$commessaId}_photos_step5_2_" . time() . ".txt";
$writeLog($logFilePhotos, $logContentPhotos, "STEP 6.2 - Photos (commessa={$commessaId})");
// 🔹 STEP 6.3: Add Analyses (AnalisiCampione) via Campione({id})/AddAnalisi bound action
$stmt = $pdo->prepare("
SELECT part_id, analysis_recordkey, analysis_name, analysis_method
FROM identification_parts_analyses
WHERE iddatadb = :iddatadb
ORDER BY part_id, id
");
$stmt->execute(['iddatadb' => $iddatadb]);
$analysesRows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$partIdToIndex = [];
foreach ($parts as $idx => $part) {
$partIdToIndex[(int)$part['part_id']] = $idx;
}
$totalAnalyses = count($analysesRows);
$addedAnalyses = 0;
$failedAnalyses = [];
$logContentStep63Analisi = "Analyses for iddatadb={$iddatadb}: total={$totalAnalyses}\n\n";
foreach ($analysesRows as $a) {
$partId = (int)$a['part_id'];
$recordKey = trim((string)($a['analysis_recordkey'] ?? ''));
$idx = $partIdToIndex[$partId] ?? null;
if ($idx === null || !isset($campioni[$idx]) || $recordKey === '') {
$logContentStep63Analisi .= "SKIP (no campione for part_id={$partId} / empty recordkey): '{$recordKey}'\n";
continue;
}
$campioneId = (int)($campioni[$idx]['IdCampione'] ?? 0);
if ($campioneId <= 0) {
$logContentStep63Analisi .= "SKIP (invalid IdCampione for part_id={$partId}): '{$recordKey}'\n";
continue;
}
$payload = ['RecordKey' => $recordKey];
$jsonPayload = json_encode($payload, JSON_UNESCAPED_SLASHES);
$logContentStep63Analisi .= "curl --location --request POST '{$apiBaseUrl}Campione({$campioneId})/AddAnalisi' \\\n" .
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{$jsonPayload}'\n";
try {
$result = $api->post("Campione({$campioneId})/AddAnalisi", $payload);
$logContentStep63Analisi .= "OK (part_id={$partId}, campione={$campioneId}): " .
($a['analysis_name'] ?? '') . "\n---\n";
$addedAnalyses++;
} catch (Exception $e) {
$errMsg = $e->getMessage();
$logContentStep63Analisi .= "FAIL: {$errMsg}\n---\n";
$failedAnalyses[] = [
'part_id' => $partId,
'campione_id' => $campioneId,
'analysis_recordkey' => $recordKey,
'analysis_name' => $a['analysis_name'] ?? '',
'error' => $errMsg,
];
}
}
$logFileStep63Analisi = $logDir . "commessa_{$commessaId}_analyses_step63_" . time() . ".txt";
$writeLog($logFileStep63Analisi, $logContentStep63Analisi, "STEP 6.3 - AddAnalisi (commessa={$commessaId})");
// 🔹 STEP 7: Update Custom Fields for CommessaWeb
if (!empty($fieldValues)) {
// GET con espansione per CustomField
@@ -510,9 +620,8 @@ try {
$writeLog($logFileStep9, $logContentStep9, "STEP 9 - InviaCommessa (commessa={$commessaId})");
// 🔹 STEP 9.5: Importazione da CommessaWeb a Commessa (commentato come richiesto)
// 🔹 STEP 9.5: Importazione da CommessaWeb a Commessa (con retry)
// Supplier call: POST api/odata/CommessaWeb(XXX)/ImportaCommessa
$importUserId = (!empty($lims_global_user_id) && is_numeric($lims_global_user_id))
? (int) $lims_global_user_id
: 285;
@@ -520,17 +629,23 @@ try {
$importPayload = [
"IdUtente" => $importUserId
];
$importResult = $api->post("CommessaWeb({$commessaId})/ImportaCommessa", $importPayload);
$importPayloadLog = json_encode($importPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
// Logga il POST
$importOutcome = importaCommessaWithRetry($api, $commessaId, $importPayload);
$importResult = $importOutcome['result'];
$importStato = $importOutcome['stato'];
$importSucceeded = $importOutcome['succeeded'];
// Logga il POST (tutti i tentativi)
$logContentStep91 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb({$commessaId})/ImportaCommessa' \\\n" .
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{$importPayloadLog}'\n\n" .
"RESPONSE:\n" . json_encode($importResult, JSON_PRETTY_PRINT);
"ATTEMPTS:\n" . $importOutcome['log'] . "\n" .
"SUCCEEDED: " . ($importSucceeded ? 'yes' : 'NO') . "\n\n" .
"LAST RESPONSE:\n" . json_encode($importResult, JSON_PRETTY_PRINT);
$logFileStep91 = $logDir . "commessa_{$commessaId}_importa_step91_" . time() . ".txt";
$writeLog($logFileStep91, $logContentStep91, "STEP 9.5 - ImportaCommessa (commessa={$commessaId})");
$writeLog($logFileStep91, $logContentStep91, "STEP 9.5 - ImportaCommessa (commessa={$commessaId}, succeeded=" . ($importSucceeded ? '1' : '0') . ")");
// 🔹 STEP 10: GET di controllo post-PATCH
$expand = "CommesseCustomFields(\$expand=CustomField)";
@@ -542,7 +657,23 @@ try {
"RESPONSE:\n" . json_encode($commessaAfterPatch, JSON_PRETTY_PRINT);
$logFileStep10 = $logDir . "commessa_{$commessaId}_get_step10_" . time() . ".txt";
$writeLog($logFileStep10, $logContentStep10, "STEP 10 - GET verify (commessa={$commessaId})");
// 🔹 STEP 10.1: Save final CodiceCommessa into datadb.commessaweb
// After ImportaCommessa, the API returns the final LIMS job code in CodiceCommessa.
// Example: CodiceCommessa = 2614795, CodiceCommessaWeb = 26C0103.
$finalCodiceCommessa = trim((string)($commessaAfterPatch['CodiceCommessa'] ?? ''));
if ($finalCodiceCommessa !== '') {
$stmt = $pdo->prepare("
UPDATE datadb
SET commessaweb = :commessaweb,
status = 'l'
WHERE iddatadb = :iddatadb
");
$stmt->execute([
'commessaweb' => substr($finalCodiceCommessa, 0, 30),
'iddatadb' => $iddatadb
]);
}
// 🔹 STEP 11: Prepare final response
$finalCommessa = [
"Cliente" => $clienteId,
@@ -557,17 +688,21 @@ try {
echo json_encode([
"success" => true,
"idcommessaweb" => $commessaId,
"commessaweb" => $commessaWebCode,
"commessaweb" => $finalCodiceCommessa ?: $commessaWebCode,
"commessaWeb" => $finalCommessa,
"commessaWebApiResponse" => $commessaWeb, // Incluso per debug
"totalCampioni" => count($campioni),
"totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []),
"totalPhotos" => count($photos),
"totalAnalyses" => $totalAnalyses,
"addedAnalyses" => $addedAnalyses,
"failedAnalyses" => $failedAnalyses,
"message" => "Export successful",
"logFiles" => [
"step5_create" => $logFileStep5,
"step5_2_photos" => $logFilePhotos,
"step6_campioni" => $logFileStep6,
"step63_analyses" => $logFileStep63Analisi,
"step7_patch" => $logFileStep7 ?? null,
"step9_1_importa" => $logFileStep91,
"step10_get" => $logFileStep10
@@ -583,6 +718,7 @@ try {
"step5_create" => $logFileStep5 ?? null,
"step5_2_photos" => $logFilePhotos ?? null,
"step6_campioni" => $logFileStep6 ?? null,
"step63_analyses" => $logFileStep63Analisi ?? null,
"step7_patch" => $logFileStep7 ?? null,
"step9_1_importa" => $logFileStep91 ?? null,
"step10_get" => $logFileStep10 ?? null
@@ -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,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 {
$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();
$webOnly = isset($_GET['web_only']) ? (int)$_GET['web_only'] : 1;
$filterString = "Matrice/IdMatrice eq $idMatrice";
if ($webOnly === 1) {
$filterString .= " and SelezionabileSuWeb eq true";
}
$filter = rawurlencode($filterString);
$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()]);
}
+95
View File
@@ -0,0 +1,95 @@
<?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();
// Campione hardcoded del tuo esempio
$idCampione = 749027;
// Analisi che stai provando ad assegnare
$targetRecordKey = '11;1218320';
$targetIdAnalisi = '1218320';
// Endpoint: recupera il campione espandendo le analisi abilitate
$endpoint = "Campione($idCampione)";
// ATTENZIONE:
// nella tua classe normalmente le opzioni vengono trasformate in query string.
// Qui serve: ?$expand=AnalisiAbilitate
$options = [
'$expand' => 'AnalisiAbilitate'
];
// Debug: salva URL usato
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$query = http_build_query($options);
$query = urldecode($query); // rende leggibile $expand invece di %24expand
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
file_put_contents(__DIR__ . '/last_url_analisi_abilitate.txt', $full_url . PHP_EOL, FILE_APPEND);
// Chiamata API
$data = $api->get($endpoint, $options);
// Recupera AnalisiAbilitate dalla risposta
$analisiAbilitate = $data['AnalisiAbilitate'] ?? [];
// Alcune API OData possono restituire collection dentro "value"
if (isset($analisiAbilitate['value']) && is_array($analisiAbilitate['value'])) {
$analisiAbilitate = $analisiAbilitate['value'];
}
// Cerca se il RecordKey / IdAnalisi che stai usando è effettivamente assegnabile
$matches = [];
foreach ($analisiAbilitate as $analisi) {
$recordKey = isset($analisi['RecordKey']) ? (string)$analisi['RecordKey'] : '';
$idAnalisi = isset($analisi['IdAnalisi']) ? (string)$analisi['IdAnalisi'] : '';
if ($recordKey === $targetRecordKey || $idAnalisi === $targetIdAnalisi) {
$matches[] = $analisi;
}
}
// Output diagnostico
$output = [
'success' => true,
'idCampione' => $idCampione,
'request_url' => $full_url,
'targetRecordKey' => $targetRecordKey,
'targetIdAnalisi' => $targetIdAnalisi,
'enabled_analyses_count' => is_array($analisiAbilitate) ? count($analisiAbilitate) : 0,
'target_found' => count($matches) > 0,
'target_matches' => $matches,
'analisi_abilitate' => $analisiAbilitate,
'raw_response' => $data
];
// Salva il JSON in locale
file_put_contents(
__DIR__ . '/analisi_abilitate_campione_749027_response.json',
json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
echo json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log_analisi_abilitate.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+88
View File
@@ -0,0 +1,88 @@
<?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();
// Campione hardcoded del tuo esempio
$idCampione = 749027;
// Matrice attesa dal log STEP 6
$expectedMatriceId = 6430;
// Endpoint con expand della Matrice
$endpoint = "Campione($idCampione)";
$options = [
'$expand' => 'Matrice'
];
// Debug URL
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$query = http_build_query($options);
$queryReadable = urldecode($query);
$full_url = $base_url . $endpoint . ($queryReadable ? '?' . $queryReadable : '');
file_put_contents(
__DIR__ . '/last_url_check_matrice.txt',
$full_url . PHP_EOL,
FILE_APPEND
);
// Chiamata API
$data = $api->get($endpoint, $options);
// Recupero Matrice dalla response
$matrice = $data['Matrice'] ?? null;
$actualMatriceId = null;
if (is_array($matrice)) {
// Provo i nomi più probabili
$actualMatriceId = $matrice['IdMatrice']
?? $matrice['idMatrice']
?? $matrice['Id']
?? $matrice['ID']
?? null;
}
$matrice_ok = ((int)$actualMatriceId === (int)$expectedMatriceId);
$output = [
'success' => true,
'idCampione' => $idCampione,
'expectedMatriceId' => $expectedMatriceId,
'actualMatriceId' => $actualMatriceId,
'matrice_ok' => $matrice_ok,
'request_url' => $full_url,
'matrice' => $matrice,
'raw_response' => $data
];
// Salva JSON completo
file_put_contents(
__DIR__ . '/check_matrice_campione_749027_response.json',
json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
echo json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log_check_matrice.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+12 -1
View File
@@ -74,10 +74,21 @@ try {
throw new Exception("Massimo numero di tentativi raggiunto per $endpoint");
}
// Cache file (1 hour TTL)
$cacheFile = __DIR__ . '/cache/clienti.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
readfile($cacheFile);
exit;
}
// Esegui la chiamata con retry
$data = makeApiRequest($api, $endpoint);
echo json_encode($data);
$json = json_encode($data);
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
file_put_contents($cacheFile, $json);
echo $json;
} catch (Exception $e) {
http_response_code(500);
$errorResponse = [
+13 -4
View File
@@ -21,17 +21,26 @@ try {
}
$results = [];
$cacheDir = __DIR__ . '/cache';
if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true);
foreach ($fieldIds as $customFieldId) {
$cacheFile = $cacheDir . '/customfield_' . $customFieldId . '.json';
// Use cache if fresh (1 hour)
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
$results[$customFieldId] = json_decode(file_get_contents($cacheFile), true);
continue;
}
$endpoint = "CustomField($customFieldId)?\$expand=CustomFieldsValues";
$data = $api->get($endpoint);
$values = $data['CustomFieldsValues'] ?? [];
$results[$customFieldId] = $values;
$results[$customFieldId] = $data['CustomFieldsValues'] ?? [];
file_put_contents($cacheFile, json_encode($values));
}
// Debug ფაილი
file_put_contents(__DIR__ . '/customfield_values_response.json', json_encode($results));
echo json_encode($results);
} catch (Exception $e) {
http_response_code(500);
@@ -0,0 +1,63 @@
<?php
require_once 'include/headscript.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(['error' => 'Invalid request method']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$template_id = isset($input['template_id']) ? (int)$input['template_id'] : 0;
$code = isset($input['code']) ? trim($input['code']) : '';
if ($template_id <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Missing or invalid template_id']);
exit;
}
if ($code === '') {
http_response_code(400);
echo json_encode(['error' => 'Missing code']);
exit;
}
/*
* TODO: Replace this block with your real API call.
* Expected response for import_json.php:
* {
* "success": true,
* "reference": "CODE123",
* "json": { ... real JSON payload ... }
* }
*/
$sampleJson = [
'data' => [[
'type' => 'trf_data_request',
'id' => $code,
'attributes' => [
'trf_type' => 'apparel',
'service_required' => 'regular',
'submitter_information' => [
'submitter_type' => 'supplier'
]
]
]]
];
echo json_encode([
'success' => true,
'reference' => $code,
'json' => $sampleJson
]);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
+37 -50
View File
@@ -1,59 +1,46 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php'; // Risale a root/vendor/
use Dotenv\Dotenv;
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
if (!Auth::check()) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; }
// Set JSON header
header('Content-Type: application/json');
// Debug: Log the path where we expect the .env file
$envPath = dirname(__DIR__, 2);
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . ' - Expected .env path: ' . $envPath . PHP_EOL, FILE_APPEND);
// Carica il file .env dalla root del progetto
try {
$dotenv = Dotenv::createImmutable($envPath);
$dotenv->load();
} catch (Exception $e) {
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - Errore caricamento .env: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
echo json_encode(['success' => false, 'message' => 'Errore caricamento configurazione: ' . $e->getMessage()]);
exit(1);
}
// Read from matrici cache (populated by get_matrici.php / warm_cache.php)
$cacheFile = __DIR__ . '/cache/matrici.json';
if (file_exists($cacheFile)) {
$data = json_decode(file_get_contents($cacheFile), true);
$matrici = $data['value'] ?? [];
} else {
// Fallback: trigger cache creation
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
$api = VisualLimsApiClient::getInstance();
$apiData = $api->get('Matrice');
$matrici = [];
foreach (($apiData['value'] ?? []) as $item) {
$nome = $item['NomeMatrice'] ?? '';
if ($nome !== '' && substr($nome, 0, 1) !== '*') {
$matrici[] = ['IdMatrice' => $item['IdMatrice'], 'NomeMatrice' => $nome, 'MacroMatrice' => $item['MacroMatrice'] ?? null];
}
}
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
file_put_contents($cacheFile, json_encode(['success' => true, 'value' => $matrici]));
}
// Recupera le variabili d'ambiente
$dbHost = $_ENV['DB_HOST'];
$dbName = $_ENV['DB_DATABASE'];
$dbUser = $_ENV['DB_USERNAME'];
$dbPass = $_ENV['DB_PASSWORD'];
$dbPrefix = $_ENV['DB_PREFIX'];
// Extract unique MacroMatrice values
$macroValues = [];
foreach ($matrici as $m) {
$macro = $m['MacroMatrice'] ?? null;
if ($macro !== null && $macro !== '' && substr($macro, 0, 1) !== '*') {
$macroValues[$macro] = true;
}
}
$macroMatrici = array_keys($macroValues);
sort($macroMatrici);
// Debug: Log database connection details (excluding password)
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . " - DB Connection: host=$dbHost, dbname=$dbName, user=$dbUser, prefix=$dbPrefix" . PHP_EOL, FILE_APPEND);
// Connessione al database MySQL
try {
$pdo = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8mb4", $dbUser, $dbPass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - Errore connessione DB: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
echo json_encode(['success' => false, 'message' => 'Errore connessione al database: ' . $e->getMessage()]);
exit(1);
}
try {
// Query per recuperare i valori distinti di MacroMatrice, escludendo quelli che iniziano con '*' e ordinandoli
$query = "SELECT DISTINCT MacroMatrice FROM {$dbPrefix}matrici WHERE MacroMatrice IS NOT NULL AND MacroMatrice NOT LIKE '*%' ORDER BY MacroMatrice ASC";
$stmt = $pdo->prepare($query);
$stmt->execute();
$macroMatrici = $stmt->fetchAll(PDO::FETCH_COLUMN);
// Debug: Log del numero di MacroMatrice recuperate
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . ' - Retrieved ' . count($macroMatrici) . ' MacroMatrice from database' . PHP_EOL, FILE_APPEND);
// Restituisci risposta JSON
echo json_encode(['success' => true, 'value' => $macroMatrici]);
} catch (PDOException $e) {
// Log errore e restituisci risposta di errore
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - Errore nel recupero delle MacroMatrice: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
echo json_encode(['success' => false, 'message' => 'Errore nel recupero delle MacroMatrice: ' . $e->getMessage()]);
exit(1);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+47
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()]);
}
+143
View File
@@ -0,0 +1,143 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
$idCliente = isset($_GET['id_cliente']) ? (int)$_GET['id_cliente'] : 0;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 3;
$signedStatus = trim($_GET['signed_status'] ?? 'all');
if ($idCliente <= 0) {
throw new Exception("Parametro id_cliente mancante o non valido.");
}
/*
* Allowed limits only.
* This prevents risky wide queries on the live LIMS.
*/
$allowedLimits = [1, 3, 5, 10];
if (!in_array($limit, $allowedLimits, true)) {
$limit = 3;
}
/*
* Allowed signature filters.
*/
$allowedSignedStatuses = ['all', 'signed', 'not_signed'];
if (!in_array($signedStatus, $allowedSignedStatuses, true)) {
$signedStatus = 'all';
}
/*
* Base filter by customer.
* We already verified that Rapporto can expand Cliente and returns Cliente.IdCliente.
*/
$filters = [
"Cliente/IdCliente eq {$idCliente}"
];
if ($signedStatus === 'signed') {
$filters[] = "Firmato eq true";
}
if ($signedStatus === 'not_signed') {
$filters[] = "Firmato eq false";
}
$filter = implode(' and ', $filters);
/*
* Important:
* - $top limits the number of reports.
* - $orderby=Data desc gets the latest reports first.
* - $expand=RapportiFiles retrieves only the PDF file metadata, not the binary PDF.
*/
$params = [
'$filter' => $filter,
'$select' => 'IdRapporto,CodiceRapporto,Data,Versione,Firmato,DataStampa',
'$expand' => 'RapportiFiles',
'$orderby' => 'Data desc',
'$top' => $limit
];
$endpoint = "Rapporto?" . http_build_query($params);
file_put_contents(
__DIR__ . '/last_rapporti_cliente_endpoint.txt',
'[' . date('Y-m-d H:i:s') . '] ' . $endpoint . PHP_EOL,
FILE_APPEND
);
$data = $api->get($endpoint);
$items = $data['value'] ?? [];
if (!is_array($items)) {
$items = [];
}
$reports = [];
foreach ($items as $item) {
$rapportiFiles = $item['RapportiFiles'] ?? [];
$pdfFiles = [];
if (is_array($rapportiFiles)) {
foreach ($rapportiFiles as $file) {
$idRapportoFile = intval($file['IdRapportoFile'] ?? 0);
if ($idRapportoFile > 0) {
$pdfFiles[] = [
'id_rapporto_file' => $idRapportoFile,
'file_name' => $file['FileName'] ?? null,
'categoria' => $file['Categoria'] ?? null,
'tipo_rapporto' => $file['TipoRapporto'] ?? null,
'download_url' => "download_rapporto_pdf.php?id_rapporto_file={$idRapportoFile}"
];
}
}
}
$reports[] = [
'id_rapporto' => $item['IdRapporto'] ?? null,
'codice_rapporto' => $item['CodiceRapporto'] ?? null,
'data' => $item['Data'] ?? null,
'data_stampa' => $item['DataStampa'] ?? null,
'versione' => $item['Versione'] ?? null,
'firmato' => $item['Firmato'] ?? null,
'pdf_files' => $pdfFiles
];
}
echo json_encode([
'success' => true,
'id_cliente' => $idCliente,
'limit' => $limit,
'signed_status' => $signedStatus,
'endpoint' => $endpoint,
'count' => count($reports),
'reports' => $reports
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log.txt',
date('Y-m-d H:i:s') . ' - get_rapporti_cliente.php - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
@@ -0,0 +1,256 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '0');
error_reporting(E_ALL);
/**
* Uso:
* rapporto_full_by_codice.php?codice=2621521
* rapporto_full_by_codice.php?codice=2621521&download=1
*/
try {
$api = VisualLimsApiClient::getInstance();
$codiceRapporto = trim($_GET['codice'] ?? '');
$downloadPdf = isset($_GET['download']) && $_GET['download'] == '1';
if ($codiceRapporto === '') {
throw new Exception("Parametro codice mancante. Usa ?codice=2621521");
}
$debugDir = __DIR__ . '/logs/lims_rapporti/';
$pdfDir = __DIR__ . '/pdf/rapporti/';
if (!is_dir($debugDir)) mkdir($debugDir, 0755, true);
if (!is_dir($pdfDir)) mkdir($pdfDir, 0755, true);
$codiceRapportoSafe = str_replace("'", "''", $codiceRapporto);
/**
* STEP 1 - Search IdRapporto
*/
$searchParams = [
'$filter' => "CodiceRapporto eq '{$codiceRapportoSafe}'",
'$select' => 'IdRapporto,CodiceRapporto,Data,Versione,Firmato,DataStampa',
'$top' => 1
];
$searchEndpoint = "Rapporto?" . http_build_query($searchParams);
$searchData = $api->get($searchEndpoint);
$rapporti = $searchData['value'] ?? [];
if (empty($rapporti)) {
echo json_encode([
'success' => false,
'message' => 'Nessun rapporto trovato per questo CodiceRapporto.',
'codice_rapporto' => $codiceRapporto
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
$rapportoBase = $rapporti[0];
$idRapporto = intval($rapportoBase['IdRapporto'] ?? 0);
if (!$idRapporto) {
throw new Exception("IdRapporto non trovato nella risposta.");
}
/**
* STEP 2 - Dettaglio Rapporto
*/
$clienteExpandError = null;
$detailEndpoint = "Rapporto({$idRapporto})?\$expand=Cliente,RapportiFiles,CampioniDatiRapporto";
try {
$detailData = $api->get($detailEndpoint);
} catch (Exception $e) {
$clienteExpandError = $e->getMessage();
$detailEndpoint = "Rapporto({$idRapporto})?\$expand=RapportiFiles,CampioniDatiRapporto";
$detailData = $api->get($detailEndpoint);
}
file_put_contents(
$debugDir . "rapporto_{$codiceRapportoSafe}_light.json",
json_encode($detailData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
);
$cliente = $detailData['Cliente'] ?? null;
$rapportiFiles = $detailData['RapportiFiles'] ?? [];
$campioniDatiRapporto = $detailData['CampioniDatiRapporto'] ?? [];
// ====================== CLIENTE ======================
$clienteInfo = [
'found' => is_array($cliente),
'id_cliente' => $cliente['IdCliente'] ?? $cliente['Id'] ?? null,
'codice_cliente' => $cliente['CodiceCliente'] ?? $cliente['Codice'] ?? null,
'nome_cliente' => $cliente['Nominativo'] ?? $cliente['RagioneSociale'] ?? $cliente['Nome'] ?? $cliente['Descrizione'] ?? null,
'partita_iva' => $cliente['PartitaIva'] ?? null,
'codice_fiscale' => $cliente['CodiceFiscale'] ?? null
];
// ====================== FILE PDF ======================
$selectedRapportoFile = null;
$idRapportoFile = null;
$pdfFileName = $codiceRapporto . '.pdf';
$pdfFullPath = $pdfDir . $pdfFileName;
if (is_array($rapportiFiles) && count($rapportiFiles) > 0) {
foreach ($rapportiFiles as $file) {
$fileName = $file['FileName'] ?? '';
$tipoRapporto = $file['TipoRapporto'] ?? '';
$categoria = $file['Categoria'] ?? '';
if (
stripos($fileName, '.pdf') !== false ||
stripos($tipoRapporto, 'Rapporto') !== false ||
stripos($categoria, 'Rapporti') !== false
) {
$selectedRapportoFile = $file;
$idRapportoFile = $file['IdRapportoFile'] ?? null;
break;
}
}
if (!$selectedRapportoFile && count($rapportiFiles) > 0) {
$selectedRapportoFile = $rapportiFiles[0];
$idRapportoFile = $rapportiFiles[0]['IdRapportoFile'] ?? null;
}
}
// ====================== DOWNLOAD PDF - DEBUG AVANZATO ======================
$pdfDownloaded = false;
$pdfError = null;
$endpointUsato = null;
if ($downloadPdf && $idRapportoFile) {
$tentativi = [
"RapportoFile({$idRapportoFile})/\$value",
"RapportoFile({$idRapportoFile})/Contenuto/\$value",
"RapportoFile({$idRapportoFile})/File/\$value",
"Rapporto({$idRapporto})/RapportiFiles({$idRapportoFile})/\$value",
"Rapporto({$idRapporto})/RapportoFile/\$value",
"GetRapportoFile?IdRapportoFile={$idRapportoFile}",
"RapportoFile/Download?Id={$idRapportoFile}",
"Rapporto/DownloadFile?IdRapporto={$idRapporto}"
];
foreach ($tentativi as $ep) {
try {
$pdfContent = $api->getRaw($ep);
if (strlen($pdfContent) > 2000) { // PDF decente deve essere più grande
file_put_contents($pdfFullPath, $pdfContent);
$pdfDownloaded = true;
$endpointUsato = $ep;
$pdfError = "SUCCESSO con: " . $ep;
break;
} else {
$pdfError = "Contenuto troppo piccolo con: " . $ep;
}
} catch (Exception $e) {
$pdfError = "Fallito con " . $ep . "" . $e->getMessage();
}
}
}
// ====================== CAMPIONI + RATING (ripristinato dal tuo originale) ======================
$campioniSintesi = [];
$ratingFinale = null;
$ratingSource = null;
$hasIrregolare = false;
$hasPositiveResults = false;
if (is_array($campioniDatiRapporto)) {
foreach ($campioniDatiRapporto as $campione) {
$giudizioRapporto = $campione['GiudizioRapporto'] ?? null;
$giudizioCertificato = $campione['GiudizioCertificato'] ?? null;
$esitoGiudizioLMR = $campione['EsitoGiudizioLMR'] ?? null;
$esitoGiudizioImpiego = $campione['EsitoGiudizioImpiego'] ?? null;
$risultatiIrregolari = ($campione['RisultatiIrregolari'] ?? false) === true;
$risultatiPositivi = ($campione['RisultatiPositivi'] ?? false) === true;
$campioniSintesi[] = [
'id_campione_dati_rapporto' => $campione['IdCampioneDatiRapporto'] ?? null,
'codice_campione' => $campione['CodiceCampione'] ?? null,
'stato_campione' => $campione['StatoCampione'] ?? null,
'stato_campione_web' => $campione['StatoCampioneWeb'] ?? null,
'matrice' => $campione['Matrice'] ?? null,
'macro_matrice' => $campione['MacroMatrice'] ?? null,
'giudizio_rapporto' => $giudizioRapporto,
'giudizio_certificato' => $giudizioCertificato,
'esito_giudizio_lmr' => $esitoGiudizioLMR,
'esito_giudizio_impiego' => $esitoGiudizioImpiego,
'risultati_positivi' => $risultatiPositivi,
'risultati_irregolari' => $risultatiIrregolari
];
if (!$ratingFinale && !empty($giudizioRapporto)) {
$ratingFinale = $giudizioRapporto;
$ratingSource = 'CampioniDatiRapporto.GiudizioRapporto';
}
if (!$ratingFinale && !empty($giudizioCertificato)) {
$ratingFinale = $giudizioCertificato;
$ratingSource = 'CampioniDatiRapporto.GiudizioCertificato';
}
if ($risultatiIrregolari) $hasIrregolare = true;
if ($risultatiPositivi) $hasPositiveResults = true;
}
}
// Fallback rating
if (!$ratingFinale) {
if ($hasIrregolare) {
$ratingFinale = 'Irregolare';
$ratingSource = 'RisultatiIrregolari';
} elseif ($hasPositiveResults || count($campioniSintesi) > 0) {
$ratingFinale = 'Regolare';
$ratingSource = 'fallback';
}
}
// ====================== RISPOSTA FINALE ======================
echo json_encode([
'success' => true,
'codice_rapporto' => $codiceRapporto,
'id_rapporto' => $idRapporto,
'search_endpoint' => $searchEndpoint,
'detail_endpoint' => $detailEndpoint,
'cliente_expand_error' => $clienteExpandError,
'rapporto_base' => $rapportoBase,
'cliente' => $clienteInfo,
'rating_finale' => $ratingFinale,
'rating_source' => $ratingSource,
'rapporti_files_count' => count($rapportiFiles),
'selected_rapporto_file' => $selectedRapportoFile,
'pdf_file_name' => $pdfFileName,
'pdf_path' => $pdfDownloaded ? $pdfFullPath : null,
'pdf_downloaded' => $pdfDownloaded,
'pdf_error' => $pdfError,
'campioni_count' => count($campioniSintesi),
'campioni_sintesi' => $campioniSintesi,
'download_requested' => $downloadPdf
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+118
View File
@@ -0,0 +1,118 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
$commessa = trim($_GET['commessa'] ?? '');
if ($commessa === '') {
throw new Exception("Parametro commessa mancante");
}
$debugDir = __DIR__ . '/logs/lims_rapporto/';
if (!is_dir($debugDir)) {
mkdir($debugDir, 0755, true);
}
/**
* STEP 1
* Provo a cercare il rapporto con filtri leggeri.
* NIENTE expand pesanti.
*/
$attempts = [];
$filters = [
'CodiceCommessa' => "CodiceCommessa eq '{$commessa}'",
'Commessa_CodiceCommessa' => "Commessa/CodiceCommessa eq '{$commessa}'",
'Commessa_IdCommessa' => is_numeric($commessa) ? "Commessa/IdCommessa eq {$commessa}" : null,
'Codice' => "Codice eq '{$commessa}'"
];
foreach ($filters as $label => $filter) {
if (!$filter) {
continue;
}
try {
$options = [
'$filter' => $filter,
'$top' => 10
];
$data = $api->get('Rapporto', $options);
$attempts[$label] = [
'success' => true,
'filter' => $filter,
'records' => isset($data['value']) && is_array($data['value']) ? count($data['value']) : null,
'data' => $data
];
file_put_contents(
$debugDir . "rapporto_{$commessa}_{$label}.json",
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
} catch (Exception $e) {
$attempts[$label] = [
'success' => false,
'filter' => $filter,
'error' => $e->getMessage()
];
}
}
/**
* STEP 2
* Prendo solo eventuali rapporti trovati.
*/
$rapportiFound = [];
foreach ($attempts as $label => $attempt) {
if (!$attempt['success']) {
continue;
}
$items = $attempt['data']['value'] ?? [];
if (!is_array($items)) {
continue;
}
foreach ($items as $item) {
$rapportiFound[] = [
'matched_by' => $label,
'rapporto' => $item
];
}
}
echo json_encode([
'success' => true,
'input_commessa' => $commessa,
'message' => 'Ricerca leggera su Rapporto completata. Se trovi un rapporto, poi recuperiamo RapportiFiles solo per quello.',
'rapporti_found_count' => count($rapportiFound),
'rapporti_found' => $rapportiFound,
'attempts_summary' => array_map(function ($a) {
return [
'success' => $a['success'],
'filter' => $a['filter'],
'records' => $a['records'] ?? null,
'error' => $a['error'] ?? null
];
}, $attempts)
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+164 -10
View File
@@ -2,25 +2,179 @@
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
$rapporto_id = 533329;
// Costruzione manuale dell'endpoint con espansione annidata
$endpoint = "Rapporto($rapporto_id)?\$expand=CampioniDatiRapporto(\$expand=AnalisiDatiRapporto,CustomFieldsDatiRapporto)";
// Esempi:
// rapporto_by_codice_expand_step.php?codice=2541111&step=base
// rapporto_by_codice_expand_step.php?codice=2541111&step=files
// rapporto_by_codice_expand_step.php?codice=2541111&step=campioni
// rapporto_by_codice_expand_step.php?codice=2541111&step=files_campioni
// Non passiamo options, già incluso nell'endpoint
$data = $api->get($endpoint);
$codiceRapporto = trim($_GET['codice'] ?? '');
// Safe step mode: default is base, but allows controlled read-only steps
$step = trim($_GET['step'] ?? 'base');
file_put_contents(__DIR__ . '/rapporto_expanded.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
echo json_encode($data);
if ($codiceRapporto === '') {
throw new Exception("Parametro codice mancante. Usa ?codice=2541111");
}
$allowedSteps = [
'base' => '',
'campioni' => 'CampioniDatiRapporto',
'files' => 'RapportiFiles,Cliente',
'cliente' => 'Cliente'
];
if (!array_key_exists($step, $allowedSteps)) {
throw new Exception("Step non valido. Usa: " . implode(', ', array_keys($allowedSteps)));
}
// Escape OData per eventuali apostrofi
$codiceRapportoSafe = str_replace("'", "''", $codiceRapporto);
// Safe version of codice rapporto for filenames
$codiceRapportoFileSafe = preg_replace('/[^a-zA-Z0-9_-]/', '_', $codiceRapporto);
/*
* STEP 1 - Trova IdRapporto partendo da CodiceRapporto.
* Query leggera, con $select e $top=1.
*/
$searchParams = [
'$filter' => "CodiceRapporto eq '{$codiceRapportoSafe}'",
'$select' => 'IdRapporto,CodiceRapporto,Data,Versione,Firmato,DataStampa',
'$top' => 1
];
$searchEndpoint = "Rapporto?" . http_build_query($searchParams);
$searchData = $api->get($searchEndpoint);
$items = $searchData['value'] ?? [];
if (!is_array($items) || count($items) === 0) {
echo json_encode([
'success' => false,
'message' => 'Nessun rapporto trovato per questo CodiceRapporto.',
'codice_rapporto' => $codiceRapporto,
'search_endpoint' => $searchEndpoint,
'search_data' => $searchData
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
$rapportoBase = $items[0];
$rapportoId = intval($rapportoBase['IdRapporto'] ?? 0);
if (!$rapportoId) {
throw new Exception("IdRapporto non trovato nella risposta.");
}
/*
* STEP 2 - Se step=base, restituisco solo la ricerca base.
*/
if ($step === 'base') {
echo json_encode([
'success' => true,
'codice_rapporto' => $codiceRapporto,
'id_rapporto' => $rapportoId,
'step' => $step,
'search_endpoint' => $searchEndpoint,
'rapporto_base' => $rapportoBase
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
/*
* STEP 3 - Espande SOLO il rapporto trovato.
*/
$expandValue = $allowedSteps[$step];
$detailParams = [
'$expand' => $expandValue
];
$detailEndpoint = "Rapporto({$rapportoId})?" . http_build_query($detailParams);
file_put_contents(
__DIR__ . '/last_rapporto_by_codice_expand_endpoint.txt',
'[' . date('Y-m-d H:i:s') . '] SEARCH: ' . $searchEndpoint . PHP_EOL .
'[' . date('Y-m-d H:i:s') . '] DETAIL: ' . $detailEndpoint . PHP_EOL,
FILE_APPEND
);
$detailData = $api->get($detailEndpoint);
$pdfFiles = [];
if ($step === 'files') {
$rapportiFiles = $detailData['RapportiFiles'] ?? [];
if (is_array($rapportiFiles)) {
foreach ($rapportiFiles as $file) {
$idRapportoFile = intval($file['IdRapportoFile'] ?? 0);
if ($idRapportoFile > 0) {
$pdfFiles[] = [
'id_rapporto_file' => $idRapportoFile,
'file_name' => $file['FileName'] ?? null,
'categoria' => $file['Categoria'] ?? null,
'tipo_rapporto' => $file['TipoRapporto'] ?? null,
'download_endpoint' => "MediaFile/DownloadStream?objectType=RapportoFile&propertyName=FileContent&objectKey={$idRapportoFile}"
];
}
}
}
}
$clienteData = null;
if ($step === 'cliente' || $step === 'files') {
$clienteData = $detailData['Cliente'] ?? null;
}
file_put_contents(
__DIR__ . "/rapporto_codice_{$codiceRapportoFileSafe}_{$step}.json",
json_encode([
'search' => $searchData,
'detail' => $detailData
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
);
$response = [
'success' => true,
'codice_rapporto' => $codiceRapporto,
'id_rapporto' => $rapportoId,
'step' => $step,
'search_endpoint' => $searchEndpoint,
'detail_endpoint' => $detailEndpoint,
'rapporto_base' => $rapportoBase,
'data' => $detailData
];
if ($step === 'files') {
$response['pdf_files'] = $pdfFiles;
}
if ($step === 'cliente' || $step === 'files') {
$response['cliente'] = $clienteData;
}
echo json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
file_put_contents(
__DIR__ . '/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()]);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
File diff suppressed because it is too large Load Diff
+263 -61
View File
@@ -10,65 +10,125 @@
<title>Template Buttons - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<style>
/* Layout flessibile per gestire dimensioni diverse */
#templateButtons {
/* Main buttons container */
.template-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
gap: 12px;
justify-content: flex-start;
/* Allinea a sinistra */
padding: 20px;
padding: 10px 0;
}
/* Definizione delle dimensioni */
/* Definizione delle dimensioni */
/* Button sizes */
.btn-small {
font-size: 12px;
padding: 6px 12px;
min-width: 100px;
min-height: 30px;
display: flex;
/* Aggiunto */
min-height: 36px;
display: inline-flex;
justify-content: center;
/* Aggiunto */
align-items: center;
/* Aggiunto */
gap: 8px;
border-radius: 8px;
text-align: center;
}
.btn-medium {
font-size: 16px;
padding: 10px 20px;
min-width: 130px;
min-height: 45px;
display: flex;
/* Aggiunto */
min-width: 140px;
min-height: 48px;
display: inline-flex;
justify-content: center;
/* Aggiunto */
align-items: center;
/* Aggiunto */
gap: 8px;
border-radius: 10px;
text-align: center;
}
.btn-large {
font-size: 20px;
padding: 14px 28px;
min-width: 180px;
min-height: 60px;
display: flex;
/* Aggiunto */
min-width: 190px;
min-height: 64px;
display: inline-flex;
justify-content: center;
/* Aggiunto */
align-items: center;
/* Aggiunto */
gap: 10px;
border-radius: 12px;
text-align: center;
}
/* Stile della barra di ricerca */
.template-icon {
font-size: 18px;
line-height: 1;
}
.btn-large .template-icon {
font-size: 22px;
}
.btn-small .template-icon {
font-size: 15px;
}
/* Search box */
#searchInput {
width: 100%;
padding: 10px;
padding: 10px 14px;
font-size: 16px;
margin-bottom: 15px;
border: 1px solid #ccc;
border-radius: 5px;
margin-bottom: 18px;
border: 1px solid #d9d9d9;
border-radius: 8px;
outline: none;
}
#searchInput:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.15rem rgba(13, 110, 253, 0.15);
}
/* Tabs */
.custom-tabs {
border-bottom: 1px solid #e5e5e5;
margin-bottom: 20px;
gap: 6px;
}
.custom-tabs .nav-link {
border: none;
border-radius: 10px 10px 0 0;
color: #555;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
}
.custom-tabs .nav-link:hover {
background: #f8f9fa;
color: #0d6efd;
}
.custom-tabs .nav-link.active {
background: #0d6efd;
color: #fff;
}
.tab-pane {
min-height: 140px;
}
.empty-message {
color: #6c757d;
font-style: italic;
padding: 10px 0;
}
.loading-message {
color: #6c757d;
padding: 10px 0;
}
</style>
</head>
@@ -87,14 +147,54 @@
<h6 class="mb-0">Active Templates</h6>
</div>
<div class="card-body">
<!-- Barra di ricerca -->
<input type="text" id="searchInput" placeholder="Search template...">
<div id="templateButtons"></div>
<ul class="nav nav-tabs custom-tabs" id="templateTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="xls-tab" data-bs-toggle="tab" data-bs-target="#xls-pane" type="button" role="tab" aria-controls="xls-pane" aria-selected="true">
<i class="bx bx-spreadsheet template-icon"></i>
XLS
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="api-tab" data-bs-toggle="tab" data-bs-target="#api-pane" type="button" role="tab" aria-controls="api-pane" aria-selected="false">
<i class="bx bx-transfer-alt template-icon"></i>
JSON/API
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pdf-tab" data-bs-toggle="tab" data-bs-target="#pdf-pane" type="button" role="tab" aria-controls="pdf-pane" aria-selected="false">
<i class="bx bx-file-pdf template-icon"></i>
PDF
</button>
</li>
</ul>
<div class="tab-content" id="templateTabsContent">
<div class="tab-pane fade show active" id="xls-pane" role="tabpanel" aria-labelledby="xls-tab">
<div id="templateButtonsXLS" class="template-buttons">
<div class="loading-message">Loading XLS templates...</div>
</div>
</div>
<div class="tab-pane fade" id="api-pane" role="tabpanel" aria-labelledby="api-tab">
<div id="templateButtonsAPI" class="template-buttons">
<div class="loading-message">Loading API templates...</div>
</div>
</div>
<div class="tab-pane fade" id="pdf-pane" role="tabpanel" aria-labelledby="pdf-tab">
<div id="templateButtonsPDF" class="template-buttons">
<div class="loading-message">Loading PDF templates...</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
@@ -104,46 +204,148 @@
<script>
document.addEventListener("DOMContentLoaded", function() {
const allTemplates = [];
const containers = {
XLS: document.getElementById("templateButtonsXLS"),
API: document.getElementById("templateButtonsAPI"),
PDF: document.getElementById("templateButtonsPDF")
};
function getSizeClass(buttonSize) {
if (buttonSize === "small") return "btn-small";
if (buttonSize === "large") return "btn-large";
return "btn-medium";
}
function getTemplateIcon(sourceType) {
switch ((sourceType || '').toUpperCase()) {
case 'XLS':
return 'bx bx-spreadsheet';
case 'API':
return 'bx bx-transfer-alt';
case 'PDF':
return 'bx bx-file-pdf';
default:
return 'bx bx-file';
}
}
function createButton(template) {
const sizeClass = getSizeClass(template.button_size);
const sourceType = (template.source_type || '').toUpperCase();
const iconClass = getTemplateIcon(sourceType);
const btn = document.createElement("a");
// Redirect based on template source type
if (sourceType === 'XLS') {
btn.href = `import_xls2.php?id=${template.id}`;
} else if (sourceType === 'API' || sourceType === 'JSON') {
btn.href = `import_json.php?id=${template.id}`;
} else if (sourceType === 'PDF') {
btn.href = `import_pdf.php?id=${template.id}`;
} else {
btn.href = `import_xls2.php?id=${template.id}`;
}
btn.className = `btn ${sizeClass}`;
btn.style.backgroundColor = template.button_bg_color || '#0d6efd';
btn.style.color = template.button_text_color || '#ffffff';
btn.setAttribute("data-label", (template.button_label || '').toLowerCase());
btn.setAttribute("data-source-type", sourceType);
btn.innerHTML = `
<i class="${iconClass} template-icon"></i>
<span>${escapeHtml(template.button_label || 'Unnamed')}</span>
`;
return btn;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function clearContainers() {
Object.values(containers).forEach(container => {
container.innerHTML = '';
});
}
function renderTemplates(searchValue = '') {
clearContainers();
const grouped = {
XLS: [],
API: [],
PDF: []
};
allTemplates.forEach(template => {
const sourceType = (template.source_type || '').toUpperCase();
const label = (template.button_label || '').toLowerCase();
if (searchValue && !label.includes(searchValue)) {
return;
}
if (grouped[sourceType]) {
grouped[sourceType].push(template);
}
});
Object.keys(grouped).forEach(type => {
const container = containers[type];
if (!container) return;
if (grouped[type].length === 0) {
container.innerHTML = `<div class="empty-message">No templates found in this section.</div>`;
return;
}
grouped[type].forEach(template => {
container.appendChild(createButton(template));
});
});
}
fetch("load_active_templates.php")
.then(response => response.json())
.then(data => {
if (!data.success) {
console.error("Error loading templates:", data.message);
clearContainers();
Object.values(containers).forEach(container => {
container.innerHTML = `<div class="empty-message">Error loading templates.</div>`;
});
return;
}
let templateButtons = document.getElementById("templateButtons");
templateButtons.innerHTML = '';
data.data.forEach(template => {
let sizeClass = "btn-medium"; // Default
if (template.button_size === "small") sizeClass = "btn-small";
if (template.button_size === "large") sizeClass = "btn-large";
let btn = document.createElement("a");
btn.href = `import_xls2.php?id=${template.id}`;
btn.className = `btn ${sizeClass}`;
btn.style.backgroundColor = template.button_bg_color;
btn.style.color = template.button_text_color;
btn.textContent = template.button_label;
btn.setAttribute("data-label", template.button_label.toLowerCase()); // Attributo per ricerca
templateButtons.appendChild(btn);
});
})
.catch(error => console.error("Fetch error:", error));
// Funzione per la ricerca live
document.getElementById("searchInput").addEventListener("input", function() {
let searchValue = this.value.toLowerCase();
document.querySelectorAll("#templateButtons a").forEach(btn => {
let label = btn.getAttribute("data-label");
if (label.includes(searchValue)) {
btn.style.display = "inline-block";
} else {
btn.style.display = "none";
if (!Array.isArray(data.data)) {
clearContainers();
Object.values(containers).forEach(container => {
container.innerHTML = `<div class="empty-message">Invalid response format.</div>`;
});
return;
}
allTemplates.push(...data.data);
renderTemplates();
})
.catch(error => {
console.error("Fetch error:", error);
clearContainers();
Object.values(containers).forEach(container => {
container.innerHTML = `<div class="empty-message">Fetch error while loading templates.</div>`;
});
});
document.getElementById("searchInput").addEventListener("input", function() {
const searchValue = this.value.toLowerCase().trim();
renderTemplates(searchValue);
});
});
</script>
+9 -12
View File
@@ -1,10 +1,6 @@
<?php
include('include/headscript.php');
// ✅ FIX timezone (Rome)
ini_set('date.timezone', 'Europe/Rome');
date_default_timezone_set('Europe/Rome');
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['template_id']) || !isset($_POST['selected_rows']) || !isset($_POST['filename'])) {
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Richiesta non valida"));
exit;
@@ -340,6 +336,7 @@ function fixedDefaultValue(array $f): string
align-items: center;
padding: 0;
border-bottom: 1px solid #dee2e6;
min-width: fit-content;
}
.grid-row:last-child {
@@ -890,9 +887,8 @@ function fixedDefaultValue(array $f): string
<div class="page-wrapper">
<div class="page-content">
<div class="mb-3 text">
<a href="historical_trf.php?id=<?= $template_id ?>&status=i" class="btn btn-warning me-2">Imported (i)</a>
<a href="historical_trf.php?id=<?= $template_id ?>&status=P" class="btn btn-primary me-2">In Progress (P)</a>
<a href="historical_trf.php?id=<?= $template_id ?>&status=l" class="btn btn-success">To LIMS (l)</a>
<a href="imported.php?id=<?= $template_id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success">To LIMS (l)</a>
</div>
<div class="card radius-10">
<div class="card-header">
@@ -1426,6 +1422,7 @@ function fixedDefaultValue(array $f): string
</div>
</form>
<div id="partsModalContainer"></div>
<div id="analysisModalContainer"></div>
<div id="annotationsModalContainer"></div>
<?php include 'photos_functions.php'; ?>
</div>
@@ -1444,6 +1441,7 @@ function fixedDefaultValue(array $f): string
<script src="photos.js"></script>
<script src="annotationsModal.js"></script>
<script src="partsTable.js"></script>
<script src="analysisModal.js"></script>
<script src="tracking.js"></script>
<script src="export_to_lims.js"></script>
<script>
@@ -2322,7 +2320,7 @@ function fixedDefaultValue(array $f): string
} else {
// Select nativa
try {
$(dropdown).select2('destroy');
if ($(dropdown).hasClass('select2-hidden-accessible')) $(dropdown).select2('destroy');
} catch (e) {}
dropdown.innerHTML = '<option value="">Seleziona...</option>';
items.forEach(value => {
@@ -2765,7 +2763,8 @@ function fixedDefaultValue(array $f): string
if (results.length > 12) {
// Select2 con ricerca
$select.select2('destroy').empty().select2({
if ($select.hasClass('select2-hidden-accessible')) $select.select2('destroy');
$select.empty().select2({
data: [{
id: '',
text: 'Seleziona...'
@@ -2776,9 +2775,7 @@ function fixedDefaultValue(array $f): string
});
} else {
// Select nativa senza Select2
try {
$select.select2('destroy');
} catch (e) {}
if ($select.hasClass('select2-hidden-accessible')) $select.select2('destroy');
$select.empty();
$select.append(new Option('Seleziona...', '', true, false));
results.forEach(r => {
+221 -38
View File
@@ -12,6 +12,7 @@ if (!file_exists(__DIR__ . '/import_debug.log')) {
error_log("Inizio importazione alle " . date('Y-m-d H:i:s'));
include('include/headscript.php');
require_once(__DIR__ . '/class/binding-functions.php');
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['template_id']) || !isset($_POST['selected_rows']) || !isset($_POST['filename'])) {
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Richiesta non valida"));
@@ -24,7 +25,8 @@ $columns = json_decode(urldecode($_POST['columns'] ?? '[]'), true);
$rows = json_decode(urldecode($_POST['rows'] ?? '[]'), true);
$excelrows = json_decode(urldecode($_POST['excelrows'] ?? '[]'), true);
$newFilename = htmlspecialchars($_POST['filename']);
$newFilename = $_POST['filename'];
$source_type = strtolower(trim($_POST['source_type'] ?? 'xls'));
$_SESSION['template_id'] = $template_id;
$_SESSION['selected_rows'] = $selected_rows;
@@ -37,6 +39,7 @@ error_log("Received Data - Template ID: $template_id, Selected Rows: " . json_en
error_log("Columns: " . json_encode($columns));
error_log("Rows: " . json_encode($rows));
error_log("Excelrows: " . json_encode($excelrows));
error_log("Source type: " . $source_type);
$user_id = $iduserlogin ?? 1;
@@ -47,7 +50,23 @@ $pdo = $db->getConnection();
$importReferenceCode = date('YmdHis') . '-' . uniqid();
// Recupera tutti i mapping dal template
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field FROM template_mapping WHERE template_id = ?");
$stmt = $pdo->prepare("
SELECT
id,
excel_column,
json_node,
data_type,
is_required,
manual_default,
is_manual,
field_label,
field_id,
main_field,
auto_value,
has_list
FROM template_mapping
WHERE template_id = ?
");
$stmt->execute([$template_id]);
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -67,12 +86,27 @@ foreach ($allMappings as $mapping) {
// Inserisci le righe selezionate in datadb
$insertedIds = [];
foreach ($selected_rows as $rowIndex) {
$row = $rows[$rowIndex] ?? null;
$excelrow = $excelrows[$rowIndex] ?? null;
// Binding JSON -> LIMS senza corrispondenza salvata, per "mapping_id|json_value".
$pendingBindings = [];
// Binding risolti in automatico durante questo import (solo per visualizzazione).
$autoBindings = [];
// Binding gia' salvati in precedenza, usati su questo import (visualizzazione + modifica).
$savedBindings = [];
foreach ($selected_rows as $loopIndex => $rowIndex) {
if ($source_type === 'json') {
// JSON import sends only selected rows in rows/excelrows
$row = $rows[$loopIndex] ?? null;
$excelrow = $excelrows[$loopIndex] ?? ('JSON-' . ($loopIndex + 1));
} else {
// XLS import keeps original row indexes
$row = $rows[$rowIndex] ?? null;
$excelrow = $excelrows[$rowIndex] ?? null;
}
if ($row === null || $excelrow === null) {
error_log("Errore: riga o excelrow mancante per rowIndex $rowIndex");
error_log("Errore: riga o excelrow mancante. Source type: $source_type, loopIndex: $loopIndex, rowIndex: $rowIndex");
continue;
}
@@ -82,6 +116,14 @@ foreach ($selected_rows as $rowIndex) {
$template = $template_stmt->fetch(PDO::FETCH_ASSOC);
$default_idclient = $template['idclient'] ?? null;
// excelrow e' INT: dal JSON arriva tipo 'JSON-1', tengo solo la parte numerica.
if (is_numeric($excelrow)) {
$excelrowDb = (int) $excelrow;
} else {
$digits = preg_replace('/\D+/', '', (string) $excelrow);
$excelrowDb = $digits !== '' ? (int) $digits : ($loopIndex + 1);
}
$values = [
$template_id,
$importReferenceCode,
@@ -90,7 +132,7 @@ foreach ($selected_rows as $rowIndex) {
$user_id,
null,
date('Y-m-d'),
$excelrow,
$excelrowDb,
$default_idclient // Aggiungi idclient
];
$sql = "INSERT INTO datadb (templateid, importreferencecode, filename_import, status, user_id, limscode, importdate, excelrow, idclient) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
@@ -104,14 +146,72 @@ foreach ($selected_rows as $rowIndex) {
foreach ($allMappings as $mapping) {
$fieldValue = null;
if (!$mapping['is_manual']) {
$excelColumn = trim($mapping['excel_column']);
$excelColumnIndex = array_search($excelColumn, array_map('trim', $columns));
if ($excelColumnIndex !== false && isset($row[$excelColumnIndex]) && $row[$excelColumnIndex] !== '') {
$fieldValue = $row[$excelColumnIndex];
error_log("Found Excel column '$excelColumn' at index $excelColumnIndex, value: " . var_export($fieldValue, true));
$sourceColumn = '';
if ($source_type === 'json') {
$sourceColumn = trim($mapping['json_node'] ?? '');
} else {
$sourceColumn = trim($mapping['excel_column'] ?? '');
}
// Fallback: if JSON node is empty, try excel_column
if ($sourceColumn === '') {
$sourceColumn = trim($mapping['excel_column'] ?? '');
}
$columnsTrimmed = array_map('trim', $columns);
$candidateColumns = [];
if ($sourceColumn !== '') {
$candidateColumns[] = $sourceColumn;
if ($source_type === 'json') {
// Common JSON path variants
$candidateColumns[] = preg_replace('/^data\[\]\./', '', $sourceColumn);
$candidateColumns[] = preg_replace('/^data\.0\./', '', $sourceColumn);
$candidateColumns[] = str_replace('data[].', 'data.0.', $sourceColumn);
$candidateColumns[] = str_replace('data.0.', 'data[].', $sourceColumn);
}
}
// Remove empty and duplicate candidates
$candidateColumns = array_values(array_unique(array_filter($candidateColumns, function ($value) {
return trim((string)$value) !== '';
})));
$sourceColumnIndex = false;
$matchedColumn = '';
foreach ($candidateColumns as $candidateColumn) {
$candidateColumn = trim($candidateColumn);
$index = array_search($candidateColumn, $columnsTrimmed);
if ($index !== false) {
$sourceColumnIndex = $index;
$matchedColumn = $candidateColumn;
break;
}
}
if ($sourceColumnIndex !== false && isset($row[$sourceColumnIndex]) && $row[$sourceColumnIndex] !== '') {
$fieldValue = $row[$sourceColumnIndex];
error_log(
"Found source column. Original: '$sourceColumn', Matched: '$matchedColumn', Index: $sourceColumnIndex, Value: " .
var_export($fieldValue, true)
);
} else {
$fieldValue = $mapping['manual_default'] ?? '';
error_log("Excel column '$excelColumn' not found or empty, using default: " . var_export($fieldValue, true));
error_log(
"Source column not found or empty. Original: '$sourceColumn'. Candidates: " .
json_encode($candidateColumns) .
". Available columns: " .
json_encode($columnsTrimmed) .
". Using default: " .
var_export($fieldValue, true)
);
}
switch ($mapping['data_type']) {
case 'INT':
@@ -126,7 +226,7 @@ foreach ($selected_rows as $rowIndex) {
case 'Testo':
case 'VARCHAR':
default:
$fieldValue = !empty($fieldValue) ? htmlspecialchars((string)$fieldValue) : ($mapping['manual_default'] ?? '');
$fieldValue = !empty($fieldValue) ? (string)$fieldValue : ($mapping['manual_default'] ?? '');
break;
}
} else {
@@ -135,10 +235,93 @@ foreach ($selected_rows as $rowIndex) {
$fieldValue = date('Y-m-d');
}
}
// Apply auto_value if field is still empty
if (($fieldValue === null || $fieldValue === '') && !empty($mapping['auto_value']) && $mapping['auto_value'] !== 'none') {
if ($mapping['auto_value'] === 'import_date') {
$fieldValue = date('Y-m-d');
} elseif ($mapping['auto_value'] === 'import_time') {
$fieldValue = date('H:i');
}
}
// Binding JSON -> LIMS solo per i campi a lista importati da JSON.
if (
$source_type === 'json'
&& !$mapping['is_manual']
&& binding_is_list_field($mapping)
&& $fieldValue !== null
&& $fieldValue !== ''
) {
$jsonValue = (string) $fieldValue;
$existing = binding_lookup($pdo, (int) $mapping['id'], $jsonValue);
if ($existing) {
$fieldValue = $existing['lims_value'];
$key = $mapping['id'] . '|' . $jsonValue;
if (!isset($savedBindings[$key])) {
$savedBindings[$key] = [
'mapping_id' => (int) $mapping['id'],
'field_id' => (int) $mapping['field_id'],
'field_label' => $mapping['field_label'],
'json_value' => $jsonValue,
'lims_value' => (string) $existing['lims_value'],
'lims_value_id' => (int) $existing['lims_value_id'],
'datadb_ids' => [],
];
}
$savedBindings[$key]['datadb_ids'][] = (int) $iddatadb;
} else {
// Nessun binding salvato: provo l'auto-match 1-a-1 sui valori LIMS.
$limsValues = binding_get_lims_values((int) $mapping['field_id']);
$autoMatch = binding_auto_match($limsValues, $jsonValue);
if ($autoMatch) {
binding_upsert(
$pdo,
(int) $template_id,
(int) $mapping['id'],
(int) $mapping['field_id'],
$jsonValue,
(int) $autoMatch['IdCustomFieldsValue'],
(string) $autoMatch['Valore'],
$user_id
);
$fieldValue = (string) $autoMatch['Valore'];
$key = $mapping['id'] . '|' . $jsonValue;
if (!isset($autoBindings[$key])) {
$autoBindings[$key] = [
'mapping_id' => (int) $mapping['id'],
'field_id' => (int) $mapping['field_id'],
'field_label' => $mapping['field_label'],
'json_value' => $jsonValue,
'lims_value' => (string) $autoMatch['Valore'],
'lims_value_id' => (int) $autoMatch['IdCustomFieldsValue'],
'datadb_ids' => [],
];
}
$autoBindings[$key]['datadb_ids'][] = (int) $iddatadb;
} else {
$key = $mapping['id'] . '|' . $jsonValue;
if (!isset($pendingBindings[$key])) {
$pendingBindings[$key] = [
'mapping_id' => (int) $mapping['id'],
'field_id' => (int) $mapping['field_id'],
'field_label' => $mapping['field_label'],
'json_value' => $jsonValue,
'datadb_ids' => [],
];
}
$pendingBindings[$key]['datadb_ids'][] = (int) $iddatadb;
}
}
}
if ($mapping['is_required'] && (is_null($fieldValue) || $fieldValue === '')) {
error_log("Required field missing for mapping ID: " . $mapping['id'] . ", field: " . $mapping['field_label']);
}
error_log("Inserting into import_data_details - Mapping ID: " . $mapping['id'] . ", Field Value: " . var_export($fieldValue, true) . ", Is Manual: " . $mapping['is_manual'] . ", Excel Column: " . ($mapping['excel_column'] ?? 'N/A') . ", Manual Default: " . ($mapping['manual_default'] ?? 'N/A'));
error_log("Inserting into import_data_details - Mapping ID: " . $mapping['id'] . ", Field Value: " . var_export($fieldValue, true) . ", Is Manual: " . $mapping['is_manual'] . ", Source Column: " . ($sourceColumn ?? 'N/A') . ", Source Type: " . $source_type . ", Manual Default: " . ($mapping['manual_default'] ?? 'N/A'));
$stmt = $pdo->prepare("INSERT INTO import_data_details (id, mapping_id, field_value) VALUES (?, ?, ?)");
$stmt->execute([$iddatadb, $mapping['id'], $fieldValue]);
error_log("Inserted into import_data_details for ID $iddatadb, Mapping ID: " . $mapping['id'] . ", Field Value: " . var_export($fieldValue, true));
@@ -147,28 +330,28 @@ foreach ($selected_rows as $rowIndex) {
$_SESSION['inserted_ids'] = $insertedIds;
$params = [
'template_id' => $template_id,
'filename' => $newFilename,
];
$importedUrl = "imported.php?id=" . urlencode($template_id) . "&importref=" . urlencode($importReferenceCode);
?>
<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
// Solo se restano binding da risolvere mostro la pagina (con anche gli auto, modificabili).
if (!empty($pendingBindings)) {
$_SESSION['pending_bindings'] = [
'template_id' => $template_id,
'importref' => $importReferenceCode,
'items' => array_values($pendingBindings),
'auto' => array_values($autoBindings),
'saved' => array_values($savedBindings),
];
header("Location: resolve_bindings.php");
exit;
}
unset($_SESSION['pending_bindings']);
// Solo auto-collegati: vado diretto alla griglia, segnalando quanti.
if (!empty($autoBindings)) {
$importedUrl .= "&autobound=" . count($autoBindings);
}
header("Location: " . $importedUrl);
exit;
?>
+829
View File
@@ -0,0 +1,829 @@
<?php
include('include/headscript.php');
// Check if a valid template ID has been provided
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Invalid ID"));
exit;
}
$id = intval($_GET['id']);
// Load template
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("SELECT * FROM excel_templates WHERE id = ?");
$stmt->execute([$id]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$template) {
header("Location: template_dashboard.php?status=error&message=" . urlencode("Template not found"));
exit;
}
// Check mappings
$stmt = $pdo->prepare("SELECT id FROM template_mapping WHERE template_id = ?");
$stmt->execute([$id]);
$hasMappings = $stmt->fetch(PDO::FETCH_ASSOC);
error_log("Loaded JSON import template: " . print_r($template, true));
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.top-scrollbar {
overflow-x: auto;
overflow-y: hidden;
width: 100%;
height: 18px;
margin-bottom: 8px;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
background: #f8f9fa;
display: none;
}
.top-scrollbar-inner {
height: 1px;
}
.table-container {
overflow-x: auto;
max-width: 100%;
margin-bottom: 20px;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 10px;
text-align: left;
border: 1px solid #dee2e6;
min-width: 120px;
max-width: 260px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
.table th:first-child,
.table td:first-child {
min-width: 50px;
max-width: 50px;
}
.table th {
background-color: #f8f9fa;
position: relative;
cursor: col-resize;
user-select: none;
}
.table th .resize-handle {
position: absolute;
top: 0;
right: 0;
width: 5px;
height: 100%;
cursor: col-resize;
background: transparent;
}
.table th .resize-handle:hover {
background: #007bff;
}
.loader {
display: none;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 10px auto;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.column-filters th {
background: #ffffff;
cursor: default;
}
.column-filters input {
width: 100%;
min-width: 80px;
}
.json-code-input {
font-size: 1.15rem;
min-height: 48px;
}
.json-paste-area {
min-height: 260px;
font-family: Consolas, Monaco, monospace;
font-size: 13px;
}
.json-help-box {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 12px 14px;
font-size: 13px;
}
.source-badge {
font-size: 12px;
}
</style>
<title><?= htmlspecialchars($template['name']) ?> - JSON Import - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<?php include('top_stat_widget.php'); ?>
<div class="mb-3 text">
<a href="imported.php?id=<?= $id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $id ?>" class="btn btn-success me-2">To LIMS (l)</a>
<a href="bindings_manage.php?template_id=<?= $id ?>" class="btn btn-outline-secondary">Binding JSON -> LIMS</a>
</div>
<div class="card radius-10">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<div>
<h6 class="mb-0"><?= htmlspecialchars($template['name']) ?> - JSON Import</h6>
<small>
Template ID: <?= $id ?>
</small>
</div>
<span class="badge bg-info text-dark">JSON mode</span>
</div>
</div>
<div class="card-body">
<?php if (!$hasMappings): ?>
<div class="alert alert-warning" role="alert">
Nessun mapping trovato per questo template. Configura i mapping prima di procedere.
</div>
<?php endif; ?>
<div class="json-help-box mb-3">
<strong>Flusso:</strong> inserisci/scansiona un codice per recuperare il JSON da API, oppure incolla manualmente un JSON nel secondo tab.
Ogni JSON aggiunto diventa una riga della tabella di preview. Quando hai finito, seleziona le righe e clicca <strong>Prosegui</strong>.
</div>
<ul class="nav nav-tabs" id="jsonImportTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="api-code-tab" data-bs-toggle="tab" data-bs-target="#api-code-pane" type="button" role="tab" aria-controls="api-code-pane" aria-selected="true">
Code / Barcode
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="paste-json-tab" data-bs-toggle="tab" data-bs-target="#paste-json-pane" type="button" role="tab" aria-controls="paste-json-pane" aria-selected="false">
Paste JSON
</button>
</li>
</ul>
<div class="tab-content border border-top-0 p-3 mb-3" id="jsonImportTabsContent">
<div class="tab-pane fade show active" id="api-code-pane" role="tabpanel" aria-labelledby="api-code-tab" tabindex="0">
<form id="jsonCodeForm" class="row g-3 align-items-end">
<div class="col-lg-8">
<label for="json_code" class="form-label">Code / Barcode</label>
<input type="text" class="form-control json-code-input" id="json_code" name="json_code" placeholder="Write or scan code" autocomplete="off" <?= !$hasMappings ? 'disabled' : '' ?>>
<small class="text-muted">Lo scanner barcode normalmente scrive qui il codice e invia Enter.</small>
</div>
<div class="col-lg-4 d-flex gap-2">
<button type="submit" class="btn btn-primary flex-fill" <?= !$hasMappings ? 'disabled' : '' ?>>Load JSON</button>
<button type="button" class="btn btn-outline-secondary" id="clearCodeBtn">Clear</button>
</div>
</form>
</div>
<div class="tab-pane fade" id="paste-json-pane" role="tabpanel" aria-labelledby="paste-json-tab" tabindex="0">
<form id="pasteJsonForm">
<div class="mb-3">
<label for="manual_json_reference" class="form-label">Reference / filename</label>
<input type="text" class="form-control" id="manual_json_reference" placeholder="Optional reference, e.g. manual-json-001">
</div>
<div class="mb-3">
<label for="manual_json" class="form-label">Paste JSON</label>
<textarea class="form-control json-paste-area" id="manual_json" placeholder='{"data":[{"id":"MM000620","attributes":{"trf_type":"apparel"}}]}' <?= !$hasMappings ? 'disabled' : '' ?>></textarea>
</div>
<button type="submit" class="btn btn-primary" <?= !$hasMappings ? 'disabled' : '' ?>>Add pasted JSON</button>
<button type="button" class="btn btn-outline-secondary" id="clearManualJsonBtn">Clear</button>
</form>
</div>
</div>
<div class="loader" id="loader"></div>
<div id="errorContainer" class="alert alert-danger mt-3" style="display:none;"></div>
<div id="successContainer" class="alert alert-success mt-3" style="display:none;"></div>
<div id="tableContainer"></div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.min.js"></script>
<?php include('jsinclude.php'); ?>
<script>
document.addEventListener("DOMContentLoaded", function() {
const TEMPLATE_ID = <?= (int)$id ?>;
const API_ENDPOINT = 'get_json_by_code.php';
const INCLUDE_SOURCE_CODE_COLUMN = true;
const UNWRAP_SINGLE_DATA_ITEM = true;
const jsonCodeForm = document.getElementById('jsonCodeForm');
const pasteJsonForm = document.getElementById('pasteJsonForm');
const jsonCodeInput = document.getElementById('json_code');
const manualJsonInput = document.getElementById('manual_json');
const manualJsonReferenceInput = document.getElementById('manual_json_reference');
const clearCodeBtn = document.getElementById('clearCodeBtn');
const clearManualJsonBtn = document.getElementById('clearManualJsonBtn');
const loader = document.getElementById('loader');
const errorContainer = document.getElementById('errorContainer');
const successContainer = document.getElementById('successContainer');
const tableContainer = document.getElementById('tableContainer');
let jsonRows = [];
let columns = [];
if (jsonCodeInput && !jsonCodeInput.disabled) {
jsonCodeInput.focus();
}
jsonCodeForm.addEventListener('submit', function(e) {
e.preventDefault();
const code = jsonCodeInput.value.trim();
if (!code) {
showError('Inserisci o scansiona un codice.');
return;
}
loadJsonFromApi(code);
});
pasteJsonForm.addEventListener('submit', function(e) {
e.preventDefault();
const rawJson = manualJsonInput.value.trim();
const reference = manualJsonReferenceInput.value.trim() || 'manual-json-' + (jsonRows.length + 1);
if (!rawJson) {
showError('Incolla un JSON prima di aggiungerlo.');
return;
}
try {
const parsedJson = JSON.parse(rawJson);
addJsonRow(parsedJson, reference, 'paste');
manualJsonInput.value = '';
manualJsonReferenceInput.value = '';
showSuccess('JSON incollato aggiunto correttamente.');
} catch (err) {
showError('JSON non valido: ' + err.message);
}
});
clearCodeBtn.addEventListener('click', function() {
jsonCodeInput.value = '';
jsonCodeInput.focus();
});
clearManualJsonBtn.addEventListener('click', function() {
manualJsonInput.value = '';
manualJsonReferenceInput.value = '';
});
function loadJsonFromApi(code) {
hideMessages();
loader.style.display = 'block';
fetch(API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
template_id: TEMPLATE_ID,
code: code
})
})
.then(response => {
if (!response.ok) {
throw new Error('HTTP status ' + response.status);
}
return response.json();
})
.then(responseData => {
loader.style.display = 'none';
if (responseData.error) {
showError(responseData.error);
return;
}
let jsonPayload = responseData;
let reference = code;
// Supported endpoint response format:
// { success: true, json: {...}, reference: "..." }
if (responseData.json !== undefined) {
jsonPayload = responseData.json;
reference = responseData.reference || responseData.filename || responseData.code || code;
}
addJsonRow(jsonPayload, reference, 'api');
jsonCodeInput.value = '';
jsonCodeInput.focus();
showSuccess('JSON recuperato e aggiunto correttamente.');
})
.catch(error => {
loader.style.display = 'none';
showError('Errore durante il recupero del JSON: ' + error.message);
});
}
function addJsonRow(jsonPayload, reference, sourceType) {
// Ogni elemento di data[] diventa una riga (non colonne data.0.*, data.1.*).
const records = extractRecords(jsonPayload);
records.forEach((record, recordIndex) => {
const flattened = flattenJson(record);
const rowReference = records.length > 1 ?
reference + '#' + (recordIndex + 1) :
reference;
if (INCLUDE_SOURCE_CODE_COLUMN) {
flattened.source_code = rowReference;
flattened.source_type = sourceType;
}
const newColumns = Object.keys(flattened).filter(col => !columns.includes(col));
columns = columns.concat(newColumns);
jsonRows.push({
excelrow: 'JSON-' + (jsonRows.length + 1),
reference: rowReference,
sourceType: sourceType,
flat: flattened
});
});
renderTable();
}
// Record da trasformare in righe: gli oggetti in data[], altrimenti il payload stesso.
function extractRecords(payload) {
if (
payload &&
typeof payload === 'object' &&
!Array.isArray(payload) &&
Array.isArray(payload.data) &&
payload.data.length > 0
) {
const items = payload.data.filter(item => item && typeof item === 'object');
if (items.length > 0) {
return items;
}
}
return [payload];
}
function flattenJson(value, prefix = '', result = {}) {
if (value === null || value === undefined) {
result[prefix || 'value'] = '';
return result;
}
if (Array.isArray(value)) {
if (value.length === 0) {
result[prefix || 'value'] = '';
return result;
}
value.forEach((item, index) => {
const newPrefix = prefix ? prefix + '.' + index : String(index);
flattenJson(item, newPrefix, result);
});
return result;
}
if (typeof value === 'object') {
const keys = Object.keys(value);
if (keys.length === 0) {
result[prefix || 'value'] = '';
return result;
}
keys.forEach(key => {
const newPrefix = prefix ? prefix + '.' + key : key;
flattenJson(value[key], newPrefix, result);
});
return result;
}
result[prefix || 'value'] = String(value);
return result;
}
function buildImportData() {
const rows = jsonRows.map(row => columns.map(col => row.flat[col] ?? ''));
const excelData = rows.map((rowData, index) => ({
excelrow: jsonRows[index].excelrow,
data: rowData
}));
return {
template_id: TEMPLATE_ID,
columns: columns,
rows: rows,
excel_data: excelData,
filename: 'json_import_' + new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
};
}
function renderTable() {
if (jsonRows.length === 0) {
tableContainer.innerHTML = '';
return;
}
const data = buildImportData();
let html = `
<form id="selectRowsForm" action="import_insert.php" method="POST">
<input type="hidden" name="template_id" value="${escapeHtml(data.template_id)}">
<input type="hidden" name="columns" value="${encodeURIComponent(JSON.stringify(data.columns))}">
<input type="hidden" name="rows" id="selectedRowsData" value="">
<input type="hidden" name="excelrows" id="selectedExcelRowsData" value="">
<input type="hidden" name="filename" value="${escapeHtml(data.filename)}">
<input type="hidden" name="source_type" value="json">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
<div>
<strong>JSON rows loaded:</strong> ${jsonRows.length}
<span class="badge bg-secondary source-badge ms-2">Columns: ${data.columns.length}</span>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-danger btn-sm" id="clearAllRowsBtn">Clear all rows</button>
<button type="submit" class="btn btn-primary" id="proceedButtonTop" disabled>Prosegui</button>
</div>
</div>
<div class="top-scrollbar" id="topTableScrollbar">
<div class="top-scrollbar-inner" id="topTableScrollbarInner"></div>
</div>
<div class="table-container" id="mainTableContainer">
<table class="table table-striped table-bordered" id="importPreviewTable">
<thead>
<tr>
<th><input type="checkbox" id="selectAll"> Select</th>
${data.columns.map(col => `<th title="${escapeHtml(col)}">${escapeHtml(readableColumnLabel(col))}<div class="resize-handle"></div></th>`).join('')}
<th>Action</th>
</tr>
<tr class="column-filters">
<th></th>
${data.columns.map((col, i) => `
<th>
<input type="text"
class="form-control form-control-sm column-filter"
data-col-index="${i}"
placeholder="Filter...">
</th>
`).join('')}
<th></th>
</tr>
</thead>
<tbody>
${data.excel_data.map((row, index) => `
<tr data-row-index="${index}">
<td><input type="checkbox" class="row-checkbox" name="selected_rows[]" value="${index}" data-excelrow="${escapeHtml(row.excelrow)}"></td>
${row.data.map(cell => `<td title="${escapeHtml(cell)}">${escapeHtml(cell)}</td>`).join('')}
<td><button type="button" class="btn btn-sm btn-outline-danger remove-row-btn" data-row-index="${index}">Remove</button></td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<button type="submit" class="btn btn-primary mt-3" id="proceedButtonBottom" disabled>Prosegui</button>
</form>
`;
tableContainer.innerHTML = html;
bindTableEvents(data);
}
function bindTableEvents(data) {
const selectRowsForm = document.getElementById('selectRowsForm');
const clearAllRowsBtn = document.getElementById('clearAllRowsBtn');
const removeRowButtons = document.querySelectorAll('.remove-row-btn');
selectRowsForm.addEventListener('submit', function(e) {
const checkedBoxes = Array.from(document.querySelectorAll('.row-checkbox:checked'));
if (checkedBoxes.length === 0) {
e.preventDefault();
alert('Seleziona almeno una riga.');
return;
}
const selectedRows = [];
const selectedExcelRows = [];
checkedBoxes.forEach((cb, newIndex) => {
const originalIndex = parseInt(cb.value, 10);
if (data.rows && data.rows[originalIndex]) {
selectedRows.push(data.rows[originalIndex]);
}
if (data.excel_data && data.excel_data[originalIndex]) {
selectedExcelRows.push(data.excel_data[originalIndex].excelrow);
}
// Reindex selected_rows so import_insert.php receives only the reduced rows array
cb.value = newIndex;
});
document.getElementById('selectedRowsData').value =
encodeURIComponent(JSON.stringify(selectedRows));
document.getElementById('selectedExcelRowsData').value =
encodeURIComponent(JSON.stringify(selectedExcelRows));
});
clearAllRowsBtn.addEventListener('click', function() {
if (!confirm('Vuoi rimuovere tutte le righe JSON caricate?')) return;
jsonRows = [];
columns = [];
renderTable();
showSuccess('Righe JSON rimosse.');
jsonCodeInput.focus();
});
removeRowButtons.forEach(button => {
button.addEventListener('click', function() {
const rowIndex = parseInt(this.dataset.rowIndex, 10);
jsonRows.splice(rowIndex, 1);
rebuildColumnsFromRows();
renderTable();
});
});
const topTableScrollbar = document.getElementById('topTableScrollbar');
const topTableScrollbarInner = document.getElementById('topTableScrollbarInner');
const mainTableContainer = document.getElementById('mainTableContainer');
const importPreviewTable = document.getElementById('importPreviewTable');
function updateTopTableScrollbar() {
if (!topTableScrollbar || !topTableScrollbarInner || !mainTableContainer || !importPreviewTable) return;
topTableScrollbarInner.style.width = importPreviewTable.scrollWidth + 'px';
if (mainTableContainer.scrollWidth > mainTableContainer.clientWidth) {
topTableScrollbar.style.display = 'block';
} else {
topTableScrollbar.style.display = 'none';
}
}
let syncingTop = false;
let syncingBottom = false;
if (topTableScrollbar && mainTableContainer) {
topTableScrollbar.addEventListener('scroll', function() {
if (syncingBottom) return;
syncingTop = true;
mainTableContainer.scrollLeft = topTableScrollbar.scrollLeft;
syncingTop = false;
});
mainTableContainer.addEventListener('scroll', function() {
if (syncingTop) return;
syncingBottom = true;
topTableScrollbar.scrollLeft = mainTableContainer.scrollLeft;
syncingBottom = false;
});
}
updateTopTableScrollbar();
setTimeout(updateTopTableScrollbar, 100);
setTimeout(updateTopTableScrollbar, 300);
window.addEventListener('resize', updateTopTableScrollbar);
const proceedButtonTop = document.getElementById('proceedButtonTop');
const proceedButtonBottom = document.getElementById('proceedButtonBottom');
const selectAllCheckbox = document.getElementById('selectAll');
const checkboxes = document.querySelectorAll('.row-checkbox');
function updateProceedButton() {
const enabled = Array.from(document.querySelectorAll('.row-checkbox')).some(cb => cb.checked);
if (proceedButtonTop) proceedButtonTop.disabled = !enabled;
if (proceedButtonBottom) proceedButtonBottom.disabled = !enabled;
}
selectAllCheckbox.addEventListener('change', function() {
const visibleRows = Array.from(document.querySelectorAll('#importPreviewTable tbody tr'))
.filter(row => row.style.display !== 'none');
visibleRows.forEach(row => {
const checkbox = row.querySelector('.row-checkbox');
if (checkbox) {
checkbox.checked = this.checked;
}
});
updateProceedButton();
});
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
const visibleCheckboxes = Array.from(document.querySelectorAll('#importPreviewTable 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();
});
});
const thElements = document.querySelectorAll('#importPreviewTable th');
thElements.forEach((th, index) => {
if (index === 0) return;
const resizeHandle = th.querySelector('.resize-handle');
if (resizeHandle) {
resizeHandle.addEventListener('mousedown', (e) => {
e.preventDefault();
const startX = e.clientX;
const startWidth = th.offsetWidth;
const onMouseMove = (e) => {
const newWidth = Math.max(60, startWidth + (e.clientX - startX));
th.style.width = `${newWidth}px`;
th.style.minWidth = `${newWidth}px`;
th.style.maxWidth = `${newWidth}px`;
const cells = document.querySelectorAll(`#importPreviewTable td:nth-child(${index + 1})`);
cells.forEach(cell => {
cell.style.width = `${newWidth}px`;
cell.style.minWidth = `${newWidth}px`;
cell.style.maxWidth = `${newWidth}px`;
});
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
}
});
const rows = document.querySelectorAll('#importPreviewTable tbody tr');
const filterInputs = document.querySelectorAll('.column-filter');
const activeFilters = {};
function applyColumnFilters() {
rows.forEach(row => {
let visible = true;
for (const [colIndexStr, filterValue] of Object.entries(activeFilters)) {
const colIndex = parseInt(colIndexStr, 10);
const cell = row.cells[colIndex + 1];
const cellText = (cell?.textContent || '').toLowerCase();
const searchText = (filterValue || '').toLowerCase().trim();
if (searchText && !cellText.includes(searchText)) {
visible = false;
break;
}
}
row.style.display = visible ? '' : 'none';
});
const visibleCheckboxes = Array.from(document.querySelectorAll('#importPreviewTable 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 => {
input.addEventListener('input', function() {
const colIndex = this.dataset.colIndex;
activeFilters[colIndex] = this.value;
applyColumnFilters();
});
});
updateProceedButton();
}
function rebuildColumnsFromRows() {
columns = [];
jsonRows.forEach(row => {
Object.keys(row.flat).forEach(col => {
if (!columns.includes(col)) {
columns.push(col);
}
});
});
}
function readableColumnLabel(columnName) {
if (!columnName) return 'Column without name';
return columnName;
}
function showError(message) {
successContainer.style.display = 'none';
errorContainer.textContent = message;
errorContainer.style.display = 'block';
}
function showSuccess(message) {
errorContainer.style.display = 'none';
successContainer.textContent = message;
successContainer.style.display = 'block';
setTimeout(() => {
successContainer.style.display = 'none';
}, 3500);
}
function hideMessages() {
errorContainer.style.display = 'none';
successContainer.style.display = 'none';
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
});
</script>
</body>
</html>
+116 -11
View File
@@ -40,6 +40,22 @@ error_log("Loaded template: " . print_r($template, true));
<?php include('cssinclude.php'); ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.top-scrollbar {
overflow-x: auto;
overflow-y: hidden;
width: 100%;
height: 18px;
margin-bottom: 8px;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
background: #f8f9fa;
display: none;
}
.top-scrollbar-inner {
height: 1px;
}
.table-container {
overflow-x: auto;
max-width: 100%;
@@ -143,16 +159,20 @@ error_log("Loaded template: " . print_r($template, true));
<div class="page-content">
<?php include('top_stat_widget.php'); ?>
<div class="mb-3 text">
<a href="historical_trf.php?id=<?= $id ?>&status=i" class="btn btn-warning me-2">Imported (i)</a>
<a href="historical_trf.php?id=<?= $id ?>&status=P" class="btn btn-primary me-2">In Progress (P)</a>
<a href="historical_trf.php?id=<?= $id ?>&status=l" class="btn btn-success">To LIMS (l)</a>
<a href="imported.php?id=<?= $id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $id ?>" class="btn btn-success">To LIMS (l)</a>
</div>
<div class="card radius-10">
<div class="card-header">
<div class="d-flex align-items-center">
<div>
<h6 class="mb-0"><?= htmlspecialchars($template['name']) ?></h6>
<small>Template ID: <?= $id ?>, Start Row: <?= $template['header_row'] ?>, Start Column: <?= $template['start_column'] ?></small>
<small>
Template ID: <?= $id ?>,
Sheet Number: <?= (int)($template['xls_sheet_index'] ?? 0) ?>,
Start Row: <?= $template['header_row'] ?>,
Start Column: <?= $template['start_column'] ?>
</small>
</div>
</div>
</div>
@@ -229,8 +249,9 @@ error_log("Loaded template: " . print_r($template, true));
const templateId = <?= $id ?>;
console.log('Template ID passed to formData:', templateId);
formData.append('template_id', templateId);
formData.append('header_row', <?= $template['header_row'] ?>);
formData.append('start_column', <?= $template['start_column'] ?>);
formData.append('header_row', <?= (int)$template['header_row'] ?>);
formData.append('start_column', <?= json_encode($template['start_column']) ?>);
formData.append('xls_sheet_index', <?= (int)($template['xls_sheet_index'] ?? 0) ?>);
fetch('process_import_xls2.php', {
method: 'POST',
@@ -316,8 +337,8 @@ error_log("Loaded template: " . print_r($template, true));
<form id="selectRowsForm" action="import_insert.php" method="POST">
<input type="hidden" name="template_id" value="${data.template_id}">
<input type="hidden" name="columns" value="${encodeURIComponent(JSON.stringify(data.columns))}">
<input type="hidden" name="rows" value="${encodeURIComponent(JSON.stringify(data.rows))}">
<input type="hidden" name="excelrows" value="${encodeURIComponent(JSON.stringify(data.excel_data.map(r => r.excelrow)))}">
<input type="hidden" name="rows" id="selectedRowsData" value="">
<input type="hidden" name="excelrows" id="selectedExcelRowsData" value="">
<input type="hidden" name="filename" value="${data.filename}">
<!-- TOP BUTTON -->
@@ -325,12 +346,19 @@ error_log("Loaded template: " . print_r($template, true));
<button type="submit" class="btn btn-primary" id="proceedButtonTop" disabled>Prosegui</button>
</div>
<div class="table-container">
<table class="table table-striped table-bordered">
<div class="top-scrollbar" id="topTableScrollbar">
<div class="top-scrollbar-inner" id="topTableScrollbarInner"></div>
</div>
<div class="table-container" id="mainTableContainer">
<table class="table table-striped table-bordered" id="importPreviewTable">
<thead>
<tr>
<th><input type="checkbox" id="selectAll"> Seleziona</th>
${data.columns.map(col => `<th>${col || 'Colonna senza nome'}<div class="resize-handle"></div></th>`).join('')}
${data.columns.map(col => {
const label = !col ? 'Colonna senza nome' : (col.match(/^__empty_\d+__$/) ? 'Colonna senza nome' : col);
return `<th>${label}<div class="resize-handle"></div></th>`;
}).join('')}
</tr>
<tr class="column-filters">
<th></th>
@@ -361,6 +389,83 @@ error_log("Loaded template: " . print_r($template, true));
`;
tableContainer.innerHTML = html;
const selectRowsForm = document.getElementById('selectRowsForm');
selectRowsForm.addEventListener('submit', function(e) {
const checkedBoxes = Array.from(document.querySelectorAll('.row-checkbox:checked'));
if (checkedBoxes.length === 0) {
e.preventDefault();
alert('Seleziona almeno una riga.');
return;
}
const selectedRows = [];
const selectedExcelRows = [];
checkedBoxes.forEach((cb, newIndex) => {
const originalIndex = parseInt(cb.value, 10);
if (data.rows && data.rows[originalIndex]) {
selectedRows.push(data.rows[originalIndex]);
}
if (data.excel_data && data.excel_data[originalIndex]) {
selectedExcelRows.push(data.excel_data[originalIndex].excelrow);
}
// Reindex selected_rows so import_insert.php receives only the reduced rows array
cb.value = newIndex;
});
document.getElementById('selectedRowsData').value =
encodeURIComponent(JSON.stringify(selectedRows));
document.getElementById('selectedExcelRowsData').value =
encodeURIComponent(JSON.stringify(selectedExcelRows));
});
const topTableScrollbar = document.getElementById('topTableScrollbar');
const topTableScrollbarInner = document.getElementById('topTableScrollbarInner');
const mainTableContainer = document.getElementById('mainTableContainer');
const importPreviewTable = document.getElementById('importPreviewTable');
function updateTopTableScrollbar() {
if (!topTableScrollbar || !topTableScrollbarInner || !mainTableContainer || !importPreviewTable) return;
topTableScrollbarInner.style.width = importPreviewTable.scrollWidth + 'px';
if (mainTableContainer.scrollWidth > mainTableContainer.clientWidth) {
topTableScrollbar.style.display = 'block';
} else {
topTableScrollbar.style.display = 'none';
}
}
let syncingTop = false;
let syncingBottom = false;
if (topTableScrollbar && mainTableContainer) {
topTableScrollbar.addEventListener('scroll', function() {
if (syncingBottom) return;
syncingTop = true;
mainTableContainer.scrollLeft = topTableScrollbar.scrollLeft;
syncingTop = false;
});
mainTableContainer.addEventListener('scroll', function() {
if (syncingTop) return;
syncingBottom = true;
topTableScrollbar.scrollLeft = mainTableContainer.scrollLeft;
syncingBottom = false;
});
}
updateTopTableScrollbar();
setTimeout(updateTopTableScrollbar, 100);
setTimeout(updateTopTableScrollbar, 300);
window.addEventListener('resize', updateTopTableScrollbar);
const proceedButtonTop = document.getElementById('proceedButtonTop');
const proceedButtonBottom = document.getElementById('proceedButtonBottom');
const selectAllCheckbox = document.getElementById('selectAll');
File diff suppressed because it is too large Load Diff
+3
View File
@@ -10,6 +10,9 @@ error_reporting(E_ALL | E_STRICT);
include('../../extra/auth.php');
//require_once __DIR__ . '/extra/auth.php';
// Laravel bootstrap (loaded by auth.php) forces UTC via config/app.php — re-apply our TZ
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
// Here we just check if user is not
// logged in, and in that case we redirect
// the user to vanguard login page.
+11 -3
View File
@@ -22,7 +22,7 @@
<ul>
<!-- <li> <a href="index.php"><i class='bx bx-radio-circle'></i>Default</a>
</li> -->
<li> <a href="import_dashboard.php"><i class='bx bx-radio-circle'></i>XLS Import</a>
<li> <a href="import_dashboard.php"><i class='bx bx-radio-circle'></i>Import AREA</a>
</li>
@@ -51,14 +51,22 @@
<ul>
<li> <a href="quotations.php"><i class='bx bx-radio-circle'></i><?php echo $quotationstitle; ?></a>
</li>
<li> <a href="bindings_manage.php"><i class='bx bx-radio-circle'></i>Binding JSON -> LIMS</a>
</li>
</ul>
</li>
<li class="menu-label">Reports</li>
<li>
<a href="rapporti_cliente_lookup.php" target="">
<div class="parent-icon"><i class="bx bx-file-find"></i>
</div>
<div class="menu-title">Ricerca Reports</div>
</a>
</li>
<li class="menu-label">Others</li>
+216 -15
View File
@@ -1,11 +1,22 @@
<?php include('include/headscript.php');
// Recupera tutte le routine dal database
// Retrieve all routines from database
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("SELECT * FROM routine");
$stmt->execute();
$routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Retrieve active API/JSON configurations
$stmt = $pdo->prepare("
SELECT id, name, provider_code, api_type, php_class_name
FROM api_configurations
WHERE is_active = 1
ORDER BY name ASC
");
$stmt->execute();
$apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="en">
@@ -18,26 +29,30 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<?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://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>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Insert new XLS Template</h5>
<h5 class="mb-0">Insert New Template</h5>
</div>
<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>
<ul class="mb-0">
<li>Template Name</li>
<li>Row Header and Column Header: where the title of the excel starts</li>
<li>Schema and client</li>
<li>Source Type</li>
<li>Schema and Client</li>
<li>Row Header, Column Header and Sheet Number only for XLS templates</li>
<li>API / JSON Configuration only for API / JSON templates</li>
</ul>
</div>
</div>
@@ -50,22 +65,86 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
</div>
</div>
</div>
<div class="card-body">
<div class="col-12">
<form id="insertTemplateForm" method="POST">
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($templatename, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="text" name="name" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($rowheader, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="number" name="header_row" class="form-control" required>
<label class="form-label">Source Type *</label>
<select name="source_type" id="sourceType" class="form-control" required>
<option value="XLS" selected>XLS</option>
<option value="API">API / JSON</option>
<option value="PDF">PDF</option>
</select>
<small class="text-muted">Choose the source used by this template</small>
</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>
<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 class="mb-3" id="xlsSheetNumberWrapper">
<label class="form-label">XLS Sheet Number</label>
<input
type="number"
name="xls_sheet_index"
id="xlsSheetIndex"
class="form-control"
min="0"
value="0">
<small class="text-muted">
Use 0 for the first sheet, 1 for the second sheet, 2 for the third sheet, and so on.
</small>
</div>
<div class="mb-3" id="apiConfigWrapper" style="display: none;">
<label class="form-label">API / JSON Configuration *</label>
<select name="api_config_id" id="apiConfigSelect" class="form-control">
<option value="">Select an API configuration...</option>
<?php foreach ($apiConfigurations as $apiConfig): ?>
<?php
$apiLabelParts = [];
if (!empty($apiConfig['name'])) {
$apiLabelParts[] = $apiConfig['name'];
}
if (!empty($apiConfig['provider_code'])) {
$apiLabelParts[] = '[' . $apiConfig['provider_code'] . ']';
}
if (!empty($apiConfig['api_type'])) {
$apiLabelParts[] = '(' . $apiConfig['api_type'] . ')';
}
if (!empty($apiConfig['php_class_name'])) {
$apiLabelParts[] = '- ' . $apiConfig['php_class_name'];
}
$apiLabel = implode(' ', $apiLabelParts);
?>
<option value="<?php echo (int)$apiConfig['id']; ?>">
<?php echo htmlspecialchars($apiLabel, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
<small class="text-muted">
Select the API/JSON configuration linked to this template.
</small>
</div>
<div class="mb-3">
@@ -86,12 +165,12 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<div class="mb-3">
<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 class="mb-3">
<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 class="mb-3">
@@ -125,6 +204,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
</option>
<?php endforeach; ?>
</select>
<div id="routineDetails" class="mt-2" style="display: none;">
<h6>Routine Details</h6>
<p><strong>Name:</strong> <span id="routineName"></span></p>
@@ -142,8 +222,10 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
</div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
@@ -167,6 +249,18 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const routineAction2 = document.getElementById("routineAction2");
const routineAction3 = document.getElementById("routineAction3");
const sourceType = document.getElementById("sourceType");
const headerRowWrapper = document.getElementById("headerRowWrapper");
const startColumnWrapper = document.getElementById("startColumnWrapper");
const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper");
const apiConfigWrapper = document.getElementById("apiConfigWrapper");
const headerRow = document.getElementById("headerRow");
const startColumn = document.getElementById("startColumn");
const xlsSheetIndex = document.getElementById("xlsSheetIndex");
const apiConfigSelect = document.getElementById("apiConfigSelect");
if (!form || !clientLoadingStatus || !schemaLoadingStatus || !routineSelect || !routineDetails) {
alert("Errore: Uno o più elementi della pagina non sono stati trovati. Contatta l'amministratore.");
return;
@@ -187,30 +281,101 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
allowClear: true
});
$('#apiConfigSelect').select2({
placeholder: "Select an API configuration...",
allowClear: true
});
function updateSourceFields() {
const selectedSource = sourceType.value;
const isXls = selectedSource === 'XLS';
const isApiJson = selectedSource === 'API';
if (isXls) {
headerRowWrapper.style.display = 'block';
startColumnWrapper.style.display = 'block';
xlsSheetNumberWrapper.style.display = 'block';
headerRow.required = true;
startColumn.required = true;
xlsSheetIndex.required = true;
headerRow.disabled = false;
startColumn.disabled = false;
xlsSheetIndex.disabled = false;
apiConfigWrapper.style.display = 'none';
apiConfigSelect.required = false;
apiConfigSelect.disabled = true;
$('#apiConfigSelect').val(null).trigger('change');
} else {
headerRowWrapper.style.display = 'none';
startColumnWrapper.style.display = 'none';
xlsSheetNumberWrapper.style.display = 'none';
headerRow.required = false;
startColumn.required = false;
xlsSheetIndex.required = false;
headerRow.disabled = true;
startColumn.disabled = true;
xlsSheetIndex.disabled = true;
if (isApiJson) {
apiConfigWrapper.style.display = 'block';
apiConfigSelect.required = true;
apiConfigSelect.disabled = false;
} else {
apiConfigWrapper.style.display = 'none';
apiConfigSelect.required = false;
apiConfigSelect.disabled = true;
$('#apiConfigSelect').val(null).trigger('change');
}
}
}
sourceType.addEventListener('change', updateSourceFields);
updateSourceFields();
async function loadClients() {
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) 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");
select.innerHTML = '<option value="">Select a client...</option>';
data.value.forEach(client => {
const nome = client.Nominativo || "Nome non disponibile";
const id = client.IdCliente || "ID non disponibile";
const option = new Option(`${nome.trim()} (ID: ${id})`, id);
const codiceCliente = (client.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);
select.add(option);
});
$(select).trigger('change');
clientLoadingStatus.textContent = "Clienti caricati.";
} catch (error) {
clientLoadingStatus.textContent = "Errore nel caricamento.";
Swal.fire({
title: "Errore!",
text: "Impossibile caricare i clienti: " + error.message,
@@ -226,16 +391,23 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
try {
schemaLoadingStatus.style.display = 'inline';
schemaLoadingStatus.textContent = 'Caricamento schemi in corso...';
const response = await fetch("get_schemi.php", {
method: "GET",
headers: {
"Content-Type": "application/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");
select.innerHTML = '<option value="">Select a schema...</option>';
const sortedSchemas = [...data.value].sort((a, b) => {
const nomeA = (a.Nome || "").trim().toLowerCase();
const nomeB = (b.Nome || "").trim().toLowerCase();
@@ -250,10 +422,12 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const option = new Option(`${nome.trim()} (ID: ${id})`, id);
select.add(option);
});
$(select).trigger('change');
schemaLoadingStatus.textContent = "Schemi caricati.";
} catch (error) {
schemaLoadingStatus.textContent = "Errore nel caricamento.";
Swal.fire({
title: "Errore!",
text: "Impossibile caricare gli schemi: " + error.message,
@@ -278,6 +452,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
});
}
}
loadData();
const routines = <?php echo json_encode($routines); ?>;
@@ -285,8 +460,10 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
function updateRoutineDetails() {
const selectedId = routineSelect.value;
routineDetails.style.display = selectedId ? 'block' : 'none';
if (selectedId) {
const routine = routines.find(r => r.idroutine == selectedId);
if (routine) {
routineName.textContent = routine.name || 'N/A';
routineDescription.textContent = routine.description || 'N/A';
@@ -308,6 +485,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
routineAction3.textContent = '';
}
}
routineSelect.addEventListener('change', updateRoutineDetails);
updateRoutineDetails();
@@ -316,6 +494,28 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
let formData = new FormData(this);
const selectedSource = sourceType.value;
if (selectedSource === 'XLS' && xlsSheetIndex.value === '') {
Swal.fire({
title: "Errore!",
text: "Inserisci il numero del foglio XLS.",
icon: "error",
confirmButtonText: "OK"
});
return;
}
if (selectedSource === 'API' && !apiConfigSelect.value) {
Swal.fire({
title: "Errore!",
text: "Seleziona una configurazione API / JSON.",
icon: "error",
confirmButtonText: "OK"
});
return;
}
const clientSelect = document.getElementById("clientSelect");
const clientId = clientSelect.value;
const selectedClientOption = clientSelect.options[clientSelect.selectedIndex];
@@ -358,6 +558,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const nameMatch = optionText.match(/^(.+?)(?:\s*\(ID:\s*\d+\))?$/);
schemaName = nameMatch ? nameMatch[1].trim() : optionText;
}
formData.append("idschema", schemaId);
formData.append("schemaname", schemaName);
+4 -1
View File
@@ -13,7 +13,10 @@ try {
}
// Recupera solo i template attivi
$stmt = $pdo->query("SELECT id, button_label, button_bg_color, button_text_color, button_size FROM excel_templates WHERE status = 'active'");
$stmt = $pdo->query("SELECT id, button_label, button_size, button_bg_color, button_text_color, source_type
FROM excel_templates
WHERE status = 'active'
ORDER BY button_label ASC");
$templates = $stmt->fetchAll(PDO::FETCH_ASSOC);
$response["success"] = true;
+5 -5
View File
@@ -33,9 +33,9 @@ try {
if ($extraFieldId) {
$stmt = $pdo->prepare("
SELECT
p.id, p.iddatadb, p.part_number, p.part_description, p.idmatrice, p.note, p.dateexpiry,
cf.value_id AS extra_value_id,
cf.value_text AS extra_value_text
p.id, p.iddatadb, p.part_number, p.part_description, p.mix, p.idmatrice, p.note, p.dateexpiry,
cf.value_id AS extra_value_id,
cf.value_text AS extra_value_text
FROM identification_parts p
LEFT JOIN identification_parts_customfields cf
ON cf.part_id = p.id AND cf.field_id = :extraFieldId
@@ -48,8 +48,8 @@ try {
]);
} else {
$stmt = $pdo->prepare("
SELECT id, iddatadb, part_number, part_description, idmatrice, note, dateexpiry,
NULL AS extra_value_id, NULL AS extra_value_text
SELECT id, iddatadb, part_number, part_description, mix, idmatrice, note, dateexpiry,
NULL AS extra_value_id, NULL AS extra_value_text
FROM identification_parts
WHERE iddatadb = :iddatadb
ORDER BY part_number ASC
File diff suppressed because it is too large Load Diff
+423
View File
@@ -0,0 +1,423 @@
<?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">
<input type="hidden" id="analysisWebOnly" value="1">
<div class="small text-success fw-semibold">
Showing WEB analyses only
</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>
+10
View File
@@ -46,6 +46,16 @@
</div>
</div>
<div class="modal-footer">
<div style="display: flex; align-items: center; gap: 6px; margin-right: auto;">
<label for="descriptionColorPickerAnnotations" style="font-size: 0.8rem; margin: 0; white-space: nowrap;">
Colore annotazioni:
</label>
<input
type="color"
id="descriptionColorPickerAnnotations"
value="#000000"
style="width: 34px; height: 28px; padding: 1px; border: 1px solid #ccc; border-radius: 4px; cursor: pointer;">
</div>
<button type="button" class="btn btn-primary btn-sm" id="addDescriptionsBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Aggiungi Lista Descrizioni</button>
<button type="button" class="btn btn-danger btn-sm" id="removeAnnotationsBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Rimuovi Descrizioni</button>
<button type="button" class="btn btn-warning btn-sm" id="undoMarkerBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Undo Marker</button>
+307 -65
View File
@@ -2,20 +2,53 @@
<div class="modal-dialog modal-xl" style="max-width: 95vw !important;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="partsModalLabel">Parti per TRF: <span id="trfHeader"></span></h5>
<h5 class="modal-title" id="partsModalLabel">
Parti per TRF:
<span id="trfHeader"></span>
</h5>
<div class="ms-auto me-3 d-flex align-items-center gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm" id="prevPartsRecordBtn" title="Record precedente">
<i class="fas fa-chevron-left"></i>
</button>
<span id="partsRecordCounter" class="text-muted" style="font-size: 12px; min-width: 70px; text-align: center;">
-
</span>
<button type="button" class="btn btn-outline-secondary btn-sm" id="nextPartsRecordBtn" title="Record successivo">
<i class="fas fa-chevron-right"></i>
</button>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row parts-row">
<div class="col-md-9">
<!-- Prima riga: Elenco Parti, Rinumera, Voce -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h6 style="margin: 0;">Elenco Parti</h6>
<div style="display: flex; align-items: center;">
<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 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>
<button type="button" class="btn btn-primary btn-sm ms-2" id="showHideImageBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; min-width: 0;">
<h6 style="margin: 0; white-space: nowrap;">Elenco Parti</h6>
<div style="display: flex; align-items: center; min-width: 0; gap: 8px;">
<button type="button"
class="btn btn-dark btn-sm open-analysis-modal-btn"
id="openAnalysisModalBtn"
style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
<i class="fas fa-flask"></i> Analysis
</button>
<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-primary btn-sm" id="clonePartsBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
<i class="fas fa-clone"></i> Clona 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 type="button" class="btn btn-outline-primary btn-sm ms-2" id="showHideImageBtn" 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-image ms-1" style="font-size: 0.8rem;"></i>
</button>
@@ -23,53 +56,62 @@
</div>
<!-- Seconda riga: +, M, MacroMatrice, Matrice globale, Propaga -->
<div style="display: flex; align-items: center; margin-bottom: 10px;">
<button type="button" class="btn btn-success btn-sm add-row-global" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; margin-right: 5px;"><i class="fas fa-plus fa-xs"></i></button>
<button type="button" class="btn btn-success btn-sm add-row-global" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; margin-right: 5px;"><i class="fas fa-plus"></i></button>
<button type="button" class="btn btn-primary btn-sm add-mix-global" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; margin-right: 10px;">M</button>
<select id="macro-matrice-filter" class="form-control form-control-sm ms-2" style="width: 200px !important; min-width: 200px !important; margin-right: 10px;">
<option value="">Tutte le MacroMatrici</option>
</select>
<select id="global-matrice" class="form-control form-control-sm" style="width: 350px !important; margin-right: 10px;"></select>
<button type="button" class="btn btn-primary btn-sm propagate-all-btn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;"><i class="fas fa-arrow-right fa-xs"></i> Propaga a tutte</button>
<button type="button" class="btn btn-primary btn-sm propagate-all-btn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;"><i class="fas fa-arrow-right"></i> Propaga a tutte</button>
</div>
<table class="table table-striped table-sm" id="partsTable">
<thead>
<tr>
<th style="width: 55px;">Num</th>
<th>Descrizione</th>
<th style="width: 200px;">Matrice</th>
<th style="width: 150px;">
<input type="date" class="form-control form-control-sm propagate-date-input" style="width: 130px; margin-left: 5px; display: inline-block;" title="Propaga data a tutte le parti">
</th>
<th style="width: 200px;">
<button type="button" class="btn btn-light btn-sm propagate-note-btn" style="padding: 0.2rem 0.4rem; font-size: 0.9rem; margin-left: 5px;" title="Propaga nota a tutte le parti">
<i class="fas fa-sticky-note"></i>
</button>
Azioni
<div class="parts-table-scroll">
<table class="table table-striped table-sm" id="partsTable">
<colgroup id="partsTableColgroup">
<col class="parts-col-num">
<col class="parts-col-description">
<col class="parts-col-matrice">
<col class="parts-col-date">
<col class="parts-col-actions">
</colgroup>
<thead>
<tr>
<th style="width: 55px;">Num</th>
<th>Descrizione</th>
<th style="width: 200px;">Matrice</th>
<th style="width: 150px;">
<input type="date" class="form-control form-control-sm propagate-date-input" style="width: 130px; margin-left: 5px; display: inline-block;" title="Propaga data a tutte le parti">
</th>
<th style="width: 200px;">
<button type="button" class="btn btn-light btn-sm propagate-note-btn" style="padding: 0.2rem 0.4rem; font-size: 0.9rem; margin-left: 5px;" title="Propaga nota a tutte le parti">
<i class="fas fa-sticky-note"></i>
</button>
Azioni
</th>
</tr>
</thead>
<tbody id="partsTableBody">
<tr data-part-id="new">
<td><input type="number" class="form-control form-control-sm part-number" value="1" style="width: 55px;"></td>
<td><input type="text" class="form-control form-control-sm part-description" placeholder="Inserisci descrizione"></td>
<td>
<div style="display: flex; align-items: center;">
<button type="button" class="btn btn-primary btn-sm propagate-matrice-btn" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; margin-right: 3px;"><i class="fas fa-arrow-right fa-xs"></i></button>
<select class="part-matrice form-control form-control-sm" style="width: 150px;"></select>
</div>
</td>
<td><input type="date" class="form-control form-control-sm part-dateexpiry" style="width: 130px;"></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-warning btn-sm add-mix-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;">M+</button>
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; display: none;"><i class="fas fa-trash fa-xs"></i></button>
<span class="save-status text-success" style="display: none; margin-left: 5px;"><i class="fas fa-check fa-xs"></i></span>
<span class="save-loading text-warning" style="display: none; margin-left: 5px;"><i class="fas fa-spinner fa-spin fa-xs"></i></span>
</td>
</tr>
</tbody>
</table>
</th>
</tr>
</thead>
<tbody id="partsTableBody">
<tr data-part-id="new">
<td><input type="number" class="form-control form-control-sm part-number" value="1" style="width: 55px;"></td>
<td><input type="text" class="form-control form-control-sm part-description" placeholder="Inserisci descrizione"></td>
<td>
<div style="display: flex; align-items: center;">
<button type="button" class="btn btn-primary btn-sm propagate-matrice-btn" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; margin-right: 3px;"><i class="fas fa-arrow-right"></i></button>
<select class="part-matrice form-control form-control-sm" style="width: 150px;"></select>
</div>
</td>
<td><input type="date" class="form-control form-control-sm part-dateexpiry" style="width: 130px;"></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-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"></i></button>
<span class="save-status text-success" style="display: none; margin-left: 5px;"><i class="fas fa-check"></i></span>
<span class="save-loading text-warning" style="display: none; margin-left: 5px;"><i class="fas fa-spinner fa-spin"></i></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-3">
<h6>Foto del Campione</h6>
@@ -188,6 +230,14 @@
z-index: 1055 !important
}
/* Fix: app.css sets .btn i { margin-top:-1em; margin-bottom:-1em; font-size:1.3rem } which collapses small icon buttons */
#partsModal .btn i {
margin-top: 0 !important;
margin-bottom: 0 !important;
font-size: 0.8rem !important;
margin-right: 2px !important;
}
/* Tabelle */
#partsTable tr {
display: table-row !important
@@ -269,28 +319,20 @@
white-space: nowrap;
}
/* Select delle righe (colonna Matrice) = 150px */
/* Select delle righe Matrice: si adatta alla colonna */
.part-matrice {
width: 300px !important;
min-width: 300px !important;
max-width: 300px !important;
flex: 0 0 300px !important;
width: 100% !important;
min-width: 0 !important;
max-width: 100% !important;
}
.part-matrice.select2-hidden-accessible+.select2 {
width: 300px !important;
min-width: 300px !important;
max-width: 300px !important;
flex: 0 0 300px !important;
width: 100% !important;
min-width: 0 !important;
max-width: 100% !important;
}
/* Colonna Descrizione (2ª colonna) = 420px */
#partsTable th:nth-child(2),
#partsTable td:nth-child(2) {
width: 300px !important;
min-width: 300px !important;
max-width: 300px !important;
}
#partsTable .part-number {
text-align: center;
@@ -321,7 +363,11 @@
border: 1px solid #aaa !important;
border-radius: 4px !important;
background: #fff !important;
max-height: 200px !important;
overflow: visible !important;
}
.select2-container--open .select2-results__options {
max-height: 220px !important;
overflow-y: auto !important;
}
@@ -495,6 +541,202 @@
}
}
/* Resizable parts table - stable */
.parts-table-scroll {
width: 100%;
overflow-x: auto;
overflow-y: visible;
contain: inline-size;
}
#partsTable {
table-layout: fixed !important;
width: max-content !important;
min-width: unset !important;
}
#partsTable col.parts-col-num {
width: 55px;
}
#partsTable col.parts-col-description {
width: 320px;
}
#partsTable col.parts-col-matrice {
width: 360px;
}
#partsTable col.parts-col-date {
width: 150px;
}
#partsTable col.parts-col-actions {
width: 230px;
}
#partsTable th,
#partsTable td {
position: relative;
box-sizing: border-box;
}
#partsTable th {
user-select: none;
}
#partsTable th .parts-resizer {
position: absolute;
top: 0;
right: 0;
width: 9px;
height: 100%;
cursor: col-resize;
z-index: 50;
background: rgba(108, 117, 125, 0.12);
border-right: 1px solid rgba(108, 117, 125, 0.45);
}
#partsTable th .parts-resizer:hover {
background: rgba(108, 117, 125, 0.25);
border-right: 2px solid rgba(73, 80, 87, 0.7);
}
#partsTable td {
overflow: hidden;
text-overflow: ellipsis;
}
#partsTable td input,
#partsTable td select,
#partsTable td .select2-container {
max-width: 100% !important;
}
/* Propagation control for dynamic extra field column */
#partsTable th.extra-field-th {
vertical-align: middle;
}
#partsTable th.extra-field-th .extra-propagate-wrapper {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
}
#partsTable th.extra-field-th .extra-propagate-label {
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 95px;
}
#partsTable th.extra-field-th .extra-propagate-input,
#partsTable th.extra-field-th .extra-propagate-select {
height: 24px !important;
font-size: 0.75rem !important;
padding: 0.1rem 0.25rem !important;
min-width: 90px;
flex: 1 1 auto;
}
#partsTable th.extra-field-th .extra-propagate-btn {
height: 24px;
min-width: 26px;
padding: 0.1rem 0.3rem !important;
font-size: 0.75rem !important;
flex: 0 0 auto;
}
#partsTable th.extra-field-th .select2-container {
flex: 1 1 auto;
min-width: 90px;
max-width: 100% !important;
}
#partsTable th.extra-field-th .select2-selection--single {
height: 24px !important;
}
#partsTable th.extra-field-th .select2-selection__rendered {
line-height: 22px !important;
font-size: 0.75rem !important;
padding-left: 4px !important;
}
#partsTable th.extra-field-th .select2-selection__arrow {
height: 22px !important;
}
/* Extra field propagation header - full column width */
#partsTable th.extra-field-th {
width: auto !important;
min-width: 220px !important;
max-width: none !important;
padding: 0.25rem !important;
}
#partsTable .extra-propagate-wrapper {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
display: flex;
flex-direction: column;
gap: 4px;
box-sizing: border-box;
}
#partsTable .extra-propagate-label {
width: 100% !important;
max-width: 100% !important;
display: block;
font-size: 0.82rem;
font-weight: 700;
line-height: 1.15;
color: #222;
white-space: nowrap !important;
overflow: visible !important;
text-overflow: unset !important;
text-align: center;
}
#partsTable .extra-propagate-control {
width: 100% !important;
display: flex;
align-items: center;
gap: 5px;
box-sizing: border-box;
}
#partsTable .extra-propagate-control .extra-propagate-input,
#partsTable .extra-propagate-control .extra-propagate-select {
flex: 1 1 auto !important;
width: 100% !important;
min-width: 0 !important;
max-width: none !important;
}
#partsTable .extra-propagate-control .select2-container {
flex: 1 1 auto !important;
width: 100% !important;
min-width: 0 !important;
max-width: none !important;
}
#partsTable .extra-propagate-control .select2-selection {
width: 100% !important;
}
#partsTable .extra-propagate-btn {
flex: 0 0 30px !important;
width: 30px !important;
height: 24px !important;
min-width: 30px !important;
padding: 0.1rem 0.25rem !important;
}
/* rosso */
</style>
+264
View File
@@ -0,0 +1,264 @@
/**
* 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 || "");
const visibleIddatadbList = Array.isArray(window.gridData)
? window.gridData
.map((r) => parseInt(r.iddatadb, 10))
.filter((v) => !isNaN(v) && v > 0)
: [];
$("#partsModal")
.data("iddatadb", iddatadb)
.data("idquotations", idquotations)
.data("visible-iddatadb-list", visibleIddatadbList);
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,
});
}
if (row) {
row.tested_component = raw;
row._dirty = true;
}
alert(`Added ${uniqueParts.length} part(s).`);
$input.val(raw);
} 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 (matrici.length === 0) {
$.ajax({
url: "get_matrici_db.php",
url: "get_matrici.php",
method: "GET",
dataType: "json",
success: function (data) {
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -1,5 +1,5 @@
document.addEventListener("DOMContentLoaded", function () {
// Funzione per caricare il contenuto del popup
// Funzione per caricare il contenuto del popup (exported for external use)
async function loadPopupContent(iddatadb, idquotations) {
const popupContent = document.getElementById("popupContent");
if (!popupContent) {
@@ -1218,4 +1218,7 @@ document.addEventListener("DOMContentLoaded", function () {
closeBtn,
});
}
// Export for external use (gridData pages)
window.loadPopupContent = loadPopupContent;
});
+36
View File
@@ -0,0 +1,36 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '0');
error_reporting(E_ALL);
set_time_limit(20);
try {
$api = VisualLimsApiClient::getInstance();
$start = microtime(true);
// Chiamata minima: 1 solo record, 1 solo campo
$data = $api->get('Rapporto', [
'$top' => 1,
'$select' => 'IdRapporto'
]);
$elapsed = round(microtime(true) - $start, 3);
echo json_encode([
'success' => true,
'elapsed_seconds' => $elapsed,
'data' => $data
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+111 -31
View File
@@ -9,48 +9,130 @@ try {
throw new Exception("Invalid request method.");
}
// Recupera e sanifica i dati
$id = intval($_POST['id']);
$name = trim($_POST['name']);
$header_row = intval($_POST['header_row']);
$start_column = trim($_POST['start_column']);
// Retrieve and sanitize form data
$id = intval($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$source_type = strtoupper(trim($_POST['source_type'] ?? 'XLS'));
$header_row = isset($_POST['header_row']) && $_POST['header_row'] !== ''
? intval($_POST['header_row'])
: null;
$start_column = trim($_POST['start_column'] ?? '');
$xls_sheet_index = isset($_POST['xls_sheet_index']) && $_POST['xls_sheet_index'] !== ''
? intval($_POST['xls_sheet_index'])
: 0;
$api_config_id = isset($_POST['api_config_id']) && $_POST['api_config_id'] !== ''
? intval($_POST['api_config_id'])
: null;
$description = trim($_POST['description'] ?? '');
$target_table = trim($_POST['target_table']);
$idclient = intval($_POST['client_id'] ?? 0); // Usa client_id dal form
$clientname = trim($_POST['client_name'] ?? ''); // Usa client_name dal form
$idschema = intval($_POST['idschema'] ?? 0); // Nuovo campo
$schemaname = trim($_POST['schemaname'] ?? ''); // Corretto da schemamaname
$idroutine = isset($_POST['idroutine']) && $_POST['idroutine'] !== '' ? intval($_POST['idroutine']) : null; // Aggiunto idroutine
$button_size = trim($_POST['button_size'] ?? 'medium'); // Nuovo campo
$button_bg_color = trim($_POST['button_bg_color'] ?? '#007bff'); // Nuovo campo
$button_text_color = trim($_POST['button_text_color'] ?? '#ffffff'); // Nuovo campo
$button_label = trim($_POST['button_label'] ?? 'Click Me'); // Nuovo campo
$target_table = trim($_POST['target_table'] ?? 'datadb');
$idclient = intval($_POST['client_id'] ?? 0);
$clientname = trim($_POST['client_name'] ?? '');
$idschema = intval($_POST['idschema'] ?? 0);
$schemaname = trim($_POST['schemaname'] ?? '');
$idroutine = isset($_POST['idroutine']) && $_POST['idroutine'] !== '' ? intval($_POST['idroutine']) : null;
$button_size = trim($_POST['button_size'] ?? 'medium');
$button_bg_color = trim($_POST['button_bg_color'] ?? '#007bff');
$button_text_color = trim($_POST['button_text_color'] ?? '#ffffff');
$button_label = trim($_POST['button_label'] ?? 'Click Me');
// Controllo sui campi obbligatori
if (empty($id) || empty($name) || empty($header_row) || empty($start_column) || empty($target_table) || $idschema <= 0) {
throw new Exception("All fields marked with * are required, including schema.");
// Allowed source types
if (!in_array($source_type, ['XLS', 'API', 'JSON', 'PDF'], true)) {
$source_type = 'XLS';
}
// Validazione del idclient
if ($idclient <= 0) {
throw new Exception("Please select a valid client.");
// Required fields validation
if ($id <= 0 || $name === '' || $target_table === '' || $idclient <= 0 || $idschema <= 0) {
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.");
}
if ($xls_sheet_index < 0) {
throw new Exception("XLS Sheet Number cannot be negative.");
}
$api_config_id = null;
}
// API/JSON validation
if ($source_type === 'API' || $source_type === 'JSON') {
if (empty($api_config_id)) {
throw new Exception("API/JSON configuration is required for API or JSON templates.");
}
$header_row = null;
$start_column = null;
$xls_sheet_index = null;
}
// PDF currently does not require XLS coordinates or API configuration
if ($source_type === 'PDF') {
$header_row = null;
$start_column = null;
$xls_sheet_index = null;
$api_config_id = null;
}
// Database connection
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Aggiorna il database, includendo i nuovi campi
$stmt = $pdo->prepare("UPDATE excel_templates
SET name = ?, 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()
WHERE id = ?");
// Optional check: verify API configuration exists and is active
if ($api_config_id !== null) {
$stmt = $pdo->prepare("
SELECT COUNT(*)
FROM api_configurations
WHERE id = ?
AND is_active = 1
");
$stmt->execute([$api_config_id]);
if ((int)$stmt->fetchColumn() === 0) {
throw new Exception("Selected API/JSON configuration does not exist or is not active.");
}
}
// Update template
$stmt = $pdo->prepare("
UPDATE excel_templates
SET
name = ?,
source_type = ?,
header_row = ?,
start_column = ?,
xls_sheet_index = ?,
api_config_id = ?,
description = ?,
target_table = ?,
idclient = ?,
clientname = ?,
schemaname = ?,
idschema = ?,
idroutine = ?,
button_size = ?,
button_bg_color = ?,
button_text_color = ?,
button_label = ?,
updated_at = NOW()
WHERE id = ?
");
$stmt->execute([
$name,
$source_type,
$header_row,
$start_column,
$xls_sheet_index,
$api_config_id,
$description,
$target_table,
$idclient,
@@ -65,12 +147,10 @@ try {
$id
]);
// rowCount potrebbe essere 0 se non ci sono modifiche, quindi consideriamo comunque un successo
$response["success"] = true;
$response["message"] = "Template updated successfully!";
} catch (Exception $e) {
$response["message"] = $e->getMessage();
}
// Restituisce un JSON per il fetch
echo json_encode($response);
+5 -2
View File
@@ -10,6 +10,9 @@ session_start();
// Includi PHPSpreadsheet
require_once '../../vendor/autoload.php';
Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2))->safeLoad();
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
$response = ['error' => '', 'rows' => [], 'columns' => [], 'template_id' => 0, 'filename' => ''];
try {
@@ -70,7 +73,7 @@ try {
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col);
$cell = $worksheet->getCell($columnLetter . $header_row);
$cellValue = $cell ? $cell->getCalculatedValue() : ''; // Usa getCalculatedValue per le formule
$headerRowData[] = htmlspecialchars($cellValue ?: '');
$headerRowData[] = $cellValue ?: '';
}
// Estrai i dati a partire dalla riga successiva
@@ -80,7 +83,7 @@ try {
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col);
$cell = $worksheet->getCell($columnLetter . $row);
$cellValue = $cell ? $cell->getCalculatedValue() : ''; // Usa getCalculatedValue per le formule
$rowData[] = htmlspecialchars($cellValue ?: '');
$rowData[] = $cellValue ?: '';
}
if (!empty(array_filter($rowData))) {
$excelData[] = $rowData;
+255 -33
View File
@@ -11,17 +11,105 @@ session_start();
require_once '../../vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
$response = ['error' => '', 'rows' => [], 'columns' => [], 'template_id' => 0, 'filename' => '', 'apply_routine' => false];
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
$response = [
'error' => '',
'rows' => [],
'columns' => [],
'template_id' => 0,
'filename' => '',
'apply_routine' => false
];
/**
* Converts a column value to a PhpSpreadsheet 1-based column index.
* Accepted values:
* - "A" => 1
* - "B" => 2
* - "AA" => 27
* - "1" => 1
* - 1 => 1
*/
function normalizeColumnIndex($value): int
{
$value = trim((string)$value);
if ($value === '') {
return 1;
}
if (ctype_digit($value)) {
return max(1, (int)$value);
}
$value = strtoupper($value);
if (preg_match('/^[A-Z]+$/', $value)) {
return Coordinate::columnIndexFromString($value);
}
return 1;
}
try {
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['excel_file'])) {
$template_id = isset($_POST['template_id']) ? intval($_POST['template_id']) : 0;
$header_row = isset($_POST['header_row']) ? intval($_POST['header_row']) : 1;
$start_column = isset($_POST['start_column']) ? intval($_POST['start_column']) : 1;
if ($template_id <= 0) {
throw new Exception("Template ID non valido.");
}
// Connessione al database
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
/*
* Recuperiamo i parametri direttamente dal template.
* Così non dipendiamo solo dal form e siamo sicuri di usare i dati salvati.
*/
$stmt = $pdo->prepare("
SELECT
id,
header_row,
start_column,
xls_sheet_index,
idroutine,
idclient
FROM excel_templates
WHERE id = ?
");
$stmt->execute([$template_id]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$template) {
throw new Exception("Template non trovato.");
}
$header_row = isset($template['header_row']) && $template['header_row'] !== null
? (int)$template['header_row']
: 1;
$start_column_raw = $template['start_column'] ?? 'A';
$start_column = normalizeColumnIndex($start_column_raw);
$xlsSheetIndex = isset($template['xls_sheet_index']) && $template['xls_sheet_index'] !== null
? (int)$template['xls_sheet_index']
: 0;
if ($header_row <= 0) {
$header_row = 1;
}
if ($xlsSheetIndex < 0) {
$xlsSheetIndex = 0;
}
// Debug del template_id ricevuto
error_log("Received template_id from POST: " . print_r($_POST['template_id'], true));
error_log("Converted template_id: $template_id");
error_log("Template XLS settings - header_row: $header_row, start_column_raw: $start_column_raw, start_column_index: $start_column, xls_sheet_index: $xlsSheetIndex");
$file = $_FILES['excel_file'];
$fileError = $file['error'];
@@ -38,23 +126,32 @@ try {
$originalFilename = basename($file['name']);
$newFilename = "{$iduserlogin}-{$timestamp}-{$originalFilename}";
$importFolder = __DIR__ . '/imported_trf/';
if (!file_exists($importFolder)) {
mkdir($importFolder, 0777, true);
}
$destination = $importFolder . $newFilename;
// Sposta il file
if (!move_uploaded_file($file['tmp_name'], $destination)) {
throw new Exception("Errore durante lo spostamento del file in $destination");
}
error_log("File spostato con successo in: $destination");
// Connessione al database
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Recupera il mapping da template_mapping
$stmt = $pdo->prepare("SELECT field_id AS excel_column, field_id AS mysql_column, data_type, is_required, default_value, is_manual FROM template_mapping WHERE template_id = ?");
$stmt = $pdo->prepare("
SELECT
field_id AS excel_column,
field_id AS mysql_column,
data_type,
is_required,
default_value,
is_manual
FROM template_mapping
WHERE template_id = ?
");
$stmt->execute([$template_id]);
$mappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -65,55 +162,175 @@ try {
$response['error'] = "Nessun mapping trovato per il template con ID $template_id";
} else {
// Carica il file rinominato con PHPSpreadsheet
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($destination);
$worksheet = $spreadsheet->getActiveSheet();
$spreadsheet = IOFactory::load($destination);
$sheetCount = $spreadsheet->getSheetCount();
$sheetNames = $spreadsheet->getSheetNames();
if ($sheetCount <= 0) {
throw new Exception("Il file XLS non contiene fogli.");
}
if ($xlsSheetIndex >= $sheetCount) {
throw new Exception(
"Il foglio XLS selezionato non esiste. " .
"Sheet Number selezionato: {$xlsSheetIndex}. " .
"Fogli disponibili: " . implode(", ", array_map(
fn($name, $index) => "{$index}={$name}",
$sheetNames,
array_keys($sheetNames)
))
);
}
// Usa il foglio configurato nel template
$worksheet = $spreadsheet->getSheet($xlsSheetIndex);
$selectedSheetName = $worksheet->getTitle();
error_log("Selected XLS sheet - index: {$xlsSheetIndex}, name: {$selectedSheetName}");
$highestRow = $worksheet->getHighestRow();
$highestColumn = $worksheet->getHighestColumn();
$highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn);
$highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
$startRow = max(1, $header_row);
$startColumn = max(1, $start_column);
// Advance startColumn to first non-empty cell in header row, matching JS behavior
for ($sc = $startColumn; $sc <= $highestColumnIndex; $sc++) {
$cl = Coordinate::stringFromColumnIndex($sc);
$cv = trim((string)($worksheet->getCell($cl . $header_row)->getCalculatedValue() ?? ''));
if ($cv !== '') {
$startColumn = $sc;
break;
}
}
// 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, " .
"sheetIndex: $xlsSheetIndex, sheetName: $selectedSheetName, " .
"startRow: $startRow, startColumn: $startColumn, " .
"highestRow: $highestRow, highestColumn: $highestColumn, highestColumnIndex: $highestColumnIndex"
);
// Validazione degli indici
if ($startRow > $highestRow) {
$response['error'] = "La riga di partenza ($startRow) supera il numero totale di righe ($highestRow).";
$response['error'] = "La riga di partenza ($startRow) supera il numero totale di righe ($highestRow) del foglio '$selectedSheetName'.";
} elseif ($startColumn > $highestColumnIndex) {
$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) del foglio '$selectedSheetName'.";
} else {
$excelData = [];
// Estrai la riga degli header
$headerRowData = [];
for ($col = $startColumn; $col <= $highestColumnIndex; $col++) {
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col);
$cell = $worksheet->getCell($columnLetter . $header_row);
$cellValue = $cell ? $cell->getCalculatedValue() : '';
$headerRowData[] = htmlspecialchars($cellValue ?: '');
// Build merge map for header row: physCol -> mergeStartCol
$mergeStartMap = [];
foreach ($worksheet->getMergeCells() as $range) {
[$startCell, $endCell] = explode(':', $range);
$mStartCol = Coordinate::columnIndexFromString(preg_replace('/\d+/', '', $startCell));
$mEndCol = 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 = Coordinate::stringFromColumnIndex($physCol);
$cell = $worksheet->getCell($columnLetter . $header_row);
$cellValue = trim((string)($cell ? $cell->getCalculatedValue() : ''));
$cellValue = preg_replace('/[\r\n\t]+/', ' ', $cellValue);
// Empty headers get __empty_N__ to match mapping page
$headerRowData[] = ($cellValue !== '') ? $cellValue : '__empty_' . $logicalNum . '__';
}
error_log("Logical headers: " . json_encode($headerRowData));
error_log("Logical cols physical indices: " . json_encode($logicalCols));
// Find which logical columns have real headers
$headerFilledIndices = [];
foreach ($headerRowData as $idx => $hVal) {
if (!str_starts_with($hVal, '__empty_')) {
$headerFilledIndices[] = $idx;
}
}
$minFilled = max(1, min(2, count($headerFilledIndices)));
// Extract data rows using logical columns
for ($row = $startRow + 1; $row <= $highestRow; $row++) {
$rowData = [];
for ($col = $startColumn; $col <= $highestColumnIndex; $col++) {
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col);
foreach ($logicalCols as $physCol) {
$columnLetter = Coordinate::stringFromColumnIndex($physCol);
$cell = $worksheet->getCell($columnLetter . $row);
$cellValue = $cell ? $cell->getCalculatedValue() : '';
$rowData[] = htmlspecialchars($cellValue ?: '');
$rowData[] = $cellValue ?: '';
}
if (!empty(array_filter($rowData))) {
$excelData[] = ['data' => $rowData, 'excelrow' => $row];
// Count how many header columns have data in this row
$filledCount = 0;
foreach ($headerFilledIndices as $idx) {
if (isset($rowData[$idx]) && trim((string)$rowData[$idx]) !== '') {
$filledCount++;
}
}
if ($filledCount >= $minFilled) {
$excelData[] = [
'data' => $rowData,
'excelrow' => $row
];
}
}
// Recupera routine dal template
$stmt = $pdo->prepare("SELECT idroutine, idclient FROM excel_templates WHERE id = ?");
$stmt->execute([$template_id]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if ($template && $template['idroutine']) {
$stmtRoutine = $pdo->prepare("SELECT idroutine, name, filename, headerrow, instruction FROM routine WHERE idroutine = ?");
if ($template && !empty($template['idroutine'])) {
$stmtRoutine = $pdo->prepare("
SELECT
idroutine,
name,
filename,
headerrow,
instruction
FROM routine
WHERE idroutine = ?
");
$stmtRoutine->execute([$template['idroutine']]);
$routineData = $stmtRoutine->fetch(PDO::FETCH_ASSOC);
@@ -125,6 +342,7 @@ try {
'filename' => $routineData['filename'] ?? '',
'headerrow' => $routineData['headerrow'] ?? $header_row
];
error_log("Routine rilevata per template {$template_id}: " . print_r($routineData, true));
} else {
error_log("Errore: Nessuna routine trovata per idroutine {$template['idroutine']}");
@@ -141,6 +359,8 @@ try {
$_SESSION['template_id'] = $template_id;
$_SESSION['headers'] = $headerRowData;
$_SESSION['mappings'] = $mappings;
$_SESSION['xls_sheet_index'] = $xlsSheetIndex;
$_SESSION['xls_sheet_name'] = $selectedSheetName;
// Includi excel_data nella risposta JSON in ogni caso
$response['excel_data'] = $excelData;
@@ -148,6 +368,8 @@ try {
$response['columns'] = $headerRowData;
$response['template_id'] = $template_id;
$response['filename'] = $newFilename;
$response['xls_sheet_index'] = $xlsSheetIndex;
$response['xls_sheet_name'] = $selectedSheetName;
}
}
} else {
+113 -14
View File
@@ -9,42 +9,141 @@ try {
throw new Exception("Invalid request method.");
}
// Recupera e sanifica i dati
$name = trim($_POST['name']);
$header_row = intval($_POST['header_row']);
$start_column = trim($_POST['start_column']);
// Retrieve and sanitize form data
$name = trim($_POST['name'] ?? '');
$source_type = strtoupper(trim($_POST['source_type'] ?? 'XLS'));
$header_row = isset($_POST['header_row']) && $_POST['header_row'] !== ''
? intval($_POST['header_row'])
: null;
$start_column = trim($_POST['start_column'] ?? '');
$xls_sheet_index = isset($_POST['xls_sheet_index']) && $_POST['xls_sheet_index'] !== ''
? intval($_POST['xls_sheet_index'])
: 0;
$api_config_id = isset($_POST['api_config_id']) && $_POST['api_config_id'] !== ''
? intval($_POST['api_config_id'])
: null;
$description = trim($_POST['description'] ?? '');
$target_table = trim($_POST['target_table']);
$target_table = trim($_POST['target_table'] ?? 'datadb');
$idclient = intval($_POST['client_id'] ?? 0);
$clientname = trim($_POST['client_name'] ?? '');
$idschema = intval($_POST['idschema'] ?? 0);
$schemaname = trim($_POST['schemaname'] ?? '');
$idroutine = isset($_POST['idroutine']) && $_POST['idroutine'] !== '' ? intval($_POST['idroutine']) : null;
$idroutine = isset($_POST['idroutine']) && $_POST['idroutine'] !== ''
? intval($_POST['idroutine'])
: null;
$button_size = trim($_POST['button_size'] ?? 'medium');
$button_bg_color = trim($_POST['button_bg_color'] ?? '#007bff');
$button_text_color = trim($_POST['button_text_color'] ?? '#ffffff');
$button_label = trim($_POST['button_label'] ?? 'Click Me');
// Controllo sui campi obbligatori
if (empty($name) || empty($header_row) || empty($start_column) || empty($target_table) || $idclient <= 0 || $idschema <= 0) {
// Normalize source type
// API / JSON is saved as API
if (!in_array($source_type, ['XLS', 'API', 'PDF'], 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.");
}
// 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.");
}
if ($xls_sheet_index < 0) {
throw new Exception("XLS Sheet Number cannot be negative.");
}
$api_config_id = null;
}
// API / JSON validation
if ($source_type === 'API') {
if (empty($api_config_id)) {
throw new Exception("API / JSON configuration is required for API / JSON templates.");
}
$header_row = null;
$start_column = null;
$xls_sheet_index = null;
}
// PDF currently does not require XLS coordinates or API configuration
if ($source_type === 'PDF') {
$header_row = null;
$start_column = null;
$xls_sheet_index = null;
$api_config_id = null;
}
// Database connection
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Inserisci il nuovo template
// Optional check: verify API configuration exists and is active
if ($api_config_id !== null) {
$stmt = $pdo->prepare("
SELECT COUNT(*)
FROM api_configurations
WHERE id = ?
AND is_active = 1
");
$stmt->execute([$api_config_id]);
if ((int)$stmt->fetchColumn() === 0) {
throw new Exception("Selected API / JSON configuration does not exist or is not active.");
}
}
// Insert the new template
$stmt = $pdo->prepare("
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)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
INSERT INTO excel_templates
(
name,
source_type,
header_row,
start_column,
xls_sheet_index,
api_config_id,
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([
$name,
$source_type,
$header_row,
$start_column,
$xls_sheet_index,
$api_config_id,
$description,
$target_table,
$idclient,
File diff suppressed because it is too large Load Diff
+700
View File
@@ -0,0 +1,700 @@
<?php include('include/headscript.php'); ?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<link href="assets/plugins/datatable/css/dataTables.bootstrap5.min.css" rel="stylesheet" />
<title>TRF-Project - Customer Reports</title>
<style>
.lookup-wrapper {
width: 100%;
max-width: none;
margin: 0;
}
.lookup-title {
font-size: 18px;
font-weight: 700;
}
.lookup-subtitle {
font-size: 13px;
color: #6c757d;
}
.compact-card .card-body {
padding: 1rem;
}
.table-report th {
font-size: 12px;
text-transform: uppercase;
color: #6c757d;
white-space: nowrap;
}
.table-report td {
font-size: 13px;
vertical-align: middle;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.status-pill-success {
background: #e8fff1;
color: #198754;
}
.status-pill-warning {
background: #fff3cd;
color: #b58100;
}
.pdf-icon-link {
width: 36px;
height: 36px;
border-radius: 10px;
background: #dc3545;
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 20px;
text-decoration: none;
transition: all 0.15s ease-in-out;
}
.pdf-icon-link:hover {
color: #fff;
opacity: 0.85;
transform: translateY(-1px);
}
.no-pdf {
font-size: 12px;
color: #adb5bd;
font-weight: 600;
}
.empty-state {
border: 1px dashed #ced4da;
border-radius: 10px;
padding: 24px;
text-align: center;
color: #6c757d;
background: #fafafa;
}
.spinner-inline {
display: none;
margin-left: 8px;
}
.customer-box {
display: none;
border: 1px solid #e9ecef;
background: #fff;
border-radius: 10px;
padding: 12px 14px;
margin-bottom: 15px;
}
.customer-label {
font-size: 11px;
text-transform: uppercase;
color: #6c757d;
font-weight: 700;
}
.customer-value {
font-size: 14px;
color: #212529;
font-weight: 700;
}
.json-preview {
display: none;
background: #111827;
color: #e5e7eb;
border-radius: 10px;
padding: 14px;
font-size: 12px;
max-height: 420px;
overflow: auto;
white-space: pre-wrap;
}
@media (max-width: 768px) {
.table-responsive {
border-radius: 10px;
}
}
.select2-container {
width: 100% !important;
}
.select2-container--default .select2-selection--single {
height: 38px;
border: 1px solid #ced4da;
border-radius: 6px;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 36px;
font-size: 14px;
color: #212529;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 36px;
}
.select2-dropdown {
z-index: 9999;
}
</style>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<?php include('top_stat_widget.php'); ?>
<div class="lookup-wrapper">
<div class="card radius-10 compact-card">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<div>
<div class="lookup-title">Customer Test Reports</div>
<div class="lookup-subtitle">
Select a VisualLims customer and retrieve the latest reports with PDF links.
</div>
</div>
</div>
</div>
<div class="card-body">
<form id="customerReportsForm" class="row g-3 align-items-end">
<div class="col-md-5">
<label for="idCliente" class="form-label fw-semibold">Customer</label>
<select id="idCliente" name="idCliente" class="form-select">
<option value="">Loading customers...</option>
</select>
</div>
<div class="col-md-2">
<label for="limitReports" class="form-label fw-semibold">Limit</label>
<select id="limitReports" name="limitReports" class="form-select">
<option value="1">Last 1</option>
<option value="3" selected>Last 3</option>
<option value="5">Last 5</option>
<option value="10">Last 10</option>
</select>
</div>
<div class="col-md-2">
<label for="signedStatus" class="form-label fw-semibold">Status</label>
<select id="signedStatus" name="signedStatus" class="form-select">
<option value="all" selected>All</option>
<option value="signed">Signed</option>
<option value="not_signed">Not signed</option>
</select>
</div>
<div class="col-md-3">
<button type="submit" id="btnSearchReports" class="btn btn-primary w-100">
<i class="bx bx-search"></i> Search Reports
<span class="spinner-border spinner-border-sm spinner-inline" id="searchSpinner" role="status" aria-hidden="true"></span>
</button>
</div>
</form>
</div>
</div>
<div id="customerBox" class="customer-box">
<div class="row g-3">
<div class="col-md-4">
<div class="customer-label">Customer Code</div>
<div class="customer-value" id="selectedCustomerCode">-</div>
</div>
<div class="col-md-8">
<div class="customer-label">Customer Name</div>
<div class="customer-value" id="selectedCustomerName">-</div>
</div>
</div>
</div>
<div id="resultContainer" style="display:none;">
<div class="card radius-10 compact-card">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<h6 class="mb-0">
Reports
<span class="badge bg-light text-dark ms-1" id="reportCountBadge">0</span>
</h6>
<button type="button" id="toggleJsonBtn" class="btn btn-sm btn-outline-secondary">
<i class="bx bx-code-alt"></i> Show JSON
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover table-report align-middle mb-0" id="reportsTable">
<thead>
<tr>
<th>Report Number</th>
<th>Report ID</th>
<th>Report Date</th>
<th>Print Date</th>
<th>Version</th>
<th>Status</th>
<th class="text-center">PDF</th>
</tr>
</thead>
<tbody id="reportsTableBody"></tbody>
</table>
</div>
<pre id="jsonPreview" class="json-preview mt-3"></pre>
</div>
</div>
</div>
<div id="emptyState" class="empty-state">
<i class="bx bx-file-find" style="font-size:34px;"></i>
<div class="mt-2 fw-semibold">No reports loaded</div>
<div class="small">Select a customer, choose the limit and click Search Reports.</div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top">
<i class='bx bxs-up-arrow-alt'></i>
</a>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script src="assets/plugins/datatable/js/jquery.dataTables.min.js"></script>
<script src="assets/plugins/datatable/js/dataTables.bootstrap5.min.js"></script>
<script>
$(document).ready(function() {
let lastJsonResponse = null;
let loadedCustomers = [];
let reportsDataTable = null;
function escapeHtml(value) {
if (value === null || value === undefined) {
return '';
}
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function formatDate(value) {
if (!value) {
return '-';
}
const date = new Date(value);
if (isNaN(date.getTime())) {
return value;
}
return date.toLocaleString('it-IT', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
function setLoading(isLoading) {
$('#btnSearchReports').prop('disabled', isLoading);
$('#searchSpinner').toggle(isLoading);
}
function resetResults() {
lastJsonResponse = null;
if (reportsDataTable !== null) {
reportsDataTable.destroy();
reportsDataTable = null;
}
$('#resultContainer').hide();
$('#emptyState').show();
$('#reportsTableBody').html('');
$('#reportCountBadge').text('0');
$('#jsonPreview').hide().text('');
$('#toggleJsonBtn').html('<i class="bx bx-code-alt"></i> Show JSON');
}
function loadCustomers() {
$.ajax({
url: 'get_clienti.php',
method: 'GET',
dataType: 'json',
success: function(response) {
const select = $('#idCliente');
select.empty();
const customers = response.value || response || [];
loadedCustomers = Array.isArray(customers) ? customers : [];
select.append('<option value="">Select customer...</option>');
loadedCustomers.forEach(function(customer) {
const idCliente = customer.IdCliente || '';
const codiceCliente = customer.CodiceCliente || '';
const nominativo = customer.Nominativo || '';
const label = codiceCliente ?
codiceCliente + ' - ' + nominativo :
nominativo;
select.append(
'<option value="' + escapeHtml(idCliente) + '" ' +
'data-code="' + escapeHtml(codiceCliente) + '" ' +
'data-name="' + escapeHtml(nominativo) + '">' +
escapeHtml(label) +
'</option>'
);
});
if ($.fn.select2) {
select.select2({
width: '100%',
placeholder: 'Search customer...',
allowClear: true,
minimumInputLength: 0,
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();
if (text.indexOf(term) > -1) {
return data;
}
return null;
},
sorter: function(data) {
const term = $('.select2-search__field').val();
if (!term) {
return data;
}
const search = term.toLowerCase();
return data.sort(function(a, b) {
const aText = (a.text || '').toLowerCase();
const bText = (b.text || '').toLowerCase();
const aStarts = aText.startsWith(search);
const bStarts = bText.startsWith(search);
if (aStarts && !bStarts) return -1;
if (!aStarts && bStarts) return 1;
return aText.localeCompare(bText);
});
}
});
}
},
error: function(xhr) {
$('#idCliente').html('<option value="">Error loading customers</option>');
let message = 'Unable to load customers.';
if (xhr.responseJSON && xhr.responseJSON.error) {
message = xhr.responseJSON.error;
}
Swal.fire({
title: 'Customer loading error',
text: message,
icon: 'error',
confirmButtonText: 'OK'
});
}
});
}
function updateSelectedCustomerBox() {
const selectedOption = $('#idCliente option:selected');
const customerCode = selectedOption.data('code') || '-';
const customerName = selectedOption.data('name') || '-';
if (!$('#idCliente').val()) {
$('#customerBox').hide();
$('#selectedCustomerCode').text('-');
$('#selectedCustomerName').text('-');
return;
}
$('#selectedCustomerCode').text(customerCode);
$('#selectedCustomerName').text(customerName);
$('#customerBox').show();
}
function renderStatus(isSigned) {
if (isSigned === true) {
return '<span class="status-pill status-pill-success"><i class="bx bx-check-circle"></i> Signed</span>';
}
if (isSigned === false) {
return '<span class="status-pill status-pill-warning"><i class="bx bx-time"></i> Not signed</span>';
}
return '-';
}
function renderPdfCell(pdfFiles) {
if (!Array.isArray(pdfFiles) || pdfFiles.length === 0) {
return '<span class="no-pdf">No PDF</span>';
}
const firstPdf = pdfFiles[0];
if (!firstPdf.download_url) {
return '<span class="no-pdf">No PDF</span>';
}
return `
<a href="${escapeHtml(firstPdf.download_url)}"
target="_blank"
class="pdf-icon-link"
title="${escapeHtml(firstPdf.file_name || 'Download PDF')}">
<i class="bx bxs-file-pdf"></i>
</a>
`;
}
function initReportsDataTable() {
if (reportsDataTable !== null) {
reportsDataTable.destroy();
reportsDataTable = null;
}
reportsDataTable = $('#reportsTable').DataTable({
paging: false,
searching: false,
info: false,
ordering: true,
order: [
[2, 'desc']
],
autoWidth: false,
responsive: true,
columnDefs: [{
targets: 6,
orderable: false
}],
language: {
emptyTable: 'No reports found',
zeroRecords: 'No matching reports found'
}
});
}
function renderReports(response) {
lastJsonResponse = response;
const reports = response.reports || [];
const tbody = $('#reportsTableBody');
tbody.empty();
$('#reportCountBadge').text(reports.length);
if (reports.length > 0) {
reports.forEach(function(report) {
const reportDateOrder = report.data || '';
const printDateOrder = report.data_stampa || '';
const statusOrder = report.firmato === true ? 1 : 0;
const row = `
<tr>
<td>
<strong>${escapeHtml(report.codice_rapporto || '-')}</strong>
</td>
<td>${escapeHtml(report.id_rapporto || '-')}</td>
<td data-order="${escapeHtml(reportDateOrder)}">${escapeHtml(formatDate(report.data))}</td>
<td data-order="${escapeHtml(printDateOrder)}">${escapeHtml(formatDate(report.data_stampa))}</td>
<td>${escapeHtml(report.versione !== null && report.versione !== undefined ? report.versione : '-')}</td>
<td data-order="${statusOrder}">${renderStatus(report.firmato)}</td>
<td class="text-center">${renderPdfCell(report.pdf_files)}</td>
</tr>
`;
tbody.append(row);
});
}
$('#jsonPreview').text(JSON.stringify(response, null, 4));
$('#emptyState').hide();
$('#resultContainer').show();
setTimeout(function() {
initReportsDataTable();
}, 50);
}
$('#idCliente').on('change', function() {
updateSelectedCustomerBox();
resetResults();
});
$('#customerReportsForm').on('submit', function(event) {
event.preventDefault();
const idCliente = $('#idCliente').val();
const limit = $('#limitReports').val();
const signedStatus = $('#signedStatus').val();
if (!idCliente) {
Swal.fire({
title: 'Missing customer',
text: 'Please select a customer.',
icon: 'warning',
confirmButtonText: 'OK'
});
return;
}
resetResults();
updateSelectedCustomerBox();
setLoading(true);
$.ajax({
url: 'get_rapporti_cliente.php',
method: 'GET',
dataType: 'json',
data: {
id_cliente: idCliente,
limit: limit,
signed_status: signedStatus
},
success: function(response) {
if (!response || response.success !== true) {
Swal.fire({
title: 'No data',
text: response && response.error ? response.error : 'No reports were returned.',
icon: 'warning',
confirmButtonText: 'OK'
});
return;
}
renderReports(response);
},
error: function(xhr) {
let message = 'Unexpected error while loading reports.';
if (xhr.responseJSON && xhr.responseJSON.error) {
message = xhr.responseJSON.error;
} else if (xhr.responseText) {
message = xhr.responseText.substring(0, 500);
}
Swal.fire({
title: 'Error',
text: message,
icon: 'error',
confirmButtonText: 'OK'
});
},
complete: function() {
setLoading(false);
}
});
});
$('#toggleJsonBtn').on('click', function() {
const jsonPreview = $('#jsonPreview');
const isVisible = jsonPreview.is(':visible');
jsonPreview.toggle(!isVisible);
if (isVisible) {
$(this).html('<i class="bx bx-code-alt"></i> Show JSON');
} else {
$(this).html('<i class="bx bx-hide"></i> Hide JSON');
}
});
loadCustomers();
});
</script>
</body>
</html>
+580
View File
@@ -0,0 +1,580 @@
<?php include('include/headscript.php'); ?>
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--favicon-->
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>TRF-Project - Test Report Lookup</title>
<style>
.compact-card .card-body {
padding: 1rem;
}
.lookup-wrapper {
max-width: 1100px;
margin: 0 auto;
}
.lookup-title {
font-size: 18px;
font-weight: 700;
}
.lookup-subtitle {
font-size: 13px;
color: #6c757d;
}
.result-section-title {
font-size: 14px;
font-weight: 700;
margin-bottom: 10px;
color: #344767;
}
.info-box {
border: 1px solid #e9ecef;
border-radius: 10px;
padding: 14px;
background: #fff;
height: 100%;
}
.info-label {
font-size: 11px;
text-transform: uppercase;
color: #6c757d;
font-weight: 700;
margin-bottom: 3px;
}
.info-value {
font-size: 14px;
color: #212529;
font-weight: 600;
word-break: break-word;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
}
.status-pill-success {
background: #e8fff1;
color: #198754;
}
.status-pill-warning {
background: #fff3cd;
color: #b58100;
}
.pdf-card {
border: 1px solid #f1d1d1;
background: #fffafa;
border-radius: 10px;
padding: 14px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.pdf-icon {
width: 46px;
height: 46px;
border-radius: 12px;
background: #dc3545;
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 24px;
flex-shrink: 0;
}
.pdf-file-name {
font-weight: 700;
font-size: 14px;
margin-bottom: 2px;
}
.pdf-meta {
font-size: 12px;
color: #6c757d;
}
.json-preview {
display: none;
background: #111827;
color: #e5e7eb;
border-radius: 10px;
padding: 14px;
font-size: 12px;
max-height: 420px;
overflow: auto;
white-space: pre-wrap;
}
.empty-state {
border: 1px dashed #ced4da;
border-radius: 10px;
padding: 24px;
text-align: center;
color: #6c757d;
background: #fafafa;
}
.spinner-inline {
display: none;
margin-left: 8px;
}
.btn-download-pdf {
white-space: nowrap;
}
@media (max-width: 768px) {
.pdf-card {
flex-direction: column;
align-items: flex-start;
}
.btn-download-pdf {
width: 100%;
}
}
</style>
</head>
<body>
<!--wrapper-->
<div class="wrapper">
<!--sidebar wrapper -->
<?php include('include/navbar.php'); ?>
<!--end sidebar wrapper -->
<!--start header -->
<?php include('include/topbar.php'); ?>
<!--end header -->
<!--start page wrapper -->
<div class="page-wrapper">
<div class="page-content">
<?php include('top_stat_widget.php'); ?>
<div class="lookup-wrapper">
<div class="card radius-10 compact-card">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<div>
<div class="lookup-title">Test Report Lookup</div>
<div class="lookup-subtitle">
Search a test report from VisualLims by report number and download the PDF if available.
</div>
</div>
</div>
</div>
<div class="card-body">
<form id="reportSearchForm" class="row g-3 align-items-end">
<div class="col-md-8">
<label for="codiceRapporto" class="form-label fw-semibold">
Report Number
</label>
<input type="text"
class="form-control"
id="codiceRapporto"
name="codiceRapporto"
placeholder="Example: 2621521"
autocomplete="off">
</div>
<div class="col-md-4">
<button type="submit" id="btnSearchReport" class="btn btn-primary w-100">
<i class="bx bx-search"></i> Proceed
<span class="spinner-border spinner-border-sm spinner-inline" id="searchSpinner" role="status" aria-hidden="true"></span>
</button>
</div>
</form>
</div>
</div>
<div id="resultContainer" style="display:none;">
<div class="card radius-10 compact-card">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<h6 class="mb-0">Report Data</h6>
<button type="button" id="toggleJsonBtn" class="btn btn-sm btn-outline-secondary">
<i class="bx bx-code-alt"></i> Show JSON
</button>
</div>
</div>
<div class="card-body">
<div class="result-section-title">General Information</div>
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Report Number</div>
<div class="info-value" id="resCodiceRapporto">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Report ID</div>
<div class="info-value" id="resIdRapporto">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Customer Code</div>
<div class="info-value" id="resCodiceCliente">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Customer Name</div>
<div class="info-value" id="resNominativoCliente">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Report Date</div>
<div class="info-value" id="resDataRapporto">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Print Date</div>
<div class="info-value" id="resDataStampa">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Version</div>
<div class="info-value" id="resVersione">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Signed</div>
<div class="info-value" id="resFirmato">-</div>
</div>
</div>
<div class="col-md-6">
<div class="info-box">
<div class="info-label">Detail Endpoint</div>
<div class="info-value small" id="resDetailEndpoint">-</div>
</div>
</div>
</div>
<div class="result-section-title">Available PDF Files</div>
<div id="pdfFilesContainer" class="mb-4"></div>
<pre id="jsonPreview" class="json-preview"></pre>
</div>
</div>
</div>
<div id="emptyState" class="empty-state">
<i class="bx bx-file-find" style="font-size:34px;"></i>
<div class="mt-2 fw-semibold">No report loaded</div>
<div class="small">Enter a report number and click Proceed.</div>
</div>
</div>
</div>
</div>
<!--end page wrapper -->
<!--start overlay-->
<div class="overlay toggle-icon"></div>
<!--end overlay-->
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top">
<i class='bx bxs-up-arrow-alt'></i>
</a>
<!--End Back To Top Button-->
<?php include('include/footer.php'); ?>
</div>
<!--end wrapper-->
<?php include('jsinclude.php'); ?>
<script>
$(document).ready(function() {
let lastJsonResponse = null;
function escapeHtml(value) {
if (value === null || value === undefined) {
return '';
}
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function formatDate(value) {
if (!value) {
return '-';
}
const date = new Date(value);
if (isNaN(date.getTime())) {
return value;
}
return date.toLocaleString('it-IT', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
function setLoading(isLoading) {
$('#btnSearchReport').prop('disabled', isLoading);
$('#searchSpinner').toggle(isLoading);
}
function resetResult() {
lastJsonResponse = null;
$('#resultContainer').hide();
$('#emptyState').show();
$('#jsonPreview').hide().text('');
$('#toggleJsonBtn').html('<i class="bx bx-code-alt"></i> Show JSON');
$('#resCodiceRapporto').text('-');
$('#resIdRapporto').text('-');
$('#resCodiceCliente').text('-');
$('#resNominativoCliente').text('-');
$('#resDataRapporto').text('-');
$('#resDataStampa').text('-');
$('#resVersione').text('-');
$('#resFirmato').text('-');
$('#resDetailEndpoint').text('-');
$('#pdfFilesContainer').html('');
}
function renderPdfFiles(pdfFiles) {
const container = $('#pdfFilesContainer');
container.empty();
if (!Array.isArray(pdfFiles) || pdfFiles.length === 0) {
container.html(`
<div class="empty-state">
<i class="bx bx-file-blank" style="font-size:28px;"></i>
<div class="mt-2 fw-semibold">No PDF available</div>
<div class="small">No report PDF file was returned by VisualLims.</div>
</div>
`);
return;
}
pdfFiles.forEach(function(file) {
const idRapportoFile = file.id_rapporto_file || '';
const fileName = file.file_name || 'report.pdf';
const categoria = file.categoria || '-';
const tipoRapporto = file.tipo_rapporto || '-';
const downloadUrl = 'download_rapporto_pdf.php?id_rapporto_file=' + encodeURIComponent(idRapportoFile);
const html = `
<div class="pdf-card mb-2">
<div class="d-flex align-items-center gap-3">
<div class="pdf-icon">
<i class="bx bxs-file-pdf"></i>
</div>
<div>
<div class="pdf-file-name">${escapeHtml(fileName)}</div>
<div class="pdf-meta">
ID File: ${escapeHtml(idRapportoFile)}
&nbsp;|&nbsp;
Category: ${escapeHtml(categoria)}
&nbsp;|&nbsp;
Type: ${escapeHtml(tipoRapporto)}
</div>
</div>
</div>
<a href="${downloadUrl}" target="_blank" class="btn btn-danger btn-sm btn-download-pdf">
<i class="bx bx-download"></i> Download PDF
</a>
</div>
`;
container.append(html);
});
}
function renderResult(response) {
lastJsonResponse = response;
const base = response.rapporto_base || {};
const data = response.data || {};
const cliente = response.cliente || data.Cliente || {};
$('#resCodiceRapporto').text(response.codice_rapporto || base.CodiceRapporto || '-');
$('#resIdRapporto').text(response.id_rapporto || base.IdRapporto || '-');
$('#resCodiceCliente').text(cliente.CodiceCliente || '-');
$('#resNominativoCliente').text(cliente.Nominativo || '-');
$('#resDataRapporto').text(formatDate(base.Data || data.Data));
$('#resDataStampa').text(formatDate(base.DataStampa || data.DataStampa));
$('#resVersione').text(base.Versione !== undefined ? base.Versione : (data.Versione !== undefined ? data.Versione : '-'));
$('#resDetailEndpoint').text(response.detail_endpoint || '-');
const isSigned = base.Firmato === true || data.Firmato === true;
if (isSigned) {
$('#resFirmato').html('<span class="status-pill status-pill-success"><i class="bx bx-check-circle"></i> Signed</span>');
} else {
$('#resFirmato').html('<span class="status-pill status-pill-warning"><i class="bx bx-time"></i> Not signed</span>');
}
renderPdfFiles(response.pdf_files || []);
$('#jsonPreview').text(JSON.stringify(response, null, 4));
$('#emptyState').hide();
$('#resultContainer').show();
}
$('#reportSearchForm').on('submit', function(event) {
event.preventDefault();
const codiceRapporto = $('#codiceRapporto').val().trim();
if (!codiceRapporto) {
Swal.fire({
title: 'Missing report number',
text: 'Please enter a report number.',
icon: 'warning',
confirmButtonText: 'OK'
});
return;
}
resetResult();
setLoading(true);
$.ajax({
url: 'get_rapporto_prova.php',
method: 'GET',
dataType: 'json',
data: {
codice: codiceRapporto,
step: 'files'
},
success: function(response) {
if (!response || response.success !== true) {
Swal.fire({
title: 'Report not found',
text: response && response.message ? response.message : 'No report was found for this number.',
icon: 'warning',
confirmButtonText: 'OK'
});
return;
}
renderResult(response);
},
error: function(xhr) {
let message = 'Unexpected error while loading the report.';
if (xhr.responseJSON && xhr.responseJSON.error) {
message = xhr.responseJSON.error;
} else if (xhr.responseJSON && xhr.responseJSON.message) {
message = xhr.responseJSON.message;
} else if (xhr.responseText) {
message = xhr.responseText.substring(0, 500);
}
Swal.fire({
title: 'Error',
text: message,
icon: 'error',
confirmButtonText: 'OK'
});
},
complete: function() {
setLoading(false);
}
});
});
$('#toggleJsonBtn').on('click', function() {
const jsonPreview = $('#jsonPreview');
const isVisible = jsonPreview.is(':visible');
jsonPreview.toggle(!isVisible);
if (isVisible) {
$(this).html('<i class="bx bx-code-alt"></i> Show JSON');
} else {
$(this).html('<i class="bx bx-hide"></i> Hide JSON');
}
});
const urlParams = new URLSearchParams(window.location.search);
const prefillCodice = urlParams.get('codice');
if (prefillCodice) {
$('#codiceRapporto').val(prefillCodice);
$('#reportSearchForm').trigger('submit');
}
});
</script>
</body>
</html>
+318
View File
@@ -0,0 +1,318 @@
<?php
include('include/headscript.php');
// Binding preparati da import_insert.php (da risolvere + collegati in automatico).
$pending = $_SESSION['pending_bindings'] ?? null;
if (empty($pending) || (empty($pending['items']) && empty($pending['auto']) && empty($pending['saved']))) {
// Niente da mostrare: vado alla griglia.
$tid = $pending['template_id'] ?? ($_SESSION['template_id'] ?? null);
$ref = $pending['importref'] ?? '';
unset($_SESSION['pending_bindings']);
if ($tid) {
header("Location: imported.php?id=" . urlencode($tid) . "&importref=" . urlencode($ref));
} else {
header("Location: xlstemplates_grid.php");
}
exit;
}
$templateId = (int) $pending['template_id'];
$importRef = (string) $pending['importref'];
$items = $pending['items'] ?? [];
$autoItems = $pending['auto'] ?? [];
$savedItems = $pending['saved'] ?? [];
// Righe gia' risolte (modificabili): auto-collegate + binding gia' salvati.
$resolvedItems = [];
foreach ($autoItems as $a) {
$a['badge'] = 'auto';
$a['badge_class'] = 'bg-success';
$resolvedItems[] = $a;
}
foreach ($savedItems as $s) {
$s['badge'] = 'salvato';
$s['badge_class'] = 'bg-secondary';
$resolvedItems[] = $s;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("SELECT name FROM excel_templates WHERE id = ?");
$stmt->execute([$templateId]);
$templateName = $stmt->fetchColumn() ?: ('Template ' . $templateId);
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<style>
.json-value {
font-family: Consolas, Monaco, monospace;
font-weight: 600;
}
.binding-status {
font-size: 12px;
}
td .select2-container {
min-width: 240px;
}
/* Allinea l'altezza di select2 a form-select/btn di Bootstrap */
.select2-container--default .select2-selection--single {
height: 38px;
border-color: #ced4da;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 36px;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 36px;
}
</style>
<title>Binding JSON -> LIMS - <?= htmlspecialchars($titlewebsite ?? '', ENT_QUOTES, 'UTF-8'); ?></title>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card radius-10">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<div>
<h6 class="mb-0">Binding JSON -> LIMS</h6>
<small><?= htmlspecialchars($templateName) ?> - Template ID: <?= $templateId ?></small>
</div>
<a href="bindings_manage.php?template_id=<?= $templateId ?>" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-cog"></i> Gestione binding
</a>
</div>
</div>
<div class="card-body">
<?php if (!empty($items)): ?>
<div class="alert alert-info">
Alcuni valori importati dal JSON non hanno ancora una corrispondenza con i valori del LIMS.
Seleziona il valore LIMS corretto per ciascuno e conferma. I binding verranno salvati e
riutilizzati nelle importazioni successive.
</div>
<?php endif; ?>
<?php if (!empty($autoItems)): ?>
<div class="alert alert-success">
<?= count($autoItems) ?> valore/i collegato/i automaticamente (corrispondenza esatta con il LIMS).
</div>
<?php endif; ?>
<div id="bindingError" class="alert alert-danger" style="display:none;"></div>
<form id="bindingForm">
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead>
<tr>
<th>Campo (template_mapping)</th>
<th>Valore JSON</th>
<th>Valore LIMS</th>
<th style="width:210px;">Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $idx => $item): ?>
<tr class="binding-row"
data-index="<?= $idx ?>"
data-mapping-id="<?= (int) $item['mapping_id'] ?>"
data-field-id="<?= (int) $item['field_id'] ?>"
data-json-value="<?= htmlspecialchars($item['json_value'], ENT_QUOTES) ?>"
data-datadb-ids="<?= htmlspecialchars(json_encode($item['datadb_ids']), ENT_QUOTES) ?>">
<td class="text-muted"><?= htmlspecialchars($item['field_label']) ?></td>
<td class="json-value"><?= htmlspecialchars($item['json_value']) ?></td>
<td>
<select class="form-select binding-select" data-field-id="<?= (int) $item['field_id'] ?>">
<option value="">Seleziona valore LIMS...</option>
</select>
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-secondary skip-binding-btn">Nessuna corrispondenza</button>
<div class="binding-status text-muted mt-1">In attesa</div>
</td>
</tr>
<?php endforeach; ?>
<?php foreach ($resolvedItems as $res): ?>
<tr class="binding-row"
data-mapping-id="<?= (int) $res['mapping_id'] ?>"
data-field-id="<?= (int) $res['field_id'] ?>"
data-json-value="<?= htmlspecialchars($res['json_value'], ENT_QUOTES) ?>"
data-datadb-ids="<?= htmlspecialchars(json_encode($res['datadb_ids']), ENT_QUOTES) ?>">
<td class="text-muted"><?= htmlspecialchars($res['field_label']) ?></td>
<td class="json-value"><?= htmlspecialchars($res['json_value']) ?></td>
<td>
<select class="form-select binding-select" data-field-id="<?= (int) $res['field_id'] ?>">
<option value="<?= (int) $res['lims_value_id'] ?>" selected><?= htmlspecialchars($res['lims_value']) ?></option>
</select>
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-secondary skip-binding-btn">Nessuna corrispondenza</button>
<div class="binding-status mt-1"><span class="badge <?= $res['badge_class'] ?>"><?= $res['badge'] ?></span></div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="d-flex justify-content-end gap-2 mt-3">
<a href="imported.php?id=<?= $templateId ?>&importref=<?= urlencode($importRef) ?>"
class="btn btn-outline-secondary">Salta per ora</a>
<button type="submit" class="btn btn-primary" id="confirmBindingsBtn" disabled>
Conferma e prosegui
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(function() {
const TEMPLATE_ID = <?= $templateId ?>;
const IMPORT_REF = <?= json_encode($importRef) ?>;
const CONTINUE_URL = 'imported.php?id=' + TEMPLATE_ID + '&importref=' + encodeURIComponent(IMPORT_REF);
const $form = $('#bindingForm');
const $btn = $('#confirmBindingsBtn');
const $error = $('#bindingError');
// Dropdown valori LIMS per riga (search_customfield_values.php).
$('.binding-select').each(function() {
const fieldId = $(this).data('field-id');
$(this).select2({
placeholder: 'Seleziona valore LIMS...',
width: '100%',
ajax: {
url: 'search_customfield_values.php',
dataType: 'json',
delay: 200,
data: params => ({
field_id: fieldId,
q: params.term || '',
limit: 50
}),
processResults: data => ({
results: data.results || []
})
},
minimumInputLength: 0
});
});
// Una riga e' pronta se ha un valore scelto oppure e' marcata "nessuna corrispondenza".
function refreshButton() {
const allReady = $('.binding-row').toArray().every(row => {
const $row = $(row);
return $row.hasClass('is-skipped') || $row.find('.binding-select').val();
});
$btn.prop('disabled', !allReady);
}
$('.binding-select').on('change', refreshButton);
refreshButton();
// "Nessuna corrispondenza": azzera il valore importato, nessun binding salvato.
$('.skip-binding-btn').on('click', function() {
const $row = $(this).closest('.binding-row');
const $select = $row.find('.binding-select');
const $status = $row.find('.binding-status');
const skipped = $row.toggleClass('is-skipped').hasClass('is-skipped');
$(this).toggleClass('btn-outline-secondary', !skipped).toggleClass('btn-secondary', skipped);
$select.val(null).trigger('change').prop('disabled', skipped);
$status.text(skipped ? 'Nessuna corrispondenza' : 'In attesa');
refreshButton();
});
$form.on('submit', function(e) {
e.preventDefault();
$error.hide();
$btn.prop('disabled', true).text('Salvataggio...');
const tasks = $('.binding-row').toArray().map(row => {
const $row = $(row);
const $status = $row.find('.binding-status');
const datadbIds = JSON.stringify($row.data('datadb-ids') || []);
const jsonValue = String($row.data('json-value'));
// Riga senza corrispondenza: azzera il valore, niente binding.
if ($row.hasClass('is-skipped')) {
return $.post('skip_binding.php', {
mapping_id: $row.data('mapping-id'),
json_value: jsonValue,
datadb_ids: datadbIds
}).then(resp => {
if (resp && resp.success) {
$status.text('Azzerato').removeClass('text-muted').addClass('text-success');
return true;
}
$status.text('Errore').addClass('text-danger');
throw new Error((resp && resp.error) || 'Errore azzeramento valore');
});
}
const $select = $row.find('.binding-select');
const selectedData = $select.select2('data')[0] || {};
return $.post('save_binding.php', {
mapping_id: $row.data('mapping-id'),
field_id: $row.data('field-id'),
template_id: TEMPLATE_ID,
json_value: jsonValue,
lims_value_id: $select.val(),
lims_value: selectedData.text || '',
datadb_ids: datadbIds
}).then(resp => {
if (resp && resp.success) {
$status.text('Salvato').removeClass('text-muted').addClass('text-success');
return true;
}
$status.text('Errore').addClass('text-danger');
throw new Error((resp && resp.error) || 'Errore salvataggio binding');
});
});
Promise.all(tasks)
.then(() => {
window.location.href = CONTINUE_URL;
})
.catch(err => {
$error.text(err.message || 'Errore durante il salvataggio dei binding.').show();
$btn.prop('disabled', false).text('Conferma e prosegui');
});
});
});
</script>
</body>
</html>
+71
View File
@@ -0,0 +1,71 @@
<?php
/**
* Routine: burberry
*
* Purpose:
* For each imported XLS row:
* - read the value from column S
* - read the value from column T
* - merge the values
* - save the final value into column S
*
* Target:
* Column S must be mapped to the destination field in the template mapping.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* This routine does not require external routine data.
* Columns are fixed.
*
* Excel column indexes are zero-based:
*
* S = 18
* T = 19
*/
$targetColumnIndex = 18; // S
$columnSIndex = 18; // S
$columnTIndex = 19; // T
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine burberry: invalid row structure at index {$rowIndex}.");
continue;
}
$valueS = trim((string)($row['data'][$columnSIndex] ?? ''));
$valueT = trim((string)($row['data'][$columnTIndex] ?? ''));
/*
* Merge values, ignoring empty values.
*/
$mergedValues = [];
if ($valueS !== '') {
$mergedValues[] = $valueS;
}
if ($valueT !== '') {
$mergedValues[] = $valueT;
}
/*
* Save final value into column S.
*/
$row['data'][$targetColumnIndex] = implode(' ', $mergedValues);
error_log(
"Routine burberry: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value in column S: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine burberry completed.");
}
+67
View File
@@ -0,0 +1,67 @@
<?php
/**
* Routine: merge_column_T_and_U_into_T
*
* Purpose:
* For each imported XLS row:
* - read the value from column T
* - read the value from column U
* - merge both values
* - save the final value into column T
*
* Target:
* Column T must be mapped to the destination field in the template mapping.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* Excel column indexes are zero-based:
*
* T = 19
* U = 20
*/
$targetColumnIndex = 19; // T
$firstColumnIndex = 19; // T
$secondColumnIndex = 20; // U
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine merge T+U: invalid row structure at index {$rowIndex}.");
continue;
}
$valueT = trim((string)($row['data'][$firstColumnIndex] ?? ''));
$valueU = trim((string)($row['data'][$secondColumnIndex] ?? ''));
/*
* Merge values, ignoring empty values.
*/
$mergedValues = [];
if ($valueT !== '') {
$mergedValues[] = $valueT;
}
if ($valueU !== '') {
$mergedValues[] = $valueU;
}
/*
* Save final value into column T.
*/
$row['data'][$targetColumnIndex] = implode(' ', $mergedValues);
error_log(
"Routine merge T+U: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value in column T: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine merge T+U completed.");
}
@@ -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;
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
/**
* Routine: paulshark
*
* Purpose:
* For each imported XLS row:
* - read the value from column D
* - read the value from column E
* - read the value from column J
* - merge the values
* - save the final value into column D
*
* Target:
* Column D must be mapped to the destination field in the template mapping.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* Excel column indexes are zero-based:
*
* D = 3
* E = 4
* J = 9
*/
$targetColumnIndex = 3; // D
$columnDIndex = 3; // D
$columnEIndex = 4; // E
$columnJIndex = 9; // J
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine paulshark: invalid row structure at index {$rowIndex}.");
continue;
}
$valueD = trim((string)($row['data'][$columnDIndex] ?? ''));
$valueE = trim((string)($row['data'][$columnEIndex] ?? ''));
$valueJ = trim((string)($row['data'][$columnJIndex] ?? ''));
/*
* Merge values, ignoring empty values.
*/
$mergedValues = [];
if ($valueD !== '') {
$mergedValues[] = $valueD;
}
if ($valueE !== '') {
$mergedValues[] = $valueE;
}
if ($valueJ !== '') {
$mergedValues[] = $valueJ;
}
/*
* Save final value into column D.
*/
$row['data'][$targetColumnIndex] = implode(' ', $mergedValues);
error_log(
"Routine paulshark: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value in column D: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine paulshark completed.");
}
@@ -0,0 +1,71 @@
<?php
/**
* Routine: Richemont Pelletteria
*
* Purpose:
* For each imported XLS row:
* - read the value from column D
* - read the value from column E
* - merge the values
* - save the final value into column D
*
* Target:
* Column D must be mapped to the destination field in the template mapping.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* This routine does not require external routine data.
* Columns are fixed.
*
* Excel column indexes are zero-based:
*
* D = 3
* E = 4
*/
$targetColumnIndex = 3; // D
$columnDIndex = 3; // D
$columnEIndex = 4; // E
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine Richemont Pelletteria: invalid row structure at index {$rowIndex}.");
continue;
}
$valueD = trim((string)($row['data'][$columnDIndex] ?? ''));
$valueE = trim((string)($row['data'][$columnEIndex] ?? ''));
/*
* Merge values, ignoring empty values.
*/
$mergedValues = [];
if ($valueD !== '') {
$mergedValues[] = $valueD;
}
if ($valueE !== '') {
$mergedValues[] = $valueE;
}
/*
* Save final value into column D.
*/
$row['data'][$targetColumnIndex] = implode(' ', $mergedValues);
error_log(
"Routine Richemont Pelletteria: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value in column D: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine Richemont Pelletteria completed.");
}
@@ -0,0 +1,114 @@
<?php
/**
* Routine: build_field_347_from_x_columns
*
* Purpose:
* For each imported XLS row:
* - check columns P to AT
* - when a cell contains "x", take the related column title from row 6
* - append the free text value from column AU
* - save the final comma-separated text into column P
*
* Target:
* Column P must be mapped to field_id 347.
*/
function applyRoutine(&$excelData, $routineData = [])
{
/*
* Excel column indexes are zero-based:
*
* P = 15
* AT = 45
* AU = 46
*/
$targetColumnIndex = 15; // P
$startColumnIndex = 15; // P
$endColumnIndex = 45; // AT
$extraColumnIndex = 46; // AU
/*
* Headers must come from XLS row 6.
* Usually they are passed inside $routineData['xls_headers'].
*/
$headers = $routineData['xls_headers'] ?? [];
if (empty($headers) || !is_array($headers)) {
error_log("Routine field_id 347: missing XLS headers from row 6.");
return;
}
foreach ($excelData as $rowIndex => &$row) {
if (!isset($row['data']) || !is_array($row['data'])) {
error_log("Routine field_id 347: invalid row structure at index {$rowIndex}.");
continue;
}
$selectedValues = [];
/*
* Check columns from P to AT.
* If the cell contains x, take the related column header.
*/
for ($columnIndex = $startColumnIndex; $columnIndex <= $endColumnIndex; $columnIndex++) {
$cellValue = strtolower(trim((string)($row['data'][$columnIndex] ?? '')));
if ($cellValue === 'x') {
$headerTitle = trim((string)($headers[$columnIndex] ?? ''));
if ($headerTitle !== '') {
$selectedValues[] = $headerTitle;
}
}
}
/*
* Add free text from column AU.
*/
$extraText = '';
if (isset($row['data'][$extraColumnIndex])) {
$extraText = trim((string)$row['data'][$extraColumnIndex]);
} elseif (isset($row['data']['AU'])) {
$extraText = trim((string)$row['data']['AU']);
}
error_log(
"Routine field_id 347: row " .
($row['excelrow'] ?? $rowIndex) .
" AU index {$extraColumnIndex} value: " .
print_r($row['data'][$extraColumnIndex] ?? null, true) .
" | AU key value: " .
print_r($row['data']['AU'] ?? null, true)
);
if ($extraText !== '') {
$selectedValues[] = $extraText;
}
/*
* Remove empty and duplicate values.
*/
$selectedValues = array_values(array_unique(array_filter($selectedValues, function ($value) {
return trim((string)$value) !== '';
})));
/*
* Save final value into column P.
* Column P must be mapped to field_id 347 in the template mapping.
*/
$row['data'][$targetColumnIndex] = implode(', ', $selectedValues);
error_log(
"Routine field_id 347: row " .
($row['excelrow'] ?? $rowIndex) .
" generated value: " .
$row['data'][$targetColumnIndex]
);
}
unset($row);
error_log("Routine field_id 347 completed.");
}
+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;
})();
+79
View File
@@ -0,0 +1,79 @@
<?php
// Salva un binding JSON -> LIMS e lo applica ai record appena importati. Ritorna JSON.
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
require_once __DIR__ . '/class/binding-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
exit;
}
$mappingId = intval($_POST['mapping_id'] ?? 0);
$fieldId = intval($_POST['field_id'] ?? 0);
$templateId = intval($_POST['template_id'] ?? 0);
$jsonValue = (string) ($_POST['json_value'] ?? '');
$limsValueId = intval($_POST['lims_value_id'] ?? 0);
$limsValue = (string) ($_POST['lims_value'] ?? '');
$datadbIds = [];
if (isset($_POST['datadb_ids'])) {
$decoded = json_decode($_POST['datadb_ids'], true);
if (is_array($decoded)) {
$datadbIds = $decoded;
}
}
if ($mappingId <= 0 || $templateId <= 0 || $jsonValue === '' || $limsValueId <= 0) {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Missing required parameters']);
exit;
}
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$createdBy = null;
if (Auth::user()) {
$createdBy = (int) Auth::user()->present()->id;
}
if ($fieldId <= 0) {
$stmt = $pdo->prepare("SELECT field_id FROM template_mapping WHERE id = ?");
$stmt->execute([$mappingId]);
$fieldId = (int) ($stmt->fetchColumn() ?: 0);
}
binding_upsert($pdo, $templateId, $mappingId, $fieldId, $jsonValue, $limsValueId, $limsValue, $createdBy);
$applied = 0;
if (!empty($datadbIds)) {
$applied = binding_apply_to_details($pdo, $mappingId, $limsValue, $datadbIds);
}
echo json_encode([
'success' => true,
'applied_rows' => $applied,
'mapping_id' => $mappingId,
'json_value' => $jsonValue,
'lims_value' => $limsValue,
'lims_value_id' => $limsValueId,
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
+1 -1
View File
@@ -23,7 +23,7 @@ try {
foreach ($_POST as $key => $value) {
if ($key !== 'iddatadb' && !in_array($key, $excludeFields)) {
$updates[] = "$key = ?";
$values[] = htmlspecialchars($value);
$values[] = $value;
}
}
$values[] = $iddatadb;
+6 -2
View File
@@ -13,7 +13,7 @@ try {
$iddatadb = intval($_POST['iddatadb']);
$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;
$testedComponent = isset($_POST['tested_component']) ? trim((string)$_POST['tested_component']) : null;
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
@@ -144,7 +144,11 @@ try {
$setParts[] = "cliente_fornitore_id = ?";
$params[] = $clienteFornitoreId;
}
// 5a3) tested_component (se presente)
if (isset($testedComponent)) {
$setParts[] = "tested_component = ?";
$params[] = ($testedComponent === '') ? null : $testedComponent;
}
// 5b) fixed fields dal POST
// QUI è il punto chiave: accettiamo SOLO colonne reali (realWhitelist),
// ma se dal JS arrivassero ancora key logiche, funzionerebbe uguale
+42 -14
View File
@@ -16,9 +16,11 @@ if (!$data || !isset($data['id'])) {
exit;
}
$mappingId = $data['id'];
$mappingId = (int)$data['id'];
$mappingType = $data['mapping_type'] ?? '';
$excelColumn = $data['excel_column'] ?? null;
$jsonNode = $data['json_node'] ?? null;
$manualDefault = $data['manual_default'] ?? null;
$autoValue = $data['auto_value'] ?? 'none';
@@ -26,7 +28,7 @@ $tablename = $data['tablename'] ?? '';
try {
// Normalize mapping type
$allowedTypes = ['', 'xls', 'manual', 'auto'];
$allowedTypes = ['', 'xls', 'json', 'manual', 'auto'];
if (!in_array($mappingType, $allowedTypes, true)) {
echo json_encode(["success" => false, "message" => "Invalid mapping_type"]);
exit;
@@ -41,43 +43,67 @@ try {
// Decide what to persist based on mapping_type
$isManual = 0;
$excelToSave = null;
$jsonNodeToSave = null;
$manualToSave = null;
$autoToSave = 'none';
if ($mappingType === 'xls') {
$isManual = 0;
$excelToSave = $excelColumn ?: null;
$jsonNodeToSave = null;
$manualToSave = null;
$autoToSave = 'none';
} elseif ($mappingType === 'json') {
$isManual = 0;
$excelToSave = null;
$jsonNodeToSave = $jsonNode ?: null;
$manualToSave = null;
$autoToSave = 'none';
} elseif ($mappingType === 'manual') {
$isManual = 1;
$excelToSave = null;
$jsonNodeToSave = null;
$manualToSave = $manualDefault;
$autoToSave = 'none';
} elseif ($mappingType === 'auto') {
$isManual = 0;
$excelToSave = null;
$jsonNodeToSave = null;
$manualToSave = null;
$autoToSave = $autoValue ?: 'none';
} else {
// reset
// Reset mapping
$isManual = 0;
$excelToSave = null;
$jsonNodeToSave = null;
$manualToSave = null;
$autoToSave = 'none';
}
$stmt = $pdo->prepare("
UPDATE template_mapping
SET
is_manual = ?,
excel_column = ?,
manual_default = ?,
auto_value = ?
WHERE id = ?
");
UPDATE template_mapping
SET
is_manual = ?,
excel_column = ?,
json_node = ?,
manual_default = ?,
auto_value = ?
WHERE id = ?
");
$result = $stmt->execute([$isManual, $excelToSave, $manualToSave, $autoToSave, $mappingId]);
$result = $stmt->execute([
$isManual,
$excelToSave,
$jsonNodeToSave,
$manualToSave,
$autoToSave,
$mappingId
]);
if (!$result) {
echo json_encode(["success" => false, "message" => "Database update failed"]);
@@ -88,15 +114,17 @@ try {
"success" => true,
"message" => "Mapping updated successfully",
"saved" => [
"id" => (int)$mappingId,
"id" => $mappingId,
"mapping_type" => $mappingType,
"is_manual" => $isManual,
"excel_column" => $excelToSave,
"json_node" => $jsonNodeToSave,
"manual_default" => $manualToSave,
"auto_value" => $autoToSave
]
]);
} catch (Exception $e) {
} catch (Throwable $e) {
echo json_encode(["success" => false, "message" => "Error: " . $e->getMessage()]);
}
exit;
+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()
]);
}
File diff suppressed because one or more lines are too long
+30 -6
View File
@@ -46,8 +46,8 @@
{
"IdSchemaCustomFields": 48,
"ConteggioClienti": 0,
"Nome": "Standard Generico \/ Generic Standard",
"Descrizione": "Schema per tutti i campioni di qualsiasi matrice escluso cuoio\/pelle\r\n\r\n"
"Nome": "Standard \/ Generico",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 49,
@@ -653,7 +653,7 @@
"IdSchemaCustomFields": 163,
"ConteggioClienti": 0,
"Nome": "Devred",
"Descrizione": "Schema creato per cliente DEVRED\r\nGR 18\/03\/2024\r\n\r\n"
"Descrizione": "Schema creato per cliente DEVRED\r\nGR 18\/03\/2024 aggiornato il 23\/04\/2026\r\n\r\n"
},
{
"IdSchemaCustomFields": 164,
@@ -730,8 +730,8 @@
{
"IdSchemaCustomFields": 177,
"ConteggioClienti": 0,
"Nome": "Phoebe philo ACC",
"Descrizione": "(scarpe, borse, cinture, occhiali, gioielleria)\r\n"
"Nome": "Phoebe Philo ",
"Descrizione": "\r\n\r\n"
},
{
"IdSchemaCustomFields": 178,
@@ -760,7 +760,7 @@
{
"IdSchemaCustomFields": 182,
"ConteggioClienti": 0,
"Nome": "Ralph Lauren - All testing V.6",
"Nome": "Ralph Lauren - All testing V.10",
"Descrizione": "AGGIORNAMENTO AL 16\/03\/2026 per estrazione fatturazione"
},
{
@@ -876,6 +876,30 @@
"ConteggioClienti": 0,
"Nome": "ROSSIMODA",
"Descrizione": "Per tutti i campioni di ROSSIMODA\r\n\r\n"
},
{
"IdSchemaCustomFields": 202,
"ConteggioClienti": 0,
"Nome": "LIMS-CIM - MAX MARA",
"Descrizione": "Schema per MAX MARA scambio dati Database"
},
{
"IdSchemaCustomFields": 203,
"ConteggioClienti": 0,
"Nome": "Vince",
"Descrizione": "Schema per tutti i campioni di VINCE\r\n\r\n"
},
{
"IdSchemaCustomFields": 204,
"ConteggioClienti": 0,
"Nome": "Max Mara",
"Descrizione": "Schema da usare per Max Mara\r\n"
},
{
"IdSchemaCustomFields": 205,
"ConteggioClienti": 0,
"Nome": "Chanel Flammability",
"Descrizione": "Schema per Chanel Flammability\r\n"
}
]
}
+118
View File
@@ -0,0 +1,118 @@
<?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;
function formatClientLabel(array $client): string
{
$name = trim($client['Nominativo'] ?? '');
$id = trim((string)($client['IdCliente'] ?? ''));
$code = trim((string)($client['CodiceCliente'] ?? ''));
$parts = explode('_', $code);
$suffix = trim($parts[1] ?? '');
if ($suffix === '' && $code !== '') {
$suffix = substr($code, 0, 1);
}
if ($suffix === '') {
$suffix = '--';
}
return $name . ' - ' . $suffix . ' (ID: ' . $id . ')';
}
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, used for loading selected value
if ($id !== null) {
foreach ($clients as $c) {
if ((int)$c['IdCliente'] === $id) {
echo json_encode([
'results' => [[
'id' => $c['IdCliente'],
'text' => formatClientLabel($c),
'IdCliente' => $c['IdCliente'],
'Nominativo' => trim($c['Nominativo'] ?? ''),
'CodiceCliente' => trim($c['CodiceCliente'] ?? '')
]]
]);
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' => formatClientLabel($c),
'IdCliente' => $c['IdCliente'],
'Nominativo' => $name,
'CodiceCliente' => $code
];
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,71 @@
<?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;
$rawLimit = intval($_GET['limit'] ?? 20);
$limit = $rawLimit <= 0 ? 0 : max(1, min(500, $rawLimit));
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 ($limit > 0 && 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]);
+73
View File
@@ -0,0 +1,73 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
// Esempio: search_rapporto_min.php?codice=2621716
$codice = trim($_GET['codice'] ?? '');
if ($codice === '') {
throw new Exception("Parametro codice mancante. Usa ?codice=2621716");
}
/*
* Minimal:
* - niente expand
* - max 5 record
* - pochi campi
*
* Se il campo Codice non è quello giusto, sotto proviamo anche CodiceRapporto.
*/
$attempts = [];
$filters = [
'Codice' => "Codice eq '{$codice}'",
'CodiceRapporto' => "CodiceRapporto eq '{$codice}'",
'Numero' => "Numero eq '{$codice}'"
];
foreach ($filters as $label => $filter) {
try {
$data = $api->get('Rapporto', [
'$filter' => $filter,
'$top' => 5,
'$select' => 'IdRapporto,Codice,CodiceRapporto,Numero,Data,Versione,Cliente'
]);
$attempts[$label] = [
'success' => true,
'filter' => $filter,
'records' => isset($data['value']) && is_array($data['value']) ? count($data['value']) : null,
'data' => $data
];
} catch (Exception $e) {
$attempts[$label] = [
'success' => false,
'filter' => $filter,
'error' => $e->getMessage()
];
}
}
echo json_encode([
'success' => true,
'input_codice' => $codice,
'attempts' => $attempts
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+55
View File
@@ -0,0 +1,55 @@
<?php
// "Nessuna corrispondenza": azzera il valore grezzo nei record importati, senza salvare binding.
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
require_once __DIR__ . '/class/binding-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
exit;
}
$mappingId = intval($_POST['mapping_id'] ?? 0);
$jsonValue = (string) ($_POST['json_value'] ?? '');
$datadbIds = [];
if (isset($_POST['datadb_ids'])) {
$decoded = json_decode($_POST['datadb_ids'], true);
if (is_array($decoded)) {
$datadbIds = $decoded;
}
}
if ($mappingId <= 0 || $jsonValue === '') {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Missing required parameters']);
exit;
}
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$cleared = binding_apply_to_details($pdo, $mappingId, '', $datadbIds);
// Rimuovo un eventuale binding gia' salvato (es. auto-collegato) per coerenza.
$del = $pdo->prepare("DELETE FROM json_lims_binding WHERE mapping_id = ? AND json_value = ?");
$del->execute([$mappingId, $jsonValue]);
echo json_encode(['success' => true, 'cleared_rows' => $cleared]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
+387 -162
View File
@@ -56,6 +56,101 @@
input:checked+.slider:before {
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;
}
.badge-source-pdf {
background-color: #fff3cd;
color: #b58100;
}
.type-filter-bar {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 12px;
}
.type-filter-btn {
border: 0;
border-radius: 999px;
padding: 7px 14px;
font-size: 13px;
font-weight: 700;
color: #fff;
opacity: 0.35;
transition: all 0.15s ease-in-out;
}
.type-filter-btn.active {
opacity: 1;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
}
.type-filter-btn[data-type="XLS"] {
background-color: #0d6efd;
}
.type-filter-btn[data-type="API"] {
background-color: #198754;
}
.type-filter-btn[data-type="PDF"] {
background-color: #b58100;
}
.type-filter-btn:hover {
transform: translateY(-1px);
}
#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>
</head>
@@ -65,18 +160,20 @@
<!--sidebar wrapper -->
<?php include('include/navbar.php'); ?>
<!--end sidebar wrapper -->
<!--start header -->
<?php include('include/topbar.php'); ?>
<!--end header -->
<!--start page wrapper -->
<div class="page-wrapper">
<div class="page-content">
<?php include('top_stat_widget.php'); ?>
<div class="card radius-10">
<div class="card radius-10 compact-card">
<div class="card-header">
<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">
<i class="fas fa-plus"></i> New Template
</a>
@@ -84,192 +181,320 @@
</div>
<div class="card-body">
<div class="type-filter-bar">
<span class="text-muted fw-semibold me-1">Filter by type:</span>
<button type="button" class="type-filter-btn active" data-type="XLS">
XLS
</button>
<button type="button" class="type-filter-btn active" data-type="API">
JSON/API
</button>
<button type="button" class="type-filter-btn active" data-type="PDF">
PDF
</button>
</div>
<div class="table-responsive">
<table id="xlsTemplatesTable" class="table table-striped table-bordered">
<table id="xlsTemplatesTable" class="table table-striped table-bordered align-middle w-100">
<thead>
<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>Actions</th>
<th>Template Name</th>
<th>Type</th>
<th>Row</th>
<th>Col</th>
<th>Description</th>
<th>Client</th>
<th>Button</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<!-- DataTables riempirà questa sezione automaticamente -->
</tbody>
<tbody></tbody>
</table>
</div>
</div>
</div>
<!--end page wrapper -->
<!--start overlay-->
<div class="overlay toggle-icon"></div>
<!--end overlay-->
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<!--End Back To Top Button-->
<?php include('include/footer.php'); ?>
</div>
</div>
<!--end page wrapper -->
<!--start overlay-->
<div class="overlay toggle-icon"></div>
<!--end overlay-->
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<!--End Back To Top Button-->
<?php include('include/footer.php'); ?>
</div>
<!--end wrapper-->
<!--end wrapper-->
<!-- search modal -->
<?php //include('include/searchmodal.php');
?>
<!-- end search modal -->
<?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/dataTables.bootstrap5.min.js"></script>
<!--start switcher-->
<?php //include('include/themeswitcher.php');
?>
<!--end switcher-->
<?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/dataTables.bootstrap5.min.js"></script>
<script>
$(document).ready(function() {
<script>
$(document).ready(function() {
$('#xlsTemplatesTable').DataTable({
processing: true,
serverSide: false,
ajax: 'load_templates.php',
columns: [{
data: 'id', // ID del template
title: "ID"
},
{
data: 'name', // Nome del template
title: "Template Name"
},
const urlParams = new URLSearchParams(window.location.search);
{
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) {
const clientName = row.clientname || "No client";
const clientId = row.idclient || "N/A";
return `${clientName} (ID: ${clientId})`;
}
},
{
data: 'button_label', // Nuova colonna per Button Label
title: "Button Label",
defaultContent: 'Click Me'
},
{
data: 'status', // Stato con Toggle Switch
title: "Status",
orderable: false,
searchable: false,
render: function(status, type, row) {
let checked = (status === "active") ? "checked" : "";
return `
if (urlParams.get('cloned') === '1') {
Swal.fire({
title: "Template cloned",
text: "The template was cloned successfully.",
icon: "success",
confirmButtonText: "OK"
});
const cleanUrl = window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl);
}
const templatesTable = $('#xlsTemplatesTable').DataTable({
processing: true,
serverSide: false,
ajax: 'load_templates.php',
pageLength: 50,
autoWidth: false,
columns: [{
data: 'id',
orderable: false,
searchable: false,
title: "Actions",
className: "table-actions text-center",
render: function(data, type, row) {
return `
<div class="d-flex justify-content-center gap-1">
<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-warning" onclick="confirmClone(${data}, '${String(row.name || '').replace(/'/g, "\\'")}')" title="Clone">
<i class="bx bx-copy"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="confirmDelete(${data})" title="Delete">
<i class="bx bx-trash"></i>
</button>
</div>
`;
}
},
{
data: 'name',
title: "Template Name",
className: "name-cell"
},
{
data: 'source_type',
title: "Type",
className: "text-center",
render: function(data, type, row) {
let sourceType = (data || 'XLS').toUpperCase();
// Treat JSON as API group for dashboard filter
if (sourceType === 'JSON') {
sourceType = 'API';
}
if (type === 'display') {
if (sourceType === 'API') {
return '<span class="badge-source badge-source-api">JSON/API</span>';
}
if (sourceType === 'PDF') {
return '<span class="badge-source badge-source-pdf">PDF</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'
},
{
data: 'status',
title: "Status",
orderable: false,
searchable: false,
className: "text-center",
render: function(status, type, row) {
let checked = (status === "active") ? "checked" : "";
return `
<label class="switch">
<input type="checkbox" class="toggle-status" data-id="${row.id}" ${checked}>
<span class="slider round"></span>
</label>
`;
}
}
],
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],
order: [
[1, 'asc']
],
language: {
search: "Cerca:",
lengthMenu: "Mostra _MENU_ elementi",
info: "Visualizzando da _START_ a _END_ di _TOTAL_ elementi",
paginate: {
first: "<?= isset($langdatatables['paginate_first']) ? $langdatatables['paginate_first'] : 'Primo' ?>",
last: "<?= isset($langdatatables['paginate_last']) ? $langdatatables['paginate_last'] : 'Ultimo' ?>",
next: "<?= isset($langdatatables['paginate_next']) ? $langdatatables['paginate_next'] : 'Successivo' ?>",
previous: "<?= isset($langdatatables['paginate_previous']) ? $langdatatables['paginate_previous'] : 'Precedente' ?>"
}
}
},
{
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>
`;
});
const activeSourceTypes = {
XLS: true,
API: true,
PDF: true
};
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
if (settings.nTable.id !== 'xlsTemplatesTable') {
return true;
}
}
],
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],
language: {
search: "Cerca:",
lengthMenu: "Mostra _MENU_ elementi",
info: "Visualizzando da _START_ a _END_ di _TOTAL_ elementi",
paginate: {
first: "<?= isset($langdatatables['paginate_first']) ? $langdatatables['paginate_first'] : 'Primo' ?>",
last: "<?= isset($langdatatables['paginate_last']) ? $langdatatables['paginate_last'] : 'Ultimo' ?>",
next: "<?= isset($langdatatables['paginate_next']) ? $langdatatables['paginate_next'] : 'Successivo' ?>",
previous: "<?= isset($langdatatables['paginate_previous']) ? $langdatatables['paginate_previous'] : 'Precedente' ?>"
}
}
});
});
function confirmDelete(id) {
Swal.fire({
title: "Are you sure?",
text: "This action cannot be undone!",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#3085d6",
confirmButtonText: "Yes, delete it!",
cancelButtonText: "Cancel"
}).then((result) => {
if (result.isConfirmed) {
window.location.href = `delete_template_xls.php?id=${id}`;
}
});
}
const api = new $.fn.dataTable.Api(settings);
const rowData = api.row(dataIndex).data();
$(document).on("change", ".toggle-status", function() {
let templateId = $(this).data("id");
let newStatus = $(this).is(":checked") ? "active" : "inactive";
let sourceType = ((rowData && rowData.source_type) ? rowData.source_type : 'XLS').toUpperCase();
$.ajax({
url: "update_template_status.php",
type: "POST",
data: {
id: templateId,
status: newStatus
},
success: function(response) {
if (response.success) {
console.log("✅ Status updated successfully.");
} else {
console.error("❌ Error updating status:", response.message);
alert("Error updating status: " + response.message);
}
},
error: function() {
console.error("❌ AJAX error.");
if (sourceType === 'JSON') {
sourceType = 'API';
}
return activeSourceTypes[sourceType] === true;
});
$('.type-filter-btn').on('click', function() {
const type = $(this).data('type');
activeSourceTypes[type] = !activeSourceTypes[type];
$(this).toggleClass('active', activeSourceTypes[type]);
const hasAtLeastOneActive = Object.values(activeSourceTypes).some(Boolean);
if (!hasAtLeastOneActive) {
activeSourceTypes[type] = true;
$(this).addClass('active');
}
$('#xlsTemplatesTable').DataTable().draw();
});
});
function confirmDelete(id) {
Swal.fire({
title: "Are you sure?",
text: "This action cannot be undone!",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#3085d6",
confirmButtonText: "Yes, delete it!",
cancelButtonText: "Cancel"
}).then((result) => {
if (result.isConfirmed) {
window.location.href = `delete_template_xls.php?id=${id}`;
}
});
}
});
});
</script>
function confirmClone(id, templateName) {
Swal.fire({
title: "Clone template?",
html: `
<div class="text-start">
<p class="mb-2">You are about to clone this template:</p>
<strong>${templateName}</strong>
<p class="mt-3 mb-0 text-muted">
The new template will be created as <strong>Copia di ${templateName}</strong> and all mappings will be copied.
</p>
</div>
`,
icon: "question",
showCancelButton: true,
confirmButtonColor: "#ffc107",
cancelButtonColor: "#6c757d",
confirmButtonText: "Yes, clone it",
cancelButtonText: "Cancel"
}).then((result) => {
if (result.isConfirmed) {
window.location.href = `clone_template.php?id=${id}`;
}
});
}
$(document).on("change", ".toggle-status", function() {
let templateId = $(this).data("id");
let newStatus = $(this).is(":checked") ? "active" : "inactive";
$.ajax({
url: "update_template_status.php",
type: "POST",
dataType: "json",
data: {
id: templateId,
status: newStatus
},
success: function(response) {
if (response.success) {
console.log("Status updated successfully.");
} else {
console.error("Error updating status:", response.message);
alert("Error updating status: " + response.message);
}
},
error: function(xhr) {
console.error("AJAX error:", xhr.responseText);
}
});
});
</script>
</body>
+3
View File
@@ -8,6 +8,9 @@ ini_set('log_errors', 1);
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2))->safeLoad();
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
header('Content-Type: application/json');
try {
+117
View File
@@ -0,0 +1,117 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
ini_set('display_errors', '1');
error_reporting(E_ALL);
set_time_limit(60);
/**
* Uso: get_pdf.php?id=591749
*/
try {
$api = VisualLimsApiClient::getInstance();
$idRapportoFile = isset($_GET['id']) ? intval($_GET['id']) : 0;
if (!$idRapportoFile) {
throw new Exception("Parametro id mancante. Usa ?id=591749");
}
// STEP 1: leggi metadati entita
$entityData = $api->get("RapportoFile(" . $idRapportoFile . ")");
$pdfBody = null;
$strategyUsed = null;
$debugLog = array();
// Strategia A - campo FileContent base64
if (!empty($entityData['FileContent'])) {
$decoded = base64_decode($entityData['FileContent'], true);
if ($decoded !== false && substr($decoded, 0, 4) === '%PDF') {
$pdfBody = $decoded;
$strategyUsed = 'A: FileContent base64';
}
}
// Strategia B - campo Contenuto base64
if (!$pdfBody && !empty($entityData['Contenuto'])) {
$decoded = base64_decode($entityData['Contenuto'], true);
if ($decoded !== false && substr($decoded, 0, 4) === '%PDF') {
$pdfBody = $decoded;
$strategyUsed = 'B: Contenuto base64';
}
}
// Recupera token e baseUrl dalla classe
$token = $api->getToken();
$baseUrl = rtrim($api->getBaseUrl(), '/');
$rawEndpoints = array(
'C: $value' => $baseUrl . '/api/odata/RapportoFile(' . $idRapportoFile . ')/$value',
'D: FileContent/$value' => $baseUrl . '/api/odata/RapportoFile(' . $idRapportoFile . ')/FileContent/$value',
'E: Contenuto/$value' => $baseUrl . '/api/odata/RapportoFile(' . $idRapportoFile . ')/Contenuto/$value',
);
foreach ($rawEndpoints as $label => $url) {
if ($pdfBody) break;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization: Bearer ' . $token,
'Accept: application/pdf, application/octet-stream, */*',
));
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$body = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$ct = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch);
$debugLog[$label] = array(
'url' => $url,
'http_code' => $httpCode,
'content_type' => $ct,
'body_start' => substr((string)$body, 0, 300),
);
if ($httpCode === 200 && is_string($body) && substr($body, 0, 4) === '%PDF') {
$pdfBody = $body;
$strategyUsed = $label;
}
}
// RISPOSTA
if ($pdfBody) {
$fileName = isset($entityData['FileName']) ? $entityData['FileName'] : 'rapporto_' . $idRapportoFile . '.pdf';
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="' . $fileName . '"');
header('Content-Length: ' . strlen($pdfBody));
header('Cache-Control: private, max-age=0, must-revalidate');
echo $pdfBody;
exit;
}
// Nessuna strategia ha funzionato
header('Content-Type: application/json; charset=utf-8');
echo json_encode(array(
'success' => false,
'message' => 'Nessuna strategia ha restituito un PDF valido.',
'id_rapporto_file' => $idRapportoFile,
'entity_fields' => array_keys($entityData ? $entityData : array()),
'entity_data' => $entityData,
'strategies_debug' => $debugLog,
), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(array(
'success' => false,
'error' => $e->getMessage(),
), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
<?php
require_once "class/VisualLimsApiClient.class.php";
include('include/headscript.php');
header("Content-Type: application/json; charset=utf-8");
try {
$api = VisualLimsApiClient::getInstance();
$commessaId = 577818;
$endpoint = "CommessaWeb({$commessaId})?\$expand=CommesseCustomFields(\$expand=CustomField)";
$result = $api->get($endpoint);
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
"success" => false,
"error" => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
@@ -0,0 +1,107 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
require_once dirname(__FILE__) . '/include/headscript.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$api = VisualLimsApiClient::getInstance();
// Get all schemas currently used in template_mapping
$stmtSchemas = $pdo->query("
SELECT DISTINCT schema_id
FROM template_mapping
WHERE schema_id IS NOT NULL
AND schema_id > 0
ORDER BY schema_id ASC
");
$schemaIds = $stmtSchemas->fetchAll(PDO::FETCH_COLUMN);
if (empty($schemaIds)) {
throw new Exception('No schema_id found in template_mapping');
}
$stmtUpdate = $pdo->prepare("
UPDATE template_mapping
SET field_order = ?
WHERE schema_id = ?
AND field_id = ?
");
$summary = [];
$totalUpdated = 0;
foreach ($schemaIds as $schemaId) {
$schemaId = (int)$schemaId;
$endpoint = "SchemaCustomField($schemaId)?\$expand=SchemiCustomFieldsDettagli(\$expand=CustomField)";
$data = $api->get($endpoint);
if (empty($data['SchemiCustomFieldsDettagli']) || !is_array($data['SchemiCustomFieldsDettagli'])) {
$summary[] = [
'schema_id' => $schemaId,
'success' => false,
'message' => 'No SchemiCustomFieldsDettagli found'
];
continue;
}
$schemaUpdated = 0;
$notFound = [];
foreach ($data['SchemiCustomFieldsDettagli'] as $detail) {
$order = intval($detail['Ordine'] ?? 9999);
$fieldId = intval($detail['CustomField']['IdCustomField'] ?? 0);
if ($fieldId <= 0) {
continue;
}
$stmtUpdate->execute([
$order,
$schemaId,
$fieldId
]);
if ($stmtUpdate->rowCount() > 0) {
$schemaUpdated++;
$totalUpdated++;
} else {
$notFound[] = [
'field_id' => $fieldId,
'order' => $order,
'label' => $detail['CustomField']['Titolo'] ?? ''
];
}
}
$summary[] = [
'schema_id' => $schemaId,
'success' => true,
'updated' => $schemaUpdated,
'not_found_count' => count($notFound),
'not_found' => $notFound
];
}
echo json_encode([
'success' => true,
'schemas_processed' => count($schemaIds),
'total_updated' => $totalUpdated,
'summary' => $summary
], JSON_PRETTY_PRINT);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => $e->getMessage()
], JSON_PRETTY_PRINT);
}
+63
View File
@@ -0,0 +1,63 @@
<?php
include('include/headscript.php');
header('Content-Type: application/json');
try {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
throw new Exception('Invalid JSON payload');
}
$templateId = isset($input['template_id']) ? (int)$input['template_id'] : 0;
$apiSampleJson = $input['api_sample_json'] ?? '';
$jsonNodes = $input['json_nodes'] ?? '';
if ($templateId <= 0) {
throw new Exception('Invalid template ID');
}
if (trim($apiSampleJson) === '') {
throw new Exception('API sample JSON is empty');
}
json_decode($apiSampleJson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('Invalid API sample JSON: ' . json_last_error_msg());
}
$decodedNodes = json_decode($jsonNodes, true);
if (!is_array($decodedNodes)) {
throw new Exception('Invalid JSON nodes array');
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("
UPDATE excel_templates
SET
api_sample_json = ?,
json_nodes = ?
WHERE id = ?
");
$stmt->execute([
$apiSampleJson,
json_encode($decodedNodes, JSON_UNESCAPED_UNICODE),
$templateId
]);
echo json_encode([
'success' => true,
'nodes_count' => count($decodedNodes)
]);
} catch (Throwable $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
+77 -26
View File
@@ -3,33 +3,84 @@ header('Content-Type: application/json');
require_once(__DIR__ . '/include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$input = json_decode(file_get_contents('php://input'), true);
$input = json_decode(file_get_contents('php://input'), true);
$id = (int)($input['id'] ?? 0);
$field = (string)($input['field'] ?? '');
$value = $input['value'] ?? null;
$id = (int)($input['id'] ?? 0);
$field = (string)($input['field'] ?? '');
$value = $input['value'] ?? null;
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid id']);
exit;
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid id']);
exit;
}
$allowed = [
'default_value',
'default_source',
'json_node',
'is_visible_import',
'is_required'
];
if (!in_array($field, $allowed, true)) {
echo json_encode(['success' => false, 'message' => 'Invalid field: ' . $field]);
exit;
}
if ($field === 'is_visible_import' || $field === 'is_required') {
$value = ((int)$value === 1) ? 1 : 0;
}
if ($field === 'default_source') {
$value = (string)$value;
if (!in_array($value, ['manual', 'json'], true)) {
echo json_encode(['success' => false, 'message' => 'Invalid default_source']);
exit;
}
// If the user goes back to manual, clear the JSON node
if ($value === 'manual') {
$sql = "
UPDATE template_fixed_mapping
SET default_source = :val, json_node = NULL
WHERE id = :id
";
$stmt = $pdo->prepare($sql);
$ok = $stmt->execute([
':val' => $value,
':id' => $id
]);
echo json_encode(['success' => (bool)$ok]);
exit;
}
}
if ($field === 'json_node') {
$value = trim((string)$value);
if ($value === '') {
$value = null;
}
}
$sql = "UPDATE template_fixed_mapping SET {$field} = :val WHERE id = :id";
$stmt = $pdo->prepare($sql);
$ok = $stmt->execute([
':val' => $value,
':id' => $id
]);
echo json_encode(['success' => (bool)$ok]);
} catch (Throwable $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
$allowed = ['default_value', 'is_visible_import', 'is_required'];
if (!in_array($field, $allowed, true)) {
echo json_encode(['success' => false, 'message' => 'Invalid field']);
exit;
}
if ($field === 'is_visible_import' || $field === 'is_required') {
$value = ((int)$value === 1) ? 1 : 0;
}
$sql = "UPDATE template_fixed_mapping SET {$field} = :val WHERE id = :id";
$stmt = $pdo->prepare($sql);
$ok = $stmt->execute([':val' => $value, ':id' => $id]);
echo json_encode(['success' => (bool)$ok]);
+46 -14
View File
@@ -3,14 +3,18 @@ ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
include('include/headscript.php'); // Assumi che questo includa la connessione DB
include('include/headscript.php');
header('Content-Type: application/json');
// Recupera il payload JSON
$data = json_decode(file_get_contents('php://input'), true);
$template_id = intval($data['template_id']);
$mapping_id = intval($data['mapping_id']);
$value = intval($data['value']);
$template_id = intval($data['template_id'] ?? 0);
$mapping_id = intval($data['mapping_id'] ?? 0);
$value = intval($data['value'] ?? 0);
// IMPORTANT: main_field may be ENUM('0','1'), so use string values
$mainValue = ($value === 1) ? '1' : '0';
if ($template_id <= 0 || $mapping_id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid IDs']);
@@ -23,19 +27,47 @@ $pdo = $db->getConnection();
try {
$pdo->beginTransaction();
if ($value === 1) {
// Setta tutti main_field a 0 per questo template
$stmt = $pdo->prepare("UPDATE template_mapping SET main_field = 0 WHERE template_id = ?");
$stmt->execute([$template_id]);
if ($mainValue === '1') {
$stmt = $pdo->prepare("
SELECT COUNT(*)
FROM template_mapping
WHERE template_id = ?
AND main_field = '1'
AND id <> ?
");
$stmt->execute([$template_id, $mapping_id]);
$currentMainCount = (int)$stmt->fetchColumn();
if ($currentMainCount >= 2) {
$pdo->rollBack();
echo json_encode([
'success' => false,
'message' => 'Maximum 2 Main fields allowed',
'currentMainCount' => $currentMainCount
]);
exit;
}
}
// Setta il valore per questo mapping
$stmt = $pdo->prepare("UPDATE template_mapping SET main_field = ? WHERE id = ? AND template_id = ?");
$stmt->execute([$value, $mapping_id, $template_id]);
$stmt = $pdo->prepare("
UPDATE template_mapping
SET main_field = ?
WHERE id = ?
AND template_id = ?
");
$stmt->execute([$mainValue, $mapping_id, $template_id]);
$pdo->commit();
echo json_encode(['success' => true]);
} catch (Exception $e) {
$pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
+182 -53
View File
@@ -2,7 +2,6 @@
header('Content-Type: application/json');
require_once 'class/db-functions.php';
// Disabilita l'output di errori HTML
ini_set('display_errors', '0');
error_reporting(E_ALL);
@@ -13,108 +12,238 @@ try {
throw new Exception("Invalid request method.");
}
// Recupera i dati dal body JSON
$input = json_decode(file_get_contents('php://input'), true);
$template_id = isset($input['template_id']) ? intval($input['template_id']) : null;
$schemajson = isset($input['schemajson']) ? trim($input['schemajson']) : '';
// Controllo sui campi obbligatori
$template_id = isset($input['template_id']) ? intval($input['template_id']) : null;
$schemajson = isset($input['schemajson']) ? trim($input['schemajson']) : '';
if (empty($template_id) || empty($schemajson)) {
throw new Exception("All fields marked with * are required, including schemajson.");
}
// Validazione del JSON
$decoded_json = json_decode($schemajson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception("Invalid JSON format for schemajson.");
}
// Connessione al database
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Recupera il target_table e verifica l'esistenza del template
$stmt = $pdo->prepare("SELECT target_table FROM excel_templates WHERE id = ?");
$stmt->execute([$template_id]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$template) {
throw new Exception("Template not found.");
}
$target_table = $template['target_table'];
// Inizia una transazione
$pdo->beginTransaction();
// Aggiorna il schemajson in excel_templates
$stmt = $pdo->prepare("UPDATE excel_templates
SET schemajson = ?, updated_at = NOW()
WHERE id = ?");
$stmt->execute([$schemajson, $template_id]);
// Estrai i campi dallo schema
$schema_id = $decoded_json['IdSchemaCustomFields'] ?? null;
$schema_fields = $decoded_json['SchemiCustomFieldsDettagli'] ?? [];
if (empty($schema_id)) {
throw new Exception("Schema ID not found in schemajson.");
}
if (empty($schema_fields)) {
throw new Exception("No fields found in schema.");
}
// Prepara la query per l'inserimento condizionato in template_mapping
$insert_stmt = $pdo->prepare("
$pdo->beginTransaction();
/*
* 1) Update raw schema JSON in excel_templates
*/
$stmt = $pdo->prepare("
UPDATE excel_templates
SET schemajson = ?, updated_at = NOW()
WHERE id = ?
");
$stmt->execute([$schemajson, $template_id]);
/*
* Prepare reusable statements
*/
// Check if mapping already exists
$checkStmt = $pdo->prepare("
SELECT id, data_type
FROM template_mapping
WHERE template_id = ?
AND field_id = ?
LIMIT 1
");
// Insert new field
$insertStmt = $pdo->prepare("
INSERT INTO template_mapping (
template_id, schema_id, field_id, data_type, is_required, default_value,
has_list, length, decimals, min_value, max_value, default_curr_date,
tablename, field_label
)
SELECT :template_id, :schema_id, :field_id, :data_type, :is_required, :default_value,
:has_list, :length, :decimals, :min_value, :max_value, :default_curr_date,
:tablename, :field_label
WHERE NOT EXISTS (
SELECT 1 FROM template_mapping
WHERE template_id = :template_id_check AND field_id = :field_id_check
template_id,
schema_id,
field_id,
field_order,
data_type,
is_required,
default_value,
has_list,
length,
decimals,
min_value,
max_value,
default_curr_date,
tablename,
field_label
) VALUES (
:template_id,
:schema_id,
:field_id,
:field_order,
:data_type,
:is_required,
:default_value,
:has_list,
:length,
:decimals,
:min_value,
:max_value,
:default_curr_date,
:tablename,
:field_label
)
");
// Itera sui campi dello schema e inserisci quelli mancanti
// Update existing field WITHOUT clearing manual_default
$updateSameTypeStmt = $pdo->prepare("
UPDATE template_mapping
SET
schema_id = :schema_id,
field_order = :field_order,
data_type = :data_type,
is_required = :is_required,
default_value = :default_value,
has_list = :has_list,
length = :length,
decimals = :decimals,
min_value = :min_value,
max_value = :max_value,
default_curr_date = :default_curr_date,
tablename = :tablename,
field_label = :field_label
WHERE id = :id
");
// Update existing field AND clear user-entered value if type changed
$updateChangedTypeStmt = $pdo->prepare("
UPDATE template_mapping
SET
schema_id = :schema_id,
data_type = :data_type,
is_required = :is_required,
default_value = :default_value,
has_list = :has_list,
length = :length,
decimals = :decimals,
min_value = :min_value,
max_value = :max_value,
default_curr_date = :default_curr_date,
tablename = :tablename,
field_label = :field_label,
manual_default = NULL,
auto_value = 'none'
WHERE id = :id
");
$currentFieldIds = [];
/*
* 2) Insert new fields and update existing fields
*/
foreach ($schema_fields as $field) {
$custom_field = $field['CustomField'] ?? [];
if (empty($custom_field['IdCustomField'])) {
continue; // Salta se manca l'ID del campo
continue;
}
$insert_stmt->execute([
':template_id' => $template_id,
':schema_id' => $schema_id,
':field_id' => $custom_field['IdCustomField'],
':data_type' => $custom_field['Tipo'] ?? 'Testo',
':is_required' => $custom_field['ObbligatorioWeb'] ? 1 : 0,
':default_value' => $custom_field['ValoreDefault'] ?? null,
':has_list' => $custom_field['Elenco'] ? 1 : 0,
':length' => $custom_field['Lunghezza'] ?? 0,
':decimals' => $custom_field['Decimali'] ?? 0,
':min_value' => $custom_field['Minimo'] ?? null,
':max_value' => $custom_field['Massimo'] ?? null,
':default_curr_date' => $custom_field['DefaultCurrDate'] ? 1 : 0,
':tablename' => $target_table,
':field_label' => $custom_field['TitoloTraduzione'] ?? '',
':template_id_check' => $template_id,
':field_id_check' => $custom_field['IdCustomField']
]);
$fieldId = (int)$custom_field['IdCustomField'];
$currentFieldIds[] = $fieldId;
$newDataType = $custom_field['Tipo'] ?? 'Testo';
$data = [
':schema_id' => $schema_id,
':field_order' => (int)($field['Ordine'] ?? 9999),
':data_type' => $newDataType,
':is_required' => !empty($custom_field['ObbligatorioWeb']) ? 1 : 0,
':default_value' => $custom_field['ValoreDefault'] ?? null,
':has_list' => !empty($custom_field['Elenco']) ? 1 : 0,
':length' => $custom_field['Lunghezza'] ?? 0,
':decimals' => $custom_field['Decimali'] ?? 0,
':min_value' => $custom_field['Minimo'] ?? null,
':max_value' => $custom_field['Massimo'] ?? null,
':default_curr_date' => !empty($custom_field['DefaultCurrDate']) ? 1 : 0,
':tablename' => $target_table,
':field_label' => $custom_field['TitoloTraduzione'] ?? ''
];
$checkStmt->execute([$template_id, $fieldId]);
$existing = $checkStmt->fetch(PDO::FETCH_ASSOC);
if (!$existing) {
// New field: insert it
$insertData = $data;
$insertData[':template_id'] = $template_id;
$insertData[':field_id'] = $fieldId;
$insertStmt->execute($insertData);
} else {
// Existing field: update schema details
$oldDataType = $existing['data_type'] ?? '';
$updateData = $data;
$updateData[':id'] = (int)$existing['id'];
if ($oldDataType !== $newDataType) {
// Type changed: clear manual value because it may no longer be valid
$updateChangedTypeStmt->execute($updateData);
} else {
// Same type: keep manual_default, excel/json mapping, visibility, main field, etc.
$updateSameTypeStmt->execute($updateData);
}
}
}
/*
* 3) Remove fields no longer present in the schema
*/
$currentFieldIds = array_values(array_unique(array_filter($currentFieldIds)));
if (!empty($currentFieldIds)) {
$placeholders = implode(',', array_fill(0, count($currentFieldIds), '?'));
$deleteParams = array_merge([$template_id], $currentFieldIds);
$deleteStmt = $pdo->prepare("
DELETE FROM template_mapping
WHERE template_id = ?
AND field_id NOT IN ($placeholders)
");
$deleteStmt->execute($deleteParams);
}
// Commit della transazione
$pdo->commit();
$response["success"] = true;
$response["message"] = "Schema JSON updated and template mappings created successfully!";
$response["message"] = "Schema JSON updated, mappings synchronized, removed fields deleted, and changed fields updated successfully.";
} catch (Exception $e) {
if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollback();
}
$response["message"] = $e->getMessage();
}
// Restituisce un JSON per il fetch
echo json_encode($response);
@@ -0,0 +1,79 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
require_once dirname(__FILE__) . '/include/headscript.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$schemaId = isset($_GET['schema_id']) && is_numeric($_GET['schema_id'])
? intval($_GET['schema_id'])
: 0;
if ($schemaId <= 0) {
throw new Exception('Missing or invalid schema_id');
}
$api = VisualLimsApiClient::getInstance();
$endpoint = "SchemaCustomField($schemaId)?\$expand=SchemiCustomFieldsDettagli(\$expand=CustomField)";
$data = $api->get($endpoint);
if (empty($data['SchemiCustomFieldsDettagli']) || !is_array($data['SchemiCustomFieldsDettagli'])) {
throw new Exception('No SchemiCustomFieldsDettagli found');
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$updated = 0;
$notFound = [];
$stmt = $pdo->prepare("
UPDATE template_mapping
SET field_order = ?
WHERE schema_id = ?
AND field_id = ?
");
foreach ($data['SchemiCustomFieldsDettagli'] as $detail) {
$order = intval($detail['Ordine'] ?? 9999);
$fieldId = intval($detail['CustomField']['IdCustomField'] ?? 0);
if ($fieldId <= 0) {
continue;
}
$stmt->execute([
$order,
$schemaId,
$fieldId
]);
if ($stmt->rowCount() > 0) {
$updated++;
} else {
$notFound[] = [
'field_id' => $fieldId,
'order' => $order,
'label' => $detail['CustomField']['Titolo'] ?? ''
];
}
}
echo json_encode([
'success' => true,
'schema_id' => $schemaId,
'updated' => $updated,
'not_found' => $notFound
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
+85 -16
View File
@@ -6,30 +6,99 @@ error_reporting(E_ALL);
require_once(__DIR__ . '/class/db-functions.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$data = json_decode(file_get_contents("php://input"), true);
if (!$data || !isset($data['template_id'], $data['xls_headers'])) {
echo json_encode(["success" => false, "message" => "Invalid or missing data"]);
exit;
}
$templateId = $data['template_id'];
$xlsHeaders = $data['xls_headers'];
try {
$stmt = $pdo->prepare("UPDATE excel_templates SET xls_headers = ? WHERE id = ?");
$result = $stmt->execute([$xlsHeaders, $templateId]);
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$data = json_decode(file_get_contents("php://input"), true);
if (!$data || !isset($data['template_id'], $data['xls_headers'])) {
echo json_encode(["success" => false, "message" => "Invalid or missing data"]);
exit;
}
$templateId = (int)$data['template_id'];
$xlsHeaders = $data['xls_headers'];
$headerRow = isset($data['header_row']) && $data['header_row'] !== ''
? (int)$data['header_row']
: null;
$startColumn = isset($data['start_column']) && $data['start_column'] !== ''
? (int)$data['start_column']
: null;
$xlsSheetIndex = isset($data['xls_sheet_index']) && $data['xls_sheet_index'] !== ''
? (int)$data['xls_sheet_index']
: null;
if ($templateId <= 0) {
echo json_encode(["success" => false, "message" => "Invalid template ID"]);
exit;
}
if ($xlsHeaders === '') {
echo json_encode(["success" => false, "message" => "XLS headers cannot be empty"]);
exit;
}
if ($headerRow !== null && $headerRow <= 0) {
echo json_encode(["success" => false, "message" => "Header row must be greater than 0"]);
exit;
}
if ($startColumn !== null && $startColumn <= 0) {
echo json_encode(["success" => false, "message" => "Start column must be greater than 0"]);
exit;
}
if ($xlsSheetIndex !== null && $xlsSheetIndex < 0) {
echo json_encode(["success" => false, "message" => "XLS sheet number cannot be negative"]);
exit;
}
$sql = "UPDATE excel_templates SET xls_headers = ?";
$params = [$xlsHeaders];
if ($headerRow !== null) {
$sql .= ", header_row = ?";
$params[] = $headerRow;
}
if ($startColumn !== null) {
$sql .= ", start_column = ?";
$params[] = $startColumn;
}
if ($xlsSheetIndex !== null) {
$sql .= ", xls_sheet_index = ?";
$params[] = $xlsSheetIndex;
}
$sql .= ", updated_at = NOW()";
$sql .= " WHERE id = ?";
$params[] = $templateId;
$stmt = $pdo->prepare($sql);
$result = $stmt->execute($params);
if (!$result) {
echo json_encode(["success" => false, "message" => "Database update failed"]);
exit;
}
echo json_encode(["success" => true, "message" => "XLS headers saved successfully"]);
echo json_encode([
"success" => true,
"message" => "XLS headers saved successfully",
"debug" => [
"template_id" => $templateId,
"header_row" => $headerRow,
"start_column" => $startColumn,
"xls_sheet_index" => $xlsSheetIndex
]
]);
} catch (Exception $e) {
echo json_encode(["success" => false, "message" => "Error: " . $e->getMessage()]);
}
exit;
+121 -4
View File
@@ -88,6 +88,58 @@ $validators[] = function (int $iddatadb, array $ctx): array {
return [];
};
// 3. All LIMS-mandatory fields must be filled.
$validators[] = function (int $iddatadb, array $ctx): array {
$record = $ctx['record'] ?? null;
if (!$record) {
return [];
}
$errors = [];
// Fixed fields (stored as columns in datadb)
foreach (($ctx['requiredFixed'] ?? []) as $key => $label) {
$col = $ctx['fixedAliasMap'][$key] ?? null;
$val = $col !== null ? ($record[$col] ?? null) : null;
if ($val === null || $val === '' || (int) $val === 0) {
$errors[] = [
'field' => $key,
'message' => $label . ' è obbligatorio.',
];
}
}
// Custom fields (values stored in import_data_details, keyed by mapping_id)
foreach (($ctx['requiredCustom'] ?? []) as $cf) {
$val = $ctx['customValues'][(int) $cf['mapping_id']] ?? null;
if ($val === null || trim((string) $val) === '') {
$errors[] = [
'field' => 'field_label:' . $cf['field_label'],
'message' => rtrim($cf['field_label'], ': ') . ' è obbligatorio.',
];
}
}
return $errors;
};
// Logical fixed_field_key - real datadb column (mirrors imported.php $fixedAliasMap)
$fixedAliasMap = [
'ClienteResponsabile' => 'cliente_responsabile_id',
'ClienteFornitore' => 'cliente_fornitore_id',
'ClienteAnalisi' => 'clienteAnalisi',
'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id',
'AnagraficaCertestObject' => 'anagrafica_certest_object_id',
'AnagraficaCertestService' => 'anagrafica_certest_service_id',
'ConsegnaRichiesta' => 'consegna_richiesta',
];
// Fixed keys NOT enforced by the generic mandatory check above:
// - ConsegnaRichiesta: handled by its dedicated validator (also checks the date)
// - ClienteFornitore / ClienteAnalisi: nullable placeholders, sent as null on
// export and accepted by LIMS.
$skipRequiredFixed = ['ConsegnaRichiesta', 'ClienteFornitore', 'ClienteAnalisi'];
// ── Main ────────────────────────────────────────────────────────────────────
try {
@@ -104,9 +156,12 @@ try {
$iddatadbList = array_column($rows, 'iddatadb');
$placeholders = implode(',', array_fill(0, count($iddatadbList), '?'));
// Records (datadb) for fixed field validation
// Records (datadb) — templateid + fixed-field columns for mandatory validation
$stmt = $pdo->prepare("
SELECT iddatadb, consegna_richiesta
SELECT iddatadb, templateid, consegna_richiesta,
cliente_responsabile_id, moltiplicatore_prezzo_id,
anagrafica_certest_object_id, anagrafica_certest_service_id,
cliente_fornitore_id, clienteAnalisi
FROM datadb
WHERE iddatadb IN ($placeholders)
");
@@ -128,6 +183,62 @@ try {
$partsInfo[(int)$r['iddatadb']][] = $r;
}
// Mandatory-field config per template
$templateIds = array_values(array_unique(array_filter(array_map(
fn ($r) => (int)($r['templateid'] ?? 0),
$recordsInfo
))));
$requiredFixedByTemplate = []; // template_id => [ fixed_field_key => label ]
$requiredCustomByTemplate = []; // template_id => [ { mapping_id, field_label }, ... ]
if (!empty($templateIds)) {
$tplPlaceholders = implode(',', array_fill(0, count($templateIds), '?'));
// Required fixed fields (is_required synced from LIMS ObbligatorioWeb)
$stmt = $pdo->prepare("
SELECT template_id, fixed_field_key
FROM template_fixed_mapping
WHERE template_id IN ($tplPlaceholders) AND is_required = 1
");
$stmt->execute($templateIds);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$key = $r['fixed_field_key'];
if (in_array($key, $skipRequiredFixed, true)) {
continue;
}
$requiredFixedByTemplate[(int)$r['template_id']][$key] = $key;
}
// Required custom fields that are visible in the import grid
$stmt = $pdo->prepare("
SELECT id AS mapping_id, template_id, field_label
FROM template_mapping
WHERE template_id IN ($tplPlaceholders)
AND is_required = 1
AND is_visible_import = 1
");
$stmt->execute($templateIds);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$requiredCustomByTemplate[(int)$r['template_id']][] = [
'mapping_id' => (int)$r['mapping_id'],
'field_label' => $r['field_label'],
];
}
}
// Custom field values per record (import_data_details.id is the FK to datadb)
$stmt = $pdo->prepare("
SELECT id AS iddatadb, mapping_id, field_value
FROM import_data_details
WHERE id IN ($placeholders)
");
$stmt->execute($iddatadbList);
$customValuesByRecord = []; // iddatadb => [ mapping_id => field_value ]
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$customValuesByRecord[(int)$r['iddatadb']][(int)$r['mapping_id']] = $r['field_value'];
}
// ── Run validators per row ──────────────────────────────────────────────
$results = [];
@@ -137,9 +248,15 @@ try {
$index = $rowInfo['index'];
// Build context for validators
$record = $recordsInfo[$iddatadb] ?? null;
$templateId = (int)($record['templateid'] ?? 0);
$ctx = [
'record' => $recordsInfo[$iddatadb] ?? null,
'parts' => $partsInfo[$iddatadb] ?? [],
'record' => $record,
'parts' => $partsInfo[$iddatadb] ?? [],
'fixedAliasMap' => $fixedAliasMap,
'requiredFixed' => $requiredFixedByTemplate[$templateId] ?? [],
'requiredCustom' => $requiredCustomByTemplate[$templateId] ?? [],
'customValues' => $customValuesByRecord[$iddatadb] ?? [],
];
$errors = [];

Some files were not shown because too many files have changed in this diff Show More