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" %}
|
||||
|
||||
{% 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/
|
||||
│ ├── 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
|
||||
|
||||
279
docs/frontend/vendor/page-templates.md
vendored
279
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
|
||||
```
|
||||
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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user