Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fda3d66d1f | |||
| ff61456d91 | |||
| 089886cb9a | |||
| 4dc010d80e | |||
| 83c6157b10 | |||
| 4dd7b89c22 |
@@ -61,3 +61,4 @@ public/userarea/schemi_base_response.json
|
||||
*.log
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -679,9 +679,7 @@
|
||||
div.style.position = "relative";
|
||||
|
||||
let html = "";
|
||||
if (meta.isAdmin) {
|
||||
html += `<button type="button" class="export-lims-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="${isExported ? "Already exported" : "Export to LIMS"}" style="background:${isExported ? "#ccc" : "#eb0b0b"}; color:white; border:none; border-radius:5px; cursor:${isExported ? "not-allowed" : "pointer"}; ${isExported ? "opacity:0.5;" : ""}" ${isExported ? "disabled" : ""}><i class="fas fa-upload"></i></button>`;
|
||||
}
|
||||
html += `<button type="button" class="save-btn action-btn" data-row="${rowIndex}" title="Save" style="background:#28a745; color:white; border:none; border-radius:5px; cursor:pointer;"><i class="fas fa-save"></i></button>`;
|
||||
html += `<button type="button" class="photos-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="Photos" style="background:#007bff; color:white; border:none; border-radius:5px; cursor:pointer;"><i class="fas fa-camera"></i></button>`;
|
||||
html += `<button type="button" class="parts-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="Parts" style="background:#ffc107; color:white; border:none; border-radius:5px; cursor:pointer;"><i class="fas fa-puzzle-piece"></i></button>`;
|
||||
|
||||
@@ -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>
|
||||
|
||||
<?php if ($canAccessTemplates) : ?>
|
||||
<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">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>
|
||||
<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>
|
||||
</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>
|
||||
<?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>
|
||||
|
||||
@@ -53,7 +53,143 @@ function normalizeColumnIndex($value): int
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim a cell value treating invisible/Unicode spaces as empty.
|
||||
* PHP's native trim() strips only ASCII whitespace (" \t\n\r\0\x0B"), so a cell
|
||||
* that contains only a non-breaking space (U+00A0), zero-width space (U+200B),
|
||||
* BOM (U+FEFF) or another Unicode space looks blank to a human but would still
|
||||
* count as "filled" — pulling a ghost column into the import or retaining a
|
||||
* visually-empty row. Normalize those to a real space before trimming.
|
||||
*/
|
||||
function cleanCellText($value): string
|
||||
{
|
||||
$raw = (string)$value;
|
||||
$cleaned = preg_replace(
|
||||
'/[\x{00A0}\x{200B}\x{FEFF}\x{2000}-\x{200A}\x{202F}\x{205F}\x{3000}]+/u',
|
||||
' ',
|
||||
$raw
|
||||
);
|
||||
|
||||
// preg_replace returns null on malformed UTF-8; fall back to the raw value.
|
||||
return trim($cleaned ?? $raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-clean an .xlsx by streaming out "ghost" cells: empty, self-closing
|
||||
* <c .../> (and <c ...></c>) elements that carry only leftover styling.
|
||||
*/
|
||||
function slimXlsxGhostCells(string $path): ?string
|
||||
{
|
||||
if (!class_exists('ZipArchive')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$slim = $path . '.slim.xlsx';
|
||||
|
||||
try {
|
||||
if (!copy($path, $slim)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Phase 1: stream-strip each worksheet to a temp file (low memory).
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($slim) !== true) {
|
||||
@unlink($slim);
|
||||
return null;
|
||||
}
|
||||
|
||||
$temps = [];
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$name = $zip->getNameIndex($i);
|
||||
if (!preg_match('#^xl/worksheets/sheet\d+\.xml$#', $name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$in = $zip->getStream($name);
|
||||
if (!$in) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'slim');
|
||||
$out = fopen($tmp, 'w');
|
||||
$carry = '';
|
||||
|
||||
while (!feof($in)) {
|
||||
$chunk = fread($in, 4194304);
|
||||
if ($chunk === '' || $chunk === false) {
|
||||
break;
|
||||
}
|
||||
// Only process up to the last complete '>' so a cell tag is
|
||||
// never split across a chunk boundary; carry the remainder.
|
||||
$buf = $carry . $chunk;
|
||||
$lastGt = strrpos($buf, '>');
|
||||
if ($lastGt === false) {
|
||||
$carry = $buf;
|
||||
continue;
|
||||
}
|
||||
$proc = substr($buf, 0, $lastGt + 1);
|
||||
$carry = substr($buf, $lastGt + 1);
|
||||
$proc = preg_replace(['#<c [^>]*/>#', '#<c [^>]*></c>#'], '', $proc);
|
||||
fwrite($out, $proc);
|
||||
}
|
||||
if ($carry !== '') {
|
||||
fwrite($out, $carry);
|
||||
}
|
||||
fclose($in);
|
||||
fclose($out);
|
||||
$temps[$name] = $tmp;
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
if (!$temps) {
|
||||
@unlink($slim);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Phase 2: swap the stripped worksheets back into the archive.
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($slim) !== true) {
|
||||
foreach ($temps as $t) {
|
||||
@unlink($t);
|
||||
}
|
||||
@unlink($slim);
|
||||
return null;
|
||||
}
|
||||
foreach ($temps as $name => $tmp) {
|
||||
$zip->deleteName($name);
|
||||
$zip->addFile($tmp, $name);
|
||||
}
|
||||
$zip->close(); // addFile streams from disk here, so unlink only after.
|
||||
|
||||
foreach ($temps as $t) {
|
||||
@unlink($t);
|
||||
}
|
||||
|
||||
return $slim;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('slimXlsxGhostCells failed: ' . $e->getMessage());
|
||||
@unlink($slim);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Quando il body POST supera post_max_size, PHP scarta $_POST e $_FILES
|
||||
// (warning "Content-Length exceeds the limit ... in Unknown on line 0") e lo
|
||||
// script riceve una richiesta vuota. Lo intercettiamo per dare un messaggio
|
||||
// chiaro invece di "Richiesta non valida".
|
||||
if (
|
||||
$_SERVER['REQUEST_METHOD'] === 'POST'
|
||||
&& empty($_POST) && empty($_FILES)
|
||||
&& (int)($_SERVER['CONTENT_LENGTH'] ?? 0) > 0
|
||||
) {
|
||||
$postMax = ini_get('post_max_size');
|
||||
throw new Exception(
|
||||
"Il file caricato supera il limite di upload del server (post_max_size = {$postMax}). " .
|
||||
"Chiedi all'amministratore di aumentare post_max_size e upload_max_filesize."
|
||||
);
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['excel_file'])) {
|
||||
$template_id = isset($_POST['template_id']) ? intval($_POST['template_id']) : 0;
|
||||
|
||||
@@ -161,8 +297,28 @@ 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);
|
||||
// Pre-clean ghost cells for .xlsx so a bloated worksheet (millions
|
||||
// of empty styled cells) doesn't make the load time out. Falls back
|
||||
// to the original file if slimming fails for any reason.
|
||||
$loadPath = $destination;
|
||||
$slimPath = null;
|
||||
if (preg_match('/\.xlsx$/i', $destination)) {
|
||||
$slimPath = slimXlsxGhostCells($destination);
|
||||
if ($slimPath !== null) {
|
||||
$loadPath = $slimPath;
|
||||
error_log("Ghost-cell pre-clean applied, loading slimmed copy: $slimPath");
|
||||
}
|
||||
}
|
||||
|
||||
// Carica il file con PHPSpreadsheet.
|
||||
$reader = IOFactory::createReaderForFile($loadPath);
|
||||
$reader->setReadEmptyCells(false);
|
||||
$spreadsheet = $reader->load($loadPath);
|
||||
|
||||
// The slimmed copy is only needed for parsing; drop it now.
|
||||
if ($slimPath !== null) {
|
||||
@unlink($slimPath);
|
||||
}
|
||||
|
||||
$sheetCount = $spreadsheet->getSheetCount();
|
||||
$sheetNames = $spreadsheet->getSheetNames();
|
||||
@@ -189,8 +345,8 @@ try {
|
||||
|
||||
error_log("Selected XLS sheet - index: {$xlsSheetIndex}, name: {$selectedSheetName}");
|
||||
|
||||
$highestRow = $worksheet->getHighestRow();
|
||||
$highestColumn = $worksheet->getHighestColumn();
|
||||
$highestRow = $worksheet->getHighestDataRow();
|
||||
$highestColumn = $worksheet->getHighestDataColumn();
|
||||
$highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
|
||||
|
||||
$startRow = max(1, $header_row);
|
||||
@@ -199,7 +355,7 @@ try {
|
||||
// Advance startColumn to first non-empty cell in header row, matching JS behavior
|
||||
for ($sc = $startColumn; $sc <= $highestColumnIndex; $sc++) {
|
||||
$cl = Coordinate::stringFromColumnIndex($sc);
|
||||
$cv = trim((string)($worksheet->getCell($cl . $header_row)->getCalculatedValue() ?? ''));
|
||||
$cv = cleanCellText($worksheet->getCell($cl . $header_row)->getCalculatedValue() ?? '');
|
||||
|
||||
if ($cv !== '') {
|
||||
$startColumn = $sc;
|
||||
@@ -207,6 +363,19 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
$lastHeaderCol = $startColumn;
|
||||
for ($hc = $startColumn; $hc <= $highestColumnIndex; $hc++) {
|
||||
$hl = Coordinate::stringFromColumnIndex($hc);
|
||||
$hv = cleanCellText($worksheet->getCell($hl . $header_row)->getCalculatedValue() ?? '');
|
||||
|
||||
if ($hv !== '') {
|
||||
$lastHeaderCol = $hc;
|
||||
}
|
||||
}
|
||||
|
||||
$highestColumnIndex = $lastHeaderCol;
|
||||
$highestColumn = Coordinate::stringFromColumnIndex($highestColumnIndex);
|
||||
|
||||
// Debug dei parametri
|
||||
error_log(
|
||||
"Processing - template_id: $template_id, " .
|
||||
@@ -269,7 +438,7 @@ try {
|
||||
|
||||
$columnLetter = Coordinate::stringFromColumnIndex($physCol);
|
||||
$cell = $worksheet->getCell($columnLetter . $header_row);
|
||||
$cellValue = trim((string)($cell ? $cell->getCalculatedValue() : ''));
|
||||
$cellValue = cleanCellText($cell ? $cell->getCalculatedValue() : '');
|
||||
$cellValue = preg_replace('/[\r\n\t]+/', ' ', $cellValue);
|
||||
|
||||
// Empty headers get __empty_N__ to match mapping page
|
||||
@@ -306,7 +475,7 @@ try {
|
||||
$filledCount = 0;
|
||||
|
||||
foreach ($headerFilledIndices as $idx) {
|
||||
if (isset($rowData[$idx]) && trim((string)$rowData[$idx]) !== '') {
|
||||
if (isset($rowData[$idx]) && cleanCellText($rowData[$idx]) !== '') {
|
||||
$filledCount++;
|
||||
}
|
||||
}
|
||||
|
||||
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">
|
||||
<form action="update-profile.php" method="POST" enctype="multipart/form-data" id="profileForm">
|
||||
<div class="row g-3">
|
||||
|
||||
<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); ?>" required>
|
||||
<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">
|
||||
|
||||
<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); ?>" required>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="last_name"
|
||||
name="last_name"
|
||||
value="<?= htmlspecialchars($surnameuser ?? '', ENT_QUOTES, 'UTF-8'); ?>"
|
||||
required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
|
||||
<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); ?>" required>
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
name="email"
|
||||
value="<?= htmlspecialchars($emailuser ?? '', ENT_QUOTES, 'UTF-8'); ?>"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="col-md-6">
|
||||
<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 class="mb-3">
|
||||
<div class="col-md-6">
|
||||
<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 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>
|
||||
<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>
|
||||
<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: ?>
|
||||
<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; ?>
|
||||
|
||||
<div>
|
||||
<div class="small-muted">Current avatar</div>
|
||||
<div class="small-muted">Allowed formats: JPG, PNG, WEBP, GIF.</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>
|
||||
<input type="hidden" name="iduserlogin" value="<?= htmlspecialchars($iduserlogin); ?>">
|
||||
<button type="submit" class="btn btn-primary">Update Profile</button>
|
||||
</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>
|
||||
</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,
|
||||
'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