# Vendor Admin Frontend - Alpine.js/Jinja2 Page Template Guide

## 📋 Overview

This guide provides complete templates for creating new vendor admin pages using the established Alpine.js + Jinja2 architecture. Follow these patterns to ensure consistency across the vendor portal.

---

## 🎯 Quick Reference

### File Structure for New Page
```
app/
├── templates/vendor/
│   └── [page-name].html          # Jinja2 template
├── static/vendor/js/
│   └── [page-name].js            # Alpine.js component
└── routes/
    └── vendor_pages.py           # Route registration
```

### Checklist for New Page
- [ ] Create Jinja2 template extending base.html
- [ ] Create Alpine.js JavaScript component
- [ ] Register route in vendor_pages.py
- [ ] Add navigation link to sidebar.html
- [ ] Test authentication
- [ ] Test data loading
- [ ] Test responsive design

---

## 📄 Template Structure

### 1. Jinja2 Template

**File:** `app/templates/vendor/[page-name].html`

```jinja2
{# app/templates/vendor/[page-name].html #}
{% extends "vendor/base.html" %}

{# Page title for browser tab #}
{% block title %}[Page Name]{% endblock %}

{# Alpine.js component name - use data() for simple pages or vendor[PageName]() for complex pages #}
{% block alpine_data %}vendor[PageName](){% endblock %}

{# Page content #}
{% 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">
        [Page Name]
    </h2>
    
    <!-- Action Buttons -->
    <div class="flex items-center space-x-3">
        <button
            @click="refresh()"
            :disabled="loading"
            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"
        >
            <span x-show="!loading" x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
            <span x-show="loading" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
            <span x-text="loading ? 'Loading...' : 'Refresh'"></span>
        </button>
        
        <button
            @click="openCreateModal()"
            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>
            <span>Add New</span>
        </button>
    </div>
</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 data...</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</p>
        <p class="text-sm" x-text="error"></p>
    </div>
</div>

<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- FILTERS & SEARCH                                                 -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div x-show="!loading" class="mb-6 bg-white rounded-lg shadow-xs dark:bg-gray-800 p-4">
    <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
        <!-- Search -->
        <div>
            <label class="block text-sm">
                <span class="text-gray-700 dark:text-gray-400">Search</span>
                <input
                    x-model="filters.search"
                    @input.debounce.300ms="applyFilters()"
                    class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:text-gray-300 dark:focus:shadow-outline-gray form-input"
                    placeholder="Search..."
                />
            </label>
        </div>
        
        <!-- Status Filter -->
        <div>
            <label class="block text-sm">
                <span class="text-gray-700 dark:text-gray-400">Status</span>
                <select
                    x-model="filters.status"
                    @change="applyFilters()"
                    class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 form-select focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray"
                >
                    <option value="">All</option>
                    <option value="active">Active</option>
                    <option value="inactive">Inactive</option>
                </select>
            </label>
        </div>
        
        <!-- Sort -->
        <div>
            <label class="block text-sm">
                <span class="text-gray-700 dark:text-gray-400">Sort By</span>
                <select
                    x-model="filters.sortBy"
                    @change="applyFilters()"
                    class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 form-select focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray"
                >
                    <option value="created_at:desc">Newest First</option>
                    <option value="created_at:asc">Oldest First</option>
                    <option value="name:asc">Name (A-Z)</option>
                    <option value="name:desc">Name (Z-A)</option>
                </select>
            </label>
        </div>
    </div>
</div>

<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- DATA 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">Name</th>
                    <th class="px-4 py-3">Status</th>
                    <th class="px-4 py-3">Date</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="items.length === 0">
                    <tr>
                        <td colspan="4" 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('inbox', 'w-12 h-12 mb-2 text-gray-300')"></span>
                                <p>No items found.</p>
                            </div>
                        </td>
                    </tr>
                </template>

                <!-- Data Rows -->
                <template x-for="item in items" :key="item.id">
                    <tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
                        <td class="px-4 py-3">
                            <div class="flex items-center text-sm">
                                <div>
                                    <p class="font-semibold" x-text="item.name"></p>
                                    <p class="text-xs text-gray-600 dark:text-gray-400" 
                                       x-text="item.description"></p>
                                </div>
                            </div>
                        </td>
                        <td class="px-4 py-3 text-xs">
                            <span class="px-2 py-1 font-semibold leading-tight rounded-full"
                                  :class="item.status === 'active' 
                                    ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' 
                                    : 'text-gray-700 bg-gray-100 dark:text-gray-100 dark:bg-gray-700'"
                                  x-text="item.status"></span>
                        </td>
                        <td class="px-4 py-3 text-sm" x-text="formatDate(item.created_at)">
                        </td>
                        <td class="px-4 py-3">
                            <div class="flex items-center space-x-2 text-sm">
                                <button
                                    @click="viewItem(item.id)"
                                    class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-gray-400 dark:hover:bg-gray-700"
                                    title="View"
                                >
                                    <span x-html="$icon('eye', 'w-5 h-5')"></span>
                                </button>
                                <button
                                    @click="editItem(item.id)"
                                    class="flex items-center justify-center p-2 text-blue-600 rounded-lg hover:bg-blue-50 dark:text-gray-400 dark:hover:bg-gray-700"
                                    title="Edit"
                                >
                                    <span x-html="$icon('pencil', 'w-5 h-5')"></span>
                                </button>
                                <button
                                    @click="deleteItem(item.id)"
                                    class="flex items-center justify-center p-2 text-red-600 rounded-lg hover:bg-red-50 dark:text-gray-400 dark:hover:bg-gray-700"
                                    title="Delete"
                                >
                                    <span x-html="$icon('delete', 'w-5 h-5')"></span>
                                </button>
                            </div>
                        </td>
                    </tr>
                </template>
            </tbody>
        </table>
    </div>
    
    <!-- Pagination -->
    <div class="px-4 py-3 text-xs font-semibold tracking-wide text-gray-500 uppercase border-t dark:border-gray-700 bg-gray-50 sm:grid-cols-9 dark:text-gray-400 dark:bg-gray-800">
        <span class="flex items-center col-span-3">
            Showing <span class="mx-1 font-semibold" x-text="pagination.from"></span>-<span class="mx-1 font-semibold" x-text="pagination.to"></span> of <span class="mx-1 font-semibold" x-text="pagination.total"></span>
        </span>
        <span class="col-span-2"></span>
        <span class="flex col-span-4 mt-2 sm:mt-auto sm:justify-end">
            <nav aria-label="Table navigation">
                <ul class="inline-flex items-center">
                    <li>
                        <button
                            @click="previousPage()"
                            :disabled="!pagination.hasPrevious"
                            class="px-3 py-1 rounded-md rounded-l-lg focus:outline-none focus:shadow-outline-purple"
                            :class="pagination.hasPrevious ? 'hover:bg-gray-100 dark:hover:bg-gray-700' : 'opacity-50 cursor-not-allowed'"
                        >
                            <span x-html="$icon('chevron-left', 'w-4 h-4')"></span>
                        </button>
                    </li>
                    <li>
                        <span class="px-3 py-1" x-text="`Page ${pagination.currentPage} of ${pagination.totalPages}`"></span>
                    </li>
                    <li>
                        <button
                            @click="nextPage()"
                            :disabled="!pagination.hasNext"
                            class="px-3 py-1 rounded-md rounded-r-lg focus:outline-none focus:shadow-outline-purple"
                            :class="pagination.hasNext ? 'hover:bg-gray-100 dark:hover:bg-gray-700' : 'opacity-50 cursor-not-allowed'"
                        >
                            <span x-html="$icon('chevron-right', 'w-4 h-4')"></span>
                        </button>
                    </li>
                </ul>
            </nav>
        </span>
    </div>
</div>

<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- MODALS (if needed)                                               -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- Create/Edit Modal -->
<div x-show="showModal" 
     x-cloak
     class="fixed inset-0 z-50 flex items-center justify-center overflow-auto bg-black bg-opacity-50"
     @click.self="closeModal()">
    <div class="relative w-full max-w-lg p-6 mx-auto bg-white rounded-lg shadow-xl dark:bg-gray-800">
        <!-- Modal Header -->
        <div class="flex items-center justify-between mb-4">
            <h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200" 
                x-text="modalTitle"></h3>
            <button @click="closeModal()" 
                    class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
                <span x-html="$icon('x', 'w-6 h-6')"></span>
            </button>
        </div>
        
        <!-- Modal Body -->
        <form @submit.prevent="saveItem()">
            <div class="space-y-4">
                <!-- Form fields here -->
                <div>
                    <label class="block text-sm">
                        <span class="text-gray-700 dark:text-gray-400">Name</span>
                        <input
                            x-model="formData.name"
                            type="text"
                            class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:text-gray-300 dark:focus:shadow-outline-gray form-input"
                            required
                        />
                    </label>
                </div>
            </div>
            
            <!-- Modal Footer -->
            <div class="flex justify-end mt-6 space-x-3">
                <button
                    type="button"
                    @click="closeModal()"
                    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 hover:bg-gray-100 focus:outline-none focus:shadow-outline-gray dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600"
                >
                    Cancel
                </button>
                <button
                    type="submit"
                    :disabled="saving"
                    class="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"
                >
                    <span x-show="!saving">Save</span>
                    <span x-show="saving">Saving...</span>
                </button>
            </div>
        </form>
    </div>
</div>

{% endblock %}

{# Page-specific JavaScript #}
{% block extra_scripts %}
<script src="{{ url_for('static', path='vendor/js/[page-name].js') }}"></script>
{% endblock %}
```

