doegoth / script.js
protae5544's picture
ปรับตำแหน้งฟิลด์ไม่ได้
7f36aec verified
'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 => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;', '`': '&#96;'
}[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);
*/