diff --git a/app/templates/vendor/dashboard.html b/app/templates/vendor/dashboard.html index a998a5e6..d0c39c4d 100644 --- a/app/templates/vendor/dashboard.html +++ b/app/templates/vendor/dashboard.html @@ -1,4 +1,4 @@ -{# app/templates/vendor/admin/dashboard.html #} +{# app/templates/vendor/dashboard.html #} {% extends "vendor/base.html" %} {% block title %}Dashboard{% endblock %} diff --git a/docs/frontend/vendor/architecture.md b/docs/frontend/vendor/architecture.md index 5e0c967b..4231a69d 100644 --- a/docs/frontend/vendor/architecture.md +++ b/docs/frontend/vendor/architecture.md @@ -45,20 +45,21 @@ app/ ├── templates/vendor/ │ ├── base.html ← Base template (layout) │ ├── login.html ← Public login page -│ ├── admin/ ← Authenticated pages -│ │ ├── dashboard.html -│ │ ├── products.html -│ │ ├── orders.html -│ │ ├── customers.html -│ │ ├── inventory.html -│ │ ├── marketplace.html -│ │ ├── team.html -│ │ └── settings.html -│ └── partials/ ← Reusable components -│ ├── header.html ← Top navigation -│ ├── sidebar.html ← Main navigation -│ ├── vendor_info.html ← Vendor details card -│ └── notifications.html ← Toast notifications +│ ├── dashboard.html ← Authenticated pages +│ ├── products.html +│ ├── orders.html +│ ├── customers.html +│ ├── inventory.html +│ ├── marketplace.html +│ ├── team.html +│ ├── settings.html +│ ├── profile.html +│ ├── partials/ ← Reusable components +│ │ ├── header.html ← Top navigation +│ │ ├── sidebar.html ← Main navigation +│ │ ├── vendor_info.html ← Vendor details card +│ │ └── notifications.html ← Toast notifications +│ └── errors/ ← Error pages │ ├── static/vendor/ │ ├── css/ @@ -87,8 +88,8 @@ app/ │ └── css/ │ └── base.css ← Global styles │ -└── api/v1/vendor/ - └── pages.py ← Route handlers +└── routes/ + └── vendor_pages.py ← Route handlers 🏗️ ARCHITECTURE LAYERS @@ -108,17 +109,17 @@ Layer 5: Database Layer 1: ROUTES (FastAPI) ────────────────────────────────────────────────────────────────── Purpose: Authentication + Template Rendering -Location: app/api/v1/vendor/pages.py +Location: app/routes/vendor_pages.py Example: - @router.get("/vendor/{vendor_code}/dashboard") + @router.get("/{vendor_code}/dashboard") async def vendor_dashboard_page( request: Request, - vendor_code: str, - current_user: User = Depends(get_current_vendor_user) + vendor_code: str = Path(..., description="Vendor code"), + current_user: User = Depends(get_current_vendor_from_cookie_or_header) ): return templates.TemplateResponse( - "vendor/admin/dashboard.html", + "vendor/dashboard.html", { "request": request, "user": current_user, @@ -142,7 +143,7 @@ Location: app/templates/vendor/ Template Hierarchy: base.html (layout) ↓ - admin/dashboard.html (page) + dashboard.html (page) ↓ partials/sidebar.html (components) @@ -181,7 +182,7 @@ Example: this.loading = true; try { this.stats = await apiClient.get( - `/api/v1/vendors/${this.vendorCode}/stats` + `/api/v1/vendor/dashboard/stats` ); } finally { this.loading = false; @@ -200,14 +201,14 @@ Responsibilities: Layer 4: API (REST) ────────────────────────────────────────────────────────────────── Purpose: Business Logic + Data Access -Location: app/api/v1/vendor/*.py (not pages.py) +Location: app/api/v1/vendor/*.py Example Endpoints: - GET /api/v1/vendors/{code}/stats - GET /api/v1/vendors/{code}/products - POST /api/v1/vendors/{code}/products - PUT /api/v1/vendors/{code}/products/{id} - DELETE /api/v1/vendors/{code}/products/{id} + GET /api/v1/vendor/dashboard/stats + GET /api/v1/vendor/products + POST /api/v1/vendor/products + PUT /api/v1/vendor/products/{id} + DELETE /api/v1/vendor/products/{id} 🔄 DATA FLOW @@ -261,19 +262,26 @@ Custom CSS Variables (vendor/css/vendor.css): Auth Flow: 1. Login → POST /api/v1/vendor/auth/login - 2. API → Return JWT token - 3. JavaScript → Store in localStorage - 4. API Client → Add to all requests - 5. Routes → Verify with get_current_vendor_user + 2. API → Return JWT token + set vendor_token cookie + 3. JavaScript → Token stored in localStorage (optional) + 4. Cookie → Automatically sent with page requests + 5. API Client → Add Authorization header for API calls + 6. Routes → Verify with get_current_vendor_from_cookie_or_header + +Token Storage: + • HttpOnly Cookie: vendor_token (path=/vendor) - For page navigation + • LocalStorage: Optional, for JavaScript API calls + • Dual authentication: Supports both cookie and header-based auth Protected Routes: - • All /vendor/{code}/admin/* routes - • Require valid JWT token + • All /vendor/{code}/* routes (except /login) + • Require valid JWT token (cookie or header) • Redirect to login if unauthorized Public Routes: • /vendor/{code}/login • No authentication required + • Uses get_current_vendor_optional to redirect if already logged in 📱 RESPONSIVE DESIGN @@ -546,8 +554,8 @@ Components: • Recent orders table • Quick actions Data Sources: - • GET /api/v1/vendors/{code}/stats - • GET /api/v1/vendors/{code}/orders?limit=5 + • GET /api/v1/vendor/dashboard/stats + • GET /api/v1/vendor/orders?limit=5 /vendor/{code}/products ────────────────────────────────────────────────────────────────── @@ -557,9 +565,9 @@ Components: • Search and filters • Create/Edit modal Data Sources: - • GET /api/v1/vendors/{code}/products - • POST /api/v1/vendors/{code}/products - • PUT /api/v1/vendors/{code}/products/{id} + • GET /api/v1/vendor/products + • POST /api/v1/vendor/products + • PUT /api/v1/vendor/products/{id} /vendor/{code}/orders ────────────────────────────────────────────────────────────────── @@ -569,8 +577,8 @@ Components: • Status filters • Order detail modal Data Sources: - • GET /api/v1/vendors/{code}/orders - • PUT /api/v1/vendors/{code}/orders/{id} + • GET /api/v1/vendor/orders + • PUT /api/v1/vendor/orders/{id} /vendor/{code}/customers ────────────────────────────────────────────────────────────────── @@ -580,7 +588,7 @@ Components: • Search functionality • Customer detail view Data Sources: - • GET /api/v1/vendors/{code}/customers + • GET /api/v1/vendor/customers /vendor/{code}/inventory ────────────────────────────────────────────────────────────────── @@ -590,8 +598,8 @@ Components: • Stock adjustment modal • Low stock alerts Data Sources: - • GET /api/v1/vendors/{code}/inventory - • PUT /api/v1/vendors/{code}/inventory/{id} + • GET /api/v1/vendor/inventory + • PUT /api/v1/vendor/inventory/{id} /vendor/{code}/marketplace ────────────────────────────────────────────────────────────────── @@ -601,8 +609,8 @@ Components: • Product browser • Import wizard Data Sources: - • GET /api/v1/vendors/{code}/marketplace/jobs - • POST /api/v1/vendors/{code}/marketplace/import + • GET /api/v1/vendor/marketplace/jobs + • POST /api/v1/vendor/marketplace/import /vendor/{code}/team ────────────────────────────────────────────────────────────────── @@ -612,8 +620,19 @@ Components: • Role management • Invitation form Data Sources: - • GET /api/v1/vendors/{code}/team - • POST /api/v1/vendors/{code}/team/invite + • GET /api/v1/vendor/team + • POST /api/v1/vendor/team/invite + +/vendor/{code}/profile +────────────────────────────────────────────────────────────────── +Purpose: Manage vendor profile and branding +Components: + • Profile information form + • Branding settings + • Business details +Data Sources: + • GET /api/v1/vendor/profile + • PUT /api/v1/vendor/profile /vendor/{code}/settings ────────────────────────────────────────────────────────────────── @@ -623,8 +642,8 @@ Components: • Form sections • Save buttons Data Sources: - • GET /api/v1/vendors/{code}/settings - • PUT /api/v1/vendors/{code}/settings + • GET /api/v1/vendor/settings + • PUT /api/v1/vendor/settings 🎓 LEARNING PATH diff --git a/docs/frontend/vendor/page-templates.md b/docs/frontend/vendor/page-templates.md index f6417055..f04dd7fd 100644 --- a/docs/frontend/vendor/page-templates.md +++ b/docs/frontend/vendor/page-templates.md @@ -11,18 +11,18 @@ This guide provides complete templates for creating new vendor admin pages using ### File Structure for New Page ``` app/ -├── templates/vendor/admin/ +├── templates/vendor/ │ └── [page-name].html # Jinja2 template ├── static/vendor/js/ │ └── [page-name].js # Alpine.js component -└── api/v1/vendor/ - └── pages.py # Route registration +└── routes/ + └── vendor_pages.py # Route registration ``` ### Checklist for New Page - [ ] Create Jinja2 template extending base.html - [ ] Create Alpine.js JavaScript component -- [ ] Register route in pages.py +- [ ] Register route in vendor_pages.py - [ ] Add navigation link to sidebar.html - [ ] Test authentication - [ ] Test data loading @@ -34,16 +34,16 @@ app/ ### 1. Jinja2 Template -**File:** `app/templates/vendor/admin/[page-name].html` +**File:** `app/templates/vendor/[page-name].html` ```jinja2 -{# app/templates/vendor/admin/[page-name].html #} +{# app/templates/vendor/[page-name].html #} {% extends "vendor/base.html" %} {# Page title for browser tab #} {% block title %}[Page Name]{% endblock %} -{# Alpine.js component name #} +{# Alpine.js component name - use data() for simple pages or vendor[PageName]() for complex pages #} {% block alpine_data %}vendor[PageName](){% endblock %} {# Page content #} @@ -347,22 +347,34 @@ app/ * Handles data loading, filtering, CRUD operations */ +// ✅ Create dedicated logger for this page +const vendor[PageName]Log = window.LogConfig.loggers.[pagename]; + function vendor[PageName]() { return { // ═══════════════════════════════════════════════════════════ - // STATE + // INHERIT BASE STATE (from init-alpine.js) + // ═══════════════════════════════════════════════════════════ + // This provides: vendorCode, currentUser, vendor, dark mode, menu states + ...data(), + + // ✅ Set page identifier (for sidebar highlighting) + currentPage: '[page-name]', + + // ═══════════════════════════════════════════════════════════ + // PAGE-SPECIFIC STATE // ═══════════════════════════════════════════════════════════ loading: false, error: '', items: [], - + // Filters filters: { search: '', status: '', sortBy: 'created_at:desc' }, - + // Pagination pagination: { currentPage: 1, @@ -374,21 +386,33 @@ function vendor[PageName]() { hasPrevious: false, hasNext: false }, - + // Modal state showModal: false, modalTitle: '', modalMode: 'create', // 'create' or 'edit' formData: {}, saving: false, - + // ═══════════════════════════════════════════════════════════ // LIFECYCLE // ═══════════════════════════════════════════════════════════ async init() { - logInfo('[PageName] page initializing...'); + // Guard against multiple initialization + if (window._vendor[PageName]Initialized) { + return; + } + window._vendor[PageName]Initialized = true; + + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + + vendor[PageName]Log.info('[PageName] page initializing...'); await this.loadData(); - logInfo('[PageName] page initialized'); + vendor[PageName]Log.info('[PageName] page initialized'); }, // ═══════════════════════════════════════════════════════════ @@ -397,7 +421,7 @@ function vendor[PageName]() { async loadData() { this.loading = true; this.error = ''; - + try { // Build query params const params = new URLSearchParams({ @@ -405,23 +429,25 @@ function vendor[PageName]() { per_page: this.pagination.perPage, ...this.filters }); - + // API call + // NOTE: apiClient prepends /api/v1, and vendor context middleware handles vendor detection + // So we just call /vendor/[endpoint] → becomes /api/v1/vendor/[endpoint] const response = await apiClient.get( - `/api/v1/vendors/${this.vendorCode}/[endpoint]?${params}` + `/vendor/[endpoint]?${params}` ); - + // Update state this.items = response.items || []; this.updatePagination(response); - - logInfo('[PageName] data loaded', { + + vendor[PageName]Log.info('[PageName] data loaded', { items: this.items.length, total: this.pagination.total }); - + } catch (error) { - logError('Failed to load [page] data', error); + vendor[PageName]Log.error('Failed to load [page] data', error); this.error = error.message || 'Failed to load data'; } finally { this.loading = false; @@ -490,64 +516,64 @@ function vendor[PageName]() { try { // Load item data const item = await apiClient.get( - `/api/v1/vendors/${this.vendorCode}/[endpoint]/${id}` + `/vendor/[endpoint]/${id}` ); - + this.modalMode = 'edit'; this.modalTitle = 'Edit Item'; this.formData = { ...item }; this.showModal = true; - + } catch (error) { - logError('Failed to load item', error); + vendor[PageName]Log.error('Failed to load item', error); alert('Failed to load item details'); } }, - + async saveItem() { this.saving = true; - + try { if (this.modalMode === 'create') { await apiClient.post( - `/api/v1/vendors/${this.vendorCode}/[endpoint]`, + `/vendor/[endpoint]`, this.formData ); - logInfo('Item created successfully'); + vendor[PageName]Log.info('Item created successfully'); } else { await apiClient.put( - `/api/v1/vendors/${this.vendorCode}/[endpoint]/${this.formData.id}`, + `/vendor/[endpoint]/${this.formData.id}`, this.formData ); - logInfo('Item updated successfully'); + vendor[PageName]Log.info('Item updated successfully'); } - + this.closeModal(); await this.loadData(); - + } catch (error) { - logError('Failed to save item', error); + vendor[PageName]Log.error('Failed to save item', error); alert(error.message || 'Failed to save item'); } finally { this.saving = false; } }, - + async deleteItem(id) { if (!confirm('Are you sure you want to delete this item?')) { return; } - + try { await apiClient.delete( - `/api/v1/vendors/${this.vendorCode}/[endpoint]/${id}` + `/vendor/[endpoint]/${id}` ); - - logInfo('Item deleted successfully'); + + vendor[PageName]Log.info('Item deleted successfully'); await this.loadData(); - + } catch (error) { - logError('Failed to delete item', error); + vendor[PageName]Log.error('Failed to delete item', error); alert(error.message || 'Failed to delete item'); } }, @@ -587,21 +613,21 @@ window.vendor[PageName] = vendor[PageName]; ### 3. Route Registration -**File:** `app/api/v1/vendor/pages.py` +**File:** `app/routes/vendor_pages.py` ```python -@router.get("/vendor/{vendor_code}/[page-name]", response_class=HTMLResponse, include_in_schema=False) +@router.get("/{vendor_code}/[page-name]", response_class=HTMLResponse, include_in_schema=False) async def vendor_[page_name]_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), - current_user: User = Depends(get_current_vendor_user) + current_user: User = Depends(get_current_vendor_from_cookie_or_header) ): """ Render [page name] page. JavaScript loads data via API. """ return templates.TemplateResponse( - "vendor/admin/[page-name].html", + "vendor/[page-name].html", { "request": request, "user": current_user, @@ -640,13 +666,19 @@ Use for: Product list, order list, customer list ```javascript async init() { + // Call parent init first + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + await this.loadData(); } async loadData() { this.loading = true; try { - const response = await apiClient.get(`/api/v1/vendors/${this.vendorCode}/items`); + const response = await apiClient.get(`/vendor/items`); this.items = response.items || []; } catch (error) { this.error = error.message; @@ -662,6 +694,12 @@ Use for: Dashboard, analytics pages ```javascript async init() { + // Call parent init first + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + await Promise.all([ this.loadStats(), this.loadRecentActivity() @@ -669,7 +707,7 @@ async init() { } async loadStats() { - const stats = await apiClient.get(`/api/v1/vendors/${this.vendorCode}/stats`); + const stats = await apiClient.get(`/vendor/stats`); this.stats = stats; } ``` @@ -680,16 +718,64 @@ Use for: Product detail, order detail ```javascript async init() { + // Call parent init first + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + await this.loadItem(); } async loadItem() { const id = this.getItemIdFromUrl(); - this.item = await apiClient.get(`/api/v1/vendors/${this.vendorCode}/items/${id}`); + this.item = await apiClient.get(`/vendor/items/${id}`); } ``` -### Pattern 4: Form with Validation +### Pattern 4: Simple Page (No Custom JavaScript) + +Use for: Coming soon pages, static pages, pages under development + +**Template:** `app/templates/vendor/[page-name].html` +```jinja2 +{# app/templates/vendor/products.html #} +{% extends "vendor/base.html" %} + +{% block title %}Products{% endblock %} + +{# Use base data() directly - no custom JavaScript needed #} +{% block alpine_data %}data(){% endblock %} + +{% block content %} +
+ This page is under development. +
+ + Back to Dashboard + +