---

### 2. Alpine.js Component

**File:** `app/static/vendor/js/[page-name].js`

```javascript
// app/static/vendor/js/[page-name].js
/**
 * [Page Name] page logic
 * Handles data loading, filtering, CRUD operations
 */

// ✅ Create dedicated logger for this page
const vendor[PageName]Log = window.LogConfig.loggers.[pagename];

function vendor[PageName]() {
    return {
        // ═══════════════════════════════════════════════════════════
        // INHERIT BASE STATE (from init-alpine.js)
        // ═══════════════════════════════════════════════════════════
        // This provides: vendorCode, currentUser, vendor, dark mode, menu states
        ...data(),

        // ✅ Set page identifier (for sidebar highlighting)
        currentPage: '[page-name]',

        // ═══════════════════════════════════════════════════════════
        // PAGE-SPECIFIC STATE
        // ═══════════════════════════════════════════════════════════
        loading: false,
        error: '',
        items: [],

        // Filters
        filters: {
            search: '',
            status: '',
            sortBy: 'created_at:desc'
        },

        // Pagination
        pagination: {
            currentPage: 1,
            perPage: 10,
            total: 0,
            totalPages: 0,
            from: 0,
            to: 0,
            hasPrevious: false,
            hasNext: false
        },

        // Modal state
        showModal: false,
        modalTitle: '',
        modalMode: 'create', // 'create' or 'edit'
        formData: {},
        saving: false,

        // ═══════════════════════════════════════════════════════════
        // LIFECYCLE
        // ═══════════════════════════════════════════════════════════
        async init() {
            // Guard against multiple initialization
            if (window._vendor[PageName]Initialized) {
                return;
            }
            window._vendor[PageName]Initialized = true;

            // IMPORTANT: Call parent init first to set vendorCode from URL
            const parentInit = data().init;
            if (parentInit) {
                await parentInit.call(this);
            }

            vendor[PageName]Log.info('[PageName] page initializing...');
            await this.loadData();
            vendor[PageName]Log.info('[PageName] page initialized');
        },
        
        // ═══════════════════════════════════════════════════════════
        // DATA LOADING
        // ═══════════════════════════════════════════════════════════
        async loadData() {
            this.loading = true;
            this.error = '';

            try {
                // Build query params
                const params = new URLSearchParams({
                    page: this.pagination.currentPage,
                    per_page: this.pagination.perPage,
                    ...this.filters
                });

                // API call
                // NOTE: apiClient prepends /api/v1, and vendor context middleware handles vendor detection
                // So we just call /vendor/[endpoint] → becomes /api/v1/vendor/[endpoint]
                const response = await apiClient.get(
                    `/vendor/[endpoint]?${params}`
                );

                // Update state
                this.items = response.items || [];
                this.updatePagination(response);

                vendor[PageName]Log.info('[PageName] data loaded', {
                    items: this.items.length,
                    total: this.pagination.total
                });

            } catch (error) {
                vendor[PageName]Log.error('Failed to load [page] data', error);
                this.error = error.message || 'Failed to load data';
            } finally {
                this.loading = false;
            }
        },
        
        async refresh() {
            await this.loadData();
        },
        
        updatePagination(response) {
            this.pagination = {
                currentPage: response.page || 1,
                perPage: response.per_page || 10,
                total: response.total || 0,
                totalPages: response.pages || 0,
                from: ((response.page - 1) * response.per_page) + 1,
                to: Math.min(response.page * response.per_page, response.total),
                hasPrevious: response.page > 1,
                hasNext: response.page < response.pages
            };
        },
        
        // ═══════════════════════════════════════════════════════════
        // FILTERING & PAGINATION
        // ═══════════════════════════════════════════════════════════
        async applyFilters() {
            this.pagination.currentPage = 1; // Reset to first page
            await this.loadData();
        },
        
        async previousPage() {
            if (this.pagination.hasPrevious) {
                this.pagination.currentPage--;
                await this.loadData();
            }
        },
        
        async nextPage() {
            if (this.pagination.hasNext) {
                this.pagination.currentPage++;
                await this.loadData();
            }
        },
        
        // ═══════════════════════════════════════════════════════════
        // CRUD OPERATIONS
        // ═══════════════════════════════════════════════════════════
        openCreateModal() {
            this.modalMode = 'create';
            this.modalTitle = 'Create New Item';
            this.formData = {
                name: '',
                description: '',
                status: 'active'
            };
            this.showModal = true;
        },
        
        async viewItem(id) {
            // Navigate to detail page or open view modal
            window.location.href = `/vendor/${this.vendorCode}/[endpoint]/${id}`;
        },
        
        async editItem(id) {
            try {
                // Load item data
                const item = await apiClient.get(
                    `/vendor/[endpoint]/${id}`
                );

                this.modalMode = 'edit';
                this.modalTitle = 'Edit Item';
                this.formData = { ...item };
                this.showModal = true;

            } catch (error) {
                vendor[PageName]Log.error('Failed to load item', error);
                alert('Failed to load item details');
            }
        },

        async saveItem() {
            this.saving = true;

            try {
                if (this.modalMode === 'create') {
                    await apiClient.post(
                        `/vendor/[endpoint]`,
                        this.formData
                    );
                    vendor[PageName]Log.info('Item created successfully');
                } else {
                    await apiClient.put(
                        `/vendor/[endpoint]/${this.formData.id}`,
                        this.formData
                    );
                    vendor[PageName]Log.info('Item updated successfully');
                }

                this.closeModal();
                await this.loadData();

            } catch (error) {
                vendor[PageName]Log.error('Failed to save item', error);
                alert(error.message || 'Failed to save item');
            } finally {
                this.saving = false;
            }
        },

        async deleteItem(id) {
            if (!confirm('Are you sure you want to delete this item?')) {
                return;
            }

            try {
                await apiClient.delete(
                    `/vendor/[endpoint]/${id}`
                );

                vendor[PageName]Log.info('Item deleted successfully');
                await this.loadData();

            } catch (error) {
                vendor[PageName]Log.error('Failed to delete item', error);
                alert(error.message || 'Failed to delete item');
            }
        },
        
        closeModal() {
            this.showModal = false;
            this.formData = {};
        },
        
        // ═══════════════════════════════════════════════════════════
        // UTILITIES
        // ═══════════════════════════════════════════════════════════
        formatDate(dateString) {
            if (!dateString) return '-';
            const date = new Date(dateString);
            return date.toLocaleDateString('en-US', {
                year: 'numeric',
                month: 'short',
                day: 'numeric'
            });
        },
        
        formatCurrency(amount) {
            return new Intl.NumberFormat('en-US', {
                style: 'currency',
                currency: 'EUR'
            }).format(amount || 0);
        }
    };
}

// Make available globally
window.vendor[PageName] = vendor[PageName];
```

