// Receipt Designer Application class ReceiptDesigner { constructor() { this.currentTemplate = null; this.selectedElement = null; this.zoomLevel = 100; this.history = []; this.historyIndex = -1; this.isWYSIWYGMode = false; this.inlineToolbar = null; this.debounceTimer = null; this.companySaveTimer = null; this.initializeApp(); this.bindEvents(); this.setupTemplates(); this.initializeDefaultMode(); // Add security protections this.addCSPProtection(); this.validateReceiptData(); } initializeApp() { // Initialize DOM elements this.receiptPage = document.getElementById('receiptPage'); this.formatToolbar = document.getElementById('formatToolbar'); this.logoModal = document.getElementById('logoModal'); // Setup default receipt data with localStorage support this.receiptData = { company: this.loadCompanyData() || { name: 'บริษัท ตัวอย่าง จำกัด', address: '123 ถนนสุขุมวิท แขวงคลองตัน เขตวัฒนา กรุงเทพฯ 10110', phone: '02-123-4567', email: 'info@company.com', taxId: '0123456789012' }, customer: { name: 'คุณลูกค้า ตัวอย่าง', address: '456 ถนนรัชดา เขตห้วยขวาง กรุงเทพฯ 10310', phone: '08-1234-5678' }, receipt: { number: 'R2024001', date: new Date().toLocaleDateString('th-TH'), dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toLocaleDateString('th-TH') }, items: [ {description: 'สินค้า/บริการ 1', quantity: 2, price: 500, total: 1000}, {description: 'สินค้า/บริการ 2', quantity: 1, price: 750, total: 750}, {description: 'สินค้า/บริการ 3', quantity: 3, price: 300, total: 900} ], totals: { subtotal: 2650, vat: 185.5, total: 2835.5 } }; } bindEvents() { // Template selection document.querySelectorAll('.template-card').forEach(card => { card.addEventListener('click', (e) => { const template = e.currentTarget.dataset.template; this.selectTemplate(template); }); }); // Toolbar buttons document.getElementById('newReceipt').addEventListener('click', () => this.newReceipt()); document.getElementById('printReceipt').addEventListener('click', () => this.printReceipt()); document.getElementById('exportPdf').addEventListener('click', () => this.exportToPDF()); // Company data buttons document.getElementById('resetCompanyData').addEventListener('click', () => this.resetCompanyData()); document.getElementById('undo').addEventListener('click', () => this.undo()); document.getElementById('redo').addEventListener('click', () => this.redo()); document.getElementById('zoomIn').addEventListener('click', () => this.zoomIn()); document.getElementById('zoomOut').addEventListener('click', () => this.zoomOut()); // Mode buttons document.getElementById('editMode').addEventListener('click', () => this.enableWYSIWYGMode()); document.getElementById('previewMode').addEventListener('click', () => this.disableWYSIWYGMode()); // Format toolbar events this.bindFormatToolbarEvents(); // Text selection for WYSIWYG document.addEventListener('mouseup', () => this.handleTextSelection()); document.addEventListener('keyup', () => this.handleTextSelection()); // Tool buttons document.getElementById('addLogo').addEventListener('click', () => this.showLogoModal()); document.getElementById('addField').addEventListener('click', () => this.addField()); // Color and font changes document.getElementById('primaryColor').addEventListener('change', (e) => { this.updatePrimaryColor(e.target.value); }); document.getElementById('secondaryColor').addEventListener('change', (e) => { this.updateSecondaryColor(e.target.value); }); document.getElementById('fontFamily').addEventListener('change', (e) => { this.updateFontFamily(e.target.value); }); // VAT Rate change document.getElementById('vatRate').addEventListener('change', () => { this.calculateTotals(); this.updateVatLabel(); }); // Modal events document.querySelector('.modal-close').addEventListener('click', () => this.hideModal()); document.getElementById('logoUpload').addEventListener('click', () => { document.getElementById('logoInput').click(); }); document.getElementById('logoInput').addEventListener('change', (e) => { this.handleLogoUpload(e); }); // Drag and drop for logo const logoUpload = document.getElementById('logoUpload'); logoUpload.addEventListener('dragover', (e) => { e.preventDefault(); logoUpload.style.backgroundColor = '#f0f8ff'; }); logoUpload.addEventListener('dragleave', () => { logoUpload.style.backgroundColor = ''; }); logoUpload.addEventListener('drop', (e) => { e.preventDefault(); logoUpload.style.backgroundColor = ''; const files = e.dataTransfer.files; if (files.length > 0) { this.processLogoFile(files[0]); } }); // Click outside to deselect document.addEventListener('click', (e) => { if (!e.target.closest('.editable') && !e.target.closest('.format-toolbar')) { this.deselectElement(); this.hideInlineToolbar(); } }); } // WYSIWYG Mode Functions enableWYSIWYGMode() { this.isWYSIWYGMode = true; document.body.classList.add('wysiwyg-mode'); // Update mode buttons document.getElementById('editMode').classList.add('active'); document.getElementById('previewMode').classList.remove('active'); // Don't show format toolbar immediately - it will appear on text selection // this.formatToolbar.style.display = 'block'; // Make all editable elements content editable this.makeElementsContentEditable(true); // Show edit mode indicator this.showEditModeIndicator(); } disableWYSIWYGMode() { this.isWYSIWYGMode = false; document.body.classList.remove('wysiwyg-mode'); // Update mode buttons document.getElementById('editMode').classList.remove('active'); document.getElementById('previewMode').classList.add('active'); // Hide format toolbar this.formatToolbar.style.display = 'none'; // Disable content editing this.makeElementsContentEditable(false); // Hide indicator this.hideEditModeIndicator(); } makeElementsContentEditable(enable) { const editableElements = this.receiptPage.querySelectorAll('.editable'); editableElements.forEach(element => { // ไม่ให้แก้ไขช่องยอดรวม ภาษี และรวมทั้งสิ้น const isCalculatedField = element.classList.contains('item-total') || element.closest('.total-box') || element.closest('.total-section') || element.closest('[class*="total"]'); if (enable && !isCalculatedField) { element.setAttribute('contenteditable', 'true'); element.addEventListener('input', this.handleContentEdit.bind(this)); element.addEventListener('focus', this.handleElementFocus.bind(this)); element.addEventListener('blur', this.handleElementBlur.bind(this)); } else { element.removeAttribute('contenteditable'); element.removeEventListener('input', this.handleContentEdit.bind(this)); element.removeEventListener('focus', this.handleElementFocus.bind(this)); element.removeEventListener('blur', this.handleElementBlur.bind(this)); } }); } handleContentEdit(e) { const element = e.target; // Sync content with receiptData this.syncElementToReceiptData(element); // ตรวจสอบว่าเป็นการแก้ไขข้อมูลในตารางสินค้าหรือไม่ if (element.classList.contains('item-quantity') || element.classList.contains('item-price') || element.closest('.items-table')) { // คำนวณยอดรวมใหม่หลังจากการแก้ไข setTimeout(() => this.calculateTotals(), 300); } // Debounced save this.debouncedSave(); } handleElementFocus(e) { this.selectedElement = e.target; e.target.classList.add('selected'); } handleElementBlur(e) { e.target.classList.remove('selected'); this.debouncedSave(); } handleTextSelection() { if (!this.isWYSIWYGMode) return; const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); const selectedElement = range.commonAncestorContainer.nodeType === Node.TEXT_NODE ? range.commonAncestorContainer.parentElement : range.commonAncestorContainer; if (selectedElement.closest('.editable')) { this.showInlineToolbar(range); this.updateToolbarState(); } } else { this.hideInlineToolbar(); } } showInlineToolbar(range) { const rect = range.getBoundingClientRect(); const toolbar = this.formatToolbar; // Show the toolbar toolbar.style.display = 'block'; // Position it above the selection const toolbarRect = toolbar.getBoundingClientRect(); let left = rect.left + rect.width / 2 - toolbarRect.width / 2; let top = rect.top - toolbarRect.height - 10; // Keep toolbar within viewport const viewport = { width: window.innerWidth, height: window.innerHeight }; if (left < 10) left = 10; if (left + toolbarRect.width > viewport.width - 10) { left = viewport.width - toolbarRect.width - 10; } if (top < 10) { // Show below selection if no room above top = rect.bottom + 10; } toolbar.style.left = left + 'px'; toolbar.style.top = top + 'px'; } hideInlineToolbar() { if (this.formatToolbar) { this.formatToolbar.style.display = 'none'; } } updateToolbarState() { // Update toolbar button states based on current selection const selection = window.getSelection(); if (selection.rangeCount === 0) return; // Check if formatting is applied const isBold = document.queryCommandState('bold'); const isItalic = document.queryCommandState('italic'); const isUnderline = document.queryCommandState('underline'); // Update button states this.toggleToolbarButton('bold', isBold); this.toggleToolbarButton('italic', isItalic); this.toggleToolbarButton('underline', isUnderline); } toggleToolbarButton(command, isActive) { const button = document.querySelector(`[data-command="${command}"]`); if (button) { if (isActive) { button.classList.add('active'); } else { button.classList.remove('active'); } } } bindFormatToolbarEvents() { // Format buttons const toolbarButtons = document.querySelectorAll('.format-toolbar .toolbar-btn[data-command]'); toolbarButtons.forEach(button => { button.addEventListener('click', (e) => { e.preventDefault(); const command = button.dataset.command; this.executeFormatCommand(command); }); }); // Font size select const fontSizeSelect = document.getElementById('fontSizeSelect'); if (fontSizeSelect) { fontSizeSelect.addEventListener('change', (e) => { this.applyFontSize(e.target.value); }); } // Text color const textColor = document.getElementById('textColor'); if (textColor) { textColor.addEventListener('change', (e) => { this.applyTextColor(e.target.value); }); } // Add item button const addItemBtn = document.getElementById('addItemBtn'); if (addItemBtn) { addItemBtn.addEventListener('click', () => { this.addNewItem(); }); } } executeFormatCommand(command = '') { document.execCommand(command, false, null); this.updateToolbarState(); this.debouncedSave(); } applyFontSize(size) { const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); if (!range.collapsed) { document.execCommand('fontSize', false, '7'); const selectedElement = range.commonAncestorContainer.parentElement; const fontElements = selectedElement.querySelectorAll('font[size="7"]'); fontElements.forEach(el => { el.style.fontSize = size + 'px'; el.removeAttribute('size'); }); this.debouncedSave(); } } } applyTextColor(color) { document.execCommand('foreColor', false, color); this.debouncedSave(); } syncElementToReceiptData(element) { const classList = Array.from(element.classList); const content = this.validateString(element.textContent.trim()); let isCompanyData = false; if (classList.includes('company-name')) { this.receiptData.company.name = content; isCompanyData = true; } else if (classList.includes('company-address')) { this.receiptData.company.address = content; isCompanyData = true; } else if (classList.includes('company-phone')) { // ลบข้อความ "โทร: " ออกหากมี const phoneContent = content.replace(/^โทร:\s*/, '').trim(); this.receiptData.company.phone = phoneContent; isCompanyData = true; } else if (classList.includes('company-email')) { // ลบข้อความ "อีเมล: " ออกหากมี const emailContent = content.replace(/^อีเมล:\s*/, '').trim(); this.receiptData.company.email = emailContent; isCompanyData = true; } else if (classList.includes('company-tax-id')) { // ลบข้อความ "เลขประจำตัวผู้เสียภาษี: " ออกหากมี const taxIdContent = content.replace(/^เลขประจำตัวผู้เสียภาษี:\s*/, '').trim(); this.receiptData.company.taxId = taxIdContent; isCompanyData = true; } else if (classList.includes('customer-name')) { this.receiptData.customer.name = content; } else if (classList.includes('customer-address')) { this.receiptData.customer.address = content; } else if (classList.includes('customer-phone')) { this.receiptData.customer.phone = content; } else if (classList.includes('receipt-number')) { // ลบเครื่องหมาย # หากมี const receiptNumber = content.replace(/^#/, '').trim(); this.receiptData.receipt.number = receiptNumber; } else if (classList.includes('receipt-date')) { this.receiptData.receipt.date = content; } else if (classList.includes('receipt-due-date')) { this.receiptData.receipt.dueDate = content; } // บันทึกข้อมูลบริษัทอัตโนมัติเมื่อมีการแก้ไข if (isCompanyData) { this.debouncedSaveCompany(); } } debouncedSave() { clearTimeout(this.debounceTimer); this.debounceTimer = setTimeout(() => { this.saveToHistory(); }, 500); } debouncedSaveCompany() { clearTimeout(this.companySaveTimer); this.companySaveTimer = setTimeout(() => { this.saveCompanyData(); }, 1000); } showEditModeIndicator() { let indicator = document.querySelector('.edit-mode-indicator'); if (!indicator) { indicator = document.createElement('div'); indicator.className = 'edit-mode-indicator'; indicator.innerHTML = ' โหมดแก้ไข'; document.body.appendChild(indicator); } indicator.style.display = 'flex'; } hideEditModeIndicator() { const indicator = document.querySelector('.edit-mode-indicator'); if (indicator) { indicator.style.display = 'none'; } } setupTemplates() { this.templates = { modern: { name: 'โมเดิร์น', html: this.getModernTemplate() }, classic: { name: 'คลาสสิก', html: this.getClassicTemplate() }, minimal: { name: 'มินิมอล', html: this.getMinimalTemplate() } }; } selectTemplate(templateName) { // Update UI document.querySelectorAll('.template-card').forEach(card => { card.classList.remove('active'); }); document.querySelector(`[data-template="${templateName}"]`).classList.add('active'); this.currentTemplate = templateName; this.receiptPage.innerHTML = this.templates[templateName].html; // Populate data into template this.populateTemplateData(); // Make elements editable this.makeElementsEditable(); // Apply WYSIWYG mode if enabled if (this.isWYSIWYGMode) { this.makeElementsContentEditable(true); } // อัปเดตข้อความภาษีให้ตรงกับอัตราที่เลือก this.updateVatLabel(); // Enable buttons document.getElementById('printReceipt').disabled = false; document.getElementById('exportPdf').disabled = false; // Save to history this.saveToHistory(); } populateTemplateData() { // Populate company data const companyName = this.receiptPage.querySelector('.company-name'); if (companyName) companyName.textContent = this.receiptData.company.name; const companyAddress = this.receiptPage.querySelector('.company-address'); if (companyAddress) companyAddress.textContent = this.receiptData.company.address; const companyPhone = this.receiptPage.querySelector('.company-phone'); if (companyPhone) companyPhone.textContent = this.receiptData.company.phone; const companyEmail = this.receiptPage.querySelector('.company-email'); if (companyEmail) companyEmail.textContent = this.receiptData.company.email; const companyTaxId = this.receiptPage.querySelector('.company-tax-id'); if (companyTaxId) companyTaxId.textContent = this.receiptData.company.taxId; // Populate customer data const customerName = this.receiptPage.querySelector('.customer-name'); if (customerName) customerName.textContent = this.receiptData.customer.name; const customerAddress = this.receiptPage.querySelector('.customer-address'); if (customerAddress) customerAddress.textContent = this.receiptData.customer.address; const customerPhone = this.receiptPage.querySelector('.customer-phone'); if (customerPhone) customerPhone.textContent = this.receiptData.customer.phone; // Populate receipt data const receiptNumber = this.receiptPage.querySelector('.receipt-number'); if (receiptNumber) receiptNumber.textContent = this.receiptData.receipt.number; const receiptDate = this.receiptPage.querySelector('.receipt-date'); if (receiptDate) receiptDate.textContent = this.receiptData.receipt.date; const receiptDueDate = this.receiptPage.querySelector('.receipt-due-date'); if (receiptDueDate) receiptDueDate.textContent = this.receiptData.receipt.dueDate; // Populate items table this.populateItemsTable(); } populateItemsTable() { const itemsTableBody = this.receiptPage.querySelector('.items-table tbody'); if (!itemsTableBody) return; // Clear existing rows except template const rows = itemsTableBody.querySelectorAll('tr:not(.item-template)'); rows.forEach(row => row.remove()); // Add items this.receiptData.items.forEach((item, index) => { this.addItemRow(item, index); }); // Update totals this.updateTotals(); } makeElementsEditable() { const editableElements = this.receiptPage.querySelectorAll('.editable'); editableElements.forEach(element => { // Basic click handler for selection (always active) element.addEventListener('click', (e) => { e.stopPropagation(); this.selectElement(element); }); // Input handler for traditional editing element.addEventListener('input', () => { if (!this.isWYSIWYGMode) { this.saveToHistory(); } }); element.addEventListener('blur', () => { if (!this.isWYSIWYGMode) { this.saveToHistory(); } }); }); // Setup add/remove buttons for items table this.setupItemTableControls(); } setupItemTableControls() { // Setup existing remove buttons this.setupRemoveButtons(); } setupRemoveButtons() { const removeButtons = this.receiptPage.querySelectorAll('.remove-item-btn'); removeButtons.forEach((button) => { button.addEventListener('click', (e) => { const row = e.target.closest('tr'); this.deleteRow(row); }); }); } addNewItem() { const newItem = { description: 'สินค้า/บริการใหม่', quantity: 1, price: 0, total: 0 }; this.receiptData.items.push(newItem); this.addItemRow(newItem, this.receiptData.items.length - 1); this.updateTotals(); this.saveToHistory(); } addItemRow(item, index) { const itemsTableBody = this.receiptPage.querySelector('.items-table tbody'); if (!itemsTableBody) return; const row = document.createElement('tr'); row.className = 'item-row'; row.innerHTML = ` ${item.description} ${item.quantity} ${item.price.toLocaleString()} ${item.total.toLocaleString()} `; itemsTableBody.appendChild(row); // Setup event listeners for this row this.setupRowEventListeners(row, index); } setupRowEventListeners(row, index) { const descriptionCell = row.querySelector('.item-description'); const quantityCell = row.querySelector('.item-quantity'); const priceCell = row.querySelector('.item-price'); const totalCell = row.querySelector('.item-total'); const removeBtn = row.querySelector('.remove-item-btn'); // Description change descriptionCell.addEventListener('input', () => { this.receiptData.items[index].description = this.validateString(descriptionCell.textContent); this.debouncedSave(); }); // Quantity change quantityCell.addEventListener('input', () => { const quantity = this.validateNumber(quantityCell.textContent); this.receiptData.items[index].quantity = quantity; this.updateItemTotal(index); this.debouncedSave(); }); // Price change priceCell.addEventListener('input', () => { const priceText = priceCell.textContent.replace(/,/g, ''); const price = this.validateNumber(priceText); this.receiptData.items[index].price = price; this.updateItemTotal(index); this.debouncedSave(); }); // Remove button removeBtn.addEventListener('click', (e) => { this.deleteRow(row); }); // Make editable if in WYSIWYG mode if (this.isWYSIWYGMode) { descriptionCell.setAttribute('contenteditable', 'true'); quantityCell.setAttribute('contenteditable', 'true'); priceCell.setAttribute('contenteditable', 'true'); } } updateItemTotal(index) { const item = this.receiptData.items[index]; item.total = item.quantity * item.price; // Update display const rows = this.receiptPage.querySelectorAll('.item-row'); if (rows[index]) { const totalCell = rows[index].querySelector('.item-total'); totalCell.textContent = item.total.toLocaleString(); } this.updateTotals(); } updateTotals() { const subtotal = this.receiptData.items.reduce((sum, item) => sum + item.total, 0); const vat = subtotal * 0.07; // 7% VAT const total = subtotal + vat; this.receiptData.totals = { subtotal: subtotal, vat: vat, total: total }; // Update display const subtotalElement = this.receiptPage.querySelector('.subtotal'); if (subtotalElement) subtotalElement.textContent = subtotal.toLocaleString(); const vatElement = this.receiptPage.querySelector('.vat'); if (vatElement) vatElement.textContent = vat.toLocaleString(); const totalElement = this.receiptPage.querySelector('.total'); if (totalElement) totalElement.textContent = total.toLocaleString(); } selectElement(element) { // Remove previous selection if (this.selectedElement) { this.selectedElement.classList.remove('selected'); } // Select new element this.selectedElement = element; element.classList.add('selected'); } deselectElement() { if (this.selectedElement) { this.selectedElement.classList.remove('selected'); this.selectedElement = null; } } updatePrimaryColor(color) { document.documentElement.style.setProperty('--primary-color', color); const primaryElements = this.receiptPage.querySelectorAll('.receipt-header, .items-table th, .receipt-title'); primaryElements.forEach(el => { if (el.classList.contains('items-table')) { el.style.backgroundColor = color; } else { el.style.color = color; } }); } updateSecondaryColor(color) { document.documentElement.style.setProperty('--secondary-color', color); } updateFontFamily(font) { this.receiptPage.style.fontFamily = font; } showLogoModal() { this.logoModal.classList.add('show'); } hideModal() { this.logoModal.classList.remove('show'); } handleLogoUpload(event) { const file = event.target.files[0]; if (file) { this.processLogoFile(file); } } processLogoFile(file) { if (!file.type.startsWith('image/')) { alert('กรุณาเลือกไฟล์รูปภาพเท่านั้น'); return; } const reader = new FileReader(); reader.onload = (e) => { const logoContainer = this.receiptPage.querySelector('.company-logo'); if (logoContainer) { logoContainer.innerHTML = `โลโก้บริษัท`; } else { // Add logo to header const header = this.receiptPage.querySelector('.receipt-header .company-info'); if (header) { const logoDiv = document.createElement('div'); logoDiv.className = 'company-logo'; logoDiv.innerHTML = `โลโก้บริษัท`; header.insertBefore(logoDiv, header.firstChild); } } this.hideModal(); this.saveToHistory(); }; reader.readAsDataURL(file); } addField() { const field = document.createElement('div'); field.className = 'editable custom-field'; field.contentEditable = true; field.textContent = 'ฟิลด์ใหม่'; field.style.margin = '0.5rem 0'; field.style.padding = '0.5rem'; field.style.border = '1px dashed #ccc'; // Add to customer section const customerSection = this.receiptPage.querySelector('.customer-section'); if (customerSection) { customerSection.appendChild(field); this.makeElementsEditable(); this.saveToHistory(); } } zoomIn() { if (this.zoomLevel < 200) { this.zoomLevel += 10; this.updateZoom(); } } zoomOut() { if (this.zoomLevel > 50) { this.zoomLevel -= 10; this.updateZoom(); } } updateZoom() { this.receiptPage.style.transform = `scale(${this.zoomLevel / 100})`; this.receiptPage.style.transformOrigin = 'top center'; document.querySelector('.zoom-level').textContent = this.zoomLevel + '%'; } saveToHistory() { const state = { html: this.receiptPage.innerHTML, template: this.currentTemplate, timestamp: Date.now() }; // Remove future states if we're not at the end this.history = this.history.slice(0, this.historyIndex + 1); // Add new state this.history.push(state); this.historyIndex = this.history.length - 1; // Limit history size if (this.history.length > 50) { this.history = this.history.slice(-50); this.historyIndex = this.history.length - 1; } } undo() { if (this.historyIndex > 0) { this.historyIndex--; const state = this.history[this.historyIndex]; this.receiptPage.innerHTML = state.html; this.currentTemplate = state.template; this.makeElementsEditable(); this.deselectElement(); } } redo() { if (this.historyIndex < this.history.length - 1) { this.historyIndex++; const state = this.history[this.historyIndex]; this.receiptPage.innerHTML = state.html; this.currentTemplate = state.template; this.makeElementsEditable(); this.deselectElement(); } } newReceipt() { if (confirm('สร้างใบเสร็จใหม่? ข้อมูลปัจจุบันจะหายไป')) { this.receiptPage.innerHTML = `

