/**
* 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.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();
}