components.js

23.91 KB
14/10/2025 14:24
JS
components.js
// 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 = '<p>ตะกร้าสินค้าว่าง</p>';
      } else {
        cartItemsContainer.innerHTML = this.items.map(item => `
          <div class="cart-item">
            <div class="cart-item-image">
              <img src="${item.image}" alt="${item.name}">
            </div>
            <div class="cart-item-info">
              <div class="cart-item-top">
                <div class="cart-item-name">${item.name}</div>
                <div class="cart-item-price">฿${item.price}</div>
              </div>
              <div class="cart-item-bottom">
                <div class="cart-item-quantity">
                  <button class="decrease-quantity" data-id="${item.id}">-</button>
                  <input type="number" value="${item.quantity}" min="1" class="quantity-input" data-id="${item.id}">
                  <button class="increase-quantity" data-id="${item.id}">+</button>
                </div>
                <div class="cart-item-remove" data-id="${item.id}" title="ลบรายการ">
                  <i class="bi bi-trash"></i>
                </div>
              </div>
            </div>
          </div>
        `).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 `
            <div class="product-card" data-id="${this.id}">
                <div class="product-image">
                    <img src="${this.image}" alt="${this.name}">
                </div>
                <div class="product-info">
                    <div class="product-category">${this.category}</div>
                    <div class="product-name">${this.name}</div>
                    <div class="product-price">฿${this.price}</div>
                    <div class="product-description">${this.description}</div>
                    <div class="product-actions">
                        <div class="quantity-control">
                            <button class="decrease-quantity">-</button>
                            <input type="number" value="1" min="1" class="quantity-input">
                            <button class="increase-quantity">+</button>
                        </div>
                        <button class="add-to-cart" data-id="${this.id}">เพิ่มลงตะกร้า</button>
                    </div>
                </div>
            </div>
        `;
  }

  renderPOSCard() {
    return `
            <div class="pos-product-card" data-id="${this.id}">
                <div class="pos-product-image">
                    <img src="${this.image}" alt="${this.name}">
                </div>
                <div class="pos-product-name">${this.name}</div>
                <div class="pos-product-price">฿${this.price}</div>
            </div>
        `;
  }

  renderAdminRow() {
    return `
            <tr data-id="${this.id}">
                <td>
                    <div class="product-image-cell">
                        <img src="${this.image}" alt="${this.name}">
                    </div>
                </td>
                <td>${this.name}</td>
                <td>${this.category}</td>
                <td>฿${this.price}</td>
                <td>${this.stock}</td>
                <td>
                    <span class="status-badge ${this.status === 'active' ? 'status-active' : 'status-inactive'}">
                        ${this.status === 'active' ? 'ใช้งาน' : 'ไม่ใช้งาน'}
                    </span>
                </td>
                <td>
                    <div class="product-actions">
                        <button class="edit-btn" data-id="${this.id}">แก้ไข</button>
                        <button class="delete-btn" data-id="${this.id}">ลบ</button>
                    </div>
                </td>
            </tr>
        `;
  }
}

// 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 `
            <div class="order-card" data-id="${this.id}">
                <div class="order-info">
                    <div class="order-number">#${this.id}</div>
                    <div class="order-customer">${this.customerName}</div>
                    <div class="order-date">${this.formatDate(this.date)}</div>
                </div>
                <div class="order-total">฿${this.total}</div>
                <div class="order-status ${statusClass}">${statusText}</div>
                <div class="order-actions">
                    <button class="view-order-btn" data-id="${this.id}">ดูรายละเอียด</button>
                    <button class="update-status-btn" data-id="${this.id}">อัปเดตสถานะ</button>
                </div>
            </div>
        `;
  }

  renderAdminRow() {
    const statusClass = `status-${this.status}`;
    const statusText = this.getStatusText(this.status);
    const paymentText = this.getPaymentMethodText(this.paymentMethod);

    return `
            <tr data-id="${this.id}">
                <td>#${this.id}</td>
                <td>${this.customerName}</td>
                <td>${this.formatDate(this.date)}</td>
                <td>฿${this.total}</td>
                <td>${paymentText}</td>
                <td>
                    <span class="order-status ${statusClass}">${statusText}</span>
                </td>
                <td>
                    <div class="order-actions">
                        <button class="view-order-btn" data-id="${this.id}">ดูรายละเอียด</button>
                    </div>
                </td>
            </tr>
        `;
  }

  renderDetails() {
    const paymentText = this.getPaymentMethodText(this.paymentMethod);
    const deliveryText = this.getDeliveryMethodText(this.deliveryMethod);

    return `
            <div class="order-details-header">
                <div>
                    <h3>คำสั่งซื้อ #${this.id}</h3>
                    <p>วันที่: ${this.formatDate(this.date)}</p>
                </div>
                <div class="order-status status-${this.status}">${this.getStatusText(this.status)}</div>
            </div>

            <div class="order-details-info">
                <div class="order-details-info-item">
                    <span>ชื่อลูกค้า:</span>
                    <span>${this.customerName}</span>
                </div>
                <div class="order-details-info-item">
                    <span>เบอร์โทรศัพท์:</span>
                    <span>${this.customerPhone}</span>
                </div>
                <div class="order-details-info-item">
                    <span>อีเมล:</span>
                    <span>${this.customerEmail || '-'}</span>
                </div>
                <div class="order-details-info-item">
                    <span>ที่อยู่:</span>
                    <span>${this.customerAddress}</span>
                </div>
                <div class="order-details-info-item">
                    <span>วิธีการจัดส่ง:</span>
                    <span>${deliveryText}</span>
                </div>
                <div class="order-details-info-item">
                    <span>วิธีการชำระเงิน:</span>
                    <span>${paymentText}</span>
                </div>
            </div>

            <div class="order-details-items">
                <h4>รายการสินค้า</h4>
                ${this.items.map(item => `
                    <div class="order-details-item">
                        <div class="order-details-item-info">
                            <div class="order-details-item-name">${item.name}</div>
                            <div class="order-details-item-price">฿${item.price} x ${item.quantity}</div>
                        </div>
                        <div class="order-details-item-quantity">${item.quantity}</div>
                        <div class="order-details-item-total">฿${item.price * item.quantity}</div>
                    </div>
                `).join('')}
            </div>

            <div class="order-details-summary">
                <div class="order-details-summary-row">
                    <span>รวม:</span>
                    <span>฿${this.subtotal}</span>
                </div>
                <div class="order-details-summary-row">
                    <span>ค่าจัดส่ง:</span>
                    <span>฿${this.shipping}</span>
                </div>
                <div class="order-details-summary-row total">
                    <span>ทั้งหมด:</span>
                    <span>฿${this.total}</span>
                </div>
            </div>
        `;
  }

  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();