Compare commits
33 Commits
56eee99a67
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 955b663322 | |||
| 4dc010d80e | |||
| 83c6157b10 | |||
| 4dd7b89c22 | |||
| dec42b4442 | |||
| dab8d9aebf | |||
| 375a10a678 | |||
| 15990be884 | |||
| c3a6dd73b6 | |||
| 44ed1186e0 | |||
| 9050cb1006 | |||
| e6820fdb62 | |||
| 5da37a7836 | |||
| c5f27cb69a | |||
| 1d81d6c996 | |||
| 0c72dbf5ae | |||
| 8455be04e1 | |||
| e42d1b9c51 | |||
| 3e69e3c322 | |||
| 0eb4f7a2ad | |||
| 4f2cfc1930 | |||
| df075dd76a | |||
| 6460454201 | |||
| 574ddbbd32 | |||
| 41f414db5c | |||
| 4a863e8c16 | |||
| b431f1d4e9 | |||
| f97b52f158 | |||
| 836fc055ec | |||
| e8dd585df4 | |||
| 198b8c08ad | |||
| 28c467d55e | |||
| 67bbd9bbbb |
+3
-1
@@ -47,6 +47,7 @@ yarn-error.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
|
||||
@@ -59,4 +60,5 @@ yarn-error.log
|
||||
# Ignora tutti i log ovunque
|
||||
*.log
|
||||
|
||||
public/userarea/cache/
|
||||
public/userarea/cache/
|
||||
public/userarea/error_log.txt
|
||||
|
||||
@@ -109,6 +109,8 @@ class LoginController extends Controller
|
||||
// Reindirizza in base al ruolo
|
||||
if ($user->hasRole('Admin')) {
|
||||
return redirect()->to('userarea/import_dashboard.php');
|
||||
} elseif ($user->hasRole('SuperUser')) {
|
||||
return redirect()->to('userarea/import_dashboard.php');
|
||||
} elseif ($user->hasRole('User')) {
|
||||
return redirect()->to('userarea/import_dashboard.php');
|
||||
}
|
||||
|
||||
@@ -431,7 +431,7 @@
|
||||
const emptyEl = modal.querySelector("#analysisEmptyBox");
|
||||
const errorEl = modal.querySelector("#analysisErrorBox");
|
||||
|
||||
const webOnly = webOnlyEl ? webOnlyEl.checked : false;
|
||||
const webOnly = true;
|
||||
const searchValue = searchEl ? searchEl.value.trim().toLowerCase() : "";
|
||||
|
||||
let visibleCount = 0;
|
||||
@@ -496,8 +496,10 @@
|
||||
emptyEl.classList.add("d-none");
|
||||
}
|
||||
|
||||
if (analysisLoadedCache[String(matrixId)]) {
|
||||
renderAnalysesList(analysisLoadedCache[String(matrixId)]);
|
||||
const cacheKey = String(matrixId) + "_WEB_ONLY";
|
||||
|
||||
if (analysisLoadedCache[cacheKey]) {
|
||||
renderAnalysesList(analysisLoadedCache[cacheKey]);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -509,13 +511,21 @@
|
||||
dataType: "json",
|
||||
data: {
|
||||
id_matrice: matrixId,
|
||||
web_only: 1,
|
||||
},
|
||||
})
|
||||
.done(function (response) {
|
||||
const analyses = Array.isArray(response.value)
|
||||
? response.value
|
||||
? response.value.filter(function (item) {
|
||||
return (
|
||||
item.SelezionabileSuWeb === true ||
|
||||
item.SelezionabileSuWeb === 1 ||
|
||||
item.SelezionabileSuWeb === "1"
|
||||
);
|
||||
})
|
||||
: [];
|
||||
analysisLoadedCache[String(matrixId)] = analyses;
|
||||
|
||||
analysisLoadedCache[cacheKey] = analyses;
|
||||
renderAnalysesList(analyses);
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
@@ -674,12 +684,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
const webOnlyEl = modal.querySelector("#analysisWebOnly");
|
||||
if (webOnlyEl) {
|
||||
webOnlyEl.addEventListener("change", function () {
|
||||
filterAnalysisList();
|
||||
});
|
||||
}
|
||||
// WEB only is now fixed by default
|
||||
|
||||
const searchEl = modal.querySelector("#analysisSearchInput");
|
||||
if (searchEl) {
|
||||
|
||||
@@ -28,11 +28,13 @@ try {
|
||||
|
||||
// 1. Load source parts
|
||||
$stmtParts = $pdo->prepare("
|
||||
SELECT id, part_number, part_description, mix, idmatrice, note, dateexpiry
|
||||
FROM identification_parts
|
||||
WHERE iddatadb = ?
|
||||
ORDER BY part_number ASC, id ASC
|
||||
");
|
||||
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);
|
||||
|
||||
|
||||
@@ -207,6 +207,21 @@ if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonText
|
||||
<input type="text" name="start_column" id="startColumn" class="form-control" value="<?php echo htmlspecialchars($template['start_column'] ?? ''); ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="xlsEndColumnWrapper">
|
||||
<label class="form-label">Last Column included</label>
|
||||
<input
|
||||
type="text"
|
||||
name="xls_end_column"
|
||||
id="xlsEndColumn"
|
||||
class="form-control"
|
||||
value="<?php echo htmlspecialchars($template['xls_end_column'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
|
||||
placeholder="Example: AN">
|
||||
<small class="text-danger fw-semibold">
|
||||
Attention: if left empty, the system will read the entire XLS/XLSX sheet.
|
||||
Dirty Excel files may cause memory errors or timeout.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="xlsSheetNumberWrapper">
|
||||
<label class="form-label">XLS Sheet Number</label>
|
||||
<input
|
||||
@@ -404,11 +419,13 @@ if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonText
|
||||
|
||||
const headerRowWrapper = document.getElementById("headerRowWrapper");
|
||||
const startColumnWrapper = document.getElementById("startColumnWrapper");
|
||||
const xlsEndColumnWrapper = document.getElementById("xlsEndColumnWrapper");
|
||||
const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper");
|
||||
const apiConfigWrapper = document.getElementById("apiConfigWrapper");
|
||||
|
||||
const headerRow = document.getElementById("headerRow");
|
||||
const startColumn = document.getElementById("startColumn");
|
||||
const xlsEndColumn = document.getElementById("xlsEndColumn");
|
||||
const xlsSheetIndex = document.getElementById("xlsSheetIndex");
|
||||
const apiConfigSelect = document.getElementById("apiConfigSelect");
|
||||
|
||||
@@ -444,13 +461,16 @@ if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonText
|
||||
if (isXls) {
|
||||
headerRowWrapper.style.display = 'block';
|
||||
startColumnWrapper.style.display = 'block';
|
||||
xlsEndColumnWrapper.style.display = 'block';
|
||||
xlsSheetNumberWrapper.style.display = 'block';
|
||||
|
||||
headerRow.required = true;
|
||||
startColumn.required = true;
|
||||
xlsEndColumn.required = false;
|
||||
|
||||
headerRow.disabled = false;
|
||||
startColumn.disabled = false;
|
||||
xlsEndColumn.disabled = false;
|
||||
xlsSheetIndex.disabled = false;
|
||||
|
||||
apiConfigWrapper.style.display = 'none';
|
||||
@@ -460,13 +480,16 @@ if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonText
|
||||
} else {
|
||||
headerRowWrapper.style.display = 'none';
|
||||
startColumnWrapper.style.display = 'none';
|
||||
xlsEndColumnWrapper.style.display = 'none';
|
||||
xlsSheetNumberWrapper.style.display = 'none';
|
||||
|
||||
headerRow.required = false;
|
||||
startColumn.required = false;
|
||||
xlsEndColumn.required = false;
|
||||
|
||||
headerRow.disabled = true;
|
||||
startColumn.disabled = true;
|
||||
xlsEndColumn.disabled = true;
|
||||
xlsSheetIndex.disabled = true;
|
||||
|
||||
if (isApiOrJson) {
|
||||
@@ -726,6 +749,22 @@ if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonText
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedSource === 'XLS' && xlsEndColumn.value.trim() !== '') {
|
||||
const lastColumnValue = xlsEndColumn.value.trim().toUpperCase();
|
||||
|
||||
if (!/^[A-Z]+$|^[1-9][0-9]*$/.test(lastColumnValue)) {
|
||||
Swal.fire({
|
||||
title: "Error!",
|
||||
text: "Last Column must be an Excel column like AN or a positive number like 40.",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
xlsEndColumn.value = lastColumnValue;
|
||||
}
|
||||
|
||||
fetch("process_edit_template_xls.php", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
|
||||
@@ -331,3 +331,35 @@
|
||||
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
|
||||
2026-06-16 14:29:15 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21037 ms: Couldn't connect to server, Risposta:
|
||||
|
||||
|
||||
2026-06-16 14:29:36 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21099 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:30:03 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21076 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:30:03 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21083 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:30:03 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21083 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:30:34 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21043 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:30:55 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21053 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:30:55 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21050 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:31:46 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21048 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:31:46 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21043 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:31:46 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21042 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:32:07 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21047 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:33:09 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21058 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:33:09 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21057 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:33:09 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21057 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:33:30 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21047 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:36:11 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21062 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:36:11 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21064 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:36:11 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21065 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:36:33 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21049 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:36:33 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21057 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:36:33 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21053 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:37:13 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21032 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:37:14 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21031 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:37:14 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21027 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 14:37:35 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21054 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 17:13:55 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21067 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 17:14:25 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21065 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 17:14:46 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21071 ms: Couldn't connect to server, Risposta:
|
||||
2026-06-16 17:14:46 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21078 ms: Couldn't connect to server, Risposta:
|
||||
|
||||
@@ -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)";
|
||||
@@ -579,11 +694,15 @@ try {
|
||||
"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
|
||||
@@ -599,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
|
||||
|
||||
@@ -18,7 +18,15 @@ try {
|
||||
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
$filter = rawurlencode("Matrice/IdMatrice eq $idMatrice");
|
||||
$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/';
|
||||
|
||||
+445
-59
@@ -36,6 +36,15 @@
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function escAttr(str) {
|
||||
if (str === null || str === undefined) return "";
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
function getDetailValue(rowIndex, mappingId) {
|
||||
return data[rowIndex].details[String(mappingId)] ?? "";
|
||||
}
|
||||
@@ -125,12 +134,35 @@
|
||||
);
|
||||
}
|
||||
|
||||
function sortSelect2ResultsByStart(data) {
|
||||
const term = $(".select2-container--open .select2-search__field").val();
|
||||
|
||||
if (!term) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const search = term.toLowerCase().trim();
|
||||
|
||||
return data.sort(function (a, b) {
|
||||
const textA = (a.text || "").toLowerCase().trim();
|
||||
const textB = (b.text || "").toLowerCase().trim();
|
||||
|
||||
const aStarts = textA.startsWith(search);
|
||||
const bStarts = textB.startsWith(search);
|
||||
|
||||
if (aStarts && !bStarts) return -1;
|
||||
if (!aStarts && bStarts) return 1;
|
||||
|
||||
return textA.localeCompare(textB, "it", { sensitivity: "base" });
|
||||
});
|
||||
}
|
||||
// Select2 AJAX config for client selects
|
||||
const clientSelect2Config = {
|
||||
placeholder: "Search client...",
|
||||
allowClear: true,
|
||||
width: "100%",
|
||||
minimumInputLength: 0,
|
||||
sorter: sortSelect2ResultsByStart,
|
||||
dropdownCssClass: "select2-dropdown-smaller",
|
||||
ajax: {
|
||||
url: "search_clienti.php",
|
||||
@@ -242,7 +274,88 @@
|
||||
return _pendingFixed[cacheKey];
|
||||
}
|
||||
|
||||
async function refreshDependentFixedFieldsForRow(rowIndex) {
|
||||
const row = data[rowIndex];
|
||||
if (!row) return;
|
||||
|
||||
const clientId = row.idclient || "";
|
||||
|
||||
// Find fixed fields that depend on idclient
|
||||
const dependentFields = Object.keys(fixedFieldApiConfig).filter(
|
||||
(key) => {
|
||||
return fixedFieldApiConfig[key].dependsOn === "idclient";
|
||||
},
|
||||
);
|
||||
|
||||
if (dependentFields.length === 0) return;
|
||||
|
||||
for (const fieldKey of dependentFields) {
|
||||
// When client changes, the old responsible is no longer reliable
|
||||
if (
|
||||
row.fixedFields &&
|
||||
Object.prototype.hasOwnProperty.call(row.fixedFields, fieldKey)
|
||||
) {
|
||||
row.fixedFields[fieldKey] = "";
|
||||
row._dirty = true;
|
||||
}
|
||||
|
||||
// Reload options for the new client
|
||||
if (clientId) {
|
||||
await loadFixedFieldOptions(fieldKey, clientId);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-render only this row so ClienteResponsabile select is rebuilt with the new options
|
||||
renderSingleRow(rowIndex);
|
||||
|
||||
// If the first row client changes, update the top propagation select too
|
||||
if (rowIndex === 0) {
|
||||
await refreshTopDependentFixedSelect(
|
||||
"ClienteResponsabile",
|
||||
clientId,
|
||||
);
|
||||
}
|
||||
|
||||
updateDirtyIndicator();
|
||||
}
|
||||
// ── Custom field dropdown data loading ─────────────────────────────────
|
||||
async function refreshTopDependentFixedSelect(fieldKey, clientId) {
|
||||
if (!topContainer || !fieldKey) return;
|
||||
|
||||
const sel = topContainer.querySelector(
|
||||
`.api-fixed-select[data-fixed-key="${fieldKey}"]`,
|
||||
);
|
||||
|
||||
if (!sel) return;
|
||||
|
||||
// Destroy Select2 if already initialized
|
||||
if ($(sel).hasClass("select2-hidden-accessible")) {
|
||||
$(sel).select2("destroy");
|
||||
}
|
||||
|
||||
sel.innerHTML = '<option value="">Seleziona...</option>';
|
||||
|
||||
if (!clientId) {
|
||||
$(sel).select2({
|
||||
placeholder: "Seleziona...",
|
||||
allowClear: true,
|
||||
width: "100%",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const items = await loadFixedFieldOptions(fieldKey, clientId);
|
||||
|
||||
items.forEach((item) => {
|
||||
sel.add(new Option(item.text, item.id));
|
||||
});
|
||||
|
||||
$(sel).select2({
|
||||
placeholder: "Seleziona...",
|
||||
allowClear: true,
|
||||
width: "100%",
|
||||
});
|
||||
}
|
||||
|
||||
// Select2 AJAX config factory for SceltaMultipla
|
||||
function sceltaSelect2Config(fieldId) {
|
||||
@@ -251,6 +364,7 @@
|
||||
allowClear: true,
|
||||
width: "100%",
|
||||
minimumInputLength: 0,
|
||||
sorter: sortSelect2ResultsByStart,
|
||||
ajax: {
|
||||
url: "search_customfield_values.php",
|
||||
dataType: "json",
|
||||
@@ -259,7 +373,7 @@
|
||||
return {
|
||||
field_id: fieldId,
|
||||
q: params.term || "",
|
||||
limit: 10,
|
||||
limit: 0, // 0 = no limit for custom field values
|
||||
};
|
||||
},
|
||||
processResults: function (data) {
|
||||
@@ -382,13 +496,13 @@
|
||||
const row = data[rowIndex];
|
||||
|
||||
switch (col.type) {
|
||||
case "main_field":
|
||||
div.innerHTML = createInputHTML(
|
||||
col,
|
||||
row.mainFieldValue || "",
|
||||
rowIndex,
|
||||
);
|
||||
case "main_field": {
|
||||
const val = getDetailValue(rowIndex, col.key);
|
||||
|
||||
div.innerHTML = createInputHTML(col, val || "", rowIndex);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "status": {
|
||||
const st = row.status || "i";
|
||||
@@ -438,7 +552,7 @@
|
||||
case "tested_component":
|
||||
div.style.overflow = "visible";
|
||||
div.innerHTML = `<div style="display:flex; align-items:center; gap:4px; width:100%; height:100%;">
|
||||
<input type="text" class="cell-input manual-input tested-component-input" value="${esc(row.tested_component || "")}" style="flex:1; min-width:0; height:28px;">
|
||||
<input type="text" class="cell-input manual-input tested-component-input" value="${escAttr(row.tested_component || "")}" style="flex:1; min-width:0; height:28px;">
|
||||
<button type="button" class="add-part-btn btn btn-sm btn-primary" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" style="display:inline-flex; align-items:center; justify-content:center; min-width:28px; width:28px; height:28px; padding:0; font-size:12px; flex-shrink:0; text-align:center;">
|
||||
<i class="fas fa-plus" style="margin:0; padding:0;"></i>
|
||||
</button>
|
||||
@@ -484,7 +598,7 @@
|
||||
const cls = col.isManual ? "manual-input" : "auto-input";
|
||||
const reqCls = col.isRequired ? " required-input" : "";
|
||||
const req = col.isRequired ? " required" : "";
|
||||
const v = esc(value);
|
||||
const v = escAttr(value);
|
||||
|
||||
if (col.dataType === "SceltaMultipla") {
|
||||
const options = buildDropdownOptionsHTML(col.fieldId, value);
|
||||
@@ -509,7 +623,7 @@
|
||||
if (col.dataType === "DATE") {
|
||||
const reqCls = col.isRequired ? " required-input" : "";
|
||||
const req = col.isRequired ? " required" : "";
|
||||
return `<input type="text" class="cell-input date-picker manual-input${reqCls} fixed-input" data-fixed-key="${col.key}" value="${esc(value)}"${req}>`;
|
||||
return `<input type="text" class="cell-input date-picker manual-input${reqCls} fixed-input" data-fixed-key="${escAttr(col.key)}" value="${escAttr(value)}"${req}>`;
|
||||
}
|
||||
|
||||
// Client-sourced fields → AJAX Select2 (like idclient)
|
||||
@@ -522,7 +636,7 @@
|
||||
const label = clientNameCache[value] || value;
|
||||
opts += `<option value="${esc(String(value))}" selected>${esc(String(label))}</option>`;
|
||||
}
|
||||
return `<select class="cell-input manual-input fixed-input searchable-client api-fixed-select${reqCls}" data-fixed-key="${col.key}" data-current-value="${esc(value)}"${req}>${opts}</select>`;
|
||||
return `<select class="cell-input manual-input fixed-input searchable-client api-fixed-select${reqCls}" data-fixed-key="${escAttr(col.key)}" data-current-value="${escAttr(value)}"${req}>${opts}</select>`;
|
||||
}
|
||||
|
||||
// Select — build from cache
|
||||
@@ -540,7 +654,7 @@
|
||||
|
||||
const reqCls = col.isRequired ? " required-input" : "";
|
||||
const req = col.isRequired ? " required" : "";
|
||||
return `<select class="cell-input manual-input fixed-input ${selectClass}${reqCls}" data-fixed-key="${col.key}" data-current-value="${esc(value)}"${req}>${options}</select>`;
|
||||
return `<select class="cell-input manual-input fixed-input ${selectClass}${reqCls}" data-fixed-key="${escAttr(col.key)}" data-current-value="${escAttr(value)}"${req}>${options}</select>`;
|
||||
}
|
||||
|
||||
function buildDropdownOptionsHTML(fieldId, selectedValue) {
|
||||
@@ -745,6 +859,143 @@
|
||||
flatpickr(this, { dateFormat: "Y-m-d", allowInput: true });
|
||||
});
|
||||
}
|
||||
function getInputTextWidth(input) {
|
||||
const span = document.createElement("span");
|
||||
const style = window.getComputedStyle(input);
|
||||
|
||||
span.style.position = "absolute";
|
||||
span.style.visibility = "hidden";
|
||||
span.style.whiteSpace = "pre";
|
||||
span.style.font = style.font;
|
||||
span.style.fontSize = style.fontSize;
|
||||
span.style.fontFamily = style.fontFamily;
|
||||
span.style.fontWeight = style.fontWeight;
|
||||
span.textContent = input.value || input.placeholder || "";
|
||||
|
||||
document.body.appendChild(span);
|
||||
|
||||
const width = span.offsetWidth + 60;
|
||||
|
||||
document.body.removeChild(span);
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
function autoExpandColumnFromInput(input) {
|
||||
if (!input) return;
|
||||
|
||||
const cell = input.closest(".grid-cell");
|
||||
if (!cell || !cell.dataset.index) return;
|
||||
|
||||
const columnIndex = parseInt(cell.dataset.index, 10);
|
||||
if (!columnIndex) return;
|
||||
|
||||
const wantedWidth = Math.max(120, getInputTextWidth(input));
|
||||
const currentWidth = cell.offsetWidth || 0;
|
||||
|
||||
// Only expand, do not shrink automatically
|
||||
if (wantedWidth <= currentWidth) return;
|
||||
|
||||
const newWidth = Math.min(wantedWidth, 900);
|
||||
|
||||
const header = document.querySelector(
|
||||
`.grid-header[data-index="${columnIndex}"]`,
|
||||
);
|
||||
|
||||
if (header) {
|
||||
header.style.flex = `0 0 ${newWidth}px`;
|
||||
}
|
||||
|
||||
const topCell = document.querySelector(
|
||||
`.grid-top .grid-cell:nth-child(${columnIndex + 1})`,
|
||||
);
|
||||
|
||||
if (topCell) {
|
||||
topCell.style.flex = `0 0 ${newWidth}px`;
|
||||
}
|
||||
|
||||
const cells = document.querySelectorAll(
|
||||
`.grid-row .grid-cell[data-index="${columnIndex}"]`,
|
||||
);
|
||||
|
||||
cells.forEach((c) => {
|
||||
c.style.flex = `0 0 ${newWidth}px`;
|
||||
});
|
||||
|
||||
const colPos = columnIndex - 1;
|
||||
|
||||
if (columns[colPos]) {
|
||||
columns[colPos].width = newWidth;
|
||||
}
|
||||
}
|
||||
|
||||
function syncVisibleRowsToGridData() {
|
||||
if (!rowContainer) return;
|
||||
|
||||
rowContainer
|
||||
.querySelectorAll(".grid-cell[data-row]")
|
||||
.forEach((cell) => {
|
||||
const rowIndex = parseInt(cell.dataset.row, 10);
|
||||
const row = data[rowIndex];
|
||||
|
||||
if (!row) return;
|
||||
|
||||
const colType = cell.dataset.colType;
|
||||
const colKey = cell.dataset.col;
|
||||
const input = cell.querySelector(".cell-input");
|
||||
|
||||
if (!input) return;
|
||||
|
||||
const value = $(input).hasClass("select2-hidden-accessible")
|
||||
? $(input).val() || ""
|
||||
: input.value || "";
|
||||
|
||||
if (colType === "detail" || colType === "main_field") {
|
||||
if (!row.details) row.details = {};
|
||||
|
||||
if (
|
||||
String(row.details[String(colKey)] ?? "") !==
|
||||
String(value)
|
||||
) {
|
||||
row.details[String(colKey)] = value;
|
||||
|
||||
if (colType === "main_field") {
|
||||
row.mainFieldValue = value;
|
||||
}
|
||||
|
||||
row._dirty = true;
|
||||
}
|
||||
} else if (colType === "fixed") {
|
||||
if (!row.fixedFields) row.fixedFields = {};
|
||||
|
||||
if (
|
||||
String(row.fixedFields[colKey] ?? "") !== String(value)
|
||||
) {
|
||||
row.fixedFields[colKey] = value;
|
||||
row._dirty = true;
|
||||
}
|
||||
} else if (colType === "idclient") {
|
||||
if (String(row.idclient ?? "") !== String(value)) {
|
||||
row.idclient = value;
|
||||
row._dirty = true;
|
||||
}
|
||||
} else if (colType === "cliente_fornitore_id") {
|
||||
if (
|
||||
String(row.cliente_fornitore_id ?? "") !== String(value)
|
||||
) {
|
||||
row.cliente_fornitore_id = value;
|
||||
row._dirty = true;
|
||||
}
|
||||
} else if (colType === "tested_component") {
|
||||
if (String(row.tested_component ?? "") !== String(value)) {
|
||||
row.tested_component = value;
|
||||
row._dirty = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updateDirtyIndicator();
|
||||
}
|
||||
|
||||
// ── Headers & Propagate row ────────────────────────────────────────────
|
||||
|
||||
@@ -886,23 +1137,28 @@
|
||||
}
|
||||
|
||||
if (config && config.dependsOn) {
|
||||
// For dependent fields: merge all cached values across all clientIds
|
||||
const allItems = new Map();
|
||||
for (const [key, items] of Object.entries(fixedFieldCache)) {
|
||||
if (key.startsWith(fieldKey + "_")) {
|
||||
items.forEach((item) =>
|
||||
allItems.set(String(item.id), item),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Dependent fixed fields, for example ClienteResponsabile:
|
||||
// use the first row client, not all cached clients.
|
||||
const firstClientId =
|
||||
data[0]?.idclient || meta.defaultIdclient || "";
|
||||
|
||||
sel.innerHTML = '<option value="">Seleziona...</option>';
|
||||
[...allItems.values()]
|
||||
.sort((a, b) =>
|
||||
String(a.text).localeCompare(String(b.text), "it", {
|
||||
sensitivity: "base",
|
||||
}),
|
||||
)
|
||||
.forEach((item) => sel.add(new Option(item.text, item.id)));
|
||||
|
||||
if (firstClientId) {
|
||||
const items =
|
||||
fixedFieldCache[fieldKey + "_" + firstClientId] || [];
|
||||
|
||||
items.forEach((item) => {
|
||||
sel.add(new Option(item.text, item.id));
|
||||
});
|
||||
}
|
||||
|
||||
$(sel).select2({
|
||||
placeholder: "Seleziona...",
|
||||
allowClear: true,
|
||||
width: "100%",
|
||||
sorter: sortSelect2ResultsByStart,
|
||||
});
|
||||
} else {
|
||||
const items = fixedFieldCache[fieldKey] || [];
|
||||
sel.innerHTML = '<option value="">Seleziona...</option>';
|
||||
@@ -950,6 +1206,8 @@
|
||||
} else if (colType === "idclient") {
|
||||
data[rowIndex].idclient = value;
|
||||
data[rowIndex]._dirty = true;
|
||||
|
||||
refreshDependentFixedFieldsForRow(rowIndex);
|
||||
} else if (colType === "cliente_fornitore_id") {
|
||||
data[rowIndex].cliente_fornitore_id = value;
|
||||
data[rowIndex]._dirty = true;
|
||||
@@ -972,6 +1230,10 @@
|
||||
const cell = e.target.closest(".grid-cell");
|
||||
if (!cell || !cell.dataset.row) return;
|
||||
|
||||
if (e.target.classList.contains("cell-input")) {
|
||||
autoExpandColumnFromInput(e.target);
|
||||
}
|
||||
|
||||
const rowIndex = parseInt(cell.dataset.row, 10);
|
||||
const colType = cell.dataset.colType;
|
||||
|
||||
@@ -983,6 +1245,16 @@
|
||||
}
|
||||
});
|
||||
|
||||
rowContainer.addEventListener("focusin", function (e) {
|
||||
if (!e.target.classList.contains("cell-input")) return;
|
||||
|
||||
autoExpandColumnFromInput(e.target);
|
||||
|
||||
setTimeout(() => {
|
||||
autoExpandColumnFromInput(e.target);
|
||||
}, 50);
|
||||
});
|
||||
|
||||
// Persist tested_component before clicking +
|
||||
document.addEventListener("mousedown", function (e) {
|
||||
const btn = e.target.closest(".add-part-btn");
|
||||
@@ -1011,62 +1283,149 @@
|
||||
const btn = e.target.closest(".propagate-btn");
|
||||
if (!btn) return;
|
||||
|
||||
const colIndex = parseInt(btn.dataset.colIndex);
|
||||
const column = btn.dataset.column;
|
||||
if (isNaN(colIndex) && !column) return;
|
||||
// Before propagating and re-rendering, persist current visible row values into gridData.
|
||||
syncVisibleRowsToGridData();
|
||||
|
||||
// Get value from the input/select in the same cell
|
||||
const cell =
|
||||
btn.closest(".grid-cell") || btn.closest(".grid-top-cell");
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
const column = btn.dataset.column || "";
|
||||
const colIndex = Number.isNaN(parseInt(btn.dataset.colIndex, 10))
|
||||
? null
|
||||
: parseInt(btn.dataset.colIndex, 10);
|
||||
|
||||
if (!column && colIndex === null) return;
|
||||
|
||||
// IMPORTANT:
|
||||
// Read ONLY the input/select inside the same top propagation cell.
|
||||
// Do not scan other top fields.
|
||||
const cell = btn.closest(".grid-top-cell");
|
||||
if (!cell) return;
|
||||
const input = cell.querySelector("select, input");
|
||||
if (!input) return;
|
||||
const value = $(input).hasClass("select2-hidden-accessible")
|
||||
? $(input).val()
|
||||
: input.value;
|
||||
|
||||
// Cache Select2 label so re-render shows name not ID
|
||||
const input = cell.querySelector(".custom-field");
|
||||
if (!input) return;
|
||||
|
||||
const value = $(input).hasClass("select2-hidden-accessible")
|
||||
? $(input).val() || ""
|
||||
: input.value || "";
|
||||
|
||||
// Do not propagate empty dropdown values.
|
||||
// This prevents wiping rows when a top Select2 is empty/not fully initialized.
|
||||
if (input.tagName === "SELECT" && value === "") {
|
||||
console.warn(
|
||||
"[gridRenderer] Empty select propagation blocked:",
|
||||
column,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache selected label so re-render can show text instead of only ID.
|
||||
if (value && $(input).hasClass("select2-hidden-accessible")) {
|
||||
const label = $(input).find("option:selected").text();
|
||||
|
||||
if (label && label !== value) {
|
||||
clientNameCache[value] = label;
|
||||
// Also cache for SceltaMultipla
|
||||
if (
|
||||
column === "idclient" ||
|
||||
column === "cliente_fornitore_id" ||
|
||||
input.classList.contains("searchable-client")
|
||||
) {
|
||||
clientNameCache[value] = label;
|
||||
}
|
||||
|
||||
const fieldId = input.dataset?.fieldId;
|
||||
if (fieldId)
|
||||
if (fieldId) {
|
||||
dropdownNameCache[fieldId + "_" + value] = label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const col = columns[colIndex] || null;
|
||||
const col = colIndex !== null ? columns[colIndex] : null;
|
||||
|
||||
console.log("[gridRenderer] Propagating ONLY:", {
|
||||
column: column,
|
||||
colIndex: colIndex,
|
||||
value: value,
|
||||
label:
|
||||
input.tagName === "SELECT"
|
||||
? $(input).find("option:selected").text()
|
||||
: value,
|
||||
});
|
||||
|
||||
if (column === "idclient") {
|
||||
data.forEach((row) => {
|
||||
const oldClientId = row.idclient || "";
|
||||
|
||||
row.idclient = value;
|
||||
|
||||
// Clear ClienteResponsabile only if client really changed.
|
||||
if (
|
||||
String(oldClientId) !== String(value) &&
|
||||
row.fixedFields &&
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
row.fixedFields,
|
||||
"ClienteResponsabile",
|
||||
)
|
||||
) {
|
||||
row.fixedFields["ClienteResponsabile"] = "";
|
||||
}
|
||||
|
||||
row._dirty = true;
|
||||
});
|
||||
} else if (column === "cliente_fornitore_id") {
|
||||
|
||||
loadFixedFieldOptions("ClienteResponsabile", value).then(() => {
|
||||
refreshTopDependentFixedSelect(
|
||||
"ClienteResponsabile",
|
||||
value,
|
||||
);
|
||||
renderVisibleRows();
|
||||
updateDirtyIndicator();
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (column === "cliente_fornitore_id") {
|
||||
data.forEach((row) => {
|
||||
row.cliente_fornitore_id = value;
|
||||
row._dirty = true;
|
||||
});
|
||||
} else if (column && column.startsWith("fixed_")) {
|
||||
|
||||
renderVisibleRows();
|
||||
updateDirtyIndicator();
|
||||
return;
|
||||
}
|
||||
|
||||
if (column && column.startsWith("fixed_")) {
|
||||
const fixedKey = column.replace("fixed_", "");
|
||||
|
||||
data.forEach((row) => {
|
||||
if (!row.fixedFields) row.fixedFields = {};
|
||||
row.fixedFields[fixedKey] = value;
|
||||
row._dirty = true;
|
||||
});
|
||||
} else if (col) {
|
||||
if (col.type === "detail" || col.type === "main_field") {
|
||||
data.forEach((row) => {
|
||||
row.details[col.key] = value;
|
||||
if (col.type === "main_field")
|
||||
row.mainFieldValue = value;
|
||||
row._dirty = true;
|
||||
});
|
||||
}
|
||||
|
||||
renderVisibleRows();
|
||||
updateDirtyIndicator();
|
||||
return;
|
||||
}
|
||||
|
||||
renderVisibleRows();
|
||||
if (col && (col.type === "detail" || col.type === "main_field")) {
|
||||
data.forEach((row) => {
|
||||
if (!row.details) row.details = {};
|
||||
row.details[col.key] = value;
|
||||
|
||||
if (col.type === "main_field") {
|
||||
row.mainFieldValue = value;
|
||||
}
|
||||
|
||||
row._dirty = true;
|
||||
});
|
||||
|
||||
renderVisibleRows();
|
||||
updateDirtyIndicator();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Select2 change events (don't bubble via native addEventListener)
|
||||
@@ -1086,6 +1445,8 @@
|
||||
if (colType === "idclient") {
|
||||
data[rowIndex].idclient = value;
|
||||
data[rowIndex]._dirty = true;
|
||||
|
||||
refreshDependentFixedFieldsForRow(rowIndex);
|
||||
} else if (colType === "cliente_fornitore_id") {
|
||||
data[rowIndex].cliente_fornitore_id = value;
|
||||
data[rowIndex]._dirty = true;
|
||||
@@ -1094,14 +1455,39 @@
|
||||
updateDirtyIndicator();
|
||||
});
|
||||
|
||||
// Cache labels on SceltaMultipla change
|
||||
// Handle SceltaMultipla changes and persist them into gridData.
|
||||
// Without this, a later renderVisibleRows() can rebuild the row with the old empty value.
|
||||
$(rowContainer).on("change", ".searchable-dropdown", function () {
|
||||
const val = $(this).val();
|
||||
const cell = this.closest(".grid-cell");
|
||||
if (!cell || !cell.dataset.row) return;
|
||||
|
||||
const rowIndex = parseInt(cell.dataset.row, 10);
|
||||
const colType = cell.dataset.colType;
|
||||
const colKey = cell.dataset.col;
|
||||
const val = $(this).val() || "";
|
||||
const fieldId = this.dataset.fieldId;
|
||||
|
||||
// Cache label so re-render shows the text instead of only the ID.
|
||||
if (val && fieldId) {
|
||||
const label = $(this).find("option:selected").text();
|
||||
if (label && label !== val)
|
||||
if (label && label !== val) {
|
||||
dropdownNameCache[fieldId + "_" + val] = label;
|
||||
}
|
||||
}
|
||||
|
||||
// Persist value into gridData.
|
||||
if (colType === "detail" || colType === "main_field") {
|
||||
if (!data[rowIndex].details) data[rowIndex].details = {};
|
||||
|
||||
data[rowIndex].details[String(colKey)] = val;
|
||||
|
||||
if (colType === "main_field") {
|
||||
data[rowIndex].mainFieldValue = val;
|
||||
}
|
||||
|
||||
data[rowIndex]._dirty = true;
|
||||
cell.classList.add("cell-changed");
|
||||
updateDirtyIndicator();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -337,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 -->
|
||||
@@ -389,6 +389,42 @@ 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');
|
||||
|
||||
+118
-63
@@ -20,9 +20,10 @@ $db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
// Recupera tutti i mapping dal template, includendo is_visible_import
|
||||
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field, is_visible_import, auto_value
|
||||
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, field_order, main_field, is_visible_import, auto_value
|
||||
FROM template_mapping
|
||||
WHERE template_id = ?");
|
||||
WHERE template_id = ?
|
||||
ORDER BY field_order ASC, id ASC");
|
||||
$stmt->execute([$template_id]);
|
||||
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
@@ -55,15 +56,22 @@ if (empty($allMappings)) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Trova il campo main_field
|
||||
$mainFieldMapping = null;
|
||||
// Find up to 2 main fields
|
||||
$mainFieldMappings = [];
|
||||
|
||||
foreach ($allMappings as $mapping) {
|
||||
if ($mapping['main_field'] == 1 && $mapping['is_visible_import'] == 1) {
|
||||
$mainFieldMapping = $mapping;
|
||||
if ((string)$mapping['main_field'] === '1' && (int)$mapping['is_visible_import'] === 1) {
|
||||
$mainFieldMappings[] = $mapping;
|
||||
}
|
||||
|
||||
if (count($mainFieldMappings) >= 2) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Backward compatibility: first main field
|
||||
$mainFieldMapping = $mainFieldMappings[0] ?? null;
|
||||
|
||||
// Recupera l'idclient di default dal template (se presente)
|
||||
$template_stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
|
||||
$template_stmt->execute([$template_id]);
|
||||
@@ -91,7 +99,7 @@ $stmt = $pdo->prepare("
|
||||
FROM datadb d
|
||||
LEFT JOIN auth_users u ON d.user_id = u.id
|
||||
{$baseWhere}
|
||||
ORDER BY d.iddatadb DESC
|
||||
ORDER BY d.excelrow ASC, d.iddatadb ASC
|
||||
{$limitClause}
|
||||
");
|
||||
$stmt->execute($baseParams);
|
||||
@@ -224,11 +232,18 @@ foreach ($importedData as $index => $row) {
|
||||
$rowObj['details'][(string)$d['mapping_id']] = $d['field_value'] ?? '';
|
||||
}
|
||||
|
||||
// Main field value
|
||||
// Main field values
|
||||
foreach ($mainFieldMappings as $mainMapping) {
|
||||
$mainDetail = array_filter($rowDetails, fn($d) => $d['mapping_id'] == $mainMapping['id']);
|
||||
$mainDetail = reset($mainDetail) ?: ['field_value' => $mainMapping['manual_default'] ?? ''];
|
||||
|
||||
$rowObj['details'][(string)$mainMapping['id']] =
|
||||
$mainDetail['field_value'] ?? $mainMapping['manual_default'] ?? '';
|
||||
}
|
||||
|
||||
// Backward compatibility: first main value
|
||||
if ($mainFieldMapping) {
|
||||
$mainDetail = array_filter($rowDetails, fn($d) => $d['mapping_id'] == $mainFieldMapping['id']);
|
||||
$mainDetail = reset($mainDetail) ?: ['field_value' => $mainFieldMapping['manual_default'] ?? ''];
|
||||
$rowObj['mainFieldValue'] = $mainDetail['field_value'] ?? $mainFieldMapping['manual_default'] ?? '';
|
||||
$rowObj['mainFieldValue'] = $rowObj['details'][(string)$mainFieldMapping['id']] ?? '';
|
||||
}
|
||||
|
||||
$rowObj['_dirty'] = false;
|
||||
@@ -238,18 +253,27 @@ foreach ($importedData as $index => $row) {
|
||||
// Build columns in display order
|
||||
$gridColumns = [];
|
||||
|
||||
// 1. Main field
|
||||
if ($mainFieldMapping) {
|
||||
$gridColumns[] = [
|
||||
'type' => 'main_field',
|
||||
'key' => (string)$mainFieldMapping['id'],
|
||||
'label' => $mainFieldMapping['field_label'],
|
||||
'dataType' => $mainFieldMapping['data_type'],
|
||||
'isManual' => (bool)$mainFieldMapping['is_manual'],
|
||||
'isRequired' => (bool)$mainFieldMapping['is_required'],
|
||||
'fieldId' => $mainFieldMapping['field_id'] ?? null,
|
||||
'width' => 150,
|
||||
];
|
||||
// 1. Main fields first, immediately after buttons
|
||||
foreach ($allMappings as $mapping) {
|
||||
if (
|
||||
(int)$mapping['is_visible_import'] === 1
|
||||
&& (string)$mapping['main_field'] === '1'
|
||||
&& trim((string)$mapping['field_label']) !== 'Tested Component:'
|
||||
) {
|
||||
$gridColumns[] = [
|
||||
'type' => 'main_field',
|
||||
'key' => (string)$mapping['id'],
|
||||
'label' => $mapping['field_label'],
|
||||
'dataType' => $mapping['data_type'],
|
||||
'isManual' => (bool)$mapping['is_manual'],
|
||||
'isRequired' => (bool)$mapping['is_required'],
|
||||
'fieldId' => $mapping['field_id'] ?? null,
|
||||
'fieldOrder' => (int)($mapping['field_order'] ?? 9999),
|
||||
'manualDefault' => $mapping['manual_default'] ?? '',
|
||||
'autoValue' => $mapping['auto_value'] ?? 'none',
|
||||
'width' => 150,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Status
|
||||
@@ -261,50 +285,30 @@ $gridColumns[] = ['type' => 'idclient', 'key' => 'idclient', 'label' => 'Client'
|
||||
// 4. Cliente Fornitore
|
||||
$gridColumns[] = ['type' => 'cliente_fornitore_id', 'key' => 'cliente_fornitore_id', 'label' => $slugMapping['ClienteFornitore'] ?? 'ClienteFornitore', 'width' => 300];
|
||||
|
||||
// 5. Auto fields
|
||||
// 5. Other custom fields in schema order
|
||||
foreach ($allMappings as $mapping) {
|
||||
if (
|
||||
!$mapping['is_manual']
|
||||
&& $mapping['main_field'] != 1
|
||||
&& $mapping['is_visible_import'] == 1
|
||||
(int)$mapping['is_visible_import'] === 1
|
||||
&& (string)$mapping['main_field'] !== '1'
|
||||
&& trim((string)$mapping['field_label']) !== 'Tested Component:'
|
||||
) {
|
||||
$isMainField = ((string)$mapping['main_field'] === '1');
|
||||
|
||||
$gridColumns[] = [
|
||||
'type' => 'detail',
|
||||
'type' => $isMainField ? 'main_field' : 'detail',
|
||||
'key' => (string)$mapping['id'],
|
||||
'label' => $mapping['field_label'],
|
||||
'dataType' => $mapping['data_type'],
|
||||
'isManual' => false,
|
||||
'isManual' => (bool)$mapping['is_manual'],
|
||||
'isRequired' => (bool)$mapping['is_required'],
|
||||
'fieldId' => $mapping['field_id'] ?? null,
|
||||
'fieldOrder' => (int)($mapping['field_order'] ?? 9999),
|
||||
'manualDefault' => $mapping['manual_default'] ?? '',
|
||||
'autoValue' => $mapping['auto_value'] ?? 'none',
|
||||
'width' => 150,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Manual fields
|
||||
foreach ($allMappings as $mapping) {
|
||||
if (
|
||||
$mapping['is_manual']
|
||||
&& $mapping['main_field'] != 1
|
||||
&& $mapping['is_visible_import'] == 1
|
||||
&& trim((string)$mapping['field_label']) !== 'Tested Component:'
|
||||
) {
|
||||
$gridColumns[] = [
|
||||
'type' => 'detail',
|
||||
'key' => (string)$mapping['id'],
|
||||
'label' => $mapping['field_label'],
|
||||
'dataType' => $mapping['data_type'],
|
||||
'isManual' => true,
|
||||
'isRequired' => (bool)$mapping['is_required'],
|
||||
'fieldId' => $mapping['field_id'] ?? null,
|
||||
'manualDefault' => $mapping['manual_default'] ?? '',
|
||||
'width' => 150,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Tested Component
|
||||
$gridColumns[] = ['type' => 'tested_component', 'key' => 'tested_component', 'label' => 'Tested Component', 'width' => 150];
|
||||
|
||||
@@ -342,7 +346,8 @@ $gridMeta = [
|
||||
'slugMapping' => $slugMapping,
|
||||
'timeLabels' => $timeLabels,
|
||||
'columns' => $gridColumns,
|
||||
'mainFieldMapping' => $mainFieldMapping,
|
||||
'mainFieldMapping' => $mainFieldMapping,
|
||||
'mainFieldMappings' => $mainFieldMappings,
|
||||
'totalRows' => count($gridDataArray),
|
||||
];
|
||||
|
||||
@@ -350,6 +355,9 @@ $gridMeta = [
|
||||
<script>
|
||||
window.gridData = <?= json_encode($gridDataArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
window.gridMeta = <?= json_encode($gridMeta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
|
||||
// Visible records in the current imported.php page
|
||||
window.visibleIddatadbList = window.gridData.map(row => parseInt(row.iddatadb, 10)).filter(Boolean);
|
||||
</script>
|
||||
|
||||
<!doctype html>
|
||||
@@ -666,7 +674,33 @@ $gridMeta = [
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.grid-row .grid-header:nth-child(2) {
|
||||
<?php if (isset($mainFieldMappings) && count($mainFieldMappings) >= 2): ?>
|
||||
|
||||
/* Sticky second Main column - only when the template has 2 Main fields */
|
||||
.grid-top .grid-cell:nth-child(3),
|
||||
#gridHeaderContainer .grid-header:nth-child(3),
|
||||
.grid-row .grid-cell:nth-child(3) {
|
||||
position: sticky !important;
|
||||
left: 360px;
|
||||
z-index: 7;
|
||||
background: white;
|
||||
overflow: visible;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#gridHeaderContainer .grid-header:nth-child(3) {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.grid-row:nth-child(even) .grid-cell:nth-child(3) {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.grid-row:hover .grid-cell:nth-child(3) {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
<?php endif; ?>.grid-row .grid-header:nth-child(2) {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
@@ -1407,6 +1441,9 @@ $gridMeta = [
|
||||
const topScrollbar = document.getElementById('topScrollbar');
|
||||
const topScrollbarInner = document.getElementById('topScrollbarInner');
|
||||
const gridContainer = document.getElementById('gridContainer');
|
||||
const gridRowContainer = document.getElementById('gridRowContainer');
|
||||
const gridHeaderContainer = document.getElementById('gridHeaderContainer');
|
||||
const gridTopContainer = document.getElementById('gridTopContainer');
|
||||
|
||||
if (!topScrollbar || !topScrollbarInner || !gridContainer) return;
|
||||
|
||||
@@ -1414,14 +1451,22 @@ $gridMeta = [
|
||||
let syncingFromGrid = false;
|
||||
|
||||
function updateTopScrollbarWidth() {
|
||||
topScrollbarInner.style.width = gridContainer.scrollWidth + 'px';
|
||||
const realWidth = Math.max(
|
||||
gridContainer.scrollWidth,
|
||||
gridHeaderContainer ? gridHeaderContainer.scrollWidth : 0,
|
||||
gridTopContainer ? gridTopContainer.scrollWidth : 0,
|
||||
gridRowContainer ? gridRowContainer.scrollWidth : 0
|
||||
);
|
||||
|
||||
// Mostra la barra solo se serve davvero
|
||||
if (gridContainer.scrollWidth > gridContainer.clientWidth) {
|
||||
topScrollbarInner.style.width = realWidth + 'px';
|
||||
|
||||
if (realWidth > gridContainer.clientWidth) {
|
||||
topScrollbar.style.display = 'block';
|
||||
} else {
|
||||
topScrollbar.style.display = 'none';
|
||||
}
|
||||
|
||||
topScrollbar.scrollLeft = gridContainer.scrollLeft;
|
||||
}
|
||||
|
||||
topScrollbar.addEventListener('scroll', function() {
|
||||
@@ -1438,14 +1483,24 @@ $gridMeta = [
|
||||
syncingFromGrid = false;
|
||||
});
|
||||
|
||||
updateTopScrollbarWidth();
|
||||
|
||||
window.addEventListener('resize', updateTopScrollbarWidth);
|
||||
|
||||
// Ritarda un attimo per sicurezza, visto che la griglia viene renderizzata via JS
|
||||
setTimeout(updateTopScrollbarWidth, 200);
|
||||
setTimeout(updateTopScrollbarWidth, 600);
|
||||
setTimeout(updateTopScrollbarWidth, 1200);
|
||||
// Recalculate after JS grid rendering
|
||||
setTimeout(updateTopScrollbarWidth, 100);
|
||||
setTimeout(updateTopScrollbarWidth, 300);
|
||||
setTimeout(updateTopScrollbarWidth, 700);
|
||||
setTimeout(updateTopScrollbarWidth, 1500);
|
||||
|
||||
// Recalculate automatically when rows/header/top controls are rendered or changed
|
||||
const observer = new MutationObserver(updateTopScrollbarWidth);
|
||||
|
||||
if (gridContainer) {
|
||||
observer.observe(gridContainer, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,102 +6,147 @@
|
||||
<div>
|
||||
<h4 class="logo-text"><?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></h4>
|
||||
</div>
|
||||
<div class="toggle-icon ms-auto"><i class='bx bx-arrow-back'></i>
|
||||
<div class="toggle-icon ms-auto">
|
||||
<i class='bx bx-arrow-back'></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--navigation-->
|
||||
<ul class="metismenu" id="menu">
|
||||
<!-- user, admin, superuser menù -->
|
||||
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('User')) || (Auth::user()->hasRole('Superuser'))) : ?>
|
||||
|
||||
<?php
|
||||
$currentUser = Auth::user();
|
||||
|
||||
$isAdmin = $currentUser->hasRole('Admin');
|
||||
$isSuperUser = $currentUser->hasRole('SuperUser');
|
||||
$isUser = $currentUser->hasRole('User');
|
||||
|
||||
$canAccessMainMenu = $isAdmin || $isSuperUser || $isUser;
|
||||
$canAccessUserAdmin = $isAdmin || $isSuperUser;
|
||||
$canAccessTemplates = $isAdmin || $isSuperUser;
|
||||
?>
|
||||
|
||||
<!-- user, admin, superuser menu -->
|
||||
<?php if ($canAccessMainMenu) : ?>
|
||||
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon"><i class='bx bx-home-alt'></i>
|
||||
<div class="parent-icon">
|
||||
<i class='bx bx-home-alt'></i>
|
||||
</div>
|
||||
<div class="menu-title">Dashboard</div>
|
||||
</a>
|
||||
<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>XLS Import
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon"><i class="bx bx-category"></i>
|
||||
</div>
|
||||
<div class="menu-title">Templates</div>
|
||||
</a>
|
||||
<ul>
|
||||
<li> <a href="templates_dashboard.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?></a>
|
||||
</li>
|
||||
<li> <a href="insert_template_xls.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?></a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
<?php if ($canAccessTemplates) : ?>
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-category"></i>
|
||||
</div>
|
||||
<div class="menu-title">Templates</div>
|
||||
</a>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="templates_dashboard.php">
|
||||
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="insert_template_xls.php">
|
||||
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon"><i class="bx bx-category"></i>
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-category"></i>
|
||||
</div>
|
||||
<div class="menu-title">Other Functions</div>
|
||||
</a>
|
||||
<ul>
|
||||
<li> <a href="quotations.php"><i class='bx bx-radio-circle'></i><?php echo $quotationstitle; ?></a>
|
||||
<li>
|
||||
<a href="quotations.php">
|
||||
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($quotationstitle, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<?php if ($canAccessUserAdmin) : ?>
|
||||
<li class="menu-label">Administration</li>
|
||||
|
||||
|
||||
|
||||
<li>
|
||||
<a href="user-admin.php">
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-user-plus"></i>
|
||||
</div>
|
||||
<div class="menu-title">User Admin</div>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<li class="menu-label">Others</li>
|
||||
|
||||
|
||||
<li>
|
||||
<a href="https://helpdesk.cesoft.io" target="_blank">
|
||||
<div class="parent-icon"><i class="bx bx-support"></i>
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-support"></i>
|
||||
</div>
|
||||
<div class="menu-title">Support</div>
|
||||
</a>
|
||||
</li>
|
||||
<?php
|
||||
endif; ?>
|
||||
<!-- admin, superuser menù -->
|
||||
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('Superuser'))) : ?>
|
||||
<?php
|
||||
endif; ?>
|
||||
<!-- admin menù -->
|
||||
<?php if (Auth::user()->hasRole('Admin')) : ?>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<!-- admin only menu -->
|
||||
<?php if ($isAdmin) : ?>
|
||||
|
||||
<li class="menu-label">Admin Menù</li>
|
||||
|
||||
<li>
|
||||
<a href="../" target="_blank">
|
||||
<div class="parent-icon"><i class="bx bx-support"></i>
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-support"></i>
|
||||
</div>
|
||||
<div class="menu-title">User Management</div>
|
||||
</a>
|
||||
</li>
|
||||
<!-- <li>
|
||||
|
||||
<!--
|
||||
<li>
|
||||
<a href="template/index.html" target="_blank">
|
||||
<div class="parent-icon"><i class="bx bx-support"></i>
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-support"></i>
|
||||
</div>
|
||||
<div class="menu-title">Template</div>
|
||||
<div class="menu-title">Template</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="https://codervent.com/rocker/documentation/index.html" target="_blank">
|
||||
<div class="parent-icon"><i class="bx bx-folder"></i>
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-folder"></i>
|
||||
</div>
|
||||
<div class="menu-title">Documentation</div>
|
||||
</a>
|
||||
</li> -->
|
||||
<?php
|
||||
endif; ?>
|
||||
</li>
|
||||
-->
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
</ul>
|
||||
<!--end navigation-->
|
||||
</div>
|
||||
@@ -94,8 +94,14 @@
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item d-flex align-items-center" href="user-profile.php"><i class="bx bx-user fs-5"></i><span>Profile</span></a>
|
||||
</li>
|
||||
<li><a class="dropdown-item d-flex align-items-center" href="settings.php"><i class="bx bx-cog fs-5"></i><span>Settings</span></a>
|
||||
</li>
|
||||
<?php if ($user->hasRole('Admin') || $user->hasRole('SuperUser')): ?>
|
||||
<li>
|
||||
<a class="dropdown-item d-flex align-items-center" href="user-admin.php">
|
||||
<i class="bx bx-user-plus fs-5"></i>
|
||||
<span>User Admin</span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<li>
|
||||
<div class="dropdown-divider mb-0"></div>
|
||||
</li>
|
||||
|
||||
@@ -95,6 +95,21 @@ $apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
<input type="text" name="start_column" id="startColumn" class="form-control" value="A" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="xlsEndColumnWrapper">
|
||||
<label class="form-label">Last Column included</label>
|
||||
<input
|
||||
type="text"
|
||||
name="xls_end_column"
|
||||
id="xlsEndColumn"
|
||||
class="form-control"
|
||||
value=""
|
||||
placeholder="Example: AN">
|
||||
<small class="text-danger fw-semibold">
|
||||
Attention: if left empty, the system will read the entire XLS/XLSX sheet.
|
||||
Dirty Excel files may cause memory errors or timeout.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="xlsSheetNumberWrapper">
|
||||
<label class="form-label">XLS Sheet Number</label>
|
||||
<input
|
||||
@@ -253,11 +268,13 @@ $apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
const headerRowWrapper = document.getElementById("headerRowWrapper");
|
||||
const startColumnWrapper = document.getElementById("startColumnWrapper");
|
||||
const xlsEndColumnWrapper = document.getElementById("xlsEndColumnWrapper");
|
||||
const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper");
|
||||
const apiConfigWrapper = document.getElementById("apiConfigWrapper");
|
||||
|
||||
const headerRow = document.getElementById("headerRow");
|
||||
const startColumn = document.getElementById("startColumn");
|
||||
const xlsEndColumn = document.getElementById("xlsEndColumn");
|
||||
const xlsSheetIndex = document.getElementById("xlsSheetIndex");
|
||||
const apiConfigSelect = document.getElementById("apiConfigSelect");
|
||||
|
||||
@@ -295,14 +312,20 @@ $apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (isXls) {
|
||||
headerRowWrapper.style.display = 'block';
|
||||
startColumnWrapper.style.display = 'block';
|
||||
xlsEndColumnWrapper.style.display = 'block';
|
||||
xlsSheetNumberWrapper.style.display = 'block';
|
||||
|
||||
headerRow.required = true;
|
||||
startColumn.required = true;
|
||||
xlsSheetIndex.required = true;
|
||||
|
||||
// Last Column is optional.
|
||||
// If empty, the import will read the entire sheet.
|
||||
xlsEndColumn.required = false;
|
||||
|
||||
headerRow.disabled = false;
|
||||
startColumn.disabled = false;
|
||||
xlsEndColumn.disabled = false;
|
||||
xlsSheetIndex.disabled = false;
|
||||
|
||||
apiConfigWrapper.style.display = 'none';
|
||||
@@ -312,16 +335,21 @@ $apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} else {
|
||||
headerRowWrapper.style.display = 'none';
|
||||
startColumnWrapper.style.display = 'none';
|
||||
xlsEndColumnWrapper.style.display = 'none';
|
||||
xlsSheetNumberWrapper.style.display = 'none';
|
||||
|
||||
headerRow.required = false;
|
||||
startColumn.required = false;
|
||||
xlsEndColumn.required = false;
|
||||
xlsSheetIndex.required = false;
|
||||
|
||||
headerRow.disabled = true;
|
||||
startColumn.disabled = true;
|
||||
xlsEndColumn.disabled = true;
|
||||
xlsSheetIndex.disabled = true;
|
||||
|
||||
xlsEndColumn.value = '';
|
||||
|
||||
if (isApiJson) {
|
||||
apiConfigWrapper.style.display = 'block';
|
||||
apiConfigSelect.required = true;
|
||||
|
||||
@@ -57,7 +57,8 @@ $isSchemajsonEmpty = empty(trim($template['schemajson'] ?? ''));
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
id,
|
||||
field_id,
|
||||
field_id,
|
||||
field_order,
|
||||
excel_column,
|
||||
json_node,
|
||||
is_manual,
|
||||
@@ -79,6 +80,7 @@ $stmt = $pdo->prepare("
|
||||
is_visible_parts
|
||||
FROM template_mapping
|
||||
WHERE template_id = ?
|
||||
ORDER BY field_order ASC, id ASC
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$mappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
@@ -205,28 +207,37 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
}
|
||||
|
||||
/* Type */
|
||||
/* Order */
|
||||
#schemaFieldsTable th:nth-child(5),
|
||||
#schemaFieldsTable td:nth-child(5) {
|
||||
width: 70px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Type */
|
||||
#schemaFieldsTable th:nth-child(6),
|
||||
#schemaFieldsTable td:nth-child(6) {
|
||||
width: 120px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Mapping = wide but NOT insane */
|
||||
#schemaFieldsTable th:nth-child(6),
|
||||
#schemaFieldsTable td:nth-child(6) {
|
||||
#schemaFieldsTable th:nth-child(7),
|
||||
#schemaFieldsTable td:nth-child(7) {
|
||||
width: 380px;
|
||||
}
|
||||
|
||||
/* Default Value = wider */
|
||||
#schemaFieldsTable th:nth-child(7),
|
||||
#schemaFieldsTable td:nth-child(7) {
|
||||
#schemaFieldsTable th:nth-child(8),
|
||||
#schemaFieldsTable td:nth-child(8) {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
/* selects fill the cell */
|
||||
#schemaFieldsTable td:nth-child(6) .form-select,
|
||||
#schemaFieldsTable td:nth-child(7) .form-control,
|
||||
#schemaFieldsTable td:nth-child(7) .form-select {
|
||||
#schemaFieldsTable td:nth-child(7) .form-select,
|
||||
#schemaFieldsTable td:nth-child(8) .form-control,
|
||||
#schemaFieldsTable td:nth-child(8) .form-select {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
@@ -254,6 +265,47 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-results__option {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.select2-json-node {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.select2-json-value {
|
||||
color: #6c757d;
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.select2-json-row {
|
||||
display: block;
|
||||
margin: -6px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.select2-results__option:has(.select2-json-row.used-option) {
|
||||
background-color: #fff3cd !important;
|
||||
color: #856404 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.select2-results__option:has(.select2-json-row.used-option).select2-results__option--highlighted {
|
||||
background-color: #ffe69c !important;
|
||||
color: #856404 !important;
|
||||
}
|
||||
|
||||
.select2-json-row.used-option .select2-json-node::after {
|
||||
content: " (already used)";
|
||||
color: #856404;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -347,6 +399,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
<th style="width:45px; text-align:center;">Import</th>
|
||||
<th style="width:45px; text-align:center;">Parts</th>
|
||||
<th style="width:320px;">Title</th>
|
||||
<th style="width:70px; text-align:center;">Order</th>
|
||||
<th style="width:120px;">Type</th>
|
||||
<th><?php echo $sourceType === 'API' ? 'JSON Mapping' : 'Mapping'; ?></th>
|
||||
<th>Default Value</th>
|
||||
@@ -383,6 +436,12 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<td class="text-center">
|
||||
<?php echo (int)($mapping['field_order'] ?? 9999); ?>
|
||||
</td>
|
||||
|
||||
<td><?php echo htmlspecialchars($mapping['data_type'] ?? 'N/A'); ?></td>
|
||||
|
||||
<td><?php echo htmlspecialchars($mapping['data_type'] ?? 'N/A'); ?></td>
|
||||
|
||||
<td>
|
||||
@@ -1000,6 +1059,42 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
return escapeHtmlText(str);
|
||||
}
|
||||
|
||||
let jsonNodeLabels = {};
|
||||
|
||||
function getLastJsonNodeName(path) {
|
||||
return String(path || '')
|
||||
.replace(/\[\]/g, '')
|
||||
.split('.')
|
||||
.pop();
|
||||
}
|
||||
|
||||
function formatJsonSampleValue(value) {
|
||||
if (value === null || value === undefined) return '';
|
||||
|
||||
let text = '';
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
text = '[Array]';
|
||||
} else if (typeof value === 'object') {
|
||||
text = '{Object}';
|
||||
} else {
|
||||
text = String(value);
|
||||
}
|
||||
|
||||
text = text
|
||||
.replace(/[\r\n\t]+/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
if (text === '/' || text === '\\/') return '';
|
||||
|
||||
if (text.length > 38) {
|
||||
text = text.substring(0, 38) + '...';
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
function flattenJsonNodes(obj, prefix = '') {
|
||||
let nodes = [];
|
||||
|
||||
@@ -1018,6 +1113,14 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
if (value !== null && typeof value === 'object') {
|
||||
nodes = nodes.concat(flattenJsonNodes(value, path));
|
||||
} else {
|
||||
const sample = formatJsonSampleValue(value);
|
||||
const shortName = getLastJsonNodeName(path);
|
||||
|
||||
jsonNodeLabels[path] = {
|
||||
shortName: shortName,
|
||||
sample: sample
|
||||
};
|
||||
|
||||
nodes.push(path);
|
||||
}
|
||||
});
|
||||
@@ -1043,18 +1146,128 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
if (!clean) return '';
|
||||
|
||||
const isUsed = uniqueUsedNodes.includes(clean) && clean !== currentValue;
|
||||
const label = isUsed ? `⚠ ${clean} (already used)` : clean;
|
||||
const info = jsonNodeLabels[clean] || {};
|
||||
const shortName = info.shortName || getLastJsonNodeName(clean);
|
||||
const sample = info.sample || '';
|
||||
|
||||
let label = shortName;
|
||||
if (sample) {
|
||||
label += ` — ${sample}`;
|
||||
}
|
||||
|
||||
if (isUsed) {
|
||||
label = `⚠ ${label} (already used)`;
|
||||
}
|
||||
|
||||
const isSelected = clean === currentValue ? 'selected' : '';
|
||||
|
||||
return `<option value="${escapeHtmlAttr(clean)}" class="${isUsed ? 'used-option' : ''}" ${isSelected}>${escapeHtmlText(label)}</option>`;
|
||||
return `
|
||||
<option
|
||||
value="${escapeHtmlAttr(clean)}"
|
||||
data-short-name="${escapeHtmlAttr(shortName)}"
|
||||
data-sample="${escapeHtmlAttr(sample)}"
|
||||
data-full-path="${escapeHtmlAttr(clean)}"
|
||||
class="${isUsed ? 'used-option' : ''}"
|
||||
data-used="${isUsed ? '1' : '0'}"
|
||||
${isSelected}>
|
||||
${escapeHtmlText(label)}
|
||||
</option>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
select.innerHTML = '<option value="">Select JSON Node</option>' + options;
|
||||
select.dataset.currentJson = currentValue;
|
||||
});
|
||||
|
||||
initSelect2ForJsonDropdowns();
|
||||
}
|
||||
|
||||
function initSelect2ForJsonDropdowns() {
|
||||
if (!(window.jQuery && $.fn.select2)) return;
|
||||
|
||||
$('.json-nodes').filter(function() {
|
||||
const tr = this.closest('tr');
|
||||
const mappingSelect = tr ? tr.querySelector('.mapping-select') : null;
|
||||
return mappingSelect && mappingSelect.value === 'json';
|
||||
}).each(function() {
|
||||
const $el = $(this);
|
||||
if (this.style.display === 'none') {
|
||||
return;
|
||||
}
|
||||
if ($el.hasClass('select2-hidden-accessible')) {
|
||||
$el.select2('destroy');
|
||||
}
|
||||
|
||||
$el.select2({
|
||||
width: '100%',
|
||||
placeholder: 'Select JSON Node',
|
||||
allowClear: true,
|
||||
dropdownAutoWidth: false,
|
||||
|
||||
templateResult: function(data) {
|
||||
if (!data.id) return data.text;
|
||||
|
||||
const option = data.element;
|
||||
const shortName = option.getAttribute('data-short-name') || data.text;
|
||||
const sample = option.getAttribute('data-sample') || '';
|
||||
const isUsed = option.getAttribute('data-used') === '1';
|
||||
|
||||
const $row = $('<span class="select2-json-row"></span>');
|
||||
if (isUsed) {
|
||||
$row.addClass('used-option');
|
||||
}
|
||||
$row.append(`<span class="select2-json-node">${escapeHtmlText(shortName)}</span>`);
|
||||
|
||||
if (sample) {
|
||||
$row.append(`<span class="select2-json-value">${escapeHtmlText(sample)}</span>`);
|
||||
}
|
||||
|
||||
return $row;
|
||||
},
|
||||
|
||||
templateSelection: function(data) {
|
||||
if (!data.id) return data.text;
|
||||
|
||||
const option = data.element;
|
||||
const shortName = option.getAttribute('data-short-name') || data.text;
|
||||
const sample = option.getAttribute('data-sample') || '';
|
||||
|
||||
if (sample) {
|
||||
return `${shortName} — ${sample}`;
|
||||
}
|
||||
|
||||
return shortName;
|
||||
},
|
||||
|
||||
matcher: function(params, data) {
|
||||
if ($.trim(params.term) === '') {
|
||||
return data;
|
||||
}
|
||||
|
||||
const term = params.term.toLowerCase();
|
||||
const option = data.element;
|
||||
|
||||
const fullPath = option?.getAttribute('data-full-path')?.toLowerCase() || '';
|
||||
const shortName = option?.getAttribute('data-short-name')?.toLowerCase() || '';
|
||||
const sample = option?.getAttribute('data-sample')?.toLowerCase() || '';
|
||||
|
||||
if (
|
||||
fullPath.includes(term) ||
|
||||
shortName.includes(term) ||
|
||||
sample.includes(term)
|
||||
) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function saveJsonNodes(sampleJson, nodes) {
|
||||
return fetch('update_api_json_nodes.php', {
|
||||
method: 'POST',
|
||||
@@ -1445,218 +1658,221 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
document.getElementById('updateSchemaButton').addEventListener('click', updateSchemaDetails);
|
||||
|
||||
document.getElementById('schemaFieldsBody').addEventListener('change', function(event) {
|
||||
if (event.target.classList.contains('mapping-select')) {
|
||||
let tr = event.target.closest('tr');
|
||||
let mappingId = event.target.getAttribute('data-id');
|
||||
if (!event.target.classList.contains('mapping-select')) return;
|
||||
|
||||
let xlsSelect = tr.querySelector('.xls-columns');
|
||||
let jsonSelect = tr.querySelector('.json-nodes');
|
||||
let manualInput = tr.querySelector('.manual-default');
|
||||
let autoSelect = tr.querySelector('.auto-value-select');
|
||||
const mappingSelect = event.target;
|
||||
const tr = mappingSelect.closest('tr');
|
||||
const mappingId = mappingSelect.getAttribute('data-id');
|
||||
|
||||
let mappedColumn = tr.querySelector('.mapped-column');
|
||||
let mappedJsonNode = tr.querySelector('.mapped-json-node');
|
||||
const xlsSelect = tr.querySelector('.xls-columns');
|
||||
const jsonSelect = tr.querySelector('.json-nodes');
|
||||
const manualInput = tr.querySelector('.manual-default');
|
||||
const autoSelect = tr.querySelector('.auto-value-select');
|
||||
|
||||
let removeBtn = tr.querySelector('.remove-xls');
|
||||
let removeJsonBtn = tr.querySelector('.remove-json');
|
||||
const mappedColumn = tr.querySelector('.mapped-column');
|
||||
const mappedJsonNode = tr.querySelector('.mapped-json-node');
|
||||
|
||||
if (event.target.value === 'xls') {
|
||||
if (xlsSelect) xlsSelect.style.display = 'block';
|
||||
if (jsonSelect) jsonSelect.style.display = 'none';
|
||||
if (autoSelect) autoSelect.style.display = 'none';
|
||||
const removeBtn = tr.querySelector('.remove-xls');
|
||||
const removeJsonBtn = tr.querySelector('.remove-json');
|
||||
|
||||
if (manualInput) {
|
||||
manualInput.style.display = 'none';
|
||||
manualInput.value = '';
|
||||
}
|
||||
|
||||
if (mappedColumn) mappedColumn.style.display = 'none';
|
||||
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
|
||||
|
||||
if (removeBtn) removeBtn.style.display = xlsSelect && xlsSelect.value ? 'inline-block' : 'none';
|
||||
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
|
||||
|
||||
} else if (event.target.value === 'json') {
|
||||
if (xlsSelect) xlsSelect.style.display = 'none';
|
||||
if (jsonSelect) jsonSelect.style.display = 'block';
|
||||
if (autoSelect) autoSelect.style.display = 'none';
|
||||
|
||||
if (manualInput) {
|
||||
manualInput.style.display = 'none';
|
||||
manualInput.value = '';
|
||||
}
|
||||
|
||||
if (mappedColumn) mappedColumn.style.display = 'none';
|
||||
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
|
||||
|
||||
if (removeBtn) removeBtn.style.display = 'none';
|
||||
if (removeJsonBtn) removeJsonBtn.style.display = jsonSelect && jsonSelect.value ? 'inline-block' : 'none';
|
||||
|
||||
} else if (event.target.value === 'manual') {
|
||||
if (xlsSelect) xlsSelect.style.display = 'none';
|
||||
if (jsonSelect) jsonSelect.style.display = 'none';
|
||||
if (autoSelect) autoSelect.style.display = 'none';
|
||||
|
||||
if (manualInput) {
|
||||
manualInput.style.display = 'block';
|
||||
}
|
||||
|
||||
if (mappedColumn) mappedColumn.style.display = 'none';
|
||||
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
|
||||
|
||||
if (removeBtn) removeBtn.style.display = 'none';
|
||||
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
|
||||
|
||||
} else if (event.target.value === 'auto') {
|
||||
if (xlsSelect) {
|
||||
xlsSelect.style.display = 'none';
|
||||
xlsSelect.value = '';
|
||||
}
|
||||
|
||||
if (jsonSelect) {
|
||||
jsonSelect.style.display = 'none';
|
||||
jsonSelect.value = '';
|
||||
}
|
||||
|
||||
if (manualInput) {
|
||||
manualInput.style.display = 'none';
|
||||
manualInput.value = '';
|
||||
}
|
||||
|
||||
if (mappedColumn) mappedColumn.style.display = 'none';
|
||||
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
|
||||
|
||||
if (removeBtn) removeBtn.style.display = 'none';
|
||||
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
|
||||
|
||||
if (autoSelect) autoSelect.style.display = 'block';
|
||||
|
||||
} else {
|
||||
if (xlsSelect) xlsSelect.style.display = 'none';
|
||||
if (jsonSelect) jsonSelect.style.display = 'none';
|
||||
if (autoSelect) autoSelect.style.display = 'none';
|
||||
|
||||
if (manualInput) {
|
||||
manualInput.style.display = 'none';
|
||||
manualInput.value = '';
|
||||
}
|
||||
|
||||
if (mappedColumn) mappedColumn.style.display = 'none';
|
||||
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
|
||||
|
||||
if (removeBtn) removeBtn.style.display = 'none';
|
||||
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
|
||||
function destroyJsonSelect2() {
|
||||
if (jsonSelect && window.jQuery && $(jsonSelect).hasClass('select2-hidden-accessible')) {
|
||||
$(jsonSelect).select2('destroy');
|
||||
}
|
||||
|
||||
saveMapping(
|
||||
mappingId,
|
||||
event.target.value,
|
||||
manualInput ? manualInput.value : '',
|
||||
xlsSelect ? xlsSelect.value : null,
|
||||
autoSelect ? autoSelect.value : null,
|
||||
jsonSelect ? jsonSelect.value : null
|
||||
);
|
||||
|
||||
if (sourceType === 'XLS') updateXlsDropdowns();
|
||||
if (sourceType === 'API') updateJsonDropdowns();
|
||||
|
||||
} else if (event.target.classList.contains('main-field-checkbox')) {
|
||||
const checkbox = event.target;
|
||||
const mappingId = checkbox.dataset.mappingId;
|
||||
const value = checkbox.checked ? 1 : 0;
|
||||
|
||||
// Se checked, deseleziona tutti gli altri visivamente
|
||||
if (checkbox.checked) {
|
||||
document.querySelectorAll('.main-field-checkbox').forEach(cb => {
|
||||
if (cb !== checkbox) cb.checked = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Salva l'aggiornamento
|
||||
fetch('update_main_field.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
template_id: <?php echo $id; ?>,
|
||||
mapping_id: mappingId,
|
||||
value: value
|
||||
})
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
console.error("❌ Error updating main_field:", data.message);
|
||||
checkbox.checked = !checkbox.checked;
|
||||
document.querySelectorAll('.main-field-checkbox').forEach(cb => {
|
||||
cb.checked = cb.dataset.originalChecked === 'true';
|
||||
});
|
||||
} else {
|
||||
document.querySelectorAll('.main-field-checkbox').forEach(cb => {
|
||||
cb.dataset.originalChecked = cb.checked;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("❌ Fetch error:", error);
|
||||
checkbox.checked = !checkbox.checked;
|
||||
document.querySelectorAll('.main-field-checkbox').forEach(cb => {
|
||||
cb.checked = cb.dataset.originalChecked === 'true';
|
||||
});
|
||||
});
|
||||
} else if (event.target.classList.contains('visible-parts-checkbox')) {
|
||||
const checkbox = event.target;
|
||||
const mappingId = checkbox.dataset.mappingId;
|
||||
const value = checkbox.checked ? 1 : 0;
|
||||
|
||||
// salva stato per rollback
|
||||
const prevChecked = checkbox.checked;
|
||||
|
||||
// ✅ UI: se sto mettendo a 1, tolgo la spunta a tutti gli altri SUBITO
|
||||
if (value === 1) {
|
||||
document.querySelectorAll('.visible-parts-checkbox').forEach(cb => {
|
||||
if (cb !== checkbox) cb.checked = false;
|
||||
});
|
||||
}
|
||||
|
||||
fetch('update_visible_parts.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
template_id: <?php echo $id; ?>,
|
||||
mapping_id: mappingId,
|
||||
value: value
|
||||
})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
console.error("❌ Error updating is_visible_parts:", data.message);
|
||||
|
||||
// rollback UI
|
||||
checkbox.checked = !prevChecked;
|
||||
|
||||
// se avevo tolto le spunte agli altri, ricarico per riallineare la UI al DB
|
||||
// (semplice e safe)
|
||||
location.reload();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("❌ Fetch error:", error);
|
||||
|
||||
// rollback UI
|
||||
checkbox.checked = !prevChecked;
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
if (mappingSelect.value === 'xls') {
|
||||
if (xlsSelect) xlsSelect.style.display = 'block';
|
||||
|
||||
destroyJsonSelect2();
|
||||
if (jsonSelect) {
|
||||
jsonSelect.style.display = 'none';
|
||||
jsonSelect.value = '';
|
||||
}
|
||||
|
||||
if (autoSelect) autoSelect.style.display = 'none';
|
||||
|
||||
if (manualInput) {
|
||||
manualInput.style.display = 'none';
|
||||
manualInput.value = '';
|
||||
}
|
||||
|
||||
if (mappedColumn) mappedColumn.style.display = 'none';
|
||||
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
|
||||
|
||||
if (removeBtn) removeBtn.style.display = xlsSelect && xlsSelect.value ? 'inline-block' : 'none';
|
||||
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
|
||||
|
||||
} else if (mappingSelect.value === 'json') {
|
||||
if (xlsSelect) {
|
||||
xlsSelect.style.display = 'none';
|
||||
xlsSelect.value = '';
|
||||
}
|
||||
|
||||
if (jsonSelect) {
|
||||
jsonSelect.style.display = 'block';
|
||||
}
|
||||
|
||||
if (autoSelect) autoSelect.style.display = 'none';
|
||||
|
||||
if (manualInput) {
|
||||
manualInput.style.display = 'none';
|
||||
manualInput.value = '';
|
||||
}
|
||||
|
||||
if (mappedColumn) mappedColumn.style.display = 'none';
|
||||
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
|
||||
|
||||
if (removeBtn) removeBtn.style.display = 'none';
|
||||
if (removeJsonBtn) removeJsonBtn.style.display = jsonSelect && jsonSelect.value ? 'inline-block' : 'none';
|
||||
|
||||
updateJsonDropdowns();
|
||||
|
||||
} else if (mappingSelect.value === 'manual') {
|
||||
if (xlsSelect) xlsSelect.style.display = 'none';
|
||||
|
||||
destroyJsonSelect2();
|
||||
if (jsonSelect) {
|
||||
jsonSelect.style.display = 'none';
|
||||
jsonSelect.value = '';
|
||||
}
|
||||
|
||||
if (autoSelect) autoSelect.style.display = 'none';
|
||||
|
||||
if (manualInput) {
|
||||
manualInput.style.display = 'block';
|
||||
}
|
||||
|
||||
if (mappedColumn) mappedColumn.style.display = 'none';
|
||||
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
|
||||
|
||||
if (removeBtn) removeBtn.style.display = 'none';
|
||||
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
|
||||
|
||||
} else if (mappingSelect.value === 'auto') {
|
||||
if (xlsSelect) {
|
||||
xlsSelect.style.display = 'none';
|
||||
xlsSelect.value = '';
|
||||
}
|
||||
|
||||
destroyJsonSelect2();
|
||||
if (jsonSelect) {
|
||||
jsonSelect.style.display = 'none';
|
||||
jsonSelect.value = '';
|
||||
}
|
||||
|
||||
if (manualInput) {
|
||||
manualInput.style.display = 'none';
|
||||
manualInput.value = '';
|
||||
}
|
||||
|
||||
if (mappedColumn) mappedColumn.style.display = 'none';
|
||||
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
|
||||
|
||||
if (removeBtn) removeBtn.style.display = 'none';
|
||||
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
|
||||
|
||||
if (autoSelect) autoSelect.style.display = 'block';
|
||||
|
||||
} else {
|
||||
if (xlsSelect) xlsSelect.style.display = 'none';
|
||||
|
||||
destroyJsonSelect2();
|
||||
if (jsonSelect) {
|
||||
jsonSelect.style.display = 'none';
|
||||
jsonSelect.value = '';
|
||||
}
|
||||
|
||||
if (autoSelect) autoSelect.style.display = 'none';
|
||||
|
||||
if (manualInput) {
|
||||
manualInput.style.display = 'none';
|
||||
manualInput.value = '';
|
||||
}
|
||||
|
||||
if (mappedColumn) mappedColumn.style.display = 'none';
|
||||
if (mappedJsonNode) mappedJsonNode.style.display = 'none';
|
||||
|
||||
if (removeBtn) removeBtn.style.display = 'none';
|
||||
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
saveMapping(
|
||||
mappingId,
|
||||
mappingSelect.value,
|
||||
manualInput ? manualInput.value : '',
|
||||
xlsSelect ? xlsSelect.value : null,
|
||||
autoSelect ? autoSelect.value : null,
|
||||
jsonSelect ? jsonSelect.value : null
|
||||
);
|
||||
|
||||
if (sourceType === 'XLS') updateXlsDropdowns();
|
||||
});
|
||||
|
||||
// Salva lo stato originale dei checkbox al caricamento
|
||||
document.querySelectorAll('.main-field-checkbox').forEach(cb => {
|
||||
cb.dataset.originalChecked = cb.checked;
|
||||
function saveJsonNodeSelection(jsonSelect) {
|
||||
if (!jsonSelect) return;
|
||||
|
||||
let tr = jsonSelect.closest('tr');
|
||||
let mappingId = jsonSelect.getAttribute('data-id');
|
||||
let manualInput = tr.querySelector('.manual-default');
|
||||
let mappedJsonNode = tr.querySelector('.mapped-json-node');
|
||||
let removeJsonBtn = tr.querySelector('.remove-json');
|
||||
let mappingSelect = tr.querySelector('.mapping-select');
|
||||
|
||||
if (mappingSelect && mappingSelect.value !== 'json') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mappedJsonNode) {
|
||||
mappedJsonNode = document.createElement('span');
|
||||
mappedJsonNode.className = 'mapped-json-node';
|
||||
mappedJsonNode.style.marginLeft = '5px';
|
||||
tr.querySelector('td:nth-child(7)').appendChild(mappedJsonNode);
|
||||
}
|
||||
|
||||
if (!removeJsonBtn) {
|
||||
removeJsonBtn = document.createElement('button');
|
||||
removeJsonBtn.className = 'btn btn-danger btn-sm remove-json';
|
||||
removeJsonBtn.textContent = 'X';
|
||||
removeJsonBtn.style.marginLeft = '5px';
|
||||
removeJsonBtn.setAttribute('data-id', mappingId);
|
||||
tr.querySelector('td:nth-child(7)').appendChild(removeJsonBtn);
|
||||
}
|
||||
|
||||
mappedJsonNode.textContent = jsonSelect.value ? `(${jsonSelect.value})` : '';
|
||||
mappedJsonNode.style.display = jsonSelect.value ? 'inline' : 'none';
|
||||
removeJsonBtn.style.display = jsonSelect.value ? 'inline-block' : 'none';
|
||||
|
||||
console.log('[JSON NODE SAVE]', {
|
||||
mappingId: mappingId,
|
||||
value: jsonSelect.value
|
||||
});
|
||||
|
||||
saveMapping(
|
||||
mappingId,
|
||||
'json',
|
||||
manualInput ? manualInput.value : '',
|
||||
null,
|
||||
null,
|
||||
jsonSelect.value
|
||||
);
|
||||
}
|
||||
|
||||
document.getElementById('schemaFieldsBody').addEventListener('change', function(event) {
|
||||
if (!event.target.classList.contains('json-nodes')) return;
|
||||
saveJsonNodeSelection(event.target);
|
||||
});
|
||||
|
||||
if (window.jQuery) {
|
||||
$(document).on('select2:select select2:clear', 'select.json-nodes', function() {
|
||||
saveJsonNodeSelection(this);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Save original Main checkbox state
|
||||
document.querySelectorAll('#schemaFieldsBody .main-field-checkbox').forEach(cb => {
|
||||
cb.dataset.originalChecked = cb.checked ? 'true' : 'false';
|
||||
});
|
||||
|
||||
// AUTO VALUE select change -> save auto_value
|
||||
@@ -1693,7 +1909,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
mappedColumn = document.createElement('span');
|
||||
mappedColumn.className = 'mapped-column';
|
||||
mappedColumn.style.marginLeft = '5px';
|
||||
tr.querySelector('td:nth-child(6)').appendChild(mappedColumn);
|
||||
tr.querySelector('td:nth-child(7)').appendChild(mappedColumn);
|
||||
}
|
||||
if (!removeBtn) {
|
||||
removeBtn = document.createElement('button');
|
||||
@@ -1701,7 +1917,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
removeBtn.textContent = 'X';
|
||||
removeBtn.style.marginLeft = '5px';
|
||||
removeBtn.setAttribute('data-id', mappingId);
|
||||
tr.querySelector('td:nth-child(6)').appendChild(removeBtn);
|
||||
tr.querySelector('td:nth-child(7)').appendChild(removeBtn);
|
||||
|
||||
removeBtn.addEventListener('click', function(e) {
|
||||
let tr = e.target.closest('tr');
|
||||
@@ -1735,51 +1951,6 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('schemaFieldsBody').addEventListener('change', function(event) {
|
||||
if (event.target.classList.contains('json-nodes')) {
|
||||
let tr = event.target.closest('tr');
|
||||
let mappingId = event.target.getAttribute('data-id');
|
||||
let manualInput = tr.querySelector('.manual-default');
|
||||
let mappedJsonNode = tr.querySelector('.mapped-json-node');
|
||||
let removeJsonBtn = tr.querySelector('.remove-json');
|
||||
|
||||
if (!mappedJsonNode) {
|
||||
mappedJsonNode = document.createElement('span');
|
||||
mappedJsonNode.className = 'mapped-json-node';
|
||||
mappedJsonNode.style.marginLeft = '5px';
|
||||
tr.querySelector('td:nth-child(6)').appendChild(mappedJsonNode);
|
||||
}
|
||||
|
||||
if (!removeJsonBtn) {
|
||||
removeJsonBtn = document.createElement('button');
|
||||
removeJsonBtn.className = 'btn btn-danger btn-sm remove-json';
|
||||
removeJsonBtn.textContent = 'X';
|
||||
removeJsonBtn.style.marginLeft = '5px';
|
||||
removeJsonBtn.setAttribute('data-id', mappingId);
|
||||
tr.querySelector('td:nth-child(6)').appendChild(removeJsonBtn);
|
||||
}
|
||||
|
||||
mappedJsonNode.textContent = event.target.value ? `(${event.target.value})` : '';
|
||||
mappedJsonNode.style.display = event.target.value ? 'inline' : 'none';
|
||||
removeJsonBtn.style.display = event.target.value ? 'inline-block' : 'none';
|
||||
|
||||
const mappingSelect = tr.querySelector('.mapping-select');
|
||||
if (mappingSelect && mappingSelect.value !== 'json') {
|
||||
return;
|
||||
}
|
||||
|
||||
saveMapping(
|
||||
mappingId,
|
||||
'json',
|
||||
manualInput ? manualInput.value : '',
|
||||
null,
|
||||
null,
|
||||
event.target.value
|
||||
);
|
||||
|
||||
updateJsonDropdowns();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('schemaFieldsBody').addEventListener('change', function(event) {
|
||||
if (event.target.classList.contains('manual-default') && event.target.tagName === 'SELECT') {
|
||||
@@ -1906,9 +2077,11 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
updateXlsDropdowns();
|
||||
}
|
||||
|
||||
if (data.success && mappingType === 'json' && jsonNode) {
|
||||
usedJsonNodesFromDB = usedJsonNodesFromDB.filter(node => node !== jsonNode);
|
||||
usedJsonNodesFromDB.push(jsonNode);
|
||||
if (data.success && mappingType === 'json') {
|
||||
usedJsonNodesFromDB = Array.from(document.querySelectorAll('select.json-nodes'))
|
||||
.map(select => select.value || select.dataset.currentJson || '')
|
||||
.filter(Boolean);
|
||||
|
||||
updateJsonDropdowns();
|
||||
}
|
||||
})
|
||||
@@ -2143,6 +2316,17 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
||||
}
|
||||
|
||||
if (sourceType === 'API' && availableJsonNodes.length) {
|
||||
const rawJson = document.getElementById('apiJsonExample')?.value || '';
|
||||
|
||||
if (rawJson.trim()) {
|
||||
try {
|
||||
jsonNodeLabels = {};
|
||||
flattenJsonNodes(JSON.parse(rawJson));
|
||||
} catch (e) {
|
||||
console.warn('Cannot rebuild JSON labels from sample JSON', e);
|
||||
}
|
||||
}
|
||||
|
||||
updateJsonDropdowns();
|
||||
}
|
||||
|
||||
|
||||
@@ -259,11 +259,9 @@ $matrixGroups = array_values($matrixGroups);
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mb-3">
|
||||
<div class="form-check m-0">
|
||||
<input class="form-check-input" type="checkbox" id="analysisWebOnly">
|
||||
<label class="form-check-label small" for="analysisWebOnly">
|
||||
Web only
|
||||
</label>
|
||||
<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;">
|
||||
|
||||
@@ -2,7 +2,24 @@
|
||||
<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">
|
||||
@@ -346,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ $(document).ready(function () {
|
||||
let quotations = [];
|
||||
let partsExtraField = null; // {field_id, field_label} oppure null
|
||||
let extraFieldOptions = []; // [{id,label}]
|
||||
let isLoadingPartsRecord = false;
|
||||
|
||||
// --- ROW ID helpers: niente più cache impazzita di jQuery .data() ---
|
||||
function getPartId($row) {
|
||||
@@ -509,21 +510,175 @@ $(document).ready(function () {
|
||||
// MODAL HANDLING
|
||||
// ===================
|
||||
function loadParts(iddatadb, idquotations, callback = null) {
|
||||
isLoadingPartsRecord = true;
|
||||
unsavedChanges = false;
|
||||
|
||||
// Store current modal context
|
||||
$("#partsModal").data("iddatadb", iddatadb || null);
|
||||
$("#partsModal").data("idquotations", idquotations || null);
|
||||
|
||||
// Store the visible record list from the main grid
|
||||
if (Array.isArray(window.visibleIddatadbList)) {
|
||||
$("#partsModal").data(
|
||||
"visible-iddatadb-list",
|
||||
window.visibleIddatadbList,
|
||||
);
|
||||
}
|
||||
|
||||
updatePartsRecordHeader(iddatadb);
|
||||
|
||||
const finishLoading = function () {
|
||||
unsavedChanges = false;
|
||||
isLoadingPartsRecord = false;
|
||||
|
||||
if (callback) callback();
|
||||
};
|
||||
|
||||
if (iddatadb) {
|
||||
loadMacroMatrici();
|
||||
initializeGlobalSelect2();
|
||||
loadPartsExtraField(iddatadb, function () {
|
||||
loadPhoto(iddatadb, idquotations);
|
||||
loadExistingParts(iddatadb, idquotations, callback);
|
||||
loadExistingParts(iddatadb, idquotations, finishLoading);
|
||||
});
|
||||
} else {
|
||||
loadPartsExtraField(iddatadb, function () {
|
||||
loadPhoto(iddatadb, idquotations);
|
||||
loadExistingParts(iddatadb, idquotations, callback);
|
||||
loadExistingParts(iddatadb, idquotations, finishLoading);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===================
|
||||
// PARTS MODAL RECORD NAVIGATION
|
||||
// ===================
|
||||
|
||||
function getVisiblePartsRecordList() {
|
||||
const listFromModal = $("#partsModal").data("visible-iddatadb-list");
|
||||
|
||||
if (Array.isArray(listFromModal) && listFromModal.length > 0) {
|
||||
return listFromModal.map((v) => parseInt(v, 10)).filter(Boolean);
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(window.visibleIddatadbList) &&
|
||||
window.visibleIddatadbList.length > 0
|
||||
) {
|
||||
return window.visibleIddatadbList
|
||||
.map((v) => parseInt(v, 10))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
if (Array.isArray(window.gridData) && window.gridData.length > 0) {
|
||||
return window.gridData
|
||||
.map((row) => parseInt(row.iddatadb, 10))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function getGridRecordById(iddatadb) {
|
||||
if (!Array.isArray(window.gridData)) return null;
|
||||
|
||||
return (
|
||||
window.gridData.find((row) => {
|
||||
return parseInt(row.iddatadb, 10) === parseInt(iddatadb, 10);
|
||||
}) || null
|
||||
);
|
||||
}
|
||||
|
||||
function getRecordHeaderLabel(iddatadb) {
|
||||
const record = getGridRecordById(iddatadb);
|
||||
|
||||
if (!record) {
|
||||
return iddatadb ? "#" + iddatadb : "";
|
||||
}
|
||||
|
||||
// Prefer main field value if available
|
||||
if (record.mainFieldValue) {
|
||||
return record.mainFieldValue;
|
||||
}
|
||||
|
||||
// Fallbacks
|
||||
if (record.importreferencecode) {
|
||||
return record.importreferencecode;
|
||||
}
|
||||
|
||||
if (record.filename_import) {
|
||||
return record.filename_import;
|
||||
}
|
||||
|
||||
return "#" + iddatadb;
|
||||
}
|
||||
|
||||
function updatePartsRecordHeader(iddatadb) {
|
||||
const list = getVisiblePartsRecordList();
|
||||
const currentId = parseInt(iddatadb, 10);
|
||||
const currentIndex = list.indexOf(currentId);
|
||||
|
||||
$("#trfHeader").text(getRecordHeaderLabel(currentId));
|
||||
|
||||
if (list.length <= 1 || currentIndex === -1) {
|
||||
$("#partsRecordCounter").text("-");
|
||||
$("#prevPartsRecordBtn").prop("disabled", true);
|
||||
$("#nextPartsRecordBtn").prop("disabled", true);
|
||||
return;
|
||||
}
|
||||
|
||||
$("#partsRecordCounter").text(currentIndex + 1 + " / " + list.length);
|
||||
|
||||
$("#prevPartsRecordBtn").prop("disabled", currentIndex <= 0);
|
||||
$("#nextPartsRecordBtn").prop(
|
||||
"disabled",
|
||||
currentIndex >= list.length - 1,
|
||||
);
|
||||
}
|
||||
|
||||
function goToAdjacentPartsRecord(direction) {
|
||||
const list = getVisiblePartsRecordList();
|
||||
const currentId = parseInt($("#partsModal").data("iddatadb"), 10);
|
||||
const currentIndex = list.indexOf(currentId);
|
||||
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
const nextIndex = currentIndex + direction;
|
||||
|
||||
if (nextIndex < 0 || nextIndex >= list.length) return;
|
||||
|
||||
if (
|
||||
!isLoadingPartsRecord &&
|
||||
unsavedChanges &&
|
||||
!confirm(
|
||||
"Hai modifiche non salvate. Vuoi cambiare record senza salvare?",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIddatadb = list[nextIndex];
|
||||
|
||||
// Reset local modal state before loading the next record
|
||||
partMatrice = {};
|
||||
unsavedChanges = false;
|
||||
$("#partsTableBody").empty();
|
||||
$("#photoSelectorContainer").empty().hide();
|
||||
$("#samplePhoto").attr("src", "");
|
||||
$(".temp-alert").remove();
|
||||
|
||||
loadParts(nextIddatadb, null);
|
||||
}
|
||||
|
||||
$(document).on("click", "#prevPartsRecordBtn", function (e) {
|
||||
e.preventDefault();
|
||||
goToAdjacentPartsRecord(-1);
|
||||
});
|
||||
|
||||
$(document).on("click", "#nextPartsRecordBtn", function (e) {
|
||||
e.preventDefault();
|
||||
goToAdjacentPartsRecord(1);
|
||||
});
|
||||
|
||||
// EVENTO PER APRIRE IL SECONDO MODALE
|
||||
$(document).on("click", "#openAnnotationsBtn", function () {
|
||||
console.log("Clic su Apri Annotazioni...");
|
||||
@@ -1090,7 +1245,10 @@ $(document).ready(function () {
|
||||
initializeExtraFieldSelect2($newRow);
|
||||
|
||||
updateRowButtons();
|
||||
markUnsaved();
|
||||
|
||||
if (!isLoadingPartsRecord) {
|
||||
markUnsaved();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================
|
||||
@@ -2142,6 +2300,8 @@ $(document).ready(function () {
|
||||
});
|
||||
|
||||
function markUnsaved() {
|
||||
if (isLoadingPartsRecord) return;
|
||||
|
||||
if (!unsavedChanges) {
|
||||
unsavedChanges = true;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,35 @@ require_once 'class/db-functions.php';
|
||||
|
||||
$response = ["success" => false, "message" => ""];
|
||||
|
||||
function excelColumnToIndex($column)
|
||||
{
|
||||
$column = strtoupper(trim((string)$column));
|
||||
|
||||
if ($column === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Numeric column index, example: 40
|
||||
if (ctype_digit($column)) {
|
||||
$index = (int)$column;
|
||||
return $index > 0 ? $index : null;
|
||||
}
|
||||
|
||||
// Excel column letters, example: A, AN, XFC
|
||||
if (!preg_match('/^[A-Z]+$/', $column)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$index = 0;
|
||||
$length = strlen($column);
|
||||
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$index = ($index * 26) + (ord($column[$i]) - ord('A') + 1);
|
||||
}
|
||||
|
||||
return $index;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
|
||||
throw new Exception("Invalid request method.");
|
||||
@@ -19,6 +48,8 @@ try {
|
||||
: null;
|
||||
|
||||
$start_column = trim($_POST['start_column'] ?? '');
|
||||
$xls_end_column = strtoupper(trim($_POST['xls_end_column'] ?? ''));
|
||||
$xls_end_column = $xls_end_column !== '' ? $xls_end_column : null;
|
||||
|
||||
$xls_sheet_index = isset($_POST['xls_sheet_index']) && $_POST['xls_sheet_index'] !== ''
|
||||
? intval($_POST['xls_sheet_index'])
|
||||
@@ -60,6 +91,24 @@ try {
|
||||
throw new Exception("XLS Sheet Number cannot be negative.");
|
||||
}
|
||||
|
||||
$startColumnIndex = excelColumnToIndex($start_column);
|
||||
|
||||
if ($startColumnIndex === null) {
|
||||
throw new Exception("Start Column is not valid. Use Excel column letters like A, AN or a positive number.");
|
||||
}
|
||||
|
||||
if ($xls_end_column !== null) {
|
||||
$endColumnIndex = excelColumnToIndex($xls_end_column);
|
||||
|
||||
if ($endColumnIndex === null) {
|
||||
throw new Exception("Last Column is not valid. Use Excel column letters like AN or a positive number.");
|
||||
}
|
||||
|
||||
if ($endColumnIndex < $startColumnIndex) {
|
||||
throw new Exception("Last Column cannot be before Start Column.");
|
||||
}
|
||||
}
|
||||
|
||||
$api_config_id = null;
|
||||
}
|
||||
|
||||
@@ -71,6 +120,7 @@ try {
|
||||
|
||||
$header_row = null;
|
||||
$start_column = null;
|
||||
$xls_end_column = null;
|
||||
$xls_sheet_index = null;
|
||||
}
|
||||
|
||||
@@ -78,6 +128,7 @@ try {
|
||||
if ($source_type === 'PDF') {
|
||||
$header_row = null;
|
||||
$start_column = null;
|
||||
$xls_end_column = null;
|
||||
$xls_sheet_index = null;
|
||||
$api_config_id = null;
|
||||
}
|
||||
@@ -109,6 +160,7 @@ try {
|
||||
source_type = ?,
|
||||
header_row = ?,
|
||||
start_column = ?,
|
||||
xls_end_column = ?,
|
||||
xls_sheet_index = ?,
|
||||
api_config_id = ?,
|
||||
description = ?,
|
||||
@@ -131,6 +183,7 @@ try {
|
||||
$source_type,
|
||||
$header_row,
|
||||
$start_column,
|
||||
$xls_end_column,
|
||||
$xls_sheet_index,
|
||||
$api_config_id,
|
||||
$description,
|
||||
|
||||
@@ -74,6 +74,7 @@ try {
|
||||
id,
|
||||
header_row,
|
||||
start_column,
|
||||
xls_end_column,
|
||||
xls_sheet_index,
|
||||
idroutine,
|
||||
idclient
|
||||
@@ -93,6 +94,12 @@ try {
|
||||
|
||||
$start_column_raw = $template['start_column'] ?? 'A';
|
||||
$start_column = normalizeColumnIndex($start_column_raw);
|
||||
$xls_end_column_raw = trim((string)($template['xls_end_column'] ?? ''));
|
||||
$xls_end_column = $xls_end_column_raw !== '' ? normalizeColumnIndex($xls_end_column_raw) : 0;
|
||||
|
||||
if ($xls_end_column > 0 && $xls_end_column < $start_column) {
|
||||
throw new Exception("Last Column cannot be before Start Column.");
|
||||
}
|
||||
|
||||
$xlsSheetIndex = isset($template['xls_sheet_index']) && $template['xls_sheet_index'] !== null
|
||||
? (int)$template['xls_sheet_index']
|
||||
@@ -109,7 +116,15 @@ try {
|
||||
// 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");
|
||||
error_log(
|
||||
"Template XLS settings - " .
|
||||
"header_row: $header_row, " .
|
||||
"start_column_raw: $start_column_raw, " .
|
||||
"start_column_index: $start_column, " .
|
||||
"xls_end_column_raw: " . ($xls_end_column_raw !== '' ? $xls_end_column_raw : '[empty = read all]') . ", " .
|
||||
"xls_end_column_index: " . ($xls_end_column > 0 ? $xls_end_column : '[no limit]') . ", " .
|
||||
"xls_sheet_index: $xlsSheetIndex"
|
||||
);
|
||||
|
||||
$file = $_FILES['excel_file'];
|
||||
$fileError = $file['error'];
|
||||
@@ -161,8 +176,33 @@ try {
|
||||
if (empty($mappings)) {
|
||||
$response['error'] = "Nessun mapping trovato per il template con ID $template_id";
|
||||
} else {
|
||||
// Carica il file rinominato con PHPSpreadsheet
|
||||
$spreadsheet = IOFactory::load($destination);
|
||||
// Load the XLS/XLSX file.
|
||||
// If Last Column is configured in the template, load only the configured column range.
|
||||
// If Last Column is empty, keep the original behavior and read the entire sheet.
|
||||
$reader = IOFactory::createReaderForFile($destination);
|
||||
$reader->setReadDataOnly(true);
|
||||
|
||||
if ($xls_end_column > 0) {
|
||||
$reader->setReadFilter(new class($start_column, $xls_end_column) implements \PhpOffice\PhpSpreadsheet\Reader\IReadFilter {
|
||||
private int $startColumn;
|
||||
private int $endColumn;
|
||||
|
||||
public function __construct(int $startColumn, int $endColumn)
|
||||
{
|
||||
$this->startColumn = $startColumn;
|
||||
$this->endColumn = $endColumn;
|
||||
}
|
||||
|
||||
public function readCell($columnAddress, $row, $worksheetName = ''): bool
|
||||
{
|
||||
$columnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($columnAddress);
|
||||
|
||||
return $columnIndex >= $this->startColumn && $columnIndex <= $this->endColumn;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$spreadsheet = $reader->load($destination);
|
||||
|
||||
$sheetCount = $spreadsheet->getSheetCount();
|
||||
$sheetNames = $spreadsheet->getSheetNames();
|
||||
@@ -193,6 +233,12 @@ try {
|
||||
$highestColumn = $worksheet->getHighestColumn();
|
||||
$highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
|
||||
|
||||
// Force the effective highest column to Last Column, if configured.
|
||||
if ($xls_end_column > 0) {
|
||||
$highestColumnIndex = $xls_end_column;
|
||||
$highestColumn = Coordinate::stringFromColumnIndex($highestColumnIndex);
|
||||
}
|
||||
|
||||
$startRow = max(1, $header_row);
|
||||
$startColumn = max(1, $start_column);
|
||||
|
||||
|
||||
@@ -4,6 +4,35 @@ require_once 'class/db-functions.php';
|
||||
|
||||
$response = ["success" => false, "message" => ""];
|
||||
|
||||
function excelColumnToIndex($column)
|
||||
{
|
||||
$column = strtoupper(trim((string)$column));
|
||||
|
||||
if ($column === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Numeric column index, example: 40
|
||||
if (ctype_digit($column)) {
|
||||
$index = (int)$column;
|
||||
return $index > 0 ? $index : null;
|
||||
}
|
||||
|
||||
// Excel column letters, example: A, AN, XFC
|
||||
if (!preg_match('/^[A-Z]+$/', $column)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$index = 0;
|
||||
$length = strlen($column);
|
||||
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$index = ($index * 26) + (ord($column[$i]) - ord('A') + 1);
|
||||
}
|
||||
|
||||
return $index;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
|
||||
throw new Exception("Invalid request method.");
|
||||
@@ -18,6 +47,8 @@ try {
|
||||
: null;
|
||||
|
||||
$start_column = trim($_POST['start_column'] ?? '');
|
||||
$xls_end_column = strtoupper(trim($_POST['xls_end_column'] ?? ''));
|
||||
$xls_end_column = $xls_end_column !== '' ? $xls_end_column : null;
|
||||
|
||||
$xls_sheet_index = isset($_POST['xls_sheet_index']) && $_POST['xls_sheet_index'] !== ''
|
||||
? intval($_POST['xls_sheet_index'])
|
||||
@@ -63,6 +94,24 @@ try {
|
||||
throw new Exception("XLS Sheet Number cannot be negative.");
|
||||
}
|
||||
|
||||
$startColumnIndex = excelColumnToIndex($start_column);
|
||||
|
||||
if ($startColumnIndex === null) {
|
||||
throw new Exception("Start Column is not valid. Use Excel column letters like A, AN or a positive number.");
|
||||
}
|
||||
|
||||
if ($xls_end_column !== null) {
|
||||
$endColumnIndex = excelColumnToIndex($xls_end_column);
|
||||
|
||||
if ($endColumnIndex === null) {
|
||||
throw new Exception("Last Column is not valid. Use Excel column letters like AN or a positive number.");
|
||||
}
|
||||
|
||||
if ($endColumnIndex < $startColumnIndex) {
|
||||
throw new Exception("Last Column cannot be before Start Column.");
|
||||
}
|
||||
}
|
||||
|
||||
$api_config_id = null;
|
||||
}
|
||||
|
||||
@@ -75,6 +124,7 @@ try {
|
||||
$header_row = null;
|
||||
$start_column = null;
|
||||
$xls_sheet_index = null;
|
||||
$xls_end_column = null;
|
||||
}
|
||||
|
||||
// PDF currently does not require XLS coordinates or API configuration
|
||||
@@ -82,6 +132,7 @@ try {
|
||||
$header_row = null;
|
||||
$start_column = null;
|
||||
$xls_sheet_index = null;
|
||||
$xls_end_column = null;
|
||||
$api_config_id = null;
|
||||
}
|
||||
|
||||
@@ -112,6 +163,7 @@ try {
|
||||
source_type,
|
||||
header_row,
|
||||
start_column,
|
||||
xls_end_column,
|
||||
xls_sheet_index,
|
||||
api_config_id,
|
||||
description,
|
||||
@@ -130,7 +182,7 @@ try {
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?,?,
|
||||
?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?,
|
||||
NOW(), NOW()
|
||||
@@ -142,6 +194,7 @@ try {
|
||||
$source_type,
|
||||
$header_row,
|
||||
$start_column,
|
||||
$xls_end_column,
|
||||
$xls_sheet_index,
|
||||
$api_config_id,
|
||||
$description,
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
@@ -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,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.");
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -882,6 +882,24 @@
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,7 +2,11 @@
|
||||
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; }
|
||||
if (!Auth::check()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
@@ -13,7 +17,8 @@ error_reporting(E_ALL);
|
||||
$fieldId = intval($_GET['field_id'] ?? 0);
|
||||
$q = mb_strtolower(trim($_GET['q'] ?? ''));
|
||||
$id = isset($_GET['id']) ? intval($_GET['id']) : null;
|
||||
$limit = max(1, min(50, intval($_GET['limit'] ?? 20)));
|
||||
$rawLimit = intval($_GET['limit'] ?? 20);
|
||||
$limit = $rawLimit <= 0 ? 0 : max(1, min(500, $rawLimit));
|
||||
|
||||
if (!$fieldId) {
|
||||
echo json_encode(['results' => []]);
|
||||
@@ -52,7 +57,7 @@ try {
|
||||
$text = $v['Valore'] ?? '';
|
||||
if ($q === '' || mb_strpos(mb_strtolower($text), $q) !== false) {
|
||||
$results[] = ['id' => $v['IdCustomFieldsValue'], 'text' => $text];
|
||||
if (count($results) >= $limit) break;
|
||||
if ($limit > 0 && count($results) >= $limit) break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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()
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ try {
|
||||
template_id,
|
||||
schema_id,
|
||||
field_id,
|
||||
field_order,
|
||||
data_type,
|
||||
is_required,
|
||||
default_value,
|
||||
@@ -97,6 +98,7 @@ try {
|
||||
:template_id,
|
||||
:schema_id,
|
||||
:field_id,
|
||||
:field_order,
|
||||
:data_type,
|
||||
:is_required,
|
||||
:default_value,
|
||||
@@ -116,6 +118,7 @@ try {
|
||||
UPDATE template_mapping
|
||||
SET
|
||||
schema_id = :schema_id,
|
||||
field_order = :field_order,
|
||||
data_type = :data_type,
|
||||
is_required = :is_required,
|
||||
default_value = :default_value,
|
||||
@@ -170,30 +173,11 @@ try {
|
||||
|
||||
$newDataType = $custom_field['Tipo'] ?? 'Testo';
|
||||
|
||||
/*
|
||||
* Required logic:
|
||||
* First read SchemiCustomFieldsDettagli.ObbligatorioWeb.
|
||||
*
|
||||
* Values:
|
||||
* - "Si" => required for this schema
|
||||
* - "No" => not required for this schema
|
||||
* - "Predefinito" => use CustomField.ObbligatorioWeb
|
||||
*/
|
||||
$schemaRequired = trim((string)($field['ObbligatorioWeb'] ?? 'Predefinito'));
|
||||
$customRequired = !empty($custom_field['ObbligatorioWeb']);
|
||||
|
||||
if (strcasecmp($schemaRequired, 'Si') === 0) {
|
||||
$isRequired = 1;
|
||||
} elseif (strcasecmp($schemaRequired, 'No') === 0) {
|
||||
$isRequired = 0;
|
||||
} else {
|
||||
$isRequired = $customRequired ? 1 : 0;
|
||||
}
|
||||
|
||||
$data = [
|
||||
':schema_id' => $schema_id,
|
||||
':field_order' => (int)($field['Ordine'] ?? 9999),
|
||||
':data_type' => $newDataType,
|
||||
':is_required' => $isRequired,
|
||||
':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,
|
||||
@@ -202,7 +186,7 @@ try {
|
||||
':max_value' => $custom_field['Massimo'] ?? null,
|
||||
':default_curr_date' => !empty($custom_field['DefaultCurrDate']) ? 1 : 0,
|
||||
':tablename' => $target_table,
|
||||
':field_label' => $custom_field['TitoloTraduzione'] ?? $custom_field['Titolo'] ?? ''
|
||||
':field_label' => $custom_field['TitoloTraduzione'] ?? ''
|
||||
];
|
||||
|
||||
$checkStmt->execute([$template_id, $fieldId]);
|
||||
|
||||
@@ -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()
|
||||
]);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,71 @@
|
||||
<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>xxx - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> - User Profile</title>
|
||||
|
||||
<!-- Select2 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
|
||||
<title>SmartTRF - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> - User Profile</title>
|
||||
|
||||
<style>
|
||||
.profile-avatar-preview {
|
||||
max-width: 100px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #e5e7eb;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.small-muted {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.select2-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.lims-loading {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.password-note {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e5e7eb;
|
||||
color: #475569;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.password-error {
|
||||
display: none;
|
||||
background: #fff1f2;
|
||||
border: 1px solid #fecdd3;
|
||||
color: #be123c;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.password-success {
|
||||
display: none;
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
color: #166534;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -18,9 +81,11 @@
|
||||
<!--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">
|
||||
@@ -31,61 +96,195 @@
|
||||
<div class="d-flex align-items-center">
|
||||
<div>
|
||||
<h6 class="mb-0">Edit Profile</h6>
|
||||
<small class="text-muted">Update your personal information and LIMS references.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form action="update-profile.php" method="POST" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label for="first_name" class="form-label">First Name</label>
|
||||
<input type="text" class="form-control" id="first_name" name="first_name" value="<?= htmlspecialchars($nameuser); ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="last_name" class="form-label">Last Name</label>
|
||||
<input type="text" class="form-control" id="last_name" name="last_name" value="<?= htmlspecialchars($surnameuser); ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input type="email" class="form-control" id="email" name="email" value="<?= htmlspecialchars($emailuser); ?>" required>
|
||||
</div>
|
||||
<form action="update-profile.php" method="POST" enctype="multipart/form-data" id="profileForm">
|
||||
<div class="row g-3">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="lims_user_id" class="form-label">Accettatore</label>
|
||||
<input type="number" class="form-control" id="lims_user_id" name="lims_user_id" value="<?= htmlspecialchars($lims_user_id ?? ''); ?>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="first_name" class="form-label">First Name</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="first_name"
|
||||
name="first_name"
|
||||
value="<?= htmlspecialchars($nameuser ?? '', ENT_QUOTES, 'UTF-8'); ?>"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="lims_global_user_id" class="form-label">LIMS Global</label>
|
||||
<input type="number" class="form-control" id="lims_global_user_id" name="lims_global_user_id" value="<?= htmlspecialchars($lims_global_user_id ?? ''); ?>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="last_name" class="form-label">Last Name</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="last_name"
|
||||
name="last_name"
|
||||
value="<?= htmlspecialchars($surnameuser ?? '', ENT_QUOTES, 'UTF-8'); ?>"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
name="email"
|
||||
value="<?= htmlspecialchars($emailuser ?? '', ENT_QUOTES, 'UTF-8'); ?>"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="lims_user_id" class="form-label">Accettatore</label>
|
||||
<select
|
||||
class="form-select"
|
||||
id="lims_user_id"
|
||||
name="lims_user_id"
|
||||
data-current-value="<?= htmlspecialchars($lims_user_id ?? '', ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<option value="">Select acceptor</option>
|
||||
</select>
|
||||
<div class="small-muted mt-1">
|
||||
Values loaded from CustomField ID 244.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="lims_global_user_id" class="form-label">LIMS Global</label>
|
||||
<select
|
||||
class="form-select"
|
||||
id="lims_global_user_id"
|
||||
name="lims_global_user_id"
|
||||
data-current-value="<?= htmlspecialchars($lims_global_user_id ?? '', ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<option value="">Select LIMS user</option>
|
||||
</select>
|
||||
<div class="small-muted mt-1">
|
||||
Values loaded from LIMS users list.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="lims-loading" id="limsLoadingMessage">
|
||||
Loading LIMS users and acceptors...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label for="avatar" class="form-label">Profile Picture</label>
|
||||
<input
|
||||
type="file"
|
||||
class="form-control"
|
||||
id="avatar"
|
||||
name="avatar"
|
||||
accept="image/jpeg,image/png,image/webp,image/gif">
|
||||
|
||||
<div class="mt-3 d-flex align-items-center gap-3">
|
||||
<?php if (!empty($photousername)): ?>
|
||||
<img
|
||||
src="../upload/users/<?= htmlspecialchars($photousername, ENT_QUOTES, 'UTF-8'); ?>"
|
||||
alt="Current Avatar"
|
||||
class="profile-avatar-preview"
|
||||
id="avatarPreview">
|
||||
<?php else: ?>
|
||||
<img
|
||||
src="../upload/users/profile.png"
|
||||
alt="Default Avatar"
|
||||
class="profile-avatar-preview"
|
||||
id="avatarPreview">
|
||||
<?php endif; ?>
|
||||
|
||||
<div>
|
||||
<div class="small-muted">Current avatar</div>
|
||||
<div class="small-muted">Allowed formats: JPG, PNG, WEBP, GIF.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="password" class="form-label">New Password</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="password"
|
||||
name="password"
|
||||
autocomplete="new-password">
|
||||
<button
|
||||
class="btn btn-outline-secondary toggle-password"
|
||||
type="button"
|
||||
data-target="password"
|
||||
aria-label="Show or hide password">
|
||||
<i class="bx bx-show"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="password_confirm" class="form-label">Confirm New Password</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="password_confirm"
|
||||
name="password_confirm"
|
||||
autocomplete="new-password">
|
||||
<button
|
||||
class="btn btn-outline-secondary toggle-password"
|
||||
type="button"
|
||||
data-target="password_confirm"
|
||||
aria-label="Show or hide password confirmation">
|
||||
<i class="bx bx-show"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="password-note" id="passwordNote">
|
||||
<strong>Important:</strong> leave both password fields empty to keep your current password unchanged.
|
||||
Type a new password only if you want to replace it.
|
||||
</div>
|
||||
|
||||
<div class="password-error mt-2" id="passwordError">
|
||||
The two password fields do not match. Please check them before saving.
|
||||
</div>
|
||||
|
||||
<div class="password-success mt-2" id="passwordSuccess">
|
||||
The two passwords match.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
name="iduserlogin"
|
||||
value="<?= htmlspecialchars($iduserlogin ?? '', ENT_QUOTES, 'UTF-8'); ?>">
|
||||
|
||||
<div class="col-md-12">
|
||||
<button type="submit" class="btn btn-primary" id="submitProfileButton">
|
||||
Update Profile
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="avatar" class="form-label">Profile Picture</label>
|
||||
<input type="file" class="form-control" id="avatar" name="avatar">
|
||||
<?php if ($photousername): ?>
|
||||
<p>Current avatar: <img src="..//upload/users/<?= htmlspecialchars($photousername); ?>" alt="Current Avatar" style="max-width: 100px;"></p>
|
||||
<?php else: ?>
|
||||
<p>Current avatar: <img src="..//upload/users/profile.png" alt="Default Avatar" style="max-width: 100px;"></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password (leave blank to keep current)</label>
|
||||
<input type="password" class="form-control" id="password" name="password">
|
||||
</div>
|
||||
<input type="hidden" name="iduserlogin" value="<?= htmlspecialchars($iduserlogin); ?>">
|
||||
<button type="submit" class="btn btn-primary">Update Profile</button>
|
||||
</form>
|
||||
</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>
|
||||
<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-->
|
||||
@@ -99,7 +298,389 @@
|
||||
<?php //include('include/themeswitcher.php');
|
||||
?>
|
||||
<!--end switcher-->
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
|
||||
<!-- Select2 JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const profileForm = document.getElementById('profileForm');
|
||||
const submitProfileButton = document.getElementById('submitProfileButton');
|
||||
|
||||
const passwordInput = document.getElementById('password');
|
||||
const passwordConfirmInput = document.getElementById('password_confirm');
|
||||
const passwordError = document.getElementById('passwordError');
|
||||
const passwordSuccess = document.getElementById('passwordSuccess');
|
||||
|
||||
const limsUserSelect = document.getElementById('lims_user_id');
|
||||
const limsGlobalUserSelect = document.getElementById('lims_global_user_id');
|
||||
const limsLoadingMessage = document.getElementById('limsLoadingMessage');
|
||||
const avatarInput = document.getElementById('avatar');
|
||||
const avatarPreview = document.getElementById('avatarPreview');
|
||||
|
||||
const currentLimsUserId = String(limsUserSelect.dataset.currentValue || '');
|
||||
const currentLimsGlobalUserId = String(limsGlobalUserSelect.dataset.currentValue || '');
|
||||
|
||||
const limsGlobalUsersEndpoint = 'get_utenti.php';
|
||||
const limsAcceptorsEndpoint = 'get_customfield_values.php?field_ids=244';
|
||||
|
||||
function validatePasswords(showEmptySuccess = false) {
|
||||
const password = passwordInput.value;
|
||||
const confirmPassword = passwordConfirmInput.value;
|
||||
|
||||
passwordError.style.display = 'none';
|
||||
passwordSuccess.style.display = 'none';
|
||||
passwordInput.classList.remove('is-invalid', 'is-valid');
|
||||
passwordConfirmInput.classList.remove('is-invalid', 'is-valid');
|
||||
|
||||
/*
|
||||
* Both empty means: keep current password.
|
||||
*/
|
||||
if (password === '' && confirmPassword === '') {
|
||||
submitProfileButton.disabled = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* One field filled and the other empty is not valid.
|
||||
*/
|
||||
if (password === '' || confirmPassword === '') {
|
||||
passwordError.textContent = 'Please fill both password fields, or leave both empty to keep the current password.';
|
||||
passwordError.style.display = 'block';
|
||||
passwordInput.classList.add('is-invalid');
|
||||
passwordConfirmInput.classList.add('is-invalid');
|
||||
submitProfileButton.disabled = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Both filled but different.
|
||||
*/
|
||||
if (password !== confirmPassword) {
|
||||
passwordError.textContent = 'The two password fields do not match. Please check them before saving.';
|
||||
passwordError.style.display = 'block';
|
||||
passwordInput.classList.add('is-invalid');
|
||||
passwordConfirmInput.classList.add('is-invalid');
|
||||
submitProfileButton.disabled = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Both filled and equal.
|
||||
*/
|
||||
passwordInput.classList.add('is-valid');
|
||||
passwordConfirmInput.classList.add('is-valid');
|
||||
passwordSuccess.style.display = showEmptySuccess ? 'block' : 'block';
|
||||
submitProfileButton.disabled = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
passwordInput.addEventListener('input', function() {
|
||||
validatePasswords();
|
||||
});
|
||||
|
||||
passwordConfirmInput.addEventListener('input', function() {
|
||||
validatePasswords();
|
||||
});
|
||||
|
||||
profileForm.addEventListener('submit', function(event) {
|
||||
if (!validatePasswords(true)) {
|
||||
event.preventDefault();
|
||||
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Password mismatch',
|
||||
text: 'Please check the password fields before saving.'
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
function initSelect2() {
|
||||
if (typeof $ === 'undefined') {
|
||||
console.error('jQuery is not loaded. Select2 cannot start.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof $.fn.select2 === 'undefined') {
|
||||
console.error('Select2 is not loaded. Check select2.min.js include.');
|
||||
return;
|
||||
}
|
||||
|
||||
$('#lims_user_id').select2({
|
||||
width: '100%',
|
||||
placeholder: 'Select acceptor',
|
||||
allowClear: true,
|
||||
minimumResultsForSearch: 0
|
||||
});
|
||||
|
||||
$('#lims_global_user_id').select2({
|
||||
width: '100%',
|
||||
placeholder: 'Select LIMS user',
|
||||
allowClear: true,
|
||||
minimumResultsForSearch: 0
|
||||
});
|
||||
}
|
||||
|
||||
function destroySelect2IfActive(selector) {
|
||||
if (typeof $ !== 'undefined' && $.fn.select2 && $(selector).hasClass('select2-hidden-accessible')) {
|
||||
$(selector).select2('destroy');
|
||||
}
|
||||
}
|
||||
|
||||
function refreshSelect2() {
|
||||
if (typeof $ === 'undefined' || typeof $.fn.select2 === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
destroySelect2IfActive('#lims_user_id');
|
||||
destroySelect2IfActive('#lims_global_user_id');
|
||||
|
||||
$('#lims_user_id').select2({
|
||||
width: '100%',
|
||||
placeholder: 'Select acceptor',
|
||||
allowClear: true,
|
||||
minimumResultsForSearch: 0
|
||||
});
|
||||
|
||||
$('#lims_global_user_id').select2({
|
||||
width: '100%',
|
||||
placeholder: 'Select LIMS user',
|
||||
allowClear: true,
|
||||
minimumResultsForSearch: 0
|
||||
});
|
||||
|
||||
$('#lims_user_id').trigger('change');
|
||||
$('#lims_global_user_id').trigger('change');
|
||||
}
|
||||
|
||||
function clearSelect(selectElement, placeholderText) {
|
||||
selectElement.innerHTML = '';
|
||||
|
||||
const option = document.createElement('option');
|
||||
option.value = '';
|
||||
option.textContent = placeholderText;
|
||||
selectElement.appendChild(option);
|
||||
}
|
||||
|
||||
function appendOption(selectElement, value, text) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const option = document.createElement('option');
|
||||
option.value = String(value);
|
||||
option.textContent = text || String(value);
|
||||
selectElement.appendChild(option);
|
||||
}
|
||||
|
||||
function setSelectValue(selectElement, value) {
|
||||
selectElement.value = value || '';
|
||||
|
||||
if (typeof $ !== 'undefined' && $.fn.select2) {
|
||||
$(selectElement).trigger('change');
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLimsGlobalUsers(data) {
|
||||
const list = Array.isArray(data) ? data : (data.value || []);
|
||||
const normalized = [];
|
||||
|
||||
list.forEach(function(item) {
|
||||
const id = item.IdUtente ?? item.id ?? item.ID ?? null;
|
||||
const text = item.Nome || item.Nominativo || item.UserName || item.Email || id;
|
||||
|
||||
if (id !== null && id !== undefined && id !== '') {
|
||||
normalized.push({
|
||||
id: String(id),
|
||||
text: String(text)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
normalized.sort(function(a, b) {
|
||||
return String(a.text).localeCompare(String(b.text), 'it', {
|
||||
sensitivity: 'base'
|
||||
});
|
||||
});
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeAcceptors(data) {
|
||||
const list = data['244'] || data[244] || [];
|
||||
const normalized = [];
|
||||
|
||||
list.forEach(function(item) {
|
||||
const id =
|
||||
item.IdCustomFieldValue ??
|
||||
item.IdCustomFieldsValue ??
|
||||
item.IdCustomFieldValues ??
|
||||
item.Id ??
|
||||
item.ID ??
|
||||
item.id ??
|
||||
item.ValueId ??
|
||||
item.value_id ??
|
||||
item.Codice ??
|
||||
null;
|
||||
|
||||
const text =
|
||||
item.Value ??
|
||||
item.Valore ??
|
||||
item.Nome ??
|
||||
item.Name ??
|
||||
item.Descrizione ??
|
||||
item.Description ??
|
||||
item.Text ??
|
||||
item.text ??
|
||||
item.Label ??
|
||||
item.label ??
|
||||
id;
|
||||
|
||||
if (id !== null && id !== undefined && id !== '') {
|
||||
normalized.push({
|
||||
id: String(id),
|
||||
text: String(text)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
normalized.sort(function(a, b) {
|
||||
return String(a.text).localeCompare(String(b.text), 'it', {
|
||||
sensitivity: 'base'
|
||||
});
|
||||
});
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function populateLimsGlobalUsers(users) {
|
||||
clearSelect(limsGlobalUserSelect, 'Select LIMS user');
|
||||
|
||||
users.forEach(function(user) {
|
||||
appendOption(limsGlobalUserSelect, user.id, user.text);
|
||||
});
|
||||
|
||||
if (currentLimsGlobalUserId !== '') {
|
||||
setSelectValue(limsGlobalUserSelect, currentLimsGlobalUserId);
|
||||
}
|
||||
}
|
||||
|
||||
function populateAcceptors(acceptors) {
|
||||
clearSelect(limsUserSelect, 'Select acceptor');
|
||||
|
||||
acceptors.forEach(function(item) {
|
||||
appendOption(limsUserSelect, item.id, item.text);
|
||||
});
|
||||
|
||||
if (currentLimsUserId !== '') {
|
||||
setSelectValue(limsUserSelect, currentLimsUserId);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLimsDropdowns() {
|
||||
try {
|
||||
const [globalResponse, acceptorsResponse] = await Promise.all([
|
||||
fetch(limsGlobalUsersEndpoint, {
|
||||
cache: 'no-store'
|
||||
}),
|
||||
fetch(limsAcceptorsEndpoint, {
|
||||
cache: 'no-store'
|
||||
})
|
||||
]);
|
||||
|
||||
if (!globalResponse.ok) {
|
||||
throw new Error('Unable to load LIMS global users.');
|
||||
}
|
||||
|
||||
if (!acceptorsResponse.ok) {
|
||||
throw new Error('Unable to load LIMS acceptors.');
|
||||
}
|
||||
|
||||
const globalData = await globalResponse.json();
|
||||
const acceptorsData = await acceptorsResponse.json();
|
||||
|
||||
const globalUsers = normalizeLimsGlobalUsers(globalData);
|
||||
const acceptors = normalizeAcceptors(acceptorsData);
|
||||
|
||||
populateLimsGlobalUsers(globalUsers);
|
||||
populateAcceptors(acceptors);
|
||||
refreshSelect2();
|
||||
|
||||
if (limsLoadingMessage) {
|
||||
limsLoadingMessage.textContent = 'LIMS users and acceptors loaded.';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
if (limsLoadingMessage) {
|
||||
limsLoadingMessage.textContent = 'Warning: unable to load LIMS users or acceptors.';
|
||||
}
|
||||
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'LIMS lists not loaded',
|
||||
text: error.message || 'Unable to load LIMS dropdown values.'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
avatarInput.addEventListener('change', function(event) {
|
||||
const file = event.target.files[0];
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(e) {
|
||||
avatarPreview.src = e.target.result;
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
document.querySelectorAll('.toggle-password').forEach(function(button) {
|
||||
button.addEventListener('click', function() {
|
||||
const targetId = button.dataset.target;
|
||||
const input = document.getElementById(targetId);
|
||||
const icon = button.querySelector('i');
|
||||
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
|
||||
if (icon) {
|
||||
icon.classList.remove('bx-show');
|
||||
icon.classList.add('bx-hide');
|
||||
}
|
||||
} else {
|
||||
input.type = 'password';
|
||||
|
||||
if (icon) {
|
||||
icon.classList.remove('bx-hide');
|
||||
icon.classList.add('bx-show');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
initSelect2();
|
||||
loadLimsDropdowns();
|
||||
validatePasswords();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Validate rows before export to LIMS.
|
||||
*
|
||||
@@ -88,6 +89,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 +157,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 +184,63 @@ 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 (excluding filed_id = 189 Tested component)
|
||||
$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
|
||||
AND id <> 189
|
||||
");
|
||||
$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 +250,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 = [];
|
||||
@@ -155,7 +274,6 @@ try {
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'results' => $results]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Validation error: " . $e->getMessage());
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
|
||||
Reference in New Issue
Block a user