---

### 3. Route Registration

**File:** `app/routes/vendor_pages.py`

```python
@router.get("/{vendor_code}/[page-name]", response_class=HTMLResponse, include_in_schema=False)
async def vendor_[page_name]_page(
    request: Request,
    vendor_code: str = Path(..., description="Vendor code"),
    current_user: User = Depends(get_current_vendor_from_cookie_or_header)
):
    """
    Render [page name] page.
    JavaScript loads data via API.
    """
    return templates.TemplateResponse(
        "vendor/[page-name].html",
        {
            "request": request,
            "user": current_user,
            "vendor_code": vendor_code,
        }
    )
```

---

### 4. Sidebar Navigation

**File:** `app/templates/vendor/partials/sidebar.html`

```jinja2
<li class="relative px-6 py-3">
    <span x-show="currentPage === '[page-name]'"
          class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg"
          aria-hidden="true"></span>
    <a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
       :class="currentPage === '[page-name]' ? 'text-gray-800 dark:text-gray-100' : ''"
       :href="`/vendor/${vendorCode}/[page-name]`">
        <span x-html="$icon('[icon-name]', 'w-5 h-5')"></span>
        <span class="ml-4">[Page Display Name]</span>
    </a>
</li>
```

---

## 🎨 Common Patterns

### Pattern 1: Simple Data List

