admin panel migration to jinja
This commit is contained in:
@@ -1,219 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html :class="{ 'theme-dark': dark }" x-data="adminDashboard()" lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Admin Dashboard - Multi-Tenant Platform</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="/static/admin/css/tailwind.output.css" />
|
||||
<style>
|
||||
[x-cloak] { display: none !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body x-cloak>
|
||||
<div class="flex h-screen bg-gray-50 dark:bg-gray-900" :class="{ 'overflow-hidden': isSideMenuOpen }">
|
||||
<!-- Sidebar Container (loaded via partial-loader) -->
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div class="flex flex-col flex-1 w-full">
|
||||
<!-- Header Container (loaded via partial-loader) -->
|
||||
<div id="header-container"></div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="h-full overflow-y-auto">
|
||||
<div class="container px-6 mx-auto grid">
|
||||
<!-- Page Header with Refresh Button -->
|
||||
<div class="flex items-center justify-between my-6">
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Dashboard
|
||||
</h2>
|
||||
<button
|
||||
@click="refresh()"
|
||||
:disabled="loading"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span x-show="!loading" x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="loading" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="loading ? 'Loading...' : 'Refresh'"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="text-center py-12">
|
||||
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading dashboard...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div x-show="error && !loading" class="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg flex items-start">
|
||||
<span x-html="$icon('exclamation', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
|
||||
<div>
|
||||
<p class="font-semibold">Error loading dashboard</p>
|
||||
<p class="text-sm" x-text="error"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div x-show="!loading" class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
||||
<!-- Card: Total Vendors -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full dark:text-orange-100 dark:bg-orange-500">
|
||||
<span x-html="$icon('user-group', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Total Vendors
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.totalVendors">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Active Users -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
|
||||
<span x-html="$icon('users', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Active Users
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.activeUsers">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Verified Vendors -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
|
||||
<span x-html="$icon('badge-check', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Verified Vendors
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.verifiedVendors">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Import Jobs -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-teal-500 bg-teal-100 rounded-full dark:text-teal-100 dark:bg-teal-500">
|
||||
<span x-html="$icon('download', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Import Jobs
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.importJobs">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Vendors Table -->
|
||||
<div x-show="!loading" class="w-full overflow-hidden rounded-lg shadow-xs">
|
||||
<div class="w-full overflow-x-auto">
|
||||
<table class="w-full whitespace-no-wrap">
|
||||
<thead>
|
||||
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
|
||||
<th class="px-4 py-3">Vendor</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
<th class="px-4 py-3">Created</th>
|
||||
<th class="px-4 py-3">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<template x-if="recentVendors.length === 0">
|
||||
<tr>
|
||||
<td colspan="4" class="px-4 py-8 text-center text-gray-600 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('user-group', 'w-12 h-12 mb-2 text-gray-300')"></span>
|
||||
<p>No vendors yet.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template x-for="vendor in recentVendors" :key="vendor.vendor_code">
|
||||
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center text-sm">
|
||||
<div class="relative hidden w-8 h-8 mr-3 rounded-full md:block">
|
||||
<div class="absolute inset-0 rounded-full bg-purple-100 dark:bg-purple-600 flex items-center justify-center">
|
||||
<span class="text-xs font-semibold text-purple-600 dark:text-purple-100" x-text="vendor.name?.charAt(0).toUpperCase() || '?'"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold" x-text="vendor.name"></p>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400" x-text="vendor.vendor_code"></p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span class="inline-flex items-center px-2 py-1 font-semibold leading-tight rounded-full"
|
||||
:class="vendor.is_verified ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-orange-700 bg-orange-100 dark:text-white dark:bg-orange-600'">
|
||||
<span x-show="vendor.is_verified" x-html="$icon('badge-check', 'w-3 h-3 mr-1')"></span>
|
||||
<span x-text="vendor.is_verified ? 'Verified' : 'Pending'"></span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm" x-text="formatDate(vendor.created_at)">
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<button
|
||||
@click="viewVendor(vendor.vendor_code)"
|
||||
class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-gray-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
||||
title="View vendor"
|
||||
>
|
||||
<span x-html="$icon('eye', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts in CORRECT ORDER -->
|
||||
<!-- 1. Partial Loader (auto-detects admin area) -->
|
||||
<script src="/static/shared/js/partial-loader.js"></script>
|
||||
|
||||
<!-- 2. Icons Helper -->
|
||||
<script src="/static/shared/js/icons.js"></script>
|
||||
|
||||
<!-- 3. Load Partials (before Alpine initializes) -->
|
||||
<script>
|
||||
(async () => {
|
||||
await window.partialLoader.loadAll({
|
||||
'header-container': 'header.html',
|
||||
'sidebar-container': 'sidebar.html'
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- 4. Base Alpine Data -->
|
||||
<script src="/static/admin/js/init-alpine.js"></script>
|
||||
|
||||
<!-- 5. API Client & Utils -->
|
||||
<script src="/static/shared/js/api-client.js"></script>
|
||||
<script src="/static/shared/js/utils.js"></script>
|
||||
|
||||
<!-- 6. Alpine.js v3 (deferred) -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
|
||||
|
||||
<!-- 7. Dashboard-specific script (AFTER Alpine loads) -->
|
||||
<script src="/static/admin/js/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
140
static/admin/js/users.js
Normal file
140
static/admin/js/users.js
Normal file
@@ -0,0 +1,140 @@
|
||||
function adminUsers() {
|
||||
return {
|
||||
// State
|
||||
users: [],
|
||||
loading: false,
|
||||
filters: {
|
||||
search: '',
|
||||
role: '',
|
||||
is_active: ''
|
||||
},
|
||||
stats: {
|
||||
total: 0,
|
||||
active: 0,
|
||||
vendors: 0,
|
||||
admins: 0
|
||||
},
|
||||
pagination: {
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
total: 0,
|
||||
pages: 0
|
||||
},
|
||||
|
||||
// Initialization
|
||||
init() {
|
||||
Logger.info('Users page initialized', 'USERS');
|
||||
this.loadUsers();
|
||||
this.loadStats();
|
||||
},
|
||||
|
||||
// Load users from API
|
||||
async loadUsers() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: this.pagination.page,
|
||||
per_page: this.pagination.per_page,
|
||||
...this.filters
|
||||
});
|
||||
|
||||
const response = await ApiClient.get(`/admin/users?${params}`);
|
||||
|
||||
if (response.items) {
|
||||
this.users = response.items;
|
||||
this.pagination.total = response.total;
|
||||
this.pagination.pages = response.pages;
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('Failed to load users', 'USERS', error);
|
||||
Utils.showToast('Failed to load users', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Load statistics
|
||||
async loadStats() {
|
||||
try {
|
||||
const response = await ApiClient.get('/admin/users/stats');
|
||||
if (response) {
|
||||
this.stats = response;
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('Failed to load stats', 'USERS', error);
|
||||
}
|
||||
},
|
||||
|
||||
// Search with debounce
|
||||
debouncedSearch: Utils.debounce(function() {
|
||||
this.pagination.page = 1;
|
||||
this.loadUsers();
|
||||
}, 500),
|
||||
|
||||
// Pagination
|
||||
nextPage() {
|
||||
if (this.pagination.page < this.pagination.pages) {
|
||||
this.pagination.page++;
|
||||
this.loadUsers();
|
||||
}
|
||||
},
|
||||
|
||||
previousPage() {
|
||||
if (this.pagination.page > 1) {
|
||||
this.pagination.page--;
|
||||
this.loadUsers();
|
||||
}
|
||||
},
|
||||
|
||||
// Actions
|
||||
viewUser(user) {
|
||||
Logger.info('View user', 'USERS', user);
|
||||
// TODO: Open view modal
|
||||
},
|
||||
|
||||
editUser(user) {
|
||||
Logger.info('Edit user', 'USERS', user);
|
||||
// TODO: Open edit modal
|
||||
},
|
||||
|
||||
async toggleUserStatus(user) {
|
||||
const action = user.is_active ? 'deactivate' : 'activate';
|
||||
if (!confirm(`Are you sure you want to ${action} ${user.username}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ApiClient.put(`/admin/users/${user.id}/status`, {
|
||||
is_active: !user.is_active
|
||||
});
|
||||
Utils.showToast(`User ${action}d successfully`, 'success');
|
||||
this.loadUsers();
|
||||
this.loadStats();
|
||||
} catch (error) {
|
||||
Logger.error(`Failed to ${action} user`, 'USERS', error);
|
||||
Utils.showToast(`Failed to ${action} user`, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async deleteUser(user) {
|
||||
if (!confirm(`Are you sure you want to delete ${user.username}? This action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ApiClient.delete(`/admin/users/${user.id}`);
|
||||
Utils.showToast('User deleted successfully', 'success');
|
||||
this.loadUsers();
|
||||
this.loadStats();
|
||||
} catch (error) {
|
||||
Logger.error('Failed to delete user', 'USERS', error);
|
||||
Utils.showToast('Failed to delete user', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
openCreateModal() {
|
||||
Logger.info('Open create user modal', 'USERS');
|
||||
// TODO: Open create modal
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,338 +1,206 @@
|
||||
// static/js/admin/vendor-edit.js
|
||||
// static/admin/js/vendor-edit.js
|
||||
|
||||
function vendorEdit() {
|
||||
// Log levels: 0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug
|
||||
const VENDOR_EDIT_LOG_LEVEL = 3;
|
||||
|
||||
const editLog = {
|
||||
error: (...args) => VENDOR_EDIT_LOG_LEVEL >= 1 && console.error('❌ [VENDOR_EDIT ERROR]', ...args),
|
||||
warn: (...args) => VENDOR_EDIT_LOG_LEVEL >= 2 && console.warn('⚠️ [VENDOR_EDIT WARN]', ...args),
|
||||
info: (...args) => VENDOR_EDIT_LOG_LEVEL >= 3 && console.info('ℹ️ [VENDOR_EDIT INFO]', ...args),
|
||||
debug: (...args) => VENDOR_EDIT_LOG_LEVEL >= 4 && console.log('🔍 [VENDOR_EDIT DEBUG]', ...args)
|
||||
};
|
||||
|
||||
function adminVendorEdit() {
|
||||
return {
|
||||
currentUser: {},
|
||||
vendor: {},
|
||||
// Inherit base layout functionality from init-alpine.js
|
||||
...data(),
|
||||
|
||||
// Vendor edit page specific state
|
||||
currentPage: 'vendor-edit',
|
||||
vendor: null,
|
||||
formData: {},
|
||||
errors: {},
|
||||
loadingVendor: true,
|
||||
loadingVendor: false,
|
||||
saving: false,
|
||||
vendorId: null,
|
||||
vendorCode: null,
|
||||
|
||||
// Confirmation modal
|
||||
confirmModal: {
|
||||
show: false,
|
||||
title: '',
|
||||
message: '',
|
||||
warning: '',
|
||||
buttonText: '',
|
||||
buttonClass: 'btn-primary',
|
||||
onConfirm: () => {},
|
||||
onCancel: null
|
||||
},
|
||||
// Initialize
|
||||
async init() {
|
||||
editLog.info('=== VENDOR EDIT PAGE INITIALIZING ===');
|
||||
|
||||
// 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 = '/admin/login';
|
||||
// Prevent multiple initializations
|
||||
if (window._vendorEditInitialized) {
|
||||
editLog.warn('Vendor edit page already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._vendorEditInitialized = true;
|
||||
|
||||
this.currentUser = Auth.getCurrentUser();
|
||||
console.log('Current user:', this.currentUser.username);
|
||||
// Get vendor code from URL
|
||||
const path = window.location.pathname;
|
||||
const match = path.match(/\/admin\/vendors\/([^\/]+)\/edit/);
|
||||
|
||||
// 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 = '/admin/dashboard.html#vendors';
|
||||
return;
|
||||
if (match) {
|
||||
this.vendorCode = match[1];
|
||||
editLog.info('Editing vendor:', this.vendorCode);
|
||||
await this.loadVendor();
|
||||
} else {
|
||||
editLog.error('No vendor code in URL');
|
||||
Utils.showToast('Invalid vendor URL', 'error');
|
||||
setTimeout(() => window.location.href = '/admin/vendors', 2000);
|
||||
}
|
||||
|
||||
console.log('Vendor ID:', this.vendorId);
|
||||
|
||||
// Load vendor details
|
||||
this.loadVendor();
|
||||
editLog.info('=== VENDOR EDIT PAGE INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
// Load vendor data
|
||||
async loadVendor() {
|
||||
editLog.info('Loading vendor data...');
|
||||
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
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const response = await apiClient.get(`/admin/vendors/${this.vendorCode}`);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.vendor = response;
|
||||
|
||||
// Initialize 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 || ''
|
||||
name: response.name || '',
|
||||
subdomain: response.subdomain || '',
|
||||
description: response.description || '',
|
||||
contact_email: response.contact_email || '',
|
||||
contact_phone: response.contact_phone || '',
|
||||
website: response.website || '',
|
||||
business_address: response.business_address || '',
|
||||
tax_number: response.tax_number || ''
|
||||
};
|
||||
|
||||
console.log('Form data populated');
|
||||
editLog.info(`Vendor loaded in ${duration}ms`, {
|
||||
vendor_code: this.vendor.vendor_code,
|
||||
name: this.vendor.name
|
||||
});
|
||||
editLog.debug('Form data initialized:', this.formData);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to load vendor:', error);
|
||||
Utils.showToast('Failed to load vendor details: ' + (error.message || 'Unknown error'), 'error');
|
||||
window.location.href = '/admin/dashboard';
|
||||
editLog.error('Failed to load vendor:', error);
|
||||
Utils.showToast('Failed to load vendor', 'error');
|
||||
setTimeout(() => window.location.href = '/admin/vendors', 2000);
|
||||
} finally {
|
||||
this.loadingVendor = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Format subdomain
|
||||
formatSubdomain() {
|
||||
this.formData.subdomain = this.formData.subdomain
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, '');
|
||||
editLog.debug('Subdomain formatted:', this.formData.subdomain);
|
||||
},
|
||||
|
||||
// Submit form
|
||||
async handleSubmit() {
|
||||
console.log('Submitting vendor update...');
|
||||
editLog.info('=== SUBMITTING VENDOR UPDATE ===');
|
||||
editLog.debug('Form data:', this.formData);
|
||||
|
||||
this.errors = {};
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
const updatedVendor = await apiClient.put(
|
||||
`/admin/vendors/${this.vendorId}`,
|
||||
const startTime = Date.now();
|
||||
const response = await apiClient.put(
|
||||
`/admin/vendors/${this.vendorCode}`,
|
||||
this.formData
|
||||
);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
console.log('✅ Vendor updated successfully');
|
||||
Utils.showToast('Vendor updated successfully!', 'success');
|
||||
this.vendor = updatedVendor;
|
||||
this.vendor = response;
|
||||
Utils.showToast('Vendor updated successfully', 'success');
|
||||
editLog.info(`Vendor updated successfully in ${duration}ms`, response);
|
||||
|
||||
// Optionally redirect back to list
|
||||
// setTimeout(() => window.location.href = '/admin/vendors', 1500);
|
||||
|
||||
// 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);
|
||||
editLog.error('Failed to update vendor:', error);
|
||||
|
||||
// Handle validation errors
|
||||
if (error.details && error.details.validation_errors) {
|
||||
error.details.validation_errors.forEach(err => {
|
||||
const field = err.loc?.[1] || err.loc?.[0];
|
||||
if (field) {
|
||||
this.errors[field] = err.msg;
|
||||
}
|
||||
});
|
||||
editLog.debug('Validation errors:', this.errors);
|
||||
}
|
||||
|
||||
Utils.showToast(error.message || 'Failed to update vendor', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
editLog.info('=== VENDOR UPDATE COMPLETE ===');
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
};
|
||||
},
|
||||
|
||||
// Toggle verification
|
||||
async toggleVerification() {
|
||||
const action = this.vendor.is_verified ? 'unverify' : 'verify';
|
||||
console.log(`Toggling verification: ${action}`);
|
||||
editLog.info(`Toggle verification: ${action}`);
|
||||
|
||||
if (!confirm(`Are you sure you want to ${action} this vendor?`)) {
|
||||
editLog.info('Verification toggle cancelled by user');
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const 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');
|
||||
const response = await apiClient.put(
|
||||
`/admin/vendors/${this.vendorCode}/verification`,
|
||||
{ is_verified: !this.vendor.is_verified }
|
||||
);
|
||||
|
||||
this.vendor = response;
|
||||
Utils.showToast(`Vendor ${action}ed successfully`, 'success');
|
||||
editLog.info(`Vendor ${action}ed successfully`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to toggle verification:', error);
|
||||
Utils.showToast('Failed to update verification status', 'error');
|
||||
editLog.error(`Failed to ${action} vendor:`, error);
|
||||
Utils.showToast(`Failed to ${action} vendor`, 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
showStatusModal() {
|
||||
// Toggle active status
|
||||
async toggleActive() {
|
||||
const action = this.vendor.is_active ? 'deactivate' : 'activate';
|
||||
const actionCap = this.vendor.is_active ? 'Deactivate' : 'Activate';
|
||||
editLog.info(`Toggle active status: ${action}`);
|
||||
|
||||
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}`);
|
||||
if (!confirm(`Are you sure you want to ${action} this vendor?\n\nThis will affect their operations.`)) {
|
||||
editLog.info('Active status toggle cancelled by user');
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const 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');
|
||||
const response = await apiClient.put(
|
||||
`/admin/vendors/${this.vendorCode}/status`,
|
||||
{ is_active: !this.vendor.is_active }
|
||||
);
|
||||
|
||||
this.vendor = response;
|
||||
Utils.showToast(`Vendor ${action}d successfully`, 'success');
|
||||
editLog.info(`Vendor ${action}d successfully`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to toggle status:', error);
|
||||
Utils.showToast('Failed to update vendor status', 'error');
|
||||
editLog.error(`Failed to ${action} vendor:`, error);
|
||||
Utils.showToast(`Failed to ${action} vendor`, '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 = '/admin/login';
|
||||
}, 500);
|
||||
},
|
||||
};
|
||||
}
|
||||
editLog.info('Vendor edit module loaded');
|
||||
@@ -1,247 +1,258 @@
|
||||
// static/admin/js/vendors.js
|
||||
// Admin Vendor Creation Component
|
||||
function vendorCreation() {
|
||||
|
||||
// Log levels: 0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug
|
||||
const VENDORS_LOG_LEVEL = 3;
|
||||
|
||||
const vendorsLog = {
|
||||
error: (...args) => VENDORS_LOG_LEVEL >= 1 && console.error('❌ [VENDORS ERROR]', ...args),
|
||||
warn: (...args) => VENDORS_LOG_LEVEL >= 2 && console.warn('⚠️ [VENDORS WARN]', ...args),
|
||||
info: (...args) => VENDORS_LOG_LEVEL >= 3 && console.info('ℹ️ [VENDORS INFO]', ...args),
|
||||
debug: (...args) => VENDORS_LOG_LEVEL >= 4 && console.log('🔍 [VENDORS DEBUG]', ...args)
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// VENDOR LIST FUNCTION
|
||||
// ============================================
|
||||
function adminVendors() {
|
||||
return {
|
||||
currentUser: {},
|
||||
formData: {
|
||||
vendor_code: '',
|
||||
name: '',
|
||||
subdomain: '',
|
||||
description: '',
|
||||
owner_email: '',
|
||||
business_phone: '',
|
||||
website: '',
|
||||
business_address: '',
|
||||
tax_number: ''
|
||||
// Inherit base layout functionality from init-alpine.js
|
||||
...data(),
|
||||
|
||||
// Vendors page specific state
|
||||
currentPage: 'vendors',
|
||||
vendors: [],
|
||||
stats: {
|
||||
total: 0,
|
||||
verified: 0,
|
||||
pending: 0,
|
||||
inactive: 0
|
||||
},
|
||||
loading: false,
|
||||
errors: {},
|
||||
showCredentials: false,
|
||||
credentials: null,
|
||||
error: null,
|
||||
|
||||
init() {
|
||||
if (!this.checkAuth()) {
|
||||
// Pagination state
|
||||
currentPage: 1,
|
||||
itemsPerPage: 10,
|
||||
|
||||
// Initialize
|
||||
async init() {
|
||||
vendorsLog.info('=== VENDORS PAGE INITIALIZING ===');
|
||||
|
||||
// Prevent multiple initializations
|
||||
if (window._vendorsInitialized) {
|
||||
vendorsLog.warn('Vendors page already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._vendorsInitialized = true;
|
||||
|
||||
await this.loadVendors();
|
||||
await this.loadStats();
|
||||
|
||||
vendorsLog.info('=== VENDORS PAGE INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
checkAuth() {
|
||||
if (!Auth.isAuthenticated()) {
|
||||
// ← CHANGED: Use new Jinja2 route
|
||||
window.location.href = '/admin/login';
|
||||
return false;
|
||||
}
|
||||
|
||||
const user = Auth.getCurrentUser();
|
||||
if (!user || user.role !== 'admin') {
|
||||
Utils.showToast('Access denied. Admin privileges required.', 'error');
|
||||
Auth.logout();
|
||||
// ← CHANGED: Use new Jinja2 route
|
||||
window.location.href = '/admin/login';
|
||||
return false;
|
||||
}
|
||||
|
||||
this.currentUser = user;
|
||||
return true;
|
||||
// Computed: Get paginated vendors for current page
|
||||
get paginatedVendors() {
|
||||
const start = (this.currentPage - 1) * this.itemsPerPage;
|
||||
const end = start + this.itemsPerPage;
|
||||
return this.vendors.slice(start, end);
|
||||
},
|
||||
|
||||
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(() => {
|
||||
// ← CHANGED: Use new Jinja2 route
|
||||
window.location.href = '/admin/login';
|
||||
}, 500);
|
||||
}
|
||||
// Computed: Total number of pages
|
||||
get totalPages() {
|
||||
return Math.ceil(this.vendors.length / this.itemsPerPage);
|
||||
},
|
||||
|
||||
// ... rest of the methods stay the same ...
|
||||
|
||||
// Auto-format vendor code (uppercase)
|
||||
formatVendorCode() {
|
||||
this.formData.vendor_code = this.formData.vendor_code
|
||||
.toUpperCase()
|
||||
.replace(/[^A-Z0-9_-]/g, '');
|
||||
// Computed: Start index for pagination display
|
||||
get startIndex() {
|
||||
if (this.vendors.length === 0) return 0;
|
||||
return (this.currentPage - 1) * this.itemsPerPage + 1;
|
||||
},
|
||||
|
||||
// Auto-format subdomain (lowercase)
|
||||
formatSubdomain() {
|
||||
this.formData.subdomain = this.formData.subdomain
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, '');
|
||||
// Computed: End index for pagination display
|
||||
get endIndex() {
|
||||
const end = this.currentPage * this.itemsPerPage;
|
||||
return end > this.vendors.length ? this.vendors.length : end;
|
||||
},
|
||||
|
||||
clearErrors() {
|
||||
this.errors = {};
|
||||
// Computed: Generate page numbers array with ellipsis
|
||||
get pageNumbers() {
|
||||
const pages = [];
|
||||
const totalPages = this.totalPages;
|
||||
const current = this.currentPage;
|
||||
|
||||
if (totalPages <= 7) {
|
||||
// Show all pages if 7 or fewer
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
// Always show first page
|
||||
pages.push(1);
|
||||
|
||||
if (current > 3) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
// Show pages around current page
|
||||
const start = Math.max(2, current - 1);
|
||||
const end = Math.min(totalPages - 1, current + 1);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
if (current < totalPages - 2) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
// Always show last page
|
||||
pages.push(totalPages);
|
||||
}
|
||||
|
||||
return pages;
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Load vendors list
|
||||
async loadVendors() {
|
||||
vendorsLog.info('Loading vendors list...');
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
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;
|
||||
}
|
||||
const startTime = Date.now();
|
||||
const response = await apiClient.get('/admin/vendors');
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// Handle different response structures
|
||||
this.vendors = response.vendors || response.items || response || [];
|
||||
|
||||
vendorsLog.info(`Vendors loaded in ${duration}ms`, {
|
||||
count: this.vendors.length,
|
||||
hasVendors: this.vendors.length > 0
|
||||
});
|
||||
|
||||
if (this.vendors.length > 0) {
|
||||
vendorsLog.debug('First vendor:', this.vendors[0]);
|
||||
}
|
||||
|
||||
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' });
|
||||
// Reset to first page when data is loaded
|
||||
this.currentPage = 1;
|
||||
|
||||
} 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'
|
||||
);
|
||||
vendorsLog.error('Failed to load vendors:', error);
|
||||
this.error = error.message || 'Failed to load vendors';
|
||||
Utils.showToast('Failed to load vendors', '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;
|
||||
// Load statistics
|
||||
async loadStats() {
|
||||
vendorsLog.info('Loading vendor statistics...');
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const response = await apiClient.get('/admin/vendors/stats');
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.stats = response;
|
||||
vendorsLog.info(`Stats loaded in ${duration}ms`, this.stats);
|
||||
|
||||
} catch (error) {
|
||||
vendorsLog.error('Failed to load stats:', error);
|
||||
// Don't show error toast for stats, just log it
|
||||
}
|
||||
},
|
||||
|
||||
copyToClipboard(text, label) {
|
||||
if (!text) {
|
||||
Utils.showToast('Nothing to copy', 'error');
|
||||
// Pagination: Go to specific page
|
||||
goToPage(page) {
|
||||
if (page === '...' || page < 1 || page > this.totalPages) {
|
||||
return;
|
||||
}
|
||||
vendorsLog.info('Going to page:', page);
|
||||
this.currentPage = page;
|
||||
},
|
||||
|
||||
// Pagination: Go to next page
|
||||
nextPage() {
|
||||
if (this.currentPage < this.totalPages) {
|
||||
vendorsLog.info('Going to next page');
|
||||
this.currentPage++;
|
||||
}
|
||||
},
|
||||
|
||||
// Pagination: Go to previous page
|
||||
previousPage() {
|
||||
if (this.currentPage > 1) {
|
||||
vendorsLog.info('Going to previous page');
|
||||
this.currentPage--;
|
||||
}
|
||||
},
|
||||
|
||||
// Format date (matches dashboard pattern)
|
||||
formatDate(dateString) {
|
||||
if (!dateString) {
|
||||
vendorsLog.debug('formatDate called with empty dateString');
|
||||
return '-';
|
||||
}
|
||||
const formatted = Utils.formatDate(dateString);
|
||||
vendorsLog.debug(`Date formatted: ${dateString} -> ${formatted}`);
|
||||
return formatted;
|
||||
},
|
||||
|
||||
// View vendor details
|
||||
viewVendor(vendorCode) {
|
||||
vendorsLog.info('Navigating to vendor details:', vendorCode);
|
||||
const url = `/admin/vendors/${vendorCode}`;
|
||||
vendorsLog.debug('Navigation URL:', url);
|
||||
window.location.href = url;
|
||||
},
|
||||
|
||||
// Edit vendor
|
||||
editVendor(vendorCode) {
|
||||
vendorsLog.info('Navigating to vendor edit:', vendorCode);
|
||||
const url = `/admin/vendors/${vendorCode}/edit`;
|
||||
vendorsLog.debug('Navigation URL:', url);
|
||||
window.location.href = url;
|
||||
},
|
||||
|
||||
// Delete vendor
|
||||
async deleteVendor(vendor) {
|
||||
vendorsLog.info('Delete vendor requested:', vendor.vendor_code);
|
||||
|
||||
if (!confirm(`Are you sure you want to delete vendor "${vendor.name}"?\n\nThis action cannot be undone.`)) {
|
||||
vendorsLog.info('Delete cancelled by user');
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
try {
|
||||
vendorsLog.info('Deleting vendor:', vendor.vendor_code);
|
||||
await apiClient.delete(`/admin/vendors/${vendor.vendor_code}`);
|
||||
|
||||
Utils.showToast('Vendor deleted successfully', 'success');
|
||||
vendorsLog.info('Vendor deleted successfully');
|
||||
|
||||
// Reload data
|
||||
await this.loadVendors();
|
||||
await this.loadStats();
|
||||
|
||||
} catch (error) {
|
||||
vendorsLog.error('Failed to delete vendor:', error);
|
||||
Utils.showToast(error.message || 'Failed to delete vendor', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
// Refresh vendors list
|
||||
async refresh() {
|
||||
vendorsLog.info('=== VENDORS REFRESH TRIGGERED ===');
|
||||
await this.loadVendors();
|
||||
await this.loadStats();
|
||||
Utils.showToast('Vendors list refreshed', 'success');
|
||||
vendorsLog.info('=== VENDORS REFRESH COMPLETE ===');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
vendorsLog.info('Vendors module loaded');
|
||||
@@ -1,115 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html :class="{ 'theme-dark': dark }" x-data="adminLogin()" lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Admin Login - Multi-Tenant Platform</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="/static/admin/css/tailwind.output.css" />
|
||||
<style>
|
||||
[x-cloak] { display: none !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900" x-cloak>
|
||||
<div class="flex-1 h-full max-w-4xl mx-auto overflow-hidden bg-white rounded-lg shadow-xl dark:bg-gray-800">
|
||||
<div class="flex flex-col overflow-y-auto md:flex-row">
|
||||
<div class="h-32 md:h-auto md:w-1/2">
|
||||
<img aria-hidden="true" class="object-cover w-full h-full dark:hidden"
|
||||
src="/static/admin/img/login-office.jpeg" alt="Office" />
|
||||
<img aria-hidden="true" class="hidden object-cover w-full h-full dark:block"
|
||||
src="/static/admin/img/login-office-dark.jpeg" alt="Office" />
|
||||
</div>
|
||||
<div class="flex items-center justify-center p-6 sm:p-12 md:w-1/2">
|
||||
<div class="w-full">
|
||||
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Admin Login
|
||||
</h1>
|
||||
|
||||
<!-- Alert Messages -->
|
||||
<div x-show="error" x-text="error"
|
||||
class="px-4 py-3 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800"
|
||||
x-transition></div>
|
||||
|
||||
<div x-show="success" x-text="success"
|
||||
class="px-4 py-3 mb-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800"
|
||||
x-transition></div>
|
||||
|
||||
<!-- Login Form -->
|
||||
<form @submit.prevent="handleLogin">
|
||||
<label class="block text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">Username</span>
|
||||
<input x-model="credentials.username"
|
||||
:disabled="loading"
|
||||
@input="clearErrors"
|
||||
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:text-gray-300 dark:focus:shadow-outline-gray form-input"
|
||||
:class="{ 'border-red-600': errors.username }"
|
||||
placeholder="Enter your username"
|
||||
autocomplete="username"
|
||||
required />
|
||||
<span x-show="errors.username" x-text="errors.username"
|
||||
class="text-xs text-red-600 dark:text-red-400"></span>
|
||||
</label>
|
||||
|
||||
<label class="block mt-4 text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">Password</span>
|
||||
<input x-model="credentials.password"
|
||||
:disabled="loading"
|
||||
@input="clearErrors"
|
||||
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:text-gray-300 dark:focus:shadow-outline-gray form-input"
|
||||
:class="{ 'border-red-600': errors.password }"
|
||||
placeholder="***************"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
required />
|
||||
<span x-show="errors.password" x-text="errors.password"
|
||||
class="text-xs text-red-600 dark:text-red-400"></span>
|
||||
</label>
|
||||
|
||||
<button type="submit" :disabled="loading"
|
||||
class="block w-full px-4 py-2 mt-4 text-sm font-medium leading-5 text-center text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span x-show="!loading">Log in</span>
|
||||
<span x-show="loading">
|
||||
<svg class="inline w-4 h-4 mr-2 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Signing in...
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<hr class="my-8" />
|
||||
|
||||
<p class="mt-4">
|
||||
<a class="text-sm font-medium text-purple-600 dark:text-purple-400 hover:underline"
|
||||
href="#">
|
||||
Forgot your password?
|
||||
</a>
|
||||
</p>
|
||||
<p class="mt-2">
|
||||
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
|
||||
href="/">
|
||||
← Back to Platform
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts in CORRECT ORDER -->
|
||||
<!-- 1. Icons FIRST (defines $icon magic) -->
|
||||
<script src="/static/shared/js/icons.js"></script>
|
||||
|
||||
<!-- 2. API Client -->
|
||||
<script src="/static/shared/js/api-client.js"></script>
|
||||
|
||||
<!-- 3. Login Logic -->
|
||||
<script src="/static/admin/js/login.js"></script>
|
||||
|
||||
<!-- 4. Alpine.js LAST with defer -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,95 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Login - Multi-Tenant Ecommerce Platform</title>
|
||||
<link rel="stylesheet" href="/static/css/shared/base.css">
|
||||
<link rel="stylesheet" href="/static/css/shared/auth.css">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-page" x-data="adminLogin()" x-cloak>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<div class="auth-logo">🔐</div>
|
||||
<h1>Admin Portal</h1>
|
||||
<p>Multi-Tenant Ecommerce Platform</p>
|
||||
</div>
|
||||
|
||||
<!-- Alert Messages -->
|
||||
<div x-show="error"
|
||||
x-text="error"
|
||||
class="alert alert-error"
|
||||
x-transition></div>
|
||||
|
||||
<div x-show="success"
|
||||
x-text="success"
|
||||
class="alert alert-success"
|
||||
x-transition></div>
|
||||
|
||||
<!-- Login Form -->
|
||||
<form @submit.prevent="handleLogin">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
x-model="credentials.username"
|
||||
:class="{ 'error': errors.username }"
|
||||
required
|
||||
autocomplete="username"
|
||||
placeholder="Enter your username"
|
||||
:disabled="loading"
|
||||
@input="clearErrors"
|
||||
>
|
||||
<div x-show="errors.username"
|
||||
x-text="errors.username"
|
||||
class="error-message show"
|
||||
x-transition></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
x-model="credentials.password"
|
||||
:class="{ 'error': errors.password }"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
placeholder="Enter your password"
|
||||
:disabled="loading"
|
||||
@input="clearErrors"
|
||||
>
|
||||
<div x-show="errors.password"
|
||||
x-text="errors.password"
|
||||
class="error-message show"
|
||||
x-transition></div>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="btn-login"
|
||||
:disabled="loading">
|
||||
<template x-if="!loading">
|
||||
<span>Sign In</span>
|
||||
</template>
|
||||
<template x-if="loading">
|
||||
<span>
|
||||
<span class="loading-spinner"></span>
|
||||
Signing in...
|
||||
</span>
|
||||
</template>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="login-footer">
|
||||
<a href="/">← Back to Platform</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/shared/api-client.js"></script>
|
||||
<script src="/static/admin/js/login.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user