// Modal component class Modal { constructor(modalId) { this.modal = document.getElementById(modalId); this.closeBtn = this.modal.querySelector('.close-modal'); // Close modal when clicking on close button if (this.closeBtn) { this.closeBtn.addEventListener('click', () => this.close()); } // Close modal when clicking outside of modal content window.addEventListener('click', (event) => { if (event.target === this.modal) { this.close(); } }); } open() { this.modal.style.display = 'block'; document.body.style.overflow = 'hidden'; } close() { this.modal.style.display = 'none'; document.body.style.overflow = 'auto'; } } // Toast notification component class Toast { constructor() { this.toast = document.getElementById('toast'); this.toastMessage = document.getElementById('toastMessage'); } show(message, type = 'success') { this.toastMessage.textContent = message; // Set background color based on type if (type === 'success') { this.toast.style.backgroundColor = '#28a745'; } else if (type === 'error') { this.toast.style.backgroundColor = '#dc3545'; } else if (type === 'warning') { this.toast.style.backgroundColor = '#ffc107'; } else if (type === 'info') { this.toast.style.backgroundColor = '#17a2b8'; } this.toast.style.display = 'block'; // Auto hide after 3 seconds setTimeout(() => { this.hide(); }, 3000); } hide() { this.toast.style.display = 'none'; } } // Cart component class Cart { constructor() { this.items = []; this.dbHelper = dbHelper; this.loadCart(); } async loadCart() { try { this.items = await this.dbHelper.getAll('cart'); this.updateCartUI(); } catch (error) { console.error('Error loading cart:', error); } } async addItem(product, quantity = 1) { try { // Check if product already exists in cart const existingItem = this.items.find(item => item.id === product.id); if (existingItem) { // Update quantity if product already exists existingItem.quantity += quantity; await this.dbHelper.update('cart', existingItem); } else { // Add new item to cart const cartItem = { ...product, quantity: quantity }; try { await this.dbHelper.add('cart', cartItem); } catch (err) { // If the key already exists (possible race or duplicate), fallback to update (put) if (err && (err.name === 'ConstraintError' || err.code === 11)) { await this.dbHelper.update('cart', cartItem); } else { throw err; } } this.items.push(cartItem); } this.updateCartUI(); return true; } catch (error) { console.error('Error adding item to cart:', error); return false; } } async updateItemQuantity(productId, quantity) { try { if (quantity <= 0) { // Remove item if quantity is 0 or less await this.removeItem(productId); return true; } // Find and update the item const item = this.items.find(item => item.id === productId); if (item) { item.quantity = quantity; await this.dbHelper.update('cart', item); this.updateCartUI(); return true; } return false; } catch (error) { console.error('Error updating item quantity:', error); return false; } } async removeItem(productId) { try { await this.dbHelper.delete('cart', productId); this.items = this.items.filter(item => item.id !== productId); this.updateCartUI(); return true; } catch (error) { console.error('Error removing item from cart:', error); return false; } } async clearCart() { try { await this.dbHelper.clear('cart'); this.items = []; this.updateCartUI(); return true; } catch (error) { console.error('Error clearing cart:', error); return false; } } getSubtotal() { return this.items.reduce((total, item) => total + (item.price * item.quantity), 0); } getTotal(shippingCost = 0) { return this.getSubtotal() + shippingCost; } getItemCount() { return this.items.reduce((count, item) => count + item.quantity, 0); } updateCartUI() { // Update cart count in header const cartCount = document.querySelector('.cart-count'); if (cartCount) { cartCount.textContent = this.getItemCount(); } // Update cart items in modal const cartItemsContainer = document.getElementById('cartItems'); if (cartItemsContainer) { if (this.items.length === 0) { cartItemsContainer.innerHTML = '

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

