feat: add PlatformSettings for pagination and vendor filter improvements
Platform Settings: - Add PlatformSettings utility in init-alpine.js with 5-min cache - Add Display tab in /admin/settings for rows_per_page config - Integrate PlatformSettings.getRowsPerPage() in all paginated pages - Standardize default per_page to 20 across all admin pages - Add documentation at docs/frontend/shared/platform-settings.md Architecture Rules: - Add JS-010: enforce PlatformSettings usage for pagination - Add JS-011: enforce standard pagination structure - Add JS-012: detect double /api/v1 prefix in apiClient calls - Implement all rules in validate_architecture.py Vendor Filter (Tom Select): - Add vendor filter to marketplace-products, vendor-products, customers, inventory, and vendor-themes pages - Add selectedVendor display panel with clear button - Add localStorage persistence for vendor selection - Fix double /api/v1 prefix in vendor-selector.js Bug Fixes: - Remove duplicate PlatformSettings from utils.js - Fix customers.js pagination structure (page_size → per_page) - Fix code-quality-violations.js pagination structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,87 @@
|
||||
{# app/templates/admin/customers.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
{% from 'shared/macros/headers.html' import page_header %}
|
||||
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/tables.html' import table_wrapper, table_header %}
|
||||
{% from 'shared/macros/pagination.html' import pagination_full %}
|
||||
{% from 'shared/macros/pagination.html' import pagination %}
|
||||
|
||||
{% block title %}Customers{% endblock %}
|
||||
|
||||
{% block alpine_data %}adminCustomers(){% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<!-- Tom Select CSS with local fallback -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/tom-select@2.4.1/dist/css/tom-select.default.min.css"
|
||||
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/vendor/tom-select.default.min.css') }}';"
|
||||
/>
|
||||
<style>
|
||||
/* Tom Select dark mode overrides */
|
||||
.dark .ts-wrapper .ts-control {
|
||||
background-color: rgb(55 65 81);
|
||||
border-color: rgb(75 85 99);
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ts-wrapper .ts-control input {
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ts-wrapper .ts-control input::placeholder {
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
.dark .ts-dropdown {
|
||||
background-color: rgb(55 65 81);
|
||||
border-color: rgb(75 85 99);
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ts-dropdown .option {
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ts-dropdown .option.active {
|
||||
background-color: rgb(147 51 234);
|
||||
color: white;
|
||||
}
|
||||
.dark .ts-dropdown .option:hover {
|
||||
background-color: rgb(75 85 99);
|
||||
}
|
||||
.dark .ts-wrapper.focus .ts-control {
|
||||
border-color: rgb(147 51 234);
|
||||
box-shadow: 0 0 0 1px rgb(147 51 234);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header('Customer Management') }}
|
||||
<!-- Page Header with Vendor Selector -->
|
||||
{% call page_header_flex(title='Customer Management', subtitle='Manage customers across all vendors') %}
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Vendor Autocomplete (Tom Select) -->
|
||||
<div class="w-80">
|
||||
<select id="vendor-select" x-ref="vendorSelect" placeholder="Filter by vendor...">
|
||||
</select>
|
||||
</div>
|
||||
{{ refresh_button(loading_var='loading', onclick='resetAndLoad()', variant='secondary') }}
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
<!-- Selected Vendor Info -->
|
||||
<div x-show="selectedVendor" x-transition class="mb-6 p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center">
|
||||
<span class="text-sm font-semibold text-purple-600 dark:text-purple-300" x-text="selectedVendor?.name?.charAt(0).toUpperCase()"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-purple-800 dark:text-purple-200" x-text="selectedVendor?.name"></span>
|
||||
<span class="ml-2 text-xs text-purple-600 dark:text-purple-400 font-mono" x-text="selectedVendor?.vendor_code"></span>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="clearVendorFilter()" class="text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 text-sm flex items-center gap-1">
|
||||
<span x-html="$icon('x', 'w-4 h-4')"></span>
|
||||
Clear filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ loading_state('Loading customers...') }}
|
||||
|
||||
@@ -109,17 +180,6 @@
|
||||
<option value="true">Active</option>
|
||||
<option value="false">Inactive</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
x-model="filters.vendor_id"
|
||||
@change="resetAndLoad()"
|
||||
class="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<option value="">All Vendors</option>
|
||||
<template x-for="vendor in vendors" :key="vendor.id">
|
||||
<option :value="vendor.id" x-text="vendor.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -235,11 +295,25 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Numbered Pagination -->
|
||||
{{ pagination_full(load_fn="loadCustomers()", item_label="customers") }}
|
||||
<!-- Pagination -->
|
||||
{{ pagination() }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<!-- Tom Select JS with local fallback -->
|
||||
<script>
|
||||
(function() {
|
||||
var script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/tom-select@2.4.1/dist/js/tom-select.complete.min.js';
|
||||
script.onerror = function() {
|
||||
console.warn('Tom Select CDN failed, loading local copy...');
|
||||
var fallbackScript = document.createElement('script');
|
||||
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/tom-select.complete.min.js") }}';
|
||||
document.head.appendChild(fallbackScript);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
})();
|
||||
</script>
|
||||
<script src="{{ url_for('static', path='admin/js/customers.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{# app/templates/admin/inventory.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
{% from 'shared/macros/pagination.html' import pagination %}
|
||||
{% from 'shared/macros/headers.html' import page_header %}
|
||||
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/tables.html' import table_wrapper %}
|
||||
{% from 'shared/macros/modals.html' import modal_simple %}
|
||||
@@ -12,7 +12,38 @@
|
||||
{% block alpine_data %}adminInventory(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header('Inventory', subtitle='Manage stock levels across all vendors') }}
|
||||
<!-- Page Header with Vendor Selector -->
|
||||
{% call page_header_flex(title='Inventory', subtitle='Manage stock levels across all vendors') %}
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Vendor Autocomplete (Tom Select) -->
|
||||
{{ vendor_selector(
|
||||
ref_name='vendorSelect',
|
||||
id='inventory-vendor-select',
|
||||
placeholder='Filter by vendor...',
|
||||
width='w-80'
|
||||
) }}
|
||||
{{ refresh_button(loading_var='loading', onclick='refresh()', variant='secondary') }}
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
<!-- Selected Vendor Info -->
|
||||
<div x-show="selectedVendor" x-transition class="mb-6 p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center">
|
||||
<span class="text-sm font-semibold text-purple-600 dark:text-purple-300" x-text="selectedVendor?.name?.charAt(0).toUpperCase()"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-purple-800 dark:text-purple-200" x-text="selectedVendor?.name"></span>
|
||||
<span class="ml-2 text-xs text-purple-600 dark:text-purple-400 font-mono" x-text="selectedVendor?.vendor_code"></span>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="clearVendorFilter()" class="text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 text-sm flex items-center gap-1">
|
||||
<span x-html="$icon('x', 'w-4 h-4')"></span>
|
||||
Clear filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ loading_state('Loading inventory...') }}
|
||||
|
||||
@@ -102,14 +133,6 @@
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<!-- Vendor Filter (Tom Select) -->
|
||||
{{ vendor_selector(
|
||||
ref_name='vendorSelect',
|
||||
id='inventory-vendor-select',
|
||||
placeholder='Filter by vendor...',
|
||||
width='w-64'
|
||||
) }}
|
||||
|
||||
<!-- Location Filter -->
|
||||
<select
|
||||
x-model="filters.location"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{# app/templates/admin/marketplace-products.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
{% from 'shared/macros/pagination.html' import pagination %}
|
||||
{% from 'shared/macros/headers.html' import page_header %}
|
||||
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/tables.html' import table_wrapper, table_header %}
|
||||
{% from 'shared/macros/modals.html' import modal_simple %}
|
||||
@@ -10,8 +10,79 @@
|
||||
|
||||
{% block alpine_data %}adminMarketplaceProducts(){% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<!-- Tom Select CSS with local fallback -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/tom-select@2.4.1/dist/css/tom-select.default.min.css"
|
||||
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/vendor/tom-select.default.min.css') }}';"
|
||||
/>
|
||||
<style>
|
||||
/* Tom Select dark mode overrides */
|
||||
.dark .ts-wrapper .ts-control {
|
||||
background-color: rgb(55 65 81);
|
||||
border-color: rgb(75 85 99);
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ts-wrapper .ts-control input {
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ts-wrapper .ts-control input::placeholder {
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
.dark .ts-dropdown {
|
||||
background-color: rgb(55 65 81);
|
||||
border-color: rgb(75 85 99);
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ts-dropdown .option {
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ts-dropdown .option.active {
|
||||
background-color: rgb(147 51 234);
|
||||
color: white;
|
||||
}
|
||||
.dark .ts-dropdown .option:hover {
|
||||
background-color: rgb(75 85 99);
|
||||
}
|
||||
.dark .ts-wrapper.focus .ts-control {
|
||||
border-color: rgb(147 51 234);
|
||||
box-shadow: 0 0 0 1px rgb(147 51 234);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header('Marketplace Products', subtitle='Master product repository - Browse all imported products from external sources') }}
|
||||
<!-- Page Header with Vendor Selector -->
|
||||
{% call page_header_flex(title='Marketplace Products', subtitle='Master product repository - Browse all imported products from external sources') %}
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Vendor Autocomplete (Tom Select) -->
|
||||
<div class="w-80">
|
||||
<select id="vendor-select" x-ref="vendorSelect" placeholder="Filter by vendor...">
|
||||
</select>
|
||||
</div>
|
||||
{{ refresh_button(loading_var='loading', onclick='refresh()', variant='secondary') }}
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
<!-- Selected Vendor Info -->
|
||||
<div x-show="selectedVendor" x-transition class="mb-6 p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center">
|
||||
<span class="text-sm font-semibold text-purple-600 dark:text-purple-300" x-text="selectedVendor?.name?.charAt(0).toUpperCase()"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-purple-800 dark:text-purple-200" x-text="selectedVendor?.name"></span>
|
||||
<span class="ml-2 text-xs text-purple-600 dark:text-purple-400 font-mono" x-text="selectedVendor?.vendor_code"></span>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="clearVendorFilter()" class="text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 text-sm flex items-center gap-1">
|
||||
<span x-html="$icon('x', 'w-4 h-4')"></span>
|
||||
Clear filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ loading_state('Loading products...') }}
|
||||
|
||||
@@ -128,18 +199,6 @@
|
||||
</template>
|
||||
</select>
|
||||
|
||||
<!-- Source Vendor Filter -->
|
||||
<select
|
||||
x-model="filters.vendor_name"
|
||||
@change="pagination.page = 1; loadProducts()"
|
||||
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none"
|
||||
>
|
||||
<option value="">All Source Vendors</option>
|
||||
<template x-for="v in sourceVendors" :key="v">
|
||||
<option :value="v" x-text="v"></option>
|
||||
</template>
|
||||
</select>
|
||||
|
||||
<!-- Product Type Filter -->
|
||||
<select
|
||||
x-model="filters.is_digital"
|
||||
@@ -161,16 +220,6 @@
|
||||
<option value="true">Active</option>
|
||||
<option value="false">Inactive</option>
|
||||
</select>
|
||||
|
||||
<!-- Refresh Button -->
|
||||
<button
|
||||
@click="refresh()"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
||||
title="Refresh products"
|
||||
>
|
||||
<span x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -233,7 +282,7 @@
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('database', 'w-12 h-12 mb-2 text-gray-300')"></span>
|
||||
<p class="font-medium">No marketplace products found</p>
|
||||
<p class="text-xs mt-1" x-text="filters.search || filters.marketplace || filters.is_active ? 'Try adjusting your search or filters' : 'Import products from the Import page'"></p>
|
||||
<p class="text-xs mt-1" x-text="filters.search || filters.marketplace || filters.is_active || selectedVendor ? 'Try adjusting your search or filters' : 'Import products from the Import page'"></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -409,5 +458,19 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<!-- Tom Select JS with local fallback -->
|
||||
<script>
|
||||
(function() {
|
||||
var script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/tom-select@2.4.1/dist/js/tom-select.complete.min.js';
|
||||
script.onerror = function() {
|
||||
console.warn('Tom Select CDN failed, loading local copy...');
|
||||
var fallbackScript = document.createElement('script');
|
||||
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/tom-select.complete.min.js") }}';
|
||||
document.head.appendChild(fallbackScript);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
})();
|
||||
</script>
|
||||
<script src="{{ url_for('static', path='admin/js/marketplace-products.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
<!-- Settings Categories Tabs -->
|
||||
{% call tabs_nav() %}
|
||||
{{ tab_button('display', 'Display', icon='view-grid') }}
|
||||
{{ tab_button('logging', 'Logging', icon='document-text') }}
|
||||
{{ tab_button('shipping', 'Shipping', icon='truck') }}
|
||||
{{ tab_button('system', 'System', icon='cog') }}
|
||||
@@ -25,6 +26,66 @@
|
||||
{{ tab_button('notifications', 'Notifications', icon='bell') }}
|
||||
{% endcall %}
|
||||
|
||||
<!-- Display Settings Tab -->
|
||||
<div x-show="activeTab === 'display'" x-transition>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-4">
|
||||
Display Configuration
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
Configure how data is displayed across the admin interface.
|
||||
</p>
|
||||
|
||||
<!-- Rows Per Page Setting -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Default Rows Per Page
|
||||
</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-3">
|
||||
Set the default number of rows shown in tables across the admin interface.
|
||||
</p>
|
||||
<div class="flex items-center gap-3">
|
||||
<template x-for="option in [10, 20, 50, 100]" :key="option">
|
||||
<button
|
||||
@click="displaySettings.rows_per_page = option"
|
||||
class="px-4 py-2 text-sm font-medium rounded-lg border transition-colors"
|
||||
:class="displaySettings.rows_per_page === option
|
||||
? 'bg-purple-600 text-white border-purple-600 dark:bg-purple-500 dark:border-purple-500'
|
||||
: 'text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600'"
|
||||
x-text="option"
|
||||
></button>
|
||||
</template>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
This setting applies to: Orders, Products, Customers, Inventory, and other tables.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="mb-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div class="flex items-start">
|
||||
<span x-html="$icon('information-circle', 'w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 mr-3 flex-shrink-0')"></span>
|
||||
<div class="text-sm text-blue-800 dark:text-blue-200">
|
||||
<p class="font-medium mb-1">When does this take effect?</p>
|
||||
<p>Changes to the rows per page setting will apply immediately to all admin tables when refreshed.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="flex items-center justify-end pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
@click="saveDisplaySettings()"
|
||||
:disabled="saving"
|
||||
class="px-6 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span x-show="!saving">Save Display Settings</span>
|
||||
<span x-show="saving">Saving...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logging Settings Tab -->
|
||||
<div x-show="activeTab === 'logging'" x-transition>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{# app/templates/admin/vendor-products.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
{% from 'shared/macros/pagination.html' import pagination %}
|
||||
{% from 'shared/macros/headers.html' import page_header %}
|
||||
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/tables.html' import table_wrapper %}
|
||||
{% from 'shared/macros/modals.html' import modal_simple %}
|
||||
@@ -10,8 +10,79 @@
|
||||
|
||||
{% block alpine_data %}adminVendorProducts(){% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<!-- Tom Select CSS with local fallback -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/tom-select@2.4.1/dist/css/tom-select.default.min.css"
|
||||
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/vendor/tom-select.default.min.css') }}';"
|
||||
/>
|
||||
<style>
|
||||
/* Tom Select dark mode overrides */
|
||||
.dark .ts-wrapper .ts-control {
|
||||
background-color: rgb(55 65 81);
|
||||
border-color: rgb(75 85 99);
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ts-wrapper .ts-control input {
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ts-wrapper .ts-control input::placeholder {
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
.dark .ts-dropdown {
|
||||
background-color: rgb(55 65 81);
|
||||
border-color: rgb(75 85 99);
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ts-dropdown .option {
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ts-dropdown .option.active {
|
||||
background-color: rgb(147 51 234);
|
||||
color: white;
|
||||
}
|
||||
.dark .ts-dropdown .option:hover {
|
||||
background-color: rgb(75 85 99);
|
||||
}
|
||||
.dark .ts-wrapper.focus .ts-control {
|
||||
border-color: rgb(147 51 234);
|
||||
box-shadow: 0 0 0 1px rgb(147 51 234);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header('Vendor Products', subtitle='Browse vendor-specific product catalogs with override capability') }}
|
||||
<!-- Page Header with Vendor Selector -->
|
||||
{% call page_header_flex(title='Vendor Products', subtitle='Browse vendor-specific product catalogs with override capability') %}
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Vendor Autocomplete (Tom Select) -->
|
||||
<div class="w-80">
|
||||
<select id="vendor-select" x-ref="vendorSelect" placeholder="Filter by vendor...">
|
||||
</select>
|
||||
</div>
|
||||
{{ refresh_button(loading_var='loading', onclick='refresh()', variant='secondary') }}
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
<!-- Selected Vendor Info -->
|
||||
<div x-show="selectedVendor" x-transition class="mb-6 p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center">
|
||||
<span class="text-sm font-semibold text-purple-600 dark:text-purple-300" x-text="selectedVendor?.name?.charAt(0).toUpperCase()"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-purple-800 dark:text-purple-200" x-text="selectedVendor?.name"></span>
|
||||
<span class="ml-2 text-xs text-purple-600 dark:text-purple-400 font-mono" x-text="selectedVendor?.vendor_code"></span>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="clearVendorFilter()" class="text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 text-sm flex items-center gap-1">
|
||||
<span x-html="$icon('x', 'w-4 h-4')"></span>
|
||||
Clear filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ loading_state('Loading products...') }}
|
||||
|
||||
@@ -116,18 +187,6 @@
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<!-- Vendor Filter -->
|
||||
<select
|
||||
x-model="filters.vendor_id"
|
||||
@change="pagination.page = 1; loadProducts()"
|
||||
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none"
|
||||
>
|
||||
<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.is_active"
|
||||
@@ -149,16 +208,6 @@
|
||||
<option value="true">Featured Only</option>
|
||||
<option value="false">Not Featured</option>
|
||||
</select>
|
||||
|
||||
<!-- Refresh Button -->
|
||||
<button
|
||||
@click="refresh()"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
||||
title="Refresh products"
|
||||
>
|
||||
<span x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -326,5 +375,19 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<!-- Tom Select JS with local fallback -->
|
||||
<script>
|
||||
(function() {
|
||||
var script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/tom-select@2.4.1/dist/js/tom-select.complete.min.js';
|
||||
script.onerror = function() {
|
||||
console.warn('Tom Select CDN failed, loading local copy...');
|
||||
var fallbackScript = document.createElement('script');
|
||||
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/tom-select.complete.min.js") }}';
|
||||
document.head.appendChild(fallbackScript);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
})();
|
||||
</script>
|
||||
<script src="{{ url_for('static', path='admin/js/vendor-products.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,34 +5,79 @@
|
||||
|
||||
{% block title %}Vendor Themes{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/css/tom-select.css" rel="stylesheet">
|
||||
<style>
|
||||
.ts-wrapper { width: 100%; }
|
||||
.ts-control {
|
||||
background-color: rgb(249 250 251) !important;
|
||||
border-color: rgb(209 213 219) !important;
|
||||
border-radius: 0.5rem !important;
|
||||
padding: 0.5rem 0.75rem !important;
|
||||
}
|
||||
.dark .ts-control {
|
||||
background-color: rgb(55 65 81) !important;
|
||||
border-color: rgb(75 85 99) !important;
|
||||
color: rgb(229 231 235) !important;
|
||||
}
|
||||
.ts-dropdown {
|
||||
border-radius: 0.5rem !important;
|
||||
border-color: rgb(209 213 219) !important;
|
||||
}
|
||||
.dark .ts-dropdown {
|
||||
background-color: rgb(55 65 81) !important;
|
||||
border-color: rgb(75 85 99) !important;
|
||||
}
|
||||
.dark .ts-dropdown .option {
|
||||
color: rgb(229 231 235) !important;
|
||||
}
|
||||
.dark .ts-dropdown .option.active {
|
||||
background-color: rgb(75 85 99) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block alpine_data %}adminVendorThemes(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header('Vendor Themes', subtitle='Customize vendor theme colors and branding') }}
|
||||
|
||||
<!-- Vendor Selection -->
|
||||
<!-- Selected Vendor Display (when filtered) -->
|
||||
<div x-show="selectedVendor" x-cloak class="mb-6">
|
||||
<div class="px-4 py-3 bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span x-html="$icon('color-swatch', 'w-6 h-6 text-purple-600')"></span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-purple-700 dark:text-purple-300">Filtered by Vendor</p>
|
||||
<p class="text-lg font-semibold text-purple-900 dark:text-purple-100" x-text="selectedVendor?.name"></p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="clearVendorFilter()"
|
||||
class="flex items-center gap-1 px-3 py-1.5 text-sm text-purple-700 dark:text-purple-300 bg-purple-100 dark:bg-purple-800 rounded-md hover:bg-purple-200 dark:hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
<span x-html="$icon('x-mark', 'w-4 h-4')"></span>
|
||||
Clear Filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vendor Search/Filter -->
|
||||
<div class="px-4 py-6 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Select Vendor
|
||||
Search Vendor
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
Choose a vendor to customize their theme
|
||||
Search for a vendor to customize their theme
|
||||
</p>
|
||||
|
||||
<div class="max-w-md">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Vendor
|
||||
</label>
|
||||
<select
|
||||
x-model="selectedVendorCode"
|
||||
@change="navigateToTheme()"
|
||||
class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600"
|
||||
>
|
||||
<option value="">Select a vendor...</option>
|
||||
<template x-for="vendor in vendors" :key="vendor.vendor_code">
|
||||
<option :value="vendor.vendor_code" x-text="`${vendor.name} (${vendor.vendor_code})`"></option>
|
||||
</template>
|
||||
</select>
|
||||
<select x-ref="vendorSelect" placeholder="Search vendor by name or code..."></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -41,13 +86,16 @@
|
||||
{{ error_state('Error loading vendors') }}
|
||||
|
||||
<!-- Vendors List -->
|
||||
<div x-show="!loading && vendors.length > 0">
|
||||
<div x-show="!loading && filteredVendors.length > 0">
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
All Vendors
|
||||
</h3>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
<span x-text="selectedVendor ? 'Selected Vendor' : 'All Vendors'"></span>
|
||||
<span class="text-sm font-normal text-gray-500 dark:text-gray-400" x-text="`(${filteredVendors.length})`"></span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<template x-for="vendor in vendors" :key="vendor.vendor_code">
|
||||
<template x-for="vendor in filteredVendors" :key="vendor.vendor_code">
|
||||
<a
|
||||
:href="`/admin/vendors/${vendor.vendor_code}/theme`"
|
||||
class="block p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-purple-500 dark:hover:border-purple-500 hover:shadow-md transition-all"
|
||||
@@ -68,7 +116,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div x-show="!loading && vendors.length === 0" class="text-center py-12">
|
||||
<div x-show="!loading && filteredVendors.length === 0" class="text-center py-12">
|
||||
<span x-html="$icon('shopping-bag', 'inline w-12 h-12 text-gray-400 mb-4')"></span>
|
||||
<p class="text-gray-600 dark:text-gray-400">No vendors found</p>
|
||||
</div>
|
||||
@@ -76,5 +124,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js"></script>
|
||||
<script src="{{ url_for('static', path='shared/js/vendor-selector.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='admin/js/vendor-themes.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user