// Admin Panel logic class AdminPanel { constructor() { this.dbHelper = dbHelper; this.products = []; this.orders = []; this.sales = []; this.settings = {}; this.currentPage = 'dashboard'; this.currentProductId = null; // Initialize modals this.productModal = new Modal('productModal'); this.categoryModal = new Modal('categoryModal'); // Toast this.toast = new Toast(); // Bind category modal events (icon preview, save/cancel) this.bindCategoryModalEvents(); // Initialize event listeners this.initEventListeners(); } async init() { try { // Initialize database await this.dbHelper.init(); // Initialize with mock data if needed await this.dbHelper.initializeWithMockData(); // Load data await this.loadData(); // Show dashboard by default this.showPage('dashboard'); console.log('Admin Panel initialized successfully'); } catch (error) { console.error('Error initializing Admin Panel:', error); } } initEventListeners() { // Sidebar menu items const menuItems = document.querySelectorAll('.menu-item'); menuItems.forEach(item => { item.addEventListener('click', () => { // Remove active class from all items menuItems.forEach(i => i.classList.remove('active')); // Add active class to clicked item item.classList.add('active'); // Show page const page = item.getAttribute('data-page'); this.showPage(page); }); }); // Add product button const addProductBtn = document.getElementById('addProductBtn'); if (addProductBtn) { addProductBtn.addEventListener('click', () => { this.openProductModal(); }); } // Product modal close button const cancelProductBtn = document.getElementById('cancelProductBtn'); if (cancelProductBtn) { cancelProductBtn.addEventListener('click', () => { this.productModal.close(); }); } // Save product button const saveProductBtn = document.getElementById('saveProductBtn'); if (saveProductBtn) { saveProductBtn.addEventListener('click', () => { this.saveProduct(); }); } // Export orders button const exportOrdersBtn = document.getElementById('exportOrdersBtn'); if (exportOrdersBtn) { exportOrdersBtn.addEventListener('click', () => { this.exportOrdersToCSV(); }); } // Save settings button const saveSettingsBtn = document.getElementById('saveSettingsBtn'); if (saveSettingsBtn) { saveSettingsBtn.addEventListener('click', () => { this.saveSettings(); }); } // Category modal cancel/save handled in bindCategoryModalEvents } async loadData() { try { // Load products const productsData = await this.dbHelper.getAll('products'); this.products = productsData.map(product => new Product(product)); // Load orders const ordersData = await this.dbHelper.getAll('orders'); this.orders = ordersData.map(order => new Order(order)); // Load sales this.sales = await this.dbHelper.getAll('sales'); // Load settings const settingsData = await this.dbHelper.getAll('settings'); this.settings = {}; settingsData.forEach(setting => { this.settings[setting.key] = setting.value; }); // Load categories from IndexedDB; fall back to mock JSON if DB empty try { const categoriesFromDB = await this.dbHelper.getAll('categories'); if (categoriesFromDB && categoriesFromDB.length > 0) { this.categories = categoriesFromDB; } else { // try loading mock file try { const resp = await fetch('mock/categories.json'); if (resp.ok) { this.categories = await resp.json(); // persist into DB for future use for (const c of this.categories) { try {await this.dbHelper.add('categories', c);} catch (e) {await this.dbHelper.update('categories', c);} } } else { this.categories = []; } } catch (err) { console.warn('Could not load categories.json', err); this.categories = []; } } } catch (err) { console.warn('Error loading categories from DB', err); this.categories = []; } // populate product category select if present this.populateProductCategorySelect(); console.log('Data loaded successfully'); } catch (error) { console.error('Error loading data:', error); } } populateProductCategorySelect() { const select = document.getElementById('productCategory'); if (!select) return; // If no categories loaded, keep existing options if (!this.categories || this.categories.length === 0) return; select.innerHTML = this.categories.map(cat => ``).join(''); } showPage(page) { // Hide all pages document.querySelectorAll('.admin-page').forEach(p => { p.classList.remove('active'); }); // Show selected page document.getElementById(`${page}Page`).classList.add('active'); // Update current page this.currentPage = page; // Load page-specific data if (page === 'dashboard') { this.loadDashboard(); } else if (page === 'products') { this.loadProducts(); } else if (page === 'categories') { this.loadCategoriesPage(); } else if (page === 'orders') { this.loadOrders(); } else if (page === 'settings') { this.loadSettings(); } } async loadCategoriesPage() { // Render categories into inline table const tbody = document.getElementById('categoriesTableBody'); if (!tbody) return; // Ensure categories are loaded if (!this.categories) this.categories = []; if (this.categories.length === 0) { tbody.innerHTML = ` ไม่มีหมวดหมู่ `; } else { tbody.innerHTML = this.categories.map((cat, idx) => ` ${cat.name} ${cat.description || '-'} ${cat.id} `).join(''); // Attach handlers tbody.querySelectorAll('button[data-action]').forEach(btn => { btn.addEventListener('click', (e) => { const idx = Number(btn.getAttribute('data-idx')); const action = btn.getAttribute('data-action'); if (action === 'edit') { this.editCategoryInline(idx); } else if (action === 'delete') { this.deleteCategoryInline(idx); } }); }); } // Hook add button const addBtn = document.getElementById('addCategoryBtn'); if (addBtn) { addBtn.onclick = () => this.addCategoryInline(); } } // Simple inline category CRUD using prompt dialogs and JSON download (filesystem writes not available client-side) addCategoryInline() { // Open category modal for adding this.openCategoryModal('add'); } editCategoryInline(idx) { // Open category modal for editing this.openCategoryModal('edit', idx); } deleteCategoryInline(idx) { const cat = this.categories[idx]; if (!cat) return; if (!confirm(`คุณต้องการลบหมวดหมู่ "${cat.name}" ใช่หรือไม่?`)) return; this.categories.splice(idx, 1); this.saveCategoriesJson(); this.loadCategoriesPage(); this.populateProductCategorySelect(); } // Category modal helpers bindCategoryModalEvents() { // Save / Cancel buttons const saveBtn = document.getElementById('saveCategoryBtn'); const cancelBtn = document.getElementById('cancelCategoryBtn'); const iconInput = document.getElementById('categoryIcon'); const suggestions = document.querySelectorAll('.icon-suggestion'); if (saveBtn) saveBtn.addEventListener('click', () => this.saveCategoryFromModal()); if (cancelBtn) cancelBtn.addEventListener('click', () => this.categoryModal.close()); if (iconInput) { iconInput.addEventListener('input', (e) => { this.updateCategoryIconPreview(e.target.value); }); } suggestions.forEach(s => { s.addEventListener('click', (e) => { const icon = s.getAttribute('data-icon'); const iconField = document.getElementById('categoryIcon'); if (iconField) iconField.value = icon; this.updateCategoryIconPreview(icon); // mark active suggestions.forEach(x => x.classList.remove('active')); s.classList.add('active'); }); }); } openCategoryModal(mode = 'add', idx = null) { // mode: 'add' or 'edit' this.currentCategoryIndex = idx; const modalTitle = document.getElementById('categoryModalTitle'); if (mode === 'add') { if (modalTitle) modalTitle.textContent = 'เพิ่มหมวดหมู่ใหม่'; this.clearCategoryForm(); document.getElementById('categoryId').disabled = false; } else if (mode === 'edit') { if (modalTitle) modalTitle.textContent = 'แก้ไขหมวดหมู่'; const cat = this.categories && this.categories[idx]; if (cat) this.fillCategoryForm(cat, idx); // prevent changing ID on edit document.getElementById('categoryId').disabled = true; } this.categoryModal.open(); } fillCategoryForm(cat, idx) { document.getElementById('categoryIndex').value = idx != null ? String(idx) : ''; document.getElementById('categoryId').value = cat.id || ''; document.getElementById('categoryName').value = cat.name || ''; document.getElementById('categoryDescription').value = cat.description || ''; document.getElementById('categoryIcon').value = cat.icon || 'bi-folder'; this.updateCategoryIconPreview(cat.icon || 'bi-folder'); // clear active suggestion and set if present document.querySelectorAll('.icon-suggestion').forEach(s => s.classList.toggle('active', s.getAttribute('data-icon') === (cat.icon || ''))); } clearCategoryForm() { document.getElementById('categoryIndex').value = ''; document.getElementById('categoryId').value = ''; document.getElementById('categoryName').value = ''; document.getElementById('categoryDescription').value = ''; document.getElementById('categoryIcon').value = 'bi-folder'; this.updateCategoryIconPreview('bi-folder'); document.querySelectorAll('.icon-suggestion').forEach(s => s.classList.remove('active')); } updateCategoryIconPreview(iconClass) { const preview = document.getElementById('categoryIconPreview'); if (!preview) return; // remove previous bi-* classes preview.className = 'bi ' + (iconClass || 'bi-folder'); } saveCategoryFromModal() { const idxStr = document.getElementById('categoryIndex').value; const idx = idxStr ? Number(idxStr) : null; const idField = document.getElementById('categoryId'); const nameField = document.getElementById('categoryName'); const descField = document.getElementById('categoryDescription'); const iconField = document.getElementById('categoryIcon'); const id = idField.value && idField.value.trim(); const name = nameField.value && nameField.value.trim(); const description = descField.value && descField.value.trim(); const icon = iconField.value && iconField.value.trim(); if (!id || !name || !icon) { this.toast.show('กรุณากรอก ID, ชื่อ และไอคอนให้ครบ', 'warning'); return; } if (idx == null) { // adding - ensure unique id if (this.categories.find(c => c.id === id)) { this.toast.show('มี ID นี้ในระบบแล้ว โปรดใช้ ID อื่น', 'error'); return; } this.categories.push({id, name, description, icon}); this.toast.show('เพิ่มหมวดหมู่สำเร็จ'); } else { // editing this.categories[idx] = {id, name, description, icon}; this.toast.show('อัปเดตหมวดหมู่สำเร็จ'); } this.saveCategoriesJson(); this.populateProductCategorySelect(); this.loadCategoriesPage(); this.categoryModal.close(); } saveCategoriesJson() { // Persist categories to IndexedDB and also trigger an optional download for manual backup (async () => { try { // Clear existing categories in DB and write current list await this.dbHelper.clear('categories'); for (const c of this.categories) { try {await this.dbHelper.add('categories', c);} catch (e) {await this.dbHelper.update('categories', c);} } } catch (e) { console.error('Error saving categories to DB', e); } })(); // Also offer a download so the developer can update mock/categories.json if needed //const dataStr = JSON.stringify(this.categories, null, 2); //const blob = new Blob([dataStr], {type: 'application/json'}); //const url = URL.createObjectURL(blob); //const a = document.createElement('a'); //a.href = url; a.download = 'categories.json'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); //alert('ดาวน์โหลดไฟล์ categories.json แล้ว — ถ้าต้องการเก็บเป็นไฟล์ต้นฉบับ ให้แทนที่ mock/categories.json'); } loadDashboard() { // Update stats document.getElementById('totalOrders').textContent = this.orders.length; document.getElementById('totalRevenue').textContent = `฿${this.calculateTotalRevenue()}`; document.getElementById('totalProducts').textContent = this.products.length; document.getElementById('totalCustomers').textContent = this.calculateTotalCustomers(); // Draw charts this.drawSalesChart(); this.drawTopProductsChart(); // Load recent orders this.loadRecentOrders(); } loadProducts() { const productsTable = document.getElementById('productsTable'); if (productsTable) { productsTable.innerHTML = this.products.map(product => product.renderAdminRow()).join(''); // Add event listeners to product actions this.attachProductTableEventListeners(); } } loadOrders() { const ordersTable = document.getElementById('ordersTable'); if (ordersTable) { ordersTable.innerHTML = this.orders.map(order => order.renderAdminRow()).join(''); // Add event listeners to order actions this.attachOrderTableEventListeners(); } } loadSettings() { // Update form fields with current settings document.getElementById('shopName').value = this.settings.shopName || ''; document.getElementById('shopAddress').value = this.settings.shopAddress || ''; document.getElementById('shopPhone').value = this.settings.shopPhone || ''; document.getElementById('shopEmail').value = this.settings.shopEmail || ''; document.getElementById('shippingCost').value = this.settings.shippingCost || 50; // Update checkboxes document.getElementById('enablePromptpay').checked = this.settings.enablePromptpay === true; document.getElementById('enableBankTransfer').checked = this.settings.enableBankTransfer === true; document.getElementById('enableCOD').checked = this.settings.enableCOD === true; document.getElementById('enableHomeDelivery').checked = this.settings.enableHomeDelivery === true; document.getElementById('enableStorePickup').checked = this.settings.enableStorePickup === true; } calculateTotalRevenue() { return this.orders.reduce((total, order) => total + order.total, 0); } calculateTotalCustomers() { // Count unique customers const uniqueCustomers = new Set(); this.orders.forEach(order => { uniqueCustomers.add(order.customerPhone); }); return uniqueCustomers.size; } drawSalesChart() { // Prepare data for the last 7 days const today = new Date(); const salesData = []; for (let i = 6; i >= 0; i--) { const date = new Date(today); date.setDate(date.getDate() - i); const dateStr = date.toISOString().split('T')[0]; // YYYY-MM-DD format // Calculate sales for this date const daySales = this.sales .filter(sale => sale.date === dateStr) .reduce((total, sale) => total + sale.amount, 0); salesData.push({ label: date.toLocaleDateString('th-TH', {day: 'numeric', month: 'short'}), value: daySales }); } // Draw chart const canvas = document.getElementById('salesChart'); if (canvas) { canvas.width = canvas.offsetWidth; canvas.height = 300; const chart = new Chart('salesChart', 'bar', salesData); chart.draw(); } } drawTopProductsChart() { // Calculate product sales const productSales = {}; this.orders.forEach(order => { order.items.forEach(item => { if (!productSales[item.id]) { productSales[item.id] = { name: item.name, quantity: 0 }; } productSales[item.id].quantity += item.quantity; }); }); // Sort by quantity and take top 5 const topProducts = Object.values(productSales) .sort((a, b) => b.quantity - a.quantity) .slice(0, 5); // Draw chart const canvas = document.getElementById('topProductsChart'); if (canvas) { canvas.width = canvas.offsetWidth; canvas.height = 300; const chart = new Chart('topProductsChart', 'pie', topProducts.map(product => ({ label: product.name, value: product.quantity }))); chart.draw(); } } loadRecentOrders() { // Sort orders by date (newest first) and take 5 const recentOrders = [...this.orders] .sort((a, b) => new Date(b.date) - new Date(a.date)) .slice(0, 5); const recentOrdersTable = document.getElementById('recentOrdersTable'); if (recentOrdersTable) { recentOrdersTable.innerHTML = recentOrders.map(order => ` #${order.id} ${order.customerName} ${order.formatDate(order.date)} ฿${order.total} ${order.getStatusText(order.status)} `).join(''); } } attachProductTableEventListeners() { // Edit buttons document.querySelectorAll('.edit-btn').forEach(button => { button.addEventListener('click', (e) => { const productId = e.target.getAttribute('data-id'); this.editProduct(productId); }); }); // Delete buttons document.querySelectorAll('.delete-btn').forEach(button => { button.addEventListener('click', (e) => { const productId = e.target.getAttribute('data-id'); this.deleteProduct(productId); }); }); } attachOrderTableEventListeners() { // View order buttons document.querySelectorAll('.view-order-btn').forEach(button => { button.addEventListener('click', (e) => { const orderId = e.target.getAttribute('data-id'); this.viewOrderDetails(orderId); }); }); } openProductModal(product = null) { // Reset form document.getElementById('productForm').reset(); // Update modal title const modalTitle = document.getElementById('productModalTitle'); if (product) { modalTitle.textContent = 'แก้ไขสินค้า'; // Fill form with product data document.getElementById('productName').value = product.name; document.getElementById('productCategory').value = product.category; document.getElementById('productPrice').value = product.price; document.getElementById('productStock').value = product.stock; document.getElementById('productDescription').value = product.description; document.getElementById('productImage').value = product.image; document.getElementById('productStatus').value = product.status; // Store current product ID this.currentProductId = product.id; } else { modalTitle.textContent = 'เพิ่มสินค้าใหม่'; this.currentProductId = null; } // Open modal this.productModal.open(); } async saveProduct() { try { // Get form values const name = document.getElementById('productName').value; const category = document.getElementById('productCategory').value; const price = parseFloat(document.getElementById('productPrice').value); const stock = parseInt(document.getElementById('productStock').value); const description = document.getElementById('productDescription').value; const image = document.getElementById('productImage').value; const status = document.getElementById('productStatus').value; // Validate form if (!name || !category || isNaN(price) || isNaN(stock)) { this.toast.show('กรุณากรอกข้อมูลให้ครบถ้วน', 'warning'); return; } // Create product object const product = { name, category, price, stock, description, image: image || `https://picsum.photos/seed/${name}/300/300.webp`, status }; if (this.currentProductId) { // Update existing product product.id = this.currentProductId; await this.dbHelper.update('products', product); this.toast.show('อัปเดตสินค้าสำเร็จ'); } else { // Add new product product.id = Date.now().toString(); await this.dbHelper.add('products', product); this.toast.show('เพิ่มสินค้าสำเร็จ'); } // Close modal this.productModal.close(); // Reload products await this.loadData(); this.loadProducts(); } catch (error) { console.error('Error saving product:', error); this.toast.show('เกิดข้อผิดพลาดในการบันทึกสินค้า', 'error'); } } async editProduct(productId) { // Find product const product = this.products.find(p => p.id === productId); if (!product) return; // Open product modal with product data this.openProductModal(product); } async deleteProduct(productId) { try { if (!confirm('คุณต้องการลบสินค้านี้ใช่หรือไม่?')) { return; } // Delete product from IndexedDB await this.dbHelper.delete('products', productId); // Reload products await this.loadData(); this.loadProducts(); this.toast.show('ลบสินค้าสำเร็จ'); } catch (error) { console.error('Error deleting product:', error); this.toast.show('เกิดข้อผิดพลาดในการลบสินค้า', 'error'); } } viewOrderDetails(orderId) { // Find order const order = this.orders.find(o => o.id === orderId); if (!order) return; // In a real app, this would open a modal with order details // For now, we'll just show an alert alert(`รายละเอียดคำสั่งซื้อ #${order.id}\nลูกค้า: ${order.customerName}\nยอดรวม: ฿${order.total}\nสถานะ: ${order.getStatusText(order.status)}`); } exportOrdersToCSV() { try { // Create CSV content const headers = ['ID', 'Customer Name', 'Date', 'Total', 'Payment Method', 'Status']; const rows = this.orders.map(order => [ order.id, order.customerName, order.date, order.total, order.paymentMethod, order.status ]); let csvContent = headers.join(',') + '\n'; rows.forEach(row => { csvContent += row.join(',') + '\n'; }); // Create blob and download const blob = new Blob([csvContent], {type: 'text/csv'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `orders_${new Date().toISOString().split('T')[0]}.csv`; a.click(); this.toast.show('ส่งออกข้อมูลสำเร็จ'); } catch (error) { console.error('Error exporting orders:', error); this.toast.show('เกิดข้อผิดพลาดในการส่งออกข้อมูล', 'error'); } } async saveSettings() { try { // Get form values const shopName = document.getElementById('shopName').value; const shopAddress = document.getElementById('shopAddress').value; const shopPhone = document.getElementById('shopPhone').value; const shopEmail = document.getElementById('shopEmail').value; const shippingCost = parseInt(document.getElementById('shippingCost').value); const enablePromptpay = document.getElementById('enablePromptpay').checked; const enableBankTransfer = document.getElementById('enableBankTransfer').checked; const enableCOD = document.getElementById('enableCOD').checked; const enableHomeDelivery = document.getElementById('enableHomeDelivery').checked; const enableStorePickup = document.getElementById('enableStorePickup').checked; // Update settings in IndexedDB await this.dbHelper.update('settings', {key: 'shopName', value: shopName}); await this.dbHelper.update('settings', {key: 'shopAddress', value: shopAddress}); await this.dbHelper.update('settings', {key: 'shopPhone', value: shopPhone}); await this.dbHelper.update('settings', {key: 'shopEmail', value: shopEmail}); await this.dbHelper.update('settings', {key: 'shippingCost', value: shippingCost}); await this.dbHelper.update('settings', {key: 'enablePromptpay', value: enablePromptpay}); await this.dbHelper.update('settings', {key: 'enableBankTransfer', value: enableBankTransfer}); await this.dbHelper.update('settings', {key: 'enableCOD', value: enableCOD}); await this.dbHelper.update('settings', {key: 'enableHomeDelivery', value: enableHomeDelivery}); await this.dbHelper.update('settings', {key: 'enableStorePickup', value: enableStorePickup}); // Reload settings await this.loadData(); this.toast.show('บันทึกการตั้งค่าสำเร็จ'); } catch (error) { console.error('Error saving settings:', error); this.toast.show('เกิดข้อผิดพลาดในการบันทึกการตั้งค่า', 'error'); } } } // Initialize Admin Panel when DOM is loaded document.addEventListener('DOMContentLoaded', () => { const adminPanel = new AdminPanel(); adminPanel.init(); });