Spaces:
Running
Running
| ; | |
| /** | |
| * 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 ? `<b>${sanitize(val)}</b>` : `<span style="color:#94a3b8">${id}</span>`; | |
| } | |
| }); | |
| // 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 ? `<b>${sanitize(val)}</b>` : ''; | |
| }); | |
| 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 = '<div class="p-4 text-center text-slate-500">No History</div>'; | |
| } 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 = ` | |
| <div class="bg-blue-100 p-2 rounded-lg text-blue-600"><i data-feather="file-text"></i></div> | |
| <div class="flex-1 min-w-0"> | |
| <div class="font-bold text-slate-800 dark:text-slate-200 truncate">${doc.fileName || 'Untitled'}</div> | |
| <div class="text-xs text-slate-500">${new Date(doc.date).toLocaleString()}</div> | |
| </div> | |
| `; | |
| 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); | |
| */ | |