const API_BASE = '/api'; let currentTab = 'products'; let editingProductId = null; // Tab switching function switchTab(tab) { currentTab = tab; // Update tab buttons document.querySelectorAll('.admin-tab').forEach(btn => { btn.classList.remove('active'); }); event.target.classList.add('active'); // Update content document.querySelectorAll('.admin-content').forEach(content => { content.classList.remove('active'); }); document.getElementById(tab + 'Tab').classList.add('active'); // Load data for the tab if (tab === 'products') { loadProducts(); } else if (tab === 'orders') { loadOrders(); } else if (tab === 'regions') { loadRegions(); } else if (tab === 'translations') { loadTranslations(); } else if (tab === 'currency') { loadCurrencyRates(); } } // Product Management async function loadProducts() { const container = document.getElementById('productsList'); try { const response = await fetch(`${API_BASE}/products`); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to load products'); } if (data.products.length === 0) { container.innerHTML = '
No products found. Add your first product!
'; return; } container.innerHTML = ` ${data.products.map(product => ` `).join('')}
Image Name Category Price Stock Actions
${product.image && product.image.trim() && (product.image.startsWith('http') || product.image.startsWith('/')) ? `${escapeHtml(product.name)}` : '
No Image
'}
${escapeHtml(product.name)} ${escapeHtml(product.category || 'general')} ${product.currency} ${(product.price / 100).toFixed(2)}
${product.taxIncluded ? '(tax included)' : '(tax excluded)'}
${product.stock === null ? 'Unlimited' : product.stock}
`; } catch (error) { container.innerHTML = `
Error: ${error.message}
`; } } function openProductForm(productId = null) { editingProductId = productId; const form = document.getElementById('productForm'); const title = document.getElementById('formTitle'); if (productId) { title.textContent = 'Edit Product'; loadProductForEdit(productId); } else { title.textContent = 'Add New Product'; document.getElementById('productFormElement').reset(); document.getElementById('imagePreviewContainer').style.display = 'none'; } form.style.display = 'block'; form.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } function closeProductForm() { document.getElementById('productForm').style.display = 'none'; editingProductId = null; document.getElementById('productFormElement').reset(); document.getElementById('imagePreviewContainer').style.display = 'none'; } async function loadProductForEdit(productId) { try { const response = await fetch(`${API_BASE}/products/${productId}`); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Product not found'); } const product = data.product; document.getElementById('productId').value = product.id; document.getElementById('productName').value = product.name; document.getElementById('productDescription').value = product.description || ''; document.getElementById('productPrice').value = (product.price / 100).toFixed(2); document.getElementById('productCurrency').value = product.currency; document.getElementById('productStock').value = product.stock || ''; document.getElementById('productCategory').value = product.category || 'general'; document.getElementById('productImage').value = product.image || ''; document.getElementById('productTaxIncluded').value = product.taxIncluded ? 'true' : 'false'; // Show image preview if URL is provided if (product.image && product.image.trim()) { const preview = document.getElementById('imagePreview'); preview.src = product.image; document.getElementById('imagePreviewContainer').style.display = 'block'; } } catch (error) { showNotification(`Error loading product: ${error.message}`, 'error'); } } async function handleProductSubmit(event) { event.preventDefault(); const form = event.target; const formData = new FormData(form); const productData = { name: formData.get('name'), description: formData.get('description') || '', price: parseFloat(formData.get('price')), currency: formData.get('currency'), stock: formData.get('stock') ? parseInt(formData.get('stock')) : null, category: formData.get('category') || 'general', image: formData.get('image') || '', taxIncluded: formData.get('taxIncluded') === 'true' }; const submitBtn = form.querySelector('button[type="submit"]'); submitBtn.disabled = true; submitBtn.textContent = 'Saving...'; try { const url = editingProductId ? `${API_BASE}/products/${editingProductId}` : `${API_BASE}/products`; const method = editingProductId ? 'PUT' : 'POST'; const response = await fetchWithCsrf(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(productData) }); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to save product'); } // Token is already refreshed by fetchWithCsrf, but ensure button is re-enabled submitBtn.disabled = false; submitBtn.textContent = 'Save Product'; showNotification(`Product ${editingProductId ? 'updated' : 'created'} successfully!`, 'success'); closeProductForm(); loadProducts(); } catch (error) { showNotification(`Error: ${error.message}`, 'error'); submitBtn.disabled = false; submitBtn.textContent = 'Save Product'; // Refresh CSRF token even on error (token might have been invalidated) if (window.refreshCsrfToken) { await window.refreshCsrfToken(); } } } async function deleteProduct(productId) { const confirmed = await showConfirm( 'Are you sure you want to delete this product? This action cannot be undone.', 'Delete Product' ); if (!confirmed) { return; } try { const response = await fetchWithCsrf(`${API_BASE}/products/${productId}`, { method: 'DELETE' }); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to delete product'); } showNotification('Product deleted successfully!', 'success'); loadProducts(); } catch (error) { showNotification(`Error: ${error.message}`, 'error'); } } function editProduct(productId) { openProductForm(productId); } // Image preview document.getElementById('productImage')?.addEventListener('input', function(e) { const url = e.target.value.trim(); const previewContainer = document.getElementById('imagePreviewContainer'); const preview = document.getElementById('imagePreview'); if (url && (url.startsWith('http') || url.startsWith('/'))) { preview.src = url; preview.onerror = function() { previewContainer.style.display = 'none'; }; preview.onload = function() { previewContainer.style.display = 'block'; }; } else { previewContainer.style.display = 'none'; } }); // Order Management async function loadOrders() { const container = document.getElementById('ordersList'); try { // Get all orders - we'll need to modify the orders route to support this // For now, we'll fetch from a new endpoint or modify existing one const response = await fetch(`${API_BASE}/orders`); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to load orders'); } if (data.orders.length === 0) { container.innerHTML = '
No orders found.
'; return; } container.innerHTML = ` ${data.orders.map(order => ` `).join('')}
Order ID Customer Items Total Status Tracking Date Actions
${order.orderId} ${order.customerInfo?.email || 'N/A'}
${order.customerInfo?.firstName || ''} ${order.customerInfo?.lastName || ''}
${order.items.map(item => `${item.productName} x${item.quantity}`).join('
')}
${order.currency} ${(order.total / 100).toFixed(2)} ${order.status} ${order.trackingId ? `${escapeHtml(order.trackingId)}
${order.carrier || ''}` : ''}
${new Date(order.createdAt).toLocaleString()}
`; } catch (error) { container.innerHTML = `
Error: ${error.message}
`; } } async function viewOrder(orderId) { try { const response = await fetch(`${API_BASE}/orders/${orderId}`); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Order not found'); } const order = data.order; const itemsHtml = order.items.map(item => ` ${escapeHtml(item.productName)} ${item.quantity} ${order.currency} ${(item.price / 100).toFixed(2)} ${order.currency} ${(item.total / 100).toFixed(2)} `).join(''); const modal = `

Order Details: ${order.orderId}

Customer Information

Email: ${order.customerInfo?.email || 'N/A'}

Name: ${order.customerInfo?.firstName || ''} ${order.customerInfo?.lastName || ''}

Phone: ${order.customerInfo?.phone || 'N/A'}

Shipping Address

${order.shippingInfo?.name || 'N/A'}
${order.shippingInfo?.address || ''}
${order.shippingInfo?.city || ''}, ${order.shippingInfo?.state || ''} ${order.shippingInfo?.zip || ''}
${order.shippingInfo?.country || ''}

Order Items

${itemsHtml}
Product Quantity Price Total

Subtotal: ${order.currency} ${(order.subtotal / 100).toFixed(2)}

${order.shippingInfo?.cost > 0 ? `

Shipping: ${order.currency} ${(order.shippingInfo.cost / 100).toFixed(2)}

` : ''} ${order.shippingInfo?.tax > 0 ? `

Tax: ${order.currency} ${(order.shippingInfo.tax / 100).toFixed(2)}

` : ''}

Total: ${order.currency} ${(order.total / 100).toFixed(2)}

Status: ${order.status}

${order.trackingId ? `

Tracking ID: ${escapeHtml(order.trackingId)}

` : ''} ${order.carrier ? `

Carrier: ${escapeHtml(order.carrier)}

` : ''} ${order.notes ? `

Notes: ${escapeHtml(order.notes)}

` : ''}

Date: ${new Date(order.createdAt).toLocaleString()}

`; document.body.insertAdjacentHTML('beforeend', modal); } catch (error) { showNotification(`Error: ${error.message}`, 'error'); } } async function updateOrderStatus(orderId) { // Fetch current order to get existing values let currentOrder = null; try { const response = await fetch(`${API_BASE}/orders/${orderId}`); const data = await response.json(); if (data.success) { currentOrder = data.order; } } catch (error) { console.error('Error fetching order:', error); } // Create modal with dropdown and tracking fields const overlay = document.createElement('div'); overlay.className = 'confirm-dialog-overlay'; overlay.innerHTML = `
Update Order
Enter shipping tracking number
Shipping carrier name
Internal notes (not visible to customer)
`; document.body.appendChild(overlay); // Close on overlay click overlay.addEventListener('click', (e) => { if (e.target === overlay) { overlay.remove(); } }); } async function handleOrderUpdate(event, orderId) { event.preventDefault(); const form = event.target; const formData = new FormData(form); const updateData = { status: formData.get('status'), trackingId: formData.get('trackingId') || null, carrier: formData.get('carrier') || null, notes: formData.get('notes') || null }; // Remove null/empty values Object.keys(updateData).forEach(key => { if (updateData[key] === null || updateData[key] === '') { delete updateData[key]; } }); try { const response = await fetchWithCsrf(`${API_BASE}/orders/${orderId}/status`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updateData) }); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to update order'); } // Close modal event.target.closest('.confirm-dialog-overlay').remove(); showNotification('Order updated successfully!', 'success'); loadOrders(); } catch (error) { showNotification(`Error: ${error.message}`, 'error'); } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ======================================== // REGION MANAGEMENT // ======================================== let currentRegionFilter = 'all'; async function loadRegions() { const container = document.getElementById('regionsList'); try { const response = await fetch(`${API_BASE}/admin/regions`); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to load regions'); } // Load stats const statsResponse = await fetch(`${API_BASE}/admin/regions/stats`); const statsData = await statsResponse.json(); if (statsData.success) { document.getElementById('regionStats').innerHTML = ` Total: ${statsData.total} | Enabled: ${statsData.enabled} | Disabled: ${statsData.disabled} `; } const regions = data.regions.filter(region => { if (currentRegionFilter === 'enabled') return region.enabled; if (currentRegionFilter === 'disabled') return !region.enabled; return true; }); if (regions.length === 0) { container.innerHTML = '
No regions found.
'; return; } container.innerHTML = ` ${regions.map(region => ` `).join('')}
Country Code Country Name Language Currency Status Actions
${escapeHtml(region.countryCode)} ${escapeHtml(region.countryName)} ${escapeHtml(region.languageCode)} ${escapeHtml(region.currencyCode)} ${region.enabled ? 'Enabled' : 'Disabled'} ${region.enabled ? `` : `` }
`; } catch (error) { container.innerHTML = `
Error: ${error.message}
`; } } async function toggleRegion(countryCode, enable) { const action = enable ? 'enable' : 'disable'; try { const response = await fetchWithCsrf(`${API_BASE}/admin/regions/${countryCode}/${action}`, { method: 'POST' }); const data = await response.json(); if (!data.success) { throw new Error(data.error || `Failed to ${action} region`); } showNotification(`Region ${countryCode} ${enable ? 'enabled' : 'disabled'} successfully!`, 'success'); loadRegions(); } catch (error) { showNotification(`Error: ${error.message}`, 'error'); } } function filterRegions(filter) { currentRegionFilter = filter; loadRegions(); } // ======================================== // TRANSLATION MANAGEMENT // ======================================== async function loadTranslations() { const container = document.getElementById('translationsList'); container.innerHTML = `

Select a product to view/manage translations

`; // Load products for dropdown try { const response = await fetch(`${API_BASE}/products`); const data = await response.json(); if (data.success && data.products.length > 0) { const select = document.getElementById('translationProductSelect'); data.products.forEach(product => { const option = document.createElement('option'); option.value = product.id; option.textContent = `${product.name} (ID: ${product.id})`; select.appendChild(option); }); } } catch (error) { console.error('Failed to load products:', error); } } async function loadProductTranslations(productId) { if (!productId) return; const container = document.getElementById('translationsList'); try { const response = await fetch(`${API_BASE}/admin/translations/product/${productId}`); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to load translations'); } if (data.translations.length === 0) { container.innerHTML = '
No translations found for this product. Add your first translation!
'; return; } container.innerHTML = ` ${data.translations.map(trans => ` `).join('')}
Field Language Translation Actions
${escapeHtml(trans.fieldName)} ${escapeHtml(trans.languageCode).toUpperCase()} ${escapeHtml(trans.translatedText)}
`; } catch (error) { container.innerHTML = `
Error: ${error.message}
`; } } function openTranslationForm() { document.getElementById('translationForm').style.display = 'block'; document.getElementById('translationFormElement').reset(); } function closeTranslationForm() { document.getElementById('translationForm').style.display = 'none'; } async function handleTranslationSubmit(event) { event.preventDefault(); const form = event.target; const formData = new FormData(form); const translationData = { entityType: formData.get('entityType'), entityId: formData.get('entityId'), fieldName: formData.get('fieldName'), languageCode: formData.get('languageCode'), translatedText: formData.get('translatedText') }; try { const response = await fetchWithCsrf(`${API_BASE}/admin/translations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(translationData) }); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to save translation'); } closeTranslationForm(); showNotification('Translation saved successfully!', 'success'); // Reload if a product was selected const selectedProduct = document.getElementById('translationProductSelect')?.value; if (selectedProduct && selectedProduct === translationData.entityId) { loadProductTranslations(selectedProduct); } } catch (error) { showNotification(`Error: ${error.message}`, 'error'); } } async function deleteTranslation(translationId) { if (!confirm('Are you sure you want to delete this translation?')) { return; } try { const response = await fetchWithCsrf(`${API_BASE}/admin/translations/${translationId}`, { method: 'DELETE' }); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to delete translation'); } showNotification('Translation deleted successfully!', 'success'); // Reload current product translations const selectedProduct = document.getElementById('translationProductSelect')?.value; if (selectedProduct) { loadProductTranslations(selectedProduct); } } catch (error) { showNotification(`Error: ${error.message}`, 'error'); } } // ======================================== // CURRENCY MANAGEMENT // ======================================== async function loadCurrencyRates() { const container = document.getElementById('currencyRatesList'); try { const response = await fetch(`${API_BASE}/currency/rates`); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to load exchange rates'); } // Update info panel if (data.rates && data.rates.length > 0) { const latestRate = data.rates[0]; const fetchedDate = new Date(latestRate.fetchedAt); const expiresDate = new Date(latestRate.expiresAt); const now = new Date(); const isExpired = now > expiresDate; document.getElementById('lastUpdated').innerHTML = ` ${fetchedDate.toLocaleString()} ${isExpired ? '
⚠️ EXPIRED' : ''} `; document.getElementById('totalRates').textContent = data.rates.length; } if (data.rates.length === 0) { container.innerHTML = '
No exchange rates found. Click "Refresh Rates Now" to fetch rates.
'; return; } container.innerHTML = ` ${data.rates.map(rate => { const expiresDate = new Date(rate.expiresAt); const now = new Date(); const isExpired = now > expiresDate; return ` `}).join('')}
From To Rate Example Updated Expires
${escapeHtml(rate.fromCurrency)} ${escapeHtml(rate.toCurrency)} ${rate.rate.toFixed(4)} 1 ${rate.fromCurrency} = ${rate.rate.toFixed(2)} ${rate.toCurrency} ${new Date(rate.fetchedAt).toLocaleString()} ${isExpired ? 'Expired' : new Date(rate.expiresAt).toLocaleTimeString()}
`; } catch (error) { container.innerHTML = `
Error: ${error.message}
`; } } async function refreshExchangeRates() { const btn = event.target; btn.disabled = true; btn.textContent = 'Refreshing...'; try { const response = await fetchWithCsrf(`${API_BASE}/currency/admin/refresh`, { method: 'POST' }); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to refresh rates'); } showNotification('Exchange rates refreshed successfully!', 'success'); // Wait a moment for the rates to be saved setTimeout(() => { loadCurrencyRates(); }, 1000); } catch (error) { showNotification(`Error: ${error.message}`, 'error'); } finally { btn.disabled = false; btn.textContent = 'Refresh Rates Now'; } } // Initialize document.addEventListener('DOMContentLoaded', () => { loadProducts(); // Make functions global window.switchTab = switchTab; window.openProductForm = openProductForm; window.closeProductForm = closeProductForm; window.handleProductSubmit = handleProductSubmit; window.deleteProduct = deleteProduct; window.editProduct = editProduct; window.viewOrder = viewOrder; window.updateOrderStatus = updateOrderStatus; window.handleOrderUpdate = handleOrderUpdate; // Region management window.loadRegions = loadRegions; window.toggleRegion = toggleRegion; window.filterRegions = filterRegions; // Translation management window.loadTranslations = loadTranslations; window.loadProductTranslations = loadProductTranslations; window.openTranslationForm = openTranslationForm; window.closeTranslationForm = closeTranslationForm; window.handleTranslationSubmit = handleTranslationSubmit; window.deleteTranslation = deleteTranslation; // Currency management window.loadCurrencyRates = loadCurrencyRates; window.refreshExchangeRates = refreshExchangeRates; });