Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a19cdf6074 | |||
| 14a7cc2738 | |||
| 601e72ca61 | |||
| 54c164478c | |||
| 965d2fa5bb | |||
| a1e91f8d76 | |||
| 153cdbca98 | |||
| 2603c86c5b | |||
| e7d733a136 | |||
| eb34cba7bb | |||
| f01bfa06c9 | |||
| 71d6fe1d98 | |||
| 33615f65d7 | |||
| 9dda4630a4 | |||
| 6d0b98bd41 | |||
| e92e107a3c | |||
| 9330a49008 | |||
| 80dbd26929 |
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
@@ -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()]);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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>';
|
||||
}
|
||||
|
||||
|
||||
@@ -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]);
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user