// 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.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.category}
${this.name}
฿${this.price}
${this.description}
`;
}
renderPOSCard() {
return `
${this.name}
฿${this.price}
`;
}
renderAdminRow() {
return `
|
|
${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.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();