| |
| 'use strict'; |
|
|
| |
| |
| |
| |
|
|
| |
| const state = { |
| data: [], |
| currentIndex: 0, |
| pdfDoc: null, |
| scale: 1, |
| originalPdfBytes: null, |
| currentFileName: '', |
| bg1Obj: '', bg1B64: '', bg2Obj: '', bg2B64: '', |
| currentView: 'template', |
| designMode: false, |
| selectedFieldId: null, |
| fieldConfig: {}, |
| currentLayoutConfig: { |
| fields: {}, |
| pages: [], |
| metadata: {} |
| }, |
| isLayoutLoaded: false |
| }; |
| |
| const DB_NAME = 'PDFWizardDB'; |
| const STORE = 'docs'; |
| let db; |
|
|
| |
| 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')); |
|
|
| |
| document.addEventListener('DOMContentLoaded', async () => { |
| try { |
| await initDB(); |
| loadFieldConfig(); |
| updateFileCount(); |
| |
| |
| 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); |
| } |
| }); |
|
|
| |
| function setupGlobalListeners() { |
| |
| document.addEventListener('toggle-theme', () => toggleTheme()); |
|
|
| |
| document.addEventListener('toggle-sidebar', () => toggleSidebar()); |
|
|
| |
| document.addEventListener('toggle-design-mode', () => toggleDesignMode()); |
| |
| 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])); |
| |
| |
| 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); |
| |
| document.getElementById('btn-export-design')?.addEventListener('click', exportLayout); |
| document.getElementById('btn-save-design')?.addEventListener('click', saveLayoutToDB); |
| document.getElementById('btn-new-design')?.addEventListener('click', newLayout); |
| |
| |
| document.querySelectorAll('.tab-btn').forEach(btn => { |
| btn.addEventListener('click', () => switchTab(btn.dataset.tab)); |
| }); |
|
|
| |
| 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)); |
| |
| ['prop-x', 'prop-y', 'prop-size', 'prop-weight', 'prop-color'].forEach(id => { |
| const el = document.getElementById(id); |
| if (el) { |
| el.addEventListener('input', updateFieldFromInputs); |
| } |
| }); |
| |
| document.querySelectorAll('.close-modal, #btn-qr-cancel').forEach(btn => { |
| btn.addEventListener('click', (e) => { |
| const modal = e.target.closest('.fixed.z-\\[200\\]'); |
| if(modal) modal.classList.add('hidden'); |
| }); |
| }); |
| |
| |
| document.getElementById('qr-url')?.addEventListener('input', updateQRPreview); |
| document.getElementById('btn-qr-apply')?.addEventListener('click', applyQRAndDownload); |
| |
| |
| document.addEventListener('show-history', showHistory); |
| |
| |
| document.addEventListener('layout-saved', updateFileCount); |
| |
| |
| 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(); |
| } |
| }); |
|
|
| |
| document.addEventListener('mousedown', onMouseDown); |
| document.addEventListener('mousemove', onMouseMove); |
| document.addEventListener('mouseup', onMouseUp); |
| } |
|
|
| |
| 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])); |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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(); |
| } |
| |
| function toggleDesignMode() { |
| state.designMode = !state.designMode; |
| document.body.classList.toggle('design-mode', state.designMode); |
| |
| |
| 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'); |
| |
| |
| 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'; |
| |
| |
| 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(); |
| |
| |
| let scale = state.scale; |
| |
| let newLeft = (e.clientX - pRect.left - dragInfo.offsetX) / scale; |
| let newTop = (e.clientY - pRect.top - dragInfo.offsetY) / scale; |
| |
| |
| newLeft = Math.max(0, newLeft); |
| newTop = Math.max(0, newTop); |
| |
| dragInfo.el.style.left = newLeft + 'px'; |
| dragInfo.el.style.top = newTop + 'px'; |
| |
| |
| 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) { |
| |
| 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; |
| } |
| |
| 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' |
| } |
| }; |
|
|
| |
| 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' |
| }; |
| }); |
|
|
| |
| ['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; |
| |
| |
| 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); |
| } |
| } |
| }); |
|
|
| |
| 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; |
| } |
| } |
| }); |
| } |
| |
| function handleBgFile(page, file) { |
| if (!file) return; |
| |
| 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; |
| } |
| |
| |
| 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>`; |
| } |
| }); |
|
|
| |
| 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'; |
|
|
| |
| 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']||'')); |
| } |
|
|
| |
| 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 }); |
| |
| 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) { |
| |
| |
| 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})`; |
| |
| p.style.marginBottom = `${(1261 * (state.scale - 1))}px`; |
| }); |
| } |
| |
| 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); |
| |
| const layoutNameInput = document.getElementById('layout-name-input') || document.getElementById('design-layout-name'); |
| if (layoutNameInput) { |
| layoutNameInput.value = layout.metadata?.name || 'Imported 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(); |
| const element = document.createElement('div'); |
| |
| |
| element.style.width = '892px'; |
| element.style.position = 'absolute'; |
| element.style.left = '-9999px'; |
| |
| ['page1', 'page2'].forEach(pid => { |
| const clone = document.getElementById(pid).cloneNode(true); |
| |
| clone.querySelectorAll('.field').forEach(f => { |
| f.classList.remove('selected'); |
| f.style.border = 'none'; |
| f.style.background = 'transparent'; |
| }); |
| element.appendChild(clone); |
| }); |
| document.body.appendChild(element); |
| |
| |
| const bg1 = element.querySelector('#bg1'); |
| const bg2 = element.querySelector('#bg2'); |
| if(state.bg1B64) bg1.src = state.bg1B64; |
| if(state.bg2B64) bg2.src = state.bg2B64; |
| |
| |
| 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)); |
|
|
| 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); |
| |
| 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); |
| |
| 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; |
| |
| |
| 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; |
| |
| |
| 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)); |
| await downloadPDF(); |
| } |
| } |
|
|
| |
| 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'); |
| |
| |
| |
| 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() { |
| |
| |
| if (!state.originalPdfBytes && state.currentView === 'pdf') { |
| toast('Please upload a base PDF first', 'error'); |
| return; |
| } |
| |
| if (state.data.length > 0) { |
| |
| await downloadPDF(); |
| return; |
| } |
| |
| |
| try { |
| loading(true, 'Processing...'); |
| const url = document.getElementById('qr-url').value; |
| |
| |
| const qrCanvas = document.createElement('canvas'); |
| |
| |
| 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); |
| |
| 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); |
| } |
| } |
|
|
| |
| 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'); |
| } |
| |
| 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); |
| } |
|
|
| |
| 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 ==='); |
| } |
|
|
| |
| if (!localStorage.getItem('pdf_wizard_usage_shown')) { |
| setTimeout(showUsageExamples, 2000); |
| localStorage.setItem('pdf_wizard_usage_shown', 'true'); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|