app.js

21.99 KB
14/10/2025 16:40
JS
app.js
// Main application logic
class App {
  constructor() {
    this.dbHelper = dbHelper;
    this.cart = null; // will initialize after DB is ready
    this.products = [];
    this.categories = []; // will be loaded from categories.json
    this.currentCategory = 'all';
    this.searchTerm = '';

    // Initialize modals
    this.cartModal = new Modal('cartModal');
    this.checkoutModal = new Modal('checkoutModal');
    this.orderConfirmationModal = new Modal('orderConfirmationModal');

    // Initialize event listeners
    this.initEventListeners();
  }

  async init() {
    try {
      // Initialize database
      await this.dbHelper.init();

      // Initialize with mock data if needed
      await this.dbHelper.initializeWithMockData();

      // Create cart now that DB is available
      this.cart = new Cart();

      // Load categories
      await this.loadCategories();

      // Load products
      await this.loadProducts();

      // Load best sellers
      await this.loadBestSellers();

      // Update cart UI
      this.cart.updateCartUI();

      console.log('App initialized successfully');
    } catch (error) {
      console.error('Error initializing app:', error);
    }
  }

  initEventListeners() {
    // Cart icon click
    const cartIcon = document.querySelector('.cart-icon');
    if (cartIcon) {
      cartIcon.addEventListener('click', () => {
        this.cartModal.open();
      });
    }

    // Continue shopping button
    const continueShoppingBtn = document.getElementById('continueShopping');
    if (continueShoppingBtn) {
      continueShoppingBtn.addEventListener('click', () => {
        this.cartModal.close();
      });
    }

    // Checkout button
    const checkoutBtn = document.getElementById('checkoutBtn');
    if (checkoutBtn) {
      checkoutBtn.addEventListener('click', async () => {
        if (this.cart.items.length === 0) {
          toast.show('ตะกร้าสินค้าว่าง', 'warning');
          return;
        }

        this.cartModal.close();
        this.checkoutModal.open();
        this.updateCheckoutSummary();
        await this.updatePaymentDetails();
      });
    }

    // Back to cart button
    const backToCartBtn = document.getElementById('backToCart');
    if (backToCartBtn) {
      backToCartBtn.addEventListener('click', () => {
        this.checkoutModal.close();
        this.cartModal.open();
      });
    }

    // Place order button
    const placeOrderBtn = document.getElementById('placeOrderBtn');
    if (placeOrderBtn) {
      placeOrderBtn.addEventListener('click', () => {
        this.placeOrder();
      });
    }

    // Continue shopping button in order confirmation
    const continueShoppingBtn2 = document.getElementById('continueShoppingBtn');
    if (continueShoppingBtn2) {
      continueShoppingBtn2.addEventListener('click', () => {
        this.orderConfirmationModal.close();
        this.cart.clearCart();
      });
    }

    // Delivery method change
    const deliveryMethods = document.querySelectorAll('input[name="deliveryMethod"]');
    deliveryMethods.forEach(method => {
      method.addEventListener('change', () => {
        this.updateCheckoutSummary();
        this.updatePaymentDetails();
        // show/hide address fields based on selection
        const delivery = document.querySelector('input[name="deliveryMethod"]:checked').value;
        const addressSection = document.getElementById('addressSection');
        if (addressSection) {
          if (delivery === 'pickup') {
            addressSection.classList.add('hidden');
          } else {
            addressSection.classList.remove('hidden');
          }
        }
      });
    });

    // Payment method change
    const paymentMethods = document.querySelectorAll('input[name="paymentMethod"]');
    paymentMethods.forEach(method => {
      method.addEventListener('change', () => {
        this.updatePaymentDetails();
      });
    });

    // Listen for cart updates to refresh checkout summary and payment details (real-time QR update)
    window.addEventListener('cart:updated', async () => {
      this.updateCheckoutSummary();
      await this.updatePaymentDetails();
    });

    // Category filter
    const categoryItems = document.querySelectorAll('.category-item');
    categoryItems.forEach(item => {
      item.addEventListener('click', () => {
        // Remove active class from all items
        categoryItems.forEach(i => i.classList.remove('active'));

        // Add active class to clicked item
        item.classList.add('active');

        // Filter products
        this.currentCategory = item.getAttribute('data-category');
        this.filterProducts();
      });
    });

    // Search functionality
    const searchInput = document.getElementById('searchInput');
    if (searchInput) {
      searchInput.addEventListener('input', (e) => {
        this.searchTerm = e.target.value.toLowerCase();
        this.filterProducts();
      });
    }

    // Contact form
    const contactForm = document.getElementById('contactForm');
    if (contactForm) {
      contactForm.addEventListener('submit', (e) => {
        e.preventDefault();
        this.handleContactForm();
      });
    }

    // Mobile menu toggle
    const mobileMenuToggle = document.querySelector('.mobile-menu-toggle');
    if (mobileMenuToggle) {
      mobileMenuToggle.addEventListener('click', () => {
        const mainNav = document.querySelector('.main-nav');
        mainNav.classList.toggle('active');
      });
    }
  }