เลือกเทมเพลตจากด้านซ้ายเพื่อเริ่มออกแบบใบเสร็จ

`; this.currentTemplate = null; this.deselectElement(); document.getElementById('printReceipt').disabled = true; document.getElementById('exportPdf').disabled = true; // Clear template selection document.querySelectorAll('.template-card').forEach(card => { card.classList.remove('active'); }); } } printReceipt() { if (this.currentTemplate) { window.print(); } } async exportToPDF() { if (!this.currentTemplate) return; try { // Show loading indicator const exportBtn = document.getElementById('exportPdf'); const originalText = exportBtn.innerHTML; exportBtn.innerHTML = ' กำลังสร้าง PDF...'; exportBtn.disabled = true; // Get the receipt page element const receiptElement = document.getElementById('receiptPage'); // Temporarily hide selection highlights and management buttons const selectedElements = receiptElement.querySelectorAll('.selected'); const actionButtons = receiptElement.querySelectorAll('.item-actions, .table-actions, .action-btn, .remove-item-btn'); selectedElements.forEach(el => { el.classList.remove('selected'); el.style.outline = 'none'; el.style.background = 'transparent'; }); actionButtons.forEach(el => { el.style.display = 'none'; }); // Clone the element to avoid modifying the original const clonedElement = receiptElement.cloneNode(true); // Add PDF optimization class clonedElement.classList.add('pdf-optimized'); // Clean up cloned element for PDF - remove all management elements const clonedActionElements = clonedElement.querySelectorAll( '.item-actions, .table-actions, .action-btn, .remove-item-btn, .toolbar-btn' ); clonedActionElements.forEach(el => el.remove()); // Remove management column headers and adjust table structure const tables = clonedElement.querySelectorAll('table'); tables.forEach(table => { // Remove management column header const headers = table.querySelectorAll('th'); headers.forEach(header => { if (header.textContent.includes('จัดการ') || header.textContent.includes('Management')) { header.remove(); } }); // Remove management cells from all rows const rows = table.querySelectorAll('tr'); rows.forEach(row => { const cells = row.querySelectorAll('td'); cells.forEach(cell => { if (cell.classList.contains('item-actions') || cell.querySelector('.remove-item-btn') || cell.querySelector('.action-btn')) { cell.remove(); } }); }); }); // Enhance styling for PDF with explicit color settings clonedElement.style.transform = 'scale(1)'; clonedElement.style.transformOrigin = 'top left'; clonedElement.style.boxShadow = 'none'; clonedElement.style.border = 'none'; clonedElement.style.background = '#ffffff'; clonedElement.style.backgroundColor = '#ffffff'; // Force all elements to have proper colors and backgrounds const allElements = clonedElement.querySelectorAll('*'); allElements.forEach(el => { // Remove any problematic styling el.style.textRendering = 'optimizeLegibility'; el.style.webkitFontSmoothing = 'antialiased'; el.style.mozOsxFontSmoothing = 'grayscale'; // Force text color if (!el.style.color || el.style.color === '') { el.style.color = '#000000'; } // Ensure no transparent backgrounds that might cause issues if (el.style.backgroundColor === 'transparent' || el.style.backgroundColor === '') { el.style.backgroundColor = 'inherit'; } // Remove shadows and effects el.style.boxShadow = 'none'; el.style.textShadow = 'none'; el.style.filter = 'none'; // Fix gradient backgrounds to solid colors if (el.style.background && el.style.background.includes('gradient')) { el.style.background = '#ffffff'; el.style.backgroundColor = '#ffffff'; } }); // Fix table styling specifically const pdfTables = clonedElement.querySelectorAll('table'); pdfTables.forEach(table => { table.style.pageBreakInside = 'avoid'; table.style.borderCollapse = 'collapse'; table.style.width = '100%'; table.style.backgroundColor = '#ffffff'; table.style.color = '#000000'; // Fix table cell padding and borders const cells = table.querySelectorAll('th, td'); cells.forEach(cell => { cell.style.padding = '8px'; cell.style.border = '1px solid #333333'; cell.style.fontSize = '14px'; cell.style.lineHeight = '1.4'; cell.style.color = '#000000'; cell.style.backgroundColor = '#ffffff'; }); // Style table headers with visible background const headers = table.querySelectorAll('th'); headers.forEach(th => { th.style.backgroundColor = '#f5f5f5'; th.style.color = '#000000'; th.style.fontWeight = '600'; th.style.textAlign = 'center'; }); }); // Fix number alignment in tables const numberCells = clonedElement.querySelectorAll('.item-quantity, .item-price, .item-total'); numberCells.forEach(cell => { cell.style.textAlign = 'right'; cell.style.fontFamily = 'monospace, Sarabun'; }); // Create a temporary container optimized for PDF rendering const tempContainer = document.createElement('div'); tempContainer.style.cssText = ` position: absolute; left: -9999px; top: 0; width: 794px; min-height: 1123px; padding: 40px; background: #ffffff !important; background-color: #ffffff !important; font-family: 'Sarabun', Arial, sans-serif; font-size: 14px; line-height: 1.4; color: #000000 !important; box-sizing: border-box; -webkit-print-color-adjust: exact; print-color-adjust: exact; overflow: visible; `; // Ensure cloned element has proper styling clonedElement.style.cssText = ` background: #ffffff !important; background-color: #ffffff !important; color: #000000 !important; width: 100% !important; min-height: auto !important; padding: 0 !important; margin: 0 !important; box-shadow: none !important; border: none !important; `; tempContainer.appendChild(clonedElement); document.body.appendChild(tempContainer); // Wait for fonts to load await document.fonts.ready; // Add a delay to ensure proper rendering await new Promise(resolve => setTimeout(resolve, 500)); // Generate canvas with debug settings console.log('Starting PDF generation...'); const canvas = await html2canvas(tempContainer, { scale: 1.5, useCORS: true, allowTaint: true, backgroundColor: '#ffffff', logging: true, removeContainer: false, imageTimeout: 10000, scrollX: 0, scrollY: 0, windowWidth: 794, windowHeight: 1123, onclone: (clonedDoc) => { // Force all elements in cloned document to have proper colors const allEls = clonedDoc.querySelectorAll('*'); allEls.forEach(el => { el.style.color = '#000000'; if (el.style.backgroundColor === 'transparent') { el.style.backgroundColor = '#ffffff'; } }); console.log('Canvas cloning completed'); }, ignoreElements: (element) => { return element.classList.contains('item-actions') || element.classList.contains('table-actions') || element.classList.contains('action-btn'); } }); console.log('Canvas generated:', canvas.width, 'x', canvas.height); // Check if canvas is valid and not completely black const ctx = canvas.getContext('2d'); const imageData = ctx.getImageData(0, 0, Math.min(100, canvas.width), Math.min(100, canvas.height)); const pixels = imageData.data; let isAllBlack = true; // Check first 100x100 pixels to see if it's all black for (let i = 0; i < pixels.length; i += 4) { const r = pixels[i]; const g = pixels[i + 1]; const b = pixels[i + 2]; if (r > 10 || g > 10 || b > 10) { // Allow for slight variations isAllBlack = false; break; } } if (isAllBlack) { console.warn('Canvas appears to be all black, attempting alternative method...'); // Show debug canvas to user const debugDiv = document.createElement('div'); debugDiv.style.cssText = ` position: fixed; top: 10px; right: 10px; z-index: 10000; background: white; border: 2px solid red; padding: 10px; max-width: 300px; `; debugDiv.innerHTML = `

