refactor: migrate templates and static files to self-contained modules

Templates Migration:
- Migrate admin templates to modules (tenancy, billing, monitoring, marketplace, etc.)
- Migrate vendor templates to modules (tenancy, billing, orders, messaging, etc.)
- Migrate storefront templates to modules (catalog, customers, orders, cart, checkout, cms)
- Migrate public templates to modules (billing, marketplace, cms)
- Keep shared templates in app/templates/ (base.html, errors/, partials/, macros/)
- Migrate letzshop partials to marketplace module

Static Files Migration:
- Migrate admin JS to modules: tenancy (23 files), core (5 files), monitoring (1 file)
- Migrate vendor JS to modules: tenancy (4 files), core (2 files)
- Migrate shared JS: vendor-selector.js to core, media-picker.js to cms
- Migrate storefront JS: storefront-layout.js to core
- Keep framework JS in static/ (api-client, utils, money, icons, log-config, lib/)
- Update all template references to use module_static paths

Naming Consistency:
- Rename static/platform/ to static/public/
- Rename app/templates/platform/ to app/templates/public/
- Update all extends and static references

Documentation:
- Update module-system.md with shared templates documentation
- Update frontend-structure.md with new module JS organization

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 14:34:16 +01:00
parent 843703258f
commit 4e28d91a78
542 changed files with 11603 additions and 9037 deletions

View File

@@ -1,196 +0,0 @@
// noqa: js-006 - async init pattern is safe, loadData has try/catch
// static/admin/js/admin-user-detail.js
// Create custom logger for admin user detail
const adminUserDetailLog = window.LogConfig.createLogger('ADMIN-USER-DETAIL');
function adminUserDetailPage() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Admin user detail page specific state
currentPage: 'admin-users',
adminUser: null,
loading: false,
saving: false,
error: null,
userId: null,
currentUserId: null,
// Initialize
async init() {
adminUserDetailLog.info('=== ADMIN USER DETAIL PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._adminUserDetailInitialized) {
adminUserDetailLog.warn('Admin user detail page already initialized, skipping...');
return;
}
window._adminUserDetailInitialized = true;
// Get current user ID
this.currentUserId = this.adminProfile?.id || null;
// Get user ID from URL
const path = window.location.pathname;
const match = path.match(/\/admin\/admin-users\/(\d+)$/);
if (match) {
this.userId = match[1];
adminUserDetailLog.info('Viewing admin user:', this.userId);
await this.loadAdminUser();
} else {
adminUserDetailLog.error('No user ID in URL');
this.error = 'Invalid admin user URL';
Utils.showToast('Invalid admin user URL', 'error');
}
adminUserDetailLog.info('=== ADMIN USER DETAIL PAGE INITIALIZATION COMPLETE ===');
},
// Load admin user data
async loadAdminUser() {
adminUserDetailLog.info('Loading admin user details...');
this.loading = true;
this.error = null;
try {
const url = `/admin/admin-users/${this.userId}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
const startTime = performance.now();
const response = await apiClient.get(url);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('GET', url, response, 'response');
window.LogConfig.logPerformance('Load Admin User Details', duration);
// Transform API response to expected format
this.adminUser = {
...response,
platforms: (response.platform_assignments || []).map(pa => ({
id: pa.platform_id,
code: pa.platform_code,
name: pa.platform_name
})),
full_name: [response.first_name, response.last_name].filter(Boolean).join(' ') || null
};
adminUserDetailLog.info(`Admin user loaded in ${duration}ms`, {
id: this.adminUser.id,
username: this.adminUser.username,
is_super_admin: this.adminUser.is_super_admin,
is_active: this.adminUser.is_active
});
adminUserDetailLog.debug('Full admin user data:', this.adminUser);
} catch (error) {
window.LogConfig.logError(error, 'Load Admin User Details');
this.error = error.message || 'Failed to load admin user details';
Utils.showToast('Failed to load admin user details', 'error');
} finally {
this.loading = false;
}
},
// Format date
formatDate(dateString) {
if (!dateString) {
return '-';
}
return Utils.formatDate(dateString);
},
// Toggle admin user status
async toggleStatus() {
const action = this.adminUser.is_active ? 'deactivate' : 'activate';
adminUserDetailLog.info(`Toggle status: ${action}`);
// Prevent self-deactivation
if (this.adminUser.id === this.currentUserId) {
Utils.showToast('You cannot deactivate your own account', 'error');
return;
}
if (!confirm(`Are you sure you want to ${action} "${this.adminUser.username}"?`)) {
adminUserDetailLog.info('Status toggle cancelled by user');
return;
}
this.saving = true;
try {
const url = `/admin/admin-users/${this.userId}/status`;
window.LogConfig.logApiCall('PUT', url, null, 'request');
const response = await apiClient.put(url);
window.LogConfig.logApiCall('PUT', url, response, 'response');
this.adminUser.is_active = response.is_active;
Utils.showToast(`Admin user ${action}d successfully`, 'success');
adminUserDetailLog.info(`Admin user ${action}d successfully`);
} catch (error) {
window.LogConfig.logError(error, `Toggle Status (${action})`);
Utils.showToast(error.message || `Failed to ${action} admin user`, 'error');
} finally {
this.saving = false;
}
},
// Delete admin user
async deleteAdminUser() {
adminUserDetailLog.info('Delete admin user requested:', this.userId);
// Prevent self-deletion
if (this.adminUser.id === this.currentUserId) {
Utils.showToast('You cannot delete your own account', 'error');
return;
}
if (!confirm(`Are you sure you want to delete admin user "${this.adminUser.username}"?\n\nThis action cannot be undone.`)) {
adminUserDetailLog.info('Delete cancelled by user');
return;
}
// Second confirmation for safety
if (!confirm(`FINAL CONFIRMATION\n\nAre you absolutely sure you want to delete "${this.adminUser.username}"?`)) {
adminUserDetailLog.info('Delete cancelled by user (second confirmation)');
return;
}
this.saving = true;
try {
const url = `/admin/admin-users/${this.userId}`;
window.LogConfig.logApiCall('DELETE', url, null, 'request');
await apiClient.delete(url);
window.LogConfig.logApiCall('DELETE', url, null, 'response');
Utils.showToast('Admin user deleted successfully', 'success');
adminUserDetailLog.info('Admin user deleted successfully');
// Redirect to admin users list
setTimeout(() => window.location.href = '/admin/admin-users', 1500);
} catch (error) {
window.LogConfig.logError(error, 'Delete Admin User');
Utils.showToast(error.message || 'Failed to delete admin user', 'error');
} finally {
this.saving = false;
}
},
// Refresh admin user data
async refresh() {
adminUserDetailLog.info('=== ADMIN USER REFRESH TRIGGERED ===');
await this.loadAdminUser();
Utils.showToast('Admin user details refreshed', 'success');
adminUserDetailLog.info('=== ADMIN USER REFRESH COMPLETE ===');
}
};
}
adminUserDetailLog.info('Admin user detail module loaded');

View File

@@ -1,339 +0,0 @@
// static/admin/js/admin-user-edit.js
// Create custom logger for admin user edit
const adminUserEditLog = window.LogConfig.createLogger('ADMIN-USER-EDIT');
function adminUserEditPage() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Admin user edit page specific state
currentPage: 'admin-users',
loading: false,
adminUser: null,
platforms: [],
errors: {},
saving: false,
userId: null,
currentUserId: null,
// Platform assignment state
showPlatformModal: false,
availablePlatforms: [],
selectedPlatformId: null,
// Confirmation modal state
showRemovePlatformModal: false,
platformToRemove: null,
// Initialize
async init() {
adminUserEditLog.info('=== ADMIN USER EDIT PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._adminUserEditInitialized) {
adminUserEditLog.warn('Admin user edit page already initialized, skipping...');
return;
}
window._adminUserEditInitialized = true;
// Get current user ID
this.currentUserId = this.adminProfile?.id || null;
// Get user ID from URL
const path = window.location.pathname;
const match = path.match(/\/admin\/admin-users\/(\d+)\/edit/);
if (match) {
this.userId = parseInt(match[1], 10);
adminUserEditLog.info('Editing admin user:', this.userId);
await this.loadAdminUser();
await this.loadAllPlatforms();
} else {
adminUserEditLog.error('No user ID in URL');
Utils.showToast('Invalid admin user URL', 'error');
setTimeout(() => window.location.href = '/admin/admin-users', 2000);
}
adminUserEditLog.info('=== ADMIN USER EDIT PAGE INITIALIZATION COMPLETE ===');
},
// Load admin user data
async loadAdminUser() {
adminUserEditLog.info('Loading admin user data...');
this.loading = true;
try {
const url = `/admin/admin-users/${this.userId}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
const startTime = performance.now();
const response = await apiClient.get(url);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('GET', url, response, 'response');
window.LogConfig.logPerformance('Load Admin User', duration);
// Transform API response
this.adminUser = {
...response,
platforms: (response.platform_assignments || []).map(pa => ({
id: pa.platform_id,
code: pa.platform_code,
name: pa.platform_name
})),
full_name: [response.first_name, response.last_name].filter(Boolean).join(' ') || null
};
adminUserEditLog.info(`Admin user loaded in ${duration}ms`, {
id: this.adminUser.id,
username: this.adminUser.username,
is_super_admin: this.adminUser.is_super_admin
});
} catch (error) {
window.LogConfig.logError(error, 'Load Admin User');
Utils.showToast('Failed to load admin user', 'error');
setTimeout(() => window.location.href = '/admin/admin-users', 2000);
} finally {
this.loading = false;
}
},
// Load all platforms for assignment
async loadAllPlatforms() {
try {
adminUserEditLog.debug('Loading all platforms...');
const response = await apiClient.get('/admin/platforms');
this.platforms = response.platforms || response.items || [];
adminUserEditLog.debug(`Loaded ${this.platforms.length} platforms`);
} catch (error) {
adminUserEditLog.error('Failed to load platforms:', error);
this.platforms = [];
}
},
// Get available platforms (not yet assigned)
get availablePlatformsForAssignment() {
if (!this.adminUser || this.adminUser.is_super_admin) return [];
const assignedIds = (this.adminUser.platforms || []).map(p => p.id);
return this.platforms.filter(p => !assignedIds.includes(p.id));
},
// Format date
formatDate(dateString) {
if (!dateString) {
return '-';
}
return Utils.formatDate(dateString);
},
// Toggle super admin status
async toggleSuperAdmin() {
const newStatus = !this.adminUser.is_super_admin;
const action = newStatus ? 'promote to' : 'demote from';
adminUserEditLog.info(`Toggle super admin: ${action}`);
// Prevent self-demotion
if (this.adminUser.id === this.currentUserId && !newStatus) {
Utils.showToast('You cannot demote yourself from super admin', 'error');
return;
}
if (!confirm(`Are you sure you want to ${action} super admin "${this.adminUser.username}"?`)) {
adminUserEditLog.info('Super admin toggle cancelled by user');
return;
}
this.saving = true;
try {
const url = `/admin/admin-users/${this.userId}/super-admin`;
window.LogConfig.logApiCall('PUT', url, { is_super_admin: newStatus }, 'request');
const response = await apiClient.put(url, { is_super_admin: newStatus });
window.LogConfig.logApiCall('PUT', url, response, 'response');
this.adminUser.is_super_admin = response.is_super_admin;
// Clear platforms if promoted to super admin
if (response.is_super_admin) {
this.adminUser.platforms = [];
}
const actionDone = newStatus ? 'promoted to' : 'demoted from';
Utils.showToast(`Admin ${actionDone} super admin successfully`, 'success');
adminUserEditLog.info(`Admin ${actionDone} super admin successfully`);
} catch (error) {
window.LogConfig.logError(error, `Toggle Super Admin (${action})`);
Utils.showToast(error.message || `Failed to ${action} super admin`, 'error');
} finally {
this.saving = false;
}
},
// Toggle admin user status
async toggleStatus() {
const action = this.adminUser.is_active ? 'deactivate' : 'activate';
adminUserEditLog.info(`Toggle status: ${action}`);
// Prevent self-deactivation
if (this.adminUser.id === this.currentUserId) {
Utils.showToast('You cannot deactivate your own account', 'error');
return;
}
if (!confirm(`Are you sure you want to ${action} "${this.adminUser.username}"?`)) {
adminUserEditLog.info('Status toggle cancelled by user');
return;
}
this.saving = true;
try {
const url = `/admin/admin-users/${this.userId}/status`;
window.LogConfig.logApiCall('PUT', url, null, 'request');
const response = await apiClient.put(url);
window.LogConfig.logApiCall('PUT', url, response, 'response');
this.adminUser.is_active = response.is_active;
Utils.showToast(`Admin user ${action}d successfully`, 'success');
adminUserEditLog.info(`Admin user ${action}d successfully`);
} catch (error) {
window.LogConfig.logError(error, `Toggle Status (${action})`);
Utils.showToast(error.message || `Failed to ${action} admin user`, 'error');
} finally {
this.saving = false;
}
},
// Assign platform to admin
async assignPlatform(platformId) {
if (!platformId) return;
adminUserEditLog.info('Assigning platform:', platformId);
this.saving = true;
try {
const url = `/admin/admin-users/${this.userId}/platforms/${platformId}`;
window.LogConfig.logApiCall('POST', url, null, 'request');
const response = await apiClient.post(url);
window.LogConfig.logApiCall('POST', url, response, 'response');
// Reload admin user to get updated platforms
await this.loadAdminUser();
Utils.showToast('Platform assigned successfully', 'success');
adminUserEditLog.info('Platform assigned successfully');
this.showPlatformModal = false;
this.selectedPlatformId = null;
} catch (error) {
window.LogConfig.logError(error, 'Assign Platform');
Utils.showToast(error.message || 'Failed to assign platform', 'error');
} finally {
this.saving = false;
}
},
// Show confirmation modal for platform removal
removePlatform(platformId) {
// Validate: platform admin must have at least one platform
if (this.adminUser.platforms.length <= 1) {
Utils.showToast('Platform admin must be assigned to at least one platform', 'error');
return;
}
this.platformToRemove = this.adminUser.platforms.find(p => p.id === platformId);
this.showRemovePlatformModal = true;
},
// Confirm and execute platform removal
async confirmRemovePlatform() {
if (!this.platformToRemove) return;
const platformId = this.platformToRemove.id;
adminUserEditLog.info('Removing platform:', platformId);
this.saving = true;
try {
const url = `/admin/admin-users/${this.userId}/platforms/${platformId}`;
window.LogConfig.logApiCall('DELETE', url, null, 'request');
await apiClient.delete(url);
window.LogConfig.logApiCall('DELETE', url, null, 'response');
// Reload admin user to get updated platforms
await this.loadAdminUser();
Utils.showToast('Platform removed successfully', 'success');
adminUserEditLog.info('Platform removed successfully');
} catch (error) {
window.LogConfig.logError(error, 'Remove Platform');
Utils.showToast(error.message || 'Failed to remove platform', 'error');
} finally {
this.saving = false;
this.platformToRemove = null;
}
},
// Open platform assignment modal
openPlatformModal() {
this.showPlatformModal = true;
this.selectedPlatformId = null;
},
// Delete admin user
async deleteAdminUser() {
adminUserEditLog.info('Delete admin user requested:', this.userId);
// Prevent self-deletion
if (this.adminUser.id === this.currentUserId) {
Utils.showToast('You cannot delete your own account', 'error');
return;
}
if (!confirm(`Are you sure you want to delete admin user "${this.adminUser.username}"?\n\nThis action cannot be undone.`)) {
adminUserEditLog.info('Delete cancelled by user');
return;
}
// Second confirmation for safety
if (!confirm(`FINAL CONFIRMATION\n\nAre you absolutely sure you want to delete "${this.adminUser.username}"?`)) {
adminUserEditLog.info('Delete cancelled by user (second confirmation)');
return;
}
this.saving = true;
try {
const url = `/admin/admin-users/${this.userId}`;
window.LogConfig.logApiCall('DELETE', url, null, 'request');
await apiClient.delete(url);
window.LogConfig.logApiCall('DELETE', url, null, 'response');
Utils.showToast('Admin user deleted successfully', 'success');
adminUserEditLog.info('Admin user deleted successfully');
// Redirect to admin users list
setTimeout(() => window.location.href = '/admin/admin-users', 1500);
} catch (error) {
window.LogConfig.logError(error, 'Delete Admin User');
Utils.showToast(error.message || 'Failed to delete admin user', 'error');
} finally {
this.saving = false;
}
}
};
}
adminUserEditLog.info('Admin user edit module loaded');

View File

