first commit

This commit is contained in:
Michaël
2026-01-29 14:13:11 -03:00
commit 2302748c87
105 changed files with 93301 additions and 0 deletions

View File

@@ -0,0 +1,961 @@
'use client';
import { useState, useEffect } from 'react';
import { useLanguage } from '@/context/LanguageContext';
import { emailsApi, EmailTemplate, EmailLog, EmailStats } from '@/lib/api';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import {
EnvelopeIcon,
PencilIcon,
DocumentDuplicateIcon,
EyeIcon,
PaperAirplaneIcon,
ClockIcon,
CheckCircleIcon,
XCircleIcon,
ExclamationTriangleIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from '@heroicons/react/24/outline';
import toast from 'react-hot-toast';
import clsx from 'clsx';
type TabType = 'templates' | 'logs' | 'compose';
const DRAFT_STORAGE_KEY = 'spanglish-email-draft';
interface EmailDraft {
eventId: string;
templateSlug: string;
customSubject: string;
customBody: string;
recipientFilter: 'all' | 'confirmed' | 'pending' | 'checked_in';
savedAt: string;
}
export default function AdminEmailsPage() {
const { t, locale } = useLanguage();
const [activeTab, setActiveTab] = useState<TabType>('templates');
const [loading, setLoading] = useState(true);
// Templates state
const [templates, setTemplates] = useState<EmailTemplate[]>([]);
const [editingTemplate, setEditingTemplate] = useState<EmailTemplate | null>(null);
const [showTemplateForm, setShowTemplateForm] = useState(false);
const [saving, setSaving] = useState(false);
// Logs state
const [logs, setLogs] = useState<EmailLog[]>([]);
const [logsOffset, setLogsOffset] = useState(0);
const [logsTotal, setLogsTotal] = useState(0);
const [selectedLog, setSelectedLog] = useState<EmailLog | null>(null);
// Stats state
const [stats, setStats] = useState<EmailStats | null>(null);
// Preview state
const [previewHtml, setPreviewHtml] = useState<string | null>(null);
const [previewSubject, setPreviewSubject] = useState<string>('');
// Template form state
const [templateForm, setTemplateForm] = useState({
name: '',
slug: '',
subject: '',
subjectEs: '',
bodyHtml: '',
bodyHtmlEs: '',
bodyText: '',
bodyTextEs: '',
description: '',
isActive: true,
});
// Compose/Draft state
const [events, setEvents] = useState<any[]>([]);
const [composeForm, setComposeForm] = useState<EmailDraft>({
eventId: '',
templateSlug: '',
customSubject: '',
customBody: '',
recipientFilter: 'confirmed',
savedAt: '',
});
const [hasDraft, setHasDraft] = useState(false);
const [sending, setSending] = useState(false);
const [showRecipientPreview, setShowRecipientPreview] = useState(false);
const [previewRecipients, setPreviewRecipients] = useState<any[]>([]);
useEffect(() => {
loadData();
loadEvents();
loadDraft();
}, []);
const loadEvents = async () => {
try {
const res = await fetch('/api/events', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('spanglish-token')}`,
},
});
if (res.ok) {
const data = await res.json();
setEvents(data.events || []);
}
} catch (error) {
console.error('Failed to load events');
}
};
const loadDraft = () => {
try {
const saved = localStorage.getItem(DRAFT_STORAGE_KEY);
if (saved) {
const draft = JSON.parse(saved) as EmailDraft;
setComposeForm(draft);
setHasDraft(true);
}
} catch (error) {
console.error('Failed to load draft');
}
};
const saveDraft = () => {
try {
const draft: EmailDraft = {
...composeForm,
savedAt: new Date().toISOString(),
};
localStorage.setItem(DRAFT_STORAGE_KEY, JSON.stringify(draft));
setHasDraft(true);
toast.success('Draft saved');
} catch (error) {
toast.error('Failed to save draft');
}
};
const clearDraft = () => {
localStorage.removeItem(DRAFT_STORAGE_KEY);
setComposeForm({
eventId: '',
templateSlug: '',
customSubject: '',
customBody: '',
recipientFilter: 'confirmed',
savedAt: '',
});
setHasDraft(false);
};
const loadRecipientPreview = async () => {
if (!composeForm.eventId) {
toast.error('Please select an event');
return;
}
try {
const res = await fetch(`/api/events/${composeForm.eventId}/attendees`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('spanglish-token')}`,
},
});
if (res.ok) {
const data = await res.json();
let attendees = data.attendees || [];
// Apply filter
if (composeForm.recipientFilter !== 'all') {
attendees = attendees.filter((a: any) => a.status === composeForm.recipientFilter);
}
setPreviewRecipients(attendees);
setShowRecipientPreview(true);
}
} catch (error) {
toast.error('Failed to load recipients');
}
};
const handleSendEmail = async () => {
if (!composeForm.eventId || !composeForm.templateSlug) {
toast.error('Please select an event and template');
return;
}
if (!confirm(`Are you sure you want to send this email to ${previewRecipients.length} recipients?`)) {
return;
}
setSending(true);
try {
const res = await emailsApi.sendToEvent(composeForm.eventId, {
templateSlug: composeForm.templateSlug,
recipientFilter: composeForm.recipientFilter,
customVariables: composeForm.customBody ? { customMessage: composeForm.customBody } : undefined,
});
if (res.success || res.sentCount > 0) {
toast.success(`Sent ${res.sentCount} emails successfully`);
if (res.failedCount > 0) {
toast.error(`${res.failedCount} emails failed`);
}
clearDraft();
setShowRecipientPreview(false);
} else {
toast.error('Failed to send emails');
}
} catch (error: any) {
toast.error(error.message || 'Failed to send emails');
} finally {
setSending(false);
}
};
useEffect(() => {
if (activeTab === 'logs') {
loadLogs();
}
}, [activeTab, logsOffset]);
const loadData = async () => {
try {
const [templatesRes, statsRes] = await Promise.all([
emailsApi.getTemplates(),
emailsApi.getStats(),
]);
setTemplates(templatesRes.templates);
setStats(statsRes.stats);
} catch (error) {
toast.error('Failed to load email data');
} finally {
setLoading(false);
}
};
const loadLogs = async () => {
try {
const res = await emailsApi.getLogs({ limit: 20, offset: logsOffset });
setLogs(res.logs);
setLogsTotal(res.pagination.total);
} catch (error) {
toast.error('Failed to load email logs');
}
};
const resetTemplateForm = () => {
setTemplateForm({
name: '',
slug: '',
subject: '',
subjectEs: '',
bodyHtml: '',
bodyHtmlEs: '',
bodyText: '',
bodyTextEs: '',
description: '',
isActive: true,
});
setEditingTemplate(null);
};
const handleEditTemplate = (template: EmailTemplate) => {
setTemplateForm({
name: template.name,
slug: template.slug,
subject: template.subject,
subjectEs: template.subjectEs || '',
bodyHtml: template.bodyHtml,
bodyHtmlEs: template.bodyHtmlEs || '',
bodyText: template.bodyText || '',
bodyTextEs: template.bodyTextEs || '',
description: template.description || '',
isActive: template.isActive,
});
setEditingTemplate(template);
setShowTemplateForm(true);
};
const handleSaveTemplate = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
try {
const data = {
...templateForm,
subjectEs: templateForm.subjectEs || undefined,
bodyHtmlEs: templateForm.bodyHtmlEs || undefined,
bodyText: templateForm.bodyText || undefined,
bodyTextEs: templateForm.bodyTextEs || undefined,
description: templateForm.description || undefined,
};
if (editingTemplate) {
await emailsApi.updateTemplate(editingTemplate.id, data);
toast.success('Template updated');
} else {
await emailsApi.createTemplate(data);
toast.success('Template created');
}
setShowTemplateForm(false);
resetTemplateForm();
loadData();
} catch (error: any) {
toast.error(error.message || 'Failed to save template');
} finally {
setSaving(false);
}
};
const handlePreviewTemplate = async (template: EmailTemplate) => {
try {
const res = await emailsApi.preview({
templateSlug: template.slug,
variables: {
attendeeName: 'John Doe',
attendeeEmail: 'john@example.com',
ticketId: 'TKT-ABC123',
eventTitle: 'Spanglish Night - January Edition',
eventDate: 'January 28, 2026',
eventTime: '7:00 PM',
eventLocation: 'Casa Cultural, Asunción',
eventLocationUrl: 'https://maps.google.com',
eventPrice: '50,000 PYG',
paymentAmount: '50,000 PYG',
paymentMethod: 'Lightning',
paymentReference: 'PAY-XYZ789',
paymentDate: 'January 28, 2026',
customMessage: 'This is a preview message.',
},
locale,
});
setPreviewSubject(res.subject);
setPreviewHtml(res.bodyHtml);
} catch (error) {
toast.error('Failed to preview template');
}
};
const handleDeleteTemplate = async (id: string) => {
if (!confirm('Are you sure you want to delete this template?')) return;
try {
await emailsApi.deleteTemplate(id);
toast.success('Template deleted');
loadData();
} catch (error: any) {
toast.error(error.message || 'Failed to delete template');
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'sent':
return <CheckCircleIcon className="w-5 h-5 text-green-500" />;
case 'failed':
return <XCircleIcon className="w-5 h-5 text-red-500" />;
case 'pending':
return <ClockIcon className="w-5 h-5 text-yellow-500" />;
case 'bounced':
return <ExclamationTriangleIcon className="w-5 h-5 text-orange-500" />;
default:
return <ClockIcon className="w-5 h-5 text-gray-500" />;
}
};
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleString(locale === 'es' ? 'es-ES' : 'en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<div className="animate-spin w-8 h-8 border-4 border-primary-yellow border-t-transparent rounded-full" />
</div>
);
}
return (
<div>
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold text-primary-dark">Email Center</h1>
</div>
{/* Stats Cards */}
{stats && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<EnvelopeIcon className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-2xl font-bold">{stats.total}</p>
<p className="text-sm text-gray-500">Total Sent</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<CheckCircleIcon className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-2xl font-bold">{stats.sent}</p>
<p className="text-sm text-gray-500">Delivered</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center">
<ClockIcon className="w-5 h-5 text-yellow-600" />
</div>
<div>
<p className="text-2xl font-bold">{stats.pending}</p>
<p className="text-sm text-gray-500">Pending</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-red-100 rounded-full flex items-center justify-center">
<XCircleIcon className="w-5 h-5 text-red-600" />
</div>
<div>
<p className="text-2xl font-bold">{stats.failed}</p>
<p className="text-sm text-gray-500">Failed</p>
</div>
</div>
</Card>
</div>
)}
{/* Tabs */}
<div className="border-b border-secondary-light-gray mb-6">
<nav className="flex gap-6">
{(['templates', 'compose', 'logs'] as TabType[]).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={clsx(
'py-3 px-1 border-b-2 font-medium text-sm transition-colors relative',
{
'border-primary-yellow text-primary-dark': activeTab === tab,
'border-transparent text-gray-500 hover:text-gray-700': activeTab !== tab,
}
)}
>
{tab === 'templates' ? 'Templates' : tab === 'compose' ? 'Compose' : 'Email Logs'}
{tab === 'compose' && hasDraft && (
<span className="absolute -top-1 -right-2 w-2 h-2 bg-primary-yellow rounded-full" />
)}
</button>
))}
</nav>
</div>
{/* Templates Tab */}
{activeTab === 'templates' && (
<div>
<div className="flex justify-between items-center mb-4">
<p className="text-gray-600">Manage email templates for booking confirmations, receipts, and updates.</p>
<Button onClick={() => { resetTemplateForm(); setShowTemplateForm(true); }}>
Create Template
</Button>
</div>
<div className="grid gap-4">
{templates.map((template) => (
<Card key={template.id} className="p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-lg">{template.name}</h3>
{template.isSystem && (
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded">System</span>
)}
{!template.isActive && (
<span className="text-xs bg-red-100 text-red-600 px-2 py-0.5 rounded">Inactive</span>
)}
</div>
<p className="text-sm text-gray-500 mt-1">{template.slug}</p>
<p className="text-sm text-gray-600 mt-2">{template.description || 'No description'}</p>
<p className="text-sm font-medium mt-2">Subject: {template.subject}</p>
{template.variables && template.variables.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{template.variables.slice(0, 5).map((v: any) => (
<span key={v.name} className="text-xs bg-primary-yellow/20 text-primary-dark px-2 py-0.5 rounded">
{`{{${v.name}}}`}
</span>
))}
{template.variables.length > 5 && (
<span className="text-xs text-gray-500">+{template.variables.length - 5} more</span>
)}
</div>
)}
</div>
<div className="flex items-center gap-2">
<button
onClick={() => handlePreviewTemplate(template)}
className="p-2 hover:bg-gray-100 rounded-btn"
title="Preview"
>
<EyeIcon className="w-5 h-5" />
</button>
<button
onClick={() => handleEditTemplate(template)}
className="p-2 hover:bg-gray-100 rounded-btn"
title="Edit"
>
<PencilIcon className="w-5 h-5" />
</button>
{!template.isSystem && (
<button
onClick={() => handleDeleteTemplate(template.id)}
className="p-2 hover:bg-red-100 text-red-600 rounded-btn"
title="Delete"
>
<XCircleIcon className="w-5 h-5" />
</button>
)}
</div>
</div>
</Card>
))}
</div>
</div>
)}
{/* Compose Tab */}
{activeTab === 'compose' && (
<div>
<Card className="p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold">Compose Email to Event Attendees</h2>
<div className="flex items-center gap-2">
{hasDraft && (
<span className="text-xs text-gray-500">
Draft saved {composeForm.savedAt ? new Date(composeForm.savedAt).toLocaleString() : ''}
</span>
)}
<Button variant="outline" size="sm" onClick={saveDraft}>
Save Draft
</Button>
{hasDraft && (
<Button variant="ghost" size="sm" onClick={clearDraft}>
Clear Draft
</Button>
)}
</div>
</div>
<div className="space-y-4">
{/* Event Selection */}
<div>
<label className="block text-sm font-medium mb-1">Select Event *</label>
<select
value={composeForm.eventId}
onChange={(e) => setComposeForm({ ...composeForm, eventId: e.target.value })}
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray"
>
<option value="">Choose an event</option>
{events.filter(e => e.status === 'published').map((event) => (
<option key={event.id} value={event.id}>
{event.title} - {new Date(event.startDatetime).toLocaleDateString()}
</option>
))}
</select>
</div>
{/* Recipient Filter */}
<div>
<label className="block text-sm font-medium mb-1">Recipients</label>
<select
value={composeForm.recipientFilter}
onChange={(e) => setComposeForm({ ...composeForm, recipientFilter: e.target.value as any })}
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray"
>
<option value="all">All attendees</option>
<option value="confirmed">Confirmed only</option>
<option value="pending">Pending only</option>
<option value="checked_in">Checked in only</option>
</select>
</div>
{/* Template Selection */}
<div>
<label className="block text-sm font-medium mb-1">Email Template *</label>
<select
value={composeForm.templateSlug}
onChange={(e) => setComposeForm({ ...composeForm, templateSlug: e.target.value })}
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray"
>
<option value="">Choose a template</option>
{templates.filter(t => t.isActive).map((template) => (
<option key={template.id} value={template.slug}>
{template.name}
</option>
))}
</select>
</div>
{/* Custom Message */}
<div>
<label className="block text-sm font-medium mb-1">
Custom Message (optional)
</label>
<textarea
value={composeForm.customBody}
onChange={(e) => setComposeForm({ ...composeForm, customBody: e.target.value })}
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray"
rows={4}
placeholder="Add a custom message that will be included in the email..."
/>
<p className="text-xs text-gray-500 mt-1">
This will be available as {'{{customMessage}}'} in the template
</p>
</div>
{/* Actions */}
<div className="flex gap-3 pt-4 border-t border-secondary-light-gray">
<Button
onClick={loadRecipientPreview}
disabled={!composeForm.eventId}
>
Preview Recipients
</Button>
</div>
</div>
</Card>
{/* Recipient Preview Modal */}
{showRecipientPreview && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<Card className="w-full max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
<div className="p-4 border-b border-secondary-light-gray">
<h2 className="text-lg font-bold">Recipient Preview</h2>
<p className="text-sm text-gray-500">
{previewRecipients.length} recipient(s) will receive this email
</p>
</div>
<div className="flex-1 overflow-y-auto p-4">
{previewRecipients.length === 0 ? (
<p className="text-center text-gray-500 py-8">
No recipients match your filter criteria
</p>
) : (
<div className="space-y-2">
{previewRecipients.map((recipient: any) => (
<div
key={recipient.id}
className="flex items-center justify-between p-3 bg-secondary-gray rounded-btn"
>
<div>
<p className="font-medium text-sm">{recipient.attendeeFirstName} {recipient.attendeeLastName || ''}</p>
<p className="text-xs text-gray-500">{recipient.attendeeEmail}</p>
</div>
<span className={clsx('badge text-xs', {
'badge-success': recipient.status === 'confirmed',
'badge-warning': recipient.status === 'pending',
'badge-info': recipient.status === 'checked_in',
'badge-gray': recipient.status === 'cancelled',
})}>
{recipient.status}
</span>
</div>
))}
</div>
)}
</div>
<div className="p-4 border-t border-secondary-light-gray flex gap-3">
<Button
onClick={handleSendEmail}
isLoading={sending}
disabled={previewRecipients.length === 0}
>
Send to {previewRecipients.length} Recipients
</Button>
<Button variant="outline" onClick={() => setShowRecipientPreview(false)}>
Cancel
</Button>
</div>
</Card>
</div>
)}
</div>
)}
{/* Logs Tab */}
{activeTab === 'logs' && (
<div>
<Card className="overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-secondary-gray">
<tr>
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">Status</th>
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">Recipient</th>
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">Subject</th>
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">Sent</th>
<th className="text-right px-6 py-3 text-sm font-medium text-gray-600">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-secondary-light-gray">
{logs.length === 0 ? (
<tr>
<td colSpan={5} className="px-6 py-12 text-center text-gray-500">
No emails sent yet
</td>
</tr>
) : (
logs.map((log) => (
<tr key={log.id} className="hover:bg-gray-50">
<td className="px-6 py-4">
<div className="flex items-center gap-2">
{getStatusIcon(log.status)}
<span className="capitalize text-sm">{log.status}</span>
</div>
</td>
<td className="px-6 py-4">
<p className="font-medium text-sm">{log.recipientName || 'Unknown'}</p>
<p className="text-sm text-gray-500">{log.recipientEmail}</p>
</td>
<td className="px-6 py-4 max-w-xs">
<p className="text-sm truncate">{log.subject}</p>
</td>
<td className="px-6 py-4 text-sm text-gray-600">
{formatDate(log.sentAt || log.createdAt)}
</td>
<td className="px-6 py-4">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => setSelectedLog(log)}
className="p-2 hover:bg-gray-100 rounded-btn"
title="View Email"
>
<EyeIcon className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination */}
{logsTotal > 20 && (
<div className="flex items-center justify-between px-6 py-4 border-t border-secondary-light-gray">
<p className="text-sm text-gray-600">
Showing {logsOffset + 1}-{Math.min(logsOffset + 20, logsTotal)} of {logsTotal}
</p>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
disabled={logsOffset === 0}
onClick={() => setLogsOffset(Math.max(0, logsOffset - 20))}
>
<ChevronLeftIcon className="w-4 h-4" />
</Button>
<Button
variant="outline"
size="sm"
disabled={logsOffset + 20 >= logsTotal}
onClick={() => setLogsOffset(logsOffset + 20)}
>
<ChevronRightIcon className="w-4 h-4" />
</Button>
</div>
</div>
)}
</Card>
</div>
)}
{/* Template Form Modal */}
{showTemplateForm && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<Card className="w-full max-w-4xl max-h-[90vh] overflow-y-auto p-6">
<h2 className="text-xl font-bold mb-6">
{editingTemplate ? 'Edit Template' : 'Create Template'}
</h2>
<form onSubmit={handleSaveTemplate} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="Template Name"
value={templateForm.name}
onChange={(e) => setTemplateForm({ ...templateForm, name: e.target.value })}
required
placeholder="e.g., Booking Confirmation"
/>
<Input
label="Slug (unique identifier)"
value={templateForm.slug}
onChange={(e) => setTemplateForm({ ...templateForm, slug: e.target.value })}
required
disabled={editingTemplate?.isSystem}
placeholder="e.g., booking-confirmation"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="Subject (English)"
value={templateForm.subject}
onChange={(e) => setTemplateForm({ ...templateForm, subject: e.target.value })}
required
placeholder="e.g., Your Spanglish ticket is confirmed"
/>
<Input
label="Subject (Spanish)"
value={templateForm.subjectEs}
onChange={(e) => setTemplateForm({ ...templateForm, subjectEs: e.target.value })}
placeholder="e.g., Tu entrada de Spanglish está confirmada"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Body HTML (English)</label>
<textarea
value={templateForm.bodyHtml}
onChange={(e) => setTemplateForm({ ...templateForm, bodyHtml: e.target.value })}
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow font-mono text-sm"
rows={8}
required
placeholder="<h2>Your Booking is Confirmed!</h2>..."
/>
<p className="text-xs text-gray-500 mt-1">
Use {`{{variableName}}`} for dynamic content. Common variables: attendeeName, eventTitle, eventDate, ticketId
</p>
</div>
<div>
<label className="block text-sm font-medium mb-1">Body HTML (Spanish)</label>
<textarea
value={templateForm.bodyHtmlEs}
onChange={(e) => setTemplateForm({ ...templateForm, bodyHtmlEs: e.target.value })}
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow font-mono text-sm"
rows={6}
placeholder="<h2>¡Tu Reserva está Confirmada!</h2>..."
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Description</label>
<textarea
value={templateForm.description}
onChange={(e) => setTemplateForm({ ...templateForm, description: e.target.value })}
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
rows={2}
placeholder="What is this template used for?"
/>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="isActive"
checked={templateForm.isActive}
onChange={(e) => setTemplateForm({ ...templateForm, isActive: e.target.checked })}
className="w-4 h-4"
/>
<label htmlFor="isActive" className="text-sm">Template is active</label>
</div>
<div className="flex gap-3 pt-4">
<Button type="submit" isLoading={saving}>
{editingTemplate ? 'Update Template' : 'Create Template'}
</Button>
<Button
type="button"
variant="outline"
onClick={() => { setShowTemplateForm(false); resetTemplateForm(); }}
>
Cancel
</Button>
</div>
</form>
</Card>
</div>
)}
{/* Preview Modal */}
{previewHtml && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<Card className="w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
<div className="flex items-center justify-between p-4 border-b border-secondary-light-gray">
<div>
<h2 className="text-lg font-bold">Email Preview</h2>
<p className="text-sm text-gray-500">Subject: {previewSubject}</p>
</div>
<Button variant="outline" size="sm" onClick={() => setPreviewHtml(null)}>
Close
</Button>
</div>
<div className="flex-1 overflow-auto">
<iframe
srcDoc={previewHtml}
className="w-full h-full min-h-[500px]"
title="Email Preview"
/>
</div>
</Card>
</div>
)}
{/* Log Detail Modal */}
{selectedLog && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<Card className="w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
<div className="flex items-center justify-between p-4 border-b border-secondary-light-gray">
<div>
<h2 className="text-lg font-bold">Email Details</h2>
<div className="flex items-center gap-2 mt-1">
{getStatusIcon(selectedLog.status)}
<span className="capitalize text-sm">{selectedLog.status}</span>
{selectedLog.errorMessage && (
<span className="text-sm text-red-500">- {selectedLog.errorMessage}</span>
)}
</div>
</div>
<Button variant="outline" size="sm" onClick={() => setSelectedLog(null)}>
Close
</Button>
</div>
<div className="p-4 space-y-2 border-b border-secondary-light-gray bg-gray-50">
<p><strong>To:</strong> {selectedLog.recipientName} &lt;{selectedLog.recipientEmail}&gt;</p>
<p><strong>Subject:</strong> {selectedLog.subject}</p>
<p><strong>Sent:</strong> {formatDate(selectedLog.sentAt || selectedLog.createdAt)}</p>
</div>
<div className="flex-1 overflow-auto">
{selectedLog.bodyHtml ? (
<iframe
srcDoc={selectedLog.bodyHtml}
className="w-full h-full min-h-[400px]"
title="Email Content"
/>
) : (
<div className="p-4 text-gray-500">Email content not available</div>
)}
</div>
</Card>
</div>
)}
</div>
);
}