Commit: 690c1f6

Commit Details

SHA690c1f6ce426c62c29e87ac132dda0f8125192ff
Tree5a67fd55b56735ab4e469c7d06c1c73c4c2b4c20
Author<f69e50@finnacloud.com> 1766368110 +0300
Committer<f69e50@finnacloud.com> 1766368110 +0300
Message
initialize backend structure with controllers, DTOs, and configuration files
GPG Signature
-----BEGIN PGP SIGNATURE-----

iQJSBAABCAA8FiEEWJb139mJI+vZ81KkoAIVSUsXI0oFAmlIo24eHHNvcGhpYS5l
cmFzbGFuQGZpbm5hY2xvdWQuY29tAAoJEKACFUlLFyNKwXYP/RvWx8mxXoZbKEVA
wQFC9UnzcoL/lElB5QMr9opKzRv4uGgFkKMDhbSnqE6NoET5H5VanOFQ9u5a4Khi
9PBTLIEBjbEqA1trC+aTDk3EplVtQYYbSn19CdMSCW7FXJNSg0IiyWKA44iH8Ts0
Xcxh59m6WcwvRDhxQDy6hCXqUa9ISNNk75KRnJS/qRGIEy94DwUYxVfCJpAfzyVu
VmdinE6kZM2GDj8MBTPQTzi6hMf/e9CcAg51tf4oNtd9tnW8QKpTsPsFy434VUDh
Vxtv/oAJ5tuTIprs2BSnyZ6Kb8RDwDdmQyuKjZMUIwZTH/TCE85SZFf5sa5tpYOH
2KekT52ffZgKw/DUtpNosW6qeHgCJlSvwY7BW7M90X5xJuahrKS04lEDBO04cIRn
bg0ayPFTp7Idhl1OuTRhWS6e344g44mJ/9sZK1sXd/0U8OKBbytk35AnCIPCEdSX
8vgjqBR9Wt3A/Kel5j2VcUFDhrAR72a9lJiQHNBicvcVu9Nd41vDnEwUDdNQv6Uc
6omEz3pkVkq+89/eW1KQM8LvrIuGQ/wIUgykvCNSCQ5oba2fjtAXzI+SmxpeCWOz
jTKZOEJyhIQE7uvaUj6/0D2JwlxbMG27fcUN7N3aKv6mSVP7hYnHaUbRLQ+f8/xU
VfU776FkUXS+4CfSooHtu+ioul9O
=ZuqE
-----END PGP SIGNATURE-----

✓ Verified

File: src/main/resources/static/js/admin.js

