// Main application logic class App { constructor() { this.dbHelper = dbHelper; this.cart = null; // will initialize after DB is ready this.products = []; this.categories = []; // will be loaded from categories.json this.currentCategory = 'all'; this.searchTerm = ''; // Initialize modals this.cartModal = new Modal('cartModal'); this.checkoutModal = new Modal('checkoutModal'); this.orderConfirmationModal = new Modal('orderConfirmationModal'); // Initialize event listeners this.initEventListeners(); } async init() { try { // Initialize database await this.dbHelper.init(); // Initialize with mock data if needed await this.dbHelper.initializeWithMockData(); // Create cart now that DB is available this.cart = new Cart(); // Load categories await this.loadCategories(); // Load products await this.loadProducts(); // Load best sellers await this.loadBestSellers(); // Update cart UI this.cart.updateCartUI(); console.log('App initialized successfully'); } catch (error) { console.error('Error initializing app:', error); } } initEventListeners() { // Cart icon click const cartIcon = document.querySelector('.cart-icon'); if (cartIcon) { cartIcon.addEventListener('click', () => { this.cartModal.open(); }); } // Continue shopping button const continueShoppingBtn = document.getElementById('continueShopping'); if (continueShoppingBtn) { continueShoppingBtn.addEventListener('click', () => { this.cartModal.close(); }); } // Checkout button const checkoutBtn = document.getElementById('checkoutBtn'); if (checkoutBtn) { checkoutBtn.addEventListener('click', async () => { if (this.cart.items.length === 0) { toast.show('ตะกร้าสินค้าว่าง', 'warning'); return; } this.cartModal.close(); this.checkoutModal.open(); this.updateCheckoutSummary(); await this.updatePaymentDetails(); }); } // Back to cart button const backToCartBtn = document.getElementById('backToCart'); if (backToCartBtn) { backToCartBtn.addEventListener('click', () => { this.checkoutModal.close(); this.cartModal.open(); }); } // Place order button const placeOrderBtn = document.getElementById('placeOrderBtn'); if (placeOrderBtn) { placeOrderBtn.addEventListener('click', () => { this.placeOrder(); }); } // Continue shopping button in order confirmation const continueShoppingBtn2 = document.getElementById('continueShoppingBtn'); if (continueShoppingBtn2) { continueShoppingBtn2.addEventListener('click', () => { this.orderConfirmationModal.close(); this.cart.clearCart(); }); } // Delivery method change const deliveryMethods = document.querySelectorAll('input[name="deliveryMethod"]'); deliveryMethods.forEach(method => { method.addEventListener('change', () => { this.updateCheckoutSummary(); this.updatePaymentDetails(); // show/hide address fields based on selection const delivery = document.querySelector('input[name="deliveryMethod"]:checked').value; const addressSection = document.getElementById('addressSection'); if (addressSection) { if (delivery === 'pickup') { addressSection.classList.add('hidden'); } else { addressSection.classList.remove('hidden'); } } }); }); // Payment method change const paymentMethods = document.querySelectorAll('input[name="paymentMethod"]'); paymentMethods.forEach(method => { method.addEventListener('change', () => { this.updatePaymentDetails(); }); }); // Listen for cart updates to refresh checkout summary and payment details (real-time QR update) window.addEventListener('cart:updated', async () => { this.updateCheckoutSummary(); await this.updatePaymentDetails(); }); // Category filter const categoryItems = document.querySelectorAll('.category-item'); 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('searchInput'); if (searchInput) { searchInput.addEventListener('input', (e) => { this.searchTerm = e.target.value.toLowerCase(); this.filterProducts(); }); } // Contact form const contactForm = document.getElementById('contactForm'); if (contactForm) { contactForm.addEventListener('submit', (e) => { e.preventDefault(); this.handleContactForm(); }); } // Mobile menu toggle const mobileMenuToggle = document.querySelector('.mobile-menu-toggle'); if (mobileMenuToggle) { mobileMenuToggle.addEventListener('click', () => { const mainNav = document.querySelector('.main-nav'); mainNav.classList.toggle('active'); }); } } async loadCategories() { try { // Prefer categories from IndexedDB (keeps admin edits persistent) const cats = await this.dbHelper.getAll('categories'); if (cats && cats.length > 0) { this.categories = cats; } else { // fallback to mock file 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 still empty, show only default 'ทั้งหมด' 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('.category-list'); 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('.category-item'); 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); } } async loadBestSellers() { try { // Get some products to display as best sellers const productsData = await this.dbHelper.getAll('products'); // Take first 4 products as best sellers (in a real app, this would be based on sales data) const bestSellers = productsData.slice(0, 4).map(product => new Product(product)); const bestSellerGrid = document.getElementById('bestSellerGrid'); if (bestSellerGrid) { bestSellerGrid.innerHTML = bestSellers.map(product => `
${product.name}
${product.name}
฿${product.price}
`).join(''); } } catch (error) { console.error('Error loading best sellers:', error); } } renderProducts() { const productGrid = document.getElementById('productGrid'); if (productGrid) { productGrid.innerHTML = this.products.map(product => product.renderCard()).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('productGrid'); if (productGrid) { if (filteredProducts.length === 0) { productGrid.innerHTML = '

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

'; } else { productGrid.innerHTML = filteredProducts.map(product => product.renderCard()).join(''); // Add event listeners to product cards this.attachProductEventListeners(); } } } attachProductEventListeners() { // Decrease quantity buttons document.querySelectorAll('.product-card .decrease-quantity').forEach(button => { button.addEventListener('click', (e) => { const input = e.target.nextElementSibling; const currentValue = parseInt(input.value); if (currentValue > 1) { input.value = currentValue - 1; } }); }); // Increase quantity buttons document.querySelectorAll('.product-card .increase-quantity').forEach(button => { button.addEventListener('click', (e) => { const input = e.target.previousElementSibling; const currentValue = parseInt(input.value); input.value = currentValue + 1; }); }); // Add to cart buttons document.querySelectorAll('.add-to-cart').forEach(button => { button.addEventListener('click', async (e) => { const productId = e.target.getAttribute('data-id'); const productCard = e.target.closest('.product-card'); const quantityInput = productCard.querySelector('.quantity-input'); const quantity = parseInt(quantityInput.value); // Find product const product = this.products.find(p => p.id === productId); if (product) { // Add to cart const success = await this.cart.addItem(product, quantity); if (success) { toast.show('สินค้าถูกเพิ่มในตะกร้าแล้ว'); // Reset quantity input quantityInput.value = 1; } else { toast.show('เกิดข้อผิดพลาดในการเพิ่มสินค้า', 'error'); } } }); }); } updateCheckoutSummary() { const deliveryMethod = document.querySelector('input[name="deliveryMethod"]:checked'); const shippingCost = deliveryMethod && deliveryMethod.value === 'pickup' ? 0 : 50; const subtotalElement = document.getElementById('checkoutSubtotal'); const shippingElement = document.getElementById('checkoutShipping'); const totalElement = document.getElementById('checkoutTotal'); if (subtotalElement) { subtotalElement.textContent = `฿${this.cart.getSubtotal()}`; } if (shippingElement) { shippingElement.textContent = `฿${shippingCost}`; } if (totalElement) { totalElement.textContent = `฿${this.cart.getTotal(shippingCost)}`; } } async updatePaymentDetails() { const paymentMethod = document.querySelector('input[name="paymentMethod"]:checked').value; // Hide all payment details const promptpayDetails = document.getElementById('promptpayDetails'); const bankDetails = document.getElementById('bankDetails'); if (promptpayDetails) promptpayDetails.classList.add('hidden'); if (bankDetails) bankDetails.classList.add('hidden'); // Show selected payment details if (paymentMethod === 'promptpay') { if (promptpayDetails) { // populate QR dynamically using shop phone and amount try { const shopPhoneEntry = await this.dbHelper.getById('settings', 'shopPhone'); const shopPhone = shopPhoneEntry && shopPhoneEntry.value ? String(shopPhoneEntry.value) : null; let idForPromptpay = ''; if (shopPhone) { // normalize phone: digits only, convert leading 0 to 66 let digits = shopPhone.replace(/\D/g, ''); if (digits.length === 10 && digits.startsWith('0')) { digits = '66' + digits.slice(1); } idForPromptpay = digits; } // compute amount including delivery method const deliveryMethod = document.querySelector('input[name="deliveryMethod"]:checked'); const shippingCost = deliveryMethod && deliveryMethod.value === 'pickup' ? 0 : 50; const amount = this.cart.getTotal(shippingCost).toFixed(2); // set QR image src (using promptpay.io service) const img = promptpayDetails.querySelector('img'); const infoP = promptpayDetails.querySelector('p'); function isValidPromptpayId(id) { // id should be digits only; valid if 11-digit starting with 66 (mobile) or 13-digit (national id) if (!id) return false; if (!/^\d+$/.test(id)) return false; if (id.length === 11 && id.startsWith('66')) return true; // mobile normalized if (id.length === 13) return true; // national id return false; } if (img && idForPromptpay) { // try a list of candidate IDs (normalized and some variants) and retry on error const candidates = []; if (isValidPromptpayId(idForPromptpay)) candidates.push(idForPromptpay); // also try without 66 prefix (raw digits) and with leading 0 if orig provided 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; function tryNext() { if (attempt >= candidates.length) { // all failed 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)'; } promptpayDetails.classList.remove('hidden'); } catch (err) { console.error('Error preparing PromptPay details:', err); // still show the block so user sees instructions promptpayDetails.classList.remove('hidden'); } } } else if (paymentMethod === 'bank') { if (bankDetails) bankDetails.classList.remove('hidden'); } } async placeOrder() { try { // Get form values const customerName = document.getElementById('customerName').value; const customerPhone = document.getElementById('customerPhone').value; const customerAddress = document.getElementById('customerAddress').value; const customerProvince = document.getElementById('customerProvince').value; const customerZipcode = document.getElementById('customerZipcode').value; const deliveryMethod = document.querySelector('input[name="deliveryMethod"]:checked').value; const paymentMethod = document.querySelector('input[name="paymentMethod"]:checked').value; // Validate form: address fields only required if delivery selected (not pickup) if (!customerName || !customerPhone) { toast.show('กรุณากรอกชื่อและเบอร์โทรศัพท์', 'warning'); return; } if (deliveryMethod !== 'pickup') { if (!customerAddress || !customerProvince || !customerZipcode) { toast.show('กรุณาระบุที่อยู่สำหรับการจัดส่ง', 'warning'); return; } } // Create order object const order = { customerName, customerPhone, // email removed intentionally customerAddress: `${customerAddress}${customerAddress ? ', ' : ''}${customerProvince ? customerProvince + ' ' : ''}${customerZipcode ? customerZipcode : ''}`, deliveryMethod, paymentMethod, items: this.cart.items.map(item => ({ id: item.id, name: item.name, price: item.price, quantity: item.quantity })), subtotal: this.cart.getSubtotal(), shipping: deliveryMethod === 'pickup' ? 0 : 50, total: this.cart.getTotal(deliveryMethod === 'pickup' ? 0 : 50), status: 'pending', date: new Date().toISOString(), type: 'online' }; // Ensure order has an id (orders objectStore uses keyPath 'id') if (!order.id) { order.id = `ORD-${Date.now()}-${Math.floor(Math.random() * 1000)}`; } // Save order to IndexedDB await this.dbHelper.add('orders', order); // Close checkout modal this.checkoutModal.close(); // Show order confirmation this.showOrderConfirmation(order); // Clear cart await this.cart.clearCart(); console.log('Order placed successfully:', order); } catch (error) { console.error('Error placing order:', error); toast.show('เกิดข้อผิดพลาดในการสั่งซื้อ', 'error'); } } showOrderConfirmation(order) { // Update order confirmation modal document.getElementById('orderNumber').textContent = `#${order.id}`; document.getElementById('orderSubtotal').textContent = `฿${order.subtotal}`; document.getElementById('orderShipping').textContent = `฿${order.shipping}`; document.getElementById('orderTotal').textContent = `฿${order.total}`; document.getElementById('orderCustomerName').textContent = order.customerName; document.getElementById('orderCustomerPhone').textContent = order.customerPhone; document.getElementById('orderCustomerAddress').textContent = order.customerAddress; document.getElementById('orderDeliveryMethod').textContent = order.deliveryMethod === 'pickup' ? 'รับที่ร้าน' : 'จัดส่งถึงบ้าน'; document.getElementById('orderPaymentMethod').textContent = this.getPaymentMethodText(order.paymentMethod); // Update order items const orderSummaryItems = document.getElementById('orderSummaryItems'); orderSummaryItems.innerHTML = order.items.map(item => `
${item.name} x ${item.quantity} ฿${item.price * item.quantity}
`).join(''); // Open modal this.orderConfirmationModal.open(); } getPaymentMethodText(method) { const methodMap = { 'promptpay': 'PromptPay', 'bank': 'โอนเงินผ่านธนาคาร', 'cod': 'เก็บเงินปลายทาง' }; return methodMap[method] || method; } handleContactForm() { // Get form values const name = document.getElementById('name').value; const email = document.getElementById('email').value; const message = document.getElementById('message').value; // In a real app, this would send the form data to a server // For now, we'll just show a success message toast.show('ข้อความของคุณถูกส่งเรียบร้อยแล้ว'); // Reset form document.getElementById('contactForm').reset(); } } // Initialize app when DOM is loaded document.addEventListener('DOMContentLoaded', () => { const app = new App(); app.init(); });