@@ -1,330 +0,0 @@
// noqa: js-006 - async init pattern is safe, loadData has try/catch
// static/admin/js/admin-users.js
// Create custom logger for admin users
const adminUsersLog = window.LogConfig.createLogger('ADMIN-USERS');
function adminUsersPage() {
return {
// Inherit base layout functionality
...data(),
// Set page identifier
currentPage: 'admin-users',
// State
adminUsers: [],
loading: false,
error: null,
currentUserId: null,
filters: {
search: '',
is_super_admin: '',
is_active: ''
},
stats: {
total_admins: 0,
super_admins: 0,
platform_admins: 0,
active_admins: 0
},
pagination: {
page: 1,
per_page: 20,
total: 0,
pages: 0
},
// Initialization
async init() {
adminUsersLog.info('=== ADMIN USERS PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._adminUsersInitialized) {
adminUsersLog.warn('Admin users page already initialized, skipping...');
return;
}
window._adminUsersInitialized = true;
// Get current user ID
this.currentUserId = this.adminProfile?.id || null;
// Load platform settings for rows per page
if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
}
await this.loadAdminUsers();
await this.loadStats();
adminUsersLog.info('=== ADMIN USERS PAGE INITIALIZATION COMPLETE ===');
},
// Format date helper
formatDate(dateString) {
if (!dateString) return '-';
return Utils.formatDate(dateString);
},
// Computed: Total number of pages
get totalPages() {
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: Generate page numbers array with ellipsis
get pageNumbers() {
const pages = [];
const totalPages = this.totalPages;
const current = this.pagination.page;
if (totalPages <= 7) {
// Show all pages if 7 or fewer
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
// Always show first page
pages.push(1);
if (current > 3) {
pages.push('...');
}
// Show pages around current page
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('...');
}
// Always show last page
pages.push(totalPages);
}
return pages;
},
// Load admin users from API
async loadAdminUsers() {
adminUsersLog.info('Loading admin users...');
this.loading = true;
this.error = null;
try {
const params = new URLSearchParams();
// Calculate skip for pagination
const skip = (this.pagination.page - 1) * this.pagination.per_page;
params.append('skip', skip);
params.append('limit', this.pagination.per_page);
if (this.filters.search) {
params.append('search', this.filters.search);
}
if (this.filters.is_super_admin === 'false') {
params.append('include_super_admins', 'false');
}
if (this.filters.is_active !== '') {
params.append('is_active', this.filters.is_active);
}
const url = `/admin/admin-users?${params}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
const startTime = performance.now();
const response = await apiClient.get(url);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('GET', url, response, 'response');
window.LogConfig.logPerformance('Load Admin Users', duration);
// Transform API response to expected format
let admins = response.admins || [];
// Apply client-side filtering for search and super admin status
if (this.filters.search) {
const searchLower = this.filters.search.toLowerCase();
admins = admins.filter(admin =>
admin.username?.toLowerCase().includes(searchLower) ||
admin.email?.toLowerCase().includes(searchLower) ||
admin.first_name?.toLowerCase().includes(searchLower) ||
admin.last_name?.toLowerCase().includes(searchLower)
);
}
// Filter by super admin status
if (this.filters.is_super_admin === 'true') {
admins = admins.filter(admin => admin.is_super_admin);
}
// Filter by active status
if (this.filters.is_active !== '') {
const isActive = this.filters.is_active === 'true';
admins = admins.filter(admin => admin.is_active === isActive);
}
// Transform platform_assignments to platforms for template
this.adminUsers = admins.map(admin => ({
...admin,
platforms: (admin.platform_assignments || []).map(pa => ({
id: pa.platform_id,
code: pa.platform_code,
name: pa.platform_name
})),
full_name: [admin.first_name, admin.last_name].filter(Boolean).join(' ') || null
}));
this.pagination.total = response.total || this.adminUsers.length;
this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page) || 1;
adminUsersLog.info(`Loaded ${this.adminUsers.length} admin users`);
} catch (error) {
window.LogConfig.logError(error, 'Load Admin Users');
this.error = error.message || 'Failed to load admin users';
Utils.showToast('Failed to load admin users', 'error');
} finally {
this.loading = false;
}
},
// Load statistics (computed from admin users data)
async loadStats() {
adminUsersLog.info('Loading admin user statistics...');
try {
// Fetch all admin users to compute stats (max 500 per API limit)
const url = '/admin/admin-users?skip=0&limit=500';
window.LogConfig.logApiCall('GET', url, null, 'request');
const response = await apiClient.get(url);
window.LogConfig.logApiCall('GET', url, response, 'response');
const admins = response.admins || [];
// Compute stats from the data
this.stats = {
total_admins: admins.length,
super_admins: admins.filter(a => a.is_super_admin).length,
platform_admins: admins.filter(a => !a.is_super_admin).length,
active_admins: admins.filter(a => a.is_active).length
};
adminUsersLog.debug('Stats computed:', this.stats);
} catch (error) {
window.LogConfig.logError(error, 'Load Admin Stats');
// Stats are non-critical, don't show error toast
}
},
// Search with debounce
debouncedSearch() {
// Clear existing timeout
if (this._searchTimeout) {
clearTimeout(this._searchTimeout);
}
// Set new timeout
this._searchTimeout = setTimeout(() => {
adminUsersLog.info('Search triggered:', this.filters.search);
this.pagination.page = 1;
this.loadAdminUsers();
}, 300);
},
// Pagination
nextPage() {
if (this.pagination.page < this.pagination.pages) {
this.pagination.page++;
adminUsersLog.info('Next page:', this.pagination.page);
this.loadAdminUsers();
}
},
previousPage() {
if (this.pagination.page > 1) {
this.pagination.page--;
adminUsersLog.info('Previous page:', this.pagination.page);
this.loadAdminUsers();
}
},
goToPage(pageNum) {
if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) {
this.pagination.page = pageNum;
adminUsersLog.info('Go to page:', this.pagination.page);
this.loadAdminUsers();
}
},
// Actions
viewAdminUser(admin) {
adminUsersLog.info('View admin user:', admin.username);
window.location.href = `/admin/admin-users/${admin.id}`;
},
editAdminUser(admin) {
adminUsersLog.info('Edit admin user:', admin.username);
window.location.href = `/admin/admin-users/${admin.id}/edit`;
},
async deleteAdminUser(admin) {
adminUsersLog.warn('Delete admin user requested:', admin.username);
// Prevent self-deletion
if (admin.id === this.currentUserId) {
Utils.showToast('You cannot delete your own account', 'error');
return;
}
if (!confirm(`Are you sure you want to delete admin user "${admin.username}"?\n\nThis action cannot be undone.`)) {
adminUsersLog.info('Delete cancelled by user');
return;
}
// Second confirmation for safety
if (!confirm(`FINAL CONFIRMATION\n\nAre you absolutely sure you want to delete "${admin.username}"?`)) {
adminUsersLog.info('Delete cancelled by user (second confirmation)');
return;
}
try {
const url = `/admin/admin-users/${admin.id}`;
window.LogConfig.logApiCall('DELETE', url, null, 'request');
await apiClient.delete(url);
Utils.showToast('Admin user deleted successfully', 'success');
adminUsersLog.info('Admin user deleted successfully');
await this.loadAdminUsers();
await this.loadStats();
} catch (error) {
window.LogConfig.logError(error, 'Delete Admin User');
Utils.showToast(error.message || 'Failed to delete admin user', 'error');
}
},
openCreateModal() {
adminUsersLog.info('Open create admin user page');
window.location.href = '/admin/admin-users/create';
}
};
}
adminUsersLog.info('Admin users module loaded');

View File

@@ -1,278 +0,0 @@
// noqa: js-006 - async init pattern is safe, loadData has try/catch
// static/admin/js/companies.js
// ✅ Use centralized logger
const companiesLog = window.LogConfig.loggers.companies || window.LogConfig.createLogger('companies');
// ============================================
// COMPANY LIST FUNCTION
// ============================================
function adminCompanies() {
return {
// Inherit base layout functionality
...data(),
// Page identifier for sidebar active state
currentPage: 'companies',
// Companies page specific state
companies: [],
stats: {
total: 0,
verified: 0,
active: 0,
totalVendors: 0
},
loading: false,
error: null,
// Search and filters
filters: {
search: '',
is_active: '',
is_verified: ''
},
// Pagination state
pagination: {
page: 1,
per_page: 20,
total: 0,
pages: 0
},
// Initialize
async init() {
companiesLog.info('=== COMPANIES PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._companiesInitialized) {
companiesLog.warn('Companies page already initialized, skipping...');
return;
}
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();
companiesLog.info('=== COMPANIES PAGE INITIALIZATION COMPLETE ===');
},
// Debounced search
debouncedSearch() {
if (this._searchTimeout) {
clearTimeout(this._searchTimeout);
}
this._searchTimeout = setTimeout(() => {
companiesLog.info('Search triggered:', this.filters.search);
this.pagination.page = 1;
this.loadCompanies();
}, 300);
},
// Computed: Get companies for current page (already paginated from server)
get paginatedCompanies() {
return this.companies;
},
// Computed: Total number of pages
get totalPages() {
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: Generate page numbers array with ellipsis
get pageNumbers() {
const pages = [];
const totalPages = this.totalPages;
const current = this.pagination.page;
if (totalPages <= 7) {
// Show all pages if 7 or fewer
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
// Always show first page
pages.push(1);
if (current > 3) {
pages.push('...');
}
// Show pages around current page
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('...');
}
// Always show last page
pages.push(totalPages);
}
return pages;
},
// Load companies with search and pagination
async loadCompanies() {
this.loading = true;
this.error = null;
try {
companiesLog.info('Fetching companies from API...');
const params = new URLSearchParams();
params.append('skip', (this.pagination.page - 1) * this.pagination.per_page);
params.append('limit', this.pagination.per_page);
if (this.filters.search) {
params.append('search', this.filters.search);
}
if (this.filters.is_active) {
params.append('is_active', this.filters.is_active);
}
if (this.filters.is_verified) {
params.append('is_verified', this.filters.is_verified);
}
const response = await apiClient.get(`/admin/companies?${params}`);
if (response.companies) {
this.companies = response.companies;
this.pagination.total = response.total;
this.pagination.pages = Math.ceil(response.total / this.pagination.per_page);
// Calculate stats from all companies (need separate call for accurate stats)
this.stats.total = response.total;
this.stats.verified = this.companies.filter(c => c.is_verified).length;
this.stats.active = this.companies.filter(c => c.is_active).length;
this.stats.totalVendors = this.companies.reduce((sum, c) => sum + (c.vendor_count || 0), 0);
companiesLog.info(`Loaded ${this.companies.length} companies (total: ${response.total})`);
} else {
companiesLog.warn('No companies in response');
this.companies = [];
}
} catch (error) {
companiesLog.error('Failed to load companies:', error);
this.error = error.message || 'Failed to load companies';
this.companies = [];
} finally {
this.loading = false;
}
},
// Edit company
editCompany(companyId) {
companiesLog.info('Edit company:', companyId);
// TODO: Navigate to edit page
window.location.href = `/admin/companies/${companyId}/edit`;
},
// Delete company
async deleteCompany(company) {
if (company.vendor_count > 0) {
companiesLog.warn('Cannot delete company with vendors');
Utils.showToast(`Cannot delete "${company.name}" because it has ${company.vendor_count} vendor(s). Please delete or reassign the vendors first.`, 'warning');
return;
}
const confirmed = confirm(
`Are you sure you want to delete "${company.name}"?\n\nThis action cannot be undone.`
);
if (!confirmed) {
companiesLog.info('Delete cancelled by user');
return;
}
try {
companiesLog.info('Deleting company:', company.id);
await apiClient.delete(`/admin/companies/${company.id}?confirm=true`);
companiesLog.info('Company deleted successfully');
// Reload companies
await this.loadCompanies();
await this.loadStats();
Utils.showToast(`Company "${company.name}" deleted successfully`, 'success');
} catch (error) {
companiesLog.error('Failed to delete company:', error);
Utils.showToast(`Failed to delete company: ${error.message}`, 'error');
}
},
// Pagination methods
previousPage() {
if (this.pagination.page > 1) {
this.pagination.page--;
companiesLog.info('Previous page:', this.pagination.page);
this.loadCompanies();
}
},
nextPage() {
if (this.pagination.page < this.totalPages) {
this.pagination.page++;
companiesLog.info('Next page:', this.pagination.page);
this.loadCompanies();
}
},
goToPage(pageNum) {
if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) {
this.pagination.page = pageNum;
companiesLog.info('Go to page:', this.pagination.page);
this.loadCompanies();
}
},
// Format date for display
formatDate(dateString) {
if (!dateString) return 'N/A';
try {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
} catch (e) {
companiesLog.error('Date parsing error:', e);
return dateString;
}
}
};
}
// Register logger for configuration
if (!window.LogConfig.loggers.companies) {
window.LogConfig.loggers.companies = window.LogConfig.createLogger('companies');
}
companiesLog.info('✅ Companies module loaded');

View File

@@ -1,146 +0,0 @@
// noqa: js-006 - async init pattern is safe, loadData has try/catch
// static/admin/js/company-detail.js
// Create custom logger for company detail
const companyDetailLog = window.LogConfig.createLogger('COMPANY-DETAIL');
function adminCompanyDetail() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Company detail page specific state
currentPage: 'company-detail',
company: null,
loading: false,
error: null,
companyId: null,
// Initialize
async init() {
companyDetailLog.info('=== COMPANY DETAIL PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._companyDetailInitialized) {
companyDetailLog.warn('Company detail page already initialized, skipping...');
return;
}
window._companyDetailInitialized = true;
// Get company ID from URL
const path = window.location.pathname;
const match = path.match(/\/admin\/companies\/(\d+)$/);
if (match) {
this.companyId = match[1];
companyDetailLog.info('Viewing company:', this.companyId);
await this.loadCompany();
} else {
companyDetailLog.error('No company ID in URL');
this.error = 'Invalid company URL';
Utils.showToast('Invalid company URL', 'error');
}
companyDetailLog.info('=== COMPANY DETAIL PAGE INITIALIZATION COMPLETE ===');
},
// Load company data
async loadCompany() {
companyDetailLog.info('Loading company details...');
this.loading = true;
this.error = null;
try {
const url = `/admin/companies/${this.companyId}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
const startTime = performance.now();
const response = await apiClient.get(url);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('GET', url, response, 'response');
window.LogConfig.logPerformance('Load Company Details', duration);
this.company = response;
companyDetailLog.info(`Company loaded in ${duration}ms`, {
id: this.company.id,
name: this.company.name,
is_verified: this.company.is_verified,
is_active: this.company.is_active,
vendor_count: this.company.vendor_count
});
companyDetailLog.debug('Full company data:', this.company);
} catch (error) {
window.LogConfig.logError(error, 'Load Company Details');
this.error = error.message || 'Failed to load company details';
Utils.showToast('Failed to load company details', 'error');
} finally {
this.loading = false;
}
},
// Format date (matches dashboard pattern)
formatDate(dateString) {
if (!dateString) {
companyDetailLog.debug('formatDate called with empty dateString');
return '-';
}
const formatted = Utils.formatDate(dateString);
companyDetailLog.debug(`Date formatted: ${dateString} -> ${formatted}`);
return formatted;
},
// Delete company
async deleteCompany() {
companyDetailLog.info('Delete company requested:', this.companyId);
if (this.company?.vendor_count > 0) {
Utils.showToast(`Cannot delete company with ${this.company.vendor_count} vendor(s). Delete vendors first.`, 'error');
return;
}
if (!confirm(`Are you sure you want to delete company "${this.company.name}"?\n\nThis action cannot be undone.`)) {
companyDetailLog.info('Delete cancelled by user');
return;
}
// Second confirmation for safety
if (!confirm(`FINAL CONFIRMATION\n\nAre you absolutely sure you want to delete "${this.company.name}"?`)) {
companyDetailLog.info('Delete cancelled by user (second confirmation)');
return;
}
try {
const url = `/admin/companies/${this.companyId}?confirm=true`;
window.LogConfig.logApiCall('DELETE', url, null, 'request');
companyDetailLog.info('Deleting company:', this.companyId);
await apiClient.delete(url);
window.LogConfig.logApiCall('DELETE', url, null, 'response');
Utils.showToast('Company deleted successfully', 'success');
companyDetailLog.info('Company deleted successfully');
// Redirect to companies list
setTimeout(() => window.location.href = '/admin/companies', 1500);
} catch (error) {
window.LogConfig.logError(error, 'Delete Company');
Utils.showToast(error.message || 'Failed to delete company', 'error');
}
},
// Refresh company data
async refresh() {
companyDetailLog.info('=== COMPANY REFRESH TRIGGERED ===');
await this.loadCompany();
Utils.showToast('Company details refreshed', 'success');
companyDetailLog.info('=== COMPANY REFRESH COMPLETE ===');
}
};
}
companyDetailLog.info('Company detail module loaded');

View File