Use for: Product list, order list, customer list

```javascript
async init() {
    // Call parent init first
    const parentInit = data().init;
    if (parentInit) {
        await parentInit.call(this);
    }

    await this.loadData();
}

async loadData() {
    this.loading = true;
    try {
        const response = await apiClient.get(`/vendor/items`);
        this.items = response.items || [];
    } catch (error) {
        this.error = error.message;
    } finally {
        this.loading = false;
    }
}
```

### Pattern 2: Dashboard with Stats

Use for: Dashboard, analytics pages

```javascript
async init() {
    // Call parent init first
    const parentInit = data().init;
    if (parentInit) {
        await parentInit.call(this);
    }

    await Promise.all([
        this.loadStats(),
        this.loadRecentActivity()
    ]);
}

async loadStats() {
    const stats = await apiClient.get(`/vendor/stats`);
    this.stats = stats;
}
```

### Pattern 3: Detail Page

Use for: Product detail, order detail

```javascript
async init() {
    // Call parent init first
    const parentInit = data().init;
    if (parentInit) {
        await parentInit.call(this);
    }

    await this.loadItem();
}

async loadItem() {
    const id = this.getItemIdFromUrl();
    this.item = await apiClient.get(`/vendor/items/${id}`);
}
```

### Pattern 4: Simple Page (No Custom JavaScript)

