Files
smoothschedule/frontend/src/pages/settings/SystemEmailTemplates.tsx
poduck fc63cf4fce Add platform email templates, staff invitations, and quota tracking
- Add PlatformEmailTemplate model and API for superuser-managed email templates
- Add PlatformStaffInvitation model with email sending via Celery tasks
- Add platform staff invite page and acceptance flow with auto-login
- Add quota tracking models (DailyAppointmentUsage, DailyAPIUsage, StorageUsage)
- Add quota status API endpoints and frontend banners
- Add storage usage service for tenant media tracking
- Fix platform user deletion with raw SQL to handle multi-tenant FK constraints
- Update EditPlatformUserModal with archive/delete buttons
- Update PlatformSidebar with email templates link for superusers
- Configure console email backend and Celery eager mode for local development

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 10:35:35 -05:00

754 lines
29 KiB
TypeScript

/**
* System Email Templates Settings Page
*
* Allows businesses to customize their automated system emails
* (welcome, appointment confirmations, reminders, etc.) using a Puck editor.
*/
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useOutletContext } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Puck, Render } from '@measured/puck';
import '@measured/puck/puck.css';
import {
Mail,
Edit2,
RotateCcw,
Eye,
X,
Check,
AlertTriangle,
Calendar,
FileSignature,
CreditCard,
Ticket,
UserPlus,
ChevronDown,
ChevronRight,
Info,
Save,
Loader2,
Code,
MonitorPlay,
} from 'lucide-react';
import api from '../../api/client';
import { getEmailEditorConfig } from '../../puck/emailConfig';
import {
SystemEmailTemplate,
SystemEmailTemplateDetail,
SystemEmailTag,
SystemEmailCategory,
SystemEmailType,
Business,
User,
} from '../../types';
// Category metadata
const CATEGORY_CONFIG: Record<SystemEmailCategory, {
label: string;
icon: React.ReactNode;
color: string;
}> = {
welcome: {
label: 'Welcome & Onboarding',
icon: <UserPlus className="h-5 w-5" />,
color: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
},
appointment: {
label: 'Appointments',
icon: <Calendar className="h-5 w-5" />,
color: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
},
contract: {
label: 'Contracts',
icon: <FileSignature className="h-5 w-5" />,
color: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
},
payment: {
label: 'Payments',
icon: <CreditCard className="h-5 w-5" />,
color: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',
},
ticket: {
label: 'Support Tickets',
icon: <Ticket className="h-5 w-5" />,
color: 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400',
},
};
// Category order for display
const CATEGORY_ORDER: SystemEmailCategory[] = [
'welcome',
'appointment',
'contract',
'payment',
'ticket',
];
const SystemEmailTemplates: React.FC = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { user } = useOutletContext<{
business: Business;
user: User;
}>();
const isOwner = user.role === 'owner';
const hasPermission = isOwner || user.effective_permissions?.can_access_settings_email_templates === true;
const [expandedCategories, setExpandedCategories] = useState<Set<SystemEmailCategory>>(
new Set(CATEGORY_ORDER)
);
const [editingTemplate, setEditingTemplate] = useState<SystemEmailTemplateDetail | null>(null);
const [showPreviewModal, setShowPreviewModal] = useState(false);
const [previewHtml, setPreviewHtml] = useState<string>('');
const [previewText, setPreviewText] = useState<string>('');
const [previewSubject, setPreviewSubject] = useState<string>('');
const [previewTab, setPreviewTab] = useState<'html' | 'text'>('html');
const [showResetConfirm, setShowResetConfirm] = useState<string | null>(null);
const [editorData, setEditorData] = useState<any>(null);
const [editorSubject, setEditorSubject] = useState<string>('');
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
// Get email editor config
const editorConfig = getEmailEditorConfig();
// Fix Puck sidebar scrolling when editor is open
useEffect(() => {
if (!editingTemplate) return;
const fixSidebars = () => {
// Target only the main left/right sidebar containers
const leftSidebar = document.querySelector('[class*="Sidebar--left"]') as HTMLElement;
const rightSidebar = document.querySelector('[class*="Sidebar--right"]') as HTMLElement;
[leftSidebar, rightSidebar].forEach((sidebar) => {
if (!sidebar) return;
// Make the main sidebar scroll
sidebar.style.maxHeight = 'calc(100vh - 300px)';
sidebar.style.overflowY = 'auto';
// Remove overflow from inner sections so only the main sidebar scrolls
const innerSections = sidebar.querySelectorAll('[class*="SidebarSection"]');
innerSections.forEach((section) => {
(section as HTMLElement).style.overflow = 'visible';
(section as HTMLElement).style.maxHeight = 'none';
});
});
};
// Run after Puck renders
const timer = setTimeout(fixSidebars, 200);
return () => clearTimeout(timer);
}, [editingTemplate]);
// Fetch all email templates
const { data: templates = [], isLoading } = useQuery<SystemEmailTemplate[]>({
queryKey: ['system-email-templates'],
queryFn: async () => {
const { data } = await api.get('/messages/email-templates/');
return data;
},
});
// Fetch single template with tags
const fetchTemplateDetail = async (emailType: SystemEmailType): Promise<SystemEmailTemplateDetail> => {
const { data } = await api.get(`/messages/email-templates/${emailType}/`);
return data;
};
// Update template mutation
const updateMutation = useMutation({
mutationFn: async ({ emailType, data }: { emailType: SystemEmailType; data: any }) => {
const response = await api.patch(`/messages/email-templates/${emailType}/`, data);
return response.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['system-email-templates'] });
},
});
// Reset template mutation
const resetMutation = useMutation({
mutationFn: async (emailType: SystemEmailType) => {
const response = await api.post(`/messages/email-templates/${emailType}/reset/`);
return response.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['system-email-templates'] });
setShowResetConfirm(null);
},
});
// Preview mutation
const previewMutation = useMutation({
mutationFn: async ({ emailType, data }: { emailType: SystemEmailType; data: any }) => {
const response = await api.post(`/messages/email-templates/${emailType}/preview/`, data);
return response.data;
},
});
// Group templates by category
const templatesByCategory = React.useMemo(() => {
const grouped: Record<SystemEmailCategory, SystemEmailTemplate[]> = {
welcome: [],
appointment: [],
contract: [],
payment: [],
ticket: [],
};
templates.forEach((template) => {
if (grouped[template.category]) {
grouped[template.category].push(template);
}
});
return grouped;
}, [templates]);
// Toggle category expansion
const toggleCategory = (category: SystemEmailCategory) => {
setExpandedCategories((prev) => {
const next = new Set(prev);
if (next.has(category)) {
next.delete(category);
} else {
next.add(category);
}
return next;
});
};
/**
* Transform puck_data to ensure id is inside props (Puck requirement).
* Puck expects: { type: 'X', props: { id: 'X-1', ... } }
* API may return: { type: 'X', id: 'X-1', props: { ... } }
*/
const transformPuckDataForEditor = (puckData: any): any => {
if (!puckData?.content) return puckData;
return {
...puckData,
content: puckData.content.map((item: any, index: number) => {
// If id is at root level but not in props, move it inside props
const rootId = item.id;
const hasPropsId = item.props?.id;
if (rootId && !hasPropsId) {
// Move id from root to inside props
const { id, ...rest } = item;
return {
...rest,
props: {
...rest.props,
id: rootId,
},
};
} else if (!hasPropsId) {
// Generate id if missing entirely
return {
...item,
props: {
...item.props,
id: `${item.type}-${index}-${Math.random().toString(36).substring(2, 10)}`,
},
};
}
return item;
}),
};
};
// Open editor for a template
const handleEdit = async (template: SystemEmailTemplate) => {
try {
const detail = await fetchTemplateDetail(template.email_type);
// Transform the data to ensure Puck can render it correctly
const transformedData = transformPuckDataForEditor(detail.puck_data);
setEditingTemplate(detail);
setEditorData(transformedData);
setEditorSubject(detail.subject_template);
setHasUnsavedChanges(false);
} catch (error) {
console.error('Failed to load template:', error);
}
};
// Handle data changes in editor
const handleEditorChange = useCallback((newData: any) => {
setEditorData(newData);
setHasUnsavedChanges(true);
}, []);
// Save template
const handleSave = async () => {
if (!editingTemplate) return;
try {
await updateMutation.mutateAsync({
emailType: editingTemplate.email_type,
data: {
subject_template: editorSubject,
puck_data: editorData,
},
});
setHasUnsavedChanges(false);
setEditingTemplate(null);
} catch (error: any) {
console.error('Failed to save template:', error);
// Show error message from API if available
const errorMsg = error?.response?.data?.subject_template?.[0] ||
error?.response?.data?.error ||
'Failed to save template';
alert(errorMsg);
}
};
// Preview template
const handlePreview = async () => {
if (!editingTemplate) return;
try {
const preview = await previewMutation.mutateAsync({
emailType: editingTemplate.email_type,
data: {
subject_template: editorSubject,
puck_data: editorData,
},
});
setPreviewSubject(preview.subject);
setPreviewHtml(preview.html);
setPreviewText(preview.text);
setShowPreviewModal(true);
} catch (error) {
console.error('Failed to generate preview:', error);
}
};
// Reset template to default
const handleReset = async (emailType: SystemEmailType) => {
try {
await resetMutation.mutateAsync(emailType);
} catch (error) {
console.error('Failed to reset template:', error);
}
};
// Close editor
const handleCloseEditor = () => {
if (hasUnsavedChanges) {
if (!confirm('You have unsaved changes. Are you sure you want to close?')) {
return;
}
}
setEditingTemplate(null);
setEditorData(null);
setEditorSubject('');
setHasUnsavedChanges(false);
};
if (!hasPermission) {
return (
<div className="text-center py-12">
<p className="text-gray-500 dark:text-gray-400">
{t('settings.noPermission', 'You do not have permission to access these settings.')}
</p>
</div>
);
}
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-8 w-8 animate-spin text-brand-600" />
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-3">
<Mail className="h-7 w-7 text-brand-600" />
{t('settings.systemEmails.title', 'System Email Templates')}
</h1>
<p className="mt-1 text-gray-500 dark:text-gray-400">
{t(
'settings.systemEmails.description',
'Customize the automated emails sent to your customers for appointments, payments, contracts, and more.'
)}
</p>
</div>
{/* Info Banner */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<div className="flex gap-3">
<Info className="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
<div className="text-sm text-blue-800 dark:text-blue-300">
<p className="font-medium mb-1">About Template Tags</p>
<p>
Use template tags like <code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">{'{{ customer_name }}'}</code> to
insert dynamic content. Available tags vary by email type and are shown when editing each template.
</p>
</div>
</div>
</div>
{/* Template Categories */}
<div className="space-y-4">
{CATEGORY_ORDER.map((category) => {
const categoryTemplates = templatesByCategory[category];
const config = CATEGORY_CONFIG[category];
const isExpanded = expandedCategories.has(category);
if (categoryTemplates.length === 0) return null;
return (
<div
key={category}
className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden"
>
{/* Category Header */}
<button
onClick={() => toggleCategory(category)}
className="w-full px-6 py-4 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
>
<div className="flex items-center gap-3">
<span className={`p-2 rounded-lg ${config.color}`}>{config.icon}</span>
<div className="text-left">
<h3 className="font-semibold text-gray-900 dark:text-white">{config.label}</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
{categoryTemplates.length} template{categoryTemplates.length !== 1 ? 's' : ''}
</p>
</div>
</div>
{isExpanded ? (
<ChevronDown className="h-5 w-5 text-gray-400" />
) : (
<ChevronRight className="h-5 w-5 text-gray-400" />
)}
</button>
{/* Template List */}
{isExpanded && (
<div className="border-t border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700">
{categoryTemplates.map((template) => (
<div
key={template.email_type}
className="px-6 py-4 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700/50"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h4 className="font-medium text-gray-900 dark:text-white">
{template.display_name}
</h4>
{template.is_customized && (
<span className="px-2 py-0.5 text-xs font-medium rounded-full bg-brand-100 text-brand-700 dark:bg-brand-900/30 dark:text-brand-400">
Customized
</span>
)}
{!template.is_active && (
<span className="px-2 py-0.5 text-xs font-medium rounded-full bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">
Disabled
</span>
)}
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-0.5 truncate">
{template.description}
</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
Subject: <span className="font-mono">{template.subject_template}</span>
</p>
</div>
{/* Actions */}
<div className="flex items-center gap-2 ml-4">
{template.is_customized && (
<button
onClick={() => setShowResetConfirm(template.email_type)}
className="p-2 text-gray-500 hover:text-amber-600 dark:hover:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-900/20 rounded-lg transition-colors"
title="Reset to default"
>
<RotateCcw className="h-4 w-4" />
</button>
)}
<button
onClick={() => handleEdit(template)}
className="flex items-center gap-1.5 px-3 py-1.5 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors text-sm font-medium"
>
<Edit2 className="h-4 w-4" />
Edit
</button>
</div>
</div>
))}
</div>
)}
</div>
);
})}
</div>
{/* Reset Confirmation Modal */}
{showResetConfirm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-md w-full overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center gap-3">
<div className="p-2 bg-amber-100 dark:bg-amber-900/30 rounded-lg">
<AlertTriangle className="h-5 w-5 text-amber-600 dark:text-amber-400" />
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Reset to Default?
</h3>
</div>
<div className="p-6">
<p className="text-gray-600 dark:text-gray-400">
This will reset the email template to its default content. Any customizations you've made will be lost.
</p>
</div>
<div className="px-6 py-4 bg-gray-50 dark:bg-gray-900/50 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-3">
<button
onClick={() => setShowResetConfirm(null)}
className="px-4 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors font-medium"
>
Cancel
</button>
<button
onClick={() => handleReset(showResetConfirm as SystemEmailType)}
disabled={resetMutation.isPending}
className="flex items-center gap-2 px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 disabled:opacity-50 transition-colors font-medium"
>
{resetMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RotateCcw className="h-4 w-4" />
)}
Reset to Default
</button>
</div>
</div>
</div>
)}
{/* Editor Modal */}
{editingTemplate && (
<div className="fixed inset-0 z-50 flex flex-col bg-white dark:bg-gray-900">
{/* Editor Header */}
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={handleCloseEditor}
className="p-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
<X className="h-5 w-5" />
</button>
<div>
<h2 className="font-semibold text-gray-900 dark:text-white">
{editingTemplate.display_name}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
{editingTemplate.description}
</p>
</div>
</div>
<div className="flex items-center gap-3">
{hasUnsavedChanges && (
<span className="flex items-center gap-1.5 px-2 py-1 bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 rounded text-xs font-medium">
<span className="w-2 h-2 bg-amber-500 rounded-full animate-pulse"></span>
Unsaved changes
</span>
)}
<button
onClick={handlePreview}
disabled={previewMutation.isPending}
className="flex items-center gap-1.5 px-3 py-1.5 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 text-sm font-medium transition-colors"
>
<Eye className="h-4 w-4" />
Preview
</button>
<button
onClick={handleSave}
disabled={updateMutation.isPending || !hasUnsavedChanges}
className="flex items-center gap-1.5 px-4 py-1.5 bg-brand-600 text-white rounded-lg hover:bg-brand-700 disabled:opacity-50 text-sm font-medium transition-colors"
>
{updateMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Save className="h-4 w-4" />
)}
Save
</button>
</div>
</div>
{/* Subject Line Editor */}
<div className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email Subject
</label>
<input
type="text"
value={editorSubject}
onChange={(e) => {
setEditorSubject(e.target.value);
setHasUnsavedChanges(true);
}}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500"
placeholder="Enter email subject..."
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Use tags like <code className="bg-gray-200 dark:bg-gray-600 px-1 rounded">{'{{ customer_name }}'}</code> for dynamic content
</p>
</div>
{/* Available Tags Panel */}
<div className="bg-gray-100 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700 px-6 py-3">
<details className="group" open>
<summary className="flex items-center gap-2 cursor-pointer text-sm font-medium text-gray-700 dark:text-gray-300">
<Code className="h-4 w-4" />
Available Template Tags ({editingTemplate.available_tags?.length || 0})
<ChevronRight className="h-4 w-4 group-open:rotate-90 transition-transform" />
</summary>
<div className="mt-3 space-y-3">
{/* Group tags by category */}
{Object.entries(
(editingTemplate.available_tags || []).reduce((acc: Record<string, any[]>, tag: any) => {
const category = tag.category || 'Other';
if (!acc[category]) acc[category] = [];
acc[category].push(tag);
return acc;
}, {})
).map(([category, tags]) => (
<div key={category}>
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1.5">{category}</p>
<div className="flex flex-wrap gap-1.5">
{(tags as any[]).map((tag: any) => (
<span
key={tag.name}
className="inline-flex items-center px-2 py-0.5 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded text-xs font-mono cursor-help hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
title={tag.description}
onClick={() => {
navigator.clipboard.writeText(`{{ ${tag.name} }}`);
}}
>
{tag.name}
</span>
))}
</div>
</div>
))}
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
Click a tag to copy it. Hover for description.
</p>
</div>
</details>
</div>
{/* Puck Editor - using cloned config like site builder */}
<div className="flex-1 overflow-hidden">
{editorData && (
<Puck
key={`email-puck-${editingTemplate?.email_type}`}
config={editorConfig}
data={editorData}
onChange={handleEditorChange}
onPublish={handleSave}
/>
)}
</div>
</div>
)}
{/* Preview Modal */}
{showPreviewModal && (
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
{/* Preview Header */}
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<div className="flex items-center gap-3">
<MonitorPlay className="h-5 w-5 text-brand-600" />
<div>
<h3 className="font-semibold text-gray-900 dark:text-white">Email Preview</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
Subject: {previewSubject}
</p>
</div>
</div>
<button
onClick={() => setShowPreviewModal(false)}
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Preview Tabs */}
<div className="px-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex gap-4">
<button
onClick={() => setPreviewTab('html')}
className={`py-3 border-b-2 font-medium text-sm transition-colors ${
previewTab === 'html'
? 'border-brand-600 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'
}`}
>
HTML Preview
</button>
<button
onClick={() => setPreviewTab('text')}
className={`py-3 border-b-2 font-medium text-sm transition-colors ${
previewTab === 'text'
? 'border-brand-600 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'
}`}
>
Plain Text
</button>
</div>
</div>
{/* Preview Content */}
<div className="flex-1 overflow-auto p-6 bg-gray-50 dark:bg-gray-900">
{previewTab === 'html' ? (
<div className="bg-white rounded-lg shadow-sm overflow-hidden max-w-2xl mx-auto">
<iframe
srcDoc={previewHtml}
className="w-full h-[500px]"
title="Email Preview"
/>
</div>
) : (
<pre className="bg-white dark:bg-gray-800 p-4 rounded-lg font-mono text-sm text-gray-800 dark:text-gray-200 whitespace-pre-wrap max-w-2xl mx-auto">
{previewText}
</pre>
)}
</div>
{/* Preview Footer */}
<div className="px-6 py-4 bg-gray-50 dark:bg-gray-900/50 border-t border-gray-200 dark:border-gray-700 flex justify-end">
<button
onClick={() => setShowPreviewModal(false)}
className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors font-medium"
>
Close
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default SystemEmailTemplates;