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

## 📋 Overview

This guide provides complete templates for creating new admin pages using the established Alpine.js + Jinja2 architecture. Follow these patterns to ensure consistency, proper initialization, and optimal performance across the admin portal.

---

## 🎯 Quick Reference

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

### Checklist for New Page
- [ ] Create Jinja2 template extending admin/base.html
- [ ] Create Alpine.js JavaScript component
- [ ] Use centralized logger (one line!)
- [ ] Add ...data() for base inheritance
- [ ] Set currentPage identifier
- [ ] Add initialization guard
- [ ] Use lowercase apiClient
- [ ] Register route in pages.py
- [ ] Add sidebar navigation link
- [ ] Test authentication
- [ ] Test dark mode
- [ ] Verify no duplicate initialization

---

## 📄 Template Structure

### 1. Jinja2 Template

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

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

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

{# ✅ CRITICAL: Alpine.js component name #}
{% block alpine_data %}admin[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 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200">
    <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.is_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.is_active ? 'Active' : 'Inactive'"></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 x-show="pagination.totalPages > 1"
         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 x-text="pagination.from"></span>
            -
            <span x-text="pagination.to"></span>
            of
            <span x-text="pagination.total"></span>
        </span>
        <span class="col-span-2"></span>
        <!-- Pagination buttons -->
        <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.currentPage === 1"
                            class="px-3 py-1 rounded-md rounded-l-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
                            aria-label="Previous"
                        >
                            <span x-html="$icon('chevron-left', 'w-4 h-4')"></span>
                        </button>
                    </li>
                    
                    <template x-for="page in paginationRange" :key="page">
                        <li>
                            <button
                                @click="goToPage(page)"
                                :class="page === pagination.currentPage 
                                    ? 'text-white bg-purple-600' 
                                    : 'hover:bg-gray-100 dark:hover:bg-gray-700'"
                                class="px-3 py-1 rounded-md focus:outline-none focus:shadow-outline-purple"
                                x-text="page"
                            ></button>
                        </li>
                    </template>
                    
                    <li>
                        <button
                            @click="nextPage()"
                            :disabled="pagination.currentPage === pagination.totalPages"
                            class="px-3 py-1 rounded-md rounded-r-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
                            aria-label="Next"
                        >
                            <span x-html="$icon('chevron-right', 'w-4 h-4')"></span>
                        </button>
                    </li>
                </ul>
            </nav>
        </span>
    </div>
</div>

<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- CREATE/EDIT MODAL                                                -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div x-show="showModal"
     x-cloak
     class="fixed inset-0 z-50 flex items-end bg-black bg-opacity-50 sm:items-center sm:justify-center"
     @click.away="closeModal()"
     @keydown.escape.window="closeModal()">
    
    <div class="w-full px-6 py-4 overflow-hidden bg-white rounded-t-lg dark:bg-gray-800 sm:rounded-lg sm:m-4 sm:max-w-xl">
        
        <!-- Modal header -->
        <div class="flex items-center justify-between pb-4 border-b dark:border-gray-700">
            <h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200"
                x-text="modalMode === 'create' ? 'Create New Item' : 'Edit Item'"></h3>
            <button
                @click="closeModal()"
                class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
            >
                <span x-html="$icon('x', 'w-6 h-6')"></span>
            </button>
        </div>
        
        <!-- Modal body -->
        <div class="mt-4">
            <form @submit.prevent="saveItem()">
                
                <!-- Name Field -->
                <label class="block text-sm mb-4">
                    <span class="text-gray-700 dark:text-gray-400">Name</span>
                    <input
                        x-model="formData.name"
                        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"
                        :class="errors.name ? 'border-red-600' : ''"
                        placeholder="Enter name"
                    />
                    <span x-show="errors.name" 
                          class="text-xs text-red-600 dark:text-red-400" 
                          x-text="errors.name"></span>
                </label>
                
                <!-- Description Field -->
                <label class="block text-sm mb-4">
                    <span class="text-gray-700 dark:text-gray-400">Description</span>
                    <textarea
                        x-model="formData.description"
                        class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 form-textarea focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray"
                        rows="3"
                        placeholder="Enter description"
                    ></textarea>
                </label>
                
                <!-- Status Field -->
                <label class="block text-sm mb-4">
                    <span class="text-gray-700 dark:text-gray-400">Status</span>
                    <select
                        x-model="formData.is_active"
                        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="true">Active</option>
                        <option :value="false">Inactive</option>
                    </select>
                </label>
                
                <!-- Modal footer -->
                <div class="flex flex-col items-center justify-end px-6 py-3 -mx-6 -mb-4 space-y-4 sm:space-y-0 sm:space-x-6 sm:flex-row bg-gray-50 dark:bg-gray-800">
                    <button
                        @click="closeModal()"
                        type="button"
                        class="w-full px-5 py-3 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 border border-gray-300 rounded-lg dark:text-gray-400 sm:px-4 sm:py-2 sm:w-auto active:bg-transparent hover:border-gray-500 focus:border-gray-500 active:text-gray-500 focus:outline-none focus:shadow-outline-gray"
                    >
                        Cancel
                    </button>
                    <button
                        type="submit"
                        :disabled="saving"
                        class="w-full px-5 py-3 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg sm:w-auto sm:px-4 sm:py-2 active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
                    >
                        <span x-show="!saving" x-text="modalMode === 'create' ? 'Create' : 'Save'"></span>
                        <span x-show="saving">Saving...</span>
                    </button>
                </div>
            </form>
        </div>
    </div>
</div>

{% endblock %}

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

---

### 2. Alpine.js Component

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

```javascript
// static/admin/js/[page-name].js
/**
 * [Page Name] Component
 * Handles [describe functionality]
 */

// ✅ CRITICAL: Use centralized logger (ONE LINE!)
const pageLog = window.LogConfig.loggers.[pageName];
// OR create custom logger if not pre-configured:
// const pageLog = window.LogConfig.createLogger('[PAGE-NAME]', window.LogConfig.logLevel);

/**
 * Main Alpine.js component for [page name]
 */
function admin[PageName]() {
    return {
        // ─────────────────────────────────────────────────────
        // ✅ CRITICAL: INHERIT BASE LAYOUT
        // ─────────────────────────────────────────────────────
        ...data(),
        
        // ✅ CRITICAL: SET PAGE IDENTIFIER
        currentPage: '[page-name]',
        
        // ─────────────────────────────────────────────────────
        // STATE
        // ─────────────────────────────────────────────────────
        
        // Loading states
        loading: false,
        saving: false,
        
        // Error handling
        error: null,
        errors: {},
        
        // Data
        items: [],
        currentItem: null,
        
        // Pagination
        pagination: {
            currentPage: 1,
            totalPages: 1,
            perPage: 10,
            total: 0,
            from: 0,
            to: 0
        },
        
        // Filters
        filters: {
            search: '',
            status: '',
            sortBy: 'created_at:desc'
        },
        
        // Modal
        showModal: false,
        modalMode: 'create', // 'create' or 'edit'
        formData: {
            name: '',
            description: '',
            is_active: true
        },
        
        // ─────────────────────────────────────────────────────
        // LIFECYCLE
        // ─────────────────────────────────────────────────────
        
        /**
         * ✅ CRITICAL: Initialize component with guard
         */
        async init() {
            pageLog.info('=== [PAGE NAME] INITIALIZING ===');
            
            // ✅ CRITICAL: Initialization guard
            if (window._[pageName]Initialized) {
                pageLog.warn('Already initialized, skipping...');
                return;
            }
            window._[pageName]Initialized = true;
            
            // Track performance
            const startTime = performance.now();
            
            try {
                // Load initial data
                await this.loadData();
                
                // Log performance
                const duration = performance.now() - startTime;
                window.LogConfig.logPerformance('[Page Name] Init', duration);
                
                pageLog.info('=== [PAGE NAME] INITIALIZATION COMPLETE ===');
                
            } catch (error) {
                window.LogConfig.logError(error, '[Page Name] Init');
                this.error = 'Failed to initialize page';
            }
        },
        
        // ─────────────────────────────────────────────────────
        // DATA LOADING
        // ─────────────────────────────────────────────────────
        
        /**
         * Load main data from API
         */
        async loadData() {
            pageLog.info('Loading data...');
            this.loading = true;
            this.error = null;
            
            try {
                const startTime = performance.now();
                
                // Build query params
                const params = new URLSearchParams({
                    page: this.pagination.currentPage,
                    per_page: this.pagination.perPage,
                    search: this.filters.search,
                    status: this.filters.status,
                    sort_by: this.filters.sortBy
                });
                
                const url = `/api/v1/admin/items?${params}`;
                
                // Log API request
                window.LogConfig.logApiCall('GET', url, null, 'request');
                
                // ✅ CRITICAL: Use lowercase apiClient
                const response = await apiClient.get(url);
                
                // Log API response
                window.LogConfig.logApiCall('GET', url, response, 'response');
                
                // Update state
                this.items = response.items || [];
                this.pagination.total = response.total || 0;
                this.pagination.totalPages = Math.ceil(
                    this.pagination.total / this.pagination.perPage
                );
                this.pagination.from = (this.pagination.currentPage - 1) * this.pagination.perPage + 1;
                this.pagination.to = Math.min(
                    this.pagination.currentPage * this.pagination.perPage,
                    this.pagination.total
                );
                
                // Log performance
                const duration = performance.now() - startTime;
                window.LogConfig.logPerformance('Load Data', duration);
                
                pageLog.info(`Data loaded successfully`, {
                    count: this.items.length,
                    duration: `${duration}ms`,
                    page: this.pagination.currentPage
                });
                
            } catch (error) {
                window.LogConfig.logError(error, 'Load Data');
                this.error = error.message || 'Failed to load data';
                Utils.showToast('Failed to load data', 'error');
            } finally {
                this.loading = false;
            }
        },
        
        /**
         * Refresh data
         */
        async refresh() {
            pageLog.info('Refreshing data...');
            this.error = null;
            await this.loadData();
            Utils.showToast('Data refreshed', 'success');
        },
        
        // ─────────────────────────────────────────────────────
        // FILTERS & SEARCH
        // ─────────────────────────────────────────────────────
        
        /**
         * Apply filters and reload data
         */
        async applyFilters() {
            pageLog.debug('Applying filters:', this.filters);
            this.pagination.currentPage = 1; // Reset to first page
            await this.loadData();
        },
        
        /**
         * Reset filters to default
         */
        async resetFilters() {
            this.filters = {
                search: '',
                status: '',
                sortBy: 'created_at:desc'
            };
            await this.applyFilters();
        },
        
        // ─────────────────────────────────────────────────────
        // PAGINATION
        // ─────────────────────────────────────────────────────
        
        /**
         * Navigate to specific page
         */
        async goToPage(page) {
            if (page < 1 || page > this.pagination.totalPages) return;
            this.pagination.currentPage = page;
            await this.loadData();
        },
        
        /**
         * Go to previous page
         */
        async previousPage() {
            await this.goToPage(this.pagination.currentPage - 1);
        },
        
        /**
         * Go to next page
         */
        async nextPage() {
            await this.goToPage(this.pagination.currentPage + 1);
        },
        
        /**
         * Get pagination range for display
         */
        get paginationRange() {
            const current = this.pagination.currentPage;
            const total = this.pagination.totalPages;
            const range = [];
            
            // Show max 5 page numbers
            let start = Math.max(1, current - 2);
            let end = Math.min(total, start + 4);
            
            // Adjust start if we're near the end
            if (end - start < 4) {
                start = Math.max(1, end - 4);
            }
            
            for (let i = start; i <= end; i++) {
                range.push(i);
            }
            
            return range;
        },
        
        // ─────────────────────────────────────────────────────
        // CRUD OPERATIONS
        // ─────────────────────────────────────────────────────
        
        /**
         * Open create modal
         */
        openCreateModal() {
            pageLog.info('Opening create modal');
            this.modalMode = 'create';
            this.formData = {
                name: '',
                description: '',
                is_active: true
            };
            this.errors = {};
            this.showModal = true;
        },
        
        /**
         * Open edit modal
         */
        async editItem(itemId) {
            pageLog.info('Opening edit modal for item:', itemId);
            
            try {
                this.modalMode = 'edit';
                
                // Load item details
                const url = `/api/v1/admin/items/${itemId}`;
                window.LogConfig.logApiCall('GET', url, null, 'request');
                
                const item = await apiClient.get(url);
                
                window.LogConfig.logApiCall('GET', url, item, 'response');
                
                // Populate form
                this.formData = { ...item };
                this.currentItem = item;
                this.errors = {};
                this.showModal = true;
                
            } catch (error) {
                window.LogConfig.logError(error, 'Edit Item');
                Utils.showToast('Failed to load item', 'error');
            }
        },
        
        /**
         * View item details
         */
        async viewItem(itemId) {
            pageLog.info('Viewing item:', itemId);
            // Navigate to detail page or open view modal
            window.location.href = `/admin/items/${itemId}`;
        },
        
        /**
         * Save item (create or update)
         */
        async saveItem() {
            pageLog.info('Saving item...');
            
            // Validate form
            if (!this.validateForm()) {
                pageLog.warn('Form validation failed');
                return;
            }
            
            this.saving = true;
            this.errors = {};
            
            try {
                let response;
                
                if (this.modalMode === 'create') {
                    // Create new item
                    const url = '/api/v1/admin/items';
                    window.LogConfig.logApiCall('POST', url, this.formData, 'request');
                    
                    response = await apiClient.post(url, this.formData);
                    
                    window.LogConfig.logApiCall('POST', url, response, 'response');
                    
                    pageLog.info('Item created successfully');
                    Utils.showToast('Item created successfully', 'success');
                    
                } else {
                    // Update existing item
                    const url = `/api/v1/admin/items/${this.currentItem.id}`;
                    window.LogConfig.logApiCall('PUT', url, this.formData, 'request');
                    
                    response = await apiClient.put(url, this.formData);
                    
                    window.LogConfig.logApiCall('PUT', url, response, 'response');
                    
                    pageLog.info('Item updated successfully');
                    Utils.showToast('Item updated successfully', 'success');
                }
                
                // Close modal and reload data
                this.closeModal();
                await this.loadData();
                
            } catch (error) {
                window.LogConfig.logError(error, 'Save Item');
                
                // Handle validation errors
                if (error.details && error.details.validation_errors) {
                    this.errors = error.details.validation_errors;
                } else {
                    Utils.showToast('Failed to save item', 'error');
                }
            } finally {
                this.saving = false;
            }
        },
        
        /**
         * Delete item
         */
        async deleteItem(itemId) {
            pageLog.info('Deleting item:', itemId);
            
            // Confirm deletion
            if (!confirm('Are you sure you want to delete this item?')) {
                return;
            }
            
            try {
                const url = `/api/v1/admin/items/${itemId}`;
                window.LogConfig.logApiCall('DELETE', url, null, 'request');
                
                await apiClient.delete(url);
                
                window.LogConfig.logApiCall('DELETE', url, null, 'response');
                
                pageLog.info('Item deleted successfully');
                Utils.showToast('Item deleted successfully', 'success');
                
                // Reload data
                await this.loadData();
                
            } catch (error) {
                window.LogConfig.logError(error, 'Delete Item');
                Utils.showToast('Failed to delete item', 'error');
            }
        },
        
        /**
         * Close modal
         */
        closeModal() {
            this.showModal = false;
            this.formData = {};
            this.errors = {};
            this.currentItem = null;
        },
        
        // ─────────────────────────────────────────────────────
        // VALIDATION
        // ─────────────────────────────────────────────────────
        
        /**
         * Validate form data
         */
        validateForm() {
            this.errors = {};
            
            if (!this.formData.name || this.formData.name.trim() === '') {
                this.errors.name = 'Name is required';
            }
            
            // Add more validation rules as needed
            
            return Object.keys(this.errors).length === 0;
        },
        
        // ─────────────────────────────────────────────────────
        // HELPERS
        // ─────────────────────────────────────────────────────
        
        /**
         * Format date for display
         */
        formatDate(dateString) {
            if (!dateString) return '-';
            return Utils.formatDate(dateString);
        },
        
        /**
         * Truncate text
         */
        truncate(text, length = 50) {
            if (!text) return '';
            if (text.length <= length) return text;
            return text.substring(0, length) + '...';
        }
    };
}

// Make available globally
window.admin[PageName] = admin[PageName];

pageLog.info('[Page Name] module loaded');
```

---

### 3. Route Registration

**File:** `app/api/v1/admin/pages.py`

```python
from fastapi import APIRouter, Request, Depends
from app.api.deps import get_current_admin_from_cookie_or_header
from app.models.database.user import User

router = APIRouter()

@router.get("/admin/[page-route]")
async def [page_name]_page(
    request: Request,
    current_user: User = Depends(get_current_admin_from_cookie_or_header)
):
    """
    [Page Name] page
    Displays [description]

    Requires admin authentication.
    """
    return templates.TemplateResponse(
        "admin/[page-name].html",
        {
            "request": request,
            "user": current_user,
        }
    )
```

---

### 4. Sidebar Navigation

**File:** `app/templates/admin/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="/admin/[page-route]">
        <span x-html="$icon('[icon-name]', 'w-5 h-5')"></span>
        <span class="ml-4">[Page Display Name]</span>
    </a>
</li>
```

---

## 🎨 Common Page Patterns

### Pattern 1: Simple Data List (GET)

**Use for:** Vendor list, user list, product list

```javascript
async init() {
    if (window._myPageInitialized) return;
    window._myPageInitialized = true;
    await this.loadData();
}

async loadData() {
    this.loading = true;
    try {
        const response = await apiClient.get('/api/v1/admin/items');
        this.items = response.items || [];
    } finally {
        this.loading = false;
    }
}
```

---

### Pattern 2: Dashboard with Stats

**Use for:** Dashboard, analytics pages

```javascript
async init() {
    if (window._dashboardInitialized) return;
    window._dashboardInitialized = true;
    
    await Promise.all([
        this.loadStats(),
        this.loadRecentActivity()
    ]);
}

async loadStats() {
    this.stats = await apiClient.get('/api/v1/admin/stats');
}

async loadRecentActivity() {
    this.activity = await apiClient.get('/api/v1/admin/recent-activity');
}
```

---

### Pattern 3: Single Item Detail/Edit

**Use for:** Vendor edit, user edit

```javascript
async init() {
    if (window._editPageInitialized) return;
    window._editPageInitialized = true;
    
    const itemId = this.getItemIdFromUrl();
    await this.loadItem(itemId);
}

async loadItem(id) {
    this.item = await apiClient.get(`/api/v1/admin/items/${id}`);
    // Populate form with item data
    this.formData = { ...this.item };
}

async saveItem() {
    if (!this.validateForm()) return;
    
    await apiClient.put(
        `/api/v1/admin/items/${this.item.id}`,
        this.formData
    );
    
    Utils.showToast('Saved successfully', 'success');
}
```

---

### Pattern 4: With Filters and Pagination

**Use for:** Large lists with filtering

```javascript
filters: {
    search: '',
    status: '',
    sortBy: 'created_at:desc'
},

pagination: {
    currentPage: 1,
    totalPages: 1,
    perPage: 10
},

async loadData() {
    const params = new URLSearchParams({
        page: this.pagination.currentPage,
        per_page: this.pagination.perPage,
        ...this.filters
    });
    
    const response = await apiClient.get(
        `/api/v1/admin/items?${params}`
    );
    
    this.items = response.items;
    this.pagination.total = response.total;
    this.pagination.totalPages = Math.ceil(
        response.total / this.pagination.perPage
    );
}

async applyFilters() {
    this.pagination.currentPage = 1; // Reset to page 1
    await this.loadData();
}
```

---

## 🔧 Best Practices

### 1. Base Layout Inheritance

**✅ ALWAYS include ...data():**
```javascript
function adminMyPage() {
    return {
        ...data(),  // ← Must be first!
        currentPage: 'my-page',
        // Your state here
    };
}
```

### 2. Initialization Guards

**✅ ALWAYS use initialization guard:**
```javascript
async init() {
    if (window._myPageInitialized) {
        log.warn('Already initialized');
        return;
    }
    window._myPageInitialized = true;
    
    await this.loadData();
}
```

### 3. API Client Usage

**✅ Use lowercase apiClient:**
```javascript
// ✅ CORRECT
await apiClient.get('/endpoint');

// ❌ WRONG
await ApiClient.get('/endpoint');
await API_CLIENT.get('/endpoint');
```

### 4. Centralized Logging

**✅ Use pre-configured logger:**
```javascript
// ✅ CORRECT - One line!
const myLog = window.LogConfig.loggers.myPage;

// ❌ WRONG - 15+ lines
const myLog = {
    error: (...args) => console.error(...),
    // ... etc
};
```

### 5. Error Handling

**✅ Handle errors gracefully:**
```javascript
try {
    await this.loadData();
} catch (error) {
    window.LogConfig.logError(error, 'Load Data');
    this.error = error.message;
    Utils.showToast('Failed to load data', 'error');
    // Don't throw - let UI handle it
}
```

### 6. Loading States

**✅ Always set loading state:**
```javascript
this.loading = true;
try {
    // ... operations
} finally {
    this.loading = false; // Always executes
}
```

### 7. Performance Tracking

**✅ Track performance for slow operations:**
```javascript
const startTime = performance.now();
await this.loadData();
const duration = performance.now() - startTime;
window.LogConfig.logPerformance('Load Data', duration);
```

---

## 📱 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
- [ ] Sidebar collapses on mobile
- [ ] Stats cards stack on mobile

---

## ✅ Testing Checklist

### Functionality
- [ ] 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

### Architecture
- [ ] Initialization guard works (no duplicate init)
- [ ] ...data() properly inherited
- [ ] currentPage set correctly
- [ ] Lowercase apiClient used
- [ ] Centralized logging used
- [ ] No console errors
- [ ] Dark mode works
- [ ] Mobile responsive

---

## 🚀 Quick Start Commands

```bash
# Create new page files
touch app/templates/admin/new-page.html
touch app/static/admin/js/new-page.js

# Copy templates (start from dashboard.js as base)
cp app/static/admin/js/dashboard.js app/static/admin/js/new-page.js

# Update files:
# 1. Change logger: dashLog → newPageLog
# 2. Change function: adminDashboard() → adminNewPage()
# 3. Change init flag: _dashboardInitialized → _newPageInitialized
# 4. Change currentPage: 'dashboard' → 'new-page'
# 5. Replace data loading logic
# 6. Update HTML template
# 7. Add route in pages.py
# 8. Add sidebar link
# 9. Test!
```

---

## 📚 Additional Resources

### Pre-configured Loggers
```javascript
window.LogConfig.loggers.dashboard
window.LogConfig.loggers.companies
window.LogConfig.loggers.vendors
window.LogConfig.loggers.vendorTheme
window.LogConfig.loggers.users
window.LogConfig.loggers.customers
window.LogConfig.loggers.products
window.LogConfig.loggers.orders
window.LogConfig.loggers.imports
window.LogConfig.loggers.audit
```

### From Base Data (via ...data())
- `this.dark` - Dark mode state
- `this.toggleTheme()` - Toggle theme
- `this.isSideMenuOpen` - Side menu state
- `this.toggleSideMenu()` - Toggle side menu
- `this.isProfileMenuOpen` - Profile menu state
- `this.toggleProfileMenu()` - Toggle profile menu

### Global Utilities
- `Utils.showToast(message, type, duration)`
- `Utils.formatDate(dateString)`
- `apiClient.get(url)`
- `apiClient.post(url, data)`
- `apiClient.put(url, data)`
- `apiClient.delete(url)`

### Icon Helper
```html
<span x-html="$icon('name', 'classes')"></span>
```

---

## ⚠️ Common Mistakes to Avoid

### 1. Missing ...data()
```javascript
// ❌ WRONG - No base inheritance
function myPage() {
    return {
        items: []
    };
}

// ✅ CORRECT
function myPage() {
    return {
        ...data(),  // Must be first!
        items: []
    };
}
```

### 2. Wrong API Client Case
```javascript
// ❌ WRONG
await ApiClient.get('/url');

// ✅ CORRECT
await apiClient.get('/url');
```

### 3. Missing Init Guard
```javascript
// ❌ WRONG
async init() {
    await this.loadData();
}

// ✅ CORRECT
async init() {
    if (window._myPageInitialized) return;
    window._myPageInitialized = true;
    await this.loadData();
}
```

### 4. Old Logging Pattern
```javascript
// ❌ WRONG - 15+ lines
const log = {
    error: (...args) => console.error(...),
    // ...
};

// ✅ CORRECT - 1 line
const log = window.LogConfig.loggers.myPage;
```

### 5. Missing currentPage
```javascript
// ❌ WRONG
return {
    ...data(),
    items: []
};

// ✅ CORRECT
return {
    ...data(),
    currentPage: 'my-page',  // Must set!
    items: []
};
```

---

This template provides a complete, production-ready pattern for building admin pages with consistent structure, proper initialization, comprehensive logging, and excellent maintainability.

---

## 🎯 Real-World Examples: Marketplace Import Pages

The marketplace import system provides two comprehensive real-world implementations demonstrating all best practices.

### 1. Self-Service Import (`/admin/marketplace`)

**Purpose**: Admin tool for triggering imports for any vendor

**Files**:
- **Template**: `app/templates/admin/marketplace.html`
- **JavaScript**: `static/admin/js/marketplace.js`
- **Route**: `app/routes/admin_pages.py` - `admin_marketplace_page()`

#### Key Features

##### Vendor Selection with Auto-Load
```javascript
// Load all vendors
async loadVendors() {
    const response = await apiClient.get('/admin/vendors?limit=1000');
    this.vendors = response.items || [];
}

// Handle vendor selection change
onVendorChange() {
    const vendorId = parseInt(this.importForm.vendor_id);
    this.selectedVendor = this.vendors.find(v => v.id === vendorId) || null;
}

// Quick fill from selected vendor's settings
quickFill(language) {
    if (!this.selectedVendor) return;
    
    const urlMap = {
        'fr': this.selectedVendor.letzshop_csv_url_fr,
        'en': this.selectedVendor.letzshop_csv_url_en,
        'de': this.selectedVendor.letzshop_csv_url_de
    };
    
    if (urlMap[language]) {
        this.importForm.csv_url = urlMap[language];
        this.importForm.language = language;
    }
}
```

##### Filter by Current User
```javascript
async loadJobs() {
    const params = new URLSearchParams({
        page: this.page,
        limit: this.limit,
        created_by_me: 'true'  // Only show jobs I triggered
    });
    
    const response = await apiClient.get(
        `/admin/marketplace-import-jobs?${params.toString()}`
    );
    
    this.jobs = response.items || [];
}
```

##### Vendor Name Helper
```javascript
getVendorName(vendorId) {
    const vendor = this.vendors.find(v => v.id === vendorId);
    return vendor ? `${vendor.name} (${vendor.vendor_code})` : `Vendor #${vendorId}`;
}
```

---

### 2. Platform Monitoring (`/admin/imports`)

**Purpose**: System-wide oversight of all import jobs

**Files**:
- **Template**: `app/templates/admin/imports.html`
- **JavaScript**: `static/admin/js/imports.js`
- **Route**: `app/routes/admin_pages.py` - `admin_imports_page()`

#### Key Features

##### Statistics Dashboard
```javascript
async loadStats() {
    const response = await apiClient.get('/admin/marketplace-import-jobs/stats');
    this.stats = {
        total: response.total || 0,
        active: (response.pending || 0) + (response.processing || 0),
        completed: response.completed || 0,
        failed: response.failed || 0
    };
}
```

**Template**:
```html
<!-- Stats Cards -->
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
    <!-- Total Jobs -->
    <div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
        <div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full">
            <span x-html="$icon('cube', 'w-5 h-5')"></span>
        </div>
        <div>
            <p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
                Total Jobs
            </p>
            <p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.total">
                0
            </p>
        </div>
    </div>
    <!-- Repeat for active, completed, failed -->
</div>
```

##### Advanced Filtering
```javascript
filters: {
    vendor_id: '',
    status: '',
    marketplace: '',
    created_by: ''  // 'me' or empty for all
},

async applyFilters() {
    this.page = 1;  // Reset to first page
    
    const params = new URLSearchParams({
        page: this.page,
        limit: this.limit
    });
    
    // Add filters
    if (this.filters.vendor_id) {
        params.append('vendor_id', this.filters.vendor_id);
    }
    if (this.filters.status) {
        params.append('status', this.filters.status);
    }
    if (this.filters.created_by === 'me') {
        params.append('created_by_me', 'true');
    }
    
    await this.loadJobs();
    await this.loadStats();  // Update stats based on filters
}
```

**Template**:
```html
<div class="grid gap-4 md:grid-cols-5">
    <!-- Vendor Filter -->
    <select x-model="filters.vendor_id" @change="applyFilters()">
        <option value="">All Vendors</option>
        <template x-for="vendor in vendors" :key="vendor.id">
            <option :value="vendor.id" 
                    x-text="`${vendor.name} (${vendor.vendor_code})`">
            </option>
        </template>
    </select>
    
    <!-- Status Filter -->
    <select x-model="filters.status" @change="applyFilters()">
        <option value="">All Statuses</option>
        <option value="pending">Pending</option>
        <option value="processing">Processing</option>
        <option value="completed">Completed</option>
        <option value="failed">Failed</option>
    </select>
    
    <!-- Creator Filter -->
    <select x-model="filters.created_by" @change="applyFilters()">
        <option value="">All Users</option>
        <option value="me">My Jobs Only</option>
    </select>
</div>
```

##### Enhanced Job Table
```html
<table>
    <thead>
        <tr>
            <th>Job ID</th>
            <th>Vendor</th>
            <th>Status</th>
            <th>Progress</th>
            <th>Created By</th> <!-- Extra column for platform monitoring -->
            <th>Actions</th>
        </tr>
    </thead>
    <tbody>
        <template x-for="job in jobs" :key="job.id">
            <tr>
                <td>#<span x-text="job.id"></span></td>
                <td><span x-text="getVendorName(job.vendor_id)"></span></td>
                <td><!-- Status badge --></td>
                <td><!-- Progress metrics --></td>
                <td><span x-text="job.created_by_name || 'System'"></span></td>
                <td><!-- Action buttons --></td>
            </tr>
        </template>
    </tbody>
</table>
```

---

## 🔄 Comparison: Two Admin Interfaces

| Feature | Self-Service (`/marketplace`) | Platform Monitoring (`/imports`) |
|---------|-------------------------------|----------------------------------|
| **Purpose** | Import products for vendors | Monitor all system imports |
| **Scope** | Personal (my jobs) | System-wide (all jobs) |
| **Primary Action** | Trigger new imports | View and analyze |
| **Jobs Shown** | Only jobs I triggered | All jobs (with filtering) |
| **Vendor Selection** | Required (select vendor to import for) | Optional (filter view) |
| **Statistics** | No | Yes (dashboard cards) |
| **Auto-Refresh** | 10 seconds | 15 seconds |
| **Filter Options** | Vendor, Status, Marketplace | Vendor, Status, Marketplace, Creator |
| **Use Case** | "I need to import for Vendor X" | "What's happening system-wide?" |

---

## 📋 Navigation Structure

### Sidebar Organization

```javascript
// Admin sidebar sections
{
    "Dashboard": [
        "Dashboard"
    ],
    "Platform Administration": [
        "Companies",
        "Vendors",
        "Users",
        "Customers",
        "Marketplace"
    ],
    "Content Management": [
        "Platform Homepage",
        "Content Pages",
        "Vendor Themes"
    ],
    "Developer Tools": [
        "Components",
        "Icons",
        "Testing Hub",
        "Code Quality"
    ],
    "Platform Monitoring": [
        "Import Jobs",
        "Application Logs"
    ],
    "Settings": [
        "Settings"
    ]
}
```

### Setting currentPage

```javascript
// marketplace.js
return {
    ...data(),
    currentPage: 'marketplace',  // Highlights "Marketplace" in sidebar
    // ...
};

// imports.js
return {
    ...data(),
    currentPage: 'imports',  // Highlights "Import Jobs" in sidebar
    // ...
};
```

### Collapsible Sections

Sidebar sections are collapsible with state persisted to localStorage:

```javascript
// Section keys used in openSections state
{
    platformAdmin: true,   // Platform Administration (default open)
    contentMgmt: false,    // Content Management
    devTools: false,       // Developer Tools
    monitoring: false      // Platform Monitoring
}

// Toggle a section
toggleSection('devTools');

// Check if section is open
if (openSections.devTools) { ... }
```

See [Sidebar Navigation](../shared/sidebar.md) for full documentation.

---

## 🎨 UI Patterns

### Success/Error Messages

```html
<!-- Success -->
<div x-show="successMessage" x-transition 
     class="mb-6 p-4 bg-green-100 border border-green-400 text-green-700 rounded-lg">
    <span x-html="$icon('check-circle', 'w-5 h-5 mr-3')"></span>
    <p class="font-semibold" x-text="successMessage"></p>
</div>

<!-- Error -->
<div x-show="error" x-transition 
     class="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg dark:bg-red-900/20">
    <span x-html="$icon('exclamation', 'w-5 h-5 mr-3')"></span>
    <div>
        <p class="font-semibold">Error</p>
        <p class="text-sm" x-text="error"></p>
    </div>
</div>
```

### Empty States

```html
<!-- Personalized empty state -->
<div x-show="!loading && jobs.length === 0" class="text-center py-12">
    <span x-html="$icon('inbox', 'inline w-12 h-12 text-gray-400 mb-4')"></span>
    <p class="text-gray-600 dark:text-gray-400">
        You haven't triggered any imports yet
    </p>
    <p class="text-sm text-gray-500">
        Start a new import using the form above
    </p>
</div>
```

### Loading States with Spinners

```html
<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 import jobs...</p>
</div>
```

### Modal Dialogs

```html
<div x-show="showJobModal" x-cloak @click.away="closeJobModal()"
     class="fixed inset-0 z-30 flex items-end bg-black bg-opacity-50 sm:items-center sm:justify-center"
     x-transition>
    <div class="w-full px-6 py-4 overflow-hidden bg-white rounded-t-lg dark:bg-gray-800 sm:rounded-lg sm:m-4 sm:max-w-2xl">
        <!-- Modal Header -->
        <div class="flex items-center justify-between mb-4">
            <h3 class="text-lg font-semibold">Import Job Details</h3>
            <button @click="closeJobModal()">
                <span x-html="$icon('close', 'w-5 h-5')"></span>
            </button>
        </div>
        
        <!-- Modal Content -->
        <div x-show="selectedJob">
            <!-- Job details grid -->
        </div>
        
        <!-- Modal Footer -->
        <div class="flex justify-end mt-6">
            <button @click="closeJobModal()" class="...">Close</button>
        </div>
    </div>
</div>
```

---

## 📚 Related Documentation

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