Use for: Coming soon pages, static pages, pages under development

**Template:** `app/templates/vendor/[page-name].html`
```jinja2
{# app/templates/vendor/products.html #}
{% extends "vendor/base.html" %}

{% block title %}Products{% endblock %}

{# Use base data() directly - no custom JavaScript needed #}
{% block alpine_data %}data(){% endblock %}

{% block content %}
<div class="flex items-center justify-between my-6">
    <h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
        Products
    </h2>
</div>

<!-- Coming Soon Notice -->
<div class="w-full mb-8 overflow-hidden rounded-lg shadow-xs">
    <div class="w-full p-12 bg-white dark:bg-gray-800 text-center">
        <div class="text-6xl mb-4">📦</div>
        <h3 class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-2">
            Products Management Coming Soon
        </h3>
        <p class="text-gray-600 dark:text-gray-400 mb-6">
            This page is under development.
        </p>
        <a href="/vendor/{{ vendor_code }}/dashboard"
           class="inline-flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700">
            Back to Dashboard
        </a>
    </div>
</div>
{% endblock %}
```

**No JavaScript file needed!** The page inherits all functionality from `init-alpine.js`.

### Pattern 5: Form with Validation

Use for: Settings, profile edit

```javascript
formData: {
    name: '',
    email: ''
},
errors: {},

validateForm() {
    this.errors = {};
    if (!this.formData.name) this.errors.name = 'Name is required';
    if (!this.formData.email) this.errors.email = 'Email is required';
    return Object.keys(this.errors).length === 0;
},

async saveForm() {
    if (!this.validateForm()) return;
    await apiClient.put(`/vendor/settings`, this.formData);
}
```

---

## 🔧 Best Practices

### 1. Error Handling
```javascript
try {
    await apiClient.get('/endpoint');
} catch (error) {
    // Use dedicated page logger
    vendorPageLog.error('Operation failed', error);
    this.error = error.message || 'An error occurred';
    // Don't throw - let UI handle gracefully
}
```

### 2. Loading States
```javascript
// Always set loading at start and end
this.loading = true;
try {
    // ... operations
} finally {
    this.loading = false; // Always executes
}
```

### 3. Data Refresh
```javascript
async refresh() {
    // Clear error before refresh
    this.error = '';
    await this.loadData();
}
```

### 4. Modal Management
```javascript
openModal() {
    this.showModal = true;
    // Reset form
    this.formData = {};
    this.errors = {};
}

closeModal() {
    this.showModal = false;
    // Clean up
    this.formData = {};
}
```

### 5. Debouncing
```html
<!-- Debounce search input -->
<input
    x-model="filters.search"
    @input.debounce.300ms="applyFilters()"
/>
```

### 6. Inherited State from Base (init-alpine.js)

All vendor pages automatically inherit these properties from the base `data()` function:

