Files
2026-08-18 16:00:06 +02:00

632 lines
22 KiB
JavaScript

/*
* 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);
}
});
})();