  async loadCategories() {
    try {
      // Prefer categories from IndexedDB (keeps admin edits persistent)
      const cats = await this.dbHelper.getAll('categories');
      if (cats && cats.length > 0) {
        this.categories = cats;
      } else {
        // fallback to mock file
        try {
          const response = await fetch('mock/categories.json');
          if (response.ok) {
            this.categories = await response.json();
          } else {
            this.categories = [];
          }
        } catch (err) {
          console.warn('Could not fetch mock categories', err);
          this.categories = [];
        }
      }

      // If still empty, show only default 'ทั้งหมด'
      if (!this.categories || this.categories.length === 0) {
        this.categories = [{id: 'all', name: 'ทั้งหมด', icon: 'bi-grid-3x3-gap'}];
      }

      this.renderCategories();
    } catch (error) {
      console.error('Error loading categories:', error);
      this.categories = [{id: 'all', name: 'ทั้งหมด', icon: 'bi-grid-3x3-gap'}];
      this.renderCategories();
    }
  }

  renderCategories() {
    const categoryList = document.querySelector('.category-list');
    if (categoryList && this.categories.length > 0) {
      // Prepend "ทั้งหมด" category dynamically
      const allCategory = `
        <div class="category-item active" data-category="all">
          <i class="bi bi-grid-3x3-gap"></i>
          <span>ทั้งหมด</span>
        </div>
      `;

      const categoryHTML = this.categories.map((cat) => `
        <div class="category-item" data-category="${cat.id}">
          <i class="bi ${cat.icon}"></i>
          <span>${cat.name}</span>
        </div>
      `).join('');

      categoryList.innerHTML = allCategory + categoryHTML;

      // Re-attach event listeners for new category items
      const categoryItems = document.querySelectorAll('.category-item');
      categoryItems.forEach(item => {
        item.addEventListener('click', () => {
          // Remove active class from all items
          categoryItems.forEach(i => i.classList.remove('active'));
          // Add active class to clicked item
          item.classList.add('active');
          // Filter products
          this.currentCategory = item.getAttribute('data-category');
          this.filterProducts();
        });
      });
    }
  }

  async loadProducts() {
    try {
      const productsData = await this.dbHelper.getAll('products');
      this.products = productsData.map(product => new Product(product));
      this.renderProducts();
    } catch (error) {
      console.error('Error loading products:', error);
    }
  }

  async loadBestSellers() {
    try {
      // Get some products to display as best sellers
      const productsData = await this.dbHelper.getAll('products');

      // Take first 4 products as best sellers (in a real app, this would be based on sales data)
      const bestSellers = productsData.slice(0, 4).map(product => new Product(product));

      const bestSellerGrid = document.getElementById('bestSellerGrid');
      if (bestSellerGrid) {
        bestSellerGrid.innerHTML = bestSellers.map(product => `
                    <div class="best-seller-card">
                        <div class="best-seller-image">
                            <img src="${product.image}" alt="${product.name}">
                        </div>
                        <div class="best-seller-name">${product.name}</div>
                        <div class="best-seller-price">฿${product.price}</div>
                    </div>
                `).join('');
      }
    } catch (error) {
      console.error('Error loading best sellers:', error);
    }
  }

