feat: add email settings with database overrides for admin and vendor
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>
This commit is contained in:
@@ -39,6 +39,45 @@ function adminSettings() {
|
||||
carrier_colissimo_label_url: '',
|
||||
carrier_xpresslogistics_label_url: ''
|
||||
},
|
||||
emailSettings: {
|
||||
provider: 'smtp',
|
||||
from_email: '',
|
||||
from_name: '',
|
||||
reply_to: '',
|
||||
smtp_host: '',
|
||||
smtp_port: 587,
|
||||
smtp_user: '',
|
||||
mailgun_domain: '',
|
||||
aws_region: '',
|
||||
debug: false,
|
||||
enabled: true,
|
||||
is_configured: false,
|
||||
has_db_overrides: false
|
||||
},
|
||||
// Email editing form (separate from display to track changes)
|
||||
emailForm: {
|
||||
provider: 'smtp',
|
||||
from_email: '',
|
||||
from_name: '',
|
||||
reply_to: '',
|
||||
smtp_host: '',
|
||||
smtp_port: 587,
|
||||
smtp_user: '',
|
||||
smtp_password: '',
|
||||
smtp_use_tls: true,
|
||||
smtp_use_ssl: false,
|
||||
sendgrid_api_key: '',
|
||||
mailgun_api_key: '',
|
||||
mailgun_domain: '',
|
||||
aws_access_key_id: '',
|
||||
aws_secret_access_key: '',
|
||||
aws_region: 'eu-west-1',
|
||||
enabled: true,
|
||||
debug: false
|
||||
},
|
||||
emailEditMode: false,
|
||||
testEmailAddress: '',
|
||||
sendingTestEmail: false,
|
||||
|
||||
async init() {
|
||||
// Guard against multiple initialization
|
||||
@@ -50,7 +89,8 @@ function adminSettings() {
|
||||
await Promise.all([
|
||||
this.loadDisplaySettings(),
|
||||
this.loadLogSettings(),
|
||||
this.loadShippingSettings()
|
||||
this.loadShippingSettings(),
|
||||
this.loadEmailSettings()
|
||||
]);
|
||||
} catch (error) {
|
||||
settingsLog.error('Init failed:', error);
|
||||
@@ -64,7 +104,8 @@ function adminSettings() {
|
||||
await Promise.all([
|
||||
this.loadDisplaySettings(),
|
||||
this.loadLogSettings(),
|
||||
this.loadShippingSettings()
|
||||
this.loadShippingSettings(),
|
||||
this.loadEmailSettings()
|
||||
]);
|
||||
},
|
||||
|
||||
@@ -266,6 +307,190 @@ function adminSettings() {
|
||||
const prefix = this.shippingSettings[`carrier_${carrier}_label_url`] || '';
|
||||
if (!prefix || !shipmentNumber) return null;
|
||||
return prefix + shipmentNumber;
|
||||
},
|
||||
|
||||
// =====================================================================
|
||||
// EMAIL SETTINGS
|
||||
// =====================================================================
|
||||
|
||||
async loadEmailSettings() {
|
||||
try {
|
||||
const data = await apiClient.get('/admin/settings/email/status');
|
||||
this.emailSettings = {
|
||||
provider: data.provider || 'smtp',
|
||||
from_email: data.from_email || '',
|
||||
from_name: data.from_name || '',
|
||||
reply_to: data.reply_to || '',
|
||||
smtp_host: data.smtp_host || '',
|
||||
smtp_port: data.smtp_port || 587,
|
||||
smtp_user: data.smtp_user || '',
|
||||
mailgun_domain: data.mailgun_domain || '',
|
||||
aws_region: data.aws_region || '',
|
||||
debug: data.debug || false,
|
||||
enabled: data.enabled !== false,
|
||||
is_configured: data.is_configured || false,
|
||||
has_db_overrides: data.has_db_overrides || false
|
||||
};
|
||||
// Populate edit form with current values
|
||||
this.populateEmailForm();
|
||||
settingsLog.info('Email settings loaded:', this.emailSettings);
|
||||
} catch (error) {
|
||||
settingsLog.error('Failed to load email settings:', error);
|
||||
// Use defaults on error
|
||||
}
|
||||
},
|
||||
|
||||
populateEmailForm() {
|
||||
// Copy current settings to form (passwords are not loaded from API)
|
||||
this.emailForm = {
|
||||
provider: this.emailSettings.provider,
|
||||
from_email: this.emailSettings.from_email,
|
||||
from_name: this.emailSettings.from_name,
|
||||
reply_to: this.emailSettings.reply_to || '',
|
||||
smtp_host: this.emailSettings.smtp_host || '',
|
||||
smtp_port: this.emailSettings.smtp_port || 587,
|
||||
smtp_user: this.emailSettings.smtp_user || '',
|
||||
smtp_password: '', // Never populated from API
|
||||
smtp_use_tls: true,
|
||||
smtp_use_ssl: false,
|
||||
sendgrid_api_key: '',
|
||||
mailgun_api_key: '',
|
||||
mailgun_domain: this.emailSettings.mailgun_domain || '',
|
||||
aws_access_key_id: '',
|
||||
aws_secret_access_key: '',
|
||||
aws_region: this.emailSettings.aws_region || 'eu-west-1',
|
||||
enabled: this.emailSettings.enabled,
|
||||
debug: this.emailSettings.debug
|
||||
};
|
||||
},
|
||||
|
||||
enableEmailEditing() {
|
||||
this.emailEditMode = true;
|
||||
this.populateEmailForm();
|
||||
},
|
||||
|
||||
cancelEmailEditing() {
|
||||
this.emailEditMode = false;
|
||||
this.populateEmailForm();
|
||||
},
|
||||
|
||||
async saveEmailSettings() {
|
||||
this.saving = true;
|
||||
this.error = null;
|
||||
this.successMessage = null;
|
||||
|
||||
try {
|
||||
// Only send non-empty values to update
|
||||
const payload = {};
|
||||
|
||||
// Always send these core fields
|
||||
if (this.emailForm.provider) payload.provider = this.emailForm.provider;
|
||||
if (this.emailForm.from_email) payload.from_email = this.emailForm.from_email;
|
||||
if (this.emailForm.from_name) payload.from_name = this.emailForm.from_name;
|
||||
if (this.emailForm.reply_to) payload.reply_to = this.emailForm.reply_to;
|
||||
payload.enabled = this.emailForm.enabled;
|
||||
payload.debug = this.emailForm.debug;
|
||||
|
||||
// Provider-specific fields
|
||||
if (this.emailForm.provider === 'smtp') {
|
||||
if (this.emailForm.smtp_host) payload.smtp_host = this.emailForm.smtp_host;
|
||||
if (this.emailForm.smtp_port) payload.smtp_port = this.emailForm.smtp_port;
|
||||
if (this.emailForm.smtp_user) payload.smtp_user = this.emailForm.smtp_user;
|
||||
if (this.emailForm.smtp_password) payload.smtp_password = this.emailForm.smtp_password;
|
||||
payload.smtp_use_tls = this.emailForm.smtp_use_tls;
|
||||
payload.smtp_use_ssl = this.emailForm.smtp_use_ssl;
|
||||
} else if (this.emailForm.provider === 'sendgrid') {
|
||||
if (this.emailForm.sendgrid_api_key) payload.sendgrid_api_key = this.emailForm.sendgrid_api_key;
|
||||
} else if (this.emailForm.provider === 'mailgun') {
|
||||
if (this.emailForm.mailgun_api_key) payload.mailgun_api_key = this.emailForm.mailgun_api_key;
|
||||
if (this.emailForm.mailgun_domain) payload.mailgun_domain = this.emailForm.mailgun_domain;
|
||||
} else if (this.emailForm.provider === 'ses') {
|
||||
if (this.emailForm.aws_access_key_id) payload.aws_access_key_id = this.emailForm.aws_access_key_id;
|
||||
if (this.emailForm.aws_secret_access_key) payload.aws_secret_access_key = this.emailForm.aws_secret_access_key;
|
||||
if (this.emailForm.aws_region) payload.aws_region = this.emailForm.aws_region;
|
||||
}
|
||||
|
||||
const data = await apiClient.put('/admin/settings/email/settings', payload);
|
||||
|
||||
this.successMessage = data.message || 'Email settings saved successfully';
|
||||
this.emailEditMode = false;
|
||||
|
||||
// Reload to get updated status
|
||||
await this.loadEmailSettings();
|
||||
|
||||
setTimeout(() => {
|
||||
this.successMessage = null;
|
||||
}, 5000);
|
||||
|
||||
settingsLog.info('Email settings saved successfully');
|
||||
} catch (error) {
|
||||
settingsLog.error('Failed to save email settings:', error);
|
||||
this.error = error.message || 'Failed to save email settings';
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
async resetEmailSettings() {
|
||||
if (!confirm('This will reset all email settings to use .env defaults. Continue?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
this.error = null;
|
||||
this.successMessage = null;
|
||||
|
||||
try {
|
||||
const data = await apiClient.delete('/admin/settings/email/settings');
|
||||
|
||||
this.successMessage = data.message || 'Email settings reset to defaults';
|
||||
this.emailEditMode = false;
|
||||
|
||||
// Reload to get .env values
|
||||
await this.loadEmailSettings();
|
||||
|
||||
setTimeout(() => {
|
||||
this.successMessage = null;
|
||||
}, 5000);
|
||||
|
||||
settingsLog.info('Email settings reset successfully');
|
||||
} catch (error) {
|
||||
settingsLog.error('Failed to reset email settings:', error);
|
||||
this.error = error.message || 'Failed to reset email settings';
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
async sendTestEmail() {
|
||||
if (!this.testEmailAddress) {
|
||||
this.error = 'Please enter a test email address';
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendingTestEmail = true;
|
||||
this.error = null;
|
||||
this.successMessage = null;
|
||||
|
||||
try {
|
||||
const data = await apiClient.post('/admin/settings/email/test', {
|
||||
to_email: this.testEmailAddress
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
this.successMessage = `Test email sent to ${this.testEmailAddress}`;
|
||||
setTimeout(() => {
|
||||
this.successMessage = null;
|
||||
}, 5000);
|
||||
} else {
|
||||
this.error = data.message || 'Failed to send test email';
|
||||
}
|
||||
} catch (error) {
|
||||
settingsLog.error('Failed to send test email:', error);
|
||||
this.error = error.message || 'Failed to send test email';
|
||||
} finally {
|
||||
this.sendingTestEmail = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
3
static/vendor/js/analytics.js
vendored
3
static/vendor/js/analytics.js
vendored
@@ -166,7 +166,8 @@ function vendorAnalytics() {
|
||||
*/
|
||||
formatNumber(num) {
|
||||
if (num === null || num === undefined) return '0';
|
||||
return num.toLocaleString();
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
return num.toLocaleString(locale);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
9
static/vendor/js/billing.js
vendored
9
static/vendor/js/billing.js
vendored
@@ -189,7 +189,8 @@ function vendorBilling() {
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
return date.toLocaleDateString(locale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
@@ -199,9 +200,11 @@ function vendorBilling() {
|
||||
formatCurrency(cents, currency = 'EUR') {
|
||||
if (cents === null || cents === undefined) return '-';
|
||||
const amount = cents / 100;
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
const currencyCode = window.VENDOR_CONFIG?.currency || currency;
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: currency
|
||||
currency: currencyCode
|
||||
}).format(amount);
|
||||
}
|
||||
};
|
||||
|
||||
9
static/vendor/js/customers.js
vendored
9
static/vendor/js/customers.js
vendored
@@ -264,7 +264,8 @@ function vendorCustomers() {
|
||||
*/
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
return new Date(dateStr).toLocaleDateString('de-DE', {
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
return new Date(dateStr).toLocaleDateString(locale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
@@ -276,9 +277,11 @@ function vendorCustomers() {
|
||||
*/
|
||||
formatPrice(cents) {
|
||||
if (!cents && cents !== 0) return '-';
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
const currency = window.VENDOR_CONFIG?.currency || 'EUR';
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
currency: currency
|
||||
}).format(cents / 100);
|
||||
},
|
||||
|
||||
|
||||
9
static/vendor/js/dashboard.js
vendored
9
static/vendor/js/dashboard.js
vendored
@@ -107,16 +107,19 @@ function vendorDashboard() {
|
||||
},
|
||||
|
||||
formatCurrency(amount) {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
const currency = window.VENDOR_CONFIG?.currency || 'EUR';
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
currency: currency
|
||||
}).format(amount || 0);
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
return date.toLocaleDateString(locale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
|
||||
51
static/vendor/js/init-alpine.js
vendored
51
static/vendor/js/init-alpine.js
vendored
@@ -218,4 +218,53 @@ function languageSelector(currentLang, enabledLanguages) {
|
||||
};
|
||||
}
|
||||
|
||||
window.languageSelector = languageSelector;
|
||||
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;
|
||||
3
static/vendor/js/inventory.js
vendored
3
static/vendor/js/inventory.js
vendored
@@ -353,7 +353,8 @@ function vendorInventory() {
|
||||
*/
|
||||
formatNumber(num) {
|
||||
if (num === null || num === undefined) return '0';
|
||||
return new Intl.NumberFormat('en-US').format(num);
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
return new Intl.NumberFormat(locale).format(num);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
9
static/vendor/js/invoices.js
vendored
9
static/vendor/js/invoices.js
vendored
@@ -379,7 +379,8 @@ function vendorInvoices() {
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return 'N/A';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('en-GB', {
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
return date.toLocaleDateString(locale, {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
@@ -392,9 +393,11 @@ function vendorInvoices() {
|
||||
formatCurrency(cents, currency = 'EUR') {
|
||||
if (cents === null || cents === undefined) return 'N/A';
|
||||
const amount = cents / 100;
|
||||
return new Intl.NumberFormat('de-LU', {
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
const currencyCode = window.VENDOR_CONFIG?.currency || currency;
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: currency
|
||||
currency: currencyCode
|
||||
}).format(amount);
|
||||
}
|
||||
};
|
||||
|
||||
3
static/vendor/js/letzshop.js
vendored
3
static/vendor/js/letzshop.js
vendored
@@ -416,7 +416,8 @@ function vendorLetzshop() {
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return 'N/A';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
return date.toLocaleDateString(locale) + ' ' + date.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' });
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
3
static/vendor/js/marketplace.js
vendored
3
static/vendor/js/marketplace.js
vendored
@@ -262,7 +262,8 @@ function vendorMarketplace() {
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('en-US', {
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
return date.toLocaleString(locale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
|
||||
9
static/vendor/js/messages.js
vendored
9
static/vendor/js/messages.js
vendored
@@ -23,6 +23,7 @@ function vendorMessages(initialConversationId = null) {
|
||||
loadingMessages: false,
|
||||
sendingMessage: false,
|
||||
creatingConversation: false,
|
||||
error: '',
|
||||
|
||||
// Conversations state
|
||||
conversations: [],
|
||||
@@ -384,7 +385,8 @@ function vendorMessages(initialConversationId = null) {
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}m`;
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}h`;
|
||||
if (diff < 172800) return 'Yesterday';
|
||||
return date.toLocaleDateString();
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
return date.toLocaleDateString(locale);
|
||||
},
|
||||
|
||||
formatTime(dateString) {
|
||||
@@ -392,11 +394,12 @@ function vendorMessages(initialConversationId = null) {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const isToday = date.toDateString() === now.toDateString();
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
|
||||
if (isToday) {
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
return date.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
return date.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
return date.toLocaleString(locale, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
3
static/vendor/js/notifications.js
vendored
3
static/vendor/js/notifications.js
vendored
@@ -250,7 +250,8 @@ function vendorNotifications() {
|
||||
if (diff < 172800) return 'Yesterday';
|
||||
|
||||
// Show full date for older dates
|
||||
return date.toLocaleDateString();
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
return date.toLocaleDateString(locale);
|
||||
},
|
||||
|
||||
// Pagination methods
|
||||
|
||||
9
static/vendor/js/order-detail.js
vendored
9
static/vendor/js/order-detail.js
vendored
@@ -188,9 +188,11 @@ function vendorOrderDetail() {
|
||||
*/
|
||||
formatPrice(cents) {
|
||||
if (cents === null || cents === undefined) return '-';
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
const currency = window.VENDOR_CONFIG?.currency || 'EUR';
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
currency: currency
|
||||
}).format(cents / 100);
|
||||
},
|
||||
|
||||
@@ -199,7 +201,8 @@ function vendorOrderDetail() {
|
||||
*/
|
||||
formatDateTime(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
return new Date(dateStr).toLocaleDateString('de-DE', {
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
return new Date(dateStr).toLocaleDateString(locale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
|
||||
9
static/vendor/js/orders.js
vendored
9
static/vendor/js/orders.js
vendored
@@ -312,9 +312,11 @@ function vendorOrders() {
|
||||
*/
|
||||
formatPrice(cents) {
|
||||
if (!cents && cents !== 0) return '-';
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
const currency = window.VENDOR_CONFIG?.currency || 'EUR';
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
currency: currency
|
||||
}).format(cents / 100);
|
||||
},
|
||||
|
||||
@@ -323,7 +325,8 @@ function vendorOrders() {
|
||||
*/
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
return new Date(dateStr).toLocaleDateString('de-DE', {
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
return new Date(dateStr).toLocaleDateString(locale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
|
||||
6
static/vendor/js/products.js
vendored
6
static/vendor/js/products.js
vendored
@@ -321,9 +321,11 @@ function vendorProducts() {
|
||||
*/
|
||||
formatPrice(cents) {
|
||||
if (!cents && cents !== 0) return '-';
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
const currency = window.VENDOR_CONFIG?.currency || 'EUR';
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
currency: currency
|
||||
}).format(cents / 100);
|
||||
},
|
||||
|
||||
|
||||
190
static/vendor/js/settings.js
vendored
190
static/vendor/js/settings.js
vendored
@@ -40,7 +40,8 @@ function vendorSettings() {
|
||||
{ id: 'branding', label: 'Branding', icon: 'color-swatch' },
|
||||
{ id: 'domains', label: 'Domains', icon: 'globe-alt' },
|
||||
{ id: 'api', label: 'API & Payments', icon: 'key' },
|
||||
{ id: 'notifications', label: 'Notifications', icon: 'bell' }
|
||||
{ id: 'notifications', label: 'Notifications', icon: 'bell' },
|
||||
{ id: 'email', label: 'Email', icon: 'envelope' }
|
||||
],
|
||||
|
||||
// Forms for different sections
|
||||
@@ -95,6 +96,38 @@ function vendorSettings() {
|
||||
storefront_locale: ''
|
||||
},
|
||||
|
||||
// Email settings
|
||||
emailSettings: null,
|
||||
emailSettingsLoading: false,
|
||||
emailProviders: [],
|
||||
emailForm: {
|
||||
from_email: '',
|
||||
from_name: '',
|
||||
reply_to_email: '',
|
||||
signature_text: '',
|
||||
signature_html: '',
|
||||
provider: 'smtp',
|
||||
// SMTP
|
||||
smtp_host: '',
|
||||
smtp_port: 587,
|
||||
smtp_username: '',
|
||||
smtp_password: '',
|
||||
smtp_use_tls: true,
|
||||
smtp_use_ssl: false,
|
||||
// SendGrid
|
||||
sendgrid_api_key: '',
|
||||
// Mailgun
|
||||
mailgun_api_key: '',
|
||||
mailgun_domain: '',
|
||||
// SES
|
||||
ses_access_key_id: '',
|
||||
ses_secret_access_key: '',
|
||||
ses_region: 'eu-west-1'
|
||||
},
|
||||
testEmailAddress: '',
|
||||
sendingTestEmail: false,
|
||||
hasEmailChanges: false,
|
||||
|
||||
// Track changes per section
|
||||
hasChanges: false,
|
||||
hasBusinessChanges: false,
|
||||
@@ -383,6 +416,161 @@ function vendorSettings() {
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// =====================================================================
|
||||
// EMAIL SETTINGS
|
||||
// =====================================================================
|
||||
|
||||
/**
|
||||
* Load email settings when email tab is activated
|
||||
*/
|
||||
async loadEmailSettings() {
|
||||
if (this.emailSettings !== null) {
|
||||
return; // Already loaded
|
||||
}
|
||||
|
||||
this.emailSettingsLoading = true;
|
||||
try {
|
||||
// Load settings and providers in parallel
|
||||
const [settingsResponse, providersResponse] = await Promise.all([
|
||||
apiClient.get('/vendor/email-settings'),
|
||||
apiClient.get('/vendor/email-settings/providers')
|
||||
]);
|
||||
|
||||
this.emailProviders = providersResponse.providers || [];
|
||||
|
||||
if (settingsResponse.configured && settingsResponse.settings) {
|
||||
this.emailSettings = settingsResponse.settings;
|
||||
this.populateEmailForm(settingsResponse.settings);
|
||||
} else {
|
||||
this.emailSettings = { is_configured: false, is_verified: false };
|
||||
}
|
||||
|
||||
vendorSettingsLog.info('Loaded email settings');
|
||||
} catch (error) {
|
||||
vendorSettingsLog.error('Failed to load email settings:', error);
|
||||
Utils.showToast('Failed to load email settings', 'error');
|
||||
} finally {
|
||||
this.emailSettingsLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Populate email form from settings
|
||||
*/
|
||||
populateEmailForm(settings) {
|
||||
this.emailForm = {
|
||||
from_email: settings.from_email || '',
|
||||
from_name: settings.from_name || '',
|
||||
reply_to_email: settings.reply_to_email || '',
|
||||
signature_text: settings.signature_text || '',
|
||||
signature_html: settings.signature_html || '',
|
||||
provider: settings.provider || 'smtp',
|
||||
// SMTP - don't populate password
|
||||
smtp_host: settings.smtp_host || '',
|
||||
smtp_port: settings.smtp_port || 587,
|
||||
smtp_username: settings.smtp_username || '',
|
||||
smtp_password: '', // Never populate password
|
||||
smtp_use_tls: settings.smtp_use_tls !== false,
|
||||
smtp_use_ssl: settings.smtp_use_ssl || false,
|
||||
// SendGrid - don't populate API key
|
||||
sendgrid_api_key: '',
|
||||
// Mailgun - don't populate API key
|
||||
mailgun_api_key: '',
|
||||
mailgun_domain: settings.mailgun_domain || '',
|
||||
// SES - don't populate secrets
|
||||
ses_access_key_id: '',
|
||||
ses_secret_access_key: '',
|
||||
ses_region: settings.ses_region || 'eu-west-1'
|
||||
};
|
||||
this.hasEmailChanges = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark email form as changed
|
||||
*/
|
||||
markEmailChanged() {
|
||||
this.hasEmailChanges = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Save email settings
|
||||
*/
|
||||
async saveEmailSettings() {
|
||||
// Validate required fields
|
||||
if (!this.emailForm.from_email || !this.emailForm.from_name) {
|
||||
Utils.showToast('From Email and From Name are required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const response = await apiClient.put('/vendor/email-settings', this.emailForm);
|
||||
|
||||
if (response.success) {
|
||||
Utils.showToast('Email settings saved', 'success');
|
||||
vendorSettingsLog.info('Email settings updated');
|
||||
|
||||
// Update local state
|
||||
this.emailSettings = response.settings;
|
||||
this.hasEmailChanges = false;
|
||||
} else {
|
||||
Utils.showToast(response.message || 'Failed to save email settings', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
vendorSettingsLog.error('Failed to save email settings:', error);
|
||||
Utils.showToast(error.message || 'Failed to save email settings', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send test email
|
||||
*/
|
||||
async sendTestEmail() {
|
||||
if (!this.testEmailAddress) {
|
||||
Utils.showToast('Please enter a test email address', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.emailSettings?.is_configured) {
|
||||
Utils.showToast('Please save your email settings first', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendingTestEmail = true;
|
||||
try {
|
||||
const response = await apiClient.post('/vendor/email-settings/verify', {
|
||||
test_email: this.testEmailAddress
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
Utils.showToast('Test email sent! Check your inbox.', 'success');
|
||||
// Update verification status
|
||||
this.emailSettings.is_verified = true;
|
||||
} else {
|
||||
Utils.showToast(response.message || 'Failed to send test email', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
vendorSettingsLog.error('Failed to send test email:', error);
|
||||
Utils.showToast(error.message || 'Failed to send test email', 'error');
|
||||
} finally {
|
||||
this.sendingTestEmail = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Switch active section - with email loading hook
|
||||
*/
|
||||
setSection(sectionId) {
|
||||
this.activeSection = sectionId;
|
||||
|
||||
// Load email settings when email tab is activated
|
||||
if (sectionId === 'email' && this.emailSettings === null) {
|
||||
this.loadEmailSettings();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
3
static/vendor/js/team.js
vendored
3
static/vendor/js/team.js
vendored
@@ -264,7 +264,8 @@ function vendorTeam() {
|
||||
*/
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
return new Date(dateStr).toLocaleDateString('de-DE', {
|
||||
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
|
||||
return new Date(dateStr).toLocaleDateString(locale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
|
||||
Reference in New Issue
Block a user