- Remove SMTP host/port/username/TLS configuration - Add Mailkening API token configuration - Update email provider dropdown (Mailketing only) - Update test email function to use Mailketing API - Update help text and validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
449 lines
19 KiB
TypeScript
449 lines
19 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { supabase } from '@/integrations/supabase/client';
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
|
import { RichTextEditor } from '@/components/RichTextEditor';
|
|
import { toast } from '@/hooks/use-toast';
|
|
import { Mail, AlertTriangle, Send, ChevronDown, ChevronUp, Webhook, Key } from 'lucide-react';
|
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
|
|
interface EmailProviderSettings {
|
|
id?: string;
|
|
provider: 'mailketing';
|
|
api_token: string;
|
|
from_name: string;
|
|
from_email: string;
|
|
}
|
|
|
|
interface NotificationTemplate {
|
|
id: string;
|
|
key: string;
|
|
name: string;
|
|
is_active: boolean;
|
|
email_subject: string;
|
|
email_body_html: string;
|
|
webhook_url: string;
|
|
last_payload_example: Record<string, unknown> | null;
|
|
}
|
|
|
|
const SHORTCODES_HELP = {
|
|
common: ['{nama}', '{email}', '{order_id}', '{tanggal_pesanan}', '{total}', '{metode_pembayaran}'],
|
|
access: ['{produk}', '{link_akses}'],
|
|
consulting: ['{tanggal_konsultasi}', '{jam_konsultasi}', '{link_meet}'],
|
|
event: ['{judul_event}', '{tanggal_event}', '{jam_event}', '{link_event}'],
|
|
};
|
|
|
|
const DEFAULT_TEMPLATES: { key: string; name: string; defaultSubject: string; defaultBody: string }[] = [
|
|
{
|
|
key: 'payment_success',
|
|
name: 'Pembayaran Berhasil',
|
|
defaultSubject: 'Pembayaran Berhasil - Order #{order_id}',
|
|
defaultBody: '<h2>Halo {nama}!</h2><p>Terima kasih, pembayaran Anda sebesar <strong>{total}</strong> telah berhasil dikonfirmasi.</p><p><strong>Detail Pesanan:</strong></p><ul><li>Order ID: {order_id}</li><li>Tanggal: {tanggal_pesanan}</li><li>Metode: {metode_pembayaran}</li></ul><p>Produk: {produk}</p>'
|
|
},
|
|
{
|
|
key: 'access_granted',
|
|
name: 'Akses Produk Diberikan',
|
|
defaultSubject: 'Akses Anda Sudah Aktif - {produk}',
|
|
defaultBody: '<h2>Halo {nama}!</h2><p>Selamat! Akses Anda ke <strong>{produk}</strong> sudah aktif.</p><p><a href="{link_akses}" style="display:inline-block;padding:12px 24px;background:#0066cc;color:white;text-decoration:none;border-radius:6px;">Akses Sekarang</a></p>'
|
|
},
|
|
{
|
|
key: 'order_created',
|
|
name: 'Pesanan Dibuat',
|
|
defaultSubject: 'Pesanan Anda #{order_id} Sedang Diproses',
|
|
defaultBody: '<h2>Halo {nama}!</h2><p>Pesanan Anda dengan nomor <strong>{order_id}</strong> telah kami terima.</p><p>Total: <strong>{total}</strong></p><p>Silakan selesaikan pembayaran sebelum batas waktu.</p>'
|
|
},
|
|
{
|
|
key: 'payment_reminder',
|
|
name: 'Pengingat Pembayaran',
|
|
defaultSubject: 'Jangan Lupa Bayar - Order #{order_id}',
|
|
defaultBody: '<h2>Halo {nama}!</h2><p>Pesanan Anda dengan nomor <strong>{order_id}</strong> menunggu pembayaran.</p><p>Total: <strong>{total}</strong></p><p>Segera selesaikan pembayaran agar tidak kedaluwarsa.</p>'
|
|
},
|
|
{
|
|
key: 'consulting_scheduled',
|
|
name: 'Konsultasi Terjadwal',
|
|
defaultSubject: 'Konsultasi Anda Sudah Terjadwal - {tanggal_konsultasi}',
|
|
defaultBody: '<h2>Halo {nama}!</h2><p>Sesi konsultasi Anda telah dikonfirmasi:</p><ul><li>Tanggal: <strong>{tanggal_konsultasi}</strong></li><li>Jam: <strong>{jam_konsultasi}</strong></li></ul><p>Link meeting: <a href="{link_meet}">{link_meet}</a></p><p>Jika ada pertanyaan, hubungi kami.</p>'
|
|
},
|
|
{
|
|
key: 'event_reminder',
|
|
name: 'Reminder Webinar/Bootcamp',
|
|
defaultSubject: 'Reminder: {judul_event} Dimulai {tanggal_event}',
|
|
defaultBody: '<h2>Halo {nama}!</h2><p>Jangan lupa, <strong>{judul_event}</strong> akan dimulai:</p><ul><li>Tanggal: {tanggal_event}</li><li>Jam: {jam_event}</li></ul><p><a href="{link_event}" style="display:inline-block;padding:12px 24px;background:#0066cc;color:white;text-decoration:none;border-radius:6px;">Bergabung</a></p>'
|
|
},
|
|
{
|
|
key: 'bootcamp_progress',
|
|
name: 'Progress Bootcamp',
|
|
defaultSubject: 'Update Progress Bootcamp Anda',
|
|
defaultBody: '<h2>Halo {nama}!</h2><p>Ini adalah update progress bootcamp Anda.</p><p>Terus semangat belajar!</p>'
|
|
},
|
|
];
|
|
|
|
const emptyEmailSettings: EmailProviderSettings = {
|
|
provider: 'mailketing',
|
|
api_token: '',
|
|
from_name: '',
|
|
from_email: '',
|
|
};
|
|
|
|
export function NotifikasiTab() {
|
|
const [emailSettings, setEmailSettings] = useState<EmailProviderSettings>(emptyEmailSettings);
|
|
const [templates, setTemplates] = useState<NotificationTemplate[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [testEmail, setTestEmail] = useState('');
|
|
const [expandedTemplates, setExpandedTemplates] = useState<Set<string>>(new Set());
|
|
const [sendingTest, setSendingTest] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, []);
|
|
|
|
const fetchData = async () => {
|
|
// Fetch email provider settings
|
|
const { data: emailData } = await supabase.from('notification_settings').select('*').single();
|
|
if (emailData) setEmailSettings(emailData);
|
|
|
|
// 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();
|
|
}
|
|
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);
|
|
};
|
|
|
|
const saveEmailSettings = async () => {
|
|
setSaving(true);
|
|
const payload = { ...emailSettings };
|
|
delete payload.id;
|
|
|
|
if (emailSettings.id) {
|
|
const { error } = await supabase.from('notification_settings').update(payload).eq('id', emailSettings.id);
|
|
if (error) toast({ title: 'Error', description: error.message, variant: 'destructive' });
|
|
else toast({ title: 'Berhasil', description: 'Pengaturan email disimpan' });
|
|
} else {
|
|
const { data, error } = await supabase.from('notification_settings').insert(payload).select().single();
|
|
if (error) toast({ title: 'Error', description: error.message, variant: 'destructive' });
|
|
else { setEmailSettings(data); toast({ title: 'Berhasil', description: 'Pengaturan email disimpan' }); }
|
|
}
|
|
setSaving(false);
|
|
};
|
|
|
|
const sendTestEmail = async () => {
|
|
if (!testEmail) return toast({ title: 'Error', description: 'Masukkan email tujuan', variant: 'destructive' });
|
|
if (!isEmailConfigured) return toast({ title: 'Error', description: 'Lengkapi konfigurasi email provider terlebih dahulu', variant: 'destructive' });
|
|
|
|
setSendingTest(true);
|
|
try {
|
|
const { data, error } = await supabase.functions.invoke('send-email-v2', {
|
|
body: {
|
|
to: testEmail,
|
|
api_token: emailSettings.api_token,
|
|
from_name: emailSettings.from_name,
|
|
from_email: emailSettings.from_email,
|
|
subject: 'Test Email dari Access Hub',
|
|
html_body: `
|
|
<h2>Test Email</h2>
|
|
<p>Ini adalah email uji coba dari aplikasi Access Hub Anda.</p>
|
|
<p>Jika Anda menerima email ini, konfigurasi Mailketing API sudah berfungsi dengan baik!</p>
|
|
<p>Kirim ke: ${testEmail}</p>
|
|
<br>
|
|
<p>Best regards,<br>Access Hub Team</p>
|
|
`,
|
|
},
|
|
});
|
|
|
|
if (error) throw error;
|
|
|
|
if (data?.success) {
|
|
toast({ title: 'Berhasil', description: data.message });
|
|
} else {
|
|
throw new Error(data?.message || 'Failed to send test email');
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Test email error:', error);
|
|
toast({ title: 'Error', description: error.message || 'Gagal mengirim email uji coba', variant: 'destructive' });
|
|
} finally {
|
|
setSendingTest(false);
|
|
}
|
|
};
|
|
|
|
const updateTemplate = async (template: NotificationTemplate) => {
|
|
const { id, key, name, ...updates } = template;
|
|
const { error } = await supabase.from('notification_templates').update(updates).eq('id', id);
|
|
if (error) toast({ title: 'Error', description: error.message, variant: 'destructive' });
|
|
else toast({ title: 'Berhasil', description: `Template "${name}" disimpan` });
|
|
};
|
|
|
|
const toggleExpand = (id: string) => {
|
|
setExpandedTemplates(prev => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) next.delete(id);
|
|
else next.add(id);
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const isEmailConfigured = emailSettings.api_token && emailSettings.from_email;
|
|
|
|
if (loading) return <div className="animate-pulse h-64 bg-muted rounded-md" />;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Email Provider Settings */}
|
|
<Card className="border-2 border-border">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Mail className="w-5 h-5" />
|
|
Pengaturan Email Provider
|
|
</CardTitle>
|
|
<CardDescription>Konfigurasi provider email untuk pengiriman notifikasi</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{!isEmailConfigured && (
|
|
<Alert variant="destructive" className="border-2">
|
|
<AlertTriangle className="w-4 h-4" />
|
|
<AlertDescription>
|
|
Konfigurasi email provider belum lengkap. Email tidak akan terkirim.
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label>Provider Email</Label>
|
|
<Select
|
|
value={emailSettings.provider}
|
|
onValueChange={(value: 'mailketing') => setEmailSettings({ ...emailSettings, provider: value })}
|
|
>
|
|
<SelectTrigger className="border-2">
|
|
<SelectValue placeholder="Pilih provider email" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="mailketing">Mailketing</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="flex items-center gap-2">
|
|
<Key className="w-4 h-4" />
|
|
API Token
|
|
</Label>
|
|
<Input
|
|
type="password"
|
|
value={emailSettings.api_token}
|
|
onChange={(e) => setEmailSettings({ ...emailSettings, api_token: e.target.value })}
|
|
placeholder="Masukkan API token dari Mailketing"
|
|
className="border-2"
|
|
/>
|
|
<p className="text-sm text-muted-foreground">
|
|
Dapatkan API token dari menu Integration di dashboard Mailketing
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>Nama Pengirim</Label>
|
|
<Input
|
|
value={emailSettings.from_name}
|
|
onChange={(e) => setEmailSettings({ ...emailSettings, from_name: e.target.value })}
|
|
placeholder="Nama Bisnis"
|
|
className="border-2"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Email Pengirim</Label>
|
|
<Input
|
|
type="email"
|
|
value={emailSettings.from_email}
|
|
onChange={(e) => setEmailSettings({ ...emailSettings, from_email: e.target.value })}
|
|
placeholder="info@domain.com"
|
|
className="border-2"
|
|
/>
|
|
<p className="text-sm text-muted-foreground">
|
|
Pastikan email sudah terdaftar di Mailketing
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-4 pt-4 border-t">
|
|
<Button onClick={saveEmailSettings} disabled={saving}>
|
|
{saving ? 'Menyimpan...' : 'Simpan'}
|
|
</Button>
|
|
<div className="flex gap-2 flex-1">
|
|
<Input
|
|
type="email"
|
|
value={testEmail}
|
|
onChange={(e) => setTestEmail(e.target.value)}
|
|
placeholder="Email uji coba"
|
|
className="border-2 max-w-xs"
|
|
/>
|
|
<Button variant="outline" onClick={sendTestEmail} className="border-2" disabled={sendingTest}>
|
|
<Send className="w-4 h-4 mr-2" />
|
|
{sendingTest ? 'Mengirim...' : 'Kirim Email Uji Coba'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Notification Templates */}
|
|
<Card className="border-2 border-border">
|
|
<CardHeader>
|
|
<CardTitle>Template Notifikasi</CardTitle>
|
|
<CardDescription>
|
|
Atur template email untuk berbagai jenis notifikasi
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="text-sm text-muted-foreground p-3 bg-muted rounded-md space-y-2">
|
|
<p className="font-medium">Shortcode yang tersedia:</p>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
<div>
|
|
<span className="font-medium">Umum:</span> {SHORTCODES_HELP.common.join(', ')}
|
|
</div>
|
|
<div>
|
|
<span className="font-medium">Akses:</span> {SHORTCODES_HELP.access.join(', ')}
|
|
</div>
|
|
<div>
|
|
<span className="font-medium">Konsultasi:</span> {SHORTCODES_HELP.consulting.join(', ')}
|
|
</div>
|
|
<div>
|
|
<span className="font-medium">Event:</span> {SHORTCODES_HELP.event.join(', ')}
|
|
</div>
|
|
</div>
|
|
<p className="text-xs mt-2 p-2 bg-background rounded border">
|
|
<strong>Penting:</strong> Email dikirim melalui Mailketing API. Pastikan API token valid
|
|
dan domain pengirim sudah terdaftar di Mailketing. Toggle "Aktifkan" hanya mengontrol
|
|
pengiriman email. Jika <code>webhook_url</code> diisi, sistem tetap akan mengirim payload
|
|
ke URL tersebut meskipun email dinonaktifkan.
|
|
</p>
|
|
</div>
|
|
|
|
{templates.map((template) => (
|
|
<Collapsible
|
|
key={template.id}
|
|
open={expandedTemplates.has(template.id)}
|
|
onOpenChange={() => toggleExpand(template.id)}
|
|
>
|
|
<div className="border-2 border-border rounded-lg">
|
|
<CollapsibleTrigger asChild>
|
|
<div className="flex items-center justify-between p-4 cursor-pointer hover:bg-muted/50">
|
|
<div className="flex items-center gap-4">
|
|
<Switch
|
|
checked={template.is_active}
|
|
onCheckedChange={(checked) => {
|
|
const updated = { ...template, is_active: checked };
|
|
setTemplates(templates.map(t => t.id === template.id ? updated : t));
|
|
updateTemplate(updated);
|
|
}}
|
|
onClick={(e) => e.stopPropagation()}
|
|
/>
|
|
<div>
|
|
<p className="font-medium">{template.name}</p>
|
|
<p className="text-sm text-muted-foreground">{template.key}</p>
|
|
</div>
|
|
</div>
|
|
{expandedTemplates.has(template.id) ? (
|
|
<ChevronUp className="w-5 h-5" />
|
|
) : (
|
|
<ChevronDown className="w-5 h-5" />
|
|
)}
|
|
</div>
|
|
</CollapsibleTrigger>
|
|
|
|
<CollapsibleContent>
|
|
<div className="p-4 pt-0 space-y-4 border-t">
|
|
<div className="space-y-2">
|
|
<Label>Subjek Email</Label>
|
|
<Input
|
|
value={template.email_subject}
|
|
onChange={(e) => {
|
|
setTemplates(templates.map(t =>
|
|
t.id === template.id ? { ...t, email_subject: e.target.value } : t
|
|
));
|
|
}}
|
|
placeholder="Subjek email..."
|
|
className="border-2"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Isi Email (HTML)</Label>
|
|
<RichTextEditor
|
|
content={template.email_body_html}
|
|
onChange={(html) => {
|
|
setTemplates(templates.map(t =>
|
|
t.id === template.id ? { ...t, email_body_html: html } : t
|
|
));
|
|
}}
|
|
placeholder="Tulis isi email..."
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="flex items-center gap-2">
|
|
<Webhook className="w-4 h-4" />
|
|
Webhook URL (opsional, untuk n8n/Zapier)
|
|
</Label>
|
|
<Input
|
|
value={template.webhook_url}
|
|
onChange={(e) => {
|
|
setTemplates(templates.map(t =>
|
|
t.id === template.id ? { ...t, webhook_url: e.target.value } : t
|
|
));
|
|
}}
|
|
placeholder="https://n8n.example.com/webhook/..."
|
|
className="border-2"
|
|
/>
|
|
</div>
|
|
|
|
{template.last_payload_example && (
|
|
<div className="space-y-2">
|
|
<Label>Contoh Payload Terakhir</Label>
|
|
<pre className="p-3 bg-muted rounded-md text-xs overflow-x-auto">
|
|
{JSON.stringify(template.last_payload_example, null, 2)}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
|
|
<Button
|
|
onClick={() => updateTemplate(template)}
|
|
className="shadow-sm"
|
|
>
|
|
Simpan Template
|
|
</Button>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</div>
|
|
</Collapsible>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|