  renderProducts() {
    const productGrid = document.getElementById('productGrid');
    if (productGrid) {
      productGrid.innerHTML = this.products.map(product => product.renderCard()).join('');

      // Add event listeners to product cards
      this.attachProductEventListeners();
    }
  }

  filterProducts() {
    let filteredProducts = this.products;

    // Filter by category
    if (this.currentCategory !== 'all') {
      filteredProducts = filteredProducts.filter(product => product.category === this.currentCategory);
    }

    // Filter by search term
    if (this.searchTerm) {
      filteredProducts = filteredProducts.filter(product =>
        product.name.toLowerCase().includes(this.searchTerm) ||
        product.description.toLowerCase().includes(this.searchTerm)
      );
    }

    // Render filtered products
    const productGrid = document.getElementById('productGrid');
    if (productGrid) {
      if (filteredProducts.length === 0) {
        productGrid.innerHTML = '<p>ไม่พบสินค้าที่ค้นหา</p>';
      } else {
        productGrid.innerHTML = filteredProducts.map(product => product.renderCard()).join('');

        // Add event listeners to product cards
        this.attachProductEventListeners();
      }
    }
  }

  attachProductEventListeners() {
    // Decrease quantity buttons
    document.querySelectorAll('.product-card .decrease-quantity').forEach(button => {
      button.addEventListener('click', (e) => {
        const input = e.target.nextElementSibling;
        const currentValue = parseInt(input.value);

        if (currentValue > 1) {
          input.value = currentValue - 1;
        }
      });
    });

    // Increase quantity buttons
    document.querySelectorAll('.product-card .increase-quantity').forEach(button => {
      button.addEventListener('click', (e) => {
        const input = e.target.previousElementSibling;
        const currentValue = parseInt(input.value);
        input.value = currentValue + 1;
      });
    });

    // Add to cart buttons
    document.querySelectorAll('.add-to-cart').forEach(button => {
      button.addEventListener('click', async (e) => {
        const productId = e.target.getAttribute('data-id');
        const productCard = e.target.closest('.product-card');
        const quantityInput = productCard.querySelector('.quantity-input');
        const quantity = parseInt(quantityInput.value);

        // Find product
        const product = this.products.find(p => p.id === productId);
        if (product) {
          // Add to cart
          const success = await this.cart.addItem(product, quantity);

          if (success) {
            toast.show('สินค้าถูกเพิ่มในตะกร้าแล้ว');

            // Reset quantity input
            quantityInput.value = 1;
          } else {
            toast.show('เกิดข้อผิดพลาดในการเพิ่มสินค้า', 'error');
          }
        }
      });
    });
  }

  updateCheckoutSummary() {
    const deliveryMethod = document.querySelector('input[name="deliveryMethod"]:checked');
    const shippingCost = deliveryMethod && deliveryMethod.value === 'pickup' ? 0 : 50;

    const subtotalElement = document.getElementById('checkoutSubtotal');
    const shippingElement = document.getElementById('checkoutShipping');
    const totalElement = document.getElementById('checkoutTotal');

    if (subtotalElement) {
      subtotalElement.textContent = `฿${this.cart.getSubtotal()}`;
    }

    if (shippingElement) {
      shippingElement.textContent = `฿${shippingCost}`;
    }

    if (totalElement) {
      totalElement.textContent = `฿${this.cart.getTotal(shippingCost)}`;
    }
  }