@@ -1,392 +0,0 @@
// static/admin/js/company-edit.js
// Create custom logger for company edit
const companyEditLog = window.LogConfig.createLogger('COMPANY-EDIT');
function adminCompanyEdit() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Company edit page specific state
currentPage: 'company-edit',
loading: false,
company: null,
formData: {},
errors: {},
loadingCompany: false,
saving: false,
companyId: null,
// Transfer ownership state
showTransferOwnershipModal: false,
transferring: false,
transferData: {
new_owner_user_id: null,
confirm_transfer: false,
transfer_reason: ''
},
// User search state
userSearchQuery: '',
userSearchResults: [],
selectedUser: null,
showUserDropdown: false,
searchingUsers: false,
searchDebounceTimer: null,
showConfirmError: false,
showOwnerError: false,
// Initialize
async init() {
companyEditLog.info('=== COMPANY EDIT PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._companyEditInitialized) {
companyEditLog.warn('Company edit page already initialized, skipping...');
return;
}
window._companyEditInitialized = true;
try {
// Get company ID from URL
const path = window.location.pathname;
const match = path.match(/\/admin\/companies\/(\d+)\/edit/);
if (match) {
this.companyId = parseInt(match[1], 10);
companyEditLog.info('Editing company:', this.companyId);
await this.loadCompany();
} else {
companyEditLog.error('No company ID in URL');
Utils.showToast('Invalid company URL', 'error');
setTimeout(() => window.location.href = '/admin/companies', 2000);
}
companyEditLog.info('=== COMPANY EDIT PAGE INITIALIZATION COMPLETE ===');
} catch (error) {
window.LogConfig.logError(error, 'Company Edit Init');
Utils.showToast('Failed to initialize page', 'error');
}
},
// Load company data
async loadCompany() {
companyEditLog.info('Loading company data...');
this.loadingCompany = true;
try {
const url = `/admin/companies/${this.companyId}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
const startTime = performance.now();
const response = await apiClient.get(url);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('GET', url, response, 'response');
window.LogConfig.logPerformance('Load Company', duration);
this.company = response;
// Initialize form data
this.formData = {
name: response.name || '',
description: response.description || '',
contact_email: response.contact_email || '',
contact_phone: response.contact_phone || '',
website: response.website || '',
business_address: response.business_address || '',
tax_number: response.tax_number || ''
};
companyEditLog.info(`Company loaded in ${duration}ms`, {
company_id: this.company.id,
name: this.company.name
});
companyEditLog.debug('Form data initialized:', this.formData);
} catch (error) {
window.LogConfig.logError(error, 'Load Company');
Utils.showToast('Failed to load company', 'error');
setTimeout(() => window.location.href = '/admin/companies', 2000);
} finally {
this.loadingCompany = false;
}
},
// Submit form
async handleSubmit() {
companyEditLog.info('=== SUBMITTING COMPANY UPDATE ===');
companyEditLog.debug('Form data:', this.formData);
this.errors = {};
this.saving = true;
try {
const url = `/admin/companies/${this.companyId}`;
window.LogConfig.logApiCall('PUT', url, this.formData, 'request');
const startTime = performance.now();
const response = await apiClient.put(url, this.formData);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('PUT', url, response, 'response');
window.LogConfig.logPerformance('Update Company', duration);
this.company = response;
Utils.showToast('Company updated successfully', 'success');
companyEditLog.info(`Company updated successfully in ${duration}ms`, response);
} catch (error) {
window.LogConfig.logError(error, 'Update Company');
// Handle validation errors
if (error.details && error.details.validation_errors) {
error.details.validation_errors.forEach(err => {
const field = err.loc?.[1] || err.loc?.[0];
if (field) {
this.errors[field] = err.msg;
}
});
companyEditLog.debug('Validation errors:', this.errors);
}
Utils.showToast(error.message || 'Failed to update company', 'error');
} finally {
this.saving = false;
companyEditLog.info('=== COMPANY UPDATE COMPLETE ===');
}
},
// Toggle verification
async toggleVerification() {
const action = this.company.is_verified ? 'unverify' : 'verify';
companyEditLog.info(`Toggle verification: ${action}`);
if (!confirm(`Are you sure you want to ${action} this company?`)) {
companyEditLog.info('Verification toggle cancelled by user');
return;
}
this.saving = true;
try {
const url = `/admin/companies/${this.companyId}/verification`;
const payload = { is_verified: !this.company.is_verified };
window.LogConfig.logApiCall('PUT', url, payload, 'request');
const response = await apiClient.put(url, payload);
window.LogConfig.logApiCall('PUT', url, response, 'response');
this.company = response;
Utils.showToast(`Company ${action}ed successfully`, 'success');
companyEditLog.info(`Company ${action}ed successfully`);
} catch (error) {
window.LogConfig.logError(error, `Toggle Verification (${action})`);
Utils.showToast(`Failed to ${action} company`, 'error');
} finally {
this.saving = false;
}
},
// Toggle active status
async toggleActive() {
const action = this.company.is_active ? 'deactivate' : 'activate';
companyEditLog.info(`Toggle active status: ${action}`);
if (!confirm(`Are you sure you want to ${action} this company?\n\nThis will affect all vendors under this company.`)) {
companyEditLog.info('Active status toggle cancelled by user');
return;
}
this.saving = true;
try {
const url = `/admin/companies/${this.companyId}/status`;
const payload = { is_active: !this.company.is_active };
window.LogConfig.logApiCall('PUT', url, payload, 'request');
const response = await apiClient.put(url, payload);
window.LogConfig.logApiCall('PUT', url, response, 'response');
this.company = response;
Utils.showToast(`Company ${action}d successfully`, 'success');
companyEditLog.info(`Company ${action}d successfully`);
} catch (error) {
window.LogConfig.logError(error, `Toggle Active Status (${action})`);
Utils.showToast(`Failed to ${action} company`, 'error');
} finally {
this.saving = false;
}
},
// Transfer company ownership
async transferOwnership() {
companyEditLog.info('=== TRANSFERRING COMPANY OWNERSHIP ===');
companyEditLog.debug('Transfer data:', this.transferData);
if (!this.transferData.new_owner_user_id) {
this.showOwnerError = true;
return;
}
if (!this.transferData.confirm_transfer) {
this.showConfirmError = true;
return;
}
// Clear errors
this.showOwnerError = false;
this.showConfirmError = false;
this.transferring = true;
try {
const url = `/admin/companies/${this.companyId}/transfer-ownership`;
const payload = {
new_owner_user_id: parseInt(this.transferData.new_owner_user_id, 10),
confirm_transfer: true,
transfer_reason: this.transferData.transfer_reason || null
};
window.LogConfig.logApiCall('POST', url, payload, 'request');
const response = await apiClient.post(url, payload);
window.LogConfig.logApiCall('POST', url, response, 'response');
Utils.showToast('Ownership transferred successfully', 'success');
companyEditLog.info('Ownership transferred successfully', response);
// Close modal and reload company data
this.showTransferOwnershipModal = false;
this.resetTransferData();
await this.loadCompany();
} catch (error) {
window.LogConfig.logError(error, 'Transfer Ownership');
Utils.showToast(error.message || 'Failed to transfer ownership', 'error');
} finally {
this.transferring = false;
companyEditLog.info('=== OWNERSHIP TRANSFER COMPLETE ===');
}
},
// Reset transfer data
resetTransferData() {
this.transferData = {
new_owner_user_id: null,
confirm_transfer: false,
transfer_reason: ''
};
this.userSearchQuery = '';
this.userSearchResults = [];
this.selectedUser = null;
this.showUserDropdown = false;
this.showConfirmError = false;
this.showOwnerError = false;
},
// Search users for transfer ownership
searchUsers() {
// Debounce search
clearTimeout(this.searchDebounceTimer);
if (this.userSearchQuery.length < 2) {
this.userSearchResults = [];
this.showUserDropdown = false;
return;
}
this.searchDebounceTimer = setTimeout(async () => {
companyEditLog.info('Searching users:', this.userSearchQuery);
this.searchingUsers = true;
this.showUserDropdown = true;
try {
const url = `/admin/users/search?q=${encodeURIComponent(this.userSearchQuery)}&limit=10`;
const response = await apiClient.get(url);
this.userSearchResults = response.users || response || [];
companyEditLog.debug('User search results:', this.userSearchResults);
} catch (error) {
window.LogConfig.logError(error, 'Search Users');
this.userSearchResults = [];
} finally {
this.searchingUsers = false;
}
}, 300);
},
// Select a user from search results
selectUser(user) {
companyEditLog.info('Selected user:', user);
this.selectedUser = user;
this.transferData.new_owner_user_id = user.id;
this.userSearchQuery = user.username;
this.showUserDropdown = false;
this.userSearchResults = [];
},
// Clear selected user
clearSelectedUser() {
this.selectedUser = null;
this.transferData.new_owner_user_id = null;
this.userSearchQuery = '';
this.userSearchResults = [];
},
// Delete company
async deleteCompany() {
companyEditLog.info('=== DELETING COMPANY ===');
if (this.company.vendor_count > 0) {
Utils.showToast(`Cannot delete company with ${this.company.vendor_count} vendors. Remove vendors first.`, 'error');
return;
}
if (!confirm(`Are you sure you want to delete company "${this.company.name}"?\n\nThis action cannot be undone.`)) {
companyEditLog.info('Company deletion cancelled by user');
return;
}
// Double confirmation for critical action
if (!confirm(`FINAL CONFIRMATION: Delete "${this.company.name}"?\n\nThis will permanently delete the company and all its data.`)) {
companyEditLog.info('Company deletion cancelled at final confirmation');
return;
}
this.saving = true;
try {
const url = `/admin/companies/${this.companyId}?confirm=true`;
window.LogConfig.logApiCall('DELETE', url, null, 'request');
const response = await apiClient.delete(url);
window.LogConfig.logApiCall('DELETE', url, response, 'response');
Utils.showToast('Company deleted successfully', 'success');
companyEditLog.info('Company deleted successfully');
// Redirect to companies list
setTimeout(() => {
window.location.href = '/admin/companies';
}, 1500);
} catch (error) {
window.LogConfig.logError(error, 'Delete Company');
Utils.showToast(error.message || 'Failed to delete company', 'error');
} finally {
this.saving = false;
companyEditLog.info('=== COMPANY DELETION COMPLETE ===');
}
}
};
}
companyEditLog.info('Company edit module loaded');

View File

@@ -1,191 +0,0 @@
// noqa: js-006 - async init pattern is safe, loadData has try/catch
// static/admin/js/dashboard.js
// ✅ Use centralized logger - ONE LINE!
const dashLog = window.LogConfig.loggers.dashboard;
function adminDashboard() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Dashboard-specific state
currentPage: 'dashboard',
stats: {
totalVendors: 0,
activeUsers: 0,
verifiedVendors: 0,
importJobs: 0
},
recentVendors: [],
loading: true,
error: null,
/**
* Initialize dashboard
*/
async init() {
// Guard against multiple initialization
if (window._dashboardInitialized) {
dashLog.warn('Dashboard already initialized, skipping...');
return;
}
window._dashboardInitialized = true;
dashLog.info('=== DASHBOARD INITIALIZING ===');
dashLog.debug('Current URL:', window.location.href);
dashLog.debug('Current pathname:', window.location.pathname);
const token = localStorage.getItem('admin_token');
dashLog.debug('Has admin_token?', !!token);
if (token) {
dashLog.debug('Token preview:', token.substring(0, 20) + '...');
}
dashLog.debug('Dashboard initialization flag set');
const startTime = performance.now();
await this.loadDashboard();
const duration = performance.now() - startTime;
window.LogConfig.logPerformance('Dashboard Init', duration);
dashLog.info('=== DASHBOARD INITIALIZATION COMPLETE ===');
},
/**
* Load all dashboard data
*/
async loadDashboard() {
dashLog.info('Loading dashboard data...');
this.loading = true;
this.error = null;
dashLog.debug('Dashboard state: loading=true, error=null');
try {
dashLog.group('Loading dashboard data');
const startTime = performance.now();
// Load stats and vendors in parallel
await Promise.all([
this.loadStats(),
this.loadRecentVendors()
]);
const duration = performance.now() - startTime;
dashLog.groupEnd();
window.LogConfig.logPerformance('Load Dashboard Data', duration);
dashLog.info(`Dashboard data loaded successfully in ${duration}ms`);
} catch (error) {
window.LogConfig.logError(error, 'Dashboard Load');
this.error = error.message;
Utils.showToast('Failed to load dashboard data', 'error');
} finally {
this.loading = false;
dashLog.debug('Dashboard state: loading=false');
dashLog.info('Dashboard load attempt finished');
}
},
/**
* Load platform statistics
*/
async loadStats() {
dashLog.info('Loading platform statistics...');
const url = '/admin/dashboard/stats/platform';
window.LogConfig.logApiCall('GET', url, null, 'request');
try {
const startTime = performance.now();
const data = await apiClient.get(url);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('GET', url, data, 'response');
window.LogConfig.logPerformance('Load Stats', duration);
// Map API response to stats cards
this.stats = {
totalVendors: data.vendors?.total_vendors || 0,
activeUsers: data.users?.active_users || 0,
verifiedVendors: data.vendors?.verified_vendors || 0,
importJobs: data.imports?.total_imports || 0
};
dashLog.info('Stats mapped:', this.stats);
} catch (error) {
dashLog.error('Failed to load stats:', error);
throw error;
}
},
/**
* Load recent vendors
*/
async loadRecentVendors() {
dashLog.info('Loading recent vendors...');
const url = '/admin/dashboard';
window.LogConfig.logApiCall('GET', url, null, 'request');
try {
const startTime = performance.now();
const data = await apiClient.get(url);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('GET', url, data, 'response');
window.LogConfig.logPerformance('Load Recent Vendors', duration);
this.recentVendors = data.recent_vendors || [];
if (this.recentVendors.length > 0) {
dashLog.info(`Loaded ${this.recentVendors.length} recent vendors`);
dashLog.debug('First vendor:', this.recentVendors[0]);
} else {
dashLog.warn('No recent vendors found');
}
} catch (error) {
dashLog.error('Failed to load recent vendors:', error);
throw error;
}
},
/**
* Format date for display
*/
formatDate(dateString) {
if (!dateString) {
dashLog.debug('formatDate called with empty dateString');
return '-';
}
const formatted = Utils.formatDate(dateString);
dashLog.debug(`Date formatted: ${dateString} -> ${formatted}`);
return formatted;
},
/**
* Navigate to vendor detail page
*/
viewVendor(vendorCode) {
dashLog.info('Navigating to vendor:', vendorCode);
const url = `/admin/vendors?code=${vendorCode}`;
dashLog.debug('Navigation URL:', url);
window.location.href = url;
},
/**
* Refresh dashboard data
*/
async refresh() {
dashLog.info('=== DASHBOARD REFRESH TRIGGERED ===');
await this.loadDashboard();
Utils.showToast('Dashboard refreshed', 'success');
dashLog.info('=== DASHBOARD REFRESH COMPLETE ===');
}
};
}
dashLog.info('Dashboard module loaded');

View File

@@ -1,428 +0,0 @@
/**
* Alpine.js v3 global data initialization
* Provides theme toggle, menu controls, sidebar sections, and page state
*/
function data() {
// ─────────────────────────────────────────────────────────────────
// Theme (dark mode) persistence
// ─────────────────────────────────────────────────────────────────
function getThemeFromLocalStorage() {
if (window.localStorage.getItem('dark')) {
return JSON.parse(window.localStorage.getItem('dark'))
}
return (
!!window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
)
}
function setThemeToLocalStorage(value) {
window.localStorage.setItem('dark', value)
}
// ─────────────────────────────────────────────────────────────────
// Sidebar sections persistence
// ─────────────────────────────────────────────────────────────────
const SIDEBAR_STORAGE_KEY = 'admin_sidebar_sections';
// Default state: Platform Administration open, others closed
const defaultSections = {
superAdmin: true, // Super admin section (only visible to super admins)
platformAdmin: true,
vendorOps: false,
marketplace: false,
billing: false,
contentMgmt: false,
devTools: false,
platformHealth: false,
monitoring: false,
settingsSection: false
};
function getSidebarSectionsFromStorage() {
try {
const stored = window.localStorage.getItem(SIDEBAR_STORAGE_KEY);
if (stored) {
return { ...defaultSections, ...JSON.parse(stored) };
}
} catch (e) {
console.warn('Failed to parse sidebar sections from localStorage:', e);
}
return { ...defaultSections };
}
function saveSidebarSectionsToStorage(sections) {
try {
window.localStorage.setItem(SIDEBAR_STORAGE_KEY, JSON.stringify(sections));
} catch (e) {
console.warn('Failed to save sidebar sections to localStorage:', e);
}
}
// ─────────────────────────────────────────────────────────────────
// Last visited page tracking (for redirect after login)
// ─────────────────────────────────────────────────────────────────
const LAST_PAGE_KEY = 'admin_last_visited_page';
const currentPath = window.location.pathname;
// Save current page (exclude login, logout, error pages)
if (currentPath.startsWith('/admin/') &&
!currentPath.includes('/login') &&
!currentPath.includes('/logout') &&
!currentPath.includes('/errors/')) {
try {
window.localStorage.setItem(LAST_PAGE_KEY, currentPath);
} catch (e) {
// Ignore storage errors
}
}
// Helper to get admin profile from localStorage
function getAdminProfileFromStorage() {
try {
// Check admin_user first (set by login), then adminProfile (legacy)
const stored = window.localStorage.getItem('admin_user') ||
window.localStorage.getItem('adminProfile');
if (stored) {
return JSON.parse(stored);
}
} catch (e) {
console.warn('Failed to parse admin profile from localStorage:', e);
}
return null;
}
// Map pages to their parent sections
const pageSectionMap = {
// Super Admin section
'admin-users': 'superAdmin',
// Platform Administration
companies: 'platformAdmin',
vendors: 'platformAdmin',
messages: 'platformAdmin',
// Vendor Operations (Products, Customers, Inventory, Orders, Shipping)
'marketplace-products': 'vendorOps',
'vendor-products': 'vendorOps',
customers: 'vendorOps',
inventory: 'vendorOps',
orders: 'vendorOps',
// Future: shipping will map to 'vendorOps'
// Marketplace
'marketplace-letzshop': 'marketplace',
// Content Management
'platform-homepage': 'contentMgmt',
'content-pages': 'contentMgmt',
'vendor-theme': 'contentMgmt',
// Developer Tools
components: 'devTools',
icons: 'devTools',
// Platform Health
testing: 'platformHealth',
'code-quality': 'platformHealth',
// Platform Monitoring
imports: 'monitoring',
'background-tasks': 'monitoring',
logs: 'monitoring',
'notifications-settings': 'monitoring',
// Platform Settings
settings: 'settingsSection',
profile: 'settingsSection',
'api-keys': 'settingsSection'
};
return {
// ─────────────────────────────────────────────────────────────────
// Theme
// ─────────────────────────────────────────────────────────────────
dark: getThemeFromLocalStorage(),
toggleTheme() {
this.dark = !this.dark
setThemeToLocalStorage(this.dark)
},
// ─────────────────────────────────────────────────────────────────
// Mobile side menu
// ─────────────────────────────────────────────────────────────────
isSideMenuOpen: false,
toggleSideMenu() {
this.isSideMenuOpen = !this.isSideMenuOpen
},
closeSideMenu() {
this.isSideMenuOpen = false
},
// ─────────────────────────────────────────────────────────────────
// Notifications menu
// ─────────────────────────────────────────────────────────────────
isNotificationsMenuOpen: false,
toggleNotificationsMenu() {
this.isNotificationsMenuOpen = !this.isNotificationsMenuOpen
},
closeNotificationsMenu() {
this.isNotificationsMenuOpen = false
},
// ─────────────────────────────────────────────────────────────────
// Profile menu
// ─────────────────────────────────────────────────────────────────
isProfileMenuOpen: false,
toggleProfileMenu() {
this.isProfileMenuOpen = !this.isProfileMenuOpen
},
closeProfileMenu() {
this.isProfileMenuOpen = false
},
// ─────────────────────────────────────────────────────────────────
// Pages menu (legacy)
// ─────────────────────────────────────────────────────────────────
isPagesMenuOpen: false,
togglePagesMenu() {
this.isPagesMenuOpen = !this.isPagesMenuOpen
},
// ─────────────────────────────────────────────────────────────────
// Collapsible sidebar sections
// ─────────────────────────────────────────────────────────────────
openSections: getSidebarSectionsFromStorage(),
toggleSection(section) {
this.openSections[section] = !this.openSections[section];
saveSidebarSectionsToStorage(this.openSections);
},
// Auto-expand section containing current page
expandSectionForCurrentPage() {
const section = pageSectionMap[this.currentPage];
if (section && !this.openSections[section]) {
this.openSections[section] = true;
saveSidebarSectionsToStorage(this.openSections);
}
},
// ─────────────────────────────────────────────────────────────────
// Page identifier - will be set by individual pages
// ─────────────────────────────────────────────────────────────────
currentPage: '',
// ─────────────────────────────────────────────────────────────────
// Admin profile and super admin flag
// ─────────────────────────────────────────────────────────────────
adminProfile: getAdminProfileFromStorage(),
get isSuperAdmin() {
return this.adminProfile?.is_super_admin === true;
},
// ─────────────────────────────────────────────────────────────────
// Dynamic menu visibility (loaded from API)
// ─────────────────────────────────────────────────────────────────
menuData: null,
menuLoading: false,
visibleMenuItems: new Set(),
async loadMenuConfig(forceReload = false) {
// Don't reload if already loaded (unless forced)
if (!forceReload && (this.menuData || this.menuLoading)) return;
// Skip if apiClient is not available (e.g., on login page)
if (typeof apiClient === 'undefined') {
console.debug('Menu config: apiClient not available');
return;
}
// Skip if not authenticated
if (!localStorage.getItem('admin_token')) {
console.debug('Menu config: no admin_token, skipping');
return;
}
this.menuLoading = true;
try {
this.menuData = await apiClient.get('/admin/menu-config/render/admin');
// Build a set of visible menu item IDs for quick lookup
this.visibleMenuItems = new Set();
for (const section of (this.menuData?.sections || [])) {
for (const item of (section.items || [])) {
this.visibleMenuItems.add(item.id);
}
}
console.debug('Menu config loaded:', this.visibleMenuItems.size, 'items');
} catch (e) {
// Silently fail - menu will show all items as fallback
console.debug('Menu config not loaded, using defaults:', e?.message || e);
} finally {
this.menuLoading = false;
}
},
async reloadSidebarMenu() {
// Force reload the sidebar menu config
this.menuData = null;
this.visibleMenuItems = new Set();
await this.loadMenuConfig(true);
},
isMenuItemVisible(menuItemId) {
// If menu not loaded yet, show all items (fallback to hardcoded)
if (!this.menuData) return true;
return this.visibleMenuItems.has(menuItemId);
},
isSectionVisible(sectionId) {
// If menu not loaded yet, show all sections
if (!this.menuData) return true;
// Check if any item in this section is visible
const section = this.menuData?.sections?.find(s => s.id === sectionId);
return section && section.items && section.items.length > 0;
}
}
}
/**
* Language selector component for i18n support
* Used by language_selector macros in templates
*
* @param {string} currentLang - Current language code (e.g., 'fr')
* @param {Array} enabledLanguages - Array of enabled language codes
* @returns {Object} Alpine.js component data
*/
function languageSelector(currentLang, enabledLanguages) {
return {
isLangOpen: false,
currentLang: currentLang || 'fr',
languages: enabledLanguages || ['fr', 'de', 'en'],
languageNames: {
'en': 'English',
'fr': 'Français',
'de': 'Deutsch',
'lb': 'Lëtzebuergesch'
},
languageFlags: {
'en': 'gb',
'fr': 'fr',
'de': 'de',
'lb': 'lu'
},
async setLanguage(lang) {
if (lang === this.currentLang) {
this.isLangOpen = false;
return;
}
try {
const response = await fetch('/api/v1/language/set', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ language: lang })
});
if (response.ok) {
this.currentLang = lang;
window.location.reload();
}
} catch (error) {
console.error('Failed to set language:', error);
}
this.isLangOpen = false;
}
};
}
// Export to window for use in templates
window.languageSelector = languageSelector;
/**
* Header messages badge component
* Shows unread message count in header
*/
function headerMessages() {
return {
unreadCount: 0,
pollInterval: null,
async init() {
await this.fetchUnreadCount();
// Poll every 30 seconds
this.pollInterval = setInterval(() => this.fetchUnreadCount(), 30000);
},
destroy() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
}
},
async fetchUnreadCount() {
try {
const response = await apiClient.get('/admin/messages/unread-count');
this.unreadCount = response.total_unread || 0;
} catch (error) {
// Silently fail - don't spam console for auth issues on login page
}
}
};
}
// Export to window
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

@@ -1,246 +0,0 @@
// static/admin/js/login.js
// noqa: js-003 - Standalone login page, doesn't use base layout
// noqa: js-004 - No sidebar on login page, doesn't need currentPage
// ✅ Use centralized logger - ONE LINE!
// Create custom logger for login page
const loginLog = window.LogConfig.createLogger('LOGIN');
function adminLogin() {
return {
dark: false,
credentials: {
username: '',
password: ''
},
loading: false,
error: null,
success: null,
errors: {},
init() {
// Guard against multiple initialization
if (window._adminLoginInitialized) return;
window._adminLoginInitialized = true;
loginLog.info('=== LOGIN PAGE INITIALIZING ===');
loginLog.debug('Current pathname:', window.location.pathname);
loginLog.debug('Current URL:', window.location.href);
// Just set theme - NO auth checking, NO token clearing!
this.dark = localStorage.getItem('theme') === 'dark';
loginLog.debug('Dark mode:', this.dark);
// DON'T clear tokens on init!
// If user lands here with a valid token, they might be navigating manually
// or got redirected. Let them try to login or navigate away.
const token = localStorage.getItem('admin_token');
if (token) {
loginLog.warn('Found existing token on login page');
loginLog.debug('Token preview:', token.substring(0, 20) + '...');
loginLog.info('Not clearing token - user may have navigated here manually');
} else {
loginLog.debug('No existing token found');
}
loginLog.info('=== LOGIN PAGE INITIALIZATION COMPLETE ===');
},
clearTokens() {
loginLog.debug('Clearing all auth tokens...');
const tokensBefore = {
admin_token: !!localStorage.getItem('admin_token'),
admin_user: !!localStorage.getItem('admin_user'),
token: !!localStorage.getItem('token')
};
loginLog.debug('Tokens before clear:', tokensBefore);
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_user');
localStorage.removeItem('token');
const tokensAfter = {
admin_token: !!localStorage.getItem('admin_token'),
admin_user: !!localStorage.getItem('admin_user'),
token: !!localStorage.getItem('token')
};
loginLog.debug('Tokens after clear:', tokensAfter);
},
clearErrors() {
loginLog.debug('Clearing form errors');
this.error = null;
this.success = null;
this.errors = {};
},
validateForm() {
loginLog.debug('Validating login form...');
this.clearErrors();
let isValid = true;
if (!this.credentials.username.trim()) {
this.errors.username = 'Username is required';
loginLog.warn('Validation failed: Username is required');
isValid = false;
}
if (!this.credentials.password) {
this.errors.password = 'Password is required';
loginLog.warn('Validation failed: Password is required');
isValid = false;
} else if (this.credentials.password.length < 6) {
this.errors.password = 'Password must be at least 6 characters';
loginLog.warn('Validation failed: Password too short');
isValid = false;
}
loginLog.info('Form validation result:', isValid ? 'VALID' : 'INVALID');
return isValid;
},
async handleLogin() {
loginLog.info('=== LOGIN ATTEMPT STARTED ===');
if (!this.validateForm()) {
loginLog.warn('Form validation failed, aborting login');
return;
}
this.loading = true;
this.clearErrors();
loginLog.debug('Login state set to loading');
try {
loginLog.info('Calling login API endpoint...');
loginLog.debug('Username:', this.credentials.username);
const url = '/admin/auth/login';
const payload = {
email_or_username: this.credentials.username.trim(),
password: this.credentials.password
};
window.LogConfig.logApiCall('POST', url, { username: payload.username }, 'request');
const startTime = performance.now();
const response = await apiClient.post(url, payload);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('POST', url, {
hasToken: !!response.access_token,
user: response.user?.username
}, 'response');
window.LogConfig.logPerformance('Login', duration);
loginLog.info(`Login API response received in ${duration}ms`);
loginLog.debug('Response structure:', {
hasToken: !!response.access_token,
hasUser: !!response.user,
userRole: response.user?.role,
userName: response.user?.username
});
// Validate response
if (!response.access_token) {
loginLog.error('Invalid response: No access token');
throw new Error('Invalid response from server - no token');
}
if (response.user && response.user.role !== 'admin') {
loginLog.error('Authorization failed: User is not admin', {
actualRole: response.user.role
});
throw new Error('Access denied. Admin privileges required.');
}
loginLog.info('Login successful, storing authentication data...');
// Store authentication data
localStorage.setItem('admin_token', response.access_token);
localStorage.setItem('token', response.access_token);
loginLog.debug('Token stored, length:', response.access_token.length);
if (response.user) {
localStorage.setItem('admin_user', JSON.stringify(response.user));
loginLog.debug('User data stored:', {
username: response.user.username,
role: response.user.role,
id: response.user.id,
is_super_admin: response.user.is_super_admin
});
}
// Verify storage
const storedToken = localStorage.getItem('admin_token');
const storedUser = localStorage.getItem('admin_user');
loginLog.info('Storage verification:', {
tokenStored: !!storedToken,
userStored: !!storedUser,
tokenLength: storedToken?.length
});
// Show success message
this.success = 'Login successful! Checking platform access...';
loginLog.info('Success message displayed to user');
// Check if platform selection is required
try {
loginLog.info('Checking accessible platforms...');
const platformsResponse = await apiClient.get('/admin/auth/accessible-platforms');
loginLog.debug('Accessible platforms response:', platformsResponse);
if (platformsResponse.requires_platform_selection) {
// Platform admin needs to select a platform
loginLog.info('Platform selection required, redirecting...');
this.success = 'Login successful! Please select a platform...';
window.location.href = '/admin/select-platform';
return;
}
} catch (platformError) {
loginLog.warn('Could not check platforms, proceeding to dashboard:', platformError);
}
// Super admin or single platform - proceed to dashboard
this.success = 'Login successful! Redirecting...';
// Check for last visited page (saved before logout)
const lastPage = localStorage.getItem('admin_last_visited_page');
const redirectTo = (lastPage && lastPage.startsWith('/admin/') && !lastPage.includes('/login') && !lastPage.includes('/select-platform'))
? lastPage
: '/admin/dashboard';
loginLog.info('=== EXECUTING REDIRECT ===');
loginLog.debug('Last visited page:', lastPage);
loginLog.debug('Target URL:', redirectTo);
// Use href instead of replace to allow back button
window.location.href = redirectTo;
} catch (error) {
window.LogConfig.logError(error, 'Login');
this.error = error.message || 'Invalid username or password. Please try again.';
loginLog.info('Error message displayed to user:', this.error);
// Only clear tokens on login FAILURE
this.clearTokens();
loginLog.info('Tokens cleared after error');
} finally {
this.loading = false;
loginLog.debug('Login state set to not loading');
loginLog.info('=== LOGIN ATTEMPT FINISHED ===');
}
},
toggleDarkMode() {
loginLog.debug('Toggling dark mode...');
this.dark = !this.dark;
localStorage.setItem('theme', this.dark ? 'dark' : 'light');
loginLog.info('Dark mode:', this.dark ? 'ON' : 'OFF');
}
}
}
loginLog.info('Login module loaded');

View File

@@ -1,187 +0,0 @@
// static/admin/js/my-menu-config.js
// Personal menu configuration for super admins
//
// TODO: BUG - Sidebar menu doesn't update immediately after changes.
// User must navigate to another page to see the updated menu.
// The issue is that Alpine.js doesn't properly track reactivity for the
// visibleMenuItems Set in init-alpine.js. Attempted fixes with reloadSidebarMenu()
// and window.location.reload() didn't work reliably.
// Possible solutions:
// 1. Convert visibleMenuItems from Set to plain object for better Alpine reactivity
// 2. Use Alpine.store() for shared state between components
// 3. Dispatch a custom event that the sidebar listens for
// 4. Force re-render of sidebar component after changes
const myMenuConfigLog = window.LogConfig?.loggers?.myMenuConfig || window.LogConfig?.createLogger?.('myMenuConfig') || console;
function adminMyMenuConfig() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Page-specific state
currentPage: 'my-menu',
loading: true,
error: null,
successMessage: null,
saving: false,
// Data
menuConfig: null,
// Computed grouped items
get groupedItems() {
if (!this.menuConfig?.items) return [];
// Group items by section
const sections = {};
for (const item of this.menuConfig.items) {
const sectionId = item.section_id;
if (!sections[sectionId]) {
sections[sectionId] = {
id: sectionId,
label: item.section_label,
isSuperAdminOnly: item.is_super_admin_only,
items: [],
visibleCount: 0
};
}
sections[sectionId].items.push(item);
if (item.is_visible) {
sections[sectionId].visibleCount++;
}
}
// Convert to array and maintain order
return Object.values(sections);
},
async init() {
// Guard against multiple initialization
if (window._adminMyMenuConfigInitialized) {
myMenuConfigLog.warn('Already initialized, skipping');
return;
}
window._adminMyMenuConfigInitialized = true;
myMenuConfigLog.info('=== MY MENU CONFIG PAGE INITIALIZING ===');
try {
await this.loadMenuConfig();
myMenuConfigLog.info('=== MY MENU CONFIG PAGE INITIALIZED ===');
} catch (error) {
myMenuConfigLog.error('Failed to initialize my menu config page:', error);
this.error = 'Failed to load page data. Please refresh.';
}
},
async refresh() {
this.error = null;
this.successMessage = null;
await this.loadMenuConfig();
},
async loadMenuConfig() {
this.loading = true;
this.error = null;
try {
this.menuConfig = await apiClient.get('/admin/menu-config/user');
myMenuConfigLog.info('Loaded menu config:', {
totalItems: this.menuConfig?.total_items,
visibleItems: this.menuConfig?.visible_items
});
} catch (error) {
myMenuConfigLog.error('Failed to load menu config:', error);
this.error = error.message || 'Failed to load menu configuration';
} finally {
this.loading = false;
}
},
async toggleVisibility(item) {
if (item.is_mandatory) {
myMenuConfigLog.warn('Cannot toggle mandatory item:', item.id);
return;
}
this.saving = true;
this.error = null;
this.successMessage = null;
const newVisibility = !item.is_visible;
try {
await apiClient.put('/admin/menu-config/user', {
menu_item_id: item.id,
is_visible: newVisibility
});
// Update local state
item.is_visible = newVisibility;
// Update counts
if (newVisibility) {
this.menuConfig.visible_items++;
this.menuConfig.hidden_items--;
} else {
this.menuConfig.visible_items--;
this.menuConfig.hidden_items++;
}
myMenuConfigLog.info('Toggled visibility:', item.id, newVisibility);
// Reload the page to refresh sidebar
window.location.reload();
} catch (error) {
myMenuConfigLog.error('Failed to toggle visibility:', error);
this.error = error.message || 'Failed to update menu visibility';
this.saving = false;
}
},
async showAll() {
if (!confirm('This will show all menu items. Continue?')) {
return;
}
this.saving = true;
this.error = null;
this.successMessage = null;
try {
await apiClient.post('/admin/menu-config/user/show-all');
myMenuConfigLog.info('Showed all menu items');
// Reload the page to refresh sidebar
window.location.reload();
} catch (error) {
myMenuConfigLog.error('Failed to show all menu items:', error);
this.error = error.message || 'Failed to show all menu items';
this.saving = false;
}
},
async resetToDefaults() {
if (!confirm('This will hide all menu items (except mandatory ones). You can then enable the ones you want. Continue?')) {
return;
}
this.saving = true;
this.error = null;
this.successMessage = null;
try {
await apiClient.post('/admin/menu-config/user/reset');
myMenuConfigLog.info('Reset menu config to defaults');
// Reload the page to refresh sidebar
window.location.reload();
} catch (error) {
myMenuConfigLog.error('Failed to reset menu config:', error);
this.error = error.message || 'Failed to reset menu configuration';
this.saving = false;
}
}
};
}

View File

@@ -1,139 +0,0 @@
/**
* Platform Detail - Alpine.js Component
*
* Displays platform overview, stats, and quick actions.
*/
const platformDetailLog = window.LogConfig.createLogger('PLATFORM_DETAIL');
function platformDetail() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Page identification
currentPage: 'platform-detail',
// State
platform: null,
stats: null,
recentPages: [],
loading: true,
error: null,
platformCode: null,
// Lifecycle
async init() {
platformDetailLog.info('=== PLATFORM DETAIL PAGE INITIALIZING ===');
// Duplicate initialization guard
if (window._platformDetailInitialized) {
platformDetailLog.warn('Platform detail page already initialized, skipping...');
return;
}
window._platformDetailInitialized = true;
try {
// Extract platform code from URL
const path = window.location.pathname;
const match = path.match(/\/admin\/platforms\/([^\/]+)$/);
if (match) {
this.platformCode = match[1];
platformDetailLog.info('Viewing platform:', this.platformCode);
await Promise.all([
this.loadPlatform(),
this.loadRecentPages(),
]);
} else {
platformDetailLog.error('No platform code in URL');
this.error = 'Platform code not found in URL';
this.loading = false;
}
platformDetailLog.info('=== PLATFORM DETAIL PAGE INITIALIZATION COMPLETE ===');
} catch (error) {
window.LogConfig.logError(error, 'Platform Detail Init');
this.error = 'Failed to initialize page';
this.loading = false;
}
},
// API Methods
async loadPlatform() {
try {
const response = await apiClient.get(`/admin/platforms/${this.platformCode}`);
this.platform = response;
platformDetailLog.info(`Loaded platform: ${this.platformCode}`);
} catch (err) {
platformDetailLog.error('Error loading platform:', err);
this.error = err.message || 'Failed to load platform';
throw err;
} finally {
this.loading = false;
}
},
async loadRecentPages() {
try {
// Load recent content pages for this platform
const response = await apiClient.get(`/admin/content-pages?platform_code=${this.platformCode}&limit=5`);
this.recentPages = response.items || response || [];
platformDetailLog.info(`Loaded ${this.recentPages.length} recent pages`);
} catch (err) {
platformDetailLog.error('Error loading recent pages:', err);
// Non-fatal - don't throw
this.recentPages = [];
}
},
// Helper Methods
getPlatformIcon(code) {
const icons = {
main: 'home',
oms: 'clipboard-list',
loyalty: 'star',
sitebuilder: 'template',
};
return icons[code] || 'globe-alt';
},
getPageTypeLabel(page) {
if (page.is_platform_page) return 'Marketing';
if (page.vendor_id) return 'Vendor Override';
return 'Vendor Default';
},
getPageTypeBadgeClass(page) {
if (page.is_platform_page) {
return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200';
}
if (page.vendor_id) {
return 'bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-200';
}
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
},
formatDate(dateString) {
if (!dateString) return '—';
const date = new Date(dateString);
return date.toLocaleDateString('fr-LU', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
},
getPlatformUrl() {
if (!this.platform) return '#';
if (this.platform.domain) {
return `https://${this.platform.domain}`;
}
// Development URL
if (this.platform.code === 'main') {
return '/';
}
return `/platforms/${this.platform.code}/`;
},
};
}

