admin and vendor backends features
This commit is contained in:
67
static/js/admin/admin-layout-templates.js
Normal file
67
static/js/admin/admin-layout-templates.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Admin Layout Templates
|
||||
* Header and Sidebar specific to Admin Portal
|
||||
*/
|
||||
|
||||
window.adminLayoutTemplates = {
|
||||
|
||||
/**
|
||||
* Admin Header
|
||||
*/
|
||||
header: () => `
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<button @click="toggleMenu()" class="menu-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h1 class="header-title">Admin Portal</h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="user-name" x-text="user?.username || 'Admin'"></span>
|
||||
<button @click="confirmLogout()" class="btn-logout">
|
||||
<i class="fas fa-sign-out-alt"></i> Logout
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
`,
|
||||
|
||||
/**
|
||||
* Admin Sidebar
|
||||
*/
|
||||
sidebar: () => `
|
||||
<aside class="admin-sidebar" :class="{ 'open': menuOpen }">
|
||||
<nav class="sidebar-nav">
|
||||
<a href="/admin/dashboard.html"
|
||||
class="nav-item"
|
||||
:class="{ 'active': isActive('dashboard') }">
|
||||
<i class="fas fa-tachometer-alt"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
<a href="/admin/vendors.html"
|
||||
class="nav-item"
|
||||
:class="{ 'active': isActive('vendors') }">
|
||||
<i class="fas fa-store"></i>
|
||||
<span>Vendors</span>
|
||||
</a>
|
||||
<a href="/admin/users.html"
|
||||
class="nav-item"
|
||||
:class="{ 'active': isActive('users') }">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Users</span>
|
||||
</a>
|
||||
<a href="/admin/marketplace.html"
|
||||
class="nav-item"
|
||||
:class="{ 'active': isActive('marketplace') }">
|
||||
<i class="fas fa-shopping-cart"></i>
|
||||
<span>Marketplace</span>
|
||||
</a>
|
||||
<a href="/admin/monitoring.html"
|
||||
class="nav-item"
|
||||
:class="{ 'active': isActive('monitoring') }">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
<span>Monitoring</span>
|
||||
</a>
|
||||
</nav>
|
||||
</aside>
|
||||
`
|
||||
};
|
||||
@@ -1 +1,203 @@
|
||||
// Admin dashboard
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
87
static/js/admin/login.js
Normal file
87
static/js/admin/login.js
Normal file
@@ -0,0 +1,87 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
338
static/js/admin/vendor-edit.js
Normal file
338
static/js/admin/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);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1 +1,241 @@
|
||||
// Vendor management
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
581
static/js/shared/alpine-components.js
Normal file
581
static/js/shared/alpine-components.js
Normal file
@@ -0,0 +1,581 @@
|
||||
/**
|
||||
* Alpine.js Components for Multi-Tenant E-commerce Platform
|
||||
* Universal component system for Admin, Vendor, and Shop sections
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// BASE MODAL SYSTEM
|
||||
// Universal modal functions used by all sections
|
||||
// =============================================================================
|
||||
|
||||
window.baseModalSystem = function() {
|
||||
return {
|
||||
// Confirmation Modal State
|
||||
confirmModal: {
|
||||
show: false,
|
||||
title: '',
|
||||
message: '',
|
||||
warning: '',
|
||||
buttonText: 'Confirm',
|
||||
buttonClass: 'btn-danger',
|
||||
onConfirm: null,
|
||||
onCancel: null
|
||||
},
|
||||
|
||||
// Success Modal State
|
||||
successModal: {
|
||||
show: false,
|
||||
title: 'Success',
|
||||
message: '',
|
||||
redirectUrl: null,
|
||||
redirectDelay: 2000
|
||||
},
|
||||
|
||||
// Error Modal State
|
||||
errorModal: {
|
||||
show: false,
|
||||
title: 'Error',
|
||||
message: '',
|
||||
details: ''
|
||||
},
|
||||
|
||||
// Loading State
|
||||
loading: false,
|
||||
|
||||
/**
|
||||
* Show confirmation modal
|
||||
* @param {Object} options - Modal configuration
|
||||
*/
|
||||
showConfirmModal(options) {
|
||||
this.confirmModal = {
|
||||
show: true,
|
||||
title: options.title || 'Confirm Action',
|
||||
message: options.message || 'Are you sure?',
|
||||
warning: options.warning || '',
|
||||
buttonText: options.buttonText || 'Confirm',
|
||||
buttonClass: options.buttonClass || 'btn-danger',
|
||||
onConfirm: options.onConfirm || null,
|
||||
onCancel: options.onCancel || null
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Close confirmation modal
|
||||
*/
|
||||
closeConfirmModal() {
|
||||
if (this.confirmModal.onCancel) {
|
||||
this.confirmModal.onCancel();
|
||||
}
|
||||
this.confirmModal.show = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle confirmation action
|
||||
*/
|
||||
async handleConfirm() {
|
||||
if (this.confirmModal.onConfirm) {
|
||||
this.closeConfirmModal();
|
||||
await this.confirmModal.onConfirm();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show success modal
|
||||
* @param {Object} options - Modal configuration
|
||||
*/
|
||||
showSuccessModal(options) {
|
||||
this.successModal = {
|
||||
show: true,
|
||||
title: options.title || 'Success',
|
||||
message: options.message || 'Operation completed successfully',
|
||||
redirectUrl: options.redirectUrl || null,
|
||||
redirectDelay: options.redirectDelay || 2000
|
||||
};
|
||||
|
||||
// Auto-redirect if URL provided
|
||||
if (this.successModal.redirectUrl) {
|
||||
setTimeout(() => {
|
||||
window.location.href = this.successModal.redirectUrl;
|
||||
}, this.successModal.redirectDelay);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Close success modal
|
||||
*/
|
||||
closeSuccessModal() {
|
||||
this.successModal.show = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show error modal
|
||||
* @param {Object} options - Modal configuration
|
||||
*/
|
||||
showErrorModal(options) {
|
||||
this.errorModal = {
|
||||
show: true,
|
||||
title: options.title || 'Error',
|
||||
message: options.message || 'An error occurred',
|
||||
details: options.details || ''
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Close error modal
|
||||
*/
|
||||
closeErrorModal() {
|
||||
this.errorModal.show = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show loading overlay
|
||||
*/
|
||||
showLoading() {
|
||||
this.loading = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide loading overlay
|
||||
*/
|
||||
hideLoading() {
|
||||
this.loading = false;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// ADMIN LAYOUT COMPONENT
|
||||
// Header, Sidebar, Navigation, Modals for Admin Section
|
||||
// =============================================================================
|
||||
|
||||
window.adminLayout = function() {
|
||||
return {
|
||||
...window.baseModalSystem(),
|
||||
|
||||
// Admin-specific state
|
||||
user: null,
|
||||
menuOpen: false,
|
||||
currentPage: '',
|
||||
|
||||
/**
|
||||
* Initialize admin layout
|
||||
*/
|
||||
async init() {
|
||||
this.currentPage = this.getCurrentPage();
|
||||
await this.loadUserData();
|
||||
},
|
||||
|
||||
/**
|
||||
* Load current admin user data
|
||||
*/
|
||||
async loadUserData() {
|
||||
try {
|
||||
const response = await apiClient.get('/admin/auth/me');
|
||||
this.user = response;
|
||||
} catch (error) {
|
||||
console.error('Failed to load user data:', error);
|
||||
// Redirect to login if not authenticated
|
||||
if (error.status === 401) {
|
||||
window.location.href = '/admin/login.html';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current page name from URL
|
||||
*/
|
||||
getCurrentPage() {
|
||||
const path = window.location.pathname;
|
||||
const page = path.split('/').pop().replace('.html', '');
|
||||
return page || 'dashboard';
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if menu item is active
|
||||
*/
|
||||
isActive(page) {
|
||||
return this.currentPage === page;
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle mobile menu
|
||||
*/
|
||||
toggleMenu() {
|
||||
this.menuOpen = !this.menuOpen;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show logout confirmation
|
||||
*/
|
||||
confirmLogout() {
|
||||
this.showConfirmModal({
|
||||
title: 'Confirm Logout',
|
||||
message: 'Are you sure you want to logout?',
|
||||
buttonText: 'Logout',
|
||||
buttonClass: 'btn-primary',
|
||||
onConfirm: () => this.logout()
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform logout
|
||||
*/
|
||||
async logout() {
|
||||
try {
|
||||
this.showLoading();
|
||||
await apiClient.post('/admin/auth/logout');
|
||||
window.location.href = '/admin/login.html';
|
||||
} catch (error) {
|
||||
this.hideLoading();
|
||||
this.showErrorModal({
|
||||
message: 'Logout failed',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// VENDOR LAYOUT COMPONENT
|
||||
// Header, Sidebar, Navigation, Modals for Vendor Dashboard
|
||||
// =============================================================================
|
||||
|
||||
window.vendorLayout = function() {
|
||||
return {
|
||||
...window.baseModalSystem(),
|
||||
|
||||
// Vendor-specific state
|
||||
user: null,
|
||||
vendor: null,
|
||||
menuOpen: false,
|
||||
currentPage: '',
|
||||
|
||||
/**
|
||||
* Initialize vendor layout
|
||||
*/
|
||||
async init() {
|
||||
this.currentPage = this.getCurrentPage();
|
||||
await this.loadUserData();
|
||||
},
|
||||
|
||||
/**
|
||||
* Load current vendor user data
|
||||
*/
|
||||
async loadUserData() {
|
||||
try {
|
||||
const response = await apiClient.get('/vendor/auth/me');
|
||||
this.user = response.user;
|
||||
this.vendor = response.vendor;
|
||||
} catch (error) {
|
||||
console.error('Failed to load user data:', error);
|
||||
if (error.status === 401) {
|
||||
window.location.href = '/vendor/login.html';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current page name from URL
|
||||
*/
|
||||
getCurrentPage() {
|
||||
const path = window.location.pathname;
|
||||
const page = path.split('/').pop().replace('.html', '');
|
||||
return page || 'dashboard';
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if menu item is active
|
||||
*/
|
||||
isActive(page) {
|
||||
return this.currentPage === page;
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle mobile menu
|
||||
*/
|
||||
toggleMenu() {
|
||||
this.menuOpen = !this.menuOpen;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show logout confirmation
|
||||
*/
|
||||
confirmLogout() {
|
||||
this.showConfirmModal({
|
||||
title: 'Confirm Logout',
|
||||
message: 'Are you sure you want to logout?',
|
||||
buttonText: 'Logout',
|
||||
buttonClass: 'btn-primary',
|
||||
onConfirm: () => this.logout()
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform logout
|
||||
*/
|
||||
async logout() {
|
||||
try {
|
||||
this.showLoading();
|
||||
await apiClient.post('/vendor/auth/logout');
|
||||
window.location.href = '/vendor/login.html';
|
||||
} catch (error) {
|
||||
this.hideLoading();
|
||||
this.showErrorModal({
|
||||
message: 'Logout failed',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// SHOP LAYOUT COMPONENT
|
||||
// Header, Cart, Search, Navigation for Customer-Facing Shop
|
||||
// =============================================================================
|
||||
|
||||
window.shopLayout = function() {
|
||||
return {
|
||||
...window.baseModalSystem(),
|
||||
|
||||
// Shop-specific state
|
||||
vendor: null,
|
||||
cart: null,
|
||||
cartCount: 0,
|
||||
sessionId: null,
|
||||
searchQuery: '',
|
||||
mobileMenuOpen: false,
|
||||
|
||||
/**
|
||||
* Initialize shop layout
|
||||
*/
|
||||
async init() {
|
||||
this.sessionId = this.getOrCreateSessionId();
|
||||
await this.detectVendor();
|
||||
if (this.vendor) {
|
||||
await this.loadCart();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Detect vendor from subdomain or vendor code
|
||||
*/
|
||||
async detectVendor() {
|
||||
try {
|
||||
const hostname = window.location.hostname;
|
||||
const subdomain = hostname.split('.')[0];
|
||||
|
||||
// Try to get vendor by subdomain first
|
||||
if (subdomain && subdomain !== 'localhost' && subdomain !== 'www') {
|
||||
this.vendor = await apiClient.get(`/public/vendors/by-subdomain/${subdomain}`);
|
||||
} else {
|
||||
// Fallback: Try to get vendor code from URL or localStorage
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const vendorCode = urlParams.get('vendor') || localStorage.getItem('vendorCode');
|
||||
|
||||
if (vendorCode) {
|
||||
this.vendor = await apiClient.get(`/public/vendors/by-code/${vendorCode}`);
|
||||
localStorage.setItem('vendorCode', vendorCode);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to detect vendor:', error);
|
||||
this.showErrorModal({
|
||||
message: 'Vendor not found',
|
||||
details: 'Unable to identify the store. Please check the URL.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get or create session ID for cart
|
||||
*/
|
||||
getOrCreateSessionId() {
|
||||
let sessionId = localStorage.getItem('cartSessionId');
|
||||
if (!sessionId) {
|
||||
sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
localStorage.setItem('cartSessionId', sessionId);
|
||||
}
|
||||
return sessionId;
|
||||
},
|
||||
|
||||
/**
|
||||
* Load cart from API
|
||||
*/
|
||||
async loadCart() {
|
||||
if (!this.vendor) return;
|
||||
|
||||
try {
|
||||
this.cart = await apiClient.get(
|
||||
`/public/vendors/${this.vendor.id}/cart/${this.sessionId}`
|
||||
);
|
||||
this.updateCartCount();
|
||||
} catch (error) {
|
||||
console.error('Failed to load cart:', error);
|
||||
this.cart = { items: [] };
|
||||
this.cartCount = 0;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update cart item count
|
||||
*/
|
||||
updateCartCount() {
|
||||
if (this.cart && this.cart.items) {
|
||||
this.cartCount = this.cart.items.reduce((sum, item) => sum + item.quantity, 0);
|
||||
} else {
|
||||
this.cartCount = 0;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add item to cart
|
||||
*/
|
||||
async addToCart(productId, quantity = 1) {
|
||||
if (!this.vendor) {
|
||||
this.showErrorModal({ message: 'Vendor not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.showLoading();
|
||||
await apiClient.post(
|
||||
`/public/vendors/${this.vendor.id}/cart/${this.sessionId}/items`,
|
||||
{ product_id: productId, quantity }
|
||||
);
|
||||
await this.loadCart();
|
||||
this.hideLoading();
|
||||
this.showSuccessModal({
|
||||
title: 'Added to Cart',
|
||||
message: 'Product added successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
this.hideLoading();
|
||||
this.showErrorModal({
|
||||
message: 'Failed to add to cart',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle mobile menu
|
||||
*/
|
||||
toggleMobileMenu() {
|
||||
this.mobileMenuOpen = !this.mobileMenuOpen;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle search
|
||||
*/
|
||||
handleSearch() {
|
||||
if (this.searchQuery.trim()) {
|
||||
window.location.href = `/shop/products.html?search=${encodeURIComponent(this.searchQuery)}`;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Go to cart page
|
||||
*/
|
||||
goToCart() {
|
||||
window.location.href = '/shop/cart.html';
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// SHOP ACCOUNT LAYOUT COMPONENT
|
||||
// Layout for customer account area (orders, profile, addresses)
|
||||
// =============================================================================
|
||||
|
||||
window.shopAccountLayout = function() {
|
||||
return {
|
||||
...window.shopLayout(),
|
||||
|
||||
// Account-specific state
|
||||
customer: null,
|
||||
currentPage: '',
|
||||
|
||||
/**
|
||||
* Initialize shop account layout
|
||||
*/
|
||||
async init() {
|
||||
this.currentPage = this.getCurrentPage();
|
||||
this.sessionId = this.getOrCreateSessionId();
|
||||
await this.detectVendor();
|
||||
await this.loadCustomerData();
|
||||
if (this.vendor) {
|
||||
await this.loadCart();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load customer data
|
||||
*/
|
||||
async loadCustomerData() {
|
||||
if (!this.vendor) return;
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(
|
||||
`/public/vendors/${this.vendor.id}/customers/me`
|
||||
);
|
||||
this.customer = response;
|
||||
} catch (error) {
|
||||
console.error('Failed to load customer data:', error);
|
||||
// Redirect to login if not authenticated
|
||||
if (error.status === 401) {
|
||||
window.location.href = `/shop/account/login.html?redirect=${encodeURIComponent(window.location.pathname)}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current page name from URL
|
||||
*/
|
||||
getCurrentPage() {
|
||||
const path = window.location.pathname;
|
||||
const page = path.split('/').pop().replace('.html', '');
|
||||
return page || 'orders';
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if menu item is active
|
||||
*/
|
||||
isActive(page) {
|
||||
return this.currentPage === page;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show logout confirmation
|
||||
*/
|
||||
confirmLogout() {
|
||||
this.showConfirmModal({
|
||||
title: 'Confirm Logout',
|
||||
message: 'Are you sure you want to logout?',
|
||||
buttonText: 'Logout',
|
||||
buttonClass: 'btn-primary',
|
||||
onConfirm: () => this.logoutCustomer()
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform customer logout
|
||||
*/
|
||||
async logoutCustomer() {
|
||||
if (!this.vendor) return;
|
||||
|
||||
try {
|
||||
this.showLoading();
|
||||
await apiClient.post(`/public/vendors/${this.vendor.id}/customers/logout`);
|
||||
window.location.href = '/shop/home.html';
|
||||
} catch (error) {
|
||||
this.hideLoading();
|
||||
this.showErrorModal({
|
||||
message: 'Logout failed',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -338,3 +338,40 @@ document.head.appendChild(style);
|
||||
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();
|
||||
}
|
||||
209
static/js/shared/modal-system.js
Normal file
209
static/js/shared/modal-system.js
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Modal System Helper Functions
|
||||
* Utility functions for modal operations across all sections
|
||||
*/
|
||||
|
||||
window.modalHelpers = {
|
||||
/**
|
||||
* Show a simple confirmation dialog
|
||||
* Returns a Promise that resolves with true/false
|
||||
*/
|
||||
async confirm(options) {
|
||||
return new Promise((resolve) => {
|
||||
const component = Alpine.$data(document.body);
|
||||
|
||||
component.showConfirmModal({
|
||||
title: options.title || 'Confirm Action',
|
||||
message: options.message || 'Are you sure?',
|
||||
warning: options.warning || '',
|
||||
buttonText: options.buttonText || 'Confirm',
|
||||
buttonClass: options.buttonClass || 'btn-danger',
|
||||
onConfirm: () => resolve(true),
|
||||
onCancel: () => resolve(false)
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a success message
|
||||
*/
|
||||
success(message, options = {}) {
|
||||
const component = Alpine.$data(document.body);
|
||||
|
||||
component.showSuccessModal({
|
||||
title: options.title || 'Success',
|
||||
message: message,
|
||||
redirectUrl: options.redirectUrl || null,
|
||||
redirectDelay: options.redirectDelay || 2000
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show an error message
|
||||
*/
|
||||
error(message, details = '') {
|
||||
const component = Alpine.$data(document.body);
|
||||
|
||||
component.showErrorModal({
|
||||
title: 'Error',
|
||||
message: message,
|
||||
details: details
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show API error with proper formatting
|
||||
*/
|
||||
apiError(error) {
|
||||
const component = Alpine.$data(document.body);
|
||||
|
||||
let message = 'An error occurred';
|
||||
let details = '';
|
||||
|
||||
if (error.message) {
|
||||
message = error.message;
|
||||
}
|
||||
|
||||
if (error.details) {
|
||||
details = typeof error.details === 'string'
|
||||
? error.details
|
||||
: JSON.stringify(error.details, null, 2);
|
||||
} else if (error.error_code) {
|
||||
details = `Error Code: ${error.error_code}`;
|
||||
}
|
||||
|
||||
component.showErrorModal({
|
||||
title: 'Error',
|
||||
message: message,
|
||||
details: details
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show loading overlay
|
||||
*/
|
||||
showLoading() {
|
||||
const component = Alpine.$data(document.body);
|
||||
component.showLoading();
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide loading overlay
|
||||
*/
|
||||
hideLoading() {
|
||||
const component = Alpine.$data(document.body);
|
||||
component.hideLoading();
|
||||
},
|
||||
|
||||
/**
|
||||
* Execute an async operation with loading state
|
||||
*/
|
||||
async withLoading(asyncFunction) {
|
||||
try {
|
||||
this.showLoading();
|
||||
const result = await asyncFunction();
|
||||
return result;
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Execute an async operation with error handling
|
||||
*/
|
||||
async withErrorHandling(asyncFunction, errorMessage = 'Operation failed') {
|
||||
try {
|
||||
return await asyncFunction();
|
||||
} catch (error) {
|
||||
console.error('Operation error:', error);
|
||||
this.apiError({
|
||||
message: errorMessage,
|
||||
details: error.message || error.toString()
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Execute an async operation with both loading and error handling
|
||||
*/
|
||||
async execute(asyncFunction, options = {}) {
|
||||
const {
|
||||
errorMessage = 'Operation failed',
|
||||
successMessage = null,
|
||||
redirectUrl = null
|
||||
} = options;
|
||||
|
||||
try {
|
||||
this.showLoading();
|
||||
const result = await asyncFunction();
|
||||
|
||||
if (successMessage) {
|
||||
this.success(successMessage, { redirectUrl });
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Operation error:', error);
|
||||
this.apiError({
|
||||
message: errorMessage,
|
||||
details: error.message || error.toString()
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Confirm a destructive action
|
||||
*/
|
||||
async confirmDelete(itemName, itemType = 'item') {
|
||||
return this.confirm({
|
||||
title: `Delete ${itemType}`,
|
||||
message: `Are you sure you want to delete "${itemName}"?`,
|
||||
warning: 'This action cannot be undone.',
|
||||
buttonText: 'Delete',
|
||||
buttonClass: 'btn-danger'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Confirm logout
|
||||
*/
|
||||
async confirmLogout() {
|
||||
return this.confirm({
|
||||
title: 'Confirm Logout',
|
||||
message: 'Are you sure you want to logout?',
|
||||
buttonText: 'Logout',
|
||||
buttonClass: 'btn-primary'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show validation errors
|
||||
*/
|
||||
validationError(errors) {
|
||||
let message = 'Please correct the following errors:';
|
||||
let details = '';
|
||||
|
||||
if (Array.isArray(errors)) {
|
||||
details = errors.join('\n');
|
||||
} else if (typeof errors === 'object') {
|
||||
details = Object.entries(errors)
|
||||
.map(([field, error]) => `${field}: ${error}`)
|
||||
.join('\n');
|
||||
} else {
|
||||
details = errors.toString();
|
||||
}
|
||||
|
||||
this.error(message, details);
|
||||
}
|
||||
};
|
||||
|
||||
// Shorthand aliases for convenience
|
||||
window.showConfirm = window.modalHelpers.confirm.bind(window.modalHelpers);
|
||||
window.showSuccess = window.modalHelpers.success.bind(window.modalHelpers);
|
||||
window.showError = window.modalHelpers.error.bind(window.modalHelpers);
|
||||
window.showLoading = window.modalHelpers.showLoading.bind(window.modalHelpers);
|
||||
window.hideLoading = window.modalHelpers.hideLoading.bind(window.modalHelpers);
|
||||
114
static/js/shared/modal-templates.js
Normal file
114
static/js/shared/modal-templates.js
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Universal Modal Templates
|
||||
* Shared across all sections: Admin, Vendor, and Shop
|
||||
*/
|
||||
|
||||
window.modalTemplates = {
|
||||
|
||||
/**
|
||||
* Confirmation Modal
|
||||
*/
|
||||
confirmModal: () => `
|
||||
<div x-show="confirmModal.show"
|
||||
x-cloak
|
||||
class="modal-backdrop"
|
||||
@click.self="closeConfirmModal()">
|
||||
<div class="modal-container" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" x-text="confirmModal.title"></h3>
|
||||
<button @click="closeConfirmModal()" class="modal-close">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="modal-message" x-text="confirmModal.message"></p>
|
||||
<div x-show="confirmModal.warning" class="modal-warning">
|
||||
<p x-text="confirmModal.warning"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button @click="closeConfirmModal()" class="btn btn-outline">
|
||||
Cancel
|
||||
</button>
|
||||
<button @click="handleConfirm()"
|
||||
class="btn"
|
||||
:class="confirmModal.buttonClass"
|
||||
x-text="confirmModal.buttonText">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
/**
|
||||
* Success Modal
|
||||
*/
|
||||
successModal: () => `
|
||||
<div x-show="successModal.show"
|
||||
x-cloak
|
||||
class="modal-backdrop"
|
||||
@click.self="closeSuccessModal()">
|
||||
<div class="modal-container" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" x-text="successModal.title"></h3>
|
||||
<button @click="closeSuccessModal()" class="modal-close">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-icon success">
|
||||
<i class="fas fa-check"></i>
|
||||
</div>
|
||||
<p class="modal-message text-center" x-text="successModal.message"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button @click="closeSuccessModal()" class="btn btn-primary">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
/**
|
||||
* Error Modal
|
||||
*/
|
||||
errorModal: () => `
|
||||
<div x-show="errorModal.show"
|
||||
x-cloak
|
||||
class="modal-backdrop"
|
||||
@click.self="closeErrorModal()">
|
||||
<div class="modal-container" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" x-text="errorModal.title"></h3>
|
||||
<button @click="closeErrorModal()" class="modal-close">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-icon error">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
<p class="modal-message text-center" x-text="errorModal.message"></p>
|
||||
<div x-show="errorModal.details" class="modal-details" x-text="errorModal.details"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button @click="closeErrorModal()" class="btn btn-primary">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
/**
|
||||
* Loading Overlay
|
||||
*/
|
||||
loadingOverlay: () => `
|
||||
<div x-show="loading"
|
||||
x-cloak
|
||||
class="loading-overlay">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
103
static/js/shop/shop-layout-templates.js
Normal file
103
static/js/shop/shop-layout-templates.js
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Shop Layout Templates
|
||||
* Header and Navigation specific to Customer-Facing Shop
|
||||
*/
|
||||
|
||||
window.shopLayoutTemplates = {
|
||||
|
||||
/**
|
||||
* Shop Header
|
||||
*/
|
||||
header: () => `
|
||||
<header class="shop-header">
|
||||
<div class="shop-header-top">
|
||||
<a href="/shop/home.html" class="shop-logo" x-text="vendor?.name || 'Shop'"></a>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="shop-search">
|
||||
<form @submit.prevent="handleSearch()" class="search-form">
|
||||
<input type="text"
|
||||
x-model="searchQuery"
|
||||
class="search-input"
|
||||
placeholder="Search products...">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="shop-actions">
|
||||
<button @click="goToCart()" class="cart-button">
|
||||
<i class="fas fa-shopping-cart"></i>
|
||||
<span x-show="cartCount > 0"
|
||||
x-text="cartCount"
|
||||
class="cart-count">
|
||||
</span>
|
||||
</button>
|
||||
<a href="/shop/account/login.html" class="btn btn-outline">
|
||||
<i class="fas fa-user"></i> Account
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="shop-nav">
|
||||
<a href="/shop/home.html"
|
||||
class="shop-nav-item"
|
||||
:class="{ 'active': isActive('home') }">
|
||||
Home
|
||||
</a>
|
||||
<a href="/shop/products.html"
|
||||
class="shop-nav-item"
|
||||
:class="{ 'active': isActive('products') }">
|
||||
Products
|
||||
</a>
|
||||
<a href="/shop/categories.html"
|
||||
class="shop-nav-item"
|
||||
:class="{ 'active': isActive('categories') }">
|
||||
Categories
|
||||
</a>
|
||||
<a href="/shop/about.html"
|
||||
class="shop-nav-item"
|
||||
:class="{ 'active': isActive('about') }">
|
||||
About
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
`,
|
||||
|
||||
/**
|
||||
* Shop Account Sidebar
|
||||
*/
|
||||
accountSidebar: () => `
|
||||
<aside class="account-sidebar">
|
||||
<nav>
|
||||
<a href="/shop/account/orders.html"
|
||||
class="account-nav-item"
|
||||
:class="{ 'active': isActive('orders') }">
|
||||
<i class="fas fa-shopping-bag"></i>
|
||||
<span>My Orders</span>
|
||||
</a>
|
||||
<a href="/shop/account/profile.html"
|
||||
class="account-nav-item"
|
||||
:class="{ 'active': isActive('profile') }">
|
||||
<i class="fas fa-user"></i>
|
||||
<span>Profile</span>
|
||||
</a>
|
||||
<a href="/shop/account/addresses.html"
|
||||
class="account-nav-item"
|
||||
:class="{ 'active': isActive('addresses') }">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
<span>Addresses</span>
|
||||
</a>
|
||||
<button @click="confirmLogout()"
|
||||
class="account-nav-item"
|
||||
style="width: 100%; text-align: left; background: none; border: none; cursor: pointer;">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</nav>
|
||||
</aside>
|
||||
`
|
||||
};
|
||||
114
static/js/vendor/dashboard.js
vendored
114
static/js/vendor/dashboard.js
vendored
@@ -1 +1,113 @@
|
||||
// Vendor dashboard
|
||||
// Vendor Dashboard Component
|
||||
function vendorDashboard() {
|
||||
return {
|
||||
currentUser: {},
|
||||
vendor: null,
|
||||
vendorRole: '',
|
||||
currentSection: 'dashboard',
|
||||
loading: false,
|
||||
stats: {
|
||||
products_count: 0,
|
||||
orders_count: 0,
|
||||
customers_count: 0,
|
||||
revenue: 0
|
||||
},
|
||||
|
||||
init() {
|
||||
if (!this.checkAuth()) {
|
||||
return;
|
||||
}
|
||||
this.loadDashboard();
|
||||
},
|
||||
|
||||
checkAuth() {
|
||||
const token = localStorage.getItem('vendor_token');
|
||||
const user = localStorage.getItem('vendor_user');
|
||||
const vendorContext = localStorage.getItem('vendor_context');
|
||||
const vendorRole = localStorage.getItem('vendor_role');
|
||||
|
||||
if (!token || !user || !vendorContext) {
|
||||
// Get vendor code from URL
|
||||
const vendorCode = this.getVendorCodeFromUrl();
|
||||
const redirectUrl = vendorCode ?
|
||||
`/vendor/${vendorCode}/login` :
|
||||
'/static/vendor/login.html';
|
||||
window.location.href = redirectUrl;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.currentUser = JSON.parse(user);
|
||||
this.vendor = JSON.parse(vendorContext);
|
||||
this.vendorRole = vendorRole || 'Member';
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('Error parsing stored data:', e);
|
||||
localStorage.removeItem('vendor_token');
|
||||
localStorage.removeItem('vendor_user');
|
||||
localStorage.removeItem('vendor_context');
|
||||
localStorage.removeItem('vendor_role');
|
||||
window.location.href = '/static/vendor/login.html';
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
getVendorCodeFromUrl() {
|
||||
// Try to get vendor code from URL path
|
||||
const pathParts = window.location.pathname.split('/').filter(p => p);
|
||||
const vendorIndex = pathParts.indexOf('vendor');
|
||||
if (vendorIndex !== -1 && pathParts[vendorIndex + 1]) {
|
||||
const code = pathParts[vendorIndex + 1];
|
||||
if (!['login', 'dashboard', 'admin', 'products', 'orders'].includes(code.toLowerCase())) {
|
||||
return code.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to query parameter
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('vendor');
|
||||
},
|
||||
|
||||
async handleLogout() {
|
||||
const confirmed = await Utils.confirm(
|
||||
'Are you sure you want to logout?',
|
||||
'Confirm Logout'
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
localStorage.removeItem('vendor_token');
|
||||
localStorage.removeItem('vendor_user');
|
||||
localStorage.removeItem('vendor_context');
|
||||
localStorage.removeItem('vendor_role');
|
||||
|
||||
Utils.showToast('Logged out successfully', 'success', 2000);
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = `/vendor/${this.vendor.vendor_code}/login`;
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
|
||||
async loadDashboard() {
|
||||
this.loading = true;
|
||||
try {
|
||||
// In future slices, load actual dashboard data
|
||||
// const data = await apiClient.get(`/vendor/dashboard/stats`);
|
||||
// this.stats = data;
|
||||
|
||||
// For now, show placeholder data
|
||||
this.stats = {
|
||||
products_count: 0,
|
||||
orders_count: 0,
|
||||
customers_count: 0,
|
||||
revenue: 0
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to load dashboard:', error);
|
||||
Utils.showToast('Failed to load dashboard data', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
170
static/js/vendor/login.js
vendored
Normal file
170
static/js/vendor/login.js
vendored
Normal file
@@ -0,0 +1,170 @@
|
||||
// Vendor Login Component
|
||||
function vendorLogin() {
|
||||
return {
|
||||
vendor: null,
|
||||
credentials: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
loading: false,
|
||||
checked: false,
|
||||
error: null,
|
||||
success: null,
|
||||
errors: {},
|
||||
|
||||
init() {
|
||||
// Check if already logged in
|
||||
if (this.checkExistingAuth()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect vendor from URL
|
||||
this.detectVendor();
|
||||
},
|
||||
|
||||
checkExistingAuth() {
|
||||
const token = localStorage.getItem('vendor_token');
|
||||
const vendorContext = localStorage.getItem('vendor_context');
|
||||
|
||||
if (token && vendorContext) {
|
||||
try {
|
||||
const vendor = JSON.parse(vendorContext);
|
||||
window.location.href = `/vendor/${vendor.vendor_code}/dashboard`;
|
||||
return true;
|
||||
} catch (e) {
|
||||
localStorage.removeItem('vendor_token');
|
||||
localStorage.removeItem('vendor_context');
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
async detectVendor() {
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const vendorCode = this.getVendorCodeFromUrl();
|
||||
|
||||
if (!vendorCode) {
|
||||
this.error = 'Vendor code not found in URL. Please use the correct vendor login link.';
|
||||
this.checked = true;
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Detected vendor code:', vendorCode);
|
||||
|
||||
// Fetch vendor information
|
||||
const response = await fetch(`/api/v1/public/vendors/by-code/${vendorCode}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Vendor not found');
|
||||
}
|
||||
|
||||
this.vendor = await response.json();
|
||||
this.checked = true;
|
||||
console.log('Loaded vendor:', this.vendor);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error detecting vendor:', error);
|
||||
this.error = 'Unable to load vendor information. The vendor may not exist or is inactive.';
|
||||
this.checked = true;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
getVendorCodeFromUrl() {
|
||||
// Try multiple methods to get vendor code
|
||||
|
||||
// Method 1: From URL path /vendor/VENDORCODE/login or /vendor/VENDORCODE/
|
||||
const pathParts = window.location.pathname.split('/').filter(p => p);
|
||||
const vendorIndex = pathParts.indexOf('vendor');
|
||||
if (vendorIndex !== -1 && pathParts[vendorIndex + 1]) {
|
||||
const code = pathParts[vendorIndex + 1];
|
||||
// Don't return if it's a generic route like 'login', 'dashboard', etc.
|
||||
if (!['login', 'dashboard', 'admin', 'products', 'orders'].includes(code.toLowerCase())) {
|
||||
return code.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
// Method 2: From query parameter ?vendor=VENDORCODE
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const queryVendor = urlParams.get('vendor');
|
||||
if (queryVendor) {
|
||||
return queryVendor.toUpperCase();
|
||||
}
|
||||
|
||||
// Method 3: From subdomain (for production)
|
||||
const hostname = window.location.hostname;
|
||||
const parts = hostname.split('.');
|
||||
if (parts.length > 2 && parts[0] !== 'www') {
|
||||
// Assume subdomain is vendor code
|
||||
return parts[0].toUpperCase();
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
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('/vendor/auth/login', {
|
||||
username: this.credentials.username.trim(),
|
||||
password: this.credentials.password,
|
||||
vendor_code: this.vendor.vendor_code
|
||||
});
|
||||
|
||||
// Store authentication data
|
||||
localStorage.setItem('vendor_token', response.access_token);
|
||||
localStorage.setItem('vendor_user', JSON.stringify(response.user));
|
||||
localStorage.setItem('vendor_context', JSON.stringify(response.vendor));
|
||||
localStorage.setItem('vendor_role', response.vendor_role);
|
||||
|
||||
// Show success message
|
||||
this.success = 'Login successful! Redirecting...';
|
||||
Utils.showToast('Login successful!', 'success', 2000);
|
||||
|
||||
// Redirect after short delay
|
||||
setTimeout(() => {
|
||||
window.location.href = `/vendor/${this.vendor.vendor_code}/dashboard`;
|
||||
}, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
static/js/vendor/vendor-layout-templates.js
vendored
Normal file
67
static/js/vendor/vendor-layout-templates.js
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Vendor Layout Templates
|
||||
* Header and Sidebar specific to Vendor Dashboard
|
||||
*/
|
||||
|
||||
window.vendorLayoutTemplates = {
|
||||
|
||||
/**
|
||||
* Vendor Header
|
||||
*/
|
||||
header: () => `
|
||||
<header class="vendor-header">
|
||||
<div class="header-left">
|
||||
<button @click="toggleMenu()" class="menu-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h1 class="header-title">Vendor Dashboard</h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="user-name" x-text="vendor?.name || 'Vendor'"></span>
|
||||
<button @click="confirmLogout()" class="btn-logout">
|
||||
<i class="fas fa-sign-out-alt"></i> Logout
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
`,
|
||||
|
||||
/**
|
||||
* Vendor Sidebar
|
||||
*/
|
||||
sidebar: () => `
|
||||
<aside class="vendor-sidebar" :class="{ 'open': menuOpen }">
|
||||
<nav class="sidebar-nav">
|
||||
<a href="/vendor/dashboard.html"
|
||||
class="nav-item"
|
||||
:class="{ 'active': isActive('dashboard') }">
|
||||
<i class="fas fa-tachometer-alt"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
<a href="/vendor/products.html"
|
||||
class="nav-item"
|
||||
:class="{ 'active': isActive('products') }">
|
||||
<i class="fas fa-box"></i>
|
||||
<span>Products</span>
|
||||
</a>
|
||||
<a href="/vendor/orders.html"
|
||||
class="nav-item"
|
||||
:class="{ 'active': isActive('orders') }">
|
||||
<i class="fas fa-shopping-bag"></i>
|
||||
<span>Orders</span>
|
||||
</a>
|
||||
<a href="/vendor/customers.html"
|
||||
class="nav-item"
|
||||
:class="{ 'active': isActive('customers') }">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Customers</span>
|
||||
</a>
|
||||
<a href="/vendor/settings.html"
|
||||
class="nav-item"
|
||||
:class="{ 'active': isActive('settings') }">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
</nav>
|
||||
</aside>
|
||||
`
|
||||
};
|
||||
Reference in New Issue
Block a user