-
👨💼
-
- Team Management Coming Soon
-
-
- This page is under development. You'll be able to manage your team members here.
-
-
- Back to Dashboard
-
+{{ loading_state('Loading team...') }}
+
+{{ error_state('Error loading team') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Pending Invitations
+
0
+
+
+
+
+
+
+
+
+ | Member |
+ Role |
+ Status |
+ Joined |
+ Actions |
+
+
+
+
+
+
+ |
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+ No team members yet
+ Invite your first team member to get started
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
Invite Team Member
+
+
+
+
+
+
+
+
+
+
+
+{{ modal_simple(
+ show_var='showEditModal',
+ title='Edit Team Member',
+ icon='pencil',
+ icon_color='blue',
+ confirm_text='Save',
+ confirm_class='bg-purple-600 hover:bg-purple-700 focus:shadow-outline-purple',
+ confirm_fn='updateMember()',
+ loading_var='saving'
+) }}
+
+
+
+ Edit
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{ modal_simple(
+ show_var='showRemoveModal',
+ title='Remove Team Member',
+ icon='exclamation-triangle',
+ icon_color='red',
+ confirm_text='Remove',
+ confirm_class='bg-red-600 hover:bg-red-700 focus:shadow-outline-red',
+ confirm_fn='removeMember()',
+ loading_var='saving'
+) }}
+
+
+ Are you sure you want to remove from the team?
+ They will lose access to this vendor.
+
+
+{% endblock %}
+
+{% block extra_scripts %}
+
{% endblock %}
diff --git a/docs/implementation/vendor-frontend-parity-plan.md b/docs/implementation/vendor-frontend-parity-plan.md
new file mode 100644
index 00000000..6dc160cd
--- /dev/null
+++ b/docs/implementation/vendor-frontend-parity-plan.md
@@ -0,0 +1,148 @@
+# Vendor Frontend Parity Plan
+
+**Created:** January 1, 2026
+**Status:** In Progress
+
+## Executive Summary
+
+The vendor frontend is now approximately 90% complete compared to admin. Phase 1 (Sidebar Refactor) and Phase 2 (Core JS Files) are complete. Only Phase 3 (New Features like notifications and analytics) remains.
+
+---
+
+## Phase 1: Sidebar Refactor ✅ COMPLETED
+
+### Goals
+- ✅ Refactor vendor sidebar to use Jinja2 macros (like admin)
+- ✅ Add collapsible sections with Alpine.js
+- ✅ Reorganize into logical groups
+- ✅ Add localStorage for section state persistence
+- ✅ Complete mobile sidebar implementation
+
+### New Sidebar Structure
+```
+Dashboard
+├── Products & Inventory (collapsible)
+│ ├── All Products
+│ ├── Inventory
+│ └── Marketplace Import
+├── Sales & Orders (collapsible)
+│ ├── Orders
+│ ├── Letzshop Orders
+│ └── Invoices
+├── Customers & Communication (collapsible)
+│ ├── Customers
+│ └── Messages
+├── Shop & Content (collapsible)
+│ └── Content Pages
+└── Account & Settings (collapsible)
+ ├── Team
+ ├── Profile
+ ├── Billing
+ └── Settings
+```
+
+### Files to Modify
+- `app/templates/vendor/partials/sidebar.html` - Main refactor
+- `static/vendor/js/init-alpine.js` - Add sidebar state management
+
+---
+
+## Phase 2: Core JavaScript Files
+
+### Priority 1 (Critical)
+| File | Purpose | Effort |
+|------|---------|--------|
+| `products.js` | Product CRUD, search, filtering, bulk ops | 4-6 hours |
+| `orders.js` | Order list, filtering, status management | 4-6 hours |
+| `inventory.js` | Stock tracking, adjustments, alerts | 3-4 hours |
+| `customers.js` | Customer list, purchase history | 3-4 hours |
+
+### Priority 2 (High)
+| File | Purpose | Effort |
+|------|---------|--------|
+| `team.js` | Member invite, role management | 2-3 hours |
+| `profile.js` | Profile editing, avatar upload | 2-3 hours |
+| `settings.js` | Settings forms, preferences | 2-3 hours |
+| `content-pages.js` | CMS page management | 3-4 hours |
+
+---
+
+## Phase 3: New Features
+
+### Priority 3 (Medium)
+- Add notifications center (page + JS)
+- Add analytics/reports page
+- Add bulk operations across pages
+
+### Priority 4 (Low)
+- Standardize API response handling
+- Add loading states consistently
+- Implement pagination for large lists
+- Add confirmation dialogs
+
+---
+
+## Feature Parity Matrix
+
+| Feature | Admin | Vendor | Status |
+|---------|:-----:|:------:|--------|
+| Dashboard | ✅ | ✅ | Complete |
+| Products | ✅ | ✅ | Complete |
+| Orders | ✅ | ✅ | Complete |
+| Customers | ✅ | ✅ | Complete |
+| Inventory | ✅ | ✅ | Complete |
+| Messages | ✅ | ✅ | Complete |
+| Billing | ✅ | ✅ | Complete |
+| Team | - | ✅ | Complete |
+| Profile | - | ✅ | Complete |
+| Settings | ✅ | ✅ | Complete |
+| Content Pages | ✅ | ✅ | Complete |
+| Notifications | ✅ | ❌ | Missing page + JS |
+| Analytics | ✅ | ❌ | Missing page |
+
+---
+
+## JavaScript Files Comparison
+
+| Type | Admin | Vendor | Target |
+|------|-------|--------|--------|
+| Total JS Files | 52 | 18 | 20+ |
+| Page Coverage | ~90% | ~90% | 90%+ |
+
+---
+
+## Timeline
+
+| Phase | Tasks | Effort |
+|-------|-------|--------|
+| Phase 1 | Sidebar refactor | 2-3 hours |
+| Phase 2 | Core JS files (8) | 2-3 days |
+| Phase 3 | New features | 2-3 days |
+| **Total** | | **5-7 days** |
+
+---
+
+## Progress Tracking
+
+### Phase 1: Sidebar Refactor ✅
+- [x] Read admin sidebar for patterns
+- [x] Create vendor sidebar macros
+- [x] Implement collapsible sections
+- [x] Add localStorage persistence
+- [x] Complete mobile sidebar
+- [x] Test all states
+
+### Phase 2: Core JS Files ✅
+- [x] products.js
+- [x] orders.js
+- [x] inventory.js
+- [x] customers.js
+- [x] team.js
+- [x] profile.js
+- [x] settings.js
+- [x] content-pages.js (already exists)
+
+### Phase 3: New Features
+- [ ] Notifications center
+- [ ] Analytics page
+- [ ] Bulk operations
diff --git a/mkdocs.yml b/mkdocs.yml
index bb483f5e..3b3cbd5b 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -166,6 +166,7 @@ nav:
- Unified Order View: implementation/unified-order-view.md
- VAT Invoice Feature: implementation/vat-invoice-feature.md
- OMS Feature Plan: implementation/oms-feature-plan.md
+ - Vendor Frontend Parity: implementation/vendor-frontend-parity-plan.md
# --- Testing ---
- Testing:
diff --git a/static/vendor/js/customers.js b/static/vendor/js/customers.js
new file mode 100644
index 00000000..d00e33af
--- /dev/null
+++ b/static/vendor/js/customers.js
@@ -0,0 +1,309 @@
+// static/vendor/js/customers.js
+/**
+ * Vendor customers management page logic
+ * View and manage customer relationships
+ */
+
+const vendorCustomersLog = window.LogConfig.loggers.vendorCustomers ||
+ window.LogConfig.createLogger('vendorCustomers', false);
+
+vendorCustomersLog.info('Loading...');
+
+function vendorCustomers() {
+ vendorCustomersLog.info('vendorCustomers() called');
+
+ return {
+ // Inherit base layout state
+ ...data(),
+
+ // Set page identifier
+ currentPage: 'customers',
+
+ // Loading states
+ loading: true,
+ error: '',
+ saving: false,
+
+ // Customers data
+ customers: [],
+ stats: {
+ total: 0,
+ active: 0,
+ new_this_month: 0
+ },
+
+ // Filters
+ filters: {
+ search: '',
+ status: ''
+ },
+
+ // Pagination
+ pagination: {
+ page: 1,
+ per_page: 20,
+ total: 0,
+ pages: 0
+ },
+
+ // Modal states
+ showDetailModal: false,
+ showOrdersModal: false,
+ selectedCustomer: null,
+ customerOrders: [],
+
+ // Debounce timer
+ searchTimeout: null,
+
+ // Computed: Total pages
+ get totalPages() {
+ return this.pagination.pages;
+ },
+
+ // Computed: Start index for pagination display
+ get startIndex() {
+ if (this.pagination.total === 0) return 0;
+ return (this.pagination.page - 1) * this.pagination.per_page + 1;
+ },
+
+ // Computed: End index for pagination display
+ get endIndex() {
+ const end = this.pagination.page * this.pagination.per_page;
+ return end > this.pagination.total ? this.pagination.total : end;
+ },
+
+ // Computed: Page numbers for pagination
+ get pageNumbers() {
+ const pages = [];
+ const totalPages = this.totalPages;
+ const current = this.pagination.page;
+
+ if (totalPages <= 7) {
+ for (let i = 1; i <= totalPages; i++) {
+ pages.push(i);
+ }
+ } else {
+ pages.push(1);
+ if (current > 3) pages.push('...');
+ const start = Math.max(2, current - 1);
+ const end = Math.min(totalPages - 1, current + 1);
+ for (let i = start; i <= end; i++) {
+ pages.push(i);
+ }
+ if (current < totalPages - 2) pages.push('...');
+ pages.push(totalPages);
+ }
+ return pages;
+ },
+
+ async init() {
+ vendorCustomersLog.info('Customers init() called');
+
+ // Guard against multiple initialization
+ if (window._vendorCustomersInitialized) {
+ vendorCustomersLog.warn('Already initialized, skipping');
+ return;
+ }
+ window._vendorCustomersInitialized = true;
+
+ // Load platform settings for rows per page
+ if (window.PlatformSettings) {
+ this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
+ }
+
+ try {
+ await this.loadCustomers();
+ } catch (error) {
+ vendorCustomersLog.error('Init failed:', error);
+ this.error = 'Failed to initialize customers page';
+ }
+
+ vendorCustomersLog.info('Customers initialization complete');
+ },
+
+ /**
+ * Load customers with filtering and pagination
+ */
+ async loadCustomers() {
+ this.loading = true;
+ this.error = '';
+
+ try {
+ const params = new URLSearchParams({
+ skip: (this.pagination.page - 1) * this.pagination.per_page,
+ limit: this.pagination.per_page
+ });
+
+ // Add filters
+ if (this.filters.search) {
+ params.append('search', this.filters.search);
+ }
+ if (this.filters.status) {
+ params.append('status', this.filters.status);
+ }
+
+ const response = await apiClient.get(`/vendor/${this.vendorCode}/customers?${params.toString()}`);
+
+ this.customers = response.customers || [];
+ this.pagination.total = response.total || 0;
+ this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page);
+
+ // Calculate stats
+ this.stats = {
+ total: this.pagination.total,
+ active: this.customers.filter(c => c.is_active !== false).length,
+ new_this_month: this.customers.filter(c => {
+ if (!c.created_at) return false;
+ const created = new Date(c.created_at);
+ const now = new Date();
+ return created.getMonth() === now.getMonth() && created.getFullYear() === now.getFullYear();
+ }).length
+ };
+
+ vendorCustomersLog.info('Loaded customers:', this.customers.length, 'of', this.pagination.total);
+ } catch (error) {
+ vendorCustomersLog.error('Failed to load customers:', error);
+ this.error = error.message || 'Failed to load customers';
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ /**
+ * Debounced search handler
+ */
+ debouncedSearch() {
+ clearTimeout(this.searchTimeout);
+ this.searchTimeout = setTimeout(() => {
+ this.pagination.page = 1;
+ this.loadCustomers();
+ }, 300);
+ },
+
+ /**
+ * Apply filter and reload
+ */
+ applyFilter() {
+ this.pagination.page = 1;
+ this.loadCustomers();
+ },
+
+ /**
+ * Clear all filters
+ */
+ clearFilters() {
+ this.filters = {
+ search: '',
+ status: ''
+ };
+ this.pagination.page = 1;
+ this.loadCustomers();
+ },
+
+ /**
+ * View customer details
+ */
+ async viewCustomer(customer) {
+ this.loading = true;
+ try {
+ const response = await apiClient.get(`/vendor/${this.vendorCode}/customers/${customer.id}`);
+ this.selectedCustomer = response;
+ this.showDetailModal = true;
+ vendorCustomersLog.info('Loaded customer details:', customer.id);
+ } catch (error) {
+ vendorCustomersLog.error('Failed to load customer details:', error);
+ Utils.showToast(error.message || 'Failed to load customer details', 'error');
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ /**
+ * View customer orders
+ */
+ async viewCustomerOrders(customer) {
+ this.loading = true;
+ try {
+ const response = await apiClient.get(`/vendor/${this.vendorCode}/customers/${customer.id}/orders`);
+ this.selectedCustomer = customer;
+ this.customerOrders = response.orders || [];
+ this.showOrdersModal = true;
+ vendorCustomersLog.info('Loaded customer orders:', customer.id, this.customerOrders.length);
+ } catch (error) {
+ vendorCustomersLog.error('Failed to load customer orders:', error);
+ Utils.showToast(error.message || 'Failed to load customer orders', 'error');
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ /**
+ * Send message to customer
+ */
+ messageCustomer(customer) {
+ window.location.href = `/vendor/${this.vendorCode}/messages?customer=${customer.id}`;
+ },
+
+ /**
+ * Get customer initials for avatar
+ */
+ getInitials(customer) {
+ const first = customer.first_name || '';
+ const last = customer.last_name || '';
+ return (first.charAt(0) + last.charAt(0)).toUpperCase() || '?';
+ },
+
+ /**
+ * Format date for display
+ */
+ formatDate(dateStr) {
+ if (!dateStr) return '-';
+ return new Date(dateStr).toLocaleDateString('de-DE', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric'
+ });
+ },
+
+ /**
+ * Format price for display
+ */
+ formatPrice(cents) {
+ if (!cents && cents !== 0) return '-';
+ return new Intl.NumberFormat('de-DE', {
+ style: 'currency',
+ currency: 'EUR'
+ }).format(cents / 100);
+ },
+
+ /**
+ * Pagination: Previous page
+ */
+ previousPage() {
+ if (this.pagination.page > 1) {
+ this.pagination.page--;
+ this.loadCustomers();
+ }
+ },
+
+ /**
+ * Pagination: Next page
+ */
+ nextPage() {
+ if (this.pagination.page < this.totalPages) {
+ this.pagination.page++;
+ this.loadCustomers();
+ }
+ },
+
+ /**
+ * Pagination: Go to specific page
+ */
+ goToPage(pageNum) {
+ if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) {
+ this.pagination.page = pageNum;
+ this.loadCustomers();
+ }
+ }
+ };
+}
diff --git a/static/vendor/js/init-alpine.js b/static/vendor/js/init-alpine.js
index 8ec1be92..69d876e2 100644
--- a/static/vendor/js/init-alpine.js
+++ b/static/vendor/js/init-alpine.js
@@ -9,6 +9,36 @@ const vendorLog = window.LogConfig.log;
console.log('[VENDOR INIT-ALPINE] Loading...');
+// Sidebar section state persistence
+const VENDOR_SIDEBAR_STORAGE_KEY = 'vendor_sidebar_sections';
+
+function getVendorSidebarSectionsFromStorage() {
+ try {
+ const stored = localStorage.getItem(VENDOR_SIDEBAR_STORAGE_KEY);
+ if (stored) {
+ return JSON.parse(stored);
+ }
+ } catch (e) {
+ console.warn('[VENDOR INIT-ALPINE] Failed to load sidebar state from localStorage:', e);
+ }
+ // Default: all sections open
+ return {
+ products: true,
+ sales: true,
+ customers: true,
+ shop: true,
+ account: true
+ };
+}
+
+function saveVendorSidebarSectionsToStorage(sections) {
+ try {
+ localStorage.setItem(VENDOR_SIDEBAR_STORAGE_KEY, JSON.stringify(sections));
+ } catch (e) {
+ console.warn('[VENDOR INIT-ALPINE] Failed to save sidebar state to localStorage:', e);
+ }
+}
+
function data() {
console.log('[VENDOR INIT-ALPINE] data() function called');
return {
@@ -21,6 +51,9 @@ function data() {
vendor: null,
vendorCode: null,
+ // Sidebar collapsible sections state
+ openSections: getVendorSidebarSectionsFromStorage(),
+
init() {
// Set current page from URL
const path = window.location.pathname;
@@ -109,6 +142,12 @@ function data() {
localStorage.setItem('theme', this.dark ? 'dark' : 'light');
},
+ // Sidebar section toggle with persistence
+ toggleSection(section) {
+ this.openSections[section] = !this.openSections[section];
+ saveVendorSidebarSectionsToStorage(this.openSections);
+ },
+
async handleLogout() {
console.log('🚪 Logging out vendor user...');
diff --git a/static/vendor/js/inventory.js b/static/vendor/js/inventory.js
new file mode 100644
index 00000000..b2d46a7a
--- /dev/null
+++ b/static/vendor/js/inventory.js
@@ -0,0 +1,365 @@
+// static/vendor/js/inventory.js
+/**
+ * Vendor inventory management page logic
+ * View and manage stock levels
+ */
+
+const vendorInventoryLog = window.LogConfig.loggers.vendorInventory ||
+ window.LogConfig.createLogger('vendorInventory', false);
+
+vendorInventoryLog.info('Loading...');
+
+function vendorInventory() {
+ vendorInventoryLog.info('vendorInventory() called');
+
+ return {
+ // Inherit base layout state
+ ...data(),
+
+ // Set page identifier
+ currentPage: 'inventory',
+
+ // Loading states
+ loading: true,
+ error: '',
+ saving: false,
+
+ // Inventory data
+ inventory: [],
+ stats: {
+ total_entries: 0,
+ total_quantity: 0,
+ low_stock_count: 0,
+ out_of_stock_count: 0
+ },
+
+ // Filters
+ filters: {
+ search: '',
+ location: '',
+ low_stock: ''
+ },
+
+ // Available locations for filter dropdown
+ locations: [],
+
+ // Pagination
+ pagination: {
+ page: 1,
+ per_page: 20,
+ total: 0,
+ pages: 0
+ },
+
+ // Modal states
+ showAdjustModal: false,
+ showSetModal: false,
+ selectedItem: null,
+
+ // Form data
+ adjustForm: {
+ quantity: 0,
+ reason: ''
+ },
+ setForm: {
+ quantity: 0
+ },
+
+ // Debounce timer
+ searchTimeout: null,
+
+ // Computed: Total pages
+ get totalPages() {
+ return this.pagination.pages;
+ },
+
+ // Computed: Start index for pagination display
+ get startIndex() {
+ if (this.pagination.total === 0) return 0;
+ return (this.pagination.page - 1) * this.pagination.per_page + 1;
+ },
+
+ // Computed: End index for pagination display
+ get endIndex() {
+ const end = this.pagination.page * this.pagination.per_page;
+ return end > this.pagination.total ? this.pagination.total : end;
+ },
+
+ // Computed: Page numbers for pagination
+ get pageNumbers() {
+ const pages = [];
+ const totalPages = this.totalPages;
+ const current = this.pagination.page;
+
+ if (totalPages <= 7) {
+ for (let i = 1; i <= totalPages; i++) {
+ pages.push(i);
+ }
+ } else {
+ pages.push(1);
+ if (current > 3) pages.push('...');
+ const start = Math.max(2, current - 1);
+ const end = Math.min(totalPages - 1, current + 1);
+ for (let i = start; i <= end; i++) {
+ pages.push(i);
+ }
+ if (current < totalPages - 2) pages.push('...');
+ pages.push(totalPages);
+ }
+ return pages;
+ },
+
+ async init() {
+ vendorInventoryLog.info('Inventory init() called');
+
+ // Guard against multiple initialization
+ if (window._vendorInventoryInitialized) {
+ vendorInventoryLog.warn('Already initialized, skipping');
+ return;
+ }
+ window._vendorInventoryInitialized = true;
+
+ // Load platform settings for rows per page
+ if (window.PlatformSettings) {
+ this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
+ }
+
+ try {
+ await this.loadInventory();
+ } catch (error) {
+ vendorInventoryLog.error('Init failed:', error);
+ this.error = 'Failed to initialize inventory page';
+ }
+
+ vendorInventoryLog.info('Inventory initialization complete');
+ },
+
+ /**
+ * Load inventory with filtering and pagination
+ */
+ async loadInventory() {
+ this.loading = true;
+ this.error = '';
+
+ try {
+ const params = new URLSearchParams({
+ skip: (this.pagination.page - 1) * this.pagination.per_page,
+ limit: this.pagination.per_page
+ });
+
+ // Add filters
+ if (this.filters.search) {
+ params.append('search', this.filters.search);
+ }
+ if (this.filters.location) {
+ params.append('location', this.filters.location);
+ }
+ if (this.filters.low_stock) {
+ params.append('low_stock', this.filters.low_stock);
+ }
+
+ const response = await apiClient.get(`/vendor/${this.vendorCode}/inventory?${params.toString()}`);
+
+ this.inventory = response.items || [];
+ this.pagination.total = response.total || 0;
+ this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page);
+
+ // Extract unique locations
+ this.extractLocations();
+
+ // Calculate stats
+ this.calculateStats();
+
+ vendorInventoryLog.info('Loaded inventory:', this.inventory.length, 'of', this.pagination.total);
+ } catch (error) {
+ vendorInventoryLog.error('Failed to load inventory:', error);
+ this.error = error.message || 'Failed to load inventory';
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ /**
+ * Extract unique locations from inventory
+ */
+ extractLocations() {
+ const locationSet = new Set(this.inventory.map(i => i.location).filter(Boolean));
+ this.locations = Array.from(locationSet).sort();
+ },
+
+ /**
+ * Calculate inventory statistics
+ */
+ calculateStats() {
+ this.stats = {
+ total_entries: this.pagination.total,
+ total_quantity: this.inventory.reduce((sum, i) => sum + (i.quantity || 0), 0),
+ low_stock_count: this.inventory.filter(i => i.quantity > 0 && i.quantity <= (i.low_stock_threshold || 5)).length,
+ out_of_stock_count: this.inventory.filter(i => i.quantity <= 0).length
+ };
+ },
+
+ /**
+ * Debounced search handler
+ */
+ debouncedSearch() {
+ clearTimeout(this.searchTimeout);
+ this.searchTimeout = setTimeout(() => {
+ this.pagination.page = 1;
+ this.loadInventory();
+ }, 300);
+ },
+
+ /**
+ * Apply filter and reload
+ */
+ applyFilter() {
+ this.pagination.page = 1;
+ this.loadInventory();
+ },
+
+ /**
+ * Clear all filters
+ */
+ clearFilters() {
+ this.filters = {
+ search: '',
+ location: '',
+ low_stock: ''
+ };
+ this.pagination.page = 1;
+ this.loadInventory();
+ },
+
+ /**
+ * Open adjust stock modal
+ */
+ openAdjustModal(item) {
+ this.selectedItem = item;
+ this.adjustForm = {
+ quantity: 0,
+ reason: ''
+ };
+ this.showAdjustModal = true;
+ },
+
+ /**
+ * Open set quantity modal
+ */
+ openSetModal(item) {
+ this.selectedItem = item;
+ this.setForm = {
+ quantity: item.quantity || 0
+ };
+ this.showSetModal = true;
+ },
+
+ /**
+ * Execute stock adjustment
+ */
+ async executeAdjust() {
+ if (!this.selectedItem || this.adjustForm.quantity === 0) return;
+
+ this.saving = true;
+ try {
+ await apiClient.post(`/vendor/${this.vendorCode}/inventory/adjust`, {
+ product_id: this.selectedItem.product_id,
+ location: this.selectedItem.location,
+ quantity: this.adjustForm.quantity,
+ reason: this.adjustForm.reason || null
+ });
+
+ vendorInventoryLog.info('Adjusted inventory:', this.selectedItem.id);
+
+ this.showAdjustModal = false;
+ this.selectedItem = null;
+
+ Utils.showToast('Stock adjusted successfully', 'success');
+
+ await this.loadInventory();
+ } catch (error) {
+ vendorInventoryLog.error('Failed to adjust inventory:', error);
+ Utils.showToast(error.message || 'Failed to adjust stock', 'error');
+ } finally {
+ this.saving = false;
+ }
+ },
+
+ /**
+ * Execute set quantity
+ */
+ async executeSet() {
+ if (!this.selectedItem || this.setForm.quantity < 0) return;
+
+ this.saving = true;
+ try {
+ await apiClient.post(`/vendor/${this.vendorCode}/inventory/set`, {
+ product_id: this.selectedItem.product_id,
+ location: this.selectedItem.location,
+ quantity: this.setForm.quantity
+ });
+
+ vendorInventoryLog.info('Set inventory quantity:', this.selectedItem.id);
+
+ this.showSetModal = false;
+ this.selectedItem = null;
+
+ Utils.showToast('Quantity set successfully', 'success');
+
+ await this.loadInventory();
+ } catch (error) {
+ vendorInventoryLog.error('Failed to set inventory:', error);
+ Utils.showToast(error.message || 'Failed to set quantity', 'error');
+ } finally {
+ this.saving = false;
+ }
+ },
+
+ /**
+ * Get stock status class
+ */
+ getStockStatus(item) {
+ if (item.quantity <= 0) return 'out';
+ if (item.quantity <= (item.low_stock_threshold || 5)) return 'low';
+ return 'ok';
+ },
+
+ /**
+ * Format number with locale
+ */
+ formatNumber(num) {
+ if (num === null || num === undefined) return '0';
+ return new Intl.NumberFormat('en-US').format(num);
+ },
+
+ /**
+ * Pagination: Previous page
+ */
+ previousPage() {
+ if (this.pagination.page > 1) {
+ this.pagination.page--;
+ this.loadInventory();
+ }
+ },
+
+ /**
+ * Pagination: Next page
+ */
+ nextPage() {
+ if (this.pagination.page < this.totalPages) {
+ this.pagination.page++;
+ this.loadInventory();
+ }
+ },
+
+ /**
+ * Pagination: Go to specific page
+ */
+ goToPage(pageNum) {
+ if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) {
+ this.pagination.page = pageNum;
+ this.loadInventory();
+ }
+ }
+ };
+}
diff --git a/static/vendor/js/orders.js b/static/vendor/js/orders.js
new file mode 100644
index 00000000..669db1a0
--- /dev/null
+++ b/static/vendor/js/orders.js
@@ -0,0 +1,354 @@
+// static/vendor/js/orders.js
+/**
+ * Vendor orders management page logic
+ * View and manage vendor's orders
+ */
+
+const vendorOrdersLog = window.LogConfig.loggers.vendorOrders ||
+ window.LogConfig.createLogger('vendorOrders', false);
+
+vendorOrdersLog.info('Loading...');
+
+function vendorOrders() {
+ vendorOrdersLog.info('vendorOrders() called');
+
+ return {
+ // Inherit base layout state
+ ...data(),
+
+ // Set page identifier
+ currentPage: 'orders',
+
+ // Loading states
+ loading: true,
+ error: '',
+ saving: false,
+
+ // Orders data
+ orders: [],
+ stats: {
+ total: 0,
+ pending: 0,
+ processing: 0,
+ completed: 0,
+ cancelled: 0
+ },
+
+ // Order statuses for filter and display
+ statuses: [
+ { value: 'pending', label: 'Pending', color: 'yellow' },
+ { value: 'processing', label: 'Processing', color: 'blue' },
+ { value: 'shipped', label: 'Shipped', color: 'indigo' },
+ { value: 'delivered', label: 'Delivered', color: 'green' },
+ { value: 'completed', label: 'Completed', color: 'green' },
+ { value: 'cancelled', label: 'Cancelled', color: 'red' },
+ { value: 'refunded', label: 'Refunded', color: 'gray' }
+ ],
+
+ // Filters
+ filters: {
+ search: '',
+ status: '',
+ date_from: '',
+ date_to: ''
+ },
+
+ // Pagination
+ pagination: {
+ page: 1,
+ per_page: 20,
+ total: 0,
+ pages: 0
+ },
+
+ // Modal states
+ showDetailModal: false,
+ showStatusModal: false,
+ selectedOrder: null,
+ newStatus: '',
+
+ // Debounce timer
+ searchTimeout: null,
+
+ // Computed: Total pages
+ get totalPages() {
+ return this.pagination.pages;
+ },
+
+ // Computed: Start index for pagination display
+ get startIndex() {
+ if (this.pagination.total === 0) return 0;
+ return (this.pagination.page - 1) * this.pagination.per_page + 1;
+ },
+
+ // Computed: End index for pagination display
+ get endIndex() {
+ const end = this.pagination.page * this.pagination.per_page;
+ return end > this.pagination.total ? this.pagination.total : end;
+ },
+
+ // Computed: Page numbers for pagination
+ get pageNumbers() {
+ const pages = [];
+ const totalPages = this.totalPages;
+ const current = this.pagination.page;
+
+ if (totalPages <= 7) {
+ for (let i = 1; i <= totalPages; i++) {
+ pages.push(i);
+ }
+ } else {
+ pages.push(1);
+ if (current > 3) pages.push('...');
+ const start = Math.max(2, current - 1);
+ const end = Math.min(totalPages - 1, current + 1);
+ for (let i = start; i <= end; i++) {
+ pages.push(i);
+ }
+ if (current < totalPages - 2) pages.push('...');
+ pages.push(totalPages);
+ }
+ return pages;
+ },
+
+ async init() {
+ vendorOrdersLog.info('Orders init() called');
+
+ // Guard against multiple initialization
+ if (window._vendorOrdersInitialized) {
+ vendorOrdersLog.warn('Already initialized, skipping');
+ return;
+ }
+ window._vendorOrdersInitialized = true;
+
+ // Load platform settings for rows per page
+ if (window.PlatformSettings) {
+ this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
+ }
+
+ try {
+ await this.loadOrders();
+ } catch (error) {
+ vendorOrdersLog.error('Init failed:', error);
+ this.error = 'Failed to initialize orders page';
+ }
+
+ vendorOrdersLog.info('Orders initialization complete');
+ },
+
+ /**
+ * Load orders with filtering and pagination
+ */
+ async loadOrders() {
+ this.loading = true;
+ this.error = '';
+
+ try {
+ const params = new URLSearchParams({
+ skip: (this.pagination.page - 1) * this.pagination.per_page,
+ limit: this.pagination.per_page
+ });
+
+ // Add filters
+ if (this.filters.search) {
+ params.append('search', this.filters.search);
+ }
+ if (this.filters.status) {
+ params.append('status', this.filters.status);
+ }
+ if (this.filters.date_from) {
+ params.append('date_from', this.filters.date_from);
+ }
+ if (this.filters.date_to) {
+ params.append('date_to', this.filters.date_to);
+ }
+
+ const response = await apiClient.get(`/vendor/${this.vendorCode}/orders?${params.toString()}`);
+
+ this.orders = response.orders || [];
+ this.pagination.total = response.total || 0;
+ this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page);
+
+ // Calculate stats
+ this.calculateStats();
+
+ vendorOrdersLog.info('Loaded orders:', this.orders.length, 'of', this.pagination.total);
+ } catch (error) {
+ vendorOrdersLog.error('Failed to load orders:', error);
+ this.error = error.message || 'Failed to load orders';
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ /**
+ * Calculate order statistics
+ */
+ calculateStats() {
+ this.stats = {
+ total: this.pagination.total,
+ pending: this.orders.filter(o => o.status === 'pending').length,
+ processing: this.orders.filter(o => o.status === 'processing').length,
+ completed: this.orders.filter(o => ['completed', 'delivered'].includes(o.status)).length,
+ cancelled: this.orders.filter(o => o.status === 'cancelled').length
+ };
+ },
+
+ /**
+ * Debounced search handler
+ */
+ debouncedSearch() {
+ clearTimeout(this.searchTimeout);
+ this.searchTimeout = setTimeout(() => {
+ this.pagination.page = 1;
+ this.loadOrders();
+ }, 300);
+ },
+
+ /**
+ * Apply filter and reload
+ */
+ applyFilter() {
+ this.pagination.page = 1;
+ this.loadOrders();
+ },
+
+ /**
+ * Clear all filters
+ */
+ clearFilters() {
+ this.filters = {
+ search: '',
+ status: '',
+ date_from: '',
+ date_to: ''
+ };
+ this.pagination.page = 1;
+ this.loadOrders();
+ },
+
+ /**
+ * View order details
+ */
+ async viewOrder(order) {
+ this.loading = true;
+ try {
+ const response = await apiClient.get(`/vendor/${this.vendorCode}/orders/${order.id}`);
+ this.selectedOrder = response;
+ this.showDetailModal = true;
+ vendorOrdersLog.info('Loaded order details:', order.id);
+ } catch (error) {
+ vendorOrdersLog.error('Failed to load order details:', error);
+ Utils.showToast(error.message || 'Failed to load order details', 'error');
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ /**
+ * Open status change modal
+ */
+ openStatusModal(order) {
+ this.selectedOrder = order;
+ this.newStatus = order.status;
+ this.showStatusModal = true;
+ },
+
+ /**
+ * Update order status
+ */
+ async updateStatus() {
+ if (!this.selectedOrder || !this.newStatus) return;
+
+ this.saving = true;
+ try {
+ await apiClient.put(`/vendor/${this.vendorCode}/orders/${this.selectedOrder.id}/status`, {
+ status: this.newStatus
+ });
+
+ Utils.showToast('Order status updated', 'success');
+ vendorOrdersLog.info('Updated order status:', this.selectedOrder.id, this.newStatus);
+
+ this.showStatusModal = false;
+ this.selectedOrder = null;
+ await this.loadOrders();
+ } catch (error) {
+ vendorOrdersLog.error('Failed to update status:', error);
+ Utils.showToast(error.message || 'Failed to update status', 'error');
+ } finally {
+ this.saving = false;
+ }
+ },
+
+ /**
+ * Get status color class
+ */
+ getStatusColor(status) {
+ const statusObj = this.statuses.find(s => s.value === status);
+ return statusObj ? statusObj.color : 'gray';
+ },
+
+ /**
+ * Get status label
+ */
+ getStatusLabel(status) {
+ const statusObj = this.statuses.find(s => s.value === status);
+ return statusObj ? statusObj.label : status;
+ },
+
+ /**
+ * Format price for display
+ */
+ formatPrice(cents) {
+ if (!cents && cents !== 0) return '-';
+ return new Intl.NumberFormat('de-DE', {
+ style: 'currency',
+ currency: 'EUR'
+ }).format(cents / 100);
+ },
+
+ /**
+ * Format date for display
+ */
+ formatDate(dateStr) {
+ if (!dateStr) return '-';
+ return new Date(dateStr).toLocaleDateString('de-DE', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ },
+
+ /**
+ * Pagination: Previous page
+ */
+ previousPage() {
+ if (this.pagination.page > 1) {
+ this.pagination.page--;
+ this.loadOrders();
+ }
+ },
+
+ /**
+ * Pagination: Next page
+ */
+ nextPage() {
+ if (this.pagination.page < this.totalPages) {
+ this.pagination.page++;
+ this.loadOrders();
+ }
+ },
+
+ /**
+ * Pagination: Go to specific page
+ */
+ goToPage(pageNum) {
+ if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) {
+ this.pagination.page = pageNum;
+ this.loadOrders();
+ }
+ }
+ };
+}
diff --git a/static/vendor/js/products.js b/static/vendor/js/products.js
new file mode 100644
index 00000000..6852c4ac
--- /dev/null
+++ b/static/vendor/js/products.js
@@ -0,0 +1,340 @@
+// static/vendor/js/products.js
+/**
+ * Vendor products management page logic
+ * View, edit, and manage vendor's product catalog
+ */
+
+const vendorProductsLog = window.LogConfig.loggers.vendorProducts ||
+ window.LogConfig.createLogger('vendorProducts', false);
+
+vendorProductsLog.info('Loading...');
+
+function vendorProducts() {
+ vendorProductsLog.info('vendorProducts() called');
+
+ return {
+ // Inherit base layout state
+ ...data(),
+
+ // Set page identifier
+ currentPage: 'products',
+
+ // Loading states
+ loading: true,
+ error: '',
+ saving: false,
+
+ // Products data
+ products: [],
+ stats: {
+ total: 0,
+ active: 0,
+ inactive: 0,
+ featured: 0
+ },
+
+ // Filters
+ filters: {
+ search: '',
+ status: '', // 'active', 'inactive', ''
+ featured: '' // 'true', 'false', ''
+ },
+
+ // Pagination
+ pagination: {
+ page: 1,
+ per_page: 20,
+ total: 0,
+ pages: 0
+ },
+
+ // Modal states
+ showDeleteModal: false,
+ showDetailModal: false,
+ selectedProduct: null,
+
+ // Debounce timer
+ searchTimeout: null,
+
+ // Computed: Total pages
+ get totalPages() {
+ return this.pagination.pages;
+ },
+
+ // Computed: Start index for pagination display
+ get startIndex() {
+ if (this.pagination.total === 0) return 0;
+ return (this.pagination.page - 1) * this.pagination.per_page + 1;
+ },
+
+ // Computed: End index for pagination display
+ get endIndex() {
+ const end = this.pagination.page * this.pagination.per_page;
+ return end > this.pagination.total ? this.pagination.total : end;
+ },
+
+ // Computed: Page numbers for pagination
+ get pageNumbers() {
+ const pages = [];
+ const totalPages = this.totalPages;
+ const current = this.pagination.page;
+
+ if (totalPages <= 7) {
+ for (let i = 1; i <= totalPages; i++) {
+ pages.push(i);
+ }
+ } else {
+ pages.push(1);
+ if (current > 3) pages.push('...');
+ const start = Math.max(2, current - 1);
+ const end = Math.min(totalPages - 1, current + 1);
+ for (let i = start; i <= end; i++) {
+ pages.push(i);
+ }
+ if (current < totalPages - 2) pages.push('...');
+ pages.push(totalPages);
+ }
+ return pages;
+ },
+
+ async init() {
+ vendorProductsLog.info('Products init() called');
+
+ // Guard against multiple initialization
+ if (window._vendorProductsInitialized) {
+ vendorProductsLog.warn('Already initialized, skipping');
+ return;
+ }
+ window._vendorProductsInitialized = true;
+
+ // Load platform settings for rows per page
+ if (window.PlatformSettings) {
+ this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
+ }
+
+ try {
+ await this.loadProducts();
+ } catch (error) {
+ vendorProductsLog.error('Init failed:', error);
+ this.error = 'Failed to initialize products page';
+ }
+
+ vendorProductsLog.info('Products initialization complete');
+ },
+
+ /**
+ * Load products with filtering and pagination
+ */
+ async loadProducts() {
+ this.loading = true;
+ this.error = '';
+
+ try {
+ const params = new URLSearchParams({
+ skip: (this.pagination.page - 1) * this.pagination.per_page,
+ limit: this.pagination.per_page
+ });
+
+ // Add filters
+ if (this.filters.search) {
+ params.append('search', this.filters.search);
+ }
+ if (this.filters.status) {
+ params.append('is_active', this.filters.status === 'active');
+ }
+ if (this.filters.featured) {
+ params.append('is_featured', this.filters.featured === 'true');
+ }
+
+ const response = await apiClient.get(`/vendor/${this.vendorCode}/products?${params.toString()}`);
+
+ this.products = response.products || [];
+ this.pagination.total = response.total || 0;
+ this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page);
+
+ // Calculate stats from response or products
+ this.stats = {
+ total: response.total || this.products.length,
+ active: this.products.filter(p => p.is_active).length,
+ inactive: this.products.filter(p => !p.is_active).length,
+ featured: this.products.filter(p => p.is_featured).length
+ };
+
+ vendorProductsLog.info('Loaded products:', this.products.length, 'of', this.pagination.total);
+ } catch (error) {
+ vendorProductsLog.error('Failed to load products:', error);
+ this.error = error.message || 'Failed to load products';
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ /**
+ * Debounced search handler
+ */
+ debouncedSearch() {
+ clearTimeout(this.searchTimeout);
+ this.searchTimeout = setTimeout(() => {
+ this.pagination.page = 1;
+ this.loadProducts();
+ }, 300);
+ },
+
+ /**
+ * Apply filter and reload
+ */
+ applyFilter() {
+ this.pagination.page = 1;
+ this.loadProducts();
+ },
+
+ /**
+ * Clear all filters
+ */
+ clearFilters() {
+ this.filters = {
+ search: '',
+ status: '',
+ featured: ''
+ };
+ this.pagination.page = 1;
+ this.loadProducts();
+ },
+
+ /**
+ * Toggle product active status
+ */
+ async toggleActive(product) {
+ this.saving = true;
+ try {
+ await apiClient.put(`/vendor/${this.vendorCode}/products/${product.id}/toggle-active`);
+ product.is_active = !product.is_active;
+ Utils.showToast(
+ product.is_active ? 'Product activated' : 'Product deactivated',
+ 'success'
+ );
+ vendorProductsLog.info('Toggled product active:', product.id, product.is_active);
+ } catch (error) {
+ vendorProductsLog.error('Failed to toggle active:', error);
+ Utils.showToast(error.message || 'Failed to update product', 'error');
+ } finally {
+ this.saving = false;
+ }
+ },
+
+ /**
+ * Toggle product featured status
+ */
+ async toggleFeatured(product) {
+ this.saving = true;
+ try {
+ await apiClient.put(`/vendor/${this.vendorCode}/products/${product.id}/toggle-featured`);
+ product.is_featured = !product.is_featured;
+ Utils.showToast(
+ product.is_featured ? 'Product marked as featured' : 'Product unmarked as featured',
+ 'success'
+ );
+ vendorProductsLog.info('Toggled product featured:', product.id, product.is_featured);
+ } catch (error) {
+ vendorProductsLog.error('Failed to toggle featured:', error);
+ Utils.showToast(error.message || 'Failed to update product', 'error');
+ } finally {
+ this.saving = false;
+ }
+ },
+
+ /**
+ * View product details
+ */
+ viewProduct(product) {
+ this.selectedProduct = product;
+ this.showDetailModal = true;
+ },
+
+ /**
+ * Confirm delete product
+ */
+ confirmDelete(product) {
+ this.selectedProduct = product;
+ this.showDeleteModal = true;
+ },
+
+ /**
+ * Execute delete product
+ */
+ async deleteProduct() {
+ if (!this.selectedProduct) return;
+
+ this.saving = true;
+ try {
+ await apiClient.delete(`/vendor/${this.vendorCode}/products/${this.selectedProduct.id}`);
+ Utils.showToast('Product deleted successfully', 'success');
+ vendorProductsLog.info('Deleted product:', this.selectedProduct.id);
+
+ this.showDeleteModal = false;
+ this.selectedProduct = null;
+ await this.loadProducts();
+ } catch (error) {
+ vendorProductsLog.error('Failed to delete product:', error);
+ Utils.showToast(error.message || 'Failed to delete product', 'error');
+ } finally {
+ this.saving = false;
+ }
+ },
+
+ /**
+ * Navigate to edit product page
+ */
+ editProduct(product) {
+ window.location.href = `/vendor/${this.vendorCode}/products/${product.id}/edit`;
+ },
+
+ /**
+ * Navigate to create product page
+ */
+ createProduct() {
+ window.location.href = `/vendor/${this.vendorCode}/products/create`;
+ },
+
+ /**
+ * Format price for display
+ */
+ formatPrice(cents) {
+ if (!cents && cents !== 0) return '-';
+ return new Intl.NumberFormat('de-DE', {
+ style: 'currency',
+ currency: 'EUR'
+ }).format(cents / 100);
+ },
+
+ /**
+ * Pagination: Previous page
+ */
+ previousPage() {
+ if (this.pagination.page > 1) {
+ this.pagination.page--;
+ this.loadProducts();
+ }
+ },
+
+ /**
+ * Pagination: Next page
+ */
+ nextPage() {
+ if (this.pagination.page < this.totalPages) {
+ this.pagination.page++;
+ this.loadProducts();
+ }
+ },
+
+ /**
+ * Pagination: Go to specific page
+ */
+ goToPage(pageNum) {
+ if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) {
+ this.pagination.page = pageNum;
+ this.loadProducts();
+ }
+ }
+ };
+}
diff --git a/static/vendor/js/profile.js b/static/vendor/js/profile.js
new file mode 100644
index 00000000..fe15b6b0
--- /dev/null
+++ b/static/vendor/js/profile.js
@@ -0,0 +1,190 @@
+// static/vendor/js/profile.js
+/**
+ * Vendor profile management page logic
+ * Edit vendor business profile and contact information
+ */
+
+const vendorProfileLog = window.LogConfig.loggers.vendorProfile ||
+ window.LogConfig.createLogger('vendorProfile', false);
+
+vendorProfileLog.info('Loading...');
+
+function vendorProfile() {
+ vendorProfileLog.info('vendorProfile() called');
+
+ return {
+ // Inherit base layout state
+ ...data(),
+
+ // Set page identifier
+ currentPage: 'profile',
+
+ // Loading states
+ loading: true,
+ error: '',
+ saving: false,
+
+ // Profile data
+ profile: null,
+
+ // Edit form
+ form: {
+ name: '',
+ contact_email: '',
+ contact_phone: '',
+ website: '',
+ business_address: '',
+ tax_number: '',
+ description: ''
+ },
+
+ // Form validation
+ errors: {},
+
+ // Track if form has changes
+ hasChanges: false,
+
+ async init() {
+ vendorProfileLog.info('Profile init() called');
+
+ // Guard against multiple initialization
+ if (window._vendorProfileInitialized) {
+ vendorProfileLog.warn('Already initialized, skipping');
+ return;
+ }
+ window._vendorProfileInitialized = true;
+
+ try {
+ await this.loadProfile();
+ } catch (error) {
+ vendorProfileLog.error('Init failed:', error);
+ this.error = 'Failed to initialize profile page';
+ }
+
+ vendorProfileLog.info('Profile initialization complete');
+ },
+
+ /**
+ * Load vendor profile
+ */
+ async loadProfile() {
+ this.loading = true;
+ this.error = '';
+
+ try {
+ const response = await apiClient.get(`/vendor/${this.vendorCode}/profile`);
+
+ this.profile = response;
+ this.form = {
+ name: response.name || '',
+ contact_email: response.contact_email || '',
+ contact_phone: response.contact_phone || '',
+ website: response.website || '',
+ business_address: response.business_address || '',
+ tax_number: response.tax_number || '',
+ description: response.description || ''
+ };
+
+ this.hasChanges = false;
+ vendorProfileLog.info('Loaded profile:', this.profile.vendor_code);
+ } catch (error) {
+ vendorProfileLog.error('Failed to load profile:', error);
+ this.error = error.message || 'Failed to load profile';
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ /**
+ * Mark form as changed
+ */
+ markChanged() {
+ this.hasChanges = true;
+ },
+
+ /**
+ * Validate form
+ */
+ validateForm() {
+ this.errors = {};
+
+ if (!this.form.name?.trim()) {
+ this.errors.name = 'Business name is required';
+ }
+
+ if (this.form.contact_email && !this.isValidEmail(this.form.contact_email)) {
+ this.errors.contact_email = 'Invalid email address';
+ }
+
+ if (this.form.website && !this.isValidUrl(this.form.website)) {
+ this.errors.website = 'Invalid URL format';
+ }
+
+ return Object.keys(this.errors).length === 0;
+ },
+
+ /**
+ * Check if email is valid
+ */
+ isValidEmail(email) {
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
+ },
+
+ /**
+ * Check if URL is valid
+ */
+ isValidUrl(url) {
+ try {
+ new URL(url);
+ return true;
+ } catch {
+ return url.match(/^(https?:\/\/)?[\w-]+(\.[\w-]+)+/) !== null;
+ }
+ },
+
+ /**
+ * Save profile changes
+ */
+ async saveProfile() {
+ if (!this.validateForm()) {
+ Utils.showToast('Please fix the errors before saving', 'error');
+ return;
+ }
+
+ this.saving = true;
+ try {
+ await apiClient.put(`/vendor/${this.vendorCode}/profile`, this.form);
+
+ Utils.showToast('Profile updated successfully', 'success');
+ vendorProfileLog.info('Profile updated');
+
+ this.hasChanges = false;
+ await this.loadProfile();
+ } catch (error) {
+ vendorProfileLog.error('Failed to save profile:', error);
+ Utils.showToast(error.message || 'Failed to save profile', 'error');
+ } finally {
+ this.saving = false;
+ }
+ },
+
+ /**
+ * Reset form to original values
+ */
+ resetForm() {
+ if (this.profile) {
+ this.form = {
+ name: this.profile.name || '',
+ contact_email: this.profile.contact_email || '',
+ contact_phone: this.profile.contact_phone || '',
+ website: this.profile.website || '',
+ business_address: this.profile.business_address || '',
+ tax_number: this.profile.tax_number || '',
+ description: this.profile.description || ''
+ };
+ }
+ this.hasChanges = false;
+ this.errors = {};
+ }
+ };
+}
diff --git a/static/vendor/js/settings.js b/static/vendor/js/settings.js
new file mode 100644
index 00000000..cdd3ba61
--- /dev/null
+++ b/static/vendor/js/settings.js
@@ -0,0 +1,178 @@
+// static/vendor/js/settings.js
+/**
+ * Vendor settings management page logic
+ * Configure vendor preferences and integrations
+ */
+
+const vendorSettingsLog = window.LogConfig.loggers.vendorSettings ||
+ window.LogConfig.createLogger('vendorSettings', false);
+
+vendorSettingsLog.info('Loading...');
+
+function vendorSettings() {
+ vendorSettingsLog.info('vendorSettings() called');
+
+ return {
+ // Inherit base layout state
+ ...data(),
+
+ // Set page identifier
+ currentPage: 'settings',
+
+ // Loading states
+ loading: true,
+ error: '',
+ saving: false,
+
+ // Settings data
+ settings: null,
+
+ // Active section
+ activeSection: 'general',
+
+ // Sections for navigation
+ sections: [
+ { id: 'general', label: 'General', icon: 'cog' },
+ { id: 'marketplace', label: 'Marketplace', icon: 'shopping-cart' },
+ { id: 'notifications', label: 'Notifications', icon: 'bell' }
+ ],
+
+ // Forms for different sections
+ generalForm: {
+ subdomain: '',
+ is_active: true
+ },
+
+ marketplaceForm: {
+ letzshop_csv_url_fr: '',
+ letzshop_csv_url_en: '',
+ letzshop_csv_url_de: ''
+ },
+
+ notificationForm: {
+ email_notifications: true,
+ order_notifications: true,
+ marketing_emails: false
+ },
+
+ // Track changes
+ hasChanges: false,
+
+ async init() {
+ vendorSettingsLog.info('Settings init() called');
+
+ // Guard against multiple initialization
+ if (window._vendorSettingsInitialized) {
+ vendorSettingsLog.warn('Already initialized, skipping');
+ return;
+ }
+ window._vendorSettingsInitialized = true;
+
+ try {
+ await this.loadSettings();
+ } catch (error) {
+ vendorSettingsLog.error('Init failed:', error);
+ this.error = 'Failed to initialize settings page';
+ }
+
+ vendorSettingsLog.info('Settings initialization complete');
+ },
+
+ /**
+ * Load vendor settings
+ */
+ async loadSettings() {
+ this.loading = true;
+ this.error = '';
+
+ try {
+ const response = await apiClient.get(`/vendor/${this.vendorCode}/settings`);
+
+ this.settings = response;
+
+ // Populate forms
+ this.generalForm = {
+ subdomain: response.subdomain || '',
+ is_active: response.is_active !== false
+ };
+
+ this.marketplaceForm = {
+ letzshop_csv_url_fr: response.letzshop_csv_url_fr || '',
+ letzshop_csv_url_en: response.letzshop_csv_url_en || '',
+ letzshop_csv_url_de: response.letzshop_csv_url_de || ''
+ };
+
+ this.hasChanges = false;
+ vendorSettingsLog.info('Loaded settings');
+ } catch (error) {
+ vendorSettingsLog.error('Failed to load settings:', error);
+ this.error = error.message || 'Failed to load settings';
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ /**
+ * Mark form as changed
+ */
+ markChanged() {
+ this.hasChanges = true;
+ },
+
+ /**
+ * Save marketplace settings
+ */
+ async saveMarketplaceSettings() {
+ this.saving = true;
+ try {
+ await apiClient.put(`/vendor/${this.vendorCode}/settings/marketplace`, this.marketplaceForm);
+
+ Utils.showToast('Marketplace settings saved', 'success');
+ vendorSettingsLog.info('Marketplace settings updated');
+
+ this.hasChanges = false;
+ } catch (error) {
+ vendorSettingsLog.error('Failed to save marketplace settings:', error);
+ Utils.showToast(error.message || 'Failed to save settings', 'error');
+ } finally {
+ this.saving = false;
+ }
+ },
+
+ /**
+ * Test Letzshop CSV URL
+ */
+ async testLetzshopUrl(lang) {
+ const url = this.marketplaceForm[`letzshop_csv_url_${lang}`];
+ if (!url) {
+ Utils.showToast('Please enter a URL first', 'error');
+ return;
+ }
+
+ this.saving = true;
+ try {
+ // Try to fetch the URL to validate it
+ const response = await fetch(url, { method: 'HEAD', mode: 'no-cors' });
+ Utils.showToast(`URL appears to be valid`, 'success');
+ } catch (error) {
+ Utils.showToast('Could not validate URL - it may still work', 'warning');
+ } finally {
+ this.saving = false;
+ }
+ },
+
+ /**
+ * Reset settings to saved values
+ */
+ resetSettings() {
+ this.loadSettings();
+ },
+
+ /**
+ * Switch active section
+ */
+ setSection(sectionId) {
+ this.activeSection = sectionId;
+ }
+ };
+}
diff --git a/static/vendor/js/team.js b/static/vendor/js/team.js
new file mode 100644
index 00000000..559ca98a
--- /dev/null
+++ b/static/vendor/js/team.js
@@ -0,0 +1,268 @@
+// static/vendor/js/team.js
+/**
+ * Vendor team management page logic
+ * Manage team members, invitations, and roles
+ */
+
+const vendorTeamLog = window.LogConfig.loggers.vendorTeam ||
+ window.LogConfig.createLogger('vendorTeam', false);
+
+vendorTeamLog.info('Loading...');
+
+function vendorTeam() {
+ vendorTeamLog.info('vendorTeam() called');
+
+ return {
+ // Inherit base layout state
+ ...data(),
+
+ // Set page identifier
+ currentPage: 'team',
+
+ // Loading states
+ loading: true,
+ error: '',
+ saving: false,
+
+ // Team data
+ members: [],
+ roles: [],
+ stats: {
+ total: 0,
+ active_count: 0,
+ pending_invitations: 0
+ },
+
+ // Modal states
+ showInviteModal: false,
+ showEditModal: false,
+ showRemoveModal: false,
+ selectedMember: null,
+
+ // Invite form
+ inviteForm: {
+ email: '',
+ first_name: '',
+ last_name: '',
+ role_name: 'staff'
+ },
+
+ // Edit form
+ editForm: {
+ role_id: null,
+ is_active: true
+ },
+
+ // Available role names for invite
+ roleOptions: [
+ { value: 'owner', label: 'Owner', description: 'Full access to all features' },
+ { value: 'manager', label: 'Manager', description: 'Manage orders, products, and team' },
+ { value: 'staff', label: 'Staff', description: 'Handle orders and products' },
+ { value: 'support', label: 'Support', description: 'Customer support access' },
+ { value: 'viewer', label: 'Viewer', description: 'Read-only access' },
+ { value: 'marketing', label: 'Marketing', description: 'Content and promotions' }
+ ],
+
+ async init() {
+ vendorTeamLog.info('Team init() called');
+
+ // Guard against multiple initialization
+ if (window._vendorTeamInitialized) {
+ vendorTeamLog.warn('Already initialized, skipping');
+ return;
+ }
+ window._vendorTeamInitialized = true;
+
+ try {
+ await Promise.all([
+ this.loadMembers(),
+ this.loadRoles()
+ ]);
+ } catch (error) {
+ vendorTeamLog.error('Init failed:', error);
+ this.error = 'Failed to initialize team page';
+ }
+
+ vendorTeamLog.info('Team initialization complete');
+ },
+
+ /**
+ * Load team members
+ */
+ async loadMembers() {
+ this.loading = true;
+ this.error = '';
+
+ try {
+ const response = await apiClient.get(`/vendor/${this.vendorCode}/team/members?include_inactive=true`);
+
+ this.members = response.members || [];
+ this.stats = {
+ total: response.total || 0,
+ active_count: response.active_count || 0,
+ pending_invitations: response.pending_invitations || 0
+ };
+
+ vendorTeamLog.info('Loaded team members:', this.members.length);
+ } catch (error) {
+ vendorTeamLog.error('Failed to load team members:', error);
+ this.error = error.message || 'Failed to load team members';
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ /**
+ * Load available roles
+ */
+ async loadRoles() {
+ try {
+ const response = await apiClient.get(`/vendor/${this.vendorCode}/team/roles`);
+ this.roles = response.roles || [];
+ vendorTeamLog.info('Loaded roles:', this.roles.length);
+ } catch (error) {
+ vendorTeamLog.error('Failed to load roles:', error);
+ }
+ },
+
+ /**
+ * Open invite modal
+ */
+ openInviteModal() {
+ this.inviteForm = {
+ email: '',
+ first_name: '',
+ last_name: '',
+ role_name: 'staff'
+ };
+ this.showInviteModal = true;
+ },
+
+ /**
+ * Send invitation
+ */
+ async sendInvitation() {
+ if (!this.inviteForm.email) {
+ Utils.showToast('Email is required', 'error');
+ return;
+ }
+
+ this.saving = true;
+ try {
+ await apiClient.post(`/vendor/${this.vendorCode}/team/invite`, this.inviteForm);
+
+ Utils.showToast('Invitation sent successfully', 'success');
+ vendorTeamLog.info('Invitation sent to:', this.inviteForm.email);
+
+ this.showInviteModal = false;
+ await this.loadMembers();
+ } catch (error) {
+ vendorTeamLog.error('Failed to send invitation:', error);
+ Utils.showToast(error.message || 'Failed to send invitation', 'error');
+ } finally {
+ this.saving = false;
+ }
+ },
+
+ /**
+ * Open edit member modal
+ */
+ openEditModal(member) {
+ this.selectedMember = member;
+ this.editForm = {
+ role_id: member.role_id,
+ is_active: member.is_active
+ };
+ this.showEditModal = true;
+ },
+
+ /**
+ * Update team member
+ */
+ async updateMember() {
+ if (!this.selectedMember) return;
+
+ this.saving = true;
+ try {
+ await apiClient.put(
+ `/vendor/${this.vendorCode}/team/members/${this.selectedMember.user_id}`,
+ this.editForm
+ );
+
+ Utils.showToast('Team member updated', 'success');
+ vendorTeamLog.info('Updated team member:', this.selectedMember.user_id);
+
+ this.showEditModal = false;
+ this.selectedMember = null;
+ await this.loadMembers();
+ } catch (error) {
+ vendorTeamLog.error('Failed to update team member:', error);
+ Utils.showToast(error.message || 'Failed to update team member', 'error');
+ } finally {
+ this.saving = false;
+ }
+ },
+
+ /**
+ * Confirm remove member
+ */
+ confirmRemove(member) {
+ this.selectedMember = member;
+ this.showRemoveModal = true;
+ },
+
+ /**
+ * Remove team member
+ */
+ async removeMember() {
+ if (!this.selectedMember) return;
+
+ this.saving = true;
+ try {
+ await apiClient.delete(`/vendor/${this.vendorCode}/team/members/${this.selectedMember.user_id}`);
+
+ Utils.showToast('Team member removed', 'success');
+ vendorTeamLog.info('Removed team member:', this.selectedMember.user_id);
+
+ this.showRemoveModal = false;
+ this.selectedMember = null;
+ await this.loadMembers();
+ } catch (error) {
+ vendorTeamLog.error('Failed to remove team member:', error);
+ Utils.showToast(error.message || 'Failed to remove team member', 'error');
+ } finally {
+ this.saving = false;
+ }
+ },
+
+ /**
+ * Get role display name
+ */
+ getRoleName(member) {
+ if (member.role_name) return member.role_name;
+ const role = this.roles.find(r => r.id === member.role_id);
+ return role ? role.name : 'Unknown';
+ },
+
+ /**
+ * Get member initials for avatar
+ */
+ getInitials(member) {
+ const first = member.first_name || member.email?.charAt(0) || '';
+ const last = member.last_name || '';
+ return (first.charAt(0) + last.charAt(0)).toUpperCase() || '?';
+ },
+
+ /**
+ * Format date for display
+ */
+ formatDate(dateStr) {
+ if (!dateStr) return '-';
+ return new Date(dateStr).toLocaleDateString('de-DE', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric'
+ });
+ }
+ };
+}