/** * Admin Dashboard JavaScript * Handles all admin panel functionality */ // Global state let currentUser = null; let currentSection = 'dashboard'; let dashboardData = {}; let ordersData = []; let productsData = []; let customersData = []; let inventoryData = []; // Dynamic path detection const BASE_PATH = (() => { const path = window.location.pathname.replace(/\/$/, ''); // Remove trailing slash const pathParts = path.split('/').filter(part => part !== ''); // Remove empty parts console.log('Current path:', path); console.log('Path parts:', pathParts); // Find admin folder index const adminIndex = pathParts.indexOf('admin'); console.log('Admin index:', adminIndex); if (adminIndex > 0) { // Return base path up to admin folder (excluding admin) const basePath = '/' + pathParts.slice(0, adminIndex).join('/'); console.log('Detected BASE_PATH:', basePath); return basePath; } // Default to root if admin not found in path console.log('Admin not found in path, using empty BASE_PATH'); return ''; })(); const ADMIN_BASE_PATH = BASE_PATH + '/admin'; // API Base URLs const API_BASE = BASE_PATH + '/api/admin'; const AUTH_API = BASE_PATH + '/api'; // Initialize dashboard document.addEventListener('DOMContentLoaded', function() { checkAuth(); setupEventListeners(); loadDashboardData(); }); // Authentication check async function checkAuth() { const token = localStorage.getItem('adminToken'); if (!token) { window.location.href = ADMIN_BASE_PATH + '/login.html'; return; } try { const response = await fetch(`${AUTH_API}/auth/me`, { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) { throw new Error('Authentication failed'); } const data = await response.json(); if (data.success && data.data.user.role === 'admin') { currentUser = data.data.user; document.getElementById('userName').textContent = currentUser.first_name || 'Admin'; } else { throw new Error('Not authorized'); } } catch (error) { console.error('Auth check failed:', error); localStorage.removeItem('adminToken'); window.location.href = ADMIN_BASE_PATH + '/login.html'; } } // Setup event listeners function setupEventListeners() { // Navigation document.querySelectorAll('.nav-link').forEach(link => { link.addEventListener('click', function(e) { e.preventDefault(); const section = this.dataset.section; showSection(section); }); }); // Sidebar toggle for mobile const sidebarToggle = document.getElementById('sidebarToggle'); if (sidebarToggle) { sidebarToggle.addEventListener('click', function() { document.getElementById('sidebar').classList.toggle('mobile-open'); }); } // Order status filter const orderStatus = document.getElementById('orderStatus'); if (orderStatus) { orderStatus.addEventListener('change', function() { loadOrders(); }); } // Customer search const customerSearch = document.getElementById('customerSearch'); if (customerSearch) { let timeout; customerSearch.addEventListener('input', function() { clearTimeout(timeout); timeout = setTimeout(() => { loadCustomers(); }, 500); }); } // Modal event listeners setupModalEventListeners(); } // Setup modal event listeners function setupModalEventListeners() { // Close modal when clicking outside const editProductModal = document.getElementById('editProductModal'); if (editProductModal) { editProductModal.addEventListener('click', function(e) { if (e.target === this) { closeEditProductModal(); } }); } // Close modal with Escape key document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { const modal = document.getElementById('editProductModal'); if (modal && modal.classList.contains('show')) { closeEditProductModal(); } } }); // Form validation on input const editProductForm = document.getElementById('editProductForm'); if (editProductForm) { // Real-time SKU validation const skuInput = document.getElementById('editProductSku'); if (skuInput) { skuInput.addEventListener('blur', function() { validateSKU(this.value, document.getElementById('editProductId').value); }); } // Price validation const priceInputs = ['editProductPrice', 'editProductSalePrice']; priceInputs.forEach(inputId => { const input = document.getElementById(inputId); if (input) { input.addEventListener('input', function() { if (this.value < 0) { this.value = 0; } }); } }); // Stock validation const stockInputs = ['editProductStock', 'editProductMinStock']; stockInputs.forEach(inputId => { const input = document.getElementById(inputId); if (input) { input.addEventListener('input', function() { if (this.value < 0) { this.value = 0; } }); } }); // Image URL preview const imageInput = document.getElementById('editProductImage'); if (imageInput) { imageInput.addEventListener('blur', function() { previewProductImage(this.value); }); } // Tag input formatting const tagsInput = document.getElementById('editProductTags'); if (tagsInput) { tagsInput.addEventListener('blur', function() { formatTags(this); }); } } } // Preview product image function previewProductImage(imageUrl) { if (!imageUrl || imageUrl.trim() === '') { return; } // Create or update image preview let previewContainer = document.getElementById('imagePreview'); if (!previewContainer) { previewContainer = document.createElement('div'); previewContainer.id = 'imagePreview'; previewContainer.style.marginTop = '0.5rem'; const imageInput = document.getElementById('editProductImage'); imageInput.parentNode.appendChild(previewContainer); } // Create image element const img = document.createElement('img'); img.style.maxWidth = '150px'; img.style.maxHeight = '150px'; img.style.borderRadius = '0.375rem'; img.style.border = '1px solid var(--border-color)'; img.style.objectFit = 'cover'; img.onload = function() { previewContainer.innerHTML = ''; previewContainer.appendChild(img); }; img.onerror = function() { previewContainer.innerHTML = 'Invalid image URL'; }; img.src = imageUrl.trim(); } // Format tags input function formatTags(input) { const tags = input.value .split(',') .map(tag => tag.trim()) .filter(tag => tag !== '') .join(', '); input.value = tags; } // Show specific section function showSection(sectionName) { // Update navigation document.querySelectorAll('.nav-link').forEach(link => { link.classList.remove('active'); }); document.querySelector(`[data-section="${sectionName}"]`).classList.add('active'); // Update page content document.querySelectorAll('.page-section').forEach(section => { section.classList.remove('active'); }); document.getElementById(`${sectionName}-section`).classList.add('active'); // Update page title const titles = { 'dashboard': 'Dashboard', 'orders': 'Orders', 'products': 'Products', 'inventory': 'Inventory', 'customers': 'Customers', 'analytics': 'Analytics', 'settings': 'Settings' }; document.getElementById('pageTitle').textContent = titles[sectionName] || sectionName; currentSection = sectionName; // Load section-specific data switch (sectionName) { case 'dashboard': loadDashboardData(); break; case 'orders': loadOrders(); break; case 'products': loadProducts(); break; case 'inventory': loadInventory(); break; case 'customers': loadCustomers(); break; } } // API request helper async function apiRequest(endpoint, options = {}) { const token = localStorage.getItem('adminToken'); const config = { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, ...options }; try { const response = await fetch(`${API_BASE}${endpoint}`, config); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Request failed'); } return data; } catch (error) { console.error('API Request failed:', error); showAlert('Error: ' + error.message, 'danger'); throw error; } } // Load dashboard data async function loadDashboardData() { try { // Load dashboard stats const response = await apiRequest('/dashboard'); dashboardData = response.data; updateDashboardStats(dashboardData); loadDashboardCharts(); loadRecentOrders(); } catch (error) { console.error('Failed to load dashboard:', error); } } // Update dashboard statistics function updateDashboardStats(data) { if (data.sales) { document.getElementById('totalRevenue').textContent = `₿${formatNumber(data.sales.total_revenue)}`; document.getElementById('revenueChange').textContent = `${data.sales.revenue_change >= 0 ? '+' : ''}${data.sales.revenue_change}%`; document.getElementById('revenueChange').className = `stat-change ${data.sales.revenue_change >= 0 ? 'positive' : 'negative'}`; } if (data.orders) { document.getElementById('totalOrders').textContent = formatNumber(data.orders.total_orders); document.getElementById('ordersChange').textContent = `${data.orders.orders_change >= 0 ? '+' : ''}${data.orders.orders_change}%`; document.getElementById('ordersChange').className = `stat-change ${data.orders.orders_change >= 0 ? 'positive' : 'negative'}`; } if (data.products) { document.getElementById('totalProducts').textContent = formatNumber(data.products.total_products); document.getElementById('productsChange').textContent = `${data.products.products_change >= 0 ? '+' : ''}${data.products.products_change}%`; document.getElementById('productsChange').className = `stat-change ${data.products.products_change >= 0 ? 'positive' : 'negative'}`; } if (data.customers) { document.getElementById('totalCustomers').textContent = formatNumber(data.customers.total_customers); document.getElementById('customersChange').textContent = `${data.customers.customers_change >= 0 ? '+' : ''}${data.customers.customers_change}%`; document.getElementById('customersChange').className = `stat-change ${data.customers.customers_change >= 0 ? 'positive' : 'negative'}`; } } // Load dashboard charts function loadDashboardCharts() { // Sales trend chart if (dashboardData.sales && dashboardData.sales.trend) { const salesCtx = document.getElementById('salesChart'); // Set canvas height salesCtx.style.height = '300px'; salesCtx.style.maxHeight = '300px'; new Chart(salesCtx.getContext('2d'), { type: 'line', data: { labels: dashboardData.sales.trend.labels || [], datasets: [{ label: 'ยอดขาย (Sales)', data: dashboardData.sales.trend.data || [], borderColor: '#2563eb', backgroundColor: 'rgba(37, 99, 235, 0.1)', tension: 0.4, borderWidth: 2, pointBackgroundColor: '#2563eb', pointBorderColor: '#ffffff', pointBorderWidth: 2, pointRadius: 4, pointHoverRadius: 6, fill: true }] }, options: { responsive: true, maintainAspectRatio: false, layout: { padding: { top: 20, right: 20, bottom: 20, left: 20 } }, plugins: { legend: { display: true, position: 'top', labels: { font: { family: 'Inter, Noto Sans Thai, sans-serif', size: 12 }, usePointStyle: true, padding: 20 } } }, scales: { x: { grid: { display: true, color: 'rgba(0, 0, 0, 0.05)' }, ticks: { font: { family: 'Inter, Noto Sans Thai, sans-serif', size: 11 } } }, y: { beginAtZero: true, grid: { display: true, color: 'rgba(0, 0, 0, 0.05)' }, ticks: { font: { family: 'Inter, Noto Sans Thai, sans-serif', size: 11 }, callback: function(value) { return '₿' + formatNumber(value); } } } }, interaction: { intersect: false, mode: 'index' } } }); } // Order status chart if (dashboardData.orders && dashboardData.orders.by_status) { const statusCtx = document.getElementById('statusChart'); const statusData = dashboardData.orders.by_status; // Set canvas height statusCtx.style.height = '300px'; statusCtx.style.maxHeight = '300px'; new Chart(statusCtx.getContext('2d'), { type: 'doughnut', data: { labels: Object.keys(statusData).map(status => { const statusMap = { 'pending': 'รอดำเนินการ', 'confirmed': 'ยืนยันแล้ว', 'processing': 'กำลังจัดเตรียม', 'shipped': 'จัดส่งแล้ว', 'delivered': 'ส่งมอบแล้ว', 'cancelled': 'ยกเลิก' }; return statusMap[status] || status; }), datasets: [{ data: Object.keys(statusData).map(status => statusData[status].count), backgroundColor: [ '#2563eb', '#16a34a', '#d97706', '#dc2626', '#64748b', '#8b5cf6' ], borderWidth: 0, hoverBorderWidth: 2, hoverBorderColor: '#ffffff' }] }, options: { responsive: true, maintainAspectRatio: false, layout: { padding: { top: 20, right: 20, bottom: 20, left: 20 } }, plugins: { legend: { display: true, position: 'bottom', labels: { font: { family: 'Inter, Noto Sans Thai, sans-serif', size: 12 }, usePointStyle: true, padding: 15, generateLabels: function(chart) { const data = chart.data; if (data.labels.length && data.datasets.length) { return data.labels.map((label, i) => { const dataset = data.datasets[0]; const value = dataset.data[i]; return { text: `${label} (${value})`, fillStyle: dataset.backgroundColor[i], strokeStyle: dataset.backgroundColor[i], lineWidth: 0, pointStyle: 'circle' }; }); } return []; } } }, tooltip: { titleFont: { family: 'Inter, Noto Sans Thai, sans-serif' }, bodyFont: { family: 'Inter, Noto Sans Thai, sans-serif' }, callbacks: { label: function(context) { const total = context.dataset.data.reduce((a, b) => a + b, 0); const percentage = ((context.parsed / total) * 100).toFixed(1); return `${context.label}: ${context.parsed} คำสั่งซื้อ (${percentage}%)`; } } } }, cutout: '60%' } }); } } // Load recent orders async function loadRecentOrders() { try { const response = await apiRequest('/orders?limit=5'); const orders = response.data.orders || []; renderOrdersTable(orders, 'recentOrdersTable', true); } catch (error) { console.error('Failed to load recent orders:', error); document.getElementById('recentOrdersTable').innerHTML = 'Failed to load orders'; } } // Load all orders async function loadOrders() { try { const status = document.getElementById('orderStatus')?.value || ''; const queryParams = status ? `?status=${status}` : ''; const response = await apiRequest(`/orders${queryParams}`); ordersData = response.data.orders || []; renderOrdersTable(ordersData, 'ordersTable'); } catch (error) { console.error('Failed to load orders:', error); document.getElementById('ordersTable').innerHTML = 'Failed to load orders'; } } // Render orders table function renderOrdersTable(orders, tableId, isRecent = false) { const tbody = document.getElementById(tableId); if (!orders || orders.length === 0) { tbody.innerHTML = `No orders found`; return; } tbody.innerHTML = orders.map(order => { const statusClass = getStatusClass(order.status); const paymentClass = getStatusClass(order.payment_status); return ` ${order.order_number} ${order.first_name || ''} ${order.last_name || ''} ${!isRecent ? `${order.email}` : ''} ${order.status} ${!isRecent ? `${order.payment_status}` : ''} ₿${formatNumber(order.total_amount)} ${formatDate(order.created_at)} `; }).join(''); } // Load products async function loadProducts() { try { const response = await apiRequest('/products'); productsData = response.data.products || []; renderProductsTable(productsData); } catch (error) { console.error('Failed to load products:', error); document.getElementById('productsTable').innerHTML = 'Failed to load products'; } } // Render products table function renderProductsTable(products) { const tbody = document.getElementById('productsTable'); if (!products || products.length === 0) { tbody.innerHTML = 'No products found'; return; } tbody.innerHTML = products.map(product => { const statusClass = product.status === 'active' ? 'success' : 'secondary'; const stockClass = product.inventory_quantity <= product.min_stock_level ? 'danger' : 'success'; return ` ${product.name} ${product.name} ${product.sku} ${product.category_name || 'Uncategorized'} ₿${formatNumber(product.base_price)} ${product.inventory_quantity || 0} ${product.status} `; }).join(''); } // Load inventory async function loadInventory() { try { const response = await apiRequest('/inventory'); inventoryData = response.data.inventory || []; // Update inventory stats if (response.data.stats) { document.getElementById('lowStockCount').textContent = response.data.stats.low_stock_count || 0; document.getElementById('outOfStockCount').textContent = response.data.stats.out_of_stock_count || 0; document.getElementById('inventoryValue').textContent = `₿${formatNumber(response.data.stats.inventory_value || 0)}`; } renderInventoryTable(inventoryData); } catch (error) { console.error('Failed to load inventory:', error); document.getElementById('inventoryTable').innerHTML = 'Failed to load inventory'; } } // Render inventory table function renderInventoryTable(inventory) { const tbody = document.getElementById('inventoryTable'); if (!inventory || inventory.length === 0) { tbody.innerHTML = 'No inventory data found'; return; } tbody.innerHTML = inventory.map(item => { let statusClass = 'success'; let statusText = 'In Stock'; if (item.current_stock === 0) { statusClass = 'danger'; statusText = 'Out of Stock'; } else if (item.current_stock <= item.min_stock_level) { statusClass = 'warning'; statusText = 'Low Stock'; } return ` ${item.name} ${item.sku} ${item.current_stock} ${item.min_stock_level} ${statusText} `; }).join(''); } // Load customers async function loadCustomers() { try { const search = document.getElementById('customerSearch')?.value || ''; const queryParams = search ? `?search=${encodeURIComponent(search)}` : ''; const response = await apiRequest(`/customers${queryParams}`); customersData = response.data.users || []; renderCustomersTable(customersData); } catch (error) { console.error('Failed to load customers:', error); document.getElementById('customersTable').innerHTML = 'Failed to load customers'; } } // Render customers table function renderCustomersTable(customers) { const tbody = document.getElementById('customersTable'); if (!customers || customers.length === 0) { tbody.innerHTML = 'No customers found'; return; } tbody.innerHTML = customers.map(customer => { const statusClass = customer.is_active ? 'success' : 'secondary'; return ` ${customer.first_name || ''} ${customer.last_name || ''} ${customer.email} ${customer.phone || 'N/A'} ${customer.total_orders || 0} ₿${formatNumber(customer.total_spent || 0)} ${customer.is_active ? 'Active' : 'Inactive'} ${formatDate(customer.created_at)} `; }).join(''); } // Utility functions function formatNumber(number) { return new Intl.NumberFormat('th-TH', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number || 0); } function formatDate(dateString) { const date = new Date(dateString); return date.toLocaleDateString('th-TH', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } function getStatusClass(status) { const statusClasses = { 'pending': 'warning', 'confirmed': 'info', 'processing': 'info', 'shipped': 'success', 'delivered': 'success', 'cancelled': 'danger', 'refunded': 'secondary', 'paid': 'success', 'failed': 'danger', 'partially_refunded': 'warning' }; return statusClasses[status] || 'secondary'; } function showAlert(message, type = 'info') { const alertDiv = document.createElement('div'); alertDiv.className = `alert alert-${type}`; alertDiv.textContent = message; document.querySelector('.content-area').insertBefore( alertDiv, document.querySelector('.content-area').firstChild ); setTimeout(() => { alertDiv.remove(); }, 5000); } // Action functions function viewOrder(orderId) { // TODO: Implement order detail modal console.log('View order:', orderId); showAlert('Order detail view coming soon', 'info'); } function updateOrderStatus(orderId) { // TODO: Implement order status update modal console.log('Update order status:', orderId); showAlert('Order status update coming soon', 'info'); } function editProduct(productId) { console.log('Edit product:', productId); // Find the product in the loaded data const product = productsData.find(p => p.id === productId); if (!product) { showAlert('Product not found', 'danger'); return; } // Populate the form with product data populateEditProductForm(product); // Load categories for dropdown loadCategoriesForEdit(); // Show the modal showEditProductModal(); } // Show edit product modal function showEditProductModal() { const modal = document.getElementById('editProductModal'); modal.classList.add('show'); modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; // Prevent background scroll } // Close edit product modal function closeEditProductModal() { const modal = document.getElementById('editProductModal'); modal.classList.remove('show'); modal.style.display = 'none'; document.body.style.overflow = 'auto'; // Restore background scroll // Clear form const form = document.getElementById('editProductForm'); form.reset(); form.classList.remove('form-loading'); // Clear custom validation messages const inputs = form.querySelectorAll('input, select, textarea'); inputs.forEach(input => { input.setCustomValidity(''); }); // Remove image preview const imagePreview = document.getElementById('imagePreview'); if (imagePreview) { imagePreview.remove(); } // Reset button state const updateBtn = document.querySelector('#editProductModal .btn-primary'); updateBtn.innerHTML = ' Update Product'; updateBtn.disabled = false; } // Populate edit form with product data function populateEditProductForm(product) { document.getElementById('editProductId').value = product.id; document.getElementById('editProductName').value = product.name || ''; document.getElementById('editProductSku').value = product.sku || ''; document.getElementById('editProductCategory').value = product.category_id || ''; document.getElementById('editProductStatus').value = product.status || 'active'; document.getElementById('editProductDescription').value = product.description || ''; document.getElementById('editProductPrice').value = product.base_price || ''; document.getElementById('editProductSalePrice').value = product.sale_price || ''; document.getElementById('editProductStock').value = product.inventory_quantity || ''; document.getElementById('editProductMinStock').value = product.min_stock_level || '5'; document.getElementById('editProductImage').value = product.primary_image || ''; document.getElementById('editProductWeight').value = product.weight || ''; document.getElementById('editProductTags').value = product.tags ? product.tags.join(', ') : ''; // Clear any previous image preview const existingPreview = document.getElementById('imagePreview'); if (existingPreview) { existingPreview.remove(); } // Show image preview if image URL exists if (product.primary_image) { previewProductImage(product.primary_image); } } // Load categories for edit dropdown async function loadCategoriesForEdit() { try { const response = await apiRequest('/categories'); const categories = response.data.categories || []; const categorySelect = document.getElementById('editProductCategory'); categorySelect.innerHTML = ''; categories.forEach(category => { const option = document.createElement('option'); option.value = category.id; option.textContent = category.name; categorySelect.appendChild(option); }); } catch (error) { console.error('Failed to load categories:', error); showAlert('Failed to load categories', 'warning'); } } // Update product async function updateProduct() { const form = document.getElementById('editProductForm'); // Basic form validation if (!form.checkValidity()) { form.reportValidity(); return; } // Collect form data const productData = { id: document.getElementById('editProductId').value, name: document.getElementById('editProductName').value.trim(), sku: document.getElementById('editProductSku').value.trim(), category_id: document.getElementById('editProductCategory').value || null, status: document.getElementById('editProductStatus').value, description: document.getElementById('editProductDescription').value.trim(), base_price: parseFloat(document.getElementById('editProductPrice').value), sale_price: document.getElementById('editProductSalePrice').value ? parseFloat(document.getElementById('editProductSalePrice').value) : null, inventory_quantity: parseInt(document.getElementById('editProductStock').value), min_stock_level: parseInt(document.getElementById('editProductMinStock').value) || 5, primary_image: document.getElementById('editProductImage').value.trim() || null, weight: document.getElementById('editProductWeight').value ? parseFloat(document.getElementById('editProductWeight').value) : null, tags: document.getElementById('editProductTags').value .split(',') .map(tag => tag.trim()) .filter(tag => tag !== '') }; // Enhanced validation const validationErrors = validateProductForm(productData); if (validationErrors.length > 0) { showAlert('Validation errors: ' + validationErrors.join(', '), 'danger'); return; } // Validate SKU uniqueness const isSkuValid = await validateSKU(productData.sku, productData.id); if (!isSkuValid) { return; } // Show loading state const updateBtn = document.querySelector('#editProductModal .btn-primary'); const originalText = updateBtn.innerHTML; updateBtn.innerHTML = ' Updating...'; updateBtn.disabled = true; // Disable form to prevent multiple submissions form.classList.add('form-loading'); try { // Update product via API const response = await apiRequest(`/products/${productData.id}`, { method: 'PUT', body: JSON.stringify(productData) }); if (response.success) { showAlert('Product updated successfully!', 'success'); // Close modal closeEditProductModal(); // Reload products table await loadProducts(); // Update products data array const productIndex = productsData.findIndex(p => p.id === productData.id); if (productIndex !== -1) { productsData[productIndex] = {...productsData[productIndex], ...response.data.product}; } // Scroll to updated product if possible scrollToProduct(productData.id); } else { throw new Error(response.message || 'Failed to update product'); } } catch (error) { console.error('Failed to update product:', error); showAlert('Failed to update product: ' + error.message, 'danger'); } finally { // Restore button and form state updateBtn.innerHTML = originalText; updateBtn.disabled = false; form.classList.remove('form-loading'); } } // Scroll to product in table after update function scrollToProduct(productId) { setTimeout(() => { const productRow = document.querySelector(`button[onclick="editProduct('${productId}')"]`)?.closest('tr'); if (productRow) { productRow.scrollIntoView({behavior: 'smooth', block: 'center'}); productRow.style.backgroundColor = '#fef3c7'; setTimeout(() => { productRow.style.backgroundColor = ''; }, 2000); } }, 100); } function deleteProduct(productId) { if (confirm('Are you sure you want to delete this product?')) { // TODO: Implement product deletion console.log('Delete product:', productId); showAlert('Product deletion coming soon', 'info'); } } function adjustStock(productId) { // TODO: Implement stock adjustment modal console.log('Adjust stock for product:', productId); showAlert('Stock adjustment coming soon', 'info'); } function viewCustomer(customerId) { // TODO: Implement customer detail modal console.log('View customer:', customerId); showAlert('Customer detail view coming soon', 'info'); } function logout() { if (confirm('Are you sure you want to logout?')) { localStorage.removeItem('adminToken'); window.location.href = ADMIN_BASE_PATH + '/login.html'; } } // Validate SKU uniqueness async function validateSKU(sku, excludeProductId = null) { if (!sku || sku.trim() === '') { return true; } try { const response = await apiRequest(`/products/validate-sku?sku=${encodeURIComponent(sku.trim())}&exclude=${excludeProductId || ''}`); const skuInput = document.getElementById('editProductSku'); if (response.data && response.data.exists) { skuInput.setCustomValidity('SKU already exists'); showAlert('SKU already exists, please choose a different one', 'warning'); return false; } else { skuInput.setCustomValidity(''); return true; } } catch (error) { console.warn('SKU validation failed:', error); return true; // Allow submission if validation fails } } // Enhanced form validation function validateProductForm(formData) { const errors = []; // Name validation if (!formData.name || formData.name.length < 2) { errors.push('Product name must be at least 2 characters long'); } // SKU validation if (!formData.sku || formData.sku.length < 2) { errors.push('SKU must be at least 2 characters long'); } // Price validation if (!formData.base_price || formData.base_price <= 0) { errors.push('Base price must be greater than 0'); } // Sale price validation if (formData.sale_price && formData.sale_price >= formData.base_price) { errors.push('Sale price must be less than base price'); } // Stock validation if (formData.inventory_quantity < 0) { errors.push('Stock quantity cannot be negative'); } // URL validation for image if (formData.primary_image) { try { new URL(formData.primary_image); } catch { errors.push('Primary image must be a valid URL'); } } return errors; } // ==================== Settings Management ==================== // Show Settings Tab function showSettingsTab(tabName) { // Hide all tabs const tabs = document.querySelectorAll('.settings-tab'); tabs.forEach(tab => tab.classList.remove('active')); // Hide all tab buttons const tabBtns = document.querySelectorAll('.settings-tabs .tab-btn'); tabBtns.forEach(btn => btn.classList.remove('active')); // Show selected tab const targetTab = document.getElementById(tabName + 'Tab'); if (targetTab) { targetTab.classList.add('active'); } // Activate corresponding button event.target.classList.add('active'); } // Settings Management Functions function addBankAccount() { const bankAccounts = document.getElementById('bankAccounts'); const newAccount = document.createElement('div'); newAccount.className = 'bank-account'; newAccount.innerHTML = `
`; bankAccounts.appendChild(newAccount); } function removeBankAccount(button) { button.closest('.bank-account').remove(); } function addShippingZone() { const shippingZones = document.getElementById('shippingZones'); const newZone = document.createElement('div'); newZone.className = 'shipping-zone'; newZone.innerHTML = `
บาท
`; shippingZones.appendChild(newZone); } function removeShippingZone(button) { button.closest('.shipping-zone').remove(); } function addCategory() { const categoriesList = document.getElementById('categoriesList'); const newCategory = document.createElement('div'); newCategory.className = 'category-item'; newCategory.innerHTML = ` `; categoriesList.appendChild(newCategory); } function removeCategory(button) { const categoryItems = document.querySelectorAll('.category-item'); if (categoryItems.length > 1) { button.closest('.category-item').remove(); } else { showAlert('At least one category is required', 'warning'); } } function addSize() { const sizesList = document.getElementById('sizesList'); const newSize = document.createElement('div'); newSize.className = 'size-item'; newSize.innerHTML = ` `; sizesList.appendChild(newSize); } function removeSize(button) { const sizeItems = document.querySelectorAll('.size-item'); if (sizeItems.length > 1) { button.closest('.size-item').remove(); } else { showAlert('At least one size is required', 'warning'); } } function addColor() { const colorsList = document.getElementById('colorsList'); const newColor = document.createElement('div'); newColor.className = 'color-item'; newColor.innerHTML = ` `; colorsList.appendChild(newColor); } function removeColor(button) { button.closest('.color-item').remove(); } function generateJWTSecret() { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?'; let result = ''; for (let i = 0; i < 64; i++) { result += characters.charAt(Math.floor(Math.random() * characters.length)); } document.getElementById('jwtSecret').value = result; showAlert('New JWT secret generated successfully', 'success'); } function saveSettings() { // Collect all settings data const settings = { general: { storeName: document.getElementById('storeName').value, storeDescription: document.getElementById('storeDescription').value, storePhone: document.getElementById('storePhone').value, storeEmail: document.getElementById('storeEmail').value, storeAddress: document.getElementById('storeAddress').value, storeFacebook: document.getElementById('storeFacebook').value, storeLine: document.getElementById('storeLine').value }, payment: { promptPayNumber: document.getElementById('promptPayNumber').value, promptPayName: document.getElementById('promptPayName').value, enableCOD: document.getElementById('enableCOD').checked, bankAccounts: collectBankAccounts() }, shipping: { defaultShippingFee: parseInt(document.getElementById('defaultShippingFee').value), freeShippingMin: parseInt(document.getElementById('freeShippingMin').value), shippingDaysMin: parseInt(document.getElementById('shippingDaysMin').value), shippingDaysMax: parseInt(document.getElementById('shippingDaysMax').value), shippingZones: collectShippingZones() }, products: { categories: collectCategories(), sizes: collectSizes(), colors: collectColors() }, notifications: { adminEmail: document.getElementById('adminEmail').value, notifyNewOrder: document.getElementById('notifyNewOrder').checked, notifyPaymentReceived: document.getElementById('notifyPaymentReceived').checked, notifyLowStock: document.getElementById('notifyLowStock').checked, sendOrderConfirmation: document.getElementById('sendOrderConfirmation').checked, sendShippingNotification: document.getElementById('sendShippingNotification').checked, lineNotifyToken: document.getElementById('lineNotifyToken').value }, system: { language: document.getElementById('systemLanguage').value, currency: document.getElementById('systemCurrency').value, timezone: document.getElementById('systemTimezone').value, apiBaseUrl: document.getElementById('apiBaseUrl').value, enableDebugMode: document.getElementById('enableDebugMode').checked, enableMaintenanceMode: document.getElementById('enableMaintenanceMode').checked, jwtSecret: document.getElementById('jwtSecret').value, sessionTimeout: parseInt(document.getElementById('sessionTimeout').value) } }; // Save to localStorage localStorage.setItem('adminSettings', JSON.stringify(settings)); // Save to API fetch(API_BASE + '/settings', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + localStorage.getItem('adminToken') }, body: JSON.stringify(settings) }) .then(response => response.json()) .then(data => { if (data.success) { showAlert('Settings saved successfully!', 'success'); } else { showAlert('Error: ' + data.message, 'danger'); } }) .catch(error => { console.error('Error saving settings:', error); showAlert('Settings saved locally (API unavailable)', 'warning'); }); } function collectBankAccounts() { const accounts = []; document.querySelectorAll('.bank-account').forEach(account => { const bankSelect = account.querySelector('select'); const accountNumber = account.querySelector('input[placeholder="Bank account number"]'); const accountName = account.querySelector('input[placeholder="Account holder name"]'); if (bankSelect.value && accountNumber.value && accountName.value) { accounts.push({ bank: bankSelect.value, accountNumber: accountNumber.value, accountName: accountName.value }); } }); return accounts; } function collectShippingZones() { const zones = []; document.querySelectorAll('.shipping-zone').forEach(zone => { const zoneName = zone.querySelector('input[placeholder="Zone name"]'); const shippingFee = zone.querySelector('input[type="number"]'); const provinces = zone.querySelector('input[placeholder*="Provinces"]'); if (zoneName.value && shippingFee.value && provinces.value) { zones.push({ name: zoneName.value, fee: parseInt(shippingFee.value), provinces: provinces.value.split(',').map(p => p.trim()) }); } }); return zones; } function collectCategories() { return Array.from(document.querySelectorAll('.category-item input')) .map(input => input.value.trim()) .filter(value => value !== ''); } function collectSizes() { return Array.from(document.querySelectorAll('.size-item input')) .map(input => input.value.trim()) .filter(value => value !== ''); } function collectColors() { const colors = []; document.querySelectorAll('.color-item').forEach(item => { const nameInput = item.querySelector('input[type="text"]'); const colorInput = item.querySelector('input[type="color"]'); if (nameInput.value.trim()) { colors.push({ name: nameInput.value.trim(), hex: colorInput.value }); } }); return colors; } function resetSettings() { if (confirm('Are you sure you want to reset all settings to default?')) { localStorage.removeItem('adminSettings'); location.reload(); } } function exportSettings() { const settings = localStorage.getItem('adminSettings'); if (settings) { const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(settings); const downloadAnchorNode = document.createElement('a'); downloadAnchorNode.setAttribute("href", dataStr); downloadAnchorNode.setAttribute("download", "admin_settings_" + new Date().toISOString().slice(0, 10) + ".json"); document.body.appendChild(downloadAnchorNode); downloadAnchorNode.click(); downloadAnchorNode.remove(); showAlert('Settings exported successfully!', 'success'); } else { showAlert('No settings found to export', 'warning'); } } function importSettings() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = function(event) { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = function(e) { try { const settings = JSON.parse(e.target.result); localStorage.setItem('adminSettings', JSON.stringify(settings)); populateSettingsForm(settings); showAlert('Settings imported successfully!', 'success'); } catch (error) { showAlert('Invalid settings file', 'danger'); } }; reader.readAsText(file); } }; input.click(); } function loadSettings() { const savedSettings = localStorage.getItem('adminSettings'); if (savedSettings) { const settings = JSON.parse(savedSettings); populateSettingsForm(settings); } } function populateSettingsForm(settings) { if (settings.general) { document.getElementById('storeName').value = settings.general.storeName || ''; document.getElementById('storeDescription').value = settings.general.storeDescription || ''; document.getElementById('storePhone').value = settings.general.storePhone || ''; document.getElementById('storeEmail').value = settings.general.storeEmail || ''; document.getElementById('storeAddress').value = settings.general.storeAddress || ''; document.getElementById('storeFacebook').value = settings.general.storeFacebook || ''; document.getElementById('storeLine').value = settings.general.storeLine || ''; } if (settings.payment) { document.getElementById('promptPayNumber').value = settings.payment.promptPayNumber || ''; document.getElementById('promptPayName').value = settings.payment.promptPayName || ''; document.getElementById('enableCOD').checked = settings.payment.enableCOD || false; } // Populate other settings as needed... } // Initialize settings when page loads if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', loadSettings); } else { loadSettings(); }