Files
orion/app/modules/messaging/static/admin/js/email-templates.js
Samir Boulahtit 3bdf1695fd fix: move init guard to prevent race conditions in email-templates
The initialization guard was placed after the await I18n.loadModule() call,
which could cause race conditions if Alpine calls init() multiple times
before the guard is set. This aligns with the pattern used by other pages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 22:26:05 +01:00

278 lines
9.9 KiB
JavaScript

/**
* Email Templates Management Page
*
* Handles:
* - Listing all platform email templates
* - Editing template content (all languages)
* - Preview and test email sending
*/
const emailTemplatesLog = window.LogConfig?.createLogger?.('emailTemplates') ||
{ info: () => {}, debug: () => {}, warn: () => {}, error: () => {} };
function emailTemplatesPage() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Set current page for navigation
currentPage: 'email-templates',
// Data
loading: true,
templates: [],
categories: [],
selectedCategory: null,
// Edit Modal
showEditModal: false,
editingTemplate: null,
editLanguage: 'en',
loadingTemplate: false,
editForm: {
subject: '',
body_html: '',
body_text: '',
variables: [],
required_variables: []
},
saving: false,
// Preview Modal
showPreviewModal: false,
previewData: null,
// Test Email Modal
showTestEmailModal: false,
testEmailAddress: '',
sendingTest: false,
// Computed
get filteredTemplates() {
if (!this.selectedCategory) {
return this.templates;
}
return this.templates.filter(t => t.category === this.selectedCategory);
},
// Lifecycle
async init() {
// Guard against duplicate initialization
if (window._adminEmailTemplatesInitialized) return;
window._adminEmailTemplatesInitialized = true;
// Load i18n translations
await I18n.loadModule('messaging');
await this.loadData();
},
// Data Loading
async loadData() {
this.loading = true;
try {
const [templatesData, categoriesData] = await Promise.all([
apiClient.get('/admin/email-templates'),
apiClient.get('/admin/email-templates/categories')
]);
this.templates = templatesData.templates || [];
this.categories = categoriesData.categories || [];
} catch (error) {
emailTemplatesLog.error('Failed to load email templates:', error);
Utils.showToast(I18n.t('messaging.messages.failed_to_load_templates'), 'error');
} finally {
this.loading = false;
}
},
// Category styling
getCategoryClass(category) {
const classes = {
'auth': 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200',
'orders': 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
'billing': 'bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200',
'system': 'bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200',
'marketing': 'bg-pink-100 dark:bg-pink-900 text-pink-800 dark:text-pink-200'
};
return classes[category] || 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200';
},
// Edit Template
async editTemplate(template) {
this.editingTemplate = template;
this.editLanguage = 'en';
this.showEditModal = true;
await this.loadTemplateLanguage();
},
async loadTemplateLanguage() {
if (!this.editingTemplate) return;
this.loadingTemplate = true;
try {
const data = await apiClient.get(
`/admin/email-templates/${this.editingTemplate.code}/${this.editLanguage}`
);
this.editForm = {
subject: data.subject || '',
body_html: data.body_html || '',
body_text: data.body_text || '',
variables: data.variables || [],
required_variables: data.required_variables || []
};
} catch (error) {
if (error.status === 404) {
// Template doesn't exist for this language yet
this.editForm = {
subject: '',
body_html: '',
body_text: '',
variables: [],
required_variables: []
};
Utils.showToast(I18n.t('messaging.messages.no_template_for_language', { language: this.editLanguage.toUpperCase() }), 'info');
} else {
emailTemplatesLog.error('Failed to load template:', error);
Utils.showToast(I18n.t('messaging.messages.failed_to_load_template'), 'error');
}
} finally {
this.loadingTemplate = false;
}
},
closeEditModal() {
this.showEditModal = false;
this.editingTemplate = null;
this.editForm = {
subject: '',
body_html: '',
body_text: '',
variables: [],
required_variables: []
};
},
async saveTemplate() {
if (!this.editingTemplate) return;
this.saving = true;
try {
await apiClient.put(
`/admin/email-templates/${this.editingTemplate.code}/${this.editLanguage}`,
{
subject: this.editForm.subject,
body_html: this.editForm.body_html,
body_text: this.editForm.body_text
}
);
Utils.showToast(I18n.t('messaging.messages.template_saved_successfully'), 'success');
// Refresh templates list
await this.loadData();
} catch (error) {
emailTemplatesLog.error('Failed to save template:', error);
Utils.showToast(error.detail || I18n.t('messaging.messages.failed_to_save_template'), 'error');
} finally {
this.saving = false;
}
},
// Preview
async previewTemplate(template) {
try {
// Use sample variables for preview
const sampleVariables = this.getSampleVariables(template.code);
const data = await apiClient.post(
`/admin/email-templates/${template.code}/preview`,
{
template_code: template.code,
language: 'en',
variables: sampleVariables
}
);
this.previewData = data;
this.showPreviewModal = true;
} catch (error) {
emailTemplatesLog.error('Failed to preview template:', error);
Utils.showToast(I18n.t('messaging.messages.failed_to_load_preview'), 'error');
}
},
getSampleVariables(templateCode) {
// Sample variables for common templates
const samples = {
'signup_welcome': {
first_name: 'John',
company_name: 'Acme Corp',
email: 'john@example.com',
vendor_code: 'acme',
login_url: 'https://example.com/login',
trial_days: '14',
tier_name: 'Business'
},
'order_confirmation': {
customer_name: 'Jane Doe',
order_number: 'ORD-12345',
order_total: '99.99',
order_items_count: '3',
order_date: '2024-01-15',
shipping_address: '123 Main St, Luxembourg City, L-1234'
},
'password_reset': {
customer_name: 'John Doe',
reset_link: 'https://example.com/reset?token=abc123',
expiry_hours: '1'
},
'team_invite': {
invitee_name: 'Jane',
inviter_name: 'John',
vendor_name: 'Acme Corp',
role: 'Admin',
accept_url: 'https://example.com/accept',
expires_in_days: '7'
}
};
return samples[templateCode] || { platform_name: 'Wizamart' };
},
// Test Email
sendTestEmail() {
this.showTestEmailModal = true;
},
async confirmSendTestEmail() {
if (!this.testEmailAddress || !this.editingTemplate) return;
this.sendingTest = true;
try {
const result = await apiClient.post(
`/admin/email-templates/${this.editingTemplate.code}/test`,
{
template_code: this.editingTemplate.code,
language: this.editLanguage,
to_email: this.testEmailAddress,
variables: this.getSampleVariables(this.editingTemplate.code)
}
);
if (result.success) {
Utils.showToast(I18n.t('messaging.messages.test_email_sent', { email: this.testEmailAddress }), 'success');
this.showTestEmailModal = false;
this.testEmailAddress = '';
} else {
Utils.showToast(result.message || I18n.t('messaging.messages.failed_to_send_test_email'), 'error');
}
} catch (error) {
emailTemplatesLog.error('Failed to send test email:', error);
Utils.showToast(I18n.t('messaging.messages.failed_to_send_test_email'), 'error');
} finally {
this.sendingTest = false;
}
}
};
}