'; } else { cartItemsContainer.innerHTML = this.items.map(item => `
${item.name}
${item.name}
฿${item.price}
`).join(''); // Add event listeners to cart item buttons this.attachCartItemEventListeners(); } } // Update cart summary this.updateCartSummary(); // Notify other parts of the app that the cart changed (for live updates) try { window.dispatchEvent(new CustomEvent('cart:updated')); } catch (e) { // ignore if CustomEvent not supported } } updateCartSummary() { const subtotalElement = document.getElementById('cartSubtotal'); const shippingElement = document.getElementById('cartShipping'); const totalElement = document.getElementById('cartTotal'); if (subtotalElement) { subtotalElement.textContent = `฿${this.getSubtotal()}`; } if (shippingElement) { // Get shipping cost based on delivery method const deliveryMethod = document.querySelector('input[name="deliveryMethod"]:checked'); const shippingCost = deliveryMethod && deliveryMethod.value === 'pickup' ? 0 : 50; shippingElement.textContent = `฿${shippingCost}`; } if (totalElement) { const deliveryMethod = document.querySelector('input[name="deliveryMethod"]:checked'); const shippingCost = deliveryMethod && deliveryMethod.value === 'pickup' ? 0 : 50; totalElement.textContent = `฿${this.getTotal(shippingCost)}`; } } attachCartItemEventListeners() { // Decrease quantity buttons document.querySelectorAll('.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(`.quantity-input[data-id="${productId}"]`); if (!input) return; const newQuantity = Math.max(1, parseInt(input.value) - 1); await this.updateItemQuantity(productId, newQuantity); input.value = newQuantity; }); }); // Increase quantity buttons document.querySelectorAll('.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(`.quantity-input[data-id="${productId}"]`); if (!input) return; const newQuantity = parseInt(input.value) + 1; await this.updateItemQuantity(productId, newQuantity); input.value = newQuantity; }); }); // Quantity input fields document.querySelectorAll('.quantity-input').forEach(input => { input.addEventListener('change', async (e) => { const productId = e.target.getAttribute('data-id'); const newQuantity = parseInt(e.target.value); if (newQuantity > 0) { await this.updateItemQuantity(productId, newQuantity); } else { e.target.value = 1; } }); }); // Remove item buttons document.querySelectorAll('.cart-item-remove').forEach(button => { button.addEventListener('click', async (e) => { const target = e.target; const removeEl = target.closest('.cart-item-remove'); const productId = removeEl ? (removeEl.getAttribute('data-id') || removeEl.dataset.id) : null; if (!productId) return; await this.removeItem(productId); }); }); } } // Product component class Product { constructor(product) { this.id = product.id; this.name = product.name; this.category = product.category; this.price = product.price; this.description = product.description; this.image = product.image; this.stock = product.stock; this.status = product.status; } renderCard() { return `
${this.name}
${this.category}
${this.name}
฿${this.price}
${this.description}
`; } renderPOSCard() { return `
${this.name}
${this.name}
฿${this.price}
`; } renderAdminRow() { return `
${this.name}
${this.name} ${this.category} ฿${this.price} ${this.stock} ${this.status === 'active' ? 'ใช้งาน' : 'ไม่ใช้งาน'}
`; } } // Order component class Order { constructor(order) { this.id = order.id; this.customerName = order.customerName; this.customerPhone = order.customerPhone; this.customerEmail = order.customerEmail; this.customerAddress = order.customerAddress; this.deliveryMethod = order.deliveryMethod; this.paymentMethod = order.paymentMethod; this.items = order.items; this.subtotal = order.subtotal; this.shipping = order.shipping; this.total = order.total; this.status = order.status; this.date = order.date; this.type = order.type || 'online'; // 'online' or 'pos' } renderCard() { const statusClass = `status-${this.status}`; const statusText = this.getStatusText(this.status); return `
#${this.id}
${this.customerName}
${this.formatDate(this.date)}
฿${this.total}
${statusText}
`; } renderAdminRow() { const statusClass = `status-${this.status}`; const statusText = this.getStatusText(this.status); const paymentText = this.getPaymentMethodText(this.paymentMethod); return ` #${this.id} ${this.customerName} ${this.formatDate(this.date)} ฿${this.total} ${paymentText} ${statusText}
`; } renderDetails() { const paymentText = this.getPaymentMethodText(this.paymentMethod); const deliveryText = this.getDeliveryMethodText(this.deliveryMethod); return `

คำสั่งซื้อ #${this.id}

วันที่: ${this.formatDate(this.date)}

${this.getStatusText(this.status)}
ชื่อลูกค้า: ${this.customerName}
เบอร์โทรศัพท์: ${this.customerPhone}
อีเมล: ${this.customerEmail || '-'}
ที่อยู่: ${this.customerAddress}
วิธีการจัดส่ง: ${deliveryText}
วิธีการชำระเงิน: ${paymentText}

รายการสินค้า

