Updated vendor documentation
This commit is contained in:
2
app/templates/vendor/dashboard.html
vendored
2
app/templates/vendor/dashboard.html
vendored
@@ -1,4 +1,4 @@
|
|||||||
{# app/templates/vendor/admin/dashboard.html #}
|
{# app/templates/vendor/dashboard.html #}
|
||||||
{% extends "vendor/base.html" %}
|
{% extends "vendor/base.html" %}
|
||||||
|
|
||||||
{% block title %}Dashboard{% endblock %}
|
{% block title %}Dashboard{% endblock %}
|
||||||
|
|||||||
121
docs/frontend/vendor/architecture.md
vendored
121
docs/frontend/vendor/architecture.md
vendored
@@ -45,20 +45,21 @@ app/
|
|||||||
├── templates/vendor/
|
├── templates/vendor/
|
||||||
│ ├── base.html ← Base template (layout)
|
│ ├── base.html ← Base template (layout)
|
||||||
│ ├── login.html ← Public login page
|
│ ├── login.html ← Public login page
|
||||||
│ ├── admin/ ← Authenticated pages
|
│ ├── dashboard.html ← Authenticated pages
|
||||||
│ │ ├── dashboard.html
|
│ ├── products.html
|
||||||
│ │ ├── products.html
|
│ ├── orders.html
|
||||||
│ │ ├── orders.html
|
│ ├── customers.html
|
||||||
│ │ ├── customers.html
|
│ ├── inventory.html
|
||||||
│ │ ├── inventory.html
|
│ ├── marketplace.html
|
||||||
│ │ ├── marketplace.html
|
│ ├── team.html
|
||||||
│ │ ├── team.html
|
│ ├── settings.html
|
||||||
│ │ └── settings.html
|
│ ├── profile.html
|
||||||
│ └── partials/ ← Reusable components
|
│ ├── partials/ ← Reusable components
|
||||||
│ ├── header.html ← Top navigation
|
│ │ ├── header.html ← Top navigation
|
||||||
│ ├── sidebar.html ← Main navigation
|
│ │ ├── sidebar.html ← Main navigation
|
||||||
│ ├── vendor_info.html ← Vendor details card
|
│ │ ├── vendor_info.html ← Vendor details card
|
||||||
│ └── notifications.html ← Toast notifications
|
│ │ └── notifications.html ← Toast notifications
|
||||||
|
│ └── errors/ ← Error pages
|
||||||
│
|
│
|
||||||
├── static/vendor/
|
├── static/vendor/
|
||||||
│ ├── css/
|
│ ├── css/
|
||||||
@@ -87,8 +88,8 @@ app/
|
|||||||
│ └── css/
|
│ └── css/
|
||||||
│ └── base.css ← Global styles
|
│ └── base.css ← Global styles
|
||||||
│
|
│
|
||||||
└── api/v1/vendor/
|
└── routes/
|
||||||
└── pages.py ← Route handlers
|
└── vendor_pages.py ← Route handlers
|
||||||
|
|
||||||
|
|
||||||
🏗️ ARCHITECTURE LAYERS
|
🏗️ ARCHITECTURE LAYERS
|
||||||
@@ -108,17 +109,17 @@ Layer 5: Database
|
|||||||
Layer 1: ROUTES (FastAPI)
|
Layer 1: ROUTES (FastAPI)
|
||||||
──────────────────────────────────────────────────────────────────
|
──────────────────────────────────────────────────────────────────
|
||||||
Purpose: Authentication + Template Rendering
|
Purpose: Authentication + Template Rendering
|
||||||
Location: app/api/v1/vendor/pages.py
|
Location: app/routes/vendor_pages.py
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
@router.get("/vendor/{vendor_code}/dashboard")
|
@router.get("/{vendor_code}/dashboard")
|
||||||
async def vendor_dashboard_page(
|
async def vendor_dashboard_page(
|
||||||
request: Request,
|
request: Request,
|
||||||
vendor_code: str,
|
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)
|
||||||
):
|
):
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"vendor/admin/dashboard.html",
|
"vendor/dashboard.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
"request": request,
|
||||||
"user": current_user,
|
"user": current_user,
|
||||||
@@ -142,7 +143,7 @@ Location: app/templates/vendor/
|
|||||||
Template Hierarchy:
|
Template Hierarchy:
|
||||||
base.html (layout)
|
base.html (layout)
|
||||||
↓
|
↓
|
||||||
admin/dashboard.html (page)
|
dashboard.html (page)
|
||||||
↓
|
↓
|
||||||
partials/sidebar.html (components)
|
partials/sidebar.html (components)
|
||||||
|
|
||||||
@@ -181,7 +182,7 @@ Example:
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
this.stats = await apiClient.get(
|
this.stats = await apiClient.get(
|
||||||
`/api/v1/vendors/${this.vendorCode}/stats`
|
`/api/v1/vendor/dashboard/stats`
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
@@ -200,14 +201,14 @@ Responsibilities:
|
|||||||
Layer 4: API (REST)
|
Layer 4: API (REST)
|
||||||
──────────────────────────────────────────────────────────────────
|
──────────────────────────────────────────────────────────────────
|
||||||
Purpose: Business Logic + Data Access
|
Purpose: Business Logic + Data Access
|
||||||
Location: app/api/v1/vendor/*.py (not pages.py)
|
Location: app/api/v1/vendor/*.py
|
||||||
|
|
||||||
Example Endpoints:
|
Example Endpoints:
|
||||||
GET /api/v1/vendors/{code}/stats
|
GET /api/v1/vendor/dashboard/stats
|
||||||
GET /api/v1/vendors/{code}/products
|
GET /api/v1/vendor/products
|
||||||
POST /api/v1/vendors/{code}/products
|
POST /api/v1/vendor/products
|
||||||
PUT /api/v1/vendors/{code}/products/{id}
|
PUT /api/v1/vendor/products/{id}
|
||||||
DELETE /api/v1/vendors/{code}/products/{id}
|
DELETE /api/v1/vendor/products/{id}
|
||||||
|
|
||||||
|
|
||||||
🔄 DATA FLOW
|
🔄 DATA FLOW
|
||||||
@@ -261,19 +262,26 @@ Custom CSS Variables (vendor/css/vendor.css):
|
|||||||
|
|
||||||
Auth Flow:
|
Auth Flow:
|
||||||
1. Login → POST /api/v1/vendor/auth/login
|
1. Login → POST /api/v1/vendor/auth/login
|
||||||
2. API → Return JWT token
|
2. API → Return JWT token + set vendor_token cookie
|
||||||
3. JavaScript → Store in localStorage
|
3. JavaScript → Token stored in localStorage (optional)
|
||||||
4. API Client → Add to all requests
|
4. Cookie → Automatically sent with page requests
|
||||||
5. Routes → Verify with get_current_vendor_user
|
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:
|
Protected Routes:
|
||||||
• All /vendor/{code}/admin/* routes
|
• All /vendor/{code}/* routes (except /login)
|
||||||
• Require valid JWT token
|
• Require valid JWT token (cookie or header)
|
||||||
• Redirect to login if unauthorized
|
• Redirect to login if unauthorized
|
||||||
|
|
||||||
Public Routes:
|
Public Routes:
|
||||||
• /vendor/{code}/login
|
• /vendor/{code}/login
|
||||||
• No authentication required
|
• No authentication required
|
||||||
|
• Uses get_current_vendor_optional to redirect if already logged in
|
||||||
|
|
||||||
|
|
||||||
📱 RESPONSIVE DESIGN
|
📱 RESPONSIVE DESIGN
|
||||||
@@ -546,8 +554,8 @@ Components:
|
|||||||
• Recent orders table
|
• Recent orders table
|
||||||
• Quick actions
|
• Quick actions
|
||||||
Data Sources:
|
Data Sources:
|
||||||
• GET /api/v1/vendors/{code}/stats
|
• GET /api/v1/vendor/dashboard/stats
|
||||||
• GET /api/v1/vendors/{code}/orders?limit=5
|
• GET /api/v1/vendor/orders?limit=5
|
||||||
|
|
||||||
/vendor/{code}/products
|
/vendor/{code}/products
|
||||||
──────────────────────────────────────────────────────────────────
|
──────────────────────────────────────────────────────────────────
|
||||||
@@ -557,9 +565,9 @@ Components:
|
|||||||
• Search and filters
|
• Search and filters
|
||||||
• Create/Edit modal
|
• Create/Edit modal
|
||||||
Data Sources:
|
Data Sources:
|
||||||
• GET /api/v1/vendors/{code}/products
|
• GET /api/v1/vendor/products
|
||||||
• POST /api/v1/vendors/{code}/products
|
• POST /api/v1/vendor/products
|
||||||
• PUT /api/v1/vendors/{code}/products/{id}
|
• PUT /api/v1/vendor/products/{id}
|
||||||
|
|
||||||
/vendor/{code}/orders
|
/vendor/{code}/orders
|
||||||
──────────────────────────────────────────────────────────────────
|
──────────────────────────────────────────────────────────────────
|
||||||
@@ -569,8 +577,8 @@ Components:
|
|||||||
• Status filters
|
• Status filters
|
||||||
• Order detail modal
|
• Order detail modal
|
||||||
Data Sources:
|
Data Sources:
|
||||||
• GET /api/v1/vendors/{code}/orders
|
• GET /api/v1/vendor/orders
|
||||||
• PUT /api/v1/vendors/{code}/orders/{id}
|
• PUT /api/v1/vendor/orders/{id}
|
||||||
|
|
||||||
/vendor/{code}/customers
|
/vendor/{code}/customers
|
||||||
──────────────────────────────────────────────────────────────────
|
──────────────────────────────────────────────────────────────────
|
||||||
@@ -580,7 +588,7 @@ Components:
|
|||||||
• Search functionality
|
• Search functionality
|
||||||
• Customer detail view
|
• Customer detail view
|
||||||
Data Sources:
|
Data Sources:
|
||||||
• GET /api/v1/vendors/{code}/customers
|
• GET /api/v1/vendor/customers
|
||||||
|
|
||||||
/vendor/{code}/inventory
|
/vendor/{code}/inventory
|
||||||
──────────────────────────────────────────────────────────────────
|
──────────────────────────────────────────────────────────────────
|
||||||
@@ -590,8 +598,8 @@ Components:
|
|||||||
• Stock adjustment modal
|
• Stock adjustment modal
|
||||||
• Low stock alerts
|
• Low stock alerts
|
||||||
Data Sources:
|
Data Sources:
|
||||||
• GET /api/v1/vendors/{code}/inventory
|
• GET /api/v1/vendor/inventory
|
||||||
• PUT /api/v1/vendors/{code}/inventory/{id}
|
• PUT /api/v1/vendor/inventory/{id}
|
||||||
|
|
||||||
/vendor/{code}/marketplace
|
/vendor/{code}/marketplace
|
||||||
──────────────────────────────────────────────────────────────────
|
──────────────────────────────────────────────────────────────────
|
||||||
@@ -601,8 +609,8 @@ Components:
|
|||||||
• Product browser
|
• Product browser
|
||||||
• Import wizard
|
• Import wizard
|
||||||
Data Sources:
|
Data Sources:
|
||||||
• GET /api/v1/vendors/{code}/marketplace/jobs
|
• GET /api/v1/vendor/marketplace/jobs
|
||||||
• POST /api/v1/vendors/{code}/marketplace/import
|
• POST /api/v1/vendor/marketplace/import
|
||||||
|
|
||||||
/vendor/{code}/team
|
/vendor/{code}/team
|
||||||
──────────────────────────────────────────────────────────────────
|
──────────────────────────────────────────────────────────────────
|
||||||
@@ -612,8 +620,19 @@ Components:
|
|||||||
• Role management
|
• Role management
|
||||||
• Invitation form
|
• Invitation form
|
||||||
Data Sources:
|
Data Sources:
|
||||||
• GET /api/v1/vendors/{code}/team
|
• GET /api/v1/vendor/team
|
||||||
• POST /api/v1/vendors/{code}/team/invite
|
• 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
|
/vendor/{code}/settings
|
||||||
──────────────────────────────────────────────────────────────────
|
──────────────────────────────────────────────────────────────────
|
||||||
@@ -623,8 +642,8 @@ Components:
|
|||||||
• Form sections
|
• Form sections
|
||||||
• Save buttons
|
• Save buttons
|
||||||
Data Sources:
|
Data Sources:
|
||||||
• GET /api/v1/vendors/{code}/settings
|
• GET /api/v1/vendor/settings
|
||||||
• PUT /api/v1/vendors/{code}/settings
|
• PUT /api/v1/vendor/settings
|
||||||
|
|
||||||
|
|
||||||
🎓 LEARNING PATH
|
🎓 LEARNING PATH
|
||||||
|
|||||||
241
docs/frontend/vendor/page-templates.md
vendored
241
docs/frontend/vendor/page-templates.md
vendored
@@ -11,18 +11,18 @@ This guide provides complete templates for creating new vendor admin pages using
|
|||||||
### File Structure for New Page
|
### File Structure for New Page
|
||||||
```
|
```
|
||||||
app/
|
app/
|
||||||
├── templates/vendor/admin/
|
├── templates/vendor/
|
||||||
│ └── [page-name].html # Jinja2 template
|
│ └── [page-name].html # Jinja2 template
|
||||||
├── static/vendor/js/
|
├── static/vendor/js/
|
||||||
│ └── [page-name].js # Alpine.js component
|
│ └── [page-name].js # Alpine.js component
|
||||||
└── api/v1/vendor/
|
└── routes/
|
||||||
└── pages.py # Route registration
|
└── vendor_pages.py # Route registration
|
||||||
```
|
```
|
||||||
|
|
||||||
### Checklist for New Page
|
### Checklist for New Page
|
||||||
- [ ] Create Jinja2 template extending base.html
|
- [ ] Create Jinja2 template extending base.html
|
||||||
- [ ] Create Alpine.js JavaScript component
|
- [ ] Create Alpine.js JavaScript component
|
||||||
- [ ] Register route in pages.py
|
- [ ] Register route in vendor_pages.py
|
||||||
- [ ] Add navigation link to sidebar.html
|
- [ ] Add navigation link to sidebar.html
|
||||||
- [ ] Test authentication
|
- [ ] Test authentication
|
||||||
- [ ] Test data loading
|
- [ ] Test data loading
|
||||||
@@ -34,16 +34,16 @@ app/
|
|||||||
|
|
||||||
### 1. Jinja2 Template
|
### 1. Jinja2 Template
|
||||||
|
|
||||||
**File:** `app/templates/vendor/admin/[page-name].html`
|
**File:** `app/templates/vendor/[page-name].html`
|
||||||
|
|
||||||
```jinja2
|
```jinja2
|
||||||
{# app/templates/vendor/admin/[page-name].html #}
|
{# app/templates/vendor/[page-name].html #}
|
||||||
{% extends "vendor/base.html" %}
|
{% extends "vendor/base.html" %}
|
||||||
|
|
||||||
{# Page title for browser tab #}
|
{# Page title for browser tab #}
|
||||||
{% block title %}[Page Name]{% endblock %}
|
{% 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 %}
|
{% block alpine_data %}vendor[PageName](){% endblock %}
|
||||||
|
|
||||||
{# Page content #}
|
{# Page content #}
|
||||||
@@ -347,10 +347,22 @@ app/
|
|||||||
* Handles data loading, filtering, CRUD operations
|
* Handles data loading, filtering, CRUD operations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// ✅ Create dedicated logger for this page
|
||||||
|
const vendor[PageName]Log = window.LogConfig.loggers.[pagename];
|
||||||
|
|
||||||
function vendor[PageName]() {
|
function vendor[PageName]() {
|
||||||
return {
|
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,
|
loading: false,
|
||||||
error: '',
|
error: '',
|
||||||
@@ -386,9 +398,21 @@ function vendor[PageName]() {
|
|||||||
// LIFECYCLE
|
// LIFECYCLE
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
async init() {
|
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();
|
await this.loadData();
|
||||||
logInfo('[PageName] page initialized');
|
vendor[PageName]Log.info('[PageName] page initialized');
|
||||||
},
|
},
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
@@ -407,21 +431,23 @@ function vendor[PageName]() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// API call
|
// 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(
|
const response = await apiClient.get(
|
||||||
`/api/v1/vendors/${this.vendorCode}/[endpoint]?${params}`
|
`/vendor/[endpoint]?${params}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
this.items = response.items || [];
|
this.items = response.items || [];
|
||||||
this.updatePagination(response);
|
this.updatePagination(response);
|
||||||
|
|
||||||
logInfo('[PageName] data loaded', {
|
vendor[PageName]Log.info('[PageName] data loaded', {
|
||||||
items: this.items.length,
|
items: this.items.length,
|
||||||
total: this.pagination.total
|
total: this.pagination.total
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} 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';
|
this.error = error.message || 'Failed to load data';
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
@@ -490,7 +516,7 @@ function vendor[PageName]() {
|
|||||||
try {
|
try {
|
||||||
// Load item data
|
// Load item data
|
||||||
const item = await apiClient.get(
|
const item = await apiClient.get(
|
||||||
`/api/v1/vendors/${this.vendorCode}/[endpoint]/${id}`
|
`/vendor/[endpoint]/${id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
this.modalMode = 'edit';
|
this.modalMode = 'edit';
|
||||||
@@ -499,7 +525,7 @@ function vendor[PageName]() {
|
|||||||
this.showModal = true;
|
this.showModal = true;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError('Failed to load item', error);
|
vendor[PageName]Log.error('Failed to load item', error);
|
||||||
alert('Failed to load item details');
|
alert('Failed to load item details');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -510,23 +536,23 @@ function vendor[PageName]() {
|
|||||||
try {
|
try {
|
||||||
if (this.modalMode === 'create') {
|
if (this.modalMode === 'create') {
|
||||||
await apiClient.post(
|
await apiClient.post(
|
||||||
`/api/v1/vendors/${this.vendorCode}/[endpoint]`,
|
`/vendor/[endpoint]`,
|
||||||
this.formData
|
this.formData
|
||||||
);
|
);
|
||||||
logInfo('Item created successfully');
|
vendor[PageName]Log.info('Item created successfully');
|
||||||
} else {
|
} else {
|
||||||
await apiClient.put(
|
await apiClient.put(
|
||||||
`/api/v1/vendors/${this.vendorCode}/[endpoint]/${this.formData.id}`,
|
`/vendor/[endpoint]/${this.formData.id}`,
|
||||||
this.formData
|
this.formData
|
||||||
);
|
);
|
||||||
logInfo('Item updated successfully');
|
vendor[PageName]Log.info('Item updated successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.closeModal();
|
this.closeModal();
|
||||||
await this.loadData();
|
await this.loadData();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError('Failed to save item', error);
|
vendor[PageName]Log.error('Failed to save item', error);
|
||||||
alert(error.message || 'Failed to save item');
|
alert(error.message || 'Failed to save item');
|
||||||
} finally {
|
} finally {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
@@ -540,14 +566,14 @@ function vendor[PageName]() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await apiClient.delete(
|
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();
|
await this.loadData();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError('Failed to delete item', error);
|
vendor[PageName]Log.error('Failed to delete item', error);
|
||||||
alert(error.message || 'Failed to delete item');
|
alert(error.message || 'Failed to delete item');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -587,21 +613,21 @@ window.vendor[PageName] = vendor[PageName];
|
|||||||
|
|
||||||
### 3. Route Registration
|
### 3. Route Registration
|
||||||
|
|
||||||
**File:** `app/api/v1/vendor/pages.py`
|
**File:** `app/routes/vendor_pages.py`
|
||||||
|
|
||||||
```python
|
```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(
|
async def vendor_[page_name]_page(
|
||||||
request: Request,
|
request: Request,
|
||||||
vendor_code: str = Path(..., description="Vendor code"),
|
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.
|
Render [page name] page.
|
||||||
JavaScript loads data via API.
|
JavaScript loads data via API.
|
||||||
"""
|
"""
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"vendor/admin/[page-name].html",
|
"vendor/[page-name].html",
|
||||||
{
|
{
|
||||||
"request": request,
|
"request": request,
|
||||||
"user": current_user,
|
"user": current_user,
|
||||||
@@ -640,13 +666,19 @@ Use for: Product list, order list, customer list
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
async init() {
|
async init() {
|
||||||
|
// Call parent init first
|
||||||
|
const parentInit = data().init;
|
||||||
|
if (parentInit) {
|
||||||
|
await parentInit.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
await this.loadData();
|
await this.loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadData() {
|
async loadData() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(`/api/v1/vendors/${this.vendorCode}/items`);
|
const response = await apiClient.get(`/vendor/items`);
|
||||||
this.items = response.items || [];
|
this.items = response.items || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error = error.message;
|
this.error = error.message;
|
||||||
@@ -662,6 +694,12 @@ Use for: Dashboard, analytics pages
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
async init() {
|
async init() {
|
||||||
|
// Call parent init first
|
||||||
|
const parentInit = data().init;
|
||||||
|
if (parentInit) {
|
||||||
|
await parentInit.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.loadStats(),
|
this.loadStats(),
|
||||||
this.loadRecentActivity()
|
this.loadRecentActivity()
|
||||||
@@ -669,7 +707,7 @@ async init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadStats() {
|
async loadStats() {
|
||||||
const stats = await apiClient.get(`/api/v1/vendors/${this.vendorCode}/stats`);
|
const stats = await apiClient.get(`/vendor/stats`);
|
||||||
this.stats = stats;
|
this.stats = stats;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -680,16 +718,64 @@ Use for: Product detail, order detail
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
async init() {
|
async init() {
|
||||||
|
// Call parent init first
|
||||||
|
const parentInit = data().init;
|
||||||
|
if (parentInit) {
|
||||||
|
await parentInit.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
await this.loadItem();
|
await this.loadItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadItem() {
|
async loadItem() {
|
||||||
const id = this.getItemIdFromUrl();
|
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
|
Use for: Settings, profile edit
|
||||||
|
|
||||||
@@ -709,7 +795,7 @@ validateForm() {
|
|||||||
|
|
||||||
async saveForm() {
|
async saveForm() {
|
||||||
if (!this.validateForm()) return;
|
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 {
|
try {
|
||||||
await apiClient.get('/endpoint');
|
await apiClient.get('/endpoint');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError('Operation failed', error);
|
// Use dedicated page logger
|
||||||
|
vendorPageLog.error('Operation failed', error);
|
||||||
this.error = error.message || 'An error occurred';
|
this.error = error.message || 'An error occurred';
|
||||||
// Don't throw - let UI handle gracefully
|
// 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
|
## 📱 Responsive Design Checklist
|
||||||
@@ -809,15 +934,15 @@ closeModal() {
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create new page files
|
# Create new page files
|
||||||
touch app/templates/vendor/admin/products.html
|
touch app/templates/vendor/products.html
|
||||||
touch app/static/vendor/js/products.js
|
touch app/static/vendor/js/products.js
|
||||||
|
|
||||||
# Copy templates
|
# 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
|
cp template.js app/static/vendor/js/products.js
|
||||||
|
|
||||||
# Update files with your page name
|
# Update files with your page name
|
||||||
# Register route in pages.py
|
# Register route in vendor_pages.py
|
||||||
# Add sidebar link
|
# Add sidebar link
|
||||||
# Test!
|
# Test!
|
||||||
```
|
```
|
||||||
@@ -826,11 +951,45 @@ cp template.js app/static/vendor/js/products.js
|
|||||||
|
|
||||||
## 📚 Additional Resources
|
## 📚 Additional Resources
|
||||||
|
|
||||||
- **Icons**: Use `$icon('icon-name', 'classes')` helper
|
### Helpers and Utilities
|
||||||
- **API Client**: Automatically handles auth tokens
|
|
||||||
- **Logging**: Use logInfo, logError, logDebug
|
- **Icons**: Use `$icon('icon-name', 'classes')` helper from `shared/js/icons.js`
|
||||||
- **Date Formatting**: Use formatDate() helper
|
- **API Client**: Automatically handles auth tokens, prepends `/api/v1` to paths
|
||||||
- **Currency**: Use formatCurrency() helper
|
- **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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user