From 5bcbd143917af780d9f296d4a815ec76901ed41a Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Sat, 13 Dec 2025 12:40:29 +0100 Subject: [PATCH] feat: add Letzshop frontend for admin and vendor portals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/routes/admin_pages.py | 19 + app/routes/vendor_pages.py | 27 + app/templates/admin/letzshop.html | 452 ++++++++++++++++ app/templates/admin/partials/sidebar.html | 1 + app/templates/vendor/letzshop.html | 599 +++++++++++++++++++++ app/templates/vendor/partials/sidebar.html | 11 + static/admin/js/letzshop.js | 276 ++++++++++ static/vendor/js/letzshop.js | 415 ++++++++++++++ 8 files changed, 1800 insertions(+) create mode 100644 app/templates/admin/letzshop.html create mode 100644 app/templates/vendor/letzshop.html create mode 100644 static/admin/js/letzshop.js create mode 100644 static/vendor/js/letzshop.js diff --git a/app/routes/admin_pages.py b/app/routes/admin_pages.py index 75f6a77a..04a12992 100644 --- a/app/routes/admin_pages.py +++ b/app/routes/admin_pages.py @@ -538,6 +538,25 @@ async def admin_marketplace_page( ) +@router.get("/letzshop", response_class=HTMLResponse, include_in_schema=False) +async def admin_letzshop_page( + request: Request, + current_user: User = Depends(get_current_admin_from_cookie_or_header), + db: Session = Depends(get_db), +): + """ + Render Letzshop management page. + Admin overview of Letzshop integration for all vendors. + """ + return templates.TemplateResponse( + "admin/letzshop.html", + { + "request": request, + "user": current_user, + }, + ) + + # ============================================================================ # PRODUCT CATALOG ROUTES # ============================================================================ diff --git a/app/routes/vendor_pages.py b/app/routes/vendor_pages.py index e6cc71d5..b9a53e14 100644 --- a/app/routes/vendor_pages.py +++ b/app/routes/vendor_pages.py @@ -273,6 +273,33 @@ async def vendor_marketplace_page( ) +# ============================================================================ +# LETZSHOP INTEGRATION +# ============================================================================ + + +@router.get( + "/{vendor_code}/letzshop", response_class=HTMLResponse, include_in_schema=False +) +async def vendor_letzshop_page( + request: Request, + vendor_code: str = Path(..., description="Vendor code"), + current_user: User = Depends(get_current_vendor_from_cookie_or_header), +): + """ + Render Letzshop integration page. + JavaScript loads orders, credentials status, and handles fulfillment operations. + """ + return templates.TemplateResponse( + "vendor/letzshop.html", + { + "request": request, + "user": current_user, + "vendor_code": vendor_code, + }, + ) + + # ============================================================================ # TEAM MANAGEMENT # ============================================================================ diff --git a/app/templates/admin/letzshop.html b/app/templates/admin/letzshop.html new file mode 100644 index 00000000..3d610b5b --- /dev/null +++ b/app/templates/admin/letzshop.html @@ -0,0 +1,452 @@ +{# app/templates/admin/letzshop.html #} +{% extends "admin/base.html" %} + +{% block title %}Letzshop Management{% endblock %} + +{% block alpine_data %}adminLetzshop(){% endblock %} + +{% block extra_scripts %} + +{% endblock %} + +{% block content %} + +
+
+

+ Letzshop Management +

+

+ Manage Letzshop integration for all vendors +

+
+ +
+ + +
+ +
+

+
+ +
+ + +
+ +
+

Error

+

+
+ +
+ + +
+ +
+
+ +
+
+

Total Vendors

+

+
+
+ + +
+
+ +
+
+

Configured

+

+
+
+ + +
+
+ +
+
+

Auto-Sync

+

+
+
+ + +
+
+ +
+
+

Pending Orders

+