${this.items.map(item => `
${item.name}
฿${item.price} x ${item.quantity}
${item.quantity}
฿${item.price * item.quantity}
`).join('')}
รวม: ฿${this.subtotal}
ค่าจัดส่ง: ฿${this.shipping}
ทั้งหมด: ฿${this.total}
`; } getStatusText(status) { const statusMap = { 'pending': 'รอดำเนินการ', 'preparing': 'กำลังเตรียม', 'ready': 'พร้อมส่ง', 'completed': 'เสร็จสิ้น', 'canceled': 'ยกเลิก' }; return statusMap[status] || status; } getPaymentMethodText(method) { const methodMap = { 'promptpay': 'PromptPay', 'bank': 'โอนเงินผ่านธนาคาร', 'cod': 'เก็บเงินปลายทาง', 'cash': 'เงินสด', 'card': 'บัตรเครดิต' }; return methodMap[method] || method; } getDeliveryMethodText(method) { const methodMap = { 'home': 'จัดส่งถึงบ้าน', 'pickup': 'รับที่ร้าน', 'instore': 'ซื้อที่ร้าน' }; return methodMap[method] || method; } formatDate(dateString) { const date = new Date(dateString); return date.toLocaleDateString('th-TH', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } } // Chart component (simple implementation) class Chart { constructor(canvasId, type, data) { this.canvas = document.getElementById(canvasId); this.ctx = this.canvas.getContext('2d'); this.type = type; this.data = data; this.colors = [ '#c9a96e', '#8b6f47', '#e8d5b7', '#d4a574', '#a67c52', '#f8d7da', '#d4edda', '#d1ecf1', '#fff3cd', '#e2e3e5' ]; } draw() { if (this.type === 'bar') { this.drawBarChart(); } else if (this.type === 'pie') { this.drawPieChart(); } } drawBarChart() { const width = this.canvas.width; const height = this.canvas.height; const padding = 40; const chartWidth = width - padding * 2; const chartHeight = height - padding * 2; // Clear canvas this.ctx.clearRect(0, 0, width, height); // Find max value const maxValue = Math.max(...this.data.map(item => item.value)); // Draw axes this.ctx.beginPath(); this.ctx.moveTo(padding, padding); this.ctx.lineTo(padding, height - padding); this.ctx.lineTo(width - padding, height - padding); this.ctx.stroke(); // Draw bars const barWidth = chartWidth / this.data.length * 0.8; const barSpacing = chartWidth / this.data.length * 0.2; this.data.forEach((item, index) => { const barHeight = (item.value / maxValue) * chartHeight; const x = padding + (barWidth + barSpacing) * index + barSpacing / 2; const y = height - padding - barHeight; // Draw bar this.ctx.fillStyle = this.colors[index % this.colors.length]; this.ctx.fillRect(x, y, barWidth, barHeight); // Draw label this.ctx.fillStyle = '#333'; this.ctx.font = '12px Kanit'; this.ctx.textAlign = 'center'; this.ctx.fillText(item.label, x + barWidth / 2, height - padding + 20); // Draw value this.ctx.fillText(item.value, x + barWidth / 2, y - 5); }); } drawPieChart() { const width = this.canvas.width; const height = this.canvas.height; const centerX = width / 2; const centerY = height / 2; const radius = Math.min(width, height) / 2 - 20; // Clear canvas this.ctx.clearRect(0, 0, width, height); // Calculate total const total = this.data.reduce((sum, item) => sum + item.value, 0); // Draw pie slices let currentAngle = -Math.PI / 2; this.data.forEach((item, index) => { const sliceAngle = (item.value / total) * 2 * Math.PI; // Draw slice this.ctx.beginPath(); this.ctx.arc(centerX, centerY, radius, currentAngle, currentAngle + sliceAngle); this.ctx.lineTo(centerX, centerY); this.ctx.fillStyle = this.colors[index % this.colors.length]; this.ctx.fill(); // Draw label const labelAngle = currentAngle + sliceAngle / 2; const labelX = centerX + Math.cos(labelAngle) * (radius * 0.7); const labelY = centerY + Math.sin(labelAngle) * (radius * 0.7); this.ctx.fillStyle = '#fff'; this.ctx.font = 'bold 12px Kanit'; this.ctx.textAlign = 'center'; this.ctx.fillText(item.label, labelX, labelY); currentAngle += sliceAngle; }); // Draw legend let legendY = 20; this.data.forEach((item, index) => { this.ctx.fillStyle = this.colors[index % this.colors.length]; this.ctx.fillRect(width - 100, legendY, 15, 15); this.ctx.fillStyle = '#333'; this.ctx.font = '12px Kanit'; this.ctx.textAlign = 'left'; this.ctx.fillText(`${item.label}: ${item.value}`, width - 80, legendY + 12); legendY += 25; }); } } // AJAX helper for mock API calls class MockAPI { constructor() { this.baseURL = 'mock'; } async get(endpoint) { try { const response = await fetch(`${this.baseURL}${endpoint}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { console.error('Error fetching data:', error); throw error; } } async post(endpoint, data) { try { // Simulate network delay await new Promise(resolve => setTimeout(resolve, 500)); // In a real app, this would send data to a server // For now, we'll just return the data with a new ID return { ...data, id: Date.now().toString(), timestamp: new Date().toISOString() }; } catch (error) { console.error('Error posting data:', error); throw error; } } async put(endpoint, data) { try { // Simulate network delay await new Promise(resolve => setTimeout(resolve, 500)); // In a real app, this would update data on a server // For now, we'll just return the updated data return data; } catch (error) { console.error('Error updating data:', error); throw error; } } async delete(endpoint) { try { // Simulate network delay await new Promise(resolve => setTimeout(resolve, 500)); // In a real app, this would delete data on a server // For now, we'll just return success return {success: true}; } catch (error) { console.error('Error deleting data:', error); throw error; } } } // Create instances const mockAPI = new MockAPI(); const toast = new Toast();