docs: add implementation plan for Letzshop jobs and tables improvements
Implementation plan covering: 1. Job details modal with proper display 2. Tab visibility clarification 3. Add vendor column to jobs table 4. Harmonize all tables with shared macro 5. Platform-wide rows per page setting 6. Build admin customer page 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
709
docs/implementation/letzshop-jobs-improvements.md
Normal file
709
docs/implementation/letzshop-jobs-improvements.md
Normal file
@@ -0,0 +1,709 @@
|
|||||||
|
# Letzshop Jobs & Tables Improvements
|
||||||
|
|
||||||
|
Implementation plan for improving the Letzshop management page jobs display and table harmonization.
|
||||||
|
|
||||||
|
## Status: Planned
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This plan addresses 6 improvements:
|
||||||
|
|
||||||
|
1. Job details modal with proper display
|
||||||
|
2. Tab visibility fix when filters cleared
|
||||||
|
3. Add vendor column to jobs table
|
||||||
|
4. Harmonize all tables with table macro
|
||||||
|
5. Platform-wide rows per page setting
|
||||||
|
6. Build admin customer page
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Job Details Modal
|
||||||
|
|
||||||
|
### Current Issue
|
||||||
|
- "View Details" shows a browser alert instead of a proper modal
|
||||||
|
- No detailed breakdown of export results
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- Create a proper modal for job details
|
||||||
|
- For exports: show products exported per language file
|
||||||
|
- Show vendor name/code
|
||||||
|
- Show full timestamps and duration
|
||||||
|
- Show error details if any
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
#### 1.1 Create Job Details Modal Template
|
||||||
|
|
||||||
|
**File:** `app/templates/admin/partials/letzshop-jobs-table.html`
|
||||||
|
|
||||||
|
Add modal after the table:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Job Details Modal -->
|
||||||
|
<div
|
||||||
|
x-show="showJobDetailsModal"
|
||||||
|
x-transition
|
||||||
|
class="fixed inset-0 z-30 flex items-center justify-center bg-black bg-opacity-50"
|
||||||
|
@click.self="showJobDetailsModal = false"
|
||||||
|
x-cloak
|
||||||
|
>
|
||||||
|
<div class="w-full max-w-lg bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6">
|
||||||
|
<header class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-lg font-semibold">Job Details</h3>
|
||||||
|
<button @click="showJobDetailsModal = false">×</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Job Info -->
|
||||||
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div><span class="font-medium">Job ID:</span> #<span x-text="selectedJobDetails?.id"></span></div>
|
||||||
|
<div><span class="font-medium">Type:</span> <span x-text="selectedJobDetails?.type"></span></div>
|
||||||
|
<div><span class="font-medium">Status:</span> <span x-text="selectedJobDetails?.status"></span></div>
|
||||||
|
<div><span class="font-medium">Vendor:</span> <span x-text="selectedJobDetails?.vendor_name || selectedVendor?.name"></span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Timestamps -->
|
||||||
|
<div class="text-sm">
|
||||||
|
<p><span class="font-medium">Started:</span> <span x-text="formatDate(selectedJobDetails?.started_at)"></span></p>
|
||||||
|
<p><span class="font-medium">Completed:</span> <span x-text="formatDate(selectedJobDetails?.completed_at)"></span></p>
|
||||||
|
<p><span class="font-medium">Duration:</span> <span x-text="formatDuration(selectedJobDetails?.started_at, selectedJobDetails?.completed_at)"></span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Export Details (for export jobs) -->
|
||||||
|
<template x-if="selectedJobDetails?.type === 'export' && selectedJobDetails?.error_details?.products_exported">
|
||||||
|
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3">
|
||||||
|
<h4 class="font-medium mb-2">Export Details</h4>
|
||||||
|
<p class="text-sm">Products exported: <span x-text="selectedJobDetails.error_details.products_exported"></span></p>
|
||||||
|
<template x-if="selectedJobDetails.error_details.files">
|
||||||
|
<div class="mt-2 space-y-1">
|
||||||
|
<template x-for="file in selectedJobDetails.error_details.files" :key="file.language">
|
||||||
|
<div class="text-xs flex justify-between">
|
||||||
|
<span x-text="file.language.toUpperCase()"></span>
|
||||||
|
<span x-text="file.error ? 'Failed: ' + file.error : file.filename + ' (' + (file.size_bytes / 1024).toFixed(1) + ' KB)'"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Error Details -->
|
||||||
|
<template x-if="selectedJobDetails?.error_message || selectedJobDetails?.error_details?.error">
|
||||||
|
<div class="bg-red-50 dark:bg-red-900/20 rounded-lg p-3">
|
||||||
|
<h4 class="font-medium text-red-700 mb-2">Error</h4>
|
||||||
|
<p class="text-sm text-red-600" x-text="selectedJobDetails?.error_message || selectedJobDetails?.error_details?.error"></p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.2 Update JavaScript State
|
||||||
|
|
||||||
|
**File:** `static/admin/js/marketplace-letzshop.js`
|
||||||
|
|
||||||
|
Add state variables:
|
||||||
|
```javascript
|
||||||
|
showJobDetailsModal: false,
|
||||||
|
selectedJobDetails: null,
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `viewJobDetails` method:
|
||||||
|
```javascript
|
||||||
|
viewJobDetails(job) {
|
||||||
|
this.selectedJobDetails = job;
|
||||||
|
this.showJobDetailsModal = true;
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.3 Update API to Return Full Details
|
||||||
|
|
||||||
|
**File:** `app/services/letzshop/order_service.py`
|
||||||
|
|
||||||
|
Update `list_letzshop_jobs` to include `error_details` in the response for export jobs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Tab Visibility Fix
|
||||||
|
|
||||||
|
### Current Issue
|
||||||
|
- When vendor filter is cleared, only 2 tabs appear (Orders, Exceptions)
|
||||||
|
- Should show all tabs: Products, Orders, Exceptions, Jobs, Settings
|
||||||
|
|
||||||
|
### Root Cause
|
||||||
|
- Products, Jobs, and Settings tabs are wrapped in `<template x-if="selectedVendor">`
|
||||||
|
- This is intentional for vendor-specific features
|
||||||
|
|
||||||
|
### Decision Required
|
||||||
|
**Option A:** Keep current behavior (vendor-specific tabs hidden when no vendor)
|
||||||
|
- Products, Jobs, Settings require a vendor context
|
||||||
|
- Cross-vendor view only shows Orders and Exceptions
|
||||||
|
|
||||||
|
**Option B:** Show all tabs but with "Select vendor" message
|
||||||
|
- All tabs visible
|
||||||
|
- Content shows prompt to select vendor
|
||||||
|
|
||||||
|
### Recommended: Option A (Current Behavior)
|
||||||
|
The current behavior is correct because:
|
||||||
|
- Products tab shows vendor's Letzshop products (needs vendor)
|
||||||
|
- Jobs tab shows vendor's jobs (needs vendor)
|
||||||
|
- Settings tab configures vendor's Letzshop (needs vendor)
|
||||||
|
- Orders and Exceptions can work cross-vendor
|
||||||
|
|
||||||
|
**No change needed** - document this as intentional behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Add Vendor Column to Jobs Table
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- Add vendor name/code column to jobs table
|
||||||
|
- Useful when viewing cross-vendor (future feature)
|
||||||
|
- Prepare for reusable jobs component
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
#### 3.1 Update API Response
|
||||||
|
|
||||||
|
**File:** `app/services/letzshop/order_service.py`
|
||||||
|
|
||||||
|
Add vendor info to job dicts:
|
||||||
|
```python
|
||||||
|
# In list_letzshop_jobs, add to each job dict:
|
||||||
|
"vendor_id": vendor_id,
|
||||||
|
"vendor_name": vendor.name if vendor else None,
|
||||||
|
"vendor_code": vendor.vendor_code if vendor else None,
|
||||||
|
```
|
||||||
|
|
||||||
|
Need to fetch vendor once at start of function.
|
||||||
|
|
||||||
|
#### 3.2 Update Table Template
|
||||||
|
|
||||||
|
**File:** `app/templates/admin/partials/letzshop-jobs-table.html`
|
||||||
|
|
||||||
|
Add column header:
|
||||||
|
```html
|
||||||
|
<th class="px-4 py-3">Vendor</th>
|
||||||
|
```
|
||||||
|
|
||||||
|
Add column data:
|
||||||
|
```html
|
||||||
|
<td class="px-4 py-3 text-sm">
|
||||||
|
<span x-text="job.vendor_code || job.vendor_name || '-'"></span>
|
||||||
|
</td>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3 Update Schema
|
||||||
|
|
||||||
|
**File:** `models/schema/letzshop.py`
|
||||||
|
|
||||||
|
Update `LetzshopJobItem` to include vendor fields:
|
||||||
|
```python
|
||||||
|
vendor_id: int | None = None
|
||||||
|
vendor_name: str | None = None
|
||||||
|
vendor_code: str | None = None
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Harmonize Tables with Table Macro
|
||||||
|
|
||||||
|
### Current State
|
||||||
|
- Different tables use different pagination styles
|
||||||
|
- Some use simple prev/next, others use numbered
|
||||||
|
- Inconsistent styling
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- All tables use `table` macro from `shared/macros/tables.html`
|
||||||
|
- Numbered pagination with page numbers
|
||||||
|
- Consistent column styling
|
||||||
|
- Rows per page selector
|
||||||
|
|
||||||
|
### Tables to Update
|
||||||
|
|
||||||
|
| Table | File | Current Pagination |
|
||||||
|
|-------|------|-------------------|
|
||||||
|
| Jobs | letzshop-jobs-table.html | Simple prev/next |
|
||||||
|
| Products | letzshop-products-tab.html | Simple prev/next |
|
||||||
|
| Orders | letzshop-orders-tab.html | Simple prev/next |
|
||||||
|
| Exceptions | letzshop-exceptions-tab.html | Simple prev/next |
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
#### 4.1 Create/Update Table Macro
|
||||||
|
|
||||||
|
**File:** `app/templates/shared/macros/tables.html`
|
||||||
|
|
||||||
|
Ensure numbered pagination macro exists:
|
||||||
|
```html
|
||||||
|
{% macro numbered_pagination(page_var, total_var, limit_var, on_change) %}
|
||||||
|
<div class="flex items-center justify-between mt-4">
|
||||||
|
<div class="text-sm text-gray-600">
|
||||||
|
Showing <span x-text="(({{ page_var }} - 1) * {{ limit_var }}) + 1"></span>
|
||||||
|
to <span x-text="Math.min({{ page_var }} * {{ limit_var }}, {{ total_var }})"></span>
|
||||||
|
of <span x-text="{{ total_var }}"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<!-- First -->
|
||||||
|
<button @click="{{ page_var }} = 1; {{ on_change }}" :disabled="{{ page_var }} <= 1">«</button>
|
||||||
|
<!-- Prev -->
|
||||||
|
<button @click="{{ page_var }}--; {{ on_change }}" :disabled="{{ page_var }} <= 1">‹</button>
|
||||||
|
<!-- Page numbers -->
|
||||||
|
<template x-for="p in getPageNumbers({{ page_var }}, Math.ceil({{ total_var }} / {{ limit_var }}))">
|
||||||
|
<button @click="{{ page_var }} = p; {{ on_change }}" :class="p === {{ page_var }} ? 'bg-purple-600 text-white' : ''">
|
||||||
|
<span x-text="p"></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<!-- Next -->
|
||||||
|
<button @click="{{ page_var }}++; {{ on_change }}" :disabled="{{ page_var }} * {{ limit_var }} >= {{ total_var }}">›</button>
|
||||||
|
<!-- Last -->
|
||||||
|
<button @click="{{ page_var }} = Math.ceil({{ total_var }} / {{ limit_var }}); {{ on_change }}" :disabled="{{ page_var }} * {{ limit_var }} >= {{ total_var }}">»</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2 Add Page Numbers Helper to JavaScript
|
||||||
|
|
||||||
|
**File:** `static/shared/js/helpers.js` or inline
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function getPageNumbers(current, total, maxVisible = 5) {
|
||||||
|
if (total <= maxVisible) {
|
||||||
|
return Array.from({length: total}, (_, i) => i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const half = Math.floor(maxVisible / 2);
|
||||||
|
let start = Math.max(1, current - half);
|
||||||
|
let end = Math.min(total, start + maxVisible - 1);
|
||||||
|
|
||||||
|
if (end - start < maxVisible - 1) {
|
||||||
|
start = Math.max(1, end - maxVisible + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from({length: end - start + 1}, (_, i) => start + i);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.3 Update Each Table
|
||||||
|
|
||||||
|
Update each table to use the macro and consistent styling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Platform-Wide Rows Per Page Setting
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- Global setting for default rows per page
|
||||||
|
- Stored in platform settings (not per-user initially)
|
||||||
|
- Used by all paginated tables
|
||||||
|
- Options: 10, 20, 50, 100
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
#### 5.1 Add Platform Setting
|
||||||
|
|
||||||
|
**File:** `models/database/platform_settings.py` (create if doesn't exist)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class PlatformSettings(Base):
|
||||||
|
__tablename__ = "platform_settings"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
key = Column(String(100), unique=True, nullable=False)
|
||||||
|
value = Column(String(500), nullable=False)
|
||||||
|
updated_at = Column(DateTime, default=datetime.utcnow)
|
||||||
|
```
|
||||||
|
|
||||||
|
Or add to existing settings table if one exists.
|
||||||
|
|
||||||
|
#### 5.2 Create Settings Service
|
||||||
|
|
||||||
|
**File:** `app/services/platform_settings_service.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
class PlatformSettingsService:
|
||||||
|
def get_setting(self, db: Session, key: str, default: Any = None) -> Any:
|
||||||
|
setting = db.query(PlatformSettings).filter_by(key=key).first()
|
||||||
|
return setting.value if setting else default
|
||||||
|
|
||||||
|
def set_setting(self, db: Session, key: str, value: Any) -> None:
|
||||||
|
setting = db.query(PlatformSettings).filter_by(key=key).first()
|
||||||
|
if setting:
|
||||||
|
setting.value = str(value)
|
||||||
|
else:
|
||||||
|
setting = PlatformSettings(key=key, value=str(value))
|
||||||
|
db.add(setting)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
def get_rows_per_page(self, db: Session) -> int:
|
||||||
|
return int(self.get_setting(db, "rows_per_page", "20"))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.3 Expose via API
|
||||||
|
|
||||||
|
**File:** `app/api/v1/admin/settings.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
@router.get("/platform/rows-per-page")
|
||||||
|
def get_rows_per_page(db: Session = Depends(get_db)):
|
||||||
|
return {"rows_per_page": platform_settings_service.get_rows_per_page(db)}
|
||||||
|
|
||||||
|
@router.put("/platform/rows-per-page")
|
||||||
|
def set_rows_per_page(
|
||||||
|
rows: int = Query(..., ge=10, le=100),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_admin: User = Depends(get_current_admin_api),
|
||||||
|
):
|
||||||
|
platform_settings_service.set_setting(db, "rows_per_page", rows)
|
||||||
|
db.commit()
|
||||||
|
return {"rows_per_page": rows}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.4 Load Setting in Frontend
|
||||||
|
|
||||||
|
**File:** `static/shared/js/app.js` or similar
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Load platform settings on app init
|
||||||
|
async function loadPlatformSettings() {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get('/admin/settings/platform/rows-per-page');
|
||||||
|
window.platformSettings = {
|
||||||
|
rowsPerPage: response.rows_per_page || 20
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
window.platformSettings = { rowsPerPage: 20 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.5 Use in Alpine Components
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In each paginated component's init:
|
||||||
|
this.limit = window.platformSettings?.rowsPerPage || 20;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
1. **Phase 1: Job Details Modal** (Quick win)
|
||||||
|
- Add modal template
|
||||||
|
- Update JS state and methods
|
||||||
|
- Test with export jobs
|
||||||
|
|
||||||
|
2. **Phase 2: Vendor Column** (Preparation)
|
||||||
|
- Update API response
|
||||||
|
- Update schema
|
||||||
|
- Add column to table
|
||||||
|
|
||||||
|
3. **Phase 3: Platform Settings** (Foundation)
|
||||||
|
- Create settings model/migration
|
||||||
|
- Create service
|
||||||
|
- Create API endpoint
|
||||||
|
- Frontend integration
|
||||||
|
|
||||||
|
4. **Phase 4: Table Harmonization** (Largest effort)
|
||||||
|
- Create/update table macros
|
||||||
|
- Add pagination helper function
|
||||||
|
- Update each table one by one
|
||||||
|
- Test thoroughly
|
||||||
|
|
||||||
|
5. **Phase 5: Documentation**
|
||||||
|
- Update component documentation
|
||||||
|
- Add settings documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files to Create/Modify
|
||||||
|
|
||||||
|
### New Files
|
||||||
|
- `models/database/platform_settings.py` (if not exists)
|
||||||
|
- `app/services/platform_settings_service.py`
|
||||||
|
- `alembic/versions/xxx_add_platform_settings.py`
|
||||||
|
|
||||||
|
### Modified Files
|
||||||
|
- `app/templates/admin/partials/letzshop-jobs-table.html`
|
||||||
|
- `app/templates/admin/partials/letzshop-products-tab.html`
|
||||||
|
- `app/templates/admin/partials/letzshop-orders-tab.html`
|
||||||
|
- `app/templates/admin/partials/letzshop-exceptions-tab.html`
|
||||||
|
- `app/templates/shared/macros/tables.html`
|
||||||
|
- `static/admin/js/marketplace-letzshop.js`
|
||||||
|
- `static/shared/js/helpers.js` or `app.js`
|
||||||
|
- `app/services/letzshop/order_service.py`
|
||||||
|
- `models/schema/letzshop.py`
|
||||||
|
- `app/api/v1/admin/settings.py` or new file
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Admin Customer Page
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- New page at `/admin/customers` to manage customers
|
||||||
|
- List all customers across vendors
|
||||||
|
- Search and filter capabilities
|
||||||
|
- View customer details and order history
|
||||||
|
- Link to vendor context
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
#### 6.1 Database Model Check
|
||||||
|
|
||||||
|
**File:** `models/database/customer.py`
|
||||||
|
|
||||||
|
Verify Customer model exists with fields:
|
||||||
|
- id, vendor_id
|
||||||
|
- email, name, phone
|
||||||
|
- shipping address fields
|
||||||
|
- created_at, updated_at
|
||||||
|
|
||||||
|
#### 6.2 Create Customer Service
|
||||||
|
|
||||||
|
**File:** `app/services/customer_service.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CustomerService:
|
||||||
|
def get_customers(
|
||||||
|
self,
|
||||||
|
db: Session,
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 20,
|
||||||
|
search: str | None = None,
|
||||||
|
vendor_id: int | None = None,
|
||||||
|
) -> tuple[list[dict], int]:
|
||||||
|
"""Get paginated customer list with optional filters."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_customer_detail(self, db: Session, customer_id: int) -> dict:
|
||||||
|
"""Get customer with order history."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_customer_stats(self, db: Session, vendor_id: int | None = None) -> dict:
|
||||||
|
"""Get customer statistics."""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.3 Create API Endpoints
|
||||||
|
|
||||||
|
**File:** `app/api/v1/admin/customers.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
router = APIRouter(prefix="/customers")
|
||||||
|
|
||||||
|
@router.get("", response_model=CustomerListResponse)
|
||||||
|
def get_customers(
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
search: str | None = Query(None),
|
||||||
|
vendor_id: int | None = Query(None),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_admin: User = Depends(get_current_admin_api),
|
||||||
|
):
|
||||||
|
"""List all customers with filtering."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@router.get("/stats", response_model=CustomerStatsResponse)
|
||||||
|
def get_customer_stats(...):
|
||||||
|
"""Get customer statistics."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@router.get("/{customer_id}", response_model=CustomerDetailResponse)
|
||||||
|
def get_customer_detail(...):
|
||||||
|
"""Get customer with order history."""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.4 Create Pydantic Schemas
|
||||||
|
|
||||||
|
**File:** `models/schema/customer.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CustomerListItem(BaseModel):
|
||||||
|
id: int
|
||||||
|
email: str
|
||||||
|
name: str | None
|
||||||
|
phone: str | None
|
||||||
|
vendor_id: int
|
||||||
|
vendor_name: str | None
|
||||||
|
order_count: int
|
||||||
|
total_spent: float
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class CustomerListResponse(BaseModel):
|
||||||
|
customers: list[CustomerListItem]
|
||||||
|
total: int
|
||||||
|
skip: int
|
||||||
|
limit: int
|
||||||
|
|
||||||
|
class CustomerDetailResponse(CustomerListItem):
|
||||||
|
shipping_address: str | None
|
||||||
|
orders: list[OrderSummary]
|
||||||
|
|
||||||
|
class CustomerStatsResponse(BaseModel):
|
||||||
|
total: int
|
||||||
|
new_this_month: int
|
||||||
|
active: int # ordered in last 90 days
|
||||||
|
by_vendor: dict[str, int]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.5 Create Admin Page Route
|
||||||
|
|
||||||
|
**File:** `app/routes/admin_pages.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
@router.get("/customers", response_class=HTMLResponse)
|
||||||
|
async def admin_customers_page(request: Request, ...):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"admin/customers.html",
|
||||||
|
{"request": request, "current_page": "customers"}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.6 Create Template
|
||||||
|
|
||||||
|
**File:** `app/templates/admin/customers.html`
|
||||||
|
|
||||||
|
Structure:
|
||||||
|
- Page header with title and stats
|
||||||
|
- Search bar and filters (vendor dropdown)
|
||||||
|
- Customer table with pagination
|
||||||
|
- Click row to view details modal
|
||||||
|
|
||||||
|
#### 6.7 Create Alpine Component
|
||||||
|
|
||||||
|
**File:** `static/admin/js/customers.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function adminCustomers() {
|
||||||
|
return {
|
||||||
|
customers: [],
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
search: '',
|
||||||
|
vendorFilter: '',
|
||||||
|
loading: false,
|
||||||
|
stats: {},
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await Promise.all([
|
||||||
|
this.loadCustomers(),
|
||||||
|
this.loadStats()
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadCustomers() { ... },
|
||||||
|
async loadStats() { ... },
|
||||||
|
async viewCustomer(id) { ... },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.8 Add to Sidebar
|
||||||
|
|
||||||
|
**File:** `app/templates/admin/partials/sidebar.html`
|
||||||
|
|
||||||
|
Add menu item:
|
||||||
|
```html
|
||||||
|
{{ menu_item('customers', '/admin/customers', 'users', 'Customers') }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customer Page Features
|
||||||
|
|
||||||
|
| Feature | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| List View | Paginated table of all customers |
|
||||||
|
| Search | Search by name, email, phone |
|
||||||
|
| Vendor Filter | Filter by vendor |
|
||||||
|
| Stats Cards | Total, new, active customers |
|
||||||
|
| Detail Modal | Customer info + order history |
|
||||||
|
| Quick Actions | View orders, send email |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
1. **Phase 1: Job Details Modal** (Quick win)
|
||||||
|
- Add modal template
|
||||||
|
- Update JS state and methods
|
||||||
|
- Test with export jobs
|
||||||
|
|
||||||
|
2. **Phase 2: Vendor Column** (Preparation)
|
||||||
|
- Update API response
|
||||||
|
- Update schema
|
||||||
|
- Add column to table
|
||||||
|
|
||||||
|
3. **Phase 3: Platform Settings** (Foundation)
|
||||||
|
- Create settings model/migration
|
||||||
|
- Create service
|
||||||
|
- Create API endpoint
|
||||||
|
- Frontend integration
|
||||||
|
|
||||||
|
4. **Phase 4: Table Harmonization** (Largest effort)
|
||||||
|
- Create/update table macros
|
||||||
|
- Add pagination helper function
|
||||||
|
- Update each table one by one
|
||||||
|
- Test thoroughly
|
||||||
|
|
||||||
|
5. **Phase 5: Admin Customer Page**
|
||||||
|
- Create service and API
|
||||||
|
- Create schemas
|
||||||
|
- Create template and JS
|
||||||
|
- Add to sidebar
|
||||||
|
|
||||||
|
6. **Phase 6: Documentation**
|
||||||
|
- Update component documentation
|
||||||
|
- Add settings documentation
|
||||||
|
- Add customer page documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files to Create/Modify
|
||||||
|
|
||||||
|
### New Files
|
||||||
|
- `models/database/platform_settings.py` (if not exists)
|
||||||
|
- `app/services/platform_settings_service.py`
|
||||||
|
- `app/services/customer_service.py`
|
||||||
|
- `app/api/v1/admin/customers.py`
|
||||||
|
- `models/schema/customer.py`
|
||||||
|
- `app/templates/admin/customers.html`
|
||||||
|
- `static/admin/js/customers.js`
|
||||||
|
- `alembic/versions/xxx_add_platform_settings.py`
|
||||||
|
|
||||||
|
### Modified Files
|
||||||
|
- `app/templates/admin/partials/letzshop-jobs-table.html`
|
||||||
|
- `app/templates/admin/partials/letzshop-products-tab.html`
|
||||||
|
- `app/templates/admin/partials/letzshop-orders-tab.html`
|
||||||
|
- `app/templates/admin/partials/letzshop-exceptions-tab.html`
|
||||||
|
- `app/templates/admin/partials/sidebar.html`
|
||||||
|
- `app/templates/shared/macros/tables.html`
|
||||||
|
- `static/admin/js/marketplace-letzshop.js`
|
||||||
|
- `static/shared/js/helpers.js` or `app.js`
|
||||||
|
- `app/services/letzshop/order_service.py`
|
||||||
|
- `models/schema/letzshop.py`
|
||||||
|
- `app/api/v1/admin/__init__.py`
|
||||||
|
- `app/routes/admin_pages.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estimated Effort
|
||||||
|
|
||||||
|
| Task | Effort |
|
||||||
|
|------|--------|
|
||||||
|
| Job Details Modal | Small |
|
||||||
|
| Tab Visibility (no change) | None |
|
||||||
|
| Vendor Column | Small |
|
||||||
|
| Platform Settings | Medium |
|
||||||
|
| Table Harmonization | Large |
|
||||||
|
| Admin Customer Page | Medium |
|
||||||
|
|
||||||
|
**Total:** Large effort
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Plan created: 2024-12-20*
|
||||||
@@ -137,6 +137,7 @@ nav:
|
|||||||
- Implementation Plans:
|
- Implementation Plans:
|
||||||
- Admin Inventory Management: implementation/inventory-admin-migration.md
|
- Admin Inventory Management: implementation/inventory-admin-migration.md
|
||||||
- Letzshop Order Import: implementation/letzshop-order-import-improvements.md
|
- Letzshop Order Import: implementation/letzshop-order-import-improvements.md
|
||||||
|
- Letzshop Jobs & Tables: implementation/letzshop-jobs-improvements.md
|
||||||
- Order Item Exceptions: implementation/order-item-exceptions.md
|
- Order Item Exceptions: implementation/order-item-exceptions.md
|
||||||
- Product Suppliers Table: implementation/product-suppliers-table.md
|
- Product Suppliers Table: implementation/product-suppliers-table.md
|
||||||
- Unified Order View: implementation/unified-order-view.md
|
- Unified Order View: implementation/unified-order-view.md
|
||||||
|
|||||||
Reference in New Issue
Block a user