  async updatePaymentDetails() {
    const paymentMethod = document.querySelector('input[name="paymentMethod"]:checked').value;

    // Hide all payment details
    const promptpayDetails = document.getElementById('promptpayDetails');
    const bankDetails = document.getElementById('bankDetails');
    if (promptpayDetails) promptpayDetails.classList.add('hidden');
    if (bankDetails) bankDetails.classList.add('hidden');

    // Show selected payment details
    if (paymentMethod === 'promptpay') {
      if (promptpayDetails) {
        // populate QR dynamically using shop phone and amount
        try {
          const shopPhoneEntry = await this.dbHelper.getById('settings', 'shopPhone');
          const shopPhone = shopPhoneEntry && shopPhoneEntry.value ? String(shopPhoneEntry.value) : null;
          let idForPromptpay = '';

          if (shopPhone) {
            // normalize phone: digits only, convert leading 0 to 66
            let digits = shopPhone.replace(/\D/g, '');
            if (digits.length === 10 && digits.startsWith('0')) {
              digits = '66' + digits.slice(1);
            }
            idForPromptpay = digits;
          }

          // compute amount including delivery method
          const deliveryMethod = document.querySelector('input[name="deliveryMethod"]:checked');
          const shippingCost = deliveryMethod && deliveryMethod.value === 'pickup' ? 0 : 50;
          const amount = this.cart.getTotal(shippingCost).toFixed(2);

          // set QR image src (using promptpay.io service)
          const img = promptpayDetails.querySelector('img');
          const infoP = promptpayDetails.querySelector('p');
          function isValidPromptpayId(id) {
            // id should be digits only; valid if 11-digit starting with 66 (mobile) or 13-digit (national id)
            if (!id) return false;
            if (!/^\d+$/.test(id)) return false;
            if (id.length === 11 && id.startsWith('66')) return true; // mobile normalized
            if (id.length === 13) return true; // national id
            return false;
          }

          if (img && idForPromptpay) {
            // try a list of candidate IDs (normalized and some variants) and retry on error
            const candidates = [];
            if (isValidPromptpayId(idForPromptpay)) candidates.push(idForPromptpay);
            // also try without 66 prefix (raw digits) and with leading 0 if orig provided
            const raw = shopPhone ? shopPhone.replace(/\D/g, '') : '';
            if (raw && !candidates.includes(raw)) candidates.push(raw);
            if (raw && raw.length === 10 && raw.startsWith('0')) {
              const normalized = '66' + raw.slice(1);
              if (!candidates.includes(normalized)) candidates.unshift(normalized);
            }

            let attempt = 0;
            function tryNext() {
              if (attempt >= candidates.length) {
                // all failed
                img.removeAttribute('src');
                if (infoP) infoP.textContent = 'ไม่สามารถโหลด PromptPay QR ได้ — โปรดตรวจสอบค่า PromptPay ID ใน settings (shopPhone)';
                return;
              }
              const id = candidates[attempt];
              const url = `https://promptpay.io/${id}/${encodeURIComponent(amount)}.png`;
              img.onload = () => {
                img.alt = `PromptPay QR - ฿${amount}`;
                if (infoP) infoP.textContent = `สแกน QR Code เพื่อชำระเงิน ฿${amount}`;
              };
              img.onerror = () => {
                attempt += 1;
                tryNext();
              };
              img.src = url;
            }

            tryNext();
          } else {
            if (img) img.removeAttribute('src');
            if (infoP) infoP.textContent = 'PromptPay ID ไม่ถูกต้องหรือยังไม่ตั้งค่าในระบบ (settings.shopPhone)';
          }

          promptpayDetails.classList.remove('hidden');
        } catch (err) {
          console.error('Error preparing PromptPay details:', err);
          // still show the block so user sees instructions
          promptpayDetails.classList.remove('hidden');
        }
      }
    } else if (paymentMethod === 'bank') {
      if (bankDetails) bankDetails.classList.remove('hidden');
    }
  }