View File

@@ -1,230 +0,0 @@
/**
* Platform Edit - Alpine.js Component
*
* Handles platform editing for multi-platform CMS.
*/
const platformEditLog = window.LogConfig.createLogger('PLATFORM_EDIT');
function platformEdit() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Page identification
currentPage: 'platform-edit',
// State
platform: null,
loading: true,
saving: false,
error: null,
success: null,
platformCode: null,
// Form data
formData: {
name: '',
description: '',
domain: '',
path_prefix: '',
logo: '',
logo_dark: '',
favicon: '',
default_language: 'fr',
supported_languages: ['fr', 'de', 'en'],
is_active: true,
is_public: true,
theme_config: {},
settings: {},
},
errors: {},
// Available languages
availableLanguages: [
{ code: 'fr', name: 'French' },
{ code: 'de', name: 'German' },
{ code: 'en', name: 'English' },
{ code: 'lu', name: 'Luxembourgish' },
],
// Lifecycle
async init() {
platformEditLog.info('=== PLATFORM EDIT PAGE INITIALIZING ===');
// Duplicate initialization guard
if (window._platformEditInitialized) {
platformEditLog.warn('Platform edit page already initialized, skipping...');
return;
}
window._platformEditInitialized = true;
try {
// Extract platform code from URL
const path = window.location.pathname;
const match = path.match(/\/admin\/platforms\/([^\/]+)\/edit/);
if (match) {
this.platformCode = match[1];
platformEditLog.info('Editing platform:', this.platformCode);
await this.loadPlatform();
} else {
platformEditLog.error('No platform code in URL');
this.error = 'Platform code not found in URL';
this.loading = false;
}
platformEditLog.info('=== PLATFORM EDIT PAGE INITIALIZATION COMPLETE ===');
} catch (error) {
window.LogConfig.logError(error, 'Platform Edit Init');
this.error = 'Failed to initialize page';
this.loading = false;
}
},
// API Methods
async loadPlatform() {
this.loading = true;
this.error = null;
try {
const response = await apiClient.get(`/admin/platforms/${this.platformCode}`);
this.platform = response;
// Populate form data
this.formData = {
name: response.name || '',
description: response.description || '',
domain: response.domain || '',
path_prefix: response.path_prefix || '',
logo: response.logo || '',
logo_dark: response.logo_dark || '',
favicon: response.favicon || '',
default_language: response.default_language || 'fr',
supported_languages: response.supported_languages || ['fr', 'de', 'en'],
is_active: response.is_active ?? true,
is_public: response.is_public ?? true,
theme_config: response.theme_config || {},
settings: response.settings || {},
};
platformEditLog.info(`Loaded platform: ${this.platformCode}`);
} catch (err) {
platformEditLog.error('Error loading platform:', err);
this.error = err.message || 'Failed to load platform';
} finally {
this.loading = false;
}
},
async handleSubmit() {
this.saving = true;
this.error = null;
this.success = null;
this.errors = {};
try {
// Build update payload (only changed fields)
const payload = {
name: this.formData.name,
description: this.formData.description || null,
domain: this.formData.domain || null,
path_prefix: this.formData.path_prefix || null,
logo: this.formData.logo || null,
logo_dark: this.formData.logo_dark || null,
favicon: this.formData.favicon || null,
default_language: this.formData.default_language,
supported_languages: this.formData.supported_languages,
is_active: this.formData.is_active,
is_public: this.formData.is_public,
};
const response = await apiClient.put(
`/admin/platforms/${this.platformCode}`,
payload
);
this.platform = response;
this.success = 'Platform updated successfully';
platformEditLog.info(`Updated platform: ${this.platformCode}`);
// Clear success message after 3 seconds
setTimeout(() => {
this.success = null;
}, 3000);
} catch (err) {
platformEditLog.error('Error updating platform:', err);
this.error = err.message || 'Failed to update platform';
// Handle validation errors
if (err.details) {
this.errors = err.details;
}
} finally {
this.saving = false;
}
},
async toggleActive() {
try {
this.formData.is_active = !this.formData.is_active;
await this.handleSubmit();
} catch (err) {
platformEditLog.error('Error toggling active status:', err);
// Revert on error
this.formData.is_active = !this.formData.is_active;
}
},
async togglePublic() {
try {
this.formData.is_public = !this.formData.is_public;
await this.handleSubmit();
} catch (err) {
platformEditLog.error('Error toggling public status:', err);
// Revert on error
this.formData.is_public = !this.formData.is_public;
}
},
// Helper Methods
isLanguageSupported(code) {
return this.formData.supported_languages.includes(code);
},
toggleLanguage(code) {
const index = this.formData.supported_languages.indexOf(code);
if (index > -1) {
// Don't allow removing the last language
if (this.formData.supported_languages.length > 1) {
this.formData.supported_languages.splice(index, 1);
}
} else {
this.formData.supported_languages.push(code);
}
},
getPlatformIcon(code) {
const icons = {
main: 'home',
oms: 'clipboard-list',
loyalty: 'star',
sitebuilder: 'template',
};
return icons[code] || 'globe-alt';
},
formatDate(dateString) {
if (!dateString) return '—';
const date = new Date(dateString);
return date.toLocaleDateString('fr-LU', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
},
};
}

View File

@@ -1,129 +0,0 @@
// noqa: js-006 - async init pattern is safe, loadData has try/catch
// static/admin/js/platform-health.js
/**
* Admin platform health monitoring page logic
* Displays system metrics, capacity thresholds, and scaling recommendations
*/
const adminPlatformHealthLog = window.LogConfig.loggers.adminPlatformHealth ||
window.LogConfig.createLogger('adminPlatformHealth', false);
adminPlatformHealthLog.info('Loading...');
function adminPlatformHealth() {
adminPlatformHealthLog.info('adminPlatformHealth() called');
return {
// Inherit base layout state
...data(),
// Set page identifier
currentPage: 'platform-health',
// Loading states
loading: true,
error: '',
// Health data
health: null,
// Auto-refresh interval (30 seconds)
refreshInterval: null,
async init() {
adminPlatformHealthLog.info('Platform Health init() called');
// Guard against multiple initialization
if (window._adminPlatformHealthInitialized) {
adminPlatformHealthLog.warn('Already initialized, skipping');
return;
}
window._adminPlatformHealthInitialized = true;
// Load initial data
await this.loadHealth();
// Set up auto-refresh every 30 seconds
this.refreshInterval = setInterval(() => {
this.loadHealth();
}, 30000);
adminPlatformHealthLog.info('Platform Health initialization complete');
},
/**
* Clean up on component destroy
*/
destroy() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
},
/**
* Load platform health data
*/
async loadHealth() {
this.loading = true;
this.error = '';
try {
const response = await apiClient.get('/admin/platform/health');
this.health = response;
adminPlatformHealthLog.info('Loaded health data:', {
status: response.overall_status,
tier: response.infrastructure_tier
});
} catch (error) {
adminPlatformHealthLog.error('Failed to load health:', error);
this.error = error.message || 'Failed to load platform health';
} finally {
this.loading = false;
}
},
/**
* Manual refresh
*/
async refresh() {
await this.loadHealth();
},
/**
* Format number with locale
*/
formatNumber(num) {
if (num === null || num === undefined) return '0';
if (typeof num === 'number' && num % 1 !== 0) {
return num.toFixed(2);
}
return new Intl.NumberFormat('en-US').format(num);
},
/**
* Format storage size
*/
formatStorage(gb) {
if (gb === null || gb === undefined) return '0 GB';
if (gb < 1) {
return (gb * 1024).toFixed(0) + ' MB';
}
return gb.toFixed(2) + ' GB';
},
/**
* Format timestamp
*/
formatTime(timestamp) {
if (!timestamp) return 'Unknown';
try {
const date = new Date(timestamp);
return date.toLocaleTimeString();
} catch (e) {
return 'Unknown';
}
}
};
}

View File

@@ -1,155 +0,0 @@
// noqa: js-006 - async init pattern is safe, loadData has try/catch
// static/admin/js/platform-homepage.js
// Use centralized logger
const platformHomepageLog = window.LogConfig.loggers.platformHomepage || window.LogConfig.createLogger('platformHomepage');
// ============================================
// PLATFORM HOMEPAGE MANAGER FUNCTION
// ============================================
function platformHomepageManager() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Page identifier for sidebar active state
currentPage: 'platform-homepage',
// Platform homepage specific state
page: null,
loading: false,
saving: false,
error: null,
successMessage: null,
// Initialize
async init() {
platformHomepageLog.info('=== PLATFORM HOMEPAGE MANAGER INITIALIZING ===');
// Prevent multiple initializations
if (window._platformHomepageInitialized) {
platformHomepageLog.warn('Platform homepage manager already initialized, skipping...');
return;
}
window._platformHomepageInitialized = true;
platformHomepageLog.group('Loading platform homepage');
await this.loadPlatformHomepage();
platformHomepageLog.groupEnd();
platformHomepageLog.info('=== PLATFORM HOMEPAGE MANAGER INITIALIZATION COMPLETE ===');
},
// Load platform homepage from API
async loadPlatformHomepage() {
this.loading = true;
this.error = null;
try {
platformHomepageLog.info('Fetching platform homepage...');
// Fetch all platform pages
const response = await apiClient.get('/admin/content-pages/platform?include_unpublished=true');
platformHomepageLog.debug('API Response:', response);
if (!response) {
throw new Error('Invalid API response');
}
// Handle response - API returns array directly
const pages = Array.isArray(response) ? response : (response.data || response.items || []);
// Find the homepage page (slug='home')
const homepage = pages.find(page => page.slug === 'home');
if (!homepage) {
platformHomepageLog.warn('Platform homepage not found, creating default...');
// Initialize with default values
this.page = {
id: null,
slug: 'home',
title: 'Welcome to Our Multi-Vendor Marketplace',
content: '<p>Connect vendors with customers worldwide. Build your online store and reach millions of shoppers.</p>',
template: 'default',
content_format: 'html',
meta_description: 'Leading multi-vendor marketplace platform. Connect with thousands of vendors and discover millions of products.',
meta_keywords: 'marketplace, multi-vendor, e-commerce, online shopping',
is_published: false,
show_in_header: false,
show_in_footer: false,
display_order: 0
};
} else {
this.page = { ...homepage };
platformHomepageLog.info('Platform homepage loaded:', this.page);
}
} catch (err) {
platformHomepageLog.error('Error loading platform homepage:', err);
this.error = err.message || 'Failed to load platform homepage';
} finally {
this.loading = false;
}
},
// Save platform homepage
async savePage() {
if (this.saving) return;
this.saving = true;
this.error = null;
this.successMessage = null;
try {
platformHomepageLog.info('Saving platform homepage...');
const payload = {
slug: 'platform_homepage',
title: this.page.title,
content: this.page.content,
content_format: this.page.content_format || 'html',
template: this.page.template,
meta_description: this.page.meta_description,
meta_keywords: this.page.meta_keywords,
is_published: this.page.is_published,
show_in_header: false, // Homepage never in header
show_in_footer: false, // Homepage never in footer
display_order: 0,
vendor_id: null // Platform default
};
platformHomepageLog.debug('Payload:', payload);
let response;
if (this.page.id) {
// Update existing page
response = await apiClient.put(`/admin/content-pages/${this.page.id}`, payload);
platformHomepageLog.info('Platform homepage updated');
} else {
// Create new page
response = await apiClient.post('/admin/content-pages/platform', payload);
platformHomepageLog.info('Platform homepage created');
}
if (response) {
// Handle response - API returns object directly
const pageData = response.data || response;
this.page = { ...pageData };
this.successMessage = 'Platform homepage saved successfully!';
// Clear success message after 3 seconds
setTimeout(() => {
this.successMessage = null;
}, 3000);
}
} catch (err) {
platformHomepageLog.error('Error saving platform homepage:', err);
this.error = err.message || 'Failed to save platform homepage';
} finally {
this.saving = false;
}
}
};
}

View File

