File manager - Edit - /home/webapp69.cm.in.th/u69319090006/shop/admin-seller.js
Back
function openAdminSlipVerifyModal(order) { const modal = document.getElementById("adminSlipVerifyModal"); const img = document.getElementById("adminSlipVerifyImg"); const info = document.getElementById("adminSlipVerifyOrderInfo"); const reasonInput = document.getElementById("adminSlipRejectReason"); const approveBtn = document.getElementById("btnAdminApproveSlipInModal"); const rejectBtn = document.getElementById("btnAdminRejectSlipInModal"); const closeBtn = document.getElementById("closeAdminSlipVerifyModalBtn"); if (!modal || !img || !info) return; let slipSrc = order.slipImage; let actualSlip = slipSrc === "dummy_slip" || !slipSrc ? (typeof MOCK_SLIP_IMAGE !== 'undefined' ? MOCK_SLIP_IMAGE : "") : slipSrc; img.src = actualSlip; const newTabBtn = document.getElementById("btnOpenAdminSlipNewTab"); if (newTabBtn) { newTabBtn.href = actualSlip; } info.innerHTML = ` <div><strong>เลขที่ออเดอร์:</strong> ${order.id}</div> <div><strong>ผู้สั่งซื้อ:</strong> ${order.customerName} (${order.shippingInfo ? order.shippingInfo.phone : '-'})</div> <div><strong>วันที่สั่งซื้อ:</strong> ${order.date}</div> <div><strong>ยอดชำระสุทธิ:</strong> <span class="text-sky font-bold" style="font-size:1.1rem">฿${order.total.toLocaleString()}</span></div> `; if (reasonInput) reasonInput.value = ""; const handleApprove = () => { const ordersDb = db.getOrders(); const targetOrder = ordersDb.find(o => o.id === order.id); if (targetOrder) { targetOrder.status = "Paid"; db.saveOrders(ordersDb); showToast(`อนุมัติการชำระเงินออเดอร์ ${order.id} สำเร็จ!`, "success"); } modal.style.display = "none"; renderAdminDashboard(); renderAdminOrders(); if (typeof renderOrderHistory === "function") renderOrderHistory(); }; const handleReject = () => { const reason = reasonInput ? reasonInput.value.trim() : ""; if (!reason) { showToast("กรุณาระบุเหตุผลในการปฏิเสธสลิปค่ะ", "danger"); if (reasonInput) reasonInput.focus(); return; } const ordersDb = db.getOrders(); const targetOrder = ordersDb.find(o => o.id === order.id); if (targetOrder) { targetOrder.status = "Failed"; targetOrder.rejectReason = reason; targetOrder.cancelReason = `ปฏิเสธสลิป: ${reason}`; db.saveOrders(ordersDb); showToast(`ปฏิเสธการชำระเงินออเดอร์ ${order.id} เรียบร้อยแล้ว`, "info"); } modal.style.display = "none"; renderAdminDashboard(); renderAdminOrders(); if (typeof renderOrderHistory === "function") renderOrderHistory(); }; if (approveBtn) approveBtn.onclick = handleApprove; if (rejectBtn) rejectBtn.onclick = handleReject; if (closeBtn) closeBtn.onclick = () => { modal.style.display = "none"; }; modal.onclick = (e) => { if (e.target === modal) modal.style.display = "none"; }; modal.style.display = "flex"; } 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", () => { openAdminSlipVerifyModal(order); }); tr.querySelector(".dash-approve-slip-btn").addEventListener("click", () => { openAdminSlipVerifyModal(order); }); tr.querySelector(".dash-reject-slip-btn").addEventListener("click", () => { openAdminSlipVerifyModal(order); }); 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; const sellerSelect = document.getElementById("adminStockFilterSeller"); const statusSelect = document.getElementById("adminStockFilterStatus"); const sellerFilter = sellerSelect ? sellerSelect.value : "all"; const statusFilter = statusSelect ? statusSelect.value : "all"; if (sellerSelect && sellerSelect.children.length <= 1) { const users = db.getUsers(); const sellers = users.filter(u => u.role === "seller"); sellers.forEach(s => { const opt = document.createElement("option"); opt.value = s.username; opt.innerText = s.name || s.username; sellerSelect.appendChild(opt); }); } if (sellerSelect && !sellerSelect.dataset.bound) { sellerSelect.dataset.bound = "true"; sellerSelect.addEventListener("change", renderAdminStock); } if (statusSelect && !statusSelect.dataset.bound) { statusSelect.dataset.bound = "true"; statusSelect.addEventListener("change", renderAdminStock); } 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; const matchSeller = sellerFilter === "all" || p.sellerId === sellerFilter; const totalStock = p.variants ? p.variants.reduce((sum, v) => sum + v.stock, 0) : (p.stock || 0); let matchStatus = true; if (statusFilter === "available") { matchStatus = totalStock > 5; } else if (statusFilter === "low") { matchStatus = totalStock >= 1 && totalStock <= 5; } else if (statusFilter === "out") { matchStatus = totalStock === 0; } return matchName && matchCat && matchSeller && matchStatus; }); 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-purple btn-xs preview-prod-btn" data-prod-id="${p.id}"> <i class="fa-solid fa-eye"></i> ดูตัวอย่างสินค้า (Preview) </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); }); // Add preview product button handler if exists const previewBtn = tr.querySelector(".preview-prod-btn"); if (previewBtn) { previewBtn.addEventListener("click", () => { openProductDetail(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); }); } // ========================================================================== // IMAGE UPLOADER MANAGER (SELLER CENTER STYLE) // ========================================================================== let tempAddImages = []; let tempEditImages = []; function renderAddImageGrid() { const grid = document.getElementById("newProdImageGrid"); if (!grid) return; let html = ""; tempAddImages.forEach((img, idx) => { html += ` <div class="upload-thumb-card"> <img src="${img}" alt="Preview ${idx + 1}"> ${idx === 0 ? '<span class="main-badge">รูปหลัก</span>' : ''} <button type="button" class="remove-img-btn" onclick="removeTempAddImage(${idx})">✕</button> </div> `; }); html += ` <label for="newProdImage" class="upload-add-card"> <i class="fa-solid fa-cloud-arrow-up"></i> <span>+ เพิ่มรูปภาพ</span> <input type="file" id="newProdImage" class="form-control" accept="image/*" multiple style="display: none;"> </label> `; grid.innerHTML = html; bindAddImageInputListener(); } function removeTempAddImage(index) { tempAddImages.splice(index, 1); renderAddImageGrid(); } window.removeTempAddImage = removeTempAddImage; function bindAddImageInputListener() { const input = document.getElementById("newProdImage"); if (!input) return; input.addEventListener("change", (e) => { const files = Array.from(e.target.files); if (files.length === 0) return; const form = document.getElementById("addProductForm"); const submitBtn = form ? form.querySelector('button[type="submit"]') : null; if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> กำลังประมวลผลรูปภาพ...'; } const promises = files.map(file => { return new Promise((resolve) => { if (file.size > 5 * 1024 * 1024) { resolve(""); return; } 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); resolve(canvas.toDataURL("image/webp", 0.7)); } catch (err) { resolve(""); } }; img.onerror = () => resolve(""); img.src = event.target.result; }; reader.onerror = () => resolve(""); reader.readAsDataURL(file); }); }); Promise.all(promises).then(results => { const valid = results.filter(r => r !== ""); tempAddImages = tempAddImages.concat(valid); if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = '<i class="fa-solid fa-plus"></i> เพิ่มสินค้าเข้าระบบ'; } renderAddImageGrid(); }); }); } function renderEditImageGrid() { const grid = document.getElementById("editProdImageGrid"); if (!grid) return; let html = ""; tempEditImages.forEach((img, idx) => { html += ` <div class="upload-thumb-card"> <img src="${img}" alt="Preview ${idx + 1}"> ${idx === 0 ? '<span class="main-badge">รูปหลัก</span>' : ''} <button type="button" class="remove-img-btn" onclick="removeTempEditImage(${idx})">✕</button> </div> `; }); html += ` <label for="editProdImage" class="upload-add-card"> <i class="fa-solid fa-cloud-arrow-up"></i> <span>+ เพิ่มรูปภาพ</span> <input type="file" id="editProdImage" class="form-control" accept="image/*" multiple style="display: none;"> </label> `; grid.innerHTML = html; bindEditImageInputListener(); } function removeTempEditImage(index) { tempEditImages.splice(index, 1); renderEditImageGrid(); } window.removeTempEditImage = removeTempEditImage; function bindEditImageInputListener() { const input = document.getElementById("editProdImage"); if (!input) return; input.addEventListener("change", (e) => { const files = Array.from(e.target.files); if (files.length === 0) return; const form = document.getElementById("editProductForm"); const submitBtn = form ? form.querySelector('button[type="submit"]') : null; if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> กำลังประมวลผลรูปภาพ...'; } const promises = files.map(file => { return new Promise((resolve) => { if (file.size > 5 * 1024 * 1024) { resolve(""); return; } 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); resolve(canvas.toDataURL("image/webp", 0.7)); } catch (err) { resolve(""); } }; img.onerror = () => resolve(""); img.src = event.target.result; }; reader.onerror = () => resolve(""); reader.readAsDataURL(file); }); }); Promise.all(promises).then(results => { const valid = results.filter(r => r !== ""); tempEditImages = tempEditImages.concat(valid); if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = '<i class="fa-solid fa-circle-check"></i> บันทึกข้อมูลที่แก้ไข'; } renderEditImageGrid(); }); }); } // 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"; // Populate tempEditImages with existing product images and render preview grid const existingImages = (product.images && product.images.length > 0) ? product.images : (product.image ? [product.image] : []); tempEditImages = [...existingImages]; renderEditImageGrid(); modal.style.display = "flex"; } function initEditProductFlow() { const modal = document.getElementById("editProductModal"); const closeBtn = document.getElementById("closeEditProductModalBtn"); const form = document.getElementById("editProductForm"); bindEditImageInputListener(); 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; product.images = [...tempEditImages]; product.image = tempEditImages[0] || ""; db.saveProducts(products); alert("แก้ไขข้อมูลสินค้าสำเร็จ!"); modal.style.display = "none"; tempEditImages = []; renderEditImageGrid(); 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"); bindAddImageInputListener(); // 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 = ""; } tempAddImages = []; renderAddImageGrid(); 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; } const newProdCatSelect = document.getElementById("newProdCategory"); if (newProdCatSelect) { newProdCatSelect.innerHTML = ""; DEFAULT_CATEGORIES.forEach(c => { newProdCatSelect.innerHTML += `<option value="${c.name}">${c.name}</option>`; }); } tempAddImages = []; renderAddImageGrid(); 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(); 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 defaultImgPlace = (typeof DEFAULT_IMAGE_PLACEHOLDERS !== 'undefined' && DEFAULT_IMAGE_PLACEHOLDERS.default) ? DEFAULT_IMAGE_PLACEHOLDERS.default : ""; const finalImagesList = tempAddImages.length > 0 ? tempAddImages : [defaultImgPlace]; const newProduct = { id: newId, name: name, category: category, sellerName: currentSellerName, sellerId: currentSellerId, description: desc, price: price, image: finalImagesList[0] || "", images: finalImagesList, active: true, colors: colors, condition: condition, rating: 5, variants: variants }; products.push(newProduct); db.saveProducts(products); alert("เพิ่มสินค้าเข้าระบบสำเร็จ!"); form.reset(); tempAddImages = []; renderAddImageGrid(); 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 let currentAdminOrderTab = "all"; function renderAdminOrders() { const tbody = document.getElementById("adminOrdersTableBody"); if (!tbody) return; tbody.innerHTML = ""; state.orders = db.getOrders(); const tabButtons = document.querySelectorAll("#adminOrderTabs .admin-tab-btn"); tabButtons.forEach(btn => { btn.onclick = () => { tabButtons.forEach(b => b.classList.remove("active")); btn.classList.add("active"); currentAdminOrderTab = btn.getAttribute("data-status"); renderAdminOrders(); }; }); let filteredOrders = state.orders; if (currentAdminOrderTab !== "all") { if (currentAdminOrderTab === "Cancelled") { filteredOrders = state.orders.filter(o => o.status === "Cancelled" || o.status === "Failed"); } else { filteredOrders = state.orders.filter(o => o.status === currentAdminOrderTab); } } if (filteredOrders.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(); filteredOrders.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") { let refundHtml = ""; if (order.refundInfo && order.refundInfo.accountNo) { refundHtml = ` <div class="refund-info-box"> <div><strong><i class="fa-solid fa-building-columns"></i> บัญชีคืนเงินลูกค้า:</strong></div> <div>ธนาคาร: ${order.refundInfo.bankName}</div> <div>เลขบัญชี: ${order.refundInfo.accountNo}</div> <div>ชื่อบัญชี: ${order.refundInfo.accountName}</div> </div> `; } 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> ${refundHtml} </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 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(); const orders = db.getOrders(); state.coupons.forEach((coupon, idx) => { const usageCount = orders.filter(o => o.couponCode === coupon.code || (o.appliedCoupon && o.appliedCoupon.code === coupon.code)).length; const tr = document.createElement("tr"); tr.innerHTML = ` <td><strong>${coupon.code}</strong></td> <td> ${coupon.type === "percent" ? `ลด ${coupon.value}%` : `ลด ฿${coupon.value}`} </td> <td>฿${(coupon.minSpend || 0).toLocaleString()}</td> <td>${coupon.expiryDate ? coupon.expiryDate : '<span style="color:var(--color-sky-slate)">ไม่มีวันหมดอายุ</span>'}</td> <td><span class="badge badge-sky">${usageCount} ครั้ง</span></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); const minSpendInput = document.getElementById("newCouponMinSpend"); const expiryDateInput = document.getElementById("newCouponExpiryDate"); const minSpend = minSpendInput ? (parseInt(minSpendInput.value) || 0) : 0; const expiryDate = expiryDateInput ? expiryDateInput.value : ""; if (!code || val <= 0) return; const coupons = db.getCoupons(); if (coupons.some(c => c.code === code)) { alert("รหัสคูปองนี้มีอยู่แล้วในระบบ"); return; } coupons.push({ code: code, type: type, value: val, minSpend: minSpend, expiryDate: expiryDate }); 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(); const approvedStatuses = ["Paid", "Preparing", "Shipped", "Success"]; const approvedOrders = orders.filter(o => approvedStatuses.includes(o.status)); let sellerRevenue = 0; let sellerOrderCount = 0; const isSellerMatch = (item) => { if (item.sellerId === currentSellerId) return true; if (state.currentUser) { if (state.currentUser.username && (item.sellerId === state.currentUser.username || item.sellerName === state.currentUser.username)) return true; if (state.currentUser.name && (item.sellerId === state.currentUser.name || item.sellerName === state.currentUser.name)) return true; if (state.currentUser.companyName && (item.sellerId === state.currentUser.companyName || item.sellerName === state.currentUser.companyName)) return true; } return false; }; approvedOrders.forEach(order => { let hasSellerProduct = false; order.items.forEach(item => { if (isSellerMatch(item)) { 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 || (state.currentUser && (p.sellerId === state.currentUser.username || p.sellerName === state.currentUser.name || p.sellerName === state.currentUser.companyName))); sellerProducts.forEach(p => { if (p.variants) { 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 = {}; const isSellerMatch = (item) => { if (item.sellerId === currentSellerId) return true; if (state.currentUser) { if (state.currentUser.username && (item.sellerId === state.currentUser.username || item.sellerName === state.currentUser.username)) return true; if (state.currentUser.name && (item.sellerId === state.currentUser.name || item.sellerName === state.currentUser.name)) return true; if (state.currentUser.companyName && (item.sellerId === state.currentUser.companyName || item.sellerName === state.currentUser.companyName)) return true; } return false; }; filteredOrders.forEach(order => { order.items.forEach(item => { if (isSellerMatch(item)) { 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 renderAdminClients() { const tbody = document.getElementById("adminClientTableBody"); if (!tbody) return; tbody.innerHTML = ""; const users = db.getUsers(); const clientQuery = (document.getElementById("adminClientSearch")?.value || "").toLowerCase(); const clients = users.filter(u => (u.role === "client" || (!u.role && u.username !== "admin")) && !u.isBanned && ( !clientQuery || u.username.toLowerCase().includes(clientQuery) || (u.name && u.name.toLowerCase().includes(clientQuery)) )); 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>`; const rawPass = typeof decodePassword === "function" ? decodePassword(c.password) : (c.password || "123456"); tr.innerHTML = ` <td><strong>${c.username}</strong></td> <td><strong>${c.name || '-'}</strong></td> <td> <div class="d-flex align-items-center gap-2" style="white-space: nowrap;"> <span class="pass-text" style="font-family: monospace; letter-spacing: 1px;">••••••••</span> <button type="button" class="btn btn-sky-outline btn-xs btn-toggle-pass" title="แสดง/ซ่อนรหัสผ่าน"> <i class="fa-solid fa-eye"></i> </button> </div> </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> `; const passBtn = tr.querySelector(".btn-toggle-pass"); const passTextSpan = tr.querySelector(".pass-text"); if (passBtn && passTextSpan) { let isRevealed = false; passBtn.addEventListener("click", () => { isRevealed = !isRevealed; if (isRevealed) { passTextSpan.innerText = rawPass; passBtn.innerHTML = `<i class="fa-solid fa-eye-slash"></i>`; passBtn.classList.remove("btn-sky-outline"); passBtn.classList.add("btn-warning-outline"); } else { passTextSpan.innerText = "••••••••"; passBtn.innerHTML = `<i class="fa-solid fa-eye"></i>`; passBtn.classList.remove("btn-warning-outline"); passBtn.classList.add("btn-sky-outline"); } }); } // 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 sellerQuery = (document.getElementById("adminSellerSearch")?.value || "").toLowerCase(); const sellers = users.filter(u => u.role === "seller" && !u.isBanned && ( !sellerQuery || u.username.toLowerCase().includes(sellerQuery) || (u.name && u.name.toLowerCase().includes(sellerQuery)) || (u.companyName && u.companyName.toLowerCase().includes(sellerQuery)) )); if (sellers.length === 0) { tbody.innerHTML = `<tr><td colspan="7" 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>`; const rawPass = typeof decodePassword === "function" ? decodePassword(s.password) : (s.password || "123456"); tr.innerHTML = ` <td><strong>${s.username}</strong></td> <td><strong>${s.name}</strong></td> <td> <div class="d-flex align-items-center gap-2" style="white-space: nowrap;"> <span class="pass-text" style="font-family: monospace; letter-spacing: 1px;">••••••••</span> <button type="button" class="btn btn-sky-outline btn-xs btn-toggle-pass" title="แสดง/ซ่อนรหัสผ่าน"> <i class="fa-solid fa-eye"></i> </button> </div> </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> `; const passBtn = tr.querySelector(".btn-toggle-pass"); const passTextSpan = tr.querySelector(".pass-text"); if (passBtn && passTextSpan) { let isRevealed = false; passBtn.addEventListener("click", () => { isRevealed = !isRevealed; if (isRevealed) { passTextSpan.innerText = rawPass; passBtn.innerHTML = `<i class="fa-solid fa-eye-slash"></i>`; passBtn.classList.remove("btn-sky-outline"); passBtn.classList.add("btn-warning-outline"); } else { passTextSpan.innerText = "••••••••"; passBtn.innerHTML = `<i class="fa-solid fa-eye"></i>`; passBtn.classList.remove("btn-warning-outline"); passBtn.classList.add("btn-sky-outline"); } }); } // 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="6" 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 style="font-size:12px; color:var(--color-sky-slate);">${b.banReason || 'ไม่ระบุเหตุผล'}</span></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() { const clientSearchInput = document.getElementById("adminClientSearch"); if (clientSearchInput && !clientSearchInput.dataset.bound) { clientSearchInput.dataset.bound = "true"; clientSearchInput.addEventListener("input", renderAdminClients); } const sellerSearchInput = document.getElementById("adminSellerSearch"); if (sellerSearchInput && !sellerSearchInput.dataset.bound) { sellerSearchInput.dataset.bound = "true"; sellerSearchInput.addEventListener("input", renderAdminSellers); } 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 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 isSellerMatch = (item) => { if (item.sellerId === currentSellerId) return true; if (state.currentUser) { if (state.currentUser.username && (item.sellerId === state.currentUser.username || item.sellerName === state.currentUser.username)) return true; if (state.currentUser.name && (item.sellerId === state.currentUser.name || item.sellerName === state.currentUser.name)) return true; if (state.currentUser.companyName && (item.sellerId === state.currentUser.companyName || item.sellerName === state.currentUser.companyName)) return true; } return false; }; const sellerOrders = state.orders.filter(order => order.items.some(item => isSellerMatch(item))); 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 => isSellerMatch(item)); 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> ${isEn ? 'Cash on Delivery' : 'เก็บเงินปลายทาง'}</div>`; } else if (isUnpaidNoSlip) { slipCellHtml = `<span style="color:var(--color-sky-slate); font-size:11px"><i class="fa-solid fa-clock"></i> ${isEn ? 'Pending Payment' : 'รอชำระเงิน'}</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 = ""; 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> ${isEn ? 'Pending Payment' : 'รอชำระเงิน'}</span> <button class="btn btn-sky-outline btn-xs seller-approve-slip-btn" data-id="${order.id}"> <i class="fa-solid fa-check"></i> ${isEn ? 'Confirm Received (Bypass Slip)' : 'ยืนยันรับเงินแล้ว (ข้ามสลิป)'} </button> <button class="btn btn-danger-outline btn-xs seller-cancel-order-btn" data-id="${order.id}"> <i class="fa-solid fa-ban"></i> ${isEn ? 'Cancel Order' : 'ยกเลิกออเดอร์'} </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}"> ${isEn ? 'Approve Payment' : 'อนุมัติรับเงิน'} </button> <button class="btn btn-danger-outline btn-xs seller-cancel-order-btn" data-id="${order.id}"> <i class="fa-solid fa-ban"></i> ${isEn ? 'Reject/Cancel' : 'ยกเลิก/ปฏิเสธรับเงิน'} </button> </div> `; } else if (order.status === "Paid" || order.status === "Preparing") { const trackPlaceholder = isEn ? "Tracking No..." : "เลขพัสดุ..."; const shipConfirmText = isEn ? "Confirm Shipment" : "ยืนยันส่งของ (ไปสถานะจัดส่งแล้ว)"; 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="${trackPlaceholder}" style="padding: 4px 8px; font-size:12px;"> <button class="btn btn-sky btn-xs ship-confirm-btn" data-id="${order.id}"> ${shipConfirmText} </button> <button class="btn btn-danger-outline btn-xs seller-cancel-order-btn" data-id="${order.id}"> <i class="fa-solid fa-ban"></i> ${isEn ? 'Cancel Order' : 'ยกเลิกออเดอร์'} </button> </div> `; } else if (order.status === "Shipped") { statusSelectHtml = ` <div class="text-success mb-1"> <i class="fa-solid fa-truck"></i> ${isEn ? 'Shipped' : 'จัดส่งแล้ว'}<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> ${isEn ? 'Mark Delivered' : 'จัดส่งสำเร็จ'} </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> ${isEn ? 'Cancelled' : 'ยกเลิกแล้ว'}<br> <small style="color:var(--color-sky-slate);">(${order.cancelReason || (isEn ? 'Unspecified' : 'ไม่ระบุ')})</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. ENTERPRISE ADMIN PANEL MODULES // ========================================================================== // -------------------------------------------------------------------------- // A. PENDING SELLER APPLICATIONS QUEUE // -------------------------------------------------------------------------- function getSellerApplications() { const saved = localStorage.getItem("sk_seller_applications"); if (saved) { try { return JSON.parse(saved); } catch (e) { } } const defaultApps = [ { id: "app-101", username: "kitti_k", storeName: "ร้านอุปกรณ์กีฬา SkySports", sellerType: "regular", phone: "089-123-4567", documents: "สำเนาบัตรประชาชน + บัญชี กสิกรไทย", appliedAt: "2026-08-05 14:30", status: "pending" }, { id: "app-102", username: "wilai_p", storeName: "ร้านเครื่องสำอาง SkyBeauty Co., Ltd.", sellerType: "corporate", companyName: "สกายบิวตี้ จำกัด", phone: "081-987-6543", documents: "หนังสือรับรองนิติบุคคล + ภ.พ.20", appliedAt: "2026-08-06 09:15", status: "pending" } ]; localStorage.setItem("sk_seller_applications", JSON.stringify(defaultApps)); return defaultApps; } function saveSellerApplications(apps) { localStorage.setItem("sk_seller_applications", JSON.stringify(apps)); } function renderAdminPendingSellerApps() { const tbody = document.getElementById("adminPendingSellerAppsTableBody"); if (!tbody) return; tbody.innerHTML = ""; const apps = getSellerApplications().filter(a => a.status === "pending"); if (apps.length === 0) { tbody.innerHTML = `<tr><td colspan="6" class="text-center py-4" style="color:var(--color-sky-slate)"><i class="fa-solid fa-circle-check text-success"></i> ไม่มีคำขอสมัครร้านค้าใหม่รอการอนุมัติในขณะนี้</td></tr>`; return; } apps.forEach(app => { const tr = document.createElement("tr"); const typeBadge = app.sellerType === "corporate" ? `<span class="badge badge-sky" style="font-size:11px;"><i class="fa-solid fa-building"></i> นิติบุคคล</span>` : `<span class="badge badge-dark" style="font-size:11px; background:#64748b;"><i class="fa-solid fa-user"></i> บุคคลธรรมดา</span>`; tr.innerHTML = ` <td><strong>@${app.username}</strong></td> <td><strong>${app.storeName}</strong> ${app.companyName ? `<br><small class="text-muted">(${app.companyName})</small>` : ''}</td> <td>${typeBadge}</td> <td>${app.phone}<br><span style="font-size:11px; color:var(--color-sky-slate);">${app.documents}</span></td> <td><span style="font-size:12px; color:var(--color-sky-slate);"><i class="fa-solid fa-clock"></i> ${app.appliedAt}</span></td> <td> <div class="flex-column gap-1" style="display:flex; gap:5px;"> <button class="btn btn-success btn-xs btn-approve-app" data-id="${app.id}"> <i class="fa-solid fa-check"></i> อนุมัติสิทธิ์ผู้ขาย </button> <button class="btn btn-danger-outline btn-xs btn-reject-app" data-id="${app.id}"> <i class="fa-solid fa-times"></i> ปฏิเสธคำขอ </button> </div> </td> `; tr.querySelector(".btn-approve-app").addEventListener("click", () => approveSellerApp(app.id)); tr.querySelector(".btn-reject-app").addEventListener("click", () => rejectSellerApp(app.id)); tbody.appendChild(tr); }); } function approveSellerApp(appId) { const apps = getSellerApplications(); const app = apps.find(a => a.id === appId); if (!app) return; if (confirm(`ยืนยันการอนุมัติสิทธิ์ผู้ขายให้แก่ "@${app.username}" (${app.storeName})?`)) { app.status = "approved"; saveSellerApplications(apps); const users = db.getUsers(); const userObj = users.find(u => u.username === app.username); if (userObj) { userObj.role = "seller"; userObj.sellerType = app.sellerType; userObj.companyName = app.storeName; db.saveUsers(users); } showToast(`อนุมัติสิทธิ์ผู้ขายแก่ @${app.username} สำเร็จแล้ว!`, "success"); renderAdminPendingSellerApps(); renderAdminClients(); renderAdminSellers(); } } function rejectSellerApp(appId) { const apps = getSellerApplications(); const app = apps.find(a => a.id === appId); if (!app) return; const reason = prompt(`ระบุเหตุผลในการปฏิเสธการสมัครร้านค้าของ "@${app.username}":`, "เอกสารไม่ครบถ้วน / ข้อมูลไม่ชัดเจน"); if (reason !== null) { app.status = "rejected"; app.rejectReason = reason; saveSellerApplications(apps); showToast(`ปฏิเสธคำขอสมัครร้านค้าของ @${app.username} เรียบร้อยแล้ว`, "info"); renderAdminPendingSellerApps(); } } // -------------------------------------------------------------------------- // B. REVIEW MODERATION SYSTEM // -------------------------------------------------------------------------- function renderAdminReviews() { const tbody = document.getElementById("adminReviewsTableBody"); if (!tbody) return; tbody.innerHTML = ""; const searchQuery = (document.getElementById("adminReviewSearch")?.value || "").toLowerCase(); const ratingFilter = document.getElementById("adminReviewFilterRating")?.value || "all"; const searchInput = document.getElementById("adminReviewSearch"); if (searchInput && !searchInput.dataset.bound) { searchInput.dataset.bound = "true"; searchInput.addEventListener("input", renderAdminReviews); } const ratingSelect = document.getElementById("adminReviewFilterRating"); if (ratingSelect && !ratingSelect.dataset.bound) { ratingSelect.dataset.bound = "true"; ratingSelect.addEventListener("change", renderAdminReviews); } const products = db.getProducts(); const allReviewsList = []; products.forEach(p => { const pReviews = typeof getProductReviews === "function" ? getProductReviews(p.id) : (MOCK_REVIEWS[p.id] || []); pReviews.forEach((r, idx) => { allReviewsList.push({ productId: p.id, productName: p.name, productImg: p.image || (typeof DEFAULT_IMAGE_PLACEHOLDERS !== 'undefined' ? DEFAULT_IMAGE_PLACEHOLDERS.default : ""), reviewIndex: idx, username: r.username, rating: r.rating, date: r.date, text: r.text }); }); }); const filtered = allReviewsList.filter(item => { const matchText = item.productName.toLowerCase().includes(searchQuery) || item.username.toLowerCase().includes(searchQuery) || item.text.toLowerCase().includes(searchQuery); const matchRating = ratingFilter === "all" || item.rating.toString() === ratingFilter; return matchText && matchRating; }); 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(r => { const tr = document.createElement("tr"); const starsHtml = "★".repeat(r.rating) + "☆".repeat(5 - r.rating); tr.innerHTML = ` <td> <div style="display:flex; align-items:center; gap:10px;"> <img src="${r.productImg}" style="width:36px; height:36px; object-fit:cover; border-radius:4px;" alt="Prod"> <span style="font-weight:600; font-size:12px;">${r.productName}</span> </div> </td> <td><strong>${r.username}</strong></td> <td><span class="text-warning" style="font-weight:bold;">${starsHtml}</span> (${r.rating})</td> <td><span style="font-size:11px; color:var(--color-sky-slate);">${r.date}</span></td> <td><div style="max-width:240px; font-size:12px; white-space:normal;">${r.text}</div></td> <td> <button class="btn btn-danger-outline btn-xs delete-review-btn"> <i class="fa-solid fa-trash-can"></i> ลบ/ซ่อนรีวิว </button> </td> `; tr.querySelector(".delete-review-btn").addEventListener("click", () => { if (confirm(`ยืนยันการลบรีวิวของ ${r.username} บนสินค้า "${r.productName}"?`)) { if (typeof MOCK_REVIEWS !== "undefined" && MOCK_REVIEWS[r.productId]) { MOCK_REVIEWS[r.productId].splice(r.reviewIndex, 1); } showToast("ลบรีวิวที่ไม่เหมาะสมออกจากระบบเรียบร้อยแล้ว!", "success"); renderAdminReviews(); renderCatalog(); } }); tbody.appendChild(tr); }); } // -------------------------------------------------------------------------- // C. COMMISSION & PAYOUT MANAGEMENT // -------------------------------------------------------------------------- function getWithdrawalRequests() { const saved = localStorage.getItem("sk_seller_withdrawals"); if (saved) { try { return JSON.parse(saved); } catch (e) { } } const defaultWithdrawals = [ { id: "w-901", sellerId: "seller1", sellerName: "ร้านอุปกรณ์ไอที SkyTech", bankName: "กสิกรไทย (KBANK)", accountNo: "123-4-56789-0", amount: 15000, requestedAt: "2026-08-06 11:20", status: "Pending" }, { id: "w-902", sellerId: "seller2", sellerName: "ร้านแฟชั่น SkyThreads", bankName: "ไทยพาณิชย์ (SCB)", accountNo: "987-6-54321-0", amount: 8500, requestedAt: "2026-08-07 08:45", status: "Pending" } ]; localStorage.setItem("sk_seller_withdrawals", JSON.stringify(defaultWithdrawals)); return defaultWithdrawals; } function saveWithdrawalRequests(list) { localStorage.setItem("sk_seller_withdrawals", JSON.stringify(list)); } function renderAdminFinancials() { const tbody = document.getElementById("adminWithdrawalsTableBody"); const rateInput = document.getElementById("commissionRateInput"); if (rateInput) { rateInput.value = localStorage.getItem("sk_commission_rate") || "5"; } const currentRate = parseFloat(localStorage.getItem("sk_commission_rate") || "5"); const orders = db.getOrders(); const approvedOrders = orders.filter(o => o.status === "Paid" || o.status === "Preparing" || o.status === "Shipped" || o.status === "Success"); const totalSales = approvedOrders.reduce((sum, o) => sum + o.total, 0); const platformCommissionEarned = totalSales * (currentRate / 100); const commFormatted = `฿${platformCommissionEarned.toLocaleString('th-TH', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; const kpiCommission = document.getElementById("kpiTotalPlatformCommission"); if (kpiCommission) kpiCommission.innerText = commFormatted; const totalCommAcc = document.getElementById("totalCommissionAccumulated"); if (totalCommAcc) totalCommAcc.innerText = commFormatted; const withdrawals = getWithdrawalRequests(); const pendingWithdrawals = withdrawals.filter(w => w.status === "Pending"); const pendingAmount = pendingWithdrawals.reduce((sum, w) => sum + w.amount, 0); const pendingFormatted = `฿${pendingAmount.toLocaleString('th-TH', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; const kpiPending = document.getElementById("kpiPendingPayoutsAmount"); if (kpiPending) kpiPending.innerText = pendingFormatted; const pendingPayoutTotalEl = document.getElementById("pendingPayoutTotal"); if (pendingPayoutTotalEl) pendingPayoutTotalEl.innerText = pendingFormatted; if (!tbody) return; tbody.innerHTML = ""; if (withdrawals.length === 0) { tbody.innerHTML = `<tr><td colspan="7" class="text-center py-4" style="color:var(--color-sky-slate)">ยังไม่มีรายการคำขอถอนเงินของผู้ขายในระบบ</td></tr>`; return; } withdrawals.forEach(w => { const tr = document.createElement("tr"); const commAmount = w.amount * (currentRate / 100); const netPayout = w.amount - commAmount; let statusBadge = `<span class="badge badge-warning">รอการโอนเงิน</span>`; if (w.status === "Approved") statusBadge = `<span class="badge badge-success">โอนเงินสำเร็จแล้ว</span>`; if (w.status === "Rejected") statusBadge = `<span class="badge badge-danger">ปฏิเสธคำขอ</span>`; tr.innerHTML = ` <td><strong>${w.sellerName}</strong></td> <td>฿${w.amount.toLocaleString()}</td> <td><small class="text-danger">-${currentRate}% (฿${commAmount.toLocaleString()})</small></td> <td><strong class="text-success" style="font-size:1.05rem;">฿${netPayout.toLocaleString()}</strong></td> <td><span style="font-size:11px;">${w.bankName}<br>เลขบัญชี: ${w.accountNo}</span></td> <td><span style="font-size:11px; color:var(--color-sky-slate);">${w.requestedAt}</span></td> <td> <div style="display:flex; flex-direction:column; gap:5px;"> ${statusBadge} ${w.status === "Pending" ? ` <div style="display:flex; gap:5px;" class="mt-1"> <button class="btn btn-success btn-xs btn-approve-payout" data-id="${w.id}"> <i class="fa-solid fa-check"></i> อนุมัติ </button> <button class="btn btn-danger-outline btn-xs btn-reject-payout" data-id="${w.id}"> <i class="fa-solid fa-xmark"></i> ปฏิเสธ </button> </div> ` : ''} </div> </td> `; const approveBtn = tr.querySelector(".btn-approve-payout"); if (approveBtn) approveBtn.onclick = () => approveWithdrawal(w.id); const rejectBtn = tr.querySelector(".btn-reject-payout"); if (rejectBtn) rejectBtn.onclick = () => rejectWithdrawal(w.id); tbody.appendChild(tr); }); } window.renderAdminFinance = renderAdminFinancials; function initFinancialsFlow() { const form = document.getElementById("platformCommissionForm"); if (form) { form.addEventListener("submit", (e) => { e.preventDefault(); const val = document.getElementById("commissionRateInput").value; localStorage.setItem("sk_commission_rate", val); showToast(`บันทึกอัตราค่าธรรมเนียมแพลตฟอร์มเป็น ${val}% สำเร็จ!`, "success"); renderAdminFinancials(); }); } } function approveWithdrawal(wId) { const withdrawals = getWithdrawalRequests(); const w = withdrawals.find(item => item.id === wId); if (w && confirm(`ยืนยันการอนุมัติโอนเงินสุทธิให้แก่ร้าน "${w.sellerName}" ยอด ฿${w.amount.toLocaleString()}?`)) { w.status = "Approved"; saveWithdrawalRequests(withdrawals); showToast(`อนุมัติโอนเงินเข้าบัญชีร้าน ${w.sellerName} สำเร็จ!`, "success"); renderAdminFinancials(); } } function rejectWithdrawal(wId) { const withdrawals = getWithdrawalRequests(); const w = withdrawals.find(item => item.id === wId); if (w && confirm(`ยืนยันการปฏิเสธคำขอถอนเงินของร้าน "${w.sellerName}"?`)) { w.status = "Rejected"; saveWithdrawalRequests(withdrawals); showToast(`ปฏิเสธคำขอถอนเงินเรียบร้อยแล้ว`, "info"); renderAdminFinancials(); } } // -------------------------------------------------------------------------- // D. HERO BANNER MANAGER & BROADCAST ANNOUNCEMENT // -------------------------------------------------------------------------- function getHeroBanners() { const saved = localStorage.getItem("sk_hero_banners"); if (saved) { try { return JSON.parse(saved); } catch (e) { } } const defaultBanners = [ { title: "ก้าวสู่อนาคตกับ SkyTech Gadgets", subtitle: "สัมผัสเทคโนโลยีอัจฉริยะ โทรศัพท์ สมาร์ตโฟน และอุปกรณ์ไอทีลดสูงสุด 50%", badge: "Tech Deals 2026", couponCode: "TECH100", bgStyle: "linear-gradient(135deg, #e0f2fe, #bae6fd, #e0f2fe)", sellerTarget: "seller1", active: true }, { title: "แต่งตัวอย่างมีสไตล์กับ SkyThreads", subtitle: "เสื้อผ้าแฟชั่นมินิมอล เนื้อผ้าเกรดพรีเมียม สวมใส่สบาย โปรแรงต้อนรับซีซันใหม่", badge: "Minimal Chic", couponCode: "FASHION20", bgStyle: "linear-gradient(135deg, #fef2f2, #fecdd3, #fef2f2)", sellerTarget: "seller2", active: true }, { title: "ส่งมอบความสุขให้สัตว์เลี้ยงตัวโปรด", subtitle: "อาหารและอุปกรณ์สำหรับน้องหมาน้องแมว เกรดโภชนาการสูง ลดพิเศษ 30%", badge: "Aqua & Pets", couponCode: "PETLOVE", bgStyle: "linear-gradient(135deg, #f0fdf4, #bbf7d0, #f0fdf4)", sellerTarget: "all", active: true } ]; localStorage.setItem("sk_hero_banners", JSON.stringify(defaultBanners)); return defaultBanners; } function saveHeroBanners(banners) { localStorage.setItem("sk_hero_banners", JSON.stringify(banners)); if (typeof renderHeroSlider === "function") { renderHeroSlider(); } } function renderAdminBanners() { const tbody = document.getElementById("adminBannersTableBody"); if (!tbody) return; tbody.innerHTML = ""; const banners = getHeroBanners(); banners.forEach((b, idx) => { const tr = document.createElement("tr"); tr.innerHTML = ` <td><strong>#${idx + 1}</strong></td> <td><span class="badge badge-sky">${b.badge || 'Banner'}</span></td> <td> <strong>${b.title}</strong><br> <small class="text-muted">${b.subtitle || '-'}</small> </td> <td><code>${b.couponCode || 'ไม่มี'}</code></td> <td>${b.active ? '<span class="badge badge-success">เปิดใช้งาน</span>' : '<span class="badge badge-danger">ปิดการใช้งาน</span>'}</td> <td> <div style="display:flex; gap:5px;"> <button class="btn btn-sky-outline btn-xs edit-banner-btn" data-index="${idx}"> <i class="fa-solid fa-pen"></i> แก้ไข </button> <button class="btn btn-danger-outline btn-xs delete-banner-btn" data-index="${idx}"> <i class="fa-solid fa-trash"></i> ลบ </button> </div> </td> `; tr.querySelector(".edit-banner-btn").addEventListener("click", () => editAdminBanner(idx)); tr.querySelector(".delete-banner-btn").addEventListener("click", () => deleteAdminBanner(idx)); tbody.appendChild(tr); }); } function updateBannerBgGenerator(customStyleStr) { const previewBox = document.getElementById("bannerBgPreviewBox"); const bgInput = document.getElementById("bannerBgStyleInput"); const startColorPicker = document.getElementById("bannerStartColor"); const startColorText = document.getElementById("bannerStartColorText"); const endColorPicker = document.getElementById("bannerEndColor"); const endColorText = document.getElementById("bannerEndColorText"); const angleSelect = document.getElementById("bannerGradientAngle"); if (!previewBox || !bgInput) return; let finalGradient = ""; if (customStyleStr) { finalGradient = customStyleStr; bgInput.value = finalGradient; } else { const start = startColorPicker ? startColorPicker.value : "#e0f2fe"; const end = endColorPicker ? endColorPicker.value : "#bae6fd"; const angle = angleSelect ? angleSelect.value : "135deg"; if (angle === "radial") { finalGradient = `radial-gradient(circle, ${start}, ${end})`; } else { finalGradient = `linear-gradient(${angle}, ${start}, ${end})`; } bgInput.value = finalGradient; } previewBox.style.background = finalGradient; } function initBannerColorPickerEvents() { const startColorPicker = document.getElementById("bannerStartColor"); const startColorText = document.getElementById("bannerStartColorText"); const endColorPicker = document.getElementById("bannerEndColor"); const endColorText = document.getElementById("bannerEndColorText"); const angleSelect = document.getElementById("bannerGradientAngle"); const bgInput = document.getElementById("bannerBgStyleInput"); if (startColorPicker && !startColorPicker.dataset.bound) { startColorPicker.dataset.bound = "true"; startColorPicker.addEventListener("input", () => { if (startColorText) startColorText.value = startColorPicker.value; updateBannerBgGenerator(); }); } if (startColorText && !startColorText.dataset.bound) { startColorText.dataset.bound = "true"; startColorText.addEventListener("input", () => { if (/^#[0-9A-Fa-f]{6}$/.test(startColorText.value)) { if (startColorPicker) startColorPicker.value = startColorText.value; updateBannerBgGenerator(); } }); } if (endColorPicker && !endColorPicker.dataset.bound) { endColorPicker.dataset.bound = "true"; endColorPicker.addEventListener("input", () => { if (endColorText) endColorText.value = endColorPicker.value; updateBannerBgGenerator(); }); } if (endColorText && !endColorText.dataset.bound) { endColorText.dataset.bound = "true"; endColorText.addEventListener("input", () => { if (/^#[0-9A-Fa-f]{6}$/.test(endColorText.value)) { if (endColorPicker) endColorPicker.value = endColorText.value; updateBannerBgGenerator(); } }); } if (angleSelect && !angleSelect.dataset.bound) { angleSelect.dataset.bound = "true"; angleSelect.addEventListener("change", () => updateBannerBgGenerator()); } if (bgInput && !bgInput.dataset.bound) { bgInput.dataset.bound = "true"; bgInput.addEventListener("input", () => { const previewBox = document.getElementById("bannerBgPreviewBox"); if (previewBox) previewBox.style.background = bgInput.value; }); } document.querySelectorAll(".gradient-preset-btn").forEach(btn => { if (!btn.dataset.bound) { btn.dataset.bound = "true"; btn.addEventListener("click", (e) => { e.preventDefault(); const style = btn.getAttribute("data-style"); if (style) { const hexes = style.match(/#[0-9a-fA-F]{6}/g); if (hexes && hexes.length >= 2) { if (startColorPicker) startColorPicker.value = hexes[0]; if (startColorText) startColorText.value = hexes[0]; if (endColorPicker) endColorPicker.value = hexes[1]; if (endColorText) endColorText.value = hexes[1]; } updateBannerBgGenerator(style); } }); } }); } function initBannersFlow() { initBannerColorPickerEvents(); const openBtn = document.getElementById("openAddBannerModalBtn"); const modal = document.getElementById("addBannerModal"); const closeBtn = document.getElementById("closeAddBannerModalBtn"); const form = document.getElementById("addBannerForm"); if (openBtn && modal) { openBtn.onclick = () => { document.getElementById("bannerEditIndex").value = "-1"; document.getElementById("addBannerModalTitle").innerText = "เพิ่มแบนเนอร์สไลด์หน้าแรก"; form.reset(); document.getElementById("bannerActiveCheck").checked = true; const startColorPicker = document.getElementById("bannerStartColor"); const startColorText = document.getElementById("bannerStartColorText"); const endColorPicker = document.getElementById("bannerEndColor"); const endColorText = document.getElementById("bannerEndColorText"); if (startColorPicker) startColorPicker.value = "#e0f2fe"; if (startColorText) startColorText.value = "#e0f2fe"; if (endColorPicker) endColorPicker.value = "#bae6fd"; if (endColorText) endColorText.value = "#bae6fd"; updateBannerBgGenerator("linear-gradient(135deg, #e0f2fe, #bae6fd)"); modal.style.display = "flex"; }; } if (closeBtn && modal) { closeBtn.onclick = () => modal.style.display = "none"; } if (form) { form.onsubmit = (e) => { e.preventDefault(); const editIdx = parseInt(document.getElementById("bannerEditIndex").value); const banners = getHeroBanners(); const newBanner = { title: document.getElementById("bannerTitleInput").value.trim(), subtitle: document.getElementById("bannerSubtitleInput").value.trim(), badge: document.getElementById("bannerBadgeTextInput").value.trim(), couponCode: document.getElementById("bannerCouponCodeInput").value.trim(), bgStyle: document.getElementById("bannerBgStyleInput").value.trim() || "linear-gradient(135deg, #e0f2fe, #bae6fd)", sellerTarget: document.getElementById("bannerSellerTargetInput").value, active: document.getElementById("bannerActiveCheck").checked }; if (editIdx >= 0 && banners[editIdx]) { banners[editIdx] = newBanner; showToast("แก้ไขข้อมูลแบนเนอร์สำเร็จ!", "success"); } else { banners.push(newBanner); showToast("เพิ่มแบนเนอร์ใหม่สำเร็จ!", "success"); } saveHeroBanners(banners); modal.style.display = "none"; renderAdminBanners(); }; } } function editAdminBanner(idx) { initBannerColorPickerEvents(); const banners = getHeroBanners(); const b = banners[idx]; if (!b) return; document.getElementById("bannerEditIndex").value = idx.toString(); document.getElementById("addBannerModalTitle").innerText = `แก้ไขแบนเนอร์สไลด์ #${idx + 1}`; document.getElementById("bannerTitleInput").value = b.title || ""; document.getElementById("bannerSubtitleInput").value = b.subtitle || ""; document.getElementById("bannerBadgeTextInput").value = b.badge || ""; document.getElementById("bannerCouponCodeInput").value = b.couponCode || ""; document.getElementById("bannerBgStyleInput").value = b.bgStyle || ""; document.getElementById("bannerSellerTargetInput").value = b.sellerTarget || "all"; document.getElementById("bannerActiveCheck").checked = b.active !== false; const previewBox = document.getElementById("bannerBgPreviewBox"); if (previewBox) previewBox.style.background = b.bgStyle || "linear-gradient(135deg, #e0f2fe, #bae6fd)"; const hexes = (b.bgStyle || "").match(/#[0-9a-fA-F]{6}/g); if (hexes && hexes.length >= 2) { const startColorPicker = document.getElementById("bannerStartColor"); const startColorText = document.getElementById("bannerStartColorText"); const endColorPicker = document.getElementById("bannerEndColor"); const endColorText = document.getElementById("bannerEndColorText"); if (startColorPicker) startColorPicker.value = hexes[0]; if (startColorText) startColorText.value = hexes[0]; if (endColorPicker) endColorPicker.value = hexes[1]; if (endColorText) endColorText.value = hexes[1]; } const modal = document.getElementById("addBannerModal"); if (modal) modal.style.display = "flex"; } function deleteAdminBanner(idx) { const banners = getHeroBanners(); if (confirm("ยืนยันการลบแบนเนอร์สไลด์นี้ออกจากระบบ?")) { banners.splice(idx, 1); saveHeroBanners(banners); showToast("ลบแบนเนอร์สไลด์สำเร็จ!", "info"); renderAdminBanners(); } } // Broadcast Announcement Logic function renderAdminAnnouncements() { const saved = localStorage.getItem("sk_active_announcement"); if (!saved) return; try { const anc = JSON.parse(saved); const textInput = document.getElementById("announcementTextInput"); const typeSelect = document.getElementById("announcementTypeSelect"); const activeCheck = document.getElementById("announcementActiveCheck"); if (textInput) textInput.value = anc.text || ""; if (typeSelect) typeSelect.value = anc.type || "info"; if (activeCheck) activeCheck.checked = anc.active !== false; } catch (e) { } } function initAnnouncementFlow() { const form = document.getElementById("broadcastAnnouncementForm"); if (form) { form.onsubmit = (e) => { e.preventDefault(); const text = document.getElementById("announcementTextInput").value.trim(); const type = document.getElementById("announcementTypeSelect").value; const active = document.getElementById("announcementActiveCheck").checked; const announcement = { text, type, active }; localStorage.setItem("sk_active_announcement", JSON.stringify(announcement)); showToast("อัปเดตและประกาศข่าวสารขึ้นหน้าแรกสำเร็จ!", "success"); renderGlobalAnnouncementBar(); }; } } function renderGlobalAnnouncementBar() { const bar = document.getElementById("globalAnnouncementBar"); const textEl = document.getElementById("announcementText"); if (!bar || !textEl) return; const saved = localStorage.getItem("sk_active_announcement"); if (!saved) { bar.style.display = "none"; return; } try { const anc = JSON.parse(saved); if (anc.active && anc.text) { textEl.innerText = anc.text; bar.className = `global-announcement-bar announcement-${anc.type || 'info'}`; bar.style.display = "block"; } else { bar.style.display = "none"; } } catch (e) { bar.style.display = "none"; } } // -------------------------------------------------------------------------- // E. REPORTS & CSV EXPORT SYSTEM // -------------------------------------------------------------------------- function initCsvExportFlow() { const exportSalesBtn = document.getElementById("exportSalesCsvBtn"); const exportOrdersBtn = document.getElementById("exportOrdersCsvBtn"); if (exportSalesBtn) { exportSalesBtn.onclick = exportSalesCSV; } if (exportOrdersBtn) { exportOrdersBtn.onclick = exportOrdersCSV; } } function exportSalesCSV() { const orders = db.getOrders().filter(o => o.status === "Paid" || o.status === "Preparing" || o.status === "Shipped" || o.status === "Success"); let csvContent = "\uFEFF"; csvContent += "OrderID,Date,CustomerName,TotalAmount,Discount,PaymentMethod,Status\n"; orders.forEach(o => { const cleanName = `"${(o.customerName || '').replace(/"/g, '""')}"`; csvContent += `${o.id},${o.date},${cleanName},${o.total},${o.discount || 0},${o.paymentMethod || 'PromptPay'},${o.status}\n`; }); downloadCsvFile(csvContent, `skymall_sales_report_${new Date().toISOString().slice(0, 10)}.csv`); showToast("ส่งออกรายงานยอดขาย (Sales CSV) สำเร็จ!", "success"); } function exportOrdersCSV() { const orders = db.getOrders(); let csvContent = "\uFEFF"; csvContent += "OrderID,Date,CustomerName,Phone,Address,ItemsCount,TotalAmount,Status\n"; orders.forEach(o => { const cleanName = `"${(o.customerName || '').replace(/"/g, '""')}"`; const phone = o.shippingInfo ? o.shippingInfo.phone : ''; const address = o.shippingInfo ? `"${(o.shippingInfo.address || '').replace(/"/g, '""')}"` : ''; const itemsCount = o.items ? o.items.reduce((sum, i) => sum + i.quantity, 0) : 0; csvContent += `${o.id},${o.date},${cleanName},${phone},${address},${itemsCount},${o.total},${o.status}\n`; }); downloadCsvFile(csvContent, `skymall_orders_report_${new Date().toISOString().slice(0, 10)}.csv`); showToast("ส่งออกรายงานคำสั่งซื้อ (Orders CSV) สำเร็จ!", "success"); } function downloadCsvFile(content, fileName) { const blob = new Blob([content], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.setAttribute("href", url); link.setAttribute("download", fileName); document.body.appendChild(link); link.click(); document.body.removeChild(link); } // -------------------------------------------------------------------------- // F. WEB SALES & ORDERS AUDIT REPORT PAGES // -------------------------------------------------------------------------- function renderAdminSalesReport() { const orders = db.getOrders(); const products = db.getProducts(); const approvedStatuses = ["Paid", "Preparing", "Shipped", "Success"]; const approvedOrders = orders.filter(o => approvedStatuses.includes(o.status)); const totalRev = approvedOrders.reduce((sum, o) => sum + o.total, 0); const totalItemsCount = approvedOrders.reduce((sum, o) => sum + (o.items ? o.items.reduce((iSum, i) => iSum + i.quantity, 0) : 0), 0); const kpiRev = document.getElementById("salesReportTotalRev"); if (kpiRev) kpiRev.innerText = `฿${totalRev.toLocaleString()}`; const kpiItems = document.getElementById("salesReportTotalItems"); if (kpiItems) kpiItems.innerText = `${totalItemsCount.toLocaleString()} ชิ้น`; const kpiOrders = document.getElementById("salesReportTotalPaidOrders"); if (kpiOrders) kpiOrders.innerText = `${approvedOrders.length} ออเดอร์`; const catStats = {}; products.forEach(p => { const cat = p.category || "อื่นๆ"; if (!catStats[cat]) { catStats[cat] = { count: 0, itemsSold: 0, revenue: 0 }; } catStats[cat].count++; }); approvedOrders.forEach(o => { if (o.items) { o.items.forEach(item => { const prod = products.find(p => p.id === item.productId || p.name === item.name); const cat = prod ? prod.category : (item.category || "อื่นๆ"); if (!catStats[cat]) { catStats[cat] = { count: 1, itemsSold: 0, revenue: 0 }; } catStats[cat].itemsSold += item.quantity; catStats[cat].revenue += (item.price * item.quantity); }); } }); const catTbody = document.getElementById("salesReportCategoryTableBody"); if (catTbody) { catTbody.innerHTML = ""; const catKeys = Object.keys(catStats); if (catKeys.length === 0) { catTbody.innerHTML = `<tr><td colspan="5" class="text-center py-3 text-muted">ยังไม่มีข้อมูลหมวดหมู่สินค้า</td></tr>`; } else { catKeys.forEach(catName => { const stat = catStats[catName]; const pct = totalRev > 0 ? ((stat.revenue / totalRev) * 100).toFixed(1) : "0.0"; const tr = document.createElement("tr"); tr.innerHTML = ` <td><strong>${catName}</strong></td> <td>${stat.count} รายการ</td> <td>${stat.itemsSold.toLocaleString()} ชิ้น</td> <td><strong class="text-sky">฿${stat.revenue.toLocaleString()}</strong></td> <td> <div class="d-flex align-items-center gap-2"> <span>${pct}%</span> <div style="flex-grow:1; background:var(--color-bg-gray); height:6px; border-radius:3px; overflow:hidden;"> <div style="width:${pct}%; background:var(--color-sky-brand); height:100%;"></div> </div> </div> </td> `; catTbody.appendChild(tr); }); } } const searchQuery = (document.getElementById("salesReportSearchInput")?.value || "").toLowerCase(); const searchInput = document.getElementById("salesReportSearchInput"); if (searchInput && !searchInput.dataset.bound) { searchInput.dataset.bound = "true"; searchInput.addEventListener("input", renderAdminSalesReport); } const transTbody = document.getElementById("salesReportTransactionsTableBody"); if (transTbody) { transTbody.innerHTML = ""; const filteredTrans = orders.filter(o => { return !searchQuery || o.id.toLowerCase().includes(searchQuery) || (o.customerName && o.customerName.toLowerCase().includes(searchQuery)); }); if (filteredTrans.length === 0) { transTbody.innerHTML = `<tr><td colspan="7" class="text-center py-4 text-muted">ไม่พบรายการสั่งซื้อตรงตามเงื่อนไขค้นหา</td></tr>`; } else { filteredTrans.forEach(o => { const tr = document.createElement("tr"); let statusBadge = `<span class="badge badge-dark">${o.status}</span>`; if (o.status === "Paid") statusBadge = `<span class="badge badge-success">ชำระเงินแล้ว</span>`; if (o.status === "Preparing") statusBadge = `<span class="badge badge-sky">เตรียมส่ง</span>`; if (o.status === "Shipped") statusBadge = `<span class="badge badge-success">จัดส่งแล้ว</span>`; if (o.status === "Success") statusBadge = `<span class="badge badge-success" style="background:#10b981; color:#fff !important;">สำเร็จ</span>`; if (o.status === "Cancelled" || o.status === "Failed") statusBadge = `<span class="badge badge-cancelled">ยกเลิก</span>`; tr.innerHTML = ` <td><strong>${o.id}</strong></td> <td><span style="font-size:12px; color:var(--color-sky-slate);">${o.date}</span></td> <td><strong>${o.customerName}</strong></td> <td>${o.paymentMethod || 'PromptPay'}</td> <td>${o.discount > 0 ? `<small class="text-success">ลด ฿${o.discount.toLocaleString()}</small>` : '-'}</td> <td><strong class="text-sky">฿${o.total.toLocaleString()}</strong></td> <td>${statusBadge}</td> `; transTbody.appendChild(tr); }); } } } function renderAdminOrdersReport() { const tbody = document.getElementById("ordersReportTableBody"); if (!tbody) return; tbody.innerHTML = ""; const orders = db.getOrders(); const searchQuery = (document.getElementById("ordersReportSearchInput")?.value || "").toLowerCase(); const statusFilter = document.getElementById("ordersReportFilterStatus")?.value || "all"; const searchInput = document.getElementById("ordersReportSearchInput"); if (searchInput && !searchInput.dataset.bound) { searchInput.dataset.bound = "true"; searchInput.addEventListener("input", renderAdminOrdersReport); } const statusSelect = document.getElementById("ordersReportFilterStatus"); if (statusSelect && !statusSelect.dataset.bound) { statusSelect.dataset.bound = "true"; statusSelect.addEventListener("change", renderAdminOrdersReport); } const filtered = orders.filter(o => { const phone = o.shippingInfo ? o.shippingInfo.phone : ''; const matchSearch = !searchQuery || o.id.toLowerCase().includes(searchQuery) || (o.customerName && o.customerName.toLowerCase().includes(searchQuery)) || phone.includes(searchQuery); const matchStatus = statusFilter === "all" || o.status === statusFilter || (statusFilter === "Cancelled" && o.status === "Failed"); return matchSearch && matchStatus; }); if (filtered.length === 0) { tbody.innerHTML = `<tr><td colspan="7" class="text-center py-4 text-muted">ไม่พบข้อมูลคำสั่งซื้อตรงตามเงื่อนไขค้นหา</td></tr>`; return; } filtered.forEach(o => { const tr = document.createElement("tr"); const phone = o.shippingInfo ? o.shippingInfo.phone : '-'; const address = o.shippingInfo ? o.shippingInfo.address : '-'; const itemsHtml = o.items ? o.items.map(i => `- ${i.name} (x${i.quantity})`).join('<br>') : '-'; let trackingInfo = '-'; if (o.trackingNumbers && Object.keys(o.trackingNumbers).length > 0) { trackingInfo = Object.entries(o.trackingNumbers).map(([seller, track]) => `<code>${track}</code>`).join(', '); } let statusBadge = `<span class="badge badge-dark">${o.status}</span>`; if (o.status === "Unpaid") statusBadge = `<span class="badge badge-dark">รอชำระเงิน</span>`; if (o.status === "Pending") statusBadge = `<span class="badge badge-warning">รอตรวจสอบ</span>`; if (o.status === "Paid") statusBadge = `<span class="badge badge-success">ชำระเงินแล้ว</span>`; if (o.status === "Preparing") statusBadge = `<span class="badge badge-sky">เตรียมส่ง</span>`; if (o.status === "Shipped") statusBadge = `<span class="badge badge-success">จัดส่งแล้ว</span>`; if (o.status === "Success") statusBadge = `<span class="badge badge-success" style="background:#10b981; color:#fff !important;">สำเร็จ</span>`; if (o.status === "Cancelled" || o.status === "Failed") statusBadge = `<span class="badge badge-cancelled">ยกเลิก</span>`; tr.innerHTML = ` <td> <strong>${o.id}</strong><br> <span style="font-size:11px; color:var(--color-sky-slate);">${o.date}</span> </td> <td> <strong>${o.customerName}</strong> (${phone})<br> <small style="color:var(--color-sky-slate); max-width:200px; display:inline-block;">${address}</small> </td> <td><div style="font-size:12px; max-width:220px; white-space:normal;">${itemsHtml}</div></td> <td><strong class="text-sky">฿${o.total.toLocaleString()}</strong></td> <td><span style="font-size:12px;">${o.paymentMethod || 'PromptPay'}</span></td> <td><span style="font-size:12px;">${trackingInfo}</span></td> <td>${statusBadge}</td> `; tbody.appendChild(tr); }); }
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Generation time: 0.24 |
proxy
|
phpinfo
|
Settings