compiling project documentation

This commit is contained in:
2025-10-26 19:59:53 +01:00
parent d79817f069
commit 99863ad80b
45 changed files with 24278 additions and 0 deletions

View 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! 🚀**

View 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

View File

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

View File

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

View 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!
══════════════════════════════════════════════════════════════════

View 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!** 🚀

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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.