Compare commits

..

18 Commits

Author SHA1 Message Date
solocla a19cdf6074 pdf test routine 2026-08-18 16:00:06 +02:00
solocla 14a7cc2738 revert stats_export files to pre-a1e91f8 version 2026-08-04 20:10:27 +02:00
solocla 601e72ca61 fixed quotations parts 2026-08-04 20:02:50 +02:00
solocla 54c164478c fixed import XLS big 2026-08-04 14:42:05 +02:00
solocla 965d2fa5bb sticky bar import XLS 2026-08-04 14:21:14 +02:00
solocla a1e91f8d76 fixed stats with comeesaa campioni 2026-08-04 12:57:00 +02:00
solocla 153cdbca98 filter analysis withput NominativoCliente 2026-08-03 12:00:37 +02:00
solocla 2603c86c5b exlcuded login time and date from validation 2026-07-30 17:07:47 +02:00
solocla e7d733a136 fixed order lines 2026-07-30 13:18:27 +02:00
solocla eb34cba7bb added search on add fields ranking 2026-07-29 16:37:58 +02:00
solocla f01bfa06c9 fixed exxcel row column 2026-07-29 12:24:58 +02:00
solocla 71d6fe1d98 fixed sample arrival and date 2026-07-23 16:00:26 +02:00
solocla 33615f65d7 Merge branch 'filter' 2026-07-23 13:09:02 +02:00
solocla 9dda4630a4 fixed modal analysis 2026-07-20 15:41:29 +02:00
solocla 6d0b98bd41 analysis fixed matrix 2026-07-20 15:21:20 +02:00
solocla e92e107a3c search by ranking custom field 2026-07-20 11:30:26 +02:00
solocla 9330a49008 fixed stats 2 2026-07-20 10:58:12 +02:00
solocla 80dbd26929 fixed stats 2026-07-20 10:47:54 +02:00
27 changed files with 2566 additions and 68 deletions
+4
View File
@@ -62,3 +62,7 @@ public/userarea/schemi_base_response.json
public/userarea/cache/
public/userarea/error_log.txt
# File PDF di esempio e importati
/public/userarea/pdftemplates/*.pdf
/public/userarea/pdftemplates/imported/*.pdf
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class ChangeExcelrowToVarchar extends AbstractMigration
{
public function up(): void
{
$this->table('datadb')
->changeColumn('excelrow', 'string', [
'limit' => 100,
'null' => true,
])
->update();
}
public function down(): void
{
$this->table('datadb')
->changeColumn('excelrow', 'integer', [
'null' => true,
])
->update();
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddPdfSupportToTemplates extends AbstractMigration
{
public function change(): void
{
// 1) PDF di esempio + metadati pagine sul template
$templates = $this->table('excel_templates');
if (!$templates->hasColumn('sample_pdf')) {
$templates->addColumn('sample_pdf', 'string', [
'limit' => 255,
'null' => true,
'default' => null,
'after' => 'sample_xlsx',
'comment' => 'Filename of the sample PDF used to draw regions',
]);
}
if (!$templates->hasColumn('pdf_page_meta')) {
$templates->addColumn('pdf_page_meta', 'text', [
'null' => true,
'default' => null,
'comment' => 'JSON: pages count and rendered dimensions per page',
]);
}
$templates->update();
// 2) Riquadri (etichetta + dato) per ogni campo mappato
$mapping = $this->table('template_mapping');
if (!$mapping->hasColumn('pdf_regions')) {
$mapping->addColumn('pdf_regions', 'text', [
'null' => true,
'default' => null,
'after' => 'json_node',
'comment' => 'JSON: {page, label:{x,y,w,h}, value:{x,y,w,h}}',
]);
}
$mapping->update();
}
}
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddPdfToSourceTypeEnum extends AbstractMigration
{
public function up(): void
{
$this->execute(
"ALTER TABLE `excel_templates`
MODIFY `source_type` ENUM('XLS','API','PDF') NOT NULL DEFAULT 'XLS'"
);
}
public function down(): void
{
$this->execute(
"ALTER TABLE `excel_templates`
MODIFY `source_type` ENUM('XLS','API') NOT NULL DEFAULT 'XLS'"
);
}
}
@@ -69,6 +69,11 @@ try {
$analisi = $data['value'] ?? [];
// Escludi le analisi con NominativoCliente compilato
$analisi = array_values(array_filter($analisi, function ($item) {
return empty(trim((string)($item['NominativoCliente'] ?? '')));
}));
echo json_encode([
'value' => $analisi,
'count' => is_array($analisi) ? count($analisi) : 0
+9 -1
View File
@@ -1296,7 +1296,7 @@
}
// Precalcola le coppie come col.key (sourceKey → targetKey)
const AUTO_COPY_KEY_PAIRS = AUTO_COPY_FIELD_PAIRS.map((p) => ({
var AUTO_COPY_KEY_PAIRS = AUTO_COPY_FIELD_PAIRS.map((p) => ({
fromKey: colKeyByFieldId(p.from),
toKey: colKeyByFieldId(p.to),
})).filter((p) => p.fromKey && p.toKey);
@@ -1571,6 +1571,10 @@
}
if (col && (col.type === "detail" || col.type === "main_field")) {
const autoPair = AUTO_COPY_KEY_PAIRS.find(
(p) => p.fromKey === String(col.key),
);
data.forEach((row) => {
if (
window.isTargetGridId &&
@@ -1580,6 +1584,10 @@
if (!row.details) row.details = {};
row.details[col.key] = value;
if (autoPair) {
row.details[autoPair.toKey] = value;
}
if (col.type === "main_field") {
row.mainFieldValue = value;
}
+3 -1
View File
@@ -297,7 +297,9 @@
wrapper.className = "template-btn-wrapper";
const btn = document.createElement("a");
btn.href = `import_xls2.php?id=${template.id}`;
btn.href = (sourceType === 'PDF') ?
`import_pdf.php?id=${template.id}` :
`import_xls2.php?id=${template.id}`;
btn.className = `btn ${sizeClass}`;
btn.style.backgroundColor = template.button_bg_color || '#0d6efd';
btn.style.color = template.button_text_color || '#ffffff';
+173
View File
@@ -0,0 +1,173 @@
<?php
ini_set('display_errors', 0); // in AJAX gli errori devono NON sporcare il JSON
error_reporting(E_ALL);
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/import_debug.log');
include('include/headscript.php');
header('Content-Type: application/json');
// Legge il body JSON
$input = json_decode(file_get_contents('php://input'), true);
if (
!$input ||
!isset($input['template_id']) ||
!isset($input['importreferencecode']) ||
!isset($input['filename']) ||
!isset($input['columns']) ||
!isset($input['batch_rows']) ||
!isset($input['batch_excelrows'])
) {
echo json_encode(['ok' => false, 'error' => 'Richiesta non valida']);
exit;
}
$template_id = intval($input['template_id']);
$importReferenceCode = (string)$input['importreferencecode'];
$newFilename = (string)$input['filename'];
$columns = $input['columns']; // array
$batchRows = $input['batch_rows']; // array di righe (ognuna array di celle)
$batchExcelrows = $input['batch_excelrows']; // array di excelrow paralleli a batchRows
$user_id = $iduserlogin ?? 1;
function normalizeColName($s): string
{
$s = (string)$s;
$s = str_replace(
["\xC2\xA0", "\xE2\x80\xAF", "\xE2\x80\x87", "\xE2\x80\x89", "\xE2\x80\x8A", "\xEF\xBB\xBF"],
' ',
$s
);
$s = preg_replace('/\s+/u', ' ', $s);
return trim((string)$s);
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Recupera i mapping
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
$stmt->execute([$template_id]);
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($allMappings)) {
echo json_encode(['ok' => false, 'error' => 'Nessun mapping trovato per il template']);
exit;
}
// idclient di default (una volta sola)
$template_stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
$template_stmt->execute([$template_id]);
$tpl = $template_stmt->fetch(PDO::FETCH_ASSOC);
$default_idclient = $tpl['idclient'] ?? null;
// lims_user_id per il campo 244 (una volta sola)
$stmtUser = $pdo->prepare("SELECT lims_user_id FROM auth_users WHERE id = ? LIMIT 1");
$stmtUser->execute([(int)$user_id]);
$limsUserId = $stmtUser->fetchColumn();
$limsUserId = ($limsUserId !== false && $limsUserId !== null && $limsUserId !== '') ? (string)$limsUserId : '';
$stmtMap = $pdo->prepare("SELECT id FROM template_mapping WHERE template_id = ? AND field_id = 244 LIMIT 1");
$stmtMap->execute([(int)$template_id]);
$mappingId244 = (int)$stmtMap->fetchColumn();
$insertedIds = [];
try {
$pdo->beginTransaction();
$insDatadb = $pdo->prepare("INSERT INTO datadb (templateid, importreferencecode, filename_import, status, user_id, limscode, importdate, excelrow, idclient) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$insDetail = $pdo->prepare("INSERT INTO import_data_details (id, mapping_id, field_value) VALUES (?, ?, ?)");
foreach ($batchRows as $i => $row) {
$excelrow = $batchExcelrows[$i] ?? null;
if ($row === null || $excelrow === null) {
continue;
}
$insDatadb->execute([
$template_id,
$importReferenceCode,
$newFilename,
'i',
$user_id,
null,
date('Y-m-d'),
$excelrow,
$default_idclient
]);
$iddatadb = $pdo->lastInsertId();
$insertedIds[] = $iddatadb;
foreach ($allMappings as $mapping) {
$fieldValue = null;
if (!$mapping['is_manual']) {
$excelColumn = trim($mapping['excel_column']);
$excelColumnIndex = array_search(
normalizeColName($excelColumn),
array_map('normalizeColName', $columns)
);
if ($excelColumnIndex !== false && isset($row[$excelColumnIndex]) && $row[$excelColumnIndex] !== '') {
$fieldValue = $row[$excelColumnIndex];
} else {
$fieldValue = $mapping['manual_default'] ?? '';
}
switch ($mapping['data_type']) {
case 'INT':
$fieldValue = is_numeric($fieldValue) ? (int)$fieldValue : ($mapping['manual_default'] ?? 0);
break;
case 'DATE':
$fieldValue = !empty($fieldValue) ? date('Y-m-d', strtotime($fieldValue)) : ($mapping['manual_default'] === 'today' ? date('Y-m-d') : ($mapping['manual_default'] ?? ''));
break;
case 'CHAR':
$fieldValue = !empty($fieldValue) ? substr((string)$fieldValue, 0, 1) : ($mapping['manual_default'] ?? '');
break;
case 'Testo':
case 'VARCHAR':
default:
$fieldValue = !empty($fieldValue) ? (string)$fieldValue : ($mapping['manual_default'] ?? '');
break;
}
} else {
$fieldValue = $mapping['manual_default'] ?? '';
if ($mapping['data_type'] === 'DATE' && $mapping['manual_default'] === 'today') {
$fieldValue = date('Y-m-d');
}
}
if (($fieldValue === null || $fieldValue === '') && !empty($mapping['auto_value']) && $mapping['auto_value'] !== 'none') {
if ($mapping['auto_value'] === 'import_date') {
$fieldValue = date('Y-m-d');
} elseif ($mapping['auto_value'] === 'import_time') {
$fieldValue = date('H:i');
}
}
$insDetail->execute([$iddatadb, $mapping['id'], $fieldValue]);
}
// Campo 244 (Accettatore) con lims_user_id
if ($limsUserId !== '' && $mappingId244 > 0) {
$insDetail->execute([$iddatadb, $mappingId244, $limsUserId]);
}
}
$pdo->commit();
echo json_encode([
'ok' => true,
'inserted' => count($insertedIds)
]);
exit;
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
error_log("[BATCH IMPORT] " . $e->getMessage());
echo json_encode([
'ok' => false,
'error' => $e->getMessage()
]);
exit;
}
+321
View File
@@ -0,0 +1,321 @@
<?php
include('include/headscript.php');
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
header("Location: template_dashboard.php?status=error&message=" . urlencode("Invalid ID"));
exit;
}
$id = intval($_GET['id']);
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("SELECT id, name, source_type, idclient, sample_pdf FROM excel_templates WHERE id = ?");
$stmt->execute([$id]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$template) {
header("Location: template_dashboard.php?status=error&message=" . urlencode("Template not found"));
exit;
}
$sourceType = strtoupper($template['source_type'] ?? '');
if ($sourceType !== 'PDF') {
// Non è un template PDF: rimanda al flusso corretto
header("Location: import_xls2.php?id=" . $id);
exit;
}
// Conta i mapping con riquadri, per avvisare se non è configurato
$stmt = $pdo->prepare("SELECT COUNT(*) FROM template_mapping WHERE template_id = ? AND pdf_regions IS NOT NULL AND pdf_regions <> ''");
$stmt->execute([$id]);
$mappedRegions = (int)$stmt->fetchColumn();
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title><?= htmlspecialchars($template['name']) ?> - PDF Import</title>
<style>
#dropZone {
border: 2px dashed #adb5bd;
border-radius: 10px;
padding: 40px 20px;
text-align: center;
color: #6c757d;
background: #f8f9fa;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
#dropZone.dragover {
background: #e7f1ff;
border-color: #0d6efd;
color: #0d6efd;
}
#dropZone .big {
font-size: 42px;
line-height: 1;
margin-bottom: 10px;
}
.file-row {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
border: 1px solid #e5e5e5;
border-radius: 8px;
margin-bottom: 8px;
font-size: 14px;
}
.file-row .status-icon {
font-size: 18px;
width: 24px;
text-align: center;
}
.file-row .fname {
font-weight: 600;
flex: 0 0 260px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-row .fvals {
flex: 1;
color: #555;
font-size: 13px;
}
.file-row.ok {
background: #eaffea;
}
.file-row.err {
background: #ffecec;
}
.file-row.pending {
background: #fff8e1;
}
.val-pair {
display: inline-block;
margin-right: 12px;
}
.val-pair b {
color: #333;
}
</style>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<?php include('top_stat_widget.php'); ?>
<div class="card radius-10">
<div class="card-header">
<h6 class="mb-0"><?= htmlspecialchars($template['name']) ?> <span class="badge bg-danger">PDF</span></h6>
<small>Template ID: <?= $id ?> — Ogni PDF diventa una riga importata.</small>
</div>
<div class="card-body">
<?php if ($mappedRegions === 0): ?>
<div class="alert alert-warning">
Questo template non ha ancora riquadri PDF configurati. Configura il mapping prima di importare.
</div>
<?php endif; ?>
<div id="dropZone">
<div class="big">📄⬇</div>
<div><strong>Trascina qui i PDF</strong> oppure clicca per selezionarli</div>
<div style="font-size:13px; margin-top:6px;">Puoi caricarne più di uno insieme</div>
<input type="file" id="pdfInput" accept="application/pdf" multiple style="display:none;">
</div>
<div class="d-flex align-items-center justify-content-between mt-3">
<span class="badge bg-secondary" id="fileCount">0 file</span>
<button id="startImportBtn" class="btn btn-primary" disabled>Importa PDF</button>
</div>
<div id="fileList" class="mt-3"></div>
<div class="mt-4">
<a href="imported.php?id=<?= $id ?>" class="btn btn-warning">Vai agli Imported</a>
<a href="template_dashboard.php" class="btn btn-secondary">Dashboard</a>
</div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script>
(function() {
const TEMPLATE_ID = <?= (int)$id ?>;
const dropZone = document.getElementById('dropZone');
const pdfInput = document.getElementById('pdfInput');
const fileList = document.getElementById('fileList');
const fileCount = document.getElementById('fileCount');
const startBtn = document.getElementById('startImportBtn');
let selectedFiles = [];
function updateCount() {
fileCount.textContent = selectedFiles.length + ' file';
startBtn.disabled = selectedFiles.length === 0;
}
function renderList() {
fileList.innerHTML = '';
selectedFiles.forEach((f, idx) => {
const row = document.createElement('div');
row.className = 'file-row pending';
row.id = 'file-row-' + idx;
row.innerHTML = `
<span class="status-icon"></span>
<span class="fname">${escapeHtml(f.name)}</span>
<span class="fvals">In attesa...</span>
`;
fileList.appendChild(row);
});
}
function escapeHtml(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function addFiles(files) {
for (const f of files) {
if (f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')) {
selectedFiles.push(f);
}
}
updateCount();
renderList();
}
// Click per selezionare
dropZone.addEventListener('click', () => pdfInput.click());
pdfInput.addEventListener('change', (e) => addFiles(e.target.files));
// Drag & drop
['dragenter', 'dragover'].forEach(ev => {
dropZone.addEventListener(ev, (e) => {
e.preventDefault();
e.stopPropagation();
dropZone.classList.add('dragover');
});
});
['dragleave', 'drop'].forEach(ev => {
dropZone.addEventListener(ev, (e) => {
e.preventDefault();
e.stopPropagation();
dropZone.classList.remove('dragover');
});
});
dropZone.addEventListener('drop', (e) => {
addFiles(e.dataTransfer.files);
});
// Genera un importreferencecode condiviso da tutto il gruppo
function makeImportRef() {
return new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14) +
'-' + Math.random().toString(36).slice(2, 10);
}
startBtn.addEventListener('click', async () => {
if (selectedFiles.length === 0) return;
startBtn.disabled = true;
startBtn.textContent = 'Importazione...';
const importRef = makeImportRef();
const formData = new FormData();
formData.append('template_id', TEMPLATE_ID);
formData.append('importreferencecode', importRef);
selectedFiles.forEach(f => formData.append('pdf_files[]', f));
// segna tutte le righe come "in corso"
selectedFiles.forEach((f, idx) => {
const row = document.getElementById('file-row-' + idx);
if (row) row.querySelector('.fvals').textContent = 'Elaborazione...';
});
try {
const resp = await fetch('process_import_pdf.php', {
method: 'POST',
body: formData
});
const data = await resp.json();
if (!data.ok) {
alert('Errore: ' + (data.error || 'sconosciuto'));
startBtn.disabled = false;
startBtn.textContent = 'Importa PDF';
return;
}
// Aggiorna ogni riga con l'esito (l'ordine dei results segue quello inviato)
data.results.forEach((res, idx) => {
const row = document.getElementById('file-row-' + idx);
if (!row) return;
const icon = row.querySelector('.status-icon');
const vals = row.querySelector('.fvals');
if (res.ok) {
row.className = 'file-row ok';
icon.textContent = '✅';
const pairs = Object.entries(res.values || {})
.map(([k, v]) => `<span class="val-pair"><b>${escapeHtml(k)}:</b> ${escapeHtml(v || '—')}</span>`)
.join('');
vals.innerHTML = pairs || 'Importato (nessun valore estratto)';
} else {
row.className = 'file-row err';
icon.textContent = '❌';
vals.textContent = res.error || 'Errore sconosciuto';
}
});
startBtn.textContent = 'Importa altri PDF';
startBtn.disabled = false;
// svuota la selezione (i file sono stati processati)
selectedFiles = [];
pdfInput.value = '';
updateCount();
} catch (err) {
console.error(err);
alert('Errore di rete durante l\'importazione.');
startBtn.disabled = false;
startBtn.textContent = 'Importa PDF';
}
});
})();
</script>
</body>
</html>
+172 -20
View File
@@ -50,6 +50,9 @@ error_log("Loaded template: " . print_r($template, true));
border-radius: 0.25rem;
background: #f8f9fa;
display: none;
position: sticky;
top: 0;
z-index: 1020;
}
.top-scrollbar-inner {
@@ -341,8 +344,9 @@ error_log("Loaded template: " . print_r($template, true));
<input type="hidden" name="excelrows" id="selectedExcelRowsData" value="">
<input type="hidden" name="filename" value="${data.filename}">
<!-- TOP BUTTON -->
<div class="d-flex justify-content-end mb-3">
<!-- TOP BUTTON -->
<div class="d-flex justify-content-end align-items-center mb-3 gap-2">
<span class="badge bg-secondary" id="selectedCountTop">0 selezionati</span>
<button type="submit" class="btn btn-primary" id="proceedButtonTop" disabled>Prosegui</button>
</div>
@@ -383,46 +387,187 @@ error_log("Loaded template: " . print_r($template, true));
</table>
</div>
<!-- BOTTOM BUTTON -->
<button type="submit" class="btn btn-primary mt-3" id="proceedButtonBottom" disabled>Prosegui</button>
<!-- BOTTOM BUTTON -->
<div class="d-flex align-items-center mt-3 gap-2">
<span class="badge bg-secondary" id="selectedCountBottom">0 selezionati</span>
<button type="submit" class="btn btn-primary" id="proceedButtonBottom" disabled>Prosegui</button>
</div>
</form>
`;
tableContainer.innerHTML = html;
const selectRowsForm = document.getElementById('selectRowsForm');
selectRowsForm.addEventListener('submit', function(e) {
// Clic sulla cella (oltre che sulla checkbox) per selezionare/deselezionare la riga
document.querySelectorAll('#importPreviewTable tbody tr').forEach(tr => {
tr.style.cursor = 'pointer';
tr.addEventListener('click', function(e) {
// Se ho cliccato proprio sulla checkbox, lascio fare a lei (evita doppio toggle)
if (e.target.classList.contains('row-checkbox')) return;
const cb = this.querySelector('.row-checkbox');
if (!cb) return;
cb.checked = !cb.checked;
cb.dispatchEvent(new Event('change', {
bubbles: true
}));
});
});
selectRowsForm.addEventListener('submit', async function(e) {
e.preventDefault(); // gestiamo tutto via AJAX a batch
const checkedBoxes = Array.from(document.querySelectorAll('.row-checkbox:checked'));
if (checkedBoxes.length === 0) {
e.preventDefault();
alert('Seleziona almeno una riga.');
return;
}
// Raccoglie le righe selezionate (riga mappata + excelrow parallelo)
const selectedRows = [];
const selectedExcelRows = [];
checkedBoxes.forEach((cb, newIndex) => {
checkedBoxes.forEach(cb => {
const originalIndex = parseInt(cb.value, 10);
if (data.rows && data.rows[originalIndex]) {
selectedRows.push(data.rows[originalIndex]);
selectedExcelRows.push(
(data.excel_data && data.excel_data[originalIndex]) ?
data.excel_data[originalIndex].excelrow :
null
);
}
if (data.excel_data && data.excel_data[originalIndex]) {
selectedExcelRows.push(data.excel_data[originalIndex].excelrow);
}
// Reindex selected_rows so import_insert.php receives only the reduced rows array
cb.value = newIndex;
});
document.getElementById('selectedRowsData').value =
encodeURIComponent(JSON.stringify(selectedRows));
const total = selectedRows.length;
if (total === 0) {
alert('Nessuna riga valida da importare.');
return;
}
document.getElementById('selectedExcelRowsData').value =
encodeURIComponent(JSON.stringify(selectedExcelRows));
// importreferencecode generato UNA volta, condiviso da tutti i batch
const importReferenceCode =
new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14) +
'-' + Math.random().toString(36).slice(2, 10);
const BATCH_SIZE = 20;
// Disabilita i pulsanti e mostra la barra di progresso
const btnTop = document.getElementById('proceedButtonTop');
const btnBottom = document.getElementById('proceedButtonBottom');
if (btnTop) btnTop.disabled = true;
if (btnBottom) btnBottom.disabled = true;
const progressHtml = `
<div id="importProgressWrap" style="
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: 20000;
display: flex;
align-items: center;
justify-content: center;">
<div style="
background: #fff;
border-radius: 10px;
padding: 25px 30px;
width: 90%;
max-width: 500px;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);">
<div class="mb-2"><strong>Importazione in corso...</strong>
<span id="importProgressText" class="float-end">0 / ${total}</span>
</div>
<div class="progress" style="height: 26px;">
<div id="importProgressBar" class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 0%;">0%</div>
</div>
<div id="importProgressError" class="alert alert-danger mt-3 mb-0" style="display:none;"></div>
</div>
</div>`;
document.body.insertAdjacentHTML('beforeend', progressHtml);
const progressBar = document.getElementById('importProgressBar');
const progressText = document.getElementById('importProgressText');
const progressErr = document.getElementById('importProgressError');
let insertedTotal = 0;
// Invia un batch e ritorna la risposta JSON
async function sendBatch(startIndex) {
const batchRows = selectedRows.slice(startIndex, startIndex + BATCH_SIZE);
const batchExcelrows = selectedExcelRows.slice(startIndex, startIndex + BATCH_SIZE);
const resp = await fetch('import_insert_batch.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
template_id: data.template_id,
importreferencecode: importReferenceCode,
filename: data.filename,
columns: data.columns,
batch_rows: batchRows,
batch_excelrows: batchExcelrows
})
});
return resp.json();
}
// Cicla i batch in sequenza
for (let start = 0; start < total; start += BATCH_SIZE) {
let result;
try {
result = await sendBatch(start);
} catch (err) {
result = {
ok: false,
error: err.message
};
}
if (!result.ok) {
// Batch fallito: chiediamo all'utente se continuare
progressErr.textContent =
'Errore su un blocco (righe ' + (start + 1) + '-' +
Math.min(start + BATCH_SIZE, total) + '): ' + (result.error || 'errore sconosciuto');
progressErr.style.display = 'block';
const continua = confirm(
'Un blocco di righe non è stato importato.\n\n' +
'Righe inserite finora: ' + insertedTotal + ' di ' + total + '.\n\n' +
'Vuoi continuare con i blocchi successivi? (Annulla = interrompi)'
);
if (!continua) {
progressBar.classList.remove('progress-bar-animated');
progressBar.classList.add('bg-warning');
return;
}
// se continua, prosegue col prossimo blocco
continue;
}
insertedTotal += (result.inserted || 0);
const done = Math.min(start + BATCH_SIZE, total);
const pct = Math.round((done / total) * 100);
progressBar.style.width = pct + '%';
progressBar.textContent = pct + '%';
progressText.textContent = insertedTotal + ' / ' + total;
}
// Fine: barra verde e redirect a imported.php
progressBar.classList.remove('progress-bar-animated', 'progress-bar-striped');
progressBar.classList.add('bg-success');
progressText.textContent = insertedTotal + ' / ' + total + ' completate';
// PAUSA temporanea di 2 secondi per vedere la barra completa (da rimuovere dopo il test)
await new Promise(r => setTimeout(r, 2000));
window.location.href = 'imported.php?id=' +
encodeURIComponent(data.template_id) +
'&importref=' + encodeURIComponent(importReferenceCode);
});
const topTableScrollbar = document.getElementById('topTableScrollbar');
@@ -472,10 +617,17 @@ error_log("Loaded template: " . print_r($template, true));
const checkboxes = document.querySelectorAll('.row-checkbox');
function updateProceedButton() {
const enabled = Array.from(checkboxes).some(cb => cb.checked);
const checkedCount = Array.from(checkboxes).filter(cb => cb.checked).length;
const enabled = checkedCount > 0;
if (proceedButtonTop) proceedButtonTop.disabled = !enabled;
if (proceedButtonBottom) proceedButtonBottom.disabled = !enabled;
const label = checkedCount + ' selezionati';
const countTop = document.getElementById('selectedCountTop');
const countBottom = document.getElementById('selectedCountBottom');
if (countTop) countTop.textContent = label;
if (countBottom) countBottom.textContent = label;
}
selectAllCheckbox.addEventListener('change', function() {
+2 -2
View File
@@ -99,7 +99,7 @@ $stmt = $pdo->prepare("
FROM datadb d
LEFT JOIN auth_users u ON d.user_id = u.id
{$baseWhere}
ORDER BY d.excelrow ASC, d.iddatadb ASC
ORDER BY CAST(d.excelrow AS UNSIGNED) ASC, d.excelrow ASC, d.iddatadb ASC
{$limitClause}
");
$stmt->execute($baseParams);
@@ -1807,4 +1807,4 @@ $gridMeta = [
</body>
</html>
</html>
+1 -1
View File
@@ -39,7 +39,7 @@
<ul>
<li>
<a href="import_dashboard.php">
<i class='bx bx-radio-circle'></i>XLS Import
<i class='bx bx-radio-circle'></i>Import Dashboard
</a>
</li>
</ul>
+6
View File
@@ -291,6 +291,7 @@ $apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
const isXls = selectedSource === 'XLS';
const isApiJson = selectedSource === 'API';
const isPdf = selectedSource === 'PDF';
if (isXls) {
headerRowWrapper.style.display = 'block';
@@ -327,6 +328,8 @@ $apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
apiConfigSelect.required = true;
apiConfigSelect.disabled = false;
} else {
// PDF (o qualsiasi altro): niente config API, niente campi XLS.
// I riquadri PDF si tracciano nella pagina di configurazione.
apiConfigWrapper.style.display = 'none';
apiConfigSelect.required = false;
apiConfigSelect.disabled = true;
@@ -516,6 +519,9 @@ $apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
return;
}
// PDF: nessuna validazione extra qui. Il PDF di esempio e i
// riquadri (etichetta + dato) si configurano nella pagina successiva.
const clientSelect = document.getElementById("clientSelect");
const clientId = clientSelect.value;
const selectedClientOption = clientSelect.options[clientSelect.selectedIndex];
+100 -12
View File
@@ -24,7 +24,9 @@ $stmt = $pdo->prepare("
schemajson,
xls_headers,
api_sample_json,
json_nodes
json_nodes,
sample_pdf,
pdf_page_meta
FROM excel_templates
WHERE id = ?
");
@@ -61,6 +63,7 @@ $stmt = $pdo->prepare("
field_order,
excel_column,
json_node,
pdf_regions,
is_manual,
manual_default,
auto_value,
@@ -117,6 +120,14 @@ if (!is_array($jsonNodes)) {
}
$apiSampleJson = $template['api_sample_json'] ?? '';
// Dati PDF: nome file di esempio e metadati pagine
$samplePdf = $template['sample_pdf'] ?? '';
$pdfPageMeta = $template['pdf_page_meta'] ? json_decode($template['pdf_page_meta'], true) : [];
if (!is_array($pdfPageMeta)) {
$pdfPageMeta = [];
}
?>
<!doctype html>
@@ -129,6 +140,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
<?php include('cssinclude.php'); ?>
<title>Configure Template <?= htmlspecialchars($template['name'], ENT_QUOTES, 'UTF-8'); ?></title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<style>
@@ -383,8 +395,20 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
</div>
</div>
<?php elseif ($sourceType === 'PDF'): ?>
<div class="alert alert-warning">
PDF source type is not implemented yet.
<div class="mb-4">
<label class="form-label">Upload PDF Example:</label>
<input type="file" id="pdfUpload" class="form-control" accept="application/pdf">
<small id="pdfStatus" class="text-muted d-block mt-1">
<?php if (!empty($samplePdf)): ?>
Current file: <a href="pdftemplates/<?php echo htmlspecialchars($samplePdf); ?>" target="_blank"><?php echo htmlspecialchars($samplePdf); ?></a>
<?php else: ?>
No PDF uploaded yet.
<?php endif; ?>
</small>
<div class="alert alert-info mt-2 mb-2" style="font-size:13px;">
Per ogni campo, clicca <strong>Traccia PDF</strong> nella tabella qui sotto: prima disegna il riquadro dell'<strong>etichetta</strong> (blu), poi quello del <strong>dato</strong> (verde).
</div>
<div id="pdfPreviewContainer" style="max-height:70vh; overflow:auto; background:#525659; padding:20px 0; border-radius:6px;"></div>
</div>
<?php endif; ?>
@@ -467,6 +491,8 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
$mappingValue = 'xls';
} elseif ($sourceType === 'API' && !empty($mapping['json_node'])) {
$mappingValue = 'json';
} elseif ($sourceType === 'PDF' && !empty($mapping['pdf_regions'])) {
$mappingValue = 'pdf';
} elseif ((int)$mapping['is_manual'] === 1) {
$mappingValue = 'manual';
} else {
@@ -486,8 +512,9 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
<option value="xls" <?php echo ($mappingValue === 'xls') ? 'selected' : ''; ?>>Map to XLS Column</option>
<?php elseif ($sourceType === 'API'): ?>
<option value="json" <?php echo ($mappingValue === 'json') ? 'selected' : ''; ?>>Map to JSON Node</option>
<?php elseif ($sourceType === 'PDF'): ?>
<option value="pdf" <?php echo ($mappingValue === 'pdf') ? 'selected' : ''; ?>>Map to PDF Region</option>
<?php endif; ?>
<option value="auto" <?php echo ($mappingValue === 'auto') ? 'selected' : ''; ?>>Auto value</option>
<?php endif; ?>
@@ -520,6 +547,33 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
<option value="export_time" <?php echo ($autoValue === 'export_time') ? 'selected' : ''; ?>>Current time (export)</option>
</select>
<?php if ($sourceType === 'PDF'): ?>
<div class="pdf-track-wrap"
style="display:<?php echo ($mappingValue === 'pdf') ? 'block' : 'none'; ?>; margin-top:6px;">
<button type="button"
class="btn btn-outline-danger btn-sm pdf-track-btn"
data-id="<?php echo (int)$mapping['id']; ?>">
Traccia PDF
</button>
<span class="pdf-mapped-info"
data-id="<?php echo (int)$mapping['id']; ?>"
style="margin-left:6px; font-weight:600; color:#198754; display:<?php echo !empty($mapping['pdf_regions']) ? 'inline' : 'none'; ?>;">
<?php
if (!empty($mapping['pdf_regions'])) {
$reg = json_decode($mapping['pdf_regions'], true);
echo '✓ pag.' . (isset($reg['page']) ? (int)$reg['page'] : '?');
}
?>
</span>
<button type="button"
class="btn btn-danger btn-sm pdf-remove-btn"
data-id="<?php echo (int)$mapping['id']; ?>"
style="margin-left:5px; display:<?php echo !empty($mapping['pdf_regions']) ? 'inline-block' : 'none'; ?>;">
X
</button>
</div>
<?php endif; ?>
<?php if ($sourceType === 'XLS' && !empty($mapping['excel_column'])): ?>
<span class="mapped-column" style="margin-left:5px;">
(<?php echo htmlspecialchars($mapping['excel_column']); ?>)
@@ -1688,6 +1742,12 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
const removeBtn = tr.querySelector('.remove-xls');
const removeJsonBtn = tr.querySelector('.remove-json');
const pdfTrackWrap = tr.querySelector('.pdf-track-wrap');
// Mostra il blocco "Traccia PDF" solo quando il tipo è pdf
if (pdfTrackWrap) {
pdfTrackWrap.style.display = (mappingSelect.value === 'pdf') ? 'block' : 'none';
}
function destroyJsonSelect2() {
if (jsonSelect && window.jQuery && $(jsonSelect).hasClass('select2-hidden-accessible')) {
$(jsonSelect).select2('destroy');
@@ -1810,14 +1870,18 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
}
saveMapping(
mappingId,
mappingSelect.value,
manualInput ? manualInput.value : '',
xlsSelect ? xlsSelect.value : null,
autoSelect ? autoSelect.value : null,
jsonSelect ? jsonSelect.value : null
);
// Per il PDF il salvataggio avviene tramite save_pdf_regions.php
// quando l'utente traccia i riquadri, non qui.
if (mappingSelect.value !== 'pdf') {
saveMapping(
mappingId,
mappingSelect.value,
manualInput ? manualInput.value : '',
xlsSelect ? xlsSelect.value : null,
autoSelect ? autoSelect.value : null,
jsonSelect ? jsonSelect.value : null
);
}
if (sourceType === 'XLS') updateXlsDropdowns();
});
@@ -2571,6 +2635,30 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
}
</script>
<?php if ($sourceType === 'PDF'): ?>
<script>
// Variabili globali usate da pdf_mapping.js
window.PDF_TEMPLATE_ID = <?php echo (int)$id; ?>;
window.PDF_SAMPLE_FILE = <?php echo json_encode($samplePdf); ?>;
window.PDF_PAGE_META = <?php echo json_encode($pdfPageMeta); ?>;
window.PDF_TARGET_TABLE = <?php echo json_encode($template['target_table']); ?>;
// Riquadri già salvati nel DB: mapping_id -> regions object
window.PDF_SAVED_REGIONS = {
<?php foreach ($mappings as $m): ?>
<?php if (!empty($m['pdf_regions'])): ?>
<?php
$decodedRegions = json_decode($m['pdf_regions'], true);
if (is_array($decodedRegions)):
?> "<?php echo (int)$m['id']; ?>": <?php echo json_encode($decodedRegions); ?>,
<?php endif; ?>
<?php endif; ?>
<?php endforeach; ?>
};
</script>
<script src="pdf_mapping.js"></script>
<?php endif; ?>
</body>
</html>
+46 -9
View File
@@ -220,23 +220,60 @@ $(document).ready(function () {
dropdownParent: $("#partsModal"),
minimumResultsForSearch: 0,
sorter: function (data) {
return data.sort(function (a, b) {
const textA = (a.text || "").toLowerCase();
const textB = (b.text || "").toLowerCase();
return textA.localeCompare(textB, "it", {
sensitivity: "base",
numeric: true,
});
const rawTerm = ((window.__extraFieldSearchTerm || "") + "")
.toLowerCase()
.trim();
const terms = rawTerm.split(/\s+/).filter(Boolean);
const firstTerm = terms[0] || "";
const scoreOf = function (text) {
const t = (text || "").toLowerCase();
if (!firstTerm) return 3;
if (t === rawTerm) return 0; // match esatto
if (t.indexOf(firstTerm) === 0) return 1; // inizia col primo termine
if (
new RegExp(
"(^|\\s)" +
firstTerm.replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&",
),
).test(t)
)
return 2; // primo termine a inizio parola
return 3; // presente ma non a inizio parola
};
return data.slice().sort(function (a, b) {
const sa = scoreOf(a.text);
const sb = scoreOf(b.text);
if (sa !== sb) return sa - sb;
return (a.text || "")
.toLowerCase()
.localeCompare((b.text || "").toLowerCase(), "it", {
sensitivity: "base",
numeric: true,
});
});
},
matcher: function (params, data) {
if ($.trim(params.term) === "") return data;
if (typeof data.text === "undefined") return null;
const term = params.term.toLowerCase();
// memorizza il termine per il sorter (Select2 non lo passa al sorter)
window.__extraFieldSearchTerm = params.term;
const terms = params.term
.toLowerCase()
.split(/\s+/)
.filter(Boolean);
const text = data.text.toLowerCase();
return text.indexOf(term) > -1 ? data : null;
// AND: tutti i termini devono essere presenti (posizione-indipendente)
for (let i = 0; i < terms.length; i++) {
if (text.indexOf(terms[i]) === -1) return null;
}
return data;
},
});
+241
View File
@@ -0,0 +1,241 @@
<?php
/*
* pdf_extract_lib.php
* ------------------------------------------------------------------
* Cuore dell'estrazione PDF. Dato un file PDF e i riquadri (pdf_regions)
* salvati per ogni campo, usa `pdftotext -bbox -enc UTF-8` per ottenere
* le parole con le loro coordinate, e per ogni riquadro "value" raccoglie
* le parole il cui centro cade dentro il rettangolo.
*
* Le coordinate di pdftotext -bbox sono in punti PDF, origine in alto a
* sinistra: lo stesso sistema in cui PDF.js ha salvato i riquadri a scala
* 1.0. Quindi il match è diretto, senza conversioni.
*
* Funzioni pubbliche:
* - pdftotext_binary(): trova il percorso del binario su Win/Linux
* - pdf_extract_words($pdfPath): array di pagine con parole+coordinate
* - pdf_extract_values($pdfPath, $mappings): mapping_id => testo estratto
* ------------------------------------------------------------------
*/
/**
* Trova il binario pdftotext in base al sistema operativo.
* Su Windows cerca prima il percorso noto, poi il PATH.
* Su Linux usa /usr/bin/pdftotext, poi il PATH.
*/
function pdftotext_binary(): string
{
$isWindows = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN');
if ($isWindows) {
// Percorso noto sull'ambiente locale
$known = 'C:\\poppler\\poppler-26.02.0\\Library\\bin\\pdftotext.exe';
if (is_file($known)) {
return $known;
}
// Fallback: confida nel PATH
return 'pdftotext.exe';
}
// Linux / VPS
if (is_file('/usr/bin/pdftotext')) {
return '/usr/bin/pdftotext';
}
return 'pdftotext';
}
/**
* Lancia pdftotext -bbox e restituisce l'XHTML grezzo.
* @throws RuntimeException se il comando fallisce.
*/
function pdf_run_bbox(string $pdfPath): string
{
if (!is_file($pdfPath)) {
throw new RuntimeException("PDF non trovato: {$pdfPath}");
}
$bin = pdftotext_binary();
// -bbox: coordinate parole; -enc UTF-8: fix simboli (€, accenti); - : stdout
$cmd = escapeshellarg($bin)
. ' -bbox -enc UTF-8 '
. escapeshellarg($pdfPath)
. ' -';
$descriptors = [
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'], // stderr
];
$proc = proc_open($cmd, $descriptors, $pipes);
if (!is_resource($proc)) {
throw new RuntimeException("Impossibile avviare pdftotext.");
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exit = proc_close($proc);
if ($exit !== 0) {
throw new RuntimeException("pdftotext ha restituito codice {$exit}: {$stderr}");
}
return $stdout;
}
/**
* Parsa l'XHTML di pdftotext -bbox in una struttura:
* [
* 1 => [ 'width'=>612, 'height'=>792, 'words'=>[ ['t'=>'Invoice','xMin'=>..,'yMin'=>..,'xMax'=>..,'yMax'=>..], ... ] ],
* 2 => [ ... ],
* ]
* Chiave = numero pagina (1-based).
*/
function pdf_parse_bbox(string $xhtml): array
{
$pages = [];
// Isola ogni blocco <page ...> ... </page>
if (!preg_match_all('#<page\b[^>]*>(.*?)</page>#si', $xhtml, $pageMatches, PREG_OFFSET_CAPTURE)) {
return $pages;
}
// Recupera anche gli attributi width/height di ogni page
preg_match_all('#<page\s+width="([0-9.]+)"\s+height="([0-9.]+)"#i', $xhtml, $dimMatches, PREG_SET_ORDER);
$pageNum = 0;
foreach ($pageMatches[1] as $idx => $inner) {
$pageNum++;
$body = $inner[0];
$width = isset($dimMatches[$idx][1]) ? (float)$dimMatches[$idx][1] : 0.0;
$height = isset($dimMatches[$idx][2]) ? (float)$dimMatches[$idx][2] : 0.0;
$words = [];
if (preg_match_all(
'#<word\s+xMin="([0-9.\-]+)"\s+yMin="([0-9.\-]+)"\s+xMax="([0-9.\-]+)"\s+yMax="([0-9.\-]+)"\s*>(.*?)</word>#si',
$body,
$wm,
PREG_SET_ORDER
)) {
foreach ($wm as $w) {
$text = html_entity_decode($w[5], ENT_QUOTES | ENT_HTML5, 'UTF-8');
$words[] = [
't' => $text,
'xMin' => (float)$w[1],
'yMin' => (float)$w[2],
'xMax' => (float)$w[3],
'yMax' => (float)$w[4],
];
}
}
$pages[$pageNum] = [
'width' => $width,
'height' => $height,
'words' => $words,
];
}
return $pages;
}
/**
* Estrae tutte le parole con coordinate da un PDF.
* Ritorna la struttura di pdf_parse_bbox().
*/
function pdf_extract_words(string $pdfPath): array
{
$xhtml = pdf_run_bbox($pdfPath);
return pdf_parse_bbox($xhtml);
}
/**
* Dato un rettangolo {x,y,w,h} e la lista parole di una pagina, restituisce
* il testo delle parole il cui CENTRO cade dentro il rettangolo, ordinate
* per riga (yMin) e poi per colonna (xMin), unite da spazio.
*
* $tolerance espande il rettangolo di N punti su ogni lato (default 2),
* per non perdere parole tracciate al pelo del bordo.
*/
function pdf_text_in_rect(array $rect, array $words, float $tolerance = 2.0): string
{
$x1 = $rect['x'] - $tolerance;
$y1 = $rect['y'] - $tolerance;
$x2 = $rect['x'] + $rect['w'] + $tolerance;
$y2 = $rect['y'] + $rect['h'] + $tolerance;
$hits = [];
foreach ($words as $w) {
$cx = ($w['xMin'] + $w['xMax']) / 2.0;
$cy = ($w['yMin'] + $w['yMax']) / 2.0;
if ($cx >= $x1 && $cx <= $x2 && $cy >= $y1 && $cy <= $y2) {
$hits[] = $w;
}
}
if (empty($hits)) {
return '';
}
// Ordina per riga (yMin arrotondato a gruppi di ~3pt) poi per xMin
usort($hits, function ($a, $b) {
$ra = round($a['yMin'] / 3.0);
$rb = round($b['yMin'] / 3.0);
if ($ra !== $rb) {
return $ra <=> $rb;
}
return $a['xMin'] <=> $b['xMin'];
});
$parts = array_map(fn($w) => $w['t'], $hits);
$text = implode(' ', $parts);
// Normalizza spazi multipli
$text = preg_replace('/\s+/u', ' ', $text);
return trim($text);
}
/**
* Estrae i valori per ogni mapping che ha pdf_regions.
*
* @param string $pdfPath percorso del PDF da leggere
* @param array $mappings righe di template_mapping (devono contenere: id, pdf_regions)
* @return array [ mapping_id => testo_estratto ] (solo per i mapping con regione value)
*
* Nota: usa la regione "value" per estrarre il dato. La regione "label" al
* momento non è usata per l'estrazione (serve per la strategia futura di
* ricerca per etichetta, es. PDF con layout variabile).
*/
function pdf_extract_values(string $pdfPath, array $mappings): array
{
$pages = pdf_extract_words($pdfPath);
$result = [];
foreach ($mappings as $m) {
if (empty($m['pdf_regions'])) {
continue;
}
$regions = json_decode($m['pdf_regions'], true);
if (!is_array($regions) || empty($regions['value']) || empty($regions['page'])) {
continue;
}
$page = (int)$regions['page'];
if (!isset($pages[$page])) {
// pagina inesistente in questo PDF: valore vuoto
$result[(int)$m['id']] = '';
continue;
}
$value = $pdf_value_rect = $regions['value'];
$text = pdf_text_in_rect($value, $pages[$page]['words']);
$result[(int)$m['id']] = $text;
}
return $result;
}
+631
View File
@@ -0,0 +1,631 @@
/*
* pdf_mapping.js
* ------------------------------------------------------------------
* Logica di anteprima PDF e tracciamento riquadri (etichetta + dato)
* per la pagina mapping_template_xls_scheme2.php quando source = PDF.
*
* Dipendenze (caricate dalla pagina PHP):
* - PDF.js (pdfjsLib) via CDN
* - Le variabili globali definite inline nella pagina:
* window.PDF_TEMPLATE_ID -> id del template (int)
* window.PDF_SAMPLE_FILE -> nome file PDF di esempio (string, può essere '')
* window.PDF_PAGE_META -> metadati pagine salvati (object/array)
*
* Convenzioni:
* - Le coordinate dei riquadri sono salvate in "unità PDF" (punti PDF,
* scala 1.0), NON in pixel schermo. Così restano valide a qualsiasi zoom.
* - Ogni campo mappato ha un bottone "Traccia PDF" che attiva la modalità
* disegno per quel campo: prima l'etichetta, poi il dato.
*
* NB: questo file NON tocca la logica XLS/API della pagina. Si attiva solo
* se sourceType === 'PDF'.
* ------------------------------------------------------------------
*/
(function () {
"use strict";
// Esci subito se non siamo su un template PDF
if (typeof sourceType === "undefined" || sourceType !== "PDF") {
return;
}
// ----- Stato modulo -----
const state = {
pdfDoc: null, // documento PDF.js caricato
pageCanvases: {}, // page number -> { canvas, viewport, scale }
renderScale: 1.5, // scala di rendering a schermo
drawing: null, // { mappingId, phase:'label'|'value', startX, startY, rectEl, page }
pendingLabel: null, // riquadro etichetta in attesa (unità PDF) durante il disegno a due fasi
activeMappingId: null, // campo attualmente in tracciamento
};
// ----- Utility DOM -----
function el(tag, attrs = {}, children = []) {
const node = document.createElement(tag);
Object.entries(attrs).forEach(([k, v]) => {
if (k === "style" && typeof v === "object") {
Object.assign(node.style, v);
} else if (k === "class") {
node.className = v;
} else if (k === "text") {
node.textContent = v;
} else {
node.setAttribute(k, v);
}
});
(Array.isArray(children) ? children : [children]).forEach((c) => {
if (c)
node.appendChild(
typeof c === "string" ? document.createTextNode(c) : c,
);
});
return node;
}
function getPdfContainer() {
return document.getElementById("pdfPreviewContainer");
}
// ----- Caricamento e rendering PDF -----
async function loadPdf(url) {
const status = document.getElementById("pdfStatus");
try {
if (status) status.textContent = "Loading PDF...";
const loadingTask = pdfjsLib.getDocument(url);
state.pdfDoc = await loadingTask.promise;
await renderAllPages();
if (status)
status.textContent = `✅ PDF loaded: ${state.pdfDoc.numPages} page(s)`;
// Salva i metadati pagine (dimensioni a scala 1.0)
savePageMeta();
// Ridisegna i riquadri già salvati nel DB
redrawSavedRegions();
} catch (err) {
console.error("PDF load error:", err);
if (status)
status.textContent = "❌ Error loading PDF: " + err.message;
}
}
async function renderAllPages() {
const container = getPdfContainer();
if (!container) return;
container.innerHTML = "";
state.pageCanvases = {};
for (let pageNum = 1; pageNum <= state.pdfDoc.numPages; pageNum++) {
const page = await state.pdfDoc.getPage(pageNum);
const viewport = page.getViewport({ scale: state.renderScale });
// Wrapper posizionato per contenere canvas + overlay riquadri
const pageWrap = el("div", {
class: "pdf-page-wrap",
"data-page": pageNum,
style: {
position: "relative",
margin: "0 auto 20px auto",
width: viewport.width + "px",
height: viewport.height + "px",
boxShadow: "0 0 6px rgba(0,0,0,0.3)",
},
});
const canvas = el("canvas");
canvas.width = viewport.width;
canvas.height = viewport.height;
canvas.style.display = "block";
const overlay = el("div", {
class: "pdf-region-overlay",
"data-page": pageNum,
style: {
position: "absolute",
top: "0",
left: "0",
width: viewport.width + "px",
height: viewport.height + "px",
cursor: "crosshair",
},
});
pageWrap.appendChild(canvas);
pageWrap.appendChild(overlay);
container.appendChild(pageWrap);
const ctx = canvas.getContext("2d");
await page.render({ canvasContext: ctx, viewport }).promise;
state.pageCanvases[pageNum] = {
canvas,
overlay,
viewport,
scale: state.renderScale,
pdfWidth: viewport.width / state.renderScale, // larghezza a scala 1.0
pdfHeight: viewport.height / state.renderScale, // altezza a scala 1.0
};
attachOverlayEvents(overlay, pageNum);
}
}
// ----- Conversione pixel schermo <-> unità PDF -----
function screenToPdf(pageNum, xPx, yPx) {
const p = state.pageCanvases[pageNum];
return { x: xPx / p.scale, y: yPx / p.scale };
}
function pdfToScreen(pageNum, xPdf, yPdf) {
const p = state.pageCanvases[pageNum];
return { x: xPdf * p.scale, y: yPdf * p.scale };
}
// ----- Disegno riquadri sull'overlay -----
function attachOverlayEvents(overlay, pageNum) {
let startX = 0,
startY = 0;
let rectEl = null;
overlay.addEventListener("mousedown", (e) => {
if (!state.activeMappingId) return; // nessun campo in tracciamento
const rect = overlay.getBoundingClientRect();
startX = e.clientX - rect.left;
startY = e.clientY - rect.top;
rectEl = el("div", {
class: "pdf-draw-rect",
style: {
position: "absolute",
left: startX + "px",
top: startY + "px",
width: "0px",
height: "0px",
border: "2px solid #dc3545",
background: "rgba(220,53,69,0.15)",
pointerEvents: "none",
},
});
overlay.appendChild(rectEl);
});
overlay.addEventListener("mousemove", (e) => {
if (!rectEl) return;
const rect = overlay.getBoundingClientRect();
const curX = e.clientX - rect.left;
const curY = e.clientY - rect.top;
const x = Math.min(startX, curX);
const y = Math.min(startY, curY);
const w = Math.abs(curX - startX);
const h = Math.abs(curY - startY);
Object.assign(rectEl.style, {
left: x + "px",
top: y + "px",
width: w + "px",
height: h + "px",
});
});
overlay.addEventListener("mouseup", (e) => {
if (!rectEl) return;
const rect = overlay.getBoundingClientRect();
const endX = e.clientX - rect.left;
const endY = e.clientY - rect.top;
const x = Math.min(startX, endX);
const y = Math.min(startY, endY);
const w = Math.abs(endX - startX);
const h = Math.abs(endY - startY);
overlay.removeChild(rectEl);
rectEl = null;
if (w < 4 || h < 4) return; // click accidentale, ignora
// Converti in unità PDF
const topLeft = screenToPdf(pageNum, x, y);
const pdfRect = {
x: topLeft.x,
y: topLeft.y,
w: w / state.pageCanvases[pageNum].scale,
h: h / state.pageCanvases[pageNum].scale,
};
handleRectDrawn(pageNum, pdfRect);
});
}
// ----- Logica a due fasi: prima etichetta, poi dato -----
function startTracking(mappingId) {
state.activeMappingId = mappingId;
state.pendingLabel = null;
setBanner(
`Traccia il riquadro dell'ETICHETTA per il campo #${mappingId} (poi il dato). ESC per annullare.`,
);
}
function handleRectDrawn(pageNum, pdfRect) {
if (!state.activeMappingId) return;
if (state.pendingLabel === null) {
// Fase 1: etichetta
state.pendingLabel = { page: pageNum, rect: pdfRect };
drawPersistentRect(
pageNum,
pdfRect,
"label",
state.activeMappingId,
);
setBanner(
`Ora traccia il riquadro del DATO per il campo #${state.activeMappingId}. ESC per annullare.`,
);
} else {
// Fase 2: dato — deve stare sulla stessa pagina dell'etichetta
if (pageNum !== state.pendingLabel.page) {
alert("Etichetta e dato devono stare sulla stessa pagina.");
return;
}
const regions = {
page: pageNum,
label: state.pendingLabel.rect,
value: pdfRect,
};
drawPersistentRect(
pageNum,
pdfRect,
"value",
state.activeMappingId,
);
saveRegions(state.activeMappingId, regions);
// Reset stato tracciamento
const doneId = state.activeMappingId;
state.activeMappingId = null;
state.pendingLabel = null;
clearBanner();
updateRowBadge(doneId, regions);
}
}
// Disegna un riquadro persistente (già confermato) sull'overlay
function drawPersistentRect(pageNum, pdfRect, kind, mappingId) {
const p = state.pageCanvases[pageNum];
if (!p) return;
const tl = pdfToScreen(pageNum, pdfRect.x, pdfRect.y);
const w = pdfRect.w * p.scale;
const h = pdfRect.h * p.scale;
const color = kind === "label" ? "#0d6efd" : "#198754";
const box = el(
"div",
{
class: "pdf-saved-rect",
"data-mapping-id": mappingId,
"data-kind": kind,
style: {
position: "absolute",
left: tl.x + "px",
top: tl.y + "px",
width: w + "px",
height: h + "px",
border: "2px solid " + color,
background:
kind === "label"
? "rgba(13,110,253,0.12)"
: "rgba(25,135,84,0.12)",
pointerEvents: "none",
},
},
[
el("span", {
style: {
position: "absolute",
top: "-16px",
left: "0",
fontSize: "10px",
fontWeight: "600",
color: "#fff",
background: color,
padding: "0 4px",
borderRadius: "3px",
whiteSpace: "nowrap",
},
text:
(kind === "label" ? "Etichetta" : "Dato") +
" #" +
mappingId,
}),
],
);
p.overlay.appendChild(box);
}
// Rimuove i riquadri disegnati per un dato campo (prima di ridisegnarli)
function clearRectsForMapping(mappingId) {
document
.querySelectorAll(`.pdf-saved-rect[data-mapping-id="${mappingId}"]`)
.forEach((node) => node.remove());
}
// ----- Banner istruzioni -----
function setBanner(msg) {
let banner = document.getElementById("pdfTrackingBanner");
if (!banner) {
banner = el("div", {
id: "pdfTrackingBanner",
class: "alert alert-info",
style: {
position: "sticky",
top: "0",
zIndex: "1000",
marginBottom: "10px",
},
});
const container = getPdfContainer();
container.parentNode.insertBefore(banner, container);
}
banner.textContent = msg;
banner.style.display = "block";
}
function clearBanner() {
const banner = document.getElementById("pdfTrackingBanner");
if (banner) banner.style.display = "none";
}
// ESC annulla il tracciamento in corso
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && state.activeMappingId) {
clearRectsForMapping(state.activeMappingId);
// ridisegna eventuali riquadri salvati per quel campo
const saved = getSavedRegionsForMapping(state.activeMappingId);
if (saved) {
if (saved.label)
drawPersistentRect(
saved.page,
saved.label,
"label",
state.activeMappingId,
);
if (saved.value)
drawPersistentRect(
saved.page,
saved.value,
"value",
state.activeMappingId,
);
}
state.activeMappingId = null;
state.pendingLabel = null;
clearBanner();
}
});
// ----- Persistenza -----
function saveRegions(mappingId, regions) {
fetch("save_pdf_regions.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: mappingId,
mapping_type: "pdf",
pdf_regions: regions,
manual_default: null,
auto_value: "none",
tablename:
typeof PDF_TARGET_TABLE !== "undefined"
? PDF_TARGET_TABLE
: "",
}),
})
.then((r) => r.json())
.then((d) => {
if (!d.success) {
console.error("❌ Error saving PDF regions:", d.message);
alert("Errore nel salvataggio dei riquadri: " + d.message);
} else {
console.log("✅ PDF regions saved for mapping", mappingId);
// aggiorna la cache locale
savedRegionsCache[mappingId] = regions;
}
})
.catch((err) =>
console.error("❌ Fetch error saving PDF regions:", err),
);
}
function savePageMeta() {
const dims = [];
Object.entries(state.pageCanvases).forEach(([pageNum, p]) => {
dims.push({
page: parseInt(pageNum, 10),
width: Math.round(p.pdfWidth),
height: Math.round(p.pdfHeight),
});
});
const meta = { pages: state.pdfDoc.numPages, dims };
fetch("update_pdf_page_meta.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
template_id: PDF_TEMPLATE_ID,
pdf_page_meta: JSON.stringify(meta),
}),
})
.then((r) => r.json())
.then((d) => {
if (!d.success)
console.error("❌ Error saving page meta:", d.message);
})
.catch((err) =>
console.error("❌ Fetch error saving page meta:", err),
);
}
// ----- Riquadri già salvati (letti dal DOM al load) -----
// Popolata dalla pagina PHP: mapping_id -> regions object
const savedRegionsCache =
typeof window.PDF_SAVED_REGIONS === "object" && window.PDF_SAVED_REGIONS
? window.PDF_SAVED_REGIONS
: {};
function getSavedRegionsForMapping(mappingId) {
return savedRegionsCache[mappingId] || null;
}
function redrawSavedRegions() {
Object.entries(savedRegionsCache).forEach(([mappingId, regions]) => {
if (!regions) return;
const page = regions.page;
if (!state.pageCanvases[page]) return;
if (regions.label)
drawPersistentRect(page, regions.label, "label", mappingId);
if (regions.value)
drawPersistentRect(page, regions.value, "value", mappingId);
updateRowBadge(mappingId, regions);
});
}
// ----- Aggiorna il badge/etichetta nella riga della tabella -----
function updateRowBadge(mappingId, regions) {
const badge = document.querySelector(
`.pdf-mapped-info[data-id="${mappingId}"]`,
);
if (badge) {
if (regions && regions.value) {
badge.textContent = `✓ pag.${regions.page}`;
badge.style.display = "inline";
} else {
badge.textContent = "";
badge.style.display = "none";
}
}
}
// ----- Aggancio dei bottoni "Traccia PDF" nelle righe -----
function wireTrackButtons() {
document.querySelectorAll(".pdf-track-btn").forEach((btn) => {
btn.addEventListener("click", () => {
const mappingId = btn.getAttribute("data-id");
if (!state.pdfDoc) {
alert("Carica prima un PDF di esempio.");
return;
}
// se il campo aveva già riquadri, li ripuliamo per ridisegnarli
clearRectsForMapping(mappingId);
startTracking(mappingId);
// scrolla alla preview
getPdfContainer().scrollIntoView({
behavior: "smooth",
block: "start",
});
});
});
}
// ----- Bottone rimuovi riquadri per campo -----
function wireRemoveButtons() {
document.querySelectorAll(".pdf-remove-btn").forEach((btn) => {
btn.addEventListener("click", () => {
const mappingId = btn.getAttribute("data-id");
clearRectsForMapping(mappingId);
delete savedRegionsCache[mappingId];
updateRowBadge(mappingId, null);
// salva reset (mapping vuoto)
fetch("save_pdf_regions.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: mappingId,
mapping_type: "",
pdf_regions: null,
manual_default: null,
auto_value: "none",
tablename:
typeof PDF_TARGET_TABLE !== "undefined"
? PDF_TARGET_TABLE
: "",
}),
})
.then((r) => r.json())
.then((d) => {
if (!d.success)
console.error(
"❌ Error removing PDF regions:",
d.message,
);
})
.catch((err) =>
console.error(
"❌ Fetch error removing PDF regions:",
err,
),
);
});
});
}
// ----- Upload del PDF di esempio -----
function wireUpload() {
const input = document.getElementById("pdfUpload");
if (!input) return;
input.addEventListener("change", (event) => {
const file = event.target.files[0];
if (!file) return;
const status = document.getElementById("pdfStatus");
if (status) status.textContent = "Uploading...";
const formData = new FormData();
formData.append("pdf_file", file);
formData.append("template_id", PDF_TEMPLATE_ID);
fetch("upload_pdf_example.php", { method: "POST", body: formData })
.then((r) => r.json())
.then((data) => {
if (!data.success) {
if (status)
status.textContent =
"❌ Upload failed: " + data.message;
return;
}
if (status) {
status.innerHTML = `✅ File uploaded: <a href="pdftemplates/${data.filename}" target="_blank">${data.filename}</a>`;
}
// carica subito per l'anteprima usando il file locale scelto
const localUrl = URL.createObjectURL(file);
loadPdf(localUrl);
})
.catch((err) => {
if (status)
status.textContent = "❌ Upload failed. Check console.";
console.error(err);
});
});
}
// ----- Init -----
document.addEventListener("DOMContentLoaded", function () {
// Configura il worker di PDF.js
if (typeof pdfjsLib !== "undefined" && pdfjsLib.GlobalWorkerOptions) {
pdfjsLib.GlobalWorkerOptions.workerSrc =
"https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
}
wireUpload();
wireTrackButtons();
wireRemoveButtons();
// Se c'è già un PDF di esempio salvato, caricalo dal server
if (typeof PDF_SAMPLE_FILE !== "undefined" && PDF_SAMPLE_FILE) {
loadPdf("pdftemplates/" + PDF_SAMPLE_FILE);
}
});
})();
+256
View File
@@ -0,0 +1,256 @@
<?php
/*
* process_import_pdf.php
* ------------------------------------------------------------------
* Riceve N PDF (multipart, campo pdf_files[]) + template_id + importreferencecode.
* Per ogni PDF:
* 1. salva il file in public/userarea/imported_pdf/
* 2. estrae i valori dai riquadri (pdf_extract_lib.php)
* 3. inserisce UNA riga in datadb + i dettagli in import_data_details
* applicando la STESSA logica di import_insert_batch.php
* (tipi di dato, auto_value, campo 244 con lims_user_id)
*
* Tutti i PDF del gruppo condividono lo stesso importreferencecode.
*
* Ritorna JSON: { ok, results: [ {filename, ok, iddatadb, values{}, error} ] }
* ------------------------------------------------------------------
*/
ini_set('display_errors', 0);
error_reporting(E_ALL);
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/pdf_import_debug.log');
include('include/headscript.php');
require_once __DIR__ . '/pdf_extract_lib.php';
header('Content-Type: application/json; charset=utf-8');
function respond(array $payload): void
{
echo json_encode($payload);
exit;
}
// ----- Validazione input -----
$template_id = isset($_POST['template_id']) ? (int)$_POST['template_id'] : 0;
$importReferenceCode = isset($_POST['importreferencecode']) ? (string)$_POST['importreferencecode'] : '';
if ($template_id <= 0) {
respond(['ok' => false, 'error' => 'template_id non valido']);
}
if ($importReferenceCode === '') {
respond(['ok' => false, 'error' => 'importreferencecode mancante']);
}
if (empty($_FILES['pdf_files']) || !is_array($_FILES['pdf_files']['name'])) {
respond(['ok' => false, 'error' => 'Nessun PDF ricevuto']);
}
$user_id = $iduserlogin ?? 1;
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// ----- Mapping del template -----
$stmt = $pdo->prepare("
SELECT id, pdf_regions, data_type, is_required, manual_default, is_manual,
field_label, field_id, main_field, auto_value
FROM template_mapping
WHERE template_id = ?
");
$stmt->execute([$template_id]);
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($allMappings)) {
respond(['ok' => false, 'error' => 'Nessun mapping trovato per il template']);
}
// ----- idclient di default -----
$tplStmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
$tplStmt->execute([$template_id]);
$tpl = $tplStmt->fetch(PDO::FETCH_ASSOC);
$default_idclient = $tpl['idclient'] ?? null;
// ----- lims_user_id per il campo 244 (come nel batch XLS) -----
$stmtUser = $pdo->prepare("SELECT lims_user_id FROM auth_users WHERE id = ? LIMIT 1");
$stmtUser->execute([(int)$user_id]);
$limsUserId = $stmtUser->fetchColumn();
$limsUserId = ($limsUserId !== false && $limsUserId !== null && $limsUserId !== '') ? (string)$limsUserId : '';
$stmtMap = $pdo->prepare("SELECT id FROM template_mapping WHERE template_id = ? AND field_id = 244 LIMIT 1");
$stmtMap->execute([(int)$template_id]);
$mappingId244 = (int)$stmtMap->fetchColumn();
// ----- Cartella di destinazione dei PDF importati -----
$importFolder = __DIR__ . DIRECTORY_SEPARATOR . 'pdftemplates' . DIRECTORY_SEPARATOR . 'imported';
if (!is_dir($importFolder)) {
@mkdir($importFolder, 0775, true);
}
/**
* Applica la conversione del valore in base al data_type,
* REPLICA della logica di import_insert_batch.php.
*/
function convertFieldValue(array $mapping, ?string $rawValue): string
{
$fieldValue = $rawValue;
if (!$mapping['is_manual']) {
// Valore estratto dal PDF; se vuoto, fallback al manual_default
if ($fieldValue === null || $fieldValue === '') {
$fieldValue = $mapping['manual_default'] ?? '';
}
switch ($mapping['data_type']) {
case 'INT':
$fieldValue = is_numeric($fieldValue) ? (int)$fieldValue : ($mapping['manual_default'] ?? 0);
break;
case 'DATE':
$fieldValue = !empty($fieldValue)
? date('Y-m-d', strtotime($fieldValue))
: ($mapping['manual_default'] === 'today' ? date('Y-m-d') : ($mapping['manual_default'] ?? ''));
break;
case 'CHAR':
$fieldValue = !empty($fieldValue) ? substr((string)$fieldValue, 0, 1) : ($mapping['manual_default'] ?? '');
break;
case 'Testo':
case 'VARCHAR':
default:
$fieldValue = !empty($fieldValue) ? (string)$fieldValue : ($mapping['manual_default'] ?? '');
break;
}
} else {
// Campo manuale: usa manual_default
$fieldValue = $mapping['manual_default'] ?? '';
if ($mapping['data_type'] === 'DATE' && $mapping['manual_default'] === 'today') {
$fieldValue = date('Y-m-d');
}
}
// auto_value se ancora vuoto (come nel batch XLS)
if (($fieldValue === null || $fieldValue === '') && !empty($mapping['auto_value']) && $mapping['auto_value'] !== 'none') {
if ($mapping['auto_value'] === 'import_date') {
$fieldValue = date('Y-m-d');
} elseif ($mapping['auto_value'] === 'import_time') {
$fieldValue = date('H:i');
}
}
return (string)($fieldValue ?? '');
}
$results = [];
$nFiles = count($_FILES['pdf_files']['name']);
for ($i = 0; $i < $nFiles; $i++) {
$origName = $_FILES['pdf_files']['name'][$i];
$tmpName = $_FILES['pdf_files']['tmp_name'][$i];
$err = $_FILES['pdf_files']['error'][$i];
$fileResult = [
'filename' => $origName,
'ok' => false,
'iddatadb' => null,
'values' => [],
'error' => '',
];
// Errore di upload
if ($err !== UPLOAD_ERR_OK) {
$fileResult['error'] = 'Errore upload (codice ' . $err . ')';
$results[] = $fileResult;
continue;
}
// Estensione PDF
if (strtolower(pathinfo($origName, PATHINFO_EXTENSION)) !== 'pdf') {
$fileResult['error'] = 'Non è un PDF';
$results[] = $fileResult;
continue;
}
// Nome sicuro e univoco
$safeBase = preg_replace('/[^A-Za-z0-9_\-]/', '_', pathinfo($origName, PATHINFO_FILENAME));
$safeBase = trim($safeBase, '_') ?: 'pdf';
$storedName = 'imp' . $template_id . '_' . $safeBase . '_' . date('YmdHis') . '_' . $i . '.pdf';
$storedPath = $importFolder . DIRECTORY_SEPARATOR . $storedName;
if (!move_uploaded_file($tmpName, $storedPath)) {
$fileResult['error'] = 'Impossibile salvare il file';
$results[] = $fileResult;
continue;
}
// ----- Estrazione dai riquadri -----
try {
$extracted = pdf_extract_values($storedPath, $allMappings); // [mapping_id => testo]
} catch (Throwable $e) {
$fileResult['error'] = 'Estrazione fallita: ' . $e->getMessage();
$results[] = $fileResult;
continue;
}
// ----- Inserimento in datadb + dettagli (una riga per PDF) -----
try {
$pdo->beginTransaction();
$insDatadb = $pdo->prepare("INSERT INTO datadb (templateid, importreferencecode, filename_import, status, user_id, limscode, importdate, excelrow, idclient) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$insDetail = $pdo->prepare("INSERT INTO import_data_details (id, mapping_id, field_value) VALUES (?, ?, ?)");
// excelrow non ha senso per il PDF: usiamo 0 (o l'indice del file)
$insDatadb->execute([
$template_id,
$importReferenceCode,
$storedName,
'i',
$user_id,
null,
date('Y-m-d'),
0,
$default_idclient
]);
$iddatadb = $pdo->lastInsertId();
$fileResult['iddatadb'] = $iddatadb;
$shownValues = [];
foreach ($allMappings as $mapping) {
$rawValue = $extracted[(int)$mapping['id']] ?? null;
$fieldValue = convertFieldValue($mapping, $rawValue);
$insDetail->execute([$iddatadb, $mapping['id'], $fieldValue]);
// per il feedback UI mostra solo i campi con riquadro
if (!empty($mapping['pdf_regions'])) {
$shownValues[$mapping['field_label'] ?? ('#' . $mapping['id'])] = $fieldValue;
}
}
// Campo 244 (Accettatore) con lims_user_id
if ($limsUserId !== '' && $mappingId244 > 0) {
$insDetail->execute([$iddatadb, $mappingId244, $limsUserId]);
}
$pdo->commit();
$fileResult['ok'] = true;
$fileResult['values'] = $shownValues;
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
error_log('[PDF IMPORT] ' . $e->getMessage());
$fileResult['error'] = 'Inserimento DB fallito: ' . $e->getMessage();
}
$results[] = $fileResult;
}
respond([
'ok' => true,
'importreferencecode' => $importReferenceCode,
'template_id' => $template_id,
'results' => $results,
]);
+50 -22
View File
@@ -4,6 +4,7 @@ include('include/headscript.php');
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$data = json_decode(file_get_contents('php://input'), true);
@@ -15,46 +16,73 @@ if (!$idquotations || empty($parts)) {
exit;
}
$part = $parts[0];
$partId = $part['id'] ?? null;
$partNumber = $part['part_number'] ?? null;
$partDescription = $part['part_description'] ?? '';
$mix = $part['mix'] ?? 'N';
try {
$pdo->beginTransaction();
$results = [];
foreach ($parts as $part) {
$partId = $part['id'] ?? null;
$partNumber = $part['part_number'] ?? null;
$partDescription = $part['part_description'] ?? '';
$mix = $part['mix'] ?? 'N';
$idmatrice = $part['idmatrice'] ?? null;
$note = $part['note'] ?? null;
$dateexpiry = $part['dateexpiry'] ?? null;
if ($partDescription) {
try {
if ($partId) {
// UPDATE se esiste già la parte
// UPDATE se la parte esiste
$stmt = $pdo->prepare("UPDATE identification_parts
SET part_number = :part_number,
part_description = :part_description,
mix = :mix,
idmatrice = :idmatrice,
note = :note,
dateexpiry = :dateexpiry,
updated_at = NOW()
WHERE id = :id");
WHERE id = :id AND idquotations = :idquotations");
$stmt->execute([
':id' => $partId,
':part_number' => $partNumber,
':part_description' => $partDescription,
':mix' => $mix
':mix' => $mix,
':idmatrice' => $idmatrice,
':note' => $note,
':dateexpiry' => $dateexpiry,
':idquotations' => $idquotations,
]);
echo json_encode(['success' => true, 'part_id' => $partId, 'part_number' => $partNumber, 'message' => 'Parte aggiornata con successo']);
} else {
// INSERT se è nuova
$results[] = [
'part_id' => $partId,
'part_number' => $partNumber,
'message' => 'Parte aggiornata con successo'
];
} else if ($partDescription || $note || $dateexpiry) {
// INSERT per nuova parte (solo se ha contenuto)
$stmt = $pdo->prepare("INSERT INTO identification_parts
(idquotations, part_number, part_description, mix, created_at, updated_at)
VALUES (:idquotations, :part_number, :part_description, :mix, NOW(), NOW())");
(idquotations, part_number, part_description, mix, idmatrice, note, dateexpiry, created_at, updated_at)
VALUES (:idquotations, :part_number, :part_description, :mix, :idmatrice, :note, :dateexpiry, NOW(), NOW())");
$stmt->execute([
':idquotations' => $idquotations,
':part_number' => $partNumber,
':part_description' => $partDescription,
':mix' => $mix
':mix' => $mix,
':idmatrice' => $idmatrice,
':note' => $note,
':dateexpiry' => $dateexpiry,
]);
$newId = $pdo->lastInsertId();
echo json_encode(['success' => true, 'part_id' => $newId, 'part_number' => $partNumber, 'message' => 'Parte salvata con successo']);
$newId = (int)$pdo->lastInsertId();
$results[] = [
'part_id' => $newId,
'part_number' => $partNumber,
'message' => 'Parte salvata con successo'
];
}
} catch (PDOException $e) {
echo json_encode(['success' => false, 'message' => 'Errore nel salvataggio: ' . $e->getMessage()]);
}
} else {
echo json_encode(['success' => false, 'message' => 'Descrizione mancante']);
$pdo->commit();
echo json_encode(['success' => true, 'results' => $results]);
} catch (PDOException $e) {
$pdo->rollBack();
echo json_encode(['success' => false, 'message' => 'Errore nel salvataggio: ' . $e->getMessage()]);
}
+143
View File
@@ -0,0 +1,143 @@
<?php
// save_pdf_regions.php
// Salva i riquadri (etichetta + dato) di un singolo campo mappato
// nella colonna pdf_regions della tabella template_mapping.
//
// Riceve JSON:
// {
// id: <mapping id>,
// mapping_type: 'pdf' | 'manual' | 'auto' | '',
// pdf_regions: { page, label:{x,y,w,h}, value:{x,y,w,h} } | null,
// manual_default: <string|null>,
// auto_value: <string>,
// tablename: <string>
// }
//
// Coerente con save_mapping_json.php: quando mapping_type = 'pdf'
// azzera excel_column/json_node e scrive pdf_regions; per gli altri
// tipi azzera pdf_regions.
header('Content-Type: application/json');
include('include/headscript.php');
function respond(bool $success, string $message = ''): void
{
echo json_encode(['success' => $success, 'message' => $message]);
exit;
}
$raw = file_get_contents('php://input');
$data = json_decode($raw, true);
if (!is_array($data)) {
respond(false, 'Invalid JSON payload.');
}
$mappingId = isset($data['id']) ? (int)$data['id'] : 0;
$mappingType = isset($data['mapping_type']) ? (string)$data['mapping_type'] : '';
$manualDef = $data['manual_default'] ?? null;
$autoValue = $data['auto_value'] ?? 'none';
$regions = $data['pdf_regions'] ?? null;
if ($mappingId <= 0) {
respond(false, 'Missing or invalid mapping id.');
}
// --- Validazione e normalizzazione dei riquadri ---
// Accettiamo solo se mapping_type = 'pdf'. Negli altri casi pdf_regions = NULL.
$pdfRegionsJson = null;
if ($mappingType === 'pdf') {
if (!is_array($regions)) {
respond(false, 'pdf_regions must be an object when mapping_type is pdf.');
}
// Valida un singolo rettangolo {x,y,w,h} con numeri >= 0
$validateRect = function ($rect): ?array {
if (!is_array($rect)) {
return null;
}
$out = [];
foreach (['x', 'y', 'w', 'h'] as $k) {
if (!isset($rect[$k]) || !is_numeric($rect[$k])) {
return null;
}
$out[$k] = (float)$rect[$k];
if ($out[$k] < 0) {
$out[$k] = 0;
}
}
return $out;
};
$page = isset($regions['page']) ? (int)$regions['page'] : 0;
if ($page < 1) {
respond(false, 'pdf_regions.page must be >= 1.');
}
// value è obbligatorio, label è opzionale (modalità "solo dato")
$valueRect = $validateRect($regions['value'] ?? null);
if ($valueRect === null) {
respond(false, 'pdf_regions.value rectangle is invalid.');
}
$labelRect = null;
if (isset($regions['label']) && $regions['label'] !== null) {
$labelRect = $validateRect($regions['label']);
if ($labelRect === null) {
respond(false, 'pdf_regions.label rectangle is invalid.');
}
}
$clean = [
'page' => $page,
'label' => $labelRect,
'value' => $valueRect,
];
$pdfRegionsJson = json_encode($clean, JSON_UNESCAPED_UNICODE);
}
// --- Normalizza manual/auto in base al tipo ---
$manualToSave = ($mappingType === 'manual') ? $manualDef : null;
$autoToSave = ($mappingType === 'auto') ? ($autoValue ?: 'none') : 'none';
// is_manual coerente con il tipo
$isManual = ($mappingType === 'manual') ? 1 : 0;
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Quando è PDF: azzero excel_column e json_node, scrivo pdf_regions.
// Quando NON è PDF: azzero pdf_regions.
$sql = "
UPDATE template_mapping
SET
pdf_regions = :pdf_regions,
excel_column = CASE WHEN :is_pdf = 1 THEN NULL ELSE excel_column END,
json_node = CASE WHEN :is_pdf2 = 1 THEN NULL ELSE json_node END,
is_manual = :is_manual,
manual_default = :manual_default,
auto_value = :auto_value
WHERE id = :id
";
$isPdf = ($mappingType === 'pdf') ? 1 : 0;
$stmt = $pdo->prepare($sql);
$stmt->execute([
':pdf_regions' => $pdfRegionsJson,
':is_pdf' => $isPdf,
':is_pdf2' => $isPdf,
':is_manual' => $isManual,
':manual_default' => $manualToSave,
':auto_value' => $autoToSave,
':id' => $mappingId,
]);
} catch (Throwable $e) {
respond(false, 'Database update failed: ' . $e->getMessage());
}
respond(true, 'PDF regions saved.');
@@ -912,6 +912,12 @@
"ConteggioClienti": 0,
"Nome": "ETON",
"Descrizione": "Schema da usare per ETON\r\n"
},
{
"IdSchemaCustomFields": 209,
"ConteggioClienti": 0,
"Nome": "LBS Flammability",
"Descrizione": "Schema per tutti i campioni di LBS con Flammability\r\n"
}
]
}
+8
View File
@@ -78,6 +78,11 @@
color: #198754;
}
.badge-source-pdf {
background-color: #ffeaea;
color: #dc3545;
}
#xlsTemplatesTable {
font-size: 13px;
}
@@ -245,6 +250,9 @@
if (sourceType === 'API') {
return '<span class="badge-source badge-source-api">API</span>';
}
if (sourceType === 'PDF') {
return '<span class="badge-source badge-source-pdf">PDF</span>';
}
return '<span class="badge-source badge-source-xls">XLS</span>';
}
+112
View File
@@ -0,0 +1,112 @@
<?php
/*
* test_pdf_extract.php
* ------------------------------------------------------------------
* Pagina di test ISOLATA per verificare l'estrazione PDF.
* Da aprire nel browser passando ?id=<template_id> di un template PDF
* che ha già i riquadri tracciati e un sample_pdf caricato.
*
* http://localhost/trf_certest/test_pdf_extract.php?id=67
*
* Mostra, per ogni campo mappato con pdf_regions, il valore estratto dal
* PDF di esempio. Serve a confermare che i riquadri prendano i dati giusti
* PRIMA di collegare l'inserimento in datadb.
*
* DA CANCELLARE dopo il test.
* ------------------------------------------------------------------
*/
include('include/headscript.php');
require_once __DIR__ . '/pdf_extract_lib.php';
header('Content-Type: text/html; charset=utf-8');
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) {
die('Passa ?id=<template_id> di un template PDF.');
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Template
$stmt = $pdo->prepare("SELECT id, name, source_type, sample_pdf FROM excel_templates WHERE id = ?");
$stmt->execute([$id]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$template) {
die('Template non trovato.');
}
if (strtoupper($template['source_type']) !== 'PDF') {
die('Questo template non è di tipo PDF (source_type = ' . htmlspecialchars($template['source_type']) . ').');
}
if (empty($template['sample_pdf'])) {
die('Nessun PDF di esempio caricato per questo template.');
}
// Percorso del PDF di esempio (adatta se la cartella è diversa)
$pdfPath = __DIR__ . '/pdftemplates/' . $template['sample_pdf'];
// Mapping con eventuali riquadri
$stmt = $pdo->prepare("
SELECT id, field_label, data_type, pdf_regions
FROM template_mapping
WHERE template_id = ?
ORDER BY field_order ASC, id ASC
");
$stmt->execute([$id]);
$mappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo "<h2>Test estrazione PDF — Template: " . htmlspecialchars($template['name']) . "</h2>";
echo "<p><strong>PDF:</strong> " . htmlspecialchars($template['sample_pdf']) . "<br>";
echo "<strong>Percorso:</strong> " . htmlspecialchars($pdfPath) . "</p>";
if (!is_file($pdfPath)) {
die('<p style="color:red">❌ File PDF non trovato al percorso indicato. Correggi $pdfPath nel test.</p>');
}
echo "<p><strong>Binario pdftotext:</strong> " . htmlspecialchars(pdftotext_binary()) . "</p>";
try {
$values = pdf_extract_values($pdfPath, $mappings);
} catch (Throwable $e) {
die('<p style="color:red">❌ Errore estrazione: ' . htmlspecialchars($e->getMessage()) . '</p>');
}
echo '<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;font-family:sans-serif;font-size:14px;">';
echo '<tr style="background:#f0f0f0;"><th>ID</th><th>Campo</th><th>Tipo</th><th>Pagina</th><th>Riquadro value</th><th>VALORE ESTRATTO</th></tr>';
foreach ($mappings as $m) {
$hasRegions = !empty($m['pdf_regions']);
$regions = $hasRegions ? json_decode($m['pdf_regions'], true) : null;
$page = $regions['page'] ?? '';
$valueRect = isset($regions['value'])
? sprintf(
'x=%.0f y=%.0f w=%.0f h=%.0f',
$regions['value']['x'],
$regions['value']['y'],
$regions['value']['w'],
$regions['value']['h']
)
: '—';
$extracted = $values[(int)$m['id']] ?? '';
$rowBg = $hasRegions ? ($extracted !== '' ? '#eaffea' : '#fff3cd') : '#ffffff';
echo '<tr style="background:' . $rowBg . ';">';
echo '<td>' . (int)$m['id'] . '</td>';
echo '<td>' . htmlspecialchars($m['field_label'] ?? '') . '</td>';
echo '<td>' . htmlspecialchars($m['data_type'] ?? '') . '</td>';
echo '<td style="text-align:center;">' . htmlspecialchars((string)$page) . '</td>';
echo '<td style="font-family:monospace;font-size:12px;">' . htmlspecialchars($valueRect) . '</td>';
echo '<td><strong>' . htmlspecialchars($extracted) . '</strong></td>';
echo '</tr>';
}
echo '</table>';
echo '<p style="color:#666;margin-top:15px;">Verde = valore estratto. Giallo = riquadro presente ma nessun testo trovato. Bianco = campo senza riquadro PDF.</p>';
echo '<p style="color:#a00;"><strong>Ricorda di cancellare questo file dopo il test.</strong></p>';
+21
View File
@@ -0,0 +1,21 @@
<?php
echo "<pre>";
echo "== SO ==\n";
echo PHP_OS . "\n\n";
echo "== disable_functions ==\n";
echo ini_get('disable_functions') ?: "(nessuna)";
echo "\n\n";
echo "== test exec con percorso completo ==\n";
if (function_exists('exec')) {
$cmd = '"C:\\poppler\\poppler-26.02.0\\Library\\bin\\pdftotext.exe" -v 2>&1';
exec($cmd, $out, $ret);
echo "return code: $ret\n";
echo implode("\n", $out);
} else {
echo "exec() NON disponibile";
}
echo "\n</pre>";
+57
View File
@@ -0,0 +1,57 @@
<?php
// update_pdf_page_meta.php
// Salva i metadati delle pagine del PDF (numero pagine e dimensioni renderizzate)
// nella colonna pdf_page_meta del template.
//
// Riceve JSON: { template_id, pdf_page_meta }
// dove pdf_page_meta è una stringa JSON tipo:
// {"pages":3,"dims":[{"page":1,"width":1000,"height":1414}, ...]}
header('Content-Type: application/json');
include('include/headscript.php');
function respond(bool $success, string $message = ''): void
{
echo json_encode(['success' => $success, 'message' => $message]);
exit;
}
$raw = file_get_contents('php://input');
$data = json_decode($raw, true);
if (!is_array($data)) {
respond(false, 'Invalid JSON payload.');
}
$templateId = isset($data['template_id']) ? (int)$data['template_id'] : 0;
$pageMeta = $data['pdf_page_meta'] ?? null;
if ($templateId <= 0) {
respond(false, 'Missing or invalid template_id.');
}
// pdf_page_meta deve essere una stringa JSON valida
if (!is_string($pageMeta)) {
respond(false, 'pdf_page_meta must be a JSON string.');
}
$decoded = json_decode($pageMeta, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
respond(false, 'pdf_page_meta is not valid JSON: ' . json_last_error_msg());
}
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("UPDATE excel_templates SET pdf_page_meta = :meta WHERE id = :id");
$stmt->execute([
':meta' => $pageMeta,
':id' => $templateId,
]);
} catch (Throwable $e) {
respond(false, 'Database update failed: ' . $e->getMessage());
}
respond(true, 'Page metadata saved.');
+95
View File
@@ -0,0 +1,95 @@
<?php
// upload_pdf_example.php
// Riceve un PDF di esempio via POST (multipart), lo salva in pdftemplates/
// e aggiorna la colonna sample_pdf del template.
//
// Rispecchia la logica di upload_xls_example.php ma per i PDF.
header('Content-Type: application/json');
include('include/headscript.php');
// --- Helper risposta JSON ---
function respond(bool $success, string $message = '', array $extra = []): void
{
echo json_encode(array_merge([
'success' => $success,
'message' => $message,
], $extra));
exit;
}
// --- Validazione input ---
$templateId = isset($_POST['template_id']) ? (int)$_POST['template_id'] : 0;
if ($templateId <= 0) {
respond(false, 'Missing or invalid template_id.');
}
if (!isset($_FILES['pdf_file']) || $_FILES['pdf_file']['error'] !== UPLOAD_ERR_OK) {
respond(false, 'No file uploaded or upload error.');
}
$file = $_FILES['pdf_file'];
// --- Controllo estensione e MIME ---
$originalName = $file['name'];
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if ($ext !== 'pdf') {
respond(false, 'Only PDF files are allowed.');
}
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if ($mime !== 'application/pdf') {
respond(false, 'The uploaded file is not a valid PDF (mime: ' . htmlspecialchars($mime) . ').');
}
// --- Cartella di destinazione ---
$targetDir = __DIR__ . '/pdftemplates';
if (!is_dir($targetDir)) {
if (!mkdir($targetDir, 0775, true) && !is_dir($targetDir)) {
respond(false, 'Cannot create destination folder pdftemplates/.');
}
}
if (!is_writable($targetDir)) {
respond(false, 'Destination folder pdftemplates/ is not writable.');
}
// --- Nome file sicuro e univoco ---
$safeBase = preg_replace('/[^A-Za-z0-9_\-]/', '_', pathinfo($originalName, PATHINFO_FILENAME));
$safeBase = trim($safeBase, '_');
if ($safeBase === '') {
$safeBase = 'sample';
}
$filename = 'tpl' . $templateId . '_' . $safeBase . '_' . date('YmdHis') . '.pdf';
$targetPath = $targetDir . '/' . $filename;
// --- Sposta il file ---
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
respond(false, 'Failed to move uploaded file.');
}
// --- Aggiorna il database ---
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("UPDATE excel_templates SET sample_pdf = :sample_pdf WHERE id = :id");
$stmt->execute([
':sample_pdf' => $filename,
':id' => $templateId,
]);
} catch (Throwable $e) {
// Se il DB fallisce, rimuovo il file per non lasciare orfani
@unlink($targetPath);
respond(false, 'Database update failed: ' . $e->getMessage());
}
respond(true, 'File uploaded successfully.', ['filename' => $filename]);
+5
View File
@@ -217,6 +217,10 @@ try {
}
// Required custom fields that are visible in the import grid (excluding filed_id = 189 Tested component)
// Required custom fields that are visible in the import grid (excluding filed_id = 189 Tested component)
// Also exclude field_id 258 (Login Date) and 350 (Login Time): auto-populated at
// export time (save_export_lims.php STEP 4a), so they must not block validation
// even when empty at this stage.
$stmt = $pdo->prepare("
SELECT id AS mapping_id, template_id, field_label
FROM template_mapping
@@ -224,6 +228,7 @@ try {
AND is_required = 1
AND is_visible_import = 1
AND id <> 189
AND field_id NOT IN (258, 350)
");
$stmt->execute($templateIds);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {