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:
2025-12-22 22:39:34 +01:00
parent 1274135091
commit 6f8434f200
27 changed files with 1966 additions and 303 deletions

View File

@@ -20,9 +20,9 @@ function codeQualityViolations() {
violations: [],
pagination: {
page: 1,
page_size: 50,
per_page: 20,
total: 0,
total_pages: 0
pages: 0
},
filters: {
validator_type: '',
@@ -33,6 +33,18 @@ function codeQualityViolations() {
},
async init() {
// Guard against multiple initialization
if (window._codeQualityViolationsInitialized) {
codeQualityViolationsLog.warn('Already initialized, skipping');
return;
}
window._codeQualityViolationsInitialized = true;
// Load platform settings for rows per page
if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
}
// Load filters from URL params
const params = new URLSearchParams(window.location.search);
this.filters.validator_type = params.get('validator_type') || '';
@@ -52,7 +64,7 @@ function codeQualityViolations() {
// Build query params
const params = {
page: this.pagination.page.toString(),
page_size: this.pagination.page_size.toString()
page_size: this.pagination.per_page.toString()
};
if (this.filters.validator_type) params.validator_type = this.filters.validator_type;
@@ -64,12 +76,8 @@ function codeQualityViolations() {
const data = await apiClient.get('/admin/code-quality/violations', params);
this.violations = data.violations;
this.pagination = {
page: data.page,
page_size: data.page_size,
total: data.total,
total_pages: data.total_pages
};
this.pagination.total = data.total;
this.pagination.pages = data.total_pages;
// Update URL with current filters (without reloading)
this.updateURL();
@@ -93,7 +101,7 @@ function codeQualityViolations() {
},
async nextPage() {
if (this.pagination.page < this.pagination.total_pages) {
if (this.pagination.page < this.pagination.pages) {
this.pagination.page++;
await this.loadViolations();
}
@@ -101,18 +109,18 @@ function codeQualityViolations() {
// Computed: Total number of pages
get totalPages() {
return this.pagination.total_pages;
return this.pagination.pages;
},
// Computed: Start index for pagination display
get startIndex() {
if (this.pagination.total === 0) return 0;
return (this.pagination.page - 1) * this.pagination.page_size + 1;
return (this.pagination.page - 1) * this.pagination.per_page + 1;
},
// Computed: End index for pagination display
get endIndex() {
const end = this.pagination.page * this.pagination.page_size;
const end = this.pagination.page * this.pagination.per_page;
return end > this.pagination.total ? this.pagination.total : end;
},

View File

@@ -35,7 +35,7 @@ function adminCompanies() {
// Pagination state
pagination: {
page: 1,
per_page: 10,
per_page: 20,
total: 0,
pages: 0
},
@@ -51,6 +51,11 @@ function adminCompanies() {
}
window._companiesInitialized = true;
// Load platform settings for rows per page
if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
}
companiesLog.group('Loading companies data');
await this.loadCompanies();
companiesLog.groupEnd();

View File

@@ -23,7 +23,6 @@ function adminCustomers() {
// Data
customers: [],
vendors: [],
stats: {
total: 0,
active: 0,
@@ -34,11 +33,13 @@ function adminCustomers() {
avg_order_value: 0
},
// Pagination
page: 1,
limit: 20,
total: 0,
skip: 0,
// Pagination (standard structure matching pagination macro)
pagination: {
page: 1,
per_page: 20,
total: 0,
pages: 0
},
// Filters
filters: {
@@ -47,51 +48,216 @@ function adminCustomers() {
vendor_id: ''
},
// Selected vendor (for prominent display and filtering)
selectedVendor: null,
// Tom Select instance
vendorSelectInstance: null,
// Computed: total pages
get totalPages() {
return Math.ceil(this.total / this.limit);
return this.pagination.pages;
},
// Computed: Start index for pagination display
get startIndex() {
if (this.pagination.total === 0) return 0;
return (this.pagination.page - 1) * this.pagination.per_page + 1;
},
// Computed: End index for pagination display
get endIndex() {
const end = this.pagination.page * this.pagination.per_page;
return end > this.pagination.total ? this.pagination.total : end;
},
// Computed: Page numbers for pagination
get pageNumbers() {
const pages = [];
const totalPages = this.totalPages;
const current = this.pagination.page;
if (totalPages <= 7) {
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
pages.push(1);
if (current > 3) {
pages.push('...');
}
const start = Math.max(2, current - 1);
const end = Math.min(totalPages - 1, current + 1);
for (let i = start; i <= end; i++) {
pages.push(i);
}
if (current < totalPages - 2) {
pages.push('...');
}
pages.push(totalPages);
}
return pages;
},
async init() {
customersLog.debug('Customers page initialized');
// Load vendors for filter dropdown
await this.loadVendors();
// Load platform settings for rows per page
if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
}
// Load initial data
await Promise.all([
this.loadCustomers(),
this.loadStats()
]);
// Initialize Tom Select for vendor filter
this.initVendorSelect();
// Check localStorage for saved vendor
const savedVendorId = localStorage.getItem('customers_selected_vendor_id');
if (savedVendorId) {
customersLog.debug('Restoring saved vendor:', savedVendorId);
// Restore vendor after a short delay to ensure TomSelect is ready
setTimeout(async () => {
await this.restoreSavedVendor(parseInt(savedVendorId));
}, 200);
// Load stats but not customers (restoreSavedVendor will do that)
await this.loadStats();
} else {
// No saved vendor - load all data
await Promise.all([
this.loadCustomers(),
this.loadStats()
]);
}
this.loading = false;
},
/**
* Load vendors for filter dropdown
* Restore saved vendor from localStorage
*/
async loadVendors() {
async restoreSavedVendor(vendorId) {
try {
const response = await apiClient.get('/admin/vendors?limit=100');
this.vendors = response.vendors || [];
const vendor = await apiClient.get(`/admin/vendors/${vendorId}`);
if (this.vendorSelectInstance && vendor) {
// Add the vendor as an option and select it
this.vendorSelectInstance.addOption({
id: vendor.id,
name: vendor.name,
vendor_code: vendor.vendor_code
});
this.vendorSelectInstance.setValue(vendor.id, true);
// Set the filter state
this.selectedVendor = vendor;
this.filters.vendor_id = vendor.id;
customersLog.debug('Restored vendor:', vendor.name);
// Load customers with the vendor filter applied
await this.loadCustomers();
}
} catch (error) {
customersLog.error('Failed to load vendors:', error);
this.vendors = [];
customersLog.error('Failed to restore saved vendor, clearing localStorage:', error);
localStorage.removeItem('customers_selected_vendor_id');
// Load unfiltered customers as fallback
await this.loadCustomers();
}
},
/**
* Initialize Tom Select for vendor autocomplete
*/
initVendorSelect() {
const selectEl = this.$refs.vendorSelect;
if (!selectEl) {
customersLog.warn('Vendor select element not found');
return;
}
// Wait for Tom Select to be available
if (typeof TomSelect === 'undefined') {
customersLog.warn('TomSelect not loaded, retrying in 100ms');
setTimeout(() => this.initVendorSelect(), 100);
return;
}
this.vendorSelectInstance = new TomSelect(selectEl, {
valueField: 'id',
labelField: 'name',
searchField: ['name', 'vendor_code'],
placeholder: 'Filter by vendor...',
allowEmptyOption: true,
load: async (query, callback) => {
try {
const response = await apiClient.get('/admin/vendors', {
search: query,
limit: 50
});
callback(response.vendors || []);
} catch (error) {
customersLog.error('Failed to search vendors:', error);
callback([]);
}
},
render: {
option: (data, escape) => {
return `<div class="flex items-center justify-between py-1">
<span>${escape(data.name)}</span>
<span class="text-xs text-gray-400 font-mono">${escape(data.vendor_code || '')}</span>
</div>`;
},
item: (data, escape) => {
return `<div>${escape(data.name)}</div>`;
}
},
onChange: (value) => {
if (value) {
const vendor = this.vendorSelectInstance.options[value];
this.selectedVendor = vendor;
this.filters.vendor_id = value;
// Save to localStorage
localStorage.setItem('customers_selected_vendor_id', value.toString());
} else {
this.selectedVendor = null;
this.filters.vendor_id = '';
// Clear from localStorage
localStorage.removeItem('customers_selected_vendor_id');
}
this.pagination.page = 1;
this.loadCustomers();
this.loadStats();
}
});
customersLog.debug('Vendor select initialized');
},
/**
* Clear vendor filter
*/
clearVendorFilter() {
if (this.vendorSelectInstance) {
this.vendorSelectInstance.clear();
}
this.selectedVendor = null;
this.filters.vendor_id = '';
// Clear from localStorage
localStorage.removeItem('customers_selected_vendor_id');
this.pagination.page = 1;
this.loadCustomers();
this.loadStats();
},
/**
* Load customers with current filters
*/
async loadCustomers() {
this.loadingCustomers = true;
this.error = '';
this.skip = (this.page - 1) * this.limit;
try {
const params = new URLSearchParams({
skip: this.skip.toString(),
limit: this.limit.toString()
skip: ((this.pagination.page - 1) * this.pagination.per_page).toString(),
limit: this.pagination.per_page.toString()
});
if (this.filters.search) {
@@ -108,7 +274,8 @@ function adminCustomers() {
const response = await apiClient.get(`/admin/customers?${params}`);
this.customers = response.customers || [];
this.total = response.total || 0;
this.pagination.total = response.total || 0;
this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page);
} catch (error) {
customersLog.error('Failed to load customers:', error);
this.error = error.message || 'Failed to load customers';
@@ -139,7 +306,7 @@ function adminCustomers() {
* Reset pagination and reload
*/
async resetAndLoad() {
this.page = 1;
this.pagination.page = 1;
await Promise.all([
this.loadCustomers(),
this.loadStats()
@@ -147,34 +314,33 @@ function adminCustomers() {
},
/**
* Go to specific page
* Go to previous page
*/
goToPage(p) {
this.page = p;
this.loadCustomers();
previousPage() {
if (this.pagination.page > 1) {
this.pagination.page--;
this.loadCustomers();
}
},
/**
* Get array of page numbers to display
* Go to next page
*/
getPageNumbers() {
const total = this.totalPages;
const current = this.page;
const maxVisible = 5;
if (total <= maxVisible) {
return Array.from({length: total}, (_, i) => i + 1);
nextPage() {
if (this.pagination.page < this.totalPages) {
this.pagination.page++;
this.loadCustomers();
}
},
const half = Math.floor(maxVisible / 2);
let start = Math.max(1, current - half);
let end = Math.min(total, start + maxVisible - 1);
if (end - start < maxVisible - 1) {
start = Math.max(1, end - maxVisible + 1);
/**
* Go to specific page
*/
goToPage(pageNum) {
if (typeof pageNum === 'number' && pageNum !== this.pagination.page) {
this.pagination.page = pageNum;
this.loadCustomers();
}
return Array.from({length: end - start + 1}, (_, i) => start + i);
},
/**

View File

@@ -115,6 +115,11 @@ function adminImports() {
}
window._adminImportsInitialized = true;
// Load platform settings for rows per page
if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
}
// IMPORTANT: Call parent init first
const parentInit = data().init;
if (parentInit) {

View File

@@ -257,4 +257,64 @@ function headerMessages() {
}
// Export to window
window.headerMessages = headerMessages;
window.headerMessages = headerMessages;
/**
* Platform Settings Utility
* Provides cached access to platform-wide settings
*/
const PlatformSettings = {
// Cache key and TTL
CACHE_KEY: 'platform_settings_cache',
CACHE_TTL: 5 * 60 * 1000, // 5 minutes
/**
* Get cached settings or fetch from API
*/
async get() {
try {
const cached = localStorage.getItem(this.CACHE_KEY);
if (cached) {
const { data, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < this.CACHE_TTL) {
return data;
}
}
// Fetch from API
const response = await apiClient.get('/admin/settings/display/public');
const settings = {
rows_per_page: response.rows_per_page || 20
};
// Cache the result
localStorage.setItem(this.CACHE_KEY, JSON.stringify({
data: settings,
timestamp: Date.now()
}));
return settings;
} catch (error) {
console.warn('Failed to load platform settings, using defaults:', error);
return { rows_per_page: 20 };
}
},
/**
* Get rows per page setting
*/
async getRowsPerPage() {
const settings = await this.get();
return settings.rows_per_page;
},
/**
* Clear the cache (call after saving settings)
*/
clearCache() {
localStorage.removeItem(this.CACHE_KEY);
}
};
// Export to window
window.PlatformSettings = PlatformSettings;

View File

@@ -47,13 +47,16 @@ function adminInventory() {
// Available locations for filter dropdown
locations: [],
// Selected vendor (for prominent display and filtering)
selectedVendor: null,
// Vendor selector controller (Tom Select)
vendorSelector: null,
// Pagination
pagination: {
page: 1,
per_page: 50,
per_page: 20,
total: 0,
pages: 0
},
@@ -131,21 +134,68 @@ function adminInventory() {
}
window._adminInventoryInitialized = true;
// Load platform settings for rows per page
if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
}
// Initialize vendor selector (Tom Select)
this.$nextTick(() => {
this.initVendorSelector();
});
// Load data in parallel
await Promise.all([
this.loadStats(),
this.loadLocations(),
this.loadInventory()
]);
// Check localStorage for saved vendor
const savedVendorId = localStorage.getItem('inventory_selected_vendor_id');
if (savedVendorId) {
adminInventoryLog.info('Restoring saved vendor:', savedVendorId);
// Restore vendor after a short delay to ensure TomSelect is ready
setTimeout(async () => {
await this.restoreSavedVendor(parseInt(savedVendorId));
}, 200);
// Load stats and locations but not inventory (restoreSavedVendor will do that)
await Promise.all([
this.loadStats(),
this.loadLocations()
]);
} else {
// No saved vendor - load all data
await Promise.all([
this.loadStats(),
this.loadLocations(),
this.loadInventory()
]);
}
adminInventoryLog.info('Inventory initialization complete');
},
/**
* Restore saved vendor from localStorage
*/
async restoreSavedVendor(vendorId) {
try {
const vendor = await apiClient.get(`/admin/vendors/${vendorId}`);
if (this.vendorSelector && vendor) {
// Use the vendor selector's setValue method
this.vendorSelector.setValue(vendor.id, vendor);
// Set the filter state
this.selectedVendor = vendor;
this.filters.vendor_id = vendor.id;
adminInventoryLog.info('Restored vendor:', vendor.name);
// Load inventory with the vendor filter applied
await this.loadInventory();
}
} catch (error) {
adminInventoryLog.warn('Failed to restore saved vendor, clearing localStorage:', error);
localStorage.removeItem('inventory_selected_vendor_id');
// Load unfiltered inventory as fallback
await this.loadInventory();
}
},
/**
* Initialize vendor selector with Tom Select
*/
@@ -159,27 +209,57 @@ function adminInventory() {
placeholder: 'Filter by vendor...',
onSelect: (vendor) => {
adminInventoryLog.info('Vendor selected:', vendor);
this.selectedVendor = vendor;
this.filters.vendor_id = vendor.id;
// Save to localStorage
localStorage.setItem('inventory_selected_vendor_id', vendor.id.toString());
this.pagination.page = 1;
this.loadLocations();
this.loadInventory();
this.loadStats();
},
onClear: () => {
adminInventoryLog.info('Vendor filter cleared');
this.selectedVendor = null;
this.filters.vendor_id = '';
// Clear from localStorage
localStorage.removeItem('inventory_selected_vendor_id');
this.pagination.page = 1;
this.loadLocations();
this.loadInventory();
this.loadStats();
}
});
},
/**
* Clear vendor filter
*/
clearVendorFilter() {
if (this.vendorSelector) {
this.vendorSelector.clear();
}
this.selectedVendor = null;
this.filters.vendor_id = '';
// Clear from localStorage
localStorage.removeItem('inventory_selected_vendor_id');
this.pagination.page = 1;
this.loadLocations();
this.loadInventory();
this.loadStats();
},
/**
* Load inventory statistics
*/
async loadStats() {
try {
const response = await apiClient.get('/admin/inventory/stats');
const params = new URLSearchParams();
if (this.filters.vendor_id) {
params.append('vendor_id', this.filters.vendor_id);
}
const url = params.toString() ? `/admin/inventory/stats?${params}` : '/admin/inventory/stats';
const response = await apiClient.get(url);
this.stats = response;
adminInventoryLog.info('Loaded stats:', this.stats);
} catch (error) {

View File

@@ -32,7 +32,7 @@ function adminLogs() {
},
pagination: {
page: 1,
per_page: 50,
per_page: 20,
total: 0,
pages: 0
},
@@ -86,7 +86,20 @@ function adminLogs() {
},
async init() {
// Guard against multiple initialization
if (window._adminLogsInitialized) {
logsLog.warn('Already initialized, skipping');
return;
}
window._adminLogsInitialized = true;
logsLog.info('=== LOGS PAGE INITIALIZING ===');
// Load platform settings for rows per page
if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
}
await this.loadStats();
await this.loadLogs();
},

View File

@@ -43,16 +43,19 @@ function adminMarketplaceProducts() {
is_digital: ''
},
// Selected vendor (for prominent display and filtering)
selectedVendor: null,
// Tom Select instance
vendorSelectInstance: null,
// Available marketplaces for filter dropdown
marketplaces: [],
// Available source vendors for filter dropdown
sourceVendors: [],
// Pagination
pagination: {
page: 1,
per_page: 50,
per_page: 20,
total: 0,
pages: 0
},
@@ -127,24 +130,171 @@ function adminMarketplaceProducts() {
}
window._adminMarketplaceProductsInitialized = true;
// Load data in parallel
await Promise.all([
this.loadStats(),
this.loadMarketplaces(),
this.loadSourceVendors(),
this.loadTargetVendors(),
this.loadProducts()
]);
// Load platform settings for rows per page
if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
}
// Initialize Tom Select for vendor filter
this.initVendorSelect();
// Check localStorage for saved vendor
const savedVendorId = localStorage.getItem('marketplace_products_selected_vendor_id');
if (savedVendorId) {
adminMarketplaceProductsLog.info('Restoring saved vendor:', savedVendorId);
// Restore vendor after a short delay to ensure TomSelect is ready
setTimeout(async () => {
await this.restoreSavedVendor(parseInt(savedVendorId));
}, 200);
// Load other data but not products (restoreSavedVendor will do that)
await Promise.all([
this.loadStats(),
this.loadMarketplaces(),
this.loadTargetVendors()
]);
} else {
// No saved vendor - load all data including unfiltered products
await Promise.all([
this.loadStats(),
this.loadMarketplaces(),
this.loadTargetVendors(),
this.loadProducts()
]);
}
adminMarketplaceProductsLog.info('Marketplace Products initialization complete');
},
/**
* Restore saved vendor from localStorage
*/
async restoreSavedVendor(vendorId) {
try {
const vendor = await apiClient.get(`/admin/vendors/${vendorId}`);
if (this.vendorSelectInstance && vendor) {
// Add the vendor as an option and select it
this.vendorSelectInstance.addOption({
id: vendor.id,
name: vendor.name,
vendor_code: vendor.vendor_code
});
this.vendorSelectInstance.setValue(vendor.id, true);
// Set the filter state
this.selectedVendor = vendor;
this.filters.vendor_name = vendor.name;
adminMarketplaceProductsLog.info('Restored vendor:', vendor.name);
// Load products with the vendor filter applied
await this.loadProducts();
}
} catch (error) {
adminMarketplaceProductsLog.warn('Failed to restore saved vendor, clearing localStorage:', error);
localStorage.removeItem('marketplace_products_selected_vendor_id');
// Load unfiltered products as fallback
await this.loadProducts();
}
},
/**
* Initialize Tom Select for vendor autocomplete
*/
initVendorSelect() {
const selectEl = this.$refs.vendorSelect;
if (!selectEl) {
adminMarketplaceProductsLog.warn('Vendor select element not found');
return;
}
// Wait for Tom Select to be available
if (typeof TomSelect === 'undefined') {
adminMarketplaceProductsLog.warn('TomSelect not loaded, retrying in 100ms');
setTimeout(() => this.initVendorSelect(), 100);
return;
}
this.vendorSelectInstance = new TomSelect(selectEl, {
valueField: 'id',
labelField: 'name',
searchField: ['name', 'vendor_code'],
placeholder: 'Filter by vendor...',
allowEmptyOption: true,
load: async (query, callback) => {
try {
const response = await apiClient.get('/admin/vendors', {
search: query,
limit: 50
});
callback(response.vendors || []);
} catch (error) {
adminMarketplaceProductsLog.error('Failed to search vendors:', error);
callback([]);
}
},
render: {
option: (data, escape) => {
return `<div class="flex items-center justify-between py-1">
<span>${escape(data.name)}</span>
<span class="text-xs text-gray-400 font-mono">${escape(data.vendor_code || '')}</span>
</div>`;
},
item: (data, escape) => {
return `<div>${escape(data.name)}</div>`;
}
},
onChange: (value) => {
if (value) {
const vendor = this.vendorSelectInstance.options[value];
this.selectedVendor = vendor;
this.filters.vendor_name = vendor.name;
// Save to localStorage
localStorage.setItem('marketplace_products_selected_vendor_id', value.toString());
} else {
this.selectedVendor = null;
this.filters.vendor_name = '';
// Clear from localStorage
localStorage.removeItem('marketplace_products_selected_vendor_id');
}
this.pagination.page = 1;
this.loadProducts();
this.loadStats();
}
});
adminMarketplaceProductsLog.info('Vendor select initialized');
},
/**
* Clear vendor filter
*/
clearVendorFilter() {
if (this.vendorSelectInstance) {
this.vendorSelectInstance.clear();
}
this.selectedVendor = null;
this.filters.vendor_name = '';
// Clear from localStorage
localStorage.removeItem('marketplace_products_selected_vendor_id');
this.pagination.page = 1;
this.loadProducts();
this.loadStats();
},
/**
* Load product statistics
*/
async loadStats() {
try {
const response = await apiClient.get('/admin/products/stats');
const params = new URLSearchParams();
if (this.filters.marketplace) {
params.append('marketplace', this.filters.marketplace);
}
if (this.filters.vendor_name) {
params.append('vendor_name', this.filters.vendor_name);
}
const url = params.toString() ? `/admin/products/stats?${params}` : '/admin/products/stats';
const response = await apiClient.get(url);
this.stats = response;
adminMarketplaceProductsLog.info('Loaded stats:', this.stats);
} catch (error) {
@@ -165,19 +315,6 @@ function adminMarketplaceProducts() {
}
},
/**
* Load available source vendors for filter (from marketplace products)
*/
async loadSourceVendors() {
try {
const response = await apiClient.get('/admin/products/vendors');
this.sourceVendors = response.vendors || [];
adminMarketplaceProductsLog.info('Loaded source vendors:', this.sourceVendors);
} catch (error) {
adminMarketplaceProductsLog.error('Failed to load source vendors:', error);
}
},
/**
* Load target vendors for copy functionality (actual vendor accounts)
*/

View File

@@ -51,7 +51,7 @@ function adminMarketplace() {
jobs: [],
pagination: {
page: 1,
per_page: 10,
per_page: 20,
total: 0,
pages: 0
},
@@ -118,6 +118,11 @@ function adminMarketplace() {
}
window._adminMarketplaceInitialized = true;
// Load platform settings for rows per page
if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
}
// Ensure form defaults are set (in case spread didn't work)
if (!this.importForm.marketplace) {
this.importForm.marketplace = 'Letzshop';

View File

@@ -58,7 +58,7 @@ function adminOrders() {
// Pagination
pagination: {
page: 1,
per_page: 50,
per_page: 20,
total: 0,
pages: 0
},
@@ -143,6 +143,11 @@ function adminOrders() {
}
window._adminOrdersInitialized = true;
// Load platform settings for rows per page
if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
}
// Initialize Tom Select for vendor filter
this.initVendorSelect();

View File

@@ -17,7 +17,10 @@ function adminSettings() {
saving: false,
error: null,
successMessage: null,
activeTab: 'logging',
activeTab: 'display',
displaySettings: {
rows_per_page: 20
},
logSettings: {
log_level: 'INFO',
log_file_max_size_mb: 10,
@@ -41,6 +44,7 @@ function adminSettings() {
try {
settingsLog.info('=== SETTINGS PAGE INITIALIZING ===');
await Promise.all([
this.loadDisplaySettings(),
this.loadLogSettings(),
this.loadShippingSettings()
]);
@@ -54,11 +58,52 @@ function adminSettings() {
this.error = null;
this.successMessage = null;
await Promise.all([
this.loadDisplaySettings(),
this.loadLogSettings(),
this.loadShippingSettings()
]);
},
async loadDisplaySettings() {
try {
const data = await apiClient.get('/admin/settings/display/rows-per-page');
this.displaySettings.rows_per_page = data.rows_per_page || 20;
settingsLog.info('Display settings loaded:', this.displaySettings);
} catch (error) {
settingsLog.error('Failed to load display settings:', error);
// Use default value on error
this.displaySettings.rows_per_page = 20;
}
},
async saveDisplaySettings() {
this.saving = true;
this.error = null;
this.successMessage = null;
try {
const data = await apiClient.put(`/admin/settings/display/rows-per-page?rows=${this.displaySettings.rows_per_page}`);
this.successMessage = data.message || 'Display settings saved successfully';
// Clear the cached platform settings so pages pick up the new value
if (window.PlatformSettings) {
window.PlatformSettings.clearCache();
}
// Auto-hide success message after 5 seconds
setTimeout(() => {
this.successMessage = null;
}, 5000);
settingsLog.info('Display settings saved successfully');
} catch (error) {
settingsLog.error('Failed to save display settings:', error);
this.error = error.response?.data?.detail || 'Failed to save display settings';
} finally {
this.saving = false;
}
},
async loadLogSettings() {
this.loading = true;
this.error = null;

View File

@@ -28,7 +28,7 @@ function adminUsers() {
},
pagination: {
page: 1,
per_page: 10,
per_page: 20,
total: 0,
pages: 0
},
@@ -36,17 +36,22 @@ function adminUsers() {
// Initialization
async init() {
usersLog.info('=== USERS PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._usersInitialized) {
usersLog.warn('Users page already initialized, skipping...');
return;
}
window._usersInitialized = true;
// Load platform settings for rows per page
if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
}
await this.loadUsers();
await this.loadStats();
usersLog.info('=== USERS PAGE INITIALIZATION COMPLETE ===');
},

View File

@@ -43,13 +43,16 @@ function adminVendorProducts() {
is_featured: ''
},
// Available vendors for filter dropdown
vendors: [],
// Selected vendor (for prominent display and filtering)
selectedVendor: null,
// Tom Select instance
vendorSelectInstance: null,
// Pagination
pagination: {
page: 1,
per_page: 50,
per_page: 20,
total: 0,
pages: 0
},
@@ -121,22 +124,162 @@ function adminVendorProducts() {
}
window._adminVendorProductsInitialized = true;
// Load data in parallel
await Promise.all([
this.loadStats(),
this.loadVendors(),
this.loadProducts()
]);
// Load platform settings for rows per page
if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
}
// Initialize Tom Select for vendor filter
this.initVendorSelect();
// Check localStorage for saved vendor
const savedVendorId = localStorage.getItem('vendor_products_selected_vendor_id');
if (savedVendorId) {
adminVendorProductsLog.info('Restoring saved vendor:', savedVendorId);
// Restore vendor after a short delay to ensure TomSelect is ready
setTimeout(async () => {
await this.restoreSavedVendor(parseInt(savedVendorId));
}, 200);
// Load stats but not products (restoreSavedVendor will do that)
await this.loadStats();
} else {
// No saved vendor - load all data including unfiltered products
await Promise.all([
this.loadStats(),
this.loadProducts()
]);
}
adminVendorProductsLog.info('Vendor Products initialization complete');
},
/**
* Restore saved vendor from localStorage
*/
async restoreSavedVendor(vendorId) {
try {
const vendor = await apiClient.get(`/admin/vendors/${vendorId}`);
if (this.vendorSelectInstance && vendor) {
// Add the vendor as an option and select it
this.vendorSelectInstance.addOption({
id: vendor.id,
name: vendor.name,
vendor_code: vendor.vendor_code
});
this.vendorSelectInstance.setValue(vendor.id, true);
// Set the filter state
this.selectedVendor = vendor;
this.filters.vendor_id = vendor.id;
adminVendorProductsLog.info('Restored vendor:', vendor.name);
// Load products with the vendor filter applied
await this.loadProducts();
}
} catch (error) {
adminVendorProductsLog.warn('Failed to restore saved vendor, clearing localStorage:', error);
localStorage.removeItem('vendor_products_selected_vendor_id');
// Load unfiltered products as fallback
await this.loadProducts();
}
},
/**
* Initialize Tom Select for vendor autocomplete
*/
initVendorSelect() {
const selectEl = this.$refs.vendorSelect;
if (!selectEl) {
adminVendorProductsLog.warn('Vendor select element not found');
return;
}
// Wait for Tom Select to be available
if (typeof TomSelect === 'undefined') {
adminVendorProductsLog.warn('TomSelect not loaded, retrying in 100ms');
setTimeout(() => this.initVendorSelect(), 100);
return;
}
this.vendorSelectInstance = new TomSelect(selectEl, {
valueField: 'id',
labelField: 'name',
searchField: ['name', 'vendor_code'],
placeholder: 'Filter by vendor...',
allowEmptyOption: true,
load: async (query, callback) => {
try {
const response = await apiClient.get('/admin/vendors', {
search: query,
limit: 50
});
callback(response.vendors || []);
} catch (error) {
adminVendorProductsLog.error('Failed to search vendors:', error);
callback([]);
}
},
render: {
option: (data, escape) => {
return `<div class="flex items-center justify-between py-1">
<span>${escape(data.name)}</span>
<span class="text-xs text-gray-400 font-mono">${escape(data.vendor_code || '')}</span>
</div>`;
},
item: (data, escape) => {
return `<div>${escape(data.name)}</div>`;
}
},
onChange: (value) => {
if (value) {
const vendor = this.vendorSelectInstance.options[value];
this.selectedVendor = vendor;
this.filters.vendor_id = value;
// Save to localStorage
localStorage.setItem('vendor_products_selected_vendor_id', value.toString());
} else {
this.selectedVendor = null;
this.filters.vendor_id = '';
// Clear from localStorage
localStorage.removeItem('vendor_products_selected_vendor_id');
}
this.pagination.page = 1;
this.loadProducts();
this.loadStats();
}
});
adminVendorProductsLog.info('Vendor select initialized');
},
/**
* Clear vendor filter
*/
clearVendorFilter() {
if (this.vendorSelectInstance) {
this.vendorSelectInstance.clear();
}
this.selectedVendor = null;
this.filters.vendor_id = '';
// Clear from localStorage
localStorage.removeItem('vendor_products_selected_vendor_id');
this.pagination.page = 1;
this.loadProducts();
this.loadStats();
},
/**
* Load product statistics
*/
async loadStats() {
try {
const response = await apiClient.get('/admin/vendor-products/stats');
const params = new URLSearchParams();
if (this.filters.vendor_id) {
params.append('vendor_id', this.filters.vendor_id);
}
const url = params.toString() ? `/admin/vendor-products/stats?${params}` : '/admin/vendor-products/stats';
const response = await apiClient.get(url);
this.stats = response;
adminVendorProductsLog.info('Loaded stats:', this.stats);
} catch (error) {
@@ -144,19 +287,6 @@ function adminVendorProducts() {
}
},
/**
* Load available vendors for filter
*/
async loadVendors() {
try {
const response = await apiClient.get('/admin/vendor-products/vendors');
this.vendors = response.vendors || [];
adminVendorProductsLog.info('Loaded vendors:', this.vendors.length);
} catch (error) {
adminVendorProductsLog.error('Failed to load vendors:', error);
}
},
/**
* Load products with filtering and pagination
*/

View File

@@ -24,6 +24,13 @@ function adminVendorThemes() {
vendors: [],
selectedVendorCode: '',
// Selected vendor for filter (Tom Select)
selectedVendor: null,
vendorSelector: null,
// Search/filter
searchQuery: '',
async init() {
// Guard against multiple initialization
if (window._adminVendorThemesInitialized) {
@@ -31,13 +38,86 @@ function adminVendorThemes() {
}
window._adminVendorThemesInitialized = true;
// Call parent init first
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
vendorThemesLog.info('Vendor Themes init() called');
// Initialize vendor selector (Tom Select)
this.$nextTick(() => {
this.initVendorSelector();
});
// Check localStorage for saved vendor
const savedVendorId = localStorage.getItem('vendor_themes_selected_vendor_id');
if (savedVendorId) {
vendorThemesLog.info('Restoring saved vendor:', savedVendorId);
await this.loadVendors();
// Restore vendor after vendors are loaded
setTimeout(async () => {
await this.restoreSavedVendor(parseInt(savedVendorId));
}, 200);
} else {
await this.loadVendors();
}
await this.loadVendors();
vendorThemesLog.info('Vendor Themes initialization complete');
},
/**
* Restore saved vendor from localStorage
*/
async restoreSavedVendor(vendorId) {
try {
const vendor = await apiClient.get(`/admin/vendors/${vendorId}`);
if (this.vendorSelector && vendor) {
// Use the vendor selector's setValue method
this.vendorSelector.setValue(vendor.id, vendor);
// Set the filter state
this.selectedVendor = vendor;
vendorThemesLog.info('Restored vendor:', vendor.name);
}
} catch (error) {
vendorThemesLog.warn('Failed to restore saved vendor, clearing localStorage:', error);
localStorage.removeItem('vendor_themes_selected_vendor_id');
}
},
/**
* Initialize vendor selector with Tom Select
*/
initVendorSelector() {
if (!this.$refs.vendorSelect) {
vendorThemesLog.warn('Vendor select element not found');
return;
}
this.vendorSelector = initVendorSelector(this.$refs.vendorSelect, {
placeholder: 'Search vendor...',
onSelect: (vendor) => {
vendorThemesLog.info('Vendor selected:', vendor);
this.selectedVendor = vendor;
// Save to localStorage
localStorage.setItem('vendor_themes_selected_vendor_id', vendor.id.toString());
},
onClear: () => {
vendorThemesLog.info('Vendor filter cleared');
this.selectedVendor = null;
// Clear from localStorage
localStorage.removeItem('vendor_themes_selected_vendor_id');
}
});
},
/**
* Clear vendor filter
*/
clearVendorFilter() {
if (this.vendorSelector) {
this.vendorSelector.clear();
}
this.selectedVendor = null;
// Clear from localStorage
localStorage.removeItem('vendor_themes_selected_vendor_id');
},
async loadVendors() {
@@ -56,6 +136,28 @@ function adminVendorThemes() {
}
},
/**
* Computed: Filtered vendors based on search and selected vendor
*/
get filteredVendors() {
let filtered = this.vendors;
// If a vendor is selected via Tom Select, show only that vendor
if (this.selectedVendor) {
filtered = this.vendors.filter(v => v.id === this.selectedVendor.id);
}
// Otherwise filter by search query
else if (this.searchQuery) {
const query = this.searchQuery.toLowerCase();
filtered = this.vendors.filter(v =>
v.name.toLowerCase().includes(query) ||
(v.vendor_code && v.vendor_code.toLowerCase().includes(query))
);
}
return filtered;
},
navigateToTheme() {
if (!this.selectedVendorCode) {
return;

View File

@@ -35,7 +35,7 @@ function adminVendors() {
// Pagination state (server-side)
pagination: {
page: 1,
per_page: 10,
per_page: 20,
total: 0,
pages: 0
},
@@ -51,6 +51,11 @@ function adminVendors() {
}
window._vendorsInitialized = true;
// Load platform settings for rows per page
if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
}
vendorsLog.group('Loading vendors data');
await this.loadVendors();
await this.loadStats();