admin login migration to new structure, new design
This commit is contained in:
1
static/admin/js/analytics.js
Normal file
1
static/admin/js/analytics.js
Normal file
@@ -0,0 +1 @@
|
||||
// Admin analytics
|
||||
203
static/admin/js/dashboard.js
Normal file
203
static/admin/js/dashboard.js
Normal file
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* Admin Dashboard Component
|
||||
* Extends adminLayout with dashboard-specific functionality
|
||||
*/
|
||||
|
||||
function adminDashboard() {
|
||||
return {
|
||||
// Inherit all adminLayout functionality
|
||||
...window.adminLayout(),
|
||||
|
||||
// Dashboard-specific state
|
||||
currentSection: 'dashboard',
|
||||
stats: {
|
||||
vendors: {},
|
||||
users: {},
|
||||
imports: {}
|
||||
},
|
||||
vendors: [],
|
||||
users: [],
|
||||
imports: [],
|
||||
recentVendors: [],
|
||||
recentImports: [],
|
||||
loading: false,
|
||||
|
||||
/**
|
||||
* Initialize dashboard
|
||||
*/
|
||||
async init() {
|
||||
// Call parent init from adminLayout
|
||||
this.currentPage = this.getCurrentPage();
|
||||
await this.loadUserData();
|
||||
|
||||
// Load dashboard data
|
||||
await this.loadDashboardData();
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all dashboard data
|
||||
*/
|
||||
async loadDashboardData() {
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
this.loadStats(),
|
||||
this.loadRecentVendors(),
|
||||
this.loadRecentImports()
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard data:', error);
|
||||
this.showErrorModal({
|
||||
message: 'Failed to load dashboard data',
|
||||
details: error.message
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load statistics
|
||||
*/
|
||||
async loadStats() {
|
||||
try {
|
||||
const response = await apiClient.get('/admin/stats');
|
||||
this.stats = response;
|
||||
} catch (error) {
|
||||
console.error('Failed to load stats:', error);
|
||||
// Don't show error modal for stats, just log it
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load recent vendors
|
||||
*/
|
||||
async loadRecentVendors() {
|
||||
try {
|
||||
const response = await apiClient.get('/admin/vendors', {
|
||||
skip: 0,
|
||||
limit: 5
|
||||
});
|
||||
this.recentVendors = response.vendors || response;
|
||||
} catch (error) {
|
||||
console.error('Failed to load recent vendors:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load recent import jobs
|
||||
*/
|
||||
async loadRecentImports() {
|
||||
try {
|
||||
const response = await apiClient.get('/admin/imports', {
|
||||
skip: 0,
|
||||
limit: 5
|
||||
});
|
||||
this.recentImports = response.imports || response;
|
||||
} catch (error) {
|
||||
console.error('Failed to load recent imports:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show different sections
|
||||
*/
|
||||
async showSection(section) {
|
||||
this.currentSection = section;
|
||||
|
||||
// Load data based on section
|
||||
if (section === 'vendors' && this.vendors.length === 0) {
|
||||
await this.loadAllVendors();
|
||||
} else if (section === 'users' && this.users.length === 0) {
|
||||
await this.loadAllUsers();
|
||||
} else if (section === 'imports' && this.imports.length === 0) {
|
||||
await this.loadAllImports();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all vendors
|
||||
*/
|
||||
async loadAllVendors() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await apiClient.get('/admin/vendors', {
|
||||
skip: 0,
|
||||
limit: 100
|
||||
});
|
||||
this.vendors = response.vendors || response;
|
||||
} catch (error) {
|
||||
console.error('Failed to load vendors:', error);
|
||||
this.showErrorModal({
|
||||
message: 'Failed to load vendors',
|
||||
details: error.message
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all users
|
||||
*/
|
||||
async loadAllUsers() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await apiClient.get('/admin/users', {
|
||||
skip: 0,
|
||||
limit: 100
|
||||
});
|
||||
this.users = response.users || response;
|
||||
} catch (error) {
|
||||
console.error('Failed to load users:', error);
|
||||
this.showErrorModal({
|
||||
message: 'Failed to load users',
|
||||
details: error.message
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all import jobs
|
||||
*/
|
||||
async loadAllImports() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await apiClient.get('/admin/imports', {
|
||||
skip: 0,
|
||||
limit: 100
|
||||
});
|
||||
this.imports = response.imports || response;
|
||||
} catch (error) {
|
||||
console.error('Failed to load import jobs:', error);
|
||||
this.showErrorModal({
|
||||
message: 'Failed to load import jobs',
|
||||
details: error.message
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Format date for display
|
||||
*/
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
57
static/admin/js/init-alpine.js
Normal file
57
static/admin/js/init-alpine.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Alpine.js v3 global data initialization
|
||||
* Provides theme toggle, menu controls, and page state
|
||||
*/
|
||||
function data() {
|
||||
function getThemeFromLocalStorage() {
|
||||
// if user already changed the theme, use it
|
||||
if (window.localStorage.getItem('dark')) {
|
||||
return JSON.parse(window.localStorage.getItem('dark'))
|
||||
}
|
||||
|
||||
// else return their preferences
|
||||
return (
|
||||
!!window.matchMedia &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
)
|
||||
}
|
||||
|
||||
function setThemeToLocalStorage(value) {
|
||||
window.localStorage.setItem('dark', value)
|
||||
}
|
||||
|
||||
return {
|
||||
dark: getThemeFromLocalStorage(),
|
||||
toggleTheme() {
|
||||
this.dark = !this.dark
|
||||
setThemeToLocalStorage(this.dark)
|
||||
},
|
||||
isSideMenuOpen: false,
|
||||
toggleSideMenu() {
|
||||
this.isSideMenuOpen = !this.isSideMenuOpen
|
||||
},
|
||||
closeSideMenu() {
|
||||
this.isSideMenuOpen = false
|
||||
},
|
||||
isNotificationsMenuOpen: false,
|
||||
toggleNotificationsMenu() {
|
||||
this.isNotificationsMenuOpen = !this.isNotificationsMenuOpen
|
||||
},
|
||||
closeNotificationsMenu() {
|
||||
this.isNotificationsMenuOpen = false
|
||||
},
|
||||
isProfileMenuOpen: false,
|
||||
toggleProfileMenu() {
|
||||
this.isProfileMenuOpen = !this.isProfileMenuOpen
|
||||
},
|
||||
closeProfileMenu() {
|
||||
this.isProfileMenuOpen = false
|
||||
},
|
||||
isPagesMenuOpen: false,
|
||||
togglePagesMenu() {
|
||||
this.isPagesMenuOpen = !this.isPagesMenuOpen
|
||||
},
|
||||
// Page identifier - will be set by individual pages
|
||||
currentPage: ''
|
||||
}
|
||||
}
|
||||
130
static/admin/js/login.js
Normal file
130
static/admin/js/login.js
Normal file
@@ -0,0 +1,130 @@
|
||||
// Admin Login Component
|
||||
function adminLogin() {
|
||||
return {
|
||||
dark: false, // For dark mode toggle
|
||||
credentials: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
success: null,
|
||||
errors: {},
|
||||
|
||||
init() {
|
||||
// Check if already logged in
|
||||
this.checkExistingAuth();
|
||||
|
||||
// Check for dark mode preference
|
||||
this.dark = localStorage.getItem('theme') === 'dark';
|
||||
},
|
||||
|
||||
checkExistingAuth() {
|
||||
const token = localStorage.getItem('admin_token') || localStorage.getItem('token');
|
||||
if (token) {
|
||||
// Verify token is still valid
|
||||
const userData = localStorage.getItem('admin_user');
|
||||
if (userData) {
|
||||
try {
|
||||
const user = JSON.parse(userData);
|
||||
if (user.role === 'admin') {
|
||||
window.location.href = '/static/admin/dashboard.html';
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid user data, clear storage
|
||||
this.clearAuthData();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
clearAuthData() {
|
||||
localStorage.removeItem('admin_token');
|
||||
localStorage.removeItem('admin_user');
|
||||
localStorage.removeItem('token');
|
||||
},
|
||||
|
||||
clearErrors() {
|
||||
this.error = null;
|
||||
this.success = null;
|
||||
this.errors = {};
|
||||
},
|
||||
|
||||
validateForm() {
|
||||
this.clearErrors();
|
||||
let isValid = true;
|
||||
|
||||
if (!this.credentials.username.trim()) {
|
||||
this.errors.username = 'Username is required';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!this.credentials.password) {
|
||||
this.errors.password = 'Password is required';
|
||||
isValid = false;
|
||||
} else if (this.credentials.password.length < 6) {
|
||||
this.errors.password = 'Password must be at least 6 characters';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
},
|
||||
|
||||
async handleLogin() {
|
||||
if (!this.validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.clearErrors();
|
||||
|
||||
try {
|
||||
// Use apiClient from api-client.js
|
||||
const response = await apiClient.post('/admin/auth/login', {
|
||||
username: this.credentials.username.trim(),
|
||||
password: this.credentials.password
|
||||
});
|
||||
|
||||
// Validate response
|
||||
if (!response.access_token) {
|
||||
throw new Error('Invalid response from server');
|
||||
}
|
||||
|
||||
// Check if user is admin (if user data is provided)
|
||||
if (response.user && response.user.role !== 'admin') {
|
||||
throw new Error('Access denied. Admin privileges required.');
|
||||
}
|
||||
|
||||
// Store authentication data
|
||||
localStorage.setItem('admin_token', response.access_token);
|
||||
localStorage.setItem('token', response.access_token); // Backup
|
||||
|
||||
if (response.user) {
|
||||
localStorage.setItem('admin_user', JSON.stringify(response.user));
|
||||
}
|
||||
|
||||
// Show success message
|
||||
this.success = 'Login successful! Redirecting...';
|
||||
|
||||
// Redirect after short delay
|
||||
setTimeout(() => {
|
||||
window.location.href = '/static/admin/dashboard.html';
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
this.error = error.message || 'Invalid username or password. Please try again.';
|
||||
|
||||
// Clear any partial auth data
|
||||
this.clearAuthData();
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
toggleDarkMode() {
|
||||
this.dark = !this.dark;
|
||||
localStorage.setItem('theme', this.dark ? 'dark' : 'light');
|
||||
}
|
||||
}
|
||||
}
|
||||
1
static/admin/js/monitoring.js
Normal file
1
static/admin/js/monitoring.js
Normal file
@@ -0,0 +1 @@
|
||||
// System monitoring
|
||||
338
static/admin/js/vendor-edit.js
Normal file
338
static/admin/js/vendor-edit.js
Normal file
@@ -0,0 +1,338 @@
|
||||
// static/js/admin/vendor-edit.js
|
||||
|
||||
function vendorEdit() {
|
||||
return {
|
||||
currentUser: {},
|
||||
vendor: {},
|
||||
formData: {},
|
||||
errors: {},
|
||||
loadingVendor: true,
|
||||
saving: false,
|
||||
vendorId: null,
|
||||
|
||||
// Confirmation modal
|
||||
confirmModal: {
|
||||
show: false,
|
||||
title: '',
|
||||
message: '',
|
||||
warning: '',
|
||||
buttonText: '',
|
||||
buttonClass: 'btn-primary',
|
||||
onConfirm: () => {},
|
||||
onCancel: null
|
||||
},
|
||||
|
||||
// Success modal
|
||||
successModal: {
|
||||
show: false,
|
||||
title: '',
|
||||
message: '',
|
||||
details: null,
|
||||
note: ''
|
||||
},
|
||||
|
||||
// Transfer ownership
|
||||
showTransferOwnership: false,
|
||||
transferring: false,
|
||||
transferData: {
|
||||
new_owner_user_id: null,
|
||||
transfer_reason: '',
|
||||
confirm_transfer: false
|
||||
},
|
||||
|
||||
init() {
|
||||
console.log('=== Vendor Edit Initialization ===');
|
||||
|
||||
// Check authentication
|
||||
if (!Auth.isAuthenticated() || !Auth.isAdmin()) {
|
||||
console.log('Not authenticated as admin, redirecting to login');
|
||||
window.location.href = '/static/admin/login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentUser = Auth.getCurrentUser();
|
||||
console.log('Current user:', this.currentUser.username);
|
||||
|
||||
// Get vendor ID from URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
this.vendorId = urlParams.get('id');
|
||||
|
||||
if (!this.vendorId) {
|
||||
console.error('No vendor ID in URL');
|
||||
alert('No vendor ID provided');
|
||||
window.location.href = '/static/admin/dashboard.html#vendors';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Vendor ID:', this.vendorId);
|
||||
|
||||
// Load vendor details
|
||||
this.loadVendor();
|
||||
},
|
||||
|
||||
async loadVendor() {
|
||||
this.loadingVendor = true;
|
||||
try {
|
||||
console.log('Loading vendor with ID:', this.vendorId);
|
||||
this.vendor = await apiClient.get(`/admin/vendors/${this.vendorId}`);
|
||||
console.log('✅ Vendor loaded:', this.vendor.vendor_code);
|
||||
console.log('Owner email:', this.vendor.owner_email);
|
||||
console.log('Contact email:', this.vendor.contact_email);
|
||||
|
||||
// Populate form data
|
||||
this.formData = {
|
||||
name: this.vendor.name,
|
||||
subdomain: this.vendor.subdomain,
|
||||
description: this.vendor.description || '',
|
||||
contact_email: this.vendor.contact_email || '',
|
||||
contact_phone: this.vendor.contact_phone || '',
|
||||
website: this.vendor.website || '',
|
||||
business_address: this.vendor.business_address || '',
|
||||
tax_number: this.vendor.tax_number || ''
|
||||
};
|
||||
|
||||
console.log('Form data populated');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to load vendor:', error);
|
||||
Utils.showToast('Failed to load vendor details: ' + (error.message || 'Unknown error'), 'error');
|
||||
window.location.href = '/static/admin/dashboard.html#vendors';
|
||||
} finally {
|
||||
this.loadingVendor = false;
|
||||
}
|
||||
},
|
||||
|
||||
formatSubdomain() {
|
||||
this.formData.subdomain = this.formData.subdomain
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, '');
|
||||
},
|
||||
|
||||
async handleSubmit() {
|
||||
console.log('Submitting vendor update...');
|
||||
this.errors = {};
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
const updatedVendor = await apiClient.put(
|
||||
`/admin/vendors/${this.vendorId}`,
|
||||
this.formData
|
||||
);
|
||||
|
||||
console.log('✅ Vendor updated successfully');
|
||||
Utils.showToast('Vendor updated successfully!', 'success');
|
||||
this.vendor = updatedVendor;
|
||||
|
||||
// Refresh form data with latest values
|
||||
this.formData.name = updatedVendor.name;
|
||||
this.formData.subdomain = updatedVendor.subdomain;
|
||||
this.formData.contact_email = updatedVendor.contact_email;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to update vendor:', error);
|
||||
Utils.showToast(error.message || 'Failed to update vendor', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
showVerificationModal() {
|
||||
const action = this.vendor.is_verified ? 'unverify' : 'verify';
|
||||
const actionCap = this.vendor.is_verified ? 'Unverify' : 'Verify';
|
||||
|
||||
this.confirmModal = {
|
||||
show: true,
|
||||
title: `${actionCap} Vendor`,
|
||||
message: `Are you sure you want to ${action} this vendor?`,
|
||||
warning: this.vendor.is_verified
|
||||
? 'Unverifying this vendor will prevent them from being publicly visible and may affect their operations.'
|
||||
: 'Verifying this vendor will make them publicly visible and allow them to operate fully.',
|
||||
buttonText: actionCap,
|
||||
buttonClass: this.vendor.is_verified ? 'btn-warning' : 'btn-success',
|
||||
onConfirm: () => this.toggleVerification(),
|
||||
onCancel: null
|
||||
};
|
||||
},
|
||||
|
||||
async toggleVerification() {
|
||||
const action = this.vendor.is_verified ? 'unverify' : 'verify';
|
||||
console.log(`Toggling verification: ${action}`);
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const result = await apiClient.put(`/admin/vendors/${this.vendorId}/verify`);
|
||||
this.vendor.is_verified = result.vendor.is_verified;
|
||||
console.log('✅ Verification toggled');
|
||||
Utils.showToast(result.message, 'success');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to toggle verification:', error);
|
||||
Utils.showToast('Failed to update verification status', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
showStatusModal() {
|
||||
const action = this.vendor.is_active ? 'deactivate' : 'activate';
|
||||
const actionCap = this.vendor.is_active ? 'Deactivate' : 'Activate';
|
||||
|
||||
this.confirmModal = {
|
||||
show: true,
|
||||
title: `${actionCap} Vendor`,
|
||||
message: `Are you sure you want to ${action} this vendor?`,
|
||||
warning: this.vendor.is_active
|
||||
? 'Deactivating this vendor will immediately suspend all their operations and make them invisible to customers.'
|
||||
: 'Activating this vendor will restore their operations and make them visible again.',
|
||||
buttonText: actionCap,
|
||||
buttonClass: this.vendor.is_active ? 'btn-danger' : 'btn-success',
|
||||
onConfirm: () => this.toggleStatus(),
|
||||
onCancel: null
|
||||
};
|
||||
},
|
||||
|
||||
async toggleStatus() {
|
||||
const action = this.vendor.is_active ? 'deactivate' : 'activate';
|
||||
console.log(`Toggling status: ${action}`);
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const result = await apiClient.put(`/admin/vendors/${this.vendorId}/status`);
|
||||
this.vendor.is_active = result.vendor.is_active;
|
||||
console.log('✅ Status toggled');
|
||||
Utils.showToast(result.message, 'success');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to toggle status:', error);
|
||||
Utils.showToast('Failed to update vendor status', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
async handleTransferOwnership() {
|
||||
// Validate inputs
|
||||
if (!this.transferData.confirm_transfer) {
|
||||
Utils.showToast('Please confirm the ownership transfer', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.transferData.new_owner_user_id) {
|
||||
Utils.showToast('Please enter the new owner user ID', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the transfer modal first
|
||||
this.showTransferOwnership = false;
|
||||
|
||||
// Wait a moment for modal to close
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
// Show final confirmation modal
|
||||
this.confirmModal = {
|
||||
show: true,
|
||||
title: '⚠️ FINAL CONFIRMATION: Transfer Ownership',
|
||||
message: `You are about to transfer ownership of "${this.vendor.name}" to user ID ${this.transferData.new_owner_user_id}.`,
|
||||
warning: `Current Owner: ${this.vendor.owner_username} (${this.vendor.owner_email})\n\n` +
|
||||
`This action will:\n` +
|
||||
`• Assign full ownership rights to the new user\n` +
|
||||
`• Demote the current owner to Manager role\n` +
|
||||
`• Be permanently logged for audit purposes\n\n` +
|
||||
`This action cannot be easily undone. Are you absolutely sure?`,
|
||||
buttonText: '🔄 Yes, Transfer Ownership',
|
||||
buttonClass: 'btn-danger',
|
||||
onConfirm: () => this.executeTransferOwnership(),
|
||||
onCancel: () => {
|
||||
// If cancelled, reopen the transfer modal with preserved data
|
||||
this.showTransferOwnership = true;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
async executeTransferOwnership() {
|
||||
console.log('Transferring ownership to user:', this.transferData.new_owner_user_id);
|
||||
this.transferring = true;
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
const result = await apiClient.post(
|
||||
`/admin/vendors/${this.vendorId}/transfer-ownership`,
|
||||
this.transferData
|
||||
);
|
||||
|
||||
console.log('✅ Ownership transferred successfully');
|
||||
|
||||
// Show beautiful success modal
|
||||
this.successModal = {
|
||||
show: true,
|
||||
title: 'Ownership Transfer Complete',
|
||||
message: `The ownership of "${this.vendor.name}" has been successfully transferred.`,
|
||||
details: {
|
||||
oldOwner: {
|
||||
username: result.old_owner.username,
|
||||
email: result.old_owner.email
|
||||
},
|
||||
newOwner: {
|
||||
username: result.new_owner.username,
|
||||
email: result.new_owner.email
|
||||
}
|
||||
},
|
||||
note: 'The transfer has been logged for audit purposes. The previous owner has been assigned the Manager role.'
|
||||
};
|
||||
|
||||
Utils.showToast('Ownership transferred successfully', 'success');
|
||||
|
||||
// Reload vendor data to reflect new owner
|
||||
await this.loadVendor();
|
||||
|
||||
// Reset transfer form data
|
||||
this.transferData = {
|
||||
new_owner_user_id: null,
|
||||
transfer_reason: '',
|
||||
confirm_transfer: false
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to transfer ownership:', error);
|
||||
const errorMsg = error.message || error.detail || 'Unknown error';
|
||||
Utils.showToast(`Transfer failed: ${errorMsg}`, 'error');
|
||||
|
||||
// Show error in modal format (reuse success modal structure)
|
||||
alert(`❌ Transfer Failed\n\n${errorMsg}\n\nPlease check the user ID and try again.`);
|
||||
|
||||
// Reopen transfer modal so user can try again
|
||||
this.showTransferOwnership = true;
|
||||
} finally {
|
||||
this.transferring = false;
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
async handleLogout() {
|
||||
// Show confirmation modal for logout
|
||||
this.confirmModal = {
|
||||
show: true,
|
||||
title: '🚪 Confirm Logout',
|
||||
message: 'Are you sure you want to logout from the Admin Portal?',
|
||||
warning: 'You will need to login again to access the admin dashboard.',
|
||||
buttonText: 'Yes, Logout',
|
||||
buttonClass: 'btn-danger',
|
||||
onConfirm: () => this.executeLogout(),
|
||||
onCancel: null
|
||||
};
|
||||
},
|
||||
|
||||
async executeLogout() {
|
||||
console.log('Logging out...');
|
||||
|
||||
// Show loading state briefly
|
||||
this.saving = true;
|
||||
|
||||
// Clear authentication
|
||||
Auth.logout();
|
||||
|
||||
// Show success message
|
||||
Utils.showToast('Logged out successfully', 'success', 1000);
|
||||
|
||||
// Redirect to login after brief delay
|
||||
setTimeout(() => {
|
||||
window.location.href = '/static/admin/login.html';
|
||||
}, 500);
|
||||
},
|
||||
};
|
||||
}
|
||||
241
static/admin/js/vendors.js
Normal file
241
static/admin/js/vendors.js
Normal file
@@ -0,0 +1,241 @@
|
||||
// Admin Vendor Creation Component
|
||||
function vendorCreation() {
|
||||
return {
|
||||
currentUser: {},
|
||||
formData: {
|
||||
vendor_code: '',
|
||||
name: '',
|
||||
subdomain: '',
|
||||
description: '',
|
||||
owner_email: '',
|
||||
business_phone: '',
|
||||
website: '',
|
||||
business_address: '',
|
||||
tax_number: ''
|
||||
},
|
||||
loading: false,
|
||||
errors: {},
|
||||
showCredentials: false,
|
||||
credentials: null,
|
||||
|
||||
init() {
|
||||
if (!this.checkAuth()) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
checkAuth() {
|
||||
if (!Auth.isAuthenticated()) {
|
||||
window.location.href = '/static/admin/login.html';
|
||||
return false;
|
||||
}
|
||||
|
||||
const user = Auth.getCurrentUser();
|
||||
if (!user || user.role !== 'admin') {
|
||||
Utils.showToast('Access denied. Admin privileges required.', 'error');
|
||||
Auth.logout();
|
||||
window.location.href = '/static/admin/login.html';
|
||||
return false;
|
||||
}
|
||||
|
||||
this.currentUser = user;
|
||||
return true;
|
||||
},
|
||||
|
||||
async handleLogout() {
|
||||
const confirmed = await Utils.confirm(
|
||||
'Are you sure you want to logout?',
|
||||
'Confirm Logout'
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
Auth.logout();
|
||||
Utils.showToast('Logged out successfully', 'success', 2000);
|
||||
setTimeout(() => {
|
||||
window.location.href = '/static/admin/login.html';
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
|
||||
// Auto-format vendor code (uppercase)
|
||||
formatVendorCode() {
|
||||
this.formData.vendor_code = this.formData.vendor_code
|
||||
.toUpperCase()
|
||||
.replace(/[^A-Z0-9_-]/g, '');
|
||||
},
|
||||
|
||||
// Auto-format subdomain (lowercase)
|
||||
formatSubdomain() {
|
||||
this.formData.subdomain = this.formData.subdomain
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, '');
|
||||
},
|
||||
|
||||
clearErrors() {
|
||||
this.errors = {};
|
||||
},
|
||||
|
||||
validateForm() {
|
||||
this.clearErrors();
|
||||
let isValid = true;
|
||||
|
||||
// Required fields validation
|
||||
if (!this.formData.vendor_code.trim()) {
|
||||
this.errors.vendor_code = 'Vendor code is required';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!this.formData.name.trim()) {
|
||||
this.errors.name = 'Vendor name is required';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!this.formData.subdomain.trim()) {
|
||||
this.errors.subdomain = 'Subdomain is required';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!this.formData.owner_email.trim()) {
|
||||
this.errors.owner_email = 'Owner email is required';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (this.formData.owner_email && !emailRegex.test(this.formData.owner_email)) {
|
||||
this.errors.owner_email = 'Invalid email format';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Subdomain validation (must start and end with alphanumeric)
|
||||
const subdomainRegex = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
||||
if (this.formData.subdomain && this.formData.subdomain.length > 1 &&
|
||||
!subdomainRegex.test(this.formData.subdomain)) {
|
||||
this.errors.subdomain = 'Subdomain must start and end with a letter or number';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
},
|
||||
|
||||
async handleSubmit() {
|
||||
if (!this.validateForm()) {
|
||||
Utils.showToast('Please fix validation errors', 'error');
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
// Prepare data (remove empty fields)
|
||||
const submitData = {};
|
||||
for (const [key, value] of Object.entries(this.formData)) {
|
||||
if (value !== '' && value !== null && value !== undefined) {
|
||||
submitData[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Submitting vendor data:', submitData);
|
||||
|
||||
const response = await apiClient.post('/admin/vendors', submitData);
|
||||
|
||||
console.log('Vendor creation response:', response);
|
||||
|
||||
// Store credentials - be flexible with response structure
|
||||
this.credentials = {
|
||||
vendor_code: response.vendor_code || this.formData.vendor_code,
|
||||
subdomain: response.subdomain || this.formData.subdomain,
|
||||
name: response.name || this.formData.name,
|
||||
owner_username: response.owner_username || `${this.formData.subdomain}_owner`,
|
||||
owner_email: response.owner_email || this.formData.owner_email,
|
||||
temporary_password: response.temporary_password || 'PASSWORD_NOT_RETURNED',
|
||||
login_url: response.login_url ||
|
||||
`http://localhost:8000/vendor/${this.formData.subdomain}/login` ||
|
||||
`${this.formData.subdomain}.platform.com/vendor/login`
|
||||
};
|
||||
|
||||
console.log('Stored credentials:', this.credentials);
|
||||
|
||||
// Check if password was returned
|
||||
if (!response.temporary_password) {
|
||||
console.warn('⚠️ Warning: temporary_password not returned from API');
|
||||
console.warn('Full API response:', response);
|
||||
Utils.showToast('Vendor created but password not returned. Check server logs.', 'warning', 5000);
|
||||
}
|
||||
|
||||
// Show credentials display
|
||||
this.showCredentials = true;
|
||||
|
||||
// Success notification
|
||||
Utils.showToast('Vendor created successfully!', 'success');
|
||||
|
||||
// Scroll to top to see credentials
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating vendor:', error);
|
||||
|
||||
// Check for specific validation errors
|
||||
if (error.message.includes('vendor_code') || error.message.includes('Vendor code')) {
|
||||
this.errors.vendor_code = 'Vendor code already exists';
|
||||
} else if (error.message.includes('subdomain')) {
|
||||
this.errors.subdomain = 'Subdomain already exists';
|
||||
} else if (error.message.includes('email')) {
|
||||
this.errors.owner_email = 'Email already in use';
|
||||
}
|
||||
|
||||
Utils.showToast(
|
||||
error.message || 'Failed to create vendor',
|
||||
'error'
|
||||
);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.formData = {
|
||||
vendor_code: '',
|
||||
name: '',
|
||||
subdomain: '',
|
||||
description: '',
|
||||
owner_email: '',
|
||||
business_phone: '',
|
||||
website: '',
|
||||
business_address: '',
|
||||
tax_number: ''
|
||||
};
|
||||
this.clearErrors();
|
||||
this.showCredentials = false;
|
||||
this.credentials = null;
|
||||
},
|
||||
|
||||
copyToClipboard(text, label) {
|
||||
if (!text) {
|
||||
Utils.showToast('Nothing to copy', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
Utils.showToast(`${label} copied to clipboard`, 'success', 2000);
|
||||
}).catch((err) => {
|
||||
console.error('Failed to copy:', err);
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
Utils.showToast(`${label} copied to clipboard`, 'success', 2000);
|
||||
} catch (err) {
|
||||
Utils.showToast('Failed to copy to clipboard', 'error');
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user