// POS System logic class POS { constructor() { this.dbHelper = dbHelper; this.cart = null; // will be created after DB is initialized this.products = []; this.categories = []; // will be loaded from categories.json this.currentCategory = 'all'; this.searchTerm = ''; this.paymentMethod = 'cash'; this.deliveryMethod = 'instore'; this.shippingCost = 0; // will be loaded from settings // Initialize modals this.receiptModal = new Modal('receiptModal'); // Initialize event listeners this.initEventListeners(); } // Prepare PromptPay QR image based on settings and current cart total async preparePromptpayQR() { try { const promptpayEl = document.getElementById('promptpayPayment'); if (!promptpayEl) return; const shopPhoneEntry = await this.dbHelper.getById('settings', 'shopPhone'); const shopPhone = shopPhoneEntry && shopPhoneEntry.value ? String(shopPhoneEntry.value) : null; let idForPromptpay = ''; if (shopPhone) { let digits = shopPhone.replace(/\D/g, ''); if (digits.length === 10 && digits.startsWith('0')) { digits = '66' + digits.slice(1); } idForPromptpay = digits; } const shipping = (this.deliveryMethod === 'instore' || this.deliveryMethod === 'pickup') ? 0 : this.shippingCost; const amount = this.cart.getTotal(shipping).toFixed(2); const img = promptpayEl.querySelector('img'); const infoP = promptpayEl.querySelector('p'); function isValidPromptpayId(id) { if (!id) return false; if (!/^\d+$/.test(id)) return false; if (id.length === 11 && id.startsWith('66')) return true; if (id.length === 13) return true; return false; } if (img && idForPromptpay) { const candidates = []; if (isValidPromptpayId(idForPromptpay)) candidates.push(idForPromptpay); const raw = shopPhone ? shopPhone.replace(/\D/g, '') : ''; if (raw && !candidates.includes(raw)) candidates.push(raw); if (raw && raw.length === 10 && raw.startsWith('0')) { const normalized = '66' + raw.slice(1); if (!candidates.includes(normalized)) candidates.unshift(normalized); } let attempt = 0; const tryNext = () => { if (attempt >= candidates.length) { img.removeAttribute('src'); if (infoP) infoP.textContent = 'ไม่สามารถโหลด PromptPay QR ได้ — โปรดตรวจสอบค่า PromptPay ID ใน settings (shopPhone)'; return; } const id = candidates[attempt]; const url = `https://promptpay.io/${id}/${encodeURIComponent(amount)}.png`; img.onload = () => { img.alt = `PromptPay QR - ฿${amount}`; if (infoP) infoP.textContent = `สแกน QR Code เพื่อชำระเงิน ฿${amount}`; }; img.onerror = () => { attempt += 1; tryNext(); }; img.src = url; }; tryNext(); } else { if (img) img.removeAttribute('src'); if (infoP) infoP.textContent = 'PromptPay ID ไม่ถูกต้องหรือยังไม่ตั้งค่าในระบบ (settings.shopPhone)'; } } catch (err) { console.error('Error preparing POS PromptPay QR:', err); } } async init() { try { // Initialize database await this.dbHelper.init(); // Initialize with mock data if needed await this.dbHelper.initializeWithMockData(); // Load categories await this.loadCategories(); // Load products await this.loadProducts(); // Create cart now that DB is available this.cart = new Cart(); // Update cart UI this.cart.updateCartUI(); console.log('POS initialized successfully'); } catch (error) { console.error('Error initializing POS:', error); } } initEventListeners() { // Clear cart button const clearCartBtn = document.getElementById('clearCartBtn'); if (clearCartBtn) { clearCartBtn.addEventListener('click', async () => { if (confirm('คุณต้องการล้างตะกร้าสินค้าใช่หรือไม่?')) { await this.cart.clearCart(); this.updatePOSCartUI(); } }); } // Category filter const categoryItems = document.querySelectorAll('.pos-category'); categoryItems.forEach(item => { item.addEventListener('click', () => { // Remove active class from all items categoryItems.forEach(i => i.classList.remove('active')); // Add active class to clicked item item.classList.add('active'); // Filter products this.currentCategory = item.getAttribute('data-category'); this.filterProducts(); }); }); // Search functionality const searchInput = document.getElementById('posSearchInput'); if (searchInput) { searchInput.addEventListener('input', (e) => { this.searchTerm = e.target.value.toLowerCase(); this.filterProducts(); }); } // Payment method selection const paymentMethods = document.querySelectorAll('.payment-method'); paymentMethods.forEach(method => { method.addEventListener('click', async () => { // Remove active class from all methods paymentMethods.forEach(m => m.classList.remove('active')); // Add active class to clicked method method.classList.add('active'); // Update payment method this.paymentMethod = method.getAttribute('data-method'); // Show/hide payment details document.getElementById('cashPayment').classList.add('hidden'); document.getElementById('promptpayPayment').classList.add('hidden'); document.getElementById('cardPayment').classList.add('hidden'); if (this.paymentMethod === 'cash') { document.getElementById('cashPayment').classList.remove('hidden'); } else if (this.paymentMethod === 'promptpay') { // prepare promptpay QR (extracted helper) await this.preparePromptpayQR(); document.getElementById('promptpayPayment').classList.remove('hidden'); } else if (this.paymentMethod === 'card') { document.getElementById('cardPayment').classList.remove('hidden'); } }); }); // Delivery method selection const deliveryMethods = document.querySelectorAll('.delivery-method'); deliveryMethods.forEach(dm => { dm.addEventListener('click', async () => { deliveryMethods.forEach(d => d.classList.remove('active')); dm.classList.add('active'); this.deliveryMethod = dm.getAttribute('data-method'); // Load shipping cost from settings try { const shippingEntry = await this.dbHelper.getById('settings', 'shippingCost'); this.shippingCost = shippingEntry && typeof shippingEntry.value !== 'undefined' ? Number(shippingEntry.value) : 0; } catch (err) { console.error('Error reading shipping cost:', err); this.shippingCost = 0; } // Show/hide customer details and address only when delivery or pickup const customerDetailsEl = document.getElementById('customerDetails'); const addressGroup = document.getElementById('customerAddressGroup'); if (this.deliveryMethod === 'delivery') { if (customerDetailsEl) customerDetailsEl.classList.remove('hidden'); if (addressGroup) addressGroup.classList.remove('hidden'); } else if (this.deliveryMethod === 'pickup') { if (customerDetailsEl) customerDetailsEl.classList.remove('hidden'); if (addressGroup) addressGroup.classList.add('hidden'); } else { if (customerDetailsEl) customerDetailsEl.classList.add('hidden'); if (addressGroup) addressGroup.classList.add('hidden'); } // Refresh summary to include shipping when applicable this.updatePOSCartSummary(); // If using PromptPay, refresh QR because amount changed if (this.paymentMethod === 'promptpay') { await this.preparePromptpayQR(); } }); }); // Cash received input const cashReceivedInput = document.getElementById('cashReceived'); if (cashReceivedInput) { cashReceivedInput.addEventListener('input', () => { this.calculateChange(); }); } // Process payment button const processPaymentBtn = document.getElementById('processPaymentBtn'); if (processPaymentBtn) { processPaymentBtn.addEventListener('click', () => { this.processPayment(); }); } // Listen for cart updates so POS UI and PromptPay QR refresh in real-time window.addEventListener('cart:updated', async () => { try { this.updatePOSCartUI(); // if current payment is PromptPay, refresh QR if (this.paymentMethod === 'promptpay') { await this.preparePromptpayQR(); } } catch (err) { console.error('Error handling cart:updated in POS:', err); } }); // Print receipt button const printReceiptBtn = document.getElementById('printReceiptBtn'); if (printReceiptBtn) { printReceiptBtn.addEventListener('click', () => { this.printReceipt(); }); } // New transaction button const newTransactionBtn = document.getElementById('newTransactionBtn'); if (newTransactionBtn) { newTransactionBtn.addEventListener('click', () => { this.newTransaction(); }); } } async loadCategories() { try { const cats = await this.dbHelper.getAll('categories'); if (cats && cats.length > 0) { this.categories = cats; } else { try { const response = await fetch('mock/categories.json'); if (response.ok) this.categories = await response.json(); else this.categories = []; } catch (err) { console.warn('Could not fetch mock categories', err); this.categories = []; } } if (!this.categories || this.categories.length === 0) this.categories = [{id: 'all', name: 'ทั้งหมด', icon: 'bi-grid-3x3-gap'}]; this.renderCategories(); } catch (error) { console.error('Error loading categories:', error); this.categories = [{id: 'all', name: 'ทั้งหมด', icon: 'bi-grid-3x3-gap'}]; this.renderCategories(); } } renderCategories() { const categoryList = document.querySelector('.pos-categories'); if (categoryList && this.categories.length > 0) { // Prepend "ทั้งหมด" category dynamically const allCategory = `
ทั้งหมด
`; const categoryHTML = this.categories.map((cat) => `
${cat.name}
`).join(''); categoryList.innerHTML = allCategory + categoryHTML; // Re-attach event listeners for new category items const categoryItems = document.querySelectorAll('.pos-category'); categoryItems.forEach(item => { item.addEventListener('click', () => { // Remove active class from all items categoryItems.forEach(i => i.classList.remove('active')); // Add active class to clicked item item.classList.add('active'); // Filter products this.currentCategory = item.getAttribute('data-category'); this.filterProducts(); }); }); } } async loadProducts() { try { const productsData = await this.dbHelper.getAll('products'); this.products = productsData.map(product => new Product(product)); this.renderProducts(); } catch (error) { console.error('Error loading products:', error); } } renderProducts() { const productGrid = document.getElementById('posProductGrid'); if (productGrid) { productGrid.innerHTML = this.products.map(product => product.renderPOSCard()).join(''); // Add event listeners to product cards this.attachProductEventListeners(); } } filterProducts() { let filteredProducts = this.products; // Filter by category if (this.currentCategory !== 'all') { filteredProducts = filteredProducts.filter(product => product.category === this.currentCategory); } // Filter by search term if (this.searchTerm) { filteredProducts = filteredProducts.filter(product => product.name.toLowerCase().includes(this.searchTerm) || product.description.toLowerCase().includes(this.searchTerm) ); } // Render filtered products const productGrid = document.getElementById('posProductGrid'); if (productGrid) { if (filteredProducts.length === 0) { productGrid.innerHTML = '

ไม่พบสินค้าที่ค้นหา

'; } else { productGrid.innerHTML = filteredProducts.map(product => product.renderPOSCard()).join(''); // Add event listeners to product cards this.attachProductEventListeners(); } } } attachProductEventListeners() { // Product card click document.querySelectorAll('.pos-product-card').forEach(card => { card.addEventListener('click', async (e) => { const productId = card.getAttribute('data-id'); // Find product const product = this.products.find(p => p.id === productId); if (product) { // Add to cart with quantity 1 const success = await this.cart.addItem(product, 1); if (success) { toast.show('สินค้าถูกเพิ่มในตะกร้าแล้ว'); this.updatePOSCartUI(); } else { toast.show('เกิดข้อผิดพลาดในการเพิ่มสินค้า', 'error'); } } }); }); } updatePOSCartUI() { // Update cart items const cartItemsContainer = document.getElementById('posCartItems'); if (cartItemsContainer) { if (this.cart.items.length === 0) { cartItemsContainer.innerHTML = '

ตะกร้าสินค้าว่าง

'; } else { cartItemsContainer.innerHTML = this.cart.items.map(item => `
${item.name}
${item.name}
฿${item.price}
`).join(''); // Add event listeners to cart item buttons this.attachPOSCartItemEventListeners(); } } // Update cart summary this.updatePOSCartSummary(); } updatePOSCartSummary() { const subtotalElement = document.getElementById('posSubtotal'); const discountElement = document.getElementById('posDiscount'); const totalElement = document.getElementById('posTotal'); if (subtotalElement) { subtotalElement.textContent = `฿${this.cart.getSubtotal()}`; } if (discountElement) { discountElement.textContent = `฿0`; } if (totalElement) { const shipping = (this.deliveryMethod === 'instore' || this.deliveryMethod === 'pickup') ? 0 : this.shippingCost; totalElement.textContent = `฿${this.cart.getTotal(shipping)}`; } } attachPOSCartItemEventListeners() { // Decrease quantity buttons document.querySelectorAll('.pos-cart-item .decrease-quantity').forEach(button => { button.addEventListener('click', async (e) => { const btn = e.currentTarget; const productId = btn.getAttribute('data-id') || btn.dataset.id; const input = document.querySelector(`.pos-cart-item .quantity-input[data-id="${productId}"]`); if (!input) return; const newQuantity = Math.max(1, parseInt(input.value) - 1); await this.cart.updateItemQuantity(productId, newQuantity); input.value = newQuantity; this.updatePOSCartUI(); }); }); // Increase quantity buttons document.querySelectorAll('.pos-cart-item .increase-quantity').forEach(button => { button.addEventListener('click', async (e) => { const btn = e.currentTarget; const productId = btn.getAttribute('data-id') || btn.dataset.id; const input = document.querySelector(`.pos-cart-item .quantity-input[data-id="${productId}"]`); if (!input) return; const newQuantity = parseInt(input.value) + 1; await this.cart.updateItemQuantity(productId, newQuantity); input.value = newQuantity; this.updatePOSCartUI(); }); }); // Quantity input fields document.querySelectorAll('.pos-cart-item .quantity-input').forEach(input => { input.addEventListener('change', async (e) => { const productId = e.target.getAttribute('data-id'); const newQuantity = Math.max(1, parseInt(e.target.value) || 1); await this.cart.updateItemQuantity(productId, newQuantity); this.updatePOSCartUI(); }); }); // Remove item buttons document.querySelectorAll('.pos-cart-item-remove').forEach(button => { button.addEventListener('click', async (e) => { const removeEl = e.target.closest('.pos-cart-item-remove'); const productId = removeEl ? (removeEl.getAttribute('data-id') || removeEl.dataset.id) : null; if (!productId) return; await this.cart.removeItem(productId); this.updatePOSCartUI(); }); }); } calculateChange() { const cashReceived = parseFloat(document.getElementById('cashReceived').value) || 0; const shipping = (this.deliveryMethod === 'instore' || this.deliveryMethod === 'pickup') ? 0 : this.shippingCost; const total = this.cart.getTotal(shipping); const change = cashReceived - total; document.getElementById('cashChange').value = change >= 0 ? change.toFixed(2) : '0.00'; } async processPayment() { try { if (this.cart.items.length === 0) { toast.show('ตะกร้าสินค้าว่าง', 'warning'); return; } // Validate payment if (this.paymentMethod === 'cash') { const cashReceived = parseFloat(document.getElementById('cashReceived').value) || 0; const shipping = (this.deliveryMethod === 'instore' || this.deliveryMethod === 'pickup') ? 0 : this.shippingCost; const total = this.cart.getTotal(shipping); if (cashReceived < total) { toast.show('จำนวนเงินที่รับมาไม่เพียงพอ', 'warning'); return; } } // Create order object const customerName = document.getElementById('customerName').value.trim() || 'ลูกค้าหน้าร้าน'; const customerPhone = document.getElementById('customerPhone').value.trim() || '-'; const customerAddress = this.deliveryMethod === 'delivery' ? document.getElementById('customerAddress').value.trim() : '-'; const shipping = (this.deliveryMethod === 'instore' || this.deliveryMethod === 'pickup') ? 0 : this.shippingCost; const order = { customerName: customerName, customerPhone: customerPhone, customerEmail: '-', customerAddress: customerAddress, deliveryMethod: this.deliveryMethod, paymentMethod: this.paymentMethod, items: this.cart.items.map(item => ({ id: item.id, name: item.name, price: item.price, quantity: item.quantity })), subtotal: this.cart.getSubtotal(), shipping: shipping, total: this.cart.getTotal(shipping), status: 'completed', date: new Date().toISOString(), type: 'pos' }; // Ensure order has an id (orders objectStore uses keyPath 'id') if (!order.id) { // Simple unique id: ORD-- order.id = `ORD-${Date.now()}-${Math.floor(Math.random() * 1000)}`; } // Save order to IndexedDB await this.dbHelper.add('orders', order); // Save to sales data await this.dbHelper.add('sales', { date: new Date().toISOString().split('T')[0], // YYYY-MM-DD format amount: order.total, type: 'pos' }); // Show receipt this.showReceipt(order); // Clear cart await this.cart.clearCart(); this.updatePOSCartUI(); console.log('POS transaction completed successfully:', order); } catch (error) { console.error('Error processing payment:', error); toast.show('เกิดข้อผิดพลาดในการชำระเงิน', 'error'); } } showReceipt(order) { // Generate receipt HTML const receiptHTML = `