@@ -1,212 +0,0 @@
// static/admin/js/platform-menu-config.js
// Platform menu configuration management
//
// TODO: BUG - Sidebar menu doesn't update immediately after changes.
// See my-menu-config.js for details and possible solutions.
const menuConfigLog = window.LogConfig?.loggers?.menuConfig || window.LogConfig?.createLogger?.('menuConfig') || console;
function adminPlatformMenuConfig(platformCode) {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Page-specific state
currentPage: 'platforms',
platformCode: platformCode,
loading: true,
error: null,
successMessage: null,
saving: false,
// Data
platform: null,
menuConfig: null,
frontendType: 'admin',
// Computed grouped items
get groupedItems() {
if (!this.menuConfig?.items) return [];
// Group items by section
const sections = {};
for (const item of this.menuConfig.items) {
const sectionId = item.section_id;
if (!sections[sectionId]) {
sections[sectionId] = {
id: sectionId,
label: item.section_label,
isSuperAdminOnly: item.is_super_admin_only,
items: [],
visibleCount: 0
};
}
sections[sectionId].items.push(item);
if (item.is_visible) {
sections[sectionId].visibleCount++;
}
}
// Convert to array and maintain order
return Object.values(sections);
},
async init() {
// Guard against duplicate initialization
if (window._platformMenuConfigInitialized) {
menuConfigLog.warn('Already initialized, skipping');
return;
}
window._platformMenuConfigInitialized = true;
menuConfigLog.info('=== PLATFORM MENU CONFIG PAGE INITIALIZING ===');
menuConfigLog.info('Platform code:', this.platformCode);
try {
await this.loadPlatform();
await this.loadPlatformMenuConfig();
menuConfigLog.info('=== PLATFORM MENU CONFIG PAGE INITIALIZED ===');
} catch (error) {
menuConfigLog.error('Failed to initialize menu config page:', error);
this.error = 'Failed to load page data. Please refresh.';
}
},
async refresh() {
this.error = null;
this.successMessage = null;
await this.loadPlatformMenuConfig();
},
async loadPlatform() {
try {
this.platform = await apiClient.get(`/admin/platforms/${this.platformCode}`);
menuConfigLog.info('Loaded platform:', this.platform?.name);
} catch (error) {
menuConfigLog.error('Failed to load platform:', error);
throw error;
}
},
async loadPlatformMenuConfig() {
this.loading = true;
this.error = null;
try {
const platformId = this.platform?.id;
if (!platformId) {
throw new Error('Platform not loaded');
}
const params = new URLSearchParams({ frontend_type: this.frontendType });
this.menuConfig = await apiClient.get(`/admin/menu-config/platforms/${platformId}?${params}`);
menuConfigLog.info('Loaded menu config:', {
frontendType: this.frontendType,
totalItems: this.menuConfig?.total_items,
visibleItems: this.menuConfig?.visible_items
});
} catch (error) {
menuConfigLog.error('Failed to load menu config:', error);
this.error = error.message || 'Failed to load menu configuration';
} finally {
this.loading = false;
}
},
async toggleVisibility(item) {
if (item.is_mandatory) {
menuConfigLog.warn('Cannot toggle mandatory item:', item.id);
return;
}
this.saving = true;
this.error = null;
this.successMessage = null;
const newVisibility = !item.is_visible;
try {
const platformId = this.platform?.id;
const params = new URLSearchParams({ frontend_type: this.frontendType });
await apiClient.put(`/admin/menu-config/platforms/${platformId}?${params}`, {
menu_item_id: item.id,
is_visible: newVisibility
});
// Update local state
item.is_visible = newVisibility;
// Update counts
if (newVisibility) {
this.menuConfig.visible_items++;
this.menuConfig.hidden_items--;
} else {
this.menuConfig.visible_items--;
this.menuConfig.hidden_items++;
}
menuConfigLog.info('Toggled visibility:', item.id, newVisibility);
// Reload the page to refresh sidebar
window.location.reload();
} catch (error) {
menuConfigLog.error('Failed to toggle visibility:', error);
this.error = error.message || 'Failed to update menu visibility';
this.saving = false;
}
},
async showAll() {
if (!confirm('This will show all menu items. Continue?')) {
return;
}
this.saving = true;
this.error = null;
this.successMessage = null;
try {
const platformId = this.platform?.id;
const params = new URLSearchParams({ frontend_type: this.frontendType });
await apiClient.post(`/admin/menu-config/platforms/${platformId}/show-all?${params}`);
menuConfigLog.info('Showed all menu items');
// Reload the page to refresh sidebar
window.location.reload();
} catch (error) {
menuConfigLog.error('Failed to show all menu items:', error);
this.error = error.message || 'Failed to show all menu items';
this.saving = false;
}
},
async resetToDefaults() {
if (!confirm('This will hide all menu items (except mandatory ones). You can then enable the ones you want. Continue?')) {
return;
}
this.saving = true;
this.error = null;
this.successMessage = null;
try {
const platformId = this.platform?.id;
const params = new URLSearchParams({ frontend_type: this.frontendType });
await apiClient.post(`/admin/menu-config/platforms/${platformId}/reset?${params}`);
menuConfigLog.info('Reset menu config to defaults');
// Reload the page to refresh sidebar
window.location.reload();
} catch (error) {
menuConfigLog.error('Failed to reset menu config:', error);
this.error = error.message || 'Failed to reset menu configuration';
this.saving = false;
}
}
};
}

View File

@@ -1,237 +0,0 @@
// static/admin/js/platform-modules.js
// Platform module configuration management
const moduleConfigLog = window.LogConfig?.loggers?.moduleConfig || window.LogConfig?.createLogger?.('moduleConfig') || console;
function adminPlatformModules(platformCode) {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Page-specific state
currentPage: 'platforms',
platformCode: platformCode,
loading: true,
error: null,
successMessage: null,
saving: false,
// Data
platform: null,
moduleConfig: null,
// Computed properties
get coreModules() {
if (!this.moduleConfig?.modules) return [];
return this.moduleConfig.modules.filter(m => m.is_core);
},
get optionalModules() {
if (!this.moduleConfig?.modules) return [];
return this.moduleConfig.modules.filter(m => !m.is_core);
},
get coreModulesCount() {
return this.coreModules.length;
},
get enabledOptionalCount() {
return this.optionalModules.filter(m => m.is_enabled).length;
},
// Module icons mapping (must match icons.js definitions)
getModuleIcon(moduleCode) {
const icons = {
'core': 'home',
'platform-admin': 'office-building',
'billing': 'credit-card',
'inventory': 'archive',
'orders': 'shopping-cart',
'marketplace': 'shopping-bag',
'customers': 'users',
'cms': 'document-text',
'analytics': 'chart-bar',
'messaging': 'chat',
'dev-tools': 'code',
'monitoring': 'chart-pie'
};
return icons[moduleCode] || 'puzzle-piece';
},
// Modules with configuration options
hasConfig(moduleCode) {
return ['billing', 'inventory', 'orders', 'marketplace',
'customers', 'cms', 'analytics', 'messaging', 'monitoring'].includes(moduleCode);
},
async init() {
// Guard against duplicate initialization
if (window._platformModulesInitialized) {
moduleConfigLog.warn('Already initialized, skipping');
return;
}
window._platformModulesInitialized = true;
moduleConfigLog.info('=== PLATFORM MODULES PAGE INITIALIZING ===');
moduleConfigLog.info('Platform code:', this.platformCode);
try {
await this.loadPlatform();
await this.loadModuleConfig();
moduleConfigLog.info('=== PLATFORM MODULES PAGE INITIALIZED ===');
} catch (error) {
moduleConfigLog.error('Failed to initialize modules page:', error);
this.error = 'Failed to load page data. Please refresh.';
}
},
async refresh() {
this.error = null;
this.successMessage = null;
await this.loadModuleConfig();
},
async loadPlatform() {
try {
this.platform = await apiClient.get(`/admin/platforms/${this.platformCode}`);
moduleConfigLog.info('Loaded platform:', this.platform?.name);
} catch (error) {
moduleConfigLog.error('Failed to load platform:', error);
throw error;
}
},
async loadModuleConfig() {
this.loading = true;
this.error = null;
try {
const platformId = this.platform?.id;
if (!platformId) {
throw new Error('Platform not loaded');
}
this.moduleConfig = await apiClient.get(`/admin/modules/platforms/${platformId}`);
moduleConfigLog.info('Loaded module config:', {
total: this.moduleConfig?.total,
enabled: this.moduleConfig?.enabled,
disabled: this.moduleConfig?.disabled
});
} catch (error) {
moduleConfigLog.error('Failed to load module config:', error);
this.error = error.message || 'Failed to load module configuration';
} finally {
this.loading = false;
}
},
async toggleModule(module) {
if (module.is_core) {
moduleConfigLog.warn('Cannot toggle core module:', module.code);
return;
}
this.saving = true;
this.error = null;
this.successMessage = null;
const action = module.is_enabled ? 'disable' : 'enable';
try {
const platformId = this.platform?.id;
const endpoint = `/admin/modules/platforms/${platformId}/${action}`;
const result = await apiClient.post(endpoint, {
module_code: module.code
});
moduleConfigLog.info(`${action}d module:`, module.code, result);
// Show success message
if (result.also_enabled?.length > 0) {
this.successMessage = `Module '${module.name}' enabled. Also enabled dependencies: ${result.also_enabled.join(', ')}`;
} else if (result.also_disabled?.length > 0) {
this.successMessage = `Module '${module.name}' disabled. Also disabled dependents: ${result.also_disabled.join(', ')}`;
} else {
this.successMessage = `Module '${module.name}' ${action}d successfully`;
}
// Reload module config to get updated state
await this.loadModuleConfig();
// Clear success message after delay
setTimeout(() => {
this.successMessage = null;
}, 5000);
} catch (error) {
moduleConfigLog.error(`Failed to ${action} module:`, error);
this.error = error.message || `Failed to ${action} module`;
} finally {
this.saving = false;
}
},
async enableAll() {
if (!confirm('This will enable all modules. Continue?')) {
return;
}
this.saving = true;
this.error = null;
this.successMessage = null;
try {
const platformId = this.platform?.id;
const result = await apiClient.post(`/admin/modules/platforms/${platformId}/enable-all`);
moduleConfigLog.info('Enabled all modules:', result);
this.successMessage = `All ${result.enabled_count} modules enabled`;
// Reload module config
await this.loadModuleConfig();
setTimeout(() => {
this.successMessage = null;
}, 5000);
} catch (error) {
moduleConfigLog.error('Failed to enable all modules:', error);
this.error = error.message || 'Failed to enable all modules';
} finally {
this.saving = false;
}
},
async disableOptional() {
if (!confirm('This will disable all optional modules, keeping only core modules. Continue?')) {
return;
}
this.saving = true;
this.error = null;
this.successMessage = null;
try {
const platformId = this.platform?.id;
const result = await apiClient.post(`/admin/modules/platforms/${platformId}/disable-optional`);
moduleConfigLog.info('Disabled optional modules:', result);
this.successMessage = `Optional modules disabled. Core modules kept: ${result.core_modules.join(', ')}`;
// Reload module config
await this.loadModuleConfig();
setTimeout(() => {
this.successMessage = null;
}, 5000);
} catch (error) {
moduleConfigLog.error('Failed to disable optional modules:', error);
this.error = error.message || 'Failed to disable optional modules';
} finally {
this.saving = false;
}
}
};
}

View File

@@ -1,80 +0,0 @@
/**
* Platforms Manager - Alpine.js Component
*
* Handles platform listing and management for multi-platform CMS.
*/
const platformsLog = window.LogConfig.createLogger('PLATFORMS');
function platformsManager() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Page identification
currentPage: "platforms",
// State
platforms: [],
loading: true,
error: null,
// Lifecycle
async init() {
platformsLog.info('=== PLATFORMS PAGE INITIALIZING ===');
// Duplicate initialization guard
if (window._adminPlatformsInitialized) {
platformsLog.warn('Platforms page already initialized, skipping...');
return;
}
window._adminPlatformsInitialized = true;
try {
await this.loadPlatforms();
platformsLog.info('=== PLATFORMS PAGE INITIALIZATION COMPLETE ===');
} catch (error) {
window.LogConfig.logError(error, 'Platforms Init');
this.error = 'Failed to initialize page';
}
},
// API Methods
async loadPlatforms() {
this.loading = true;
this.error = null;
try {
const response = await apiClient.get("/admin/platforms");
this.platforms = response.platforms || [];
platformsLog.info(`Loaded ${this.platforms.length} platforms`);
} catch (err) {
platformsLog.error("Error loading platforms:", err);
this.error = err.message || "Failed to load platforms";
} finally {
this.loading = false;
}
},
// Helper Methods
getPlatformIcon(code) {
const icons = {
oms: "clipboard-list",
loyalty: "star",
sitebuilder: "template",
default: "globe-alt",
};
return icons[code] || icons.default;
},
formatDate(dateString) {
if (!dateString) return "—";
const date = new Date(dateString);
return date.toLocaleDateString("fr-LU", {
year: "numeric",
month: "short",
day: "numeric",
});
},
};
}

View File

@@ -1,158 +0,0 @@
// static/admin/js/select-platform.js
// Platform selection page for platform admins
const platformLog = window.LogConfig ? window.LogConfig.createLogger('PLATFORM_SELECT') : console;
function selectPlatform() {
return {
dark: false,
loading: true,
selecting: false,
error: null,
platforms: [],
isSuperAdmin: false,
async init() {
platformLog.info('=== PLATFORM SELECTION PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._platformSelectInitialized) {
platformLog.warn('Platform selection page already initialized, skipping...');
return;
}
window._platformSelectInitialized = true;
// Set theme
this.dark = localStorage.getItem('theme') === 'dark';
// Check if user is logged in
const token = localStorage.getItem('admin_token');
if (!token) {
platformLog.warn('No token found, redirecting to login');
window.location.href = '/admin/login';
return;
}
// Load accessible platforms
await this.loadPlatforms();
},
async loadPlatforms() {
this.loading = true;
this.error = null;
try {
platformLog.info('Fetching accessible platforms...');
const response = await apiClient.get('/admin/auth/accessible-platforms');
platformLog.debug('Platforms response:', response);
this.isSuperAdmin = response.is_super_admin;
this.platforms = response.platforms || [];
if (this.isSuperAdmin) {
platformLog.info('User is super admin, redirecting to dashboard...');
setTimeout(() => {
window.location.href = '/admin/dashboard';
}, 1500);
return;
}
if (!response.requires_platform_selection && this.platforms.length === 1) {
// Only one platform assigned, auto-select it
platformLog.info('Single platform assigned, auto-selecting...');
await this.selectPlatform(this.platforms[0]);
return;
}
platformLog.info(`Loaded ${this.platforms.length} platforms`);
} catch (error) {
platformLog.error('Failed to load platforms:', error);
if (error.message && error.message.includes('401')) {
// Token expired or invalid
window.location.href = '/admin/login';
return;
}
this.error = error.message || 'Failed to load platforms. Please try again.';
} finally {
this.loading = false;
}
},
async selectPlatform(platform) {
if (this.selecting) return;
this.selecting = true;
this.error = null;
platformLog.info(`Selecting platform: ${platform.code}`);
try {
const response = await apiClient.post(
`/admin/auth/select-platform?platform_id=${platform.id}`
);
platformLog.debug('Platform selection response:', response);
if (response.access_token) {
// Store new token with platform context
localStorage.setItem('admin_token', response.access_token);
localStorage.setItem('token', response.access_token);
// Store selected platform info
localStorage.setItem('admin_platform', JSON.stringify({
id: platform.id,
code: platform.code,
name: platform.name
}));
// Update user data if provided
if (response.user) {
localStorage.setItem('admin_user', JSON.stringify(response.user));
}
platformLog.info('Platform selected successfully, redirecting to dashboard...');
// Redirect to dashboard or last visited page
const lastPage = localStorage.getItem('admin_last_visited_page');
const redirectTo = (lastPage && lastPage.startsWith('/admin/') && !lastPage.includes('/login') && !lastPage.includes('/select-platform'))
? lastPage
: '/admin/dashboard';
window.location.href = redirectTo;
} else {
throw new Error('No token received from server');
}
} catch (error) {
platformLog.error('Platform selection failed:', error);
this.error = error.message || 'Failed to select platform. Please try again.';
this.selecting = false;
}
},
async logout() {
platformLog.info('Logging out...');
try {
await apiClient.post('/admin/auth/logout');
} catch (error) {
platformLog.error('Logout API error:', error);
} finally {
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_user');
localStorage.removeItem('admin_platform');
localStorage.removeItem('token');
window.location.href = '/admin/login';
}
},
toggleDarkMode() {
this.dark = !this.dark;
localStorage.setItem('theme', this.dark ? 'dark' : 'light');
}
};
}
platformLog.info('Platform selection module loaded');

View File

