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:
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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}/`;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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',
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -1,307 +0,0 @@
|
||||
// static/shared/js/media-picker.js
|
||||
/**
|
||||
* Media Picker Helper Functions
|
||||
*
|
||||
* Provides Alpine.js mixin for media library picker functionality.
|
||||
* Used in product create/edit forms to select images from vendor's media library.
|
||||
*
|
||||
* Usage:
|
||||
* In your Alpine component:
|
||||
* return {
|
||||
* ...mediaPickerMixin(vendorIdGetter, multiSelect),
|
||||
* // your other data/methods
|
||||
* }
|
||||
*/
|
||||
|
||||
// Use centralized logger
|
||||
const mediaPickerLog = window.LogConfig.loggers.mediaPicker ||
|
||||
window.LogConfig.createLogger('mediaPicker', false);
|
||||
|
||||
/**
|
||||
* Create media picker mixin for Alpine.js components
|
||||
*
|
||||
* @param {Function} vendorIdGetter - Function that returns the current vendor ID
|
||||
* @param {boolean} multiSelect - Allow selecting multiple images
|
||||
* @returns {Object} Alpine.js mixin object
|
||||
*/
|
||||
function mediaPickerMixin(vendorIdGetter, multiSelect = false) {
|
||||
return {
|
||||
// Modal visibility
|
||||
showMediaPicker: false,
|
||||
showMediaPickerAdditional: false,
|
||||
|
||||
// Picker state
|
||||
mediaPickerState: {
|
||||
loading: false,
|
||||
uploading: false,
|
||||
media: [],
|
||||
selected: [],
|
||||
total: 0,
|
||||
skip: 0,
|
||||
limit: 24,
|
||||
search: '',
|
||||
},
|
||||
|
||||
// Which picker is active (main or additional)
|
||||
activePickerTarget: 'main',
|
||||
|
||||
/**
|
||||
* Open media picker for main image
|
||||
*/
|
||||
openMediaPickerMain() {
|
||||
this.activePickerTarget = 'main';
|
||||
this.mediaPickerState.selected = [];
|
||||
this.showMediaPicker = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Open media picker for additional images
|
||||
*/
|
||||
openMediaPickerAdditional() {
|
||||
this.activePickerTarget = 'additional';
|
||||
this.mediaPickerState.selected = [];
|
||||
this.showMediaPickerAdditional = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Load media library from API
|
||||
*/
|
||||
async loadMediaLibrary() {
|
||||
const vendorId = typeof vendorIdGetter === 'function' ? vendorIdGetter() : vendorIdGetter;
|
||||
|
||||
if (!vendorId) {
|
||||
mediaPickerLog.warn('No vendor ID available');
|
||||
return;
|
||||
}
|
||||
|
||||
this.mediaPickerState.loading = true;
|
||||
this.mediaPickerState.skip = 0;
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
skip: '0',
|
||||
limit: this.mediaPickerState.limit.toString(),
|
||||
media_type: 'image',
|
||||
});
|
||||
|
||||
if (this.mediaPickerState.search) {
|
||||
params.append('search', this.mediaPickerState.search);
|
||||
}
|
||||
|
||||
const response = await apiClient.get(
|
||||
`/admin/media/vendors/${vendorId}?${params.toString()}`
|
||||
);
|
||||
|
||||
this.mediaPickerState.media = response.media || [];
|
||||
this.mediaPickerState.total = response.total || 0;
|
||||
} catch (error) {
|
||||
mediaPickerLog.error('Failed to load media library:', error);
|
||||
window.dispatchEvent(new CustomEvent('toast', {
|
||||
detail: { message: 'Failed to load media library', type: 'error' }
|
||||
}));
|
||||
} finally {
|
||||
this.mediaPickerState.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load more media (pagination)
|
||||
*/
|
||||
async loadMoreMedia() {
|
||||
const vendorId = typeof vendorIdGetter === 'function' ? vendorIdGetter() : vendorIdGetter;
|
||||
|
||||
if (!vendorId) return;
|
||||
|
||||
this.mediaPickerState.loading = true;
|
||||
this.mediaPickerState.skip += this.mediaPickerState.limit;
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
skip: this.mediaPickerState.skip.toString(),
|
||||
limit: this.mediaPickerState.limit.toString(),
|
||||
media_type: 'image',
|
||||
});
|
||||
|
||||
if (this.mediaPickerState.search) {
|
||||
params.append('search', this.mediaPickerState.search);
|
||||
}
|
||||
|
||||
const response = await apiClient.get(
|
||||
`/admin/media/vendors/${vendorId}?${params.toString()}`
|
||||
);
|
||||
|
||||
this.mediaPickerState.media = [
|
||||
...this.mediaPickerState.media,
|
||||
...(response.media || [])
|
||||
];
|
||||
} catch (error) {
|
||||
mediaPickerLog.error('Failed to load more media:', error);
|
||||
} finally {
|
||||
this.mediaPickerState.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Upload a new media file
|
||||
*/
|
||||
async uploadMediaFile(event) {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const vendorId = typeof vendorIdGetter === 'function' ? vendorIdGetter() : vendorIdGetter;
|
||||
|
||||
if (!vendorId) {
|
||||
window.dispatchEvent(new CustomEvent('toast', {
|
||||
detail: { message: 'Please select a vendor first', type: 'error' }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
if (!file.type.startsWith('image/')) {
|
||||
window.dispatchEvent(new CustomEvent('toast', {
|
||||
detail: { message: 'Please select an image file', type: 'error' }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size (10MB max)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
window.dispatchEvent(new CustomEvent('toast', {
|
||||
detail: { message: 'Image must be less than 10MB', type: 'error' }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
this.mediaPickerState.uploading = true;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await apiClient.postFormData(
|
||||
`/admin/media/vendors/${vendorId}/upload?folder=products`,
|
||||
formData
|
||||
);
|
||||
|
||||
if (response.success && response.media) {
|
||||
// Add to beginning of media list
|
||||
this.mediaPickerState.media.unshift(response.media);
|
||||
this.mediaPickerState.total++;
|
||||
|
||||
// Auto-select the uploaded image
|
||||
this.toggleMediaSelection(response.media);
|
||||
|
||||
window.dispatchEvent(new CustomEvent('toast', {
|
||||
detail: { message: 'Image uploaded successfully', type: 'success' }
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
mediaPickerLog.error('Failed to upload image:', error);
|
||||
window.dispatchEvent(new CustomEvent('toast', {
|
||||
detail: { message: error.message || 'Failed to upload image', type: 'error' }
|
||||
}));
|
||||
} finally {
|
||||
this.mediaPickerState.uploading = false;
|
||||
// Clear the file input
|
||||
event.target.value = '';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle media selection
|
||||
*/
|
||||
toggleMediaSelection(media) {
|
||||
const index = this.mediaPickerState.selected.findIndex(m => m.id === media.id);
|
||||
|
||||
if (index > -1) {
|
||||
// Deselect
|
||||
this.mediaPickerState.selected.splice(index, 1);
|
||||
} else {
|
||||
if (multiSelect) {
|
||||
// Multi-select: add to selection
|
||||
this.mediaPickerState.selected.push(media);
|
||||
} else {
|
||||
// Single-select: replace selection
|
||||
this.mediaPickerState.selected = [media];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if media is selected
|
||||
*/
|
||||
isMediaSelected(mediaId) {
|
||||
return this.mediaPickerState.selected.some(m => m.id === mediaId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Confirm selection and call the appropriate callback
|
||||
*/
|
||||
confirmMediaSelection() {
|
||||
const selected = this.mediaPickerState.selected;
|
||||
|
||||
if (selected.length === 0) return;
|
||||
|
||||
if (this.activePickerTarget === 'main') {
|
||||
// Main image: use first selected
|
||||
this.setMainImage(selected[0]);
|
||||
this.showMediaPicker = false;
|
||||
} else {
|
||||
// Additional images: add all selected
|
||||
this.addAdditionalImages(selected);
|
||||
this.showMediaPickerAdditional = false;
|
||||
}
|
||||
|
||||
// Clear selection
|
||||
this.mediaPickerState.selected = [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the main image (override in your component)
|
||||
*/
|
||||
setMainImage(media) {
|
||||
if (this.form) {
|
||||
this.form.primary_image_url = media.url;
|
||||
}
|
||||
mediaPickerLog.info('Main image set:', media.url);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add additional images (override in your component)
|
||||
*/
|
||||
addAdditionalImages(mediaList) {
|
||||
if (this.form && Array.isArray(this.form.additional_images)) {
|
||||
const newUrls = mediaList.map(m => m.url);
|
||||
this.form.additional_images = [
|
||||
...this.form.additional_images,
|
||||
...newUrls
|
||||
];
|
||||
}
|
||||
mediaPickerLog.info('Additional images added:', mediaList.map(m => m.url));
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove an additional image by index
|
||||
*/
|
||||
removeAdditionalImage(index) {
|
||||
if (this.form && Array.isArray(this.form.additional_images)) {
|
||||
this.form.additional_images.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the main image
|
||||
*/
|
||||
clearMainImage() {
|
||||
if (this.form) {
|
||||
this.form.primary_image_url = '';
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Export for module systems
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { mediaPickerMixin };
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
// static/shared/js/vendor-selector.js
|
||||
/**
|
||||
* Shared Vendor Selector Module
|
||||
* =============================
|
||||
* Provides a reusable Tom Select-based vendor autocomplete component.
|
||||
*
|
||||
* Features:
|
||||
* - Async search with debouncing (150ms)
|
||||
* - Searches by vendor name and code
|
||||
* - Dark mode support
|
||||
* - Caches recent searches
|
||||
* - Graceful fallback if Tom Select not available
|
||||
*
|
||||
* Usage:
|
||||
* // In Alpine.js component init():
|
||||
* this.$nextTick(() => {
|
||||
* this.vendorSelector = initVendorSelector(this.$refs.vendorSelect, {
|
||||
* onSelect: (vendor) => this.handleVendorSelect(vendor),
|
||||
* onClear: () => this.handleVendorClear(),
|
||||
* minChars: 2,
|
||||
* maxOptions: 50
|
||||
* });
|
||||
* });
|
||||
*
|
||||
* // To programmatically set a value:
|
||||
* this.vendorSelector.setValue(vendorId);
|
||||
*
|
||||
* // To clear:
|
||||
* this.vendorSelector.clear();
|
||||
*/
|
||||
|
||||
const vendorSelectorLog = window.LogConfig?.loggers?.vendorSelector ||
|
||||
window.LogConfig?.createLogger?.('vendorSelector', false) ||
|
||||
{ info: console.log, warn: console.warn, error: console.error }; // noqa: js-001 - fallback if logger not ready
|
||||
|
||||
/**
|
||||
* Check if Tom Select is available, with retry logic
|
||||
* @param {Function} callback - Called when Tom Select is available
|
||||
* @param {number} maxRetries - Maximum retry attempts
|
||||
* @param {number} retryDelay - Delay between retries in ms
|
||||
*/
|
||||
function waitForTomSelect(callback, maxRetries = 20, retryDelay = 100) {
|
||||
let retries = 0;
|
||||
|
||||
function check() {
|
||||
if (typeof TomSelect !== 'undefined') {
|
||||
callback();
|
||||
} else if (retries < maxRetries) {
|
||||
retries++;
|
||||
vendorSelectorLog.info(`Waiting for TomSelect... (attempt ${retries}/${maxRetries})`);
|
||||
setTimeout(check, retryDelay);
|
||||
} else {
|
||||
vendorSelectorLog.error('TomSelect not available after maximum retries');
|
||||
}
|
||||
}
|
||||
|
||||
check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a vendor selector on the given element
|
||||
* @param {HTMLElement} selectElement - The select element to enhance
|
||||
* @param {Object} options - Configuration options
|
||||
* @param {Function} options.onSelect - Callback when vendor is selected (receives vendor object)
|
||||
* @param {Function} options.onClear - Callback when selection is cleared
|
||||
* @param {number} options.minChars - Minimum characters before search (default: 2)
|
||||
* @param {number} options.maxOptions - Maximum options to show (default: 50)
|
||||
* @param {string} options.placeholder - Placeholder text
|
||||
* @param {string} options.apiEndpoint - API endpoint for search (default: '/admin/vendors')
|
||||
* @returns {Object} Controller object with setValue() and clear() methods
|
||||
*/
|
||||
function initVendorSelector(selectElement, options = {}) {
|
||||
if (!selectElement) {
|
||||
vendorSelectorLog.error('Vendor selector element not provided');
|
||||
return null;
|
||||
}
|
||||
|
||||
const config = {
|
||||
minChars: options.minChars || 2,
|
||||
maxOptions: options.maxOptions || 50,
|
||||
placeholder: options.placeholder || selectElement.getAttribute('placeholder') || 'Search vendor by name or code...',
|
||||
apiEndpoint: options.apiEndpoint || '/admin/vendors', // Note: apiClient adds /api/v1 prefix
|
||||
onSelect: options.onSelect || (() => {}),
|
||||
onClear: options.onClear || (() => {})
|
||||
};
|
||||
|
||||
let tomSelectInstance = null;
|
||||
|
||||
// Controller object returned to caller
|
||||
const controller = {
|
||||
/**
|
||||
* Set the selected vendor by ID
|
||||
* @param {number} vendorId - Vendor ID to select
|
||||
* @param {Object} vendorData - Optional vendor data to avoid API call
|
||||
*/
|
||||
setValue: async function(vendorId, vendorData = null) {
|
||||
if (!tomSelectInstance) return;
|
||||
|
||||
if (vendorData) {
|
||||
// Add option and set value
|
||||
tomSelectInstance.addOption({
|
||||
id: vendorData.id,
|
||||
name: vendorData.name,
|
||||
vendor_code: vendorData.vendor_code
|
||||
});
|
||||
tomSelectInstance.setValue(vendorData.id, true);
|
||||
} else if (vendorId) {
|
||||
// Fetch vendor data and set
|
||||
try {
|
||||
const response = await apiClient.get(`${config.apiEndpoint}/${vendorId}`);
|
||||
tomSelectInstance.addOption({
|
||||
id: response.id,
|
||||
name: response.name,
|
||||
vendor_code: response.vendor_code
|
||||
});
|
||||
tomSelectInstance.setValue(response.id, true);
|
||||
} catch (error) {
|
||||
vendorSelectorLog.error('Failed to load vendor:', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the selection
|
||||
*/
|
||||
clear: function() {
|
||||
if (tomSelectInstance) {
|
||||
tomSelectInstance.clear();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the Tom Select instance
|
||||
*/
|
||||
getInstance: function() {
|
||||
return tomSelectInstance;
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroy the Tom Select instance
|
||||
*/
|
||||
destroy: function() {
|
||||
if (tomSelectInstance) {
|
||||
tomSelectInstance.destroy();
|
||||
tomSelectInstance = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize Tom Select when available
|
||||
waitForTomSelect(() => {
|
||||
vendorSelectorLog.info('Initializing vendor selector');
|
||||
|
||||
tomSelectInstance = new TomSelect(selectElement, {
|
||||
valueField: 'id',
|
||||
labelField: 'name',
|
||||
searchField: ['name', 'vendor_code'],
|
||||
maxOptions: config.maxOptions,
|
||||
placeholder: config.placeholder,
|
||||
|
||||
// Async search with debouncing
|
||||
load: async function(query, callback) {
|
||||
if (query.length < config.minChars) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(
|
||||
`${config.apiEndpoint}?search=${encodeURIComponent(query)}&limit=${config.maxOptions}`
|
||||
);
|
||||
|
||||
const vendors = (response.vendors || []).map(v => ({
|
||||
id: v.id,
|
||||
name: v.name,
|
||||
vendor_code: v.vendor_code
|
||||
}));
|
||||
|
||||
vendorSelectorLog.info(`Found ${vendors.length} vendors for "${query}"`);
|
||||
callback(vendors);
|
||||
} catch (error) {
|
||||
vendorSelectorLog.error('Vendor search failed:', error);
|
||||
callback([]);
|
||||
}
|
||||
},
|
||||
|
||||
// Custom rendering
|
||||
render: {
|
||||
option: function(data, escape) {
|
||||
return `<div class="flex justify-between items-center py-1">
|
||||
<span class="font-medium">${escape(data.name)}</span>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500 ml-2 font-mono">${escape(data.vendor_code)}</span>
|
||||
</div>`;
|
||||
},
|
||||
item: function(data, escape) {
|
||||
return `<div class="flex items-center gap-2">
|
||||
<span>${escape(data.name)}</span>
|
||||
<span class="text-xs text-gray-400 font-mono">(${escape(data.vendor_code)})</span>
|
||||
</div>`;
|
||||
},
|
||||
no_results: function() {
|
||||
return '<div class="no-results py-2 px-3 text-gray-500 dark:text-gray-400">No vendors found</div>';
|
||||
},
|
||||
loading: function() {
|
||||
return '<div class="loading py-2 px-3 text-gray-500 dark:text-gray-400">Searching...</div>';
|
||||
}
|
||||
},
|
||||
|
||||
// Event handlers
|
||||
onChange: function(value) {
|
||||
if (value) {
|
||||
const selectedOption = this.options[value];
|
||||
if (selectedOption) {
|
||||
vendorSelectorLog.info('Vendor selected:', selectedOption);
|
||||
config.onSelect({
|
||||
id: parseInt(value),
|
||||
name: selectedOption.name,
|
||||
vendor_code: selectedOption.vendor_code
|
||||
});
|
||||
}
|
||||
} else {
|
||||
vendorSelectorLog.info('Vendor selection cleared');
|
||||
config.onClear();
|
||||
}
|
||||
},
|
||||
|
||||
// Performance settings
|
||||
loadThrottle: 150, // Debounce search requests
|
||||
closeAfterSelect: true,
|
||||
hideSelected: false,
|
||||
persist: true, // Cache options
|
||||
createOnBlur: false,
|
||||
create: false
|
||||
});
|
||||
|
||||
vendorSelectorLog.info('Vendor selector initialized');
|
||||
});
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
// Export to window for global access
|
||||
window.initVendorSelector = initVendorSelector;
|
||||
@@ -1,295 +0,0 @@
|
||||
// static/storefront/js/storefront-layout.js
|
||||
/**
|
||||
* Shop Layout Component
|
||||
* Provides base functionality for vendor shop pages
|
||||
* Works with vendor-specific themes
|
||||
*/
|
||||
|
||||
const shopLog = {
|
||||
info: (...args) => console.info('🛒 [SHOP]', ...args),
|
||||
warn: (...args) => console.warn('⚠️ [SHOP]', ...args),
|
||||
error: (...args) => console.error('❌ [SHOP]', ...args),
|
||||
debug: (...args) => console.log('🔍 [SHOP]', ...args)
|
||||
};
|
||||
|
||||
/**
|
||||
* Shop Layout Data
|
||||
* Base Alpine.js component for shop pages
|
||||
*/
|
||||
function shopLayoutData() {
|
||||
return {
|
||||
// Theme state
|
||||
dark: localStorage.getItem('shop-theme') === 'dark',
|
||||
|
||||
// UI state
|
||||
mobileMenuOpen: false,
|
||||
searchOpen: false,
|
||||
loading: false,
|
||||
cartCount: 0,
|
||||
|
||||
// Cart state
|
||||
cart: [],
|
||||
sessionId: null,
|
||||
|
||||
// Initialize
|
||||
init() {
|
||||
shopLog.info('Shop layout initializing...');
|
||||
|
||||
// Get or create session ID
|
||||
this.sessionId = this.getOrCreateSessionId();
|
||||
shopLog.debug('Session ID:', this.sessionId);
|
||||
|
||||
// Load cart from localStorage
|
||||
this.loadCart();
|
||||
|
||||
// Listen for cart updates
|
||||
window.addEventListener('cart-updated', () => {
|
||||
this.loadCart();
|
||||
});
|
||||
|
||||
shopLog.info('Shop layout initialized');
|
||||
},
|
||||
|
||||
// Get or create session ID
|
||||
getOrCreateSessionId() {
|
||||
let sessionId = localStorage.getItem('cart_session_id');
|
||||
if (!sessionId) {
|
||||
sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
localStorage.setItem('cart_session_id', sessionId);
|
||||
shopLog.info('Created new session ID:', sessionId);
|
||||
}
|
||||
return sessionId;
|
||||
},
|
||||
|
||||
// Theme management
|
||||
toggleTheme() {
|
||||
this.dark = !this.dark;
|
||||
localStorage.setItem('shop-theme', this.dark ? 'dark' : 'light');
|
||||
shopLog.debug('Theme toggled:', this.dark ? 'dark' : 'light');
|
||||
},
|
||||
|
||||
// Mobile menu
|
||||
toggleMobileMenu() {
|
||||
this.mobileMenuOpen = !this.mobileMenuOpen;
|
||||
if (this.mobileMenuOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
},
|
||||
|
||||
closeMobileMenu() {
|
||||
this.mobileMenuOpen = false;
|
||||
document.body.style.overflow = '';
|
||||
},
|
||||
|
||||
// Search
|
||||
openSearch() {
|
||||
this.searchOpen = true;
|
||||
shopLog.debug('Search opened');
|
||||
// Focus search input after a short delay
|
||||
setTimeout(() => {
|
||||
const input = document.querySelector('#search-input');
|
||||
if (input) input.focus();
|
||||
}, 100);
|
||||
},
|
||||
|
||||
closeSearch() {
|
||||
this.searchOpen = false;
|
||||
},
|
||||
|
||||
// Cart management
|
||||
loadCart() {
|
||||
try {
|
||||
const cartData = localStorage.getItem('shop-cart');
|
||||
if (cartData) {
|
||||
this.cart = JSON.parse(cartData);
|
||||
this.cartCount = this.cart.reduce((sum, item) => sum + item.quantity, 0);
|
||||
}
|
||||
} catch (error) {
|
||||
shopLog.error('Failed to load cart:', error);
|
||||
this.cart = [];
|
||||
this.cartCount = 0;
|
||||
}
|
||||
},
|
||||
|
||||
addToCart(product, quantity = 1) {
|
||||
shopLog.info('Adding to cart:', product.name, 'x', quantity);
|
||||
|
||||
// Find existing item
|
||||
const existingIndex = this.cart.findIndex(item => item.id === product.id);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// Update quantity
|
||||
this.cart[existingIndex].quantity += quantity;
|
||||
} else {
|
||||
// Add new item
|
||||
this.cart.push({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
image: product.image,
|
||||
quantity: quantity
|
||||
});
|
||||
}
|
||||
|
||||
// Save and update
|
||||
this.saveCart();
|
||||
this.showToast(`${product.name} added to cart`, 'success');
|
||||
},
|
||||
|
||||
updateCartItem(productId, quantity) {
|
||||
const index = this.cart.findIndex(item => item.id === productId);
|
||||
if (index !== -1) {
|
||||
if (quantity <= 0) {
|
||||
this.cart.splice(index, 1);
|
||||
} else {
|
||||
this.cart[index].quantity = quantity;
|
||||
}
|
||||
this.saveCart();
|
||||
}
|
||||
},
|
||||
|
||||
removeFromCart(productId) {
|
||||
this.cart = this.cart.filter(item => item.id !== productId);
|
||||
this.saveCart();
|
||||
this.showToast('Item removed from cart', 'info');
|
||||
},
|
||||
|
||||
clearCart() {
|
||||
this.cart = [];
|
||||
this.saveCart();
|
||||
this.showToast('Cart cleared', 'info');
|
||||
},
|
||||
|
||||
saveCart() {
|
||||
try {
|
||||
localStorage.setItem('shop-cart', JSON.stringify(this.cart));
|
||||
this.cartCount = this.cart.reduce((sum, item) => sum + item.quantity, 0);
|
||||
|
||||
// Dispatch custom event
|
||||
window.dispatchEvent(new CustomEvent('cart-updated'));
|
||||
|
||||
shopLog.debug('Cart saved:', this.cart.length, 'items');
|
||||
} catch (error) {
|
||||
shopLog.error('Failed to save cart:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// Get cart total
|
||||
get cartTotal() {
|
||||
return this.cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
},
|
||||
|
||||
// Toast notifications
|
||||
showToast(message, type = 'info') {
|
||||
const container = document.getElementById('toast-container');
|
||||
if (!container) return;
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type} transform transition-all duration-300 mb-2`;
|
||||
|
||||
// Color based on type
|
||||
const colors = {
|
||||
success: 'bg-green-500',
|
||||
error: 'bg-red-500',
|
||||
warning: 'bg-yellow-500',
|
||||
info: 'bg-blue-500'
|
||||
};
|
||||
|
||||
// noqa: SEC-015 - message is application-controlled
|
||||
toast.innerHTML = `
|
||||
<div class="${colors[type]} text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-3">
|
||||
<span>${message}</span>
|
||||
<button onclick="this.parentElement.parentElement.remove()"
|
||||
class="ml-4 hover:opacity-75">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
// Auto-remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
},
|
||||
|
||||
// Format currency using configured locale
|
||||
formatPrice(amount) {
|
||||
if (!amount && amount !== 0) return '';
|
||||
const locale = window.SHOP_CONFIG?.locale || 'fr-LU';
|
||||
const currency = window.SHOP_CONFIG?.currency || 'EUR';
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: currency
|
||||
}).format(amount);
|
||||
},
|
||||
|
||||
// Format date
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Make available globally
|
||||
window.shopLayoutData = shopLayoutData;
|
||||
|
||||
/**
|
||||
* Language Selector Component
|
||||
* Alpine.js component for language switching
|
||||
*/
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.languageSelector = languageSelector;
|
||||
|
||||
shopLog.info('Shop layout module loaded');
|
||||
129
static/vendor/js/dashboard.js
vendored
129
static/vendor/js/dashboard.js
vendored
@@ -1,129 +0,0 @@
|
||||
// app/static/vendor/js/dashboard.js
|
||||
/**
|
||||
* Vendor dashboard page logic
|
||||
*/
|
||||
|
||||
// ✅ Use centralized logger (with safe fallback)
|
||||
const vendorDashLog = window.LogConfig.loggers.dashboard ||
|
||||
window.LogConfig.createLogger('dashboard', false);
|
||||
|
||||
vendorDashLog.info('Loading...');
|
||||
vendorDashLog.info('[VENDOR DASHBOARD] data function exists?', typeof data);
|
||||
|
||||
function vendorDashboard() {
|
||||
vendorDashLog.info('[VENDOR DASHBOARD] vendorDashboard() called');
|
||||
vendorDashLog.info('[VENDOR DASHBOARD] data function exists inside?', typeof data);
|
||||
|
||||
return {
|
||||
// ✅ Inherit base layout state (includes vendorCode, dark mode, menu states)
|
||||
...data(),
|
||||
|
||||
// ✅ Set page identifier
|
||||
currentPage: 'dashboard',
|
||||
|
||||
loading: false,
|
||||
error: '',
|
||||
stats: {
|
||||
products_count: 0,
|
||||
orders_count: 0,
|
||||
customers_count: 0,
|
||||
revenue: 0
|
||||
},
|
||||
recentOrders: [],
|
||||
recentProducts: [],
|
||||
|
||||
async init() {
|
||||
// Guard against multiple initialization
|
||||
if (window._vendorDashboardInitialized) {
|
||||
return;
|
||||
}
|
||||
window._vendorDashboardInitialized = true;
|
||||
|
||||
try {
|
||||
// IMPORTANT: Call parent init first to set vendorCode from URL
|
||||
const parentInit = data().init;
|
||||
if (parentInit) {
|
||||
await parentInit.call(this);
|
||||
}
|
||||
|
||||
await this.loadDashboardData();
|
||||
} catch (error) {
|
||||
vendorDashLog.error('Failed to initialize dashboard:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async loadDashboardData() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
// Load stats
|
||||
// NOTE: apiClient prepends /api/v1, and vendor context middleware handles vendor detection
|
||||
// So we just call /vendor/dashboard/stats → becomes /api/v1/vendor/dashboard/stats
|
||||
const statsResponse = await apiClient.get(
|
||||
`/vendor/dashboard/stats`
|
||||
);
|
||||
|
||||
// Map API response to stats (similar to admin dashboard pattern)
|
||||
this.stats = {
|
||||
products_count: statsResponse.products?.total || 0,
|
||||
orders_count: statsResponse.orders?.total || 0,
|
||||
customers_count: statsResponse.customers?.total || 0,
|
||||
revenue: statsResponse.revenue?.total || 0
|
||||
};
|
||||
|
||||
// Load recent orders
|
||||
const ordersResponse = await apiClient.get(
|
||||
`/vendor/orders?limit=5&sort=created_at:desc`
|
||||
);
|
||||
this.recentOrders = ordersResponse.items || [];
|
||||
|
||||
// Load recent products
|
||||
const productsResponse = await apiClient.get(
|
||||
`/vendor/products?limit=5&sort=created_at:desc`
|
||||
);
|
||||
this.recentProducts = productsResponse.items || [];
|
||||
|
||||
vendorDashLog.info('Dashboard data loaded', {
|
||||
stats: this.stats,
|
||||
orders: this.recentOrders.length,
|
||||
products: this.recentProducts.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
vendorDashLog.error('Failed to load dashboard data', error);
|
||||
this.error = 'Failed to load dashboard data. Please try refreshing the page.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async refresh() {
|
||||
try {
|
||||
await this.loadDashboardData();
|
||||
} catch (error) {
|
||||
vendorDashLog.error('Failed to refresh dashboard:', error);
|
||||
}
|
||||
},
|
||||
|
||||
formatCurrency(amount) {
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
const currency = window.VENDOR_CONFIG?.currency || 'EUR';
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: currency
|
||||
}).format(amount || 0);
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
return date.toLocaleDateString(locale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
270
static/vendor/js/init-alpine.js
vendored
270
static/vendor/js/init-alpine.js
vendored
@@ -1,270 +0,0 @@
|
||||
// app/static/vendor/js/init-alpine.js
|
||||
/**
|
||||
* Alpine.js initialization for vendor pages
|
||||
* Provides common data and methods for all vendor pages
|
||||
*/
|
||||
|
||||
// ✅ Use centralized logger
|
||||
const vendorLog = window.LogConfig.log;
|
||||
|
||||
console.log('[VENDOR INIT-ALPINE] Loading...');
|
||||
|
||||
// Sidebar section state persistence
|
||||
const VENDOR_SIDEBAR_STORAGE_KEY = 'vendor_sidebar_sections';
|
||||
|
||||
function getVendorSidebarSectionsFromStorage() {
|
||||
try {
|
||||
const stored = localStorage.getItem(VENDOR_SIDEBAR_STORAGE_KEY);
|
||||
if (stored) {
|
||||
return JSON.parse(stored);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[VENDOR INIT-ALPINE] Failed to load sidebar state from localStorage:', e);
|
||||
}
|
||||
// Default: all sections open
|
||||
return {
|
||||
products: true,
|
||||
sales: true,
|
||||
customers: true,
|
||||
shop: true,
|
||||
account: true
|
||||
};
|
||||
}
|
||||
|
||||
function saveVendorSidebarSectionsToStorage(sections) {
|
||||
try {
|
||||
localStorage.setItem(VENDOR_SIDEBAR_STORAGE_KEY, JSON.stringify(sections));
|
||||
} catch (e) {
|
||||
console.warn('[VENDOR INIT-ALPINE] Failed to save sidebar state to localStorage:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function data() {
|
||||
console.log('[VENDOR INIT-ALPINE] data() function called');
|
||||
return {
|
||||
dark: false,
|
||||
isSideMenuOpen: false,
|
||||
isNotificationsMenuOpen: false,
|
||||
isProfileMenuOpen: false,
|
||||
currentPage: '',
|
||||
currentUser: {},
|
||||
vendor: null,
|
||||
vendorCode: null,
|
||||
|
||||
// Sidebar collapsible sections state
|
||||
openSections: getVendorSidebarSectionsFromStorage(),
|
||||
|
||||
init() {
|
||||
// Set current page from URL
|
||||
const path = window.location.pathname;
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
this.currentPage = segments[segments.length - 1] || 'dashboard';
|
||||
|
||||
// Get vendor code from URL
|
||||
if (segments[0] === 'vendor' && segments[1]) {
|
||||
this.vendorCode = segments[1];
|
||||
}
|
||||
|
||||
// Load user from localStorage
|
||||
const user = localStorage.getItem('currentUser');
|
||||
if (user) {
|
||||
this.currentUser = JSON.parse(user);
|
||||
}
|
||||
|
||||
// Load theme preference
|
||||
const theme = localStorage.getItem('theme');
|
||||
if (theme === 'dark') {
|
||||
this.dark = true;
|
||||
}
|
||||
|
||||
// Load vendor info
|
||||
this.loadVendorInfo();
|
||||
|
||||
// Save last visited page (for redirect after login)
|
||||
// Exclude login, logout, onboarding, error pages
|
||||
if (!path.includes('/login') &&
|
||||
!path.includes('/logout') &&
|
||||
!path.includes('/onboarding') &&
|
||||
!path.includes('/errors/')) {
|
||||
try {
|
||||
localStorage.setItem('vendor_last_visited_page', path);
|
||||
} catch (e) {
|
||||
// Ignore storage errors
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async loadVendorInfo() {
|
||||
if (!this.vendorCode) return;
|
||||
|
||||
try {
|
||||
// apiClient prepends /api/v1, so /vendor/info/{code} → /api/v1/vendor/info/{code}
|
||||
const response = await apiClient.get(`/vendor/info/${this.vendorCode}`);
|
||||
this.vendor = response;
|
||||
vendorLog.debug('Vendor info loaded', this.vendor);
|
||||
} catch (error) {
|
||||
vendorLog.error('Failed to load vendor info', error);
|
||||
}
|
||||
},
|
||||
|
||||
toggleSideMenu() {
|
||||
this.isSideMenuOpen = !this.isSideMenuOpen;
|
||||
},
|
||||
|
||||
closeSideMenu() {
|
||||
this.isSideMenuOpen = false;
|
||||
},
|
||||
|
||||
toggleNotificationsMenu() {
|
||||
this.isNotificationsMenuOpen = !this.isNotificationsMenuOpen;
|
||||
if (this.isNotificationsMenuOpen) {
|
||||
this.isProfileMenuOpen = false;
|
||||
}
|
||||
},
|
||||
|
||||
closeNotificationsMenu() {
|
||||
this.isNotificationsMenuOpen = false;
|
||||
},
|
||||
|
||||
toggleProfileMenu() {
|
||||
this.isProfileMenuOpen = !this.isProfileMenuOpen;
|
||||
if (this.isProfileMenuOpen) {
|
||||
this.isNotificationsMenuOpen = false;
|
||||
}
|
||||
},
|
||||
|
||||
closeProfileMenu() {
|
||||
this.isProfileMenuOpen = false;
|
||||
},
|
||||
|
||||
toggleTheme() {
|
||||
this.dark = !this.dark;
|
||||
localStorage.setItem('theme', this.dark ? 'dark' : 'light');
|
||||
},
|
||||
|
||||
// Sidebar section toggle with persistence
|
||||
toggleSection(section) {
|
||||
this.openSections[section] = !this.openSections[section];
|
||||
saveVendorSidebarSectionsToStorage(this.openSections);
|
||||
},
|
||||
|
||||
async handleLogout() {
|
||||
console.log('🚪 Logging out vendor user...');
|
||||
|
||||
try {
|
||||
// Call logout API
|
||||
await apiClient.post('/vendor/auth/logout');
|
||||
console.log('✅ Logout API called successfully');
|
||||
} catch (error) {
|
||||
console.error('⚠️ Logout API error (continuing anyway):', error);
|
||||
} finally {
|
||||
// Clear vendor tokens only (not admin or customer tokens)
|
||||
// Keep vendor_last_visited_page so user returns to same page after login
|
||||
console.log('🧹 Clearing vendor tokens...');
|
||||
localStorage.removeItem('vendor_token');
|
||||
localStorage.removeItem('vendor_user');
|
||||
localStorage.removeItem('currentUser');
|
||||
localStorage.removeItem('vendorCode');
|
||||
// Note: Do NOT use localStorage.clear() - it would clear admin/customer tokens too
|
||||
|
||||
console.log('🔄 Redirecting to login...');
|
||||
window.location.href = `/vendor/${this.vendorCode}/login`;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Language Selector Component
|
||||
* Alpine.js component for language switching in vendor dashboard
|
||||
*/
|
||||
function languageSelector(currentLang, enabledLanguages) {
|
||||
return {
|
||||
isLangOpen: false,
|
||||
currentLang: currentLang || 'fr',
|
||||
languages: enabledLanguages || ['en', 'fr', 'de', 'lb'],
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.languageSelector = languageSelector;
|
||||
|
||||
/**
|
||||
* Email Settings Warning Component
|
||||
* Shows warning banner when vendor email settings are not configured
|
||||
*
|
||||
* Usage in template:
|
||||
* <div x-data="emailSettingsWarning()" x-show="showWarning">...</div>
|
||||
*/
|
||||
function emailSettingsWarning() {
|
||||
return {
|
||||
showWarning: false,
|
||||
loading: true,
|
||||
vendorCode: null,
|
||||
|
||||
async init() {
|
||||
// Get vendor code from URL
|
||||
const path = window.location.pathname;
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
if (segments[0] === 'vendor' && segments[1]) {
|
||||
this.vendorCode = segments[1];
|
||||
}
|
||||
|
||||
// Skip if we're on the settings page (to avoid showing banner on config page)
|
||||
if (path.includes('/settings')) {
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check email settings status
|
||||
await this.checkEmailStatus();
|
||||
},
|
||||
|
||||
async checkEmailStatus() {
|
||||
try {
|
||||
const response = await apiClient.get('/vendor/email-settings/status');
|
||||
// Show warning if not configured
|
||||
this.showWarning = !response.is_configured;
|
||||
} catch (error) {
|
||||
// Don't show warning on error (might be 401, etc.)
|
||||
console.debug('[EmailWarning] Failed to check email status:', error);
|
||||
this.showWarning = false;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.emailSettingsWarning = emailSettingsWarning;
|
||||
174
static/vendor/js/login.js
vendored
174
static/vendor/js/login.js
vendored
@@ -1,174 +0,0 @@
|
||||
// app/static/vendor/js/login.js
|
||||
// noqa: js-003 - Standalone login page without vendor layout
|
||||
// noqa: js-004 - Standalone page has no currentPage sidebar highlight
|
||||
/**
|
||||
* Vendor login page logic
|
||||
*/
|
||||
|
||||
// Create custom logger for vendor login page
|
||||
const vendorLoginLog = window.LogConfig.createLogger('VENDOR-LOGIN');
|
||||
|
||||
function vendorLogin() {
|
||||
return {
|
||||
credentials: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
vendor: null,
|
||||
vendorCode: null,
|
||||
loading: false,
|
||||
checked: false,
|
||||
error: '',
|
||||
success: '',
|
||||
errors: {},
|
||||
dark: false,
|
||||
|
||||
async init() {
|
||||
// Guard against multiple initialization
|
||||
if (window._vendorLoginInitialized) return;
|
||||
window._vendorLoginInitialized = true;
|
||||
|
||||
try {
|
||||
vendorLoginLog.info('=== VENDOR LOGIN PAGE INITIALIZING ===');
|
||||
|
||||
// Load theme
|
||||
const theme = localStorage.getItem('theme');
|
||||
if (theme === 'dark') {
|
||||
this.dark = true;
|
||||
}
|
||||
vendorLoginLog.debug('Dark mode:', this.dark);
|
||||
|
||||
// Get vendor code from URL path
|
||||
const pathSegments = window.location.pathname.split('/').filter(Boolean);
|
||||
if (pathSegments[0] === 'vendor' && pathSegments[1]) {
|
||||
this.vendorCode = pathSegments[1];
|
||||
vendorLoginLog.debug('Vendor code from URL:', this.vendorCode);
|
||||
await this.loadVendor();
|
||||
}
|
||||
this.checked = true;
|
||||
vendorLoginLog.info('=== VENDOR LOGIN PAGE INITIALIZATION COMPLETE ===');
|
||||
} catch (error) {
|
||||
vendorLoginLog.error('Failed to initialize login page:', error);
|
||||
this.checked = true;
|
||||
}
|
||||
},
|
||||
|
||||
async loadVendor() {
|
||||
vendorLoginLog.info('Loading vendor information...');
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await apiClient.get(`/vendor/info/${this.vendorCode}`);
|
||||
this.vendor = response;
|
||||
vendorLoginLog.info('Vendor loaded successfully:', {
|
||||
code: this.vendor.code,
|
||||
name: this.vendor.name
|
||||
});
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Load Vendor');
|
||||
this.error = 'Failed to load vendor information';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async handleLogin() {
|
||||
vendorLoginLog.info('=== VENDOR LOGIN ATTEMPT STARTED ===');
|
||||
this.clearErrors();
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
if (!this.credentials.username) {
|
||||
this.errors.username = 'Username is required';
|
||||
}
|
||||
if (!this.credentials.password) {
|
||||
this.errors.password = 'Password is required';
|
||||
}
|
||||
|
||||
if (Object.keys(this.errors).length > 0) {
|
||||
vendorLoginLog.warn('Validation failed:', this.errors);
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
vendorLoginLog.info('Calling vendor login API...');
|
||||
vendorLoginLog.debug('Username:', this.credentials.username);
|
||||
vendorLoginLog.debug('Vendor code:', this.vendorCode);
|
||||
|
||||
window.LogConfig.logApiCall('POST', '/vendor/auth/login', {
|
||||
username: this.credentials.username,
|
||||
vendor_code: this.vendorCode
|
||||
}, 'request');
|
||||
|
||||
const startTime = performance.now();
|
||||
const response = await apiClient.post('/vendor/auth/login', {
|
||||
email_or_username: this.credentials.username,
|
||||
password: this.credentials.password,
|
||||
vendor_code: this.vendorCode
|
||||
});
|
||||
const duration = performance.now() - startTime;
|
||||
|
||||
window.LogConfig.logApiCall('POST', '/vendor/auth/login', {
|
||||
hasToken: !!response.access_token,
|
||||
user: response.user?.username
|
||||
}, 'response');
|
||||
window.LogConfig.logPerformance('Vendor Login', duration);
|
||||
|
||||
vendorLoginLog.info('Login successful!');
|
||||
vendorLoginLog.debug('Storing authentication data...');
|
||||
|
||||
// Store token with correct key that apiClient expects
|
||||
localStorage.setItem('vendor_token', response.access_token);
|
||||
localStorage.setItem('currentUser', JSON.stringify(response.user));
|
||||
localStorage.setItem('vendorCode', this.vendorCode);
|
||||
vendorLoginLog.debug('Token stored as vendor_token in localStorage');
|
||||
|
||||
this.success = 'Login successful! Redirecting...';
|
||||
|
||||
// Check for last visited page (saved before logout)
|
||||
const lastPage = localStorage.getItem('vendor_last_visited_page');
|
||||
const validLastPage = lastPage &&
|
||||
lastPage.startsWith(`/vendor/${this.vendorCode}/`) &&
|
||||
!lastPage.includes('/login') &&
|
||||
!lastPage.includes('/onboarding');
|
||||
const redirectTo = validLastPage ? lastPage : `/vendor/${this.vendorCode}/dashboard`;
|
||||
|
||||
vendorLoginLog.info('Last visited page:', lastPage);
|
||||
vendorLoginLog.info('Redirecting to:', redirectTo);
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = redirectTo;
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Vendor Login');
|
||||
|
||||
if (error.status === 401) {
|
||||
this.error = 'Invalid username or password';
|
||||
} else if (error.status === 403) {
|
||||
this.error = 'Your account does not have access to this vendor';
|
||||
} else {
|
||||
this.error = error.message || 'Login failed. Please try again.';
|
||||
}
|
||||
vendorLoginLog.info('Error message displayed to user:', this.error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
vendorLoginLog.info('=== VENDOR LOGIN ATTEMPT FINISHED ===');
|
||||
}
|
||||
},
|
||||
|
||||
clearErrors() {
|
||||
vendorLoginLog.debug('Clearing form errors');
|
||||
this.error = '';
|
||||
this.errors = {};
|
||||
},
|
||||
|
||||
toggleDarkMode() {
|
||||
vendorLoginLog.debug('Toggling dark mode...');
|
||||
this.dark = !this.dark;
|
||||
localStorage.setItem('theme', this.dark ? 'dark' : 'light');
|
||||
vendorLoginLog.info('Dark mode:', this.dark ? 'ON' : 'OFF');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
vendorLoginLog.info('Vendor login module loaded');
|
||||
196
static/vendor/js/profile.js
vendored
196
static/vendor/js/profile.js
vendored
@@ -1,196 +0,0 @@
|
||||
// static/vendor/js/profile.js
|
||||
/**
|
||||
* Vendor profile management page logic
|
||||
* Edit vendor business profile and contact information
|
||||
*/
|
||||
|
||||
const vendorProfileLog = window.LogConfig.loggers.vendorProfile ||
|
||||
window.LogConfig.createLogger('vendorProfile', false);
|
||||
|
||||
vendorProfileLog.info('Loading...');
|
||||
|
||||
function vendorProfile() {
|
||||
vendorProfileLog.info('vendorProfile() called');
|
||||
|
||||
return {
|
||||
// Inherit base layout state
|
||||
...data(),
|
||||
|
||||
// Set page identifier
|
||||
currentPage: 'profile',
|
||||
|
||||
// Loading states
|
||||
loading: true,
|
||||
error: '',
|
||||
saving: false,
|
||||
|
||||
// Profile data
|
||||
profile: null,
|
||||
|
||||
// Edit form
|
||||
form: {
|
||||
name: '',
|
||||
contact_email: '',
|
||||
contact_phone: '',
|
||||
website: '',
|
||||
business_address: '',
|
||||
tax_number: '',
|
||||
description: ''
|
||||
},
|
||||
|
||||
// Form validation
|
||||
errors: {},
|
||||
|
||||
// Track if form has changes
|
||||
hasChanges: false,
|
||||
|
||||
async init() {
|
||||
vendorProfileLog.info('Profile init() called');
|
||||
|
||||
// Guard against multiple initialization
|
||||
if (window._vendorProfileInitialized) {
|
||||
vendorProfileLog.warn('Already initialized, skipping');
|
||||
return;
|
||||
}
|
||||
window._vendorProfileInitialized = true;
|
||||
|
||||
// IMPORTANT: Call parent init first to set vendorCode from URL
|
||||
const parentInit = data().init;
|
||||
if (parentInit) {
|
||||
await parentInit.call(this);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.loadProfile();
|
||||
} catch (error) {
|
||||
vendorProfileLog.error('Init failed:', error);
|
||||
this.error = 'Failed to initialize profile page';
|
||||
}
|
||||
|
||||
vendorProfileLog.info('Profile initialization complete');
|
||||
},
|
||||
|
||||
/**
|
||||
* Load vendor profile
|
||||
*/
|
||||
async loadProfile() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(`/vendor/profile`);
|
||||
|
||||
this.profile = response;
|
||||
this.form = {
|
||||
name: response.name || '',
|
||||
contact_email: response.contact_email || '',
|
||||
contact_phone: response.contact_phone || '',
|
||||
website: response.website || '',
|
||||
business_address: response.business_address || '',
|
||||
tax_number: response.tax_number || '',
|
||||
description: response.description || ''
|
||||
};
|
||||
|
||||
this.hasChanges = false;
|
||||
vendorProfileLog.info('Loaded profile:', this.profile.vendor_code);
|
||||
} catch (error) {
|
||||
vendorProfileLog.error('Failed to load profile:', error);
|
||||
this.error = error.message || 'Failed to load profile';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark form as changed
|
||||
*/
|
||||
markChanged() {
|
||||
this.hasChanges = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate form
|
||||
*/
|
||||
validateForm() {
|
||||
this.errors = {};
|
||||
|
||||
if (!this.form.name?.trim()) {
|
||||
this.errors.name = 'Business name is required';
|
||||
}
|
||||
|
||||
if (this.form.contact_email && !this.isValidEmail(this.form.contact_email)) {
|
||||
this.errors.contact_email = 'Invalid email address';
|
||||
}
|
||||
|
||||
if (this.form.website && !this.isValidUrl(this.form.website)) {
|
||||
this.errors.website = 'Invalid URL format';
|
||||
}
|
||||
|
||||
return Object.keys(this.errors).length === 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if email is valid
|
||||
*/
|
||||
isValidEmail(email) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if URL is valid
|
||||
*/
|
||||
isValidUrl(url) {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return url.match(/^(https?:\/\/)?[\w-]+(\.[\w-]+)+/) !== null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Save profile changes
|
||||
*/
|
||||
async saveProfile() {
|
||||
if (!this.validateForm()) {
|
||||
Utils.showToast('Please fix the errors before saving', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
await apiClient.put(`/vendor/profile`, this.form);
|
||||
|
||||
Utils.showToast('Profile updated successfully', 'success');
|
||||
vendorProfileLog.info('Profile updated');
|
||||
|
||||
this.hasChanges = false;
|
||||
await this.loadProfile();
|
||||
} catch (error) {
|
||||
vendorProfileLog.error('Failed to save profile:', error);
|
||||
Utils.showToast(error.message || 'Failed to save profile', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset form to original values
|
||||
*/
|
||||
resetForm() {
|
||||
if (this.profile) {
|
||||
this.form = {
|
||||
name: this.profile.name || '',
|
||||
contact_email: this.profile.contact_email || '',
|
||||
contact_phone: this.profile.contact_phone || '',
|
||||
website: this.profile.website || '',
|
||||
business_address: this.profile.business_address || '',
|
||||
tax_number: this.profile.tax_number || '',
|
||||
description: this.profile.description || ''
|
||||
};
|
||||
}
|
||||
this.hasChanges = false;
|
||||
this.errors = {};
|
||||
}
|
||||
};
|
||||
}
|
||||
576
static/vendor/js/settings.js
vendored
576
static/vendor/js/settings.js
vendored
@@ -1,576 +0,0 @@
|
||||
// static/vendor/js/settings.js
|
||||
/**
|
||||
* Vendor settings management page logic
|
||||
* Configure vendor preferences and integrations
|
||||
*/
|
||||
|
||||
const vendorSettingsLog = window.LogConfig.loggers.vendorSettings ||
|
||||
window.LogConfig.createLogger('vendorSettings', false);
|
||||
|
||||
vendorSettingsLog.info('Loading...');
|
||||
|
||||
function vendorSettings() {
|
||||
vendorSettingsLog.info('vendorSettings() called');
|
||||
|
||||
return {
|
||||
// Inherit base layout state
|
||||
...data(),
|
||||
|
||||
// Set page identifier
|
||||
currentPage: 'settings',
|
||||
|
||||
// Loading states
|
||||
loading: true,
|
||||
error: '',
|
||||
saving: false,
|
||||
|
||||
// Settings data from API
|
||||
settings: null,
|
||||
|
||||
// Active section
|
||||
activeSection: 'general',
|
||||
|
||||
// Sections for navigation
|
||||
sections: [
|
||||
{ id: 'general', label: 'General', icon: 'cog' },
|
||||
{ id: 'business', label: 'Business Info', icon: 'office-building' },
|
||||
{ id: 'localization', label: 'Localization', icon: 'globe' },
|
||||
{ id: 'marketplace', label: 'Marketplace', icon: 'shopping-cart' },
|
||||
{ id: 'invoices', label: 'Invoices', icon: 'document-text' },
|
||||
{ id: 'branding', label: 'Branding', icon: 'color-swatch' },
|
||||
{ id: 'domains', label: 'Domains', icon: 'globe-alt' },
|
||||
{ id: 'api', label: 'API & Payments', icon: 'key' },
|
||||
{ id: 'notifications', label: 'Notifications', icon: 'bell' },
|
||||
{ id: 'email', label: 'Email', icon: 'envelope' }
|
||||
],
|
||||
|
||||
// Forms for different sections
|
||||
generalForm: {
|
||||
subdomain: '',
|
||||
is_active: true
|
||||
},
|
||||
|
||||
businessForm: {
|
||||
name: '',
|
||||
description: '',
|
||||
contact_email: '',
|
||||
contact_phone: '',
|
||||
website: '',
|
||||
business_address: '',
|
||||
tax_number: ''
|
||||
},
|
||||
|
||||
// Track which fields are inherited from company
|
||||
businessInherited: {
|
||||
contact_email: false,
|
||||
contact_phone: false,
|
||||
website: false,
|
||||
business_address: false,
|
||||
tax_number: false
|
||||
},
|
||||
|
||||
// Company name for display
|
||||
companyName: '',
|
||||
|
||||
marketplaceForm: {
|
||||
letzshop_csv_url_fr: '',
|
||||
letzshop_csv_url_en: '',
|
||||
letzshop_csv_url_de: '',
|
||||
letzshop_default_tax_rate: null,
|
||||
letzshop_boost_sort: '',
|
||||
letzshop_delivery_method: '',
|
||||
letzshop_preorder_days: null
|
||||
},
|
||||
|
||||
notificationForm: {
|
||||
email_notifications: true,
|
||||
order_notifications: true,
|
||||
marketing_emails: false
|
||||
},
|
||||
|
||||
localizationForm: {
|
||||
default_language: 'fr',
|
||||
dashboard_language: 'fr',
|
||||
storefront_language: 'fr',
|
||||
storefront_languages: ['fr', 'de', 'en'],
|
||||
storefront_locale: ''
|
||||
},
|
||||
|
||||
// Email settings
|
||||
emailSettings: null,
|
||||
emailSettingsLoading: false,
|
||||
emailProviders: [],
|
||||
emailForm: {
|
||||
from_email: '',
|
||||
from_name: '',
|
||||
reply_to_email: '',
|
||||
signature_text: '',
|
||||
signature_html: '',
|
||||
provider: 'smtp',
|
||||
// SMTP
|
||||
smtp_host: '',
|
||||
smtp_port: 587,
|
||||
smtp_username: '',
|
||||
smtp_password: '',
|
||||
smtp_use_tls: true,
|
||||
smtp_use_ssl: false,
|
||||
// SendGrid
|
||||
sendgrid_api_key: '',
|
||||
// Mailgun
|
||||
mailgun_api_key: '',
|
||||
mailgun_domain: '',
|
||||
// SES
|
||||
ses_access_key_id: '',
|
||||
ses_secret_access_key: '',
|
||||
ses_region: 'eu-west-1'
|
||||
},
|
||||
testEmailAddress: '',
|
||||
sendingTestEmail: false,
|
||||
hasEmailChanges: false,
|
||||
|
||||
// Track changes per section
|
||||
hasChanges: false,
|
||||
hasBusinessChanges: false,
|
||||
hasLocalizationChanges: false,
|
||||
hasMarketplaceChanges: false,
|
||||
|
||||
async init() {
|
||||
vendorSettingsLog.info('Settings init() called');
|
||||
|
||||
// Guard against multiple initialization
|
||||
if (window._vendorSettingsInitialized) {
|
||||
vendorSettingsLog.warn('Already initialized, skipping');
|
||||
return;
|
||||
}
|
||||
window._vendorSettingsInitialized = true;
|
||||
|
||||
// IMPORTANT: Call parent init first to set vendorCode from URL
|
||||
const parentInit = data().init;
|
||||
if (parentInit) {
|
||||
await parentInit.call(this);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.loadSettings();
|
||||
} catch (error) {
|
||||
vendorSettingsLog.error('Init failed:', error);
|
||||
this.error = 'Failed to initialize settings page';
|
||||
}
|
||||
|
||||
vendorSettingsLog.info('Settings initialization complete');
|
||||
},
|
||||
|
||||
/**
|
||||
* Load vendor settings
|
||||
*/
|
||||
async loadSettings() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(`/vendor/settings`);
|
||||
|
||||
this.settings = response;
|
||||
|
||||
// Populate general form
|
||||
this.generalForm = {
|
||||
subdomain: response.subdomain || '',
|
||||
is_active: response.is_active !== false
|
||||
};
|
||||
|
||||
// Populate business info form with inheritance tracking
|
||||
const biz = response.business_info || {};
|
||||
this.businessForm = {
|
||||
name: response.name || '',
|
||||
description: response.description || '',
|
||||
contact_email: biz.contact_email_override || '',
|
||||
contact_phone: biz.contact_phone_override || '',
|
||||
website: biz.website_override || '',
|
||||
business_address: biz.business_address_override || '',
|
||||
tax_number: biz.tax_number_override || ''
|
||||
};
|
||||
this.businessInherited = {
|
||||
contact_email: biz.contact_email_inherited || false,
|
||||
contact_phone: biz.contact_phone_inherited || false,
|
||||
website: biz.website_inherited || false,
|
||||
business_address: biz.business_address_inherited || false,
|
||||
tax_number: biz.tax_number_inherited || false
|
||||
};
|
||||
this.companyName = biz.company_name || '';
|
||||
|
||||
// Populate localization form from nested structure
|
||||
const loc = response.localization || {};
|
||||
this.localizationForm = {
|
||||
default_language: loc.default_language || 'fr',
|
||||
dashboard_language: loc.dashboard_language || 'fr',
|
||||
storefront_language: loc.storefront_language || 'fr',
|
||||
storefront_languages: loc.storefront_languages || ['fr', 'de', 'en'],
|
||||
storefront_locale: loc.storefront_locale || ''
|
||||
};
|
||||
|
||||
// Populate marketplace form from nested structure
|
||||
const lz = response.letzshop || {};
|
||||
this.marketplaceForm = {
|
||||
letzshop_csv_url_fr: lz.csv_url_fr || '',
|
||||
letzshop_csv_url_en: lz.csv_url_en || '',
|
||||
letzshop_csv_url_de: lz.csv_url_de || '',
|
||||
letzshop_default_tax_rate: lz.default_tax_rate,
|
||||
letzshop_boost_sort: lz.boost_sort || '',
|
||||
letzshop_delivery_method: lz.delivery_method || '',
|
||||
letzshop_preorder_days: lz.preorder_days
|
||||
};
|
||||
|
||||
// Reset all change flags
|
||||
this.hasChanges = false;
|
||||
this.hasBusinessChanges = false;
|
||||
this.hasLocalizationChanges = false;
|
||||
this.hasMarketplaceChanges = false;
|
||||
|
||||
vendorSettingsLog.info('Loaded settings');
|
||||
} catch (error) {
|
||||
vendorSettingsLog.error('Failed to load settings:', error);
|
||||
this.error = error.message || 'Failed to load settings';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark general form as changed
|
||||
*/
|
||||
markChanged() {
|
||||
this.hasChanges = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark business form as changed
|
||||
*/
|
||||
markBusinessChanged() {
|
||||
this.hasBusinessChanges = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark localization form as changed
|
||||
*/
|
||||
markLocalizationChanged() {
|
||||
this.hasLocalizationChanges = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark marketplace form as changed
|
||||
*/
|
||||
markMarketplaceChanged() {
|
||||
this.hasMarketplaceChanges = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get effective value for a business field (override or inherited)
|
||||
*/
|
||||
getEffectiveBusinessValue(field) {
|
||||
const override = this.businessForm[field];
|
||||
if (override) return override;
|
||||
// Return the effective value from settings (includes company inheritance)
|
||||
return this.settings?.business_info?.[field] || '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if field is using inherited value
|
||||
*/
|
||||
isFieldInherited(field) {
|
||||
return this.businessInherited[field] && !this.businessForm[field];
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset a business field to inherit from company
|
||||
*/
|
||||
resetToCompany(field) {
|
||||
this.businessForm[field] = '';
|
||||
this.hasBusinessChanges = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Save business info
|
||||
*/
|
||||
async saveBusinessInfo() {
|
||||
this.saving = true;
|
||||
try {
|
||||
// Determine which fields should be reset to company values
|
||||
const resetFields = [];
|
||||
for (const field of ['contact_email', 'contact_phone', 'website', 'business_address', 'tax_number']) {
|
||||
if (!this.businessForm[field] && this.settings?.business_info?.[field]) {
|
||||
resetFields.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name: this.businessForm.name,
|
||||
description: this.businessForm.description,
|
||||
contact_email: this.businessForm.contact_email || null,
|
||||
contact_phone: this.businessForm.contact_phone || null,
|
||||
website: this.businessForm.website || null,
|
||||
business_address: this.businessForm.business_address || null,
|
||||
tax_number: this.businessForm.tax_number || null,
|
||||
reset_to_company: resetFields
|
||||
};
|
||||
|
||||
await apiClient.put(`/vendor/settings/business-info`, payload);
|
||||
|
||||
Utils.showToast('Business info saved', 'success');
|
||||
vendorSettingsLog.info('Business info updated');
|
||||
|
||||
// Reload to get updated inheritance flags
|
||||
await this.loadSettings();
|
||||
this.hasBusinessChanges = false;
|
||||
} catch (error) {
|
||||
vendorSettingsLog.error('Failed to save business info:', error);
|
||||
Utils.showToast(error.message || 'Failed to save business info', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Save marketplace settings (Letzshop)
|
||||
*/
|
||||
async saveMarketplaceSettings() {
|
||||
this.saving = true;
|
||||
try {
|
||||
await apiClient.put(`/vendor/settings/letzshop`, this.marketplaceForm);
|
||||
|
||||
Utils.showToast('Marketplace settings saved', 'success');
|
||||
vendorSettingsLog.info('Marketplace settings updated');
|
||||
|
||||
this.hasMarketplaceChanges = false;
|
||||
} catch (error) {
|
||||
vendorSettingsLog.error('Failed to save marketplace settings:', error);
|
||||
Utils.showToast(error.message || 'Failed to save settings', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Test Letzshop CSV URL
|
||||
*/
|
||||
async testLetzshopUrl(lang) {
|
||||
const url = this.marketplaceForm[`letzshop_csv_url_${lang}`];
|
||||
if (!url) {
|
||||
Utils.showToast('Please enter a URL first', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
// Try to fetch the URL to validate it
|
||||
const response = await fetch(url, { method: 'HEAD', mode: 'no-cors' });
|
||||
Utils.showToast(`URL appears to be valid`, 'success');
|
||||
} catch (error) {
|
||||
Utils.showToast('Could not validate URL - it may still work', 'warning');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset settings to saved values
|
||||
*/
|
||||
resetSettings() {
|
||||
this.loadSettings();
|
||||
},
|
||||
|
||||
/**
|
||||
* Switch active section
|
||||
*/
|
||||
setSection(sectionId) {
|
||||
this.activeSection = sectionId;
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle a storefront language
|
||||
*/
|
||||
toggleStorefrontLanguage(langCode) {
|
||||
const index = this.localizationForm.storefront_languages.indexOf(langCode);
|
||||
if (index === -1) {
|
||||
this.localizationForm.storefront_languages.push(langCode);
|
||||
} else {
|
||||
this.localizationForm.storefront_languages.splice(index, 1);
|
||||
}
|
||||
this.hasLocalizationChanges = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Save localization settings
|
||||
*/
|
||||
async saveLocalizationSettings() {
|
||||
this.saving = true;
|
||||
try {
|
||||
await apiClient.put(`/vendor/settings/localization`, this.localizationForm);
|
||||
|
||||
Utils.showToast('Localization settings saved', 'success');
|
||||
vendorSettingsLog.info('Localization settings updated');
|
||||
|
||||
this.hasLocalizationChanges = false;
|
||||
} catch (error) {
|
||||
vendorSettingsLog.error('Failed to save localization settings:', error);
|
||||
Utils.showToast(error.message || 'Failed to save settings', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// =====================================================================
|
||||
// EMAIL SETTINGS
|
||||
// =====================================================================
|
||||
|
||||
/**
|
||||
* Load email settings when email tab is activated
|
||||
*/
|
||||
async loadEmailSettings() {
|
||||
if (this.emailSettings !== null) {
|
||||
return; // Already loaded
|
||||
}
|
||||
|
||||
this.emailSettingsLoading = true;
|
||||
try {
|
||||
// Load settings and providers in parallel
|
||||
const [settingsResponse, providersResponse] = await Promise.all([
|
||||
apiClient.get('/vendor/email-settings'),
|
||||
apiClient.get('/vendor/email-settings/providers')
|
||||
]);
|
||||
|
||||
this.emailProviders = providersResponse.providers || [];
|
||||
|
||||
if (settingsResponse.configured && settingsResponse.settings) {
|
||||
this.emailSettings = settingsResponse.settings;
|
||||
this.populateEmailForm(settingsResponse.settings);
|
||||
} else {
|
||||
this.emailSettings = { is_configured: false, is_verified: false };
|
||||
}
|
||||
|
||||
vendorSettingsLog.info('Loaded email settings');
|
||||
} catch (error) {
|
||||
vendorSettingsLog.error('Failed to load email settings:', error);
|
||||
Utils.showToast('Failed to load email settings', 'error');
|
||||
} finally {
|
||||
this.emailSettingsLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Populate email form from settings
|
||||
*/
|
||||
populateEmailForm(settings) {
|
||||
this.emailForm = {
|
||||
from_email: settings.from_email || '',
|
||||
from_name: settings.from_name || '',
|
||||
reply_to_email: settings.reply_to_email || '',
|
||||
signature_text: settings.signature_text || '',
|
||||
signature_html: settings.signature_html || '',
|
||||
provider: settings.provider || 'smtp',
|
||||
// SMTP - don't populate password
|
||||
smtp_host: settings.smtp_host || '',
|
||||
smtp_port: settings.smtp_port || 587,
|
||||
smtp_username: settings.smtp_username || '',
|
||||
smtp_password: '', // Never populate password
|
||||
smtp_use_tls: settings.smtp_use_tls !== false,
|
||||
smtp_use_ssl: settings.smtp_use_ssl || false,
|
||||
// SendGrid - don't populate API key
|
||||
sendgrid_api_key: '',
|
||||
// Mailgun - don't populate API key
|
||||
mailgun_api_key: '',
|
||||
mailgun_domain: settings.mailgun_domain || '',
|
||||
// SES - don't populate secrets
|
||||
ses_access_key_id: '',
|
||||
ses_secret_access_key: '',
|
||||
ses_region: settings.ses_region || 'eu-west-1'
|
||||
};
|
||||
this.hasEmailChanges = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark email form as changed
|
||||
*/
|
||||
markEmailChanged() {
|
||||
this.hasEmailChanges = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Save email settings
|
||||
*/
|
||||
async saveEmailSettings() {
|
||||
// Validate required fields
|
||||
if (!this.emailForm.from_email || !this.emailForm.from_name) {
|
||||
Utils.showToast('From Email and From Name are required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const response = await apiClient.put('/vendor/email-settings', this.emailForm);
|
||||
|
||||
if (response.success) {
|
||||
Utils.showToast('Email settings saved', 'success');
|
||||
vendorSettingsLog.info('Email settings updated');
|
||||
|
||||
// Update local state
|
||||
this.emailSettings = response.settings;
|
||||
this.hasEmailChanges = false;
|
||||
} else {
|
||||
Utils.showToast(response.message || 'Failed to save email settings', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
vendorSettingsLog.error('Failed to save email settings:', error);
|
||||
Utils.showToast(error.message || 'Failed to save email settings', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send test email
|
||||
*/
|
||||
async sendTestEmail() {
|
||||
if (!this.testEmailAddress) {
|
||||
Utils.showToast('Please enter a test email address', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.emailSettings?.is_configured) {
|
||||
Utils.showToast('Please save your email settings first', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendingTestEmail = true;
|
||||
try {
|
||||
const response = await apiClient.post('/vendor/email-settings/verify', {
|
||||
test_email: this.testEmailAddress
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
Utils.showToast('Test email sent! Check your inbox.', 'success');
|
||||
// Update verification status
|
||||
this.emailSettings.is_verified = true;
|
||||
} else {
|
||||
Utils.showToast(response.message || 'Failed to send test email', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
vendorSettingsLog.error('Failed to send test email:', error);
|
||||
Utils.showToast(error.message || 'Failed to send test email', 'error');
|
||||
} finally {
|
||||
this.sendingTestEmail = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Switch active section - with email loading hook
|
||||
*/
|
||||
setSection(sectionId) {
|
||||
this.activeSection = sectionId;
|
||||
|
||||
// Load email settings when email tab is activated
|
||||
if (sectionId === 'email' && this.emailSettings === null) {
|
||||
this.loadEmailSettings();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
275
static/vendor/js/team.js
vendored
275
static/vendor/js/team.js
vendored
@@ -1,275 +0,0 @@
|
||||
// static/vendor/js/team.js
|
||||
/**
|
||||
* Vendor team management page logic
|
||||
* Manage team members, invitations, and roles
|
||||
*/
|
||||
|
||||
const vendorTeamLog = window.LogConfig.loggers.vendorTeam ||
|
||||
window.LogConfig.createLogger('vendorTeam', false);
|
||||
|
||||
vendorTeamLog.info('Loading...');
|
||||
|
||||
function vendorTeam() {
|
||||
vendorTeamLog.info('vendorTeam() called');
|
||||
|
||||
return {
|
||||
// Inherit base layout state
|
||||
...data(),
|
||||
|
||||
// Set page identifier
|
||||
currentPage: 'team',
|
||||
|
||||
// Loading states
|
||||
loading: true,
|
||||
error: '',
|
||||
saving: false,
|
||||
|
||||
// Team data
|
||||
members: [],
|
||||
roles: [],
|
||||
stats: {
|
||||
total: 0,
|
||||
active_count: 0,
|
||||
pending_invitations: 0
|
||||
},
|
||||
|
||||
// Modal states
|
||||
showInviteModal: false,
|
||||
showEditModal: false,
|
||||
showRemoveModal: false,
|
||||
selectedMember: null,
|
||||
|
||||
// Invite form
|
||||
inviteForm: {
|
||||
email: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
role_name: 'staff'
|
||||
},
|
||||
|
||||
// Edit form
|
||||
editForm: {
|
||||
role_id: null,
|
||||
is_active: true
|
||||
},
|
||||
|
||||
// Available role names for invite
|
||||
roleOptions: [
|
||||
{ value: 'owner', label: 'Owner', description: 'Full access to all features' },
|
||||
{ value: 'manager', label: 'Manager', description: 'Manage orders, products, and team' },
|
||||
{ value: 'staff', label: 'Staff', description: 'Handle orders and products' },
|
||||
{ value: 'support', label: 'Support', description: 'Customer support access' },
|
||||
{ value: 'viewer', label: 'Viewer', description: 'Read-only access' },
|
||||
{ value: 'marketing', label: 'Marketing', description: 'Content and promotions' }
|
||||
],
|
||||
|
||||
async init() {
|
||||
vendorTeamLog.info('Team init() called');
|
||||
|
||||
// Guard against multiple initialization
|
||||
if (window._vendorTeamInitialized) {
|
||||
vendorTeamLog.warn('Already initialized, skipping');
|
||||
return;
|
||||
}
|
||||
window._vendorTeamInitialized = true;
|
||||
|
||||
// IMPORTANT: Call parent init first to set vendorCode from URL
|
||||
const parentInit = data().init;
|
||||
if (parentInit) {
|
||||
await parentInit.call(this);
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
this.loadMembers(),
|
||||
this.loadRoles()
|
||||
]);
|
||||
} catch (error) {
|
||||
vendorTeamLog.error('Init failed:', error);
|
||||
this.error = 'Failed to initialize team page';
|
||||
}
|
||||
|
||||
vendorTeamLog.info('Team initialization complete');
|
||||
},
|
||||
|
||||
/**
|
||||
* Load team members
|
||||
*/
|
||||
async loadMembers() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(`/vendor/team/members?include_inactive=true`);
|
||||
|
||||
this.members = response.members || [];
|
||||
this.stats = {
|
||||
total: response.total || 0,
|
||||
active_count: response.active_count || 0,
|
||||
pending_invitations: response.pending_invitations || 0
|
||||
};
|
||||
|
||||
vendorTeamLog.info('Loaded team members:', this.members.length);
|
||||
} catch (error) {
|
||||
vendorTeamLog.error('Failed to load team members:', error);
|
||||
this.error = error.message || 'Failed to load team members';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load available roles
|
||||
*/
|
||||
async loadRoles() {
|
||||
try {
|
||||
const response = await apiClient.get(`/vendor/team/roles`);
|
||||
this.roles = response.roles || [];
|
||||
vendorTeamLog.info('Loaded roles:', this.roles.length);
|
||||
} catch (error) {
|
||||
vendorTeamLog.error('Failed to load roles:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Open invite modal
|
||||
*/
|
||||
openInviteModal() {
|
||||
this.inviteForm = {
|
||||
email: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
role_name: 'staff'
|
||||
};
|
||||
this.showInviteModal = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Send invitation
|
||||
*/
|
||||
async sendInvitation() {
|
||||
if (!this.inviteForm.email) {
|
||||
Utils.showToast('Email is required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
await apiClient.post(`/vendor/team/invite`, this.inviteForm);
|
||||
|
||||
Utils.showToast('Invitation sent successfully', 'success');
|
||||
vendorTeamLog.info('Invitation sent to:', this.inviteForm.email);
|
||||
|
||||
this.showInviteModal = false;
|
||||
await this.loadMembers();
|
||||
} catch (error) {
|
||||
vendorTeamLog.error('Failed to send invitation:', error);
|
||||
Utils.showToast(error.message || 'Failed to send invitation', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Open edit member modal
|
||||
*/
|
||||
openEditModal(member) {
|
||||
this.selectedMember = member;
|
||||
this.editForm = {
|
||||
role_id: member.role_id,
|
||||
is_active: member.is_active
|
||||
};
|
||||
this.showEditModal = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update team member
|
||||
*/
|
||||
async updateMember() {
|
||||
if (!this.selectedMember) return;
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
await apiClient.put(
|
||||
`/vendor/${this.vendorCode}/team/members/${this.selectedMember.user_id}`,
|
||||
this.editForm
|
||||
);
|
||||
|
||||
Utils.showToast('Team member updated', 'success');
|
||||
vendorTeamLog.info('Updated team member:', this.selectedMember.user_id);
|
||||
|
||||
this.showEditModal = false;
|
||||
this.selectedMember = null;
|
||||
await this.loadMembers();
|
||||
} catch (error) {
|
||||
vendorTeamLog.error('Failed to update team member:', error);
|
||||
Utils.showToast(error.message || 'Failed to update team member', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Confirm remove member
|
||||
*/
|
||||
confirmRemove(member) {
|
||||
this.selectedMember = member;
|
||||
this.showRemoveModal = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove team member
|
||||
*/
|
||||
async removeMember() {
|
||||
if (!this.selectedMember) return;
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
await apiClient.delete(`/vendor/team/members/${this.selectedMember.user_id}`);
|
||||
|
||||
Utils.showToast('Team member removed', 'success');
|
||||
vendorTeamLog.info('Removed team member:', this.selectedMember.user_id);
|
||||
|
||||
this.showRemoveModal = false;
|
||||
this.selectedMember = null;
|
||||
await this.loadMembers();
|
||||
} catch (error) {
|
||||
vendorTeamLog.error('Failed to remove team member:', error);
|
||||
Utils.showToast(error.message || 'Failed to remove team member', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get role display name
|
||||
*/
|
||||
getRoleName(member) {
|
||||
if (member.role_name) return member.role_name;
|
||||
const role = this.roles.find(r => r.id === member.role_id);
|
||||
return role ? role.name : 'Unknown';
|
||||
},
|
||||
|
||||
/**
|
||||
* Get member initials for avatar
|
||||
*/
|
||||
getInitials(member) {
|
||||
const first = member.first_name || member.email?.charAt(0) || '';
|
||||
const last = member.last_name || '';
|
||||
return (first.charAt(0) + last.charAt(0)).toUpperCase() || '?';
|
||||
},
|
||||
|
||||
/**
|
||||
* Format date for display
|
||||
*/
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
return new Date(dateStr).toLocaleDateString(locale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user