frontend migration to jinja, alpine.js

This commit is contained in:
2025-10-26 20:04:10 +01:00
parent 2c3223f9f9
commit 091067a729
47 changed files with 673 additions and 8691 deletions

View File

@@ -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);
}
}
};
}

View 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');

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>