Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fda3d66d1f | |||
| ff61456d91 | |||
| 089886cb9a |
@@ -679,9 +679,7 @@
|
|||||||
div.style.position = "relative";
|
div.style.position = "relative";
|
||||||
|
|
||||||
let html = "";
|
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="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="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="photos-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="Photos" style="background:#007bff; color:white; border:none; border-radius:5px; cursor:pointer;"><i class="fas fa-camera"></i></button>`;
|
||||||
html += `<button type="button" class="parts-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="Parts" style="background:#ffc107; color:white; border:none; border-radius:5px; cursor:pointer;"><i class="fas fa-puzzle-piece"></i></button>`;
|
html += `<button type="button" class="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>`;
|
||||||
|
|||||||
@@ -53,7 +53,143 @@ function normalizeColumnIndex($value): int
|
|||||||
return 1;
|
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 {
|
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'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['excel_file'])) {
|
||||||
$template_id = isset($_POST['template_id']) ? intval($_POST['template_id']) : 0;
|
$template_id = isset($_POST['template_id']) ? intval($_POST['template_id']) : 0;
|
||||||
|
|
||||||
@@ -161,8 +297,28 @@ 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
|
// Pre-clean ghost cells for .xlsx so a bloated worksheet (millions
|
||||||
$spreadsheet = IOFactory::load($destination);
|
// 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();
|
$sheetCount = $spreadsheet->getSheetCount();
|
||||||
$sheetNames = $spreadsheet->getSheetNames();
|
$sheetNames = $spreadsheet->getSheetNames();
|
||||||
@@ -189,8 +345,8 @@ try {
|
|||||||
|
|
||||||
error_log("Selected XLS sheet - index: {$xlsSheetIndex}, name: {$selectedSheetName}");
|
error_log("Selected XLS sheet - index: {$xlsSheetIndex}, name: {$selectedSheetName}");
|
||||||
|
|
||||||
$highestRow = $worksheet->getHighestRow();
|
$highestRow = $worksheet->getHighestDataRow();
|
||||||
$highestColumn = $worksheet->getHighestColumn();
|
$highestColumn = $worksheet->getHighestDataColumn();
|
||||||
$highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
|
$highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
|
||||||
|
|
||||||
$startRow = max(1, $header_row);
|
$startRow = max(1, $header_row);
|
||||||
@@ -199,7 +355,7 @@ try {
|
|||||||
// Advance startColumn to first non-empty cell in header row, matching JS behavior
|
// Advance startColumn to first non-empty cell in header row, matching JS behavior
|
||||||
for ($sc = $startColumn; $sc <= $highestColumnIndex; $sc++) {
|
for ($sc = $startColumn; $sc <= $highestColumnIndex; $sc++) {
|
||||||
$cl = Coordinate::stringFromColumnIndex($sc);
|
$cl = Coordinate::stringFromColumnIndex($sc);
|
||||||
$cv = trim((string)($worksheet->getCell($cl . $header_row)->getCalculatedValue() ?? ''));
|
$cv = cleanCellText($worksheet->getCell($cl . $header_row)->getCalculatedValue() ?? '');
|
||||||
|
|
||||||
if ($cv !== '') {
|
if ($cv !== '') {
|
||||||
$startColumn = $sc;
|
$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
|
// Debug dei parametri
|
||||||
error_log(
|
error_log(
|
||||||
"Processing - template_id: $template_id, " .
|
"Processing - template_id: $template_id, " .
|
||||||
@@ -269,7 +438,7 @@ try {
|
|||||||
|
|
||||||
$columnLetter = Coordinate::stringFromColumnIndex($physCol);
|
$columnLetter = Coordinate::stringFromColumnIndex($physCol);
|
||||||
$cell = $worksheet->getCell($columnLetter . $header_row);
|
$cell = $worksheet->getCell($columnLetter . $header_row);
|
||||||
$cellValue = trim((string)($cell ? $cell->getCalculatedValue() : ''));
|
$cellValue = cleanCellText($cell ? $cell->getCalculatedValue() : '');
|
||||||
$cellValue = preg_replace('/[\r\n\t]+/', ' ', $cellValue);
|
$cellValue = preg_replace('/[\r\n\t]+/', ' ', $cellValue);
|
||||||
|
|
||||||
// Empty headers get __empty_N__ to match mapping page
|
// Empty headers get __empty_N__ to match mapping page
|
||||||
@@ -306,7 +475,7 @@ try {
|
|||||||
$filledCount = 0;
|
$filledCount = 0;
|
||||||
|
|
||||||
foreach ($headerFilledIndices as $idx) {
|
foreach ($headerFilledIndices as $idx) {
|
||||||
if (isset($rowData[$idx]) && trim((string)$rowData[$idx]) !== '') {
|
if (isset($rowData[$idx]) && cleanCellText($rowData[$idx]) !== '') {
|
||||||
$filledCount++;
|
$filledCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user