diff --git a/app/templates/vendor/customers.html b/app/templates/vendor/customers.html index 9be58642..edd7a3b5 100644 --- a/app/templates/vendor/customers.html +++ b/app/templates/vendor/customers.html @@ -1,31 +1,288 @@ {# app/templates/vendor/customers.html #} {% extends "vendor/base.html" %} +{% from 'shared/macros/pagination.html' import pagination %} +{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %} +{% from 'shared/macros/alerts.html' import loading_state, error_state %} {% block title %}Customers{% endblock %} -{% block alpine_data %}data(){% endblock %} +{% block alpine_data %}vendorCustomers(){% endblock %} {% block content %} -
-

- Customers -

+ +{% call page_header_flex(title='Customers', subtitle='View and manage your customer relationships') %} +
+ {{ refresh_button(loading_var='loading', onclick='loadCustomers()', variant='secondary') }} +
+{% endcall %} + +{{ loading_state('Loading customers...') }} + +{{ error_state('Error loading customers') }} + + +
+ +
+
+ +
+
+

Total Customers

+

0

+
+
+ + +
+
+ +
+
+

Active

+

0

+
+
+ + +
+
+ +
+
+

New This Month

+

0

+
+
- -
-
-
👥
-

- Customer Management Coming Soon -

-

- This page is under development. You'll be able to manage your customers here. -

- - Back to Dashboard - + +
+
+ +
+
+ + + + +
+
+ + + + + + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + +
CustomerEmailJoinedOrdersActions
+
+ +

No customers found

+

Customers will appear here when they make purchases

+
+
+
+
+ + +
+ {{ pagination( + current_page='pagination.page', + total_pages='totalPages', + total_items='pagination.total', + start_index='startIndex', + end_index='endIndex', + page_numbers='pageNumbers', + previous_fn='previousPage()', + next_fn='nextPage()', + goto_fn='goToPage' + ) }} +
+ + +
+
+
+

Customer Details

+ +
+
+
+
+ +
+
+

+

+
+
+
+
+

Phone

+

+
+
+

Joined

+

+
+
+

Total Orders

+

+
+
+

Total Spent

+

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

+ Orders for +

+ +
+
+ + +
+
+ +
{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/app/templates/vendor/inventory.html b/app/templates/vendor/inventory.html index 007c3fad..649895c8 100644 --- a/app/templates/vendor/inventory.html +++ b/app/templates/vendor/inventory.html @@ -1,31 +1,285 @@ {# app/templates/vendor/inventory.html #} {% extends "vendor/base.html" %} +{% from 'shared/macros/pagination.html' import pagination %} +{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %} +{% from 'shared/macros/alerts.html' import loading_state, error_state %} +{% from 'shared/macros/modals.html' import modal_simple %} {% block title %}Inventory{% endblock %} -{% block alpine_data %}data(){% endblock %} +{% block alpine_data %}vendorInventory(){% endblock %} {% block content %} -
-

- Inventory -

-
+ +{% call page_header_flex(title='Inventory', subtitle='Manage your stock levels') %} +
+ {{ refresh_button(loading_var='loading', onclick='loadInventory()', variant='secondary') }} +
+{% endcall %} - -
-
-
📊
-

- Inventory Management Coming Soon -

-

- This page is under development. You'll be able to manage your inventory here. -

- - Back to Dashboard - +{{ loading_state('Loading inventory...') }} + +{{ error_state('Error loading inventory') }} + + +
+ +
+
+ +
+
+

Total Entries

+

0

+
+
+ + +
+
+ +
+
+

Total Stock

+

0

+
+
+ + +
+
+ +
+
+

Low Stock

+

0

+
+
+ + +
+
+ +
+
+

Out of Stock

+

0

+
+ + +
+
+ +
+
+ + + + +
+
+ + + + + + + + + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + +
ProductSKULocationQuantityStatusActions
+
+ +

No inventory found

+

Add products and set their stock levels

+
+
+
+
+ + +
+ {{ pagination( + current_page='pagination.page', + total_pages='totalPages', + total_items='pagination.total', + start_index='startIndex', + end_index='endIndex', + page_numbers='pageNumbers', + previous_fn='previousPage()', + next_fn='nextPage()', + goto_fn='goToPage' + ) }} +
+ + +{{ modal_simple( + show_var='showAdjustModal', + title='Adjust Stock', + icon='plus-minus', + icon_color='blue', + confirm_text='Adjust', + confirm_class='bg-purple-600 hover:bg-purple-700 focus:shadow-outline-purple', + confirm_fn='executeAdjust()', + loading_var='saving' +) }} + + + +{{ modal_simple( + show_var='showSetModal', + title='Set Quantity', + icon='pencil', + icon_color='purple', + confirm_text='Set', + confirm_class='bg-purple-600 hover:bg-purple-700 focus:shadow-outline-purple', + confirm_fn='executeSet()', + loading_var='saving' +) }} + +{% endblock %} + +{% block extra_scripts %} + {% endblock %} diff --git a/app/templates/vendor/orders.html b/app/templates/vendor/orders.html index 08dd8926..f315751c 100644 --- a/app/templates/vendor/orders.html +++ b/app/templates/vendor/orders.html @@ -1,31 +1,258 @@ {# app/templates/vendor/orders.html #} {% extends "vendor/base.html" %} +{% from 'shared/macros/pagination.html' import pagination %} +{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %} +{% from 'shared/macros/alerts.html' import loading_state, error_state %} +{% from 'shared/macros/modals.html' import modal_simple %} {% block title %}Orders{% endblock %} -{% block alpine_data %}data(){% endblock %} +{% block alpine_data %}vendorOrders(){% endblock %} {% block content %} -
-

- Orders -

-
+ +{% call page_header_flex(title='Orders', subtitle='View and manage your orders') %} +
+ {{ refresh_button(loading_var='loading', onclick='loadOrders()', variant='secondary') }} +
+{% endcall %} - -
-
-
🛒
-

- Order Management Coming Soon -

-

- This page is under development. You'll be able to manage your orders here. -

- - Back to Dashboard - +{{ loading_state('Loading orders...') }} + +{{ error_state('Error loading orders') }} + + +
+ +
+
+ +
+
+

Total Orders

+

0

+
+
+ + +
+
+ +
+
+

Pending

+

0

+
+
+ + +
+
+ +
+
+

Processing

+

0

+
+
+ + +
+
+ +
+
+

Completed

+

0

+
+ + +
+
+ +
+
+ + + + +
+
+ + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + +
Order #CustomerDateTotalStatusActions
+
+ +

No orders found

+

Orders will appear here when customers make purchases

+
+
+
+
+ + +
+ {{ pagination( + current_page='pagination.page', + total_pages='totalPages', + total_items='pagination.total', + start_index='startIndex', + end_index='endIndex', + page_numbers='pageNumbers', + previous_fn='previousPage()', + next_fn='nextPage()', + goto_fn='goToPage' + ) }} +
+ + +{{ modal_simple( + show_var='showStatusModal', + title='Update Order Status', + icon='pencil-square', + icon_color='blue', + confirm_text='Update', + confirm_class='bg-purple-600 hover:bg-purple-700 focus:shadow-outline-purple', + confirm_fn='updateStatus()', + loading_var='saving' +) }} + +{% endblock %} + +{% block extra_scripts %} + {% endblock %} diff --git a/app/templates/vendor/partials/sidebar.html b/app/templates/vendor/partials/sidebar.html index c0006203..f52c9e80 100644 --- a/app/templates/vendor/partials/sidebar.html +++ b/app/templates/vendor/partials/sidebar.html @@ -1,237 +1,144 @@ {# app/templates/vendor/partials/sidebar.html #} -{# -Vendor sidebar - loads vendor data client-side via JavaScript -Follows same pattern as admin sidebar -#} +{# Collapsible sidebar sections with localStorage persistence - matching admin pattern #} - - diff --git a/app/templates/vendor/products.html b/app/templates/vendor/products.html index a4e150b9..832adaf9 100644 --- a/app/templates/vendor/products.html +++ b/app/templates/vendor/products.html @@ -1,31 +1,279 @@ {# app/templates/vendor/products.html #} {% extends "vendor/base.html" %} +{% from 'shared/macros/pagination.html' import pagination %} +{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %} +{% from 'shared/macros/alerts.html' import loading_state, error_state %} +{% from 'shared/macros/tables.html' import table_wrapper %} +{% from 'shared/macros/modals.html' import modal_simple %} {% block title %}Products{% endblock %} -{% block alpine_data %}data(){% endblock %} +{% block alpine_data %}vendorProducts(){% endblock %} {% block content %} -
-

- Products -

-
+ +{% call page_header_flex(title='Products', subtitle='Manage your product catalog') %} +
+ {{ refresh_button(loading_var='loading', onclick='loadProducts()', variant='secondary') }} + +
+{% endcall %} - -
-
-
📦
-

- Products Management Coming Soon -

-

- This page is under development. You'll be able to manage your product catalog here. -

- - Back to Dashboard - +{{ loading_state('Loading products...') }} + +{{ error_state('Error loading products') }} + + +
+ +
+
+ +
+
+

Total Products

+

0

+
+
+ + +
+
+ +
+
+

Active

+

0

+
+
+ + +
+
+ +
+
+

Inactive

+

0

+
+
+ + +
+
+ +
+
+

Featured

+

0

+
+ + +
+
+ +
+
+ + + + +
+
+ + + + + + + + + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + +
ProductSKUPriceStatusFeaturedActions
+
+ +

No products found

+

Add your first product to get started

+ +
+
+
+
+ + +
+ {{ pagination( + current_page='pagination.page', + total_pages='totalPages', + total_items='pagination.total', + start_index='startIndex', + end_index='endIndex', + page_numbers='pageNumbers', + previous_fn='previousPage()', + next_fn='nextPage()', + goto_fn='goToPage' + ) }} +
+ + +{{ modal_simple( + show_var='showDeleteModal', + title='Delete Product', + icon='exclamation-triangle', + icon_color='red', + confirm_text='Delete', + confirm_class='bg-red-600 hover:bg-red-700 focus:shadow-outline-red', + confirm_fn='deleteProduct()', + loading_var='saving' +) }} + +{% endblock %} + +{% block extra_scripts %} + {% endblock %} diff --git a/app/templates/vendor/profile.html b/app/templates/vendor/profile.html index 62622c7e..5d0f538f 100644 --- a/app/templates/vendor/profile.html +++ b/app/templates/vendor/profile.html @@ -1,31 +1,206 @@ {# app/templates/vendor/profile.html #} {% extends "vendor/base.html" %} +{% from 'shared/macros/headers.html' import page_header_flex %} +{% from 'shared/macros/alerts.html' import loading_state, error_state %} {% block title %}Profile{% endblock %} -{% block alpine_data %}data(){% endblock %} +{% block alpine_data %}vendorProfile(){% endblock %} {% block content %} -
-

- Profile -

-
+ +{% call page_header_flex(title='Profile', subtitle='Manage your business information') %} +
+ + +
+{% endcall %} - -
-
-
👤
-

- Profile Management Coming Soon -

-

- This page is under development. You'll be able to manage your profile information here. -

- - Back to Dashboard - +{{ loading_state('Loading profile...') }} + +{{ error_state('Error loading profile') }} + + +
+ +
+
+

Business Information

+

Basic information about your business

+
+
+
+ +
+ + +

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

Contact Information

+

How customers can reach you

+
+
+
+ +
+ + +

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

+
+
+
+
+ + +
+
+

Account Information

+

Your vendor account details (read-only)

+
+
+
+ +
+ +

+
+ + +
+ +

+
+ + +
+ + + Verified +
+
+
{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/app/templates/vendor/settings.html b/app/templates/vendor/settings.html index 873977fb..8a5a3c7e 100644 --- a/app/templates/vendor/settings.html +++ b/app/templates/vendor/settings.html @@ -1,31 +1,258 @@ {# app/templates/vendor/settings.html #} {% extends "vendor/base.html" %} +{% from 'shared/macros/headers.html' import page_header_flex %} +{% from 'shared/macros/alerts.html' import loading_state, error_state %} {% block title %}Settings{% endblock %} -{% block alpine_data %}data(){% endblock %} +{% block alpine_data %}vendorSettings(){% endblock %} {% block content %} -
-

- Settings -

-
+ +{% call page_header_flex(title='Settings', subtitle='Configure your vendor preferences') %} +{% endcall %} - -
-
-
⚙️
-

- Settings Coming Soon -

-

- This page is under development. You'll be able to configure your vendor settings here. -

- - Back to Dashboard - +{{ loading_state('Loading settings...') }} + +{{ error_state('Error loading settings') }} + + +
+
+ +
+
+ +
+
+ + +
+ +
+
+

General Settings

+

Basic vendor configuration

+
+
+
+ +
+ +
+ + + .yourplatform.com + +
+

Contact support to change your subdomain

+
+ + +
+
+

Store Status

+

Your store is currently visible to customers

+
+ +
+ + +
+
+

Verification Status

+

Verified vendors get a badge on their store

+
+ +
+
+
+
+ + +
+
+

Marketplace Integration

+

Configure external marketplace feeds

+
+
+
+ +
+

Letzshop CSV Feed URLs

+

+ Enter the URLs for your Letzshop product feeds in different languages. +

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

Notification Preferences

+

Control how you receive notifications

+
+
+
+ +
+
+

Email Notifications

+

Receive important updates via email

+
+ +
+ + +
+
+

Order Notifications

+

Get notified when you receive new orders

+
+ +
+ + +
+
+

Marketing Emails

+

Receive tips, updates, and promotional content

+
+ +
+ +

+ Note: Notification settings are currently display-only. Full notification management coming soon. +

+
+
+
+
{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/app/templates/vendor/team.html b/app/templates/vendor/team.html index e9f691aa..fc29f7be 100644 --- a/app/templates/vendor/team.html +++ b/app/templates/vendor/team.html @@ -1,31 +1,279 @@ {# app/templates/vendor/team.html #} {% extends "vendor/base.html" %} +{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %} +{% from 'shared/macros/alerts.html' import loading_state, error_state %} +{% from 'shared/macros/modals.html' import modal_simple %} {% block title %}Team{% endblock %} -{% block alpine_data %}data(){% endblock %} +{% block alpine_data %}vendorTeam(){% endblock %} {% block content %} -
-

- Team Management -

-
+ +{% call page_header_flex(title='Team', subtitle='Manage your team members and roles') %} +
+ {{ refresh_button(loading_var='loading', onclick='loadMembers()', variant='secondary') }} + +
+{% endcall %} - -
-
-
👨‍💼
-

- 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') }} + + +
+ +
+
+ +
+
+

Total Members

+

0

+
+
+ + +
+
+ +
+
+

Active

+

0

+
+
+ + +
+
+ +
+
+

Pending Invitations

+

0

+
+ + +
+
+ + + + + + + + + + + + + + + + + +
MemberRoleStatusJoinedActions
+
+ +

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' +) }} + + + +{{ 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' +) }} + +{% 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' + }); + } + }; +}