/* * 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: ${data.filename}`; } // 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); } }); })();