admin.js

27.14 KB
14/10/2025 16:37
JS
admin.js
// 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();
});