// 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.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 = `
เลขที่: #${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();
});