// 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 => `<option value="${cat.id}">${cat.name}</option>`).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 = `
<tr><td colspan="5" class="text-center text-muted">ไม่มีหมวดหมู่</td></tr>
`;
} else {
tbody.innerHTML = this.categories.map((cat, idx) => `
<tr>
<td class="text-center"><i class="bi ${cat.icon} fs-4"></i></td>
<td><strong>${cat.name}</strong></td>
<td><small class="text-muted">${cat.description || '-'}</small></td>
<td><code>${cat.id}</code></td>
<td class="text-center">
<button class="btn btn-sm btn-outline-primary me-1" data-idx="${idx}" data-action="edit">✏️</button>
<button class="btn btn-sm btn-outline-danger" data-idx="${idx}" data-action="delete">🗑️</button>
</td>
</tr>
`).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 => `
<tr>
<td>#${order.id}</td>
<td>${order.customerName}</td>
<td>${order.formatDate(order.date)}</td>
<td>฿${order.total}</td>
<td>
<span class="order-status status-${order.status}">${order.getStatusText(order.status)}</span>
</td>
</tr>
`).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();
});