+
+
+
+ + +
+ +
+ + +
+
+ + + + + + + + + + + + + + + + +
VendorStatusAuto-SyncLast SyncOrdersActions
+
+ +
+ + Showing - of + + + + + +
+
+ + +
+
+
+

+ Configure Letzshop - +

+ +
+ +
+
+ +
+ +
+ + +
+
+ + +
+ +
+ + +
+ + +
+
+ +
+ +
+ + +
+
+
+
+
+ + +
+
+
+

+ Orders - +

+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + +
OrderCustomerTotalStatusDate
+

No orders found

+
+
+
+{% endblock %} diff --git a/app/templates/admin/partials/sidebar.html b/app/templates/admin/partials/sidebar.html index b0bcf87a..2e74cc1e 100644 --- a/app/templates/admin/partials/sidebar.html +++ b/app/templates/admin/partials/sidebar.html @@ -81,6 +81,7 @@ {{ menu_item('marketplace-products', '/admin/marketplace-products', 'database', 'Marketplace Products') }} {{ menu_item('vendor-products', '/admin/vendor-products', 'cube', 'Vendor Products') }} {{ menu_item('marketplace', '/admin/marketplace', 'cloud-download', 'Import') }} + {{ menu_item('letzshop', '/admin/letzshop', 'shopping-cart', 'Letzshop Orders') }} {% endcall %} diff --git a/app/templates/vendor/letzshop.html b/app/templates/vendor/letzshop.html new file mode 100644 index 00000000..66019a51 --- /dev/null +++ b/app/templates/vendor/letzshop.html @@ -0,0 +1,599 @@ +{# app/templates/vendor/letzshop.html #} +{% extends "vendor/base.html" %} + +{% block title %}Letzshop Orders{% endblock %} + +{% block alpine_data %}vendorLetzshop(){% endblock %} + +{% block extra_scripts %} + +{% endblock %} + +{% block content %} + +
+
+

+ Letzshop Orders +

+

+ Manage orders from Letzshop marketplace +

+
+
+ + +
+
+ + +
+ +
+

+
+ +
+ + +
+ +
+

Error

+

+
+ +
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+ +
+
+

Connection

+

+
+
+ + +
+
+ +
+
+

Pending

+

+
+
+ + +
+
+ +
+
+

Confirmed

+

+
+
+ + +
+
+ +
+
+

Shipped

+

+
+
+
+ + +
+ +
+ + +
+
+ + + + + + + + + + + + + + + + +
OrderCustomerTotalStatusDateActions
+
+ +
+ + Showing - of + + + + + +
+
+
+ + +
+
+
+

+ Letzshop API Configuration +

+ +
+
+ +
+ +
+ + +
+

+ Get your API key from the Letzshop merchant portal +

+
+ + +
+ +

+ Automatically import new orders periodically +

+
+ + +
+ + +
+
+ + +
+

Last Sync

+
+

+ Status: + +

+

+ Time: + +

+

+ Error: + +

+
+
+ + +
+ + + + + +
+
+
+
+
+ + +
+
+
+

Set Tracking Information

+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ + +
+
+
+

Order Details

+ +
+ +
+
+
+ Order Number: + +
+
+ Status: + +
+
+ Customer: + +
+
+ Total: + +
+
+ Tracking: + +
+
+ Created: + +
+
+ +
+

Items

+
+ +
+
+
+
+
+{% endblock %} diff --git a/app/templates/vendor/partials/sidebar.html b/app/templates/vendor/partials/sidebar.html index 8fe9dac7..9d5361c8 100644 --- a/app/templates/vendor/partials/sidebar.html +++ b/app/templates/vendor/partials/sidebar.html @@ -95,6 +95,17 @@ Follows same pattern as admin sidebar Orders +
  • + + + + Letzshop Orders + +
  • 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'); diff --git a/static/vendor/js/letzshop.js b/static/vendor/js/letzshop.js new file mode 100644 index 00000000..a306fb5a --- /dev/null +++ b/static/vendor/js/letzshop.js @@ -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' }); + } + }; +}