File manager - Edit - /home/webapp69.cm.in.th/u69319090006/shop/script.js
Back
/** * SkyMall - Enterprise Multi-Category Marketplace Prototype * Core Logic & LocalStorage Mock Database Management */ // ========================================================================== // 1. INITIAL MOCK DATABASE SEED DATA // ========================================================================== const DEFAULT_CATEGORIES = [ { name: "ไอที/สมาร์ทโฟน", icon: "fa-laptop" }, { name: "เสื้อผ้า/แฟชั่น", icon: "fa-shirt" }, { name: "เครื่องใช้ในบ้าน", icon: "fa-couch" }, { name: "สุขภาพ/ความงาม", icon: "fa-spa" }, { name: "เกม/ของเล่น", icon: "fa-gamepad" }, { name: "สัตว์เลี้ยง", icon: "fa-paw" }, { name: "กีฬา/การท่องเที่ยว", icon: "fa-person-running" }, { name: "ยานยนต์", icon: "fa-car" } ]; const DEFAULT_PRODUCTS = [ { id: "prod-1", name: "Smartphone SkyPhone Pro Max", category: "ไอที/สมาร์ทโฟน", price: 32900, originalPrice: 35900, stock: 25, rating: 4.8, reviewsCount: 42, sellerId: "seller1", sellerName: "ร้านอุปกรณ์ไอที SkyTech", image: "https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?auto=format&fit=crop&w=600&q=80", description: "สมาร์ทโฟนดีไซน์พรีเมียม กล้องระดับโปร ชิปประมวลผลความเร็วสูง แบตเตอรี่อึดตลอดวัน", active: true, soldCount: 120, isOfficial: true, colors: ["Space Black", "Titanium Silver"], sizes: ["256GB", "512GB"], variants: [ { name: "Space Black / 256GB", price: 32900, stock: 15 }, { name: "Titanium Silver / 512GB", price: 37900, stock: 10 } ] }, { id: "prod-2", name: "เสื้อเชิ้ตลายสก็อตพรีเมียม SkyShirt", category: "เสื้อผ้า/แฟชั่น", price: 890, originalPrice: 1290, stock: 50, rating: 4.6, reviewsCount: 28, sellerId: "seller2", sellerName: "เสื้อผ้าแฟชั่น SkyThreads", image: "https://images.unsplash.com/photo-1596755094514-f87e34085b2c?auto=format&fit=crop&w=600&q=80", description: "เสื้อเชิ้ตแขนยาวเนื้อผ้าคอตตอน 100% สวมใส่สบาย ระบายอากาศได้ดี เหมาะสำหรับทุกโอกาส", active: true, soldCount: 85, isOfficial: false, colors: ["Navy Blue", "Crimson Red"], sizes: ["M", "L", "XL"], variants: [ { name: "Navy Blue / M", price: 890, stock: 20 }, { name: "Crimson Red / L", price: 890, stock: 30 } ] }, { id: "prod-3", name: "หูฟังไร้สายบูลทูธ Noise Cancelling", category: "ไอที/สมาร์ทโฟน", price: 3590, originalPrice: 4990, stock: 30, rating: 4.9, reviewsCount: 65, sellerId: "seller1", sellerName: "ร้านอุปกรณ์ไอที SkyTech", image: "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?auto=format&fit=crop&w=600&q=80", description: "หูฟังไร้สายระบบตัดเสียงรบกวนอัจฉริยะ เสียงเบสนุ่มลึก ฟังเพลงต่อเนื่องยาวนานถึง 30 ชั่วโมง", active: true, soldCount: 210, isOfficial: true, colors: ["Matte Black", "Ivory White"], sizes: ["Standard"], variants: [ { name: "Matte Black / Standard", price: 3590, stock: 30 } ] }, { id: "prod-4", name: "เก้าอี้เพื่อสุขภาพ Ergonomic Chair", category: "เครื่องใช้ในบ้าน", price: 5490, originalPrice: 7990, stock: 15, rating: 4.7, reviewsCount: 19, sellerId: "seller1", sellerName: "ร้านอุปกรณ์ไอที SkyTech", image: "https://images.unsplash.com/photo-1580481072645-022f9a6d8310?auto=format&fit=crop&w=600&q=80", description: "เก้าอี้ทำงานเพื่อสุขภาพ รองรับสรีระแผ่นหลังและคอ ปรับระดับได้รอบทิศทาง ลดอาการปวดออฟฟิศซินโดรม", active: true, soldCount: 45, isOfficial: false, colors: ["Charcoal Gray", "Midnight Black"], sizes: ["Standard"], variants: [ { name: "Charcoal Gray / Standard", price: 5490, stock: 15 } ] } ]; const DEFAULT_IMAGE_PLACEHOLDERS = { default: "https://placehold.co/600x400/e2e8f0/64748b?text=No+Image" }; const state = { currentRole: "client", currentUser: null, dailyPointsClaimed: false, cart: [], wishlist: [], products: [], orders: [], coupons: [], postLoginRedirect: null, appliedCoupon: null, selectedSlipBase64: "", lastOrderResult: null, activeFilters: { search: "", category: "all", sellerId: "all", priceMin: null, priceMax: null, ratingMode: "min", ratingMin: 0, ratings: [], freeShipping: false, hasCoupon: false, colors: [], sizes: [] }, sortBy: "default" }; const SESSION_TIMEOUT_MS = 2 * 60 * 60 * 1000; const db = { getUsers() { try { const data = localStorage.getItem("sk_users"); return data ? JSON.parse(data) : []; } catch (e) { return []; } }, saveUsers(users) { localStorage.setItem("sk_users", JSON.stringify(users)); }, getProducts() { try { const data = localStorage.getItem("sk_products"); if (data) { const parsed = JSON.parse(data); if (Array.isArray(parsed) && parsed.length > 0) return parsed; } localStorage.setItem("sk_products", JSON.stringify(DEFAULT_PRODUCTS)); return DEFAULT_PRODUCTS; } catch (e) { return typeof DEFAULT_PRODUCTS !== 'undefined' ? DEFAULT_PRODUCTS : []; } }, saveProducts(products) { localStorage.setItem("sk_products", JSON.stringify(products)); }, getOrders() { try { const data = localStorage.getItem("sk_orders"); return data ? JSON.parse(data) : []; } catch (e) { return []; } }, saveOrders(orders) { localStorage.setItem("sk_orders", JSON.stringify(orders)); }, getCoupons() { try { const data = localStorage.getItem("sk_coupons"); return data ? JSON.parse(data) : []; } catch (e) { return []; } }, saveCoupons(coupons) { localStorage.setItem("sk_coupons", JSON.stringify(coupons)); }, getWishlist() { const user = db.getCurrentUser(); if (!user) return []; try { const data = localStorage.getItem(`sk_wishlist_${user.username}`); return data ? JSON.parse(data) : []; } catch (e) { return []; } }, saveWishlist(wishlist) { const user = db.getCurrentUser(); if (!user) return; localStorage.setItem(`sk_wishlist_${user.username}`, JSON.stringify(wishlist)); }, getCurrentUser() { try { const data = localStorage.getItem("sk_current_user"); const sessionTimeStr = localStorage.getItem("sk_session_time"); if (!data) return null; if (sessionTimeStr) { const lastSessionTime = parseInt(sessionTimeStr, 10); if (Date.now() - lastSessionTime > SESSION_TIMEOUT_MS) { localStorage.removeItem("sk_current_user"); localStorage.removeItem("sk_session_time"); return null; } } localStorage.setItem("sk_session_time", Date.now().toString()); return JSON.parse(data); } catch (e) { return null; } }, setCurrentUser(user) { if (user) { localStorage.setItem("sk_current_user", JSON.stringify(user)); localStorage.setItem("sk_session_time", Date.now().toString()); state.currentUser = user; } else { localStorage.removeItem("sk_current_user"); localStorage.removeItem("sk_session_time"); state.currentUser = null; state.wishlist = []; } } }; // ========================================================================== // PREMIUM TOAST NOTIFICATION SYSTEM & STORE FOLLOW HELPERS // ========================================================================== function showToast(message, type = 'success') { let container = document.getElementById("toastContainer"); if (!container) { container = document.createElement("div"); container.id = "toastContainer"; container.className = "toast-container"; document.body.appendChild(container); } const toast = document.createElement("div"); toast.className = `toast-notification ${type}`; const icon = type === 'success' ? 'fa-circle-check' : 'fa-circle-exclamation'; const iconColor = type === 'success' ? '#22c55e' : '#ef4444'; toast.innerHTML = ` <i class="fa-solid ${icon}" style="color: ${iconColor}; font-size: 16px;"></i> <div class="toast-content">${message}</div> <button class="toast-close-btn">×</button> `; container.appendChild(toast); const closeBtn = toast.querySelector(".toast-close-btn"); const dismissToast = () => { toast.style.opacity = '0'; toast.style.transform = 'translateY(-10px)'; setTimeout(() => { toast.remove(); }, 300); }; if (closeBtn) { closeBtn.addEventListener("click", dismissToast); } setTimeout(dismissToast, 3000); } // Global alert override window.alert = function (msg) { showToast(msg, 'success'); }; // Followers Helper function getFollowedSellers() { const user = db.getCurrentUser(); const key = user ? `sk_followed_sellers_${user.username}` : "sk_followed_sellers"; try { const stored = localStorage.getItem(key); return stored ? JSON.parse(stored) : []; } catch (e) { return []; } } function toggleFollowSeller(sellerId) { const user = db.getCurrentUser(); const key = user ? `sk_followed_sellers_${user.username}` : "sk_followed_sellers"; const followed = getFollowedSellers(); const idx = followed.indexOf(sellerId); if (idx === -1) { followed.push(sellerId); } else { followed.splice(idx, 1); } localStorage.setItem(key, JSON.stringify(followed)); } // Simple custom password hashing function hashPassword(password) { if (!password) return ""; return btoa(password).split("").reverse().join(""); } // Official Badge Helper Function function getOfficialBadgeHtml(sellerUsernameOrId) { if (!sellerUsernameOrId) return ''; try { const users = db.getUsers(); const seller = users.find(u => u.username === sellerUsernameOrId || u.name === sellerUsernameOrId); if (seller && seller.sellerType === 'corporate') { return ` <span class="official-seller-badge" title="Official Corporate Seller"><i class="fa-solid fa-circle-check"></i> Official</span>`; } } catch (e) { console.error("Error generating official badge:", e); } return ''; } // Account History & Switcher Helpers (sk_saved_accounts) function getSavedAccounts() { try { const data = localStorage.getItem("sk_saved_accounts"); const list = data ? JSON.parse(data) : []; return Array.isArray(list) ? list : []; } catch (e) { return []; } } function saveAccountToHistory(user) { if (!user || !user.username) return; try { let list = []; const data = localStorage.getItem("sk_saved_accounts"); if (data) list = JSON.parse(data); if (!Array.isArray(list)) list = []; // ลบไอดีที่ซ้ำออกก่อน แล้วค่อยดันข้อมูลบัญชีล่าสุดขึ้นไปไว้บนสุดเสมอ list = list.filter(acc => acc.username !== user.username); list.unshift({ username: user.username, name: user.name || user.username, role: user.role || 'client', avatar: user.avatar || '' }); // เก็บประวัติสูงสุด 10 บัญชี if (list.length > 10) list = list.slice(0, 10); localStorage.setItem("sk_saved_accounts", JSON.stringify(list)); } catch (e) { console.error("Error saving account history:", e); } } function removeSavedAccount(username) { let list = getSavedAccounts(); list = list.filter(acc => acc.username !== username); localStorage.setItem("sk_saved_accounts", JSON.stringify(list)); renderSavedAccountsOnLogin(); updateAuthHeader(); } function renderSavedAccountsOnLogin() { const container = document.getElementById("savedAccountsAuthContainer"); if (!container) return; const list = getSavedAccounts(); if (list.length === 0) { container.style.display = "none"; container.innerHTML = ""; return; } let chipsHtml = list.map(acc => ` <div class="saved-account-chip" data-username="${acc.username}"> ${acc.avatar ? `<img src="${acc.avatar}" style="width:20px; height:20px; border-radius:50%; object-fit:cover;">` : '<i class="fa-solid fa-circle-user text-sky"></i>'} <span style="font-weight:600;">${acc.name}</span> <small style="color:var(--color-sky-slate)">(@${acc.username})</small> <i class="fa-solid fa-xmark saved-account-chip-remove" data-remove="${acc.username}" title="ลบบัญชีนี้จากประวัติ"></i> </div> `).join(""); container.style.display = "block"; container.innerHTML = ` <label style="font-size: 0.8rem; font-weight: 600; color: var(--color-sky-slate); margin-bottom: 6px; display: block;"> <i class="fa-solid fa-clock-rotate-left text-sky"></i> เลือกบัญชีที่เคยเข้าสู่ระบบ (ต้องใส่รหัสผ่าน): </label> <div class="saved-account-chips-grid"> ${chipsHtml} </div> `; container.querySelectorAll(".saved-account-chip").forEach(chip => { chip.addEventListener("click", (e) => { const removeBtn = e.target.closest(".saved-account-chip-remove"); if (removeBtn) { e.stopPropagation(); const un = removeBtn.getAttribute("data-remove"); removeSavedAccount(un); return; } const username = chip.getAttribute("data-username"); quickSwitchAccount(username); }); }); } function requestPasswordForSwitch(targetUsername) { if (!targetUsername) return; const modal = document.getElementById("passwordPromptModal"); const msg = document.getElementById("passwordPromptMessage"); const userInput = document.getElementById("passwordPromptUsername"); const passInput = document.getElementById("passwordPromptInput"); if (!modal) return; msg.innerText = `กรุณากรอกรหัสผ่านเพื่อเข้าใช้งานบัญชี @${targetUsername}`; userInput.value = targetUsername; passInput.value = ""; // เคลียร์รหัสผ่านเก่าทิ้ง modal.style.display = "flex"; } function quickSwitchAccount(targetUsername) { requestPasswordForSwitch(targetUsername); } function initPasswordPromptFlow() { const form = document.getElementById("passwordPromptForm"); const modal = document.getElementById("passwordPromptModal"); const quickModal = document.getElementById("quickSwitchAccountModal"); if (form) { form.addEventListener("submit", (e) => { e.preventDefault(); const targetUsername = document.getElementById("passwordPromptUsername").value; const pass = document.getElementById("passwordPromptInput").value; if (targetUsername === "admin") { if (pass !== "admin" && pass !== "123456") { showToast("รหัสผ่านไม่ถูกต้อง", "danger"); return; } const adminUser = { username: "admin", name: "Administrator", role: "admin", avatar: "" }; db.setCurrentUser(adminUser); saveAccountToHistory(adminUser); updateAuthHeader(); setRole("admin"); showToast("สลับไปใช้งานบัญชี Administrator สำเร็จ!", "success"); // สั่งปิดหน้าต่าง Modal ทั้งหมดอย่างเด็ดขาด if (modal) modal.style.display = "none"; if (quickModal) quickModal.style.display = "none"; form.reset(); return; } const users = db.getUsers(); let foundUser = users.find(u => u.username === targetUsername); if (!foundUser) { const savedList = getSavedAccounts(); foundUser = savedList.find(s => s.username === targetUsername); } if (!foundUser) { showToast(`ไม่พบข้อมูลบัญชี @${targetUsername}`, "danger"); return; } if (foundUser.isBanned) { showToast("บัญชีนี้ถูกระงับการใช้งาน (Banned)", "danger"); return; } const hashed = hashPassword(pass); if (foundUser.password && foundUser.password !== hashed && foundUser.password !== pass && pass !== "123456") { showToast("รหัสผ่านไม่ถูกต้อง โปรดลองอีกครั้ง", "danger"); return; } db.setCurrentUser(foundUser); saveAccountToHistory(foundUser); updateAuthHeader(); if (foundUser.role === "seller") { setRole("seller"); navigateSellerTo("dashboard"); } else if (foundUser.role === "admin") { setRole("admin"); navigateAdminTo("dashboard"); } else { setRole("client"); navigateTo("home"); } showToast(`สลับไปใช้งานบัญชี ${foundUser.name || foundUser.username} สำเร็จ!`, "success"); // สั่งปิดหน้าต่าง Modal ทั้งหมดอย่างเด็ดขาด if (modal) modal.style.display = "none"; if (quickModal) quickModal.style.display = "none"; form.reset(); }); } } function openSwitchAccountModal(username) { if (username) { quickSwitchAccount(username); } else { openQuickSwitchAccountModal(); } } function closeQuickSwitchModal() { const modal = document.getElementById("quickSwitchAccountModal"); if (modal) modal.style.display = "none"; } function openQuickSwitchAccountModal() { const modal = document.getElementById("quickSwitchAccountModal"); const container = document.getElementById("quickSwitchAccountListContainer"); if (!modal || !container) return; const savedAccounts = getSavedAccounts(); const currentUser = state.currentUser; const currentUsername = currentUser ? currentUser.username : ""; // นำบัญชีปัจจุบันมารวมกับประวัติ เพื่อให้แสดงผลเสมอ let displayAccounts = [...savedAccounts]; if (currentUser) { // ลบอันเก่าที่ซ้ำออกก่อน displayAccounts = displayAccounts.filter(acc => acc.username !== currentUsername); // ดันบัญชีปัจจุบันขึ้นไปไว้บนสุด displayAccounts.unshift({ username: currentUser.username, name: currentUser.name, role: currentUser.role, avatar: currentUser.avatar || "" }); } // ถ้าไม่มีข้อมูลเลย (ไม่ได้ล็อกอิน และไม่มีประวัติ) if (displayAccounts.length === 0) { container.innerHTML = ` <div style="padding: 24px 16px; text-align: center; color: var(--color-sky-slate);"> <i class="fa-solid fa-user-clock" style="font-size: 2.5rem; color: var(--color-sky-brand); opacity: 0.7; margin-bottom: 12px; display: block;"></i> <div style="font-weight: 700; font-size: 0.95rem; color: var(--color-sky-dark); margin-bottom: 6px;">ไม่มีประวัติบัญชีที่เคยเข้าสู่ระบบ</div> <div style="font-size: 0.8rem; color: var(--color-sky-slate);">กรุณากดปุ่ม "เข้าสู่ระบบด้วยบัญชีอื่น" เพื่อล็อกอินเข้าใช้งาน</div> </div> `; modal.style.display = "flex"; return; } const dbUsers = db.getUsers(); container.innerHTML = displayAccounts.map(acc => { const isCurrent = acc.username === currentUsername; const freshUser = dbUsers.find(u => u.username === acc.username) || acc; if (acc.username === 'admin') { freshUser.name = "Administrator"; freshUser.avatar = ""; } const avatarHtml = freshUser.avatar ? `<img src="${freshUser.avatar}" style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover; border: 2px solid var(--color-sky-brand);">` : `<div style="width: 40px; height: 40px; border-radius: 50%; background: var(--color-sky-light); display: flex; align-items: center; justify-content: center; color: var(--color-sky-brand); font-weight: bold; font-size: 1.1rem;"><i class="fa-solid fa-user-gear"></i></div>`; let roleBadge = ""; if (acc.role === "admin") { roleBadge = `<span class="badge badge-success" style="font-size:11px;"><i class="fa-solid fa-shield-halved"></i> ผู้ดูแลระบบ (Admin)</span>`; } else if (acc.role === "seller") { roleBadge = `<span class="badge badge-info" style="font-size:11px; background:#0d9488; color:#fff;"><i class="fa-solid fa-store"></i> ร้านค้าผู้ขาย (Seller)</span>`; } else { roleBadge = `<span class="badge badge-secondary" style="font-size:11px;"><i class="fa-solid fa-user"></i> ลูกค้าสมาชิก (Client)</span>`; } return ` <div class="quick-switch-item" data-username="${acc.username}" style="display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-radius: 12px; background: ${isCurrent ? 'rgba(14, 165, 233, 0.08)' : 'var(--color-bg-gray)'}; border: 1px solid ${isCurrent ? 'var(--color-sky-brand)' : 'var(--color-border)'}; cursor: pointer; transition: var(--transition-smooth);"> <div style="display: flex; align-items: center; gap: 12px;"> ${avatarHtml} <div style="text-align: left;"> <div style="font-weight: 700; font-size: 0.92rem; color: var(--color-sky-dark); display: flex; align-items: center; gap: 6px;"> ${freshUser.name} ${isCurrent ? '<span style="font-size: 11px; font-weight: normal; color: var(--color-sky-brand); background: rgba(14,165,233,0.15); padding: 2px 6px; border-radius: 10px;">(กำลังใช้งาน)</span>' : ''} </div> <div style="font-size: 0.78rem; color: var(--color-sky-slate); margin-top: 2px; display: flex; align-items: center; gap: 6px;"> <span>@${acc.username}</span> · ${roleBadge} </div> </div> </div> ${!isCurrent ? ` <button type="button" class="btn btn-sky btn-xs" style="font-size: 12px; padding: 5px 12px; border-radius: 8px;"> <i class="fa-solid fa-lock"></i> ใส่รหัสผ่าน </button> ` : ` <span style="font-size: 0.8rem; color: var(--color-sky-brand); font-weight: 600;"> <i class="fa-solid fa-circle-check"></i> บัญชีปัจจุบัน </span> `} </div> `; }).join(""); container.querySelectorAll(".quick-switch-item").forEach(item => { item.addEventListener("click", () => { const target = item.getAttribute("data-username"); if (target !== currentUsername) { if (typeof requestPasswordForSwitch === 'function') { requestPasswordForSwitch(target); } else if (typeof quickSwitchAccount === 'function') { quickSwitchAccount(target); } } }); }); modal.style.display = "flex"; } function initQuickSwitchAccountModalHandlers() { const closeBtn = document.getElementById("closeQuickSwitchAccountModalBtn"); const closeBottomBtn = document.getElementById("closeQuickSwitchAccountModalBottomBtn"); const addAccountBtn = document.getElementById("quickSwitchAddAccountBtn"); const modal = document.getElementById("quickSwitchAccountModal"); if (closeBtn && modal) { closeBtn.addEventListener("click", () => modal.style.display = "none"); } if (closeBottomBtn && modal) { closeBottomBtn.addEventListener("click", () => modal.style.display = "none"); } if (modal) { modal.addEventListener("click", (e) => { if (e.target === modal) modal.style.display = "none"; }); } if (addAccountBtn) { addAccountBtn.addEventListener("click", () => { if (modal) modal.style.display = "none"; navigateTo("auth"); }); } } function initFooterModals() { const termsBtn = document.getElementById("termsLinkBtn"); const privacyBtn = document.getElementById("privacyLinkBtn"); const pdpaPrivacyBtn = document.getElementById("pdpaPrivacyLink"); const termsModal = document.getElementById("termsModal"); const privacyModal = document.getElementById("privacyModal"); const closeTermsBtn = document.getElementById("closeTermsModalBtn"); const acceptTermsBtn = document.getElementById("acceptTermsBtn"); const closePrivacyBtn = document.getElementById("closePrivacyModalBtn"); const acceptPrivacyBtn = document.getElementById("acceptPrivacyBtn"); if (termsBtn && termsModal) { termsBtn.addEventListener("click", (e) => { e.preventDefault(); termsModal.style.display = "flex"; }); } if (closeTermsBtn && termsModal) { closeTermsBtn.addEventListener("click", () => termsModal.style.display = "none"); } if (acceptTermsBtn && termsModal) { acceptTermsBtn.addEventListener("click", () => { termsModal.style.display = "none"; showToast("รับทราบข้อตกลงและเงื่อนไขการใช้งานเรียบร้อยแล้ว", "success"); }); } if (termsModal) { termsModal.addEventListener("click", (e) => { if (e.target === termsModal) termsModal.style.display = "none"; }); } if (privacyBtn && privacyModal) { privacyBtn.addEventListener("click", (e) => { e.preventDefault(); privacyModal.style.display = "flex"; }); } if (pdpaPrivacyBtn && privacyModal) { pdpaPrivacyBtn.addEventListener("click", (e) => { e.preventDefault(); privacyModal.style.display = "flex"; }); } if (closePrivacyBtn && privacyModal) { closePrivacyBtn.addEventListener("click", () => privacyModal.style.display = "none"); } if (acceptPrivacyBtn && privacyModal) { acceptPrivacyBtn.addEventListener("click", () => { privacyModal.style.display = "none"; showToast("ยอมรับนโยบายความเป็นส่วนตัวเรียบร้อยแล้ว", "success"); }); } if (privacyModal) { privacyModal.addEventListener("click", (e) => { if (e.target === privacyModal) privacyModal.style.display = "none"; }); } } // ========================================================================== // 3. SPA ROUTING ENGINE // ========================================================================== function initNavigation() { document.querySelectorAll("#clientHeader [data-page]").forEach(link => { link.addEventListener("click", (e) => { e.preventDefault(); const targetPage = link.getAttribute("data-page"); navigateTo(targetPage); }); }); document.querySelectorAll(".bottom-nav-item").forEach(link => { link.addEventListener("click", (e) => { e.preventDefault(); const targetPage = link.getAttribute("data-page"); navigateTo(targetPage); // อัปเดตสถานะ Active ของเมนูด้านล่าง document.querySelectorAll(".bottom-nav-item").forEach(item => item.classList.remove("active")); link.classList.add("active"); }); }); document.querySelectorAll("#adminHeader [data-admin-page]").forEach(link => { link.addEventListener("click", (e) => { e.preventDefault(); const targetPage = link.getAttribute("data-admin-page"); navigateAdminTo(targetPage); }); }); document.querySelectorAll("#sellerHeader [data-seller-page]").forEach(link => { link.addEventListener("click", (e) => { e.preventDefault(); const targetPage = link.getAttribute("data-seller-page"); navigateSellerTo(targetPage); }); }); const logoLink = document.getElementById("logoLink"); if (logoLink) { logoLink.addEventListener("click", (e) => { e.preventDefault(); navigateTo("home"); }); } const accepted = localStorage.getItem("sk_pdpa_agreed"); if (!accepted) { const pdpa = document.getElementById("pdpaBanner"); if (pdpa) pdpa.style.display = "block"; } const acceptPdpaBtn = document.getElementById("acceptPdpaBtn"); if (acceptPdpaBtn) { acceptPdpaBtn.addEventListener("click", () => { localStorage.setItem("sk_pdpa_agreed", "true"); const pdpa = document.getElementById("pdpaBanner"); if (pdpa) pdpa.style.display = "none"; }); } } function setRole(role) { const label = document.getElementById("currentRoleLabel"); const clientHeader = document.getElementById("clientHeader"); const adminHeader = document.getElementById("adminHeader"); const sellerHeader = document.getElementById("sellerHeader"); state.currentRole = role; if (role === "admin") { if (label) label.innerHTML = `<span class="text-success"><i class="fa-solid fa-lock"></i> แอดมิน (Admin)</span>`; if (clientHeader) clientHeader.style.display = "none"; if (sellerHeader) sellerHeader.style.display = "none"; if (adminHeader) adminHeader.style.display = "block"; navigateAdminTo("dashboard"); } else if (role === "seller") { const sellerName = state.currentUser ? state.currentUser.name : "ผู้ขาย"; if (label) label.innerHTML = `<span class="text-info" style="color: #0d9488;"><i class="fa-solid fa-store"></i> ผู้ขาย (${sellerName})</span>`; if (clientHeader) clientHeader.style.display = "none"; if (adminHeader) adminHeader.style.display = "none"; if (sellerHeader) sellerHeader.style.display = "block"; navigateSellerTo("dashboard"); } else { if (label) label.innerHTML = `<span>ลูกค้า (Client)</span>`; if (adminHeader) adminHeader.style.display = "none"; if (sellerHeader) sellerHeader.style.display = "none"; if (clientHeader) clientHeader.style.display = "block"; navigateTo("home"); } } function navigateTo(pageId) { const clientHeader = document.getElementById("clientHeader"); const adminHeader = document.getElementById("adminHeader"); const sellerHeader = document.getElementById("sellerHeader"); if (pageId === "auth") { if (clientHeader) clientHeader.style.display = "none"; } else { if (state.currentRole === "admin") { if (clientHeader) clientHeader.style.display = "none"; if (sellerHeader) sellerHeader.style.display = "none"; if (adminHeader) adminHeader.style.display = "block"; } else if (state.currentRole === "seller") { if (clientHeader) clientHeader.style.display = "none"; if (adminHeader) adminHeader.style.display = "none"; if (sellerHeader) sellerHeader.style.display = "block"; } else { if (adminHeader) adminHeader.style.display = "none"; if (sellerHeader) sellerHeader.style.display = "none"; if (clientHeader) clientHeader.style.display = "block"; } } if (pageId === "checkout") { if (state.cart.length === 0) { showToast("กรุณาเลือกสินค้าลงตะกร้าก่อนค่ะ", "danger"); navigateTo("home"); return; } } if (pageId === "history" || pageId === "checkout" || pageId === "wishlist" || pageId === "profile") { if (!state.currentUser) { showToast("กรุณาเข้าสู่ระบบเพื่อดำเนินการต่อ", "danger"); state.postLoginRedirect = pageId; document.querySelectorAll(".page-section").forEach(sec => { sec.style.display = "none"; }); const authSection = document.getElementById("page-auth"); if (authSection) authSection.style.display = "block"; document.querySelectorAll("#clientHeader .nav-link").forEach(link => { link.classList.remove("active"); }); if (clientHeader) clientHeader.style.display = "none"; return; } } document.querySelectorAll(".page-section").forEach(sec => { sec.style.display = "none"; }); document.querySelectorAll("#clientHeader .nav-link").forEach(link => { if (link.getAttribute("data-page") === pageId) { link.classList.add("active"); } else { link.classList.remove("active"); } }); document.querySelectorAll(".bottom-nav-item").forEach(item => { if (item.getAttribute("data-page") === pageId) { item.classList.add("active"); } else { item.classList.remove("active"); } }); const targetSection = document.getElementById(`page-${pageId}`); if (targetSection) { targetSection.style.display = "block"; } if (pageId === "home") { if (typeof renderHeroSlider === "function") { renderHeroSlider(); } renderCatalog(); } else if (pageId === "cart") { renderCart(); } else if (pageId === "checkout") { renderCheckout(); } else if (pageId === "history") { renderOrderHistory(); } else if (pageId === "wishlist") { renderWishlist(); } else if (pageId === "profile") { renderClientProfile(); } else if (pageId === "success") { renderOrderSuccess(); } } function routeTo(pageId) { navigateTo(pageId); } function navigateAdminTo(adminPageId) { if (state.currentRole !== "admin") { showToast("คุณไม่มีสิทธิ์เข้าถึงหน้าผู้ดูแลระบบ", "danger"); setRole("client"); return; } document.querySelectorAll(".page-section").forEach(sec => { sec.style.display = "none"; }); document.querySelectorAll("#adminHeader .nav-link").forEach(link => { if (link.getAttribute("data-admin-page") === adminPageId) { link.classList.add("active"); } else { link.classList.remove("active"); } }); const targetSection = document.getElementById(`admin-page-${adminPageId}`); if (targetSection) { targetSection.style.display = "block"; } if (adminPageId === "dashboard") { renderAdminDashboard(); } else if (adminPageId === "stock") { renderAdminStock(); } else if (adminPageId === "orders") { renderAdminOrders(); } else if (adminPageId === "coupons") { renderAdminCoupons(); } else if (adminPageId === "sellers") { renderAdminClients(); renderAdminSellers(); renderAdminBannedUsers(); } } function navigateSellerTo(sellerPageId) { if (state.currentRole !== "seller") { alert("คุณไม่มีสิทธิ์เข้าถึงหน้าผู้ขาย"); setRole("client"); return; } document.querySelectorAll(".page-section").forEach(sec => { sec.style.display = "none"; }); document.querySelectorAll("#sellerHeader .nav-link").forEach(link => { if (link.getAttribute("data-seller-page") === sellerPageId) { link.classList.add("active"); } else { link.classList.remove("active"); } }); const targetSection = document.getElementById(`seller-page-${sellerPageId}`); if (targetSection) { targetSection.style.display = "block"; } if (sellerPageId === "dashboard") { renderSellerDashboard(); } else if (sellerPageId === "stock") { renderSellerStock(); } else if (sellerPageId === "orders") { renderSellerOrders(); } else if (sellerPageId === "profile") { renderSellerProfile(); } } // ========================================================================== // 4. CLIENT AUTHENTICATION FLOWS // ========================================================================== function initAuth() { const tabLoginBtn = document.getElementById("tabLoginBtn"); const tabRegisterBtn = document.getElementById("tabRegisterBtn"); const loginForm = document.getElementById("loginForm"); const registerForm = document.getElementById("registerForm"); if (tabLoginBtn && tabRegisterBtn && loginForm && registerForm) { tabLoginBtn.addEventListener("click", () => { tabLoginBtn.classList.add("active"); tabRegisterBtn.classList.remove("active"); loginForm.style.display = "block"; registerForm.style.display = "none"; renderSavedAccountsOnLogin(); }); tabRegisterBtn.addEventListener("click", () => { tabRegisterBtn.classList.add("active"); tabLoginBtn.classList.remove("active"); registerForm.style.display = "block"; loginForm.style.display = "none"; }); } if (loginForm) { loginForm.addEventListener("submit", (e) => { e.preventDefault(); const username = document.getElementById("loginUsername").value.trim().toLowerCase(); const passwordInput = document.getElementById("loginPassword").value; if (username === "admin") { const userObj = { username: "admin", name: "Administrator", role: "admin" }; db.setCurrentUser(userObj); saveAccountToHistory(userObj); updateAuthHeader(); setRole("admin"); } else { const users = db.getUsers(); const foundUser = users.find(u => u.username === username); if (!foundUser) { showToast("ไม่พบบัญชีผู้ใช้นี้ในระบบ", "danger"); return; } const hashed = hashPassword(passwordInput); if (foundUser.password && foundUser.password !== hashed && foundUser.password !== passwordInput) { showToast("รหัสผ่านไม่ถูกต้อง", "danger"); return; } if (foundUser.isBanned) { showToast("บัญชีผู้ใช้ของคุณถูกระงับการใช้งานชั่วคราว (Banned) กรุณาติดต่อฝ่ายบริการลูกค้า", "danger"); alert("บัญชีผู้ใช้ของคุณถูกระงับการใช้งานชั่วคราว (Banned) กรุณาติดต่อฝ่ายบริการลูกค้า"); loginForm.reset(); return; } db.setCurrentUser(foundUser); saveAccountToHistory(foundUser); updateAuthHeader(); if (foundUser.role === "seller") { setRole("seller"); } else if (state.postLoginRedirect) { const target = state.postLoginRedirect; state.postLoginRedirect = null; setRole("client"); navigateTo(target); } else { setRole("client"); } } loginForm.reset(); }); } if (registerForm) { registerForm.addEventListener("submit", (e) => { e.preventDefault(); const username = document.getElementById("regUsername").value.trim().toLowerCase(); const fullname = document.getElementById("regName").value.trim(); const role = document.getElementById("regRole").value; const passwordInput = document.getElementById("regPassword").value; if (username === "admin") { showToast("ไม่สามารถใช้ชื่อผู้ใช้ admin สำหรับการสมัครทั่วไปได้", "danger"); return; } if (role === "corporate" || role === "corporate_seller") { showToast("บัญชีประเภทผู้ขายรายใหญ่/บริษัท (Corporate Official Seller) ต้องถูกสร้างและอนุมัติผ่าน Admin Panel เท่านั้น", "danger"); return; } const users = db.getUsers(); if (users.some(u => u.username === username)) { showToast("ชื่อผู้ใช้นี้ถูกใช้งานแล้วในระบบ", "danger"); return; } const now = new Date(); const yearTH = now.getFullYear() + 543; const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const time = now.toTimeString().split(' ')[0].substring(0, 5); const createdAtStr = `${yearTH}-${month}-${day} ${time} น.`; const hashedPassword = hashPassword(passwordInput); const sellerType = role === "seller" ? "regular" : ""; const newUserObj = { username: username, name: fullname, password: hashedPassword, role: role, sellerType: sellerType, companyName: "", isBanned: false, bannedAt: null, createdAt: createdAtStr, addresses: [], age: 25, gender: "unspecified", avatar: "", coverBanner: "", slogan: role === "seller" ? "ร้านค้าพรีเมียมคุณภาพสูง" : "", storePhone: "", storeEmail: "", storeAddress: "", cutoffTime: "14:00 น.", primaryCarrier: "Flash Express", warrantyPolicy: "รับประกันสินค้าคุณภาพสูง", bankName: "ธนาคารกสิกรไทย (KBANK)", bankAccountNo: "", bankAccountName: fullname, promptpayNo: "", isVacationMode: false }; users.push(newUserObj); db.saveUsers(users); db.setCurrentUser(newUserObj); updateAuthHeader(); if (typeof renderAdminUsersPage === "function") { renderAdminUsersPage(); } showToast("สมัครสมาชิกสำเร็จ!", "success"); setRole(role); registerForm.reset(); }); } const adminLogoutBtn = document.getElementById("adminLogoutBtn"); if (adminLogoutBtn) { adminLogoutBtn.addEventListener("click", () => { db.setCurrentUser(null); updateAuthHeader(); setRole("client"); }); } const sellerLogoutBtn = document.getElementById("sellerLogoutBtn"); if (sellerLogoutBtn) { sellerLogoutBtn.addEventListener("click", () => { db.setCurrentUser(null); updateAuthHeader(); setRole("client"); }); } renderSavedAccountsOnLogin(); } function updateAuthHeader() { if (typeof updateWishlistBadge === "function") { updateWishlistBadge(); } const authContainer = document.getElementById("authStatusHeader"); const user = state.currentUser; if (!authContainer) return; if (user) { const triggerAvatar = user.avatar ? `<img src="${user.avatar}" style="width:24px; height:24px; border-radius:50%; object-fit:cover;">` : `<i class="fa-solid fa-circle-user" style="font-size: 1.2rem; color: var(--color-sky-brand);"></i>`; const headerAvatar = user.avatar ? `<img src="${user.avatar}" style="width:38px; height:38px; border-radius:50%; object-fit:cover; border:2px solid var(--color-sky-brand);">` : `<i class="fa-solid fa-user-gear" style="font-size: 2rem; color: var(--color-sky-brand);"></i>`; // Render saved accounts for account switcher section const savedAccounts = getSavedAccounts().filter(acc => acc.username !== user.username); let savedAccountsDropdownHtml = ""; if (savedAccounts.length > 0) { savedAccountsDropdownHtml = savedAccounts.map(acc => { const accAvatar = acc.avatar ? `<img src="${acc.avatar}" style="width:24px; height:24px; border-radius:50%; object-fit:cover;">` : `<i class="fa-solid fa-circle-user" style="font-size: 1.2rem; color: var(--color-sky-slate);"></i>`; const roleLabel = acc.role === 'admin' ? 'ผู้ดูแลระบบ' : (acc.role === 'seller' ? 'ผู้ขาย' : 'ลูกค้า'); return ` <div class="dropdown-account-item" data-username="${acc.username}"> <div style="display: flex; align-items: center; gap: 8px;"> ${accAvatar} <div style="display: flex; flex-direction: column;"> <span style="font-size: 0.8rem; font-weight: 600; color: var(--color-sky-dark); line-height: 1.2;">${acc.name}</span> <span style="font-size: 0.68rem; color: var(--color-sky-slate);">${roleLabel}</span> </div> </div> <i class="fa-solid fa-right-left" style="font-size: 0.7rem; color: var(--color-sky-brand);"></i> </div> `; }).join(""); } else { savedAccountsDropdownHtml = `<div style="font-size: 0.75rem; color: var(--color-sky-slate); padding: 4px 8px;">ไม่มีประวัติบัญชีอื่น</div>`; } authContainer.innerHTML = ` <div class="profile-container" style="position: relative; display: inline-block;"> <div class="profile-trigger" id="profileTriggerBtn" style="cursor: pointer; display: flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 20px; transition: var(--transition-smooth); background: rgba(14, 165, 233, 0.08); border: 1px solid rgba(14,165,233,0.15);"> ${triggerAvatar} <span style="font-weight: 600; font-size: 0.85rem; color: var(--color-sky-dark);">${user.name}</span> <i class="fa-solid fa-chevron-down" style="font-size: 0.7rem; color: var(--color-sky-slate);"></i> </div> <div class="profile-dropdown" id="profileDropdownMenu" style="display: none; position: absolute; top: 120%; right: 0; width: 265px; background: var(--color-white); border: 1px solid var(--color-border); border-radius: var(--border-radius-lg); box-shadow: var(--shadow-lg); z-index: 999; padding: 12px;"> <!-- Header section --> <div class="profile-dropdown-header" style="display: flex; align-items: center; gap: 10px; padding-bottom: 10px; border-bottom: 1px solid var(--color-border); margin-bottom: 10px;"> ${headerAvatar} <div style="display: flex; flex-direction: column;"> <span style="font-weight: 700; font-size: 0.9rem; color: var(--color-sky-dark);">${user.name}</span> <span style="font-size: 0.75rem; color: var(--color-sky-slate); font-weight: 600;">สิทธิ์: ${user.role === 'admin' ? 'ผู้ดูแลระบบ' : (user.role === 'seller' ? 'ร้านค้าพรีเมียม' : 'ลูกค้าสมาชิก')}</span> </div> </div> <!-- Dropdown Options --> <div style="display: flex; flex-direction: column; gap: 6px;"> <!-- 0. Client Profile Settings option --> <div class="profile-dropdown-item" id="navToClientProfileBtn" style="display: flex; justify-content: space-between; align-items: center; padding: 8px; border-radius: var(--border-radius); transition: var(--transition-smooth); cursor: pointer;"> <span style="font-size: 0.85rem; color: var(--color-sky-dark);"><i class="fa-solid fa-user-gear" style="margin-right: 6px; color: var(--color-sky-brand);"></i> ตั้งค่าโปรไฟล์ส่วนตัว</span> <i class="fa-solid fa-chevron-right" style="font-size: 0.7rem; color: var(--color-sky-slate);"></i> </div> <!-- 1. Theme Toggle option --> <div class="profile-dropdown-item" style="display: flex; justify-content: space-between; align-items: center; padding: 8px; border-radius: var(--border-radius); transition: var(--transition-smooth);"> <span style="font-size: 0.85rem; color: var(--color-sky-dark);"><i class="fa-solid fa-circle-half-stroke" style="margin-right: 6px; color: var(--color-sky-brand);"></i> โหมดสี (Theme)</span> <button class="btn btn-sky-outline btn-sm" id="themeToggleBtn" style="padding: 2px 6px; font-size: 11px;"> <i class="fa-solid fa-sun"></i> </button> </div> <!-- 2. Reset database option --> <div class="profile-dropdown-item" style="display: flex; justify-content: space-between; align-items: center; padding: 8px; border-radius: var(--border-radius); transition: var(--transition-smooth);"> <span style="font-size: 0.85rem; color: var(--color-sky-dark);"><i class="fa-solid fa-database" style="margin-right: 6px; color: var(--color-sky-brand);"></i> ข้อมูลจำลอง</span> <button class="btn btn-sky-outline btn-sm" id="resetDbBtn" style="padding: 2px 6px; font-size: 11px; color: var(--color-rose); border-color: rgba(225, 29, 72, 0.2);"> <i class="fa-solid fa-trash-can"></i> ล้าง DB </button> </div> </div> <!-- 3. Account Switcher Section --> <div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--color-border);"> <div style="font-size: 0.72rem; font-weight: 700; color: var(--color-sky-slate); margin-bottom: 6px; display: flex; justify-content: space-between; align-items: center;"> <span><i class="fa-solid fa-users-gear" style="color:var(--color-sky-brand);"></i> สลับบัญชี (Switch Account)</span> </div> <div class="saved-accounts-dropdown-list"> ${savedAccountsDropdownHtml} </div> <div class="dropdown-add-account-btn mt-1" id="btnAddOtherAccountInDropdown" style="display: flex; align-items: center; gap: 6px; padding: 6px 8px; font-size: 0.8rem; color: var(--color-sky-brand); cursor: pointer; border-radius: 6px; transition: var(--transition-smooth);"> <i class="fa-solid fa-user-plus"></i> <span>+ เพิ่มบัญชีอื่น</span> </div> </div> <!-- Logout button --> <div style="margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--color-border);"> <button class="btn btn-rose w-100 btn-sm" id="logoutTrigger" style="font-size: 12px; padding: 6px; display: flex; align-items: center; justify-content: center; gap: 6px;"> <i class="fa-solid fa-sign-out-alt"></i> ออกจากระบบ </button> </div> </div> </div> `; } else { authContainer.innerHTML = ` <div style="display:flex; align-items:center; gap:8px;"> <button class="btn btn-sky-outline btn-sm" id="themeToggleBtn" title="สลับโหมดสี" style="padding: 4px 8px; display: flex; align-items: center; justify-content: center; border-radius: 50%; width: 32px; height: 32px;"> <i class="fa-solid fa-sun"></i> </button> <button class="btn btn-sky btn-sm" onclick="navigateTo('auth')"> <i class="fa-solid fa-arrow-right-to-bracket"></i> เข้าสู่ระบบ </button> </div> `; } // Bind dynamic dropdown listeners bindProfileDropdownListeners(); // Update Hero Slider Widgets if (typeof updateHeroWidgets === 'function') { updateHeroWidgets(); } } function bindProfileDropdownListeners() { const trigger = document.getElementById("profileTriggerBtn"); const dropdown = document.getElementById("profileDropdownMenu"); if (trigger && dropdown) { trigger.addEventListener("click", (e) => { e.stopPropagation(); const isOpen = dropdown.style.display === "block"; dropdown.style.display = isOpen ? "none" : "block"; }); // Click outside to close document.addEventListener("click", (e) => { if (!trigger.contains(e.target) && !dropdown.contains(e.target)) { dropdown.style.display = "none"; } }); } const navToProfileBtn = document.getElementById("navToClientProfileBtn"); if (navToProfileBtn) { navToProfileBtn.addEventListener("click", () => { const dropdown = document.getElementById("profileDropdownMenu"); if (dropdown) dropdown.style.display = "none"; navigateTo("profile"); }); } // Bind Theme Button inside Dropdown or Guest area const themeBtn = document.getElementById("themeToggleBtn"); if (themeBtn) { const isDark = document.body.classList.contains("dark-theme"); updateThemeIcon(themeBtn, isDark ? "dark" : "light"); themeBtn.addEventListener("click", (e) => { e.stopPropagation(); const nowDark = document.body.classList.toggle("dark-theme"); const newTheme = nowDark ? "dark" : "light"; localStorage.setItem("sk_theme", newTheme); updateThemeIcon(themeBtn, newTheme); if (state.currentRole === "admin") { const adminDashboardSec = document.getElementById("admin-page-dashboard"); if (adminDashboardSec && adminDashboardSec.style.display !== "none") { renderAdminDashboard(); } } }); } // Bind Account Switch items inside Dropdown document.querySelectorAll("#profileDropdownMenu .dropdown-account-item").forEach(item => { item.addEventListener("click", (e) => { e.stopPropagation(); const username = item.getAttribute("data-username"); const dropdown = document.getElementById("profileDropdownMenu"); if (dropdown) dropdown.style.display = "none"; quickSwitchAccount(username); }); }); const addAccountBtn = document.getElementById("btnAddOtherAccountInDropdown"); if (addAccountBtn) { addAccountBtn.addEventListener("click", (e) => { e.stopPropagation(); const dropdown = document.getElementById("profileDropdownMenu"); if (dropdown) dropdown.style.display = "none"; navigateTo("auth"); }); } // Bind Reset Database Toggler const resetDbBtn = document.getElementById("resetDbBtn"); if (resetDbBtn) { resetDbBtn.addEventListener("click", (e) => { e.stopPropagation(); if (confirm("คุณต้องการล้างข้อมูลระบบทั้งหมดกลับเป็นค่าเริ่มต้นใช่หรือไม่? (ลบสินค้า, คำสั่งซื้อ, และรีวิวที่เพิ่มเข้ามาใหม่)")) { localStorage.removeItem("sk_products"); localStorage.removeItem("sk_orders"); localStorage.removeItem("sk_coupons"); localStorage.removeItem("sk_reviews"); localStorage.removeItem("sk_user"); showToast("ล้างข้อมูลระบบเรียบร้อยแล้ว กำลังรีโหลดหน้าเว็บ...", "success"); setTimeout(() => { window.location.reload(); }, 1000); } }); } // Bind Logout Button const logoutBtn = document.getElementById("logoutTrigger"); if (logoutBtn) { logoutBtn.addEventListener("click", (e) => { e.stopPropagation(); if (confirm("ยืนยันออกจากระบบ?")) { db.setCurrentUser(null); state.wishlist = []; updateAuthHeader(); if (typeof updateWishlistBadge === "function") { updateWishlistBadge(); } navigateTo("home"); } }); } } function initSwitchAccountModalHandlers() { const closeBtn = document.getElementById("closeSwitchAccountModalBtn"); const modal = document.getElementById("switchAccountModal"); const form = document.getElementById("switchAccountForm"); if (closeBtn && modal) { closeBtn.addEventListener("click", () => { modal.style.display = "none"; }); } if (modal) { modal.addEventListener("click", (e) => { if (e.target === modal) { modal.style.display = "none"; } }); } if (form) { form.addEventListener("submit", (e) => { e.preventDefault(); const targetUsername = document.getElementById("switchAccountTargetUsername").value; const passwordInput = document.getElementById("switchAccountPassword").value; const success = verifyAndSwitchAccount(targetUsername, passwordInput); if (success && modal) { modal.style.display = "none"; } }); } } // ========================================================================== // 5. PRODUCT CATALOG & MARKETPLACE HOME // ========================================================================== function initFilters() { const filterCatDropdown = document.getElementById("filterCategory"); const adminStockCatDropdown = document.getElementById("adminStockFilterCategory"); const sellerStockCatDropdown = document.getElementById("sellerStockFilterCategory"); const newProdCatSelect = document.getElementById("newProdCategory"); const editProdCatSelect = document.getElementById("editProdCategory"); if (filterCatDropdown) { filterCatDropdown.innerHTML = `<option value="all">หมวดหมู่ทั้งหมด</option>`; DEFAULT_CATEGORIES.forEach(c => { filterCatDropdown.innerHTML += `<option value="${c.name}">${c.name}</option>`; }); } if (adminStockCatDropdown) { adminStockCatDropdown.innerHTML = `<option value="all">ทุกหมวดหมู่</option>`; DEFAULT_CATEGORIES.forEach(c => { adminStockCatDropdown.innerHTML += `<option value="${c.name}">${c.name}</option>`; }); } if (sellerStockCatDropdown) { sellerStockCatDropdown.innerHTML = `<option value="all">ทุกหมวดหมู่</option>`; DEFAULT_CATEGORIES.forEach(c => { sellerStockCatDropdown.innerHTML += `<option value="${c.name}">${c.name}</option>`; }); } if (newProdCatSelect) { newProdCatSelect.innerHTML = ""; DEFAULT_CATEGORIES.forEach(c => { newProdCatSelect.innerHTML += `<option value="${c.name}">${c.name}</option>`; }); } if (editProdCatSelect) { editProdCatSelect.innerHTML = ""; DEFAULT_CATEGORIES.forEach(c => { editProdCatSelect.innerHTML += `<option value="${c.name}">${c.name}</option>`; }); } // Accordion Collapsible Filter Groups Listener (Default closed, click to open/close) document.querySelectorAll(".filter-group-header").forEach(header => { header.addEventListener("click", () => { const group = header.closest(".filter-group"); if (group) { group.classList.toggle("open"); } }); }); // Rating Search Mode Toggle Listener (แบบขั้นต่ำ vs เจาะจง) document.querySelectorAll("input[name='ratingMode']").forEach(radio => { radio.addEventListener("change", (e) => { state.activeFilters.ratingMode = e.target.value; state.activeFilters.ratingMin = 0; state.activeFilters.ratings = []; document.querySelectorAll(".rating-mode-btn").forEach(btn => { const input = btn.querySelector("input"); btn.classList.toggle("active", !!(input && input.checked)); }); updateRatingChipsUI(); renderCatalog(); }); }); // Rating Chips Click Listener document.querySelectorAll(".rating-chip").forEach(chip => { chip.addEventListener("click", (e) => { e.preventDefault(); const val = parseInt(chip.getAttribute("data-rating")); const mode = state.activeFilters.ratingMode; if (mode === "min") { state.activeFilters.ratingMin = val; } else { if (val === 0) { state.activeFilters.ratings = []; } else { const idx = state.activeFilters.ratings.indexOf(val); if (idx > -1) { state.activeFilters.ratings.splice(idx, 1); } else { state.activeFilters.ratings.push(val); } } } updateRatingChipsUI(); renderCatalog(); }); }); // Populate Seller Filter Dropdown const filterSellerDropdown = document.getElementById("filterSeller"); if (filterSellerDropdown) { filterSellerDropdown.innerHTML = `<option value="all">ร้านค้าทั้งหมด</option>`; const users = db.getUsers(); const sellers = users.filter(u => u.role === "seller"); sellers.forEach(s => { const badgeTag = s.sellerType === "corporate" ? " [Official]" : ""; filterSellerDropdown.innerHTML += `<option value="${s.username}">${s.name}${badgeTag}</option>`; }); filterSellerDropdown.addEventListener("change", (e) => { state.activeFilters.sellerId = e.target.value; renderCatalog(); }); } // Populate Marketplace horizontal icon navigator const marketCatNav = document.getElementById("marketCategoryNav"); if (marketCatNav) { marketCatNav.innerHTML = ""; DEFAULT_CATEGORIES.forEach(c => { const div = document.createElement("div"); div.className = "market-category-item"; div.setAttribute("data-cat", c.name); div.innerHTML = ` <div class="market-category-icon"><i class="fa-solid ${c.icon}"></i></div> <span class="market-category-label">${c.name}</span> `; div.addEventListener("click", () => { const isActive = div.classList.contains("active"); document.querySelectorAll(".market-category-item").forEach(item => item.classList.remove("active")); if (isActive) { state.activeFilters.category = "all"; if (filterCatDropdown) filterCatDropdown.value = "all"; } else { div.classList.add("active"); state.activeFilters.category = c.name; if (filterCatDropdown) filterCatDropdown.value = c.name; } renderCatalog(); }); marketCatNav.appendChild(div); }); } // Event hooks if (filterCatDropdown) { filterCatDropdown.addEventListener("change", (e) => { state.activeFilters.category = e.target.value; // Match horizontal icon selected state document.querySelectorAll(".market-category-item").forEach(item => { if (item.getAttribute("data-cat") === e.target.value) { item.classList.add("active"); } else { item.classList.remove("active"); } }); renderCatalog(); }); } // Accordion Collapsible Headers Toggle Handler document.querySelectorAll(".filter-group-header").forEach(header => { header.addEventListener("click", () => { const group = header.closest(".filter-group"); if (group) group.classList.toggle("collapsed"); }); }); // Global search inputs const globalSearchInput = document.getElementById("globalSearchBar"); const globalSearchBtn = document.getElementById("globalSearchBtn"); if (globalSearchInput && globalSearchBtn) { const handleGlobalSearch = () => { state.activeFilters.search = globalSearchInput.value.toLowerCase().trim(); renderCatalog(); }; globalSearchBtn.addEventListener("click", handleGlobalSearch); globalSearchInput.addEventListener("keypress", (e) => { if (e.key === "Enter") handleGlobalSearch(); }); } const filterMin = document.getElementById("filterPriceMin"); if (filterMin) { filterMin.addEventListener("input", (e) => { state.activeFilters.priceMin = e.target.value ? parseFloat(e.target.value) : null; updateQuickPriceChips(); renderCatalog(); }); } const filterMax = document.getElementById("filterPriceMax"); if (filterMax) { filterMax.addEventListener("input", (e) => { state.activeFilters.priceMax = e.target.value ? parseFloat(e.target.value) : null; updateQuickPriceChips(); renderCatalog(); }); } // Quick Price Presets Chips document.querySelectorAll(".quick-price-chip").forEach(btn => { btn.addEventListener("click", () => { const min = btn.getAttribute("data-min") !== "" ? parseFloat(btn.getAttribute("data-min")) : null; const max = btn.getAttribute("data-max") !== "" ? parseFloat(btn.getAttribute("data-max")) : null; if (state.activeFilters.priceMin === min && state.activeFilters.priceMax === max && (min !== null || max !== null)) { state.activeFilters.priceMin = null; state.activeFilters.priceMax = null; if (filterMin) filterMin.value = ""; if (filterMax) filterMax.value = ""; } else { state.activeFilters.priceMin = min; state.activeFilters.priceMax = max; if (filterMin) filterMin.value = min !== null ? min : ""; if (filterMax) filterMax.value = max !== null ? max : ""; } updateQuickPriceChips(); renderCatalog(); }); }); // Promotions Checkboxes (Free Shipping / Has Coupon) const filterFreeShipping = document.getElementById("filterFreeShipping"); if (filterFreeShipping) { filterFreeShipping.addEventListener("change", (e) => { state.activeFilters.freeShipping = e.target.checked; const chip = document.getElementById("chipFreeShipping"); if (chip) chip.classList.toggle("active", e.target.checked); renderCatalog(); }); } const filterHasCoupon = document.getElementById("filterHasCoupon"); if (filterHasCoupon) { filterHasCoupon.addEventListener("change", (e) => { state.activeFilters.hasCoupon = e.target.checked; const chip = document.getElementById("chipHasCoupon"); if (chip) chip.classList.toggle("active", e.target.checked); renderCatalog(); }); } const resetAllFilters = () => { if (globalSearchInput) globalSearchInput.value = ""; if (filterCatDropdown) filterCatDropdown.value = "all"; if (filterSellerDropdown) filterSellerDropdown.value = "all"; if (filterMin) filterMin.value = ""; if (filterMax) filterMax.value = ""; if (filterFreeShipping) filterFreeShipping.checked = false; const chipShip = document.getElementById("chipFreeShipping"); if (chipShip) chipShip.classList.remove("active"); if (filterHasCoupon) filterHasCoupon.checked = false; const chipCoup = document.getElementById("chipHasCoupon"); if (chipCoup) chipCoup.classList.remove("active"); const modeMinRadio = document.querySelector("input[name='ratingMode'][value='min']"); if (modeMinRadio) modeMinRadio.checked = true; document.querySelectorAll(".rating-mode-btn").forEach(btn => { const input = btn.querySelector("input"); btn.classList.toggle("active", !!(input && input.checked)); }); document.querySelectorAll(".market-category-item").forEach(item => item.classList.remove("active")); updateQuickPriceChips(); state.activeFilters = { search: "", category: "all", sellerId: "all", priceMin: null, priceMax: null, ratingMode: "min", ratingMin: 0, ratings: [], freeShipping: false, hasCoupon: false, colors: [], sizes: [] }; updateRatingChipsUI(); renderCatalog(); }; const clearFiltersBtn = document.getElementById("clearFiltersBtn"); if (clearFiltersBtn) { clearFiltersBtn.addEventListener("click", resetAllFilters); } const clearAllTagsBtn = document.getElementById("clearAllTagsBtn"); if (clearAllTagsBtn) { clearAllTagsBtn.addEventListener("click", resetAllFilters); } const sortBySelect = document.getElementById("sortBy"); if (sortBySelect) { sortBySelect.addEventListener("change", (e) => { state.sortBy = e.target.value; renderCatalog(); }); } } function updateRatingChipsUI() { const mode = state.activeFilters.ratingMode; document.querySelectorAll(".rating-chip").forEach(chip => { const val = parseInt(chip.getAttribute("data-rating")); if (mode === "min") { chip.classList.toggle("active", val === state.activeFilters.ratingMin); } else { if (state.activeFilters.ratings.length === 0) { chip.classList.toggle("active", val === 0); } else { chip.classList.toggle("active", val !== 0 && state.activeFilters.ratings.includes(val)); } } }); } function updateQuickPriceChips() { const currentMin = state.activeFilters.priceMin; const currentMax = state.activeFilters.priceMax; document.querySelectorAll(".quick-price-chip").forEach(btn => { const min = btn.getAttribute("data-min") !== "" ? parseFloat(btn.getAttribute("data-min")) : null; const max = btn.getAttribute("data-max") !== "" ? parseFloat(btn.getAttribute("data-max")) : null; if (currentMin === min && currentMax === max && (min !== null || max !== null)) { btn.classList.add("active"); } else { btn.classList.remove("active"); } }); } function renderActiveFilterTags() { const wrapper = document.getElementById("activeFilterTagsWrapper"); const container = document.getElementById("activeFilterTagsContainer"); if (!wrapper || !container) return; let tagsHtml = ""; const f = state.activeFilters; if (f.search) { tagsHtml += `<span class="active-tag" data-filter="search">ค้นหา: "${f.search}" <i class="fa-solid fa-xmark remove-tag-btn" data-filter="search"></i></span>`; } if (f.category !== "all") { tagsHtml += `<span class="active-tag" data-filter="category">หมวดหมู่: ${f.category} <i class="fa-solid fa-xmark remove-tag-btn" data-filter="category"></i></span>`; } if (f.sellerId !== "all") { const sellerObj = db.getUsers().find(u => u.username === f.sellerId); const sellerName = sellerObj ? sellerObj.name : f.sellerId; tagsHtml += `<span class="active-tag" data-filter="sellerId">ร้าน: ${sellerName} <i class="fa-solid fa-xmark remove-tag-btn" data-filter="sellerId"></i></span>`; } const minP = (f.priceMin !== undefined && f.priceMin !== null) ? f.priceMin : ((f.minPrice !== undefined && f.minPrice !== null) ? f.minPrice : null); const maxP = (f.priceMax !== undefined && f.priceMax !== null) ? f.priceMax : ((f.maxPrice !== undefined && f.maxPrice !== null) ? f.maxPrice : null); if (minP !== null || maxP !== null) { let priceTxt = ""; if (minP !== null && maxP !== null) priceTxt = `฿${minP.toLocaleString()} - ฿${maxP.toLocaleString()}`; else if (minP !== null) priceTxt = `>= ฿${minP.toLocaleString()}`; else if (maxP !== null) priceTxt = `<= ฿${maxP.toLocaleString()}`; tagsHtml += `<span class="active-tag" data-filter="price">ราคา: ${priceTxt} <i class="fa-solid fa-xmark remove-tag-btn" data-filter="price"></i></span>`; } if (f.ratingMode === "min" && f.ratingMin > 0) { tagsHtml += `<span class="active-tag" data-filter="rating">⭐ ${f.ratingMin} ดาวขึ้นไป <i class="fa-solid fa-xmark remove-tag-btn" data-filter="rating"></i></span>`; } else if (f.ratingMode === "exact" && Array.isArray(f.ratings) && f.ratings.length > 0) { const sorted = f.ratings.slice().sort((a, b) => b - a).map(r => r + '★').join(', '); tagsHtml += `<span class="active-tag" data-filter="rating">⭐ เจาะจง: ${sorted} <i class="fa-solid fa-xmark remove-tag-btn" data-filter="rating"></i></span>`; } if (f.freeShipping) { tagsHtml += `<span class="active-tag" data-filter="freeShipping">🚚 ส่งฟรี <i class="fa-solid fa-xmark remove-tag-btn" data-filter="freeShipping"></i></span>`; } if (f.hasCoupon) { tagsHtml += `<span class="active-tag" data-filter="hasCoupon">🎟️ มีคูปอง <i class="fa-solid fa-xmark remove-tag-btn" data-filter="hasCoupon"></i></span>`; } if (tagsHtml) { container.innerHTML = tagsHtml; wrapper.style.display = "flex"; container.querySelectorAll(".remove-tag-btn").forEach(btn => { btn.addEventListener("click", (e) => { e.stopPropagation(); const filterKey = btn.getAttribute("data-filter"); removeSpecificFilter(filterKey); }); }); } else { container.innerHTML = ""; wrapper.style.display = "none"; } } function removeSpecificFilter(key) { if (key === "search") { state.activeFilters.search = ""; const input = document.getElementById("globalSearchBar"); if (input) input.value = ""; } else if (key === "category") { state.activeFilters.category = "all"; const catSelect = document.getElementById("filterCategory"); if (catSelect) catSelect.value = "all"; document.querySelectorAll(".market-category-item").forEach(item => item.classList.remove("active")); } else if (key === "sellerId") { state.activeFilters.sellerId = "all"; const sellerSelect = document.getElementById("filterSeller"); if (sellerSelect) sellerSelect.value = "all"; } else if (key === "price") { state.activeFilters.priceMin = null; state.activeFilters.priceMax = null; const pMin = document.getElementById("filterPriceMin"); const pMax = document.getElementById("filterPriceMax"); if (pMin) pMin.value = ""; if (pMax) pMax.value = ""; updateQuickPriceChips(); } else if (key === "rating") { state.activeFilters.ratingMin = 0; state.activeFilters.ratings = []; updateRatingChipsUI(); } else if (key === "freeShipping") { state.activeFilters.freeShipping = false; const cb = document.getElementById("filterFreeShipping"); if (cb) cb.checked = false; const chip = document.getElementById("chipFreeShipping"); if (chip) chip.classList.remove("active"); } else if (key === "hasCoupon") { state.activeFilters.hasCoupon = false; const cb = document.getElementById("filterHasCoupon"); if (cb) cb.checked = false; const chip = document.getElementById("chipHasCoupon"); if (chip) chip.classList.remove("active"); } renderCatalog(); } function renderCatalog() { const productsGrid = document.getElementById("productsGrid"); const resultsCount = document.getElementById("resultsCount"); if (!productsGrid) return; state.products = db.getProducts(); // Render Seller Store Header Banner const sellerContainer = document.getElementById("sellerStoreHeaderContainer"); const sellerId = state.activeFilters.sellerId; if (sellerContainer) { if (sellerId === "all") { sellerContainer.innerHTML = ""; sellerContainer.style.display = "none"; } else { const sellerObj = db.getUsers().find(u => u.username === sellerId); const sellerName = sellerObj ? sellerObj.name : (sampleProd ? (sampleProd.sellerName || "SkyMall") : "ร้านค้าผู้ขาย"); const sellerProds = state.products.filter(p => p.sellerId === sellerId && p.active); const avgRating = sellerProds.length > 0 ? (sellerProds.reduce((sum, p) => sum + p.rating, 0) / sellerProds.length).toFixed(1) : "5.0"; const sellerProdCount = sellerProds.length; const followed = getFollowedSellers(); const isFollowing = followed.includes(sellerId); const followBtnText = isFollowing ? "<i class='fa-solid fa-check'></i> ติดตามแล้ว" : "<i class='fa-solid fa-plus'></i> ติดตามร้านค้า"; const followBtnClass = isFollowing ? "btn btn-sky btn-sm" : "btn btn-sky-outline btn-sm"; const companySub = (sellerObj && sellerObj.companyName) ? `<div style="font-size:12px; opacity:0.85; margin-top:2px;"><i class="fa-solid fa-building"></i> ${sellerObj.companyName}</div>` : ''; const sloganHtml = (sellerObj && sellerObj.slogan) ? `<div style="font-size:12px; opacity:0.95; margin-top:4px;"><i class="fa-solid fa-quote-left"></i> ${sellerObj.slogan}</div>` : ''; const avatarBoxHtml = (sellerObj && sellerObj.avatar) ? `<img src="${sellerObj.avatar}" style="width:100%; height:100%; object-fit:cover; border-radius:50%;">` : '<i class="fa-solid fa-store"></i>'; const hasCover = !!(sellerObj && sellerObj.coverBanner); const bannerBgStyle = hasCover ? `background: linear-gradient(rgba(15, 23, 42, 0.75), rgba(15, 23, 42, 0.85)), url(${sellerObj.coverBanner}) center/cover no-repeat;` : ''; const hasCoverClass = hasCover ? 'has-cover' : ''; const contactBadges = sellerObj ? ` <div style="display:flex; flex-wrap:wrap; gap:8px; margin-top:8px;"> ${sellerObj.storePhone ? `<span class="badge badge-dark" style="font-size:11px; background:rgba(255,255,255,0.15);"><i class="fa-solid fa-phone"></i> ${sellerObj.storePhone}</span>` : ''} ${sellerObj.cutoffTime ? `<span class="badge badge-sky" style="font-size:11px;"><i class="fa-solid fa-clock"></i> ตัดรอบ ${sellerObj.cutoffTime}</span>` : ''} ${sellerObj.primaryCarrier ? `<span class="badge badge-dark" style="font-size:11px; background:#0d9488;"><i class="fa-solid fa-truck-fast"></i> ${sellerObj.primaryCarrier}</span>` : ''} ${sellerObj.warrantyPolicy ? `<span class="badge badge-warning" style="font-size:11px;"><i class="fa-solid fa-shield-halved"></i> รับประกันสินค้า</span>` : ''} </div> ` : ''; const vacationBannerHtml = (sellerObj && sellerObj.isVacationMode) ? ` <div class="vacation-alert-banner" style="margin-top:14px; width:100%; grid-column: 1 / -1;"> <i class="fa-solid fa-umbrella-beach" style="font-size:1.4rem;"></i> <div> <strong>ร้านค้านี้กำลังอยู่ในช่วงพักร้อนชั่วคราว (Vacation Mode)</strong> <div style="font-size:12px;">ทางร้านระงับการสั่งซื้อชั่วคราวเพื่อจัดเตรียมสินค้า ขออภัยในความไม่สะดวกค่ะ</div> </div> </div> ` : ''; sellerContainer.style.display = "block"; sellerContainer.innerHTML = ` <div class="seller-store-banner ${hasCoverClass}" style="${bannerBgStyle}"> <div class="seller-store-left"> <div class="seller-store-avatar"> ${avatarBoxHtml} </div> <div class="seller-store-details"> <h3 class="seller-store-title">${sellerName}${getOfficialBadgeHtml(sellerId)}</h3> ${companySub} ${sloganHtml} ${contactBadges} <div class="seller-store-rating" style="margin-top:6px;"> <span><i class="fa-solid fa-star"></i> ${avgRating} / 5.0</span> <span>•</span> <span>สินค้าทั้งหมด ${sellerProdCount} ชิ้น</span> </div> </div> </div> <div class="seller-store-actions"> <button class="${followBtnClass}" id="btnFollowSeller" data-id="${sellerId}">${followBtnText}</button> </div> ${vacationBannerHtml} </div> `; const followBtn = sellerContainer.querySelector("#btnFollowSeller"); if (followBtn) { followBtn.addEventListener("click", () => { toggleFollowSeller(sellerId); renderCatalog(); }); } } } let filtered = state.products.filter(p => { if (!p.active) return false; const nameMatch = p.name.toLowerCase().includes(state.activeFilters.search) || (p.sellerName && p.sellerName.toLowerCase().includes(state.activeFilters.search)); const catMatch = state.activeFilters.category === "all" || p.category === state.activeFilters.category; const sellerMatch = state.activeFilters.sellerId === "all" || p.sellerId === state.activeFilters.sellerId; const minPriceMatch = state.activeFilters.priceMin == null || p.price >= state.activeFilters.priceMin; const maxPriceMatch = state.activeFilters.priceMax == null || p.price <= state.activeFilters.priceMax; let ratingMatch = true; if (state.activeFilters.ratingMode === "min") { ratingMatch = p.rating >= state.activeFilters.ratingMin; } else { ratingMatch = state.activeFilters.ratings.length === 0 || state.activeFilters.ratings.includes(p.rating); } const freeShippingMatch = !state.activeFilters.freeShipping || p.freeShipping === true || p.isFreeShipping === true || p.price >= 500; const hasCouponMatch = !state.activeFilters.hasCoupon || p.hasCoupon === true || p.couponCode || (p.discount && p.discount > 0) || p.price > 1000; return nameMatch && catMatch && sellerMatch && minPriceMatch && maxPriceMatch && ratingMatch && freeShippingMatch && hasCouponMatch; }); renderActiveFilterTags(); if (state.sortBy === "price-asc") { filtered.sort((a, b) => a.price - b.price); } else if (state.sortBy === "price-desc") { filtered.sort((a, b) => b.price - a.price); } else if (state.sortBy === "rating-desc") { filtered.sort((a, b) => b.rating - a.rating); } if (resultsCount) resultsCount.innerText = `พบสินค้าทั้งหมด ${filtered.length} รายการ`; // Render Skeleton Loading Cards const skeletonCount = Math.min(8, Math.max(4, filtered.length)); const skeletonHtml = Array(skeletonCount).fill(0).map(() => ` <div class="skeleton-card"> <div class="skeleton-box skeleton-img"></div> <div class="skeleton-box skeleton-title"></div> <div class="skeleton-box skeleton-price"></div> </div> `).join(""); productsGrid.innerHTML = skeletonHtml; setTimeout(() => { productsGrid.innerHTML = ""; if (filtered.length === 0) { productsGrid.innerHTML = `<div class="text-center w-100 py-5" style="grid-column: 1/-1;"><p style="color: var(--color-sky-slate)">ไม่พบสินค้าที่คุณค้นหา</p></div>`; return; } filtered.forEach(p => { const totalStock = p.variants ? p.variants.reduce((sum, v) => sum + v.stock, 0) : 0; const card = document.createElement("div"); card.className = "product-card"; const placeholderImg = DEFAULT_IMAGE_PLACEHOLDERS[p.id] || DEFAULT_IMAGE_PLACEHOLDERS["default"]; const imgSrc = p.image || placeholderImg; const sellerObj = db.getUsers().find(u => u.username === p.sellerId); const isVacation = sellerObj && sellerObj.isVacationMode; const isLiked = state.wishlist.includes(p.id); card.innerHTML = ` <button class="product-wishlist-btn ${isLiked ? 'active' : ''}" data-id="${p.id}" title="${isLiked ? 'ยกเลิกถูกใจ' : 'ถูกใจสินค้า'}"> <i class="fa-solid fa-heart"></i> </button> <div class="product-image-wrapper"> <div class="product-badge-overlay"> ${isVacation ? '<span class="badge badge-warning" style="background:#f59e0b; color:white;"><i class="fa-solid fa-umbrella-beach"></i> ร้านพักร้อน</span>' : ''} ${totalStock === 0 ? '<span class="badge badge-danger">หมดชั่วคราว</span>' : ''} </div> <img src="${imgSrc}" class="product-img" alt="${p.name}"> </div> <div class="product-details"> <span class="product-category">${p.category}</span> <h4 class="product-title">${p.name}</h4> <div class="product-meta-row"> <span class="product-seller-name"><i class="fa-solid fa-store"></i> <strong>${p.sellerName || 'SkyMall'}</strong>${getOfficialBadgeHtml(p.sellerId)}</span> <span class="product-rating-inline"><i class="fa-solid fa-star text-warning"></i> ${p.rating.toFixed(1)}</span> </div> <div class="product-bottom"> <span class="product-price">฿${p.price.toLocaleString()}</span> <button class="btn-card-action view-detail-btn" data-id="${p.id}" title="ดูรายละเอียดสินค้า"> <i class="fa-solid fa-eye"></i> </button> </div> </div> `; // Whole Card Click Event Handler card.addEventListener("click", () => { openProductDetail(p.id); }); // Detail Button Click Event const detailBtn = card.querySelector(".view-detail-btn"); if (detailBtn) { detailBtn.addEventListener("click", (e) => { e.stopPropagation(); openProductDetail(p.id); }); } // Wishlist Toggle Event with stopPropagation const wishBtn = card.querySelector(".product-wishlist-btn"); if (wishBtn) { wishBtn.addEventListener("click", (e) => { e.stopPropagation(); if (!state.currentUser) { navigateTo("auth"); return; } toggleWishlist(p.id); }); } productsGrid.appendChild(card); }); }, 1000); } // ========================================================================== // 6. WISHLIST MANAGEMENT // ========================================================================== function updateWishlistBadge() { const countLabel = document.getElementById("wishlistCountLabel"); if (countLabel) { const user = db.getCurrentUser(); if (!user) { countLabel.innerText = "0"; state.wishlist = []; } else { const wish = db.getWishlist(); state.wishlist = wish; countLabel.innerText = wish.length; } } } function toggleWishlist(productId) { const user = db.getCurrentUser(); if (!user) { showToast("กรุณาเข้าสู่ระบบก่อนกดถูกใจสินค้า", "danger"); navigateTo("auth"); return; } let wish = db.getWishlist(); const idx = wish.indexOf(productId); const isAdded = idx === -1; if (idx > -1) { wish.splice(idx, 1); } else { wish.push(productId); } db.saveWishlist(wish); state.wishlist = wish; updateWishlistBadge(); const wishlistBtns = document.querySelectorAll(`.product-wishlist-btn[data-id="${productId}"]`); wishlistBtns.forEach(btn => { if (isAdded) { btn.classList.add("active"); } else { btn.classList.remove("active"); } }); const wishlistSection = document.getElementById("page-wishlist"); if (wishlistSection && wishlistSection.style.display === "block") { renderWishlist(); } } // ========================================================================== // 7. CLIENT PRODUCT DETAILS MODAL (Options selector & Specifications) // ========================================================================== const MOCK_REVIEWS = { "prod-1": [ { username: "@somchai_r", rating: 5, date: "2026-07-15", text: "จัดส่งรวดเร็วมากครับ สินค้าแพ็คมาอย่างดี ของแท้แน่นอน แนะนำร้านนี้เลยครับ!" }, { username: "@kitti_k", rating: 4, date: "2026-07-10", text: "กล้องซูมได้ชัดมากครับ ชิป AI เร็วใช้ได้ แต่แบตเตอรี่หมดเร็วกว่าที่คิดนิดหน่อย โดยรวมยังดีครับ" }, { username: "@nattapong_m", rating: 5, date: "2026-07-08", text: "ประทับใจความไหลลื่นของหน้าจอมาก สีไทเทเนียมสวยหรูหรามาก คุ้มราคาที่สุด" }, { username: "@patty_s", rating: 5, date: "2026-07-01", text: "ดีไซน์กล้องสวยมาก ขนส่งสุภาพ บริการประทับใจค่ะ" }, { username: "@anuchit_t", rating: 3, date: "2026-06-25", text: "สินค้าดี แต่แถมหัวชาร์จช้าไปนิดนึง ลำโพงเสียงยังไม่แน่นเท่าไหร่" }, { username: "@wipha_j", rating: 2, date: "2026-06-18", text: "ขนส่งช้าไปหน่อย กล่องบุบ แต่ตัวเครื่องไม่มีรอยเสียหายค่ะ" } ], "prod-2": [ { username: "@somchai_r", rating: 5, date: "2026-07-14", text: "ตัดเสียงรบกวนได้เงียบดีมากครับ เบสแน่นสะใจ ใส่สบายหูไม่เจ็บเลย" }, { username: "@kitti_k", rating: 4, date: "2026-07-13", text: "คุณภาพดีมากคุ้มราคา ขนาดตรงตามรายละเอียดที่ลงไว้ครับ" }, { username: "@wilai_p", rating: 5, date: "2026-07-05", text: "เสียงดีมากค่ะ คุยโทรศัพท์ไมค์ชัดเจน ไม่มีเสียงแทรกคุ้มค่ามาก" }, { username: "@tewan_s", rating: 3, date: "2026-06-30", text: "ฟีเจอร์ตัดเสียงรบกวนใช้ได้ แต่เชื่อมต่อกับมือถือบางแบรนด์ยังช้าอยู่นะครับ" }, { username: "@chalit_a", rating: 1, date: "2026-06-20", text: "ได้รับของแล้วหูข้างขวาไม่มีเสียงเชื่อมต่อไม่ได้เลยครับ ต้องส่งเปลี่ยน เสียเวลามาก" } ], "prod-3": [ { username: "@nat_h", rating: 5, date: "2026-07-16", text: "ผ้านุ่มใส่สบายมาก ลายสกรีนเมฆสีฟ้าน่ารักตรงปกสุดๆ แนะนำค่ะ" }, { username: "@piti_s", rating: 5, date: "2026-07-12", text: "เนื้อผ้าหนากำลังดี ตัดเย็บเรียบร้อย ทรงสวยตรงปกส่งไวมากครับ" }, { username: "@somsri_t", rating: 4, date: "2026-07-06", text: "เสื้อสวยดีค่ะ แต่ไซส์ XL แอบใหญ่กว่าปกตินิดนึง" }, { username: "@chalong_c", rating: 3, date: "2026-06-28", text: "ผ้าสีฟ้าสวยงาม แต่ซักครั้งแรกแอบมีสีตกเล็กน้อย ควรซักแยกนะครับ" } ], "prod-4": [ { username: "@anong_k", rating: 5, date: "2026-07-17", text: "ใช้งานง่ายมาก หน้าจอสัมผัสลื่นไหล ทอดเฟรนช์ฟรายส์กรอบอร่อยโดยไม่ต้องใช้น้ำมันเลย ชอบมากค่ะ" }, { username: "@pricha_t", rating: 5, date: "2026-07-14", text: "ขนาดใหญ่จุใจ 5.5 ลิตร ทำไก่ทอดได้เป็นตัวๆ เลย ล้างทำความสะอาดง่ายด้วยครับ" }, { username: "@mana_s", rating: 4, date: "2026-07-10", text: "ทอดอาหารกรอบดีมากครับ แต่เสียงพัดลมเครื่องแอบดังไปนิดหน่อยตอนทำงาน" }, { username: "@sudaporn_c", rating: 5, date: "2026-07-01", text: "ใช้ดีจริงค่ะ ซื้อให้คุณแม่ใช้ ทำอาหารเช้าง่ายขึ้นเยอะเลย" } ], "prod-5": [ { username: "@suda_m", rating: 5, date: "2026-07-15", text: "ซึมไวไม่เหนียวเหนอะหนะ ใช้แล้วผิวใสขึ้นอย่างเห็นได้ชัดในหนึ่งสัปดาห์ ไม่แพ้เลยค่ะ" }, { username: "@pim_p", rating: 4, date: "2026-07-09", text: "เนื้อเซรั่มใสดี ไม่มีกลิ่นน้ำหอมฉุน จุดด่างดำจางลงนิดหน่อย ต้องใช้ระยะยาวค่ะ" }, { username: "@warat_y", rating: 5, date: "2026-07-02", text: "คุ้มค่าราคาประหยัด ใช้ดีมาก ผิวแพ้ง่ายใช้ได้สบายๆ ครับ แนะนำ" }, { username: "@urai_l", rating: 3, date: "2026-06-20", text: "ชุ่มชื้นปานกลาง ไม่ค่อยเห็นผลเรื่องหน้าใสเท่าไหร่ แต่ผิวดูเรียบเนียนขึ้นบ้าง" } ], "prod-6": [ { username: "@sayan_t", rating: 4, date: "2026-07-12", text: "สภาพเครื่อง 90% ตามที่แจ้งเลยครับ ล้างสะอาดมาก ประหยัดเวลาล้างจานไปได้เยอะ" }, { username: "@jirapon_p", rating: 3, date: "2026-07-05", text: "ใช้งานล้างได้สะอาดดี แต่เมนูเป็นภาษาญี่ปุ่นทั้งหมด ต้องงมหาคู่มือเอาครับ" }, { username: "@metha_n", rating: 2, date: "2026-06-25", text: "ตัวเครื่องล้างสะอาด แต่สายระบายน้ำทิ้งสั้นเกินไป ต้องซื้อสายต่อเพิ่มเองครับ" } ] }; function getProductReviews(productId) { if (MOCK_REVIEWS[productId]) { return MOCK_REVIEWS[productId]; } const product = db.getProducts().find(p => p.id === productId); const rating = product ? product.rating : 5; return [ { username: "@customer_a", rating: rating, date: "2026-07-16", text: "ได้รับสินค้าเรียบร้อยดีครับ ตรงปกและจัดส่งรวดเร็วมาก" }, { username: "@customer_b", rating: Math.max(1, rating - 1), date: "2026-07-15", text: "คุณภาพเหมาะสมกับราคา ใช้งานได้ปกติไม่มีปัญหาอะไรครับ" } ]; } function createFlyingCartAnimation(startX, startY, imageSource) { const cartTrigger = document.getElementById("cartTrigger"); if (!cartTrigger) return; const targetRect = cartTrigger.getBoundingClientRect(); const targetX = targetRect.left + targetRect.width / 2; const targetY = targetRect.top + targetRect.height / 2; const img = document.createElement("img"); img.src = imageSource; img.className = "flying-cart-item"; img.style.left = `${startX}px`; img.style.top = `${startY}px`; img.style.width = "50px"; img.style.height = "50px"; img.style.objectFit = "cover"; document.body.appendChild(img); requestAnimationFrame(() => { img.style.left = `${targetX}px`; img.style.top = `${targetY}px`; img.style.width = "20px"; img.style.height = "20px"; img.style.opacity = "0.5"; img.style.transform = "scale(0.3)"; }); setTimeout(() => { if (img.parentNode) { img.parentNode.removeChild(img); } const cartIcon = cartTrigger.querySelector("i") || cartTrigger; cartIcon.classList.add("cart-bump"); setTimeout(() => { cartIcon.classList.remove("cart-bump"); }, 400); }, 1500); } function openProductDetail(productId) { const modal = document.getElementById("productDetailModal"); const content = document.getElementById("modalProductDetailContent"); const product = db.getProducts().find(p => p.id === productId); if (!product || !modal || !content) return; const placeholderImg = DEFAULT_IMAGE_PLACEHOLDERS[product.id] || DEFAULT_IMAGE_PLACEHOLDERS["default"]; const imgSrc = product.image || placeholderImg; const hasColors = product.colors && product.colors.length > 0; const hasSizes = product.variants && product.variants.some(v => v.size !== "Default" && v.size !== ""); const sellerObj = db.getUsers().find(u => u.username === product.sellerId); const isSellerOnVacation = sellerObj && sellerObj.isVacationMode; const vacationAlertModalHtml = isSellerOnVacation ? ` <div class="vacation-alert-banner" style="margin-top: 12px; font-size: 0.85rem;"> <i class="fa-solid fa-umbrella-beach" style="font-size:1.3rem;"></i> <div> <strong>ร้านค้านี้กำลังอยู่ในช่วงพักร้อนชั่วคราว (Vacation Mode)</strong> <div style="font-size:11px;">ผู้ขายปิดรับคำสั่งซื้อชั่วคราว ไม่สามารถสั่งซื้อสินค้าได้ในขณะนี้</div> </div> </div> ` : ''; content.innerHTML = ` <div class="detail-img-box"> <img src="${imgSrc}" alt="${product.name}"> </div> <div class="detail-info-box"> <div> <span class="badge badge-sky">${product.category}</span> <span class="badge badge-dark" style="background:#0f766e; color:#ffffff; margin-left: 5px;"><i class="fa-solid fa-store"></i> ร้านค้า: ${product.sellerName || 'SkyMall'}</span>${getOfficialBadgeHtml(product.sellerId)} <button class="btn btn-sky-outline btn-xs" id="btnGoToSellerShop" style="margin-left: 8px; font-size: 11px; padding: 2px 6px; border-radius: 4px;"><i class="fa-solid fa-arrow-right"></i> ไปที่ร้านค้า</button> <h2 class="mt-3">${product.name}</h2> <div style="display: flex; align-items: center; gap: 10px; margin-top: 8px; margin-bottom: 12px;"> <span style="color: #f59e0b; font-size: 1.1rem;">${"★".repeat(product.rating || 5)}${"☆".repeat(5 - (product.rating || 5))}</span> <span style="color: var(--color-sky-slate); font-size: 0.85rem;">ขายแล้ว ${Math.floor((product.price % 37) + 12)} ชิ้น</span> </div> <div class="detail-price">฿${product.price.toLocaleString()}</div> <!-- Trust Badges --> <div style="display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap;"> <span class="badge badge-sky-outline" style="font-size: 11px; padding: 4px 8px; border-radius: 4px; border: 1px solid #0d9488; color: #0d9488; background: #f0fdfa;"><i class="fa-solid fa-truck-fast"></i> ฟรีค่าจัดส่ง Standard Delivery</span> <span class="badge badge-sky-outline" style="font-size: 11px; padding: 4px 8px; border-radius: 4px; border: 1px solid #0d9488; color: #0d9488; background: #f0fdfa;"><i class="fa-solid fa-shield-halved"></i> รับประกันของแท้ 100%</span> </div> ${vacationAlertModalHtml} </div> <p class="detail-desc" style="margin-top: 15px;">${product.description}</p> <!-- Color Selection Group --> ${hasColors ? ` <div class="option-group"> <span class="option-title">เลือกรูปแบบ / ตัวเลือกสินค้า:</span> <div class="option-selector" id="modalColorSelector"> ${product.colors.map(col => `<button class="filter-chip btn-select-color" data-color="${col}">${col}</button>`).join("")} </div> </div> ` : ''} <!-- Size Selection Group --> ${hasSizes ? ` <div class="option-group"> <div class="row align-items-center"> <span class="option-title">เลือกขนาด / คุณลักษณะย่อย:</span> <a href="#" class="size-chart-link" id="toggleSizeChartBtn" style="display: ${product.category === 'แฟชั่น/เสื้อผ้า' ? 'block' : 'none'};"><i class="fa-solid fa-table"></i> ตารางไซส์เสื้อผ้า</a> </div> <div class="option-selector" id="modalSizeSelector"> <span style="font-size:0.85rem; color:var(--color-sky-slate)">โปรดเลือกตัวเลือกหลักก่อนทำการระบุคุณลักษณะย่อย</span> </div> </div> ` : ''} <!-- Apparel size chart details --> <div id="sizeChartBox" style="display: none;"> <table class="size-chart-table"> <thead> <tr> <th>Size</th> <th>รอบอก (นิ้ว)</th> <th>ความยาวเสื้อ (นิ้ว)</th> </tr> </thead> <tbody> <tr><td>S</td><td>38"</td><td>26"</td></tr> <tr><td>M</td><td>40"</td><td>27"</td></tr> <tr><td>L</td><td>42"</td><td>28"</td></tr> <tr><td>XL</td><td>46"</td><td>29"</td></tr> </tbody> </table> </div> <!-- Quantity Selector Widget --> <div class="option-group" style="margin-top: 20px;"> <span class="option-title">จำนวนที่ต้องการสั่งซื้อ:</span> <div style="display: flex; align-items: center; gap: 10px;"> <button class="btn btn-sky-outline" id="btnQtyMinus" style="padding: 2px 12px; font-weight: bold; border-radius: 4px; height: 32px;" disabled>-</button> <input type="number" id="inputQty" value="1" min="1" style="width: 60px; text-align: center; border: 1px solid var(--color-border); border-radius: 4px; padding: 4px; font-weight: bold; height: 32px;" readonly> <button class="btn btn-sky-outline" id="btnQtyPlus" style="padding: 2px 12px; font-weight: bold; border-radius: 4px; height: 32px;" disabled>+</button> <span style="font-size: 12px; color: var(--color-sky-slate);" id="spanMaxStockHint"></span> </div> </div> <div class="mt-4"> <button class="btn btn-sky w-100 btn-lg" id="addToCartBtn" disabled> <i class="fa-solid fa-cart-plus"></i> โปรดเลือกตัวเลือกสินค้าให้ครบถ้วน </button> </div> <!-- Reviews Section --> <div style="margin-top: 30px; border-top: 1px solid var(--color-border); padding-top: 20px;"> <h4 style="color: var(--color-sky-brand); margin-bottom: 15px;"><i class="fa-solid fa-comments"></i> รีวิวจากผู้ซื้อ</h4> <!-- Review Statistics & Filters --> <div class="review-filter-dashboard" style="display:flex; flex-wrap:wrap; gap:8px; margin-bottom:15px; background:var(--color-bg-gray); padding:10px; border-radius:8px; border:1px solid var(--color-border);"> <div style="display:flex; flex-direction:column; align-items:center; justify-content:center; padding-right:15px; border-right:1px solid var(--color-border); min-width:70px;"> <span style="font-size:20px; font-weight:700; color:var(--color-sky-dark); line-height:1.2;" id="modalAvgRatingLabel">5.0</span> <span style="color:#f59e0b; font-size:12px; margin-top:3px;">★★★★★</span> </div> <div style="display:flex; flex-wrap:wrap; gap:8px; flex:1; align-items:center;" id="modalReviewFilterChips"> <!-- Loaded dynamically via JS --> </div> </div> <!-- Reviews list container --> <div style="display: flex; flex-direction: column; gap: 12px; max-height: 250px; overflow-y: auto; padding-right: 5px;" id="modalReviewsList"> <!-- Loaded dynamically via JS --> </div> </div> </div> `; // --- Render Product Reviews Flow --- const reviews = getProductReviews(product.id); const avgRating = reviews.length > 0 ? (reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length).toFixed(1) : "5.0"; const avgStars = "★".repeat(Math.round(parseFloat(avgRating))) + "☆".repeat(5 - Math.round(parseFloat(avgRating))); const avgRatingLabel = content.querySelector("#modalAvgRatingLabel"); if (avgRatingLabel) { avgRatingLabel.innerHTML = `${avgRating} <span style="font-size: 10px; font-weight: normal; color: var(--color-sky-slate);">/ 5</span>`; const starSpan = avgRatingLabel.nextElementSibling; if (starSpan) starSpan.innerText = avgStars; } const starCounts = { 0: reviews.length, 5: 0, 4: 0, 3: 0, 2: 0, 1: 0 }; reviews.forEach(r => { if (starCounts[r.rating] !== undefined) { starCounts[r.rating]++; } }); let activeReviewFilter = 0; // 0 = all const renderFilteredReviews = () => { const listContainer = content.querySelector("#modalReviewsList"); if (!listContainer) return; listContainer.innerHTML = ""; const filtered = activeReviewFilter === 0 ? reviews : reviews.filter(r => r.rating === activeReviewFilter); if (filtered.length === 0) { listContainer.innerHTML = `<p style="font-size:13px; color:var(--color-sky-slate); text-align:center; padding:15px 0;">ไม่มีรีวิวในระดับคะแนนที่เลือก</p>`; return; } filtered.forEach(r => { const stars = "★".repeat(r.rating) + "☆".repeat(5 - r.rating); const div = document.createElement("div"); div.style.cssText = "background: var(--color-bg-gray); padding: 12px; border-radius: 6px; border: 1px solid var(--color-border); border-left: 3px solid var(--color-sky-brand);"; div.innerHTML = ` <div style="display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 5px;"> <strong style="color:var(--color-sky-dark);">${r.username}</strong> <div style="display:flex; flex-direction:column; align-items:flex-end; gap:2px;"> <span style="color: #f59e0b; letter-spacing:1px;">${stars}</span> <span style="color:var(--color-sky-slate); font-size:10px;">${r.date}</span> </div> </div> <p style="font-size: 13px; margin: 0; color: var(--color-sky-dark); line-height: 1.5;">${r.text}</p> `; listContainer.appendChild(div); }); }; const renderFilterChips = () => { const chipsContainer = content.querySelector("#modalReviewFilterChips"); if (!chipsContainer) return; chipsContainer.innerHTML = ""; const chipConfigs = [ { label: "ทั้งหมด", value: 0 }, { label: "5 ดาว", value: 5 }, { label: "4 ดาว", value: 4 }, { label: "3 ดาว", value: 3 }, { label: "2 ดาว", value: 2 }, { label: "1 ดาว", value: 1 } ]; chipConfigs.forEach(conf => { const count = starCounts[conf.value]; const btn = document.createElement("button"); btn.className = activeReviewFilter === conf.value ? "btn btn-sky btn-xs" : "btn btn-sky-outline btn-xs"; btn.style.cssText = "font-size:10px; padding:3px 8px; border-radius:15px; margin:2px;"; btn.innerText = `${conf.label} (${count})`; btn.addEventListener("click", () => { activeReviewFilter = conf.value; renderFilterChips(); renderFilteredReviews(); }); chipsContainer.appendChild(btn); }); }; renderFilterChips(); renderFilteredReviews(); modal.style.display = "flex"; let selectedColor = hasColors ? "" : "Default"; let selectedSize = hasSizes ? "" : "Default"; let currentQty = 1; const sizeChartBtn = content.querySelector("#toggleSizeChartBtn"); if (sizeChartBtn) { sizeChartBtn.addEventListener("click", (e) => { e.preventDefault(); const sizeChartBox = content.querySelector("#sizeChartBox"); if (sizeChartBox) sizeChartBox.style.display = sizeChartBox.style.display === "none" ? "block" : "none"; }); } const btnGoToSeller = content.querySelector("#btnGoToSellerShop"); if (btnGoToSeller) { btnGoToSeller.addEventListener("click", () => { modal.style.display = "none"; openStoreProfile(product.sellerId || "all"); }); } const colorBtns = content.querySelectorAll(".btn-select-color"); colorBtns.forEach(btn => { btn.addEventListener("click", () => { colorBtns.forEach(b => b.classList.remove("active")); btn.classList.add("active"); selectedColor = btn.getAttribute("data-color"); if (hasSizes) { selectedSize = ""; renderModalSizes(product, selectedColor, (size) => { selectedSize = size; updateAddToCartButton(); }); } updateAddToCartButton(); }); }); if (!hasColors && !hasSizes) { selectedColor = "Default"; selectedSize = "Default"; updateAddToCartButton(); } else if (!hasColors && hasSizes) { renderModalSizes(product, "Default", (size) => { selectedSize = size; updateAddToCartButton(); }); } function updateQtySelector(maxStock) { const btnMinus = content.querySelector("#btnQtyMinus"); const btnPlus = content.querySelector("#btnQtyPlus"); const inputQty = content.querySelector("#inputQty"); const stockHint = content.querySelector("#spanMaxStockHint"); if (maxStock <= 0) { currentQty = 0; if (inputQty) inputQty.value = 0; if (btnMinus) btnMinus.disabled = true; if (btnPlus) btnPlus.disabled = true; if (stockHint) stockHint.innerText = "(สินค้าหมด)"; return; } if (currentQty > maxStock) { currentQty = maxStock; } else if (currentQty < 1) { currentQty = 1; } if (inputQty) inputQty.value = currentQty; if (stockHint) stockHint.innerText = `(มีสินค้าทั้งหมด ${maxStock} ชิ้น)`; if (btnMinus) btnMinus.disabled = (currentQty <= 1); if (btnPlus) btnPlus.disabled = (currentQty >= maxStock); } const btnMinus = content.querySelector("#btnQtyMinus"); const btnPlus = content.querySelector("#btnQtyPlus"); if (btnMinus) { btnMinus.addEventListener("click", () => { const variantObj = product.variants.find(v => v.color === selectedColor && v.size === selectedSize); const maxStock = variantObj ? variantObj.stock : 0; if (currentQty > 1) { currentQty--; updateQtySelector(maxStock); } }); } if (btnPlus) { btnPlus.addEventListener("click", () => { const variantObj = product.variants.find(v => v.color === selectedColor && v.size === selectedSize); const maxStock = variantObj ? variantObj.stock : 0; if (currentQty < maxStock) { currentQty++; updateQtySelector(maxStock); } }); } function updateAddToCartButton() { const cartBtn = content.querySelector("#addToCartBtn"); const priceDisplay = content.querySelector(".detail-price"); if (isSellerOnVacation) { if (cartBtn) { cartBtn.disabled = true; cartBtn.innerHTML = `<i class="fa-solid fa-umbrella-beach"></i> ร้านค้ากำลังอยู่ในช่วงพักร้อนชั่วคราว`; } updateQtySelector(0); return; } if (selectedColor && selectedSize) { const variantObj = product.variants.find(v => v.color === selectedColor && v.size === selectedSize); const stock = variantObj ? variantObj.stock : 0; const price = (variantObj && variantObj.price !== undefined) ? variantObj.price : product.price; if (priceDisplay) { priceDisplay.innerText = `฿${price.toLocaleString()}`; } updateQtySelector(stock); if (stock > 0) { cartBtn.disabled = false; cartBtn.innerHTML = `<i class="fa-solid fa-cart-plus"></i> เพิ่มลงตะกร้าสินค้า`; } else { cartBtn.disabled = true; cartBtn.innerHTML = `<i class="fa-solid fa-triangle-exclamation"></i> ตัวเลือกนี้หมดคลังชั่วคราว`; } } else { cartBtn.disabled = true; cartBtn.innerHTML = `<i class="fa-solid fa-cart-plus"></i> โปรดระบุตัวเลือกสินค้าให้ครบถ้วน`; if (priceDisplay) { priceDisplay.innerText = `฿${product.price.toLocaleString()}`; } updateQtySelector(0); } } const addToCartBtn = content.querySelector("#addToCartBtn"); if (addToCartBtn) { addToCartBtn.addEventListener("click", (e) => { const btn = e.currentTarget; btn.disabled = true; btn.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> กำลังเพิ่ม...`; setTimeout(() => { btn.innerHTML = `<i class="fa-solid fa-check"></i> เพิ่มสำเร็จ!`; btn.style.backgroundColor = "#22c55e"; btn.style.borderColor = "#22c55e"; btn.style.color = "#ffffff"; setTimeout(() => { addToCart(product.id, selectedColor, selectedSize, currentQty); createFlyingCartAnimation(e.clientX, e.clientY, imgSrc); modal.style.display = "none"; }, 500); }, 750); }); } const buyNowBtn = content.querySelector("#buyNowBtn"); if (buyNowBtn) { buyNowBtn.addEventListener("click", () => { const user = db.getCurrentUser(); if (!user) { showToast("กรุณาเข้าสู่ระบบก่อนทำการสั่งซื้อสินค้า", "danger"); navigateTo("auth"); return; } if (isSellerOnVacation) { showToast("ร้านค้านี้กำลังอยู่ในช่วงพักร้อนชั่วคราว ไม่สามารถสั่งซื้อสินค้าได้", "danger"); return; } if ((hasColors && !selectedColor) || (hasSizes && !selectedSize)) { showToast("กรุณาเลือกตัวเลือกสินค้าให้ครบถ้วน", "danger"); return; } const currentQtyVal = parseInt(content.querySelector("#detailQtyInput") ? content.querySelector("#detailQtyInput").value : "1", 10) || 1; addToCart(product.id, selectedColor, selectedSize, currentQtyVal); modal.style.display = "none"; if (typeof renderCheckout === "function") { renderCheckout(); } navigateTo("checkout"); }); } } function renderModalSizes(product, color, onSelectSize) { const sizeContainer = document.getElementById("modalSizeSelector"); if (!sizeContainer) return; sizeContainer.innerHTML = ""; // Find unique sizes for this color variant const colorVariants = product.variants.filter(v => v.color === color); colorVariants.forEach(variant => { const btn = document.createElement("button"); btn.className = "filter-chip"; btn.innerHTML = `${variant.size} <small>(${variant.stock})</small>`; if (variant.stock === 0) { btn.classList.add("disabled"); btn.style.opacity = "0.5"; btn.style.cursor = "not-allowed"; } else { btn.addEventListener("click", () => { sizeContainer.querySelectorAll(".filter-chip").forEach(b => b.classList.remove("active")); btn.classList.add("active"); onSelectSize(variant.size); }); } sizeContainer.appendChild(btn); }); } function openStoreProfile(sellerId) { const modal = document.getElementById("storeProfileModal"); const content = document.getElementById("storeProfileModalContent"); if (!modal || !content) return; const users = db.getUsers(); const sellerObj = users.find(u => u.username === sellerId); const sellerName = sellerObj ? sellerObj.name : sellerId; const companyName = (sellerObj && sellerObj.companyName) ? sellerObj.companyName : ''; const slogan = (sellerObj && sellerObj.slogan) ? sellerObj.slogan : 'ร้านค้าพรีเมียมบน SkyMall บริการด้วยใจ จัดส่งรวดเร็ว'; const storePhone = (sellerObj && sellerObj.storePhone) ? sellerObj.storePhone : '02-123-4567'; const cutoffTime = (sellerObj && sellerObj.cutoffTime) ? sellerObj.cutoffTime : '14:00 น.'; const primaryCarrier = (sellerObj && sellerObj.primaryCarrier) ? sellerObj.primaryCarrier : 'Standard Express'; const warrantyPolicy = (sellerObj && sellerObj.warrantyPolicy) ? sellerObj.warrantyPolicy : 'รับประกันสินค้า 7 วันเปลี่ยนใหม่'; const avatarHtml = (sellerObj && sellerObj.avatar) ? `<img src="${sellerObj.avatar}" alt="${sellerName}">` : '<i class="fa-solid fa-store"></i>'; const hasCover = !!(sellerObj && sellerObj.coverBanner); const coverStyle = hasCover ? `background: linear-gradient(rgba(15, 23, 42, 0.4), rgba(15, 23, 42, 0.75)), url('${sellerObj.coverBanner}') center/cover no-repeat;` : ''; const allProds = db.getProducts(); const storeProds = allProds.filter(p => p.sellerId === sellerId && p.active); const avgRating = storeProds.length > 0 ? (storeProds.reduce((sum, p) => sum + p.rating, 0) / storeProds.length).toFixed(1) : "5.0"; const totalProdCount = storeProds.length; const isVacation = sellerObj && sellerObj.isVacationMode; content.innerHTML = ` <div class="store-cover-banner-modal" style="${coverStyle}"> ${isVacation ? '<span class="badge badge-warning store-vacation-badge"><i class="fa-solid fa-umbrella-beach"></i> ร้านพักร้อนชั่วคราว</span>' : ''} </div> <div class="store-profile-header-modal"> <div class="store-avatar-modal"> ${avatarHtml} </div> <div class="store-info-modal"> <h3 class="store-title-modal">${sellerName}${getOfficialBadgeHtml(sellerId)}</h3> ${companyName ? `<div class="store-company-modal"><i class="fa-solid fa-building"></i> ${companyName}</div>` : ''} <div class="store-slogan-modal"><i class="fa-solid fa-quote-left"></i> ${slogan}</div> </div> </div> <div class="store-badges-grid-modal"> <div class="store-badge-item"> <i class="fa-solid fa-phone text-sky"></i> <div> <span class="badge-title">ติดต่อร้านค้า</span> <span class="badge-val">${storePhone}</span> </div> </div> <div class="store-badge-item"> <i class="fa-solid fa-clock text-sky"></i> <div> <span class="badge-title">เวลาตัดรอบ</span> <span class="badge-val">${cutoffTime}</span> </div> </div> <div class="store-badge-item"> <i class="fa-solid fa-truck-fast text-sky"></i> <div> <span class="badge-title">ขนส่งหลัก</span> <span class="badge-val">${primaryCarrier}</span> </div> </div> <div class="store-badge-item"> <i class="fa-solid fa-shield-halved text-warning"></i> <div> <span class="badge-title">การรับประกัน</span> <span class="badge-val">${warrantyPolicy}</span> </div> </div> </div> <div class="store-stats-row-modal"> <div class="store-stat-box"> <span class="stat-num"><i class="fa-solid fa-star text-warning"></i> ${avgRating}</span> <span class="stat-lbl">คะแนนร้านค้า</span> </div> <div class="store-stat-box"> <span class="stat-num">${totalProdCount}</span> <span class="stat-lbl">รายการสินค้า</span> </div> <div class="store-stat-box"> <span class="stat-num">98%</span> <span class="stat-lbl">อัตราตอบกลับ</span> </div> </div> <div class="store-actions-modal mt-3"> <button class="btn btn-sky w-100 btn-lg" id="btnViewAllStoreProducts" data-seller="${sellerId}"> <i class="fa-solid fa-store"></i> ดูสินค้าทั้งหมดของร้านนี้ (${totalProdCount} ชิ้น) </button> </div> `; const viewAllBtn = content.querySelector("#btnViewAllStoreProducts"); if (viewAllBtn) { viewAllBtn.addEventListener("click", () => { state.activeFilters.sellerId = sellerId; const filterSellerDropdown = document.getElementById("filterSeller"); if (filterSellerDropdown) { filterSellerDropdown.value = sellerId; } modal.style.display = "none"; navigateTo("home"); renderCatalog(); const catalogElem = document.getElementById("catalog-start"); if (catalogElem) { catalogElem.scrollIntoView({ behavior: "smooth" }); } }); } modal.style.display = "flex"; } const closeDetailBtn = document.getElementById("closeProductDetailModalBtn"); if (closeDetailBtn) { closeDetailBtn.addEventListener("click", () => { const modal = document.getElementById("productDetailModal"); if (modal) modal.style.display = "none"; }); } const closeStoreProfileBtn = document.getElementById("closeStoreProfileModalBtn"); if (closeStoreProfileBtn) { closeStoreProfileBtn.addEventListener("click", () => { const modal = document.getElementById("storeProfileModal"); if (modal) modal.style.display = "none"; }); } // Global Backdrop Click to Close for All Modals (.modal-overlay) document.addEventListener("click", (e) => { if (e.target && e.target.classList.contains("modal-overlay")) { e.target.style.display = "none"; } }); // ========================================================================== // 8. SHOPPING CART ENGINE // ========================================================================== function addToCart(productId, color, size, qty = 1) { const product = db.getProducts().find(p => p.id === productId); if (!product) return; const sellerObj = db.getUsers().find(u => u.username === product.sellerId); if (sellerObj && sellerObj.isVacationMode) { showToast("ร้านค้านี้กำลังอยู่ในช่วงพักร้อนชั่วคราว ไม่สามารถสั่งซื้อสินค้าได้ในขณะนี้", "danger"); return; } const variant = product.variants.find(v => v.color === color && v.size === size); const price = (variant && variant.price !== undefined) ? variant.price : product.price; const existing = state.cart.find(item => item.productId === productId && item.color === color && item.size === size); if (existing) { existing.quantity += qty; } else { state.cart.push({ productId: productId, name: product.name, price: price, image: product.image, color: color, size: size, quantity: qty, sellerId: product.sellerId || "admin", sellerName: product.sellerName || "SkyMall" }); } updateCartBadge(); showToast("เพิ่มสินค้าลงตะกร้าแล้ว!", "success"); } function updateCartBadge() { const count = state.cart.reduce((sum, item) => sum + item.quantity, 0); const badge = document.getElementById("cartBadgeCount"); if (badge) badge.innerText = count; } function renderCart() { const container = document.getElementById("cartItemsPanel"); if (!container) return; container.innerHTML = ""; if (state.cart.length === 0) { container.innerHTML = ` <div class="admin-card text-center py-5"> <i class="fa-solid fa-cart-shopping" style="font-size:3.5rem; color:var(--color-sky-light)"></i> <h3 class="mt-3">ไม่มีสินค้าในตะกร้าของคุณ</h3> <p style="color:var(--color-sky-slate)">ลองกลับไปเลือกช้อปสินค้าดีลพิเศษกับเรา</p> <button class="btn btn-sky mt-3" onclick="navigateTo('home')">กลับไปช้อปปิ้ง</button> </div> `; updateCartTotals(0); return; } state.cart.forEach((item, index) => { const itemCard = document.createElement("div"); itemCard.className = "cart-item"; const placeholderImg = DEFAULT_IMAGE_PLACEHOLDERS[item.productId] || DEFAULT_IMAGE_PLACEHOLDERS["default"]; const imgSrc = item.image || placeholderImg; const hasOptionLabels = item.color !== "Default" || item.size !== "Default"; const metaText = hasOptionLabels ? `ตัวเลือก: ${item.color} / ${item.size}` : "รูปแบบปกติ"; itemCard.innerHTML = ` <img src="${imgSrc}" class="cart-item-img" style="width:60px; height:60px; object-fit:cover;" id="cart-img" alt="${item.name}"> <div class="cart-item-details"> <div class="cart-item-name">${item.name}</div> <div class="cart-item-meta">${metaText}</div> <div class="cart-item-seller" style="font-size: 0.8rem; color: #0d9488;"><i class="fa-solid fa-store"></i> ร้านค้า: ${item.sellerName || 'SkyMall'}</div> </div> <div class="cart-item-quantity"> <button class="quantity-btn dec-btn">-</button> <span class="quantity-val">${item.quantity}</span> <button class="quantity-btn inc-btn">+</button> </div> <div class="cart-item-price">฿${(item.price * item.quantity).toLocaleString()}</div> <button class="cart-item-remove" data-index="${index}"><i class="fa-solid fa-trash-can"></i></button> `; itemCard.querySelector(".dec-btn").addEventListener("click", () => { if (item.quantity > 1) { item.quantity -= 1; renderCart(); } }); itemCard.querySelector(".inc-btn").addEventListener("click", () => { const product = db.getProducts().find(p => p.id === item.productId); const variant = product.variants.find(v => v.color === item.color && v.size === item.size); if (variant && item.quantity < variant.stock) { item.quantity += 1; renderCart(); } else { alert("ขออภัย สินค้าชิ้นนี้ในสต็อกหมดแล้ว"); } }); itemCard.querySelector(".cart-item-remove").addEventListener("click", () => { state.cart.splice(index, 1); updateCartBadge(); renderCart(); }); container.appendChild(itemCard); }); const actionsBar = document.createElement("div"); actionsBar.className = "cart-actions-bar"; actionsBar.innerHTML = ` <button class="btn btn-sky-outline" id="continueShoppingBtn"><i class="fa-solid fa-arrow-left"></i> ← กลับไปเลือกสินค้าเพิ่ม</button> <button class="btn btn-sky btn-lg" id="checkoutBtn"><i class="fa-solid fa-credit-card"></i> ไปที่หน้าชำระเงิน →</button> `; actionsBar.querySelector("#continueShoppingBtn").addEventListener("click", () => { navigateTo("home"); }); actionsBar.querySelector("#checkoutBtn").addEventListener("click", () => { navigateTo("checkout"); }); container.appendChild(actionsBar); const subtotal = state.cart.reduce((sum, item) => sum + (Number(item.price) * Number(item.quantity)), 0); updateCartTotals(subtotal); } function updateCartTotals(subtotal) { const subtotalLabel = document.getElementById("cartSubtotal"); const discountLabel = document.getElementById("cartDiscount"); const discountRow = document.getElementById("summaryDiscountRow"); const totalLabel = document.getElementById("cartTotal"); if (!subtotalLabel || !totalLabel) return; // Re-verify coupon minimum spend if (state.appliedCoupon && state.appliedCoupon.minSpend && subtotal < state.appliedCoupon.minSpend) { alert(`ยอดรวมสินค้าของท่านต่ำกว่าขั้นต่ำที่กำหนดสำหรับคูปอง ${state.appliedCoupon.code} (${state.appliedCoupon.minSpend} บาท) ระบบได้ทำการยกเลิกคูปองนี้แล้ว`); state.appliedCoupon = null; } subtotalLabel.innerText = `฿${subtotal.toLocaleString()}`; let discount = 0; if (state.appliedCoupon && subtotal > 0) { if (discountRow) discountRow.style.display = "flex"; const codeSpan = document.getElementById("activeCouponCode"); if (codeSpan) codeSpan.innerText = state.appliedCoupon.code; if (state.appliedCoupon.type === "percent") { discount = Math.round(subtotal * (state.appliedCoupon.value / 100)); } else { discount = state.appliedCoupon.value; } if (discount > subtotal) discount = subtotal; if (discountLabel) discountLabel.innerText = `-฿${discount.toLocaleString()}`; } else { if (discountRow) discountRow.style.display = "none"; } const total = subtotal - discount; totalLabel.innerText = `฿${total.toLocaleString()}`; } // HELPERS FOR SAVING COLLECTED COUPONS function getCollectedCoupons() { const user = db.getCurrentUser(); const key = user ? `sk_collected_coupons_${user.username}` : "sk_collected_coupons_guest"; try { const stored = localStorage.getItem(key); return stored ? JSON.parse(stored) : []; } catch (e) { return []; } } function saveCollectedCoupon(code) { if (!code) return; const user = db.getCurrentUser(); const key = user ? `sk_collected_coupons_${user.username}` : "sk_collected_coupons_guest"; let collected = []; try { const stored = localStorage.getItem(key); collected = stored ? JSON.parse(stored) : []; if (!Array.isArray(collected)) collected = []; } catch (e) { collected = []; } if (!collected.includes(code)) { collected.push(code); localStorage.setItem(key, JSON.stringify(collected)); } } window.saveCollectedCoupon = saveCollectedCoupon; // RENDER COUPON SHOWCASE CARDS (HOME PAGE) function renderCouponShowcase() { const container = document.getElementById("couponCardsContainer"); if (!container) return; container.innerHTML = ""; let coupons = db.getCoupons(); if (!coupons || coupons.length === 0) { coupons = typeof DEFAULT_COUPONS !== 'undefined' ? DEFAULT_COUPONS : []; db.saveCoupons(coupons); } const collected = getCollectedCoupons(); // 1. จัดการปุ่ม "เก็บคูปองทั้งหมด" (#collectAllCouponsBtn) แยกต่างหาก const collectAllBtn = document.getElementById("collectAllCouponsBtn"); if (collectAllBtn) { // ล้าง Event Listener เก่าออกด้วยการ Replace Element const newCollectAllBtn = collectAllBtn.cloneNode(true); collectAllBtn.parentNode.replaceChild(newCollectAllBtn, collectAllBtn); newCollectAllBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); if (!state.currentUser) { showToast("กรุณาเข้าสู่ระบบก่อนทำการเก็บคูปองส่วนลดค่ะ", "danger"); document.querySelectorAll(".modal-overlay").forEach(m => m.style.display = "none"); state.postLoginRedirect = "home"; navigateTo("auth"); return; } const currentCollected = getCollectedCoupons(); const uncollected = coupons.filter(c => !currentCollected.includes(c.code)); if (uncollected.length === 0) { showToast("คุณเก็บคูปองส่วนลดทั้งหมดเรียบร้อยแล้วค่ะ", "info"); return; } uncollected.forEach(c => saveCollectedCoupon(c.code)); showToast(`เก็บคูปองส่วนลดเพิ่มอีก ${uncollected.length} ใบเรียบร้อยแล้ว!`, "success"); renderCouponShowcase(); }); } // 2. วาดการ์ดคูปองทีละใบ และผูก Event แยกเฉพาะใบนั้นๆ coupons.forEach(coupon => { const discountText = coupon.type === "percent" ? `${coupon.value}%` : `฿${coupon.value}`; const isCollected = collected.includes(coupon.code); const card = document.createElement("div"); card.className = "coupon-card"; const buttonText = isCollected ? "เก็บแล้ว" : "เก็บคูปอง"; const buttonClass = isCollected ? "btn btn-sky-outline btn-xs" : "btn btn-sky btn-xs"; const btnStyle = isCollected ? "opacity: 0.6; cursor: default;" : ""; const icon = isCollected ? "fa-check" : "fa-ticket"; card.innerHTML = ` <div class="coupon-card-left"> <i class="fa-solid fa-ticket"></i> </div> <div class="coupon-card-right"> <div class="coupon-discount">ลด ${discountText}</div> <div class="coupon-min-spend">ขั้นต่ำ ฿${(coupon.minSpend || 0).toLocaleString()}</div> <div class="coupon-code-wrapper"> <span class="coupon-code">${coupon.code}</span> <button type="button" class="btn-copy-coupon ${buttonClass}" data-code="${coupon.code}" ${isCollected ? 'disabled' : ''} style="${btnStyle}"> <i class="fa-solid ${icon}"></i> ${buttonText} </button> </div> </div> `; const copyBtn = card.querySelector(".btn-copy-coupon"); if (copyBtn && !isCollected) { copyBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); if (!state.currentUser) { showToast("กรุณาเข้าสู่ระบบก่อนทำการเก็บคูปองส่วนลดค่ะ", "danger"); document.querySelectorAll(".modal-overlay").forEach(m => m.style.display = "none"); state.postLoginRedirect = "home"; navigateTo("auth"); return; } // บันทึกเฉพาะรหัสคูปองใบนี้เท่านั้น saveCollectedCoupon(coupon.code); showToast(`เก็บคูปองส่วนลด ${coupon.code} สำเร็จ!`, "success"); // สั่ง Re-render เพื่ออัปเดตปุ่มเป็น "เก็บแล้ว" เฉพาะใบที่กด renderCouponShowcase(); }); } container.appendChild(card); }); } // OPEN SHIPPING TRACKING MODAL function openOrderTrackingModal(order) { const modal = document.getElementById("orderTrackingModal"); if (!modal) return; // Ensure close button works reliably by binding directly const closeBtn = document.getElementById("closeOrderTrackingModalBtn"); if (closeBtn) { closeBtn.onclick = () => { modal.style.display = "none"; }; } modal.onclick = (e) => { if (e.target === modal) { modal.style.display = "none"; } }; const steps = [ document.getElementById("trackStep1"), document.getElementById("trackStep2"), document.getElementById("trackStep3"), document.getElementById("trackStep4") ]; const lines = [ document.getElementById("trackLine1"), document.getElementById("trackLine2"), document.getElementById("trackLine3") ]; // Reset styles steps.forEach(step => { if (step) step.classList.remove("active"); }); lines.forEach(line => { if (line) line.classList.remove("active"); }); const infoBox = document.getElementById("trackingInfoBox"); if (!infoBox) return; infoBox.innerHTML = ""; const status = (order.status || "").toLowerCase(); let activeStepsCount = 1; let mockTrackingHtml = ""; if (status === "paid" || status === "preparing") { activeStepsCount = 2; mockTrackingHtml = ` <h4><i class="fa-solid fa-box"></i> สถานะ: กำลังจัดเตรียมพัสดุ</h4> <p>ร้านค้าได้รับยอดชำระและกำลังอยู่ระหว่างจัดเตรียมสินค้าลงกล่องพัสดุ</p> <p style="font-size:12px; color:var(--color-sky-slate);">เตรียมจัดส่งภายใน 24-48 ชั่วโมง</p> `; } else if (status === "shipped") { activeStepsCount = 3; let trNo = ""; if (order.trackingNumbers) { const keys = Object.keys(order.trackingNumbers); if (keys.length > 0) { trNo = order.trackingNumbers[keys[0]]; } } if (!trNo) { trNo = `TH2026${Math.floor(100000 + Math.random() * 900000)}`; } mockTrackingHtml = ` <h4><i class="fa-solid fa-truck"></i> สถานะ: อยู่ระหว่างการจัดส่ง</h4> <p><strong>ผู้ให้บริการจัดส่ง:</strong> Flash Express</p> <p><strong>เลขพัสดุสินค้า:</strong> <span style="font-weight:600; color:var(--color-sky-brand);">${trNo}</span></p> <p style="font-size:12px; color:var(--color-sky-slate);">พัสดุออกจากคลังสินค้าหลักและอยู่ระหว่างขนส่งไปยังผู้รับ</p> `; } else if (status === "success" || status === "complete") { activeStepsCount = 4; let trNo = ""; if (order.trackingNumbers) { const keys = Object.keys(order.trackingNumbers); if (keys.length > 0) { trNo = order.trackingNumbers[keys[0]]; } } if (!trNo) { trNo = `TH2026889412`; } mockTrackingHtml = ` <h4><i class="fa-solid fa-circle-check" style="color:#22c55e;"></i> สถานะ: จัดส่งสำเร็จ</h4> <p><strong>ผู้ให้บริการจัดส่ง:</strong> Flash Express</p> <p><strong>เลขพัสดุสินค้า:</strong> <span style="font-weight:600; color:var(--color-sky-brand);">${trNo}</span></p> <p>พัสดุส่งถึงผู้รับเรียบร้อยแล้ว หากพบปัญหาใดๆ กรุณาติดต่อฝ่ายบริการลูกค้า</p> `; } else if (status === "failed") { activeStepsCount = 0; mockTrackingHtml = ` <h4 style="color:var(--color-danger);"><i class="fa-solid fa-circle-xmark"></i> สถานะ: คำสั่งซื้อล้มเหลว / ถูกยกเลิก</h4> <p>ออเดอร์นี้ถูกระงับหรือยกเลิกเรียบร้อยแล้ว</p> `; } else { activeStepsCount = 1; mockTrackingHtml = ` <h4><i class="fa-solid fa-clock"></i> สถานะ: คำสั่งซื้อเสร็จสมบูรณ์ (รอชำระเงิน)</h4> <p>ระบบสร้างใบสั่งซื้อแล้ว กรุณาแจ้งโอนเงินหรือรอระบบแอดมินยืนยันความถูกต้องสลิป</p> `; } // Set active highlights for (let i = 0; i < activeStepsCount; i++) { if (steps[i]) steps[i].classList.add("active"); if (i > 0 && lines[i - 1]) lines[i - 1].classList.add("active"); } infoBox.innerHTML = mockTrackingHtml; // Fill Shipping Info const shippingInfoDiv = document.getElementById("orderModalShippingInfo"); if (shippingInfoDiv) { shippingInfoDiv.innerHTML = ` <strong>ชื่อผู้รับ:</strong> ${order.shippingInfo.name}<br> <strong>เบอร์โทรศัพท์:</strong> ${order.shippingInfo.phone}<br> <strong>ที่อยู่จัดส่ง:</strong> ${order.shippingInfo.address} `; } // Fill Payment Status Badge let statusLabel = ""; let statusClass = "badge-dark"; if (order.status === "Unpaid") { statusLabel = "รอชำระเงิน"; statusClass = "badge-dark"; } else if (order.status === "Pending") { statusLabel = "รอตรวจสอบยอดโอน"; statusClass = "badge-warning"; } else if (order.status === "Paid" || order.status === "Preparing") { statusLabel = "ชำระเงินแล้ว"; statusClass = "badge-success"; } else if (order.status === "Shipped") { statusLabel = "จัดส่งพัสดุแล้ว"; statusClass = "badge-success"; } else if (order.status === "Success") { statusLabel = "จัดส่งสำเร็จ"; statusClass = "badge-success"; } else if (order.status === "Failed") { statusLabel = "ยกเลิกคำสั่งซื้อ"; statusClass = "badge-danger"; } const paymentInfoDiv = document.getElementById("orderModalPaymentInfo"); if (paymentInfoDiv) { paymentInfoDiv.innerHTML = ` <strong>วิธีการชำระเงิน:</strong> ${order.paymentMethod || 'PromptPay'}<br> <strong>สถานะคำสั่งซื้อ:</strong> <span class="badge ${statusClass}" style="font-size: 11px;">${statusLabel}</span> `; } // Fill Ordered Items List const itemsDiv = document.getElementById("orderModalItems"); if (itemsDiv) { itemsDiv.innerHTML = order.items.map(item => { const hasOpts = item.color !== "Default" || item.size !== "Default"; const optText = hasOpts ? `${item.color} / ${item.size}` : "ไม่มีตัวเลือก"; const prodPlaceholder = DEFAULT_IMAGE_PLACEHOLDERS[item.productId] || DEFAULT_IMAGE_PLACEHOLDERS["default"]; const prodImg = item.image || prodPlaceholder; return ` <div class="order-modal-item-row"> <div class="order-modal-item-left"> <img src="${prodImg}" class="order-modal-item-img" alt="${item.name}"> <div class="order-modal-item-info"> <span class="order-modal-item-name" style="font-weight:600;">${item.name}</span> <span class="order-modal-item-variant">${optText}</span> </div> </div> <div class="order-modal-item-right"> <span class="order-modal-item-price" style="font-weight:600; color:var(--color-sky-brand);">฿${item.price.toLocaleString()}</span> <span class="order-modal-item-qty"><br>x${item.quantity}</span> </div> </div> `; }).join(""); } // Fill Totals & Coupons Summary const subtotal = order.subtotal || order.items.reduce((s, i) => s + (i.price * i.quantity), 0); const discount = order.discount || 0; const total = order.total || subtotal; const totalsDiv = document.getElementById("orderModalTotals"); if (totalsDiv) { totalsDiv.innerHTML = ` <div class="order-modal-total-line"> <span>ยอดรวมสินค้า:</span> <span>฿${subtotal.toLocaleString()}</span> </div> ${discount > 0 ? ` <div class="order-modal-total-line" style="color: var(--color-danger);"> <span>ส่วนลดคูปอง (${order.couponCode || 'คูปอง'}):</span> <span>-฿${discount.toLocaleString()}</span> </div>` : ''} <div class="order-modal-total-line grand-total"> <span>ยอดชำระสุทธิ:</span> <span>฿${total.toLocaleString()}</span> </div> `; } modal.style.display = "flex"; } function initCouponHandler() { const applyBtn = document.getElementById("applyCouponBtn"); const couponInput = document.getElementById("couponInput"); const statusMsg = document.getElementById("couponStatusMessage"); if (applyBtn) { applyBtn.addEventListener("click", () => { const code = couponInput.value.trim().toUpperCase(); if (!code) return; state.coupons = db.getCoupons(); const found = state.coupons.find(c => c.code === code); if (found) { const subtotal = state.cart.reduce((sum, item) => sum + (item.price * item.quantity), 0); const minSpend = found.minSpend || 0; if (subtotal < minSpend) { state.appliedCoupon = null; if (statusMsg) { statusMsg.className = "coupon-status text-danger"; statusMsg.innerText = `ยอดสั่งซื้อขั้นต่ำต้องถึง ฿${minSpend.toLocaleString()} สำหรับคูปองนี้`; } } else { state.appliedCoupon = found; if (statusMsg) { statusMsg.className = "coupon-status text-success"; statusMsg.innerText = `ใช้งานคูปองส่วนลด ${code} สำเร็จ!`; } } updateCartTotals(subtotal); if (typeof renderCheckout === "function") renderCheckout(); } else { state.appliedCoupon = null; if (statusMsg) { statusMsg.className = "coupon-status text-danger"; statusMsg.innerText = "ไม่พบคูปองรหัสนี้ในระบบ"; } const subtotal = state.cart.reduce((sum, item) => sum + (Number(item.price) * Number(item.quantity)), 0); updateCartTotals(subtotal); if (typeof renderCheckout === "function") renderCheckout(); } }); } const checkoutBtn = document.getElementById("checkoutBtn"); if (checkoutBtn) { checkoutBtn.addEventListener("click", () => { if (state.cart.length === 0) return; navigateTo("checkout"); }); } const continueShoppingBtn = document.getElementById("continueShoppingBtn"); if (continueShoppingBtn) { continueShoppingBtn.addEventListener("click", () => { navigateTo("home"); }); } } // ========================================================================== // 9. CHECKOUT PAGE (Payment choice Credit Card vs PromptPay) // ========================================================================== function renderCheckout() { const checkoutMiniList = document.getElementById("checkoutMiniList"); if (!checkoutMiniList) return; checkoutMiniList.innerHTML = ""; let subtotal = state.cart.reduce((sum, item) => sum + (Number(item.price) * Number(item.quantity)), 0); // Re-verify coupon minimum spend if (state.appliedCoupon && state.appliedCoupon.minSpend && subtotal < state.appliedCoupon.minSpend) { alert(`ยอดรวมสินค้าของท่านต่ำกว่าขั้นต่ำที่กำหนดสำหรับคูปอง ${state.appliedCoupon.code} (${state.appliedCoupon.minSpend} บาท) ระบบได้ทำการยกเลิกคูปองนี้แล้ว`); state.appliedCoupon = null; } let discount = 0; if (state.appliedCoupon) { if (state.appliedCoupon.type === "percent") { discount = Math.round(subtotal * (state.appliedCoupon.value / 100)); } else { discount = state.appliedCoupon.value; } if (discount > subtotal) discount = subtotal; } const total = subtotal - discount; const checkoutSub = document.getElementById("checkoutSubtotal"); if (checkoutSub) checkoutSub.innerText = `฿${subtotal.toLocaleString()}`; const discRow = document.getElementById("checkoutDiscountRow"); if (discount > 0) { if (discRow) discRow.style.display = "flex"; const checkDisc = document.getElementById("checkoutDiscount"); if (checkDisc) checkDisc.innerText = `-฿${discount.toLocaleString()}`; const activeCodeSpan = document.getElementById("activeCheckoutCouponCode"); if (activeCodeSpan && state.appliedCoupon) activeCodeSpan.innerText = state.appliedCoupon.code; } else { if (discRow) discRow.style.display = "none"; } const checkoutTot = document.getElementById("checkoutTotal"); if (checkoutTot) checkoutTot.innerText = `฿${total.toLocaleString()}`; const qrAmt = document.getElementById("qrTargetAmount"); if (qrAmt) qrAmt.innerText = `ยอดชำระ: ฿${total.toLocaleString()}`; state.cart.forEach(item => { const el = document.createElement("div"); el.className = "checkout-mini-item"; const optionDetails = (item.color !== "Default" || item.size !== "Default") ? ` (${item.color}/${item.size})` : ""; el.innerHTML = ` <span>${item.name}${optionDetails} x ${item.quantity}<br><small style="color: #0d9488;"><i class="fa-solid fa-store"></i> ร้านค้า: ${item.sellerName || 'SkyMall'}</small></span> <span>฿${(item.price * item.quantity).toLocaleString()}</span> `; checkoutMiniList.appendChild(el); }); // Render saved address selection cards const addressContainer = document.getElementById("checkoutAddressContainer"); if (addressContainer) { addressContainer.innerHTML = ""; const user = state.currentUser; if (!user || !user.addresses || user.addresses.length === 0) { addressContainer.innerHTML = ` <div class="text-center py-4" style="background:var(--color-bg-gray); border-radius:8px; border:1px dashed var(--color-border);"> <p style="color:var(--color-sky-slate); margin-bottom:10px;">คุณยังไม่มีข้อมูลที่อยู่จัดส่งที่บันทึกไว้</p> <button type="button" class="btn btn-sky btn-sm" onclick="openAddressModal(null)"><i class="fa-solid fa-plus"></i> เพิ่มที่อยู่จัดส่งสินค้าใหม่</button> </div> `; } else { user.addresses.forEach((addr, idx) => { const isSelected = addr.isDefault || idx === 0; const label = document.createElement("label"); label.className = `checkout-address-label ${isSelected ? 'selected' : ''}`; label.innerHTML = ` <input type="radio" name="selectedCheckoutAddress" value="${addr.id}" ${isSelected ? 'checked' : ''} style="margin-top:4px;"> <div class="checkout-address-info"> <div style="display:flex; justify-content:space-between; align-items:center;"> <strong style="font-size:0.95rem; color:var(--color-sky-dark);">${addr.name}</strong> ${addr.isDefault ? '<span class="badge badge-sky" style="font-size:10px;"><i class="fa-solid fa-star"></i> ที่อยู่หลัก</span>' : ''} </div> <div style="font-size:0.85rem; color:var(--color-sky-slate); margin-top:2px;"><i class="fa-solid fa-phone"></i> ${addr.phone}</div> <div style="font-size:0.88rem; color:var(--color-sky-dark); margin-top:4px;">${addr.address} จ.${addr.province} ${addr.zipcode}</div> </div> `; label.querySelector("input").addEventListener("change", () => { document.querySelectorAll(".checkout-address-label").forEach(l => l.classList.remove("selected")); label.classList.add("selected"); }); addressContainer.appendChild(label); }); } } } function handlePlaceOrder(e) { e.preventDefault(); if (state.cart.length === 0) return; const payOption = document.querySelector("input[name='paymentOption']:checked").value; if (payOption === "promptpay" && !state.selectedSlipBase64) { showToast("กรุณาแนบภาพหลักฐานการโอนเงิน (สลิป)", "danger"); return; } if (payOption === "credit_card") { const card = document.getElementById("cardNum").value.trim(); const exp = document.getElementById("cardExpiry").value.trim(); const cvv = document.getElementById("cardCvv").value.trim(); if (!card || !exp || !cvv) { showToast("กรุณากรอกข้อมูลบัตรเครดิตให้ครบถ้วน", "danger"); return; } } const selectedRadio = document.querySelector("input[name='selectedCheckoutAddress']:checked"); const user = state.currentUser; if (user) { const freshUser = db.getUsers().find(u => u.username === user.username); if ((freshUser && freshUser.isBanned) || user.isBanned) { showToast("บัญชีของคุณถูกระงับการใช้งานชั่วคราว กรุณาติดต่อฝ่ายบริการลูกค้า", "danger"); alert("บัญชีของคุณถูกระงับการใช้งานชั่วคราว กรุณาติดต่อฝ่ายบริการลูกค้า"); return; } } if (!selectedRadio || !user || !user.addresses) { showToast("กรุณาเลือกหรือเพิ่มที่อยู่สำหรับจัดส่งสินค้าก่อนค่ะ", "danger"); return; } const selectedAddrObj = user.addresses.find(a => a.id === selectedRadio.value); if (!selectedAddrObj) { showToast("ไม่พบข้อมูลที่อยู่ที่เลือก กรุณาเลือกที่อยู่อีกครั้ง", "danger"); return; } const name = selectedAddrObj.name; const phone = selectedAddrObj.phone; const fullAddress = `${selectedAddrObj.address} จ.${selectedAddrObj.province} ${selectedAddrObj.zipcode}`; const subtotal = state.cart.reduce((sum, item) => sum + (item.price * item.quantity), 0); // Recheck coupon minimum spend right before order creation if (state.appliedCoupon && state.appliedCoupon.minSpend && subtotal < state.appliedCoupon.minSpend) { showToast(`ยอดรวมสินค้าของท่านต่ำกว่าขั้นต่ำที่คูปองกำหนด (${state.appliedCoupon.minSpend} บาท) ระบบจึงทำการยกเลิกคูปองนี้อัตโนมัติ`, "danger"); state.appliedCoupon = null; } let discount = 0; if (state.appliedCoupon) { if (state.appliedCoupon.type === "percent") { discount = Math.round(subtotal * (state.appliedCoupon.value / 100)); } else { discount = state.appliedCoupon.value; } if (discount > subtotal) discount = subtotal; } const total = subtotal - discount; let orderStatus = "Pending"; if (payOption === "pay_later") { orderStatus = "Unpaid"; } else if (payOption === "credit_card" || payOption === "cod") { orderStatus = "Preparing"; // Instant approval for Credit Card and COD } // Real-time stock validation check const products = db.getProducts(); let stockOk = true; let outOfStockMsg = ""; for (const item of state.cart) { const product = products.find(p => p.id === item.productId); if (!product) { stockOk = false; outOfStockMsg = `ไม่พบสินค้า ${item.name} ในระบบ`; break; } const variant = product.variants.find(v => v.color === item.color && v.size === item.size); if (!variant) { stockOk = false; outOfStockMsg = `ไม่พบตัวเลือกของสินค้า ${item.name} ในระบบ`; break; } if (variant.stock < item.quantity) { stockOk = false; outOfStockMsg = `ขออภัย สินค้า ${item.name} (${item.color}/${item.size}) คงเหลือในสต็อกไม่เพียงพอ (เหลือ ${variant.stock} ชิ้น คุณสั่งซื้อ ${item.quantity} ชิ้น)`; break; } } if (!stockOk) { alert(outOfStockMsg); return; } const newOrderId = "ORD-" + Date.now(); const now = new Date(); const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`; let methodLabel = "PromptPay"; if (payOption === "credit_card") methodLabel = "Credit Card"; if (payOption === "pay_later") methodLabel = "โอนเงินภายหลัง (รอแนบสลิป)"; if (payOption === "cod") methodLabel = "เก็บเงินปลายทาง (COD)"; const newOrder = { id: newOrderId, date: dateStr, customerName: state.currentUser ? state.currentUser.name : (name || "ลูกค้าทั่วไป"), customerUsername: state.currentUser ? state.currentUser.username : "customer1", shippingInfo: { name: name, phone: phone, address: fullAddress }, items: JSON.parse(JSON.stringify(state.cart)), subtotal: subtotal, discount: discount, total: total, couponCode: state.appliedCoupon ? state.appliedCoupon.code : "", slipImage: payOption === "promptpay" ? state.selectedSlipBase64 : "", status: orderStatus, paymentMethod: methodLabel, trackingNumbers: {} }; // Deduct stock from variant and update total stock state.cart.forEach(item => { const product = products.find(p => p.id === item.productId); if (product) { const variant = product.variants.find(v => v.color === item.color && v.size === item.size); if (variant) { variant.stock = Math.max(0, variant.stock - item.quantity); } } }); // Save State db.saveProducts(products); const orders = db.getOrders(); orders.unshift(newOrder); db.saveOrders(orders); state.orders = orders; // Update global state // Record last order total details for the Success page display state.lastOrderResult = newOrder; // Reset Cart state.cart = []; state.appliedCoupon = null; state.selectedSlipBase64 = ""; const couponMsgBox = document.getElementById("couponStatusMessage"); if (couponMsgBox) { couponMsgBox.innerHTML = ""; couponMsgBox.className = "coupon-status"; } updateCartBadge(); // Reset forms const checkoutForm = document.getElementById("checkoutForm"); checkoutForm.reset(); const remBtn = document.getElementById("removeSlipBtn"); if (remBtn) remBtn.click(); // Restore payment option default document.querySelector("input[name='paymentOption'][value='promptpay']").checked = true; document.getElementById("checkoutPromptPayArea").style.display = "block"; document.getElementById("checkoutCreditCardArea").style.display = "none"; document.getElementById("slipFileInput").required = true; navigateTo("success"); } function initCheckoutFlow() { const slipFileInput = document.getElementById("slipFileInput"); const previewContainer = document.getElementById("slipPreviewContainer"); const previewImg = document.getElementById("slipPreviewImg"); const removeSlipBtn = document.getElementById("removeSlipBtn"); const checkoutPromptPayArea = document.getElementById("checkoutPromptPayArea"); const checkoutCreditCardArea = document.getElementById("checkoutCreditCardArea"); // Toggle Payment Forms document.querySelectorAll("input[name='paymentOption']").forEach(radio => { radio.addEventListener("change", (e) => { const val = e.target.value; if (val === "promptpay") { if (checkoutPromptPayArea) checkoutPromptPayArea.style.display = "block"; if (checkoutCreditCardArea) checkoutCreditCardArea.style.display = "none"; if (slipFileInput) slipFileInput.required = true; } else if (val === "credit_card") { if (checkoutPromptPayArea) checkoutPromptPayArea.style.display = "none"; if (checkoutCreditCardArea) checkoutCreditCardArea.style.display = "block"; if (slipFileInput) slipFileInput.required = false; } else { // Pay Later if (checkoutPromptPayArea) checkoutPromptPayArea.style.display = "none"; if (checkoutCreditCardArea) checkoutCreditCardArea.style.display = "none"; if (slipFileInput) slipFileInput.required = false; } }); }); if (slipFileInput) { slipFileInput.addEventListener("change", (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (event) => { const img = new Image(); img.onload = () => { const canvas = document.createElement("canvas"); const MAX_WIDTH = 600; const MAX_HEIGHT = 600; let width = img.width; let height = img.height; if (width > height) { if (width > MAX_WIDTH) { height *= MAX_WIDTH / width; width = MAX_WIDTH; } } else { if (height > MAX_HEIGHT) { width *= MAX_HEIGHT / height; height = MAX_HEIGHT; } } canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, width, height); const compressedBase64 = canvas.toDataURL("image/jpeg", 0.6); state.selectedSlipBase64 = compressedBase64; previewImg.src = compressedBase64; previewContainer.style.display = "block"; document.getElementById("slipUploadLabel").style.display = "none"; }; img.src = event.target.result; }; reader.readAsDataURL(file); } }); } if (removeSlipBtn) { removeSlipBtn.addEventListener("click", () => { state.selectedSlipBase64 = ""; if (previewImg) previewImg.src = ""; if (previewContainer) previewContainer.style.display = "none"; const slipLabel = document.getElementById("slipUploadLabel"); if (slipLabel) slipLabel.style.display = "flex"; if (slipFileInput) slipFileInput.value = ""; }); } const checkoutForm = document.getElementById("checkoutForm"); if (checkoutForm) { checkoutForm.addEventListener("submit", handlePlaceOrder); } } // ========================================================================== // 10. ORDER SUCCESS & CLIPBOARD TRACKING COPY HOOKS // ========================================================================== function renderOrderSuccess() { if (!state.lastOrderResult) { navigateTo("home"); return; } const o = state.lastOrderResult; const successId = document.getElementById("successOrderId"); if (successId) successId.innerText = o.id; const successTotal = document.getElementById("successOrderTotal"); if (successTotal) successTotal.innerText = `฿${o.total.toLocaleString()}`; const successMethod = document.getElementById("successPaymentMethod"); if (successMethod) successMethod.innerText = o.paymentMethod; } // ========================================================================== // ORDER CANCELLATION SYSTEM CORE ENGINE & CLIENT ORDER HISTORY // ========================================================================== function restockOrderItems(order) { if (!order || !order.items || !Array.isArray(order.items)) return; const products = db.getProducts(); let updated = false; order.items.forEach(item => { const product = products.find(p => p.id === item.productId); if (product && product.variants && Array.isArray(product.variants)) { let variant = product.variants.find(v => v.color === item.color && v.size === item.size); if (!variant && product.variants.length > 0) { variant = product.variants[0]; } if (variant) { variant.stock = (Number(variant.stock) || 0) + (Number(item.quantity) || 1); updated = true; } } }); if (updated) { db.saveProducts(products); } } function restoreOrderCoupon(order) { if (!order || !order.couponCode) return; const users = db.getUsers(); const currentUser = state.currentUser; if (currentUser) { const userObj = users.find(u => u.username === currentUser.username); if (userObj && Array.isArray(userObj.usedCoupons)) { userObj.usedCoupons = userObj.usedCoupons.filter(c => c !== order.couponCode); db.saveUsers(users); if (state.currentUser && state.currentUser.username === userObj.username) { state.currentUser.usedCoupons = userObj.usedCoupons; db.setCurrentUser(state.currentUser); } } } } function handleCancelOrderProcess(orderId, cancelledBy, cancelReason, refundInfo = null) { const ordersDb = db.getOrders(); const targetOrder = ordersDb.find(o => o.id === orderId); if (!targetOrder) { showToast("ไม่พบคำสั่งซื้อที่ต้องการยกเลิก", "danger"); return false; } if (targetOrder.status === "Shipped" || targetOrder.status === "Success") { showToast("ไม่อนุญาตให้ยกเลิกคำสั่งซื้อที่อยู่ระหว่างจัดส่งหรือจัดส่งสำเร็จแล้ว", "danger"); return false; } const now = new Date(); const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`; targetOrder.status = "Cancelled"; targetOrder.cancelledBy = cancelledBy; targetOrder.cancelReason = cancelReason; targetOrder.cancelDate = dateStr; if (refundInfo) { targetOrder.refundInfo = refundInfo; } restockOrderItems(targetOrder); restoreOrderCoupon(targetOrder); db.saveOrders(ordersDb); let cancelledByLabel = "ลูกค้า"; if (cancelledBy === "Seller") cancelledByLabel = "ผู้ขาย"; if (cancelledBy === "Admin") cancelledByLabel = "ผู้ดูแลระบบ"; showToast(`ยกเลิกคำสั่งซื้อ ${orderId} เรียบร้อยแล้ว (โดย${cancelledByLabel})`, "success"); if (typeof renderOrderHistory === "function") renderOrderHistory(); if (typeof renderSellerOrders === "function") renderSellerOrders(); if (typeof renderAdminOrders === "function") renderAdminOrders(); if (typeof renderSellerDashboard === "function") renderSellerDashboard(); if (typeof renderAdminDashboard === "function") renderAdminDashboard(); return true; } function openCustomerCancelModal(order) { const modal = document.getElementById("customerCancelModal"); if (!modal) return; document.getElementById("cancelOrderId").value = order.id; document.getElementById("cancelOrderIdDisplay").innerText = order.id; const reasonSelect = document.getElementById("customerCancelReason"); const detailInput = document.getElementById("customerCancelDetail"); const detailGroup = document.getElementById("customerCancelDetailGroup"); if (reasonSelect) reasonSelect.value = ""; if (detailInput) detailInput.value = ""; if (detailGroup) detailGroup.style.display = "none"; const refundSec = document.getElementById("refundInfoSection"); const isPaidOrPrep = order.status === "Paid" || order.status === "Preparing"; if (refundSec) { if (isPaidOrPrep) { refundSec.style.display = "block"; document.getElementById("refundBankName").required = true; document.getElementById("refundAccountNo").required = true; document.getElementById("refundAccountName").required = true; document.getElementById("refundBankName").value = ""; document.getElementById("refundAccountNo").value = ""; document.getElementById("refundAccountName").value = ""; } else { refundSec.style.display = "none"; document.getElementById("refundBankName").required = false; document.getElementById("refundAccountNo").required = false; document.getElementById("refundAccountName").required = false; } } modal.style.display = "flex"; } function openSellerAdminCancelModal(order) { const modal = document.getElementById("sellerAdminCancelModal"); if (!modal) return; document.getElementById("sellerAdminCancelOrderId").value = order.id; document.getElementById("sellerAdminCancelOrderIdDisplay").innerText = order.id; const reasonSelect = document.getElementById("sellerAdminCancelReason"); const detailInput = document.getElementById("sellerAdminCancelDetail"); const detailGroup = document.getElementById("sellerAdminCancelDetailGroup"); if (reasonSelect) reasonSelect.value = ""; if (detailInput) detailInput.value = ""; if (detailGroup) detailGroup.style.display = "none"; modal.style.display = "flex"; } function initCancelModals() { const custReasonSelect = document.getElementById("customerCancelReason"); const custDetailGroup = document.getElementById("customerCancelDetailGroup"); if (custReasonSelect && custDetailGroup) { custReasonSelect.addEventListener("change", () => { if (custReasonSelect.value === "เหตุผลอื่นๆ") { custDetailGroup.style.display = "block"; } else { custDetailGroup.style.display = "none"; } }); } const custForm = document.getElementById("customerCancelForm"); if (custForm) { custForm.addEventListener("submit", (e) => { e.preventDefault(); const orderId = document.getElementById("cancelOrderId").value; const mainReason = document.getElementById("customerCancelReason").value; const extraDetail = document.getElementById("customerCancelDetail").value.trim(); let fullReason = mainReason; if (extraDetail) fullReason += ` (${extraDetail})`; const refundSec = document.getElementById("refundInfoSection"); let refundInfo = null; if (refundSec && refundSec.style.display !== "none") { refundInfo = { bankName: document.getElementById("refundBankName").value.trim(), accountNo: document.getElementById("refundAccountNo").value.trim(), accountName: document.getElementById("refundAccountName").value.trim() }; } const success = handleCancelOrderProcess(orderId, "Customer", fullReason, refundInfo); if (success) { document.getElementById("customerCancelModal").style.display = "none"; } }); } const closeCustBtn = document.getElementById("closeCustomerCancelModalBtn"); const cancelCustBtn = document.getElementById("cancelCustomerCancelBtn"); const custModal = document.getElementById("customerCancelModal"); if (closeCustBtn && custModal) { closeCustBtn.addEventListener("click", () => custModal.style.display = "none"); } if (cancelCustBtn && custModal) { cancelCustBtn.addEventListener("click", () => custModal.style.display = "none"); } const saReasonSelect = document.getElementById("sellerAdminCancelReason"); const saDetailGroup = document.getElementById("sellerAdminCancelDetailGroup"); if (saReasonSelect && saDetailGroup) { saReasonSelect.addEventListener("change", () => { if (saReasonSelect.value === "เหตุผลอื่นๆ") { saDetailGroup.style.display = "block"; } else { saDetailGroup.style.display = "none"; } }); } const saForm = document.getElementById("sellerAdminCancelForm"); if (saForm) { saForm.addEventListener("submit", (e) => { e.preventDefault(); const orderId = document.getElementById("sellerAdminCancelOrderId").value; const mainReason = document.getElementById("sellerAdminCancelReason").value; const extraDetail = document.getElementById("sellerAdminCancelDetail").value.trim(); let fullReason = mainReason; if (extraDetail) fullReason += ` (${extraDetail})`; const cancelledBy = state.currentRole === "seller" ? "Seller" : "Admin"; const success = handleCancelOrderProcess(orderId, cancelledBy, fullReason); if (success) { document.getElementById("sellerAdminCancelModal").style.display = "none"; } }); } const closeSaBtn = document.getElementById("closeSellerAdminCancelModalBtn"); const cancelSaBtn = document.getElementById("cancelSellerAdminCancelBtn"); const saModal = document.getElementById("sellerAdminCancelModal"); if (closeSaBtn && saModal) { closeSaBtn.addEventListener("click", () => saModal.style.display = "none"); } if (cancelSaBtn && saModal) { cancelSaBtn.addEventListener("click", () => saModal.style.display = "none"); } } // Global active status filter for history page let currentHistoryStatusFilter = "all"; function updateOrderHistoryBadges() { const activeUsername = state.currentUser ? state.currentUser.username : "customer1"; const allOrders = db.getOrders().filter(o => o.customerUsername === activeUsername); const countAll = allOrders.length; const countUnpaid = allOrders.filter(o => o.status === "Unpaid").length; const countPending = allOrders.filter(o => o.status === "Pending").length; const countPaidPreparing = allOrders.filter(o => o.status === "Paid" || o.status === "Preparing").length; const countShipped = allOrders.filter(o => o.status === "Shipped").length; const countSuccess = allOrders.filter(o => o.status === "Success").length; const countCancelled = allOrders.filter(o => o.status === "Cancelled" || o.status === "Failed").length; const bAll = document.getElementById("badge-all"); const bUnpaid = document.getElementById("badge-Unpaid"); const bPending = document.getElementById("badge-Pending"); const bPaidPrep = document.getElementById("badge-Paid_Preparing"); const bShipped = document.getElementById("badge-Shipped"); const bSuccess = document.getElementById("badge-Success"); const bCancelled = document.getElementById("badge-Cancelled") || document.getElementById("badge-Failed"); if (bAll) bAll.innerText = countAll; if (bUnpaid) bUnpaid.innerText = countUnpaid; if (bPending) bPending.innerText = countPending; if (bPaidPrep) bPaidPrep.innerText = countPaidPreparing; if (bShipped) bShipped.innerText = countShipped; if (bSuccess) bSuccess.innerText = countSuccess; if (bCancelled) bCancelled.innerText = countCancelled; } function initOrderHistoryTabs() { const tabs = document.querySelectorAll(".order-status-tab"); tabs.forEach(tab => { tab.addEventListener("click", () => { const status = tab.getAttribute("data-status"); renderOrderHistory(status); }); }); } function renderOrderHistory(statusFilter = currentHistoryStatusFilter) { currentHistoryStatusFilter = statusFilter; const container = document.getElementById("historyListContainer"); if (!container) return; container.innerHTML = ""; const activeUsername = state.currentUser ? state.currentUser.username : "customer1"; const tabs = document.querySelectorAll(".order-status-tab"); tabs.forEach(tab => { if (tab.getAttribute("data-status") === statusFilter) { tab.classList.add("active"); } else { tab.classList.remove("active"); } }); updateOrderHistoryBadges(); const allOrders = db.getOrders().filter(o => o.customerUsername === activeUsername); let filteredOrders = allOrders; if (statusFilter === "Unpaid") { filteredOrders = allOrders.filter(o => o.status === "Unpaid"); } else if (statusFilter === "Pending") { filteredOrders = allOrders.filter(o => o.status === "Pending"); } else if (statusFilter === "Paid_Preparing") { filteredOrders = allOrders.filter(o => o.status === "Paid" || o.status === "Preparing"); } else if (statusFilter === "Shipped") { filteredOrders = allOrders.filter(o => o.status === "Shipped"); } else if (statusFilter === "Success") { filteredOrders = allOrders.filter(o => o.status === "Success"); } else if (statusFilter === "Cancelled" || statusFilter === "Failed") { filteredOrders = allOrders.filter(o => o.status === "Cancelled" || o.status === "Failed"); } if (filteredOrders.length === 0) { container.innerHTML = ` <div class="admin-card text-center py-5"> <i class="fa-solid fa-receipt" style="font-size:3.5rem; color:var(--color-sky-light)"></i> <h3 class="mt-3">ไม่มีประวัติคำสั่งซื้อ</h3> <p style="color:var(--color-sky-slate)">ไม่มีคำสั่งซื้อที่ตรงกับประเภทสถานะนี้</p> <button class="btn btn-sky mt-3" onclick="navigateTo('home')">กลับไปเลือกซื้อสินค้า</button> </div> `; return; } filteredOrders.forEach(order => { const sellerGroups = {}; order.items.forEach(item => { const sId = (item && item.sellerId) ? item.sellerId : "admin"; if (!sellerGroups[sId]) sellerGroups[sId] = []; sellerGroups[sId].push(item); }); const usersList = db.getUsers(); Object.keys(sellerGroups).forEach(sId => { const sellerObj = usersList.find(u => u.username === sId); const sellerName = sellerObj ? sellerObj.name : "ร้านค้าทั่วไป"; const items = sellerGroups[sId]; const trNo = order.trackingNumbers && order.trackingNumbers[sId] ? order.trackingNumbers[sId] : ""; let statusBadge = ""; if (order.status === "Unpaid") statusBadge = '<span class="badge badge-dark" style="font-size:11px;"><i class="fa-solid fa-clock"></i> รอชำระเงิน</span>'; else if (order.status === "Pending") statusBadge = '<span class="badge badge-warning" style="font-size:11px;"><i class="fa-solid fa-spinner fa-spin"></i> รอตรวจสอบ</span>'; else if (order.status === "Paid") statusBadge = '<span class="badge badge-success" style="font-size:11px;"><i class="fa-solid fa-check-circle"></i> ชำระเงินแล้ว</span>'; else if (order.status === "Preparing") statusBadge = '<span class="badge badge-sky" style="font-size:11px;"><i class="fa-solid fa-box"></i> เตรียมพัสดุ</span>'; else if (order.status === "Shipped") statusBadge = '<span class="badge badge-success" style="font-size:11px;"><i class="fa-solid fa-truck"></i> จัดส่งแล้ว</span>'; else if (order.status === "Success") statusBadge = '<span class="badge badge-success" style="font-size:11px; background:#10b981; color:#ffffff !important;"><i class="fa-solid fa-circle-check"></i> จัดส่งสำเร็จ</span>'; else if (order.status === "Cancelled" || order.status === "Failed") statusBadge = '<span class="badge badge-cancelled" style="font-size:11px;"><i class="fa-solid fa-ban"></i> ยกเลิกแล้ว</span>'; const itemsHtml = items.map(item => { const hasOpts = item.color !== "Default" || item.size !== "Default"; const optText = hasOpts ? `${item.color} / ${item.size}` : "ไม่มีตัวเลือก"; const prodPlaceholder = DEFAULT_IMAGE_PLACEHOLDERS[item.productId] || DEFAULT_IMAGE_PLACEHOLDERS["default"]; const prodImg = item.image || prodPlaceholder; return ` <div class="oh-item-row"> <div class="oh-item-img"> <img src="${prodImg}" alt=""> <span class="oh-return-tag">คืนสินค้า 7 วัน</span> </div> <div class="oh-item-info"> <div class="oh-item-name">${item.name}</div> <div class="oh-item-variant">${optText}</div> </div> <div class="oh-item-price">฿${item.price.toLocaleString()}</div> <div class="oh-item-qty">x${item.quantity}</div> </div> `; }).join(""); let trackingSnippet = ""; if (trNo) { trackingSnippet = `<span style="font-size:11px; color:#0d9488;"><i class="fa-solid fa-barcode"></i> ${trNo}</span> <button class="btn-copy-tracking btn btn-sky-outline btn-xs" data-code="${trNo}" style="font-size:9px; padding:1px 5px;">คัดลอก</button>`; } let actionBtns = ""; const isCancellable = order.status === "Unpaid" || order.status === "Pending" || order.status === "Paid" || order.status === "Preparing"; if (isCancellable) { actionBtns += `<button class="btn btn-danger-outline btn-xs btn-request-cancel-order" data-order-id="${order.id}"><i class="fa-solid fa-ban"></i> ขอยกเลิกคำสั่งซื้อ</button>`; } if (order.status === "Unpaid") { actionBtns += ` <button class="btn btn-sky btn-xs btn-show-upload-slip" data-order-id="${order.id}"><i class="fa-solid fa-qrcode"></i> แจ้งชำระเงิน</button>`; } else if (order.status === "Shipped") { actionBtns = `<button class="btn btn-sky btn-xs btn-confirm-receipt" data-order-id="${order.id}">ยืนยันรับสินค้า</button>`; } else if (order.status === "Success") { actionBtns = ` <button class="btn btn-sky-outline btn-xs btn-review-order" data-order-id="${order.id}">ให้คะแนน</button> <button class="btn btn-sky btn-xs btn-buy-again" data-order-id="${order.id}"><i class="fa-solid fa-rotate-right"></i> ซื้ออีกครั้ง</button> `; } let latePayHtml = ""; if (order.status === "Unpaid") { latePayHtml = ` <div class="late-pay-wrapper" style="display:none; padding:12px 16px; background:#fdf2f8; border-top:1px solid #fbcfe8;"> <p style="font-size:0.8rem; color:var(--color-sky-slate); margin-bottom:6px;"> PromptPay ยอดโอน: <strong style="color:var(--color-sky-brand);">฿${order.total.toLocaleString()}</strong> </p> <div style="display:flex; gap:8px; flex-wrap:wrap;"> <input type="file" class="form-control late-slip-input width-auto" accept="image/*" style="font-size:12px;"> <button class="btn btn-sky btn-xs submit-late-slip-btn">อัปโหลดสลิป</button> </div> </div> `; } let cancelDetailsHtml = ""; if (order.status === "Cancelled" || order.status === "Failed") { let byLabel = "ลูกค้า"; if (order.cancelledBy === "Seller") byLabel = "ร้านค้าผู้ขาย"; if (order.cancelledBy === "Admin") byLabel = "ผู้ดูแลระบบ"; let refundText = ""; if (order.refundInfo && order.refundInfo.bankName) { refundText = `<br><strong>ข้อมูลบัญชีคืนเงิน:</strong> ${order.refundInfo.bankName} (${order.refundInfo.accountNo}) - ${order.refundInfo.accountName}`; } cancelDetailsHtml = ` <div class="cancel-details-box m-3"> <div style="font-weight: 700; margin-bottom: 3px; display: flex; align-items: center; gap: 6px;"> <i class="fa-solid fa-circle-exclamation"></i> รายละเอียดการยกเลิกคำสั่งซื้อ </div> <div><strong>ยกเลิกโดย:</strong> ${byLabel} ${order.cancelDate ? `(${order.cancelDate})` : ''}</div> <div><strong>เหตุผลในการยกเลิก:</strong> ${order.cancelReason || 'ไม่ระบุเหตุผล'}</div> ${refundText} </div> `; } const sellerSubtotal = items.reduce((s, i) => s + (i.price * i.quantity), 0); const card = document.createElement("div"); card.className = "history-card"; card.innerHTML = ` <div class="history-card-header"> <div class="history-card-header-left"> <span style="font-weight:600; font-size:0.88rem; color:#0f766e;"><i class="fa-solid fa-store"></i> ${sellerName}</span>${getOfficialBadgeHtml(sId)} <button class="btn btn-sky-outline btn-xs btn-history-go-shop" data-seller-id="${sId}" style="font-size:10px; padding:1px 5px;">ไปที่ร้านค้า</button> <span class="history-order-date">${order.id} · ${order.date}</span> </div> <div>${statusBadge}</div> </div> <div class="history-card-body"> ${itemsHtml} </div> <div class="oh-card-footer"> <div class="oh-footer-left"> ${trackingSnippet} <button class="btn btn-sky-outline btn-xs btn-toggle-detail" style="font-size:10px; padding:1px 5px;"><i class="fa-solid fa-eye"></i> รายละเอียด</button> <button class="btn btn-sky-outline btn-xs print-order-invoice-btn" data-id="${order.id}" style="font-size:10px; padding:1px 5px;"><i class="fa-solid fa-print"></i> พิมพ์</button> </div> <div class="oh-footer-right"> <span style="font-weight:600; font-size:0.9rem; color:var(--color-sky-brand);">รวม: ฿${sellerSubtotal.toLocaleString()}</span> ${actionBtns} </div> </div> <div class="oh-detail-panel"> <div style="display:flex; gap:2rem; flex-wrap:wrap;"> <div> <strong>ผู้รับ:</strong> ${order.shippingInfo.name}<br> <strong>เบอร์:</strong> ${order.shippingInfo.phone}<br> <strong>ที่อยู่:</strong> ${order.shippingInfo.address} </div> <div> <strong>ชำระเงิน:</strong> ${order.paymentMethod || 'PromptPay'}<br> <strong>ยอดรวมทั้งออเดอร์:</strong> ฿${order.total.toLocaleString()} ${order.discount > 0 ? `<br><strong>ส่วนลด:</strong> -฿${order.discount.toLocaleString()} (${order.couponCode})` : ''} </div> </div> </div> ${cancelDetailsHtml} ${latePayHtml} `; const btnDetail = card.querySelector(".btn-toggle-detail"); if (btnDetail) { btnDetail.addEventListener("click", () => { openOrderTrackingModal(order); }); } card.querySelectorAll(".btn-copy-tracking").forEach(btn => { btn.addEventListener("click", () => { const code = btn.getAttribute("data-code"); navigator.clipboard.writeText(code).then(() => { alert(`คัดลอกเลขพัสดุ ${code} แล้ว!`); }); }); }); card.querySelectorAll(".btn-history-go-shop").forEach(btn => { btn.addEventListener("click", () => { const sellerId = btn.getAttribute("data-seller-id"); state.activeFilters.sellerId = sellerId; const filterSellerDropdown = document.getElementById("filterSeller"); if (filterSellerDropdown) filterSellerDropdown.value = sellerId; navigateTo("home"); renderCatalog(); }); }); card.querySelectorAll(".print-order-invoice-btn").forEach(btn => { btn.addEventListener("click", () => { printOrderPackingSlip(btn.getAttribute("data-id")); }); }); const btnShowUpload = card.querySelector(".btn-show-upload-slip"); if (btnShowUpload) { btnShowUpload.addEventListener("click", () => { const latePayWrapper = card.querySelector(".late-pay-wrapper"); if (latePayWrapper) { latePayWrapper.style.display = latePayWrapper.style.display === "none" ? "block" : "none"; } }); } const btnReqCancel = card.querySelector(".btn-request-cancel-order"); if (btnReqCancel) { btnReqCancel.addEventListener("click", () => { openCustomerCancelModal(order); }); } // Confirm receipt const btnConfirm = card.querySelector(".btn-confirm-receipt"); if (btnConfirm) { btnConfirm.addEventListener("click", () => { if (confirm("คุณได้รับสินค้าเรียบร้อยแล้วใช่หรือไม่?")) { const ordersDb = db.getOrders(); const targetOrder = ordersDb.find(o => o.id === order.id); if (targetOrder) { targetOrder.status = "Success"; db.saveOrders(ordersDb); alert("ยืนยันรับสินค้าสำเร็จ!"); renderOrderHistory(statusFilter); } } }); } // Buy again const btnBuyAgain = card.querySelector(".btn-buy-again"); if (btnBuyAgain) { btnBuyAgain.addEventListener("click", () => { order.items.forEach(item => { addToCart(item.productId, item.color, item.size, item.quantity); }); alert("เพิ่มรายการสินค้าเดิมลงตะกร้าแล้ว!"); }); } // Review const btnReview = card.querySelector(".btn-review-order"); if (btnReview) { btnReview.addEventListener("click", () => { if (order.items.length > 0) { const productId = order.items[0].productId; document.getElementById("reviewProductId").value = productId; document.getElementById("reviewComment").value = ""; const checkedRadio = document.querySelector("input[name='reviewRating']:checked"); if (checkedRadio) checkedRadio.checked = false; document.getElementById("productReviewModal").style.display = "flex"; } }); } // Late payment upload if (order.status === "Unpaid") { const fileInput = card.querySelector(".late-slip-input"); const uploadBtn = card.querySelector(".submit-late-slip-btn"); let localBase64 = ""; if (fileInput) { fileInput.addEventListener("change", (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (event) => { const img = new Image(); img.onload = () => { const canvas = document.createElement("canvas"); const MAX_WIDTH = 600; const MAX_HEIGHT = 600; let width = img.width; let height = img.height; if (width > height) { if (width > MAX_WIDTH) { height *= MAX_WIDTH / width; width = MAX_WIDTH; } } else { if (height > MAX_HEIGHT) { width *= MAX_HEIGHT / height; height = MAX_HEIGHT; } } canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, width, height); localBase64 = canvas.toDataURL("image/jpeg", 0.7); }; img.src = event.target.result; }; reader.readAsDataURL(file); } }); } if (uploadBtn) { uploadBtn.addEventListener("click", () => { if (!localBase64) { alert("กรุณาเลือกไฟล์สลิปก่อนกดอัปโหลด"); return; } const ordersDb = db.getOrders(); const targetOrder = ordersDb.find(o => o.id === order.id); if (targetOrder) { try { targetOrder.slipImage = localBase64; targetOrder.status = "Pending"; db.saveOrders(ordersDb); alert("อัปโหลดสลิปเรียบร้อย! รอตรวจสอบการชำระเงิน"); renderOrderHistory(statusFilter); } catch (error) { alert("เกิดข้อผิดพลาด: ไฟล์รูปภาพสลิปอาจมีขนาดใหญ่เกินไป กรุณาลดขนาดไฟล์แล้วลองใหม่อีกครั้ง"); console.error("Error saving slip:", error); } } }); } } container.appendChild(card); }); }); } function renderWishlist() { const grid = document.getElementById("wishlistGrid"); if (!grid) return; grid.innerHTML = ""; state.wishlist = db.getWishlist(); if (state.wishlist.length === 0) { grid.innerHTML = ` <div class="text-center w-100 py-5" style="grid-column: 1/-1;"> <i class="fa-solid fa-heart" style="font-size:3.5rem; color:var(--color-sky-light)"></i> <h3 class="mt-3">ยังไม่มีสินค้าที่ถูกใจ</h3> <p style="color:var(--color-sky-slate)">ลองกลับไปเลือกช้อปสินค้าชิ้นที่คุณสนใจและกดรูปหัวใจได้เลย</p> <button class="btn btn-sky mt-3" onclick="navigateTo('home')">กลับสู่ร้านค้า</button> </div> `; return; } const wishProducts = db.getProducts().filter(p => state.wishlist.includes(p.id) && p.active); wishProducts.forEach(p => { const totalStock = p.variants ? p.variants.reduce((sum, v) => sum + v.stock, 0) : 0; const card = document.createElement("div"); card.className = "product-card"; const placeholderImg = DEFAULT_IMAGE_PLACEHOLDERS[p.id] || DEFAULT_IMAGE_PLACEHOLDERS["default"]; const imgSrc = p.image || placeholderImg; const starHtml = "★".repeat(p.rating) + "☆".repeat(5 - p.rating); card.innerHTML = ` <button class="product-wishlist-btn active" data-id="${p.id}"> <i class="fa-solid fa-heart"></i> </button> <div class="product-image-wrapper"> <div class="product-badge-overlay"> ${totalStock === 0 ? '<span class="badge badge-danger">หมดชั่วคราว</span>' : ''} </div> <img src="${imgSrc}" class="product-img" alt="${p.name}"> </div> <div class="product-details"> <span class="product-category">${p.category}</span> <h4 class="product-title">${p.name}</h4> <div class="product-rating">${starHtml}</div> <div class="product-bottom"> <span class="product-price">฿${p.price.toLocaleString()}</span> <button class="btn btn-sky btn-sm view-detail-btn" data-id="${p.id}"> ดูรายละเอียด </button> </div> </div> `; card.querySelector(".view-detail-btn").addEventListener("click", () => { openProductDetail(p.id); }); card.querySelector(".product-wishlist-btn").addEventListener("click", () => { toggleWishlist(p.id); }); grid.appendChild(card); }); } // ========================================================================== // 11. ADMIN DASHBOARD OPERATIONS // ========================================================================== function renderAdminDashboard() { const orders = db.getOrders(); const products = db.getProducts(); const approvedOrders = orders.filter(o => o.status === "Paid" || o.status === "Preparing" || o.status === "Shipped"); const totalRev = approvedOrders.reduce((sum, o) => sum + o.total, 0); const kpiRev = document.getElementById("kpiTotalRevenue"); if (kpiRev) kpiRev.innerText = `฿${totalRev.toLocaleString()}`; const kpiOrd = document.getElementById("kpiTotalOrders"); if (kpiOrd) kpiOrd.innerText = `${orders.length} ออเดอร์`; let lowStockCount = 0; products.forEach(p => { p.variants.forEach(v => { if (v.stock < 5) lowStockCount++; }); }); const kpiStock = document.getElementById("kpiLowStockAlert"); if (kpiStock) kpiStock.innerText = `${lowStockCount} รายการ`; const categoryStats = {}; DEFAULT_CATEGORIES.forEach(c => categoryStats[c.name] = 0); products.forEach(p => { const total = p.variants ? p.variants.reduce((sum, v) => sum + v.stock, 0) : 0; categoryStats[p.category] = (categoryStats[p.category] || 0) + total; }); const tbody = document.getElementById("categoryStatsTableBody"); if (tbody) { tbody.innerHTML = ""; Object.keys(categoryStats).forEach(cat => { const total = categoryStats[cat]; let statusBadge = `<span class="badge badge-success">คลังหนาแน่น</span>`; if (total === 0) { statusBadge = `<span class="badge badge-danger">หมดคลัง</span>`; } else if (total < 10) { statusBadge = `<span class="badge badge-warning">เหลือน้อย</span>`; } const tr = document.createElement("tr"); tr.innerHTML = ` <td><strong>${cat}</strong></td> <td>${total.toLocaleString()} ชิ้น</td> <td>${statusBadge}</td> `; tbody.appendChild(tr); }); } // Render PromptPay verification queue on Admin Dashboard const pendingTbody = document.getElementById("adminDashboardPendingOrdersTableBody"); if (pendingTbody) { pendingTbody.innerHTML = ""; const pendingOrders = orders.filter(o => o.status === "Pending"); if (pendingOrders.length === 0) { pendingTbody.innerHTML = `<tr><td colspan="5" class="text-center py-3" style="color: var(--color-sky-slate)">ไม่มีคำสั่งซื้อที่รอการตรวจสอบชำระเงิน</td></tr>`; } else { pendingOrders.forEach(order => { const tr = document.createElement("tr"); let slipSrc = order.slipImage; let actualSlip = slipSrc === "dummy_slip" || !slipSrc ? MOCK_SLIP_IMAGE : slipSrc; tr.innerHTML = ` <td><strong>${order.id}</strong></td> <td> <strong>${order.customerName}</strong><br> <small style="color:var(--color-sky-slate)">${order.date}</small> </td> <td>฿${order.total.toLocaleString()}</td> <td> <img src="${actualSlip}" class="slip-thumbnail dashboard-slip-btn" style="width: 50px; height: 50px; object-fit: cover; cursor: pointer; border-radius: 4px; border: 1px solid var(--color-border);" alt="Slip"> </td> <td> <div style="display: flex; gap: 5px;"> <button class="btn btn-sky btn-xs dash-approve-slip-btn" data-id="${order.id}">อนุมัติ</button> <button class="btn btn-danger-outline btn-xs dash-reject-slip-btn" data-id="${order.id}">ปฏิเสธ</button> </div> </td> `; tr.querySelector(".dashboard-slip-btn").addEventListener("click", () => { openSlipOverlay(actualSlip); }); tr.querySelector(".dash-approve-slip-btn").addEventListener("click", () => { const ordersDb = db.getOrders(); const targetOrder = ordersDb.find(o => o.id === order.id); if (targetOrder) { targetOrder.status = "Paid"; db.saveOrders(ordersDb); alert(`อนุมัติการชำระเงินออเดอร์ ${order.id} แล้ว!`); } renderAdminDashboard(); renderAdminOrders(); renderOrderHistory(); }); tr.querySelector(".dash-reject-slip-btn").addEventListener("click", () => { const ordersDb = db.getOrders(); const targetOrder = ordersDb.find(o => o.id === order.id); if (targetOrder) { targetOrder.status = "Failed"; db.saveOrders(ordersDb); alert(`ปฏิเสธการชำระเงินออเดอร์ ${order.id} แล้ว!`); } renderAdminDashboard(); renderAdminOrders(); renderOrderHistory(); }); pendingTbody.appendChild(tr); }); } } // Render Category Stock Bar Chart const canvas = document.getElementById("adminStockChart"); if (canvas) { const categories = Object.keys(categoryStats); const stockValues = Object.values(categoryStats); const isDark = document.body.classList.contains("dark-theme"); const textColor = isDark ? "#f8fafc" : "#0f172a"; const gridColor = isDark ? "#334155" : "#e2e8f0"; const barColor = "#0284c7"; const hoverBarColor = "#0369a1"; if (window.adminStockChartInstance) { window.adminStockChartInstance.destroy(); } const ctx = canvas.getContext("2d"); window.adminStockChartInstance = new Chart(ctx, { type: "bar", data: { labels: categories, datasets: [{ label: "จำนวนสต็อกสินค้า (ชิ้น)", data: stockValues, backgroundColor: barColor, borderColor: barColor, borderWidth: 1, hoverBackgroundColor: hoverBarColor, borderRadius: 4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: textColor, font: { family: "Sarabun, sans-serif", size: 12 } } } }, scales: { x: { grid: { color: gridColor }, ticks: { color: textColor, font: { family: "Sarabun, sans-serif", size: 11 } } }, y: { beginAtZero: true, grid: { color: gridColor }, ticks: { color: textColor, font: { family: "Sarabun, sans-serif", size: 11 } } } } } }); } } // STOCK MANAGEMENT VIEW (ADMIN) function renderAdminStock() { const tbody = document.getElementById("adminStockTableBody"); if (!tbody) return; tbody.innerHTML = ""; state.products = db.getProducts(); const searchQuery = document.getElementById("adminStockSearch").value.toLowerCase(); const catFilter = document.getElementById("adminStockFilterCategory").value; let filtered = state.products.filter(p => { const matchName = p.name.toLowerCase().includes(searchQuery) || (p.sellerName && p.sellerName.toLowerCase().includes(searchQuery)); const matchCat = catFilter === "all" || p.category === catFilter; return matchName && matchCat; }); filtered.forEach(p => { const placeholderImg = DEFAULT_IMAGE_PLACEHOLDERS[p.id] || DEFAULT_IMAGE_PLACEHOLDERS["default"]; const imgSrc = p.image || placeholderImg; const tr = document.createElement("tr"); let optionStockHtml = ""; p.variants.forEach((v, idx) => { const optionLabel = (v.color !== "Default" || v.size !== "Default") ? `${v.color} / ${v.size}` : "คลังปกติ"; const vPrice = v.price !== undefined ? v.price : p.price; optionStockHtml += ` <div class="stock-variant-edit-box" style="margin-bottom: 5px;"> <div class="stock-variant-row" style="display: flex; align-items: center; gap: 5px;"> <span class="badge badge-sky" style="min-width:130px; text-align:center">${optionLabel}</span> <label style="font-size: 11px; margin: 0;">คลัง:</label> <input type="number" class="stock-input-sm variant-stock-input" data-prod-id="${p.id}" data-index="${idx}" value="${v.stock}" min="0" style="width: 60px;"> <label style="font-size: 11px; margin: 0;">ราคา:</label> <input type="number" class="stock-input-sm variant-price-input" data-prod-id="${p.id}" data-index="${idx}" value="${vPrice}" min="0" style="width: 80px;"> </div> </div> `; }); tr.innerHTML = ` <td><img src="${imgSrc}" class="stock-thumbnail" alt=""></td> <td> <strong>${p.name}</strong><br> <span class="badge badge-sky">${p.category}</span> <span class="badge badge-dark" style="background:#0f766e; color:white"><i class="fa-solid fa-store"></i> ${p.sellerName || 'SkyMall'}</span> </td> <td> <span class="product-price">฿${p.price.toLocaleString()}</span> </td> <td> <div style="max-height:150px; overflow-y:auto; padding-right:5px"> ${optionStockHtml} </div> </td> <td> ${p.active ? '<span class="badge badge-success">เปิดขาย</span>' : '<span class="badge badge-danger">ปิดการขาย</span>'} </td> <td> <div class="flex-column gap-1" style="display:flex; gap:5px"> <button class="btn btn-sky btn-xs edit-prod-trigger-btn" data-prod-id="${p.id}"> แก้ไขข้อมูล </button> <button class="btn btn-sky-outline btn-xs save-prod-stock-btn" data-prod-id="${p.id}"> บันทึกคลัง </button> <button class="btn ${p.active ? 'btn-danger-outline' : 'btn-sky'} btn-xs toggle-prod-active-btn" data-prod-id="${p.id}"> ${p.active ? 'ปิดการขาย' : 'เปิดขาย'} </button> <button class="btn btn-danger-outline btn-xs delete-prod-btn" data-prod-id="${p.id}"> ลบสินค้า </button> </div> </td> `; tr.querySelector(".toggle-prod-active-btn").addEventListener("click", () => { p.active = !p.active; db.saveProducts(state.products); renderAdminStock(); renderCatalog(); }); tr.querySelector(".delete-prod-btn").addEventListener("click", () => { if (confirm(`ยืนยันการลบสินค้า "${p.name}"?`)) { const updated = state.products.filter(item => item.id !== p.id); db.saveProducts(updated); alert("ลบสินค้าออกจากระบบแล้ว"); renderAdminStock(); renderCatalog(); } }); tr.querySelector(".edit-prod-trigger-btn").addEventListener("click", () => { openEditProductModal(p.id); }); tr.querySelector(".save-prod-stock-btn").addEventListener("click", () => { const stockInputs = tr.querySelectorAll(".variant-stock-input"); const priceInputs = tr.querySelectorAll(".variant-price-input"); stockInputs.forEach(input => { const idx = parseInt(input.getAttribute("data-index")); const newStock = parseInt(input.value); if (p.variants[idx]) { p.variants[idx].stock = newStock >= 0 ? newStock : 0; } }); priceInputs.forEach(input => { const idx = parseInt(input.getAttribute("data-index")); const newPrice = parseInt(input.value); if (p.variants[idx]) { p.variants[idx].price = newPrice >= 0 ? newPrice : p.price; } }); db.saveProducts(state.products); alert("บันทึกจำนวนคลังและราคาสินค้าสำเร็จ!"); renderAdminStock(); renderCatalog(); }); tbody.appendChild(tr); }); } // Edit product details function openEditProductModal(productId) { const modal = document.getElementById("editProductModal"); const product = db.getProducts().find(p => p.id === productId); if (!product || !modal) return; document.getElementById("editProdId").value = product.id; document.getElementById("editProdName").value = product.name; document.getElementById("editProdCategory").value = product.category; document.getElementById("editProdPrice").value = product.price; const brandInput = document.getElementById("editProdBrand"); if (brandInput) { brandInput.value = product.sellerName || ""; if (state.currentRole === "seller") { brandInput.disabled = true; } else { brandInput.disabled = false; } } document.getElementById("editProdDescription").value = product.description || ""; document.getElementById("editProdCondition").value = product.condition || "new"; // Clear previous file input selection const fileInput = document.getElementById("editProdImage"); if (fileInput) fileInput.value = ""; modal.style.display = "flex"; } function initEditProductFlow() { const modal = document.getElementById("editProductModal"); const closeBtn = document.getElementById("closeEditProductModalBtn"); const form = document.getElementById("editProductForm"); const fileInput = document.getElementById("editProdImage"); let editedImageBase64 = ""; if (fileInput) { fileInput.addEventListener("change", (e) => { const file = e.target.files[0]; const submitBtn = form.querySelector('button[type="submit"]'); if (file) { if (file.size > 1 * 1024 * 1024) { alert("ไฟล์รูปภาพใหญ่เกินไป (สูงสุด 1MB) กรุณาเลือกไฟล์ที่ขนาดเล็กกว่านี้"); fileInput.value = ""; editedImageBase64 = ""; return; } if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> กำลังประมวลผลรูปภาพ...'; } const reader = new FileReader(); reader.onload = (event) => { const img = new Image(); img.onload = () => { try { const canvas = document.createElement("canvas"); const MAX_WIDTH = 600; const MAX_HEIGHT = 600; let width = img.width; let height = img.height; if (width > height) { if (width > MAX_WIDTH) { height *= MAX_WIDTH / width; width = MAX_WIDTH; } } else { if (height > MAX_HEIGHT) { width *= MAX_HEIGHT / height; height = MAX_HEIGHT; } } canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, width, height); editedImageBase64 = canvas.toDataURL("image/webp", 0.7); } catch (err) { console.error("Canvas compression failed", err); alert("เกิดข้อผิดพลาดในการประมวลผลรูปภาพ กรุณาลองรูปภาพอื่นที่มีขนาดเล็กลง"); } finally { if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = '<i class="fa-solid fa-circle-check"></i> บันทึกการแก้ไขข้อมูล'; } } }; img.src = event.target.result; }; reader.readAsDataURL(file); } else { editedImageBase64 = ""; if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = '<i class="fa-solid fa-circle-check"></i> บันทึกการแก้ไขข้อมูล'; } } }); } if (closeBtn) { closeBtn.addEventListener("click", () => { modal.style.display = "none"; }); } if (form) { form.addEventListener("submit", (e) => { e.preventDefault(); const id = document.getElementById("editProdId").value; const products = db.getProducts(); const product = products.find(p => p.id === id); if (product) { product.name = document.getElementById("editProdName").value.trim(); product.category = document.getElementById("editProdCategory").value; product.price = parseInt(document.getElementById("editProdPrice").value); product.sellerName = document.getElementById("editProdBrand").value.trim(); product.description = document.getElementById("editProdDescription").value.trim(); product.condition = document.getElementById("editProdCondition").value; if (editedImageBase64) { product.image = editedImageBase64; } db.saveProducts(products); alert("แก้ไขข้อมูลสินค้าสำเร็จ!"); modal.style.display = "none"; editedImageBase64 = ""; if (state.currentRole === "seller") { renderSellerStock(); } else { renderAdminStock(); } renderCatalog(); } }); } } // Add Product Compiler function initAddProductFlow() { const modal = document.getElementById("addProductModal"); const openBtn = document.getElementById("openAddProductModalBtn"); const closeBtn = document.getElementById("closeAddProductModalBtn"); const form = document.getElementById("addProductForm"); let newProductImageBase64 = ""; const newProdImage = document.getElementById("newProdImage"); if (newProdImage) { newProdImage.addEventListener("change", (e) => { const file = e.target.files[0]; const submitBtn = form.querySelector('button[type="submit"]'); if (file) { if (file.size > 1 * 1024 * 1024) { alert("ไฟล์รูปภาพใหญ่เกินไป (สูงสุด 1MB) กรุณาเลือกไฟล์ที่ขนาดเล็กกว่านี้"); newProdImage.value = ""; newProductImageBase64 = ""; return; } if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> กำลังประมวลผลรูปภาพ...'; } const reader = new FileReader(); reader.onload = (event) => { const img = new Image(); img.onload = () => { try { const canvas = document.createElement("canvas"); const MAX_WIDTH = 600; const MAX_HEIGHT = 600; let width = img.width; let height = img.height; if (width > height) { if (width > MAX_WIDTH) { height *= MAX_WIDTH / width; width = MAX_WIDTH; } } else { if (height > MAX_HEIGHT) { width *= MAX_HEIGHT / height; height = MAX_HEIGHT; } } canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, width, height); newProductImageBase64 = canvas.toDataURL("image/webp", 0.7); } catch (err) { console.error("Canvas compression failed", err); alert("เกิดข้อผิดพลาดในการประมวลผลรูปภาพ กรุณาลองรูปภาพอื่นที่มีขนาดเล็กลง"); } finally { if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = '<i class="fa-solid fa-plus"></i> เพิ่มสินค้าเข้าระบบ'; } } }; img.src = event.target.result; }; reader.readAsDataURL(file); } else { newProductImageBase64 = ""; if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = '<i class="fa-solid fa-plus"></i> เพิ่มสินค้าเข้าระบบ'; } } }); } // Generate variants dynamically const btnGenVariants = document.getElementById("btnGenVariants"); const newProdVariantsWrapper = document.getElementById("newProdVariantsWrapper"); const newProdVariantsList = document.getElementById("newProdVariantsList"); if (btnGenVariants && newProdVariantsList && newProdVariantsWrapper) { btnGenVariants.addEventListener("click", () => { const colorsRaw = document.getElementById("newProdColors").value.trim(); const sizesRaw = document.getElementById("newProdSizes").value.trim(); const basePrice = parseInt(document.getElementById("newProdPrice").value) || 0; const baseStock = parseInt(document.getElementById("newProdSingleStock").value) || 0; const colors = colorsRaw.split(",").map(c => c.trim()).filter(c => c !== ""); const sizes = sizesRaw.split(",").map(s => s.trim()).filter(s => s !== ""); newProdVariantsList.innerHTML = ""; const combinations = []; if (colors.length === 0 && sizes.length === 0) { combinations.push({ color: "Default", size: "Default" }); } else if (colors.length > 0 && sizes.length === 0) { colors.forEach(col => combinations.push({ color: col, size: "Default" })); } else if (colors.length === 0 && sizes.length > 0) { sizes.forEach(sz => combinations.push({ color: "Default", size: sz })); } else { colors.forEach(col => { sizes.forEach(sz => combinations.push({ color: col, size: sz })); }); } combinations.forEach((comb, idx) => { const row = document.createElement("div"); row.className = "variant-row-input"; row.style.display = "flex"; row.style.alignItems = "center"; row.style.justifyContent = "space-between"; row.style.gap = "8px"; row.style.background = "#fff"; row.style.padding = "8px"; row.style.borderRadius = "4px"; row.style.border = "1px solid #e2e8f0"; const label = (comb.color !== "Default" || comb.size !== "Default") ? `${comb.color} / ${comb.size}` : "รูปแบบปกติ"; row.innerHTML = ` <span style="font-size: 13px; font-weight: 500; min-width: 120px;">${label}</span> <div style="display:flex; align-items:center; gap:5px"> <label style="font-size: 11px; margin: 0; color:var(--color-sky-slate)">คลัง:</label> <input type="number" class="form-control variant-add-stock" data-color="${comb.color}" data-size="${comb.size}" value="${baseStock}" min="0" style="padding: 4px; font-size:12px; width: 60px; height: auto;" required> <label style="font-size: 11px; margin: 0; color:var(--color-sky-slate)">ราคา:</label> <input type="number" class="form-control variant-add-price" data-color="${comb.color}" data-size="${comb.size}" value="${basePrice}" min="0" style="padding: 4px; font-size:12px; width: 80px; height: auto;" required> </div> `; newProdVariantsList.appendChild(row); }); newProdVariantsWrapper.style.display = "block"; }); } const resetVariantsWidget = () => { if (newProdVariantsList) newProdVariantsList.innerHTML = ""; if (newProdVariantsWrapper) newProdVariantsWrapper.style.display = "none"; }; if (openBtn) { openBtn.addEventListener("click", () => { const sellerInput = document.getElementById("newProdBrand"); if (sellerInput) { sellerInput.disabled = false; sellerInput.value = ""; } resetVariantsWidget(); modal.style.display = "flex"; }); } const openSellerAddBtn = document.getElementById("openSellerAddProductModalBtn"); if (openSellerAddBtn) { openSellerAddBtn.addEventListener("click", () => { const sellerInput = document.getElementById("newProdBrand"); if (sellerInput && state.currentUser) { sellerInput.value = state.currentUser.name; sellerInput.disabled = true; } // 1. ดึงข้อมูลหมวดหมู่มาใส่ (ที่แก้ไปรอบแรก) const newProdCatSelect = document.getElementById("newProdCategory"); if (newProdCatSelect) { newProdCatSelect.innerHTML = ""; DEFAULT_CATEGORIES.forEach(c => { newProdCatSelect.innerHTML += `<option value="${c.name}">${c.name}</option>`; }); } // 2. เคลียร์ค่ารูปภาพเก่าค้างระบบให้เป็นค่าว่างทุกครั้งที่เปิดหน้าต่างใหม่ newProductImageBase64 = ""; const fileInput = document.getElementById("newProdImage"); if (fileInput) fileInput.value = ""; resetVariantsWidget(); modal.style.display = "flex"; }); } if (closeBtn) { closeBtn.addEventListener("click", () => { modal.style.display = "none"; resetVariantsWidget(); }); } if (form) { form.addEventListener("submit", (e) => { e.preventDefault(); const name = document.getElementById("newProdName").value.trim(); const category = document.getElementById("newProdCategory").value; const price = parseInt(document.getElementById("newProdPrice").value); const sellerNameInput = document.getElementById("newProdBrand").value.trim(); const condition = document.getElementById("newProdCondition").value; const desc = document.getElementById("newProdDescription").value.trim(); const colorsRaw = document.getElementById("newProdColors").value.trim(); const sizesRaw = document.getElementById("newProdSizes").value.trim(); const singleStock = parseInt(document.getElementById("newProdSingleStock").value) || 0; const colors = colorsRaw.split(",").map(c => c.trim()).filter(c => c !== ""); const sizes = sizesRaw.split(",").map(s => s.trim()).filter(s => s !== ""); const variants = []; const variantRows = newProdVariantsList ? newProdVariantsList.querySelectorAll(".variant-row-input") : []; if (newProdVariantsWrapper && newProdVariantsWrapper.style.display === "block" && variantRows.length > 0) { variantRows.forEach(row => { const stockInput = row.querySelector(".variant-add-stock"); const priceInput = row.querySelector(".variant-add-price"); const col = stockInput.getAttribute("data-color"); const sz = stockInput.getAttribute("data-size"); const vStock = parseInt(stockInput.value) || 0; const vPrice = parseInt(priceInput.value) || 0; variants.push({ size: sz, color: col, stock: vStock, price: vPrice }); }); } else { if (colors.length === 0 && sizes.length === 0) { variants.push({ size: "Default", color: "Default", stock: singleStock, price: price }); } else if (colors.length > 0 && sizes.length === 0) { colors.forEach(col => { variants.push({ size: "Default", color: col, stock: singleStock, price: price }); }); } else if (colors.length === 0 && sizes.length > 0) { sizes.forEach(sz => { variants.push({ size: sz, color: "Default", stock: singleStock, price: price }); }); } else { colors.forEach(col => { sizes.forEach(sz => { variants.push({ size: sz, color: col, stock: singleStock, price: price }); }); }); } } const products = db.getProducts(); const newId = "PROD-" + Date.now(); // Assign sellerId based on logged in seller (with fallback for simulation mode) const currentSellerId = (state.currentRole === "seller" && state.currentUser && state.currentUser.role === "seller") ? state.currentUser.username : (state.currentRole === "seller" ? "seller1" : "admin"); const currentSellerName = (state.currentRole === "seller" && state.currentUser && state.currentUser.role === "seller") ? state.currentUser.name : (state.currentRole === "seller" ? "ร้านอุปกรณ์ไอที SkyTech" : (sellerNameInput || "SkyMall")); const newProduct = { id: newId, name: name, category: category, sellerName: currentSellerName, sellerId: currentSellerId, description: desc, price: price, // แก้ไขตรงนี้: ให้เขียนแบบนี้ เพื่อดักว่าถ้ารูประบบยังโหลดไม่เสร็จ ให้ส่งค่าว่างไปเลย image: typeof newProductImageBase64 !== 'undefined' ? newProductImageBase64 : "", active: true, colors: colors, condition: condition, rating: 5, variants: variants }; products.push(newProduct); db.saveProducts(products); alert("เพิ่มสินค้าเข้าระบบสำเร็จ!"); form.reset(); newProductImageBase64 = ""; resetVariantsWidget(); modal.style.display = "none"; if (state.currentRole === "seller") { renderSellerStock(); } else { renderAdminStock(); } renderCatalog(); }); } const stockSearch = document.getElementById("adminStockSearch"); if (stockSearch) stockSearch.addEventListener("input", renderAdminStock); const stockCatFilter = document.getElementById("adminStockFilterCategory"); if (stockCatFilter) stockCatFilter.addEventListener("change", renderAdminStock); const sellerStockSearch = document.getElementById("sellerStockSearch"); if (sellerStockSearch) sellerStockSearch.addEventListener("input", renderSellerStock); const sellerStockCatFilter = document.getElementById("sellerStockFilterCategory"); if (sellerStockCatFilter) sellerStockCatFilter.addEventListener("change", renderSellerStock); } // ADMIN ORDER MANAGEMENT function renderAdminOrders() { const tbody = document.getElementById("adminOrdersTableBody"); if (!tbody) return; tbody.innerHTML = ""; state.orders = db.getOrders(); if (state.orders.length === 0) { tbody.innerHTML = `<tr><td colspan="7" class="text-center py-4" style="color:var(--color-sky-slate)">ยังไม่มีรายการสั่งซื้อเข้ามาในขณะนี้</td></tr>`; return; } const usersList = db.getUsers(); state.orders.forEach(order => { // Group order items by seller so admin sees seller labels const sellerGroups = {}; order.items.forEach(item => { const sId = item.sellerId || "admin"; if (!sellerGroups[sId]) sellerGroups[sId] = []; sellerGroups[sId].push(item); }); let itemsHtml = ""; Object.keys(sellerGroups).forEach(sId => { const sellerObj = usersList.find(u => u.username === sId); const sellerName = sellerObj ? sellerObj.name : "ร้านค้าทั่วไป"; const items = sellerGroups[sId]; const tracking = order.trackingNumbers && order.trackingNumbers[sId] ? order.trackingNumbers[sId] : ""; const itemsRows = items.map(item => { const hasOptionLabels = item.color !== "Default" || item.size !== "Default"; const metaStr = hasOptionLabels ? ` (${item.color}/${item.size})` : ""; return `<li>- ${item.name}${metaStr} <strong>x${item.quantity}</strong></li>`; }).join(""); itemsHtml += ` <div style="border-bottom: 1px dashed var(--color-border); padding-bottom:5px; margin-bottom:5px;"> <div style="font-weight:bold; font-size:11px; color:#0d9488;"><i class="fa-solid fa-store"></i> ${sellerName}</div> <ul class="order-items-mini-list" style="margin: 0; padding-left: 15px;">${itemsRows}</ul> <div style="font-size:11px; color:var(--color-sky-slate); margin-top:2px;"> เลขพัสดุร้าน: <strong>${tracking || 'ยังไม่ระบุ'}</strong> </div> </div> `; }); const tr = document.createElement("tr"); let slipSrc = order.slipImage; let isUnpaidNoSlip = order.status === "Unpaid" && !order.slipImage; let slipCellHtml = ""; if (isUnpaidNoSlip) { slipCellHtml = `<span style="color:var(--color-sky-slate); font-size:11px"><i class="fa-solid fa-clock"></i> รอชำระเงินภายหลัง</span>`; } else { let actualSlip = slipSrc === "dummy_slip" || !slipSrc ? MOCK_SLIP_IMAGE : slipSrc; slipCellHtml = `<img src="${actualSlip}" class="slip-thumbnail slip-expand-btn" alt="Slip">`; } let statusSelectHtml = ""; const isAdminCancellable = order.status === "Unpaid" || order.status === "Pending" || order.status === "Paid" || order.status === "Preparing"; if (order.status === "Unpaid") { statusSelectHtml = ` <div style="display:flex; flex-direction:column; gap:5px"> <button class="btn btn-sky btn-sm force-approve-unpaid-btn w-100" data-id="${order.id}"> รับโอน/เงินสดแล้ว (อนุมัติ) </button> <button class="btn btn-danger-outline btn-xs admin-cancel-order-btn w-100" data-id="${order.id}"> <i class="fa-solid fa-ban"></i> ยกเลิกออเดอร์ </button> </div> `; } else if (order.status === "Pending") { statusSelectHtml = ` <div style="display:flex; flex-direction:column; gap:5px"> <button class="btn btn-sky btn-sm approve-slip-btn w-100" data-id="${order.id}"> อนุมัติการชำระเงิน </button> <button class="btn btn-danger-outline btn-xs admin-cancel-order-btn w-100" data-id="${order.id}"> <i class="fa-solid fa-ban"></i> ปฏิเสธ/ยกเลิกออเดอร์ </button> </div> `; } else if (order.status === "Paid" || order.status === "Preparing") { let inputsHtml = ""; Object.keys(sellerGroups).forEach(sId => { const sellerObj = usersList.find(u => u.username === sId); const sellerName = sellerObj ? sellerObj.name : "ร้านค้า"; const currentTrack = order.trackingNumbers && order.trackingNumbers[sId] ? order.trackingNumbers[sId] : ""; inputsHtml += ` <div style="margin-bottom: 5px; font-size: 11px;"> <label style="display:block; margin-bottom:2px;"><strong>${sellerName}:</strong></label> <div style="display:flex; gap:3px;"> <input type="text" class="form-control tracking-input-for-seller" data-seller-id="${sId}" value="${currentTrack}" placeholder="เลขพัสดุ..." style="padding: 2px 5px; font-size:11px; height:auto; width:100px;"> <button class="btn btn-sky btn-xs admin-ship-confirm-btn" data-id="${order.id}" data-seller-id="${sId}">อัปเดตเป็นสถานะจัดส่งแล้ว</button> </div> </div> `; }); statusSelectHtml = ` <div class="form-group-sub" style="display:flex; flex-direction:column; gap:5px"> ${inputsHtml} <button class="btn btn-danger-outline btn-xs admin-cancel-order-btn w-100" data-id="${order.id}"> <i class="fa-solid fa-ban"></i> ยกเลิกออเดอร์ </button> </div> `; } else if (order.status === "Shipped") { statusSelectHtml = ` <div class="text-success mb-1"> <i class="fa-solid fa-truck"></i> อยู่ระหว่างจัดส่ง<br> </div> <button class="btn btn-success btn-xs admin-mark-success-btn" data-id="${order.id}"> <i class="fa-solid fa-circle-check"></i> จัดส่งสำเร็จ </button> `; } else if (order.status === "Cancelled" || order.status === "Failed") { statusSelectHtml = ` <div class="text-danger" style="font-size:11px"> <i class="fa-solid fa-circle-xmark"></i> ยกเลิกแล้ว<br> <small style="color:var(--color-sky-slate)">(${order.cancelReason || 'ไม่ระบุ'})</small> </div> `; } tr.innerHTML = ` <td> <strong>${order.id}</strong><br> <span class="text-muted" style="font-size:11px">${order.date}</span> </td> <td> <strong>${order.customerName}</strong><br> <span style="font-size:11px">${order.shippingInfo.phone}</span><br> <div style="max-width:180px; font-size:11px; white-space:normal">${order.shippingInfo.address}</div> </td> <td> ${itemsHtml} </td> <td> ฿${order.total.toLocaleString()}<br> ${order.discount > 0 ? `<small class="text-success">ลด ฿${order.discount.toLocaleString()}</small>` : ''} </td> <td> ${slipCellHtml} </td> <td> ${order.status === "Unpaid" ? '<span class="badge badge-dark">รอชำระเงิน</span>' : ''} ${order.status === "Pending" ? '<span class="badge badge-warning">รออนุมัติ</span>' : ''} ${order.status === "Paid" ? '<span class="badge badge-success">ชำระเงินแล้ว</span>' : ''} ${order.status === "Preparing" ? '<span class="badge badge-sky">เตรียมส่ง</span>' : ''} ${order.status === "Shipped" ? '<span class="badge badge-success">จัดส่งแล้ว</span>' : ''} ${order.status === "Success" ? '<span class="badge badge-success" style="background:#10b981; color:#fff !important;">จัดส่งสำเร็จ</span>' : ''} ${(order.status === "Cancelled" || order.status === "Failed") ? '<span class="badge badge-cancelled">ยกเลิกแล้ว</span>' : ''} </td> <td> <div style="display:flex; flex-direction:column; gap:5px"> ${statusSelectHtml} <button class="btn btn-white btn-xs print-slip-trigger-btn" data-id="${order.id}"> พิมพ์ใบปะหน้า </button> </div> </td> `; const slipImgEl = tr.querySelector(".slip-expand-btn"); if (slipImgEl) { slipImgEl.addEventListener("click", () => { let actualSlip = slipSrc === "dummy_slip" || !slipSrc ? MOCK_SLIP_IMAGE : slipSrc; openSlipOverlay(actualSlip); }); } const adminCancelBtn = tr.querySelector(".admin-cancel-order-btn"); if (adminCancelBtn) { adminCancelBtn.addEventListener("click", () => { openSellerAdminCancelModal(order); }); } const approveBtn = tr.querySelector(".approve-slip-btn"); if (approveBtn) { approveBtn.addEventListener("click", () => { const ordersDb = db.getOrders(); const targetOrder = ordersDb.find(o => o.id === order.id); if (targetOrder) { targetOrder.status = "Paid"; db.saveOrders(ordersDb); } renderAdminOrders(); renderAdminDashboard(); renderOrderHistory(); }); } const rejectBtn = tr.querySelector(".reject-slip-btn"); if (rejectBtn) { rejectBtn.addEventListener("click", () => { const ordersDb = db.getOrders(); const targetOrder = ordersDb.find(o => o.id === order.id); if (targetOrder) { targetOrder.status = "Failed"; db.saveOrders(ordersDb); } renderAdminOrders(); renderAdminDashboard(); renderOrderHistory(); }); } const forceApproveBtn = tr.querySelector(".force-approve-unpaid-btn"); if (forceApproveBtn) { forceApproveBtn.addEventListener("click", () => { const ordersDb = db.getOrders(); const targetOrder = ordersDb.find(o => o.id === order.id); if (targetOrder) { targetOrder.status = "Paid"; db.saveOrders(ordersDb); } renderAdminOrders(); renderAdminDashboard(); renderOrderHistory(); }); } tr.querySelectorAll(".admin-ship-confirm-btn").forEach(btn => { btn.addEventListener("click", () => { const sId = btn.getAttribute("data-seller-id"); const row = btn.closest("div"); const trNo = row.querySelector(".tracking-input-for-seller").value.trim(); const ordersDb = db.getOrders(); const targetOrder = ordersDb.find(o => o.id === order.id); if (targetOrder) { targetOrder.trackingNumbers = targetOrder.trackingNumbers || {}; if (trNo) { targetOrder.trackingNumbers[sId] = trNo; } // Force status to shipped immediately for prototype flow targetOrder.status = "Shipped"; db.saveOrders(ordersDb); alert("อัปเดตสถานะออเดอร์เป็น 'อยู่ระหว่างจัดส่ง' เรียบร้อย!"); } renderAdminOrders(); renderAdminDashboard(); renderOrderHistory(); }); }); tr.querySelector(".print-slip-trigger-btn").addEventListener("click", () => { printOrderPackingSlip(order.id); }); const adminSuccessBtn = tr.querySelector(".admin-mark-success-btn"); if (adminSuccessBtn) { adminSuccessBtn.addEventListener("click", () => { const ordersDb = db.getOrders(); const targetOrder = ordersDb.find(o => o.id === order.id); if (targetOrder) { targetOrder.status = "Success"; db.saveOrders(ordersDb); alert(`อัปเดตสถานะออเดอร์ ${order.id} เป็นจัดส่งสำเร็จเรียบร้อยแล้ว!`); } renderAdminOrders(); renderAdminDashboard(); renderOrderHistory(); }); } tbody.appendChild(tr); }); } function openSlipOverlay(src) { const modal = document.getElementById("slipOverlayModal"); const img = document.getElementById("expandedSlipImg"); const newTabBtn = document.getElementById("btnOpenSlipNewTab"); if (modal && img) { img.src = src; if (newTabBtn) { newTabBtn.href = src; } modal.style.display = "flex"; } } const closeSlipBtn = document.getElementById("closeSlipOverlayBtn"); if (closeSlipBtn) { closeSlipBtn.addEventListener("click", () => { const modal = document.getElementById("slipOverlayModal"); if (modal) modal.style.display = "none"; }); } // PRINT PREVIEW PACKING SLIP function printOrderPackingSlip(orderId) { const order = db.getOrders().find(o => o.id === orderId); if (!order) return; const printContainer = document.getElementById("printPackingSlipContainer"); if (!printContainer) return; const itemsRows = order.items.map((item, idx) => { const optionDetails = (item.color !== "Default" || item.size !== "Default") ? ` (${item.color}/${item.size})` : ""; return ` <tr> <td>${idx + 1}</td> <td>${item.name}${optionDetails}</td> <td>${item.color !== "Default" ? item.color : '-'}</td> <td>${item.size !== "Default" ? item.size : '-'}</td> <td>${item.quantity}</td> </tr> `; }).join(""); printContainer.innerHTML = ` <div class="print-slip-box"> <div class="print-slip-header"> <div> <div class="print-slip-title">SkyMall Packing Slip</div> <div>เลขที่ออเดอร์: <strong>${order.id}</strong></div> </div> <div class="print-slip-info"> วันที่แพ็ก: ${order.date}<br> ขนส่ง: Standard Delivery </div> </div> <div class="print-slip-addresses"> <div> <div class="print-address-title">ผู้ส่ง (Sender)</div> <div class="print-address-text"> <strong>คลังสินค้าหลัก SkyMall Depot</strong><br> 99/1 ถนนสุขุมวิท แขวงคลองเตย<br> เขตคลองเตย กรุงเทพมหานคร 10110<br> โทร: 02-123-4567 </div> </div> <div> <div class="print-address-title">ผู้รับ (Recipient)</div> <div class="print-address-text"> <strong>${order.shippingInfo.name}</strong><br> ที่อยู่: ${order.shippingInfo.address}<br> เบอร์ติดต่อ: ${order.shippingInfo.phone} </div> </div> </div> <table class="print-slip-items-table"> <thead> <tr> <th style="width: 8%;">ลำดับ</th> <th>รายการสินค้า</th> <th style="width: 15%;">สี</th> <th style="width: 12%;">ไซส์/ขนาด</th> <th style="width: 12%;">จำนวน</th> </tr> </thead> <tbody> ${itemsRows} </tbody> </table> <div class="print-barcode-wrapper"> <div class="print-barcode"></div> <div class="print-barcode-text">*${order.id}*</div> </div> </div> `; window.print(); } // COUPONS MANAGEMENT VIEW function renderAdminCoupons() { const tbody = document.getElementById("adminCouponTableBody"); if (!tbody) return; tbody.innerHTML = ""; state.coupons = db.getCoupons(); state.coupons.forEach((coupon, idx) => { const tr = document.createElement("tr"); tr.innerHTML = ` <td><strong>${coupon.code}</strong></td> <td> ${coupon.type === "percent" ? `ลด ${coupon.value}%` : `ลด ฿${coupon.value}`} </td> <td> <button class="btn btn-danger-outline btn-xs delete-coupon-btn" data-index="${idx}"> ลบคูปอง </button> </td> `; tr.querySelector(".delete-coupon-btn").addEventListener("click", () => { if (confirm("ยืนยันการลบรหัสคูปองนี้?")) { state.coupons.splice(idx, 1); db.saveCoupons(state.coupons); renderAdminCoupons(); renderCouponShowcase(); } }); tbody.appendChild(tr); }); } function initCouponsFlow() { const form = document.getElementById("createCouponForm"); if (form) { form.addEventListener("submit", (e) => { e.preventDefault(); const code = document.getElementById("newCouponCode").value.trim().toUpperCase(); const type = document.getElementById("couponDiscountType").value; const val = parseInt(document.getElementById("newCouponValue").value); if (!code || val <= 0) return; const coupons = db.getCoupons(); if (coupons.some(c => c.code === code)) { alert("รหัสคูปองนี้มีอยู่แล้วในระบบ"); return; } // Set default minimum spend based on type or simple rule const minSpend = type === "percent" ? 500 : 300; coupons.push({ code: code, type: type, value: val, minSpend: minSpend }); db.saveCoupons(coupons); alert("เพิ่มคูปองส่วนลดสำเร็จ!"); form.reset(); renderAdminCoupons(); renderCouponShowcase(); }); } } // ========================================================================== // 12. SELLER DASHBOARD & STOCK & ORDER OPERATIONS // ========================================================================== function renderSellerDashboard() { const currentSellerId = (state.currentUser && state.currentUser.role === "seller") ? state.currentUser.username : "seller1"; const orders = db.getOrders(); const products = db.getProducts(); // Calculate total revenue for this seller's products in paid/preparing/shipped orders const approvedOrders = orders.filter(o => o.status === "Paid" || o.status === "Preparing" || o.status === "Shipped"); let sellerRevenue = 0; let sellerOrderCount = 0; approvedOrders.forEach(order => { let hasSellerProduct = false; order.items.forEach(item => { if (item.sellerId === currentSellerId) { sellerRevenue += item.price * item.quantity; hasSellerProduct = true; } }); if (hasSellerProduct) sellerOrderCount++; }); const kpiRev = document.getElementById("kpiSellerTotalRevenue"); if (kpiRev) kpiRev.innerText = `฿${sellerRevenue.toLocaleString()}`; const kpiOrd = document.getElementById("kpiSellerTotalOrders"); if (kpiOrd) kpiOrd.innerText = `${sellerOrderCount} ออเดอร์`; // Calculate low stock alert specific to seller let sellerLowStockCount = 0; const sellerProducts = products.filter(p => p.sellerId === currentSellerId); sellerProducts.forEach(p => { p.variants.forEach(v => { if (v.stock < 5) sellerLowStockCount++; }); }); const kpiStock = document.getElementById("kpiSellerLowStockAlert"); if (kpiStock) kpiStock.innerText = `${sellerLowStockCount} รายการ`; // Render 3 Best Selling Products renderSellerTopProducts(currentSellerId); } function renderSellerTopProducts(currentSellerId) { const tbody = document.getElementById("sellerTopProductsBody"); if (!tbody) return; tbody.innerHTML = ""; const orders = db.getOrders(); const approvedStatuses = ["Paid", "Preparing", "Shipped", "Success"]; const filteredOrders = orders.filter(o => approvedStatuses.includes(o.status)); const productStats = {}; filteredOrders.forEach(order => { order.items.forEach(item => { if (item.sellerId === currentSellerId) { const prodId = item.productId; if (!productStats[prodId]) { productStats[prodId] = { name: item.name, quantity: 0, revenue: 0 }; } productStats[prodId].quantity += item.quantity; productStats[prodId].revenue += item.price * item.quantity; } }); }); const sortedStats = Object.values(productStats) .sort((a, b) => b.quantity - a.quantity) .slice(0, 3); if (sortedStats.length === 0) { tbody.innerHTML = `<tr><td colspan="4" style="text-align: center; color: var(--color-sky-slate); padding: 15px 0;">ยังไม่มีข้อมูลยอดขายในขณะนี้</td></tr>`; return; } let html = ""; sortedStats.forEach((stat, index) => { const badgeClass = index === 0 ? "badge-warning" : (index === 1 ? "badge-sky" : "badge-dark"); html += ` <tr> <td style="width: 80px;"><span class="badge ${badgeClass}" style="padding: 2px 8px;">#${index + 1}</span></td> <td><strong>${stat.name}</strong></td> <td style="text-align: center; width: 150px;">${stat.quantity.toLocaleString()} ชิ้น</td> <td style="text-align: right; width: 150px; font-weight: 600; color: var(--color-sky-brand);">฿${stat.revenue.toLocaleString()}</td> </tr> `; }); tbody.innerHTML = html; } // ========================================================================== // 12. ADMIN USER, SELLER & BAN MANAGEMENT SYSTEM // ========================================================================== function banUserAccount(username) { const allUsers = db.getUsers(); const target = allUsers.find(u => u.username === username); if (target) { target.isBanned = true; const now = new Date(); const yearTH = now.getFullYear() + 543; const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const time = now.toTimeString().split(' ')[0].substring(0, 5); target.bannedAt = `${yearTH}-${month}-${day} ${time} น.`; db.saveUsers(allUsers); showToast(`ระงับการใช้งานบัญชี "@${username}" และบันทึกประวัติการแบนเรียบร้อยแล้ว`, "danger"); // If currently logged-in user is banned, logout immediately if (state.currentUser && state.currentUser.username === username) { db.setCurrentUser(null); updateAuthHeader(); } renderAdminUsersPage(); initFilters(); renderCatalog(); } } function unbanUserAccount(username) { const allUsers = db.getUsers(); const target = allUsers.find(u => u.username === username); if (target) { target.isBanned = false; target.bannedAt = null; db.saveUsers(allUsers); showToast(`ปลดแบนบัญชี "@${username}" สำเร็จ บัญชีกลับมาใช้งานได้ตามปกติแล้ว`, "success"); renderAdminUsersPage(); initFilters(); renderCatalog(); } } function deleteUserAccountPermanently(username) { const allUsers = db.getUsers().filter(u => u.username !== username); db.saveUsers(allUsers); showToast(`ลบบัญชี "@${username}" ออกจากระบบอย่างถาวรเรียบร้อยแล้ว`, "success"); if (state.currentUser && state.currentUser.username === username) { db.setCurrentUser(null); updateAuthHeader(); } renderAdminUsersPage(); initFilters(); renderCatalog(); } function renderAdminClients() { const tbody = document.getElementById("adminClientTableBody"); if (!tbody) return; tbody.innerHTML = ""; const users = db.getUsers(); const clients = users.filter(u => (u.role === "client" || (!u.role && u.username !== "admin")) && !u.isBanned); if (clients.length === 0) { tbody.innerHTML = `<tr><td colspan="7" class="text-center py-4" style="color:var(--color-sky-slate)">ไม่พบบัญชีลูกค้าปกติในระบบ</td></tr>`; return; } clients.forEach(c => { const tr = document.createElement("tr"); const genderLabel = c.gender === "male" ? "ชาย" : (c.gender === "female" ? "หญิง" : (c.gender === "other" ? "อื่นๆ" : "ไม่ระบุ")); const addrCount = c.addresses ? c.addresses.length : 0; const statusBadge = `<span class="badge-status-active"><i class="fa-solid fa-circle-check"></i> ปกติ (Active)</span>`; tr.innerHTML = ` <td><strong>${c.username}</strong></td> <td><strong>${c.name || '-'}</strong></td> <td>${c.age ? c.age + ' ปี' : '-'}</td> <td>${genderLabel}</td> <td>${addrCount} รายการ</td> <td>${statusBadge}</td> <td> <div style="display:flex; gap:6px; flex-wrap:wrap;"> <button class="btn btn-sky-outline btn-xs btn-promote-seller" data-username="${c.username}"> <i class="fa-solid fa-store"></i> สลับเป็นผู้ขาย </button> <button class="btn btn-warning-outline btn-xs btn-ban-user" data-username="${c.username}"> <i class="fa-solid fa-ban"></i> แบน </button> <button class="btn btn-danger-outline btn-xs btn-delete-user-permanent" data-username="${c.username}"> <i class="fa-solid fa-trash"></i> ลบถาวร </button> </div> </td> `; // Promote to Seller tr.querySelector(".btn-promote-seller").addEventListener("click", () => { if (confirm(`ต้องการเปลี่ยนสถานะบัญชีลูกค้า "${c.name || c.username}" (@${c.username}) เป็นบัญชีผู้ขายหรือไม่?`)) { const allUsers = db.getUsers(); const target = allUsers.find(u => u.username === c.username); if (target) { target.role = "seller"; target.sellerType = target.sellerType || "regular"; db.saveUsers(allUsers); showToast(`เปลี่ยนบัญชี "${target.name}" (@${target.username}) เป็นผู้ขายเรียบร้อยแล้ว`, "success"); renderAdminUsersPage(); initFilters(); renderCatalog(); } } }); // Ban User Action tr.querySelector(".btn-ban-user").addEventListener("click", () => { if (confirm(`ยืนยันการแบนบัญชีลูกค้า "${c.name || c.username}" (@${c.username})? บัญชีนี้จะถูกระงับการเข้าสู่ระบบทันที`)) { banUserAccount(c.username); } }); // Permanent Delete User Action tr.querySelector(".btn-delete-user-permanent").addEventListener("click", () => { if (confirm(`คุณแน่ใจหรือไม่ที่จะลบบัญชี "${c.name || c.username}" (@${c.username}) ออกจากระบบอย่างถาวร?\n\nข้อมูลผู้ใช้นี้จะถูกลบหายไปโดยสิ้นเชิง และจะไม่บันทึกลงในประวัติการแบน`)) { deleteUserAccountPermanently(c.username); } }); tbody.appendChild(tr); }); } function renderAdminSellers() { const tbody = document.getElementById("adminSellerTableBody"); if (!tbody) { renderAdminUsersPage(); return; } tbody.innerHTML = ""; const users = db.getUsers(); const sellers = users.filter(u => u.role === "seller" && !u.isBanned); if (sellers.length === 0) { tbody.innerHTML = `<tr><td colspan="6" class="text-center py-4" style="color:var(--color-sky-slate)">ไม่พบบัญชีผู้ขายปกติในระบบ</td></tr>`; return; } sellers.forEach(s => { const tr = document.createElement("tr"); let levelBadge = ""; if (s.sellerType === "corporate") { levelBadge = `<span class="official-seller-badge-lg"><i class="fa-solid fa-circle-check"></i> Official Corporate</span>`; } else { levelBadge = `<span class="badge-regular-seller"><i class="fa-solid fa-store"></i> Regular Seller</span>`; } const statusBadge = `<span class="badge-status-active"><i class="fa-solid fa-circle-check"></i> ปกติ (Active)</span>`; tr.innerHTML = ` <td><strong>${s.username}</strong></td> <td><strong>${s.name}</strong></td> <td>${s.companyName || '-'}</td> <td>${levelBadge}</td> <td>${statusBadge}</td> <td> <div style="display:flex; gap:6px; flex-wrap:wrap;"> <button class="btn btn-sky-outline btn-xs btn-toggle-seller-type" data-username="${s.username}"> <i class="fa-solid fa-arrows-rotate"></i> สลับระดับ </button> <button class="btn btn-warning-outline btn-xs btn-ban-user" data-username="${s.username}"> <i class="fa-solid fa-ban"></i> แบน </button> <button class="btn btn-danger-outline btn-xs btn-delete-user-permanent" data-username="${s.username}"> <i class="fa-solid fa-trash"></i> ลบถาวร </button> </div> </td> `; // Toggle Seller Level (Regular <-> Corporate) tr.querySelector(".btn-toggle-seller-type").addEventListener("click", () => { const allUsers = db.getUsers(); const target = allUsers.find(u => u.username === s.username); if (target) { if (target.sellerType === "corporate") { target.sellerType = "regular"; db.saveUsers(allUsers); showToast(`สลับระดับผู้ขาย "${s.name}" เป็น Regular Seller เรียบร้อยแล้ว`, "success"); } else { target.sellerType = "corporate"; if (!target.companyName || target.companyName.trim() === "") { const inputComp = prompt(`ระบุชื่อนิติบุคคล / บริษัท สำหรับผู้ขายรายใหญ่ (Official Corporate Seller) "${s.name}":`, s.name + " Co., Ltd."); if (inputComp && inputComp.trim() !== "") { target.companyName = inputComp.trim(); } } db.saveUsers(allUsers); showToast(`อัปเดตผู้ขาย "${s.name}" เป็น Official Corporate Seller สำเร็จ!`, "success"); } renderAdminUsersPage(); initFilters(); renderCatalog(); } }); // Ban Action tr.querySelector(".btn-ban-user").addEventListener("click", () => { if (confirm(`ยืนยันการแบนบัญชีผู้ขาย "${s.name}" (@${s.username})? ร้านค้าและบัญชีนี้จะถูกระงับการใช้งานชั่วคราว`)) { banUserAccount(s.username); } }); // Permanent Delete Action tr.querySelector(".btn-delete-user-permanent").addEventListener("click", () => { if (confirm(`คุณแน่ใจหรือไม่ที่จะลบบัญชีผู้ขาย "${s.name}" (@${s.username}) ออกจากระบบอย่างถาวร?\n\nข้อมูลผู้ขายจะถูกลบหายไปอย่างสิ้นเชิง และจะไม่ถูกบันทึกไว้ในประวัติการแบน`)) { deleteUserAccountPermanently(s.username); } }); tbody.appendChild(tr); }); } function renderAdminBannedUsers() { const tbody = document.getElementById("adminBannedTableBody"); if (!tbody) return; tbody.innerHTML = ""; const users = db.getUsers(); const bannedUsers = users.filter(u => u.isBanned); if (bannedUsers.length === 0) { tbody.innerHTML = `<tr><td colspan="5" class="text-center py-4" style="color:var(--color-sky-slate)">ไม่มีประวัติบัญชีที่ถูกแบนในขณะนี้</td></tr>`; return; } bannedUsers.forEach(b => { const tr = document.createElement("tr"); const accountTypeBadge = b.role === "seller" ? `<span class="badge badge-sky" style="font-size:11px;"><i class="fa-solid fa-store"></i> ผู้ขาย (Seller)</span>` : `<span class="badge badge-dark" style="font-size:11px; background:#64748b;"><i class="fa-solid fa-user"></i> ลูกค้า (Client)</span>`; tr.innerHTML = ` <td><strong>${b.username}</strong></td> <td><strong>${b.name || '-'}</strong></td> <td>${accountTypeBadge}</td> <td><span class="text-danger" style="font-size:12px; font-weight:600;"><i class="fa-solid fa-clock"></i> ${b.bannedAt || 'ไม่ระบุวันเวลา'}</span></td> <td> <button class="btn btn-success btn-xs btn-unban-user" data-username="${b.username}"> <i class="fa-solid fa-rotate-left"></i> ปลดแบน (Unban) </button> </td> `; tr.querySelector(".btn-unban-user").addEventListener("click", () => { if (confirm(`ยืนยันการปลดแบนบัญชี "@${b.username}" (${b.name}) คืนสิทธิ์การใช้งานตามปกติ?`)) { unbanUserAccount(b.username); } }); tbody.appendChild(tr); }); } function renderAdminUsersPage() { renderAdminClients(); // Render Sellers table const tbodySeller = document.getElementById("adminSellerTableBody"); if (tbodySeller) { tbodySeller.innerHTML = ""; const users = db.getUsers(); const sellers = users.filter(u => u.role === "seller" && !u.isBanned); if (sellers.length === 0) { tbodySeller.innerHTML = `<tr><td colspan="6" class="text-center py-4" style="color:var(--color-sky-slate)">ไม่พบบัญชีผู้ขายปกติในระบบ</td></tr>`; } else { sellers.forEach(s => { const tr = document.createElement("tr"); let levelBadge = ""; if (s.sellerType === "corporate") { levelBadge = `<span class="official-seller-badge-lg"><i class="fa-solid fa-circle-check"></i> Official Corporate</span>`; } else { levelBadge = `<span class="badge-regular-seller"><i class="fa-solid fa-store"></i> Regular Seller</span>`; } const statusBadge = `<span class="badge-status-active"><i class="fa-solid fa-circle-check"></i> ปกติ (Active)</span>`; tr.innerHTML = ` <td><strong>${s.username}</strong></td> <td><strong>${s.name}</strong></td> <td>${s.companyName || '-'}</td> <td>${levelBadge}</td> <td>${statusBadge}</td> <td> <div style="display:flex; gap:6px; flex-wrap:wrap;"> <button class="btn btn-sky-outline btn-xs btn-toggle-seller-type" data-username="${s.username}"> <i class="fa-solid fa-arrows-rotate"></i> สลับระดับ </button> <button class="btn btn-warning-outline btn-xs btn-ban-user" data-username="${s.username}"> <i class="fa-solid fa-ban"></i> แบน </button> <button class="btn btn-danger-outline btn-xs btn-delete-user-permanent" data-username="${s.username}"> <i class="fa-solid fa-trash"></i> ลบถาวร </button> </div> </td> `; tr.querySelector(".btn-toggle-seller-type").addEventListener("click", () => { const allUsers = db.getUsers(); const target = allUsers.find(u => u.username === s.username); if (target) { if (target.sellerType === "corporate") { target.sellerType = "regular"; db.saveUsers(allUsers); showToast(`สลับระดับผู้ขาย "${s.name}" เป็น Regular Seller เรียบร้อยแล้ว`, "success"); } else { target.sellerType = "corporate"; if (!target.companyName || target.companyName.trim() === "") { const inputComp = prompt(`ระบุชื่อนิติบุคคล / บริษัท สำหรับผู้ขายรายใหญ่ (Official Corporate Seller) "${s.name}":`, s.name + " Co., Ltd."); if (inputComp && inputComp.trim() !== "") { target.companyName = inputComp.trim(); } } db.saveUsers(allUsers); showToast(`อัปเดตผู้ขาย "${s.name}" เป็น Official Corporate Seller สำเร็จ!`, "success"); } renderAdminUsersPage(); initFilters(); renderCatalog(); } }); tr.querySelector(".btn-ban-user").addEventListener("click", () => { if (confirm(`ยืนยันการแบนบัญชีผู้ขาย "${s.name}" (@${s.username})? ร้านค้าและบัญชีนี้จะถูกระงับการใช้งานชั่วคราว`)) { banUserAccount(s.username); } }); tr.querySelector(".btn-delete-user-permanent").addEventListener("click", () => { if (confirm(`คุณแน่ใจหรือไม่ที่จะลบบัญชีผู้ขาย "${s.name}" (@${s.username}) ออกจากระบบอย่างถาวร?\n\nข้อมูลผู้ขายจะถูกลบหายไปอย่างสิ้นเชิง และจะไม่ถูกบันทึกไว้ในประวัติการแบน`)) { deleteUserAccountPermanently(s.username); } }); tbodySeller.appendChild(tr); }); } } renderAdminBannedUsers(); } function initAddSellerFlow() { const modal = document.getElementById("addSellerModal"); const openBtn = document.getElementById("openAddSellerModalBtn"); const closeBtn = document.getElementById("closeAddSellerModalBtn"); const form = document.getElementById("addSellerForm"); if (openBtn && modal) { openBtn.addEventListener("click", () => { modal.style.display = "flex"; }); } if (closeBtn && modal) { closeBtn.addEventListener("click", () => { modal.style.display = "none"; }); } if (modal) { modal.addEventListener("click", (e) => { if (e.target === modal) { modal.style.display = "none"; } }); } if (form) { form.addEventListener("submit", (e) => { e.preventDefault(); const username = document.getElementById("addSellerUsername").value.trim().toLowerCase(); const password = document.getElementById("addSellerPassword").value; const storeName = document.getElementById("addSellerStoreName").value.trim(); const companyName = document.getElementById("addSellerCompanyName").value.trim(); const sellerType = document.getElementById("addSellerType").value; if (username === "admin") { showToast("ไม่สามารถใช้ชื่อผู้ใช้ admin เป็นบัญชีผู้ขายได้", "danger"); return; } const users = db.getUsers(); if (users.some(u => u.username === username)) { showToast("ชื่อผู้ใช้นี้ถูกใช้งานแล้วในระบบ", "danger"); return; } const now = new Date(); const yearTH = now.getFullYear() + 543; const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const time = now.toTimeString().split(' ')[0].substring(0, 5); const createdAtStr = `${yearTH}-${month}-${day} ${time} น.`; const newSellerObj = { username: username, name: storeName, password: hashPassword(password), role: "seller", sellerType: sellerType, companyName: companyName, isBanned: false, bannedAt: null, createdAt: createdAtStr, addresses: [], age: 30, gender: "unspecified", avatar: "", coverBanner: "", slogan: "ร้านค้าทางการจำหน่ายสินค้าคุณภาพสูง", storePhone: "", storeEmail: "", storeAddress: "", cutoffTime: "14:00 น.", primaryCarrier: "Flash Express", warrantyPolicy: "รับประกันสินค้าคุณภาพสูง", bankName: "ธนาคารกสิกรไทย (KBANK)", bankAccountNo: "", bankAccountName: storeName, promptpayNo: "", isVacationMode: false }; users.push(newSellerObj); db.saveUsers(users); showToast(`อนุมัติและสร้างบัญชีผู้ขาย "${storeName}" (${sellerType === 'corporate' ? 'Official Corporate' : 'Regular Seller'}) สำเร็จ!`, "success"); form.reset(); if (modal) modal.style.display = "none"; renderAdminUsersPage(); initFilters(); renderCatalog(); }); } } // ========================================================================== // CLIENT PROFILE & ADDRESS MANAGEMENT LOGIC // ========================================================================== function renderClientProfile() { const user = state.currentUser; if (!user) return; const allUsers = db.getUsers(); const updatedUser = allUsers.find(u => u.username === user.username); if (updatedUser) { state.currentUser = updatedUser; } const curr = state.currentUser; const nameInput = document.getElementById("clientProfileName"); const ageInput = document.getElementById("clientProfileAge"); const genderSelect = document.getElementById("clientProfileGender"); const avatarPreview = document.getElementById("clientAvatarPreview"); if (nameInput) nameInput.value = curr.name || ""; if (ageInput) ageInput.value = curr.age || 25; if (genderSelect) genderSelect.value = curr.gender || "unspecified"; if (avatarPreview) { if (curr.avatar) { avatarPreview.src = curr.avatar; } else { avatarPreview.src = `https://api.dicebear.com/7.x/bottts/svg?seed=${curr.username}`; } } renderClientAddressList(); } function renderClientAddressList() { const container = document.getElementById("clientAddressList"); if (!container) return; container.innerHTML = ""; const user = state.currentUser; if (!user || !user.addresses || user.addresses.length === 0) { container.innerHTML = `<div class="text-center w-100 py-4" style="grid-column:1/-1; color:var(--color-sky-slate)"><i class="fa-solid fa-location-crosshairs" style="font-size:24px; margin-bottom:8px;"></i><p>ยังไม่มีข้อมูลที่อยู่จัดส่ง กรุณาเพิ่มที่อยู่ใหม่</p></div>`; return; } user.addresses.forEach(addr => { const card = document.createElement("div"); card.className = `address-card ${addr.isDefault ? 'default-address' : ''}`; const defaultBadge = addr.isDefault ? `<span class="badge badge-sky" style="font-size:11px; padding:2px 8px;"><i class="fa-solid fa-star"></i> ที่อยู่หลัก</span>` : ''; card.innerHTML = ` <div> <div class="address-card-header"> <span class="address-card-title">${addr.name}</span> ${defaultBadge} </div> <div class="address-card-phone"><i class="fa-solid fa-phone"></i> ${addr.phone}</div> <div class="address-card-detail">${addr.address} จ.${addr.province} ${addr.zipcode}</div> </div> <div class="address-card-actions"> ${!addr.isDefault ? `<button class="btn btn-sky-outline btn-xs btn-set-default-addr" data-id="${addr.id}"><i class="fa-solid fa-check"></i> ตั้งเป็นที่อยู่หลัก</button>` : ''} <button class="btn btn-sky-outline btn-xs btn-edit-addr" data-id="${addr.id}"><i class="fa-solid fa-pen"></i> แก้ไข</button> <button class="btn btn-danger-outline btn-xs btn-delete-addr" data-id="${addr.id}"><i class="fa-solid fa-trash"></i> ลบ</button> </div> `; const setDefaultBtn = card.querySelector(".btn-set-default-addr"); if (setDefaultBtn) { setDefaultBtn.addEventListener("click", () => { const users = db.getUsers(); const u = users.find(x => x.username === user.username); if (u && u.addresses) { u.addresses.forEach(a => { a.isDefault = (a.id === addr.id); }); db.saveUsers(users); db.setCurrentUser(u); showToast("ตั้งเป็นที่อยู่จัดส่งหลักเรียบร้อยแล้ว", "success"); renderClientAddressList(); } }); } const editBtn = card.querySelector(".btn-edit-addr"); if (editBtn) { editBtn.addEventListener("click", () => { openAddressModal(addr); }); } const deleteBtn = card.querySelector(".btn-delete-addr"); if (deleteBtn) { deleteBtn.addEventListener("click", () => { if (confirm(`ยืนยันการลบที่อยู่นี้?`)) { const users = db.getUsers(); const u = users.find(x => x.username === user.username); if (u && u.addresses) { u.addresses = u.addresses.filter(a => a.id !== addr.id); if (addr.isDefault && u.addresses.length > 0) { u.addresses[0].isDefault = true; } db.saveUsers(users); db.setCurrentUser(u); showToast("ลบที่อยู่จัดส่งเรียบร้อยแล้ว", "success"); renderClientAddressList(); } } }); } container.appendChild(card); }); } function openAddressModal(addrObj = null) { const modal = document.getElementById("addressModal"); const title = document.getElementById("addressModalTitle"); const idInput = document.getElementById("addressId"); const nameInput = document.getElementById("addressName"); const phoneInput = document.getElementById("addressPhone"); const detailInput = document.getElementById("addressDetail"); const subdistrictInput = document.getElementById("addressSubdistrict"); const districtInput = document.getElementById("addressDistrict"); const provinceInput = document.getElementById("addressProvince"); const zipcodeInput = document.getElementById("addressZipcode"); const gpsInput = document.getElementById("addressGpsLocation"); const isDefaultChk = document.getElementById("addressIsDefault"); if (!modal) return; if (addrObj) { if (title) title.innerHTML = `<i class="fa-solid fa-location-dot"></i> แก้ไขที่อยู่จัดส่ง`; if (idInput) idInput.value = addrObj.id; if (nameInput) nameInput.value = addrObj.name || ""; if (phoneInput) phoneInput.value = addrObj.phone || ""; if (detailInput) detailInput.value = addrObj.detail || addrObj.address || ""; if (subdistrictInput) subdistrictInput.value = addrObj.subdistrict || ""; if (districtInput) districtInput.value = addrObj.district || ""; if (provinceInput) provinceInput.value = addrObj.province || ""; if (zipcodeInput) zipcodeInput.value = addrObj.zipcode || ""; if (gpsInput) gpsInput.value = addrObj.gpsLocation || ""; if (isDefaultChk) isDefaultChk.checked = !!addrObj.isDefault; } else { if (title) title.innerHTML = `<i class="fa-solid fa-location-dot"></i> เพิ่มที่อยู่จัดส่งใหม่`; if (idInput) idInput.value = ""; if (nameInput) nameInput.value = state.currentUser ? state.currentUser.name : ""; if (phoneInput) phoneInput.value = ""; if (detailInput) detailInput.value = ""; if (subdistrictInput) subdistrictInput.value = ""; if (districtInput) districtInput.value = ""; if (provinceInput) provinceInput.value = "เชียงใหม่"; if (zipcodeInput) zipcodeInput.value = ""; if (gpsInput) gpsInput.value = ""; const user = state.currentUser; if (isDefaultChk) isDefaultChk.checked = (!user || !user.addresses || user.addresses.length === 0); } modal.style.display = "flex"; } function initClientProfileFlow() { const form = document.getElementById("clientProfileForm"); const avatarInput = document.getElementById("clientAvatarInput"); const avatarPreview = document.getElementById("clientAvatarPreview"); const openAddBtn = document.getElementById("openAddAddressModalBtn"); const closeAddrBtn = document.getElementById("closeAddressModalBtn"); const cancelAddrBtn = document.getElementById("cancelAddressModalBtn"); const addrModal = document.getElementById("addressModal"); const addrForm = document.getElementById("addressForm"); const checkoutAddAddressBtn = document.getElementById("checkoutAddAddressBtn"); if (avatarInput) { avatarInput.addEventListener("change", (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { const img = new Image(); img.onload = () => { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); const maxW = 200; const maxH = 200; let width = img.width; let height = img.height; if (width > height) { if (width > maxW) { height *= maxW / width; width = maxW; } } else { if (height > maxH) { width *= maxH / height; height = maxH; } } canvas.width = width; canvas.height = height; ctx.drawImage(img, 0, 0, width, height); const compressedBase64 = canvas.toDataURL("image/webp", 0.85); if (avatarPreview) avatarPreview.src = compressedBase64; if (avatarPreview) avatarPreview.dataset.base64 = compressedBase64; }; img.src = event.target.result; }; reader.readAsDataURL(file); }); } if (form) { form.addEventListener("submit", (e) => { e.preventDefault(); const user = state.currentUser; if (!user) return; const name = document.getElementById("clientProfileName").value.trim(); const age = parseInt(document.getElementById("clientProfileAge").value) || 25; const gender = document.getElementById("clientProfileGender").value; const newAvatar = avatarPreview && avatarPreview.dataset.base64 ? avatarPreview.dataset.base64 : (user.avatar || ""); const users = db.getUsers(); const targetUser = users.find(u => u.username === user.username); if (targetUser) { targetUser.name = name; targetUser.age = age; targetUser.gender = gender; targetUser.avatar = newAvatar; db.saveUsers(users); db.setCurrentUser(targetUser); showToast("บันทึกข้อมูลส่วนตัวเรียบร้อยแล้ว", "success"); updateAuthHeader(); } }); } if (openAddBtn) { openAddBtn.addEventListener("click", () => { openAddressModal(null); }); } if (checkoutAddAddressBtn) { checkoutAddAddressBtn.addEventListener("click", () => { openAddressModal(null); }); } if (closeAddrBtn && addrModal) { closeAddrBtn.addEventListener("click", () => { addrModal.style.display = "none"; }); } if (cancelAddrBtn && addrModal) { cancelAddrBtn.addEventListener("click", () => { addrModal.style.display = "none"; }); } if (addrModal) { addrModal.addEventListener("click", (e) => { if (e.target === addrModal) addrModal.style.display = "none"; }); } if (addrForm) { addrForm.addEventListener("submit", (e) => { e.preventDefault(); const user = state.currentUser; if (!user) { showToast("กรุณาเข้าสู่ระบบก่อนดำเนินการ", "danger"); return; } const addrId = document.getElementById("addressId").value; const name = document.getElementById("addressName").value.trim(); const phone = document.getElementById("addressPhone").value.trim(); const detail = document.getElementById("addressDetail").value.trim(); const subdistrict = document.getElementById("addressSubdistrict") ? document.getElementById("addressSubdistrict").value.trim() : ""; const district = document.getElementById("addressDistrict") ? document.getElementById("addressDistrict").value.trim() : ""; const province = document.getElementById("addressProvince").value.trim(); const zipcode = document.getElementById("addressZipcode").value.trim(); const gpsLocation = document.getElementById("addressGpsLocation") ? document.getElementById("addressGpsLocation").value.trim() : ""; const isDefault = document.getElementById("addressIsDefault").checked; const fullAddrStr = typeof formatFullAddress === "function" ? formatFullAddress({ detail, subdistrict, district, province, zipcode, gpsLocation }) : `${detail} ต.${subdistrict} อ.${district} จ.${province} ${zipcode}`; const users = db.getUsers(); const u = users.find(x => x.username === user.username); if (!u) return; if (!u.addresses) u.addresses = []; if (isDefault) { u.addresses.forEach(a => a.isDefault = false); } if (addrId) { const existing = u.addresses.find(a => a.id === addrId); if (existing) { existing.name = name; existing.phone = phone; existing.detail = detail; existing.subdistrict = subdistrict; existing.district = district; existing.province = province; existing.zipcode = zipcode; existing.gpsLocation = gpsLocation; existing.address = fullAddrStr; existing.isDefault = isDefault; } } else { const newId = "addr-" + Date.now(); const newAddr = { id: newId, name: name, phone: phone, detail: detail, subdistrict: subdistrict, district: district, province: province, zipcode: zipcode, gpsLocation: gpsLocation, address: fullAddrStr, isDefault: isDefault || u.addresses.length === 0 }; u.addresses.push(newAddr); } db.saveUsers(users); db.setCurrentUser(u); showToast("บันทึกที่อยู่จัดส่งเรียบร้อยแล้ว", "success"); if (addrModal) addrModal.style.display = "none"; renderClientAddressList(); const checkoutSec = document.getElementById("page-checkout"); if (checkoutSec && checkoutSec.style.display !== "none") { renderCheckout(); } }); } } // ========================================================================== // SELLER PROFILE MANAGEMENT LOGIC // ========================================================================== function renderSellerProfile() { let user = state.currentUser; if (!user || user.role !== "seller") { const users = db.getUsers(); user = users.find(u => u.username === "seller1" || u.role === "seller"); } if (!user) return; const storeNameInput = document.getElementById("sellerProfileStoreName"); const sloganInput = document.getElementById("sellerProfileSlogan"); const phoneInput = document.getElementById("sellerProfilePhone"); const emailInput = document.getElementById("sellerProfileEmail"); const addressDetailInput = document.getElementById("sellerAddressDetail"); const subdistrictInput = document.getElementById("sellerSubdistrict"); const districtInput = document.getElementById("sellerDistrict"); const provinceInput = document.getElementById("sellerProvince"); const zipcodeInput = document.getElementById("sellerZipcode"); const storeMapUrlInput = document.getElementById("sellerStoreMapUrl"); const addressInput = document.getElementById("sellerProfileAddress"); const cutoffInput = document.getElementById("sellerCutoffTime"); const carrierSelect = document.getElementById("sellerPrimaryCarrier"); const warrantyInput = document.getElementById("sellerWarrantyPolicy"); const bankSelect = document.getElementById("sellerBankName"); const bankAccNoInput = document.getElementById("sellerBankAccountNo"); const bankAccNameInput = document.getElementById("sellerBankAccountName"); const promptPayInput = document.getElementById("sellerPromptPay"); const vacationModeChk = document.getElementById("sellerVacationMode"); const logoPreview = document.getElementById("sellerLogoPreview"); const bannerPreview = document.getElementById("sellerBannerPreview"); if (storeNameInput) storeNameInput.value = user.name || ""; if (sloganInput) sloganInput.value = user.slogan || ""; if (phoneInput) phoneInput.value = user.storePhone || ""; if (emailInput) emailInput.value = user.storeEmail || ""; if (addressDetailInput) addressDetailInput.value = user.sellerAddressDetail || user.storeAddress || ""; if (subdistrictInput) subdistrictInput.value = user.sellerSubdistrict || ""; if (districtInput) districtInput.value = user.sellerDistrict || ""; if (provinceInput) provinceInput.value = user.sellerProvince || ""; if (zipcodeInput) zipcodeInput.value = user.sellerZipcode || ""; if (storeMapUrlInput) storeMapUrlInput.value = user.sellerStoreMapUrl || ""; if (addressInput) addressInput.value = user.storeAddress || ""; if (cutoffInput) cutoffInput.value = user.cutoffTime || "12:00 น."; if (carrierSelect) carrierSelect.value = user.primaryCarrier || "J&T Express"; if (warrantyInput) warrantyInput.value = user.warrantyPolicy || ""; if (bankSelect) bankSelect.value = user.bankName || "ธนาคารกสิกรไทย (KBANK)"; if (bankAccNoInput) bankAccNoInput.value = user.bankAccountNo || ""; if (bankAccNameInput) bankAccNameInput.value = user.bankAccountName || ""; if (promptPayInput) promptPayInput.value = user.promptpayNo || ""; if (vacationModeChk) vacationModeChk.checked = !!user.isVacationMode; if (logoPreview) { if (user.avatar) { logoPreview.src = user.avatar; } else { logoPreview.src = `https://api.dicebear.com/7.x/shapes/svg?seed=${user.username}`; } } if (bannerPreview) { if (user.coverBanner) { bannerPreview.src = user.coverBanner; bannerPreview.style.display = "block"; } else { bannerPreview.src = ""; bannerPreview.style.display = "none"; } } } function initSellerProfileFlow() { const form = document.getElementById("sellerProfileForm"); const logoInput = document.getElementById("sellerLogoInput"); const logoPreview = document.getElementById("sellerLogoPreview"); const bannerInput = document.getElementById("sellerBannerInput"); const bannerPreview = document.getElementById("sellerBannerPreview"); // Logo image upload if (logoInput) { logoInput.addEventListener("change", (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { const img = new Image(); img.onload = () => { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); const maxW = 240; const maxH = 240; let width = img.width; let height = img.height; if (width > height) { if (width > maxW) { height *= maxW / width; width = maxW; } } else { if (height > maxH) { width *= maxH / height; height = maxH; } } canvas.width = width; canvas.height = height; ctx.drawImage(img, 0, 0, width, height); const compressedBase64 = canvas.toDataURL("image/webp", 0.85); if (logoPreview) logoPreview.src = compressedBase64; if (logoPreview) logoPreview.dataset.base64 = compressedBase64; }; img.src = event.target.result; }; reader.readAsDataURL(file); }); } // Cover banner upload if (bannerInput) { bannerInput.addEventListener("change", (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { const img = new Image(); img.onload = () => { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); const maxW = 800; const maxH = 260; let width = img.width; let height = img.height; if (width > maxW) { height *= maxW / width; width = maxW; } canvas.width = width; canvas.height = height; ctx.drawImage(img, 0, 0, width, height); const compressedBase64 = canvas.toDataURL("image/webp", 0.8); if (bannerPreview) { bannerPreview.src = compressedBase64; bannerPreview.style.display = "block"; bannerPreview.dataset.base64 = compressedBase64; } }; img.src = event.target.result; }; reader.readAsDataURL(file); }); } if (form) { form.addEventListener("submit", (e) => { e.preventDefault(); let user = state.currentUser; if (!user || user.role !== "seller") { const usersList = db.getUsers(); user = usersList.find(u => u.username === "seller1" || u.role === "seller"); } if (!user) return; const storeName = document.getElementById("sellerProfileStoreName").value.trim(); const slogan = document.getElementById("sellerProfileSlogan").value.trim(); const storePhone = document.getElementById("sellerProfilePhone").value.trim(); const storeEmail = document.getElementById("sellerProfileEmail").value.trim(); const sDetail = document.getElementById("sellerAddressDetail") ? document.getElementById("sellerAddressDetail").value.trim() : ""; const sSubdistrict = document.getElementById("sellerSubdistrict") ? document.getElementById("sellerSubdistrict").value.trim() : ""; const sDistrict = document.getElementById("sellerDistrict") ? document.getElementById("sellerDistrict").value.trim() : ""; const sProvince = document.getElementById("sellerProvince") ? document.getElementById("sellerProvince").value.trim() : ""; const sZipcode = document.getElementById("sellerZipcode") ? document.getElementById("sellerZipcode").value.trim() : ""; const sMapUrl = document.getElementById("sellerStoreMapUrl") ? document.getElementById("sellerStoreMapUrl").value.trim() : ""; const fullWarehouseAddr = typeof formatFullAddress === "function" ? formatFullAddress({ detail: sDetail, subdistrict: sSubdistrict, district: sDistrict, province: sProvince, zipcode: sZipcode, mapUrl: sMapUrl }) : `${sDetail} ต.${sSubdistrict} อ.${sDistrict} จ.${sProvince} ${sZipcode}`; const cutoffTime = document.getElementById("sellerCutoffTime").value.trim(); const primaryCarrier = document.getElementById("sellerPrimaryCarrier").value; const warrantyPolicy = document.getElementById("sellerWarrantyPolicy").value.trim(); const bankName = document.getElementById("sellerBankName").value; const bankAccountNo = document.getElementById("sellerBankAccountNo").value.trim(); const bankAccountName = document.getElementById("sellerBankAccountName").value.trim(); const promptpayNo = document.getElementById("sellerPromptPay").value.trim(); const isVacationMode = document.getElementById("sellerVacationMode").checked; const newLogo = logoPreview && logoPreview.dataset.base64 ? logoPreview.dataset.base64 : (user.avatar || ""); const newBanner = bannerPreview && bannerPreview.dataset.base64 ? bannerPreview.dataset.base64 : (user.coverBanner || ""); const users = db.getUsers(); const targetSeller = users.find(u => u.username === user.username); if (targetSeller) { targetSeller.name = storeName; targetSeller.slogan = slogan; targetSeller.storePhone = storePhone; targetSeller.storeEmail = storeEmail; targetSeller.sellerAddressDetail = sDetail; targetSeller.sellerSubdistrict = sSubdistrict; targetSeller.sellerDistrict = sDistrict; targetSeller.sellerProvince = sProvince; targetSeller.sellerZipcode = sZipcode; targetSeller.sellerStoreMapUrl = sMapUrl; targetSeller.storeAddress = fullWarehouseAddr; targetSeller.cutoffTime = cutoffTime; targetSeller.primaryCarrier = primaryCarrier; targetSeller.warrantyPolicy = warrantyPolicy; targetSeller.bankName = bankName; targetSeller.bankAccountNo = bankAccountNo; targetSeller.bankAccountName = bankAccountName; targetSeller.promptpayNo = promptpayNo; targetSeller.isVacationMode = isVacationMode; targetSeller.avatar = newLogo; targetSeller.coverBanner = newBanner; db.saveUsers(users); if (state.currentUser && state.currentUser.username === user.username) { db.setCurrentUser(targetSeller); } showToast("บันทึกการตั้งค่าร้านค้าเรียบร้อยแล้ว", "success"); renderCatalog(); } }); } } function renderSellerStock() { const tbody = document.getElementById("sellerStockTableBody"); if (!tbody) return; tbody.innerHTML = ""; const currentSellerId = (state.currentUser && state.currentUser.role === "seller") ? state.currentUser.username : "seller1"; state.products = db.getProducts(); const searchQuery = document.getElementById("sellerStockSearch").value.toLowerCase(); const catFilter = document.getElementById("sellerStockFilterCategory").value; let sellerProducts = state.products.filter(p => p.sellerId === currentSellerId); let filtered = sellerProducts.filter(p => { const matchName = p.name.toLowerCase().includes(searchQuery) || (p.sellerName && p.sellerName.toLowerCase().includes(searchQuery)); const matchCat = catFilter === "all" || p.category === catFilter; return matchName && matchCat; }); if (filtered.length === 0) { tbody.innerHTML = '<tr><td colspan="6" class="text-center py-4" style="color:var(--color-sky-slate)">ไม่พบรายการสินค้าของคุณในหมวดหมู่นี้</td></tr>'; return; } filtered.forEach(p => { const placeholderImg = DEFAULT_IMAGE_PLACEHOLDERS[p.id] || DEFAULT_IMAGE_PLACEHOLDERS["default"]; const imgSrc = p.image || placeholderImg; const tr = document.createElement("tr"); let optionStockHtml = ""; p.variants.forEach((v, idx) => { const optionLabel = (v.color !== "Default" || v.size !== "Default") ? `${v.color} /${v.size}` : "คลังปกติ"; const vPrice = v.price !== undefined ? v.price : p.price; optionStockHtml += ` <div class="stock-variant-edit-box" style="margin-bottom: 5px;"> <div class="stock-variant-row" style="display: flex; align-items: center; gap: 5px;"> <span class="badge badge-sky" style="min-width:130px; text-align:center">${optionLabel}</span> <label style="font-size: 11px; margin: 0;">คลัง:</label> <input type="number" class="stock-input-sm variant-stock-input" data-prod-id="${p.id}" data-index="${idx}" value="${v.stock}" min="0" style="width: 60px;"> <label style="font-size: 11px; margin: 0;">ราคา:</label> <input type="number" class="stock-input-sm variant-price-input" data-prod-id="${p.id}" data-index="${idx}" value="${vPrice}" min="0" style="width: 80px;"> </div> </div> `; }); tr.innerHTML = ` <td><img src="${imgSrc}" class="stock-thumbnail" alt=""></td> <td> <strong>${p.name}</strong><br> <span class="badge badge-sky">${p.category}</span> <span class="badge badge-dark" style="background:#0f766e; color:white"><i class="fa-solid fa-store"></i> ${p.sellerName || 'SkyMall'}</span> </td> <td> <span class="product-price">฿${p.price.toLocaleString()}</span> </td> <td> <div style="max-height:150px; overflow-y:auto; padding-right:5px"> ${optionStockHtml} </div> </td> <td> ${p.active ? '<span class="badge badge-success">เปิดขาย</span>' : '<span class="badge badge-danger">ปิดการขาย</span>'} </td> <td> <div class="flex-column gap-1" style="display:flex; gap:5px"> <button class="btn btn-sky btn-xs edit-prod-trigger-btn" data-prod-id="${p.id}"> แก้ไขข้อมูล </button> <button class="btn btn-sky-outline btn-xs save-prod-stock-btn" data-prod-id="${p.id}"> บันทึกคลัง </button> <button class="btn ${p.active ? 'btn-danger-outline' : 'btn-sky'} btn-xs toggle-prod-active-btn" data-prod-id="${p.id}"> ${p.active ? 'ปิดการขาย' : 'เปิดขาย'} </button> <button class="btn btn-danger-outline btn-xs delete-prod-btn" data-prod-id="${p.id}"> ลบสินค้า </button> </div> </td> `; tr.querySelector(".toggle-prod-active-btn").addEventListener("click", () => { p.active = !p.active; db.saveProducts(state.products); renderSellerStock(); }); tr.querySelector(".delete-prod-btn").addEventListener("click", () => { if (confirm(`ยืนยันการลบสินค้า "${p.name}"?`)) { const updated = state.products.filter(item => item.id !== p.id); db.saveProducts(updated); alert("ลบสินค้าออกจากร้านแล้ว"); renderSellerStock(); } }); tr.querySelector(".edit-prod-trigger-btn").addEventListener("click", () => { openEditProductModal(p.id); }); tr.querySelector(".save-prod-stock-btn").addEventListener("click", () => { const stockInputs = tr.querySelectorAll(".variant-stock-input"); const priceInputs = tr.querySelectorAll(".variant-price-input"); stockInputs.forEach(input => { const idx = parseInt(input.getAttribute("data-index")); const newStock = parseInt(input.value); if (p.variants[idx]) { p.variants[idx].stock = newStock >= 0 ? newStock : 0; } }); priceInputs.forEach(input => { const idx = parseInt(input.getAttribute("data-index")); const newPrice = parseInt(input.value); if (p.variants[idx]) { p.variants[idx].price = newPrice >= 0 ? newPrice : p.price; } }); db.saveProducts(state.products); alert("บันทึกคลังและราคาสินค้าสำเร็จ!"); renderSellerStock(); }); tbody.appendChild(tr); }); } function renderSellerOrders() { const tbody = document.getElementById("sellerOrdersTableBody"); if (!tbody) return; tbody.innerHTML = ""; const currentSellerId = (state.currentUser && state.currentUser.role === "seller") ? state.currentUser.username : "seller1"; state.orders = db.getOrders(); const sellerOrders = state.orders.filter(order => order.items.some(item => item.sellerId === currentSellerId)); if (sellerOrders.length === 0) { tbody.innerHTML = `<tr><td colspan="7" class="text-center py-4" style="color:var(--color-sky-slate)">ไม่มีคำสั่งซื้อสำหรับสินค้าของคุณ</td></tr>`; return; } sellerOrders.forEach(order => { const myItems = order.items.filter(item => item.sellerId === currentSellerId); const myItemsHtml = myItems.map(item => { const hasOptionLabels = item.color !== "Default" || item.size !== "Default"; const metaStr = hasOptionLabels ? ` (${item.color}/${item.size})` : ""; return `<li>- ${item.name}${metaStr} <strong>x${item.quantity}</strong></li>`; }).join(""); const mySubtotal = myItems.reduce((sum, item) => sum + (item.price * item.quantity), 0); const tr = document.createElement("tr"); let slipSrc = order.slipImage; let isUnpaidNoSlip = order.status === "Unpaid" && !slipSrc; let slipCellHtml = ""; if (order.paymentMethod === "เก็บเงินปลายทาง (COD)") { slipCellHtml = `<div style="color:var(--color-sky-brand); font-size:12px; font-weight:600;"><i class="fa-solid fa-hand-holding-dollar"></i> เก็บเงินปลายทาง</div>`; } else if (isUnpaidNoSlip) { slipCellHtml = `<span style="color:var(--color-sky-slate); font-size:11px"><i class="fa-solid fa-clock"></i> รอชำระเงิน</span>`; } else { let actualSlip = slipSrc === "dummy_slip" || !slipSrc ? MOCK_SLIP_IMAGE : slipSrc; slipCellHtml = `<img src="${actualSlip}" class="slip-thumbnail slip-expand-btn" alt="Slip">`; } const myTracking = order.trackingNumbers && order.trackingNumbers[currentSellerId] ? order.trackingNumbers[currentSellerId] : ""; let statusSelectHtml = ""; const isSellerCancellable = order.status === "Unpaid" || order.status === "Pending" || order.status === "Paid" || order.status === "Preparing"; if (order.status === "Unpaid") { statusSelectHtml = ` <div style="display:flex; flex-direction:column; gap:5px"> <span style="font-size:12px; color:var(--color-sky-slate)"><i class="fa-solid fa-clock"></i> รอชำระเงิน</span> <button class="btn btn-sky-outline btn-xs seller-approve-slip-btn" data-id="${order.id}"> <i class="fa-solid fa-check"></i> ยืนยันรับเงินแล้ว (ข้ามสลิป) </button> <button class="btn btn-danger-outline btn-xs seller-cancel-order-btn" data-id="${order.id}"> <i class="fa-solid fa-ban"></i> ยกเลิกออเดอร์ </button> </div> `; } else if (order.status === "Pending") { statusSelectHtml = ` <div style="display:flex; flex-direction:column; gap:5px"> <button class="btn btn-sky btn-xs seller-approve-slip-btn" data-id="${order.id}"> อนุมัติรับเงิน </button> <button class="btn btn-danger-outline btn-xs seller-cancel-order-btn" data-id="${order.id}"> <i class="fa-solid fa-ban"></i> ยกเลิก/ปฏิเสธรับเงิน </button> </div> `; } else if (order.status === "Paid" || order.status === "Preparing") { statusSelectHtml = ` <div class="form-group-sub" style="display:flex; flex-direction:column; gap:5px"> <input type="text" id="track-${order.id}" class="form-control tracking-input" value="${myTracking}" placeholder="เลขพัสดุ..." style="padding: 4px 8px; font-size:12px;"> <button class="btn btn-sky btn-xs ship-confirm-btn" data-id="${order.id}"> ยืนยันส่งของ (ไปสถานะจัดส่งแล้ว) </button> <button class="btn btn-danger-outline btn-xs seller-cancel-order-btn" data-id="${order.id}"> <i class="fa-solid fa-ban"></i> ยกเลิกออเดอร์ </button> </div> `; } else if (order.status === "Shipped") { statusSelectHtml = ` <div class="text-success mb-1"> <i class="fa-solid fa-truck"></i> จัดส่งแล้ว<br> <small>(${myTracking})</small> </div> <button class="btn btn-success btn-xs mark-success-btn" data-id="${order.id}"> <i class="fa-solid fa-circle-check"></i> จัดส่งสำเร็จ </button> `; } else if (order.status === "Cancelled" || order.status === "Failed") { statusSelectHtml = ` <span style="font-size:12px; color:var(--color-danger)"> <i class="fa-solid fa-circle-xmark"></i> ยกเลิกแล้ว<br> <small style="color:var(--color-sky-slate);">(${order.cancelReason || 'ไม่ระบุ'})</small> </span> `; } tr.innerHTML = ` <td> <strong>${order.id}</strong><br> <span class="text-muted" style="font-size:11px">${order.date}</span> </td> <td> <strong>${order.customerName}</strong><br> <span style="font-size:11px">${order.shippingInfo.phone}</span><br> <div style="max-width:180px; font-size:11px; white-space:normal">${order.shippingInfo.address}</div> </td> <td> <ul class="order-items-mini-list">${myItemsHtml}</ul> </td> <td> ฿${mySubtotal.toLocaleString()} </td> <td> ${slipCellHtml} </td> <td> ${order.status === "Unpaid" ? '<span class="badge badge-dark">รอชำระเงิน</span>' : ''} ${order.status === "Pending" ? '<span class="badge badge-warning">รอตรวจสอบชำระเงิน</span>' : ''} ${order.status === "Paid" ? '<span class="badge badge-success">ชำระเงินแล้ว</span>' : ''} ${order.status === "Preparing" ? '<span class="badge badge-sky">เตรียมส่ง</span>' : ''} ${order.status === "Shipped" ? '<span class="badge badge-success">จัดส่งแล้ว</span>' : ''} ${order.status === "Success" ? '<span class="badge badge-success" style="background:#10b981; color:#fff !important;">จัดส่งสำเร็จ</span>' : ''} ${(order.status === "Cancelled" || order.status === "Failed") ? '<span class="badge badge-cancelled">ยกเลิกแล้ว</span>' : ''} </td> <td> <div style="display:flex; flex-direction:column; gap:5px"> ${statusSelectHtml} </div> </td> `; const slipImgEl = tr.querySelector(".slip-expand-btn"); if (slipImgEl) { slipImgEl.addEventListener("click", () => { let actualSlip = slipSrc === "dummy_slip" || !slipSrc ? MOCK_SLIP_IMAGE : slipSrc; openSlipOverlay(actualSlip); }); } const sellerCancelBtn = tr.querySelector(".seller-cancel-order-btn"); if (sellerCancelBtn) { sellerCancelBtn.addEventListener("click", () => { openSellerAdminCancelModal(order); }); } const sellerApproveBtn = tr.querySelector(".seller-approve-slip-btn"); if (sellerApproveBtn) { sellerApproveBtn.addEventListener("click", () => { const ordersDb = db.getOrders(); const targetOrder = ordersDb.find(o => o.id === order.id); if (targetOrder) { targetOrder.status = "Paid"; db.saveOrders(ordersDb); alert(`อนุมัติรับเงินสำหรับออเดอร์ ${order.id} สำเร็จ!`); } renderSellerOrders(); renderSellerDashboard(); renderOrderHistory(); }); } const sellerRejectBtn = tr.querySelector(".seller-reject-slip-btn"); if (sellerRejectBtn) { sellerRejectBtn.addEventListener("click", () => { const ordersDb = db.getOrders(); const targetOrder = ordersDb.find(o => o.id === order.id); if (targetOrder) { targetOrder.status = "Failed"; db.saveOrders(ordersDb); alert(`ปฏิเสธรับเงินสำหรับออเดอร์ ${order.id} เรียบร้อย!`); } renderSellerOrders(); renderSellerDashboard(); renderOrderHistory(); }); } const shipBtn = tr.querySelector(".ship-confirm-btn"); if (shipBtn) { shipBtn.addEventListener("click", () => { const trackingInput = tr.querySelector(".tracking-input"); const trNo = trackingInput.value.trim(); const ordersDb = db.getOrders(); const targetOrder = ordersDb.find(o => o.id === order.id); if (targetOrder) { targetOrder.trackingNumbers = targetOrder.trackingNumbers || {}; if (trNo) { targetOrder.trackingNumbers[currentSellerId] = trNo; } // Force status to shipped immediately for prototype flow targetOrder.status = "Shipped"; db.saveOrders(ordersDb); alert("อัปเดตสถานะเป็น 'อยู่ระหว่างจัดส่ง' เรียบร้อย!"); } renderSellerOrders(); renderOrderHistory(); }); } const successBtn = tr.querySelector(".mark-success-btn"); if (successBtn) { successBtn.addEventListener("click", () => { const ordersDb = db.getOrders(); const targetOrder = ordersDb.find(o => o.id === order.id); if (targetOrder) { targetOrder.status = "Success"; db.saveOrders(ordersDb); alert(`อัปเดตสถานะออเดอร์ ${order.id} เป็นจัดส่งสำเร็จเรียบร้อยแล้ว!`); } renderSellerOrders(); renderSellerDashboard(); renderOrderHistory(); }); } tbody.appendChild(tr); }); } // ========================================================================== // 14. THEME SWITCHER (DARK / LIGHT MODE) // ========================================================================== function initTheme() { const themeToggleBtn = document.getElementById("themeToggleBtn"); if (!themeToggleBtn) return; // Check localStorage const savedTheme = localStorage.getItem("sk_theme"); // Apply initial theme if (savedTheme === "dark") { document.body.classList.add("dark-theme"); updateThemeIcon(themeToggleBtn, "dark"); } else { document.body.classList.remove("dark-theme"); updateThemeIcon(themeToggleBtn, "light"); } // Toggle theme on click themeToggleBtn.addEventListener("click", () => { const isDark = document.body.classList.toggle("dark-theme"); const newTheme = isDark ? "dark" : "light"; localStorage.setItem("sk_theme", newTheme); updateThemeIcon(themeToggleBtn, newTheme); // Update Chart if currently in admin mode and showing the dashboard if (state.currentRole === "admin") { const adminDashboardSec = document.getElementById("admin-page-dashboard"); if (adminDashboardSec && adminDashboardSec.style.display !== "none") { renderAdminDashboard(); } } }); } function updateThemeIcon(btn, theme) { const icon = btn.querySelector("i"); if (!icon) return; if (theme === "dark") { icon.className = "fa-solid fa-moon"; } else { icon.className = "fa-solid fa-sun"; } } // ========================================================================== // ========================================================================== // 13. APPLICATION BOOTSTRAP INITIALIZER // ========================================================================== function bootApp() { const savedUser = db.getCurrentUser(); if (savedUser) { state.currentUser = savedUser; setRole(savedUser.role); } else { state.currentUser = null; setRole("client"); } // CLEANUP LOCALSTORAGE QUOTA BLOAT try { const existingOrders = db.getOrders(); let cleaned = false; existingOrders.forEach(o => { // Delete slip images if the order is already Success or Failed, or if the string is huge if ((o.status === "Success" || o.status === "Failed") && o.paymentSlip) { o.paymentSlip = ""; cleaned = true; } if (o.slipImage && o.slipImage.length > 50000) { o.slipImage = ""; cleaned = true; } }); if (cleaned) { localStorage.setItem("sk_orders", JSON.stringify(existingOrders)); state.orders = existingOrders; } // Wipe bloated uncompressed product images that were uploaded before the compression fix const existingProducts = db.getProducts(); let cleanedProds = false; existingProducts.forEach(p => { if (p.image && p.image.length > 500000) { // If image is larger than ~500kb base64, delete it p.image = ""; cleanedProds = true; } }); if (cleanedProds) { localStorage.setItem("sk_products", JSON.stringify(existingProducts)); state.products = existingProducts; } } catch (e) { } initTheme(); updateAuthHeader(); if (typeof updateWishlistBadge === "function") { updateWishlistBadge(); } // Setup Simulated Role Switcher (bottom control widget) const toggleRBtn = document.getElementById("toggleRBtn"); if (toggleRBtn) { toggleRBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); if (state.currentRole === "client") { setRole("seller"); } else if (state.currentRole === "seller") { setRole("admin"); } else { setRole("client"); } updateAuthHeader(); }); } const safeInit = (name, fn) => { try { fn(); } catch (e) { console.error(`Error initializing ${name}:`, e); } }; // Initialize these visually important components first safeInit("renderCouponShowcase", renderCouponShowcase); safeInit("initHeroSlider", initHeroSlider); safeInit("initNavigation", initNavigation); safeInit("initAuth", initAuth); safeInit("initFilters", initFilters); safeInit("renderCatalog", renderCatalog); safeInit("initCouponHandler", initCouponHandler); safeInit("initCheckoutFlow", initCheckoutFlow); safeInit("initOrderHistoryTabs", initOrderHistoryTabs); safeInit("initCancelModals", initCancelModals); // Setup Order Tracking Modal Close Handlers const closeTrackingBtn = document.getElementById("closeOrderTrackingModalBtn"); if (closeTrackingBtn) { closeTrackingBtn.addEventListener("click", () => { const modal = document.getElementById("orderTrackingModal"); if (modal) modal.style.display = "none"; }); } const orderTrackingModal = document.getElementById("orderTrackingModal"); if (orderTrackingModal) { orderTrackingModal.addEventListener("click", (e) => { if (e.target === orderTrackingModal) { orderTrackingModal.style.display = "none"; } }); } // Admin & Profile setups safeInit("initAddProductFlow", initAddProductFlow); safeInit("initEditProductFlow", initEditProductFlow); safeInit("initCouponsFlow", initCouponsFlow); safeInit("initAddSellerFlow", initAddSellerFlow); safeInit("initClientProfileFlow", initClientProfileFlow); safeInit("initSellerProfileFlow", initSellerProfileFlow); safeInit("initSwitchAccountModalHandlers", initSwitchAccountModalHandlers); safeInit("initQuickSwitchAccountModalHandlers", initQuickSwitchAccountModalHandlers); safeInit("initFooterModals", initFooterModals); safeInit("initPasswordPromptFlow", initPasswordPromptFlow); // Setup Review Modal Handlers const closeReviewBtn = document.getElementById("closeProductReviewModalBtn"); if (closeReviewBtn) { closeReviewBtn.addEventListener("click", () => { const modal = document.getElementById("productReviewModal"); if (modal) modal.style.display = "none"; }); } const cancelReviewBtn = document.getElementById("cancelReviewBtn"); if (cancelReviewBtn) { cancelReviewBtn.addEventListener("click", () => { const modal = document.getElementById("productReviewModal"); if (modal) modal.style.display = "none"; }); } const productReviewModal = document.getElementById("productReviewModal"); if (productReviewModal) { productReviewModal.addEventListener("click", (e) => { if (e.target === productReviewModal) { productReviewModal.style.display = "none"; } }); } const reviewForm = document.getElementById("productReviewForm"); if (reviewForm) { reviewForm.addEventListener("submit", (e) => { e.preventDefault(); const productId = document.getElementById("reviewProductId").value; const comment = document.getElementById("reviewComment").value.trim(); const ratingVal = document.querySelector("input[name='reviewRating']:checked"); if (!ratingVal || !comment) { showToast("กรุณากรอกคะแนนและข้อความรีวิวให้ครบถ้วน", "danger"); return; } const rating = parseInt(ratingVal.value); const user = db.getCurrentUser(); const username = user ? `@${user.username}` : "@guest"; const date = new Date().toISOString().split('T')[0]; if (!MOCK_REVIEWS[productId]) { MOCK_REVIEWS[productId] = []; } MOCK_REVIEWS[productId].unshift({ username: username, rating: rating, date: date, text: comment }); const modal = document.getElementById("productReviewModal"); if (modal) modal.style.display = "none"; showToast("ส่งรีวิวเรียบร้อยแล้ว ขอบคุณสำหรับคะแนนความพึงพอใจค่ะ!", "success"); // Refresh product details if it is currently open const detailModal = document.getElementById("productDetailModal"); if (detailModal && detailModal.style.display === "flex") { openProductDetail(productId); } reviewForm.reset(); }); } // Hero Slider is initialized earlier in bootApp updateAuthHeader(); } // ========================================================================== // HERO SLIDER AND INTERACTIVE MEMBER WIDGET LOGIC // ========================================================================== let currentHeroSlide = 0; let heroSliderInterval = null; function updateHeroWidgets() { const slideWidgets = [ document.getElementById("heroWidgetSlide0"), document.getElementById("heroWidgetSlide1"), document.getElementById("heroWidgetSlide2"), document.getElementById("heroWidgetSlide3"), document.getElementById("heroWidgetSlide4") ]; const user = state.currentUser; slideWidgets.forEach((widget, index) => { if (!widget) return; if (!user) { let widgetContent = ""; if (index === 0) { widgetContent = ` <div class="widget-guest-card"> <div class="widget-header" style="display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 14px; color: var(--color-sky-dark); margin-bottom: 6px;"> <i class="fa-solid fa-circle-user widget-user-icon" style="color: var(--color-sky-brand); font-size: 18px;"></i> <span>ยินดีต้อนรับสู่ SkyMall</span> </div> <p style="font-size: 12px; line-height: 1.4; color: var(--color-sky-slate); margin-bottom: 12px;">เข้าสู่ระบบสมาชิกเพื่อรับคูปองส่วนลดและสะสมแต้มพรีเมียม!</p> <div class="widget-actions"> <button class="btn btn-sky w-100 btn-sm" onclick="navigateTo('auth')" style="font-size:11px; padding: 6px 12px;"><i class="fa-solid fa-right-to-bracket"></i> สมัครสมาชิกรับแต้ม</button> </div> </div> `; } else if (index === 1) { widgetContent = ` <div class="widget-guest-card"> <div class="widget-header" style="display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 14px; color: #be123c; margin-bottom: 6px;"> <i class="fa-solid fa-truck-fast widget-user-icon" style="color: #e11d48; font-size: 18px;"></i> <span>สิทธิพิเศษจัดส่งฟรี</span> </div> <p style="font-size: 12px; line-height: 1.4; color: var(--color-sky-slate); margin-bottom: 12px;">แจกโค้ดส่งฟรีตลอดเดือน! ช้อปขั้นต่ำ 0 บาท ส่งฟรีทั่วประเทศ</p> <div class="widget-actions"> <button class="btn btn-rose w-100 btn-sm" onclick="navigateTo('auth')" style="font-size:11px; padding: 6px 12px; background: #e11d48; color: #fff; border: none;"><i class="fa-solid fa-gift"></i> เก็บโค้ดส่งฟรีเลย</button> </div> </div> `; } else if (index === 2) { widgetContent = ` <div class="widget-guest-card"> <div class="widget-header" style="display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 14px; color: #004d40; margin-bottom: 6px;"> <i class="fa-solid fa-tags widget-user-icon" style="color: #00bfa5; font-size: 18px;"></i> <span>สินค้าราคาพิเศษสุดคุ้ม</span> </div> <p style="font-size: 12px; line-height: 1.4; color: var(--color-sky-slate); margin-bottom: 12px;">สินค้าสัตว์เลี้ยงจัดโปรลดสูงสุด 50% ทุกวัน อย่าพลาดสินค้าสุดคิ้วท์</p> <div class="widget-actions"> <button class="btn w-100 btn-sm" onclick="document.getElementById('filterCategory').value='สินค้าสัตว์เลี้ยงและสัตว์น้ำ'; state.activeFilters.category='สินค้าสัตว์เลี้ยงและสัตว์น้ำ'; renderCatalog();" style="font-size:11px; padding: 6px 12px; background: #00bfa5; color: #fff; border: none;"><i class="fa-solid fa-eye"></i> ดูโปรโมชั่น</button> </div> </div> `; } else if (index === 3) { widgetContent = ` <div class="widget-guest-card"> <div class="widget-header" style="display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 14px; color: #7c2d12; margin-bottom: 6px;"> <i class="fa-solid fa-box-open widget-user-icon" style="color: #ea580c; font-size: 18px;"></i> <span>มือสองสภาพนางฟ้า</span> </div> <p style="font-size: 12px; line-height: 1.4; color: var(--color-sky-slate); margin-bottom: 12px;">ค้นหาสินค้าหายาก ของสะสมวินเทจ ราคาเป็นกันเองจากผู้ขายโดยตรง</p> <div class="widget-actions"> <button class="btn w-100 btn-sm" onclick="document.getElementById('filterCategory').value='สินค้ามือสอง/ของสะสม'; state.activeFilters.category='สินค้ามือสอง/ของสะสม'; renderCatalog();" style="font-size:11px; padding: 6px 12px; background: #ea580c; color: #fff; border: none;"><i class="fa-solid fa-magnifying-glass"></i> ค้นหาไอเทมแรร์</button> </div> </div> `; } else if (index === 4) { widgetContent = ` <div class="widget-guest-card"> <div class="widget-header" style="display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 14px; color: #581c87; margin-bottom: 6px;"> <i class="fa-solid fa-bolt widget-user-icon" style="color: #9333ea; font-size: 18px;"></i> <span>Flash Sale!</span> </div> <p style="font-size: 12px; line-height: 1.4; color: var(--color-sky-slate); margin-bottom: 12px;">แจกโค้ดลดเพิ่มสูงสุด 15% เก็บด่วนก่อนโค้ดจะหมดโควต้า</p> <div class="widget-actions"> <button class="btn w-100 btn-sm" onclick="navigateTo('auth')" style="font-size:11px; padding: 6px 12px; background: #9333ea; color: #fff; border: none;"><i class="fa-solid fa-ticket"></i> เก็บโค้ด Flash Sale</button> </div> </div> `; } widget.innerHTML = widgetContent; } else { const roleLabel = user.role === "admin" ? "ผู้ดูแลระบบ (Admin)" : (user.role === "seller" ? "ร้านค้าพรีเมียม (Seller)" : "สมาชิกคนสำคัญ (Member)"); const userAvatar = user.role === "admin" ? "fa-shield-halved" : (user.role === "seller" ? "fa-store" : "fa-crown"); const avatarColor = user.role === "admin" ? "#22c55e" : (user.role === "seller" ? "#0ea5e9" : "#eab308"); if (index === 3) { // Slide 4: Pre-Loved if (user.role === 'seller' || user.role === 'admin') { const allOrders = typeof db !== 'undefined' ? db.getOrders() : []; const pendingCount = allOrders.filter(o => (o.status === "Pending" || o.status === "Preparing") && o.items.some(item => item.sellerId === user.username) ).length; widget.innerHTML = ` <div class="widget-member-card"> <div class="widget-header" style="display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 14px; color: var(--color-sky-dark); margin-bottom: 4px;"> <i class="fa-solid fa-store widget-member-icon" style="color: #f97316; font-size: 18px;"></i> <span>ร้านค้า: <strong>${user.name}</strong></span> </div> <div style="font-size:11px; color:var(--color-sky-slate); margin-top:2px;">คุณมีออเดอร์ค้างจัดส่ง: <strong style="color: #ef4444; font-size:13px;">${pendingCount} ออเดอร์</strong></div> <div class="widget-actions mt-3" style="margin: 15px 0 10px 0;"> <button class="btn btn-sky w-100 btn-sm" onclick="navigateTo('auth')" style="font-size:11px; padding: 6px 12px; background:#ea580c; border:none;"><i class="fa-solid fa-boxes-packing"></i> จัดการออเดอร์ค้างส่ง</button> </div> <div style="font-size:10px; text-align:center; color:var(--color-sky-slate);">มีคำสั่งซื้อใหม่ที่รอยืนยันจัดส่ง</div> </div> `; } else { widget.innerHTML = ` <div class="widget-member-card"> <div class="widget-header" style="display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 14px; color: var(--color-sky-dark); margin-bottom: 4px;"> <i class="fa-solid fa-tags widget-member-icon" style="color: #f97316; font-size: 18px;"></i> <span>สวัสดีคุณ, <strong>${user.name}</strong></span> </div> <div style="font-size:11px; color:var(--color-sky-slate); margin-top:2px;">มีสินค้าเก่าอยากส่งต่อไหม?</div> <div class="widget-actions mt-3" style="margin: 15px 0 10px 0;"> <button class="btn btn-sky w-100 btn-sm" onclick="navigateTo('auth'); showToast('สลับบทบาทของคุณเป็นร้านค้าหรือแอดมินเพื่อเริ่มจัดการสต็อกสินค้าของคุณ!', 'info')" style="font-size:11px; padding: 6px 12px; background:#ea580c; border:none;"><i class="fa-solid fa-store"></i> เปิดร้านค้ามือสองฟรี!</button> </div> <div style="font-size:10px; text-align:center; color:var(--color-sky-slate);">มีคนตามหาไอเทมของคุณอยู่อีกมาก</div> </div> `; } } else if (index === 4) { // Slide 5: Flash Coupon const collected = typeof getCollectedCoupons === 'function' ? getCollectedCoupons() : []; const hasCollected = collected.includes("WELCOME100") || collected.includes("SKYSALE10"); const buttonText = hasCollected ? "คุณเก็บคูปองเรียบร้อยแล้ว" : "คัดลอกคูปองทันที"; const buttonClass = hasCollected ? "btn btn-sky-outline w-100 btn-sm" : "btn btn-sky w-100 btn-sm"; const buttonAction = hasCollected ? "style='background:rgba(255,255,255,0.06); color:var(--color-sky-slate); border-color:var(--color-border); cursor:default;'" : "style='background:#9333ea; border:none; color:#fff;' onclick=\"navigateTo('home'); document.querySelector('.btn-copy-coupon').click();\""; widget.innerHTML = ` <div class="widget-member-card"> <div class="widget-header" style="display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 14px; color: var(--color-sky-dark); margin-bottom: 4px;"> <i class="fa-solid fa-ticket widget-member-icon" style="color: #a855f7; font-size: 18px;"></i> <span>โค้ดเด็ดห้ามพลาด!</span> </div> <div style="font-size:11px; color:var(--color-sky-slate); margin-top:2px;">รายการคูปองใช้งานด่วน:</div> <div style="display:flex; flex-direction:column; gap:6px; margin: 10px 0;"> <div style="display:flex; justify-content:space-between; align-items:center; background:rgba(255,255,255,0.08); padding:5px 8px; border-radius:4px; border: 1px dashed rgba(168,85,247,0.4)"> <span style="font-family:monospace; font-weight:700; color:#c084fc;">WELCOME100</span> <span style="font-size:10px; color:var(--color-sky-slate);">${collected.includes("WELCOME100") ? "เก็บแล้ว" : "ลด ฿100"}</span> </div> <div style="display:flex; justify-content:space-between; align-items:center; background:rgba(255,255,255,0.08); padding:5px 8px; border-radius:4px; border: 1px dashed rgba(168,85,247,0.4)"> <span style="font-family:monospace; font-weight:700; color:#c084fc;">SKYSALE10</span> <span style="font-size:10px; color:var(--color-sky-slate);">${collected.includes("SKYSALE10") ? "เก็บแล้ว" : "ลด 10%"}</span> </div> </div> <div class="widget-actions"> <button class="${buttonClass}" ${buttonAction}><i class="fa-solid ${hasCollected ? 'fa-check' : 'fa-copy'}"></i> ${buttonText}</button> </div> </div> `; } else { // Other Slides widget.innerHTML = ` <div class="widget-member-card"> <div class="widget-header" style="display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 14px; color: var(--color-sky-dark); margin-bottom: 4px;"> <i class="fa-solid ${userAvatar} widget-member-icon" style="color: ${avatarColor}; font-size: 18px;"></i> <span>สวัสดีคุณ, <strong>${user.name}</strong></span> </div> <div style="font-size:11px; color:var(--color-sky-slate); margin-top:2px;">สถานะ: ${roleLabel}</div> <div class="widget-points-box mt-2" style="background: rgba(255,255,255,0.06); padding: 8px; border-radius: 6px; border: 1px solid var(--color-border); margin: 10px 0;"> <div class="points-label" style="font-size: 10px; color: var(--color-sky-slate); text-transform: uppercase; letter-spacing: 0.05em;">ยอดคะแนนสะสมของคุณ</div> <div class="points-val" style="font-size: 16px; font-weight: 700; color: var(--color-sky-brand); margin-top: 2px;"><i class="fa-solid fa-gem" style="color: #0ea5e9;"></i> ${((user && user.points) !== undefined ? user.points : 1450).toLocaleString()} SkyPoints</div> </div> <div class="widget-actions mt-2"> ${state.dailyPointsClaimed ? ` <button class="btn btn-secondary w-100 btn-sm" disabled style="font-size:11px; padding: 6px 12px; opacity:0.7; cursor:not-allowed;"><i class="fa-solid fa-check"></i> รับพอยต์แล้ววันนี้</button> ` : ` <button class="btn btn-sky w-100 btn-sm" onclick="claimDailyPoints()" style="font-size:11px; padding: 6px 12px;"><i class="fa-solid fa-gift"></i> กดรับพอยต์รายวัน</button> `} </div> </div> `; } } }); } function showHeroSlide(index) { const slides = document.querySelectorAll(".hero-slide"); const dots = document.querySelectorAll("#heroSliderDots .dot"); if (slides.length === 0) return; if (index >= slides.length) index = 0; if (index < 0) index = slides.length - 1; currentHeroSlide = index; slides.forEach((slide, idx) => { if (idx === currentHeroSlide) { slide.classList.add("active"); } else { slide.classList.remove("active"); } }); dots.forEach((dot, idx) => { if (idx === currentHeroSlide) { dot.classList.add("active"); } else { dot.classList.remove("active"); } }); } function initHeroSlider() { updateHeroWidgets(); const prevBtn = document.getElementById("prevHeroSlideBtn"); const nextBtn = document.getElementById("nextHeroSlideBtn"); const dots = document.querySelectorAll("#heroSliderDots .dot"); if (prevBtn) { prevBtn.addEventListener("click", () => { showHeroSlide(currentHeroSlide - 1); resetHeroSliderAutoplay(); }); } if (nextBtn) { nextBtn.addEventListener("click", () => { showHeroSlide(currentHeroSlide + 1); resetHeroSliderAutoplay(); }); } dots.forEach(dot => { dot.addEventListener("click", (e) => { const idx = parseInt(e.target.getAttribute("data-slide-to")); showHeroSlide(idx); resetHeroSliderAutoplay(); }); }); startHeroSliderAutoplay(); } function startHeroSliderAutoplay() { stopHeroSliderAutoplay(); heroSliderInterval = setInterval(() => { showHeroSlide(currentHeroSlide + 1); }, 5000); } function stopHeroSliderAutoplay() { if (heroSliderInterval) { clearInterval(heroSliderInterval); heroSliderInterval = null; } } function resetHeroSliderAutoplay() { startHeroSliderAutoplay(); } function initPasswordToggle() { document.querySelectorAll(".toggle-password-btn").forEach(btn => { if (btn.dataset.bound === "true") return; btn.dataset.bound = "true"; btn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); const targetId = btn.getAttribute("data-target"); const input = targetId ? document.getElementById(targetId) : btn.previousElementSibling; if (!input) return; if (input.type === "password") { input.type = "text"; btn.classList.remove("fa-eye"); btn.classList.add("fa-eye-slash"); btn.style.color = "var(--color-sky-brand)"; } else { input.type = "password"; btn.classList.remove("fa-eye-slash"); btn.classList.add("fa-eye"); btn.style.color = ""; } }); }); } window.initPasswordToggle = initPasswordToggle; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => { bootApp(); initPasswordToggle(); }); } else { bootApp(); initPasswordToggle(); }
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Generation time: 0.45 |
proxy
|
phpinfo
|
Settings