'use strict'; /** * PDF Layout Wizard - Main Logic * Handles State, PDF Generation, Drag & Drop, and Event Bus */ // --- Global State --- const state = { data: [], currentIndex: 0, pdfDoc: null, scale: 1, originalPdfBytes: null, currentFileName: '', bg1Obj: '', bg1B64: '', bg2Obj: '', bg2B64: '', currentView: 'template', // 'template' | 'pdf' designMode: false, selectedFieldId: null, fieldConfig: {}, currentLayoutConfig: { fields: {}, pages: [], metadata: {} }, isLayoutLoaded: false }; // --- Database Config --- const DB_NAME = 'PDFWizardDB'; const STORE = 'docs'; let db; // --- Constants --- const fieldToKey = { 'photo': '4', 'f1': '7', 'f2': '1', 'f3': '2', 'f4': '8', 'f5': '9', 'f6': '1', 'f7': '5', 'f8': '6', 'f9': '3', 'f10': '10', 'f12': '11', 'f13': '12', 'f14': '7', 'f15': '1', 'f16': '3', 'f17': '10', 'f18': '8' }; const fieldIds = Object.keys(fieldToKey).filter(k => k.startsWith('f')); // --- Initialization --- document.addEventListener('DOMContentLoaded', async () => { try { await initDB(); loadFieldConfig(); updateFileCount(); // Initialize PDF.js Worker if (window.pdfjsLib) { window.pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; } setupGlobalListeners(); debugLog('System Ready'); } catch (e) { toast('Initialization Error: ' + e.message, 'error'); debugLog(e); } }); // --- Event Listeners (Event Bus Pattern) --- function setupGlobalListeners() { // Theme Toggle document.addEventListener('toggle-theme', () => toggleTheme()); // Sidebar Toggle document.addEventListener('toggle-sidebar', () => toggleSidebar()); // Design Mode Toggle document.addEventListener('toggle-design-mode', () => toggleDesignMode()); // File Uploads (handled by Sidebar Component, logic here) document.getElementById('bg1-file')?.addEventListener('change', e => handleBgFile('1', e.target.files[0])); document.getElementById('bg2-file')?.addEventListener('change', e => handleBgFile('2', e.target.files[0])); document.getElementById('json-file')?.addEventListener('change', handleJsonUpload); document.getElementById('pdf-upload')?.addEventListener('change', handlePdfUpload); document.getElementById('layout-import')?.addEventListener('change', (e) => importLayout(e.target.files[0])); // Sidebar Actions document.getElementById('btn-apply-bg')?.addEventListener('click', applyBg); document.getElementById('btn-load-sample')?.addEventListener('click', loadSample); document.getElementById('btn-download-pdf')?.addEventListener('click', downloadPDF); document.getElementById('btn-download-all')?.addEventListener('click', downloadAllPDFs); document.getElementById('btn-qr-modal')?.addEventListener('click', openQRModal); document.getElementById('btn-export-layout')?.addEventListener('click', exportLayout); document.getElementById('btn-save-layout')?.addEventListener('click', saveLayoutToDB); document.getElementById('btn-new-layout')?.addEventListener('click', newLayout); // Design Mode Actions document.getElementById('btn-export-design')?.addEventListener('click', exportLayout); document.getElementById('btn-save-design')?.addEventListener('click', saveLayoutToDB); document.getElementById('btn-new-design')?.addEventListener('click', newLayout); // View Switching document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => switchTab(btn.dataset.tab)); }); // Zoom Controls document.getElementById('zoom-in').addEventListener('click', zoomIn); document.getElementById('zoom-out').addEventListener('click', zoomOut); document.getElementById('current-pg').addEventListener('change', (e) => goToPage(e.target.value)); // Property Controls ['prop-x', 'prop-y', 'prop-size', 'prop-weight', 'prop-color'].forEach(id => { const el = document.getElementById(id); if (el) { el.addEventListener('input', updateFieldFromInputs); } }); // Modals document.querySelectorAll('.close-modal, #btn-qr-cancel').forEach(btn => { btn.addEventListener('click', (e) => { const modal = e.target.closest('.fixed.z-\\[200\\]'); // Select modal wrapper if(modal) modal.classList.add('hidden'); }); }); // QR Actions document.getElementById('qr-url')?.addEventListener('input', updateQRPreview); document.getElementById('btn-qr-apply')?.addEventListener('click', applyQRAndDownload); // History document.addEventListener('show-history', showHistory); // Layout Management document.addEventListener('layout-saved', updateFileCount); // Keyboard document.addEventListener('keydown', (e) => { if (e.ctrlKey || e.metaKey) { if (e.key === '=') { e.preventDefault(); zoomIn(); } if (e.key === '-') { e.preventDefault(); zoomOut(); } if (e.key === 's') { e.preventDefault(); downloadPDF(); } } if (e.key === 'Escape') { document.querySelectorAll('.fixed.z-\\[200\\]').forEach(m => m.classList.add('hidden')); if(state.designMode) toggleDesignMode(); // Optional: exit design mode on ESC } }); // Drag & Drop (Global) document.addEventListener('mousedown', onMouseDown); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); } // --- Helper Functions --- function debugLog(msg) { const c = document.getElementById('debug-console'); if(c) { const div = document.createElement('div'); div.textContent = `> ${msg}`; c.appendChild(div); c.scrollTop = c.scrollHeight; } console.log(msg); } function toast(msg, type = 'info') { const t = document.getElementById('toast'); const m = document.getElementById('toast-message'); m.textContent = msg; t.className = `fixed top-20 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white px-6 py-3 rounded-lg shadow-xl z-[1000] transition-all duration-300 border-l-4 flex items-center gap-3 max-w-[90%] pointer-events-none translate-y-0 opacity-100 ${ type === 'success' ? 'border-green-500' : type === 'error' ? 'border-red-500' : 'border-blue-500' }`; const icon = t.querySelector('i'); icon.setAttribute('data-feather', type === 'success' ? 'check-circle' : type === 'error' ? 'alert-circle' : 'info'); feather.replace(); setTimeout(() => { t.classList.remove('translate-y-0', 'opacity-100'); t.classList.add('-translate-y-24', 'opacity-0'); }, 3000); } function loading(show, text) { const o = document.getElementById('loading-overlay'); const txt = document.getElementById('loading-text'); if (show) { txt.textContent = text || 'Processing...'; o.classList.remove('hidden'); o.classList.add('flex'); } else { o.classList.add('hidden'); o.classList.remove('flex'); } } function sanitize(str) { return String(str || '').replace(/[&<>'"`]/g, tag => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '`': '`' }[tag])); } // --- Database (IndexedDB) --- function initDB() { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, 1); req.onupgradeneeded = (e) => { const d = e.target.result; if (!d.objectStoreNames.contains(STORE)) d.createObjectStore(STORE, { keyPath: 'id' }); }; req.onsuccess = () => { db = req.result; resolve(); }; req.onerror = () => reject(req.error); }); } async function saveDoc(doc) { if (!doc.id) doc.id = Date.now().toString(); return new Promise((resolve, reject) => { const tx = db.transaction(STORE, 'readwrite'); tx.objectStore(STORE).put(doc); tx.oncomplete = () => { resolve(); updateFileCount(); }; tx.onerror = () => reject(tx.error); }); } async function getAllDocs() { return new Promise((resolve, reject) => { const tx = db.transaction(STORE, 'readonly'); const req = tx.objectStore(STORE).getAll(); req.onsuccess = () => resolve(req.result || []); req.onerror = () => reject(req.error); }); } async function updateFileCount() { const docs = await getAllDocs(); const footerCount = document.querySelector('#file-count'); if (footerCount) footerCount.textContent = docs.length; } // --- Theme & UI --- function toggleTheme() { const html = document.documentElement; const current = html.getAttribute('data-theme'); const next = current === 'dark' ? 'light' : 'dark'; html.setAttribute('data-theme', next); toast(`Switched to ${next} mode`, 'success'); } function toggleSidebar() { const sb = document.getElementById('sidebar'); const ov = document.getElementById('sidebar-overlay'); sb.classList.toggle('-translate-x-full'); ov.classList.toggle('hidden'); } function switchTab(tab) { state.currentView = tab; document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === tab)); document.getElementById('template-view').classList.toggle('hidden', tab !== 'template'); document.getElementById('template-view').classList.toggle('flex', tab === 'template'); document.getElementById('pdf-view').classList.toggle('hidden', tab !== 'pdf'); document.getElementById('pdf-view').classList.toggle('flex', tab === 'pdf'); if (tab === 'pdf' && state.pdfDoc) { renderPDF(); document.getElementById('total-pg').textContent = state.pdfDoc.numPages; } else { document.getElementById('total-pg').textContent = '2'; } state.scale = 1; updateZoomDisplay(); } // --- Design Mode & Drag & Drop --- function toggleDesignMode() { state.designMode = !state.designMode; document.body.classList.toggle('design-mode', state.designMode); // Toggle Visibility of Panels const standardPanel = document.getElementById('sidebar-standard'); const designPanel = document.getElementById('sidebar-design'); if (state.designMode) { if(standardPanel) standardPanel.style.display = 'none'; if(designPanel) designPanel.style.display = 'block'; toast('Design Mode Active', 'info'); updateFieldPositionsFromState(); } else { if(standardPanel) standardPanel.style.display = 'block'; if(designPanel) designPanel.style.display = 'none'; deselectField(); toast('Design Mode Off', 'info'); } } function updateFieldPositionsFromState() { if (state.currentLayoutConfig.fields) { Object.keys(state.currentLayoutConfig.fields).forEach(fieldId => { const field = state.currentLayoutConfig.fields[fieldId]; const element = document.getElementById(fieldId); if (element && field.position) { element.style.top = field.position.top; element.style.left = field.position.left; if (field.style) { Object.assign(element.style, field.style); } } }); } } function selectField(id) { if (!state.designMode) return; deselectField(); state.selectedFieldId = id; const el = document.getElementById(id); if (el) { el.classList.add('selected'); // Populate Design Sidebar const noSel = document.getElementById('no-field-selected'); const controls = document.getElementById('field-controls'); if (noSel) noSel.style.display = 'none'; if (controls) controls.style.display = 'block'; document.getElementById('prop-id').textContent = id; document.getElementById('prop-x').value = parseFloat(el.style.left) || 0; document.getElementById('prop-y').value = parseFloat(el.style.top) || 0; if (el.tagName === 'IMG') { document.getElementById('prop-size').parentElement.style.display = 'none'; document.getElementById('prop-color').parentElement.style.display = 'none'; document.getElementById('prop-weight').parentElement.style.display = 'none'; } else { document.getElementById('prop-size').parentElement.style.display = 'block'; document.getElementById('prop-color').parentElement.style.display = 'block'; document.getElementById('prop-weight').parentElement.style.display = 'block'; document.getElementById('prop-size').value = parseFloat(el.style.fontSize) || 14; document.getElementById('prop-weight').value = el.style.fontWeight || 'normal'; document.getElementById('prop-color').value = rgbToHex(el.style.color) || '#000000'; } } } function deselectField() { if (state.selectedFieldId) { const el = document.getElementById(state.selectedFieldId); if (el) el.classList.remove('selected'); } state.selectedFieldId = null; const noSel = document.getElementById('no-field-selected'); const controls = document.getElementById('field-controls'); if (noSel) noSel.style.display = 'block'; if (controls) controls.style.display = 'none'; } function updateFieldFromInputs() { if (!state.selectedFieldId) return; const el = document.getElementById(state.selectedFieldId); if (!el) return; const x = parseFloat(document.getElementById('prop-x').value) || 0; const y = parseFloat(document.getElementById('prop-y').value) || 0; el.style.left = x + 'px'; el.style.top = y + 'px'; // Update state.fieldConfig if (!state.fieldConfig[state.selectedFieldId]) { state.fieldConfig[state.selectedFieldId] = {}; } state.fieldConfig[state.selectedFieldId].top = y + 'px'; state.fieldConfig[state.selectedFieldId].left = x + 'px'; if (el.tagName !== 'IMG') { const size = document.getElementById('prop-size').value || '14'; const color = document.getElementById('prop-color').value || '#000000'; const weight = document.getElementById('prop-weight').value || 'normal'; el.style.fontSize = size + 'px'; el.style.color = color; el.style.fontWeight = weight; state.fieldConfig[state.selectedFieldId].fontSize = size + 'px'; state.fieldConfig[state.selectedFieldId].color = color; state.fieldConfig[state.selectedFieldId].fontWeight = weight; } saveFieldConfig(); } let dragInfo = { active: false, el: null, offsetX: 0, offsetY: 0 }; function onMouseDown(e) { if (!state.designMode) return; if (e.target.classList.contains('field')) { dragInfo.active = true; dragInfo.el = e.target; const rect = dragInfo.el.getBoundingClientRect(); dragInfo.offsetX = e.clientX - rect.left; dragInfo.offsetY = e.clientY - rect.top; selectField(dragInfo.el.id); e.preventDefault(); } } function onMouseMove(e) { if (!dragInfo.active || !dragInfo.el) return; e.preventDefault(); const parent = dragInfo.el.parentElement; const pRect = parent.getBoundingClientRect(); // Calculate new position relative to parent, compensating for zoom let scale = state.scale; let newLeft = (e.clientX - pRect.left - dragInfo.offsetX) / scale; let newTop = (e.clientY - pRect.top - dragInfo.offsetY) / scale; // Ensure non-negative positions newLeft = Math.max(0, newLeft); newTop = Math.max(0, newTop); dragInfo.el.style.left = newLeft + 'px'; dragInfo.el.style.top = newTop + 'px'; // Update Inputs if (state.selectedFieldId === dragInfo.el.id) { document.getElementById('prop-x').value = Math.round(newLeft); document.getElementById('prop-y').value = Math.round(newTop); } } function onMouseUp() { if (dragInfo.active && dragInfo.el) { // Save config on drop if (!state.fieldConfig[dragInfo.el.id]) { state.fieldConfig[dragInfo.el.id] = {}; } state.fieldConfig[dragInfo.el.id].left = dragInfo.el.style.left; state.fieldConfig[dragInfo.el.id].top = dragInfo.el.style.top; saveFieldConfig(); } dragInfo.active = false; dragInfo.el = null; } // --- Layout Config --- function saveFieldConfig() { localStorage.setItem('pdf_field_config', JSON.stringify(state.fieldConfig)); saveLayoutToStorage(); } function loadFieldConfig() { const saved = localStorage.getItem('pdf_field_config'); if (saved) { try { state.fieldConfig = JSON.parse(saved); Object.keys(state.fieldConfig).forEach(id => { const el = document.getElementById(id); const cfg = state.fieldConfig[id]; if (el && cfg) { Object.assign(el.style, cfg); } }); } catch (e) { console.warn('Failed to load field config:', e); } } loadLayoutFromStorage(); } function saveLayoutToStorage() { const layoutData = { fields: {}, pages: [], metadata: { version: '1.0', savedAt: new Date().toISOString(), name: state.currentLayoutName || 'default' } }; // Capture all field positions and styles document.querySelectorAll('.field').forEach(field => { layoutData.fields[field.id] = { position: { top: field.style.top, left: field.style.left }, style: { fontSize: field.style.fontSize, fontWeight: field.style.fontWeight, color: field.style.color, fontFamily: field.style.fontFamily }, type: field.tagName === 'IMG' ? 'image' : 'text', page: field.closest('.page')?.id || 'page1' }; }); // Capture page configurations ['page1', 'page2'].forEach(pageId => { const page = document.getElementById(pageId); if (page) { layoutData.pages.push({ id: pageId, background: page.querySelector('img[src*="bg"]')?.src || '', dimensions: { width: page.offsetWidth, height: page.offsetHeight } }); } }); state.currentLayoutConfig = layoutData; localStorage.setItem('pdf_layout_config', JSON.stringify(layoutData)); return layoutData; } function loadLayoutFromStorage() { const saved = localStorage.getItem('pdf_layout_config'); if (saved) { try { const parsed = JSON.parse(saved); state.currentLayoutConfig = parsed; applyLayoutConfig(parsed); state.isLayoutLoaded = true; } catch (e) { console.warn('Failed to load layout config:', e); } } } function applyLayoutConfig(config) { if (!config || !config.fields) return; // Apply field configurations Object.keys(config.fields).forEach(fieldId => { const fieldConfig = config.fields[fieldId]; const element = document.getElementById(fieldId); if (element && fieldConfig.position) { element.style.top = fieldConfig.position.top; element.style.left = fieldConfig.position.left; if (fieldConfig.style) { Object.assign(element.style, fieldConfig.style); } } }); // Apply page backgrounds config.pages?.forEach(pageConfig => { const pageElement = document.getElementById(pageConfig.id); if (pageElement && pageConfig.background) { const bgImg = pageElement.querySelector('img[id^="bg"]'); if (bgImg) { bgImg.src = pageConfig.background; } } }); } // --- File Handling --- function handleBgFile(page, file) { if (!file) return; // Support PDF or Image if (file.type !== 'application/pdf' && !file.type.startsWith('image/')) { toast('Invalid file type. Use PDF or Image.', 'error'); return; } const obj = URL.createObjectURL(file); if (page === '1') { if (state.bg1Obj) URL.revokeObjectURL(state.bg1Obj); state.bg1Obj = obj; document.getElementById('bg1-name').textContent = file.name; } else { if (state.bg2Obj) URL.revokeObjectURL(state.bg2Obj); state.bg2Obj = obj; document.getElementById('bg2-name').textContent = file.name; } // Convert to Base64 for PDF generation const reader = new FileReader(); reader.onload = e => { if (page === '1') state.bg1B64 = e.target.result; else state.bg2B64 = e.target.result; }; reader.readAsDataURL(file); } function applyBg() { if (state.bg1Obj) document.getElementById('bg1').src = state.bg1Obj; if (state.bg2Obj) document.getElementById('bg2').src = state.bg2Obj; toast('Background Applied', 'success'); } function handleJsonUpload(e) { const file = e.target.files[0]; if (!file) return; if (!file.name.toLowerCase().endsWith('.json')) { toast('Please select a JSON file', 'error'); return; } document.getElementById('json-name').textContent = file.name; const reader = new FileReader(); reader.onload = ev => { try { const raw = JSON.parse(ev.target.result); if (!Array.isArray(raw) || raw.length === 0) throw new Error('Empty or invalid data'); state.data = raw; state.currentIndex = 0; populateDataDropdown(); showData(0); toast(`Loaded ${raw.length} records`, 'success'); } catch (err) { toast('Invalid JSON Format', 'error'); debugLog(err); } }; reader.readAsText(file); } function loadSample() { state.data = [ { "1": "Mr. Sample One", "2": "Position 1", "3": "Dept A", "4": "", "5": "01/01/2025", "6": "31/12/2025", "7": "Test Co., Ltd.", "8": "Bangkok", "9": "10110", "10": "Thailand", "11": "REF001", "12": "DOC001" }, { "1": "Ms. Sample Two", "2": "Position 2", "3": "Dept B", "4": "", "5": "01/02/2025", "6": "28/02/2026", "7": "Example Co., Ltd.", "8": "Chiang Mai", "9": "50200", "10": "Thailand", "11": "REF002", "12": "DOC002" } ]; state.currentIndex = 0; populateDataDropdown(); showData(0); toast('Sample Data Loaded', 'success'); } function populateDataDropdown() { const sel = document.getElementById('data-select'); sel.innerHTML = ''; state.data.forEach((item, idx) => { const opt = document.createElement('option'); opt.value = idx; opt.textContent = item['1'] || `Item ${idx+1}`; sel.appendChild(opt); }); sel.disabled = state.data.length <= 1; sel.value = state.currentIndex; sel.onchange = (e) => showData(e.target.value); } function showData(idx) { state.currentIndex = parseInt(idx); const data = state.data[state.currentIndex]; fieldIds.forEach(id => { const key = fieldToKey[id]; const val = data[key] || ''; const el = document.getElementById(id); if (el) { el.innerHTML = val ? `${sanitize(val)}` : `${id}`; } }); // Photo let photoUrl = data[fieldToKey['photo']] || ''; if (typeof photoUrl === 'string' && !photoUrl.startsWith('http') && !photoUrl.startsWith('data')) { photoUrl = ''; } document.getElementById('photo').src = photoUrl || 'https://via.placeholder.com/110x138?text=No+Photo'; // QR Codes const qrApi = 'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data='; document.getElementById('qr1').src = qrApi + encodeURIComponent(window.location.href + '?id=' + (data['12']||'')); document.getElementById('qr2').src = qrApi + encodeURIComponent(window.location.href + '?p=2&id=' + (data['12']||'')); } // --- PDF Viewer --- async function handlePdfUpload(e) { const file = e.target.files[0]; if (!file || file.type !== 'application/pdf') { toast('Invalid PDF file', 'error'); return; } document.getElementById('pdf-name').textContent = file.name; const reader = new FileReader(); reader.onload = async (ev) => { loading(true, 'Loading PDF...'); try { const buffer = ev.target.result; state.originalPdfBytes = buffer; state.currentFileName = file.name; state.pdfDoc = await window.pdfjsLib.getDocument({ data: buffer }).promise; switchTab('pdf'); } catch (err) { toast('Failed to load PDF', 'error'); debugLog(err); } finally { loading(false); } }; reader.readAsArrayBuffer(file); } async function renderPDF() { if (!state.pdfDoc) return; const container = document.getElementById('pdf-content'); container.innerHTML = ''; for (let i = 1; i <= state.pdfDoc.numPages; i++) { const page = await state.pdfDoc.getPage(i); const viewport = page.getViewport({ scale: 1.5 * state.scale }); // Base scale for viewing const canvas = document.createElement('canvas'); canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: canvas.getContext('2d'), viewport: viewport }).promise; const div = document.createElement('div'); div.className = 'pdf-page-wrapper mb-4'; div.appendChild(canvas); container.appendChild(div); } } function goToPage(num) { // Implement if single page viewer needed, currently scrolls all // Or simply update counter document.getElementById('current-pg').value = num; } function zoomIn() { state.scale = Math.min(3, state.scale + 0.1); updateZoomDisplay(); if (state.currentView === 'pdf') renderPDF(); else updateTemplateZoom(); } function zoomOut() { state.scale = Math.max(0.2, state.scale - 0.1); updateZoomDisplay(); if (state.currentView === 'pdf') renderPDF(); else updateTemplateZoom(); } function updateZoomDisplay() { document.getElementById('zoom-text').textContent = Math.round(state.scale * 100) + '%'; } function updateTemplateZoom() { document.querySelectorAll('.page').forEach(p => { p.style.transform = `scale(${state.scale})`; // Adjust margin to prevent overlap p.style.marginBottom = `${(1261 * (state.scale - 1))}px`; }); } // --- Export & Generation --- async function exportLayout() { const layout = saveLayoutToStorage(); const name = layout.metadata.name || 'layout'; const blob = new Blob([JSON.stringify(layout, null, 2)], { type: 'application/json' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `${name}_${new Date().getTime()}.json`; link.click(); toast('Layout Exported', 'success'); } async function importLayout(file) { if (!file || !file.name.endsWith('.json')) { toast('Please select a layout JSON file', 'error'); return; } document.getElementById('layout-import-name').textContent = file.name; const reader = new FileReader(); reader.onload = async (e) => { try { const layout = JSON.parse(e.target.result); if (!layout.fields || !layout.pages) { throw new Error('Invalid layout format'); } state.currentLayoutConfig = layout; applyLayoutConfig(layout); // Update form inputs const layoutNameInput = document.getElementById('layout-name-input') || document.getElementById('design-layout-name'); if (layoutNameInput) { layoutNameInput.value = layout.metadata?.name || 'Imported Layout'; } // Apply layout applyLayoutConfig(layout); toast('Layout Imported Successfully', 'success'); state.isLayoutLoaded = true; } catch (err) { toast('Failed to import layout: ' + err.message, 'error'); debugLog(err); } }; reader.readAsText(file); } async function downloadPDF() { if (state.data.length === 0) { toast('No data to export', 'error'); return; } loading(true, 'Generating PDF...'); try { const data = state.data[state.currentIndex]; const layout = await saveLayoutToStorage(); // Capture current layout const element = document.createElement('div'); // Prepare HTML snapshot with current layout element.style.width = '892px'; element.style.position = 'absolute'; element.style.left = '-9999px'; ['page1', 'page2'].forEach(pid => { const clone = document.getElementById(pid).cloneNode(true); // Cleanup UI classes clone.querySelectorAll('.field').forEach(f => { f.classList.remove('selected'); f.style.border = 'none'; f.style.background = 'transparent'; }); element.appendChild(clone); }); document.body.appendChild(element); // Ensure BGs are loaded (Base64) const bg1 = element.querySelector('#bg1'); const bg2 = element.querySelector('#bg2'); if(state.bg1B64) bg1.src = state.bg1B64; if(state.bg2B64) bg2.src = state.bg2B64; // Populate Data fieldIds.forEach(id => { const key = fieldToKey[id]; const val = data[key] || ''; const el = element.querySelector('#'+id); if(el) el.innerHTML = val ? `${sanitize(val)}` : ''; }); const photo = element.querySelector('#photo'); photo.src = data[fieldToKey['photo']] || 'https://via.placeholder.com/110x138?text=No+Photo'; await new Promise(r => setTimeout(r, 500)); // Render wait const opt = { margin: 0, filename: `${data['1'] || state.currentLayoutName || 'doc'}.pdf`, image: { type: 'jpeg', quality: 0.98 }, html2canvas: { scale: 2, useCORS: true }, jsPDF: { unit: 'px', format: [892, 1261], orientation: 'portrait' } }; await html2pdf().set(opt).from(element).save(); document.body.removeChild(element); // Save layout to history const doc = { id: Date.now().toString(), fileName: data['1'] ? `${data['1']}.pdf` : 'generated.pdf', date: new Date().toISOString(), layout: layout, data: data }; await saveDoc(doc); toast('PDF Downloaded & Layout Saved', 'success'); } catch (e) { toast('Generation Failed', 'error'); debugLog(e); } finally { loading(false); } } async function exportLayout() { const layout = await saveLayoutToStorage(); const blob = new Blob([JSON.stringify(layout, null, 2)], { type: 'application/json' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `${layout.metadata.name || 'layout'}_${new Date().getTime()}.json`; link.click(); toast('Layout Exported', 'success'); } async function importLayout(file) { if (!file || !file.name.endsWith('.json')) { toast('Please select a layout JSON file', 'error'); return; } const reader = new FileReader(); reader.onload = async (e) => { try { const layout = JSON.parse(e.target.result); if (!layout.fields || !layout.pages) { throw new Error('Invalid layout format'); } state.currentLayoutConfig = layout; applyLayoutConfig(layout); // Update form inputs const layoutNameInput = document.getElementById('layout-name-input') || document.getElementById('design-layout-name'); if (layoutNameInput) { layoutNameInput.value = layout.metadata?.name || 'Unnamed Layout'; } toast('Layout Imported Successfully', 'success'); state.isLayoutLoaded = true; } catch (err) { toast('Failed to import layout: ' + err.message, 'error'); debugLog(err); } }; reader.readAsText(file); } async function saveLayoutToDB() { try { const layout = saveLayoutToStorage(); const doc = { id: `layout_${Date.now()}`, fileName: `${layout.metadata.name}_layout.json`, date: new Date().toISOString(), type: 'layout', data: layout, metadata: layout.metadata }; await saveDoc(doc); toast('Layout Saved to History', 'success'); return layout; } catch (err) { toast('Failed to save layout', 'error'); debugLog(err); return null; } } function newLayout() { const confirmNew = confirm('Create new layout? Unsaved changes will be lost.'); if (!confirmNew) return; // Reset all fields to default positions document.querySelectorAll('.field').forEach(field => { field.style.top = ''; field.style.left = ''; field.style.fontSize = ''; field.style.fontWeight = ''; field.style.color = ''; field.classList.remove('selected'); }); state.fieldConfig = {}; state.currentLayoutConfig = { fields: {}, pages: [], metadata: {} }; state.isLayoutLoaded = false; // Reset layout name input const layoutNameInput = document.getElementById('layout-name-input') || document.getElementById('design-layout-name'); if (layoutNameInput) { layoutNameInput.value = 'New Layout'; } deselectField(); localStorage.removeItem('pdf_field_config'); localStorage.removeItem('pdf_layout_config'); toast('New Layout Created', 'success'); } async function downloadAllPDFs() { if (state.data.length === 0) return; if (!confirm(`Download ${state.data.length} PDFs?`)) return; for (let i = 0; i < state.data.length; i++) { state.currentIndex = i; showData(i); await new Promise(r => setTimeout(r, 200)); // Delay await downloadPDF(); } } // --- QR Code Logic --- function openQRModal() { document.getElementById('qr-url').value = window.location.href; updateQRPreview(); document.getElementById('qr-modal').classList.remove('hidden'); } function updateQRPreview() { const url = document.getElementById('qr-url').value; const canvas = document.getElementById('qr-preview-canvas'); const ctx = canvas.getContext('2d'); // Generate using simple QR library or API // Using API for simplicity in preview const img = new Image(); img.crossOrigin = "Anonymous"; img.src = `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(url)}`; img.onload = () => { ctx.clearRect(0,0,150,136); ctx.drawImage(img, 0, 0, 150, 136); }; } async function applyQRAndDownload() { // This requires embedding QR into existing PDF // Simplified logic: Generate PDF then embed QR if (!state.originalPdfBytes && state.currentView === 'pdf') { toast('Please upload a base PDF first', 'error'); return; } if (state.data.length > 0) { // If using Template mode (simplified) await downloadPDF(); return; } // PDF Mode logic (Requires pdf-lib) try { loading(true, 'Processing...'); const url = document.getElementById('qr-url').value; // Generate QR as DataURL const qrCanvas = document.createElement('canvas'); // ... (generation logic using qrcode library) // For brevity, utilizing the API for the image source const qrImgUrl = `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(url)}`; const pdfDoc = await PDFLib.PDFDocument.load(state.originalPdfBytes); const pages = pdfDoc.getPages(); const qrImage = await pdfDoc.embedPng(qrImgUrl); const pngDims = qrImage.scale(0.5); // Adjust size pages[0].drawImage(qrImage, { x: 50, y: 50, width: pngDims.width, height: pngDims.height, }); const pdfBytes = await pdfDoc.save(); const blob = new Blob([pdfBytes], { type: 'application/pdf' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `qr_${state.currentFileName}`; link.click(); toast('QR Added & Downloaded', 'success'); document.getElementById('qr-modal').classList.add('hidden'); } catch (e) { toast('Error adding QR', 'error'); debugLog(e); } finally { loading(false); } } // --- History --- async function showHistory() { const list = document.getElementById('history-list'); list.innerHTML = ''; const docs = await getAllDocs(); if (docs.length === 0) { list.innerHTML = '
No History
'; } else { docs.forEach(doc => { const div = document.createElement('div'); div.className = 'flex items-center gap-3 p-3 hover:bg-slate-50 dark:hover:bg-slate-700/50 rounded-lg cursor-pointer'; div.innerHTML = `
${doc.fileName || 'Untitled'}
${new Date(doc.date).toLocaleString()}
`; div.onclick = () => { if(doc.data) { state.data = doc.data; populateDataDropdown(); showData(0); } document.getElementById('history-modal').classList.add('hidden'); toast('History Loaded', 'success'); }; list.appendChild(div); }); feather.replace(); } document.getElementById('history-modal').classList.remove('hidden'); } // Utils function rgbToHex(rgb) { if (!rgb) return '#000000'; if (rgb.startsWith('#')) return rgb; const rgbValues = rgb.match(/\d+/g); if (!rgbValues) return '#000000'; return "#" + ((1 << 24) + (parseInt(rgbValues[0]) << 16) + (parseInt(rgbValues[1]) << 8) + parseInt(rgbValues[2])).toString(16).slice(1); } // --- Complete Usage Examples --- function showUsageExamples() { console.log('=== PDF Layout Wizard - Complete Usage Examples ===\n'); console.log('1. BASIC USAGE:'); console.log(' - Load sample data: Click "Load Sample" in sidebar'); console.log(' - Edit field positions: Toggle Design Mode (pencil icon in navbar)'); console.log(' - Download PDF: Click "Download PDF" in sidebar'); console.log('\n2. ADVANCED FEATURES:'); console.log(' ├─ Layout Management:'); console.log(' │ • Export Layout: Design Mode → Export Layout'); console.log(' │ • Import Layout: Sidebar → Import Layout'); console.log(' │ • Save Layout: Design Mode → Save Layout to History'); console.log(' │ • New Layout: Design Mode → New Layout'); console.log(' ├─ Data Management:'); console.log(' │ • JSON Import: Sidebar → Import JSON'); console.log(' │ • Data Dashboard: Navbar → Database icon'); console.log(' │ • Record Switching: Sidebar dropdown'); console.log(' ├─ PDF Features:'); console.log(' │ • PDF Upload: Sidebar → Import PDF'); console.log(' │ • PDF Viewer: Tab switch → PDF Viewer'); console.log(' │ • QR Integration: Navbar → Grid icon'); console.log(' └─ Export Options:'); console.log(' • Single PDF: Sidebar → Download PDF'); console.log(' • Batch Export: Sidebar → Download All'); console.log(' • Layout Export: Design Mode → Export Layout'); console.log('\n3. KEYBOARD SHORTCUTS:'); console.log(' - Ctrl + S: Download PDF'); console.log(' - Ctrl + =: Zoom In'); console.log(' - Ctrl + -: Zoom Out'); console.log(' - ESC: Close modals / Exit Design Mode'); console.log('\n4. DESIGN MODE WORKFLOW:'); console.log(' Step 1: Click Design Mode button (pencil icon)'); console.log(' Step 2: Click and drag fields to position'); console.log(' Step 3: Select field to edit properties (size, color, weight)'); console.log(' Step 4: Export layout for reuse'); console.log(' Step 5: Exit Design Mode (ESC or button)'); console.log('\n5. DATA WORKFLOW:'); console.log(' • JSON Format: Must be array of objects with numbered keys (1-12)'); console.log(' • Dashboard: Open in new tab for full CRUD operations'); console.log(' • Real-time Update: Dashboard changes reflect in main app'); console.log('\n6. PDF WORKFLOW:'); console.log(' • Upload Base PDF: Use PDF as template'); console.log(' • View Mode: Switch to PDF tab to preview'); console.log(' • QR Addition: Add QR codes to existing PDF'); console.log(' • Zoom Controls: Bottom HUD controls'); console.log('\n7. HISTORY & STORAGE:'); console.log(' • Auto-save: All generations saved to history'); console.log(' • Layout Persistence: Saved in browser IndexedDB'); console.log(' • Restore: Click history items to restore'); console.log(' • Export/Import: Share layouts across devices'); console.log('\n8. CUSTOMIZATION:'); console.log(' • CSS Variables: Modify :root colors in style.css'); console.log(' • Field Mapping: Edit fieldToKey object in script.js'); console.log(' • Default Layouts: Create preset layouts'); console.log(' • Branding: Update navbar title and icons'); console.log(' • Theme: Toggle light/dark mode'); console.log('\n9. SAMPLE COMMANDS:'); console.log(' loadSample() // Load demo data'); console.log(' toggleDesignMode() // Toggle field editing'); console.log(' exportLayout() // Save current layout'); console.log(' newLayout() // Reset to blank layout'); console.log(' showData(0) // Show first data record'); console.log(' downloadPDF() // Generate PDF'); console.log(' saveLayoutToDB() // Save layout to history'); console.log(' importLayout(file) // Import layout JSON'); console.log('\n10. TROUBLESHOOTING:'); console.log(' • Images not showing: Enable CORS in browser'); console.log(' • PDF generation slow: Reduce image sizes'); console.log(' • Layout not saving: Check browser storage limits'); console.log(' • QR codes broken: Verify URL includes protocol'); console.log(' • Fields misaligned: Check zoom level is 100%'); console.log('\n=== Ready to use! Check dashboard.html for data management ==='); } // Auto-show usage on first load if (!localStorage.getItem('pdf_wizard_usage_shown')) { setTimeout(showUsageExamples, 2000); localStorage.setItem('pdf_wizard_usage_shown', 'true'); } // Example of programmatic usage: /* // 1. Create custom data const customData = [{ "1": "John Doe", "2": "Senior Developer", "3": "Engineering", "4": "https://example.com/photo.jpg", "5": "2024-01-15", "6": "2025-01-15", "7": "Tech Corp", "8": "San Francisco", "9": "94105", "10": "USA", "11": "EMP001", "12": "DOC2024001" }]; // 2. Load data programmatically state.data = customData; populateDataDropdown(); showData(0); // 3. Enable design mode toggleDesignMode(); // 4. Export layout programmatically setTimeout(() => { document.getElementById('layout-name-input').value = 'My Custom Layout'; exportLayout(); }, 5000); // 5. Generate PDF with data setTimeout(downloadPDF, 8000); */