pdf test routine
This commit is contained in:
@@ -62,3 +62,7 @@ public/userarea/schemi_base_response.json
|
|||||||
|
|
||||||
public/userarea/cache/
|
public/userarea/cache/
|
||||||
public/userarea/error_log.txt
|
public/userarea/error_log.txt
|
||||||
|
|
||||||
|
# File PDF di esempio e importati
|
||||||
|
/public/userarea/pdftemplates/*.pdf
|
||||||
|
/public/userarea/pdftemplates/imported/*.pdf
|
||||||
@@ -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'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -297,7 +297,9 @@
|
|||||||
wrapper.className = "template-btn-wrapper";
|
wrapper.className = "template-btn-wrapper";
|
||||||
|
|
||||||
const btn = document.createElement("a");
|
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.className = `btn ${sizeClass}`;
|
||||||
btn.style.backgroundColor = template.button_bg_color || '#0d6efd';
|
btn.style.backgroundColor = template.button_bg_color || '#0d6efd';
|
||||||
btn.style.color = template.button_text_color || '#ffffff';
|
btn.style.color = template.button_text_color || '#ffffff';
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a href="import_dashboard.php">
|
<a href="import_dashboard.php">
|
||||||
<i class='bx bx-radio-circle'></i>XLS Import
|
<i class='bx bx-radio-circle'></i>Import Dashboard
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ $apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
|
|
||||||
const isXls = selectedSource === 'XLS';
|
const isXls = selectedSource === 'XLS';
|
||||||
const isApiJson = selectedSource === 'API';
|
const isApiJson = selectedSource === 'API';
|
||||||
|
const isPdf = selectedSource === 'PDF';
|
||||||
|
|
||||||
if (isXls) {
|
if (isXls) {
|
||||||
headerRowWrapper.style.display = 'block';
|
headerRowWrapper.style.display = 'block';
|
||||||
@@ -327,6 +328,8 @@ $apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
apiConfigSelect.required = true;
|
apiConfigSelect.required = true;
|
||||||
apiConfigSelect.disabled = false;
|
apiConfigSelect.disabled = false;
|
||||||
} else {
|
} else {
|
||||||
|
// PDF (o qualsiasi altro): niente config API, niente campi XLS.
|
||||||
|
// I riquadri PDF si tracciano nella pagina di configurazione.
|
||||||
apiConfigWrapper.style.display = 'none';
|
apiConfigWrapper.style.display = 'none';
|
||||||
apiConfigSelect.required = false;
|
apiConfigSelect.required = false;
|
||||||
apiConfigSelect.disabled = true;
|
apiConfigSelect.disabled = true;
|
||||||
@@ -516,6 +519,9 @@ $apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
return;
|
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 clientSelect = document.getElementById("clientSelect");
|
||||||
const clientId = clientSelect.value;
|
const clientId = clientSelect.value;
|
||||||
const selectedClientOption = clientSelect.options[clientSelect.selectedIndex];
|
const selectedClientOption = clientSelect.options[clientSelect.selectedIndex];
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ $stmt = $pdo->prepare("
|
|||||||
schemajson,
|
schemajson,
|
||||||
xls_headers,
|
xls_headers,
|
||||||
api_sample_json,
|
api_sample_json,
|
||||||
json_nodes
|
json_nodes,
|
||||||
|
sample_pdf,
|
||||||
|
pdf_page_meta
|
||||||
FROM excel_templates
|
FROM excel_templates
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
");
|
");
|
||||||
@@ -61,6 +63,7 @@ $stmt = $pdo->prepare("
|
|||||||
field_order,
|
field_order,
|
||||||
excel_column,
|
excel_column,
|
||||||
json_node,
|
json_node,
|
||||||
|
pdf_regions,
|
||||||
is_manual,
|
is_manual,
|
||||||
manual_default,
|
manual_default,
|
||||||
auto_value,
|
auto_value,
|
||||||
@@ -117,6 +120,14 @@ if (!is_array($jsonNodes)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$apiSampleJson = $template['api_sample_json'] ?? '';
|
$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>
|
<!doctype html>
|
||||||
@@ -129,6 +140,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
|||||||
<?php include('cssinclude.php'); ?>
|
<?php include('cssinclude.php'); ?>
|
||||||
<title>Configure Template <?= htmlspecialchars($template['name'], ENT_QUOTES, 'UTF-8'); ?></title>
|
<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/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" />
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -383,8 +395,20 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php elseif ($sourceType === 'PDF'): ?>
|
<?php elseif ($sourceType === 'PDF'): ?>
|
||||||
<div class="alert alert-warning">
|
<div class="mb-4">
|
||||||
PDF source type is not implemented yet.
|
<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>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
@@ -467,6 +491,8 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
|||||||
$mappingValue = 'xls';
|
$mappingValue = 'xls';
|
||||||
} elseif ($sourceType === 'API' && !empty($mapping['json_node'])) {
|
} elseif ($sourceType === 'API' && !empty($mapping['json_node'])) {
|
||||||
$mappingValue = 'json';
|
$mappingValue = 'json';
|
||||||
|
} elseif ($sourceType === 'PDF' && !empty($mapping['pdf_regions'])) {
|
||||||
|
$mappingValue = 'pdf';
|
||||||
} elseif ((int)$mapping['is_manual'] === 1) {
|
} elseif ((int)$mapping['is_manual'] === 1) {
|
||||||
$mappingValue = 'manual';
|
$mappingValue = 'manual';
|
||||||
} else {
|
} else {
|
||||||
@@ -486,8 +512,9 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
|||||||
<option value="xls" <?php echo ($mappingValue === 'xls') ? 'selected' : ''; ?>>Map to XLS Column</option>
|
<option value="xls" <?php echo ($mappingValue === 'xls') ? 'selected' : ''; ?>>Map to XLS Column</option>
|
||||||
<?php elseif ($sourceType === 'API'): ?>
|
<?php elseif ($sourceType === 'API'): ?>
|
||||||
<option value="json" <?php echo ($mappingValue === 'json') ? 'selected' : ''; ?>>Map to JSON Node</option>
|
<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; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<option value="auto" <?php echo ($mappingValue === 'auto') ? 'selected' : ''; ?>>Auto value</option>
|
<option value="auto" <?php echo ($mappingValue === 'auto') ? 'selected' : ''; ?>>Auto value</option>
|
||||||
<?php endif; ?>
|
<?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>
|
<option value="export_time" <?php echo ($autoValue === 'export_time') ? 'selected' : ''; ?>>Current time (export)</option>
|
||||||
</select>
|
</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'])): ?>
|
<?php if ($sourceType === 'XLS' && !empty($mapping['excel_column'])): ?>
|
||||||
<span class="mapped-column" style="margin-left:5px;">
|
<span class="mapped-column" style="margin-left:5px;">
|
||||||
(<?php echo htmlspecialchars($mapping['excel_column']); ?>)
|
(<?php echo htmlspecialchars($mapping['excel_column']); ?>)
|
||||||
@@ -1688,6 +1742,12 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
|||||||
const removeBtn = tr.querySelector('.remove-xls');
|
const removeBtn = tr.querySelector('.remove-xls');
|
||||||
const removeJsonBtn = tr.querySelector('.remove-json');
|
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() {
|
function destroyJsonSelect2() {
|
||||||
if (jsonSelect && window.jQuery && $(jsonSelect).hasClass('select2-hidden-accessible')) {
|
if (jsonSelect && window.jQuery && $(jsonSelect).hasClass('select2-hidden-accessible')) {
|
||||||
$(jsonSelect).select2('destroy');
|
$(jsonSelect).select2('destroy');
|
||||||
@@ -1810,14 +1870,18 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
|||||||
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
|
if (removeJsonBtn) removeJsonBtn.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
saveMapping(
|
// Per il PDF il salvataggio avviene tramite save_pdf_regions.php
|
||||||
mappingId,
|
// quando l'utente traccia i riquadri, non qui.
|
||||||
mappingSelect.value,
|
if (mappingSelect.value !== 'pdf') {
|
||||||
manualInput ? manualInput.value : '',
|
saveMapping(
|
||||||
xlsSelect ? xlsSelect.value : null,
|
mappingId,
|
||||||
autoSelect ? autoSelect.value : null,
|
mappingSelect.value,
|
||||||
jsonSelect ? jsonSelect.value : null
|
manualInput ? manualInput.value : '',
|
||||||
);
|
xlsSelect ? xlsSelect.value : null,
|
||||||
|
autoSelect ? autoSelect.value : null,
|
||||||
|
jsonSelect ? jsonSelect.value : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (sourceType === 'XLS') updateXlsDropdowns();
|
if (sourceType === 'XLS') updateXlsDropdowns();
|
||||||
});
|
});
|
||||||
@@ -2571,6 +2635,30 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
|
|||||||
}
|
}
|
||||||
</script>
|
</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>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -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,
|
||||||
|
]);
|
||||||
@@ -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,
|
"ConteggioClienti": 0,
|
||||||
"Nome": "ETON",
|
"Nome": "ETON",
|
||||||
"Descrizione": "Schema da usare per ETON\r\n"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -78,6 +78,11 @@
|
|||||||
color: #198754;
|
color: #198754;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-source-pdf {
|
||||||
|
background-color: #ffeaea;
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
#xlsTemplatesTable {
|
#xlsTemplatesTable {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
@@ -245,6 +250,9 @@
|
|||||||
if (sourceType === 'API') {
|
if (sourceType === 'API') {
|
||||||
return '<span class="badge-source badge-source-api">API</span>';
|
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>';
|
return '<span class="badge-source badge-source-xls">XLS</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>';
|
||||||
@@ -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>";
|
||||||
@@ -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.');
|
||||||
@@ -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]);
|
||||||
Reference in New Issue
Block a user