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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Vendor |
+ Status |
+ Auto-Sync |
+ Last Sync |
+ Orders |
+ Actions |
+
+
+
+
+
+ |
+
+ Loading vendors...
+ |
+
+
+
+
+ |
+
+ No vendors found
+ |
+
+
+
+
+ |
+
+ |
+
+
+ |
+
+
+ Enabled
+
+
+ Disabled
+
+ |
+
+
+ Never
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+ Showing - of
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Configure Letzshop -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Order |
+ Customer |
+ Total |
+ Status |
+ Date |
+
+
+
+
+
+ |
+ |
+ |
+
+
+ |
+ |
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Order |
+ Customer |
+ Total |
+ Status |
+ Date |
+ Actions |
+
+
+
+
+
+ |
+
+ Loading orders...
+ |
+
+
+
+
+ |
+
+ No orders found
+ Click "Import Orders" to fetch orders from Letzshop
+ Configure your API key in Settings to get started
+ |
+
+
+
+
+ |
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+ Showing - of
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Letzshop API Configuration
+
+
+
+
+
+
+
+
+
+
+
+ Set Tracking Information
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Order Number:
+
+
+
+ Status:
+
+
+
+ Customer:
+
+
+
+ Total:
+
+
+
+ Tracking:
+
+
+
+ Created:
+
+
+
+
+
+
+
+
+{% 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' });
+ }
+ };
+}