Updated vendor documentation

This commit is contained in:
2025-11-22 07:43:26 +01:00
parent 608fa8b95c
commit e5f5a2ebf8
3 changed files with 290 additions and 112 deletions

View File

@@ -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

View File

@@ -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 %}
<div class="flex items-center justify-between my-6">
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
Products
</h2>
</div>
<!-- Coming Soon Notice -->
<div class="w-full mb-8 overflow-hidden rounded-lg shadow-xs">
<div class="w-full p-12 bg-white dark:bg-gray-800 text-center">
<div class="text-6xl mb-4">📦</div>
<h3 class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-2">
Products Management Coming Soon
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-6">
This page is under development.
</p>
<a href="/vendor/{{ vendor_code }}/dashboard"
class="inline-flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700">
Back to Dashboard
</a>
</div>
</div>
{% endblock %}
```
**No JavaScript file needed!** The page inherits all functionality from `init-alpine.js`.
### Pattern 5: Form with Validation
Use for: Settings, profile edit
@@ -709,7 +795,7 @@ validateForm() {
async saveForm() {
if (!this.validateForm()) return;
await apiClient.put(`/api/v1/vendors/${this.vendorCode}/settings`, this.formData);
await apiClient.put(`/vendor/settings`, this.formData);
}
```
@@ -722,7 +808,8 @@ async saveForm() {
try {
await apiClient.get('/endpoint');
} catch (error) {
logError('Operation failed', error);
// Use dedicated page logger
vendorPageLog.error('Operation failed', error);
this.error = error.message || 'An error occurred';
// Don't throw - let UI handle gracefully
}
@@ -773,6 +860,44 @@ closeModal() {
/>
```
### 6. Inherited State from Base (init-alpine.js)
All vendor pages automatically inherit these properties from the base `data()` function:
```javascript
// ✅ Available in all pages via ...data() spread
{
// Vendor context (set by parent init)
vendorCode: '', // Extracted from URL path
vendor: null, // Loaded from API
currentUser: {}, // Loaded from localStorage
// UI state
dark: false, // Dark mode toggle
isSideMenuOpen: false,
isNotificationsMenuOpen: false,
isProfileMenuOpen: false,
currentPage: '', // Override this in your component
// Methods
init() { ... }, // MUST call this via parent init pattern
loadVendorInfo() { ... },
handleLogout() { ... }
}
```
**Important:** Always call parent `init()` before your page logic:
```javascript
async init() {
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
// Now vendorCode and vendor are available
await this.loadData();
}
```
---
## 📱 Responsive Design Checklist
@@ -809,15 +934,15 @@ closeModal() {
```bash
# Create new page files
touch app/templates/vendor/admin/products.html
touch app/templates/vendor/products.html
touch app/static/vendor/js/products.js
# Copy templates
cp template.html app/templates/vendor/admin/products.html
cp template.html app/templates/vendor/products.html
cp template.js app/static/vendor/js/products.js
# Update files with your page name
# Register route in pages.py
# Register route in vendor_pages.py
# Add sidebar link
# Test!
```
@@ -826,11 +951,45 @@ cp template.js app/static/vendor/js/products.js
## 📚 Additional Resources
- **Icons**: Use `$icon('icon-name', 'classes')` helper
- **API Client**: Automatically handles auth tokens
- **Logging**: Use logInfo, logError, logDebug
- **Date Formatting**: Use formatDate() helper
- **Currency**: Use formatCurrency() helper
### Helpers and Utilities
- **Icons**: Use `$icon('icon-name', 'classes')` helper from `shared/js/icons.js`
- **API Client**: Automatically handles auth tokens, prepends `/api/v1` to paths
- **Logging**: Create dedicated logger per page: `const myPageLog = window.LogConfig.loggers.pagename;`
- **Date Formatting**: Use `formatDate()` helper (available in your component)
- **Currency**: Use `formatCurrency()` helper (available in your component)
### Reusable Partials
You can include reusable template partials in your pages:
```jinja2
{# Display vendor information card #}
{% include 'vendor/partials/vendor_info.html' %}
{# Already included in base.html #}
{% include 'vendor/partials/sidebar.html' %}
{% include 'vendor/partials/header.html' %}
```
### API Endpoint Pattern
All vendor API calls follow this pattern:
- **JavaScript**: `apiClient.get('/vendor/endpoint')`
- **Becomes**: `/api/v1/vendor/endpoint`
- **Middleware**: Automatically detects vendor from cookie/header context
- **No need** to include `vendorCode` in API path
### Script Loading Order (from base.html)
The base template loads scripts in this specific order:
1. Log Configuration (`log-config.js`)
2. Icons (`icons.js`)
3. Alpine Base Data (`init-alpine.js`) - provides `data()` function
4. Utils (`utils.js`)
5. API Client (`api-client.js`)
6. Alpine.js library (deferred)
7. Page-specific scripts (your custom JS)
---