ร้านขนมไทย

123 ถนนขนมไทย แขวงขนมหวาน เขตหวานกรอบ กรุงเทพฯ 10100

โทร: 02-123-4567

เลขที่: #${order.id}

วันที่: ${new Date(order.date).toLocaleDateString('th-TH')}

เวลา: ${new Date(order.date).toLocaleTimeString('th-TH')}

พนักงาน: แอดมิน

ชื่อผู้รับ: ${order.customerName}

เบอร์โทร: ${order.customerPhone}

${order.customerAddress !== '-' ? `

ที่อยู่: ${order.customerAddress}

` : ''}
${order.items.map(item => `
${item.name} x ${item.quantity} ฿${item.price * item.quantity}
`).join('')}
รวม: ฿${order.subtotal}
ค่าจัดส่ง: ฿${order.shipping}
ส่วนลด: ฿0
ทั้งหมด: ฿${order.total}
${order.paymentMethod === 'cash' ? `
รับเงิน: ฿${document.getElementById('cashReceived').value}
เงินทอน: ฿${document.getElementById('cashChange').value}
` : ''}
`; // Update receipt modal document.getElementById('receiptContent').innerHTML = receiptHTML; // Open receipt modal this.receiptModal.open(); } printReceipt() { // Print only the receipt content by opening a new window with the receipt HTML. try { const receiptEl = document.getElementById('receiptContent'); if (!receiptEl) { window.print(); return; } const receiptHTML = receiptEl.innerHTML; const printWindow = window.open('', '_blank', 'toolbar=0,location=0,menubar=0,width=800,height=900'); if (!printWindow) { // Popup blocked — fallback to window.print() window.print(); return; } // Use a minimal inline stylesheet to print only the receipt content const inlineStyles = ` body { font-family: 'Mitr', sans-serif; color: #333; padding: 20px; } .receipt { max-width: 700px; margin: 0 auto; background: #fff; } .receipt-header { text-align: center; margin-bottom: 12px; } .receipt-header h2 { color: #8b6f47; margin: 0 0 6px 0; } .receipt-info p, .receipt-summary-row, .receipt-item { font-size: 14px; margin: 4px 0; } .receipt-items { margin: 12px 0; } .receipt-item { display:flex; justify-content:space-between; } .receipt-summary-row { display:flex; justify-content:space-between; } .receipt-summary-row.total { font-weight:700; margin-top:8px; } @media print { body { -webkit-print-color-adjust: exact; } } `; printWindow.document.open(); printWindow.document.write(`ใบเสร็จ${receiptHTML}`); printWindow.document.close(); printWindow.focus(); // Give the new window some time to load styles/images before printing setTimeout(() => { try { printWindow.print(); } catch (e) { console.warn('Print failed in popup, falling back to main window print', e); window.print(); } // Close the print window after printing printWindow.close(); }, 500); } catch (err) { console.error('Error printing receipt:', err); window.print(); } } newTransaction() { // Close receipt modal this.receiptModal.close(); // Reset payment method document.querySelectorAll('.payment-method').forEach(m => m.classList.remove('active')); document.querySelector('.payment-method[data-method="cash"]').classList.add('active'); this.paymentMethod = 'cash'; // Reset delivery method document.querySelectorAll('.delivery-method').forEach(d => d.classList.remove('active')); document.querySelector('.delivery-method[data-method="instore"]').classList.add('active'); this.deliveryMethod = 'instore'; // Hide customer details and address document.getElementById('customerDetails').classList.add('hidden'); document.getElementById('customerAddressGroup').classList.add('hidden'); // Reset customer fields document.getElementById('customerName').value = ''; document.getElementById('customerPhone').value = ''; document.getElementById('customerAddress').value = ''; // Show cash payment details document.getElementById('cashPayment').classList.remove('hidden'); document.getElementById('promptpayPayment').classList.add('hidden'); document.getElementById('cardPayment').classList.add('hidden'); // Reset cash inputs document.getElementById('cashReceived').value = ''; document.getElementById('cashChange').value = '0.00'; // Update summary this.updatePOSCartSummary(); } } // Initialize POS when DOM is loaded document.addEventListener('DOMContentLoaded', () => { const pos = new POS(); pos.init(); });