Debug: Canvas รูปแบบไม่ถูกต้อง

`; document.body.appendChild(debugDiv); const debugCanvas = debugDiv.querySelector('#debugCanvas'); debugCanvas.width = Math.min(280, canvas.width); debugCanvas.height = Math.min(200, canvas.height); const debugCtx = debugCanvas.getContext('2d'); debugCtx.drawImage(canvas, 0, 0, debugCanvas.width, debugCanvas.height); // Try with minimal settings const fallbackCanvas = await html2canvas(tempContainer, { scale: 1, backgroundColor: '#ffffff', logging: true, useCORS: false, allowTaint: false, onrendered: (canvas) => { console.log('Fallback canvas rendered'); } }); if (fallbackCanvas && fallbackCanvas.width > 0 && fallbackCanvas.height > 0) { console.log('Using fallback canvas'); canvas = fallbackCanvas; } else { throw new Error('ไม่สามารถสร้าง Canvas ได้ กรุณาลองใหม่อีกครั้ง'); } } // Clean up temporary container document.body.removeChild(tempContainer); // Restore original elements actionButtons.forEach(el => { el.style.display = ''; }); // Create PDF from canvas await this.createPDFFromCanvas(canvas); } catch (error) { console.error('Export PDF Error:', error); this.showNotification('❌ เกิดข้อผิดพลาดในการส่งออก PDF: ' + error.message, 'error'); } finally { // Restore button const exportBtn = document.getElementById('exportPdf'); exportBtn.innerHTML = ' Export PDF'; exportBtn.disabled = false; } } async createPDFFromCanvas(canvas) { // Create PDF with better settings const {jsPDF} = window.jspdf; const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4', compress: true, precision: 2 }); // Calculate optimal dimensions with better fitting const pdfWidth = 210; // A4 width in mm const pdfHeight = 297; // A4 height in mm const margin = 10; // Small margin for better appearance const availableWidth = pdfWidth - (margin * 2); const availableHeight = pdfHeight - (margin * 2); // Calculate scaling to fit content properly const scaleX = availableWidth / (canvas.width / 3.78); // Convert pixels to mm const scaleY = availableHeight / (canvas.height / 3.78); const scale = Math.min(scaleX, scaleY, 1); // Don't scale up const imgWidth = (canvas.width / 3.78) * scale; const imgHeight = (canvas.height / 3.78) * scale; // Center the content const offsetX = margin + (availableWidth - imgWidth) / 2; const offsetY = margin; // Convert canvas to optimized image with white background const tempCanvas = document.createElement('canvas'); tempCanvas.width = canvas.width; tempCanvas.height = canvas.height; const tempCtx = tempCanvas.getContext('2d'); // Fill with white background first tempCtx.fillStyle = '#ffffff'; tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height); // Then draw the original canvas on top tempCtx.drawImage(canvas, 0, 0); const imgData = tempCanvas.toDataURL('image/jpeg', 0.95); // Smart page handling if (imgHeight <= availableHeight) { // Single page - center content pdf.addImage(imgData, 'JPEG', offsetX, offsetY, imgWidth, imgHeight); } else { // Multiple pages with proper breaks let currentY = offsetY; let remainingHeight = imgHeight; let sourceY = 0; while (remainingHeight > 0) { const currentPageHeight = Math.min(remainingHeight, availableHeight); const sourceHeight = (currentPageHeight / imgHeight) * canvas.height; // Create canvas section for current page const pageCanvas = document.createElement('canvas'); const pageCtx = pageCanvas.getContext('2d'); pageCanvas.width = canvas.width; pageCanvas.height = sourceHeight; // Fill with white background pageCtx.fillStyle = '#ffffff'; pageCtx.fillRect(0, 0, pageCanvas.width, pageCanvas.height); // Draw section of original canvas pageCtx.drawImage(canvas, 0, sourceY, canvas.width, sourceHeight, 0, 0, canvas.width, sourceHeight); const pageImgData = pageCanvas.toDataURL('image/jpeg', 0.95); // Add to PDF pdf.addImage(pageImgData, 'JPEG', offsetX, currentY, imgWidth, currentPageHeight); // Prepare for next page remainingHeight -= currentPageHeight; sourceY += sourceHeight; if (remainingHeight > 0) { pdf.addPage(); currentY = offsetY; } } } // Add metadata to PDF pdf.setProperties({ title: `ใบเสร็จรับเงิน ${this.receiptData.receipt.number}`, subject: 'Receipt', author: this.receiptData.company.name, creator: 'Receipt Designer', producer: 'Receipt Designer' }); // Generate clean filename const receiptNumber = this.receiptData.receipt.number?.replace(/[^a-zA-Z0-9]/g, '') || 'R001'; const now = new Date(); const dateStr = now.toISOString().slice(0, 10).replace(/-/g, ''); const timeStr = now.toTimeString().slice(0, 5).replace(':', ''); const filename = `Receipt-${receiptNumber}-${dateStr}-${timeStr}.pdf`; // Save PDF pdf.save(filename); // Success message with file info this.showNotification(`✅ ส่งออก PDF สำเร็จ! ไฟล์: ${filename}`, 'success'); } showNotification(message, type = 'info') { // Create notification element const notification = document.createElement('div'); notification.className = `notification notification-${type}`; notification.textContent = message; notification.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 1rem 1.5rem; border-radius: 6px; color: white; font-weight: 500; z-index: 10000; max-width: 300px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); transform: translateX(100%); transition: transform 0.3s ease; `; // Set background color based on type switch (type) { case 'success': notification.style.background = '#27ae60'; break; case 'error': notification.style.background = '#e74c3c'; break; default: notification.style.background = '#3498db'; } document.body.appendChild(notification); // Animate in setTimeout(() => { notification.style.transform = 'translateX(0)'; }, 100); // Auto remove after 3 seconds setTimeout(() => { notification.style.transform = 'translateX(100%)'; setTimeout(() => { if (notification.parentNode) { document.body.removeChild(notification); } }, 300); }, 3000); } // รายการสินค้า Methods addRow() { if (!this.currentTemplate) return; const table = this.receiptPage.querySelector('.items-table tbody'); if (!table) return; const newIndex = table.children.length; const newRow = document.createElement('tr'); newRow.setAttribute('data-row-index', newIndex); // สร้างแถวใหม่ตามรูปแบบของเทมเพลต if (this.currentTemplate === 'modern') { newRow.innerHTML = ` รายการใหม่ 1 0 0 `; } else if (this.currentTemplate === 'classic') { newRow.style.borderBottom = '1px solid #2c3e50'; newRow.innerHTML = ` รายการใหม่ 1 0 0 `; } else if (this.currentTemplate === 'minimal') { newRow.style.borderBottom = '1px solid #f5f5f5'; newRow.innerHTML = ` รายการใหม่ 1 0 0 `; } table.appendChild(newRow); // อัปเดต index ของแถวทั้งหมด this.updateRowIndexes(); // ทำให้แถวใหม่แก้ไขได้ this.makeElementsEditable(); // เพิ่มข้อมูลในอาร์เรย์ this.receiptData.items.push({ description: 'รายการใหม่', quantity: 1, price: 0, total: 0 }); // คำนวณยอดรวมใหม่ this.calculateTotals(); this.saveToHistory(); this.showNotification('✅ เพิ่มรายการใหม่แล้ว', 'success'); } deleteRow(indexOrRow) { if (!this.currentTemplate) return; const table = this.receiptPage.querySelector('.items-table tbody, .items-table'); const rows = table.querySelectorAll('tr'); if (rows.length <= 1) { this.showNotification('⚠️ ต้องมีรายการอย่างน้อย 1 รายการ', 'error'); return; } let index; let targetRow; // ตรวจสอบว่าส่งมา index หรือ DOM element if (typeof indexOrRow === 'number') { index = indexOrRow; targetRow = rows[index]; } else { // ส่งมา DOM element targetRow = indexOrRow; index = Array.from(rows).indexOf(targetRow); } if (index >= 0 && index < rows.length && targetRow) { // ลบแถวจาก DOM targetRow.remove(); // ลบข้อมูลจากอาร์เรย์ this.receiptData.items.splice(index, 1); // อัปเดต index ของแถวทั้งหมด this.updateRowIndexes(); // คำนวณยอดรวมใหม่ this.calculateTotals(); this.saveToHistory(); this.showNotification('✅ ลบรายการแล้ว', 'success'); } } updateRowIndexes() { const table = this.receiptPage.querySelector('.items-table tbody, .items-table'); const rows = table.querySelectorAll('tr'); rows.forEach((row, index) => { row.setAttribute('data-row-index', index); const deleteBtn = row.querySelector('.delete-row'); if (deleteBtn) { deleteBtn.setAttribute('onclick', `receiptDesigner.deleteRow(${index})`); } }); } calculateTotals() { if (!this.currentTemplate) return; const table = this.receiptPage.querySelector('.items-table tbody'); const rows = table.querySelectorAll('tr'); let subtotal = 0; // คำนวณยอดรวมของแต่ละรายการ และรวมทั้งหมด rows.forEach((row, index) => { const quantityEl = row.querySelector('.item-quantity'); const priceEl = row.querySelector('.item-price'); const totalEl = row.querySelector('.item-total'); if (quantityEl && priceEl && totalEl) { const quantity = parseFloat(quantityEl.textContent.replace(/,/g, '')) || 0; const price = parseFloat(priceEl.textContent.replace(/,/g, '')) || 0; const total = quantity * price; // อัปเดตยอดรวมในตาราง totalEl.textContent = total.toLocaleString(); // อัปเดตข้อมูลในอาร์เรย์ if (this.receiptData.items[index]) { this.receiptData.items[index].quantity = quantity; this.receiptData.items[index].price = price; this.receiptData.items[index].total = total; } subtotal += total; } }); // คำนวณภาษี const vatSelector = document.getElementById('vatRate'); const vatRate = vatSelector ? (parseFloat(vatSelector.value) / 100) : 0.07; const vat = subtotal * vatRate; const total = subtotal + vat; // อัปเดตข้อมูลยอดรวม this.receiptData.totals.subtotal = subtotal; this.receiptData.totals.vat = vat; this.receiptData.totals.total = total; // อัปเดตการแสดงผลยอดรวม this.updateTotalDisplay(); this.saveToHistory(); this.showNotification('✅ คำนวณยอดรวมแล้ว', 'success'); } updateTotalDisplay() { const subTotal = document.getElementById('subTotal'); const vatTotal = document.getElementById('vatTotal'); const grandTotal = document.getElementById('grandTotal'); if (subTotal) { subTotal.textContent = this.receiptData.totals.subtotal.toLocaleString(); } if (vatTotal) { vatTotal.textContent = this.receiptData.totals.vat.toLocaleString(); } if (grandTotal) { grandTotal.textContent = this.receiptData.totals.total.toLocaleString(); } // อัปเดตข้อความภาษีให้ตรงกับอัตราที่เลือก this.updateVatLabel(); } // อัปเดต makeElementsEditable เพื่อจัดการการเปลี่ยนแปลงตัวเลข makeElementsEditable() { const editableElements = this.receiptPage.querySelectorAll('.editable'); editableElements.forEach(element => { // ลบ event listeners เก่า element.removeEventListener('click', this.selectElementHandler); element.removeEventListener('input', this.inputHandler); element.removeEventListener('blur', this.blurHandler); // เพิ่ม event listeners ใหม่ this.selectElementHandler = (e) => { e.stopPropagation(); this.selectElement(element); }; this.inputHandler = () => { // ถ้าเป็นฟิลด์ตัวเลขในตาราง ให้คำนวณใหม่ if (element.dataset.type === 'number' || element.classList.contains('item-quantity') || element.classList.contains('item-price')) { setTimeout(() => this.calculateTotals(), 300); } this.saveToHistory(); }; this.blurHandler = () => { this.saveToHistory(); }; element.addEventListener('click', this.selectElementHandler); element.addEventListener('input', this.inputHandler); element.addEventListener('blur', this.blurHandler); }); } // Template Methods getModernTemplate() { return `

