admin login migration to new structure, new design
This commit is contained in:
@@ -1 +0,0 @@
|
||||
// Admin analytics
|
||||
@@ -1,203 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
// Admin Login Component
|
||||
function adminLogin() {
|
||||
return {
|
||||
credentials: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
success: null,
|
||||
errors: {},
|
||||
|
||||
init() {
|
||||
// Check if already logged in
|
||||
this.checkExistingAuth();
|
||||
},
|
||||
|
||||
checkExistingAuth() {
|
||||
if (Auth.isAuthenticated() && Auth.isAdmin()) {
|
||||
window.location.href = '/static/admin/dashboard.html';
|
||||
}
|
||||
},
|
||||
|
||||
clearErrors() {
|
||||
this.error = 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;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
},
|
||||
|
||||
async handleLogin() {
|
||||
if (!this.validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.clearErrors();
|
||||
|
||||
try {
|
||||
const response = await apiClient.post('/admin/auth/login', {
|
||||
username: this.credentials.username.trim(),
|
||||
password: this.credentials.password
|
||||
});
|
||||
|
||||
// Check if user is admin
|
||||
if (response.user.role !== 'admin') {
|
||||
throw new Error('Access denied. Admin privileges required.');
|
||||
}
|
||||
|
||||
// Store authentication data
|
||||
localStorage.setItem('admin_token', response.access_token);
|
||||
localStorage.setItem('admin_user', JSON.stringify(response.user));
|
||||
|
||||
// Show success message
|
||||
this.success = 'Login successful! Redirecting...';
|
||||
Utils.showToast('Login successful!', 'success', 2000);
|
||||
|
||||
// Redirect after short delay
|
||||
setTimeout(() => {
|
||||
window.location.href = '/static/admin/dashboard.html';
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
this.error = error.message || 'Login failed. Please check your credentials.';
|
||||
Utils.showToast(this.error, 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
// System monitoring
|
||||
@@ -1,338 +0,0 @@
|
||||
// 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);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,377 +0,0 @@
|
||||
// static/js/shared/api-client.js
|
||||
/**
|
||||
* API Client for Multi-Tenant Ecommerce Platform
|
||||
*
|
||||
* Provides utilities for:
|
||||
* - Making authenticated API calls
|
||||
* - Token management
|
||||
* - Error handling
|
||||
* - Request/response interceptors
|
||||
*/
|
||||
|
||||
const API_BASE_URL = '/api/v1';
|
||||
|
||||
/**
|
||||
* API Client Class
|
||||
*/
|
||||
class APIClient {
|
||||
constructor(baseURL = API_BASE_URL) {
|
||||
this.baseURL = baseURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored authentication token
|
||||
*/
|
||||
getToken() {
|
||||
return localStorage.getItem('admin_token') || localStorage.getItem('vendor_token');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default headers with authentication
|
||||
*/
|
||||
getHeaders(additionalHeaders = {}) {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...additionalHeaders
|
||||
};
|
||||
|
||||
const token = this.getToken();
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make API request
|
||||
*/
|
||||
async request(endpoint, options = {}) {
|
||||
const url = `${this.baseURL}${endpoint}`;
|
||||
|
||||
const config = {
|
||||
...options,
|
||||
headers: this.getHeaders(options.headers)
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, config);
|
||||
|
||||
// Handle 401 Unauthorized
|
||||
if (response.status === 401) {
|
||||
this.handleUnauthorized();
|
||||
throw new Error('Unauthorized - please login again');
|
||||
}
|
||||
|
||||
// Parse response
|
||||
const data = await response.json();
|
||||
|
||||
// Handle non-OK responses
|
||||
if (!response.ok) {
|
||||
throw new Error(data.detail || data.message || 'Request failed');
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
} catch (error) {
|
||||
console.error('API request failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET request
|
||||
*/
|
||||
async get(endpoint, params = {}) {
|
||||
const queryString = new URLSearchParams(params).toString();
|
||||
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
|
||||
|
||||
return this.request(url, {
|
||||
method: 'GET'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST request
|
||||
*/
|
||||
async post(endpoint, data = {}) {
|
||||
return this.request(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT request
|
||||
*/
|
||||
async put(endpoint, data = {}) {
|
||||
return this.request(endpoint, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE request
|
||||
*/
|
||||
async delete(endpoint) {
|
||||
return this.request(endpoint, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle unauthorized access
|
||||
*/
|
||||
handleUnauthorized() {
|
||||
localStorage.removeItem('admin_token');
|
||||
localStorage.removeItem('admin_user');
|
||||
localStorage.removeItem('vendor_token');
|
||||
localStorage.removeItem('vendor_user');
|
||||
|
||||
// Redirect to appropriate login page
|
||||
if (window.location.pathname.includes('/admin/')) {
|
||||
window.location.href = '/static/admin/login.html';
|
||||
} else if (window.location.pathname.includes('/vendor/')) {
|
||||
window.location.href = '/static/vendor/login.html';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create global API client instance
|
||||
const apiClient = new APIClient();
|
||||
|
||||
/**
|
||||
* Authentication helpers
|
||||
*/
|
||||
const Auth = {
|
||||
/**
|
||||
* Check if user is authenticated
|
||||
*/
|
||||
isAuthenticated() {
|
||||
const token = localStorage.getItem('admin_token') || localStorage.getItem('vendor_token');
|
||||
return !!token;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current user
|
||||
*/
|
||||
getCurrentUser() {
|
||||
const userStr = localStorage.getItem('admin_user') || localStorage.getItem('vendor_user');
|
||||
if (!userStr) return null;
|
||||
|
||||
try {
|
||||
return JSON.parse(userStr);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if user is admin
|
||||
*/
|
||||
isAdmin() {
|
||||
const user = this.getCurrentUser();
|
||||
return user && user.role === 'admin';
|
||||
},
|
||||
|
||||
/**
|
||||
* Login
|
||||
*/
|
||||
async login(username, password) {
|
||||
const response = await apiClient.post('/auth/login', {
|
||||
username,
|
||||
password
|
||||
});
|
||||
|
||||
// Store token and user
|
||||
if (response.user.role === 'admin') {
|
||||
localStorage.setItem('admin_token', response.access_token);
|
||||
localStorage.setItem('admin_user', JSON.stringify(response.user));
|
||||
} else {
|
||||
localStorage.setItem('vendor_token', response.access_token);
|
||||
localStorage.setItem('vendor_user', JSON.stringify(response.user));
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
logout() {
|
||||
localStorage.removeItem('admin_token');
|
||||
localStorage.removeItem('admin_user');
|
||||
localStorage.removeItem('vendor_token');
|
||||
localStorage.removeItem('vendor_user');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility functions
|
||||
*/
|
||||
const Utils = {
|
||||
/**
|
||||
* Format date
|
||||
*/
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-GB', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Format datetime
|
||||
*/
|
||||
formatDateTime(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('en-GB', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Format currency
|
||||
*/
|
||||
formatCurrency(amount, currency = 'EUR') {
|
||||
if (amount === null || amount === undefined) return '-';
|
||||
return new Intl.NumberFormat('en-GB', {
|
||||
style: 'currency',
|
||||
currency: currency
|
||||
}).format(amount);
|
||||
},
|
||||
|
||||
/**
|
||||
* Debounce function
|
||||
*/
|
||||
debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Show toast notification
|
||||
*/
|
||||
showToast(message, type = 'info', duration = 3000) {
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
|
||||
// Style
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 16px 24px;
|
||||
background: ${type === 'success' ? '#4caf50' : type === 'error' ? '#f44336' : '#2196f3'};
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 10000;
|
||||
animation: slideIn 0.3s ease;
|
||||
max-width: 400px;
|
||||
`;
|
||||
|
||||
// Add to page
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Remove after duration
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOut 0.3s ease';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, duration);
|
||||
},
|
||||
|
||||
/**
|
||||
* Confirm dialog
|
||||
*/
|
||||
async confirm(message, title = 'Confirm') {
|
||||
return window.confirm(`${title}\n\n${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Add animation styles
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Export for use in other scripts
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { APIClient, apiClient, Auth, Utils };
|
||||
}
|
||||
|
||||
// Table scroll detection helper
|
||||
function initTableScrollDetection() {
|
||||
const observer = new MutationObserver(() => {
|
||||
const tables = document.querySelectorAll('.table-responsive');
|
||||
tables.forEach(table => {
|
||||
if (!table.hasAttribute('data-scroll-initialized')) {
|
||||
table.setAttribute('data-scroll-initialized', 'true');
|
||||
|
||||
table.addEventListener('scroll', function() {
|
||||
if (this.scrollLeft > 0) {
|
||||
this.classList.add('is-scrolled');
|
||||
} else {
|
||||
this.classList.remove('is-scrolled');
|
||||
}
|
||||
});
|
||||
|
||||
// Check initial state
|
||||
if (table.scrollLeft > 0) {
|
||||
table.classList.add('is-scrolled');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initTableScrollDetection);
|
||||
} else {
|
||||
initTableScrollDetection();
|
||||
}
|
||||
Reference in New Issue
Block a user