```javascript
// ✅ Available in all pages via ...data() spread
{
    // Vendor context (set by parent init)
    vendorCode: '',        // Extracted from URL path
    vendor: null,          // Loaded from API
    currentUser: {},       // Loaded from localStorage

    // UI state
    dark: false,           // Dark mode toggle
    isSideMenuOpen: false,
    isNotificationsMenuOpen: false,
    isProfileMenuOpen: false,
    currentPage: '',       // Override this in your component

    // Methods
    init() { ... },        // MUST call this via parent init pattern
    loadVendorInfo() { ... },
    handleLogout() { ... }
}
```

**Important:** Always call parent `init()` before your page logic:
```javascript
async init() {
    const parentInit = data().init;
    if (parentInit) {
        await parentInit.call(this);
    }
    // Now vendorCode and vendor are available
    await this.loadData();
}
```

---

## 📱 Responsive Design Checklist

- [ ] Table scrolls horizontally on mobile
- [ ] Modal is scrollable on small screens
- [ ] Filters stack vertically on mobile
- [ ] Action buttons adapt to screen size
- [ ] Text truncates appropriately
- [ ] Icons remain visible

---

## ✅ Testing Checklist

- [ ] Page loads without errors
- [ ] Data loads correctly
- [ ] Loading state displays
- [ ] Error state handles failures
- [ ] Empty state shows when no data
- [ ] Filters work correctly
- [ ] Pagination works
- [ ] Create operation works
- [ ] Edit operation works
- [ ] Delete operation works
- [ ] Modal opens/closes
- [ ] Form validation works
- [ ] Dark mode works
- [ ] Mobile responsive

---

## 🚀 Quick Start Commands

```bash
# Create new page files
touch app/templates/vendor/products.html
touch app/static/vendor/js/products.js

# Copy templates
cp template.html app/templates/vendor/products.html
cp template.js app/static/vendor/js/products.js

# Update files with your page name
# Register route in vendor_pages.py
# Add sidebar link
# Test!
```

---

## 📚 Additional Resources

### Helpers and Utilities

- **Icons**: Use `$icon('icon-name', 'classes')` helper from `shared/js/icons.js`
- **API Client**: Automatically handles auth tokens, prepends `/api/v1` to paths
- **Logging**: Create dedicated logger per page: `const myPageLog = window.LogConfig.loggers.pagename;`
- **Date Formatting**: Use `formatDate()` helper (available in your component)
- **Currency**: Use `formatCurrency()` helper (available in your component)

### Reusable Partials

You can include reusable template partials in your pages:

```jinja2
{# Display vendor information card #}
{% include 'vendor/partials/vendor_info.html' %}

{# Already included in base.html #}
{% include 'vendor/partials/sidebar.html' %}
{% include 'vendor/partials/header.html' %}
```

### API Endpoint Pattern

All vendor API calls follow this pattern:
- **JavaScript**: `apiClient.get('/vendor/endpoint')`
- **Becomes**: `/api/v1/vendor/endpoint`
- **Middleware**: Automatically detects vendor from cookie/header context
- **No need** to include `vendorCode` in API path

### Script Loading Order (from base.html)

The base template loads scripts in this specific order:
1. Log Configuration (`log-config.js`)
2. Icons (`icons.js`)
3. Alpine Base Data (`init-alpine.js`) - provides `data()` function
4. Utils (`utils.js`)
5. API Client (`api-client.js`)
6. Alpine.js library (deferred)
7. Page-specific scripts (your custom JS)

---

This template provides a complete, production-ready pattern for building vendor admin pages with consistent structure, error handling, and user experience.

---

## 🎯 Real-World Example: Marketplace Import Page

The marketplace import page is a comprehensive real-world implementation demonstrating all best practices.

### Implementation Files

**Template**: `app/templates/vendor/marketplace.html`
**JavaScript**: `static/vendor/js/marketplace.js`
**Route**: `app/routes/vendor_pages.py` - `vendor_marketplace_page()`

### Key Features Demonstrated