@@ -1,509 +0,0 @@
// static/admin/js/settings.js
// noqa: JS-003 - Uses ...baseData which is data() with safety check
const settingsLog = window.LogConfig?.loggers?.settings || console;
function adminSettings() {
// Get base data with safety check for standalone usage
const baseData = typeof data === 'function' ? data() : {};
return {
// Inherit base layout functionality from init-alpine.js
...baseData,
// Settings-specific state
currentPage: 'settings',
loading: true,
saving: false,
error: null,
successMessage: null,
activeTab: 'display',
displaySettings: {
rows_per_page: 20
},
logSettings: {
log_level: 'INFO',
log_file_max_size_mb: 10,
log_file_backup_count: 5,
db_log_retention_days: 30,
file_logging_enabled: true,
db_logging_enabled: true
},
notificationSettings: {
email_enabled: true,
in_app_enabled: true,
critical_only: false
},
shippingSettings: {
carrier_greco_label_url: 'https://dispatchweb.fr/Tracky/Home/',
carrier_colissimo_label_url: '',
carrier_xpresslogistics_label_url: ''
},
emailSettings: {
provider: 'smtp',
from_email: '',
from_name: '',
reply_to: '',
smtp_host: '',
smtp_port: 587,
smtp_user: '',
mailgun_domain: '',
aws_region: '',
debug: false,
enabled: true,
is_configured: false,
has_db_overrides: false
},
// Email editing form (separate from display to track changes)
emailForm: {
provider: 'smtp',
from_email: '',
from_name: '',
reply_to: '',
smtp_host: '',
smtp_port: 587,
smtp_user: '',
smtp_password: '',
smtp_use_tls: true,
smtp_use_ssl: false,
sendgrid_api_key: '',
mailgun_api_key: '',
mailgun_domain: '',
aws_access_key_id: '',
aws_secret_access_key: '',
aws_region: 'eu-west-1',
enabled: true,
debug: false
},
emailEditMode: false,
testEmailAddress: '',
sendingTestEmail: false,
testEmailError: null,
testEmailSuccess: null,
async init() {
// Guard against multiple initialization
if (window._adminSettingsInitialized) return;
window._adminSettingsInitialized = true;
try {
settingsLog.info('=== SETTINGS PAGE INITIALIZING ===');
await Promise.all([
this.loadDisplaySettings(),
this.loadLogSettings(),
this.loadShippingSettings(),
this.loadEmailSettings()
]);
} catch (error) {
settingsLog.error('Init failed:', error);
this.error = 'Failed to initialize settings page';
}
},
async refresh() {
this.error = null;
this.successMessage = null;
await Promise.all([
this.loadDisplaySettings(),
this.loadLogSettings(),
this.loadShippingSettings(),
this.loadEmailSettings()
]);
},
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;
try {
const data = await apiClient.get('/admin/logs/settings');
this.logSettings = data;
settingsLog.info('Log settings loaded:', this.logSettings);
} catch (error) {
settingsLog.error('Failed to load log settings:', error);
this.error = error.response?.data?.detail || 'Failed to load log settings';
} finally {
this.loading = false;
}
},
async saveLogSettings() {
this.saving = true;
this.error = null;
this.successMessage = null;
try {
const data = await apiClient.put('/admin/logs/settings', this.logSettings);
this.successMessage = data.message || 'Log settings saved successfully';
// Auto-hide success message after 5 seconds
setTimeout(() => {
this.successMessage = null;
}, 5000);
settingsLog.info('Log settings saved successfully');
} catch (error) {
settingsLog.error('Failed to save log settings:', error);
this.error = error.response?.data?.detail || 'Failed to save log settings';
} finally {
this.saving = false;
}
},
async cleanupOldLogs() {
if (!confirm(`This will delete all logs older than ${this.logSettings.db_log_retention_days} days. Continue?`)) {
return;
}
this.error = null;
this.successMessage = null;
try {
const data = await apiClient.delete(
`/admin/logs/database/cleanup?retention_days=${this.logSettings.db_log_retention_days}&confirm=true`
);
this.successMessage = data.message || 'Old logs cleaned up successfully';
// Auto-hide success message after 5 seconds
setTimeout(() => {
this.successMessage = null;
}, 5000);
settingsLog.info('Old logs cleaned up successfully');
} catch (error) {
settingsLog.error('Failed to cleanup logs:', error);
this.error = error.response?.data?.detail || 'Failed to cleanup old logs';
}
},
async saveNotificationSettings() {
this.saving = true;
this.error = null;
this.successMessage = null;
try {
// TODO: Implement API endpoint for notification settings
// const data = await apiClient.put('/admin/notifications/settings', this.notificationSettings);
// For now, just show success (settings are client-side only)
this.successMessage = 'Notification settings saved successfully';
// Auto-hide success message after 5 seconds
setTimeout(() => {
this.successMessage = null;
}, 5000);
settingsLog.info('Notification settings saved:', this.notificationSettings);
} catch (error) {
settingsLog.error('Failed to save notification settings:', error);
this.error = error.response?.data?.detail || 'Failed to save notification settings';
} finally {
this.saving = false;
}
},
async loadShippingSettings() {
try {
// Load each carrier setting with defaults to avoid 404 errors
const carriers = [
{ name: 'greco', default: 'https://dispatchweb.fr/Tracky/Home/' },
{ name: 'colissimo', default: '' },
{ name: 'xpresslogistics', default: '' }
];
for (const carrier of carriers) {
const key = `carrier_${carrier.name}_label_url`;
// Use default query param to avoid 404 for non-existent settings
const data = await apiClient.get(`/admin/settings/${key}?default=${encodeURIComponent(carrier.default)}`);
if (data && data.value !== undefined) {
this.shippingSettings[key] = data.value;
}
}
settingsLog.info('Shipping settings loaded:', this.shippingSettings);
} catch (error) {
settingsLog.error('Failed to load shipping settings:', error);
// On error, keep existing defaults
}
},
async saveShippingSettings() {
this.saving = true;
this.error = null;
this.successMessage = null;
try {
// Save each carrier setting using upsert
const carriers = [
{ key: 'carrier_greco_label_url', name: 'Greco' },
{ key: 'carrier_colissimo_label_url', name: 'Colissimo' },
{ key: 'carrier_xpresslogistics_label_url', name: 'XpressLogistics' }
];
for (const carrier of carriers) {
await apiClient.post('/admin/settings/upsert', {
key: carrier.key,
value: this.shippingSettings[carrier.key] || '',
category: 'shipping',
value_type: 'string',
description: `Label URL prefix for ${carrier.name} carrier`
});
}
this.successMessage = 'Shipping settings saved successfully';
// Auto-hide success message after 5 seconds
setTimeout(() => {
this.successMessage = null;
}, 5000);
settingsLog.info('Shipping settings saved:', this.shippingSettings);
} catch (error) {
settingsLog.error('Failed to save shipping settings:', error);
this.error = error.response?.data?.detail || 'Failed to save shipping settings';
} finally {
this.saving = false;
}
},
getShippingLabelUrl(carrier, shipmentNumber) {
// Helper to generate full label URL
const prefix = this.shippingSettings[`carrier_${carrier}_label_url`] || '';
if (!prefix || !shipmentNumber) return null;
return prefix + shipmentNumber;
},
// =====================================================================
// EMAIL SETTINGS
// =====================================================================
async loadEmailSettings() {
try {
const data = await apiClient.get('/admin/settings/email/status');
this.emailSettings = {
provider: data.provider || 'smtp',
from_email: data.from_email || '',
from_name: data.from_name || '',
reply_to: data.reply_to || '',
smtp_host: data.smtp_host || '',
smtp_port: data.smtp_port || 587,
smtp_user: data.smtp_user || '',
mailgun_domain: data.mailgun_domain || '',
aws_region: data.aws_region || '',
debug: data.debug || false,
enabled: data.enabled !== false,
is_configured: data.is_configured || false,
has_db_overrides: data.has_db_overrides || false
};
// Populate edit form with current values
this.populateEmailForm();
settingsLog.info('Email settings loaded:', this.emailSettings);
} catch (error) {
settingsLog.error('Failed to load email settings:', error);
// Use defaults on error
}
},
populateEmailForm() {
// Copy current settings to form (passwords are not loaded from API)
this.emailForm = {
provider: this.emailSettings.provider,
from_email: this.emailSettings.from_email,
from_name: this.emailSettings.from_name,
reply_to: this.emailSettings.reply_to || '',
smtp_host: this.emailSettings.smtp_host || '',
smtp_port: this.emailSettings.smtp_port || 587,
smtp_user: this.emailSettings.smtp_user || '',
smtp_password: '', // Never populated from API
smtp_use_tls: true,
smtp_use_ssl: false,
sendgrid_api_key: '',
mailgun_api_key: '',
mailgun_domain: this.emailSettings.mailgun_domain || '',
aws_access_key_id: '',
aws_secret_access_key: '',
aws_region: this.emailSettings.aws_region || 'eu-west-1',
enabled: this.emailSettings.enabled,
debug: this.emailSettings.debug
};
},
enableEmailEditing() {
this.emailEditMode = true;
this.populateEmailForm();
},
cancelEmailEditing() {
this.emailEditMode = false;
this.populateEmailForm();
},
async saveEmailSettings() {
this.saving = true;
this.error = null;
this.successMessage = null;
try {
// Only send non-empty values to update
const payload = {};
// Always send these core fields
if (this.emailForm.provider) payload.provider = this.emailForm.provider;
if (this.emailForm.from_email) payload.from_email = this.emailForm.from_email;
if (this.emailForm.from_name) payload.from_name = this.emailForm.from_name;
if (this.emailForm.reply_to) payload.reply_to = this.emailForm.reply_to;
payload.enabled = this.emailForm.enabled;
payload.debug = this.emailForm.debug;
// Provider-specific fields
if (this.emailForm.provider === 'smtp') {
if (this.emailForm.smtp_host) payload.smtp_host = this.emailForm.smtp_host;
if (this.emailForm.smtp_port) payload.smtp_port = this.emailForm.smtp_port;
if (this.emailForm.smtp_user) payload.smtp_user = this.emailForm.smtp_user;
if (this.emailForm.smtp_password) payload.smtp_password = this.emailForm.smtp_password;
payload.smtp_use_tls = this.emailForm.smtp_use_tls;
payload.smtp_use_ssl = this.emailForm.smtp_use_ssl;
} else if (this.emailForm.provider === 'sendgrid') {
if (this.emailForm.sendgrid_api_key) payload.sendgrid_api_key = this.emailForm.sendgrid_api_key;
} else if (this.emailForm.provider === 'mailgun') {
if (this.emailForm.mailgun_api_key) payload.mailgun_api_key = this.emailForm.mailgun_api_key;
if (this.emailForm.mailgun_domain) payload.mailgun_domain = this.emailForm.mailgun_domain;
} else if (this.emailForm.provider === 'ses') {
if (this.emailForm.aws_access_key_id) payload.aws_access_key_id = this.emailForm.aws_access_key_id;
if (this.emailForm.aws_secret_access_key) payload.aws_secret_access_key = this.emailForm.aws_secret_access_key;
if (this.emailForm.aws_region) payload.aws_region = this.emailForm.aws_region;
}
const data = await apiClient.put('/admin/settings/email/settings', payload);
this.successMessage = data.message || 'Email settings saved successfully';
this.emailEditMode = false;
// Reload to get updated status
await this.loadEmailSettings();
setTimeout(() => {
this.successMessage = null;
}, 5000);
settingsLog.info('Email settings saved successfully');
} catch (error) {
settingsLog.error('Failed to save email settings:', error);
this.error = error.message || 'Failed to save email settings';
} finally {
this.saving = false;
}
},
async resetEmailSettings() {
if (!confirm('This will reset all email settings to use .env defaults. Continue?')) {
return;
}
this.saving = true;
this.error = null;
this.successMessage = null;
try {
const data = await apiClient.delete('/admin/settings/email/settings');
this.successMessage = data.message || 'Email settings reset to defaults';
this.emailEditMode = false;
// Reload to get .env values
await this.loadEmailSettings();
setTimeout(() => {
this.successMessage = null;
}, 5000);
settingsLog.info('Email settings reset successfully');
} catch (error) {
settingsLog.error('Failed to reset email settings:', error);
this.error = error.message || 'Failed to reset email settings';
} finally {
this.saving = false;
}
},
async sendTestEmail() {
if (!this.testEmailAddress) {
this.testEmailError = 'Please enter a test email address';
return;
}
this.sendingTestEmail = true;
this.testEmailError = null;
this.testEmailSuccess = null;
try {
settingsLog.info('Sending test email to:', this.testEmailAddress);
const data = await apiClient.post('/admin/settings/email/test', {
to_email: this.testEmailAddress
});
settingsLog.info('Test email response:', data);
if (data.success) {
this.testEmailSuccess = `Test email sent to ${this.testEmailAddress}`;
setTimeout(() => {
this.testEmailSuccess = null;
}, 5000);
} else {
settingsLog.error('Test email failed:', data.message);
// Extract the first line of error for cleaner display
let errorMsg = data.message || 'Failed to send test email';
if (errorMsg.includes('\n')) {
errorMsg = errorMsg.split('\n')[0];
}
this.testEmailError = errorMsg;
}
} catch (error) {
settingsLog.error('Failed to send test email (exception):', error);
this.testEmailError = error.message || 'Failed to send test email';
} finally {
this.sendingTestEmail = false;
settingsLog.info('sendingTestEmail set to false, testEmailError:', this.testEmailError);
}
}
};
}
settingsLog.info('Settings module loaded');

View File

@@ -1,157 +0,0 @@
// static/admin/js/user-create.js
// Create custom logger for admin user create
const userCreateLog = window.LogConfig.createLogger('ADMIN-USER-CREATE');
function adminUserCreate() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Admin user create page specific state
currentPage: 'admin-users',
loading: false,
formData: {
username: '',
email: '',
password: '',
first_name: '',
last_name: '',
is_super_admin: false,
platform_ids: []
},
platforms: [],
errors: {},
saving: false,
// Initialize
async init() {
userCreateLog.info('=== ADMIN USER CREATE PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._userCreateInitialized) {
userCreateLog.warn('Admin user create page already initialized, skipping...');
return;
}
window._userCreateInitialized = true;
// Load platforms for admin assignment
await this.loadPlatforms();
userCreateLog.info('=== ADMIN USER CREATE PAGE INITIALIZATION COMPLETE ===');
},
// Load available platforms
async loadPlatforms() {
try {
userCreateLog.debug('Loading platforms...');
const response = await apiClient.get('/admin/platforms');
this.platforms = response.platforms || response.items || [];
userCreateLog.debug(`Loaded ${this.platforms.length} platforms`);
} catch (error) {
userCreateLog.error('Failed to load platforms:', error);
this.platforms = [];
}
},
// Validate form
validateForm() {
this.errors = {};
if (!this.formData.username.trim()) {
this.errors.username = 'Username is required';
}
if (!this.formData.email.trim()) {
this.errors.email = 'Email is required';
}
if (!this.formData.password || this.formData.password.length < 6) {
this.errors.password = 'Password must be at least 6 characters';
}
// Platform admin validation: must have at least one platform
if (!this.formData.is_super_admin) {
if (!this.formData.platform_ids || this.formData.platform_ids.length === 0) {
this.errors.platform_ids = 'Platform admins must be assigned to at least one platform';
}
}
return Object.keys(this.errors).length === 0;
},
// Submit form
async handleSubmit() {
userCreateLog.info('=== CREATING ADMIN USER ===');
userCreateLog.debug('Form data:', { ...this.formData, password: '[REDACTED]' });
if (!this.validateForm()) {
userCreateLog.warn('Validation failed:', this.errors);
Utils.showToast('Please fix the errors before submitting', 'error');
return;
}
this.saving = true;
try {
// Use admin-users endpoint for creating admin users
const url = '/admin/admin-users';
const payload = {
email: this.formData.email,
username: this.formData.username,
password: this.formData.password,
first_name: this.formData.first_name || null,
last_name: this.formData.last_name || null,
is_super_admin: this.formData.is_super_admin,
platform_ids: this.formData.is_super_admin ? [] : this.formData.platform_ids.map(id => parseInt(id))
};
window.LogConfig.logApiCall('POST', url, { ...payload, password: '[REDACTED]' }, 'request');
const startTime = performance.now();
const response = await apiClient.post(url, payload);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('POST', url, response, 'response');
window.LogConfig.logPerformance('Create Admin User', duration);
const userType = this.formData.is_super_admin ? 'Super admin' : 'Platform admin';
Utils.showToast(`${userType} created successfully`, 'success');
userCreateLog.info(`${userType} created successfully in ${duration}ms`, response);
// Redirect to the admin users list
setTimeout(() => {
window.location.href = `/admin/admin-users/${response.id}`;
}, 1500);
} catch (error) {
window.LogConfig.logError(error, 'Create Admin User');
// Handle validation errors
if (error.details && error.details.validation_errors) {
error.details.validation_errors.forEach(err => {
const field = err.loc?.[1] || err.loc?.[0];
if (field) {
this.errors[field] = err.msg;
}
});
userCreateLog.debug('Validation errors:', this.errors);
}
// Handle specific errors
if (error.message) {
if (error.message.includes('Email already')) {
this.errors.email = 'This email is already registered';
} else if (error.message.includes('Username already')) {
this.errors.username = 'This username is already taken';
}
}
Utils.showToast(error.message || 'Failed to create admin user', 'error');
} finally {
this.saving = false;
userCreateLog.info('=== ADMIN USER CREATION COMPLETE ===');
}
}
};
}
userCreateLog.info('Admin user create module loaded');

View File

@@ -1,176 +0,0 @@
// noqa: js-006 - async init pattern is safe, loadData has try/catch
// static/admin/js/user-detail.js
// Create custom logger for user detail
const userDetailLog = window.LogConfig.createLogger('USER-DETAIL');
function adminUserDetail() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// User detail page specific state
currentPage: 'user-detail',
user: null,
loading: false,
saving: false,
error: null,
userId: null,
// Initialize
async init() {
userDetailLog.info('=== USER DETAIL PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._userDetailInitialized) {
userDetailLog.warn('User detail page already initialized, skipping...');
return;
}
window._userDetailInitialized = true;
// Get user ID from URL
const path = window.location.pathname;
const match = path.match(/\/admin\/users\/(\d+)$/);
if (match) {
this.userId = match[1];
userDetailLog.info('Viewing user:', this.userId);
await this.loadUser();
} else {
userDetailLog.error('No user ID in URL');
this.error = 'Invalid user URL';
Utils.showToast('Invalid user URL', 'error');
}
userDetailLog.info('=== USER DETAIL PAGE INITIALIZATION COMPLETE ===');
},
// Load user data
async loadUser() {
userDetailLog.info('Loading user details...');
this.loading = true;
this.error = null;
try {
const url = `/admin/users/${this.userId}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
const startTime = performance.now();
const response = await apiClient.get(url);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('GET', url, response, 'response');
window.LogConfig.logPerformance('Load User Details', duration);
this.user = response;
userDetailLog.info(`User loaded in ${duration}ms`, {
id: this.user.id,
username: this.user.username,
role: this.user.role,
is_active: this.user.is_active
});
userDetailLog.debug('Full user data:', this.user);
} catch (error) {
window.LogConfig.logError(error, 'Load User Details');
this.error = error.message || 'Failed to load user details';
Utils.showToast('Failed to load user details', 'error');
} finally {
this.loading = false;
}
},
// Format date
formatDate(dateString) {
if (!dateString) {
return '-';
}
return Utils.formatDate(dateString);
},
// Toggle user status
async toggleStatus() {
const action = this.user.is_active ? 'deactivate' : 'activate';
userDetailLog.info(`Toggle status: ${action}`);
if (!confirm(`Are you sure you want to ${action} ${this.user.username}?`)) {
userDetailLog.info('Status toggle cancelled by user');
return;
}
this.saving = true;
try {
const url = `/admin/users/${this.userId}/status`;
window.LogConfig.logApiCall('PUT', url, null, 'request');
const response = await apiClient.put(url);
window.LogConfig.logApiCall('PUT', url, response, 'response');
this.user.is_active = response.is_active;
Utils.showToast(`User ${action}d successfully`, 'success');
userDetailLog.info(`User ${action}d successfully`);
} catch (error) {
window.LogConfig.logError(error, `Toggle Status (${action})`);
Utils.showToast(error.message || `Failed to ${action} user`, 'error');
} finally {
this.saving = false;
}
},
// Delete user
async deleteUser() {
userDetailLog.info('Delete user requested:', this.userId);
if (this.user?.owned_companies_count > 0) {
Utils.showToast(`Cannot delete user who owns ${this.user.owned_companies_count} company(ies). Transfer ownership first.`, 'error');
return;
}
if (!confirm(`Are you sure you want to delete "${this.user.username}"?\n\nThis action cannot be undone.`)) {
userDetailLog.info('Delete cancelled by user');
return;
}
// Second confirmation for safety
if (!confirm(`FINAL CONFIRMATION\n\nAre you absolutely sure you want to delete "${this.user.username}"?`)) {
userDetailLog.info('Delete cancelled by user (second confirmation)');
return;
}
this.saving = true;
try {
const url = `/admin/users/${this.userId}`;
window.LogConfig.logApiCall('DELETE', url, null, 'request');
await apiClient.delete(url);
window.LogConfig.logApiCall('DELETE', url, null, 'response');
Utils.showToast('User deleted successfully', 'success');
userDetailLog.info('User deleted successfully');
// Redirect to users list
setTimeout(() => window.location.href = '/admin/users', 1500);
} catch (error) {
window.LogConfig.logError(error, 'Delete User');
Utils.showToast(error.message || 'Failed to delete user', 'error');
} finally {
this.saving = false;
}
},
// Refresh user data
async refresh() {
userDetailLog.info('=== USER REFRESH TRIGGERED ===');
await this.loadUser();
Utils.showToast('User details refreshed', 'success');
userDetailLog.info('=== USER REFRESH COMPLETE ===');
}
};
}
userDetailLog.info('User detail module loaded');

View File

@@ -1,229 +0,0 @@
// static/admin/js/user-edit.js
// Create custom logger for user edit
const userEditLog = window.LogConfig.createLogger('USER-EDIT');
function adminUserEdit() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// User edit page specific state
currentPage: 'user-edit',
loading: false,
user: null,
formData: {},
errors: {},
loadingUser: false,
saving: false,
userId: null,
// Initialize
async init() {
userEditLog.info('=== USER EDIT PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._userEditInitialized) {
userEditLog.warn('User edit page already initialized, skipping...');
return;
}
window._userEditInitialized = true;
try {
// Get user ID from URL
const path = window.location.pathname;
const match = path.match(/\/admin\/users\/(\d+)\/edit/);
if (match) {
this.userId = parseInt(match[1], 10);
userEditLog.info('Editing user:', this.userId);
await this.loadUser();
} else {
userEditLog.error('No user ID in URL');
Utils.showToast('Invalid user URL', 'error');
setTimeout(() => window.location.href = '/admin/users', 2000);
}
userEditLog.info('=== USER EDIT PAGE INITIALIZATION COMPLETE ===');
} catch (error) {
window.LogConfig.logError(error, 'User Edit Init');
Utils.showToast('Failed to initialize page', 'error');
}
},
// Load user data
async loadUser() {
userEditLog.info('Loading user data...');
this.loadingUser = true;
try {
const url = `/admin/users/${this.userId}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
const startTime = performance.now();
const response = await apiClient.get(url);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('GET', url, response, 'response');
window.LogConfig.logPerformance('Load User', duration);
this.user = response;
// Initialize form data
this.formData = {
username: response.username || '',
email: response.email || '',
first_name: response.first_name || '',
last_name: response.last_name || '',
role: response.role || 'vendor',
is_email_verified: response.is_email_verified || false
};
userEditLog.info(`User loaded in ${duration}ms`, {
user_id: this.user.id,
username: this.user.username
});
userEditLog.debug('Form data initialized:', this.formData);
} catch (error) {
window.LogConfig.logError(error, 'Load User');
Utils.showToast('Failed to load user', 'error');
setTimeout(() => window.location.href = '/admin/users', 2000);
} finally {
this.loadingUser = false;
}
},
// Format date
formatDate(dateString) {
if (!dateString) {
return '-';
}
return Utils.formatDate(dateString);
},
// Submit form
async handleSubmit() {
userEditLog.info('=== SUBMITTING USER UPDATE ===');
userEditLog.debug('Form data:', this.formData);
this.errors = {};
this.saving = true;
try {
const url = `/admin/users/${this.userId}`;
window.LogConfig.logApiCall('PUT', url, this.formData, 'request');
const startTime = performance.now();
const response = await apiClient.put(url, this.formData);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('PUT', url, response, 'response');
window.LogConfig.logPerformance('Update User', duration);
this.user = response;
Utils.showToast('User updated successfully', 'success');
userEditLog.info(`User updated successfully in ${duration}ms`, response);
} catch (error) {
window.LogConfig.logError(error, 'Update User');
// Handle validation errors
if (error.details && error.details.validation_errors) {
error.details.validation_errors.forEach(err => {
const field = err.loc?.[1] || err.loc?.[0];
if (field) {
this.errors[field] = err.msg;
}
});
userEditLog.debug('Validation errors:', this.errors);
}
Utils.showToast(error.message || 'Failed to update user', 'error');
} finally {
this.saving = false;
userEditLog.info('=== USER UPDATE COMPLETE ===');
}
},
// Toggle user status
async toggleStatus() {
const action = this.user.is_active ? 'deactivate' : 'activate';
userEditLog.info(`Toggle status: ${action}`);
if (!confirm(`Are you sure you want to ${action} ${this.user.username}?`)) {
userEditLog.info('Status toggle cancelled by user');
return;
}
this.saving = true;
try {
const url = `/admin/users/${this.userId}/status`;
window.LogConfig.logApiCall('PUT', url, null, 'request');
const response = await apiClient.put(url);
window.LogConfig.logApiCall('PUT', url, response, 'response');
this.user.is_active = response.is_active;
Utils.showToast(`User ${action}d successfully`, 'success');
userEditLog.info(`User ${action}d successfully`);
} catch (error) {
window.LogConfig.logError(error, `Toggle Status (${action})`);
Utils.showToast(error.message || `Failed to ${action} user`, 'error');
} finally {
this.saving = false;
}
},
// Delete user
async deleteUser() {
userEditLog.info('=== DELETING USER ===');
if (this.user.owned_companies_count > 0) {
Utils.showToast(`Cannot delete user who owns ${this.user.owned_companies_count} company(ies). Transfer ownership first.`, 'error');
return;
}
if (!confirm(`Are you sure you want to delete user "${this.user.username}"?\n\nThis action cannot be undone.`)) {
userEditLog.info('User deletion cancelled by user');
return;
}
// Double confirmation for critical action
if (!confirm(`FINAL CONFIRMATION: Delete "${this.user.username}"?\n\nThis will permanently delete the user.`)) {
userEditLog.info('User deletion cancelled at final confirmation');
return;
}
this.saving = true;
try {
const url = `/admin/users/${this.userId}`;
window.LogConfig.logApiCall('DELETE', url, null, 'request');
await apiClient.delete(url);
window.LogConfig.logApiCall('DELETE', url, null, 'response');
Utils.showToast('User deleted successfully', 'success');
userEditLog.info('User deleted successfully');
// Redirect to users list
setTimeout(() => {
window.location.href = '/admin/users';
}, 1500);
} catch (error) {
window.LogConfig.logError(error, 'Delete User');
Utils.showToast(error.message || 'Failed to delete user', 'error');
} finally {
this.saving = false;
userEditLog.info('=== USER DELETION COMPLETE ===');
}
}
};
}
userEditLog.info('User edit module loaded');

