Email template management

This commit is contained in:
James Pattinson
2025-11-10 16:07:22 +00:00
parent 43b13ef52d
commit 7fd237c28b
17 changed files with 1421 additions and 259 deletions
@@ -0,0 +1,401 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
interface EmailTemplate {
template_key: string;
name: string;
subject: string;
html_body: string;
text_body: string;
variables: string; // This comes as JSON string from backend
is_active: boolean;
}
const EmailTemplateManagement: React.FC = () => {
const [templates, setTemplates] = useState<EmailTemplate[]>([]);
const [loading, setLoading] = useState(true);
const [editingTemplate, setEditingTemplate] = useState<EmailTemplate | null>(null);
const [showEditForm, setShowEditForm] = useState(false);
useEffect(() => {
fetchTemplates();
}, []);
const fetchTemplates = async () => {
try {
const token = localStorage.getItem('token');
const response = await axios.get('/api/v1/email-templates/', {
headers: { Authorization: `Bearer ${token}` }
});
setTemplates(response.data);
} catch (error) {
console.error('Error fetching email templates:', error);
} finally {
setLoading(false);
}
};
const handleEditTemplate = (template: EmailTemplate) => {
setEditingTemplate(template);
setShowEditForm(true);
};
const handleSaveTemplate = async (updatedTemplate: EmailTemplate) => {
try {
const token = localStorage.getItem('token');
await axios.put(`/api/v1/email-templates/${updatedTemplate.template_key}`, updatedTemplate, {
headers: { Authorization: `Bearer ${token}` }
});
setShowEditForm(false);
setEditingTemplate(null);
fetchTemplates(); // Refresh the list
} catch (error) {
console.error('Error updating email template:', error);
}
};
const handleCancelEdit = () => {
setShowEditForm(false);
setEditingTemplate(null);
};
if (loading) {
return <div style={{ padding: '20px', textAlign: 'center' }}>Loading email templates...</div>;
}
return (
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '20px' }}>
<div style={{ marginBottom: '20px' }}>
<button
onClick={fetchTemplates}
style={{
padding: '8px 16px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Refresh Templates
</button>
</div>
<div style={{ display: 'grid', gap: '20px', gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))' }}>
{templates.map((template) => (
<div
key={template.template_key}
style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '20px',
backgroundColor: 'white',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
<h3 style={{ margin: 0, color: '#333', fontSize: '18px' }}>{template.name}</h3>
<div>
<span style={{
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px',
fontWeight: 'bold',
backgroundColor: template.is_active ? '#d4edda' : '#f8d7da',
color: template.is_active ? '#155724' : '#721c24'
}}>
{template.is_active ? 'Active' : 'Inactive'}
</span>
<button
onClick={() => handleEditTemplate(template)}
style={{
marginLeft: '10px',
padding: '6px 12px',
backgroundColor: '#28a745',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
Edit
</button>
</div>
</div>
<div style={{ marginBottom: '10px' }}>
<strong style={{ color: '#666' }}>Key:</strong> <code style={{ backgroundColor: '#f8f9fa', padding: '2px 4px', borderRadius: '3px' }}>{template.template_key}</code>
</div>
<div style={{ marginBottom: '10px' }}>
<strong style={{ color: '#666' }}>Subject:</strong> <span style={{ color: '#333' }}>{template.subject}</span>
</div>
<div style={{ marginBottom: '15px' }}>
<strong style={{ color: '#666' }}>Variables:</strong>
<div style={{ marginTop: '5px', fontFamily: 'monospace', fontSize: '14px', color: '#333' }}>
{(() => {
try {
const vars = JSON.parse(template.variables);
return Array.isArray(vars) ? vars.join(', ') : template.variables;
} catch {
return template.variables;
}
})()}
</div>
</div>
<div>
<strong style={{ color: '#666' }}>HTML Body Preview:</strong>
<div
style={{
marginTop: '8px',
padding: '12px',
backgroundColor: '#f8f9fa',
border: '1px solid #e9ecef',
borderRadius: '4px',
maxHeight: '200px',
overflow: 'auto',
fontSize: '13px',
lineHeight: '1.4',
color: '#333'
}}
dangerouslySetInnerHTML={{ __html: template.html_body.substring(0, 300) + '...' }}
/>
</div>
</div>
))}
</div>
{showEditForm && editingTemplate && (
<EmailTemplateEditForm
template={editingTemplate}
onSave={handleSaveTemplate}
onCancel={handleCancelEdit}
/>
)}
</div>
);
};
interface EmailTemplateEditFormProps {
template: EmailTemplate;
onSave: (template: EmailTemplate) => void;
onCancel: () => void;
}
const EmailTemplateEditForm: React.FC<EmailTemplateEditFormProps> = ({ template, onSave, onCancel }) => {
const [formData, setFormData] = useState({
name: template.name,
subject: template.subject,
html_body: template.html_body,
text_body: template.text_body,
variables: (() => {
try {
const vars = JSON.parse(template.variables);
return Array.isArray(vars) ? vars : [];
} catch {
return [];
}
})(),
is_active: template.is_active
});
const handleChange = (field: keyof EmailTemplate, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const dataToSave = {
template_key: template.template_key,
...formData,
variables: JSON.stringify(formData.variables)
};
onSave(dataToSave);
}; return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000
}}>
<div style={{
backgroundColor: 'white',
padding: '20px',
borderRadius: '8px',
width: '90%',
maxWidth: '800px',
maxHeight: '90vh',
overflow: 'auto'
}}>
<h3 style={{ marginTop: 0, marginBottom: '20px' }}>Edit Email Template: {template.name}</h3>
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Template Key:
</label>
<input
type="text"
value={template.template_key}
disabled
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#f5f5f5'
}}
/>
<small style={{ color: '#666' }}>Template key cannot be changed</small>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Name:
</label>
<input
type="text"
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
required
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Subject:
</label>
<input
type="text"
value={formData.subject}
onChange={(e) => handleChange('subject', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
required
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Variables (comma-separated):
</label>
<input
type="text"
value={formData.variables.join(', ')}
onChange={(e) => handleChange('variables', e.target.value.split(',').map(v => v.trim()))}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
HTML Body:
</label>
<textarea
value={formData.html_body}
onChange={(e) => handleChange('html_body', e.target.value)}
rows={15}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px',
fontFamily: 'monospace',
fontSize: '14px'
}}
required
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Text Body:
</label>
<textarea
value={formData.text_body}
onChange={(e) => handleChange('text_body', e.target.value)}
rows={10}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px',
fontFamily: 'monospace',
fontSize: '14px'
}}
required
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'flex', alignItems: 'center' }}>
<input
type="checkbox"
checked={formData.is_active}
onChange={(e) => handleChange('is_active', e.target.checked)}
style={{ marginRight: '8px' }}
/>
Active
</label>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px' }}>
<button
type="button"
onClick={onCancel}
style={{
padding: '8px 16px',
backgroundColor: '#6c757d',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Cancel
</button>
<button
type="submit"
style={{
padding: '8px 16px',
backgroundColor: '#28a745',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Save Changes
</button>
</div>
</form>
</div>
</div>
);
};
export default EmailTemplateManagement;
+22 -23
View File
@@ -1,7 +1,6 @@
import React, { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { authService } from '../services/membershipService';
import SuperAdminMenu from './SuperAdminMenu';
interface ProfileMenuProps {
userName: string;
@@ -11,7 +10,6 @@ interface ProfileMenuProps {
const ProfileMenu: React.FC<ProfileMenuProps> = ({ userName, userRole }) => {
const [isOpen, setIsOpen] = useState(false);
const [showChangePassword, setShowChangePassword] = useState(false);
const [showSuperAdmin, setShowSuperAdmin] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const navigate = useNavigate();
@@ -38,15 +36,6 @@ const ProfileMenu: React.FC<ProfileMenuProps> = ({ userName, userRole }) => {
setIsOpen(false);
};
const handleSuperAdmin = () => {
setShowSuperAdmin(true);
setIsOpen(false);
};
const handleCloseSuperAdmin = () => {
setShowSuperAdmin(false);
};
const dropdownStyle: React.CSSProperties = {
position: 'absolute',
top: '100%',
@@ -94,17 +83,31 @@ const ProfileMenu: React.FC<ProfileMenuProps> = ({ userName, userRole }) => {
{isOpen && (
<div style={dropdownStyle}>
{userRole === 'super_admin' && (
<button
style={{ ...menuItemStyle, borderRadius: '4px 4px 0 0' }}
onClick={handleSuperAdmin}
>
Super Admin Panel
</button>
<>
<button
style={{ ...menuItemStyle, borderRadius: '4px 4px 0 0' }}
onClick={() => {
navigate('/membership-tiers');
setIsOpen(false);
}}
>
Membership Tiers
</button>
<button
style={{ ...menuItemStyle, borderTop: '1px solid #eee', borderRadius: '0' }}
onClick={() => {
navigate('/email-templates');
setIsOpen(false);
}}
>
Email Templates
</button>
</>
)}
<button
style={{
...menuItemStyle,
borderRadius: userRole === 'super_admin' ? '0' : '4px 4px 0 0',
borderRadius: userRole === 'super_admin' ? '0 0 4px 4px' : '4px 4px 0 0',
borderTop: userRole === 'super_admin' ? '1px solid #eee' : 'none'
}}
onClick={handleChangePassword}
@@ -122,11 +125,7 @@ const ProfileMenu: React.FC<ProfileMenuProps> = ({ userName, userRole }) => {
</div>
{showChangePassword && (
<ChangePasswordModal onClose={handleCloseChangePassword} />
)}
{showSuperAdmin && (
<SuperAdminMenu onClose={handleCloseSuperAdmin} />
<ChangePasswordModal onClose={() => setShowChangePassword(false)} />
)}
</>
);
+15 -4
View File
@@ -1,12 +1,13 @@
import React, { useState, useEffect } from 'react';
import { membershipService, MembershipTier, MembershipTierCreateData, MembershipTierUpdateData } from '../services/membershipService';
import EmailTemplateManagement from './EmailTemplateManagement';
interface SuperAdminMenuProps {
onClose: () => void;
}
const SuperAdminMenu: React.FC<SuperAdminMenuProps> = ({ onClose }) => {
const [activeTab, setActiveTab] = useState<'tiers' | 'users' | 'system'>('tiers');
const [activeTab, setActiveTab] = useState<'tiers' | 'users' | 'email' | 'system'>('tiers');
const [tiers, setTiers] = useState<MembershipTier[]>([]);
const [loading, setLoading] = useState(false);
const [showCreateForm, setShowCreateForm] = useState(false);
@@ -31,9 +32,9 @@ const SuperAdminMenu: React.FC<SuperAdminMenuProps> = ({ onClose }) => {
}
};
const handleCreateTier = async (data: MembershipTierCreateData) => {
const handleCreateTier = async (data: MembershipTierCreateData | MembershipTierUpdateData) => {
try {
await membershipService.createTier(data);
await membershipService.createTier(data as MembershipTierCreateData);
setShowCreateForm(false);
loadTiers();
} catch (error: any) {
@@ -97,6 +98,12 @@ const SuperAdminMenu: React.FC<SuperAdminMenuProps> = ({ onClose }) => {
>
User Management
</button>
<button
onClick={() => setActiveTab('email')}
className={activeTab === 'email' ? 'tab-active' : 'tab-inactive'}
>
Email Templates
</button>
<button
onClick={() => setActiveTab('system')}
className={activeTab === 'system' ? 'tab-active' : 'tab-inactive'}
@@ -128,6 +135,10 @@ const SuperAdminMenu: React.FC<SuperAdminMenuProps> = ({ onClose }) => {
</div>
)}
{activeTab === 'email' && (
<EmailTemplateManagement />
)}
{activeTab === 'system' && (
<div style={{ padding: '20px', textAlign: 'center' }} className="super-admin-placeholder">
System settings coming soon...
@@ -143,7 +154,7 @@ interface TierManagementProps {
loading: boolean;
showCreateForm: boolean;
editingTier: MembershipTier | null;
onCreateTier: (data: MembershipTierCreateData) => void;
onCreateTier: (data: MembershipTierCreateData | MembershipTierUpdateData) => void;
onUpdateTier: (tierId: number, data: MembershipTierUpdateData) => void;
onDeleteTier: (tierId: number) => void;
onShowCreateForm: () => void;