  async placeOrder() {
    try {
      // Get form values
      const customerName = document.getElementById('customerName').value;
      const customerPhone = document.getElementById('customerPhone').value;
      const customerAddress = document.getElementById('customerAddress').value;
      const customerProvince = document.getElementById('customerProvince').value;
      const customerZipcode = document.getElementById('customerZipcode').value;

      const deliveryMethod = document.querySelector('input[name="deliveryMethod"]:checked').value;
      const paymentMethod = document.querySelector('input[name="paymentMethod"]:checked').value;

      // Validate form: address fields only required if delivery selected (not pickup)
      if (!customerName || !customerPhone) {
        toast.show('กรุณากรอกชื่อและเบอร์โทรศัพท์', 'warning');
        return;
      }
      if (deliveryMethod !== 'pickup') {
        if (!customerAddress || !customerProvince || !customerZipcode) {
          toast.show('กรุณาระบุที่อยู่สำหรับการจัดส่ง', 'warning');
          return;
        }
      }

      // Create order object
      const order = {
        customerName,
        customerPhone,
        // email removed intentionally
        customerAddress: `${customerAddress}${customerAddress ? ', ' : ''}${customerProvince ? customerProvince + ' ' : ''}${customerZipcode ? customerZipcode : ''}`,
        deliveryMethod,
        paymentMethod,
        items: this.cart.items.map(item => ({
          id: item.id,
          name: item.name,
          price: item.price,
          quantity: item.quantity
        })),
        subtotal: this.cart.getSubtotal(),
        shipping: deliveryMethod === 'pickup' ? 0 : 50,
        total: this.cart.getTotal(deliveryMethod === 'pickup' ? 0 : 50),
        status: 'pending',
        date: new Date().toISOString(),
        type: 'online'
      };

      // Ensure order has an id (orders objectStore uses keyPath 'id')
      if (!order.id) {
        order.id = `ORD-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
      }

      // Save order to IndexedDB
      await this.dbHelper.add('orders', order);

      // Close checkout modal
      this.checkoutModal.close();

      // Show order confirmation
      this.showOrderConfirmation(order);

      // Clear cart
      await this.cart.clearCart();

      console.log('Order placed successfully:', order);
    } catch (error) {
      console.error('Error placing order:', error);
      toast.show('เกิดข้อผิดพลาดในการสั่งซื้อ', 'error');
    }
  }

  showOrderConfirmation(order) {
    // Update order confirmation modal
    document.getElementById('orderNumber').textContent = `#${order.id}`;
    document.getElementById('orderSubtotal').textContent = `฿${order.subtotal}`;
    document.getElementById('orderShipping').textContent = `฿${order.shipping}`;
    document.getElementById('orderTotal').textContent = `฿${order.total}`;
    document.getElementById('orderCustomerName').textContent = order.customerName;
    document.getElementById('orderCustomerPhone').textContent = order.customerPhone;
    document.getElementById('orderCustomerAddress').textContent = order.customerAddress;
    document.getElementById('orderDeliveryMethod').textContent = order.deliveryMethod === 'pickup' ? 'รับที่ร้าน' : 'จัดส่งถึงบ้าน';
    document.getElementById('orderPaymentMethod').textContent = this.getPaymentMethodText(order.paymentMethod);

    // Update order items
    const orderSummaryItems = document.getElementById('orderSummaryItems');
    orderSummaryItems.innerHTML = order.items.map(item => `
            <div class="order-summary-item">
                <span>${item.name} x ${item.quantity}</span>
                <span>฿${item.price * item.quantity}</span>
            </div>
        `).join('');

    // Open modal
    this.orderConfirmationModal.open();
  }

  getPaymentMethodText(method) {
    const methodMap = {
      'promptpay': 'PromptPay',
      'bank': 'โอนเงินผ่านธนาคาร',
      'cod': 'เก็บเงินปลายทาง'
    };

    return methodMap[method] || method;
  }

  handleContactForm() {
    // Get form values
    const name = document.getElementById('name').value;
    const email = document.getElementById('email').value;
    const message = document.getElementById('message').value;

    // In a real app, this would send the form data to a server
    // For now, we'll just show a success message
    toast.show('ข้อความของคุณถูกส่งเรียบร้อยแล้ว');

    // Reset form
    document.getElementById('contactForm').reset();
  }
}

// Initialize app when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
  const app = new App();
  app.init();
});