Compare commits

...

6 Commits

15 changed files with 2588 additions and 97 deletions
+1
View File
@@ -61,3 +61,4 @@ public/userarea/schemi_base_response.json
*.log *.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 // Reindirizza in base al ruolo
if ($user->hasRole('Admin')) { if ($user->hasRole('Admin')) {
return redirect()->to('userarea/import_dashboard.php'); return redirect()->to('userarea/import_dashboard.php');
} elseif ($user->hasRole('SuperUser')) {
return redirect()->to('userarea/import_dashboard.php');
} elseif ($user->hasRole('User')) { } elseif ($user->hasRole('User')) {
return redirect()->to('userarea/import_dashboard.php'); return redirect()->to('userarea/import_dashboard.php');
} }
+39
View File
@@ -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'] ?? ''); ?>"> <input type="text" name="start_column" id="startColumn" class="form-control" value="<?php echo htmlspecialchars($template['start_column'] ?? ''); ?>">
</div> </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"> <div class="mb-3" id="xlsSheetNumberWrapper">
<label class="form-label">XLS Sheet Number</label> <label class="form-label">XLS Sheet Number</label>
<input <input
@@ -404,11 +419,13 @@ if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonText
const headerRowWrapper = document.getElementById("headerRowWrapper"); const headerRowWrapper = document.getElementById("headerRowWrapper");
const startColumnWrapper = document.getElementById("startColumnWrapper"); const startColumnWrapper = document.getElementById("startColumnWrapper");
const xlsEndColumnWrapper = document.getElementById("xlsEndColumnWrapper");
const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper"); const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper");
const apiConfigWrapper = document.getElementById("apiConfigWrapper"); const apiConfigWrapper = document.getElementById("apiConfigWrapper");
const headerRow = document.getElementById("headerRow"); const headerRow = document.getElementById("headerRow");
const startColumn = document.getElementById("startColumn"); const startColumn = document.getElementById("startColumn");
const xlsEndColumn = document.getElementById("xlsEndColumn");
const xlsSheetIndex = document.getElementById("xlsSheetIndex"); const xlsSheetIndex = document.getElementById("xlsSheetIndex");
const apiConfigSelect = document.getElementById("apiConfigSelect"); const apiConfigSelect = document.getElementById("apiConfigSelect");
@@ -444,13 +461,16 @@ if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonText
if (isXls) { if (isXls) {
headerRowWrapper.style.display = 'block'; headerRowWrapper.style.display = 'block';
startColumnWrapper.style.display = 'block'; startColumnWrapper.style.display = 'block';
xlsEndColumnWrapper.style.display = 'block';
xlsSheetNumberWrapper.style.display = 'block'; xlsSheetNumberWrapper.style.display = 'block';
headerRow.required = true; headerRow.required = true;
startColumn.required = true; startColumn.required = true;
xlsEndColumn.required = false;
headerRow.disabled = false; headerRow.disabled = false;
startColumn.disabled = false; startColumn.disabled = false;
xlsEndColumn.disabled = false;
xlsSheetIndex.disabled = false; xlsSheetIndex.disabled = false;
apiConfigWrapper.style.display = 'none'; apiConfigWrapper.style.display = 'none';
@@ -460,13 +480,16 @@ if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonText
} else { } else {
headerRowWrapper.style.display = 'none'; headerRowWrapper.style.display = 'none';
startColumnWrapper.style.display = 'none'; startColumnWrapper.style.display = 'none';
xlsEndColumnWrapper.style.display = 'none';
xlsSheetNumberWrapper.style.display = 'none'; xlsSheetNumberWrapper.style.display = 'none';
headerRow.required = false; headerRow.required = false;
startColumn.required = false; startColumn.required = false;
xlsEndColumn.required = false;
headerRow.disabled = true; headerRow.disabled = true;
startColumn.disabled = true; startColumn.disabled = true;
xlsEndColumn.disabled = true;
xlsSheetIndex.disabled = true; xlsSheetIndex.disabled = true;
if (isApiOrJson) { if (isApiOrJson) {
@@ -726,6 +749,22 @@ if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonText
return; 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", { fetch("process_edit_template_xls.php", {
method: "POST", method: "POST",
body: formData body: formData
+32
View File
@@ -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: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: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-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:
+70
View File
@@ -477,6 +477,71 @@ try {
$logFilePhotos = $logDir . "commessa_{$commessaId}_photos_step5_2_" . time() . ".txt"; $logFilePhotos = $logDir . "commessa_{$commessaId}_photos_step5_2_" . time() . ".txt";
$writeLog($logFilePhotos, $logContentPhotos, "STEP 6.2 - Photos (commessa={$commessaId})"); $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 // 🔹 STEP 7: Update Custom Fields for CommessaWeb
if (!empty($fieldValues)) { if (!empty($fieldValues)) {
// GET con espansione per CustomField // GET con espansione per CustomField
@@ -629,11 +694,15 @@ try {
"totalCampioni" => count($campioni), "totalCampioni" => count($campioni),
"totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []), "totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []),
"totalPhotos" => count($photos), "totalPhotos" => count($photos),
"totalAnalyses" => $totalAnalyses,
"addedAnalyses" => $addedAnalyses,
"failedAnalyses" => $failedAnalyses,
"message" => "Export successful", "message" => "Export successful",
"logFiles" => [ "logFiles" => [
"step5_create" => $logFileStep5, "step5_create" => $logFileStep5,
"step5_2_photos" => $logFilePhotos, "step5_2_photos" => $logFilePhotos,
"step6_campioni" => $logFileStep6, "step6_campioni" => $logFileStep6,
"step63_analyses" => $logFileStep63Analisi,
"step7_patch" => $logFileStep7 ?? null, "step7_patch" => $logFileStep7 ?? null,
"step9_1_importa" => $logFileStep91, "step9_1_importa" => $logFileStep91,
"step10_get" => $logFileStep10 "step10_get" => $logFileStep10
@@ -649,6 +718,7 @@ try {
"step5_create" => $logFileStep5 ?? null, "step5_create" => $logFileStep5 ?? null,
"step5_2_photos" => $logFilePhotos ?? null, "step5_2_photos" => $logFilePhotos ?? null,
"step6_campioni" => $logFileStep6 ?? null, "step6_campioni" => $logFileStep6 ?? null,
"step63_analyses" => $logFileStep63Analisi ?? null,
"step7_patch" => $logFileStep7 ?? null, "step7_patch" => $logFileStep7 ?? null,
"step9_1_importa" => $logFileStep91 ?? null, "step9_1_importa" => $logFileStep91 ?? null,
"step10_get" => $logFileStep10 ?? null "step10_get" => $logFileStep10 ?? null
+84 -39
View File
@@ -6,102 +6,147 @@
<div> <div>
<h4 class="logo-text"><?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></h4> <h4 class="logo-text"><?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></h4>
</div> </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>
</div> </div>
<!--navigation--> <!--navigation-->
<ul class="metismenu" id="menu"> <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> <li>
<a href="javascript:;" class="has-arrow"> <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>
<div class="menu-title">Dashboard</div> <div class="menu-title">Dashboard</div>
</a> </a>
<ul> <ul>
<!-- <li> <a href="index.php"><i class='bx bx-radio-circle'></i>Default</a> <li>
</li> --> <a href="import_dashboard.php">
<li> <a href="import_dashboard.php"><i class='bx bx-radio-circle'></i>XLS Import</a> <i class='bx bx-radio-circle'></i>XLS Import
</a>
</li> </li>
</ul> </ul>
</li> </li>
<?php if ($canAccessTemplates) : ?>
<li> <li>
<a href="javascript:;" class="has-arrow"> <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>
<div class="menu-title">Templates</div> <div class="menu-title">Templates</div>
</a> </a>
<ul> <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="templates_dashboard.php">
<li> <a href="insert_template_xls.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?></a> <i class='bx bx-radio-circle'></i><?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?>
</li> </a>
</ul>
</li> </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> <li>
<a href="javascript:;" class="has-arrow"> <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>
<div class="menu-title">Other Functions</div> <div class="menu-title">Other Functions</div>
</a> </a>
<ul> <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> </li>
</ul> </ul>
</li> </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 class="menu-label">Others</li>
<li> <li>
<a href="https://helpdesk.cesoft.io" target="_blank"> <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>
<div class="menu-title">Support</div> <div class="menu-title">Support</div>
</a> </a>
</li> </li>
<?php
endif; ?> <?php endif; ?>
<!-- admin, superuser menù -->
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('Superuser'))) : ?>
<?php <!-- admin only menu -->
endif; ?> <?php if ($isAdmin) : ?>
<!-- admin menù -->
<?php if (Auth::user()->hasRole('Admin')) : ?>
<li class="menu-label">Admin Menù</li> <li class="menu-label">Admin Menù</li>
<li> <li>
<a href="../" target="_blank"> <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>
<div class="menu-title">User Management</div> <div class="menu-title">User Management</div>
</a> </a>
</li> </li>
<!-- <li>
<!--
<li>
<a href="template/index.html" target="_blank"> <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>
<div class="menu-title">Template</div> <div class="menu-title">Template</div>
</a> </a>
</li> </li>
<li> <li>
<a href="https://codervent.com/rocker/documentation/index.html" target="_blank"> <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>
<div class="menu-title">Documentation</div> <div class="menu-title">Documentation</div>
</a> </a>
</li> --> </li>
<?php -->
endif; ?>
<?php endif; ?>
</ul> </ul>
<!--end navigation--> <!--end navigation-->
</div> </div>
+7 -1
View File
@@ -94,8 +94,14 @@
<ul class="dropdown-menu dropdown-menu-end"> <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><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>
<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> <?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> </li>
<?php endif; ?>
<li> <li>
<div class="dropdown-divider mb-0"></div> <div class="dropdown-divider mb-0"></div>
</li> </li>
+28
View File
@@ -95,6 +95,21 @@ $apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
<input type="text" name="start_column" id="startColumn" class="form-control" value="A" required> <input type="text" name="start_column" id="startColumn" class="form-control" value="A" required>
</div> </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"> <div class="mb-3" id="xlsSheetNumberWrapper">
<label class="form-label">XLS Sheet Number</label> <label class="form-label">XLS Sheet Number</label>
<input <input
@@ -253,11 +268,13 @@ $apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
const headerRowWrapper = document.getElementById("headerRowWrapper"); const headerRowWrapper = document.getElementById("headerRowWrapper");
const startColumnWrapper = document.getElementById("startColumnWrapper"); const startColumnWrapper = document.getElementById("startColumnWrapper");
const xlsEndColumnWrapper = document.getElementById("xlsEndColumnWrapper");
const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper"); const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper");
const apiConfigWrapper = document.getElementById("apiConfigWrapper"); const apiConfigWrapper = document.getElementById("apiConfigWrapper");
const headerRow = document.getElementById("headerRow"); const headerRow = document.getElementById("headerRow");
const startColumn = document.getElementById("startColumn"); const startColumn = document.getElementById("startColumn");
const xlsEndColumn = document.getElementById("xlsEndColumn");
const xlsSheetIndex = document.getElementById("xlsSheetIndex"); const xlsSheetIndex = document.getElementById("xlsSheetIndex");
const apiConfigSelect = document.getElementById("apiConfigSelect"); const apiConfigSelect = document.getElementById("apiConfigSelect");
@@ -295,14 +312,20 @@ $apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (isXls) { if (isXls) {
headerRowWrapper.style.display = 'block'; headerRowWrapper.style.display = 'block';
startColumnWrapper.style.display = 'block'; startColumnWrapper.style.display = 'block';
xlsEndColumnWrapper.style.display = 'block';
xlsSheetNumberWrapper.style.display = 'block'; xlsSheetNumberWrapper.style.display = 'block';
headerRow.required = true; headerRow.required = true;
startColumn.required = true; startColumn.required = true;
xlsSheetIndex.required = true; xlsSheetIndex.required = true;
// Last Column is optional.
// If empty, the import will read the entire sheet.
xlsEndColumn.required = false;
headerRow.disabled = false; headerRow.disabled = false;
startColumn.disabled = false; startColumn.disabled = false;
xlsEndColumn.disabled = false;
xlsSheetIndex.disabled = false; xlsSheetIndex.disabled = false;
apiConfigWrapper.style.display = 'none'; apiConfigWrapper.style.display = 'none';
@@ -312,16 +335,21 @@ $apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
} else { } else {
headerRowWrapper.style.display = 'none'; headerRowWrapper.style.display = 'none';
startColumnWrapper.style.display = 'none'; startColumnWrapper.style.display = 'none';
xlsEndColumnWrapper.style.display = 'none';
xlsSheetNumberWrapper.style.display = 'none'; xlsSheetNumberWrapper.style.display = 'none';
headerRow.required = false; headerRow.required = false;
startColumn.required = false; startColumn.required = false;
xlsEndColumn.required = false;
xlsSheetIndex.required = false; xlsSheetIndex.required = false;
headerRow.disabled = true; headerRow.disabled = true;
startColumn.disabled = true; startColumn.disabled = true;
xlsEndColumn.disabled = true;
xlsSheetIndex.disabled = true; xlsSheetIndex.disabled = true;
xlsEndColumn.value = '';
if (isApiJson) { if (isApiJson) {
apiConfigWrapper.style.display = 'block'; apiConfigWrapper.style.display = 'block';
apiConfigSelect.required = true; apiConfigSelect.required = true;
@@ -4,6 +4,35 @@ require_once 'class/db-functions.php';
$response = ["success" => false, "message" => ""]; $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 { try {
if ($_SERVER["REQUEST_METHOD"] !== "POST") { if ($_SERVER["REQUEST_METHOD"] !== "POST") {
throw new Exception("Invalid request method."); throw new Exception("Invalid request method.");
@@ -19,6 +48,8 @@ try {
: null; : null;
$start_column = trim($_POST['start_column'] ?? ''); $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'] !== '' $xls_sheet_index = isset($_POST['xls_sheet_index']) && $_POST['xls_sheet_index'] !== ''
? intval($_POST['xls_sheet_index']) ? intval($_POST['xls_sheet_index'])
@@ -60,6 +91,24 @@ try {
throw new Exception("XLS Sheet Number cannot be negative."); 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; $api_config_id = null;
} }
@@ -71,6 +120,7 @@ try {
$header_row = null; $header_row = null;
$start_column = null; $start_column = null;
$xls_end_column = null;
$xls_sheet_index = null; $xls_sheet_index = null;
} }
@@ -78,6 +128,7 @@ try {
if ($source_type === 'PDF') { if ($source_type === 'PDF') {
$header_row = null; $header_row = null;
$start_column = null; $start_column = null;
$xls_end_column = null;
$xls_sheet_index = null; $xls_sheet_index = null;
$api_config_id = null; $api_config_id = null;
} }
@@ -109,6 +160,7 @@ try {
source_type = ?, source_type = ?,
header_row = ?, header_row = ?,
start_column = ?, start_column = ?,
xls_end_column = ?,
xls_sheet_index = ?, xls_sheet_index = ?,
api_config_id = ?, api_config_id = ?,
description = ?, description = ?,
@@ -131,6 +183,7 @@ try {
$source_type, $source_type,
$header_row, $header_row,
$start_column, $start_column,
$xls_end_column,
$xls_sheet_index, $xls_sheet_index,
$api_config_id, $api_config_id,
$description, $description,
+49 -3
View File
@@ -74,6 +74,7 @@ try {
id, id,
header_row, header_row,
start_column, start_column,
xls_end_column,
xls_sheet_index, xls_sheet_index,
idroutine, idroutine,
idclient idclient
@@ -93,6 +94,12 @@ try {
$start_column_raw = $template['start_column'] ?? 'A'; $start_column_raw = $template['start_column'] ?? 'A';
$start_column = normalizeColumnIndex($start_column_raw); $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 $xlsSheetIndex = isset($template['xls_sheet_index']) && $template['xls_sheet_index'] !== null
? (int)$template['xls_sheet_index'] ? (int)$template['xls_sheet_index']
@@ -109,7 +116,15 @@ try {
// Debug del template_id ricevuto // Debug del template_id ricevuto
error_log("Received template_id from POST: " . print_r($_POST['template_id'], true)); error_log("Received template_id from POST: " . print_r($_POST['template_id'], true));
error_log("Converted template_id: $template_id"); 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']; $file = $_FILES['excel_file'];
$fileError = $file['error']; $fileError = $file['error'];
@@ -161,8 +176,33 @@ try {
if (empty($mappings)) { if (empty($mappings)) {
$response['error'] = "Nessun mapping trovato per il template con ID $template_id"; $response['error'] = "Nessun mapping trovato per il template con ID $template_id";
} else { } else {
// Carica il file rinominato con PHPSpreadsheet // Load the XLS/XLSX file.
$spreadsheet = IOFactory::load($destination); // 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(); $sheetCount = $spreadsheet->getSheetCount();
$sheetNames = $spreadsheet->getSheetNames(); $sheetNames = $spreadsheet->getSheetNames();
@@ -193,6 +233,12 @@ try {
$highestColumn = $worksheet->getHighestColumn(); $highestColumn = $worksheet->getHighestColumn();
$highestColumnIndex = Coordinate::columnIndexFromString($highestColumn); $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); $startRow = max(1, $header_row);
$startColumn = max(1, $start_column); $startColumn = max(1, $start_column);
@@ -4,6 +4,35 @@ require_once 'class/db-functions.php';
$response = ["success" => false, "message" => ""]; $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 { try {
if ($_SERVER["REQUEST_METHOD"] !== "POST") { if ($_SERVER["REQUEST_METHOD"] !== "POST") {
throw new Exception("Invalid request method."); throw new Exception("Invalid request method.");
@@ -18,6 +47,8 @@ try {
: null; : null;
$start_column = trim($_POST['start_column'] ?? ''); $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'] !== '' $xls_sheet_index = isset($_POST['xls_sheet_index']) && $_POST['xls_sheet_index'] !== ''
? intval($_POST['xls_sheet_index']) ? intval($_POST['xls_sheet_index'])
@@ -63,6 +94,24 @@ try {
throw new Exception("XLS Sheet Number cannot be negative."); 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; $api_config_id = null;
} }
@@ -75,6 +124,7 @@ try {
$header_row = null; $header_row = null;
$start_column = null; $start_column = null;
$xls_sheet_index = null; $xls_sheet_index = null;
$xls_end_column = null;
} }
// PDF currently does not require XLS coordinates or API configuration // PDF currently does not require XLS coordinates or API configuration
@@ -82,6 +132,7 @@ try {
$header_row = null; $header_row = null;
$start_column = null; $start_column = null;
$xls_sheet_index = null; $xls_sheet_index = null;
$xls_end_column = null;
$api_config_id = null; $api_config_id = null;
} }
@@ -112,6 +163,7 @@ try {
source_type, source_type,
header_row, header_row,
start_column, start_column,
xls_end_column,
xls_sheet_index, xls_sheet_index,
api_config_id, api_config_id,
description, description,
@@ -130,7 +182,7 @@ try {
) )
VALUES VALUES
( (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
NOW(), NOW() NOW(), NOW()
@@ -142,6 +194,7 @@ try {
$source_type, $source_type,
$header_row, $header_row,
$start_column, $start_column,
$xls_end_column,
$xls_sheet_index, $xls_sheet_index,
$api_config_id, $api_config_id,
$description, $description,
+2 -2
View File
@@ -730,8 +730,8 @@
{ {
"IdSchemaCustomFields": 177, "IdSchemaCustomFields": 177,
"ConteggioClienti": 0, "ConteggioClienti": 0,
"Nome": "Phoebe philo ACC", "Nome": "Phoebe Philo ",
"Descrizione": "(scarpe, borse, cinture, occhiali, gioielleria)\r\n" "Descrizione": "\r\n\r\n"
}, },
{ {
"IdSchemaCustomFields": 178, "IdSchemaCustomFields": 178,
File diff suppressed because it is too large Load Diff
+604 -23
View File
@@ -8,8 +8,71 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!--favicon--> <!--favicon-->
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" /> <link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?> <?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> </head>
<body> <body>
@@ -18,9 +81,11 @@
<!--sidebar wrapper --> <!--sidebar wrapper -->
<?php include('include/navbar.php'); ?> <?php include('include/navbar.php'); ?>
<!--end sidebar wrapper --> <!--end sidebar wrapper -->
<!--start header --> <!--start header -->
<?php include('include/topbar.php'); ?> <?php include('include/topbar.php'); ?>
<!--end header --> <!--end header -->
<!--start page wrapper --> <!--start page wrapper -->
<div class="page-wrapper"> <div class="page-wrapper">
<div class="page-content"> <div class="page-content">
@@ -31,61 +96,195 @@
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div> <div>
<h6 class="mb-0">Edit Profile</h6> <h6 class="mb-0">Edit Profile</h6>
<small class="text-muted">Update your personal information and LIMS references.</small>
</div> </div>
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<form action="update-profile.php" method="POST" enctype="multipart/form-data"> <form action="update-profile.php" method="POST" enctype="multipart/form-data" id="profileForm">
<div class="mb-3"> <div class="row g-3">
<div class="col-md-6">
<label for="first_name" class="form-label">First Name</label> <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> <input
type="text"
class="form-control"
id="first_name"
name="first_name"
value="<?= htmlspecialchars($nameuser ?? '', ENT_QUOTES, 'UTF-8'); ?>"
required>
</div> </div>
<div class="mb-3">
<div class="col-md-6">
<label for="last_name" class="form-label">Last Name</label> <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> <input
type="text"
class="form-control"
id="last_name"
name="last_name"
value="<?= htmlspecialchars($surnameuser ?? '', ENT_QUOTES, 'UTF-8'); ?>"
required>
</div> </div>
<div class="mb-3">
<div class="col-md-12">
<label for="email" class="form-label">Email</label> <label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" value="<?= htmlspecialchars($emailuser); ?>" required> <input
type="email"
class="form-control"
id="email"
name="email"
value="<?= htmlspecialchars($emailuser ?? '', ENT_QUOTES, 'UTF-8'); ?>"
required>
</div> </div>
<div class="mb-3"> <div class="col-md-6">
<label for="lims_user_id" class="form-label">Accettatore</label> <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 ?? ''); ?>"> <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>
<div class="mb-3"> <div class="col-md-6">
<label for="lims_global_user_id" class="form-label">LIMS Global</label> <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 ?? ''); ?>"> <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>
<div class="mb-3"> <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> <label for="avatar" class="form-label">Profile Picture</label>
<input type="file" class="form-control" id="avatar" name="avatar"> <input
<?php if ($photousername): ?> type="file"
<p>Current avatar: <img src="..//upload/users/<?= htmlspecialchars($photousername); ?>" alt="Current Avatar" style="max-width: 100px;"></p> 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: ?> <?php else: ?>
<p>Current avatar: <img src="..//upload/users/profile.png" alt="Default Avatar" style="max-width: 100px;"></p> <img
src="../upload/users/profile.png"
alt="Default Avatar"
class="profile-avatar-preview"
id="avatarPreview">
<?php endif; ?> <?php endif; ?>
<div>
<div class="small-muted">Current avatar</div>
<div class="small-muted">Allowed formats: JPG, PNG, WEBP, GIF.</div>
</div> </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> </div>
<input type="hidden" name="iduserlogin" value="<?= htmlspecialchars($iduserlogin); ?>"> </div>
<button type="submit" class="btn btn-primary">Update Profile</button>
<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>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!--end page wrapper --> <!--end page wrapper -->
<!--start overlay--> <!--start overlay-->
<div class="overlay toggle-icon"></div> <div class="overlay toggle-icon"></div>
<!--end overlay--> <!--end overlay-->
<!--Start Back To Top Button--> <!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a> <a href="javaScript:;" class="back-to-top">
<i class='bx bxs-up-arrow-alt'></i>
</a>
<!--End Back To Top Button--> <!--End Back To Top Button-->
<?php include('include/footer.php'); ?> <?php include('include/footer.php'); ?>
</div> </div>
<!--end wrapper--> <!--end wrapper-->
@@ -99,7 +298,389 @@
<?php //include('include/themeswitcher.php'); <?php //include('include/themeswitcher.php');
?> ?>
<!--end switcher--> <!--end switcher-->
<?php include('jsinclude.php'); ?> <?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> </body>
</html> </html>
+122 -4
View File
@@ -1,4 +1,5 @@
<?php <?php
/** /**
* Validate rows before export to LIMS. * Validate rows before export to LIMS.
* *
@@ -88,6 +89,58 @@ $validators[] = function (int $iddatadb, array $ctx): array {
return []; 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 ──────────────────────────────────────────────────────────────────── // ── Main ────────────────────────────────────────────────────────────────────
try { try {
@@ -104,9 +157,12 @@ try {
$iddatadbList = array_column($rows, 'iddatadb'); $iddatadbList = array_column($rows, 'iddatadb');
$placeholders = implode(',', array_fill(0, count($iddatadbList), '?')); $placeholders = implode(',', array_fill(0, count($iddatadbList), '?'));
// Records (datadb) for fixed field validation // Records (datadb) — templateid + fixed-field columns for mandatory validation
$stmt = $pdo->prepare(" $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 FROM datadb
WHERE iddatadb IN ($placeholders) WHERE iddatadb IN ($placeholders)
"); ");
@@ -128,6 +184,63 @@ try {
$partsInfo[(int)$r['iddatadb']][] = $r; $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 ────────────────────────────────────────────── // ── Run validators per row ──────────────────────────────────────────────
$results = []; $results = [];
@@ -137,9 +250,15 @@ try {
$index = $rowInfo['index']; $index = $rowInfo['index'];
// Build context for validators // Build context for validators
$record = $recordsInfo[$iddatadb] ?? null;
$templateId = (int)($record['templateid'] ?? 0);
$ctx = [ $ctx = [
'record' => $recordsInfo[$iddatadb] ?? null, 'record' => $record,
'parts' => $partsInfo[$iddatadb] ?? [], 'parts' => $partsInfo[$iddatadb] ?? [],
'fixedAliasMap' => $fixedAliasMap,
'requiredFixed' => $requiredFixedByTemplate[$templateId] ?? [],
'requiredCustom' => $requiredCustomByTemplate[$templateId] ?? [],
'customValues' => $customValuesByRecord[$iddatadb] ?? [],
]; ];
$errors = []; $errors = [];
@@ -155,7 +274,6 @@ try {
} }
echo json_encode(['success' => true, 'results' => $results]); echo json_encode(['success' => true, 'results' => $results]);
} catch (Exception $e) { } catch (Exception $e) {
error_log("Validation error: " . $e->getMessage()); error_log("Validation error: " . $e->getMessage());
echo json_encode(['success' => false, 'message' => $e->getMessage()]); echo json_encode(['success' => false, 'message' => $e->getMessage()]);