feat: add Letzshop frontend for admin and vendor portals

Add complete frontend UI for Letzshop marketplace integration:

Admin portal (/admin/letzshop):
- Vendor overview with Letzshop status cards
- Vendor table with configuration state and sync info
- Configuration modal for API credentials
- Connection testing and manual sync triggers
- Orders modal for viewing vendor orders

Vendor portal (/vendor/{code}/letzshop):
- Orders tab with import, confirm, reject actions
- Settings tab for API credentials management
- Tracking modal for shipment updates
- Order details modal with line items
- Stats display for order status counts

Also includes:
- Routes for both admin and vendor Letzshop pages
- Sidebar navigation updates for both portals
- Alpine.js data functions for reactive UI state

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-13 12:40:29 +01:00
parent 448f01f82b
commit 5bcbd14391
8 changed files with 1800 additions and 0 deletions

276
static/admin/js/letzshop.js Normal file
View File

@@ -0,0 +1,276 @@
// static/admin/js/letzshop.js
/**
* Admin Letzshop management page logic
*/
console.log('[ADMIN LETZSHOP] Loading...');
function adminLetzshop() {
console.log('[ADMIN LETZSHOP] adminLetzshop() called');
return {
// Inherit base layout state
...data(),
// Set page identifier
currentPage: 'letzshop',
// Loading states
loading: false,
savingConfig: false,
loadingOrders: false,
// Messages
error: '',
successMessage: '',
// Vendors data
vendors: [],
totalVendors: 0,
page: 1,
limit: 50,
// Filters
filters: {
configuredOnly: false
},
// Stats
stats: {
total: 0,
configured: 0,
autoSync: 0,
pendingOrders: 0
},
// Configuration modal
showConfigModal: false,
selectedVendor: null,
vendorCredentials: null,
configForm: {
api_key: '',
auto_sync_enabled: false,
sync_interval_minutes: 15
},
showApiKey: false,
// Orders modal
showOrdersModal: false,
vendorOrders: [],
async init() {
// Guard against multiple initialization
if (window._adminLetzshopInitialized) {
return;
}
window._adminLetzshopInitialized = true;
console.log('[ADMIN LETZSHOP] Initializing...');
await this.loadVendors();
},
/**
* Load vendors with Letzshop status
*/
async loadVendors() {
this.loading = true;
this.error = '';
try {
const params = new URLSearchParams({
skip: ((this.page - 1) * this.limit).toString(),
limit: this.limit.toString(),
configured_only: this.filters.configuredOnly.toString()
});
const response = await apiClient.get(`/admin/letzshop/vendors?${params}`);
this.vendors = response.vendors || [];
this.totalVendors = response.total || 0;
// Calculate stats
this.stats.total = this.totalVendors;
this.stats.configured = this.vendors.filter(v => v.is_configured).length;
this.stats.autoSync = this.vendors.filter(v => v.auto_sync_enabled).length;
this.stats.pendingOrders = this.vendors.reduce((sum, v) => sum + (v.pending_orders || 0), 0);
console.log('[ADMIN LETZSHOP] Loaded vendors:', this.vendors.length);
} catch (error) {
console.error('[ADMIN LETZSHOP] Failed to load vendors:', error);
this.error = error.message || 'Failed to load vendors';
} finally {
this.loading = false;
}
},
/**
* Refresh all data
*/
async refreshData() {
await this.loadVendors();
this.successMessage = 'Data refreshed';
setTimeout(() => this.successMessage = '', 3000);
},
/**
* Open configuration modal for a vendor
*/
async openConfigModal(vendor) {
this.selectedVendor = vendor;
this.vendorCredentials = null;
this.configForm = {
api_key: '',
auto_sync_enabled: vendor.auto_sync_enabled || false,
sync_interval_minutes: 15
};
this.showApiKey = false;
this.showConfigModal = true;
// Load existing credentials if configured
if (vendor.is_configured) {
try {
const response = await apiClient.get(`/admin/letzshop/vendors/${vendor.vendor_id}/credentials`);
this.vendorCredentials = response;
this.configForm.auto_sync_enabled = response.auto_sync_enabled;
this.configForm.sync_interval_minutes = response.sync_interval_minutes || 15;
} catch (error) {
if (error.status !== 404) {
console.error('[ADMIN LETZSHOP] Failed to load credentials:', error);
}
}
}
},
/**
* Save vendor configuration
*/
async saveVendorConfig() {
if (!this.configForm.api_key && !this.vendorCredentials) {
this.error = 'Please enter an API key';
return;
}
this.savingConfig = true;
this.error = '';
try {
const payload = {
auto_sync_enabled: this.configForm.auto_sync_enabled,
sync_interval_minutes: parseInt(this.configForm.sync_interval_minutes)
};
if (this.configForm.api_key) {
payload.api_key = this.configForm.api_key;
}
await apiClient.post(
`/admin/letzshop/vendors/${this.selectedVendor.vendor_id}/credentials`,
payload
);
this.showConfigModal = false;
this.successMessage = 'Configuration saved successfully';
await this.loadVendors();
} catch (error) {
console.error('[ADMIN LETZSHOP] Failed to save config:', error);
this.error = error.message || 'Failed to save configuration';
} finally {
this.savingConfig = false;
setTimeout(() => this.successMessage = '', 5000);
}
},
/**
* Delete vendor configuration
*/
async deleteVendorConfig() {
if (!confirm('Are you sure you want to remove Letzshop configuration for this vendor?')) {
return;
}
try {
await apiClient.delete(`/admin/letzshop/vendors/${this.selectedVendor.vendor_id}/credentials`);
this.showConfigModal = false;
this.successMessage = 'Configuration removed';
await this.loadVendors();
} catch (error) {
console.error('[ADMIN LETZSHOP] Failed to delete config:', error);
this.error = error.message || 'Failed to remove configuration';
}
setTimeout(() => this.successMessage = '', 5000);
},
/**
* Test connection for a vendor
*/
async testConnection(vendor) {
this.error = '';
try {
const response = await apiClient.post(`/admin/letzshop/vendors/${vendor.vendor_id}/test`);
if (response.success) {
this.successMessage = `Connection successful for ${vendor.vendor_name} (${response.response_time_ms?.toFixed(0)}ms)`;
} else {
this.error = response.error_details || 'Connection failed';
}
} catch (error) {
console.error('[ADMIN LETZSHOP] Connection test failed:', error);
this.error = error.message || 'Connection test failed';
}
setTimeout(() => this.successMessage = '', 5000);
},
/**
* Trigger sync for a vendor
*/
async triggerSync(vendor) {
this.error = '';
try {
const response = await apiClient.post(`/admin/letzshop/vendors/${vendor.vendor_id}/sync`);
if (response.success) {
this.successMessage = response.message || 'Sync completed';
await this.loadVendors();
} else {
this.error = response.message || 'Sync failed';
}
} catch (error) {
console.error('[ADMIN LETZSHOP] Sync failed:', error);
this.error = error.message || 'Sync failed';
}
setTimeout(() => this.successMessage = '', 5000);
},
/**
* View orders for a vendor
*/
async viewOrders(vendor) {
this.selectedVendor = vendor;
this.vendorOrders = [];
this.loadingOrders = true;
this.showOrdersModal = true;
try {
const response = await apiClient.get(`/admin/letzshop/vendors/${vendor.vendor_id}/orders?limit=100`);
this.vendorOrders = response.orders || [];
} catch (error) {
console.error('[ADMIN LETZSHOP] Failed to load orders:', error);
this.error = error.message || 'Failed to load orders';
} finally {
this.loadingOrders = false;
}
},
/**
* 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' });
}
};
}
console.log('[ADMIN LETZSHOP] Module loaded');

415
static/vendor/js/letzshop.js vendored Normal file
View File

@@ -0,0 +1,415 @@
// 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: ''
},
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' });
}
};
}