- Add database fields for language preferences: - Vendor: dashboard_language, storefront_language, storefront_languages - User: preferred_language - Customer: preferred_language - Add language middleware for request-level language detection: - Cookie-based persistence - Browser Accept-Language fallback - Vendor storefront language constraints - Add language API endpoints (/api/v1/language/*): - POST /set - Set language preference - GET /current - Get current language info - GET /list - List available languages - DELETE /clear - Clear preference - Add i18n utilities (app/utils/i18n.py): - JSON-based translation loading - Jinja2 template integration - Language resolution helpers - Add reusable language selector macros for templates - Add languageSelector() Alpine.js component - Add translation files (en, fr, de, lb) in static/locales/ - Add architecture rules documentation for language implementation - Update marketplace-product-detail.js to use native language names 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
483 lines
16 KiB
JavaScript
483 lines
16 KiB
JavaScript
// static/vendor/js/letzshop.js
|
|
/**
|
|
* Vendor Letzshop orders management page logic
|
|
*/
|
|
|
|
console.log('[VENDOR LETZSHOP] Loading...');
|
|
|
|
function vendorLetzshop() {
|
|
console.log('[VENDOR LETZSHOP] vendorLetzshop() called');
|
|
|
|
return {
|
|
// Inherit base layout state
|
|
...data(),
|
|
|
|
// Set page identifier
|
|
currentPage: 'letzshop',
|
|
|
|
// Tab state
|
|
activeTab: 'orders',
|
|
|
|
// Loading states
|
|
loading: false,
|
|
importing: false,
|
|
saving: false,
|
|
testing: false,
|
|
submittingTracking: false,
|
|
|
|
// Messages
|
|
error: '',
|
|
successMessage: '',
|
|
|
|
// Integration status
|
|
status: {
|
|
is_configured: false,
|
|
is_connected: false,
|
|
auto_sync_enabled: false,
|
|
last_sync_at: null,
|
|
last_sync_status: null
|
|
},
|
|
|
|
// Credentials
|
|
credentials: null,
|
|
credentialsForm: {
|
|
api_key: '',
|
|
auto_sync_enabled: false,
|
|
sync_interval_minutes: 15
|
|
},
|
|
showApiKey: false,
|
|
|
|
// Orders
|
|
orders: [],
|
|
totalOrders: 0,
|
|
page: 1,
|
|
limit: 20,
|
|
filters: {
|
|
sync_status: ''
|
|
},
|
|
|
|
// Order stats
|
|
orderStats: {
|
|
pending: 0,
|
|
confirmed: 0,
|
|
rejected: 0,
|
|
shipped: 0
|
|
},
|
|
|
|
// Modals
|
|
showTrackingModal: false,
|
|
showOrderModal: false,
|
|
selectedOrder: null,
|
|
trackingForm: {
|
|
tracking_number: '',
|
|
tracking_carrier: ''
|
|
},
|
|
|
|
// Export
|
|
exportLanguage: 'fr',
|
|
exportIncludeInactive: false,
|
|
exporting: false,
|
|
|
|
async init() {
|
|
// Guard against multiple initialization
|
|
if (window._vendorLetzshopInitialized) {
|
|
return;
|
|
}
|
|
window._vendorLetzshopInitialized = true;
|
|
|
|
// Call parent init first to set vendorCode from URL
|
|
const parentInit = data().init;
|
|
if (parentInit) {
|
|
await parentInit.call(this);
|
|
}
|
|
|
|
await this.loadStatus();
|
|
await this.loadOrders();
|
|
},
|
|
|
|
/**
|
|
* Load integration status
|
|
*/
|
|
async loadStatus() {
|
|
try {
|
|
const response = await apiClient.get('/vendor/letzshop/status');
|
|
this.status = response;
|
|
|
|
if (this.status.is_configured) {
|
|
await this.loadCredentials();
|
|
}
|
|
} catch (error) {
|
|
console.error('[VENDOR LETZSHOP] Failed to load status:', error);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load credentials (masked)
|
|
*/
|
|
async loadCredentials() {
|
|
try {
|
|
const response = await apiClient.get('/vendor/letzshop/credentials');
|
|
this.credentials = response;
|
|
this.credentialsForm.auto_sync_enabled = response.auto_sync_enabled;
|
|
this.credentialsForm.sync_interval_minutes = response.sync_interval_minutes;
|
|
} catch (error) {
|
|
// 404 means not configured, which is fine
|
|
if (error.status !== 404) {
|
|
console.error('[VENDOR LETZSHOP] Failed to load credentials:', error);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load orders
|
|
*/
|
|
async loadOrders() {
|
|
this.loading = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
skip: ((this.page - 1) * this.limit).toString(),
|
|
limit: this.limit.toString()
|
|
});
|
|
|
|
if (this.filters.sync_status) {
|
|
params.append('sync_status', this.filters.sync_status);
|
|
}
|
|
|
|
const response = await apiClient.get(`/vendor/letzshop/orders?${params}`);
|
|
this.orders = response.orders;
|
|
this.totalOrders = response.total;
|
|
|
|
// Calculate stats
|
|
await this.loadOrderStats();
|
|
} catch (error) {
|
|
console.error('[VENDOR LETZSHOP] Failed to load orders:', error);
|
|
this.error = error.message || 'Failed to load orders';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load order stats by fetching counts for each status
|
|
*/
|
|
async loadOrderStats() {
|
|
try {
|
|
// Get all orders without filter to calculate stats
|
|
const allResponse = await apiClient.get('/vendor/letzshop/orders?limit=1000');
|
|
const allOrders = allResponse.orders || [];
|
|
|
|
this.orderStats = {
|
|
pending: allOrders.filter(o => o.sync_status === 'pending').length,
|
|
confirmed: allOrders.filter(o => o.sync_status === 'confirmed').length,
|
|
rejected: allOrders.filter(o => o.sync_status === 'rejected').length,
|
|
shipped: allOrders.filter(o => o.sync_status === 'shipped').length
|
|
};
|
|
} catch (error) {
|
|
console.error('[VENDOR LETZSHOP] Failed to load order stats:', error);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Refresh all data
|
|
*/
|
|
async refreshData() {
|
|
await this.loadStatus();
|
|
await this.loadOrders();
|
|
this.successMessage = 'Data refreshed';
|
|
setTimeout(() => this.successMessage = '', 3000);
|
|
},
|
|
|
|
/**
|
|
* Import orders from Letzshop
|
|
*/
|
|
async importOrders() {
|
|
if (!this.status.is_configured) {
|
|
this.error = 'Please configure your API key first';
|
|
this.activeTab = 'settings';
|
|
return;
|
|
}
|
|
|
|
this.importing = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
const response = await apiClient.post('/vendor/letzshop/orders/import', {
|
|
operation: 'order_import'
|
|
});
|
|
|
|
if (response.success) {
|
|
this.successMessage = response.message;
|
|
await this.loadOrders();
|
|
} else {
|
|
this.error = response.message || 'Import failed';
|
|
}
|
|
} catch (error) {
|
|
console.error('[VENDOR LETZSHOP] Import failed:', error);
|
|
this.error = error.message || 'Failed to import orders';
|
|
} finally {
|
|
this.importing = false;
|
|
setTimeout(() => this.successMessage = '', 5000);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Save credentials
|
|
*/
|
|
async saveCredentials() {
|
|
if (!this.credentialsForm.api_key && !this.credentials) {
|
|
this.error = 'Please enter an API key';
|
|
return;
|
|
}
|
|
|
|
this.saving = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
const payload = {
|
|
auto_sync_enabled: this.credentialsForm.auto_sync_enabled,
|
|
sync_interval_minutes: parseInt(this.credentialsForm.sync_interval_minutes)
|
|
};
|
|
|
|
if (this.credentialsForm.api_key) {
|
|
payload.api_key = this.credentialsForm.api_key;
|
|
}
|
|
|
|
const response = await apiClient.post('/vendor/letzshop/credentials', payload);
|
|
this.credentials = response;
|
|
this.credentialsForm.api_key = '';
|
|
this.status.is_configured = true;
|
|
this.successMessage = 'Credentials saved successfully';
|
|
} catch (error) {
|
|
console.error('[VENDOR LETZSHOP] Failed to save credentials:', error);
|
|
this.error = error.message || 'Failed to save credentials';
|
|
} finally {
|
|
this.saving = false;
|
|
setTimeout(() => this.successMessage = '', 5000);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Test connection
|
|
*/
|
|
async testConnection() {
|
|
this.testing = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
const response = await apiClient.post('/vendor/letzshop/test');
|
|
|
|
if (response.success) {
|
|
this.successMessage = `Connection successful (${response.response_time_ms?.toFixed(0)}ms)`;
|
|
} else {
|
|
this.error = response.error_details || 'Connection failed';
|
|
}
|
|
} catch (error) {
|
|
console.error('[VENDOR LETZSHOP] Connection test failed:', error);
|
|
this.error = error.message || 'Connection test failed';
|
|
} finally {
|
|
this.testing = false;
|
|
setTimeout(() => this.successMessage = '', 5000);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Delete credentials
|
|
*/
|
|
async deleteCredentials() {
|
|
if (!confirm('Are you sure you want to remove your Letzshop credentials?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await apiClient.delete('/vendor/letzshop/credentials');
|
|
this.credentials = null;
|
|
this.status.is_configured = false;
|
|
this.credentialsForm = {
|
|
api_key: '',
|
|
auto_sync_enabled: false,
|
|
sync_interval_minutes: 15
|
|
};
|
|
this.successMessage = 'Credentials removed';
|
|
} catch (error) {
|
|
console.error('[VENDOR LETZSHOP] Failed to delete credentials:', error);
|
|
this.error = error.message || 'Failed to remove credentials';
|
|
}
|
|
setTimeout(() => this.successMessage = '', 5000);
|
|
},
|
|
|
|
/**
|
|
* Confirm order
|
|
*/
|
|
async confirmOrder(order) {
|
|
if (!confirm('Confirm this order?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await apiClient.post(`/vendor/letzshop/orders/${order.id}/confirm`);
|
|
|
|
if (response.success) {
|
|
this.successMessage = 'Order confirmed';
|
|
await this.loadOrders();
|
|
} else {
|
|
this.error = response.message || 'Failed to confirm order';
|
|
}
|
|
} catch (error) {
|
|
console.error('[VENDOR LETZSHOP] Failed to confirm order:', error);
|
|
this.error = error.message || 'Failed to confirm order';
|
|
}
|
|
setTimeout(() => this.successMessage = '', 5000);
|
|
},
|
|
|
|
/**
|
|
* Reject order
|
|
*/
|
|
async rejectOrder(order) {
|
|
if (!confirm('Reject this order? This action cannot be undone.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await apiClient.post(`/vendor/letzshop/orders/${order.id}/reject`);
|
|
|
|
if (response.success) {
|
|
this.successMessage = 'Order rejected';
|
|
await this.loadOrders();
|
|
} else {
|
|
this.error = response.message || 'Failed to reject order';
|
|
}
|
|
} catch (error) {
|
|
console.error('[VENDOR LETZSHOP] Failed to reject order:', error);
|
|
this.error = error.message || 'Failed to reject order';
|
|
}
|
|
setTimeout(() => this.successMessage = '', 5000);
|
|
},
|
|
|
|
/**
|
|
* Open tracking modal
|
|
*/
|
|
openTrackingModal(order) {
|
|
this.selectedOrder = order;
|
|
this.trackingForm = {
|
|
tracking_number: order.tracking_number || '',
|
|
tracking_carrier: order.tracking_carrier || ''
|
|
};
|
|
this.showTrackingModal = true;
|
|
},
|
|
|
|
/**
|
|
* Submit tracking
|
|
*/
|
|
async submitTracking() {
|
|
if (!this.trackingForm.tracking_number || !this.trackingForm.tracking_carrier) {
|
|
this.error = 'Please fill in all fields';
|
|
return;
|
|
}
|
|
|
|
this.submittingTracking = true;
|
|
|
|
try {
|
|
const response = await apiClient.post(
|
|
`/vendor/letzshop/orders/${this.selectedOrder.id}/tracking`,
|
|
this.trackingForm
|
|
);
|
|
|
|
if (response.success) {
|
|
this.showTrackingModal = false;
|
|
this.successMessage = 'Tracking information saved';
|
|
await this.loadOrders();
|
|
} else {
|
|
this.error = response.message || 'Failed to save tracking';
|
|
}
|
|
} catch (error) {
|
|
console.error('[VENDOR LETZSHOP] Failed to set tracking:', error);
|
|
this.error = error.message || 'Failed to save tracking';
|
|
} finally {
|
|
this.submittingTracking = false;
|
|
}
|
|
setTimeout(() => this.successMessage = '', 5000);
|
|
},
|
|
|
|
/**
|
|
* View order details
|
|
*/
|
|
viewOrderDetails(order) {
|
|
this.selectedOrder = order;
|
|
this.showOrderModal = true;
|
|
},
|
|
|
|
/**
|
|
* Format date for display
|
|
*/
|
|
formatDate(dateStr) {
|
|
if (!dateStr) return 'N/A';
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
},
|
|
|
|
/**
|
|
* Download product export CSV
|
|
*/
|
|
async downloadExport() {
|
|
this.exporting = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
language: this.exportLanguage,
|
|
include_inactive: this.exportIncludeInactive.toString()
|
|
});
|
|
|
|
// Get the token for authentication
|
|
const token = localStorage.getItem('wizamart_token');
|
|
if (!token) {
|
|
throw new Error('Not authenticated');
|
|
}
|
|
|
|
const response = await fetch(`/api/v1/vendor/letzshop/export?${params}`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.detail || 'Export failed');
|
|
}
|
|
|
|
// Get filename from Content-Disposition header
|
|
const contentDisposition = response.headers.get('Content-Disposition');
|
|
let filename = `letzshop_export_${this.exportLanguage}.csv`;
|
|
if (contentDisposition) {
|
|
const match = contentDisposition.match(/filename="(.+)"/);
|
|
if (match) {
|
|
filename = match[1];
|
|
}
|
|
}
|
|
|
|
// Download the file
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = filename;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
this.successMessage = `Export downloaded: ${filename}`;
|
|
} catch (error) {
|
|
console.error('[VENDOR LETZSHOP] Export failed:', error);
|
|
this.error = error.message || 'Failed to export products';
|
|
} finally {
|
|
this.exporting = false;
|
|
setTimeout(() => this.successMessage = '', 5000);
|
|
}
|
|
}
|
|
};
|
|
}
|