${this.receiptData.company.name}

${this.receiptData.company.address}

โทร: ${this.receiptData.company.phone}

อีเมล: ${this.receiptData.company.email}

เลขประจำตัวผู้เสียภาษี: ${this.receiptData.company.taxId}

ใบเสร็จรับเงิน

เลขที่: ${this.receiptData.receipt.number}
วันที่: ${this.receiptData.receipt.date}
ครบกำหนด: ${this.receiptData.receipt.dueDate}

ลูกค้า

${this.receiptData.customer.name}

${this.receiptData.customer.address}

โทร: ${this.receiptData.customer.phone}

${this.receiptData.items.map((item, index) => ` `).join('')}
รายการ จำนวน ราคาต่อหน่วย รวม จัดการ
${item.description} ${item.quantity} ${item.price.toLocaleString()} ${item.total.toLocaleString()}
ยอดรวม: ${this.receiptData.totals.subtotal.toLocaleString()}
ภาษีมูลค่าเพิ่ม: ${this.receiptData.totals.vat.toLocaleString()}
รวมทั้งสิ้น: ${this.receiptData.totals.total.toLocaleString()}
`; } getClassicTemplate() { return `

${this.receiptData.company.name}

${this.receiptData.company.address}

โทร: ${this.receiptData.company.phone} | อีเมล: ${this.receiptData.company.email}

