Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 955b663322 |
@@ -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
|
||||||
|
|||||||
@@ -679,7 +679,9 @@
|
|||||||
div.style.position = "relative";
|
div.style.position = "relative";
|
||||||
|
|
||||||
let html = "";
|
let html = "";
|
||||||
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>`;
|
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="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>`;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -53,143 +53,7 @@ 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;
|
||||||
|
|
||||||
@@ -206,10 +70,11 @@ try {
|
|||||||
* Così non dipendiamo solo dal form e siamo sicuri di usare i dati salvati.
|
* Così non dipendiamo solo dal form e siamo sicuri di usare i dati salvati.
|
||||||
*/
|
*/
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
header_row,
|
header_row,
|
||||||
start_column,
|
start_column,
|
||||||
|
xls_end_column,
|
||||||
xls_sheet_index,
|
xls_sheet_index,
|
||||||
idroutine,
|
idroutine,
|
||||||
idclient
|
idclient
|
||||||
@@ -229,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']
|
||||||
@@ -245,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'];
|
||||||
@@ -278,14 +157,14 @@ try {
|
|||||||
|
|
||||||
// Recupera il mapping da template_mapping
|
// Recupera il mapping da template_mapping
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT
|
SELECT
|
||||||
field_id AS excel_column,
|
field_id AS excel_column,
|
||||||
field_id AS mysql_column,
|
field_id AS mysql_column,
|
||||||
data_type,
|
data_type,
|
||||||
is_required,
|
is_required,
|
||||||
default_value,
|
default_value,
|
||||||
is_manual
|
is_manual
|
||||||
FROM template_mapping
|
FROM template_mapping
|
||||||
WHERE template_id = ?
|
WHERE template_id = ?
|
||||||
");
|
");
|
||||||
$stmt->execute([$template_id]);
|
$stmt->execute([$template_id]);
|
||||||
@@ -297,28 +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 {
|
||||||
// Pre-clean ghost cells for .xlsx so a bloated worksheet (millions
|
// Load the XLS/XLSX file.
|
||||||
// of empty styled cells) doesn't make the load time out. Falls back
|
// If Last Column is configured in the template, load only the configured column range.
|
||||||
// to the original file if slimming fails for any reason.
|
// If Last Column is empty, keep the original behavior and read the entire sheet.
|
||||||
$loadPath = $destination;
|
$reader = IOFactory::createReaderForFile($destination);
|
||||||
$slimPath = null;
|
$reader->setReadDataOnly(true);
|
||||||
if (preg_match('/\.xlsx$/i', $destination)) {
|
|
||||||
$slimPath = slimXlsxGhostCells($destination);
|
if ($xls_end_column > 0) {
|
||||||
if ($slimPath !== null) {
|
$reader->setReadFilter(new class($start_column, $xls_end_column) implements \PhpOffice\PhpSpreadsheet\Reader\IReadFilter {
|
||||||
$loadPath = $slimPath;
|
private int $startColumn;
|
||||||
error_log("Ghost-cell pre-clean applied, loading slimmed copy: $slimPath");
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carica il file con PHPSpreadsheet.
|
$spreadsheet = $reader->load($destination);
|
||||||
$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();
|
||||||
@@ -345,17 +229,23 @@ try {
|
|||||||
|
|
||||||
error_log("Selected XLS sheet - index: {$xlsSheetIndex}, name: {$selectedSheetName}");
|
error_log("Selected XLS sheet - index: {$xlsSheetIndex}, name: {$selectedSheetName}");
|
||||||
|
|
||||||
$highestRow = $worksheet->getHighestDataRow();
|
$highestRow = $worksheet->getHighestRow();
|
||||||
$highestColumn = $worksheet->getHighestDataColumn();
|
$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);
|
||||||
|
|
||||||
// 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 = cleanCellText($worksheet->getCell($cl . $header_row)->getCalculatedValue() ?? '');
|
$cv = trim((string)($worksheet->getCell($cl . $header_row)->getCalculatedValue() ?? ''));
|
||||||
|
|
||||||
if ($cv !== '') {
|
if ($cv !== '') {
|
||||||
$startColumn = $sc;
|
$startColumn = $sc;
|
||||||
@@ -363,19 +253,6 @@ 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, " .
|
||||||
@@ -438,7 +315,7 @@ try {
|
|||||||
|
|
||||||
$columnLetter = Coordinate::stringFromColumnIndex($physCol);
|
$columnLetter = Coordinate::stringFromColumnIndex($physCol);
|
||||||
$cell = $worksheet->getCell($columnLetter . $header_row);
|
$cell = $worksheet->getCell($columnLetter . $header_row);
|
||||||
$cellValue = cleanCellText($cell ? $cell->getCalculatedValue() : '');
|
$cellValue = trim((string)($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
|
||||||
@@ -475,7 +352,7 @@ try {
|
|||||||
$filledCount = 0;
|
$filledCount = 0;
|
||||||
|
|
||||||
foreach ($headerFilledIndices as $idx) {
|
foreach ($headerFilledIndices as $idx) {
|
||||||
if (isset($rowData[$idx]) && cleanCellText($rowData[$idx]) !== '') {
|
if (isset($rowData[$idx]) && trim((string)$rowData[$idx]) !== '') {
|
||||||
$filledCount++;
|
$filledCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -491,13 +368,13 @@ try {
|
|||||||
// Recupera routine dal template
|
// Recupera routine dal template
|
||||||
if ($template && !empty($template['idroutine'])) {
|
if ($template && !empty($template['idroutine'])) {
|
||||||
$stmtRoutine = $pdo->prepare("
|
$stmtRoutine = $pdo->prepare("
|
||||||
SELECT
|
SELECT
|
||||||
idroutine,
|
idroutine,
|
||||||
name,
|
name,
|
||||||
filename,
|
filename,
|
||||||
headerrow,
|
headerrow,
|
||||||
instruction
|
instruction
|
||||||
FROM routine
|
FROM routine
|
||||||
WHERE idroutine = ?
|
WHERE idroutine = ?
|
||||||
");
|
");
|
||||||
$stmtRoutine->execute([$template['idroutine']]);
|
$stmtRoutine->execute([$template['idroutine']]);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user