View File

@@ -1,299 +0,0 @@
// noqa: js-006 - async init pattern is safe, loadData has try/catch
// static/admin/js/users.js
// ✅ Use centralized logger - ONE LINE!
const usersLog = window.LogConfig.loggers.users;
function adminUsers() {
return {
// Inherit base layout functionality
...data(),
// Set page identifier
currentPage: 'users',
// State
users: [],
loading: false,
error: null,
filters: {
search: '',
role: '',
is_active: ''
},
stats: {
total_users: 0,
active_users: 0,
inactive_users: 0,
admin_users: 0
},
pagination: {
page: 1,
per_page: 20,
total: 0,
pages: 0
},
// 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 ===');
},
// Format date helper
formatDate(dateString) {
if (!dateString) return '-';
return Utils.formatDate(dateString);
},
// Computed: Total number of pages
get totalPages() {
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: Generate page numbers array with ellipsis
get pageNumbers() {
const pages = [];
const totalPages = this.totalPages;
const current = this.pagination.page;
if (totalPages <= 7) {
// Show all pages if 7 or fewer
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
// Always show first page
pages.push(1);
if (current > 3) {
pages.push('...');
}
// Show pages around current page
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('...');
}
// Always show last page
pages.push(totalPages);
}
return pages;
},
// Load users from API
async loadUsers() {
usersLog.info('Loading users...');
this.loading = true;
this.error = null;
try {
const params = new URLSearchParams();
params.append('page', this.pagination.page);
params.append('per_page', this.pagination.per_page);
if (this.filters.search) {
params.append('search', this.filters.search);
}
if (this.filters.role) {
params.append('role', this.filters.role);
}
if (this.filters.is_active) {
params.append('is_active', this.filters.is_active);
}
const url = `/admin/users?${params}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
const startTime = performance.now();
const response = await apiClient.get(url);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('GET', url, response, 'response');
window.LogConfig.logPerformance('Load Users', duration);
if (response.items) {
this.users = response.items;
this.pagination.total = response.total;
this.pagination.pages = response.pages;
this.pagination.page = response.page;
this.pagination.per_page = response.per_page;
usersLog.info(`Loaded ${this.users.length} users`);
}
} catch (error) {
window.LogConfig.logError(error, 'Load Users');
this.error = error.message || 'Failed to load users';
Utils.showToast('Failed to load users', 'error');
} finally {
this.loading = false;
}
},
// Load statistics
async loadStats() {
usersLog.info('Loading user statistics...');
try {
const url = '/admin/users/stats';
window.LogConfig.logApiCall('GET', url, null, 'request');
const response = await apiClient.get(url); // ✅ Fixed: lowercase apiClient
window.LogConfig.logApiCall('GET', url, response, 'response');
if (response) {
this.stats = response;
usersLog.debug('Stats loaded:', this.stats);
}
} catch (error) {
window.LogConfig.logError(error, 'Load Stats');
}
},
// Search with debounce
debouncedSearch() {
// Clear existing timeout
if (this._searchTimeout) {
clearTimeout(this._searchTimeout);
}
// Set new timeout
this._searchTimeout = setTimeout(() => {
usersLog.info('Search triggered:', this.filters.search);
this.pagination.page = 1;
this.loadUsers();
}, 300);
},
// Pagination
nextPage() {
if (this.pagination.page < this.pagination.pages) {
this.pagination.page++;
usersLog.info('Next page:', this.pagination.page);
this.loadUsers();
}
},
previousPage() {
if (this.pagination.page > 1) {
this.pagination.page--;
usersLog.info('Previous page:', this.pagination.page);
this.loadUsers();
}
},
goToPage(pageNum) {
if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) {
this.pagination.page = pageNum;
usersLog.info('Go to page:', this.pagination.page);
this.loadUsers();
}
},
// Actions
viewUser(user) {
usersLog.info('View user:', user.username);
window.location.href = `/admin/users/${user.id}`;
},
editUser(user) {
usersLog.info('Edit user:', user.username);
window.location.href = `/admin/users/${user.id}/edit`;
},
async toggleUserStatus(user) {
const action = user.is_active ? 'deactivate' : 'activate';
usersLog.info(`Toggle user status: ${action}`, user.username);
if (!confirm(`Are you sure you want to ${action} ${user.username}?`)) {
usersLog.info('Status toggle cancelled by user');
return;
}
try {
const url = `/admin/users/${user.id}/status`;
window.LogConfig.logApiCall('PUT', url, { is_active: !user.is_active }, 'request');
await apiClient.put(url, { // ✅ Fixed: lowercase apiClient
is_active: !user.is_active
});
Utils.showToast(`User ${action}d successfully`, 'success');
usersLog.info(`User ${action}d successfully`);
await this.loadUsers();
await this.loadStats();
} catch (error) {
window.LogConfig.logError(error, `Toggle User Status (${action})`);
Utils.showToast(`Failed to ${action} user`, 'error');
}
},
async deleteUser(user) {
usersLog.warn('Delete user requested:', user.username);
if (!confirm(`Are you sure you want to delete ${user.username}? This action cannot be undone.`)) {
usersLog.info('Delete cancelled by user');
return;
}
try {
const url = `/admin/users/${user.id}`;
window.LogConfig.logApiCall('DELETE', url, null, 'request');
await apiClient.delete(url); // ✅ Fixed: lowercase apiClient
Utils.showToast('User deleted successfully', 'success');
usersLog.info('User deleted successfully');
await this.loadUsers();
await this.loadStats();
} catch (error) {
window.LogConfig.logError(error, 'Delete User');
Utils.showToast('Failed to delete user', 'error');
}
},
openCreateModal() {
usersLog.info('Open create user modal');
window.location.href = '/admin/users/create';
}
};
}
usersLog.info('Users module loaded');

View File

@@ -1,206 +0,0 @@
// static/admin/js/vendor-create.js
/**
* Admin Vendor Create Page
* Handles vendor creation form with company selection
*/
// Use centralized logger
const vendorCreateLog = window.LogConfig.loggers.vendors;
vendorCreateLog.info('Loading vendor create module...');
function adminVendorCreate() {
vendorCreateLog.debug('adminVendorCreate() called');
return {
// Inherit base layout functionality
...data(),
// Page identifier
currentPage: 'vendors',
// Companies list for dropdown
companies: [],
loadingCompanies: true,
// Platforms list for selection
platforms: [],
// Form data matching VendorCreate schema
formData: {
company_id: '',
vendor_code: '',
subdomain: '',
name: '',
description: '',
letzshop_csv_url_fr: '',
letzshop_csv_url_en: '',
letzshop_csv_url_de: '',
platform_ids: []
},
// UI state
loading: false,
successMessage: false,
errorMessage: '',
createdVendor: null,
// Initialize
async init() {
// Guard against multiple initialization
if (window._adminVendorCreateInitialized) return;
window._adminVendorCreateInitialized = true;
try {
vendorCreateLog.info('Initializing vendor create page');
await Promise.all([
this.loadCompanies(),
this.loadPlatforms()
]);
} catch (error) {
vendorCreateLog.error('Failed to initialize vendor create:', error);
}
},
// Load companies for dropdown
async loadCompanies() {
this.loadingCompanies = true;
try {
const response = await apiClient.get('/admin/companies?limit=1000');
this.companies = response.companies || [];
vendorCreateLog.debug('Loaded companies:', this.companies.length);
} catch (error) {
vendorCreateLog.error('Failed to load companies:', error);
this.errorMessage = 'Failed to load companies. Please refresh the page.';
} finally {
this.loadingCompanies = false;
}
},
// Load platforms for selection
async loadPlatforms() {
try {
const response = await apiClient.get('/admin/platforms');
this.platforms = response.platforms || response.items || [];
vendorCreateLog.debug('Loaded platforms:', this.platforms.length);
} catch (error) {
vendorCreateLog.error('Failed to load platforms:', error);
this.platforms = [];
}
},
// Auto-generate subdomain from vendor name
autoGenerateSubdomain() {
if (!this.formData.name) {
return;
}
// Convert name to subdomain format
const subdomain = this.formData.name
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '') // Remove special chars
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-') // Replace multiple hyphens with single
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
this.formData.subdomain = subdomain;
vendorCreateLog.debug('Auto-generated subdomain:', subdomain);
},
// Create vendor
async createVendor() {
this.loading = true;
this.errorMessage = '';
this.successMessage = false;
this.createdVendor = null;
try {
vendorCreateLog.info('Creating vendor:', {
company_id: this.formData.company_id,
vendor_code: this.formData.vendor_code,
name: this.formData.name
});
// Prepare payload - only include non-empty values
const payload = {
company_id: parseInt(this.formData.company_id),
vendor_code: this.formData.vendor_code.toUpperCase(),
subdomain: this.formData.subdomain.toLowerCase(),
name: this.formData.name
};
// Add optional fields if provided
if (this.formData.description) {
payload.description = this.formData.description;
}
if (this.formData.letzshop_csv_url_fr) {
payload.letzshop_csv_url_fr = this.formData.letzshop_csv_url_fr;
}
if (this.formData.letzshop_csv_url_en) {
payload.letzshop_csv_url_en = this.formData.letzshop_csv_url_en;
}
if (this.formData.letzshop_csv_url_de) {
payload.letzshop_csv_url_de = this.formData.letzshop_csv_url_de;
}
// Add platform assignments
if (this.formData.platform_ids && this.formData.platform_ids.length > 0) {
payload.platform_ids = this.formData.platform_ids.map(id => parseInt(id));
}
const response = await apiClient.post('/admin/vendors', payload);
vendorCreateLog.info('Vendor created successfully:', response.vendor_code);
// Store created vendor details
this.createdVendor = {
vendor_code: response.vendor_code,
name: response.name,
subdomain: response.subdomain,
company_name: response.company_name
};
this.successMessage = true;
// Reset form
this.formData = {
company_id: '',
vendor_code: '',
subdomain: '',
name: '',
description: '',
letzshop_csv_url_fr: '',
letzshop_csv_url_en: '',
letzshop_csv_url_de: '',
platform_ids: []
};
// Scroll to top to show success message
window.scrollTo({ top: 0, behavior: 'smooth' });
// Redirect after 3 seconds
setTimeout(() => {
window.location.href = `/admin/vendors/${response.vendor_code}`;
}, 3000);
} catch (error) {
vendorCreateLog.error('Failed to create vendor:', error);
// Parse error message
if (error.message) {
this.errorMessage = error.message;
} else if (error.detail) {
this.errorMessage = error.detail;
} else {
this.errorMessage = 'Failed to create vendor. Please try again.';
}
window.scrollTo({ top: 0, behavior: 'smooth' });
} finally {
this.loading = false;
}
}
};
}
vendorCreateLog.info('Vendor create module loaded');

View File

@@ -1,223 +0,0 @@
// noqa: js-006 - async init pattern is safe, loadData has try/catch
// static/admin/js/vendor-detail.js
// ✅ Use centralized logger - ONE LINE!
// Create custom logger for vendor detail
const detailLog = window.LogConfig.createLogger('VENDOR-DETAIL');
function adminVendorDetail() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Vendor detail page specific state
currentPage: 'vendor-detail',
vendor: null,
subscription: null,
loading: false,
error: null,
vendorCode: null,
showSubscriptionModal: false,
// Initialize
async init() {
detailLog.info('=== VENDOR DETAIL PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._vendorDetailInitialized) {
detailLog.warn('Vendor detail page already initialized, skipping...');
return;
}
window._vendorDetailInitialized = true;
// Get vendor code from URL
const path = window.location.pathname;
const match = path.match(/\/admin\/vendors\/([^\/]+)$/);
if (match) {
this.vendorCode = match[1];
detailLog.info('Viewing vendor:', this.vendorCode);
await this.loadVendor();
// Load subscription after vendor is loaded
if (this.vendor?.id) {
await this.loadSubscription();
}
} else {
detailLog.error('No vendor code in URL');
this.error = 'Invalid vendor URL';
Utils.showToast('Invalid vendor URL', 'error');
}
detailLog.info('=== VENDOR DETAIL PAGE INITIALIZATION COMPLETE ===');
},
// Load vendor data
async loadVendor() {
detailLog.info('Loading vendor details...');
this.loading = true;
this.error = null;
try {
const url = `/admin/vendors/${this.vendorCode}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
const startTime = performance.now();
const response = await apiClient.get(url);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('GET', url, response, 'response');
window.LogConfig.logPerformance('Load Vendor Details', duration);
this.vendor = response;
detailLog.info(`Vendor loaded in ${duration}ms`, {
vendor_code: this.vendor.vendor_code,
name: this.vendor.name,
is_verified: this.vendor.is_verified,
is_active: this.vendor.is_active
});
detailLog.debug('Full vendor data:', this.vendor);
} catch (error) {
window.LogConfig.logError(error, 'Load Vendor Details');
this.error = error.message || 'Failed to load vendor details';
Utils.showToast('Failed to load vendor details', 'error');
} finally {
this.loading = false;
}
},
// Format date (matches dashboard pattern)
formatDate(dateString) {
if (!dateString) {
detailLog.debug('formatDate called with empty dateString');
return '-';
}
const formatted = Utils.formatDate(dateString);
detailLog.debug(`Date formatted: ${dateString} -> ${formatted}`);
return formatted;
},
// Load subscription data for this vendor
async loadSubscription() {
if (!this.vendor?.id) {
detailLog.warn('Cannot load subscription: no vendor ID');
return;
}
detailLog.info('Loading subscription for vendor:', this.vendor.id);
try {
const url = `/admin/subscriptions/${this.vendor.id}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
const response = await apiClient.get(url);
window.LogConfig.logApiCall('GET', url, response, 'response');
this.subscription = response;
detailLog.info('Subscription loaded:', {
tier: this.subscription?.tier,
status: this.subscription?.status,
orders_this_period: this.subscription?.orders_this_period
});
} catch (error) {
// 404 means no subscription exists - that's OK
if (error.status === 404) {
detailLog.info('No subscription found for vendor');
this.subscription = null;
} else {
detailLog.warn('Failed to load subscription:', error.message);
}
}
},
// Get usage bar color based on percentage
getUsageBarColor(current, limit) {
if (!limit || limit === 0) return 'bg-blue-500';
const percent = (current / limit) * 100;
if (percent >= 90) return 'bg-red-500';
if (percent >= 75) return 'bg-yellow-500';
return 'bg-green-500';
},
// Create a new subscription for this vendor
async createSubscription() {
if (!this.vendor?.id) {
Utils.showToast('No vendor loaded', 'error');
return;
}
detailLog.info('Creating subscription for vendor:', this.vendor.id);
try {
// Create a trial subscription with default tier
const url = `/admin/subscriptions/${this.vendor.id}`;
const data = {
tier: 'essential',
status: 'trial',
trial_days: 14,
is_annual: false
};
window.LogConfig.logApiCall('POST', url, data, 'request');
const response = await apiClient.post(url, data);
window.LogConfig.logApiCall('POST', url, response, 'response');
this.subscription = response;
Utils.showToast('Subscription created successfully', 'success');
detailLog.info('Subscription created:', this.subscription);
} catch (error) {
window.LogConfig.logError(error, 'Create Subscription');
Utils.showToast(error.message || 'Failed to create subscription', 'error');
}
},
// Delete vendor
async deleteVendor() {
detailLog.info('Delete vendor requested:', this.vendorCode);
if (!confirm(`Are you sure you want to delete vendor "${this.vendor.name}"?\n\nThis action cannot be undone and will delete:\n- All products\n- All orders\n- All customers\n- All team members`)) {
detailLog.info('Delete cancelled by user');
return;
}
// Second confirmation for safety
if (!confirm(`FINAL CONFIRMATION\n\nType the vendor code to confirm: ${this.vendor.vendor_code}\n\nAre you absolutely sure?`)) {
detailLog.info('Delete cancelled by user (second confirmation)');
return;
}
try {
const url = `/admin/vendors/${this.vendorCode}?confirm=true`;
window.LogConfig.logApiCall('DELETE', url, null, 'request');
detailLog.info('Deleting vendor:', this.vendorCode);
await apiClient.delete(url);
window.LogConfig.logApiCall('DELETE', url, null, 'response');
Utils.showToast('Vendor deleted successfully', 'success');
detailLog.info('Vendor deleted successfully');
// Redirect to vendors list
setTimeout(() => window.location.href = '/admin/vendors', 1500);
} catch (error) {
window.LogConfig.logError(error, 'Delete Vendor');
Utils.showToast(error.message || 'Failed to delete vendor', 'error');
}
},
// Refresh vendor data
async refresh() {
detailLog.info('=== VENDOR REFRESH TRIGGERED ===');
await this.loadVendor();
Utils.showToast('Vendor details refreshed', 'success');
detailLog.info('=== VENDOR REFRESH COMPLETE ===');
}
};
}
detailLog.info('Vendor detail module loaded');

View File

@@ -1,311 +0,0 @@
// static/admin/js/vendor-edit.js
// ✅ Use centralized logger - ONE LINE!
// Create custom logger for vendor edit
const editLog = window.LogConfig.createLogger('VENDOR-EDIT');
function adminVendorEdit() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Vendor edit page specific state
currentPage: 'vendor-edit',
loading: false,
vendor: null,
formData: {},
errors: {},
loadingVendor: false,
saving: false,
vendorCode: null,
// Initialize
async init() {
editLog.info('=== VENDOR EDIT PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._vendorEditInitialized) {
editLog.warn('Vendor edit page already initialized, skipping...');
return;
}
window._vendorEditInitialized = true;
try {
// Get vendor code from URL
const path = window.location.pathname;
const match = path.match(/\/admin\/vendors\/([^\/]+)\/edit/);
if (match) {
this.vendorCode = match[1];
editLog.info('Editing vendor:', this.vendorCode);
await this.loadVendor();
} else {
editLog.error('No vendor code in URL');
Utils.showToast('Invalid vendor URL', 'error');
setTimeout(() => window.location.href = '/admin/vendors', 2000);
}
editLog.info('=== VENDOR EDIT PAGE INITIALIZATION COMPLETE ===');
} catch (error) {
window.LogConfig.logError(error, 'Vendor Edit Init');
Utils.showToast('Failed to initialize page', 'error');
}
},
// Load vendor data
async loadVendor() {
editLog.info('Loading vendor data...');
this.loadingVendor = true;
try {
const url = `/admin/vendors/${this.vendorCode}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
const startTime = performance.now();
const response = await apiClient.get(url);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('GET', url, response, 'response');
window.LogConfig.logPerformance('Load Vendor', duration);
this.vendor = response;
// Initialize form data
// For contact fields: empty if inherited (shows placeholder), actual value if override
this.formData = {
name: response.name || '',
subdomain: response.subdomain || '',
description: response.description || '',
// Contact fields: empty string for inherited (will show company value as placeholder)
contact_email: response.contact_email_inherited ? '' : (response.contact_email || ''),
contact_phone: response.contact_phone_inherited ? '' : (response.contact_phone || ''),
website: response.website_inherited ? '' : (response.website || ''),
business_address: response.business_address_inherited ? '' : (response.business_address || ''),
tax_number: response.tax_number_inherited ? '' : (response.tax_number || ''),
// Marketplace URLs (no inheritance)
letzshop_csv_url_fr: response.letzshop_csv_url_fr || '',
letzshop_csv_url_en: response.letzshop_csv_url_en || '',
letzshop_csv_url_de: response.letzshop_csv_url_de || ''
};
editLog.info(`Vendor loaded in ${duration}ms`, {
vendor_code: this.vendor.vendor_code,
name: this.vendor.name
});
editLog.debug('Form data initialized:', this.formData);
} catch (error) {
window.LogConfig.logError(error, 'Load Vendor');
Utils.showToast('Failed to load vendor', 'error');
setTimeout(() => window.location.href = '/admin/vendors', 2000);
} finally {
this.loadingVendor = false;
}
},
// Format subdomain
formatSubdomain() {
this.formData.subdomain = this.formData.subdomain
.toLowerCase()
.replace(/[^a-z0-9-]/g, '');
editLog.debug('Subdomain formatted:', this.formData.subdomain);
},
// Submit form
async handleSubmit() {
editLog.info('=== SUBMITTING VENDOR UPDATE ===');
editLog.debug('Form data:', this.formData);
this.errors = {};
this.saving = true;
try {
const url = `/admin/vendors/${this.vendorCode}`;
window.LogConfig.logApiCall('PUT', url, this.formData, 'request');
const startTime = performance.now();
const response = await apiClient.put(url, this.formData);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('PUT', url, response, 'response');
window.LogConfig.logPerformance('Update Vendor', duration);
this.vendor = response;
Utils.showToast('Vendor updated successfully', 'success');
editLog.info(`Vendor updated successfully in ${duration}ms`, response);
// Optionally redirect back to list
// setTimeout(() => window.location.href = '/admin/vendors', 1500);
} catch (error) {
window.LogConfig.logError(error, 'Update Vendor');
// Handle validation errors
if (error.details && error.details.validation_errors) {
error.details.validation_errors.forEach(err => {
const field = err.loc?.[1] || err.loc?.[0];
if (field) {
this.errors[field] = err.msg;
}
});
editLog.debug('Validation errors:', this.errors);
}
Utils.showToast(error.message || 'Failed to update vendor', 'error');
} finally {
this.saving = false;
editLog.info('=== VENDOR UPDATE COMPLETE ===');
}
},
// Toggle verification
async toggleVerification() {
const action = this.vendor.is_verified ? 'unverify' : 'verify';
editLog.info(`Toggle verification: ${action}`);
if (!confirm(`Are you sure you want to ${action} this vendor?`)) {
editLog.info('Verification toggle cancelled by user');
return;
}
this.saving = true;
try {
const url = `/admin/vendors/${this.vendorCode}/verification`;
const payload = { is_verified: !this.vendor.is_verified };
window.LogConfig.logApiCall('PUT', url, payload, 'request');
const response = await apiClient.put(url, payload);
window.LogConfig.logApiCall('PUT', url, response, 'response');
this.vendor = response;
Utils.showToast(`Vendor ${action}ed successfully`, 'success');
editLog.info(`Vendor ${action}ed successfully`);
} catch (error) {
window.LogConfig.logError(error, `Toggle Verification (${action})`);
Utils.showToast(`Failed to ${action} vendor`, 'error');
} finally {
this.saving = false;
}
},
// Toggle active status
async toggleActive() {
const action = this.vendor.is_active ? 'deactivate' : 'activate';
editLog.info(`Toggle active status: ${action}`);
if (!confirm(`Are you sure you want to ${action} this vendor?\n\nThis will affect their operations.`)) {
editLog.info('Active status toggle cancelled by user');
return;
}
this.saving = true;
try {
const url = `/admin/vendors/${this.vendorCode}/status`;
const payload = { is_active: !this.vendor.is_active };
window.LogConfig.logApiCall('PUT', url, payload, 'request');
const response = await apiClient.put(url, payload);
window.LogConfig.logApiCall('PUT', url, response, 'response');
this.vendor = response;
Utils.showToast(`Vendor ${action}d successfully`, 'success');
editLog.info(`Vendor ${action}d successfully`);
} catch (error) {
window.LogConfig.logError(error, `Toggle Active Status (${action})`);
Utils.showToast(`Failed to ${action} vendor`, 'error');
} finally {
this.saving = false;
}
},
// Delete vendor
async deleteVendor() {
editLog.info('Delete vendor requested');
const vendorName = this.vendor?.name || this.vendorCode;
if (!confirm(`Are you sure you want to delete "${vendorName}"?\n\n⚠️ WARNING: This will permanently delete:\n• All products\n• All orders\n• All customers\n• All team members\n\nThis action cannot be undone!`)) {
editLog.info('Delete cancelled by user');
return;
}
// Double confirmation for safety
if (!confirm(`FINAL CONFIRMATION\n\nType OK to permanently delete "${vendorName}" and ALL associated data.`)) {
editLog.info('Delete cancelled at final confirmation');
return;
}
this.saving = true;
try {
const url = `/admin/vendors/${this.vendorCode}?confirm=true`;
window.LogConfig.logApiCall('DELETE', url, null, 'request');
const response = await apiClient.delete(url);
window.LogConfig.logApiCall('DELETE', url, response, 'response');
Utils.showToast('Vendor deleted successfully', 'success');
editLog.info('Vendor deleted successfully');
// Redirect to vendors list
setTimeout(() => {
window.location.href = '/admin/vendors';
}, 1500);
} catch (error) {
window.LogConfig.logError(error, 'Delete Vendor');
Utils.showToast(error.message || 'Failed to delete vendor', 'error');
} finally {
this.saving = false;
}
},
// ===== Contact Field Inheritance Methods =====
/**
* Reset a single contact field to inherit from company.
* Sets the field to empty string, which the backend converts to null (inherit).
* @param {string} fieldName - The contact field to reset
*/
resetFieldToCompany(fieldName) {
const contactFields = ['contact_email', 'contact_phone', 'website', 'business_address', 'tax_number'];
if (!contactFields.includes(fieldName)) {
editLog.warn('Invalid contact field:', fieldName);
return;
}
editLog.info(`Resetting ${fieldName} to inherit from company`);
this.formData[fieldName] = '';
},
/**
* Reset all contact fields to inherit from company.
*/
resetAllContactToCompany() {
editLog.info('Resetting all contact fields to inherit from company');
const contactFields = ['contact_email', 'contact_phone', 'website', 'business_address', 'tax_number'];
contactFields.forEach(field => {
this.formData[field] = '';
});
Utils.showToast('All contact fields reset to company defaults', 'info');
},
/**
* Check if any contact field has a value (not empty = has override).
* @returns {boolean} True if at least one contact field has a value
*/
hasAnyContactOverride() {
const contactFields = ['contact_email', 'contact_phone', 'website', 'business_address', 'tax_number'];
return contactFields.some(field => this.formData[field]);
}
};
}
editLog.info('Vendor edit module loaded');