เลขประจำตัวผู้เสียภาษี: ${this.receiptData.company.taxId}

ใบเสร็จรับเงิน

เลขที่: ${this.receiptData.receipt.number}

วันที่: ${this.receiptData.receipt.date}

ครบกำหนด: ${this.receiptData.receipt.dueDate}

รายละเอียดลูกค้า

ชื่อ: ${this.receiptData.customer.name}

ที่อยู่: ${this.receiptData.customer.address}

โทรศัพท์: ${this.receiptData.customer.phone}

${this.receiptData.items.map((item, index) => ` `).join('')}
รายการสินค้า/บริการ จำนวน ราคาต่อหน่วย (บาท) จำนวนเงิน (บาท) จัดการ
${item.description} ${item.quantity} ${item.price.toLocaleString()} ${item.total.toLocaleString()}
รวมเป็นเงิน: ${this.receiptData.totals.subtotal.toLocaleString()} บาท
ภาษีมูลค่าเพิ่ม: ${this.receiptData.totals.vat.toLocaleString()} บาท
รวมทั้งสิ้น: ${this.receiptData.totals.total.toLocaleString()} บาท

ขอบพระคุณที่ใช้บริการ

กรุณาเก็บใบเสร็จนี้ไว้เป็นหลักฐานการชำระเงิน

