E-Commerce-Module/frontend/src/pages/Admin/EmailTemplatesPage.jsx

660 lines
No EOL
22 KiB
JavaScript

import React, { useState, useEffect, useRef } from 'react';
import {
Box,
Typography,
Paper,
Tabs,
Tab,
TextField,
Button,
Grid,
Divider,
CircularProgress,
Alert,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Accordion,
AccordionSummary,
AccordionDetails,
List,
ListItem,
ListItemText,
Chip,
Tooltip,
Card,
CardContent
} from '@mui/material';
import {
Add as AddIcon,
Delete as DeleteIcon,
Edit as EditIcon,
Save as SaveIcon,
Visibility as PreviewIcon,
ExpandMore as ExpandMoreIcon,
FormatBold as BoldIcon,
FormatItalic as FormatItalicIcon,
FormatListBulleted as BulletListIcon,
FormatListNumbered as NumberedListIcon,
Link as LinkIcon,
Title as TitleIcon,
Info as InfoIcon
} from '@mui/icons-material';
import EmailEditor from 'react-email-editor';
import { useAdminSettingsByCategory, useDeleteSetting, useUpdateSetting } from '../../hooks/settingsAdminHooks';
// Available email template types
const EMAIL_TYPES = [
{ id: 'login_code', name: 'Login Code', description: 'Sent when a user requests a login code' },
{ id: 'shipping_notification', name: 'Shipping Notification', description: 'Sent when an order is shipped' },
{ id: 'order_confirmation', name: 'Order Confirmation', description: 'Sent when an order is placed' },
{ id: 'low_stock_alert', name: 'Low Stock Alert', description: 'Sent when product stock falls below threshold' },
{ id: 'welcome_email', name: 'Welcome Email', description: 'Sent when a user registers for the first time' },
{ id: 'custom', name: 'Custom Template', description: 'A custom email template for any purpose' }
];
// Template variable placeholders for each email type
const TEMPLATE_VARIABLES = {
login_code: [
{ key: '{{code}}', description: 'The login verification code' },
{ key: '{{loginLink}}', description: 'Direct login link with the code' },
{ key: '{{email}}', description: 'User\'s email address' }
],
shipping_notification: [
{ key: '{{first_name}}', description: 'Customer\'s first name' },
{ key: '{{order_id}}', description: 'Order identifier' },
{ key: '{{tracking_number}}', description: 'Shipping tracking number' },
{ key: '{{carrier}}', description: 'Shipping carrier name' },
{ key: '{{tracking_link}}', description: 'Link to track the package' },
{ key: '{{shipped_date}}', description: 'Date the order was shipped' },
{ key: '{{estimated_delivery}}', description: 'Estimated delivery date/time' },
{ key: '{{items_html}}', description: 'HTML table of ordered items' },
{ key: '{{customer_message}}', description: 'Optional message from staff' }
],
order_confirmation: [
{ key: '{{first_name}}', description: 'Customer\'s first name' },
{ key: '{{order_id}}', description: 'Order identifier' },
{ key: '{{order_date}}', description: 'Date the order was placed' },
{ key: '{{order_total}}', description: 'Total amount of the order' },
{ key: '{{shipping_address}}', description: 'Shipping address' },
{ key: '{{items_html}}', description: 'HTML table of ordered items' }
],
low_stock_alert: [
{ key: '{{product_name}}', description: 'Name of the product low in stock' },
{ key: '{{current_stock}}', description: 'Current stock quantity' },
{ key: '{{threshold}}', description: 'Low stock threshold' }
],
welcome_email: [
{ key: '{{first_name}}', description: 'User\'s first name' },
{ key: '{{email}}', description: 'User\'s email address' }
],
custom: [] // Custom templates might have any variables
};
// Sample placeholder data for preview
const PREVIEW_DATA = {
login_code: {
code: '123456',
loginLink: 'https://example.com/verify?code=123456&email=user@example.com',
email: 'user@example.com'
},
shipping_notification: {
first_name: 'Jane',
order_id: 'ORD-1234567',
tracking_number: 'TRK123456789',
carrier: 'FedEx',
tracking_link: 'https://www.fedex.com/track?123456789',
shipped_date: '2025-04-29',
estimated_delivery: '2-3 business days',
items_html: `
<tr>
<td style="padding: 10px; border-bottom: 1px solid #eee;">Amethyst Geode</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">1</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">$49.99</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">$49.99</td>
</tr>
<tr>
<td style="padding: 10px; border-bottom: 1px solid #eee;">Driftwood Piece</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">2</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">$14.99</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">$29.98</td>
</tr>
`,
customer_message: 'Thank you for your order! We packaged it with extra care.'
},
order_confirmation: {
first_name: 'John',
order_id: 'ORD-9876543',
order_date: '2025-04-29',
order_total: '$94.97',
shipping_address: '123 Main St, Anytown, CA 12345',
items_html: `
<tr>
<td style="padding: 10px; border-bottom: 1px solid #eee;">Polished Labradorite</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">1</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">$29.99</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">$29.99</td>
</tr>
<tr>
<td style="padding: 10px; border-bottom: 1px solid #eee;">Fossil Fish</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">1</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">$64.98</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">$64.98</td>
</tr>
`
},
low_stock_alert: {
product_name: 'Amethyst Geode',
current_stock: '2',
threshold: '5'
},
welcome_email: {
first_name: 'Emily',
email: 'emily@example.com'
},
custom: {}
};
// Default templates
const DEFAULT_TEMPLATES = {
login_code: {
// Simplified template structure for React Email Editor
body: {
rows: [
{
cells: [1],
columns: [
{
contents: [
{
type: "text",
values: {
containerPadding: "10px",
textAlign: "left",
text: "<h1>Your login code is: {{code}}</h1><p>This code will expire in 15 minutes.</p><p>Or click <a href=\"{{loginLink}}\">here</a> to log in directly.</p>"
}
}
]
}
]
}
]
}
},
shipping_notification: {
// Simplified template - the actual structure would be more complex in the real editor
body: {
rows: [
{
cells: [1],
columns: [
{
contents: [
{
type: "text",
values: {
containerPadding: "10px",
textAlign: "center",
text: "<h1>Your Order Has Shipped!</h1><p>Order #{{order_id}}</p>"
}
}
]
}
]
},
{
cells: [1],
columns: [
{
contents: [
{
type: "text",
values: {
containerPadding: "10px",
textAlign: "left",
text: "<p>Hello {{first_name}},</p><p>Good news! Your order has been shipped and is on its way to you.</p>"
}
}
]
}
]
}
]
}
},
welcome_email: {
// Simplified template
body: {
rows: [
{
cells: [1],
columns: [
{
contents: [
{
type: "text",
values: {
containerPadding: "10px",
textAlign: "center",
text: "<h1>Welcome to Rocks, Bones & Sticks!</h1>"
}
}
]
}
]
},
{
cells: [1],
columns: [
{
contents: [
{
type: "text",
values: {
containerPadding: "10px",
textAlign: "left",
text: "<p>Hello {{first_name}},</p><p>Thank you for creating an account with us. We're excited to have you join our community!</p>"
}
}
]
}
]
}
]
}
}
};
const EmailTemplatesPage = () => {
const [activeTab, setActiveTab] = useState(0);
const [editingTemplate, setEditingTemplate] = useState(null);
const [templateList, setTemplateList] = useState([]);
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
const [previewContent, setPreviewContent] = useState('');
const emailEditorRef = useRef(null);
const { data: emailSettings, isLoading, error } = useAdminSettingsByCategory('email_templates');
const deleteSettingMutation = useDeleteSetting();
const updateSetting = useUpdateSetting();
useEffect(() => {
if (emailSettings) {
const templates = emailSettings.map(setting => {
try {
const templateData = JSON.parse(setting.value);
return { id: setting.key, ...templateData };
} catch {
return null;
}
}).filter(Boolean);
setTemplateList(templates);
}
}, [emailSettings]);
const handleTabChange = (e, newValue) => {
// Only allow tab switching if not currently editing a template
if (!editingTemplate) {
setActiveTab(newValue);
}
};
const handleEditTemplate = (template) => {
setEditingTemplate({ ...template });
// If the email editor is loaded, set its design
if (emailEditorRef.current && template.design) {
setTimeout(() => {
emailEditorRef.current.editor.loadDesign(template.design);
}, 500);
}
};
const handleSaveTemplate = async () => {
if (!editingTemplate || !emailEditorRef.current) return;
try {
// Save the design from the email editor
emailEditorRef.current.editor.exportHtml(async (data) => {
const { design, html } = data;
// Update the template with the new design and HTML
const updatedTemplate = {
...editingTemplate,
design: design, // Store the design JSON for future editing
content: html, // Store the generated HTML for rendering
updatedAt: new Date().toISOString()
};
await updateSetting.mutateAsync({
key: updatedTemplate.id,
value: JSON.stringify(updatedTemplate),
category: 'email_templates'
});
setTemplateList(prev => prev.map(t => t.id === updatedTemplate.id ? updatedTemplate : t));
setEditingTemplate(null);
});
} catch (error) {
console.error('Failed to save template:', error);
}
};
const handlePreviewTemplate = (template) => {
if (template.content) {
setPreviewContent(`
<div style="max-width:600px;margin:0 auto;border:1px solid #ccc">
<div style="padding:10px;background:#f5f5f5;font-weight:bold">Subject: ${template.subject}</div>
${template.content}
</div>
`);
setPreviewDialogOpen(true);
} else if (emailEditorRef.current) {
emailEditorRef.current.editor.exportHtml((data) => {
const { html } = data;
setPreviewContent(`
<div style="max-width:600px;margin:0 auto;border:1px solid #ccc">
<div style="padding:10px;background:#f5f5f5;font-weight:bold">Subject: ${template.subject}</div>
${html}
</div>
`);
setPreviewDialogOpen(true);
});
}
};
const handleCreateTemplate = () => {
const templateType = EMAIL_TYPES[activeTab === 0 ? 5 : activeTab - 1].id;
const templateName = EMAIL_TYPES[activeTab === 0 ? 5 : activeTab - 1].name;
// Create default HTML content for the new template
let defaultContent = `<h1>Your ${templateName}</h1><p>Start editing this template to customize it for your needs.</p>`;
// Add sample placeholders based on template type
if (templateType === 'login_code') {
defaultContent = `<h1>Your login code is: {{code}}</h1>
<p>This code will expire in 15 minutes.</p>
<p>Or click <a href="{{loginLink}}">here</a> to log in directly.</p>`;
} else if (templateType === 'shipping_notification') {
defaultContent = `<h1>Your Order Has Shipped!</h1>
<p>Hello {{first_name}},</p>
<p>Good news! Your order #{{order_id}} has been shipped and is on its way to you.</p>`;
} else if (templateType === 'welcome_email') {
defaultContent = `<h1>Welcome to Rocks, Bones & Sticks!</h1>
<p>Hello {{first_name}},</p>
<p>Thank you for creating an account with us. We're excited to have you join our community!</p>`;
}
const newTemplate = {
id: `email_template_${Date.now()}`,
name: `New ${templateName}`,
type: templateType,
subject: `Your ${templateName}`,
content: defaultContent,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
setEditingTemplate(newTemplate);
};
const onEditorReady = () => {
// You can perform any setup actions here when the editor is loaded
console.log('Email editor is ready');
// If there's a template being edited, load its design
if (editingTemplate?.design && emailEditorRef.current) {
emailEditorRef.current.editor.loadDesign(editingTemplate.design);
}
// If there's no design but we have HTML content, create a default design with that content
else if (editingTemplate?.content && emailEditorRef.current) {
const defaultDesign = {
body: {
rows: [
{
cells: [1],
columns: [
{
contents: [
{
type: "html",
values: {
html: editingTemplate.content,
containerPadding: "10px"
}
}
]
}
]
}
]
}
};
emailEditorRef.current.editor.loadDesign(defaultDesign);
}
// If it's a new template with no design or content, load the default template
else if (editingTemplate && emailEditorRef.current) {
// Try to load a default template for the template type
const defaultTemplate = DEFAULT_TEMPLATES[editingTemplate.type] || {
body: {
rows: [
{
cells: [1],
columns: [
{
contents: [
{
type: "html",
values: {
html: "<h1>Your " + editingTemplate.name + "</h1><p>Start editing your email template here.</p>",
containerPadding: "10px"
}
}
]
}
]
}
]
}
};
emailEditorRef.current.editor.loadDesign(defaultTemplate);
}
};
if (isLoading) return <Box textAlign="center" py={5}><CircularProgress /></Box>;
if (error) return <Alert severity="error">Error loading templates.</Alert>;
let first_name = "{{first_name}}"
return (
<Box>
<Typography variant="h4" mb={2}>Email Templates</Typography>
<Paper sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', px: 2 }}>
<Tabs
value={activeTab}
onChange={handleTabChange}
variant="scrollable"
scrollButtons="auto"
disabled={!!editingTemplate}
>
<Tab label="All" />
{EMAIL_TYPES.map(type => <Tab key={type.id} label={type.name} />)}
</Tabs>
{activeTab > 0 && (
<Button
startIcon={<AddIcon />}
color="primary"
variant="contained"
onClick={handleCreateTemplate}
sx={{ my: 1 }}
disabled={!!editingTemplate}
>
Create {EMAIL_TYPES[activeTab - 1]?.name} Template
</Button>
)}
</Box>
</Paper>
{!editingTemplate ? (
<Grid container spacing={2}>
{templateList.length > 0 ? (
templateList
.filter(template => activeTab === 0 || template.type === EMAIL_TYPES[activeTab - 1]?.id)
.map(template => (
<Grid item xs={12} md={6} key={template.id}>
<Card>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="h6">{template.name}</Typography>
<Chip
label={EMAIL_TYPES.find(t => t.id === template.type)?.name || 'Unknown'}
size="small"
color="primary"
variant="outlined"
/>
</Box>
<Typography variant="caption" display="block" color="text.secondary" gutterBottom>
Last updated: {new Date(template.updatedAt).toLocaleString()}
</Typography>
<Typography variant="body2" color="text.secondary">
Subject: {template.subject}
</Typography>
<Box mt={2} display="flex" gap={1}>
<Button
onClick={() => handleEditTemplate(template)}
startIcon={<EditIcon />}
variant="outlined"
size="small"
>
Edit
</Button>
<Button
onClick={() => handlePreviewTemplate(template)}
startIcon={<PreviewIcon />}
variant="outlined"
size="small"
>
Preview
</Button>
</Box>
</CardContent>
</Card>
</Grid>
))
) : (
<Grid item xs={12}>
<Alert severity="info">
No email templates found. Click "Create Template" to create your first template.
</Alert>
</Grid>
)}
</Grid>
) : (
// Edit view
<Box mb={3}>
<Paper sx={{ p: 2, mb: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="Template Name"
value={editingTemplate?.name || ''}
onChange={(e) => setEditingTemplate(prev => ({ ...prev, name: e.target.value }))}
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="Subject Line"
value={editingTemplate?.subject || ''}
onChange={(e) => setEditingTemplate(prev => ({ ...prev, subject: e.target.value }))}
/>
</Grid>
</Grid>
</Paper>
<Paper sx={{ p: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Typography variant="subtitle1" gutterBottom sx={{ mr: 1 }}>Email Content</Typography>
</Box>
<Alert severity="info" sx={{ mb: 3 }}>
<Box>
<Typography variant="subtitle1" fontWeight="medium">Tips for creating effective email templates:</Typography>
<ol>
<li>For best results, design your emails visually in Figma first</li>
<li>Export your design as HTML or use an email-specific design tool</li>
<li>Copy the HTML into an HTML block in this editor</li>
<li>Add dynamic variables like {`{{first_name}}`} as text where needed</li>
<li>Reference "Available Template Variables" below this tip</li>
</ol>
</Box>
</Alert>
<Accordion sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Available Template Variables</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography variant="body2" gutterBottom>
You can use these variables in your message. They will be replaced when the email is sent.
</Typography>
<List dense>
{TEMPLATE_VARIABLES[editingTemplate.type]?.map(variable => (
<ListItem key={variable.key}>
<ListItemText
primary={variable.key}
secondary={variable.description}
/>
</ListItem>
))}
</List>
</AccordionDetails>
</Accordion>
<Box sx={{ mb: 2, display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
<Button onClick={() => setEditingTemplate(null)}>Cancel</Button>
<Button
onClick={() => handlePreviewTemplate(editingTemplate)}
startIcon={<PreviewIcon />}
>
Preview
</Button>
<Button
onClick={handleSaveTemplate}
startIcon={<SaveIcon />}
variant="contained"
disabled={updateSetting.isLoading}
>
{updateSetting.isLoading ? <CircularProgress size={24} /> : 'Save Changes'}
</Button>
</Box>
<Box sx={{ border: '1px solid', borderColor: 'divider', borderRadius: 1, height: '600px' }}>
<EmailEditor
ref={emailEditorRef}
onReady={onEditorReady}
minHeight="600px"
/>
</Box>
</Paper>
</Box>
)}
{/* Preview Dialog */}
<Dialog open={previewDialogOpen} onClose={() => setPreviewDialogOpen(false)} maxWidth="md" fullWidth>
<DialogTitle>Email Preview</DialogTitle>
<DialogContent>
<Box dangerouslySetInnerHTML={{ __html: previewContent }} />
</DialogContent>
<DialogActions>
<Button onClick={() => setPreviewDialogOpen(false)}>Close</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default EmailTemplatesPage;