View File

@@ -1,332 +0,0 @@
// static/admin/js/vendor-theme.js (FIXED VERSION)
/**
* Vendor Theme Editor - Alpine.js Component
* Manages theme customization for vendor shops
*
* REQUIRES: log-config.js to be loaded first
*/
// ============================================================================
// LOGGING CONFIGURATION (using centralized logger)
// ============================================================================
// Use the pre-configured theme logger from centralized log-config.js
const themeLog = window.LogConfig.loggers.vendorTheme;
// ============================================================================
// ALPINE.JS COMPONENT
// ============================================================================
function adminVendorTheme() {
return {
// ✅ CRITICAL: Inherit base layout functionality
...data(),
// ✅ CRITICAL: Set page identifier
currentPage: 'vendor-theme',
// Page state
vendorCode: null,
vendor: null,
loading: true,
saving: false,
error: null,
// Theme data structure matching VendorTheme model
themeData: {
theme_name: 'default',
colors: {
primary: '#6366f1',
secondary: '#8b5cf6',
accent: '#ec4899',
background: '#ffffff',
text: '#1f2937',
border: '#e5e7eb'
},
fonts: {
heading: 'Inter, sans-serif',
body: 'Inter, sans-serif',
size_base: '16px',
size_heading: '2rem'
},
layout: {
style: 'grid',
header_position: 'fixed',
product_card_style: 'card',
sidebar_position: 'left'
},
branding: {
logo_url: '',
favicon_url: '',
banner_url: ''
},
custom_css: '',
social_links: {
facebook: '',
instagram: '',
twitter: '',
linkedin: ''
}
},
// Available presets
presets: [],
selectedPreset: null,
// ====================================================================
// INITIALIZATION
// ====================================================================
async init() {
// Guard against multiple initialization
if (window._adminVendorThemeInitialized) return;
window._adminVendorThemeInitialized = true;
themeLog.info('Initializing vendor theme editor');
// Start performance timer
const startTime = performance.now();
try {
// Extract vendor code from URL
const urlParts = window.location.pathname.split('/');
this.vendorCode = urlParts[urlParts.indexOf('vendors') + 1];
themeLog.debug('Vendor code from URL:', this.vendorCode);
if (!this.vendorCode) {
throw new Error('Vendor code not found in URL');
}
// Load data in parallel
themeLog.group('Loading theme data');
await Promise.all([
this.loadVendor(),
this.loadTheme(),
this.loadPresets()
]);
themeLog.groupEnd();
// Log performance
const duration = performance.now() - startTime;
window.LogConfig.logPerformance('Theme Editor Init', duration);
themeLog.info('Theme editor initialized successfully');
} catch (error) {
// Use centralized error logger
window.LogConfig.logError(error, 'Theme Editor Init');
this.error = error.message || 'Failed to initialize theme editor';
Utils.showToast(this.error, 'error');
} finally {
this.loading = false;
}
},
// ====================================================================
// DATA LOADING
// ====================================================================
async loadVendor() {
themeLog.info('Loading vendor data');
const url = `/admin/vendors/${this.vendorCode}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
try {
// ✅ FIX: apiClient returns data directly, not response.data
const response = await apiClient.get(url);
// ✅ Direct assignment - response IS the data
this.vendor = response;
window.LogConfig.logApiCall('GET', url, this.vendor, 'response');
themeLog.debug('Vendor loaded:', this.vendor);
} catch (error) {
themeLog.error('Failed to load vendor:', error);
throw error;
}
},
async loadTheme() {
themeLog.info('Loading theme data');
const url = `/admin/vendor-themes/${this.vendorCode}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
try {
// ✅ FIX: apiClient returns data directly
const response = await apiClient.get(url);
// Merge with default theme data
this.themeData = {
...this.themeData,
...response
};
window.LogConfig.logApiCall('GET', url, this.themeData, 'response');
themeLog.debug('Theme loaded:', this.themeData);
} catch (error) {
themeLog.warn('Failed to load theme, using defaults:', error);
// Continue with default theme
}
},
async loadPresets() {
themeLog.info('Loading theme presets');
const url = '/admin/vendor-themes/presets';
window.LogConfig.logApiCall('GET', url, null, 'request');
try {
// ✅ FIX: apiClient returns data directly
const response = await apiClient.get(url);
// ✅ Access presets directly from response, not response.data.presets
this.presets = response.presets || [];
window.LogConfig.logApiCall('GET', url, response, 'response');
themeLog.debug(`Loaded ${this.presets.length} presets`);
} catch (error) {
themeLog.error('Failed to load presets:', error);
this.presets = [];
}
},
// ====================================================================
// THEME OPERATIONS
// ====================================================================
async saveTheme() {
if (this.saving) return;
themeLog.info('Saving theme changes');
this.saving = true;
this.error = null;
const startTime = performance.now();
try {
const url = `/admin/vendor-themes/${this.vendorCode}`;
window.LogConfig.logApiCall('PUT', url, this.themeData, 'request');
// ✅ FIX: apiClient returns data directly
const response = await apiClient.put(url, this.themeData);
window.LogConfig.logApiCall('PUT', url, response, 'response');
const duration = performance.now() - startTime;
window.LogConfig.logPerformance('Save Theme', duration);
themeLog.info('Theme saved successfully');
Utils.showToast('Theme saved successfully', 'success');
} catch (error) {
window.LogConfig.logError(error, 'Save Theme');
this.error = 'Failed to save theme';
Utils.showToast(this.error, 'error');
} finally {
this.saving = false;
}
},
async applyPreset(presetName) {
themeLog.info(`Applying preset: ${presetName}`);
this.saving = true;
try {
const url = `/admin/vendor-themes/${this.vendorCode}/preset/${presetName}`;
window.LogConfig.logApiCall('POST', url, null, 'request');
// ✅ FIX: apiClient returns data directly
const response = await apiClient.post(url);
window.LogConfig.logApiCall('POST', url, response, 'response');
// ✅ FIX: Access theme directly from response, not response.data.theme
if (response && response.theme) {
this.themeData = {
...this.themeData,
...response.theme
};
}
themeLog.info(`Preset '${presetName}' applied successfully`);
Utils.showToast(`Applied ${presetName} preset`, 'success');
} catch (error) {
window.LogConfig.logError(error, 'Apply Preset');
Utils.showToast('Failed to apply preset', 'error');
} finally {
this.saving = false;
}
},
async resetTheme() {
if (!confirm('Reset theme to default? This cannot be undone.')) {
return;
}
themeLog.warn('Resetting theme to default');
this.saving = true;
try {
const url = `/admin/vendor-themes/${this.vendorCode}`;
window.LogConfig.logApiCall('DELETE', url, null, 'request');
await apiClient.delete(url);
window.LogConfig.logApiCall('DELETE', url, null, 'response');
// Reload theme data
await this.loadTheme();
themeLog.info('Theme reset successfully');
Utils.showToast('Theme reset to default', 'success');
} catch (error) {
window.LogConfig.logError(error, 'Reset Theme');
Utils.showToast('Failed to reset theme', 'error');
} finally {
this.saving = false;
}
},
// ====================================================================
// UTILITY METHODS
// ====================================================================
previewTheme() {
themeLog.debug('Opening theme preview');
const previewUrl = `/vendor/${this.vendor?.subdomain || this.vendorCode}`;
window.open(previewUrl, '_blank');
},
updateColor(key, value) {
themeLog.debug(`Color updated: ${key} = ${value}`);
this.themeData.colors[key] = value;
},
updateFont(type, value) {
themeLog.debug(`Font updated: ${type} = ${value}`);
this.themeData.fonts[type] = value;
},
updateLayout(key, value) {
themeLog.debug(`Layout updated: ${key} = ${value}`);
this.themeData.layout[key] = value;
}
};
}
// ============================================================================
// MODULE LOADED
// ============================================================================
themeLog.info('Vendor theme editor module loaded');

View File

@@ -1,171 +0,0 @@
// noqa: js-006 - async init pattern is safe, loadData has try/catch
// static/admin/js/vendor-themes.js
/**
* Admin vendor themes selection page
*/
// ✅ Use centralized logger
const vendorThemesLog = window.LogConfig.loggers.vendorTheme;
vendorThemesLog.info('Loading...');
function adminVendorThemes() {
vendorThemesLog.debug('adminVendorThemes() called');
return {
// Inherit base layout state
...data(),
// Set page identifier
currentPage: 'vendor-theme',
// State
loading: false,
error: '',
vendors: [],
selectedVendorCode: '',
// Selected vendor for filter (Tom Select)
selectedVendor: null,
vendorSelector: null,
// Search/filter
searchQuery: '',
async init() {
// Guard against multiple initialization
if (window._adminVendorThemesInitialized) {
return;
}
window._adminVendorThemesInitialized = true;
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();
}
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() {
this.loading = true;
this.error = '';
try {
const response = await apiClient.get('/admin/vendors?limit=1000');
this.vendors = response.vendors || [];
vendorThemesLog.debug('Loaded vendors:', this.vendors.length);
} catch (error) {
vendorThemesLog.error('Failed to load vendors:', error);
this.error = error.message || 'Failed to load vendors';
} finally {
this.loading = false;
}
},
/**
* 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;
}
window.location.href = `/admin/vendors/${this.selectedVendorCode}/theme`;
}
};
}
vendorThemesLog.info('Module loaded');

View File

@@ -1,329 +0,0 @@
// noqa: js-006 - async init pattern is safe, loadData has try/catch
// static/admin/js/vendors.js
// ✅ Use centralized logger - ONE LINE!
const vendorsLog = window.LogConfig.loggers.vendors;
// ============================================
// VENDOR LIST FUNCTION
// ============================================
function adminVendors() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// ✅ CRITICAL: Page identifier for sidebar active state
currentPage: 'vendors',
// Vendors page specific state
vendors: [],
stats: {
total: 0,
verified: 0,
pending: 0,
inactive: 0
},
loading: false,
error: null,
// Search and filters
filters: {
search: '',
is_active: '',
is_verified: ''
},
// Pagination state (server-side)
pagination: {
page: 1,
per_page: 20,
total: 0,
pages: 0
},
// Initialize
async init() {
vendorsLog.info('=== VENDORS PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._vendorsInitialized) {
vendorsLog.warn('Vendors page already initialized, skipping...');
return;
}
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();
vendorsLog.groupEnd();
vendorsLog.info('=== VENDORS PAGE INITIALIZATION COMPLETE ===');
},
// Debounced search
debouncedSearch() {
if (this._searchTimeout) {
clearTimeout(this._searchTimeout);
}
this._searchTimeout = setTimeout(() => {
vendorsLog.info('Search triggered:', this.filters.search);
this.pagination.page = 1;
this.loadVendors();
}, 300);
},
// Computed: Get vendors for current page (already paginated from server)
get paginatedVendors() {
return this.vendors;
},
// Computed: Total number of pages
get totalPages() {
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: Generate page numbers array with ellipsis
get pageNumbers() {
const pages = [];
const totalPages = this.totalPages;
const current = this.pagination.page;
if (totalPages <= 7) {
// Show all pages if 7 or fewer
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
// Always show first page
pages.push(1);
if (current > 3) {
pages.push('...');
}
// Show pages around current page
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('...');
}
// Always show last page
pages.push(totalPages);
}
return pages;
},
// Load vendors list with search and pagination
async loadVendors() {
vendorsLog.info('Loading vendors list...');
this.loading = true;
this.error = null;
try {
// Build query parameters
const params = new URLSearchParams();
params.append('skip', (this.pagination.page - 1) * this.pagination.per_page);
params.append('limit', this.pagination.per_page);
if (this.filters.search) {
params.append('search', this.filters.search);
}
if (this.filters.is_active !== '') {
params.append('is_active', this.filters.is_active);
}
if (this.filters.is_verified !== '') {
params.append('is_verified', this.filters.is_verified);
}
const url = `/admin/vendors?${params}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
const startTime = performance.now();
const response = await apiClient.get(url);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('GET', url, response, 'response');
window.LogConfig.logPerformance('Load Vendors', duration);
// Handle response with pagination info
if (response.vendors) {
this.vendors = response.vendors;
this.pagination.total = response.total;
this.pagination.pages = Math.ceil(response.total / this.pagination.per_page);
vendorsLog.info(`Loaded ${this.vendors.length} vendors (total: ${response.total})`);
} else {
// Fallback for different response structures
this.vendors = response.items || response || [];
this.pagination.total = this.vendors.length;
this.pagination.pages = Math.ceil(this.vendors.length / this.pagination.per_page);
vendorsLog.info(`Vendors loaded in ${duration}ms`, {
count: this.vendors.length,
hasVendors: this.vendors.length > 0
});
}
if (this.vendors.length > 0) {
vendorsLog.debug('First vendor:', this.vendors[0]);
}
} catch (error) {
window.LogConfig.logError(error, 'Load Vendors');
this.error = error.message || 'Failed to load vendors';
Utils.showToast('Failed to load vendors', 'error');
} finally {
this.loading = false;
}
},
// Load statistics
async loadStats() {
vendorsLog.info('Loading vendor statistics...');
try {
const url = '/admin/vendors/stats';
window.LogConfig.logApiCall('GET', url, null, 'request');
const startTime = performance.now();
const response = await apiClient.get(url);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('GET', url, response, 'response');
window.LogConfig.logPerformance('Load Vendor Stats', duration);
this.stats = response;
vendorsLog.info(`Stats loaded in ${duration}ms`, this.stats);
} catch (error) {
window.LogConfig.logError(error, 'Load Vendor Stats');
// Don't show error toast for stats, just log it
}
},
// Pagination: Go to specific page
goToPage(pageNum) {
if (pageNum === '...' || pageNum < 1 || pageNum > this.totalPages) {
return;
}
vendorsLog.info('Going to page:', pageNum);
this.pagination.page = pageNum;
this.loadVendors();
},
// Pagination: Go to next page
nextPage() {
if (this.pagination.page < this.totalPages) {
vendorsLog.info('Going to next page');
this.pagination.page++;
this.loadVendors();
}
},
// Pagination: Go to previous page
previousPage() {
if (this.pagination.page > 1) {
vendorsLog.info('Going to previous page');
this.pagination.page--;
this.loadVendors();
}
},
// Format date (matches dashboard pattern)
formatDate(dateString) {
if (!dateString) {
vendorsLog.debug('formatDate called with empty dateString');
return '-';
}
const formatted = Utils.formatDate(dateString);
vendorsLog.debug(`Date formatted: ${dateString} -> ${formatted}`);
return formatted;
},
// View vendor details
viewVendor(vendorCode) {
vendorsLog.info('Navigating to vendor details:', vendorCode);
const url = `/admin/vendors/${vendorCode}`;
vendorsLog.debug('Navigation URL:', url);
window.location.href = url;
},
// Edit vendor
editVendor(vendorCode) {
vendorsLog.info('Navigating to vendor edit:', vendorCode);
const url = `/admin/vendors/${vendorCode}/edit`;
vendorsLog.debug('Navigation URL:', url);
window.location.href = url;
},
// Delete vendor
async deleteVendor(vendor) {
vendorsLog.info('Delete vendor requested:', vendor.vendor_code);
if (!confirm(`Are you sure you want to delete vendor "${vendor.name}"?\n\nThis action cannot be undone.`)) {
vendorsLog.info('Delete cancelled by user');
return;
}
try {
const url = `/admin/vendors/${vendor.vendor_code}`;
window.LogConfig.logApiCall('DELETE', url, null, 'request');
vendorsLog.info('Deleting vendor:', vendor.vendor_code);
await apiClient.delete(url);
window.LogConfig.logApiCall('DELETE', url, null, 'response');
Utils.showToast('Vendor deleted successfully', 'success');
vendorsLog.info('Vendor deleted successfully');
// Reload data
await this.loadVendors();
await this.loadStats();
} catch (error) {
window.LogConfig.logError(error, 'Delete Vendor');
Utils.showToast(error.message || 'Failed to delete vendor', 'error');
}
},
// Refresh vendors list
async refresh() {
vendorsLog.info('=== VENDORS REFRESH TRIGGERED ===');
vendorsLog.group('Refreshing vendors data');
await this.loadVendors();
await this.loadStats();
vendorsLog.groupEnd();
Utils.showToast('Vendors list refreshed', 'success');
vendorsLog.info('=== VENDORS REFRESH COMPLETE ===');
}
};
}
vendorsLog.info('Vendors module loaded');