#### 1. Complete Form Handling
```javascript
// Import form with validation
importForm: {
    csv_url: '',
    marketplace: 'Letzshop',
    language: 'fr',
    batch_size: 1000
},

async startImport() {
    if (!this.importForm.csv_url) {
        this.error = 'Please enter a CSV URL';
        return;
    }
    
    this.importing = true;
    try {
        const response = await apiClient.post('/vendor/marketplace/import', {
            source_url: this.importForm.csv_url,
            marketplace: this.importForm.marketplace,
            batch_size: this.importForm.batch_size
        });
        
        this.successMessage = `Import job #${response.job_id} started!`;
        await this.loadJobs();  // Refresh list
    } catch (error) {
        this.error = error.message;
    } finally {
        this.importing = false;
    }
}
```

#### 2. Auto-Refresh for Active Jobs
```javascript
startAutoRefresh() {
    this.autoRefreshInterval = setInterval(async () => {
        const hasActiveJobs = this.jobs.some(job =>
            job.status === 'pending' || job.status === 'processing'
        );
        
        if (hasActiveJobs) {
            await this.loadJobs();
        }
    }, 10000);  // Every 10 seconds
}
```

#### 3. Quick Fill from Settings
```javascript
// Load vendor settings
async loadVendorSettings() {
    const response = await apiClient.get('/vendor/settings');
    this.vendorSettings = {
        letzshop_csv_url_fr: response.letzshop_csv_url_fr || '',
        letzshop_csv_url_en: response.letzshop_csv_url_en || '',
        letzshop_csv_url_de: response.letzshop_csv_url_de || ''
    };
}

// Quick fill function
quickFill(language) {
    const urlMap = {
        'fr': this.vendorSettings.letzshop_csv_url_fr,
        'en': this.vendorSettings.letzshop_csv_url_en,
        'de': this.vendorSettings.letzshop_csv_url_de
    };
    
    if (urlMap[language]) {
        this.importForm.csv_url = urlMap[language];
        this.importForm.language = language;
    }
}
```

#### 4. Job Details Modal
```javascript
async viewJobDetails(jobId) {
    try {
        const response = await apiClient.get(`/vendor/marketplace/imports/${jobId}`);
        this.selectedJob = response;
        this.showJobModal = true;
    } catch (error) {
        this.error = error.message;
    }
}
```

#### 5. Pagination
```javascript
async nextPage() {
    if (this.page * this.limit < this.totalJobs) {
        this.page++;
        await this.loadJobs();
    }
}
```

#### 6. Utility Functions
```javascript
formatDate(dateString) {
    if (!dateString) return 'N/A';
    const date = new Date(dateString);
    return date.toLocaleString('en-US', {
        year: 'numeric',
        month: 'short',
        day: 'numeric',
        hour: '2-digit',
        minute: '2-digit'
    });
}

calculateDuration(job) {
    if (!job.started_at) return 'Not started';
    
    const start = new Date(job.started_at);
    const end = job.completed_at ? new Date(job.completed_at) : new Date();
    const durationMs = end - start;
    
    const seconds = Math.floor(durationMs / 1000);
    const minutes = Math.floor(seconds / 60);
    const hours = Math.floor(minutes / 60);
    
    if (hours > 0) {
        return `${hours}h ${minutes % 60}m`;
    } else if (minutes > 0) {
        return `${minutes}m ${seconds % 60}s`;
    }
    return `${seconds}s`;
}
```

### Template Features

#### Dynamic Status Badges
```html
<span class="px-2 py-1 font-semibold leading-tight rounded-full text-xs"
      :class="{
          'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': job.status === 'completed',
          'text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100': job.status === 'processing',
          'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': job.status === 'pending',
          'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100': job.status === 'failed'
      }"
      x-text="job.status.toUpperCase()">
</span>
```

#### Conditional Display
```html
<!-- Quick fill buttons -->
<button
    type="button"
    @click="quickFill('fr')"
    x-show="vendorSettings.letzshop_csv_url_fr"
    class="...">
    <span x-html="$icon('lightning-bolt', 'w-3 h-3 mr-1')"></span>
    French CSV
</button>
```

#### Progress Metrics
```html
<div class="space-y-1">
    <div class="text-xs text-gray-600 dark:text-gray-400">
        <span class="text-green-600" x-text="job.imported_count"></span> imported,
        <span class="text-blue-600" x-text="job.updated_count"></span> updated
    </div>
    <div x-show="job.error_count > 0" class="text-xs text-red-600">
        <span x-text="job.error_count"></span> errors
    </div>
</div>
```

---

## 📚 Related Documentation

- [Marketplace Integration Guide](../../guides/marketplace-integration.md) - Complete marketplace system documentation
- [Admin Page Templates](../admin/page-templates.md) - Admin page patterns
- [Icons Guide](../../development/icons-guide.md) - Available icons

