Fix email template preview UX and debug template loading

- Convert EmailTemplatePreview from bottom card to modal Dialog component
- Replace problematic bottom preview with clean modal popup
- Add proper modal state management (open/close handlers)
- Debug template loading with comprehensive error handling and logging
- Add user feedback for template seeding and loading errors
- Improve fetchData() and seedTemplates() with try-catch blocks
- Add console logging for debugging template initialization

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
dwindown
2025-12-22 20:57:33 +07:00
parent f743a79674
commit 1982033ac4
2 changed files with 242 additions and 142 deletions

View File

@@ -3,8 +3,16 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog';
import { toast } from '@/hooks/use-toast';
import { Eye, Send, Mail } from 'lucide-react';
import { Eye, Send, Mail, X } from 'lucide-react';
import { EmailTemplateRenderer, ShortcodeProcessor } from '@/lib/email-templates/master-template';
interface NotificationTemplate {
@@ -22,9 +30,17 @@ interface EmailTemplatePreviewProps {
template: NotificationTemplate;
onTest?: (template: NotificationTemplate) => void;
isTestSending?: boolean;
open: boolean;
onClose: () => void;
}
export function EmailTemplatePreview({ template, onTest, isTestSending = false }: EmailTemplatePreviewProps) {
export function EmailTemplatePreview({
template,
onTest,
isTestSending = false,
open,
onClose
}: EmailTemplatePreviewProps) {
const [previewMode, setPreviewMode] = useState<'master' | 'content'>('master');
const [testEmail, setTestEmail] = useState('');
const [showTestForm, setShowTestForm] = useState(false);
@@ -60,109 +76,154 @@ export function EmailTemplatePreview({ template, onTest, isTestSending = false }
const previewHtml = generatePreview();
return (
<div className="space-y-4">
{/* Preview Controls */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Label>Preview Mode:</Label>
<select
value={previewMode}
onChange={(e) => setPreviewMode(e.target.value as 'master' | 'content')}
className="border-2 px-3 py-1 rounded"
>
<option value="master">Master Template</option>
<option value="content">Content Only</option>
</select>
</div>
<div className="flex items-center gap-2">
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Mail className="w-5 h-5" />
Preview: {template.name}
</DialogTitle>
<DialogDescription>
Preview template email dengan master styling
</DialogDescription>
<Button
variant="outline"
size="sm"
onClick={() => setShowTestForm(!showTestForm)}
variant="ghost"
size="icon"
onClick={onClose}
>
<Send className="w-4 h-4 mr-2" />
{showTestForm ? 'Cancel' : 'Test Email'}
<X className="w-4 h-4" />
</Button>
</div>
</div>
</DialogHeader>
{/* Test Email Form */}
{showTestForm && (
<div className="p-4 border-2 border-gray-300 rounded-lg bg-gray-50">
<div className="space-y-3">
<Label htmlFor="test-email">Test Email Address:</Label>
<div className="flex gap-2">
<Input
id="test-email"
type="email"
value={testEmail}
onChange={(e) => setTestEmail(e.target.value)}
placeholder="test@example.com"
className="flex-1"
/>
<Button
onClick={handleTestEmail}
disabled={!testEmail || isTestSending}
<div className="space-y-4">
{/* Preview Controls */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Label>Preview Mode:</Label>
<select
value={previewMode}
onChange={(e) => setPreviewMode(e.target.value as 'master' | 'content')}
className="border-2 px-3 py-1 rounded"
>
<Mail className="w-4 h-4 mr-2" />
{isTestSending ? 'Sending...' : 'Send Test'}
<option value="master">Master Template</option>
<option value="content">Content Only</option>
</select>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setShowTestForm(!showTestForm)}
>
<Send className="w-4 h-4 mr-2" />
{showTestForm ? 'Cancel' : 'Test Email'}
</Button>
</div>
<p className="text-sm text-muted-foreground">
This will send a test email with dummy data for all available shortcodes.
</p>
</div>
</div>
)}
{/* Preview Info */}
<Alert>
<Eye className="w-4 h-4" />
<AlertDescription>
{previewMode === 'master'
? 'Showing complete email template with header, footer, and styling applied.'
: 'Showing only the content section without master template styling.'
}
</AlertDescription>
</Alert>
{/* Test Email Form */}
{showTestForm && (
<div className="p-4 border-2 border-gray-300 rounded-lg bg-gray-50">
<div className="space-y-3">
<Label htmlFor="test-email">Test Email Address:</Label>
<div className="flex gap-2">
<Input
id="test-email"
type="email"
value={testEmail}
onChange={(e) => setTestEmail(e.target.value)}
placeholder="test@example.com"
className="flex-1"
/>
<Button
onClick={handleTestEmail}
disabled={!testEmail || isTestSending}
>
<Mail className="w-4 h-4 mr-2" />
{isTestSending ? 'Sending...' : 'Send Test'}
</Button>
</div>
<p className="text-sm text-muted-foreground">
This will send a test email with dummy data for all available shortcodes.
</p>
</div>
</div>
)}
{/* Email Preview */}
<div className="border-2 border-gray-300 rounded-lg overflow-hidden">
<div className="bg-gray-100 px-4 py-2 border-b border-gray-300">
<span className="text-sm font-mono text-gray-600">
{previewMode === 'master' ? 'Full Email Preview' : 'Content Preview'}
</span>
</div>
<div className="bg-white" style={{ height: '600px', overflow: 'hidden' }}>
<iframe
srcDoc={previewHtml}
className="w-full h-full border-0"
style={{
height: '100%',
overflow: 'hidden'
}}
sandbox="allow-same-origin"
scrolling="no"
/>
</div>
</div>
{/* Preview Info */}
<Alert>
<Eye className="w-4 h-4" />
<AlertDescription>
{previewMode === 'master'
? 'Showing complete email template with header, footer, and styling applied.'
: 'Showing only the content section without master template styling.'
}
</AlertDescription>
</Alert>
{/* Shortcodes Used */}
<div className="p-3 bg-blue-50 border border-blue-200 rounded">
<h4 className="font-semibold text-sm mb-2">Shortcodes Used in This Template:</h4>
<div className="grid grid-cols-2 gap-2 text-sm">
{['{nama}', '{email}', '{order_id}', '{tanggal_pesanan}', '{total}', '{metode_pembayaran}',
'{produk}', '{link_akses}', '{tanggal_konsultasi}', '{jam_konsultasi}', '{link_meet}',
'{judul_event}', '{tanggal_event}', '{jam_event}', '{link_event}'].filter(shortcode =>
template.email_subject.includes(shortcode) || template.email_body_html.includes(shortcode)
).map(shortcode => (
<code key={shortcode} className="bg-blue-100 px-2 py-1 rounded text-xs">
{shortcode}
</code>
))}
{/* Email Preview */}
<div className="border-2 border-gray-300 rounded-lg overflow-hidden">
<div className="bg-gray-100 px-4 py-2 border-b border-gray-300">
<span className="text-sm font-mono text-gray-600">
{previewMode === 'master' ? 'Full Email Preview' : 'Content Preview'}
</span>
</div>
<div className="bg-white" style={{ height: '500px', overflow: 'hidden' }}>
<iframe
srcDoc={previewHtml}
className="w-full h-full border-0"
style={{
height: '100%',
overflow: 'hidden'
}}
sandbox="allow-same-origin"
scrolling="no"
/>
</div>
</div>
{/* Shortcodes Used */}
<div className="p-3 bg-blue-50 border border-blue-200 rounded">
<h4 className="font-semibold text-sm mb-2">Shortcodes Used in This Template:</h4>
<div className="grid grid-cols-2 gap-2 text-sm">
{['{nama}', '{email}', '{order_id}', '{tanggal_pesanan}', '{total}', '{metode_pembayaran}',
'{produk}', '{link_akses}', '{tanggal_konsultasi}', '{jam_konsultasi}', '{link_meet}',
'{judul_event}', '{tanggal_event}', '{jam_event}', '{link_event}'].filter(shortcode =>
template.email_subject.includes(shortcode) || template.email_body_html.includes(shortcode)
).map(shortcode => (
<code key={shortcode} className="bg-blue-100 px-2 py-1 rounded text-xs">
{shortcode}
</code>
))}
</div>
</div>
{/* Template Actions */}
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Close
</Button>
{!showTestForm && (
<Button
onClick={() => setShowTestForm(true)}
className="flex-1"
>
<Send className="w-4 h-4 mr-2" />
Test Email
</Button>
)}
{showTestForm && (
<Button
onClick={() => setShowTestForm(false)}
variant="outline"
>
Cancel
</Button>
)}
</DialogFooter>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -346,36 +346,93 @@ export function NotifikasiTab() {
const [loading, setLoading] = useState(true);
const [expandedTemplates, setExpandedTemplates] = useState<Set<string>>(new Set());
const [testingTemplate, setTestingTemplate] = useState<string | null>(null);
const [selectedTemplate, setSelectedTemplate] = useState<NotificationTemplate | null>(null);
const [previewMode, setPreviewMode] = useState<'master' | 'content'>('master');
const [previewTemplate, setPreviewTemplate] = useState<NotificationTemplate | null>(null);
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
// Fetch templates
const { data: templatesData } = await supabase.from('notification_templates').select('*').order('key');
if (templatesData && templatesData.length > 0) {
setTemplates(templatesData);
} else {
// Seed default templates if none exist
await seedTemplates();
try {
console.log('Fetching templates...');
// Fetch templates
const { data: templatesData, error: fetchError } = await supabase.from('notification_templates').select('*').order('key');
if (fetchError) {
console.error('Error fetching templates:', fetchError);
toast({
title: 'Error',
description: 'Gagal mengambil template: ' + fetchError.message,
variant: 'destructive'
});
setLoading(false);
return;
}
console.log('Templates data:', templatesData);
if (templatesData && templatesData.length > 0) {
console.log('Setting templates from database:', templatesData.length);
setTemplates(templatesData);
} else {
console.log('No templates found, seeding default templates...');
// Seed default templates if none exist
await seedTemplates();
}
} catch (error) {
console.error('Unexpected error in fetchData:', error);
toast({
title: 'Error',
description: 'Terjadi kesalahan tak terduga saat mengambil data',
variant: 'destructive'
});
} finally {
setLoading(false);
}
setLoading(false);
};
const seedTemplates = async () => {
const toInsert = DEFAULT_TEMPLATES.map(t => ({
key: t.key,
name: t.name,
is_active: false,
email_subject: t.defaultSubject,
email_body_html: t.defaultBody,
webhook_url: '',
}));
const { data, error } = await supabase.from('notification_templates').insert(toInsert).select();
if (!error && data) setTemplates(data);
try {
console.log('Seeding default templates...');
const toInsert = DEFAULT_TEMPLATES.map(t => ({
key: t.key,
name: t.name,
is_active: false,
email_subject: t.defaultSubject,
email_body_html: t.defaultBody,
webhook_url: '',
}));
console.log('Inserting templates:', toInsert.length);
const { data, error } = await supabase.from('notification_templates').insert(toInsert).select();
if (error) {
console.error('Error seeding templates:', error);
toast({
title: 'Error',
description: 'Gagal membuat template default: ' + error.message,
variant: 'destructive'
});
return;
}
console.log('Templates seeded successfully:', data);
if (data) {
setTemplates(data);
toast({
title: 'Berhasil',
description: `Berhasil membuat ${data.length} template default`
});
}
} catch (error) {
console.error('Unexpected error in seedTemplates:', error);
toast({
title: 'Error',
description: 'Terjadi kesalahan saat membuat template default',
variant: 'destructive'
});
}
};
@@ -587,7 +644,8 @@ export function NotifikasiTab() {
<Button
onClick={() => {
updateTemplate(template);
setSelectedTemplate(template);
setPreviewTemplate(template);
setIsPreviewOpen(true);
}}
className="shadow-sm flex-1"
>
@@ -620,33 +678,14 @@ export function NotifikasiTab() {
</CardContent>
</Card>
{/* Consolidated Email Preview */}
{selectedTemplate && (
<Card className="border-2 border-border">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Preview: {selectedTemplate.name}</span>
<Button
variant="outline"
size="sm"
onClick={() => setSelectedTemplate(null)}
>
Tutup Preview
</Button>
</CardTitle>
<CardDescription>
Preview template email dengan master styling
</CardDescription>
</CardHeader>
<CardContent>
<EmailTemplatePreview
template={selectedTemplate}
onTest={sendTestEmail}
isTestSending={testingTemplate === selectedTemplate.id}
/>
</CardContent>
</Card>
)}
{/* Modal Email Preview */}
<EmailTemplatePreview
template={previewTemplate!}
open={isPreviewOpen}
onClose={() => setIsPreviewOpen(false)}
onTest={sendTestEmail}
isTestSending={testingTemplate === previewTemplate?.id}
/>
</div>
);
}