Platform Email Settings (Admin): - Add GET/PUT/DELETE /admin/settings/email/* endpoints - Settings stored in admin_settings table override .env values - Support all providers: SMTP, SendGrid, Mailgun, Amazon SES - Edit mode UI with provider-specific configuration forms - Reset to .env defaults functionality - Test email to verify configuration Vendor Email Settings: - Add VendorEmailSettings model with one-to-one vendor relationship - Migration: v0a1b2c3d4e5_add_vendor_email_settings.py - Service: vendor_email_settings_service.py with tier validation - API endpoints: /vendor/email-settings/* (CRUD, status, verify) - Email tab in vendor settings page with full configuration - Warning banner until email is configured (like billing warnings) - Premium providers (SendGrid, Mailgun, SES) tier-gated to Business+ Email Service Updates: - get_platform_email_config(db) checks DB first, then .env - Configurable provider classes accept config dict - EmailService uses database-aware providers - Vendor emails use vendor's own SMTP (Wizamart doesn't pay) - "Powered by Wizamart" footer for Essential/Professional tiers - White-label (no footer) for Business/Enterprise tiers Other: - Add scripts/install.py for first-time platform setup - Add make install target - Update init-prod to include email template seeding 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
270 lines
8.5 KiB
JavaScript
270 lines
8.5 KiB
JavaScript
// app/static/vendor/js/init-alpine.js
|
|
/**
|
|
* Alpine.js initialization for vendor pages
|
|
* Provides common data and methods for all vendor pages
|
|
*/
|
|
|
|
// ✅ Use centralized logger
|
|
const vendorLog = window.LogConfig.log;
|
|
|
|
console.log('[VENDOR INIT-ALPINE] Loading...');
|
|
|
|
// Sidebar section state persistence
|
|
const VENDOR_SIDEBAR_STORAGE_KEY = 'vendor_sidebar_sections';
|
|
|
|
function getVendorSidebarSectionsFromStorage() {
|
|
try {
|
|
const stored = localStorage.getItem(VENDOR_SIDEBAR_STORAGE_KEY);
|
|
if (stored) {
|
|
return JSON.parse(stored);
|
|
}
|
|
} catch (e) {
|
|
console.warn('[VENDOR INIT-ALPINE] Failed to load sidebar state from localStorage:', e);
|
|
}
|
|
// Default: all sections open
|
|
return {
|
|
products: true,
|
|
sales: true,
|
|
customers: true,
|
|
shop: true,
|
|
account: true
|
|
};
|
|
}
|
|
|
|
function saveVendorSidebarSectionsToStorage(sections) {
|
|
try {
|
|
localStorage.setItem(VENDOR_SIDEBAR_STORAGE_KEY, JSON.stringify(sections));
|
|
} catch (e) {
|
|
console.warn('[VENDOR INIT-ALPINE] Failed to save sidebar state to localStorage:', e);
|
|
}
|
|
}
|
|
|
|
function data() {
|
|
console.log('[VENDOR INIT-ALPINE] data() function called');
|
|
return {
|
|
dark: false,
|
|
isSideMenuOpen: false,
|
|
isNotificationsMenuOpen: false,
|
|
isProfileMenuOpen: false,
|
|
currentPage: '',
|
|
currentUser: {},
|
|
vendor: null,
|
|
vendorCode: null,
|
|
|
|
// Sidebar collapsible sections state
|
|
openSections: getVendorSidebarSectionsFromStorage(),
|
|
|
|
init() {
|
|
// Set current page from URL
|
|
const path = window.location.pathname;
|
|
const segments = path.split('/').filter(Boolean);
|
|
this.currentPage = segments[segments.length - 1] || 'dashboard';
|
|
|
|
// Get vendor code from URL
|
|
if (segments[0] === 'vendor' && segments[1]) {
|
|
this.vendorCode = segments[1];
|
|
}
|
|
|
|
// Load user from localStorage
|
|
const user = localStorage.getItem('currentUser');
|
|
if (user) {
|
|
this.currentUser = JSON.parse(user);
|
|
}
|
|
|
|
// Load theme preference
|
|
const theme = localStorage.getItem('theme');
|
|
if (theme === 'dark') {
|
|
this.dark = true;
|
|
}
|
|
|
|
// Load vendor info
|
|
this.loadVendorInfo();
|
|
|
|
// Save last visited page (for redirect after login)
|
|
// Exclude login, logout, onboarding, error pages
|
|
if (!path.includes('/login') &&
|
|
!path.includes('/logout') &&
|
|
!path.includes('/onboarding') &&
|
|
!path.includes('/errors/')) {
|
|
try {
|
|
localStorage.setItem('vendor_last_visited_page', path);
|
|
} catch (e) {
|
|
// Ignore storage errors
|
|
}
|
|
}
|
|
},
|
|
|
|
async loadVendorInfo() {
|
|
if (!this.vendorCode) return;
|
|
|
|
try {
|
|
// apiClient prepends /api/v1, so /vendor/{code} → /api/v1/vendor/{code}
|
|
const response = await apiClient.get(`/vendor/${this.vendorCode}`);
|
|
this.vendor = response;
|
|
vendorLog.debug('Vendor info loaded', this.vendor);
|
|
} catch (error) {
|
|
vendorLog.error('Failed to load vendor info', error);
|
|
}
|
|
},
|
|
|
|
toggleSideMenu() {
|
|
this.isSideMenuOpen = !this.isSideMenuOpen;
|
|
},
|
|
|
|
closeSideMenu() {
|
|
this.isSideMenuOpen = false;
|
|
},
|
|
|
|
toggleNotificationsMenu() {
|
|
this.isNotificationsMenuOpen = !this.isNotificationsMenuOpen;
|
|
if (this.isNotificationsMenuOpen) {
|
|
this.isProfileMenuOpen = false;
|
|
}
|
|
},
|
|
|
|
closeNotificationsMenu() {
|
|
this.isNotificationsMenuOpen = false;
|
|
},
|
|
|
|
toggleProfileMenu() {
|
|
this.isProfileMenuOpen = !this.isProfileMenuOpen;
|
|
if (this.isProfileMenuOpen) {
|
|
this.isNotificationsMenuOpen = false;
|
|
}
|
|
},
|
|
|
|
closeProfileMenu() {
|
|
this.isProfileMenuOpen = false;
|
|
},
|
|
|
|
toggleTheme() {
|
|
this.dark = !this.dark;
|
|
localStorage.setItem('theme', this.dark ? 'dark' : 'light');
|
|
},
|
|
|
|
// Sidebar section toggle with persistence
|
|
toggleSection(section) {
|
|
this.openSections[section] = !this.openSections[section];
|
|
saveVendorSidebarSectionsToStorage(this.openSections);
|
|
},
|
|
|
|
async handleLogout() {
|
|
console.log('🚪 Logging out vendor user...');
|
|
|
|
try {
|
|
// Call logout API
|
|
await apiClient.post('/vendor/auth/logout');
|
|
console.log('✅ Logout API called successfully');
|
|
} catch (error) {
|
|
console.error('⚠️ Logout API error (continuing anyway):', error);
|
|
} finally {
|
|
// Clear vendor tokens only (not admin or customer tokens)
|
|
// Keep vendor_last_visited_page so user returns to same page after login
|
|
console.log('🧹 Clearing vendor tokens...');
|
|
localStorage.removeItem('vendor_token');
|
|
localStorage.removeItem('vendor_user');
|
|
localStorage.removeItem('currentUser');
|
|
localStorage.removeItem('vendorCode');
|
|
// Note: Do NOT use localStorage.clear() - it would clear admin/customer tokens too
|
|
|
|
console.log('🔄 Redirecting to login...');
|
|
window.location.href = `/vendor/${this.vendorCode}/login`;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Language Selector Component
|
|
* Alpine.js component for language switching in vendor dashboard
|
|
*/
|
|
function languageSelector(currentLang, enabledLanguages) {
|
|
return {
|
|
isLangOpen: false,
|
|
currentLang: currentLang || 'fr',
|
|
languages: enabledLanguages || ['en', 'fr', 'de', 'lb'],
|
|
languageNames: {
|
|
'en': 'English',
|
|
'fr': 'Français',
|
|
'de': 'Deutsch',
|
|
'lb': 'Lëtzebuergesch'
|
|
},
|
|
languageFlags: {
|
|
'en': 'gb',
|
|
'fr': 'fr',
|
|
'de': 'de',
|
|
'lb': 'lu'
|
|
},
|
|
async setLanguage(lang) {
|
|
if (lang === this.currentLang) {
|
|
this.isLangOpen = false;
|
|
return;
|
|
}
|
|
try {
|
|
const response = await fetch('/api/v1/language/set', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ language: lang })
|
|
});
|
|
if (response.ok) {
|
|
this.currentLang = lang;
|
|
window.location.reload();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to set language:', error);
|
|
}
|
|
this.isLangOpen = false;
|
|
}
|
|
};
|
|
}
|
|
|
|
window.languageSelector = languageSelector;
|
|
|
|
/**
|
|
* Email Settings Warning Component
|
|
* Shows warning banner when vendor email settings are not configured
|
|
*
|
|
* Usage in template:
|
|
* <div x-data="emailSettingsWarning()" x-show="showWarning">...</div>
|
|
*/
|
|
function emailSettingsWarning() {
|
|
return {
|
|
showWarning: false,
|
|
loading: true,
|
|
vendorCode: null,
|
|
|
|
async init() {
|
|
// Get vendor code from URL
|
|
const path = window.location.pathname;
|
|
const segments = path.split('/').filter(Boolean);
|
|
if (segments[0] === 'vendor' && segments[1]) {
|
|
this.vendorCode = segments[1];
|
|
}
|
|
|
|
// Skip if we're on the settings page (to avoid showing banner on config page)
|
|
if (path.includes('/settings')) {
|
|
this.loading = false;
|
|
return;
|
|
}
|
|
|
|
// Check email settings status
|
|
await this.checkEmailStatus();
|
|
},
|
|
|
|
async checkEmailStatus() {
|
|
try {
|
|
const response = await apiClient.get('/vendor/email-settings/status');
|
|
// Show warning if not configured
|
|
this.showWarning = !response.is_configured;
|
|
} catch (error) {
|
|
// Don't show warning on error (might be 401, etc.)
|
|
console.debug('[EmailWarning] Failed to check email status:', error);
|
|
this.showWarning = false;
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
window.emailSettingsWarning = emailSettingsWarning; |