frontend migration to jinja, alpine.js
This commit is contained in:
@@ -32,8 +32,9 @@ function adminComponents() {
|
||||
{ id: 'cards', name: 'Cards', icon: 'collection' },
|
||||
{ id: 'badges', name: 'Badges', icon: 'tag' },
|
||||
{ id: 'tables', name: 'Tables', icon: 'table' },
|
||||
{ id: 'modals', name: 'Modals', icon: 'window' },
|
||||
{ id: 'alerts', name: 'Alerts', icon: 'exclamation' }
|
||||
{ id: 'modals', name: 'Modals', icon: 'view-grid-add' },
|
||||
{ id: 'alerts', name: 'Alerts', icon: 'exclamation' },
|
||||
{ id: 'charts', name: 'Charts', icon: 'chart-pie' }
|
||||
],
|
||||
|
||||
// Sample form data for examples
|
||||
@@ -53,6 +54,10 @@ function adminComponents() {
|
||||
required: 'This field is required'
|
||||
},
|
||||
|
||||
// Modal state variables for examples
|
||||
showExampleModal: false,
|
||||
showFormModal: false,
|
||||
|
||||
// ✅ CRITICAL: Proper initialization with guard
|
||||
async init() {
|
||||
componentsLog.info('=== COMPONENTS PAGE INITIALIZING ===');
|
||||
@@ -72,6 +77,11 @@ function adminComponents() {
|
||||
this.setActiveSectionFromHash();
|
||||
});
|
||||
|
||||
// Initialize charts after DOM is ready
|
||||
this.$nextTick(() => {
|
||||
this.initializeCharts();
|
||||
});
|
||||
|
||||
componentsLog.info('=== COMPONENTS PAGE INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
@@ -114,11 +124,18 @@ function adminComponents() {
|
||||
async copyCode(code) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
Utils.showToast('Code copied to clipboard!', 'success');
|
||||
// Use the global Utils.showToast function
|
||||
if (typeof Utils !== 'undefined' && Utils.showToast) {
|
||||
Utils.showToast('Code copied to clipboard!', 'success');
|
||||
} else {
|
||||
componentsLog.warn('Utils.showToast not available');
|
||||
}
|
||||
componentsLog.debug('Code copied to clipboard');
|
||||
} catch (error) {
|
||||
componentsLog.error('Failed to copy code:', error);
|
||||
Utils.showToast('Failed to copy code', 'error');
|
||||
if (typeof Utils !== 'undefined' && Utils.showToast) {
|
||||
Utils.showToast('Failed to copy code', 'error');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -132,7 +149,145 @@ function adminComponents() {
|
||||
warning: 'Please review your input.',
|
||||
info: 'Here is some information.'
|
||||
};
|
||||
Utils.showToast(messages[type] || messages.info, type);
|
||||
|
||||
if (typeof Utils !== 'undefined' && Utils.showToast) {
|
||||
Utils.showToast(messages[type] || messages.info, type);
|
||||
} else {
|
||||
componentsLog.error('Utils.showToast not available');
|
||||
alert(messages[type] || messages.info); // Fallback to alert
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize Chart.js charts
|
||||
*/
|
||||
initializeCharts() {
|
||||
try {
|
||||
// Check if Chart.js is loaded
|
||||
if (typeof Chart === 'undefined') {
|
||||
componentsLog.warn('Chart.js not loaded, skipping chart initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
componentsLog.info('Initializing charts...');
|
||||
|
||||
// Pie Chart
|
||||
const pieCanvas = document.getElementById('examplePieChart');
|
||||
if (pieCanvas) {
|
||||
const pieConfig = {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
datasets: [{
|
||||
data: [33, 33, 33],
|
||||
backgroundColor: ['#0694a2', '#7e3af2', '#1c64f2'],
|
||||
label: 'Dataset 1',
|
||||
}],
|
||||
labels: ['Shoes', 'Shirts', 'Bags'],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
cutoutPercentage: 80,
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
new Chart(pieCanvas, pieConfig);
|
||||
componentsLog.debug('Pie chart initialized');
|
||||
}
|
||||
|
||||
// Line Chart
|
||||
const lineCanvas = document.getElementById('exampleLineChart');
|
||||
if (lineCanvas) {
|
||||
const lineConfig = {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
|
||||
datasets: [{
|
||||
label: 'Organic',
|
||||
backgroundColor: '#0694a2',
|
||||
borderColor: '#0694a2',
|
||||
data: [43, 48, 40, 54, 67, 73, 70],
|
||||
fill: false,
|
||||
}, {
|
||||
label: 'Paid',
|
||||
fill: false,
|
||||
backgroundColor: '#7e3af2',
|
||||
borderColor: '#7e3af2',
|
||||
data: [24, 50, 64, 74, 52, 51, 65],
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltips: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
hover: {
|
||||
mode: 'nearest',
|
||||
intersect: true,
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: 'Month',
|
||||
},
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: 'Value',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
new Chart(lineCanvas, lineConfig);
|
||||
componentsLog.debug('Line chart initialized');
|
||||
}
|
||||
|
||||
// Bar Chart
|
||||
const barCanvas = document.getElementById('exampleBarChart');
|
||||
if (barCanvas) {
|
||||
const barConfig = {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
|
||||
datasets: [{
|
||||
label: 'Shoes',
|
||||
backgroundColor: '#0694a2',
|
||||
borderColor: '#0694a2',
|
||||
borderWidth: 1,
|
||||
data: [43, 48, 40, 54, 67, 73, 70],
|
||||
}, {
|
||||
label: 'Bags',
|
||||
backgroundColor: '#7e3af2',
|
||||
borderColor: '#7e3af2',
|
||||
borderWidth: 1,
|
||||
data: [24, 50, 64, 74, 52, 51, 65],
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
new Chart(barCanvas, barConfig);
|
||||
componentsLog.debug('Bar chart initialized');
|
||||
}
|
||||
|
||||
componentsLog.info('All charts initialized successfully');
|
||||
} catch (error) {
|
||||
componentsLog.error('Error initializing charts:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
278
static/admin/js/vendor-theme.js
Normal file
278
static/admin/js/vendor-theme.js
Normal file
@@ -0,0 +1,278 @@
|
||||
// static/admin/js/vendor-theme.js
|
||||
/**
|
||||
* Vendor Theme Management Component
|
||||
* Follows the established Alpine.js pattern from FRONTEND_ALPINE_PAGE_TEMPLATE.md
|
||||
*/
|
||||
|
||||
const THEME_LOG_LEVEL = 3;
|
||||
|
||||
const themeLog = {
|
||||
error: (...args) => THEME_LOG_LEVEL >= 1 && console.error('❌ [THEME ERROR]', ...args),
|
||||
warn: (...args) => THEME_LOG_LEVEL >= 2 && console.warn('⚠️ [THEME WARN]', ...args),
|
||||
info: (...args) => THEME_LOG_LEVEL >= 3 && console.info('ℹ️ [THEME INFO]', ...args),
|
||||
debug: (...args) => THEME_LOG_LEVEL >= 4 && console.log('🔍 [THEME DEBUG]', ...args)
|
||||
};
|
||||
|
||||
// Theme presets
|
||||
const THEME_PRESETS = {
|
||||
modern: {
|
||||
colors: {
|
||||
primary: "#6366f1",
|
||||
secondary: "#8b5cf6",
|
||||
accent: "#ec4899"
|
||||
},
|
||||
fonts: {
|
||||
heading: "Inter, sans-serif",
|
||||
body: "Inter, sans-serif"
|
||||
},
|
||||
layout: {
|
||||
style: "grid",
|
||||
header: "fixed"
|
||||
}
|
||||
},
|
||||
classic: {
|
||||
colors: {
|
||||
primary: "#1e40af",
|
||||
secondary: "#7c3aed",
|
||||
accent: "#dc2626"
|
||||
},
|
||||
fonts: {
|
||||
heading: "Georgia, serif",
|
||||
body: "Arial, sans-serif"
|
||||
},
|
||||
layout: {
|
||||
style: "list",
|
||||
header: "static"
|
||||
}
|
||||
},
|
||||
minimal: {
|
||||
colors: {
|
||||
primary: "#000000",
|
||||
secondary: "#404040",
|
||||
accent: "#666666"
|
||||
},
|
||||
fonts: {
|
||||
heading: "Helvetica, sans-serif",
|
||||
body: "Helvetica, sans-serif"
|
||||
},
|
||||
layout: {
|
||||
style: "grid",
|
||||
header: "transparent"
|
||||
}
|
||||
},
|
||||
vibrant: {
|
||||
colors: {
|
||||
primary: "#f59e0b",
|
||||
secondary: "#ef4444",
|
||||
accent: "#8b5cf6"
|
||||
},
|
||||
fonts: {
|
||||
heading: "Poppins, sans-serif",
|
||||
body: "Open Sans, sans-serif"
|
||||
},
|
||||
layout: {
|
||||
style: "masonry",
|
||||
header: "fixed"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function vendorThemeData() {
|
||||
return {
|
||||
// ✅ CRITICAL: Inherit base layout functionality
|
||||
...data(),
|
||||
|
||||
// ✅ CRITICAL: Set page identifier
|
||||
currentPage: 'vendor-theme',
|
||||
|
||||
// Page state
|
||||
loading: false,
|
||||
saving: false,
|
||||
vendor: null,
|
||||
vendorCode: window.location.pathname.split('/')[3], // Extract from /admin/vendors/{code}/theme
|
||||
|
||||
// Theme data
|
||||
themeData: {
|
||||
colors: {
|
||||
primary: "#6366f1",
|
||||
secondary: "#8b5cf6",
|
||||
accent: "#ec4899"
|
||||
},
|
||||
fonts: {
|
||||
heading: "Inter, sans-serif",
|
||||
body: "Inter, sans-serif"
|
||||
},
|
||||
layout: {
|
||||
style: "grid",
|
||||
header: "fixed"
|
||||
},
|
||||
custom_css: ""
|
||||
},
|
||||
|
||||
originalTheme: null, // For detecting changes
|
||||
|
||||
// ✅ CRITICAL: Proper initialization with guard
|
||||
async init() {
|
||||
themeLog.info('=== VENDOR THEME PAGE INITIALIZING ===');
|
||||
|
||||
// Prevent multiple initializations
|
||||
if (window._vendorThemeInitialized) {
|
||||
themeLog.warn('Page already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._vendorThemeInitialized = true;
|
||||
|
||||
// Load data
|
||||
await this.loadVendor();
|
||||
await this.loadTheme();
|
||||
|
||||
themeLog.info('=== VENDOR THEME PAGE INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
// Load vendor info
|
||||
async loadVendor() {
|
||||
themeLog.info('Loading vendor:', this.vendorCode);
|
||||
|
||||
try {
|
||||
// ✅ CRITICAL: Use lowercase apiClient
|
||||
const response = await apiClient.get(`/api/v1/admin/vendors/${this.vendorCode}`);
|
||||
this.vendor = response;
|
||||
themeLog.info('Vendor loaded:', this.vendor.name);
|
||||
} catch (error) {
|
||||
themeLog.error('Failed to load vendor:', error);
|
||||
Utils.showToast('Failed to load vendor', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
// Load theme configuration
|
||||
async loadTheme() {
|
||||
themeLog.info('Loading theme...');
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Get vendor's theme config from vendor object
|
||||
if (this.vendor && this.vendor.theme_config) {
|
||||
this.themeData = {
|
||||
colors: this.vendor.theme_config.colors || this.themeData.colors,
|
||||
fonts: this.vendor.theme_config.fonts || this.themeData.fonts,
|
||||
layout: this.vendor.theme_config.layout || this.themeData.layout,
|
||||
custom_css: this.vendor.theme_config.custom_css || ""
|
||||
};
|
||||
} else {
|
||||
themeLog.info('No theme config found, using defaults');
|
||||
}
|
||||
|
||||
// Store original for change detection
|
||||
this.originalTheme = JSON.parse(JSON.stringify(this.themeData));
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
themeLog.info(`Theme loaded in ${duration}ms`, this.themeData);
|
||||
|
||||
} catch (error) {
|
||||
themeLog.error('Failed to load theme:', error);
|
||||
Utils.showToast('Failed to load theme', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Save theme configuration
|
||||
async saveTheme() {
|
||||
themeLog.info('Saving theme...');
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Update vendor with new theme_config
|
||||
const updateData = {
|
||||
theme_config: this.themeData
|
||||
};
|
||||
|
||||
const response = await apiClient.put(
|
||||
`/api/v1/admin/vendors/${this.vendorCode}`,
|
||||
updateData
|
||||
);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
themeLog.info(`Theme saved in ${duration}ms`);
|
||||
|
||||
// Update vendor data
|
||||
this.vendor = response;
|
||||
this.originalTheme = JSON.parse(JSON.stringify(this.themeData));
|
||||
|
||||
Utils.showToast('Theme saved successfully', 'success');
|
||||
|
||||
} catch (error) {
|
||||
themeLog.error('Failed to save theme:', error);
|
||||
Utils.showToast('Failed to save theme', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Apply preset theme
|
||||
applyPreset(presetName) {
|
||||
themeLog.info('Applying preset:', presetName);
|
||||
|
||||
if (!THEME_PRESETS[presetName]) {
|
||||
themeLog.error('Unknown preset:', presetName);
|
||||
return;
|
||||
}
|
||||
|
||||
const preset = THEME_PRESETS[presetName];
|
||||
|
||||
// Apply preset values
|
||||
this.themeData.colors = { ...preset.colors };
|
||||
this.themeData.fonts = { ...preset.fonts };
|
||||
this.themeData.layout = { ...preset.layout };
|
||||
|
||||
Utils.showToast(`Applied ${presetName} theme preset`, 'success');
|
||||
},
|
||||
|
||||
// Reset to default theme
|
||||
resetToDefault() {
|
||||
themeLog.info('Resetting to default theme');
|
||||
|
||||
// Confirm with user
|
||||
if (!confirm('Are you sure you want to reset to the default theme? This will discard all customizations.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.themeData = {
|
||||
colors: {
|
||||
primary: "#6366f1",
|
||||
secondary: "#8b5cf6",
|
||||
accent: "#ec4899"
|
||||
},
|
||||
fonts: {
|
||||
heading: "Inter, sans-serif",
|
||||
body: "Inter, sans-serif"
|
||||
},
|
||||
layout: {
|
||||
style: "grid",
|
||||
header: "fixed"
|
||||
},
|
||||
custom_css: ""
|
||||
};
|
||||
|
||||
Utils.showToast('Theme reset to default', 'info');
|
||||
},
|
||||
|
||||
// Check if theme has unsaved changes
|
||||
hasChanges() {
|
||||
if (!this.originalTheme) return false;
|
||||
return JSON.stringify(this.themeData) !== JSON.stringify(this.originalTheme);
|
||||
},
|
||||
|
||||
// Format date helper
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
return Utils.formatDate(dateString);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
themeLog.info('Vendor theme module loaded');
|
||||
@@ -1,72 +0,0 @@
|
||||
<!-- static/admin/partials/base-layout.html -->
|
||||
<!DOCTYPE html>
|
||||
<html :class="{ 'theme-dark': dark }" lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title data-page-title>Admin Panel - Multi-Tenant Platform</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<link rel="stylesheet" href="/static/admin/css/tailwind.output.css" />
|
||||
|
||||
<!-- Alpine Cloak -->
|
||||
<style>
|
||||
[x-cloak] { display: none !important; }
|
||||
</style>
|
||||
|
||||
<!-- Page-specific styles slot -->
|
||||
<slot name="head"></slot>
|
||||
</head>
|
||||
<body x-cloak>
|
||||
<div class="flex h-screen bg-gray-50 dark:bg-gray-900" :class="{ 'overflow-hidden': isSideMenuOpen }">
|
||||
<!-- Sidebar Container -->
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div class="flex flex-col flex-1 w-full">
|
||||
<!-- Header Container -->
|
||||
<div id="header-container"></div>
|
||||
|
||||
<!-- Main Content Area (Child pages inject content here) -->
|
||||
<main class="h-full overflow-y-auto">
|
||||
<div class="container px-6 mx-auto grid">
|
||||
<!-- Page content slot -->
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Core Scripts (loaded for all pages) -->
|
||||
<!-- 1. Partial Loader -->
|
||||
<script src="/static/shared/js/partial-loader.js"></script>
|
||||
|
||||
<!-- 2. Icons Helper -->
|
||||
<script src="/static/shared/js/icons.js"></script>
|
||||
|
||||
<!-- 3. Load Header & Sidebar -->
|
||||
<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 -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
|
||||
|
||||
<!-- Page-specific scripts slot -->
|
||||
<slot name="scripts"></slot>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,11 +0,0 @@
|
||||
<DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>User management</title>
|
||||
</head>
|
||||
<body>
|
||||
<-- User management -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,498 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Edit Vendor - Admin Portal</title>
|
||||
<link rel="stylesheet" href="/static/css/shared/base.css">
|
||||
<link rel="stylesheet" href="/static/css/admin/admin.css">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
</head>
|
||||
<body x-data="vendorEdit()" x-init="init()" x-cloak>
|
||||
<!-- Header -->
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<h1>🔐 Admin Dashboard</h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="user-info">Welcome, <strong x-text="currentUser.username"></strong></span>
|
||||
<button class="btn-logout" @click="handleLogout">Logout</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Container -->
|
||||
<div class="admin-container">
|
||||
<!-- Sidebar -->
|
||||
<aside class="admin-sidebar">
|
||||
<nav>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="/static/admin/dashboard.html" class="nav-link">
|
||||
📊 Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/static/admin/dashboard.html#vendors" class="nav-link active">
|
||||
🏪 Vendors
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/static/admin/dashboard.html#users" class="nav-link">
|
||||
👥 Users
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/static/admin/dashboard.html#imports" class="nav-link">
|
||||
📦 Import Jobs
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="admin-content">
|
||||
<!-- Loading State -->
|
||||
<div x-show="loadingVendor" class="loading">
|
||||
<span class="loading-spinner loading-spinner-lg"></span>
|
||||
<p class="loading-text">Loading vendor details...</p>
|
||||
</div>
|
||||
|
||||
<!-- Edit Form -->
|
||||
<div x-show="!loadingVendor && vendor">
|
||||
<div class="content-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
Edit Vendor: <span x-text="vendor?.name"></span>
|
||||
</h2>
|
||||
<div>
|
||||
<a href="/static/admin/dashboard.html#vendors" class="btn btn-secondary">
|
||||
← Back to Vendor Management
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="quick-actions mb-3">
|
||||
<button
|
||||
@click="showVerificationModal()"
|
||||
class="btn"
|
||||
:class="vendor?.is_verified ? 'btn-warning' : 'btn-success'"
|
||||
:disabled="saving">
|
||||
<span x-text="vendor?.is_verified ? '❌ Unverify Vendor' : '✅ Verify Vendor'"></span>
|
||||
</button>
|
||||
<button
|
||||
@click="showStatusModal()"
|
||||
class="btn"
|
||||
:class="vendor?.is_active ? 'btn-danger' : 'btn-success'"
|
||||
:disabled="saving">
|
||||
<span x-text="vendor?.is_active ? '🔒 Deactivate Vendor' : '🔓 Activate Vendor'"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="form-grid">
|
||||
<!-- Left Column -->
|
||||
<div class="form-column">
|
||||
<h3 class="form-section-title">Basic Information</h3>
|
||||
|
||||
<!-- Vendor Code (read-only) -->
|
||||
<div class="form-group">
|
||||
<label for="vendorCode">Vendor Code</label>
|
||||
<input
|
||||
type="text"
|
||||
id="vendorCode"
|
||||
name="vendor_code"
|
||||
x-model="vendor.vendor_code"
|
||||
disabled
|
||||
class="form-control-disabled"
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="form-help">Cannot be changed after creation</div>
|
||||
</div>
|
||||
|
||||
<!-- Vendor Name -->
|
||||
<div class="form-group">
|
||||
<label for="name">
|
||||
Vendor Name <span class="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="vendor_name"
|
||||
x-model="formData.name"
|
||||
:class="{ 'error': errors.name }"
|
||||
required
|
||||
maxlength="255"
|
||||
:disabled="saving"
|
||||
autocomplete="organization"
|
||||
>
|
||||
<div x-show="errors.name"
|
||||
x-text="errors.name"
|
||||
class="error-message show"></div>
|
||||
</div>
|
||||
|
||||
<!-- Subdomain -->
|
||||
<div class="form-group">
|
||||
<label for="subdomain">
|
||||
Subdomain <span class="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="subdomain"
|
||||
name="subdomain"
|
||||
x-model="formData.subdomain"
|
||||
@input="formatSubdomain"
|
||||
:class="{ 'error': errors.subdomain }"
|
||||
required
|
||||
maxlength="100"
|
||||
:disabled="saving"
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="form-help">Lowercase letters, numbers, and hyphens only</div>
|
||||
<div x-show="errors.subdomain"
|
||||
x-text="errors.subdomain"
|
||||
class="error-message show"></div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
x-model="formData.description"
|
||||
rows="3"
|
||||
:disabled="saving"
|
||||
autocomplete="off"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="form-column">
|
||||
<h3 class="form-section-title">Contact & Business Information</h3>
|
||||
|
||||
<!-- Owner Email (read-only with warning) -->
|
||||
<div class="form-group">
|
||||
<label for="ownerEmail">Owner Email (Login)</label>
|
||||
<input
|
||||
type="email"
|
||||
id="ownerEmail"
|
||||
name="owner_email"
|
||||
x-model="vendor.owner_email"
|
||||
disabled
|
||||
class="form-control-disabled"
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="form-help">
|
||||
⚠️ Owner email cannot be changed here. Use "Transfer Ownership" below to change the owner.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Email (editable) -->
|
||||
<div class="form-group">
|
||||
<label for="contactEmail">Business Contact Email</label>
|
||||
<input
|
||||
type="email"
|
||||
id="contactEmail"
|
||||
name="contact_email"
|
||||
x-model="formData.contact_email"
|
||||
:disabled="saving"
|
||||
autocomplete="email"
|
||||
>
|
||||
<div class="form-help">
|
||||
Public business contact email (can be different from owner email)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Phone -->
|
||||
<div class="form-group">
|
||||
<label for="contactPhone">Contact Phone</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="contactPhone"
|
||||
name="contact_phone"
|
||||
x-model="formData.contact_phone"
|
||||
:disabled="saving"
|
||||
autocomplete="tel"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Website -->
|
||||
<div class="form-group">
|
||||
<label for="website">Website</label>
|
||||
<input
|
||||
type="url"
|
||||
id="website"
|
||||
name="website"
|
||||
x-model="formData.website"
|
||||
:disabled="saving"
|
||||
autocomplete="url"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Business Address -->
|
||||
<div class="form-group">
|
||||
<label for="businessAddress">Business Address</label>
|
||||
<textarea
|
||||
id="businessAddress"
|
||||
name="business_address"
|
||||
x-model="formData.business_address"
|
||||
rows="3"
|
||||
:disabled="saving"
|
||||
autocomplete="street-address"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Tax Number -->
|
||||
<div class="form-group">
|
||||
<label for="taxNumber">Tax Number</label>
|
||||
<input
|
||||
type="text"
|
||||
id="taxNumber"
|
||||
name="tax_number"
|
||||
x-model="formData.tax_number"
|
||||
:disabled="saving"
|
||||
autocomplete="off"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transfer Ownership Section -->
|
||||
<div class="form-section-divider"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<h3 class="form-section-title">⚠️ Change Vendor Owner</h3>
|
||||
<p class="text-muted mb-2">
|
||||
To change the owner to a different user account, use the Transfer Ownership feature.
|
||||
This will assign ownership to another user and demote the current owner to Manager.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
@click="showTransferOwnership = true"
|
||||
class="btn btn-warning"
|
||||
:disabled="saving">
|
||||
🔄 Transfer Ownership to Different User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button"
|
||||
class="btn btn-secondary"
|
||||
@click="window.location.href='/static/admin/dashboard.html#vendors'"
|
||||
:disabled="saving">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit"
|
||||
class="btn btn-primary"
|
||||
:disabled="saving">
|
||||
<span x-show="!saving">💾 Save Changes</span>
|
||||
<span x-show="saving">
|
||||
<span class="loading-spinner"></span>
|
||||
Saving...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation Modal (for verify/status changes) -->
|
||||
<div x-show="confirmModal.show"
|
||||
class="modal-overlay"
|
||||
@click.self="confirmModal.onCancel ? confirmModal.onCancel() : (confirmModal.show = false)"
|
||||
x-transition>
|
||||
<div class="modal-content modal-sm">
|
||||
<div class="modal-header">
|
||||
<h3 x-text="confirmModal.title"></h3>
|
||||
<button @click="confirmModal.onCancel ? confirmModal.onCancel() : (confirmModal.show = false)" class="btn-close">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<p x-text="confirmModal.message"></p>
|
||||
|
||||
<div x-show="confirmModal.warning" class="alert alert-warning mt-3" style="white-space: pre-line;">
|
||||
<strong>⚠️ Warning:</strong><br>
|
||||
<span x-text="confirmModal.warning"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
@click="confirmModal.onCancel ? confirmModal.onCancel() : (confirmModal.show = false)"
|
||||
class="btn btn-secondary"
|
||||
:disabled="saving">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="confirmModal.onConfirm(); confirmModal.show = false"
|
||||
class="btn"
|
||||
:class="confirmModal.buttonClass"
|
||||
:disabled="saving">
|
||||
<span x-show="!saving" x-text="confirmModal.buttonText"></span>
|
||||
<span x-show="saving">
|
||||
<span class="loading-spinner"></span>
|
||||
Processing...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Modal (for transfer ownership success) -->
|
||||
<div x-show="successModal.show"
|
||||
class="modal-overlay"
|
||||
@click.self="successModal.show = false"
|
||||
x-transition>
|
||||
<div class="modal-content modal-md">
|
||||
<div class="modal-header modal-header-success">
|
||||
<h3>✅ <span x-text="successModal.title"></span></h3>
|
||||
<button @click="successModal.show = false" class="btn-close">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="success-icon-wrapper">
|
||||
<div class="success-icon">✓</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center mb-4" x-text="successModal.message"></p>
|
||||
|
||||
<!-- Transfer Details -->
|
||||
<div x-show="successModal.details" class="transfer-details">
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Previous Owner:</div>
|
||||
<div class="detail-value">
|
||||
<strong x-text="successModal.details?.oldOwner?.username"></strong>
|
||||
<br>
|
||||
<span class="text-muted" x-text="successModal.details?.oldOwner?.email"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-arrow">→</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">New Owner:</div>
|
||||
<div class="detail-value">
|
||||
<strong x-text="successModal.details?.newOwner?.username"></strong>
|
||||
<br>
|
||||
<span class="text-muted" x-text="successModal.details?.newOwner?.email"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="successModal.note" class="alert alert-info mt-3">
|
||||
<strong>ℹ️ Note:</strong>
|
||||
<span x-text="successModal.note"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
@click="successModal.show = false"
|
||||
class="btn btn-primary btn-block">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation Modal (for verify/status changes) -->
|
||||
|
||||
<!-- Transfer Ownership Modal -->
|
||||
<div x-show="showTransferOwnership"
|
||||
class="modal-overlay"
|
||||
@click.self="showTransferOwnership = false"
|
||||
x-transition>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>🔄 Transfer Vendor Ownership</h3>
|
||||
<button @click="showTransferOwnership = false" class="btn-close">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning mb-3">
|
||||
<strong>⚠️ Warning:</strong> This will transfer complete ownership to another user.
|
||||
The current owner will be demoted to Manager role.
|
||||
</div>
|
||||
|
||||
<div class="info-box mb-3">
|
||||
<strong>Current Owner:</strong><br>
|
||||
<span x-text="vendor?.owner_username"></span> (<span x-text="vendor?.owner_email"></span>)
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="newOwnerId">
|
||||
New Owner User ID <span class="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="newOwnerId"
|
||||
name="new_owner_user_id"
|
||||
x-model.number="transferData.new_owner_user_id"
|
||||
required
|
||||
placeholder="Enter user ID"
|
||||
min="1"
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="form-help">
|
||||
Enter the ID of the user who will become the new owner
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="transferReason">Reason for Transfer</label>
|
||||
<textarea
|
||||
id="transferReason"
|
||||
name="transfer_reason"
|
||||
x-model="transferData.transfer_reason"
|
||||
rows="3"
|
||||
placeholder="Optional: Why is ownership being transferred?"
|
||||
autocomplete="off"
|
||||
></textarea>
|
||||
<div class="form-help">
|
||||
This will be logged for audit purposes
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirmTransfer" class="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="confirmTransfer"
|
||||
name="confirm_transfer"
|
||||
x-model="transferData.confirm_transfer"
|
||||
>
|
||||
I confirm this ownership transfer
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
@click="showTransferOwnership = false"
|
||||
class="btn btn-secondary"
|
||||
:disabled="transferring">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="handleTransferOwnership()"
|
||||
class="btn btn-danger"
|
||||
:disabled="!transferData.confirm_transfer || !transferData.new_owner_user_id || transferring">
|
||||
<span x-show="!transferring">🔄 Transfer Ownership</span>
|
||||
<span x-show="transferring">
|
||||
<span class="loading-spinner"></span>
|
||||
Transferring...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/shared/js/api-client.js"></script>
|
||||
<script src="/static/admin/js/vendor-edit.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,201 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html :class="{ 'theme-dark': dark }" x-data="vendorsData()" lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vendors - Admin Portal</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 -->
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div class="flex flex-col flex-1 w-full">
|
||||
<!-- Header Container -->
|
||||
<div id="header-container"></div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="h-full overflow-y-auto">
|
||||
<div class="container px-6 mx-auto grid">
|
||||
<div class="flex items-center justify-between my-6">
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Vendor Management
|
||||
</h2>
|
||||
<button class="flex items-center justify-between px-4 py-2 text-sm font-medium leading-5 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">
|
||||
<!-- Heroicon: plus -->
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
Create Vendor
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Placeholder Content -->
|
||||
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<div class="text-center py-12">
|
||||
<div class="mb-4">
|
||||
<!-- Heroicon: shopping-bag (outline, large) -->
|
||||
<svg class="mx-auto h-24 w-24 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mb-2 text-xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Vendor Management
|
||||
</h3>
|
||||
<p class="mb-6 text-gray-600 dark:text-gray-400">
|
||||
This page will display vendor list and management tools.
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Features to be implemented:
|
||||
</p>
|
||||
<ul class="mt-4 text-sm text-gray-600 dark:text-gray-400 space-y-2">
|
||||
<li>• View all vendors with filtering and search</li>
|
||||
<li>• Create new vendors with detailed forms</li>
|
||||
<li>• Edit vendor information</li>
|
||||
<li>• Verify/unverify vendors</li>
|
||||
<li>• View vendor statistics</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Example vendors table structure (for reference) -->
|
||||
<div 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 Code</th>
|
||||
<th class="px-4 py-3">Name</th>
|
||||
<th class="px-4 py-3">Subdomain</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="vendors.length === 0">
|
||||
<tr>
|
||||
<td colspan="6" class="px-4 py-8 text-sm text-center text-gray-600 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<svg class="w-16 h-16 mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path>
|
||||
</svg>
|
||||
<p class="font-medium">No vendors found</p>
|
||||
<p class="text-xs mt-1">Create your first vendor to get started</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template x-for="vendor in vendors" :key="vendor.id">
|
||||
<tr class="text-gray-700 dark:text-gray-400">
|
||||
<td class="px-4 py-3">
|
||||
<p class="font-semibold text-sm" x-text="vendor.vendor_code"></p>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm" x-text="vendor.name"></td>
|
||||
<td class="px-4 py-3 text-sm" x-text="vendor.subdomain"></td>
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span class="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'"
|
||||
x-text="vendor.is_verified ? 'Verified' : 'Pending'">
|
||||
</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-4 text-sm">
|
||||
<button class="flex items-center justify-between px-2 py-2 text-sm font-medium leading-5 text-purple-600 rounded-lg dark:text-gray-400 focus:outline-none focus:shadow-outline-gray"
|
||||
aria-label="Edit">
|
||||
<svg class="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="flex items-center justify-between px-2 py-2 text-sm font-medium leading-5 text-red-600 rounded-lg dark:text-red-400 focus:outline-none focus:shadow-outline-gray"
|
||||
aria-label="Delete">
|
||||
<svg class="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load partials BEFORE Alpine -->
|
||||
<script src="/static/shared/js/partial-loader.js"></script>
|
||||
<script src="/static/shared/js/icons.js"></script>
|
||||
<script>
|
||||
(async () => {
|
||||
await window.partialLoader.loadAll({
|
||||
'header-container': 'header.html',
|
||||
'sidebar-container': 'sidebar.html'
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Alpine.js v3 -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
|
||||
|
||||
<!-- Initialize Alpine data -->
|
||||
<script src="/static/admin/js/init-alpine.js"></script>
|
||||
|
||||
<!-- Vendors-specific logic -->
|
||||
<script>
|
||||
function vendorsData() {
|
||||
return {
|
||||
...data(), // Spread base data from init-alpine.js
|
||||
currentPage: 'vendors',
|
||||
vendors: [],
|
||||
loading: false,
|
||||
|
||||
async init() {
|
||||
await this.loadVendors();
|
||||
},
|
||||
|
||||
async loadVendors() {
|
||||
this.loading = true;
|
||||
try {
|
||||
// Replace with your actual API endpoint
|
||||
const response = await fetch('/api/v1/admin/vendors', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.vendors = data.vendors || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading vendors:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user