compiling project documentation
This commit is contained in:
916
docs/__temp/__PROJECT_ROADMAP/19_migration_plan_FINAL.md
Normal file
916
docs/__temp/__PROJECT_ROADMAP/19_migration_plan_FINAL.md
Normal file
@@ -0,0 +1,916 @@
|
||||
# Vendor & Users Pages Migration Plan - FINAL
|
||||
## Based on Actual Legacy Files
|
||||
|
||||
**Date:** October 23, 2025
|
||||
**Status:** Icons fixed ✅ | Logout working ✅ | Dashboard migrated ✅
|
||||
|
||||
---
|
||||
|
||||
## 📁 Legacy Files Analysis
|
||||
|
||||
### Existing Files:
|
||||
1. **vendors.html** - Basic placeholder (needs vendor LIST implementation)
|
||||
2. **vendor-edit.html** - Detailed edit form (OLD CSS, needs redesign)
|
||||
3. **vendors.js** - Only has `vendorCreation()` function (NOT vendor list)
|
||||
4. **vendor-edit.js** - Complete edit functionality (uses OLD auth/api pattern)
|
||||
5. **init-alpine.js** - Base Alpine data (theme, menus)
|
||||
|
||||
### Key Findings:
|
||||
- ✅ `vendors.js` currently only has CREATE vendor function
|
||||
- ❌ NO vendor LIST function exists yet
|
||||
- ✅ `vendor-edit.js` exists but uses OLD patterns:
|
||||
- Uses `apiClient` (should use `ApiClient`)
|
||||
- Uses `Auth.isAuthenticated()` pattern
|
||||
- Uses `Utils.confirm()` and custom modals
|
||||
- ✅ Dashboard pattern: Uses `ApiClient`, `Logger`, `Utils.showToast`
|
||||
|
||||
### Pattern Differences:
|
||||
|
||||
**OLD Pattern (vendor-edit.js):**
|
||||
```javascript
|
||||
// OLD API client (lowercase)
|
||||
const response = await apiClient.get('/admin/vendors/1');
|
||||
|
||||
// OLD Auth pattern
|
||||
if (!Auth.isAuthenticated()) { ... }
|
||||
const user = Auth.getCurrentUser();
|
||||
```
|
||||
|
||||
**NEW Pattern (dashboard.js):**
|
||||
```javascript
|
||||
// NEW API client (uppercase)
|
||||
const response = await ApiClient.get('/admin/vendors');
|
||||
|
||||
// NEW Auth - handled by cookie, no client-side check needed
|
||||
// Just call API and let middleware handle it
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Migration Strategy
|
||||
|
||||
### Task 1: Create Vendor List Function (HIGH PRIORITY) 🏪
|
||||
|
||||
**Current State:** vendors.js only has `vendorCreation()` function
|
||||
**Goal:** Add `adminVendors()` function for the vendor LIST page
|
||||
|
||||
#### 1.1 Update vendors.js - Add Vendor List Function
|
||||
|
||||
**File:** `static/admin/js/vendors.js`
|
||||
|
||||
**Action:** ADD new function (keep existing `vendorCreation()` function):
|
||||
|
||||
```javascript
|
||||
// static/admin/js/vendors.js
|
||||
|
||||
// ============================================
|
||||
// VENDOR LIST FUNCTION (NEW - Add this)
|
||||
// ============================================
|
||||
function adminVendors() {
|
||||
return {
|
||||
// State
|
||||
vendors: [],
|
||||
stats: {
|
||||
total: 0,
|
||||
verified: 0,
|
||||
pending: 0,
|
||||
inactive: 0
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
// Initialize
|
||||
async init() {
|
||||
Logger.info('Vendors page initialized', 'VENDORS');
|
||||
await this.loadVendors();
|
||||
await this.loadStats();
|
||||
},
|
||||
|
||||
// Load vendors list
|
||||
async loadVendors() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
try {
|
||||
const response = await ApiClient.get('/admin/vendors');
|
||||
// Handle different response structures
|
||||
this.vendors = response.vendors || response.items || response || [];
|
||||
Logger.info('Vendors loaded', 'VENDORS', { count: this.vendors.length });
|
||||
} catch (error) {
|
||||
Logger.error('Failed to load vendors', 'VENDORS', error);
|
||||
this.error = error.message || 'Failed to load vendors';
|
||||
Utils.showToast('Failed to load vendors', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Load statistics
|
||||
async loadStats() {
|
||||
try {
|
||||
const response = await ApiClient.get('/admin/vendors/stats');
|
||||
this.stats = response;
|
||||
Logger.info('Stats loaded', 'VENDORS', this.stats);
|
||||
} catch (error) {
|
||||
Logger.error('Failed to load stats', 'VENDORS', error);
|
||||
// Don't show error toast for stats, just log it
|
||||
}
|
||||
},
|
||||
|
||||
// Format date (matches dashboard pattern)
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
},
|
||||
|
||||
// View vendor details
|
||||
viewVendor(vendorCode) {
|
||||
Logger.info('View vendor', 'VENDORS', { vendorCode });
|
||||
// Navigate to details page or open modal
|
||||
window.location.href = `/admin/vendors/${vendorCode}`;
|
||||
},
|
||||
|
||||
// Edit vendor
|
||||
editVendor(vendorCode) {
|
||||
Logger.info('Edit vendor', 'VENDORS', { vendorCode });
|
||||
window.location.href = `/admin/vendors/${vendorCode}/edit`;
|
||||
},
|
||||
|
||||
// Delete vendor
|
||||
async deleteVendor(vendor) {
|
||||
if (!confirm(`Are you sure you want to delete vendor "${vendor.name}"?\n\nThis action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ApiClient.delete(`/admin/vendors/${vendor.vendor_code}`);
|
||||
Utils.showToast('Vendor deleted successfully', 'success');
|
||||
await this.loadVendors();
|
||||
await this.loadStats();
|
||||
} catch (error) {
|
||||
Logger.error('Failed to delete vendor', 'VENDORS', error);
|
||||
Utils.showToast(error.message || 'Failed to delete vendor', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
// Open create modal/page
|
||||
openCreateModal() {
|
||||
Logger.info('Open create vendor', 'VENDORS');
|
||||
// Navigate to create page (or open modal)
|
||||
window.location.href = '/admin/vendors/create';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// VENDOR CREATION FUNCTION (EXISTING - Keep this)
|
||||
// ============================================
|
||||
function vendorCreation() {
|
||||
// ... keep your existing vendorCreation function as is ...
|
||||
// (the code you already have)
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 Create Vendors List Template
|
||||
|
||||
**File:** `app/templates/admin/vendors.html`
|
||||
|
||||
```jinja2
|
||||
{# app/templates/admin/vendors.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Vendors{% endblock %}
|
||||
|
||||
{% block alpine_data %}adminVendors(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Header -->
|
||||
<div class="flex items-center justify-between my-6">
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Vendor Management
|
||||
</h2>
|
||||
<a
|
||||
href="/admin/vendors/create"
|
||||
class="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 focus:outline-none focus:shadow-outline-purple"
|
||||
>
|
||||
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
|
||||
Create Vendor
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="text-center py-12">
|
||||
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading vendors...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div x-show="error && !loading" class="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg flex items-start">
|
||||
<span x-html="$icon('exclamation', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
|
||||
<div>
|
||||
<p class="font-semibold">Error loading vendors</p>
|
||||
<p class="text-sm" x-text="error"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards - EXACTLY like dashboard -->
|
||||
<div x-show="!loading" class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
||||
<!-- Card: Total Vendors -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full dark:text-orange-100 dark:bg-orange-500">
|
||||
<span x-html="$icon('user-group', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Total Vendors
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.total || 0">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Verified Vendors -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
|
||||
<span x-html="$icon('badge-check', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Verified Vendors
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.verified || 0">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Pending Verification -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-yellow-500 bg-yellow-100 rounded-full dark:text-yellow-100 dark:bg-yellow-500">
|
||||
<span x-html="$icon('clock', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Pending
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.pending || 0">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Inactive Vendors -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-red-500 bg-red-100 rounded-full dark:text-red-100 dark:bg-red-500">
|
||||
<span x-html="$icon('x-circle', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Inactive
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.inactive || 0">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vendors Table - EXACTLY like dashboard table -->
|
||||
<div x-show="!loading" class="w-full overflow-hidden rounded-lg shadow-xs">
|
||||
<div class="w-full overflow-x-auto">
|
||||
<table class="w-full whitespace-no-wrap">
|
||||
<thead>
|
||||
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
|
||||
<th class="px-4 py-3">Vendor</th>
|
||||
<th class="px-4 py-3">Subdomain</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
<th class="px-4 py-3">Created</th>
|
||||
<th class="px-4 py-3">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<!-- Empty State -->
|
||||
<template x-if="vendors.length === 0">
|
||||
<tr>
|
||||
<td colspan="5" class="px-4 py-8 text-center text-gray-600 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('user-group', 'w-12 h-12 mb-2 text-gray-300')"></span>
|
||||
<p class="font-medium">No vendors found</p>
|
||||
<p class="text-xs mt-1">Create your first vendor to get started</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Vendor Rows -->
|
||||
<template x-for="vendor in vendors" :key="vendor.id || vendor.vendor_code">
|
||||
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<!-- Vendor Info with Avatar -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center text-sm">
|
||||
<div class="relative hidden w-8 h-8 mr-3 rounded-full md:block">
|
||||
<div class="absolute inset-0 rounded-full bg-purple-100 dark:bg-purple-600 flex items-center justify-center">
|
||||
<span class="text-xs font-semibold text-purple-600 dark:text-purple-100"
|
||||
x-text="vendor.name?.charAt(0).toUpperCase() || '?'"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold" x-text="vendor.name"></p>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400" x-text="vendor.vendor_code"></p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Subdomain -->
|
||||
<td class="px-4 py-3 text-sm" x-text="vendor.subdomain"></td>
|
||||
|
||||
<!-- Status Badge -->
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span class="inline-flex items-center px-2 py-1 font-semibold leading-tight rounded-full"
|
||||
:class="vendor.is_verified ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-orange-700 bg-orange-100 dark:text-white dark:bg-orange-600'">
|
||||
<span x-show="vendor.is_verified" x-html="$icon('badge-check', 'w-3 h-3 mr-1')"></span>
|
||||
<span x-text="vendor.is_verified ? 'Verified' : 'Pending'"></span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Created Date -->
|
||||
<td class="px-4 py-3 text-sm" x-text="formatDate(vendor.created_at)"></td>
|
||||
|
||||
<!-- Actions -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<!-- View Button -->
|
||||
<button
|
||||
@click="viewVendor(vendor.vendor_code)"
|
||||
class="flex items-center justify-center p-2 text-blue-600 rounded-lg hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
||||
title="View details"
|
||||
>
|
||||
<span x-html="$icon('eye', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
|
||||
<!-- Edit Button -->
|
||||
<button
|
||||
@click="editVendor(vendor.vendor_code)"
|
||||
class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
||||
title="Edit vendor"
|
||||
>
|
||||
<span x-html="$icon('pencil', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
|
||||
<!-- Delete Button -->
|
||||
<button
|
||||
@click="deleteVendor(vendor)"
|
||||
class="flex items-center justify-center p-2 text-red-600 rounded-lg hover:bg-red-50 dark:text-red-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
||||
title="Delete vendor"
|
||||
>
|
||||
<span x-html="$icon('trash', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script src="{{ url_for('static', path='admin/js/vendors.js') }}"></script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
**Checklist Task 1:**
|
||||
- [ ] Add `adminVendors()` function to `static/admin/js/vendors.js` (keep existing `vendorCreation()`)
|
||||
- [ ] Create `app/templates/admin/vendors.html` template
|
||||
- [ ] Add backend route for `/admin/vendors`
|
||||
- [ ] Test vendor list page loads
|
||||
- [ ] Test stats cards display
|
||||
- [ ] Test vendor table displays data
|
||||
- [ ] Test action buttons work
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Migrate Vendor Edit Page (HIGH PRIORITY) ✏️
|
||||
|
||||
**Current State:** vendor-edit.js exists but uses OLD patterns
|
||||
**Goal:** Update to use NEW patterns (ApiClient, Logger, no Auth checks)
|
||||
|
||||
#### 2.1 Update vendor-edit.js to NEW Pattern
|
||||
|
||||
**File:** `static/admin/js/vendor-edit.js`
|
||||
|
||||
**Changes Needed:**
|
||||
1. Replace `apiClient` → `ApiClient`
|
||||
2. Remove `Auth.isAuthenticated()` checks (handled by backend)
|
||||
3. Replace `Utils.confirm()` → Use `confirm()`
|
||||
4. Remove custom modals, use simple confirms
|
||||
5. Add Logger calls
|
||||
6. Update route patterns
|
||||
|
||||
```javascript
|
||||
// static/admin/js/vendor-edit.js
|
||||
|
||||
function adminVendorEdit() {
|
||||
return {
|
||||
// State
|
||||
vendor: null,
|
||||
formData: {},
|
||||
errors: {},
|
||||
loadingVendor: false,
|
||||
saving: false,
|
||||
vendorCode: null,
|
||||
|
||||
// Initialize
|
||||
async init() {
|
||||
Logger.info('Vendor edit page initialized', 'VENDOR_EDIT');
|
||||
|
||||
// Get vendor code from URL
|
||||
const path = window.location.pathname;
|
||||
const match = path.match(/\/admin\/vendors\/([^\/]+)\/edit/);
|
||||
|
||||
if (match) {
|
||||
this.vendorCode = match[1];
|
||||
Logger.info('Editing vendor', 'VENDOR_EDIT', { vendorCode: this.vendorCode });
|
||||
await this.loadVendor();
|
||||
} else {
|
||||
Logger.error('No vendor code in URL', 'VENDOR_EDIT');
|
||||
Utils.showToast('Invalid vendor URL', 'error');
|
||||
setTimeout(() => window.location.href = '/admin/vendors', 2000);
|
||||
}
|
||||
},
|
||||
|
||||
// Load vendor data
|
||||
async loadVendor() {
|
||||
this.loadingVendor = true;
|
||||
try {
|
||||
// CHANGED: apiClient → ApiClient
|
||||
const response = await ApiClient.get(`/admin/vendors/${this.vendorCode}`);
|
||||
this.vendor = response;
|
||||
|
||||
// Initialize form data
|
||||
this.formData = {
|
||||
name: response.name || '',
|
||||
subdomain: response.subdomain || '',
|
||||
description: response.description || '',
|
||||
contact_email: response.contact_email || '',
|
||||
contact_phone: response.contact_phone || '',
|
||||
website: response.website || '',
|
||||
business_address: response.business_address || '',
|
||||
tax_number: response.tax_number || ''
|
||||
};
|
||||
|
||||
Logger.info('Vendor loaded', 'VENDOR_EDIT', {
|
||||
vendor_code: this.vendor.vendor_code,
|
||||
name: this.vendor.name
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.error('Failed to load vendor', 'VENDOR_EDIT', error);
|
||||
Utils.showToast('Failed to load vendor', 'error');
|
||||
setTimeout(() => window.location.href = '/admin/vendors', 2000);
|
||||
} finally {
|
||||
this.loadingVendor = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Format subdomain
|
||||
formatSubdomain() {
|
||||
this.formData.subdomain = this.formData.subdomain
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, '');
|
||||
},
|
||||
|
||||
// Submit form
|
||||
async handleSubmit() {
|
||||
Logger.info('Submitting vendor update', 'VENDOR_EDIT');
|
||||
this.errors = {};
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
// CHANGED: apiClient → ApiClient
|
||||
const response = await ApiClient.put(
|
||||
`/admin/vendors/${this.vendorCode}`,
|
||||
this.formData
|
||||
);
|
||||
|
||||
this.vendor = response;
|
||||
Utils.showToast('Vendor updated successfully', 'success');
|
||||
Logger.info('Vendor updated', 'VENDOR_EDIT', response);
|
||||
|
||||
// Optionally redirect back to list
|
||||
// setTimeout(() => window.location.href = '/admin/vendors', 1500);
|
||||
} catch (error) {
|
||||
Logger.error('Failed to update vendor', 'VENDOR_EDIT', error);
|
||||
|
||||
// Handle validation errors
|
||||
if (error.details && error.details.validation_errors) {
|
||||
error.details.validation_errors.forEach(err => {
|
||||
const field = err.loc?.[1] || err.loc?.[0];
|
||||
if (field) {
|
||||
this.errors[field] = err.msg;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Utils.showToast(error.message || 'Failed to update vendor', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Toggle verification
|
||||
async toggleVerification() {
|
||||
const action = this.vendor.is_verified ? 'unverify' : 'verify';
|
||||
|
||||
// CHANGED: Simple confirm instead of custom modal
|
||||
if (!confirm(`Are you sure you want to ${action} this vendor?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
// CHANGED: apiClient → ApiClient
|
||||
const response = await ApiClient.put(
|
||||
`/admin/vendors/${this.vendorCode}/verification`,
|
||||
{ is_verified: !this.vendor.is_verified }
|
||||
);
|
||||
|
||||
this.vendor = response;
|
||||
Utils.showToast(`Vendor ${action}ed successfully`, 'success');
|
||||
Logger.info(`Vendor ${action}ed`, 'VENDOR_EDIT');
|
||||
} catch (error) {
|
||||
Logger.error(`Failed to ${action} vendor`, 'VENDOR_EDIT', error);
|
||||
Utils.showToast(`Failed to ${action} vendor`, 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Toggle active status
|
||||
async toggleActive() {
|
||||
const action = this.vendor.is_active ? 'deactivate' : 'activate';
|
||||
|
||||
// CHANGED: Simple confirm instead of custom modal
|
||||
if (!confirm(`Are you sure you want to ${action} this vendor?\n\nThis will affect their operations.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
// CHANGED: apiClient → ApiClient
|
||||
const response = await ApiClient.put(
|
||||
`/admin/vendors/${this.vendorCode}/status`,
|
||||
{ is_active: !this.vendor.is_active }
|
||||
);
|
||||
|
||||
this.vendor = response;
|
||||
Utils.showToast(`Vendor ${action}d successfully`, 'success');
|
||||
Logger.info(`Vendor ${action}d`, 'VENDOR_EDIT');
|
||||
} catch (error) {
|
||||
Logger.error(`Failed to ${action} vendor`, 'VENDOR_EDIT', error);
|
||||
Utils.showToast(`Failed to ${action} vendor`, 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 Create Vendor Edit Template
|
||||
|
||||
**File:** `app/templates/admin/vendor-edit.html`
|
||||
|
||||
```jinja2
|
||||
{# app/templates/admin/vendor-edit.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Edit Vendor{% endblock %}
|
||||
|
||||
{% block alpine_data %}adminVendorEdit(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Header -->
|
||||
<div class="flex items-center justify-between my-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Edit Vendor
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1" x-show="vendor">
|
||||
<span x-text="vendor?.name"></span>
|
||||
<span class="text-gray-400">•</span>
|
||||
<span x-text="vendor?.vendor_code"></span>
|
||||
</p>
|
||||
</div>
|
||||
<a href="/admin/vendors"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800 hover:border-gray-400 focus:outline-none">
|
||||
<span x-html="$icon('arrow-left', 'w-4 h-4 mr-2')"></span>
|
||||
Back to Vendors
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div x-show="loadingVendor" class="text-center py-12">
|
||||
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading vendor...</p>
|
||||
</div>
|
||||
|
||||
<!-- Edit Form -->
|
||||
<div x-show="!loadingVendor && vendor" class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<!-- Quick Actions -->
|
||||
<div class="flex items-center gap-3 mb-6 pb-6 border-b dark:border-gray-700">
|
||||
<button
|
||||
@click="toggleVerification()"
|
||||
:disabled="saving"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
|
||||
:class="vendor?.is_verified ? 'bg-orange-600 hover:bg-orange-700' : 'bg-green-600 hover:bg-green-700'">
|
||||
<span x-html="$icon(vendor?.is_verified ? 'x-circle' : 'badge-check', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="vendor?.is_verified ? 'Unverify Vendor' : 'Verify Vendor'"></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="toggleActive()"
|
||||
:disabled="saving"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
|
||||
:class="vendor?.is_active ? 'bg-red-600 hover:bg-red-700' : 'bg-green-600 hover:bg-green-700'">
|
||||
<span x-html="$icon(vendor?.is_active ? 'lock-closed' : 'lock-open', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="vendor?.is_active ? 'Deactivate' : 'Activate'"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="grid gap-6 mb-8 md:grid-cols-2">
|
||||
<!-- Left Column: Basic Info -->
|
||||
<div>
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Basic Information
|
||||
</h3>
|
||||
|
||||
<!-- Vendor Code (readonly) -->
|
||||
<label class="block mb-4">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Vendor Code
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
x-model="vendor.vendor_code"
|
||||
disabled
|
||||
class="block w-full mt-1 text-sm bg-gray-100 border-gray-300 rounded-md dark:bg-gray-700 dark:text-gray-400 dark:border-gray-600 cursor-not-allowed"
|
||||
>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||
Cannot be changed after creation
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<!-- Name -->
|
||||
<label class="block mb-4">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Vendor Name <span class="text-red-600">*</span>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
x-model="formData.name"
|
||||
required
|
||||
maxlength="255"
|
||||
:disabled="saving"
|
||||
class="block w-full mt-1 text-sm border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
|
||||
:class="{ 'border-red-600': errors.name }"
|
||||
>
|
||||
<span x-show="errors.name" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.name"></span>
|
||||
</label>
|
||||
|
||||
<!-- Subdomain -->
|
||||
<label class="block mb-4">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Subdomain <span class="text-red-600">*</span>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
x-model="formData.subdomain"
|
||||
@input="formatSubdomain()"
|
||||
required
|
||||
maxlength="100"
|
||||
:disabled="saving"
|
||||
class="block w-full mt-1 text-sm border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
|
||||
:class="{ 'border-red-600': errors.subdomain }"
|
||||
>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||
Lowercase letters, numbers, and hyphens only
|
||||
</span>
|
||||
<span x-show="errors.subdomain" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.subdomain"></span>
|
||||
</label>
|
||||
|
||||
<!-- Description -->
|
||||
<label class="block mb-4">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Description
|
||||
</span>
|
||||
<textarea
|
||||
x-model="formData.description"
|
||||
rows="3"
|
||||
:disabled="saving"
|
||||
class="block w-full mt-1 text-sm border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea"
|
||||
></textarea>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Contact Info -->
|
||||
<div>
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Contact Information
|
||||
</h3>
|
||||
|
||||
<!-- Owner Email (readonly) -->
|
||||
<label class="block mb-4">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Owner Email
|
||||
</span>
|
||||
<input
|
||||
type="email"
|
||||
x-model="vendor.owner_email"
|
||||
disabled
|
||||
class="block w-full mt-1 text-sm bg-gray-100 border-gray-300 rounded-md dark:bg-gray-700 dark:text-gray-400 dark:border-gray-600 cursor-not-allowed"
|
||||
>
|
||||
</label>
|
||||
|
||||
<!-- Contact Email -->
|
||||
<label class="block mb-4">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Contact Email <span class="text-red-600">*</span>
|
||||
</span>
|
||||
<input
|
||||
type="email"
|
||||
x-model="formData.contact_email"
|
||||
required
|
||||
:disabled="saving"
|
||||
class="block w-full mt-1 text-sm border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
|
||||
:class="{ 'border-red-600': errors.contact_email }"
|
||||
>
|
||||
<span x-show="errors.contact_email" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.contact_email"></span>
|
||||
</label>
|
||||
|
||||
<!-- Phone -->
|
||||
<label class="block mb-4">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Phone
|
||||
</span>
|
||||
<input
|
||||
type="tel"
|
||||
x-model="formData.contact_phone"
|
||||
:disabled="saving"
|
||||
class="block w-full mt-1 text-sm border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
|
||||
>
|
||||
</label>
|
||||
|
||||
<!-- Website -->
|
||||
<label class="block mb-4">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Website
|
||||
</span>
|
||||
<input
|
||||
type="url"
|
||||
x-model="formData.website"
|
||||
:disabled="saving"
|
||||
placeholder="https://example.com"
|
||||
class="block w-full mt-1 text-sm border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Business Details -->
|
||||
<div class="mb-8">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Business Details
|
||||
</h3>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<!-- Business Address -->
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Business Address
|
||||
</span>
|
||||
<textarea
|
||||
x-model="formData.business_address"
|
||||
rows="3"
|
||||
:disabled="saving"
|
||||
class="block w-full mt-1 text-sm border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea"
|
||||
></textarea>
|
||||
</label>
|
||||
|
||||
<!-- Tax Number -->
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Tax Number
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
x-model="formData.tax_number"
|
||||
:disabled="saving"
|
||||
class="block w-full mt-1 text-sm border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<a
|
||||
href="/admin/vendors"
|
||||
class="px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800 hover:border-gray-400 focus:outline-none">
|
||||
Cancel
|
||||
</a>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="saving"
|
||||
class="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 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span x-show="!saving">Save Changes</span>
|
||||
<span x-show="saving" class="flex items-center">
|
||||
<span x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
Saving...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script src="{{ url_for('static', path='admin/js/vendor-edit.js') }}"></script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
**Checklist Task 2:**
|
||||
- [ ] Update `static/admin/js/vendor-edit.js` with NEW patterns
|
||||
- [ ] Create `app/templates/admin/vendor-edit.html` template
|
||||
- [ ] Add backend route for `/admin/vendors/{vendor_code}/edit`
|
||||
- [ ] Test vendor edit page loads
|
||||
- [ ] Test form submission works
|
||||
- [ ] Test quick action buttons (verify/activate)
|
||||
- [ ] Test validation error display
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Create Users Page (MEDIUM PRIORITY) 👥
|
||||
|
||||
**File:** `app/templates/admin/users.html`
|
||||
**File:** `static/admin/js/users.js`
|
||||
|
||||
Create from scratch - same pattern as vendors list page (see previous plan sections for full code)
|
||||
|
||||
---
|
||||
|
||||
## ⏰ Updated Time Estimates
|
||||
|
||||
| Task | Estimated Time | Priority |
|
||||
|------|---------------|----------|
|
||||
| Task 1: Add Vendor List Function | 45-60 min | HIGH |
|
||||
| Task 2: Update Vendor Edit to NEW pattern | 60-75 min | HIGH |
|
||||
| Task 3: Create Users Page | 45-60 min | MEDIUM |
|
||||
| Testing & Verification | 30-45 min | HIGH |
|
||||
| Cleanup | 15 min | LOW |
|
||||
|
||||
**Total: 3-4 hours**
|
||||
|
||||
---
|
||||
|
||||
## ✅ Success Criteria
|
||||
|
||||
By end of session:
|
||||
- [ ] Vendor LIST page working with stats and table
|
||||
- [ ] Vendor EDIT page using NEW patterns (ApiClient, Logger)
|
||||
- [ ] Users page created and working
|
||||
- [ ] All pages match dashboard styling
|
||||
- [ ] No console errors
|
||||
- [ ] Old patterns removed from vendor-edit.js
|
||||
|
||||
---
|
||||
|
||||
## 📝 Key Pattern Changes
|
||||
|
||||
### OLD Pattern (vendor-edit.js):
|
||||
```javascript
|
||||
// OLD
|
||||
const vendor = await apiClient.get('/admin/vendors/1');
|
||||
if (!Auth.isAuthenticated()) { ... }
|
||||
Utils.confirm('Message', 'Title');
|
||||
```
|
||||
|
||||
### NEW Pattern (dashboard.js):
|
||||
```javascript
|
||||
// NEW
|
||||
const vendor = await ApiClient.get('/admin/vendors/1');
|
||||
// No Auth checks - backend handles it
|
||||
confirm('Message with details');
|
||||
Logger.info('Action', 'COMPONENT', data);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Let's migrate! 🚀**
|
||||
752
docs/__temp/__PROJECT_ROADMAP/FRONTEND_DOCUMENTATION_PLAN.md
Normal file
752
docs/__temp/__PROJECT_ROADMAP/FRONTEND_DOCUMENTATION_PLAN.md
Normal file
@@ -0,0 +1,752 @@
|
||||
# Frontend Documentation Plan - LetzShop Platform
|
||||
|
||||
## 📚 Documentation Structure Overview
|
||||
|
||||
This documentation plan focuses on **practical guides** for implementing new features in the three main sections of the platform: **Admin**, **Vendor**, and **Shop** (customer-facing).
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Documentation Goals
|
||||
|
||||
1. **Enable rapid development** - Team members can implement new pages in 15-30 minutes
|
||||
2. **Ensure consistency** - All pages follow the same Alpine.js architecture patterns
|
||||
3. **Reduce errors** - Common pitfalls are documented and preventable
|
||||
4. **Facilitate onboarding** - New developers can contribute within days, not weeks
|
||||
5. **Maintain quality** - Code quality standards are embedded in the guides
|
||||
|
||||
---
|
||||
|
||||
## 📖 Proposed Documentation Structure
|
||||
|
||||
```
|
||||
docs/
|
||||
├── frontend/
|
||||
│ ├── index.md # Frontend overview & navigation
|
||||
│ │
|
||||
│ ├── 01-getting-started/
|
||||
│ │ ├── overview.md # Tech stack & architecture summary
|
||||
│ │ ├── setup.md # Local development setup
|
||||
│ │ ├── project-structure.md # File organization
|
||||
│ │ └── naming-conventions.md # Naming standards
|
||||
│ │
|
||||
│ ├── 02-core-concepts/
|
||||
│ │ ├── alpine-architecture.md # Alpine.js patterns we use
|
||||
│ │ ├── template-system.md # Jinja2 templates & inheritance
|
||||
│ │ ├── state-management.md # Reactive state with Alpine.js
|
||||
│ │ ├── api-integration.md # Using apiClient
|
||||
│ │ └── common-pitfalls.md # Variable conflicts & other issues
|
||||
│ │
|
||||
│ ├── 03-implementation-guides/
|
||||
│ │ ├── admin-section/
|
||||
│ │ │ ├── creating-admin-page.md # Step-by-step admin page guide
|
||||
│ │ │ ├── admin-page-template.md # Copy-paste template
|
||||
│ │ │ ├── sidebar-integration.md # Adding menu items
|
||||
│ │ │ └── admin-examples.md # Real examples (dashboard, vendors)
|
||||
│ │ │
|
||||
│ │ ├── vendor-section/
|
||||
│ │ │ ├── creating-vendor-page.md # Step-by-step vendor page guide
|
||||
│ │ │ ├── vendor-page-template.md # Copy-paste template
|
||||
│ │ │ └── vendor-examples.md # Real examples
|
||||
│ │ │
|
||||
│ │ └── shop-section/
|
||||
│ │ ├── creating-shop-page.md # Step-by-step shop page guide
|
||||
│ │ ├── shop-page-template.md # Copy-paste template
|
||||
│ │ └── shop-examples.md # Real examples
|
||||
│ │
|
||||
│ ├── 04-ui-components/
|
||||
│ │ ├── component-library.md # Overview & reference
|
||||
│ │ ├── forms.md # All form components
|
||||
│ │ ├── buttons.md # Button styles
|
||||
│ │ ├── cards.md # Card components
|
||||
│ │ ├── tables.md # Table patterns
|
||||
│ │ ├── modals.md # Modal dialogs
|
||||
│ │ ├── badges.md # Status badges
|
||||
│ │ ├── alerts-toasts.md # Notifications
|
||||
│ │ └── icons.md # Icon usage
|
||||
│ │
|
||||
│ ├── 05-common-patterns/
|
||||
│ │ ├── data-loading.md # Loading states & error handling
|
||||
│ │ ├── pagination.md # Client-side pagination
|
||||
│ │ ├── filtering-sorting.md # Table operations
|
||||
│ │ ├── form-validation.md # Validation patterns
|
||||
│ │ ├── crud-operations.md # Create, Read, Update, Delete
|
||||
│ │ └── real-time-updates.md # WebSocket/polling patterns
|
||||
│ │
|
||||
│ ├── 06-advanced-topics/
|
||||
│ │ ├── performance.md # Optimization techniques
|
||||
│ │ ├── dark-mode.md # Theme implementation
|
||||
│ │ ├── accessibility.md # A11y guidelines
|
||||
│ │ ├── responsive-design.md # Mobile-first approach
|
||||
│ │ └── debugging.md # Debugging techniques
|
||||
│ │
|
||||
│ ├── 07-testing/
|
||||
│ │ ├── testing-overview.md # Testing strategy
|
||||
│ │ ├── testing-hub-guide.md # Using the testing hub
|
||||
│ │ └── manual-testing-checklist.md # QA checklist
|
||||
│ │
|
||||
│ └── 08-reference/
|
||||
│ ├── alpine-js-reference.md # Alpine.js quick reference
|
||||
│ ├── api-client-reference.md # apiClient methods
|
||||
│ ├── utils-reference.md # Utility functions
|
||||
│ ├── css-classes.md # Tailwind classes we use
|
||||
│ └── troubleshooting.md # Common issues & solutions
|
||||
│
|
||||
└── backend/ # Backend docs (later)
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Priority Documentation Order
|
||||
|
||||
### Phase 1: Core Essentials (Week 1)
|
||||
**Goal:** Team can create basic pages immediately
|
||||
|
||||
1. **`01-getting-started/overview.md`**
|
||||
- Quick tech stack summary
|
||||
- Architecture diagram
|
||||
- 5-minute quick start
|
||||
|
||||
2. **`02-core-concepts/alpine-architecture.md`** ⭐ CRITICAL
|
||||
- The `...data()` pattern
|
||||
- `currentPage` identifier
|
||||
- Initialization guards
|
||||
- Variable naming (avoid conflicts!)
|
||||
- Based on FRONTEND_ARCHITECTURE_OVERVIEW.txt
|
||||
|
||||
3. **`03-implementation-guides/admin-section/creating-admin-page.md`** ⭐ CRITICAL
|
||||
- Step-by-step guide
|
||||
- 10-minute implementation time
|
||||
- Includes sidebar integration
|
||||
- Based on ALPINE_PAGE_TEMPLATE.md + COMPLETE_IMPLEMENTATION_GUIDE.md
|
||||
|
||||
4. **`03-implementation-guides/admin-section/admin-page-template.md`** ⭐ CRITICAL
|
||||
- Copy-paste HTML template
|
||||
- Copy-paste JavaScript template
|
||||
- Includes all essential patterns
|
||||
|
||||
### Phase 2: Component Library (Week 2)
|
||||
**Goal:** Team knows all available UI components
|
||||
|
||||
5. **`04-ui-components/component-library.md`**
|
||||
- Live examples
|
||||
- Based on Components page we created
|
||||
|
||||
6. **`04-ui-components/forms.md`**
|
||||
- All form inputs
|
||||
- Validation patterns
|
||||
- Based on UI_COMPONENTS.md
|
||||
|
||||
7. **`04-ui-components/icons.md`**
|
||||
- How to use icons
|
||||
- Category reference
|
||||
- Based on Icons Browser we created
|
||||
|
||||
### Phase 3: Common Patterns (Week 3)
|
||||
**Goal:** Team can implement common features
|
||||
|
||||
8. **`05-common-patterns/pagination.md`**
|
||||
- Based on PAGINATION_DOCUMENTATION.md
|
||||
- Avoid `currentPage` conflict!
|
||||
|
||||
9. **`05-common-patterns/crud-operations.md`**
|
||||
- List, create, edit, delete patterns
|
||||
- Based on vendors.js patterns
|
||||
|
||||
10. **`05-common-patterns/data-loading.md`**
|
||||
- Loading states
|
||||
- Error handling
|
||||
- Based on dashboard.js patterns
|
||||
|
||||
### Phase 4: Other Sections (Week 4)
|
||||
**Goal:** Vendor and Shop sections documented
|
||||
|
||||
11. **`03-implementation-guides/vendor-section/creating-vendor-page.md`**
|
||||
12. **`03-implementation-guides/shop-section/creating-shop-page.md`**
|
||||
|
||||
### Phase 5: Reference & Polish (Week 5)
|
||||
**Goal:** Complete reference documentation
|
||||
|
||||
13. **`02-core-concepts/common-pitfalls.md`** ⭐ IMPORTANT
|
||||
- Variable name conflicts (currentPage!)
|
||||
- Based on VENDORS_SIDEBAR_FIX.md
|
||||
|
||||
14. **`08-reference/troubleshooting.md`**
|
||||
15. All remaining reference docs
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Core Documentation Files (Detailed Specs)
|
||||
|
||||
### 1. `02-core-concepts/alpine-architecture.md`
|
||||
|
||||
**Content:**
|
||||
```markdown
|
||||
# Alpine.js Architecture Pattern
|
||||
|
||||
## Overview
|
||||
Our frontend uses Alpine.js with a specific inheritance pattern...
|
||||
|
||||
## The Base Pattern
|
||||
[Include FRONTEND_ARCHITECTURE_OVERVIEW.txt content]
|
||||
|
||||
## File Structure
|
||||
[Show the correct file locations]
|
||||
|
||||
## Critical Rules
|
||||
1. ALWAYS use `...data()` first
|
||||
2. ALWAYS set `currentPage` identifier
|
||||
3. ALWAYS use lowercase `apiClient`
|
||||
4. ALWAYS include initialization guard
|
||||
5. ALWAYS use unique variable names
|
||||
|
||||
## Common Mistakes
|
||||
[Include vendor currentPage conflict example]
|
||||
```
|
||||
|
||||
**Based on:**
|
||||
- FRONTEND_ARCHITECTURE_OVERVIEW.txt
|
||||
- ALPINE_PAGE_TEMPLATE.md
|
||||
- VENDORS_SIDEBAR_FIX.md
|
||||
|
||||
---
|
||||
|
||||
### 2. `03-implementation-guides/admin-section/creating-admin-page.md`
|
||||
|
||||
**Content:**
|
||||
```markdown
|
||||
# Creating a New Admin Page (10-Minute Guide)
|
||||
|
||||
## Prerequisites
|
||||
- [ ] Backend API endpoint exists
|
||||
- [ ] Route added to pages.py
|
||||
- [ ] Sidebar menu item planned
|
||||
|
||||
## Step 1: Create HTML Template (3 minutes)
|
||||
|
||||
Copy this template to `app/templates/admin/your-page.html`:
|
||||
|
||||
[Include complete template from ALPINE_PAGE_TEMPLATE.md]
|
||||
|
||||
## Step 2: Create JavaScript Component (5 minutes)
|
||||
|
||||
Copy this template to `static/admin/js/your-page.js`:
|
||||
|
||||
[Include complete JS template]
|
||||
|
||||
## Step 3: Add Sidebar Menu Item (1 minute)
|
||||
|
||||
[Show exact HTML to add to sidebar]
|
||||
|
||||
## Step 4: Add Route (1 minute)
|
||||
|
||||
[Show exact Python code for pages.py]
|
||||
|
||||
## Step 5: Test (2 minutes)
|
||||
|
||||
Checklist:
|
||||
- [ ] Page loads
|
||||
- [ ] Sidebar shows purple bar
|
||||
- [ ] Data loads from API
|
||||
- [ ] Dark mode works
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Issue 1: Sidebar not showing purple bar
|
||||
**Cause:** Variable name conflict
|
||||
**Solution:** [Link to common-pitfalls.md]
|
||||
|
||||
### Issue 2: Data not loading
|
||||
**Cause:** API endpoint mismatch
|
||||
**Solution:** Check apiClient.get() URL
|
||||
```
|
||||
|
||||
**Based on:**
|
||||
- ALPINE_PAGE_TEMPLATE.md
|
||||
- COMPLETE_IMPLEMENTATION_GUIDE.md
|
||||
- All the architecture documents
|
||||
|
||||
---
|
||||
|
||||
### 3. `05-common-patterns/pagination.md`
|
||||
|
||||
**Content:**
|
||||
```markdown
|
||||
# Client-Side Pagination Pattern
|
||||
|
||||
## Overview
|
||||
Pagination splits data into pages...
|
||||
|
||||
## Quick Start
|
||||
|
||||
[Include PAGINATION_QUICK_START.txt content]
|
||||
|
||||
## Full Implementation
|
||||
|
||||
[Include PAGINATION_DOCUMENTATION.md content]
|
||||
|
||||
## ⚠️ CRITICAL: Avoid Variable Conflicts
|
||||
|
||||
When implementing pagination, DO NOT name your pagination variable `currentPage`
|
||||
if your page uses the sidebar!
|
||||
|
||||
**Wrong:**
|
||||
```javascript
|
||||
currentPage: 'vendors', // For sidebar
|
||||
currentPage: 1, // For pagination - OVERWRITES ABOVE!
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
```javascript
|
||||
currentPage: 'vendors', // For sidebar
|
||||
page: 1, // For pagination - different name!
|
||||
```
|
||||
|
||||
[Link to VENDORS_SIDEBAR_FIX.md for full explanation]
|
||||
```
|
||||
|
||||
**Based on:**
|
||||
- PAGINATION_DOCUMENTATION.md
|
||||
- PAGINATION_QUICK_START.txt
|
||||
- VENDORS_SIDEBAR_FIX.md
|
||||
|
||||
---
|
||||
|
||||
### 4. `04-ui-components/component-library.md`
|
||||
|
||||
**Content:**
|
||||
```markdown
|
||||
# UI Component Library
|
||||
|
||||
## Live Reference
|
||||
Visit `/admin/components` to see all components with live examples.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
[Include UI_COMPONENTS_QUICK_REFERENCE.md content]
|
||||
|
||||
## Detailed Components
|
||||
|
||||
[Include UI_COMPONENTS.md content]
|
||||
|
||||
## Copy-Paste Examples
|
||||
|
||||
Each component includes:
|
||||
- Visual example
|
||||
- HTML code
|
||||
- Alpine.js bindings
|
||||
- Dark mode support
|
||||
```
|
||||
|
||||
**Based on:**
|
||||
- UI_COMPONENTS.md
|
||||
- UI_COMPONENTS_QUICK_REFERENCE.md
|
||||
- The Components page we created
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Documentation Features
|
||||
|
||||
### 1. **Code Templates**
|
||||
Every guide includes ready-to-use code templates:
|
||||
- ✅ Complete, working code
|
||||
- ✅ Comments explaining each part
|
||||
- ✅ No placeholders that need changing (except obvious ones like "your-page")
|
||||
|
||||
### 2. **Visual Diagrams**
|
||||
Use ASCII diagrams and flowcharts:
|
||||
```
|
||||
User Request → FastAPI Route → Jinja2 Template → HTML + Alpine.js → Browser
|
||||
```
|
||||
|
||||
### 3. **Before/After Examples**
|
||||
Show incorrect vs correct implementations:
|
||||
|
||||
**❌ Wrong:**
|
||||
```javascript
|
||||
currentPage: 'vendors',
|
||||
currentPage: 1 // Conflict!
|
||||
```
|
||||
|
||||
**✅ Correct:**
|
||||
```javascript
|
||||
currentPage: 'vendors',
|
||||
page: 1 // Different name
|
||||
```
|
||||
|
||||
### 4. **Checklists**
|
||||
Every guide ends with a testing checklist:
|
||||
- [ ] Page loads
|
||||
- [ ] Sidebar active indicator works
|
||||
- [ ] API data loads
|
||||
- [ ] Pagination works
|
||||
- [ ] Dark mode works
|
||||
|
||||
### 5. **Troubleshooting Sections**
|
||||
Common issues with solutions:
|
||||
```
|
||||
Problem: Sidebar indicator not showing
|
||||
Solution: Check for variable name conflicts
|
||||
Reference: docs/frontend/08-reference/troubleshooting.md#sidebar-issues
|
||||
```
|
||||
|
||||
### 6. **Time Estimates**
|
||||
Each task shows expected completion time:
|
||||
- Creating admin page: **10 minutes**
|
||||
- Adding pagination: **5 minutes**
|
||||
- Adding form validation: **15 minutes**
|
||||
|
||||
### 7. **Cross-References**
|
||||
Heavy linking between related topics:
|
||||
```
|
||||
See also:
|
||||
- [Alpine.js Architecture](../02-core-concepts/alpine-architecture.md)
|
||||
- [Common Pitfalls](../02-core-concepts/common-pitfalls.md)
|
||||
- [Pagination Pattern](../05-common-patterns/pagination.md)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation Standards
|
||||
|
||||
### Writing Style
|
||||
- **Practical first** - Show code, then explain
|
||||
- **Concise** - Get to the point quickly
|
||||
- **Examples everywhere** - Real code from actual pages
|
||||
- **Searchable** - Good headings, keywords, tags
|
||||
|
||||
### Code Examples
|
||||
```javascript
|
||||
// ✅ Good example - complete and working
|
||||
function adminDashboard() {
|
||||
return {
|
||||
...data(),
|
||||
currentPage: 'dashboard',
|
||||
stats: {},
|
||||
|
||||
async init() {
|
||||
if (window._dashboardInitialized) return;
|
||||
window._dashboardInitialized = true;
|
||||
await this.loadStats();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ❌ Bad example - incomplete, needs work
|
||||
function myPage() {
|
||||
return {
|
||||
// TODO: Add your code here
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Warning Boxes
|
||||
Use admonitions for critical information:
|
||||
|
||||
```markdown
|
||||
!!! warning "Critical: Variable Name Conflicts"
|
||||
Never use `currentPage` for both sidebar identification and pagination!
|
||||
This will cause the sidebar active indicator to break.
|
||||
|
||||
**Solution:** Use `page` or `pageNumber` for pagination.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Technical Implementation
|
||||
|
||||
### MkDocs Configuration Update
|
||||
|
||||
Update `mkdocs.yml` to include frontend section:
|
||||
|
||||
```yaml
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Getting Started:
|
||||
- Installation: getting-started/installation.md
|
||||
- Quick Start: getting-started/quickstart.md
|
||||
- Database Setup: getting-started/database-setup.md
|
||||
- Configuration: getting-started/configuration.md
|
||||
|
||||
# NEW: Frontend Documentation
|
||||
- Frontend:
|
||||
- Overview: frontend/index.md
|
||||
- Getting Started:
|
||||
- Overview: frontend/01-getting-started/overview.md
|
||||
- Setup: frontend/01-getting-started/setup.md
|
||||
- Project Structure: frontend/01-getting-started/project-structure.md
|
||||
- Naming Conventions: frontend/01-getting-started/naming-conventions.md
|
||||
- Core Concepts:
|
||||
- Alpine.js Architecture: frontend/02-core-concepts/alpine-architecture.md
|
||||
- Template System: frontend/02-core-concepts/template-system.md
|
||||
- State Management: frontend/02-core-concepts/state-management.md
|
||||
- API Integration: frontend/02-core-concepts/api-integration.md
|
||||
- Common Pitfalls: frontend/02-core-concepts/common-pitfalls.md
|
||||
- Implementation Guides:
|
||||
- Admin Section:
|
||||
- Creating Admin Page: frontend/03-implementation-guides/admin-section/creating-admin-page.md
|
||||
- Admin Page Template: frontend/03-implementation-guides/admin-section/admin-page-template.md
|
||||
- Sidebar Integration: frontend/03-implementation-guides/admin-section/sidebar-integration.md
|
||||
- Examples: frontend/03-implementation-guides/admin-section/admin-examples.md
|
||||
- Vendor Section:
|
||||
- Creating Vendor Page: frontend/03-implementation-guides/vendor-section/creating-vendor-page.md
|
||||
- Vendor Page Template: frontend/03-implementation-guides/vendor-section/vendor-page-template.md
|
||||
- Examples: frontend/03-implementation-guides/vendor-section/vendor-examples.md
|
||||
- Shop Section:
|
||||
- Creating Shop Page: frontend/03-implementation-guides/shop-section/creating-shop-page.md
|
||||
- Shop Page Template: frontend/03-implementation-guides/shop-section/shop-page-template.md
|
||||
- Examples: frontend/03-implementation-guides/shop-section/shop-examples.md
|
||||
- UI Components:
|
||||
- Component Library: frontend/04-ui-components/component-library.md
|
||||
- Forms: frontend/04-ui-components/forms.md
|
||||
- Buttons: frontend/04-ui-components/buttons.md
|
||||
- Cards: frontend/04-ui-components/cards.md
|
||||
- Tables: frontend/04-ui-components/tables.md
|
||||
- Modals: frontend/04-ui-components/modals.md
|
||||
- Badges: frontend/04-ui-components/badges.md
|
||||
- Alerts & Toasts: frontend/04-ui-components/alerts-toasts.md
|
||||
- Icons: frontend/04-ui-components/icons.md
|
||||
- Common Patterns:
|
||||
- Data Loading: frontend/05-common-patterns/data-loading.md
|
||||
- Pagination: frontend/05-common-patterns/pagination.md
|
||||
- Filtering & Sorting: frontend/05-common-patterns/filtering-sorting.md
|
||||
- Form Validation: frontend/05-common-patterns/form-validation.md
|
||||
- CRUD Operations: frontend/05-common-patterns/crud-operations.md
|
||||
- Real-time Updates: frontend/05-common-patterns/real-time-updates.md
|
||||
- Advanced Topics:
|
||||
- Performance: frontend/06-advanced-topics/performance.md
|
||||
- Dark Mode: frontend/06-advanced-topics/dark-mode.md
|
||||
- Accessibility: frontend/06-advanced-topics/accessibility.md
|
||||
- Responsive Design: frontend/06-advanced-topics/responsive-design.md
|
||||
- Debugging: frontend/06-advanced-topics/debugging.md
|
||||
- Testing:
|
||||
- Testing Overview: frontend/07-testing/testing-overview.md
|
||||
- Testing Hub Guide: frontend/07-testing/testing-hub-guide.md
|
||||
- Manual Testing: frontend/07-testing/manual-testing-checklist.md
|
||||
- Reference:
|
||||
- Alpine.js Quick Reference: frontend/08-reference/alpine-js-reference.md
|
||||
- API Client Reference: frontend/08-reference/api-client-reference.md
|
||||
- Utils Reference: frontend/08-reference/utils-reference.md
|
||||
- CSS Classes: frontend/08-reference/css-classes.md
|
||||
- Troubleshooting: frontend/08-reference/troubleshooting.md
|
||||
|
||||
# Existing sections
|
||||
- API:
|
||||
- Overview: api/index.md
|
||||
# ... rest of API docs
|
||||
|
||||
- Testing:
|
||||
- Testing Guide: testing/testing-guide.md
|
||||
- Test Maintenance: testing/test-maintenance.md
|
||||
|
||||
# Backend docs come later
|
||||
# - Backend:
|
||||
# - Architecture: backend/architecture.md
|
||||
# - ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Success Metrics
|
||||
|
||||
### Team Adoption
|
||||
- [ ] 100% of team can create a basic admin page in <15 minutes
|
||||
- [ ] 90% of new pages follow architecture correctly on first try
|
||||
- [ ] <5% of PRs have sidebar indicator issues
|
||||
- [ ] <10% of PRs have variable naming conflicts
|
||||
|
||||
### Documentation Quality
|
||||
- [ ] Every guide has working code examples
|
||||
- [ ] Every guide has a testing checklist
|
||||
- [ ] Every guide links to related topics
|
||||
- [ ] Every guide has time estimates
|
||||
|
||||
### Onboarding Speed
|
||||
- [ ] New developers can create first page within 1 day
|
||||
- [ ] New developers can work independently within 3 days
|
||||
- [ ] Reduced onboarding questions by 80%
|
||||
|
||||
---
|
||||
|
||||
## 📅 Implementation Timeline
|
||||
|
||||
### Week 1: Core Essentials
|
||||
- Write Alpine.js Architecture guide
|
||||
- Write Creating Admin Page guide
|
||||
- Create Admin Page Template
|
||||
- Write Common Pitfalls guide
|
||||
|
||||
### Week 2: Components & Patterns
|
||||
- Document Component Library
|
||||
- Document Forms, Buttons, Cards
|
||||
- Document Pagination pattern
|
||||
- Document CRUD operations
|
||||
|
||||
### Week 3: Reference & Vendor
|
||||
- Complete Reference section
|
||||
- Write Vendor section guides
|
||||
- Create Vendor templates
|
||||
|
||||
### Week 4: Shop & Polish
|
||||
- Write Shop section guides
|
||||
- Create Shop templates
|
||||
- Review and polish all docs
|
||||
- Add missing cross-references
|
||||
|
||||
### Week 5: Testing & Launch
|
||||
- Internal review with team
|
||||
- Fix any issues found
|
||||
- Launch documentation
|
||||
- Gather feedback
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Maintenance Plan
|
||||
|
||||
### Regular Updates
|
||||
- **Weekly:** Check for new common issues
|
||||
- **Monthly:** Review and update examples
|
||||
- **Quarterly:** Major documentation review
|
||||
|
||||
### Version Control
|
||||
- All documentation in Git
|
||||
- Changes reviewed like code
|
||||
- Version numbers for major updates
|
||||
|
||||
### Feedback Loop
|
||||
- Add "Was this helpful?" to each page
|
||||
- Collect common questions
|
||||
- Update docs based on feedback
|
||||
|
||||
---
|
||||
|
||||
## 📊 Documentation Metrics
|
||||
|
||||
Track these metrics:
|
||||
1. **Page views** - Which docs are most used?
|
||||
2. **Search terms** - What are people looking for?
|
||||
3. **Time on page** - Are docs too long/short?
|
||||
4. **Bounce rate** - Are people finding what they need?
|
||||
5. **Questions in Slack** - Are docs answering questions?
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Learning Path for New Developers
|
||||
|
||||
### Day 1: Foundations
|
||||
1. Read: Overview
|
||||
2. Read: Alpine.js Architecture
|
||||
3. Read: Creating Admin Page guide
|
||||
4. Exercise: Create a simple "Hello World" admin page
|
||||
|
||||
### Day 2: Practice
|
||||
1. Read: Component Library
|
||||
2. Read: Data Loading pattern
|
||||
3. Exercise: Create admin page that loads data from API
|
||||
|
||||
### Day 3: Patterns
|
||||
1. Read: Pagination pattern
|
||||
2. Read: CRUD operations
|
||||
3. Exercise: Create full CRUD page with pagination
|
||||
|
||||
### Day 4: Real Work
|
||||
1. Read: Common Pitfalls
|
||||
2. Read: Troubleshooting
|
||||
3. Exercise: Implement first real feature
|
||||
|
||||
### Day 5: Independence
|
||||
- Work on real tickets independently
|
||||
- Refer to docs as needed
|
||||
- Ask questions when stuck
|
||||
|
||||
---
|
||||
|
||||
## 📝 Documentation Templates
|
||||
|
||||
### Guide Template
|
||||
```markdown
|
||||
# [Feature/Pattern Name]
|
||||
|
||||
## Overview
|
||||
Brief 2-3 sentence description
|
||||
|
||||
## Prerequisites
|
||||
- [ ] Requirement 1
|
||||
- [ ] Requirement 2
|
||||
|
||||
## Quick Start (5 minutes)
|
||||
Fastest path to working code
|
||||
|
||||
## Step-by-Step Guide
|
||||
Detailed instructions
|
||||
|
||||
## Common Issues
|
||||
Problems and solutions
|
||||
|
||||
## Testing Checklist
|
||||
- [ ] Test 1
|
||||
- [ ] Test 2
|
||||
|
||||
## See Also
|
||||
- [Related doc 1](link)
|
||||
- [Related doc 2](link)
|
||||
```
|
||||
|
||||
### Reference Template
|
||||
```markdown
|
||||
# [API/Component Name] Reference
|
||||
|
||||
## Overview
|
||||
What it does
|
||||
|
||||
## Usage
|
||||
Basic usage example
|
||||
|
||||
## API
|
||||
Full API documentation
|
||||
|
||||
## Examples
|
||||
Multiple real-world examples
|
||||
|
||||
## See Also
|
||||
Related references
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Ready to Implement
|
||||
|
||||
This documentation plan provides:
|
||||
|
||||
1. **Clear structure** - Organized by role and task
|
||||
2. **Practical focus** - Implementation guides, not theory
|
||||
3. **Real examples** - Based on actual working code
|
||||
4. **Team-oriented** - Designed for collaboration
|
||||
5. **Maintainable** - Easy to update and extend
|
||||
|
||||
**Next Steps:**
|
||||
1. Review and approve this plan
|
||||
2. Start with Phase 1 (Core Essentials)
|
||||
3. Write docs one at a time
|
||||
4. Get team feedback early
|
||||
5. Iterate and improve
|
||||
|
||||
**Estimated effort:**
|
||||
- 5 weeks for initial documentation
|
||||
- 2-4 hours per week for maintenance
|
||||
- Massive time savings for team (100+ hours/year)
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Benefits
|
||||
|
||||
Once complete, your team will have:
|
||||
|
||||
✅ **Faster development** - 10-15 minute page creation
|
||||
✅ **Fewer bugs** - Common mistakes documented
|
||||
✅ **Better code quality** - Patterns enforced through docs
|
||||
✅ **Easier onboarding** - New devs productive in days
|
||||
✅ **Reduced questions** - Self-service documentation
|
||||
✅ **Scalable knowledge** - Team expertise captured
|
||||
|
||||
**ROI:** Pays for itself after 2-3 features implemented! 📈
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,399 @@
|
||||
# Work Plan - October 22, 2025
|
||||
## Jinja2 Migration: Polish & Complete Admin Panel
|
||||
|
||||
**Current Status:** Core migration complete ✅ | Auth loop fixed ✅ | Minor issues remaining ⚠️
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Today's Goals
|
||||
|
||||
1. ✅ Fix icon system and utils.js conflicts
|
||||
2. ✅ Test and verify logout flow
|
||||
3. ✅ Test all admin pages (vendors, users)
|
||||
4. ✅ Create remaining templates
|
||||
5. ✅ Clean up and remove old code
|
||||
|
||||
**Estimated Time:** 3-4 hours
|
||||
|
||||
---
|
||||
|
||||
## 📋 Task List
|
||||
|
||||
### Priority 1: Fix Icon/Utils Conflicts (HIGH) ⚠️
|
||||
|
||||
**Issue Reported:**
|
||||
> "Share some outputs about $icons issues and utils already declared"
|
||||
|
||||
#### Task 1.1: Investigate Icon Issues
|
||||
- [ ] Check browser console for icon-related errors
|
||||
- [ ] Verify `icons.js` is loaded only once
|
||||
- [ ] Check for duplicate `window.icon` declarations
|
||||
- [ ] Test icon rendering in all templates
|
||||
|
||||
**Files to Check:**
|
||||
- `static/shared/js/icons.js`
|
||||
- `app/templates/admin/base.html` (script order)
|
||||
- `app/templates/admin/login.html` (script order)
|
||||
|
||||
**Expected Issues:**
|
||||
```javascript
|
||||
// Possible duplicate declaration
|
||||
Uncaught SyntaxError: Identifier 'icon' has already been declared
|
||||
// or
|
||||
Warning: window.icon is already defined
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
- Ensure `icons.js` loaded only once per page
|
||||
- Remove any duplicate `icon()` function declarations
|
||||
- Verify Alpine magic helper `$icon()` is registered correctly
|
||||
|
||||
#### Task 1.2: Investigate Utils Issues
|
||||
- [ ] Check for duplicate `Utils` object declarations
|
||||
- [ ] Verify `utils.js` loaded only once
|
||||
- [ ] Test all utility functions (formatDate, showToast, etc.)
|
||||
|
||||
**Files to Check:**
|
||||
- `static/shared/js/utils.js`
|
||||
- `static/shared/js/api-client.js` (Utils defined here too?)
|
||||
|
||||
**Potential Fix:**
|
||||
```javascript
|
||||
// Option 1: Use namespace to avoid conflicts
|
||||
if (typeof window.Utils === 'undefined') {
|
||||
window.Utils = { /* ... */ };
|
||||
}
|
||||
|
||||
// Option 2: Remove duplicate definitions
|
||||
// Keep Utils only in one place (either utils.js OR api-client.js)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Priority 2: Test Logout Flow (HIGH) 🔐
|
||||
|
||||
#### Task 2.1: Test Logout Button
|
||||
- [ ] Click logout in header
|
||||
- [ ] Verify cookie is deleted
|
||||
- [ ] Verify localStorage is cleared
|
||||
- [ ] Verify redirect to login page
|
||||
- [ ] Verify cannot access dashboard after logout
|
||||
|
||||
**Test Script:**
|
||||
```javascript
|
||||
// Before logout
|
||||
console.log('Cookie:', document.cookie);
|
||||
console.log('localStorage:', localStorage.getItem('admin_token'));
|
||||
|
||||
// Click logout
|
||||
|
||||
// After logout (should be empty)
|
||||
console.log('Cookie:', document.cookie); // Should not contain admin_token
|
||||
console.log('localStorage:', localStorage.getItem('admin_token')); // Should be null
|
||||
```
|
||||
|
||||
#### Task 2.2: Update Logout Endpoint (if needed)
|
||||
**File:** `app/api/v1/admin/auth.py`
|
||||
|
||||
Already implemented, just verify:
|
||||
```python
|
||||
@router.post("/logout")
|
||||
def admin_logout(response: Response):
|
||||
# Clears the cookie
|
||||
response.delete_cookie(key="admin_token", path="/")
|
||||
return {"message": "Logged out successfully"}
|
||||
```
|
||||
|
||||
#### Task 2.3: Update Header Logout Button
|
||||
**File:** `app/templates/partials/header.html`
|
||||
|
||||
Verify logout button calls the correct endpoint:
|
||||
```html
|
||||
<button @click="handleLogout()">
|
||||
Logout
|
||||
</button>
|
||||
|
||||
<script>
|
||||
function handleLogout() {
|
||||
// Call logout API
|
||||
fetch('/api/v1/admin/auth/logout', { method: 'POST' })
|
||||
.then(() => {
|
||||
// Clear localStorage
|
||||
localStorage.clear();
|
||||
// Redirect
|
||||
window.location.href = '/admin/login';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Priority 3: Test All Admin Pages (MEDIUM) 📄
|
||||
|
||||
#### Task 3.1: Test Vendors Page
|
||||
- [ ] Navigate to `/admin/vendors`
|
||||
- [ ] Verify page loads with authentication
|
||||
- [ ] Check if template exists or needs creation
|
||||
- [ ] Test vendor list display
|
||||
- [ ] Test vendor creation button
|
||||
|
||||
**If template missing:**
|
||||
Create `app/templates/admin/vendors.html`
|
||||
|
||||
#### Task 3.2: Test Users Page
|
||||
- [ ] Navigate to `/admin/users`
|
||||
- [ ] Verify page loads with authentication
|
||||
- [ ] Check if template exists or needs creation
|
||||
- [ ] Test user list display
|
||||
|
||||
**If template missing:**
|
||||
Create `app/templates/admin/users.html`
|
||||
|
||||
#### Task 3.3: Test Navigation
|
||||
- [ ] Click all sidebar links
|
||||
- [ ] Verify no 404 errors
|
||||
- [ ] Verify active state highlights correctly
|
||||
- [ ] Test breadcrumbs (if applicable)
|
||||
|
||||
---
|
||||
|
||||
### Priority 4: Create Missing Templates (MEDIUM) 📝
|
||||
|
||||
#### Task 4.1: Create Vendors Template
|
||||
**File:** `app/templates/admin/vendors.html`
|
||||
|
||||
```jinja2
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Vendors Management{% endblock %}
|
||||
|
||||
{% block alpine_data %}adminVendors(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="my-6">
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Vendors Management
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Vendor list content -->
|
||||
<div x-data="adminVendors()">
|
||||
<!-- Your existing vendors.html content here -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script src="{{ url_for('static', path='admin/js/vendors.js') }}"></script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
#### Task 4.2: Create Users Template
|
||||
**File:** `app/templates/admin/users.html`
|
||||
|
||||
Similar structure to vendors template.
|
||||
|
||||
#### Task 4.3: Verify Vendor Edit Page
|
||||
Check if vendor-edit needs a template or if it's a modal/overlay.
|
||||
|
||||
---
|
||||
|
||||
### Priority 5: Cleanup (LOW) 🧹
|
||||
|
||||
#### Task 5.1: Remove Old Static HTML Files
|
||||
- [ ] Delete `static/admin/dashboard.html` (if exists)
|
||||
- [ ] Delete `static/admin/vendors.html` (if exists)
|
||||
- [ ] Delete `static/admin/users.html` (if exists)
|
||||
- [ ] Delete `static/admin/partials/` directory
|
||||
|
||||
**Before deleting:** Backup files just in case!
|
||||
|
||||
#### Task 5.2: Remove Partial Loader
|
||||
- [ ] Delete `static/shared/js/partial-loader.js`
|
||||
- [ ] Remove any references to `partialLoader` in code
|
||||
- [ ] Search codebase: `grep -r "partial-loader" .`
|
||||
|
||||
#### Task 5.3: Clean Up frontend.py
|
||||
**File:** `app/routes/frontend.py`
|
||||
|
||||
- [ ] Remove commented-out admin routes
|
||||
- [ ] Or delete file entirely if only contained admin routes
|
||||
- [ ] Update imports if needed
|
||||
|
||||
#### Task 5.4: Production Mode Preparation
|
||||
- [ ] Set log levels to production (INFO or WARN)
|
||||
- [ ] Update cookie `secure=True` for production
|
||||
- [ ] Remove debug console.logs
|
||||
- [ ] Test with production settings
|
||||
|
||||
**Update log levels:**
|
||||
```javascript
|
||||
// static/admin/js/log-config.js
|
||||
GLOBAL_LEVEL: isDevelopment ? 4 : 2, // Debug in dev, Warnings in prod
|
||||
LOGIN: isDevelopment ? 4 : 1, // Full debug in dev, errors only in prod
|
||||
API_CLIENT: isDevelopment ? 3 : 1, // Info in dev, errors only in prod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Checklist
|
||||
|
||||
### Comprehensive Testing
|
||||
- [ ] Fresh login (clear all data first)
|
||||
- [ ] Dashboard loads correctly
|
||||
- [ ] Stats cards display data
|
||||
- [ ] Recent vendors table works
|
||||
- [ ] Sidebar navigation works
|
||||
- [ ] Dark mode toggle works
|
||||
- [ ] Logout clears auth and redirects
|
||||
- [ ] Cannot access dashboard after logout
|
||||
- [ ] Vendors page loads
|
||||
- [ ] Users page loads
|
||||
- [ ] No console errors
|
||||
- [ ] No 404 errors in Network tab
|
||||
- [ ] Icons display correctly
|
||||
- [ ] All Alpine.js components work
|
||||
|
||||
### Browser Testing
|
||||
- [ ] Chrome/Edge
|
||||
- [ ] Firefox
|
||||
- [ ] Safari (if available)
|
||||
- [ ] Mobile view (responsive)
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Debugging Guide
|
||||
|
||||
### If Icons Don't Display:
|
||||
```javascript
|
||||
// Check in console:
|
||||
console.log('window.icon:', typeof window.icon);
|
||||
console.log('window.Icons:', typeof window.Icons);
|
||||
console.log('$icon available:', typeof Alpine !== 'undefined' && Alpine.magic('icon'));
|
||||
|
||||
// Test manually:
|
||||
document.body.innerHTML += window.icon('home', 'w-6 h-6');
|
||||
```
|
||||
|
||||
### If Utils Undefined:
|
||||
```javascript
|
||||
// Check in console:
|
||||
console.log('Utils:', typeof Utils);
|
||||
console.log('Utils methods:', Object.keys(Utils || {}));
|
||||
|
||||
// Test manually:
|
||||
Utils.showToast('Test message', 'info');
|
||||
```
|
||||
|
||||
### If Auth Fails:
|
||||
```javascript
|
||||
// Check storage:
|
||||
console.log('localStorage token:', localStorage.getItem('admin_token'));
|
||||
console.log('Cookie:', document.cookie);
|
||||
|
||||
// Test API manually:
|
||||
fetch('/api/v1/admin/auth/me', {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('admin_token')}` }
|
||||
}).then(r => r.json()).then(console.log);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Documentation Tasks
|
||||
|
||||
### Update Documentation
|
||||
- [ ] Update project README with new architecture
|
||||
- [ ] Document authentication flow (cookies + localStorage)
|
||||
- [ ] Document template structure
|
||||
- [ ] Add deployment notes (dev vs production)
|
||||
- [ ] Update API documentation if needed
|
||||
|
||||
### Code Comments
|
||||
- [ ] Add comments to complex authentication code
|
||||
- [ ] Document cookie settings and rationale
|
||||
- [ ] Explain dual token storage pattern
|
||||
- [ ] Add JSDoc comments to JavaScript functions
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next Phase Preview (After Today)
|
||||
|
||||
### Vendor Portal Migration
|
||||
1. Apply same Jinja2 pattern to vendor routes
|
||||
2. Create vendor templates (login, dashboard, etc.)
|
||||
3. Implement vendor authentication (separate cookie: `vendor_token`)
|
||||
4. Test vendor flows
|
||||
|
||||
### Customer/Shop Migration
|
||||
1. Customer authentication system
|
||||
2. Shop templates
|
||||
3. Shopping cart (consider cookie vs localStorage)
|
||||
4. "Remember Me" implementation
|
||||
|
||||
### Advanced Features
|
||||
1. "Remember Me" checkbox (30-day cookies)
|
||||
2. Session management
|
||||
3. Multiple device logout
|
||||
4. Security enhancements (CSRF tokens)
|
||||
|
||||
---
|
||||
|
||||
## ⏰ Time Estimates
|
||||
|
||||
| Task | Estimated Time | Priority |
|
||||
|------|---------------|----------|
|
||||
| Fix icon/utils issues | 30-45 min | HIGH |
|
||||
| Test logout flow | 15-30 min | HIGH |
|
||||
| Test admin pages | 30 min | MEDIUM |
|
||||
| Create missing templates | 45-60 min | MEDIUM |
|
||||
| Cleanup old code | 30 min | LOW |
|
||||
| Testing & verification | 30-45 min | HIGH |
|
||||
| Documentation | 30 min | LOW |
|
||||
|
||||
**Total: 3-4 hours**
|
||||
|
||||
---
|
||||
|
||||
## ✅ Success Criteria for Today
|
||||
|
||||
By end of day, we should have:
|
||||
- [ ] All icons displaying correctly
|
||||
- [ ] No JavaScript errors in console
|
||||
- [ ] Logout flow working perfectly
|
||||
- [ ] All admin pages accessible and working
|
||||
- [ ] Templates for vendors and users pages
|
||||
- [ ] Old code cleaned up
|
||||
- [ ] Comprehensive testing completed
|
||||
- [ ] Documentation updated
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Stretch Goals (If Time Permits)
|
||||
|
||||
1. Add loading states to all buttons
|
||||
2. Improve error messages (user-friendly)
|
||||
3. Add success/error toasts to all operations
|
||||
4. Implement "Remember Me" checkbox
|
||||
5. Start vendor portal migration
|
||||
6. Add unit tests for authentication
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support Resources
|
||||
|
||||
### If Stuck:
|
||||
- Review yesterday's complete file implementations
|
||||
- Check browser console for detailed logs (log level 4)
|
||||
- Use test-auth-flow.html for systematic testing
|
||||
- Check Network tab for HTTP requests/responses
|
||||
|
||||
### Reference Files:
|
||||
- `static/admin/test-auth-flow.html` - Testing interface
|
||||
- `TESTING_CHECKLIST.md` - Systematic testing guide
|
||||
- Yesterday's complete file updates (in conversation)
|
||||
|
||||
---
|
||||
|
||||
**Good luck with today's tasks! 🚀**
|
||||
|
||||
Remember: Take breaks, test
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,520 @@
|
||||
# Jinja2 Migration Progress - Admin Panel
|
||||
|
||||
**Date:** October 20, 2025
|
||||
**Project:** Multi-Tenant E-commerce Platform
|
||||
**Goal:** Migrate from static HTML files to Jinja2 server-rendered templates
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Current Status: DEBUGGING AUTH LOOP
|
||||
|
||||
We successfully set up the Jinja2 infrastructure but are experiencing authentication redirect loops. We're in the process of simplifying the auth flow to resolve this.
|
||||
|
||||
---
|
||||
|
||||
## ✅ What's Been Completed
|
||||
|
||||
### 1. Infrastructure Setup ✅
|
||||
|
||||
- [x] Added Jinja2Templates to `main.py`
|
||||
- [x] Created `app/templates/` directory structure
|
||||
- [x] Created `app/api/v1/admin/pages.py` for HTML routes
|
||||
- [x] Integrated pages router into the main app
|
||||
|
||||
**Files Created:**
|
||||
```
|
||||
app/
|
||||
├── templates/
|
||||
│ ├── admin/
|
||||
│ │ ├── base.html ✅ Created
|
||||
│ │ ├── login.html ✅ Created
|
||||
│ │ └── dashboard.html ✅ Created
|
||||
│ └── partials/
|
||||
│ ├── header.html ✅ Moved from static
|
||||
│ └── sidebar.html ✅ Moved from static
|
||||
└── api/
|
||||
└── v1/
|
||||
└── admin/
|
||||
└── pages.py ✅ Created
|
||||
```
|
||||
|
||||
### 2. Route Configuration ✅
|
||||
|
||||
**New Jinja2 Routes (working):**
|
||||
- `/admin/` → redirects to `/admin/dashboard`
|
||||
- `/admin/login` → login page (no auth)
|
||||
- `/admin/dashboard` → dashboard page (requires auth)
|
||||
- `/admin/vendors` → vendors page (requires auth)
|
||||
- `/admin/users` → users page (requires auth)
|
||||
|
||||
**Old Static Routes (disabled):**
|
||||
- Commented out admin routes in `app/routes/frontend.py`
|
||||
- Old `/static/admin/*.html` routes no longer active
|
||||
|
||||
### 3. Exception Handler Updates ✅
|
||||
|
||||
- [x] Updated `app/exceptions/handler.py` to redirect HTML requests on 401
|
||||
- [x] Added `_is_html_page_request()` helper function
|
||||
- [x] Server-side redirects working for unauthenticated page access
|
||||
|
||||
### 4. JavaScript Updates ✅
|
||||
|
||||
Updated all JavaScript files to use new routes:
|
||||
|
||||
**Files Updated:**
|
||||
- `static/admin/js/dashboard.js` - viewVendor() uses `/admin/vendors`
|
||||
- `static/admin/js/login.js` - redirects to `/admin/dashboard`
|
||||
- `static/admin/js/vendors.js` - auth checks use `/admin/login`
|
||||
- `static/admin/js/vendor-edit.js` - all redirects updated
|
||||
- `static/shared/js/api-client.js` - handleUnauthorized() uses `/admin/login`
|
||||
|
||||
### 5. Template Structure ✅
|
||||
|
||||
**Base Template (`app/templates/admin/base.html`):**
|
||||
- Server-side includes for header and sidebar (no more AJAX loading!)
|
||||
- Proper script loading order
|
||||
- Alpine.js integration
|
||||
- No more `partial-loader.js`
|
||||
|
||||
**Dashboard Template (`app/templates/admin/dashboard.html`):**
|
||||
- Extends base template
|
||||
- Uses Alpine.js `adminDashboard()` component
|
||||
- Stats cards and recent vendors table
|
||||
|
||||
**Login Template (`app/templates/admin/login.html`):**
|
||||
- Standalone page (doesn't extend base)
|
||||
- Uses Alpine.js `adminLogin()` component
|
||||
|
||||
---
|
||||
|
||||
## ❌ Current Problem: Authentication Loop
|
||||
|
||||
### Issue Description
|
||||
|
||||
Getting infinite redirect loops in various scenarios:
|
||||
1. After login → redirects back to login
|
||||
2. On login page → continuous API calls to `/admin/auth/me`
|
||||
3. Dashboard → redirects to login → redirects to dashboard
|
||||
|
||||
### Root Causes Identified
|
||||
|
||||
1. **Multiple redirect handlers fighting:**
|
||||
- Server-side: `handler.py` redirects on 401 for HTML pages
|
||||
- Client-side: `api-client.js` also redirects on 401
|
||||
- Both triggering simultaneously
|
||||
|
||||
2. **Login page checking auth on init:**
|
||||
- Calls `/admin/auth/me` on page load
|
||||
- Gets 401 → triggers redirect
|
||||
- Creates loop
|
||||
|
||||
3. **Token not being sent properly:**
|
||||
- Token stored but API calls not including it
|
||||
- Gets 401 even with valid token
|
||||
|
||||
### Latest Approach (In Progress)
|
||||
|
||||
Simplifying to minimal working version:
|
||||
- Login page does NOTHING on init (no auth checking)
|
||||
- API client does NOT redirect (just throws errors)
|
||||
- Server ONLY redirects browser HTML requests (not API calls)
|
||||
- One source of truth for auth handling
|
||||
|
||||
---
|
||||
|
||||
## 📝 Files Modified (Complete List)
|
||||
|
||||
### Backend Files
|
||||
|
||||
1. **`main.py`**
|
||||
```python
|
||||
# Added:
|
||||
- Jinja2Templates import and configuration
|
||||
- admin_pages router include at /admin prefix
|
||||
```
|
||||
|
||||
2. **`app/api/main.py`** (unchanged - just includes v1 routes)
|
||||
|
||||
3. **`app/api/v1/admin/__init__.py`**
|
||||
```python
|
||||
# Added:
|
||||
- import pages
|
||||
- router.include_router(pages.router, tags=["admin-pages"])
|
||||
```
|
||||
|
||||
4. **`app/api/v1/admin/pages.py`** (NEW FILE)
|
||||
```python
|
||||
# Contains:
|
||||
- @router.get("/") - root redirect
|
||||
- @router.get("/login") - login page
|
||||
- @router.get("/dashboard") - dashboard page
|
||||
- @router.get("/vendors") - vendors page
|
||||
- @router.get("/users") - users page
|
||||
```
|
||||
|
||||
5. **`app/routes/frontend.py`**
|
||||
```python
|
||||
# Changed:
|
||||
- Commented out all /admin/ routes
|
||||
- Left vendor and shop routes active
|
||||
```
|
||||
|
||||
6. **`app/exceptions/handler.py`**
|
||||
```python
|
||||
# Added:
|
||||
- 401 redirect logic for HTML pages
|
||||
- _is_html_page_request() helper
|
||||
# Status: Needs simplification
|
||||
```
|
||||
|
||||
### Frontend Files
|
||||
|
||||
1. **`static/admin/js/login.js`**
|
||||
```javascript
|
||||
// Changed:
|
||||
- Removed /static/admin/ paths
|
||||
- Updated to /admin/ paths
|
||||
- checkExistingAuth() logic
|
||||
# Status: Needs simplification
|
||||
```
|
||||
|
||||
2. **`static/admin/js/dashboard.js`**
|
||||
```javascript
|
||||
// Changed:
|
||||
- viewVendor() uses /admin/vendors
|
||||
# Status: Working
|
||||
```
|
||||
|
||||
3. **`static/admin/js/vendors.js`**
|
||||
```javascript
|
||||
// Changed:
|
||||
- checkAuth() redirects to /admin/login
|
||||
- handleLogout() redirects to /admin/login
|
||||
# Status: Not tested yet
|
||||
```
|
||||
|
||||
4. **`static/admin/js/vendor-edit.js`**
|
||||
```javascript
|
||||
// Changed:
|
||||
- All /static/admin/ paths to /admin/
|
||||
# Status: Not tested yet
|
||||
```
|
||||
|
||||
5. **`static/shared/js/api-client.js`**
|
||||
```javascript
|
||||
// Changed:
|
||||
- handleUnauthorized() uses /admin/login
|
||||
# Status: Needs simplification - causing loops
|
||||
```
|
||||
|
||||
6. **`static/shared/js/utils.js`** (unchanged - working fine)
|
||||
|
||||
### Template Files (NEW)
|
||||
|
||||
1. **`app/templates/admin/base.html`** ✅
|
||||
- Master layout with sidebar and header
|
||||
- Script loading in correct order
|
||||
- No partial-loader.js
|
||||
|
||||
2. **`app/templates/admin/login.html`** ✅
|
||||
- Standalone login page
|
||||
- Alpine.js adminLogin() component
|
||||
|
||||
3. **`app/templates/admin/dashboard.html`** ✅
|
||||
- Extends base.html
|
||||
- Alpine.js adminDashboard() component
|
||||
|
||||
4. **`app/templates/partials/header.html`** ✅
|
||||
- Top navigation bar
|
||||
- Updated logout link to /admin/login
|
||||
|
||||
5. **`app/templates/partials/sidebar.html`** ✅
|
||||
- Side navigation menu
|
||||
- Updated all links to /admin/* paths
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Next Steps (Tomorrow)
|
||||
|
||||
### Immediate Priority: Fix Auth Loop
|
||||
|
||||
Apply the simplified approach from the last message:
|
||||
|
||||
1. **Simplify `login.js`:**
|
||||
```javascript
|
||||
// Remove all auth checking on init
|
||||
// Just show login form
|
||||
// Only redirect after successful login
|
||||
```
|
||||
|
||||
2. **Simplify `api-client.js`:**
|
||||
```javascript
|
||||
// Remove handleUnauthorized() redirect logic
|
||||
// Just throw errors, don't redirect
|
||||
// Let server handle redirects
|
||||
```
|
||||
|
||||
3. **Simplify `handler.py`:**
|
||||
```javascript
|
||||
// Only redirect browser HTML requests (text/html accept header)
|
||||
// Don't redirect API calls (application/json)
|
||||
// Don't redirect if already on login page
|
||||
```
|
||||
|
||||
**Test Flow:**
|
||||
1. Navigate to `/admin/login` → should show form (no loops)
|
||||
2. Login → should redirect to `/admin/dashboard`
|
||||
3. Dashboard → should load with sidebar/header
|
||||
4. No console errors, no 404s for partials
|
||||
|
||||
### After Auth Works
|
||||
|
||||
1. **Create remaining page templates:**
|
||||
- `app/templates/admin/vendors.html`
|
||||
- `app/templates/admin/users.html`
|
||||
- `app/templates/admin/vendor-edit.html`
|
||||
|
||||
2. **Test all admin flows:**
|
||||
- Login ✓
|
||||
- Dashboard ✓
|
||||
- Vendors list
|
||||
- Vendor create
|
||||
- Vendor edit
|
||||
- User management
|
||||
|
||||
3. **Cleanup:**
|
||||
- Remove old static HTML files
|
||||
- Remove `app/routes/frontend.py` admin routes completely
|
||||
- Remove `partial-loader.js`
|
||||
|
||||
4. **Migrate vendor portal:**
|
||||
- Same process for `/vendor/*` routes
|
||||
- Create vendor templates
|
||||
- Update vendor JavaScript files
|
||||
|
||||
---
|
||||
|
||||
## 📚 Key Learnings
|
||||
|
||||
### What Worked
|
||||
|
||||
1. ✅ **Server-side template rendering** - Clean, fast, no AJAX for partials
|
||||
2. ✅ **Jinja2 integration** - Easy to set up, works with FastAPI
|
||||
3. ✅ **Route separation** - HTML routes in `pages.py`, API routes separate
|
||||
4. ✅ **Template inheritance** - `base.html` + `{% extends %}` pattern
|
||||
|
||||
### What Caused Issues
|
||||
|
||||
1. ❌ **Multiple redirect handlers** - Client + server both handling 401
|
||||
2. ❌ **Auth checking on login page** - Created loops
|
||||
3. ❌ **Complex error handling** - Too many places making decisions
|
||||
4. ❌ **Path inconsistencies** - Old `/static/admin/` vs new `/admin/`
|
||||
|
||||
### Best Practices Identified
|
||||
|
||||
1. **Single source of truth for redirects** - Choose server OR client, not both
|
||||
2. **Login page should be dumb** - No auth checking, just show form
|
||||
3. **API client should be simple** - Fetch data, throw errors, don't redirect
|
||||
4. **Server handles page-level auth** - FastAPI dependencies + exception handler
|
||||
5. **Clear separation** - HTML pages vs API endpoints
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ Project Structure (Current)
|
||||
|
||||
```
|
||||
project/
|
||||
├── main.py ✅ Updated
|
||||
├── app/
|
||||
│ ├── api/
|
||||
│ │ ├── main.py ✅ Unchanged
|
||||
│ │ └── v1/
|
||||
│ │ └── admin/
|
||||
│ │ ├── __init__.py ✅ Updated
|
||||
│ │ ├── pages.py ✅ NEW
|
||||
│ │ ├── auth.py ✅ Existing (API routes)
|
||||
│ │ ├── vendors.py ✅ Existing (API routes)
|
||||
│ │ └── dashboard.py ✅ Existing (API routes)
|
||||
│ ├── routes/
|
||||
│ │ └── frontend.py ⚠️ Partially disabled
|
||||
│ ├── exceptions/
|
||||
│ │ └── handler.py ⚠️ Needs simplification
|
||||
│ └── templates/ ✅ NEW
|
||||
│ ├── admin/
|
||||
│ │ ├── base.html
|
||||
│ │ ├── login.html
|
||||
│ │ └── dashboard.html
|
||||
│ └── partials/
|
||||
│ ├── header.html
|
||||
│ └── sidebar.html
|
||||
└── static/
|
||||
├── admin/
|
||||
│ ├── js/
|
||||
│ │ ├── login.js ⚠️ Needs simplification
|
||||
│ │ ├── dashboard.js ✅ Updated
|
||||
│ │ ├── vendors.js ✅ Updated
|
||||
│ │ └── vendor-edit.js ✅ Updated
|
||||
│ └── css/
|
||||
│ └── tailwind.output.css ✅ Unchanged
|
||||
└── shared/
|
||||
└── js/
|
||||
├── api-client.js ⚠️ Needs simplification
|
||||
├── utils.js ✅ Working
|
||||
└── icons.js ✅ Working
|
||||
```
|
||||
|
||||
**Legend:**
|
||||
- ✅ = Working correctly
|
||||
- ⚠️ = Needs attention/debugging
|
||||
- ❌ = Not working/causing issues
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Debug Commands
|
||||
|
||||
### Clear localStorage (Browser Console)
|
||||
```javascript
|
||||
localStorage.clear();
|
||||
```
|
||||
|
||||
### Check stored tokens
|
||||
```javascript
|
||||
console.log('admin_token:', localStorage.getItem('admin_token'));
|
||||
console.log('admin_user:', localStorage.getItem('admin_user'));
|
||||
```
|
||||
|
||||
### Test API call manually
|
||||
```javascript
|
||||
fetch('/api/v1/admin/auth/me', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('admin_token')}`
|
||||
}
|
||||
}).then(r => r.json()).then(d => console.log(d));
|
||||
```
|
||||
|
||||
### Check current route
|
||||
```javascript
|
||||
console.log('Current path:', window.location.pathname);
|
||||
console.log('Full URL:', window.location.href);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 Reference: Working Code Snippets
|
||||
|
||||
### Minimal Login.js (To Try Tomorrow)
|
||||
|
||||
```javascript
|
||||
function adminLogin() {
|
||||
return {
|
||||
dark: false,
|
||||
credentials: { username: '', password: '' },
|
||||
loading: false,
|
||||
error: null,
|
||||
success: null,
|
||||
errors: {},
|
||||
|
||||
init() {
|
||||
this.dark = localStorage.getItem('theme') === 'dark';
|
||||
// NO AUTH CHECKING - just show form
|
||||
},
|
||||
|
||||
async handleLogin() {
|
||||
if (!this.validateForm()) return;
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: this.credentials.username,
|
||||
password: this.credentials.password
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.message);
|
||||
|
||||
localStorage.setItem('admin_token', data.access_token);
|
||||
localStorage.setItem('admin_user', JSON.stringify(data.user));
|
||||
|
||||
this.success = 'Login successful!';
|
||||
setTimeout(() => window.location.href = '/admin/dashboard', 500);
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Simplified API Client Request Method
|
||||
|
||||
```javascript
|
||||
async request(endpoint, options = {}) {
|
||||
const url = `${this.baseURL}${endpoint}`;
|
||||
const config = {
|
||||
...options,
|
||||
headers: this.getHeaders(options.headers)
|
||||
};
|
||||
|
||||
const response = await fetch(url, config);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Request failed');
|
||||
}
|
||||
|
||||
return data;
|
||||
// NO REDIRECT LOGIC HERE!
|
||||
}
|
||||
```
|
||||
|
||||
### Simplified Exception Handler
|
||||
|
||||
```python
|
||||
if exc.status_code == 401:
|
||||
accept_header = request.headers.get("accept", "")
|
||||
is_browser = "text/html" in accept_header
|
||||
|
||||
if is_browser and not request.url.path.endswith("/login"):
|
||||
if request.url.path.startswith("/admin"):
|
||||
return RedirectResponse(url="/admin/login", status_code=302)
|
||||
|
||||
# Return JSON for API calls
|
||||
return JSONResponse(status_code=exc.status_code, content=exc.to_dict())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Questions to Answer Tomorrow
|
||||
|
||||
1. Does the simplified auth flow work without loops?
|
||||
2. Can we successfully login and access dashboard?
|
||||
3. Are tokens being sent correctly in API requests?
|
||||
4. Do we need the auth check on login page at all?
|
||||
5. Should we move ALL redirect logic to server-side?
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Success Criteria
|
||||
|
||||
The migration will be considered successful when:
|
||||
|
||||
- [ ] Login page loads without loops
|
||||
- [ ] Login succeeds and redirects to dashboard
|
||||
- [ ] Dashboard displays with sidebar and header
|
||||
- [ ] No 404 errors for partials
|
||||
- [ ] Icons display correctly
|
||||
- [ ] Stats cards load data from API
|
||||
- [ ] Navigation between admin pages works
|
||||
- [ ] Logout works correctly
|
||||
|
||||
---
|
||||
|
||||
**End of Session - October 20, 2025**
|
||||
|
||||
Good work today! We made significant progress on the infrastructure. Tomorrow we'll resolve the auth loop and complete the admin panel migration.
|
||||
284
docs/__temp/__PROJECT_ROADMAP/ROUTE_MIGRATION_SUMMARY.txt
Normal file
284
docs/__temp/__PROJECT_ROADMAP/ROUTE_MIGRATION_SUMMARY.txt
Normal file
@@ -0,0 +1,284 @@
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ ROUTE MIGRATION: Static → Jinja2 Templates ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
📦 WHAT YOU GET
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
3 New Route Files:
|
||||
├─ admin_pages.py ......... Admin HTML routes (12 routes)
|
||||
├─ vendor_pages.py ........ Vendor HTML routes (13 routes)
|
||||
└─ shop_pages.py .......... Shop HTML routes (19 routes)
|
||||
|
||||
1 Migration Guide:
|
||||
└─ MIGRATION_GUIDE.md ..... Complete step-by-step guide
|
||||
|
||||
|
||||
🎯 KEY IMPROVEMENTS
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Before (frontend.py):
|
||||
❌ Static FileResponse
|
||||
❌ No authentication
|
||||
❌ No dynamic content
|
||||
❌ Poor SEO
|
||||
|
||||
After (New files):
|
||||
✅ Jinja2 Templates
|
||||
✅ Server-side auth
|
||||
✅ Dynamic rendering
|
||||
✅ Better SEO
|
||||
|
||||
|
||||
📁 FILE STRUCTURE
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
app/
|
||||
├── api/v1/
|
||||
│ ├── admin/
|
||||
│ │ └── pages.py ←─────────── Admin routes (NEW)
|
||||
│ ├── vendor/
|
||||
│ │ └── pages.py ←─────────── Vendor routes (NEW)
|
||||
│ └── shop/
|
||||
│ └── pages.py ←─────────── Shop routes (NEW)
|
||||
├── routes/
|
||||
│ └── frontend.py ←──────────── DELETE after migration
|
||||
└── templates/
|
||||
├── admin/ ←────────────── Admin HTML templates
|
||||
├── vendor/ ←────────────── Vendor HTML templates
|
||||
└── shop/ ←────────────── Shop HTML templates
|
||||
|
||||
|
||||
🚀 QUICK INSTALL (5 STEPS)
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Step 1: Create directories
|
||||
$ mkdir -p app/api/v1/admin
|
||||
$ mkdir -p app/api/v1/vendor
|
||||
$ mkdir -p app/api/v1/shop
|
||||
|
||||
Step 2: Copy new route files
|
||||
$ cp admin_pages.py app/api/v1/admin/pages.py
|
||||
$ cp vendor_pages.py app/api/v1/vendor/pages.py
|
||||
$ cp shop_pages.py app/api/v1/shop/pages.py
|
||||
|
||||
Step 3: Update main router (see MIGRATION_GUIDE.md)
|
||||
- Include new routers in app/api/v1/router.py
|
||||
|
||||
Step 4: Test routes
|
||||
$ uvicorn app.main:app --reload
|
||||
- Visit http://localhost:8000/admin/dashboard
|
||||
- Visit http://localhost:8000/vendor/ACME/dashboard
|
||||
- Visit http://localhost:8000/shop/
|
||||
|
||||
Step 5: Remove old frontend.py
|
||||
$ mv app/routes/frontend.py app/routes/frontend.py.backup
|
||||
|
||||
|
||||
📋 ROUTE BREAKDOWN
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Admin Routes (admin_pages.py):
|
||||
✅ /admin/ → Redirect to login
|
||||
✅ /admin/login → Login page
|
||||
✅ /admin/dashboard → Dashboard
|
||||
✅ /admin/vendors → Vendor list
|
||||
✅ /admin/vendors/create → Create vendor form
|
||||
✅ /admin/vendors/{code} → Vendor details
|
||||
✅ /admin/vendors/{code}/edit → Edit vendor form
|
||||
✅ /admin/users → User management
|
||||
✅ /admin/imports → Import history
|
||||
✅ /admin/settings → Platform settings
|
||||
|
||||
Vendor Routes (vendor_pages.py):
|
||||
✅ /vendor/{code}/ → Redirect to login
|
||||
✅ /vendor/{code}/login → Login page
|
||||
✅ /vendor/{code}/dashboard → Dashboard
|
||||
✅ /vendor/{code}/products → Product management
|
||||
✅ /vendor/{code}/orders → Order management
|
||||
✅ /vendor/{code}/customers → Customer management
|
||||
✅ /vendor/{code}/inventory → Inventory management
|
||||
✅ /vendor/{code}/marketplace → Marketplace imports
|
||||
✅ /vendor/{code}/team → Team management
|
||||
✅ /vendor/{code}/settings → Vendor settings
|
||||
✅ /vendor/login → Fallback login (query param)
|
||||
✅ /vendor/dashboard → Fallback dashboard
|
||||
|
||||
Shop Routes (shop_pages.py):
|
||||
Public Routes:
|
||||
✅ /shop/ → Homepage
|
||||
✅ /shop/products → Product catalog
|
||||
✅ /shop/products/{id} → Product detail
|
||||
✅ /shop/categories/{slug} → Category page
|
||||
✅ /shop/cart → Shopping cart
|
||||
✅ /shop/checkout → Checkout
|
||||
✅ /shop/search → Search results
|
||||
✅ /shop/account/register → Registration
|
||||
✅ /shop/account/login → Customer login
|
||||
|
||||
Authenticated Routes:
|
||||
✅ /shop/account/dashboard → Account dashboard
|
||||
✅ /shop/account/orders → Order history
|
||||
✅ /shop/account/orders/{id} → Order detail
|
||||
✅ /shop/account/profile → Profile settings
|
||||
✅ /shop/account/addresses → Address management
|
||||
✅ /shop/account/wishlist → Wishlist
|
||||
✅ /shop/account/settings → Account settings
|
||||
|
||||
Static Pages:
|
||||
✅ /shop/about → About us
|
||||
✅ /shop/contact → Contact us
|
||||
✅ /shop/faq → FAQ
|
||||
✅ /shop/privacy → Privacy policy
|
||||
✅ /shop/terms → Terms & conditions
|
||||
|
||||
|
||||
🔑 KEY FEATURES
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
✅ Server-Side Authentication
|
||||
- Admin routes require admin role
|
||||
- Vendor routes require vendor role
|
||||
- Shop account routes require customer role
|
||||
- Public routes accessible to all
|
||||
|
||||
✅ Dynamic Content Rendering
|
||||
- User data passed to templates
|
||||
- Server-side rendering for SEO
|
||||
- Context variables for personalization
|
||||
|
||||
✅ Template Inheritance
|
||||
- Base templates for consistent layout
|
||||
- Block overrides for page-specific content
|
||||
- Shared components
|
||||
|
||||
✅ Proper URL Structure
|
||||
- RESTful URL patterns
|
||||
- Vendor code in URL path
|
||||
- Clear separation of concerns
|
||||
|
||||
|
||||
⚠️ IMPORTANT NOTES
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Route Order Matters!
|
||||
❌ WRONG:
|
||||
@router.get("/vendors/{code}")
|
||||
@router.get("/vendors/create") ← Never matches!
|
||||
|
||||
✅ CORRECT:
|
||||
@router.get("/vendors/create") ← Specific first
|
||||
@router.get("/vendors/{code}") ← Parameterized last
|
||||
|
||||
Authentication Dependencies:
|
||||
- get_current_admin_user → Admin pages
|
||||
- get_current_vendor_user → Vendor pages
|
||||
- get_current_customer_user → Shop account pages
|
||||
|
||||
Template Paths:
|
||||
- Must match directory structure
|
||||
- Use forward slashes: "admin/dashboard.html"
|
||||
- Base path: "app/templates/"
|
||||
|
||||
|
||||
🧪 TESTING CHECKLIST
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Admin Routes:
|
||||
□ /admin/ redirects to /admin/login
|
||||
□ /admin/login shows login form
|
||||
□ /admin/dashboard requires auth
|
||||
□ /admin/vendors shows vendor list
|
||||
□ /admin/vendors/{code}/edit loads correctly
|
||||
|
||||
Vendor Routes:
|
||||
□ /vendor/ACME/ redirects to login
|
||||
□ /vendor/ACME/login shows login form
|
||||
□ /vendor/ACME/dashboard requires auth
|
||||
□ All /admin/* subroutes work
|
||||
|
||||
Shop Routes:
|
||||
□ /shop/ shows products
|
||||
□ /shop/products/{id} shows product
|
||||
□ /shop/cart works without auth
|
||||
□ /shop/account/dashboard requires auth
|
||||
□ /shop/account/orders shows orders
|
||||
|
||||
|
||||
🔧 COMMON ISSUES
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Problem: "Template not found"
|
||||
→ Check templates directory path
|
||||
→ Verify template file exists
|
||||
→ Check forward slashes in path
|
||||
|
||||
Problem: "401 Unauthorized"
|
||||
→ Check auth dependency is defined
|
||||
→ Verify token is being sent
|
||||
→ Check user role matches requirement
|
||||
|
||||
Problem: "Route conflict"
|
||||
→ Reorder routes (specific before parameterized)
|
||||
→ Check for duplicate routes
|
||||
→ Review route registration order
|
||||
|
||||
Problem: "Module not found"
|
||||
→ Check __init__.py exists in directories
|
||||
→ Verify import paths
|
||||
→ Restart server after adding files
|
||||
|
||||
|
||||
📊 COMPARISON TABLE
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Feature │ Old (frontend.py) │ New (pages.py)
|
||||
─────────────────┼───────────────────┼────────────────
|
||||
Authentication │ Client-side │ Server-side ✅
|
||||
Dynamic Content │ None │ Full Python ✅
|
||||
Template Reuse │ Copy-paste │ Inheritance ✅
|
||||
SEO │ Poor │ Good ✅
|
||||
Security │ Client only │ Server validates ✅
|
||||
Maintainability │ Medium │ High ✅
|
||||
Lines of Code │ ~200 │ ~600 (organized) ✅
|
||||
|
||||
|
||||
💡 PRO TIPS
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
1. Test incrementally
|
||||
- Migrate one section at a time
|
||||
- Keep old routes until new ones work
|
||||
|
||||
2. Use template inheritance
|
||||
- Create base.html for each section
|
||||
- Override blocks in child templates
|
||||
|
||||
3. Pass user data in context
|
||||
- Available in templates via {{ user.name }}
|
||||
- No extra API calls needed
|
||||
|
||||
4. Handle both auth and non-auth
|
||||
- Some routes public (login, register)
|
||||
- Some routes require auth (dashboard)
|
||||
|
||||
5. Follow RESTful patterns
|
||||
- /vendors/create not /create-vendor
|
||||
- /vendors/{code}/edit not /edit-vendor/{code}
|
||||
|
||||
|
||||
📖 NEXT STEPS
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
1. Read MIGRATION_GUIDE.md for detailed steps
|
||||
2. Install new route files
|
||||
3. Update main router
|
||||
4. Test each section
|
||||
5. Remove old frontend.py
|
||||
6. Update documentation
|
||||
|
||||
|
||||
══════════════════════════════════════════════════════════════════
|
||||
Migration made easy! 🚀
|
||||
Your app is now using modern Jinja2 templates!
|
||||
══════════════════════════════════════════════════════════════════
|
||||
512
docs/__temp/__PROJECT_ROADMAP/implementation_roadmap.md
Normal file
512
docs/__temp/__PROJECT_ROADMAP/implementation_roadmap.md
Normal file
@@ -0,0 +1,512 @@
|
||||
# Implementation Roadmap
|
||||
## Multi-Tenant Ecommerce Platform - Complete Development Guide
|
||||
|
||||
**Last Updated**: October 11, 2025
|
||||
**Project Status**: Slice 1 In Progress (~75% complete)
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation Structure
|
||||
|
||||
Your complete vertical slice documentation is organized as follows:
|
||||
|
||||
```
|
||||
docs/slices/
|
||||
├── 00_slices_overview.md ← Start here for overview
|
||||
├── 00_implementation_roadmap.md ← This file - your guide
|
||||
├── 01_slice1_admin_vendor_foundation.md
|
||||
├── 02_slice2_marketplace_import.md
|
||||
├── 03_slice3_product_catalog.md
|
||||
├── 04_slice4_customer_shopping.md
|
||||
└── 05_slice5_order_processing.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Quick Start Guide
|
||||
|
||||
### For Current Development (Slice 1)
|
||||
1. ✅ Read `01_slice1_admin_vendor_foundation.md`
|
||||
2. ✅ Review what's marked as complete vs. in-progress
|
||||
3. ⏳ Focus on vendor login and dashboard pages
|
||||
4. ⏳ Complete testing checklist
|
||||
5. ⏳ Deploy to staging
|
||||
|
||||
### For Future Slices
|
||||
1. Complete current slice 100%
|
||||
2. Read next slice documentation
|
||||
3. Set up backend (models, schemas, services, APIs)
|
||||
4. Build frontend (Jinja2 templates + Alpine.js)
|
||||
5. Test thoroughly
|
||||
6. Move to next slice
|
||||
|
||||
---
|
||||
|
||||
## 📊 Current Status Overview
|
||||
|
||||
### Slice 1: Multi-Tenant Foundation (75% Complete)
|
||||
|
||||
#### ✅ Completed
|
||||
- Backend database models (User, Vendor, Role, VendorUser)
|
||||
- Authentication system (JWT, bcrypt)
|
||||
- Admin service layer (vendor creation with owner)
|
||||
- Admin API endpoints (CRUD, dashboard)
|
||||
- Vendor context middleware (subdomain + path detection)
|
||||
- Admin login page (HTML + Alpine.js)
|
||||
- Admin dashboard (HTML + Alpine.js)
|
||||
- Admin vendor creation page (HTML + Alpine.js)
|
||||
|
||||
#### ⏳ In Progress
|
||||
- Vendor login page (frontend)
|
||||
- Vendor dashboard page (frontend)
|
||||
- Testing and debugging
|
||||
- Deployment configuration
|
||||
|
||||
#### 📋 To Do
|
||||
- Complete vendor login/dashboard
|
||||
- Full testing (see Slice 1 checklist)
|
||||
- Documentation updates
|
||||
- Staging deployment
|
||||
|
||||
### Slices 2-5: Not Started
|
||||
|
||||
All future slices have complete documentation ready to implement.
|
||||
|
||||
---
|
||||
|
||||
## 🗓️ Development Timeline
|
||||
|
||||
### Week 1: Slice 1 - Foundation ⏳ CURRENT
|
||||
**Days 1-3**: ✅ Backend complete
|
||||
**Days 4-5**: ⏳ Frontend completion
|
||||
|
||||
**Deliverable**: Admin can create vendors, vendors can log in
|
||||
|
||||
### Week 2: Slice 2 - Marketplace Import
|
||||
**Days 1-3**: Backend (CSV import, Celery tasks, MarketplaceProduct model)
|
||||
**Days 4-5**: Frontend (import UI with Alpine.js, status tracking)
|
||||
|
||||
**Deliverable**: Vendors can import products from Letzshop CSV
|
||||
|
||||
### Week 3: Slice 3 - Product Catalog
|
||||
**Days 1-3**: Backend (Product model, publishing, inventory)
|
||||
**Days 4-5**: Frontend (product management, catalog UI)
|
||||
|
||||
**Deliverable**: Vendors can manage product catalog
|
||||
|
||||
### Week 4: Slice 4 - Customer Shopping
|
||||
**Days 1-3**: Backend (Customer model, Cart, public APIs)
|
||||
**Days 4-5**: Frontend (shop pages, cart with Alpine.js)
|
||||
|
||||
**Deliverable**: Customers can browse and shop
|
||||
|
||||
### Week 5: Slice 5 - Order Processing
|
||||
**Days 1-3**: Backend (Order model, checkout, order management)
|
||||
**Days 4-5**: Frontend (checkout flow, order history)
|
||||
|
||||
**Deliverable**: Complete order workflow, platform ready for production
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Technology Stack
|
||||
|
||||
### Backend
|
||||
- **Framework**: FastAPI (Python 3.11+)
|
||||
- **Database**: PostgreSQL + SQLAlchemy ORM
|
||||
- **Authentication**: JWT tokens + bcrypt
|
||||
- **Background Jobs**: Celery + Redis/RabbitMQ
|
||||
- **API Docs**: Auto-generated OpenAPI/Swagger
|
||||
|
||||
### Frontend
|
||||
- **Templating**: Jinja2 (server-side rendering)
|
||||
- **JavaScript**: Alpine.js v3.x (15KB, CDN-based)
|
||||
- **CSS**: Custom CSS with CSS variables
|
||||
- **AJAX**: Fetch API (vanilla JavaScript)
|
||||
- **No Build Step**: Everything runs directly in browser
|
||||
|
||||
### Why This Stack?
|
||||
- ✅ **Alpine.js**: Lightweight reactivity without build complexity
|
||||
- ✅ **Jinja2**: Server-side rendering for SEO and performance
|
||||
- ✅ **No Build Step**: Faster development, easier deployment
|
||||
- ✅ **FastAPI**: Modern Python, async support, auto-docs
|
||||
- ✅ **PostgreSQL**: Robust, reliable, feature-rich
|
||||
|
||||
---
|
||||
|
||||
## 📋 Implementation Checklist
|
||||
|
||||
Use this checklist to track your progress across all slices:
|
||||
|
||||
### Slice 1: Foundation
|
||||
- [x] Backend models created
|
||||
- [x] Authentication system working
|
||||
- [x] Admin service layer complete
|
||||
- [x] Admin API endpoints working
|
||||
- [x] Vendor context middleware working
|
||||
- [x] Admin login page created
|
||||
- [x] Admin dashboard created
|
||||
- [x] Admin vendor creation page created
|
||||
- [ ] Vendor login page created
|
||||
- [ ] Vendor dashboard page created
|
||||
- [ ] All tests passing
|
||||
- [ ] Deployed to staging
|
||||
|
||||
### Slice 2: Marketplace Import
|
||||
- [ ] MarketplaceProduct model
|
||||
- [ ] MarketplaceImportJob model
|
||||
- [ ] CSV processing service
|
||||
- [ ] Celery tasks configured
|
||||
- [ ] Import API endpoints
|
||||
- [ ] Import UI pages
|
||||
- [ ] Status tracking with Alpine.js
|
||||
- [ ] All tests passing
|
||||
|
||||
### Slice 3: Product Catalog
|
||||
- [ ] Product model complete
|
||||
- [ ] Inventory model complete
|
||||
- [ ] Product service layer
|
||||
- [ ] Publishing logic
|
||||
- [ ] Product API endpoints
|
||||
- [ ] Product management UI
|
||||
- [ ] Catalog browsing
|
||||
- [ ] All tests passing
|
||||
|
||||
### Slice 4: Customer Shopping
|
||||
- [ ] Customer model
|
||||
- [ ] Cart model
|
||||
- [ ] Customer service layer
|
||||
- [ ] Cart service layer
|
||||
- [ ] Public product APIs
|
||||
- [ ] Shop homepage
|
||||
- [ ] Product detail pages
|
||||
- [ ] Shopping cart UI
|
||||
- [ ] Customer registration/login
|
||||
- [ ] All tests passing
|
||||
|
||||
### Slice 5: Order Processing
|
||||
- [ ] Order model
|
||||
- [ ] OrderItem model
|
||||
- [ ] Order service layer
|
||||
- [ ] Checkout logic
|
||||
- [ ] Order API endpoints
|
||||
- [ ] Checkout UI (multi-step)
|
||||
- [ ] Customer order history
|
||||
- [ ] Vendor order management
|
||||
- [ ] Email notifications
|
||||
- [ ] Payment integration (Stripe)
|
||||
- [ ] All tests passing
|
||||
- [ ] Production ready
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Each Slice Must Include
|
||||
|
||||
### Backend Checklist
|
||||
- [ ] Database models defined
|
||||
- [ ] Pydantic schemas created
|
||||
- [ ] Service layer implemented
|
||||
- [ ] API endpoints created
|
||||
- [ ] Exception handling added
|
||||
- [ ] Database migrations applied
|
||||
- [ ] Unit tests written
|
||||
- [ ] Integration tests written
|
||||
|
||||
### Frontend Checklist
|
||||
- [ ] Jinja2 templates created
|
||||
- [ ] Alpine.js components implemented
|
||||
- [ ] CSS styling applied
|
||||
- [ ] API integration working
|
||||
- [ ] Loading states added
|
||||
- [ ] Error handling added
|
||||
- [ ] Mobile responsive
|
||||
- [ ] Browser tested (Chrome, Firefox, Safari)
|
||||
|
||||
### Documentation Checklist
|
||||
- [ ] Slice documentation updated
|
||||
- [ ] API endpoints documented
|
||||
- [ ] Frontend components documented
|
||||
- [ ] Testing checklist completed
|
||||
- [ ] Known issues documented
|
||||
- [ ] Next steps identified
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Development Workflow
|
||||
|
||||
### Starting a New Slice
|
||||
|
||||
1. **Read Documentation**
|
||||
```bash
|
||||
# Open the slice markdown file
|
||||
code docs/slices/0X_sliceX_name.md
|
||||
```
|
||||
|
||||
2. **Set Up Backend**
|
||||
```bash
|
||||
# Create database models
|
||||
# Create Pydantic schema
|
||||
# Implement service layer
|
||||
# Create API endpoints
|
||||
# Write tests
|
||||
```
|
||||
|
||||
3. **Set Up Frontend**
|
||||
```bash
|
||||
# Create Jinja2 templates
|
||||
# Add Alpine.js components
|
||||
# Style with CSS
|
||||
# Test in browser
|
||||
```
|
||||
|
||||
4. **Test Thoroughly**
|
||||
```bash
|
||||
# Run backend tests
|
||||
pytest tests/
|
||||
|
||||
# Manual testing
|
||||
# Use testing checklist in slice docs
|
||||
```
|
||||
|
||||
5. **Deploy & Demo**
|
||||
```bash
|
||||
# Deploy to staging
|
||||
# Demo to stakeholders
|
||||
# Gather feedback
|
||||
```
|
||||
|
||||
### Daily Development Flow
|
||||
|
||||
**Morning**
|
||||
- Review slice documentation
|
||||
- Identify today's goals (backend or frontend)
|
||||
- Check testing checklist
|
||||
|
||||
**During Development**
|
||||
- Follow code patterns from slice docs
|
||||
- Use Alpine.js examples provided
|
||||
- Keep vendor isolation in mind
|
||||
- Test incrementally
|
||||
|
||||
**End of Day**
|
||||
- Update slice documentation with progress
|
||||
- Mark completed items in checklist
|
||||
- Note any blockers or issues
|
||||
- Commit code with meaningful messages
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Alpine.js Patterns
|
||||
|
||||
### Basic Component Pattern
|
||||
```javascript
|
||||
function myComponent() {
|
||||
return {
|
||||
// State
|
||||
data: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
// Lifecycle
|
||||
init() {
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
// Methods
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
try {
|
||||
this.data = await apiClient.get('/api/endpoint');
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Template Usage
|
||||
```html
|
||||
<div x-data="myComponent()" x-init="init()">
|
||||
<div x-show="loading">Loading...</div>
|
||||
<div x-show="error" x-text="error"></div>
|
||||
<div x-show="!loading && !error">
|
||||
<template x-for="item in data" :key="item.id">
|
||||
<div x-text="item.name"></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Common Directives
|
||||
- `x-data` - Component state
|
||||
- `x-init` - Initialization
|
||||
- `x-show` - Toggle visibility
|
||||
- `x-if` - Conditional rendering
|
||||
- `x-for` - Loop through arrays
|
||||
- `x-model` - Two-way binding
|
||||
- `@click` - Event handling
|
||||
- `:class` - Dynamic classes
|
||||
- `x-text` - Text content
|
||||
- `x-html` - HTML content
|
||||
|
||||
---
|
||||
|
||||
## 📚 Key Resources
|
||||
|
||||
### Documentation Files
|
||||
- `00_slices_overview.md` - Complete overview
|
||||
- `01_slice1_admin_vendor_foundation.md` - Current work
|
||||
- `../quick_start_guide.md` - Setup guide
|
||||
- `../css_structure_guide.txt` - CSS organization
|
||||
- `../css_quick_reference.txt` - CSS usage
|
||||
- `../12.project_readme_final.md` - Complete README
|
||||
|
||||
### External Resources
|
||||
- [Alpine.js Documentation](https://alpinejs.dev/)
|
||||
- [FastAPI Documentation](https://fastapi.tiangolo.com/)
|
||||
- [Jinja2 Documentation](https://jinja.palletsprojects.com/)
|
||||
- [SQLAlchemy Documentation](https://docs.sqlalchemy.org/)
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Common Pitfalls to Avoid
|
||||
|
||||
### Backend
|
||||
- ❌ Forgetting vendor isolation in queries
|
||||
- ❌ Not validating vendor_id in API endpoints
|
||||
- ❌ Skipping database indexes
|
||||
- ❌ Not handling edge cases
|
||||
- ❌ Missing error handling
|
||||
|
||||
### Frontend
|
||||
- ❌ Not handling loading states
|
||||
- ❌ Not displaying error messages
|
||||
- ❌ Forgetting mobile responsiveness
|
||||
- ❌ Not testing in multiple browsers
|
||||
- ❌ Mixing vendor contexts
|
||||
|
||||
### General
|
||||
- ❌ Skipping tests
|
||||
- ❌ Not updating documentation
|
||||
- ❌ Moving to next slice before completing current
|
||||
- ❌ Not following naming conventions
|
||||
- ❌ Committing without testing
|
||||
|
||||
---
|
||||
|
||||
## ✅ Quality Gates
|
||||
|
||||
Before moving to the next slice, ensure:
|
||||
|
||||
1. **All Features Complete**
|
||||
- All user stories implemented
|
||||
- All acceptance criteria met
|
||||
- All API endpoints working
|
||||
|
||||
2. **All Tests Pass**
|
||||
- Backend unit tests
|
||||
- Backend integration tests
|
||||
- Frontend manual testing
|
||||
- Security testing (vendor isolation)
|
||||
|
||||
3. **Documentation Updated**
|
||||
- Slice documentation current
|
||||
- API docs updated
|
||||
- Testing checklist completed
|
||||
|
||||
4. **Code Quality**
|
||||
- Follows naming conventions
|
||||
- No console errors
|
||||
- No security vulnerabilities
|
||||
- Performance acceptable
|
||||
|
||||
5. **Stakeholder Approval**
|
||||
- Demo completed
|
||||
- Feedback incorporated
|
||||
- Sign-off received
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Success Metrics
|
||||
|
||||
### After Slice 1
|
||||
- ✅ Admin can create vendors
|
||||
- ✅ Vendors can log in
|
||||
- ✅ Vendor isolation works
|
||||
- ✅ Context detection works
|
||||
|
||||
### After Slice 2
|
||||
- ✅ Vendors can import CSVs
|
||||
- ✅ Background processing works
|
||||
- ✅ Import tracking functional
|
||||
|
||||
### After Slice 3
|
||||
- ✅ Products published to catalog
|
||||
- ✅ Inventory management working
|
||||
- ✅ Product customization enabled
|
||||
|
||||
### After Slice 4
|
||||
- ✅ Customers can browse products
|
||||
- ✅ Shopping cart functional
|
||||
- ✅ Customer accounts working
|
||||
|
||||
### After Slice 5
|
||||
- ✅ Complete checkout workflow
|
||||
- ✅ Order management operational
|
||||
- ✅ **Platform production-ready!**
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Ready to Start?
|
||||
|
||||
### Current Focus: Complete Slice 1
|
||||
|
||||
**Your immediate next steps:**
|
||||
|
||||
1. ✅ Read `01_slice1_admin_vendor_foundation.md`
|
||||
2. ⏳ Complete vendor login page (`templates/vendor/login.html`)
|
||||
3. ⏳ Complete vendor dashboard (`templates/vendor/dashboard.html`)
|
||||
4. ⏳ Test complete admin → vendor flow
|
||||
5. ⏳ Check all items in Slice 1 testing checklist
|
||||
6. ⏳ Deploy to staging
|
||||
7. ⏳ Demo to stakeholders
|
||||
8. ✅ Move to Slice 2
|
||||
|
||||
### Need Help?
|
||||
|
||||
- Check the slice documentation for detailed implementation
|
||||
- Review Alpine.js examples in the docs
|
||||
- Look at CSS guides for styling
|
||||
- Test frequently and incrementally
|
||||
- Update documentation as you progress
|
||||
|
||||
---
|
||||
|
||||
## 💡 Pro Tips
|
||||
|
||||
1. **Work Incrementally**: Complete one component at a time
|
||||
2. **Test Continuously**: Don't wait until the end to test
|
||||
3. **Follow Patterns**: Use the examples in slice documentation
|
||||
4. **Document as You Go**: Update docs while code is fresh
|
||||
5. **Ask for Reviews**: Get feedback early and often
|
||||
6. **Celebrate Progress**: Each completed slice is a milestone!
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
If you need assistance:
|
||||
- Review the slice-specific documentation
|
||||
- Check the testing checklists
|
||||
- Look at the example code provided
|
||||
- Refer to the technology stack documentation
|
||||
|
||||
---
|
||||
|
||||
**Ready to build an amazing multi-tenant ecommerce platform?**
|
||||
|
||||
**Start with**: `01_slice1_admin_vendor_foundation.md`
|
||||
|
||||
**You've got this!** 🚀
|
||||
1070
docs/__temp/__PROJECT_ROADMAP/slice1_doc.md
Normal file
1070
docs/__temp/__PROJECT_ROADMAP/slice1_doc.md
Normal file
File diff suppressed because it is too large
Load Diff
808
docs/__temp/__PROJECT_ROADMAP/slice2_doc.md
Normal file
808
docs/__temp/__PROJECT_ROADMAP/slice2_doc.md
Normal file
@@ -0,0 +1,808 @@
|
||||
# Slice 2: Marketplace Product Import
|
||||
## Vendor Imports Products from Letzshop
|
||||
|
||||
**Status**: 📋 NOT STARTED
|
||||
**Timeline**: Week 2 (5 days)
|
||||
**Prerequisites**: Slice 1 complete
|
||||
|
||||
## 🎯 Slice Objectives
|
||||
|
||||
Enable vendors to import product catalogs from Letzshop marketplace via CSV files.
|
||||
|
||||
### User Stories
|
||||
- As a Vendor Owner, I can configure my Letzshop CSV URL
|
||||
- As a Vendor Owner, I can trigger product imports from Letzshop
|
||||
- As a Vendor Owner, I can view import job status and history
|
||||
- The system processes CSV data in the background
|
||||
- As a Vendor Owner, I can see real-time import progress
|
||||
|
||||
### Success Criteria
|
||||
- [ ] Vendor can configure Letzshop CSV URL (FR, EN, DE)
|
||||
- [ ] Vendor can trigger import jobs manually
|
||||
- [ ] System downloads and processes CSV files
|
||||
- [ ] Import status updates in real-time (Alpine.js)
|
||||
- [ ] Import history is properly tracked
|
||||
- [ ] Error handling for failed imports
|
||||
- [ ] Products stored in staging area (MarketplaceProduct table)
|
||||
- [ ] Large CSV files process without timeout
|
||||
|
||||
## 📋 Backend Implementation
|
||||
|
||||
### Database Models
|
||||
|
||||
#### MarketplaceProduct Model (`models/database/marketplace_product.py`)
|
||||
|
||||
```python
|
||||
class MarketplaceProduct(Base, TimestampMixin):
|
||||
"""
|
||||
Staging table for imported marketplace products
|
||||
Products stay here until vendor publishes them to catalog
|
||||
"""
|
||||
__tablename__ = "marketplace_products"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
|
||||
import_job_id = Column(Integer, ForeignKey("marketplace_import_jobs.id"))
|
||||
|
||||
# External identifiers
|
||||
external_sku = Column(String, nullable=False, index=True)
|
||||
marketplace = Column(String, default="letzshop") # Future: other marketplaces
|
||||
|
||||
# Product information (from CSV)
|
||||
title = Column(String, nullable=False)
|
||||
description = Column(Text)
|
||||
price = Column(Numeric(10, 2))
|
||||
currency = Column(String(3), default="EUR")
|
||||
|
||||
# Categories and attributes
|
||||
category = Column(String)
|
||||
brand = Column(String)
|
||||
attributes = Column(JSON, default=dict) # Store all CSV columns
|
||||
|
||||
# Images
|
||||
image_urls = Column(JSON, default=list) # List of image URLs
|
||||
|
||||
# Inventory
|
||||
stock_quantity = Column(Integer)
|
||||
is_in_stock = Column(Boolean, default=True)
|
||||
|
||||
# Status
|
||||
is_selected = Column(Boolean, default=False) # Ready to publish?
|
||||
is_published = Column(Boolean, default=False) # Already in catalog?
|
||||
published_product_id = Column(Integer, ForeignKey("products.id"), nullable=True)
|
||||
|
||||
# Metadata
|
||||
language = Column(String(2)) # 'fr', 'en', 'de'
|
||||
raw_data = Column(JSON) # Store complete CSV row
|
||||
|
||||
# Relationships
|
||||
vendor = relationship("Vendor", back_populates="marketplace_products")
|
||||
import_job = relationship("MarketplaceImportJob", back_populates="products")
|
||||
published_product = relationship("Product", back_populates="marketplace_source")
|
||||
|
||||
# Indexes
|
||||
__table_args__ = (
|
||||
Index('ix_marketplace_vendor_sku', 'vendor_id', 'external_sku'),
|
||||
Index('ix_marketplace_selected', 'vendor_id', 'is_selected'),
|
||||
)
|
||||
```
|
||||
|
||||
#### MarketplaceImportJob Model (`models/database/marketplace.py`)
|
||||
|
||||
```python
|
||||
class MarketplaceImportJob(Base, TimestampMixin):
|
||||
"""Track CSV import jobs"""
|
||||
__tablename__ = "marketplace_import_jobs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
|
||||
|
||||
# Job details
|
||||
marketplace = Column(String, default="letzshop")
|
||||
csv_url = Column(String, nullable=False)
|
||||
language = Column(String(2)) # 'fr', 'en', 'de'
|
||||
|
||||
# Status tracking
|
||||
status = Column(
|
||||
String,
|
||||
default="pending"
|
||||
) # pending, processing, completed, failed
|
||||
|
||||
# Progress
|
||||
total_rows = Column(Integer, default=0)
|
||||
processed_rows = Column(Integer, default=0)
|
||||
imported_count = Column(Integer, default=0)
|
||||
updated_count = Column(Integer, default=0)
|
||||
error_count = Column(Integer, default=0)
|
||||
|
||||
# Timing
|
||||
started_at = Column(DateTime, nullable=True)
|
||||
completed_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Error handling
|
||||
error_message = Column(Text, nullable=True)
|
||||
error_details = Column(JSON, nullable=True)
|
||||
|
||||
# Relationships
|
||||
vendor = relationship("Vendor", back_populates="import_jobs")
|
||||
products = relationship("MarketplaceProduct", back_populates="import_job")
|
||||
```
|
||||
|
||||
### Pydantic Schemas
|
||||
|
||||
#### Import Schemas (`models/schema/marketplace.py`)
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
class MarketplaceImportCreate(BaseModel):
|
||||
"""Create new import job"""
|
||||
csv_url: HttpUrl
|
||||
language: str = Field(..., regex="^(fr|en|de)$")
|
||||
marketplace: str = "letzshop"
|
||||
|
||||
class MarketplaceImportJobResponse(BaseModel):
|
||||
"""Import job details"""
|
||||
id: int
|
||||
vendor_id: int
|
||||
marketplace: str
|
||||
csv_url: str
|
||||
language: str
|
||||
status: str
|
||||
total_rows: int
|
||||
processed_rows: int
|
||||
imported_count: int
|
||||
updated_count: int
|
||||
error_count: int
|
||||
started_at: Optional[datetime]
|
||||
completed_at: Optional[datetime]
|
||||
error_message: Optional[str]
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class MarketplaceProductResponse(BaseModel):
|
||||
"""Marketplace product in staging"""
|
||||
id: int
|
||||
vendor_id: int
|
||||
external_sku: str
|
||||
title: str
|
||||
description: Optional[str]
|
||||
price: float
|
||||
currency: str
|
||||
category: Optional[str]
|
||||
brand: Optional[str]
|
||||
stock_quantity: Optional[int]
|
||||
is_in_stock: bool
|
||||
is_selected: bool
|
||||
is_published: bool
|
||||
image_urls: List[str]
|
||||
language: str
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
```
|
||||
|
||||
### Service Layer
|
||||
|
||||
#### Marketplace Service (`app/services/marketplace_service.py`)
|
||||
|
||||
```python
|
||||
from typing import List, Dict, Any
|
||||
import csv
|
||||
import requests
|
||||
from io import StringIO
|
||||
from sqlalchemy.orm import Session
|
||||
from models.database.marketplace_product import MarketplaceProduct
|
||||
from models.database.marketplace import MarketplaceImportJob
|
||||
|
||||
class MarketplaceService:
|
||||
"""Handle marketplace product imports"""
|
||||
|
||||
async def create_import_job(
|
||||
self,
|
||||
vendor_id: int,
|
||||
csv_url: str,
|
||||
language: str,
|
||||
db: Session
|
||||
) -> MarketplaceImportJob:
|
||||
"""Create new import job and start processing"""
|
||||
|
||||
# Create job record
|
||||
job = MarketplaceImportJob(
|
||||
vendor_id=vendor_id,
|
||||
csv_url=csv_url,
|
||||
language=language,
|
||||
marketplace="letzshop",
|
||||
status="pending"
|
||||
)
|
||||
db.add(job)
|
||||
db.commit()
|
||||
db.refresh(job)
|
||||
|
||||
# Trigger background processing
|
||||
from tasks.marketplace_import import process_csv_import
|
||||
process_csv_import.delay(job.id)
|
||||
|
||||
return job
|
||||
|
||||
def process_csv_import(self, job_id: int, db: Session):
|
||||
"""
|
||||
Process CSV import (called by Celery task)
|
||||
This is a long-running operation
|
||||
"""
|
||||
job = db.query(MarketplaceImportJob).get(job_id)
|
||||
if not job:
|
||||
return
|
||||
|
||||
try:
|
||||
# Update status
|
||||
job.status = "processing"
|
||||
job.started_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
# Download CSV
|
||||
response = requests.get(job.csv_url, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
# Parse CSV
|
||||
csv_content = StringIO(response.text)
|
||||
reader = csv.DictReader(csv_content)
|
||||
|
||||
# Count total rows
|
||||
rows = list(reader)
|
||||
job.total_rows = len(rows)
|
||||
db.commit()
|
||||
|
||||
# Process each row
|
||||
for idx, row in enumerate(rows):
|
||||
try:
|
||||
self._process_csv_row(job, row, db)
|
||||
job.processed_rows = idx + 1
|
||||
|
||||
# Commit every 100 rows
|
||||
if idx % 100 == 0:
|
||||
db.commit()
|
||||
|
||||
except Exception as e:
|
||||
job.error_count += 1
|
||||
# Log error but continue
|
||||
|
||||
# Final commit
|
||||
job.status = "completed"
|
||||
job.completed_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
except Exception as e:
|
||||
job.status = "failed"
|
||||
job.error_message = str(e)
|
||||
job.completed_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
def _process_csv_row(
|
||||
self,
|
||||
job: MarketplaceImportJob,
|
||||
row: Dict[str, Any],
|
||||
db: Session
|
||||
):
|
||||
"""Process single CSV row"""
|
||||
|
||||
# Extract fields from CSV
|
||||
external_sku = row.get('SKU') or row.get('sku')
|
||||
if not external_sku:
|
||||
raise ValueError("Missing SKU in CSV row")
|
||||
|
||||
# Check if product already exists
|
||||
existing = db.query(MarketplaceProduct).filter(
|
||||
MarketplaceProduct.vendor_id == job.vendor_id,
|
||||
MarketplaceProduct.external_sku == external_sku
|
||||
).first()
|
||||
|
||||
# Parse image URLs
|
||||
image_urls = []
|
||||
for i in range(1, 6): # Support up to 5 images
|
||||
img_url = row.get(f'Image{i}') or row.get(f'image_{i}')
|
||||
if img_url:
|
||||
image_urls.append(img_url)
|
||||
|
||||
if existing:
|
||||
# Update existing product
|
||||
existing.title = row.get('Title') or row.get('title')
|
||||
existing.description = row.get('Description')
|
||||
existing.price = float(row.get('Price', 0))
|
||||
existing.stock_quantity = int(row.get('Stock', 0))
|
||||
existing.is_in_stock = existing.stock_quantity > 0
|
||||
existing.category = row.get('Category')
|
||||
existing.brand = row.get('Brand')
|
||||
existing.image_urls = image_urls
|
||||
existing.raw_data = row
|
||||
existing.import_job_id = job.id
|
||||
|
||||
job.updated_count += 1
|
||||
else:
|
||||
# Create new product
|
||||
product = MarketplaceProduct(
|
||||
vendor_id=job.vendor_id,
|
||||
import_job_id=job.id,
|
||||
external_sku=external_sku,
|
||||
marketplace="letzshop",
|
||||
title=row.get('Title') or row.get('title'),
|
||||
description=row.get('Description'),
|
||||
price=float(row.get('Price', 0)),
|
||||
currency="EUR",
|
||||
category=row.get('Category'),
|
||||
brand=row.get('Brand'),
|
||||
stock_quantity=int(row.get('Stock', 0)),
|
||||
is_in_stock=int(row.get('Stock', 0)) > 0,
|
||||
image_urls=image_urls,
|
||||
language=job.language,
|
||||
raw_data=row,
|
||||
is_selected=False,
|
||||
is_published=False
|
||||
)
|
||||
db.add(product)
|
||||
job.imported_count += 1
|
||||
|
||||
def get_import_jobs(
|
||||
self,
|
||||
vendor_id: int,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 20
|
||||
) -> List[MarketplaceImportJob]:
|
||||
"""Get import job history for vendor"""
|
||||
return db.query(MarketplaceImportJob).filter(
|
||||
MarketplaceImportJob.vendor_id == vendor_id
|
||||
).order_by(
|
||||
MarketplaceImportJob.created_at.desc()
|
||||
).offset(skip).limit(limit).all()
|
||||
|
||||
def get_marketplace_products(
|
||||
self,
|
||||
vendor_id: int,
|
||||
db: Session,
|
||||
import_job_id: Optional[int] = None,
|
||||
is_selected: Optional[bool] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
) -> List[MarketplaceProduct]:
|
||||
"""Get marketplace products in staging"""
|
||||
query = db.query(MarketplaceProduct).filter(
|
||||
MarketplaceProduct.vendor_id == vendor_id,
|
||||
MarketplaceProduct.is_published == False # Only unpublished
|
||||
)
|
||||
|
||||
if import_job_id:
|
||||
query = query.filter(MarketplaceProduct.import_job_id == import_job_id)
|
||||
|
||||
if is_selected is not None:
|
||||
query = query.filter(MarketplaceProduct.is_selected == is_selected)
|
||||
|
||||
return query.order_by(
|
||||
MarketplaceProduct.created_at.desc()
|
||||
).offset(skip).limit(limit).all()
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
|
||||
#### Marketplace Endpoints (`app/api/v1/vendor/marketplace.py`)
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/import", response_model=MarketplaceImportJobResponse)
|
||||
async def trigger_import(
|
||||
import_data: MarketplaceImportCreate,
|
||||
current_user: User = Depends(get_current_vendor_user),
|
||||
vendor: Vendor = Depends(get_current_vendor),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Trigger CSV import from marketplace"""
|
||||
service = MarketplaceService()
|
||||
job = await service.create_import_job(
|
||||
vendor_id=vendor.id,
|
||||
csv_url=str(import_data.csv_url),
|
||||
language=import_data.language,
|
||||
db=db
|
||||
)
|
||||
return job
|
||||
|
||||
@router.get("/jobs", response_model=List[MarketplaceImportJobResponse])
|
||||
async def get_import_jobs(
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
current_user: User = Depends(get_current_vendor_user),
|
||||
vendor: Vendor = Depends(get_current_vendor),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get import job history"""
|
||||
service = MarketplaceService()
|
||||
jobs = service.get_import_jobs(vendor.id, db, skip, limit)
|
||||
return jobs
|
||||
|
||||
@router.get("/jobs/{job_id}", response_model=MarketplaceImportJobResponse)
|
||||
async def get_import_job(
|
||||
job_id: int,
|
||||
current_user: User = Depends(get_current_vendor_user),
|
||||
vendor: Vendor = Depends(get_current_vendor),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get specific import job status"""
|
||||
job = db.query(MarketplaceImportJob).filter(
|
||||
MarketplaceImportJob.id == job_id,
|
||||
MarketplaceImportJob.vendor_id == vendor.id
|
||||
).first()
|
||||
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="Import job not found")
|
||||
|
||||
return job
|
||||
|
||||
@router.get("/products", response_model=List[MarketplaceProductResponse])
|
||||
async def get_marketplace_products(
|
||||
import_job_id: Optional[int] = None,
|
||||
is_selected: Optional[bool] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
current_user: User = Depends(get_current_vendor_user),
|
||||
vendor: Vendor = Depends(get_current_vendor),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get products in marketplace staging area"""
|
||||
service = MarketplaceService()
|
||||
products = service.get_marketplace_products(
|
||||
vendor.id, db, import_job_id, is_selected, skip, limit
|
||||
)
|
||||
return products
|
||||
```
|
||||
|
||||
### Background Tasks
|
||||
|
||||
#### Celery Task (`tasks/marketplace_import.py`)
|
||||
|
||||
```python
|
||||
from celery import shared_task
|
||||
from app.core.database import SessionLocal
|
||||
from app.services.marketplace_service import MarketplaceService
|
||||
|
||||
@shared_task(bind=True, max_retries=3)
|
||||
def process_csv_import(self, job_id: int):
|
||||
"""
|
||||
Process CSV import in background
|
||||
This can take several minutes for large files
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
service = MarketplaceService()
|
||||
service.process_csv_import(job_id, db)
|
||||
except Exception as e:
|
||||
# Retry on failure
|
||||
raise self.retry(exc=e, countdown=60)
|
||||
finally:
|
||||
db.close()
|
||||
```
|
||||
|
||||
## 🎨 Frontend Implementation
|
||||
|
||||
### Templates
|
||||
|
||||
#### Import Dashboard (`templates/vendor/marketplace/imports.html`)
|
||||
|
||||
```html
|
||||
{% extends "vendor/base_vendor.html" %}
|
||||
|
||||
{% block title %}Product Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="marketplaceImport()" x-init="loadJobs()">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h1>Marketplace Import</h1>
|
||||
<button @click="showImportModal = true" class="btn btn-primary">
|
||||
New Import
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Import Configuration Card -->
|
||||
<div class="card mb-3">
|
||||
<h3>Letzshop CSV URLs</h3>
|
||||
<div class="config-grid">
|
||||
<div>
|
||||
<label>French (FR)</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="vendor.letzshop_csv_url_fr"
|
||||
class="form-control"
|
||||
readonly
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label>English (EN)</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="vendor.letzshop_csv_url_en"
|
||||
class="form-control"
|
||||
readonly
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label>German (DE)</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="vendor.letzshop_csv_url_de"
|
||||
class="form-control"
|
||||
readonly
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted mt-2">
|
||||
Configure these URLs in vendor settings
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Import Jobs List -->
|
||||
<div class="card">
|
||||
<h3>Import History</h3>
|
||||
|
||||
<template x-if="jobs.length === 0 && !loading">
|
||||
<p class="text-muted">No imports yet. Start your first import!</p>
|
||||
</template>
|
||||
|
||||
<template x-if="jobs.length > 0">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Language</th>
|
||||
<th>Status</th>
|
||||
<th>Progress</th>
|
||||
<th>Results</th>
|
||||
<th>Started</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="job in jobs" :key="job.id">
|
||||
<tr>
|
||||
<td><strong x-text="`#${job.id}`"></strong></td>
|
||||
<td x-text="job.language.toUpperCase()"></td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
:class="{
|
||||
'badge-warning': job.status === 'pending' || job.status === 'processing',
|
||||
'badge-success': job.status === 'completed',
|
||||
'badge-danger': job.status === 'failed'
|
||||
}"
|
||||
x-text="job.status"
|
||||
></span>
|
||||
</td>
|
||||
<td>
|
||||
<template x-if="job.status === 'processing'">
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-fill"
|
||||
:style="`width: ${(job.processed_rows / job.total_rows * 100)}%`"
|
||||
></div>
|
||||
</div>
|
||||
<small x-text="`${job.processed_rows} / ${job.total_rows}`"></small>
|
||||
</template>
|
||||
<template x-if="job.status === 'completed'">
|
||||
<span x-text="`${job.total_rows} rows`"></span>
|
||||
</template>
|
||||
</td>
|
||||
<td>
|
||||
<template x-if="job.status === 'completed'">
|
||||
<div class="text-sm">
|
||||
<div>✓ <span x-text="job.imported_count"></span> imported</div>
|
||||
<div>↻ <span x-text="job.updated_count"></span> updated</div>
|
||||
<template x-if="job.error_count > 0">
|
||||
<div class="text-danger">✗ <span x-text="job.error_count"></span> errors</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</td>
|
||||
<td x-text="formatDate(job.started_at)"></td>
|
||||
<td>
|
||||
<button
|
||||
@click="viewProducts(job.id)"
|
||||
class="btn btn-sm"
|
||||
:disabled="job.status !== 'completed'"
|
||||
>
|
||||
View Products
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- New Import Modal -->
|
||||
<div x-show="showImportModal" class="modal-overlay" @click.self="showImportModal = false">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>New Import</h3>
|
||||
<button @click="showImportModal = false" class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form @submit.prevent="triggerImport()">
|
||||
<div class="form-group">
|
||||
<label>Language</label>
|
||||
<select x-model="newImport.language" class="form-control" required>
|
||||
<option value="">Select language</option>
|
||||
<option value="fr">French (FR)</option>
|
||||
<option value="en">English (EN)</option>
|
||||
<option value="de">German (DE)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>CSV URL</label>
|
||||
<input
|
||||
type="url"
|
||||
x-model="newImport.csv_url"
|
||||
class="form-control"
|
||||
placeholder="https://..."
|
||||
required
|
||||
>
|
||||
<div class="form-help">
|
||||
Or use configured URL:
|
||||
<button
|
||||
type="button"
|
||||
@click="newImport.csv_url = vendor.letzshop_csv_url_fr"
|
||||
class="btn-link"
|
||||
x-show="newImport.language === 'fr'"
|
||||
>
|
||||
Use FR URL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" @click="showImportModal = false" class="btn btn-secondary">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="importing">
|
||||
<span x-show="!importing">Start Import</span>
|
||||
<span x-show="importing" class="loading-spinner"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.vendorData = {{ vendor|tojson }};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function marketplaceImport() {
|
||||
return {
|
||||
vendor: window.vendorData,
|
||||
jobs: [],
|
||||
loading: false,
|
||||
importing: false,
|
||||
showImportModal: false,
|
||||
newImport: {
|
||||
language: '',
|
||||
csv_url: ''
|
||||
},
|
||||
|
||||
async loadJobs() {
|
||||
this.loading = true;
|
||||
try {
|
||||
this.jobs = await apiClient.get('/api/v1/vendor/marketplace/jobs');
|
||||
|
||||
// Poll for active jobs
|
||||
const activeJobs = this.jobs.filter(j =>
|
||||
j.status === 'pending' || j.status === 'processing'
|
||||
);
|
||||
|
||||
if (activeJobs.length > 0) {
|
||||
setTimeout(() => this.loadJobs(), 3000); // Poll every 3 seconds
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Failed to load imports', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async triggerImport() {
|
||||
this.importing = true;
|
||||
try {
|
||||
const job = await apiClient.post('/api/v1/vendor/marketplace/import', {
|
||||
csv_url: this.newImport.csv_url,
|
||||
language: this.newImport.language
|
||||
});
|
||||
|
||||
this.jobs.unshift(job);
|
||||
this.showImportModal = false;
|
||||
this.newImport = { language: '', csv_url: '' };
|
||||
|
||||
showNotification('Import started successfully', 'success');
|
||||
|
||||
// Start polling
|
||||
setTimeout(() => this.loadJobs(), 3000);
|
||||
} catch (error) {
|
||||
showNotification(error.message || 'Import failed', 'error');
|
||||
} finally {
|
||||
this.importing = false;
|
||||
}
|
||||
},
|
||||
|
||||
viewProducts(jobId) {
|
||||
window.location.href = `/vendor/marketplace/products?job_id=${jobId}`;
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
return new Date(dateString).toLocaleString();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
## ✅ Testing Checklist
|
||||
|
||||
### Backend Tests
|
||||
- [ ] CSV download works with valid URL
|
||||
- [ ] CSV parsing handles various formats
|
||||
- [ ] Products created in staging table
|
||||
- [ ] Duplicate SKUs are updated, not duplicated
|
||||
- [ ] Import job status updates correctly
|
||||
- [ ] Progress tracking is accurate
|
||||
- [ ] Error handling works for invalid CSV
|
||||
- [ ] Large CSV files (10,000+ rows) process successfully
|
||||
- [ ] Celery tasks execute correctly
|
||||
|
||||
### Frontend Tests
|
||||
- [ ] Import page loads correctly
|
||||
- [ ] New import modal works
|
||||
- [ ] Import jobs display in table
|
||||
- [ ] Real-time progress updates (polling)
|
||||
- [ ] Completed imports show results
|
||||
- [ ] Can view products from import
|
||||
- [ ] Error states display correctly
|
||||
- [ ] Loading states work correctly
|
||||
|
||||
### Integration Tests
|
||||
- [ ] Complete import workflow works end-to-end
|
||||
- [ ] Vendor isolation maintained
|
||||
- [ ] API endpoints require authentication
|
||||
- [ ] Vendor can only see their own imports
|
||||
|
||||
## 🚀 Deployment Checklist
|
||||
|
||||
- [ ] Celery worker running
|
||||
- [ ] Redis/RabbitMQ configured for Celery
|
||||
- [ ] Database migrations applied
|
||||
- [ ] CSV download timeout configured
|
||||
- [ ] Error logging configured
|
||||
- [ ] Background task monitoring set up
|
||||
|
||||
## ➡️ Next Steps
|
||||
|
||||
After completing Slice 2, move to **Slice 3: Product Catalog Management** to enable vendors to publish imported products to their catalog.
|
||||
|
||||
---
|
||||
|
||||
**Slice 2 Status**: 📋 Not Started
|
||||
**Dependencies**: Slice 1 must be complete
|
||||
**Estimated Duration**: 5 days
|
||||
624
docs/__temp/__PROJECT_ROADMAP/slice3_doc.md
Normal file
624
docs/__temp/__PROJECT_ROADMAP/slice3_doc.md
Normal file
@@ -0,0 +1,624 @@
|
||||
# Slice 3: Product Catalog Management
|
||||
## Vendor Selects and Publishes Products
|
||||
|
||||
**Status**: 📋 NOT STARTED
|
||||
**Timeline**: Week 3 (5 days)
|
||||
**Prerequisites**: Slice 1 & 2 complete
|
||||
|
||||
## 🎯 Slice Objectives
|
||||
|
||||
Enable vendors to browse imported products, select which to publish, customize them, and manage their product catalog.
|
||||
|
||||
### User Stories
|
||||
- As a Vendor Owner, I can browse imported products from staging
|
||||
- As a Vendor Owner, I can select which products to publish to my catalog
|
||||
- As a Vendor Owner, I can customize product information (pricing, descriptions)
|
||||
- As a Vendor Owner, I can manage my published product catalog
|
||||
- As a Vendor Owner, I can manually add products (not from marketplace)
|
||||
- As a Vendor Owner, I can manage inventory for catalog products
|
||||
|
||||
### Success Criteria
|
||||
- [ ] Vendor can browse all imported products in staging
|
||||
- [ ] Vendor can filter/search staging products
|
||||
- [ ] Vendor can select products for publishing
|
||||
- [ ] Vendor can customize product details before/after publishing
|
||||
- [ ] Published products appear in vendor catalog
|
||||
- [ ] Vendor can manually create products
|
||||
- [ ] Vendor can update product inventory
|
||||
- [ ] Vendor can activate/deactivate products
|
||||
- [ ] Product operations are properly isolated by vendor
|
||||
|
||||
## 📋 Backend Implementation
|
||||
|
||||
### Database Models
|
||||
|
||||
#### Product Model (`models/database/product.py`)
|
||||
|
||||
```python
|
||||
class Product(Base, TimestampMixin):
|
||||
"""
|
||||
Vendor's published product catalog
|
||||
These are customer-facing products
|
||||
"""
|
||||
__tablename__ = "products"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
|
||||
|
||||
# Basic information
|
||||
sku = Column(String, nullable=False, index=True)
|
||||
title = Column(String, nullable=False)
|
||||
description = Column(Text)
|
||||
short_description = Column(String(500))
|
||||
|
||||
# Pricing
|
||||
price = Column(Numeric(10, 2), nullable=False)
|
||||
compare_at_price = Column(Numeric(10, 2)) # Original price for discounts
|
||||
cost_per_item = Column(Numeric(10, 2)) # For profit tracking
|
||||
currency = Column(String(3), default="EUR")
|
||||
|
||||
# Categorization
|
||||
category = Column(String)
|
||||
subcategory = Column(String)
|
||||
brand = Column(String)
|
||||
tags = Column(JSON, default=list)
|
||||
|
||||
# SEO
|
||||
slug = Column(String, unique=True, index=True)
|
||||
meta_title = Column(String)
|
||||
meta_description = Column(String)
|
||||
|
||||
# Images
|
||||
featured_image = Column(String) # Main product image
|
||||
image_urls = Column(JSON, default=list) # Additional images
|
||||
|
||||
# Status
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_featured = Column(Boolean, default=False)
|
||||
is_on_sale = Column(Boolean, default=False)
|
||||
|
||||
# Inventory (simple - detailed in Inventory model)
|
||||
track_inventory = Column(Boolean, default=True)
|
||||
stock_quantity = Column(Integer, default=0)
|
||||
low_stock_threshold = Column(Integer, default=10)
|
||||
|
||||
# Marketplace source (if imported)
|
||||
marketplace_product_id = Column(
|
||||
Integer,
|
||||
ForeignKey("marketplace_products.id"),
|
||||
nullable=True
|
||||
)
|
||||
external_sku = Column(String, nullable=True) # Original marketplace SKU
|
||||
|
||||
# Additional data
|
||||
attributes = Column(JSON, default=dict) # Custom attributes
|
||||
weight = Column(Numeric(10, 2)) # For shipping
|
||||
dimensions = Column(JSON) # {length, width, height}
|
||||
|
||||
# Relationships
|
||||
vendor = relationship("Vendor", back_populates="products")
|
||||
marketplace_source = relationship(
|
||||
"MarketplaceProduct",
|
||||
back_populates="published_product"
|
||||
)
|
||||
inventory_records = relationship("Inventory", back_populates="product")
|
||||
order_items = relationship("OrderItem", back_populates="product")
|
||||
|
||||
# Indexes
|
||||
__table_args__ = (
|
||||
Index('ix_product_vendor_sku', 'vendor_id', 'sku'),
|
||||
Index('ix_product_active', 'vendor_id', 'is_active'),
|
||||
Index('ix_product_featured', 'vendor_id', 'is_featured'),
|
||||
)
|
||||
```
|
||||
|
||||
#### Inventory Model (`models/database/inventory.py`)
|
||||
|
||||
```python
|
||||
class Inventory(Base, TimestampMixin):
|
||||
"""Track product inventory by location"""
|
||||
__tablename__ = "inventory"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
|
||||
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
|
||||
|
||||
# Location
|
||||
location_name = Column(String, default="Default") # Warehouse name
|
||||
|
||||
# Quantities
|
||||
available_quantity = Column(Integer, default=0)
|
||||
reserved_quantity = Column(Integer, default=0) # Pending orders
|
||||
|
||||
# Relationships
|
||||
vendor = relationship("Vendor")
|
||||
product = relationship("Product", back_populates="inventory_records")
|
||||
movements = relationship("InventoryMovement", back_populates="inventory")
|
||||
|
||||
class InventoryMovement(Base, TimestampMixin):
|
||||
"""Track inventory changes"""
|
||||
__tablename__ = "inventory_movements"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
inventory_id = Column(Integer, ForeignKey("inventory.id"), nullable=False)
|
||||
|
||||
# Movement details
|
||||
movement_type = Column(String) # 'received', 'sold', 'adjusted', 'returned'
|
||||
quantity_change = Column(Integer) # Positive or negative
|
||||
|
||||
# Context
|
||||
reference_type = Column(String, nullable=True) # 'order', 'import', 'manual'
|
||||
reference_id = Column(Integer, nullable=True)
|
||||
notes = Column(Text)
|
||||
|
||||
# Relationships
|
||||
inventory = relationship("Inventory", back_populates="movements")
|
||||
```
|
||||
|
||||
### Pydantic Schemas
|
||||
|
||||
#### Product Schemas (`models/schema/product.py`)
|
||||
|
||||
```python
|
||||
class ProductCreate(BaseModel):
|
||||
"""Create product from scratch"""
|
||||
sku: str
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
price: float = Field(..., gt=0)
|
||||
compare_at_price: Optional[float] = None
|
||||
cost_per_item: Optional[float] = None
|
||||
category: Optional[str] = None
|
||||
brand: Optional[str] = None
|
||||
tags: List[str] = []
|
||||
image_urls: List[str] = []
|
||||
track_inventory: bool = True
|
||||
stock_quantity: int = 0
|
||||
is_active: bool = True
|
||||
|
||||
class ProductPublishFromMarketplace(BaseModel):
|
||||
"""Publish product from marketplace staging"""
|
||||
marketplace_product_id: int
|
||||
custom_title: Optional[str] = None
|
||||
custom_description: Optional[str] = None
|
||||
custom_price: Optional[float] = None
|
||||
custom_sku: Optional[str] = None
|
||||
stock_quantity: int = 0
|
||||
is_active: bool = True
|
||||
|
||||
class ProductUpdate(BaseModel):
|
||||
"""Update existing product"""
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
short_description: Optional[str] = None
|
||||
price: Optional[float] = None
|
||||
compare_at_price: Optional[float] = None
|
||||
category: Optional[str] = None
|
||||
brand: Optional[str] = None
|
||||
tags: Optional[List[str]] = None
|
||||
image_urls: Optional[List[str]] = None
|
||||
is_active: Optional[bool] = None
|
||||
is_featured: Optional[bool] = None
|
||||
stock_quantity: Optional[int] = None
|
||||
|
||||
class ProductResponse(BaseModel):
|
||||
"""Product details"""
|
||||
id: int
|
||||
vendor_id: int
|
||||
sku: str
|
||||
title: str
|
||||
description: Optional[str]
|
||||
price: float
|
||||
compare_at_price: Optional[float]
|
||||
category: Optional[str]
|
||||
brand: Optional[str]
|
||||
tags: List[str]
|
||||
featured_image: Optional[str]
|
||||
image_urls: List[str]
|
||||
is_active: bool
|
||||
is_featured: bool
|
||||
stock_quantity: int
|
||||
marketplace_product_id: Optional[int]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
```
|
||||
|
||||
### Service Layer
|
||||
|
||||
#### Product Service (`app/services/product_service.py`)
|
||||
|
||||
```python
|
||||
class ProductService:
|
||||
"""Handle product catalog operations"""
|
||||
|
||||
async def publish_from_marketplace(
|
||||
self,
|
||||
vendor_id: int,
|
||||
publish_data: ProductPublishFromMarketplace,
|
||||
db: Session
|
||||
) -> Product:
|
||||
"""Publish marketplace product to catalog"""
|
||||
|
||||
# Get marketplace product
|
||||
mp_product = db.query(MarketplaceProduct).filter(
|
||||
MarketplaceProduct.id == publish_data.marketplace_product_id,
|
||||
MarketplaceProduct.vendor_id == vendor_id,
|
||||
MarketplaceProduct.is_published == False
|
||||
).first()
|
||||
|
||||
if not mp_product:
|
||||
raise ProductNotFoundError("Marketplace product not found")
|
||||
|
||||
# Check if SKU already exists
|
||||
sku = publish_data.custom_sku or mp_product.external_sku
|
||||
existing = db.query(Product).filter(
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.sku == sku
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise ProductAlreadyExistsError(f"Product with SKU {sku} already exists")
|
||||
|
||||
# Create product
|
||||
product = Product(
|
||||
vendor_id=vendor_id,
|
||||
sku=sku,
|
||||
title=publish_data.custom_title or mp_product.title,
|
||||
description=publish_data.custom_description or mp_product.description,
|
||||
price=publish_data.custom_price or mp_product.price,
|
||||
currency=mp_product.currency,
|
||||
category=mp_product.category,
|
||||
brand=mp_product.brand,
|
||||
image_urls=mp_product.image_urls,
|
||||
featured_image=mp_product.image_urls[0] if mp_product.image_urls else None,
|
||||
marketplace_product_id=mp_product.id,
|
||||
external_sku=mp_product.external_sku,
|
||||
stock_quantity=publish_data.stock_quantity,
|
||||
is_active=publish_data.is_active,
|
||||
slug=self._generate_slug(publish_data.custom_title or mp_product.title)
|
||||
)
|
||||
|
||||
db.add(product)
|
||||
|
||||
# Mark marketplace product as published
|
||||
mp_product.is_published = True
|
||||
mp_product.published_product_id = product.id
|
||||
|
||||
# Create initial inventory record
|
||||
inventory = Inventory(
|
||||
vendor_id=vendor_id,
|
||||
product_id=product.id,
|
||||
location_name="Default",
|
||||
available_quantity=publish_data.stock_quantity,
|
||||
reserved_quantity=0
|
||||
)
|
||||
db.add(inventory)
|
||||
|
||||
# Record inventory movement
|
||||
if publish_data.stock_quantity > 0:
|
||||
movement = InventoryMovement(
|
||||
inventory_id=inventory.id,
|
||||
movement_type="received",
|
||||
quantity_change=publish_data.stock_quantity,
|
||||
reference_type="import",
|
||||
notes="Initial stock from marketplace import"
|
||||
)
|
||||
db.add(movement)
|
||||
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
return product
|
||||
|
||||
async def create_product(
|
||||
self,
|
||||
vendor_id: int,
|
||||
product_data: ProductCreate,
|
||||
db: Session
|
||||
) -> Product:
|
||||
"""Create product manually"""
|
||||
|
||||
# Check SKU uniqueness
|
||||
existing = db.query(Product).filter(
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.sku == product_data.sku
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise ProductAlreadyExistsError(f"SKU {product_data.sku} already exists")
|
||||
|
||||
product = Product(
|
||||
vendor_id=vendor_id,
|
||||
**product_data.dict(),
|
||||
slug=self._generate_slug(product_data.title),
|
||||
featured_image=product_data.image_urls[0] if product_data.image_urls else None
|
||||
)
|
||||
|
||||
db.add(product)
|
||||
|
||||
# Create inventory
|
||||
if product_data.track_inventory:
|
||||
inventory = Inventory(
|
||||
vendor_id=vendor_id,
|
||||
product_id=product.id,
|
||||
available_quantity=product_data.stock_quantity
|
||||
)
|
||||
db.add(inventory)
|
||||
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
return product
|
||||
|
||||
def get_products(
|
||||
self,
|
||||
vendor_id: int,
|
||||
db: Session,
|
||||
is_active: Optional[bool] = None,
|
||||
category: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
) -> List[Product]:
|
||||
"""Get vendor's product catalog"""
|
||||
|
||||
query = db.query(Product).filter(Product.vendor_id == vendor_id)
|
||||
|
||||
if is_active is not None:
|
||||
query = query.filter(Product.is_active == is_active)
|
||||
|
||||
if category:
|
||||
query = query.filter(Product.category == category)
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
Product.title.ilike(f"%{search}%"),
|
||||
Product.sku.ilike(f"%{search}%"),
|
||||
Product.brand.ilike(f"%{search}%")
|
||||
)
|
||||
)
|
||||
|
||||
return query.order_by(
|
||||
Product.created_at.desc()
|
||||
).offset(skip).limit(limit).all()
|
||||
|
||||
async def update_product(
|
||||
self,
|
||||
vendor_id: int,
|
||||
product_id: int,
|
||||
update_data: ProductUpdate,
|
||||
db: Session
|
||||
) -> Product:
|
||||
"""Update product details"""
|
||||
|
||||
product = db.query(Product).filter(
|
||||
Product.id == product_id,
|
||||
Product.vendor_id == vendor_id
|
||||
).first()
|
||||
|
||||
if not product:
|
||||
raise ProductNotFoundError()
|
||||
|
||||
# Update fields
|
||||
update_dict = update_data.dict(exclude_unset=True)
|
||||
for field, value in update_dict.items():
|
||||
setattr(product, field, value)
|
||||
|
||||
# Update stock if changed
|
||||
if 'stock_quantity' in update_dict:
|
||||
self._update_inventory(product, update_dict['stock_quantity'], db)
|
||||
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
return product
|
||||
|
||||
def _generate_slug(self, title: str) -> str:
|
||||
"""Generate URL-friendly slug"""
|
||||
import re
|
||||
slug = title.lower()
|
||||
slug = re.sub(r'[^a-z0-9]+', '-', slug)
|
||||
slug = slug.strip('-')
|
||||
return slug
|
||||
|
||||
def _update_inventory(
|
||||
self,
|
||||
product: Product,
|
||||
new_quantity: int,
|
||||
db: Session
|
||||
):
|
||||
"""Update product inventory"""
|
||||
inventory = db.query(Inventory).filter(
|
||||
Inventory.product_id == product.id
|
||||
).first()
|
||||
|
||||
if inventory:
|
||||
quantity_change = new_quantity - inventory.available_quantity
|
||||
inventory.available_quantity = new_quantity
|
||||
|
||||
# Record movement
|
||||
movement = InventoryMovement(
|
||||
inventory_id=inventory.id,
|
||||
movement_type="adjusted",
|
||||
quantity_change=quantity_change,
|
||||
reference_type="manual",
|
||||
notes="Manual adjustment"
|
||||
)
|
||||
db.add(movement)
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
|
||||
#### Product Endpoints (`app/api/v1/vendor/products.py`)
|
||||
|
||||
```python
|
||||
@router.get("", response_model=List[ProductResponse])
|
||||
async def get_products(
|
||||
is_active: Optional[bool] = None,
|
||||
category: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
current_user: User = Depends(get_current_vendor_user),
|
||||
vendor: Vendor = Depends(get_current_vendor),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get vendor's product catalog"""
|
||||
service = ProductService()
|
||||
products = service.get_products(
|
||||
vendor.id, db, is_active, category, search, skip, limit
|
||||
)
|
||||
return products
|
||||
|
||||
@router.post("", response_model=ProductResponse)
|
||||
async def create_product(
|
||||
product_data: ProductCreate,
|
||||
current_user: User = Depends(get_current_vendor_user),
|
||||
vendor: Vendor = Depends(get_current_vendor),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create product manually"""
|
||||
service = ProductService()
|
||||
product = await service.create_product(vendor.id, product_data, db)
|
||||
return product
|
||||
|
||||
@router.post("/from-marketplace", response_model=ProductResponse)
|
||||
async def publish_from_marketplace(
|
||||
publish_data: ProductPublishFromMarketplace,
|
||||
current_user: User = Depends(get_current_vendor_user),
|
||||
vendor: Vendor = Depends(get_current_vendor),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Publish marketplace product to catalog"""
|
||||
service = ProductService()
|
||||
product = await service.publish_from_marketplace(
|
||||
vendor.id, publish_data, db
|
||||
)
|
||||
return product
|
||||
|
||||
@router.get("/{product_id}", response_model=ProductResponse)
|
||||
async def get_product(
|
||||
product_id: int,
|
||||
current_user: User = Depends(get_current_vendor_user),
|
||||
vendor: Vendor = Depends(get_current_vendor),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get product details"""
|
||||
product = db.query(Product).filter(
|
||||
Product.id == product_id,
|
||||
Product.vendor_id == vendor.id
|
||||
).first()
|
||||
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
return product
|
||||
|
||||
@router.put("/{product_id}", response_model=ProductResponse)
|
||||
async def update_product(
|
||||
product_id: int,
|
||||
update_data: ProductUpdate,
|
||||
current_user: User = Depends(get_current_vendor_user),
|
||||
vendor: Vendor = Depends(get_current_vendor),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update product"""
|
||||
service = ProductService()
|
||||
product = await service.update_product(vendor.id, product_id, update_data, db)
|
||||
return product
|
||||
|
||||
@router.put("/{product_id}/toggle-active")
|
||||
async def toggle_product_active(
|
||||
product_id: int,
|
||||
current_user: User = Depends(get_current_vendor_user),
|
||||
vendor: Vendor = Depends(get_current_vendor),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Activate/deactivate product"""
|
||||
product = db.query(Product).filter(
|
||||
Product.id == product_id,
|
||||
Product.vendor_id == vendor.id
|
||||
).first()
|
||||
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
product.is_active = not product.is_active
|
||||
db.commit()
|
||||
|
||||
return {"is_active": product.is_active}
|
||||
|
||||
@router.delete("/{product_id}")
|
||||
async def delete_product(
|
||||
product_id: int,
|
||||
current_user: User = Depends(get_current_vendor_user),
|
||||
vendor: Vendor = Depends(get_current_vendor),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Remove product from catalog"""
|
||||
product = db.query(Product).filter(
|
||||
Product.id == product_id,
|
||||
Product.vendor_id == vendor.id
|
||||
).first()
|
||||
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
# Mark as inactive instead of deleting
|
||||
product.is_active = False
|
||||
db.commit()
|
||||
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
## 🎨 Frontend Implementation
|
||||
|
||||
### Templates
|
||||
|
||||
#### Browse Marketplace Products (`templates/vendor/marketplace/browse.html`)
|
||||
|
||||
Uses Alpine.js for reactive filtering, selection, and bulk publishing.
|
||||
|
||||
#### Product Catalog (`templates/vendor/products/list.html`)
|
||||
|
||||
Product management interface with search, filters, and quick actions.
|
||||
|
||||
#### Product Edit (`templates/vendor/products/edit.html`)
|
||||
|
||||
Detailed product editing with image management and inventory tracking.
|
||||
|
||||
## ✅ Testing Checklist
|
||||
|
||||
### Backend Tests
|
||||
- [ ] Product publishing from marketplace works
|
||||
- [ ] Manual product creation works
|
||||
- [ ] Product updates work correctly
|
||||
- [ ] Inventory tracking is accurate
|
||||
- [ ] SKU uniqueness is enforced
|
||||
- [ ] Vendor isolation maintained
|
||||
- [ ] Product search/filtering works
|
||||
- [ ] Slug generation works correctly
|
||||
|
||||
### Frontend Tests
|
||||
- [ ] Browse marketplace products
|
||||
- [ ] Select multiple products for publishing
|
||||
- [ ] Publish single product with customization
|
||||
- [ ] View product catalog
|
||||
- [ ] Edit product details
|
||||
- [ ] Toggle product active status
|
||||
- [ ] Delete/deactivate products
|
||||
- [ ] Search and filter products
|
||||
|
||||
## ➡️ Next Steps
|
||||
|
||||
After completing Slice 3, move to **Slice 4: Customer Shopping Experience** to build the public-facing shop.
|
||||
|
||||
---
|
||||
|
||||
**Slice 3 Status**: 📋 Not Started
|
||||
**Dependencies**: Slices 1 & 2 must be complete
|
||||
**Estimated Duration**: 5 days
|
||||
887
docs/__temp/__PROJECT_ROADMAP/slice4_doc.md
Normal file
887
docs/__temp/__PROJECT_ROADMAP/slice4_doc.md
Normal file
@@ -0,0 +1,887 @@
|
||||
# Slice 4: Customer Shopping Experience
|
||||
## Customers Browse and Shop on Vendor Stores
|
||||
|
||||
**Status**: 📋 NOT STARTED
|
||||
**Timeline**: Week 4 (5 days)
|
||||
**Prerequisites**: Slices 1, 2, & 3 complete
|
||||
|
||||
## 🎯 Slice Objectives
|
||||
|
||||
Build the public-facing customer shop where customers can browse products, register accounts, and add items to cart.
|
||||
|
||||
### User Stories
|
||||
- As a Customer, I can browse products on a vendor's shop
|
||||
- As a Customer, I can view detailed product information
|
||||
- As a Customer, I can search for products
|
||||
- As a Customer, I can register for a vendor-specific account
|
||||
- As a Customer, I can log into my account
|
||||
- As a Customer, I can add products to my shopping cart
|
||||
- As a Customer, I can manage my cart (update quantities, remove items)
|
||||
- Cart persists across sessions
|
||||
|
||||
### Success Criteria
|
||||
- [ ] Customers can browse products without authentication
|
||||
- [ ] Product catalog displays correctly with images and prices
|
||||
- [ ] Product detail pages show complete information
|
||||
- [ ] Search functionality works
|
||||
- [ ] Customers can register vendor-specific accounts
|
||||
- [ ] Customer login/logout works
|
||||
- [ ] Shopping cart is functional with Alpine.js reactivity
|
||||
- [ ] Cart persists (session-based before login, user-based after)
|
||||
- [ ] Customer data is properly isolated by vendor
|
||||
- [ ] Mobile responsive design
|
||||
|
||||
## 📋 Backend Implementation
|
||||
|
||||
### Database Models
|
||||
|
||||
#### Customer Model (`models/database/customer.py`)
|
||||
|
||||
```python
|
||||
class Customer(Base, TimestampMixin):
|
||||
"""
|
||||
Vendor-scoped customer accounts
|
||||
Each customer belongs to ONE vendor
|
||||
"""
|
||||
__tablename__ = "customers"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
|
||||
|
||||
# Authentication
|
||||
email = Column(String, nullable=False, index=True)
|
||||
hashed_password = Column(String, nullable=False)
|
||||
|
||||
# Personal information
|
||||
first_name = Column(String)
|
||||
last_name = Column(String)
|
||||
phone = Column(String)
|
||||
|
||||
# Customer metadata
|
||||
customer_number = Column(String, unique=True, index=True) # Auto-generated
|
||||
|
||||
# Preferences
|
||||
language = Column(String(2), default="en")
|
||||
newsletter_subscribed = Column(Boolean, default=False)
|
||||
marketing_emails = Column(Boolean, default=True)
|
||||
preferences = Column(JSON, default=dict)
|
||||
|
||||
# Statistics
|
||||
total_orders = Column(Integer, default=0)
|
||||
total_spent = Column(Numeric(10, 2), default=0)
|
||||
|
||||
# Status
|
||||
is_active = Column(Boolean, default=True)
|
||||
email_verified = Column(Boolean, default=False)
|
||||
last_login_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Relationships
|
||||
vendor = relationship("Vendor", back_populates="customers")
|
||||
addresses = relationship("CustomerAddress", back_populates="customer", cascade="all, delete-orphan")
|
||||
orders = relationship("Order", back_populates="customer")
|
||||
cart = relationship("Cart", back_populates="customer", uselist=False)
|
||||
|
||||
# Indexes
|
||||
__table_args__ = (
|
||||
Index('ix_customer_vendor_email', 'vendor_id', 'email', unique=True),
|
||||
)
|
||||
|
||||
class CustomerAddress(Base, TimestampMixin):
|
||||
"""Customer shipping/billing addresses"""
|
||||
__tablename__ = "customer_addresses"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False)
|
||||
|
||||
# Address type
|
||||
address_type = Column(String, default="shipping") # shipping, billing, both
|
||||
is_default = Column(Boolean, default=False)
|
||||
|
||||
# Address details
|
||||
first_name = Column(String)
|
||||
last_name = Column(String)
|
||||
company = Column(String)
|
||||
address_line1 = Column(String, nullable=False)
|
||||
address_line2 = Column(String)
|
||||
city = Column(String, nullable=False)
|
||||
state_province = Column(String)
|
||||
postal_code = Column(String, nullable=False)
|
||||
country = Column(String, nullable=False, default="LU")
|
||||
phone = Column(String)
|
||||
|
||||
# Relationships
|
||||
customer = relationship("Customer", back_populates="addresses")
|
||||
```
|
||||
|
||||
#### Cart Model (`models/database/cart.py`)
|
||||
|
||||
```python
|
||||
class Cart(Base, TimestampMixin):
|
||||
"""Shopping cart - session or customer-based"""
|
||||
__tablename__ = "carts"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
|
||||
|
||||
# Owner (one of these must be set)
|
||||
customer_id = Column(Integer, ForeignKey("customers.id"), nullable=True)
|
||||
session_id = Column(String, nullable=True, index=True) # For guest users
|
||||
|
||||
# Cart metadata
|
||||
currency = Column(String(3), default="EUR")
|
||||
|
||||
# Relationships
|
||||
vendor = relationship("Vendor")
|
||||
customer = relationship("Customer", back_populates="cart")
|
||||
items = relationship("CartItem", back_populates="cart", cascade="all, delete-orphan")
|
||||
|
||||
# Computed properties
|
||||
@property
|
||||
def total_items(self) -> int:
|
||||
return sum(item.quantity for item in self.items)
|
||||
|
||||
@property
|
||||
def subtotal(self) -> Decimal:
|
||||
return sum(item.line_total for item in self.items)
|
||||
|
||||
__table_args__ = (
|
||||
Index('ix_cart_vendor_session', 'vendor_id', 'session_id'),
|
||||
Index('ix_cart_vendor_customer', 'vendor_id', 'customer_id'),
|
||||
)
|
||||
|
||||
class CartItem(Base, TimestampMixin):
|
||||
"""Individual items in cart"""
|
||||
__tablename__ = "cart_items"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
cart_id = Column(Integer, ForeignKey("carts.id"), nullable=False)
|
||||
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
|
||||
|
||||
# Item details
|
||||
quantity = Column(Integer, nullable=False, default=1)
|
||||
unit_price = Column(Numeric(10, 2), nullable=False) # Snapshot at time of add
|
||||
|
||||
# Relationships
|
||||
cart = relationship("Cart", back_populates="items")
|
||||
product = relationship("Product")
|
||||
|
||||
@property
|
||||
def line_total(self) -> Decimal:
|
||||
return self.unit_price * self.quantity
|
||||
```
|
||||
|
||||
### Pydantic Schemas
|
||||
|
||||
#### Customer Schemas (`models/schema/customer.py`)
|
||||
|
||||
```python
|
||||
class CustomerRegister(BaseModel):
|
||||
"""Customer registration"""
|
||||
email: EmailStr
|
||||
password: str = Field(..., min_length=8)
|
||||
first_name: str
|
||||
last_name: str
|
||||
phone: Optional[str] = None
|
||||
newsletter_subscribed: bool = False
|
||||
|
||||
class CustomerLogin(BaseModel):
|
||||
"""Customer login"""
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
class CustomerResponse(BaseModel):
|
||||
"""Customer details"""
|
||||
id: int
|
||||
vendor_id: int
|
||||
email: str
|
||||
first_name: str
|
||||
last_name: str
|
||||
phone: Optional[str]
|
||||
customer_number: str
|
||||
total_orders: int
|
||||
total_spent: float
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class CustomerAddressCreate(BaseModel):
|
||||
"""Create address"""
|
||||
address_type: str = "shipping"
|
||||
is_default: bool = False
|
||||
first_name: str
|
||||
last_name: str
|
||||
company: Optional[str] = None
|
||||
address_line1: str
|
||||
address_line2: Optional[str] = None
|
||||
city: str
|
||||
state_province: Optional[str] = None
|
||||
postal_code: str
|
||||
country: str = "LU"
|
||||
phone: Optional[str] = None
|
||||
|
||||
class CustomerAddressResponse(BaseModel):
|
||||
"""Address details"""
|
||||
id: int
|
||||
address_type: str
|
||||
is_default: bool
|
||||
first_name: str
|
||||
last_name: str
|
||||
company: Optional[str]
|
||||
address_line1: str
|
||||
address_line2: Optional[str]
|
||||
city: str
|
||||
state_province: Optional[str]
|
||||
postal_code: str
|
||||
country: str
|
||||
phone: Optional[str]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
```
|
||||
|
||||
#### Cart Schemas (`models/schema/cart.py`)
|
||||
|
||||
```python
|
||||
class CartItemAdd(BaseModel):
|
||||
"""Add item to cart"""
|
||||
product_id: int
|
||||
quantity: int = Field(..., gt=0)
|
||||
|
||||
class CartItemUpdate(BaseModel):
|
||||
"""Update cart item"""
|
||||
quantity: int = Field(..., gt=0)
|
||||
|
||||
class CartItemResponse(BaseModel):
|
||||
"""Cart item details"""
|
||||
id: int
|
||||
product_id: int
|
||||
product_title: str
|
||||
product_image: Optional[str]
|
||||
product_sku: str
|
||||
quantity: int
|
||||
unit_price: float
|
||||
line_total: float
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class CartResponse(BaseModel):
|
||||
"""Complete cart"""
|
||||
id: int
|
||||
vendor_id: int
|
||||
total_items: int
|
||||
subtotal: float
|
||||
currency: str
|
||||
items: List[CartItemResponse]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
```
|
||||
|
||||
### Service Layer
|
||||
|
||||
#### Customer Service (`app/services/customer_service.py`)
|
||||
|
||||
```python
|
||||
class CustomerService:
|
||||
"""Handle customer operations"""
|
||||
|
||||
def __init__(self):
|
||||
self.auth_manager = AuthManager()
|
||||
|
||||
async def register_customer(
|
||||
self,
|
||||
vendor_id: int,
|
||||
customer_data: CustomerRegister,
|
||||
db: Session
|
||||
) -> Customer:
|
||||
"""Register new customer for vendor"""
|
||||
|
||||
# Check if email already exists for this vendor
|
||||
existing = db.query(Customer).filter(
|
||||
Customer.vendor_id == vendor_id,
|
||||
Customer.email == customer_data.email
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise CustomerAlreadyExistsError("Email already registered")
|
||||
|
||||
# Generate customer number
|
||||
customer_number = self._generate_customer_number(vendor_id, db)
|
||||
|
||||
# Create customer
|
||||
customer = Customer(
|
||||
vendor_id=vendor_id,
|
||||
email=customer_data.email,
|
||||
hashed_password=self.auth_manager.hash_password(customer_data.password),
|
||||
first_name=customer_data.first_name,
|
||||
last_name=customer_data.last_name,
|
||||
phone=customer_data.phone,
|
||||
customer_number=customer_number,
|
||||
newsletter_subscribed=customer_data.newsletter_subscribed,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
db.add(customer)
|
||||
db.commit()
|
||||
db.refresh(customer)
|
||||
|
||||
return customer
|
||||
|
||||
async def authenticate_customer(
|
||||
self,
|
||||
vendor_id: int,
|
||||
email: str,
|
||||
password: str,
|
||||
db: Session
|
||||
) -> Tuple[Customer, str]:
|
||||
"""Authenticate customer and return token"""
|
||||
|
||||
customer = db.query(Customer).filter(
|
||||
Customer.vendor_id == vendor_id,
|
||||
Customer.email == email
|
||||
).first()
|
||||
|
||||
if not customer:
|
||||
raise InvalidCredentialsError()
|
||||
|
||||
if not customer.is_active:
|
||||
raise CustomerInactiveError()
|
||||
|
||||
if not self.auth_manager.verify_password(password, customer.hashed_password):
|
||||
raise InvalidCredentialsError()
|
||||
|
||||
# Update last login
|
||||
customer.last_login_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
# Generate JWT token
|
||||
token = self.auth_manager.create_access_token({
|
||||
"sub": str(customer.id),
|
||||
"email": customer.email,
|
||||
"vendor_id": vendor_id,
|
||||
"type": "customer"
|
||||
})
|
||||
|
||||
return customer, token
|
||||
|
||||
def _generate_customer_number(self, vendor_id: int, db: Session) -> str:
|
||||
"""Generate unique customer number"""
|
||||
# Format: VENDOR_CODE-YYYYMMDD-XXXX
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
vendor = db.query(Vendor).get(vendor_id)
|
||||
date_str = datetime.utcnow().strftime("%Y%m%d")
|
||||
|
||||
# Count customers today
|
||||
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0)
|
||||
count = db.query(Customer).filter(
|
||||
Customer.vendor_id == vendor_id,
|
||||
Customer.created_at >= today_start
|
||||
).count()
|
||||
|
||||
return f"{vendor.vendor_code}-{date_str}-{count+1:04d}"
|
||||
```
|
||||
|
||||
#### Cart Service (`app/services/cart_service.py`)
|
||||
|
||||
```python
|
||||
class CartService:
|
||||
"""Handle shopping cart operations"""
|
||||
|
||||
async def get_or_create_cart(
|
||||
self,
|
||||
vendor_id: int,
|
||||
db: Session,
|
||||
customer_id: Optional[int] = None,
|
||||
session_id: Optional[str] = None
|
||||
) -> Cart:
|
||||
"""Get existing cart or create new one"""
|
||||
|
||||
if customer_id:
|
||||
cart = db.query(Cart).filter(
|
||||
Cart.vendor_id == vendor_id,
|
||||
Cart.customer_id == customer_id
|
||||
).first()
|
||||
else:
|
||||
cart = db.query(Cart).filter(
|
||||
Cart.vendor_id == vendor_id,
|
||||
Cart.session_id == session_id
|
||||
).first()
|
||||
|
||||
if not cart:
|
||||
cart = Cart(
|
||||
vendor_id=vendor_id,
|
||||
customer_id=customer_id,
|
||||
session_id=session_id
|
||||
)
|
||||
db.add(cart)
|
||||
db.commit()
|
||||
db.refresh(cart)
|
||||
|
||||
return cart
|
||||
|
||||
async def add_to_cart(
|
||||
self,
|
||||
cart: Cart,
|
||||
product_id: int,
|
||||
quantity: int,
|
||||
db: Session
|
||||
) -> CartItem:
|
||||
"""Add product to cart"""
|
||||
|
||||
# Verify product exists and is active
|
||||
product = db.query(Product).filter(
|
||||
Product.id == product_id,
|
||||
Product.vendor_id == cart.vendor_id,
|
||||
Product.is_active == True
|
||||
).first()
|
||||
|
||||
if not product:
|
||||
raise ProductNotFoundError()
|
||||
|
||||
# Check if product already in cart
|
||||
existing_item = db.query(CartItem).filter(
|
||||
CartItem.cart_id == cart.id,
|
||||
CartItem.product_id == product_id
|
||||
).first()
|
||||
|
||||
if existing_item:
|
||||
# Update quantity
|
||||
existing_item.quantity += quantity
|
||||
db.commit()
|
||||
db.refresh(existing_item)
|
||||
return existing_item
|
||||
else:
|
||||
# Add new item
|
||||
cart_item = CartItem(
|
||||
cart_id=cart.id,
|
||||
product_id=product_id,
|
||||
quantity=quantity,
|
||||
unit_price=product.price
|
||||
)
|
||||
db.add(cart_item)
|
||||
db.commit()
|
||||
db.refresh(cart_item)
|
||||
return cart_item
|
||||
|
||||
async def update_cart_item(
|
||||
self,
|
||||
cart_item_id: int,
|
||||
quantity: int,
|
||||
cart: Cart,
|
||||
db: Session
|
||||
) -> CartItem:
|
||||
"""Update cart item quantity"""
|
||||
|
||||
cart_item = db.query(CartItem).filter(
|
||||
CartItem.id == cart_item_id,
|
||||
CartItem.cart_id == cart.id
|
||||
).first()
|
||||
|
||||
if not cart_item:
|
||||
raise CartItemNotFoundError()
|
||||
|
||||
cart_item.quantity = quantity
|
||||
db.commit()
|
||||
db.refresh(cart_item)
|
||||
|
||||
return cart_item
|
||||
|
||||
async def remove_from_cart(
|
||||
self,
|
||||
cart_item_id: int,
|
||||
cart: Cart,
|
||||
db: Session
|
||||
):
|
||||
"""Remove item from cart"""
|
||||
|
||||
cart_item = db.query(CartItem).filter(
|
||||
CartItem.id == cart_item_id,
|
||||
CartItem.cart_id == cart.id
|
||||
).first()
|
||||
|
||||
if not cart_item:
|
||||
raise CartItemNotFoundError()
|
||||
|
||||
db.delete(cart_item)
|
||||
db.commit()
|
||||
|
||||
async def clear_cart(self, cart: Cart, db: Session):
|
||||
"""Clear all items from cart"""
|
||||
|
||||
db.query(CartItem).filter(CartItem.cart_id == cart.id).delete()
|
||||
db.commit()
|
||||
|
||||
async def merge_carts(
|
||||
self,
|
||||
session_cart_id: int,
|
||||
customer_cart_id: int,
|
||||
db: Session
|
||||
):
|
||||
"""Merge session cart into customer cart after login"""
|
||||
|
||||
session_cart = db.query(Cart).get(session_cart_id)
|
||||
customer_cart = db.query(Cart).get(customer_cart_id)
|
||||
|
||||
if not session_cart or not customer_cart:
|
||||
return
|
||||
|
||||
# Move items from session cart to customer cart
|
||||
for item in session_cart.items:
|
||||
# Check if product already in customer cart
|
||||
existing = db.query(CartItem).filter(
|
||||
CartItem.cart_id == customer_cart.id,
|
||||
CartItem.product_id == item.product_id
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
existing.quantity += item.quantity
|
||||
else:
|
||||
item.cart_id = customer_cart.id
|
||||
|
||||
# Delete session cart
|
||||
db.delete(session_cart)
|
||||
db.commit()
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
|
||||
#### Public Product Endpoints (`app/api/v1/public/vendors/products.py`)
|
||||
|
||||
```python
|
||||
@router.get("", response_model=List[ProductResponse])
|
||||
async def get_public_products(
|
||||
vendor_id: int,
|
||||
category: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
min_price: Optional[float] = None,
|
||||
max_price: Optional[float] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get public product catalog (no auth required)"""
|
||||
|
||||
query = db.query(Product).filter(
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.is_active == True
|
||||
)
|
||||
|
||||
if category:
|
||||
query = query.filter(Product.category == category)
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
Product.title.ilike(f"%{search}%"),
|
||||
Product.description.ilike(f"%{search}%")
|
||||
)
|
||||
)
|
||||
|
||||
if min_price:
|
||||
query = query.filter(Product.price >= min_price)
|
||||
|
||||
if max_price:
|
||||
query = query.filter(Product.price <= max_price)
|
||||
|
||||
products = query.order_by(
|
||||
Product.is_featured.desc(),
|
||||
Product.created_at.desc()
|
||||
).offset(skip).limit(limit).all()
|
||||
|
||||
return products
|
||||
|
||||
@router.get("/{product_id}", response_model=ProductResponse)
|
||||
async def get_public_product(
|
||||
vendor_id: int,
|
||||
product_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get product details (no auth required)"""
|
||||
|
||||
product = db.query(Product).filter(
|
||||
Product.id == product_id,
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.is_active == True
|
||||
).first()
|
||||
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
return product
|
||||
|
||||
@router.get("/search")
|
||||
async def search_products(
|
||||
vendor_id: int,
|
||||
q: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Search products"""
|
||||
# Implement search logic
|
||||
pass
|
||||
```
|
||||
|
||||
#### Customer Auth Endpoints (`app/api/v1/public/vendors/auth.py`)
|
||||
|
||||
```python
|
||||
@router.post("/register", response_model=CustomerResponse)
|
||||
async def register_customer(
|
||||
vendor_id: int,
|
||||
customer_data: CustomerRegister,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Register new customer"""
|
||||
service = CustomerService()
|
||||
customer = await service.register_customer(vendor_id, customer_data, db)
|
||||
return customer
|
||||
|
||||
@router.post("/login")
|
||||
async def login_customer(
|
||||
vendor_id: int,
|
||||
credentials: CustomerLogin,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Customer login"""
|
||||
service = CustomerService()
|
||||
customer, token = await service.authenticate_customer(
|
||||
vendor_id, credentials.email, credentials.password, db
|
||||
)
|
||||
|
||||
return {
|
||||
"access_token": token,
|
||||
"token_type": "bearer",
|
||||
"customer": CustomerResponse.from_orm(customer)
|
||||
}
|
||||
```
|
||||
|
||||
#### Cart Endpoints (`app/api/v1/public/vendors/cart.py`)
|
||||
|
||||
```python
|
||||
@router.get("/{session_id}", response_model=CartResponse)
|
||||
async def get_cart(
|
||||
vendor_id: int,
|
||||
session_id: str,
|
||||
current_customer: Optional[Customer] = Depends(get_current_customer_optional),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get cart (session or customer)"""
|
||||
service = CartService()
|
||||
cart = await service.get_or_create_cart(
|
||||
vendor_id,
|
||||
db,
|
||||
customer_id=current_customer.id if current_customer else None,
|
||||
session_id=session_id if not current_customer else None
|
||||
)
|
||||
return cart
|
||||
|
||||
@router.post("/{session_id}/items", response_model=CartItemResponse)
|
||||
async def add_to_cart(
|
||||
vendor_id: int,
|
||||
session_id: str,
|
||||
item_data: CartItemAdd,
|
||||
current_customer: Optional[Customer] = Depends(get_current_customer_optional),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Add item to cart"""
|
||||
service = CartService()
|
||||
cart = await service.get_or_create_cart(vendor_id, db, current_customer.id if current_customer else None, session_id)
|
||||
item = await service.add_to_cart(cart, item_data.product_id, item_data.quantity, db)
|
||||
return item
|
||||
```
|
||||
|
||||
## 🎨 Frontend Implementation
|
||||
|
||||
### Templates
|
||||
|
||||
#### Shop Homepage (`templates/shop/home.html`)
|
||||
|
||||
```html
|
||||
{% extends "shop/base_shop.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="shopHome()" x-init="loadFeaturedProducts()">
|
||||
<!-- Hero Section -->
|
||||
<div class="hero-section">
|
||||
<h1>Welcome to {{ vendor.name }}</h1>
|
||||
<p>{{ vendor.description }}</p>
|
||||
<a href="/shop/products" class="btn btn-primary btn-lg">
|
||||
Shop Now
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Featured Products -->
|
||||
<div class="products-section">
|
||||
<h2>Featured Products</h2>
|
||||
<div class="product-grid">
|
||||
<template x-for="product in featuredProducts" :key="product.id">
|
||||
<div class="product-card">
|
||||
<a :href="`/shop/products/${product.id}`">
|
||||
<img :src="product.featured_image || '/static/images/no-image.png'"
|
||||
:alt="product.title">
|
||||
<h3 x-text="product.title"></h3>
|
||||
<p class="price">€<span x-text="product.price.toFixed(2)"></span></p>
|
||||
</a>
|
||||
<button @click="addToCart(product.id)" class="btn btn-primary btn-sm">
|
||||
Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
#### Product Detail (`templates/shop/product.html`)
|
||||
|
||||
```html
|
||||
{% extends "shop/base_shop.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="productDetail()" x-init="loadProduct()">
|
||||
<div class="product-detail">
|
||||
<!-- Product Images -->
|
||||
<div class="product-images">
|
||||
<img :src="product.featured_image" :alt="product.title" class="main-image">
|
||||
</div>
|
||||
|
||||
<!-- Product Info -->
|
||||
<div class="product-info">
|
||||
<h1 x-text="product.title"></h1>
|
||||
<div class="price-section">
|
||||
<span class="price">€<span x-text="product.price"></span></span>
|
||||
<template x-if="product.compare_at_price">
|
||||
<span class="compare-price">€<span x-text="product.compare_at_price"></span></span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="product-description" x-html="product.description"></div>
|
||||
|
||||
<!-- Quantity Selector -->
|
||||
<div class="quantity-selector">
|
||||
<label>Quantity</label>
|
||||
<input
|
||||
type="number"
|
||||
x-model.number="quantity"
|
||||
:min="1"
|
||||
:max="product.stock_quantity"
|
||||
>
|
||||
<span class="stock-info" x-text="`${product.stock_quantity} in stock`"></span>
|
||||
</div>
|
||||
|
||||
<!-- Add to Cart -->
|
||||
<button
|
||||
@click="addToCart()"
|
||||
class="btn btn-primary btn-lg"
|
||||
:disabled="!canAddToCart || adding"
|
||||
>
|
||||
<span x-show="!adding">Add to Cart</span>
|
||||
<span x-show="adding" class="loading-spinner"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.productId = {{ product.id }};
|
||||
window.vendorId = {{ vendor.id }};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function productDetail() {
|
||||
return {
|
||||
product: {},
|
||||
quantity: 1,
|
||||
adding: false,
|
||||
loading: false,
|
||||
|
||||
get canAddToCart() {
|
||||
return this.product.stock_quantity >= this.quantity && this.quantity > 0;
|
||||
},
|
||||
|
||||
async loadProduct() {
|
||||
this.loading = true;
|
||||
try {
|
||||
this.product = await apiClient.get(
|
||||
`/api/v1/public/vendors/${window.vendorId}/products/${window.productId}`
|
||||
);
|
||||
} catch (error) {
|
||||
showNotification('Failed to load product', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async addToCart() {
|
||||
this.adding = true;
|
||||
try {
|
||||
const sessionId = getOrCreateSessionId();
|
||||
await apiClient.post(
|
||||
`/api/v1/public/vendors/${window.vendorId}/cart/${sessionId}/items`,
|
||||
{
|
||||
product_id: this.product.id,
|
||||
quantity: this.quantity
|
||||
}
|
||||
);
|
||||
|
||||
showNotification('Added to cart!', 'success');
|
||||
updateCartCount(); // Update cart icon
|
||||
} catch (error) {
|
||||
showNotification(error.message || 'Failed to add to cart', 'error');
|
||||
} finally {
|
||||
this.adding = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
#### Shopping Cart (`templates/shop/cart.html`)
|
||||
|
||||
Full Alpine.js reactive cart with real-time totals and quantity updates.
|
||||
|
||||
## ✅ Testing Checklist
|
||||
|
||||
### Backend Tests
|
||||
- [ ] Customer registration works
|
||||
- [ ] Duplicate email prevention works
|
||||
- [ ] Customer login/authentication works
|
||||
- [ ] Customer number generation is unique
|
||||
- [ ] Public product browsing works without auth
|
||||
- [ ] Product search/filtering works
|
||||
- [ ] Cart creation works (session and customer)
|
||||
- [ ] Add to cart works
|
||||
- [ ] Update cart quantity works
|
||||
- [ ] Remove from cart works
|
||||
- [ ] Cart persists across sessions
|
||||
- [ ] Cart merges after login
|
||||
- [ ] Vendor isolation maintained
|
||||
|
||||
### Frontend Tests
|
||||
- [ ] Shop homepage loads
|
||||
- [ ] Product listing displays
|
||||
- [ ] Product search works
|
||||
- [ ] Product detail page works
|
||||
- [ ] Customer registration form works
|
||||
- [ ] Customer login works
|
||||
- [ ] Add to cart works
|
||||
- [ ] Cart updates in real-time (Alpine.js)
|
||||
- [ ] Cart icon shows count
|
||||
- [ ] Mobile responsive
|
||||
|
||||
## ➡️ Next Steps
|
||||
|
||||
After completing Slice 4, move to **Slice 5: Order Processing** to complete the checkout flow and order management.
|
||||
|
||||
---
|
||||
|
||||
**Slice 4 Status**: 📋 Not Started
|
||||
**Dependencies**: Slices 1, 2, & 3 must be complete
|
||||
**Estimated Duration**: 5 days
|
||||
1628
docs/__temp/__PROJECT_ROADMAP/slice5_doc.md
Normal file
1628
docs/__temp/__PROJECT_ROADMAP/slice5_doc.md
Normal file
File diff suppressed because it is too large
Load Diff
306
docs/__temp/__PROJECT_ROADMAP/slice_overview.md
Normal file
306
docs/__temp/__PROJECT_ROADMAP/slice_overview.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# Multi-Tenant Ecommerce Platform - Vertical Slices Overview
|
||||
|
||||
## 📋 Development Approach
|
||||
|
||||
This project follows a **vertical slice development approach**, delivering complete, working user workflows incrementally. Each slice is fully functional and provides immediate value.
|
||||
|
||||
## 🎯 Technology Stack
|
||||
|
||||
### Backend
|
||||
- **Framework**: FastAPI (Python 3.11+)
|
||||
- **Database**: PostgreSQL with SQLAlchemy ORM
|
||||
- **Authentication**: JWT tokens with bcrypt
|
||||
- **Background Jobs**: Celery (for async tasks)
|
||||
- **API Documentation**: Auto-generated OpenAPI/Swagger
|
||||
|
||||
### Frontend
|
||||
- **Templating**: Jinja2 (server-side rendering)
|
||||
- **JavaScript Framework**: Alpine.js v3.x (15KB, CDN-based)
|
||||
- **Styling**: Custom CSS with CSS variables
|
||||
- **AJAX**: Vanilla JavaScript with Fetch API
|
||||
- **No Build Step**: Everything runs directly in the browser
|
||||
|
||||
### Why Alpine.js + Jinja2?
|
||||
- ✅ **Lightweight**: Only 15KB, no build step required
|
||||
- ✅ **Perfect Jinja2 Integration**: Works seamlessly with server-side templates
|
||||
- ✅ **Reactive State**: Modern UX without framework complexity
|
||||
- ✅ **Scoped Components**: Natural vendor isolation
|
||||
- ✅ **Progressive Enhancement**: Works even if JS fails
|
||||
- ✅ **Minimal Learning Curve**: Feels like inline JavaScript
|
||||
|
||||
## 📚 Slice Documentation Structure
|
||||
|
||||
Each slice has its own comprehensive markdown file:
|
||||
|
||||
### Slice 1: Multi-Tenant Foundation ✅ IN PROGRESS
|
||||
**File**: `01_slice1_admin_vendor_foundation.md`
|
||||
- Admin creates vendors through admin interface
|
||||
- Vendor owner login with context detection
|
||||
- Complete vendor data isolation
|
||||
- **Status**: Backend mostly complete, frontend in progress
|
||||
|
||||
### Slice 2: Marketplace Integration
|
||||
**File**: `02_slice2_marketplace_import.md`
|
||||
- CSV import from Letzshop marketplace
|
||||
- Background job processing
|
||||
- Product staging area
|
||||
- Import status tracking with Alpine.js
|
||||
|
||||
### Slice 3: Product Catalog Management
|
||||
**File**: `03_slice3_product_catalog.md`
|
||||
- Browse imported products in staging
|
||||
- Select and publish to vendor catalog
|
||||
- Product customization (pricing, descriptions)
|
||||
- Inventory management
|
||||
|
||||
### Slice 4: Customer Shopping Experience
|
||||
**File**: `04_slice4_customer_shopping.md`
|
||||
- Public product browsing
|
||||
- Customer registration/login
|
||||
- Shopping cart with Alpine.js reactivity
|
||||
- Product search functionality
|
||||
|
||||
### Slice 5: Order Processing
|
||||
**File**: `05_slice5_order_processing.md`
|
||||
- Checkout workflow
|
||||
- Order placement
|
||||
- Order management (vendor side)
|
||||
- Order history (customer side)
|
||||
|
||||
## 🎯 Slice Completion Criteria
|
||||
|
||||
Each slice must pass these gates before moving to the next:
|
||||
|
||||
### Technical Criteria
|
||||
- [ ] All backend endpoints implemented and tested
|
||||
- [ ] Frontend pages created with Jinja2 templates
|
||||
- [ ] Alpine.js components working (where applicable)
|
||||
- [ ] Database migrations applied successfully
|
||||
- [ ] Service layer business logic complete
|
||||
- [ ] Exception handling implemented
|
||||
- [ ] API documentation updated
|
||||
|
||||
### Quality Criteria
|
||||
- [ ] Manual testing complete (all user flows)
|
||||
- [ ] Security validation (vendor isolation)
|
||||
- [ ] Performance acceptable (basic load testing)
|
||||
- [ ] No console errors in browser
|
||||
- [ ] Responsive design works on mobile
|
||||
- [ ] Code follows project conventions
|
||||
|
||||
### Documentation Criteria
|
||||
- [ ] Slice markdown file updated
|
||||
- [ ] API endpoints documented
|
||||
- [ ] Frontend components documented
|
||||
- [ ] Database changes documented
|
||||
- [ ] Testing checklist completed
|
||||
|
||||
## 🗓️ Estimated Timeline
|
||||
|
||||
### Week 1: Slice 1 - Foundation ⏳ Current
|
||||
- Days 1-3: Backend completion (vendor context, admin APIs)
|
||||
- Days 4-5: Frontend completion (admin pages, vendor login)
|
||||
- **Deliverable**: Admin can create vendors, vendor owners can log in
|
||||
|
||||
### Week 2: Slice 2 - Import
|
||||
- Days 1-3: Import backend (CSV processing, job tracking)
|
||||
- Days 4-5: Import frontend (upload UI, status tracking)
|
||||
- **Deliverable**: Vendors can import products from Letzshop
|
||||
|
||||
### Week 3: Slice 3 - Catalog
|
||||
- Days 1-3: Catalog backend (product publishing, inventory)
|
||||
- Days 4-5: Catalog frontend (product management UI)
|
||||
- **Deliverable**: Vendors can manage product catalog
|
||||
|
||||
### Week 4: Slice 4 - Shopping
|
||||
- Days 1-3: Customer backend (registration, cart, products)
|
||||
- Days 4-5: Shop frontend (product browsing, cart)
|
||||
- **Deliverable**: Customers can browse and add to cart
|
||||
|
||||
### Week 5: Slice 5 - Orders
|
||||
- Days 1-3: Order backend (checkout, order management)
|
||||
- Days 4-5: Order frontend (checkout flow, order history)
|
||||
- **Deliverable**: Complete order workflow functional
|
||||
|
||||
## 📊 Progress Tracking
|
||||
|
||||
### ✅ Completed
|
||||
- Database schema design
|
||||
- Core models (User, Vendor, Roles)
|
||||
- Authentication system
|
||||
- Admin service layer
|
||||
- Vendor context detection middleware
|
||||
|
||||
### 🔄 In Progress (Slice 1)
|
||||
- Admin frontend pages (login, dashboard, vendors)
|
||||
- Vendor frontend pages (login, dashboard)
|
||||
- Admin API endpoints refinement
|
||||
- Frontend-backend integration
|
||||
|
||||
### 📋 Upcoming (Slice 2)
|
||||
- MarketplaceProduct model
|
||||
- ImportJob model
|
||||
- CSV processing service
|
||||
- Import frontend with Alpine.js
|
||||
|
||||
## 🎨 Frontend Architecture Pattern
|
||||
|
||||
### Page Structure (Jinja2 + Alpine.js)
|
||||
```html
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="componentName()">
|
||||
<!-- Alpine.js reactive component -->
|
||||
<h1 x-text="title"></h1>
|
||||
|
||||
<!-- Jinja2 for initial data -->
|
||||
<script>
|
||||
window.initialData = {{ data|tojson }};
|
||||
</script>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/admin/component.js"></script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### Alpine.js Component Pattern
|
||||
```javascript
|
||||
function componentName() {
|
||||
return {
|
||||
// State
|
||||
data: window.initialData || [],
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
// Lifecycle
|
||||
init() {
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
// Methods
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await apiClient.get('/api/endpoint');
|
||||
this.data = response;
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔑 Key Principles
|
||||
|
||||
### 1. Complete Features
|
||||
Each slice delivers a complete, working feature from database to UI.
|
||||
|
||||
### 2. Vendor Isolation
|
||||
All slices maintain strict vendor data isolation and context detection.
|
||||
|
||||
### 3. Progressive Enhancement
|
||||
- HTML works without JavaScript
|
||||
- Alpine.js enhances interactivity
|
||||
- Jinja2 provides server-side rendering
|
||||
|
||||
### 4. API-First Design
|
||||
- Backend exposes RESTful APIs
|
||||
- Frontend consumes APIs via Fetch
|
||||
- Clear separation of concerns
|
||||
|
||||
### 5. Clean Architecture
|
||||
- Service layer for business logic
|
||||
- Repository pattern for data access
|
||||
- Exception-first error handling
|
||||
- Dependency injection
|
||||
|
||||
## 📖 Documentation Files
|
||||
|
||||
### Slice Files (This Directory)
|
||||
- `00_slices_overview.md` - This file
|
||||
- `01_slice1_admin_vendor_foundation.md`
|
||||
- `02_slice2_marketplace_import.md`
|
||||
- `03_slice3_product_catalog.md`
|
||||
- `04_slice4_customer_shopping.md`
|
||||
- `05_slice5_order_processing.md`
|
||||
|
||||
### Supporting Documentation
|
||||
- `../quick_start_guide.md` - Get running in 15 minutes
|
||||
- `../css_structure_guide.txt` - CSS organization
|
||||
- `../css_quick_reference.txt` - CSS usage guide
|
||||
- `../12.project_readme_final.md` - Complete project README
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### For Current Development (Slice 1)
|
||||
1. Read `01_slice1_admin_vendor_foundation.md`
|
||||
2. Follow setup in `../quick_start_guide.md`
|
||||
3. Complete Slice 1 testing checklist
|
||||
4. Move to Slice 2
|
||||
|
||||
### For New Features
|
||||
1. Review this overview
|
||||
2. Read the relevant slice documentation
|
||||
3. Follow the implementation pattern
|
||||
4. Test thoroughly before moving forward
|
||||
|
||||
## 💡 Tips for Success
|
||||
|
||||
### Working with Slices
|
||||
- ✅ Complete one slice fully before starting the next
|
||||
- ✅ Test each slice thoroughly
|
||||
- ✅ Update documentation as you go
|
||||
- ✅ Commit code after each slice completion
|
||||
- ✅ Demo each slice to stakeholders
|
||||
|
||||
### Alpine.js Best Practices
|
||||
- Keep components small and focused
|
||||
- Use `x-data` for component state
|
||||
- Use `x-init` for initialization
|
||||
- Prefer `x-show` over `x-if` for toggles
|
||||
- Use Alpine directives, not vanilla JS DOM manipulation
|
||||
|
||||
### Jinja2 Best Practices
|
||||
- Extend base templates
|
||||
- Use template inheritance
|
||||
- Pass initial data from backend
|
||||
- Keep logic in backend, not templates
|
||||
- Use filters for formatting
|
||||
|
||||
## 🎯 Success Metrics
|
||||
|
||||
### By End of Slice 1
|
||||
- Admin can create vendors ✅
|
||||
- Vendor owners can log in ⏳
|
||||
- Vendor context detection works ✅
|
||||
- Complete data isolation verified
|
||||
|
||||
### By End of Slice 2
|
||||
- Vendors can import CSV files
|
||||
- Import jobs tracked in background
|
||||
- Product staging area functional
|
||||
|
||||
### By End of Slice 3
|
||||
- Products published to catalog
|
||||
- Inventory management working
|
||||
- Product customization enabled
|
||||
|
||||
### By End of Slice 4
|
||||
- Customers can browse products
|
||||
- Shopping cart functional
|
||||
- Customer accounts working
|
||||
|
||||
### By End of Slice 5
|
||||
- Complete checkout workflow
|
||||
- Order management operational
|
||||
- Platform ready for production
|
||||
|
||||
---
|
||||
|
||||
**Next Steps**: Start with `01_slice1_admin_vendor_foundation.md` to continue your current work on Slice 1.
|
||||
Reference in New Issue
Block a user