`; } getMinimalTemplate() { return `

${this.receiptData.company.name}

${this.receiptData.company.email}

RECEIPT

#${this.receiptData.receipt.number}

จาก

${this.receiptData.company.name}

${this.receiptData.company.address}

ถึง

${this.receiptData.customer.name}

${this.receiptData.customer.address}

${this.receiptData.items.map((item, index) => ` `).join('')}
รายการ จำนวน ราคา รวม จัดการ
${item.description} ${item.quantity} ${item.price.toLocaleString()} ${item.total.toLocaleString()}
ยอดรวม: ${this.receiptData.totals.subtotal.toLocaleString()}
ภาษี: ${this.receiptData.totals.vat.toLocaleString()}
รวมทั้งสิ้น: ${this.receiptData.totals.total.toLocaleString()}

วันที่: ${this.receiptData.receipt.date}

ขอบคุณสำหรับการใช้บริการ

`; } // Initialize default mode to preview initializeDefaultMode() { // Start in preview mode this.disableWYSIWYGMode(); } // Enhanced sync function for all editable elements syncAllElementsToReceiptData() { const editableElements = this.receiptPage.querySelectorAll('.editable'); editableElements.forEach(element => { this.syncElementToReceiptData(element); }); } // Update display from receiptData (reverse sync) updateDisplayFromReceiptData() { this.populateTemplateData(); } // Security functions for XSS prevention sanitizeHTML(str) { if (typeof str !== 'string') return ''; // Create a temporary div to safely extract text content const temp = document.createElement('div'); temp.textContent = str; return temp.innerHTML; } sanitizeInput(input) { if (typeof input !== 'string') return ''; // Remove script tags and dangerous attributes return input .replace(/)<[^<]*)*<\/script>/gi, '') .replace(/javascript:/gi, '') .replace(/on\w+\s*=/gi, '') .replace(/data:text\/html/gi, '') .trim(); } validateNumber(value) { const num = parseFloat(value); return isNaN(num) ? 0 : Math.max(0, num); } validateString(value, maxLength = 1000) { if (typeof value !== 'string') return ''; return this.sanitizeInput(value).substring(0, maxLength); } // Enhanced input validation and CSP protection validateReceiptData() { // Validate company data this.receiptData.company.name = this.validateString(this.receiptData.company.name, 200); this.receiptData.company.address = this.validateString(this.receiptData.company.address, 500); this.receiptData.company.phone = this.validateString(this.receiptData.company.phone, 50); this.receiptData.company.email = this.validateString(this.receiptData.company.email, 100); this.receiptData.company.taxId = this.validateString(this.receiptData.company.taxId, 50); // Validate customer data this.receiptData.customer.name = this.validateString(this.receiptData.customer.name, 200); this.receiptData.customer.address = this.validateString(this.receiptData.customer.address, 500); this.receiptData.customer.phone = this.validateString(this.receiptData.customer.phone, 50); // Validate receipt data this.receiptData.receipt.number = this.validateString(this.receiptData.receipt.number, 50); this.receiptData.receipt.date = this.validateString(this.receiptData.receipt.date, 50); this.receiptData.receipt.dueDate = this.validateString(this.receiptData.receipt.dueDate, 50); // Validate items this.receiptData.items = this.receiptData.items.filter(item => { item.description = this.validateString(item.description, 200); item.quantity = this.validateNumber(item.quantity); item.price = this.validateNumber(item.price); item.total = this.validateNumber(item.total); // Remove empty items return item.description.length > 0; }); // Validate totals this.receiptData.totals.subtotal = this.validateNumber(this.receiptData.totals.subtotal); this.receiptData.totals.vat = this.validateNumber(this.receiptData.totals.vat); this.receiptData.totals.total = this.validateNumber(this.receiptData.totals.total); } // Content Security Policy helpers addCSPProtection() { // Prevent inline script execution const meta = document.createElement('meta'); meta.httpEquiv = 'Content-Security-Policy'; meta.content = "default-src 'self'; script-src 'self' 'unsafe-eval' https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdnjs.cloudflare.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self';"; document.head.appendChild(meta); } // Company Data Management loadCompanyData() { try { const savedData = localStorage.getItem('receiptCompanyData'); return savedData ? JSON.parse(savedData) : null; } catch (error) { console.error('Error loading company data:', error); return null; } } saveCompanyData() { try { localStorage.setItem('receiptCompanyData', JSON.stringify(this.receiptData.company)); } catch (error) { console.error('Error saving company data:', error); } } resetCompanyData() { if (confirm('คุณต้องการรีเซ็ตข้อมูลบริษัทเป็นค่าเริ่มต้นหรือไม่?')) { localStorage.removeItem('receiptCompanyData'); this.receiptData.company = { name: 'บริษัท ตัวอย่าง จำกัด', address: '123 ถนนสุขุมวิท แขวงคลองตัน เขตวัฒนา กรุงเทพฯ 10110', phone: '02-123-4567', email: 'info@company.com', taxId: '0123456789012' }; // อัปเดตหน้าจอใหม่ if (this.currentTemplate) { this.selectTemplate(this.currentTemplate); } this.showNotification('✅ รีเซ็ตข้อมูลบริษัทแล้ว', 'success'); } } updateVatLabel() { const vatSelector = document.getElementById('vatRate'); const vatRate = vatSelector ? parseFloat(vatSelector.value) : 7; const vatLabels = this.receiptPage.querySelectorAll('#vatLabel'); let labelText; if (vatRate === 0) { labelText = 'ภาษี:'; } else { labelText = `ภาษีมูลค่าเพิ่ม ${vatRate}%:`; } vatLabels.forEach(label => { label.textContent = labelText; }); } } // Initialize the application when DOM is loaded let receiptDesigner; document.addEventListener('DOMContentLoaded', () => { receiptDesigner = new ReceiptDesigner(); });