1 const API_BASE = '/api';
2
3 let currentTab = 'products';
4 let editingProductId = null;
5
6 // Tab switching
7 function switchTab(tab) {
8 currentTab = tab;
9
10 // Update tab buttons
11 document.querySelectorAll('.admin-tab').forEach(btn => {
12 btn.classList.remove('active');
13 });
14 event.target.classList.add('active');
15
16 // Update content
17 document.querySelectorAll('.admin-content').forEach(content => {
18 content.classList.remove('active');
19 });
20 document.getElementById(tab + 'Tab').classList.add('active');
21
22 // Load data for the tab
23 if (tab === 'products') {
24 loadProducts();
25 } else if (tab === 'orders') {
26 loadOrders();
27 } else if (tab === 'regions') {
28 loadRegions();
29 } else if (tab === 'translations') {
30 loadTranslations();
31 } else if (tab === 'currency') {
32 loadCurrencyRates();
33 }
34 }
35
36 // Product Management
37 async function loadProducts() {
38 const container = document.getElementById('productsList');
39
40 try {
41 const response = await fetch(`${API_BASE}/products`);
42 const data = await response.json();
43
44 if (!data.success) {
45 throw new Error(data.error || 'Failed to load products');
46 }
47
48 if (data.products.length === 0) {
49 container.innerHTML = '<div style="padding: 2rem; text-align: center; color: #666;">No products found. Add your first product!</div>';
50 return;
51 }
52
53 container.innerHTML = `
54 <table>
55 <thead>
56 <tr>
57 <th>Image</th>
58 <th>Name</th>
59 <th>Category</th>
60 <th>Price</th>
61 <th>Stock</th>
62 <th>Actions</th>
63 </tr>
64 </thead>
65 <tbody>
66 ${data.products.map(product => `
67 <tr>
68 <td>
69 ${product.image && product.image.trim() && (product.image.startsWith('http') || product.image.startsWith('/'))
70 ? `<img src="${escapeHtml(product.image)}" alt="${escapeHtml(product.name)}" style="width: 60px; height: 60px; object-fit: cover; border-radius: 4px;" onerror="this.style.display='none'">`
71 : '<div style="width: 60px; height: 60px; background: #f0f0f0; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 0.7rem; color: #999;">No Image</div>'}
72 </td>
73 <td><strong>${escapeHtml(product.name)}</strong></td>
74 <td>${escapeHtml(product.category || 'general')}</td>
75 <td>
76 ${product.currency} ${(product.price / 100).toFixed(2)}
77 <br><small style="color: #666;">${product.taxIncluded ? '(tax included)' : '(tax excluded)'}</small>
78 </td>
79 <td>${product.stock === null ? 'Unlimited' : product.stock}</td>
80 <td>
81 <button class="btn btn-small" onclick="editProduct('${product.id}')">Edit</button>
82 <button class="btn btn-small btn-danger" onclick="deleteProduct('${product.id}')">Delete</button>
83 </td>
84 </tr>
85 `).join('')}
86 </tbody>
87 </table>
88 `;
89 } catch (error) {
90 container.innerHTML = `<div class="error">Error: ${error.message}</div>`;
91 }
92 }
93
94 function openProductForm(productId = null) {
95 editingProductId = productId;
96 const form = document.getElementById('productForm');
97 const title = document.getElementById('formTitle');
98
99 if (productId) {
100 title.textContent = 'Edit Product';
101 loadProductForEdit(productId);
102 } else {
103 title.textContent = 'Add New Product';
104 document.getElementById('productFormElement').reset();
105 document.getElementById('imagePreviewContainer').style.display = 'none';
106 }
107
108 form.style.display = 'block';
109 form.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
110 }
111
112 function closeProductForm() {
113 document.getElementById('productForm').style.display = 'none';
114 editingProductId = null;
115 document.getElementById('productFormElement').reset();
116 document.getElementById('imagePreviewContainer').style.display = 'none';
117 }
118
119 async function loadProductForEdit(productId) {
120 try {
121 const response = await fetch(`${API_BASE}/products/${productId}`);
122 const data = await response.json();
123
124 if (!data.success) {
125 throw new Error(data.error || 'Product not found');
126 }
127
128 const product = data.product;
129 document.getElementById('productId').value = product.id;
130 document.getElementById('productName').value = product.name;
131 document.getElementById('productDescription').value = product.description || '';
132 document.getElementById('productPrice').value = (product.price / 100).toFixed(2);
133 document.getElementById('productCurrency').value = product.currency;
134 document.getElementById('productStock').value = product.stock || '';
135 document.getElementById('productCategory').value = product.category || 'general';
136 document.getElementById('productImage').value = product.image || '';
137 document.getElementById('productTaxIncluded').value = product.taxIncluded ? 'true' : 'false';
138
139 // Show image preview if URL is provided
140 if (product.image && product.image.trim()) {
141 const preview = document.getElementById('imagePreview');
142 preview.src = product.image;
143 document.getElementById('imagePreviewContainer').style.display = 'block';
144 }
145 } catch (error) {
146 showNotification(`Error loading product: ${error.message}`, 'error');
147 }
148 }
149
150 async function handleProductSubmit(event) {
151 event.preventDefault();
152
153 const form = event.target;
154 const formData = new FormData(form);
155
156 const productData = {
157 name: formData.get('name'),
158 description: formData.get('description') || '',
159 price: parseFloat(formData.get('price')),
160 currency: formData.get('currency'),
161 stock: formData.get('stock') ? parseInt(formData.get('stock')) : null,
162 category: formData.get('category') || 'general',
163 image: formData.get('image') || '',
164 taxIncluded: formData.get('taxIncluded') === 'true'
165 };
166
167 const submitBtn = form.querySelector('button[type="submit"]');
168 submitBtn.disabled = true;
169 submitBtn.textContent = 'Saving...';
170
171 try {
172 const url = editingProductId
173 ? `${API_BASE}/products/${editingProductId}`
174 : `${API_BASE}/products`;
175 const method = editingProductId ? 'PUT' : 'POST';
176
177 const response = await fetchWithCsrf(url, {
178 method,
179 headers: {
180 'Content-Type': 'application/json'
181 },
182 body: JSON.stringify(productData)
183 });
184
185 const data = await response.json();
186
187 if (!data.success) {
188 throw new Error(data.error || 'Failed to save product');
189 }
190
191 // Token is already refreshed by fetchWithCsrf, but ensure button is re-enabled
192 submitBtn.disabled = false;
193 submitBtn.textContent = 'Save Product';
194
195 showNotification(`Product ${editingProductId ? 'updated' : 'created'} successfully!`, 'success');
196 closeProductForm();
197 loadProducts();
198 } catch (error) {
199 showNotification(`Error: ${error.message}`, 'error');
200 submitBtn.disabled = false;
201 submitBtn.textContent = 'Save Product';
202
203 // Refresh CSRF token even on error (token might have been invalidated)
204 if (window.refreshCsrfToken) {
205 await window.refreshCsrfToken();
206 }
207 }
208 }
209
210 async function deleteProduct(productId) {
211 const confirmed = await showConfirm(
212 'Are you sure you want to delete this product? This action cannot be undone.',
213 'Delete Product'
214 );
215
216 if (!confirmed) {
217 return;
218 }
219
220 try {
221 const response = await fetchWithCsrf(`${API_BASE}/products/${productId}`, {
222 method: 'DELETE'
223 });
224
225 const data = await response.json();
226
227 if (!data.success) {
228 throw new Error(data.error || 'Failed to delete product');
229 }
230
231 showNotification('Product deleted successfully!', 'success');
232 loadProducts();
233 } catch (error) {
234 showNotification(`Error: ${error.message}`, 'error');
235 }
236 }
237
238 function editProduct(productId) {
239 openProductForm(productId);
240 }
241
242 // Image preview
243 document.getElementById('productImage')?.addEventListener('input', function(e) {
244 const url = e.target.value.trim();
245 const previewContainer = document.getElementById('imagePreviewContainer');
246 const preview = document.getElementById('imagePreview');
247
248 if (url && (url.startsWith('http') || url.startsWith('/'))) {
249 preview.src = url;
250 preview.onerror = function() {
251 previewContainer.style.display = 'none';
252 };
253 preview.onload = function() {
254 previewContainer.style.display = 'block';
255 };
256 } else {
257 previewContainer.style.display = 'none';
258 }
259 });
260
261 // Order Management
262 async function loadOrders() {
263 const container = document.getElementById('ordersList');
264
265 try {
266 // Get all orders - we'll need to modify the orders route to support this
267 // For now, we'll fetch from a new endpoint or modify existing one
268 const response = await fetch(`${API_BASE}/orders`);
269 const data = await response.json();
270
271 if (!data.success) {
272 throw new Error(data.error || 'Failed to load orders');
273 }
274
275 if (data.orders.length === 0) {
276 container.innerHTML = '<div style="padding: 2rem; text-align: center; color: #666;">No orders found.</div>';
277 return;
278 }
279
280 container.innerHTML = `
281 <table>
282 <thead>
283 <tr>
284 <th>Order ID</th>
285 <th>Customer</th>
286 <th>Items</th>
287 <th>Total</th>
288 <th>Status</th>
289 <th>Tracking</th>
290 <th>Date</th>
291 <th>Actions</th>
292 </tr>
293 </thead>
294 <tbody>
295 ${data.orders.map(order => `
296 <tr>
297 <td><strong>${order.orderId}</strong></td>
298 <td>
299 ${order.customerInfo?.email || 'N/A'}<br>
300 <small style="color: #666;">${order.customerInfo?.firstName || ''} ${order.customerInfo?.lastName || ''}</small>
301 </td>
302 <td>
303 ${order.items.map(item => `${item.productName} x${item.quantity}`).join('<br>')}
304 </td>
305 <td><strong>${order.currency} ${(order.total / 100).toFixed(2)}</strong></td>
306 <td>
307 <span class="status-badge status-${order.status}">${order.status}</span>
308 </td>
309 <td>
310 ${order.trackingId ? `<strong>${escapeHtml(order.trackingId)}</strong><br><small style="color: #666;">${order.carrier || ''}</small>` : '<span style="color: #999;">—</span>'}
311 </td>
312 <td>${new Date(order.createdAt).toLocaleString()}</td>
313 <td>
314 <button class="btn btn-small" onclick="viewOrder('${order.orderId}')">View</button>
315 <button class="btn btn-small" onclick="updateOrderStatus('${order.orderId}')">Update</button>
316 </td>
317 </tr>
318 `).join('')}
319 </tbody>
320 </table>
321 `;
322 } catch (error) {
323 container.innerHTML = `<div class="error">Error: ${error.message}</div>`;
324 }
325 }
326
327 async function viewOrder(orderId) {
328 try {
329 const response = await fetch(`${API_BASE}/orders/${orderId}`);
330 const data = await response.json();
331
332 if (!data.success) {
333 throw new Error(data.error || 'Order not found');
334 }
335
336 const order = data.order;
337 const itemsHtml = order.items.map(item => `
338 <tr>
339 <td>${escapeHtml(item.productName)}</td>
340 <td>${item.quantity}</td>
341 <td>${order.currency} ${(item.price / 100).toFixed(2)}</td>
342 <td>${order.currency} ${(item.total / 100).toFixed(2)}</td>
343 </tr>
344 `).join('');
345 const modal = `
346 <div class="order-modal-overlay" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 2000; display: flex; align-items: center; justify-content: center; padding: 2rem;">
347 <div style="background: white; border-radius: 12px; padding: 2rem; max-width: 800px; max-height: 90vh; overflow-y: auto; width: 100%;">
348 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
349 <h3>Order Details: ${order.orderId}</h3>
350 <button onclick="this.closest('.order-modal-overlay').remove()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer;">&times;</button>
351 </div>
352
353 <div style="margin-bottom: 1.5rem;">
354 <h4>Customer Information</h4>
355 <p><strong>Email:</strong> ${order.customerInfo?.email || 'N/A'}</p>
356 <p><strong>Name:</strong> ${order.customerInfo?.firstName || ''} ${order.customerInfo?.lastName || ''}</p>
357 <p><strong>Phone:</strong> ${order.customerInfo?.phone || 'N/A'}</p>
358 </div>
359
360 <div style="margin-bottom: 1.5rem;">
361 <h4>Shipping Address</h4>
362 <p>${order.shippingInfo?.name || 'N/A'}<br>
363 ${order.shippingInfo?.address || ''}<br>
364 ${order.shippingInfo?.city || ''}, ${order.shippingInfo?.state || ''} ${order.shippingInfo?.zip || ''}<br>
365 ${order.shippingInfo?.country || ''}</p>
366 </div>
367
368 <div style="margin-bottom: 1.5rem;">
369 <h4>Order Items</h4>
370 <table style="width: 100%; border-collapse: collapse;">
371 <thead>
372 <tr style="background: #f8f9fa;">
373 <th style="padding: 0.5rem; text-align: left;">Product</th>
374 <th style="padding: 0.5rem; text-align: left;">Quantity</th>
375 <th style="padding: 0.5rem; text-align: left;">Price</th>
376 <th style="padding: 0.5rem; text-align: left;">Total</th>
377 </tr>
378 </thead>
379 <tbody>
380 ${itemsHtml}
381 </tbody>
382 </table>
383 </div>
384
385 <div>
386 <p><strong>Subtotal:</strong> ${order.currency} ${(order.subtotal / 100).toFixed(2)}</p>
387 ${order.shippingInfo?.cost > 0 ? `<p><strong>Shipping:</strong> ${order.currency} ${(order.shippingInfo.cost / 100).toFixed(2)}</p>` : ''}
388 ${order.shippingInfo?.tax > 0 ? `<p><strong>Tax:</strong> ${order.currency} ${(order.shippingInfo.tax / 100).toFixed(2)}</p>` : ''}
389 <p><strong>Total:</strong> ${order.currency} ${(order.total / 100).toFixed(2)}</p>
390 <p><strong>Status:</strong> <span class="status-badge status-${order.status}">${order.status}</span></p>
391 ${order.trackingId ? `<p><strong>Tracking ID:</strong> ${escapeHtml(order.trackingId)}</p>` : ''}
392 ${order.carrier ? `<p><strong>Carrier:</strong> ${escapeHtml(order.carrier)}</p>` : ''}
393 ${order.notes ? `<p><strong>Notes:</strong> ${escapeHtml(order.notes)}</p>` : ''}
394 <p><strong>Date:</strong> ${new Date(order.createdAt).toLocaleString()}</p>
395 </div>
396 </div>
397 </div>
398 `;
399
400 document.body.insertAdjacentHTML('beforeend', modal);
401 } catch (error) {
402 showNotification(`Error: ${error.message}`, 'error');
403 }
404 }
405
406 async function updateOrderStatus(orderId) {
407 // Fetch current order to get existing values
408 let currentOrder = null;
409 try {
410 const response = await fetch(`${API_BASE}/orders/${orderId}`);
411 const data = await response.json();
412 if (data.success) {
413 currentOrder = data.order;
414 }
415 } catch (error) {
416 console.error('Error fetching order:', error);
417 }
418
419 // Create modal with dropdown and tracking fields
420 const overlay = document.createElement('div');
421 overlay.className = 'confirm-dialog-overlay';
422
423 overlay.innerHTML = `
424 <div class="confirm-dialog" style="max-width: 600px;">
425 <div class="confirm-dialog-title">Update Order</div>
426 <form id="updateOrderForm" onsubmit="handleOrderUpdate(event, '${orderId}')">
427 <div class="form-group" style="margin-bottom: 1.5rem;">
428 <label>Status *</label>
429 <select id="orderStatus" name="status" required style="width: 100%; padding: 0.875rem; border: 2px solid var(--border-color); border-radius: 10px; font-size: 1rem;">
430 <option value="pending" ${currentOrder?.status === 'pending' ? 'selected' : ''}>Pending</option>
431 <option value="processing" ${currentOrder?.status === 'processing' ? 'selected' : ''}>Processing</option>
432 <option value="completed" ${currentOrder?.status === 'completed' ? 'selected' : ''}>Completed</option>
433 <option value="cancelled" ${currentOrder?.status === 'cancelled' ? 'selected' : ''}>Cancelled</option>
434 </select>
435 </div>
436
437 <div class="form-group" style="margin-bottom: 1.5rem;">
438 <label>Tracking ID</label>
439 <input type="text" id="trackingId" name="trackingId" placeholder="e.g., 1Z999AA10123456784" value="${currentOrder?.trackingId || ''}" style="width: 100%; padding: 0.875rem; border: 2px solid var(--border-color); border-radius: 10px; font-size: 1rem;">
440 <small style="color: #666; font-size: 0.85rem;">Enter shipping tracking number</small>
441 </div>
442
443 <div class="form-group" style="margin-bottom: 1.5rem;">
444 <label>Carrier</label>
445 <input type="text" id="carrier" name="carrier" placeholder="e.g., UPS, FedEx, DHL" value="${currentOrder?.carrier || ''}" style="width: 100%; padding: 0.875rem; border: 2px solid var(--border-color); border-radius: 10px; font-size: 1rem;">
446 <small style="color: #666; font-size: 0.85rem;">Shipping carrier name</small>
447 </div>
448
449 <div class="form-group" style="margin-bottom: 1.5rem;">
450 <label>Notes</label>
451 <textarea id="notes" name="notes" placeholder="Internal notes about this order" rows="3" style="width: 100%; padding: 0.875rem; border: 2px solid var(--border-color); border-radius: 10px; font-size: 1rem; font-family: inherit; resize: vertical;">${currentOrder?.notes || ''}</textarea>
452 <small style="color: #666; font-size: 0.85rem;">Internal notes (not visible to customer)</small>
453 </div>
454
455 <div class="confirm-dialog-actions">
456 <button type="button" class="btn-cancel" onclick="this.closest('.confirm-dialog-overlay').remove()">Cancel</button>
457 <button type="submit" class="btn-confirm">Update Order</button>
458 </div>
459 </form>
460 </div>
461 `;
462
463 document.body.appendChild(overlay);
464
465 // Close on overlay click
466 overlay.addEventListener('click', (e) => {
467 if (e.target === overlay) {
468 overlay.remove();
469 }
470 });
471 }
472
473 async function handleOrderUpdate(event, orderId) {
474 event.preventDefault();
475 const form = event.target;
476 const formData = new FormData(form);
477
478 const updateData = {
479 status: formData.get('status'),
480 trackingId: formData.get('trackingId') || null,
481 carrier: formData.get('carrier') || null,
482 notes: formData.get('notes') || null
483 };
484
485 // Remove null/empty values
486 Object.keys(updateData).forEach(key => {
487 if (updateData[key] === null || updateData[key] === '') {
488 delete updateData[key];
489 }
490 });
491
492 try {
493 const response = await fetchWithCsrf(`${API_BASE}/orders/${orderId}/status`, {
494 method: 'PATCH',
495 headers: {
496 'Content-Type': 'application/json'
497 },
498 body: JSON.stringify(updateData)
499 });
500
501 const data = await response.json();
502
503 if (!data.success) {
504 throw new Error(data.error || 'Failed to update order');
505 }
506
507 // Close modal
508 event.target.closest('.confirm-dialog-overlay').remove();
509
510 showNotification('Order updated successfully!', 'success');
511 loadOrders();
512 } catch (error) {
513 showNotification(`Error: ${error.message}`, 'error');
514 }
515 }
516
517 function escapeHtml(text) {
518 const div = document.createElement('div');
519 div.textContent = text;
520 return div.innerHTML;
521 }
522
523 // ========================================
524 // REGION MANAGEMENT
525 // ========================================
526
527 let currentRegionFilter = 'all';
528
529 async function loadRegions() {
530 const container = document.getElementById('regionsList');
531
532 try {
533 const response = await fetch(`${API_BASE}/admin/regions`);
534 const data = await response.json();
535
536 if (!data.success) {
537 throw new Error(data.error || 'Failed to load regions');
538 }
539
540 // Load stats
541 const statsResponse = await fetch(`${API_BASE}/admin/regions/stats`);
542 const statsData = await statsResponse.json();
543 if (statsData.success) {
544 document.getElementById('regionStats').innerHTML = `
545 <strong>Total:</strong> ${statsData.total} |
546 <strong>Enabled:</strong> <span style="color: #28a745;">${statsData.enabled}</span> |
547 <strong>Disabled:</strong> <span style="color: #dc3545;">${statsData.disabled}</span>
548 `;
549 }
550
551 const regions = data.regions.filter(region => {
552 if (currentRegionFilter === 'enabled') return region.enabled;
553 if (currentRegionFilter === 'disabled') return !region.enabled;
554 return true;
555 });
556
557 if (regions.length === 0) {
558 container.innerHTML = '<div style="padding: 2rem; text-align: center; color: #666;">No regions found.</div>';
559 return;
560 }
561
562 container.innerHTML = `
563 <table>
564 <thead>
565 <tr>
566 <th>Country Code</th>
567 <th>Country Name</th>
568 <th>Language</th>
569 <th>Currency</th>
570 <th>Status</th>
571 <th>Actions</th>
572 </tr>
573 </thead>
574 <tbody>
575 ${regions.map(region => `
576 <tr>
577 <td><strong>${escapeHtml(region.countryCode)}</strong></td>
578 <td>${escapeHtml(region.countryName)}</td>
579 <td>${escapeHtml(region.languageCode)}</td>
580 <td>${escapeHtml(region.currencyCode)}</td>
581 <td>
582 <span class="status-badge ${region.enabled ? 'status-completed' : 'status-cancelled'}">
583 ${region.enabled ? 'Enabled' : 'Disabled'}
584 </span>
585 </td>
586 <td>
587 ${region.enabled
588 ? `<button class="btn btn-small btn-danger" onclick="toggleRegion('${region.countryCode}', false)">Disable</button>`
589 : `<button class="btn btn-small btn-success" onclick="toggleRegion('${region.countryCode}', true)">Enable</button>`
590 }
591 </td>
592 </tr>
593 `).join('')}
594 </tbody>
595 </table>
596 `;
597 } catch (error) {
598 container.innerHTML = `<div class="error">Error: ${error.message}</div>`;
599 }
600 }
601
602 async function toggleRegion(countryCode, enable) {
603 const action = enable ? 'enable' : 'disable';
604
605 try {
606 const response = await fetchWithCsrf(`${API_BASE}/admin/regions/${countryCode}/${action}`, {
607 method: 'POST'
608 });
609
610 const data = await response.json();
611
612 if (!data.success) {
613 throw new Error(data.error || `Failed to ${action} region`);
614 }
615
616 showNotification(`Region ${countryCode} ${enable ? 'enabled' : 'disabled'} successfully!`, 'success');
617 loadRegions();
618 } catch (error) {
619 showNotification(`Error: ${error.message}`, 'error');
620 }
621 }
622
623 function filterRegions(filter) {
624 currentRegionFilter = filter;
625 loadRegions();
626 }
627
628 // ========================================
629 // TRANSLATION MANAGEMENT
630 // ========================================
631
632 async function loadTranslations() {
633 const container = document.getElementById('translationsList');
634 container.innerHTML = `
635 <div style="padding: 2rem; text-align: center;">
636 <p style="color: #666; margin-bottom: 1rem;">Select a product to view/manage translations</p>
637 <select id="translationProductSelect" onchange="loadProductTranslations(this.value)" style="padding: 0.5rem 1rem; border: 2px solid var(--border-color); border-radius: 8px; font-size: 1rem;">
638 <option value="">Select a product...</option>
639 </select>
640 </div>
641 `;
642
643 // Load products for dropdown
644 try {
645 const response = await fetch(`${API_BASE}/products`);
646 const data = await response.json();
647 if (data.success && data.products.length > 0) {
648 const select = document.getElementById('translationProductSelect');
649 data.products.forEach(product => {
650 const option = document.createElement('option');
651 option.value = product.id;
652 option.textContent = `${product.name} (ID: ${product.id})`;
653 select.appendChild(option);
654 });
655 }
656 } catch (error) {
657 console.error('Failed to load products:', error);
658 }
659 }
660
661 async function loadProductTranslations(productId) {
662 if (!productId) return;
663
664 const container = document.getElementById('translationsList');
665
666 try {
667 const response = await fetch(`${API_BASE}/admin/translations/product/${productId}`);
668 const data = await response.json();
669
670 if (!data.success) {
671 throw new Error(data.error || 'Failed to load translations');
672 }
673
674 if (data.translations.length === 0) {
675 container.innerHTML = '<div style="padding: 2rem; text-align: center; color: #666;">No translations found for this product. Add your first translation!</div>';
676 return;
677 }
678
679 container.innerHTML = `
680 <table>
681 <thead>
682 <tr>
683 <th>Field</th>
684 <th>Language</th>
685 <th>Translation</th>
686 <th>Actions</th>
687 </tr>
688 </thead>
689 <tbody>
690 ${data.translations.map(trans => `
691 <tr>
692 <td><strong>${escapeHtml(trans.fieldName)}</strong></td>
693 <td>
694 <span class="status-badge status-processing">${escapeHtml(trans.languageCode).toUpperCase()}</span>
695 </td>
696 <td style="max-width: 400px;">${escapeHtml(trans.translatedText)}</td>
697 <td>
698 <button class="btn btn-small btn-danger" onclick="deleteTranslation('${trans.id}')">Delete</button>
699 </td>
700 </tr>
701 `).join('')}
702 </tbody>
703 </table>
704 `;
705 } catch (error) {
706 container.innerHTML = `<div class="error">Error: ${error.message}</div>`;
707 }
708 }
709
710 function openTranslationForm() {
711 document.getElementById('translationForm').style.display = 'block';
712 document.getElementById('translationFormElement').reset();
713 }
714
715 function closeTranslationForm() {
716 document.getElementById('translationForm').style.display = 'none';
717 }
718
719 async function handleTranslationSubmit(event) {
720 event.preventDefault();
721 const form = event.target;
722 const formData = new FormData(form);
723
724 const translationData = {
725 entityType: formData.get('entityType'),
726 entityId: formData.get('entityId'),
727 fieldName: formData.get('fieldName'),
728 languageCode: formData.get('languageCode'),
729 translatedText: formData.get('translatedText')
730 };
731
732 try {
733 const response = await fetchWithCsrf(`${API_BASE}/admin/translations`, {
734 method: 'POST',
735 headers: {
736 'Content-Type': 'application/json'
737 },
738 body: JSON.stringify(translationData)
739 });
740
741 const data = await response.json();
742
743 if (!data.success) {
744 throw new Error(data.error || 'Failed to save translation');
745 }
746
747 closeTranslationForm();
748 showNotification('Translation saved successfully!', 'success');
749
750 // Reload if a product was selected
751 const selectedProduct = document.getElementById('translationProductSelect')?.value;
752 if (selectedProduct && selectedProduct === translationData.entityId) {
753 loadProductTranslations(selectedProduct);
754 }
755 } catch (error) {
756 showNotification(`Error: ${error.message}`, 'error');
757 }
758 }
759
760 async function deleteTranslation(translationId) {
761 if (!confirm('Are you sure you want to delete this translation?')) {
762 return;
763 }
764
765 try {
766 const response = await fetchWithCsrf(`${API_BASE}/admin/translations/${translationId}`, {
767 method: 'DELETE'
768 });
769
770 const data = await response.json();
771
772 if (!data.success) {
773 throw new Error(data.error || 'Failed to delete translation');
774 }
775
776 showNotification('Translation deleted successfully!', 'success');
777
778 // Reload current product translations
779 const selectedProduct = document.getElementById('translationProductSelect')?.value;
780 if (selectedProduct) {
781 loadProductTranslations(selectedProduct);
782 }
783 } catch (error) {
784 showNotification(`Error: ${error.message}`, 'error');
785 }
786 }
787
788 // ========================================
789 // CURRENCY MANAGEMENT
790 // ========================================
791
792 async function loadCurrencyRates() {
793 const container = document.getElementById('currencyRatesList');
794
795 try {
796 const response = await fetch(`${API_BASE}/currency/rates`);
797 const data = await response.json();
798
799 if (!data.success) {
800 throw new Error(data.error || 'Failed to load exchange rates');
801 }
802
803 // Update info panel
804 if (data.rates && data.rates.length > 0) {
805 const latestRate = data.rates[0];
806 const fetchedDate = new Date(latestRate.fetchedAt);
807 const expiresDate = new Date(latestRate.expiresAt);
808 const now = new Date();
809 const isExpired = now > expiresDate;
810
811 document.getElementById('lastUpdated').innerHTML = `
812 ${fetchedDate.toLocaleString()}
813 ${isExpired ? '<br><span style="color: #e74c3c; font-weight: bold;">⚠️ EXPIRED</span>' : ''}
814 `;
815 document.getElementById('totalRates').textContent = data.rates.length;
816 }
817
818 if (data.rates.length === 0) {
819 container.innerHTML = '<div style="padding: 2rem; text-align: center; color: #666;">No exchange rates found. Click "Refresh Rates Now" to fetch rates.</div>';
820 return;
821 }
822
823 container.innerHTML = `
824 <table>
825 <thead>
826 <tr>
827 <th>From</th>
828 <th>To</th>
829 <th>Rate</th>
830 <th>Example</th>
831 <th>Updated</th>
832 <th>Expires</th>
833 </tr>
834 </thead>
835 <tbody>
836 ${data.rates.map(rate => {
837 const expiresDate = new Date(rate.expiresAt);
838 const now = new Date();
839 const isExpired = now > expiresDate;
840
841 return `
842 <tr style="${isExpired ? 'opacity: 0.6;' : ''}">
843 <td><strong>${escapeHtml(rate.fromCurrency)}</strong></td>
844 <td><strong>${escapeHtml(rate.toCurrency)}</strong></td>
845 <td style="font-family: monospace; font-size: 1.1rem;">${rate.rate.toFixed(4)}</td>
846 <td style="color: #666; font-size: 0.9rem;">
847 1 ${rate.fromCurrency} = ${rate.rate.toFixed(2)} ${rate.toCurrency}
848 </td>
849 <td>${new Date(rate.fetchedAt).toLocaleString()}</td>
850 <td>
851 <span class="status-badge ${isExpired ? 'status-cancelled' : 'status-completed'}">
852 ${isExpired ? 'Expired' : new Date(rate.expiresAt).toLocaleTimeString()}
853 </span>
854 </td>
855 </tr>
856 `}).join('')}
857 </tbody>
858 </table>
859 `;
860 } catch (error) {
861 container.innerHTML = `<div class="error">Error: ${error.message}</div>`;
862 }
863 }
864
865 async function refreshExchangeRates() {
866 const btn = event.target;
867 btn.disabled = true;
868 btn.textContent = 'Refreshing...';
869
870 try {
871 const response = await fetchWithCsrf(`${API_BASE}/currency/admin/refresh`, {
872 method: 'POST'
873 });
874
875 const data = await response.json();
876
877 if (!data.success) {
878 throw new Error(data.error || 'Failed to refresh rates');
879 }
880
881 showNotification('Exchange rates refreshed successfully!', 'success');
882
883 // Wait a moment for the rates to be saved
884 setTimeout(() => {
885 loadCurrencyRates();
886 }, 1000);
887 } catch (error) {
888 showNotification(`Error: ${error.message}`, 'error');
889 } finally {
890 btn.disabled = false;
891 btn.textContent = 'Refresh Rates Now';
892 }
893 }
894
895 // Initialize
896 document.addEventListener('DOMContentLoaded', () => {
897 loadProducts();
898
899 // Make functions global
900 window.switchTab = switchTab;
901 window.openProductForm = openProductForm;
902 window.closeProductForm = closeProductForm;
903 window.handleProductSubmit = handleProductSubmit;
904 window.deleteProduct = deleteProduct;
905 window.editProduct = editProduct;
906 window.viewOrder = viewOrder;
907 window.updateOrderStatus = updateOrderStatus;
908 window.handleOrderUpdate = handleOrderUpdate;
909
910 // Region management
911 window.loadRegions = loadRegions;
912 window.toggleRegion = toggleRegion;
913 window.filterRegions = filterRegions;
914
915 // Translation management
916 window.loadTranslations = loadTranslations;
917 window.loadProductTranslations = loadProductTranslations;
918 window.openTranslationForm = openTranslationForm;
919 window.closeTranslationForm = closeTranslationForm;
920 window.handleTranslationSubmit = handleTranslationSubmit;
921 window.deleteTranslation = deleteTranslation;
922
923 // Currency management
924 window.loadCurrencyRates = loadCurrencyRates;
925 window.refreshExchangeRates = refreshExchangeRates;
926 });
927