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

View File

@@ -346,36 +346,93 @@ export function NotifikasiTab() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [expandedTemplates, setExpandedTemplates] = useState<Set<string>>(new Set()); const [expandedTemplates, setExpandedTemplates] = useState<Set<string>>(new Set());
const [testingTemplate, setTestingTemplate] = useState<string | null>(null); const [testingTemplate, setTestingTemplate] = useState<string | null>(null);
const [selectedTemplate, setSelectedTemplate] = useState<NotificationTemplate | null>(null); const [previewTemplate, setPreviewTemplate] = useState<NotificationTemplate | null>(null);
const [previewMode, setPreviewMode] = useState<'master' | 'content'>('master'); const [isPreviewOpen, setIsPreviewOpen] = useState(false);
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
}, []); }, []);
const fetchData = async () => { const fetchData = async () => {
// Fetch templates try {
const { data: templatesData } = await supabase.from('notification_templates').select('*').order('key'); console.log('Fetching templates...');
if (templatesData && templatesData.length > 0) { // Fetch templates
setTemplates(templatesData); const { data: templatesData, error: fetchError } = await supabase.from('notification_templates').select('*').order('key');
} else {
// Seed default templates if none exist if (fetchError) {
await seedTemplates(); 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 seedTemplates = async () => {
const toInsert = DEFAULT_TEMPLATES.map(t => ({ try {
key: t.key, console.log('Seeding default templates...');
name: t.name, const toInsert = DEFAULT_TEMPLATES.map(t => ({
is_active: false, key: t.key,
email_subject: t.defaultSubject, name: t.name,
email_body_html: t.defaultBody, is_active: false,
webhook_url: '', email_subject: t.defaultSubject,
})); email_body_html: t.defaultBody,
const { data, error } = await supabase.from('notification_templates').insert(toInsert).select(); webhook_url: '',
if (!error && data) setTemplates(data); }));
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 <Button
onClick={() => { onClick={() => {
updateTemplate(template); updateTemplate(template);
setSelectedTemplate(template); setPreviewTemplate(template);
setIsPreviewOpen(true);
}} }}
className="shadow-sm flex-1" className="shadow-sm flex-1"
> >
@@ -620,33 +678,14 @@ export function NotifikasiTab() {
</CardContent> </CardContent>
</Card> </Card>
{/* Consolidated Email Preview */} {/* Modal Email Preview */}
{selectedTemplate && ( <EmailTemplatePreview
<Card className="border-2 border-border"> template={previewTemplate!}
<CardHeader> open={isPreviewOpen}
<CardTitle className="flex items-center justify-between"> onClose={() => setIsPreviewOpen(false)}
<span>Preview: {selectedTemplate.name}</span> onTest={sendTestEmail}
<Button isTestSending={testingTemplate === previewTemplate?.id}
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>
)}
</div> </div>
); );
} }