Email System Fixes: - Fix email sending after payment: handle-order-paid now calls send-notification instead of send-email-v2 directly, properly processing template variables - Fix order_created email timing: sent immediately after order creation, before payment QR code generation - Update email templates to use short order ID (8 chars) instead of full UUID - Add working "Akses Sekarang" buttons to payment_success and access_granted emails - Add platform_url column to platform_settings for email links OTP Verification Flow: - Create dedicated /confirm-otp page for users who close registration modal - Add link in checkout modal and email to dedicated OTP page - Update OTP email template with better copywriting and dedicated page link - Fix send-auth-otp to fetch platform settings for dynamic brand_name and platform_url - Auto-login users after OTP verification in checkout flow Admin Features: - Add delete user functionality with cascade deletion of all related data - Update IntegrasiTab to read/write email settings from platform_settings only - Add test email template for email configuration testing Cleanup: - Remove obsolete send-consultation-reminder and send-test-email functions - Update send-email-v2 to read email config from platform_settings - Remove footer links (Ubah Preferensi/Unsubscribe) from email templates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
558 lines
22 KiB
TypeScript
558 lines
22 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 { Textarea } from '@/components/ui/textarea';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import { toast } from '@/hooks/use-toast';
|
|
import { Puzzle, Webhook, MessageSquare, Calendar, Mail, Link as LinkIcon, Key, Send, AlertTriangle } from 'lucide-react';
|
|
|
|
interface IntegrationSettings {
|
|
id?: string;
|
|
integration_n8n_base_url: string;
|
|
integration_whatsapp_number: string;
|
|
integration_whatsapp_url: string;
|
|
integration_google_calendar_id: string;
|
|
google_oauth_config?: string;
|
|
integration_email_provider: string;
|
|
integration_email_api_base_url: string;
|
|
integration_email_api_token: string;
|
|
integration_email_from_name: string;
|
|
integration_email_from_email: string;
|
|
integration_privacy_url: string;
|
|
integration_terms_url: string;
|
|
integration_n8n_test_mode: boolean;
|
|
}
|
|
|
|
const emptySettings: IntegrationSettings = {
|
|
integration_n8n_base_url: '',
|
|
integration_whatsapp_number: '',
|
|
integration_whatsapp_url: '',
|
|
integration_google_calendar_id: '',
|
|
integration_email_provider: 'mailketing',
|
|
integration_email_api_base_url: '',
|
|
integration_email_api_token: '',
|
|
integration_email_from_name: '',
|
|
integration_email_from_email: '',
|
|
integration_privacy_url: '/privacy',
|
|
integration_terms_url: '/terms',
|
|
integration_n8n_test_mode: false,
|
|
};
|
|
|
|
export function IntegrasiTab() {
|
|
const [settings, setSettings] = useState<IntegrationSettings>(emptySettings);
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [testEmail, setTestEmail] = useState('');
|
|
const [sendingTest, setSendingTest] = useState(false);
|
|
const [isTestRunning, setIsTestRunning] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetchSettings();
|
|
}, []);
|
|
|
|
const fetchSettings = async () => {
|
|
const { data: platformData } = await supabase
|
|
.from('platform_settings')
|
|
.select('*')
|
|
.single();
|
|
|
|
if (platformData) {
|
|
setSettings({
|
|
id: platformData.id,
|
|
integration_n8n_base_url: platformData.integration_n8n_base_url || '',
|
|
integration_whatsapp_number: platformData.integration_whatsapp_number || '',
|
|
integration_whatsapp_url: platformData.integration_whatsapp_url || '',
|
|
integration_google_calendar_id: platformData.integration_google_calendar_id || '',
|
|
google_oauth_config: platformData.google_oauth_config || '',
|
|
integration_email_provider: platformData.integration_email_provider || 'mailketing',
|
|
integration_email_api_base_url: platformData.integration_email_api_base_url || '',
|
|
integration_email_api_token: platformData.integration_email_api_token || '',
|
|
integration_email_from_name: platformData.integration_email_from_name || platformData.brand_email_from_name || '',
|
|
integration_email_from_email: platformData.integration_email_from_email || '',
|
|
integration_privacy_url: platformData.integration_privacy_url || '/privacy',
|
|
integration_terms_url: platformData.integration_terms_url || '/terms',
|
|
integration_n8n_test_mode: platformData.integration_n8n_test_mode || false,
|
|
});
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
const saveSettings = async () => {
|
|
setSaving(true);
|
|
|
|
try {
|
|
// Save platform settings (includes email settings)
|
|
const platformPayload = {
|
|
integration_n8n_base_url: settings.integration_n8n_base_url,
|
|
integration_whatsapp_number: settings.integration_whatsapp_number,
|
|
integration_whatsapp_url: settings.integration_whatsapp_url,
|
|
integration_google_calendar_id: settings.integration_google_calendar_id,
|
|
google_oauth_config: settings.google_oauth_config,
|
|
integration_email_provider: settings.integration_email_provider,
|
|
integration_email_api_base_url: settings.integration_email_api_base_url,
|
|
integration_email_api_token: settings.integration_email_api_token,
|
|
integration_email_from_name: settings.integration_email_from_name,
|
|
integration_email_from_email: settings.integration_email_from_email,
|
|
integration_privacy_url: settings.integration_privacy_url,
|
|
integration_terms_url: settings.integration_terms_url,
|
|
integration_n8n_test_mode: settings.integration_n8n_test_mode,
|
|
};
|
|
|
|
if (settings.id) {
|
|
const { error: platformError } = await supabase
|
|
.from('platform_settings')
|
|
.update(platformPayload)
|
|
.eq('id', settings.id);
|
|
|
|
if (platformError) {
|
|
// If schema cache error, try saving OAuth config separately via raw SQL
|
|
if (platformError.code === 'PGRST204' && settings.google_oauth_config) {
|
|
console.log('Schema cache error, using fallback RPC method');
|
|
const { error: rpcError } = await supabase.rpc('exec_sql', {
|
|
sql: `UPDATE platform_settings SET google_oauth_config = '${settings.google_oauth_config.replace(/'/g, "''")}'::jsonb WHERE id = '${settings.id}'`
|
|
});
|
|
|
|
if (rpcError) {
|
|
// Save other fields without the problematic column
|
|
const { error: retryError } = await supabase
|
|
.from('platform_settings')
|
|
.update({
|
|
integration_n8n_base_url: settings.integration_n8n_base_url,
|
|
integration_whatsapp_number: settings.integration_whatsapp_number,
|
|
integration_whatsapp_url: settings.integration_whatsapp_url,
|
|
integration_google_calendar_id: settings.integration_google_calendar_id,
|
|
integration_email_provider: settings.integration_email_provider,
|
|
integration_email_api_base_url: settings.integration_email_api_base_url,
|
|
integration_email_api_token: settings.integration_email_api_token,
|
|
integration_email_from_name: settings.integration_email_from_name,
|
|
integration_email_from_email: settings.integration_email_from_email,
|
|
integration_privacy_url: settings.integration_privacy_url,
|
|
integration_terms_url: settings.integration_terms_url,
|
|
integration_n8n_test_mode: settings.integration_n8n_test_mode,
|
|
})
|
|
.eq('id', settings.id);
|
|
|
|
if (retryError) throw retryError;
|
|
toast({ title: 'Peringatan', description: 'Pengaturan disimpan tapi Service Account JSON perlu disimpan manual. Hubungi admin.' });
|
|
} else {
|
|
toast({ title: 'Berhasil', description: 'Service Account JSON disimpan via RPC' });
|
|
}
|
|
} else {
|
|
throw platformError;
|
|
}
|
|
}
|
|
}
|
|
|
|
toast({ title: 'Berhasil', description: 'Pengaturan integrasi disimpan' });
|
|
} catch (error: any) {
|
|
toast({ title: 'Error', description: error.message, variant: 'destructive' });
|
|
}
|
|
|
|
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 {
|
|
// Get brand name for test email
|
|
const { data: platformData } = await supabase
|
|
.from('platform_settings')
|
|
.select('brand_name')
|
|
.single();
|
|
|
|
const brandName = platformData?.brand_name || 'ACCESS HUB';
|
|
|
|
// Test email content using proper HTML template
|
|
const testEmailContent = `
|
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
<h2 style="color: #333;">Email Test - ${brandName}</h2>
|
|
|
|
<p>Halo,</p>
|
|
|
|
<p>Ini adalah email tes dari sistem <strong>${brandName}</strong>.</p>
|
|
|
|
<div style="margin: 20px 0; padding: 15px; background-color: #f0f0f0; border-left: 4px solid #000;">
|
|
<p style="margin: 0; font-size: 14px;">
|
|
<strong>✓ Konfigurasi email berhasil!</strong><br>
|
|
Email Anda telah terkirim dengan benar menggunakan provider: <strong>Mailketing</strong>
|
|
</p>
|
|
</div>
|
|
|
|
<p style="font-size: 14px; color: #666;">
|
|
Jika Anda menerima email ini, berarti konfigurasi email sudah benar.
|
|
</p>
|
|
|
|
<p style="font-size: 14px;">
|
|
Terima kasih,<br>
|
|
Tim ${brandName}
|
|
</p>
|
|
</div>
|
|
`;
|
|
|
|
const { data, error } = await supabase.functions.invoke('send-notification', {
|
|
body: {
|
|
template_key: 'test_email',
|
|
recipient_email: testEmail,
|
|
recipient_name: 'Admin',
|
|
variables: {
|
|
brand_name: brandName,
|
|
test_email: testEmail
|
|
}
|
|
},
|
|
});
|
|
|
|
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 isEmailConfigured = settings.integration_email_api_token && settings.integration_email_from_email;
|
|
|
|
if (loading) return <div className="animate-pulse h-64 bg-muted rounded-md" />;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* n8n / Webhook */}
|
|
<Card className="border-2 border-border">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Webhook className="w-5 h-5" />
|
|
n8n / Webhook
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Konfigurasi URL untuk integrasi otomatisasi
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label>Base URL n8n / Integrasi</Label>
|
|
<Input
|
|
value={settings.integration_n8n_base_url}
|
|
onChange={(e) => setSettings({ ...settings, integration_n8n_base_url: e.target.value })}
|
|
placeholder="https://automation.domain.com"
|
|
className="border-2"
|
|
/>
|
|
<p className="text-sm text-muted-foreground">
|
|
Digunakan sebagai target default untuk webhook lanjutan. webhook_url per template tetap harus URL lengkap.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between p-4 border rounded-lg space-y-0">
|
|
<div className="space-y-0.5">
|
|
<Label>Mode Test n8n</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Aktifkan untuk menggunakan webhook path /webhook-test/ instead of /webhook/
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={settings.integration_n8n_test_mode}
|
|
onCheckedChange={(checked) => setSettings({ ...settings, integration_n8n_test_mode: checked })}
|
|
/>
|
|
</div>
|
|
|
|
{settings.integration_n8n_test_mode && (
|
|
<Alert>
|
|
<AlertTriangle className="w-4 h-4" />
|
|
<AlertDescription>
|
|
Mode test aktif: Webhook akan menggunakan path <code>/webhook-test/</code>
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* WhatsApp */}
|
|
<Card className="border-2 border-border">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<MessageSquare className="w-5 h-5" />
|
|
WhatsApp
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Nomor kontak untuk dukungan dan notifikasi
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>Nomor WhatsApp Dukungan</Label>
|
|
<Input
|
|
value={settings.integration_whatsapp_number}
|
|
onChange={(e) => setSettings({ ...settings, integration_whatsapp_number: e.target.value })}
|
|
placeholder="+62812xxxx"
|
|
className="border-2"
|
|
/>
|
|
<p className="text-sm text-muted-foreground">
|
|
Ditampilkan di konfirmasi konsultasi
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>URL WhatsApp Click-to-Chat (Opsional)</Label>
|
|
<Input
|
|
value={settings.integration_whatsapp_url}
|
|
onChange={(e) => setSettings({ ...settings, integration_whatsapp_url: e.target.value })}
|
|
placeholder="https://wa.me/62812..."
|
|
className="border-2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Google Calendar */}
|
|
<Card className="border-2 border-border">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Calendar className="w-5 h-5" />
|
|
Google Calendar
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Untuk pembuatan event konsultasi otomatis
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label>ID Kalender Google untuk Konsultasi</Label>
|
|
<Input
|
|
value={settings.integration_google_calendar_id}
|
|
onChange={(e) => setSettings({ ...settings, integration_google_calendar_id: e.target.value })}
|
|
placeholder="your-calendar@gmail.com"
|
|
className="border-2"
|
|
/>
|
|
<p className="text-sm text-muted-foreground">
|
|
Backend akan menggunakan ID ini untuk membuat event
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="flex items-center gap-2">
|
|
<Key className="w-4 h-4" />
|
|
Google OAuth Config
|
|
</Label>
|
|
<Textarea
|
|
value={settings.google_oauth_config || ''}
|
|
onChange={(e) => setSettings({ ...settings, google_oauth_config: e.target.value })}
|
|
placeholder='{"client_id": "...", "client_secret": "...", "refresh_token": "..."}'
|
|
className="min-h-[120px] font-mono text-sm border-2"
|
|
/>
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-muted-foreground">
|
|
OAuth2 credentials untuk personal Gmail account. Gunakan <a href="/get-google-refresh-token.html" target="_blank" className="text-blue-600 underline">tool ini</a> untuk generate refresh token.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
variant="outline"
|
|
onClick={async () => {
|
|
if (!settings.integration_google_calendar_id || !settings.google_oauth_config) {
|
|
toast({ title: "Error", description: "Lengkapi Calendar ID dan OAuth Config", variant: "destructive" });
|
|
return;
|
|
}
|
|
|
|
if (isTestRunning) {
|
|
return; // Prevent React Strict Mode double-call
|
|
}
|
|
|
|
setIsTestRunning(true);
|
|
|
|
try {
|
|
const { data, error } = await supabase.functions.invoke('create-google-meet-event', {
|
|
body: {
|
|
slot_id: 'test-connection',
|
|
date: new Date().toISOString().split('T')[0],
|
|
start_time: '14:00:00',
|
|
end_time: '15:00:00',
|
|
client_name: 'Test Connection',
|
|
client_email: 'test@example.com',
|
|
topic: 'Connection Test',
|
|
},
|
|
});
|
|
|
|
if (error) throw error;
|
|
if (data?.success) {
|
|
toast({ title: "Berhasil", description: "Google Calendar API berfungsi! Event test dibuat." });
|
|
} else {
|
|
throw new Error(data?.message || 'Connection failed');
|
|
}
|
|
} catch (err: any) {
|
|
toast({ title: "Error", description: err.message, variant: "destructive" });
|
|
} finally {
|
|
setIsTestRunning(false);
|
|
}
|
|
}}
|
|
disabled={isTestRunning}
|
|
className="w-full border-2"
|
|
>
|
|
Test Google Calendar Connection
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Email Provider */}
|
|
<Card className="border-2 border-border">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Mail className="w-5 h-5" />
|
|
Provider Email
|
|
</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={settings.integration_email_provider}
|
|
onValueChange={(value: 'mailketing') => setSettings({ ...settings, integration_email_provider: value })}
|
|
>
|
|
<SelectTrigger className="border-2">
|
|
<SelectValue placeholder="Pilih provider email" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="mailketing">Mailketing</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{settings.integration_email_provider === 'mailketing' && (
|
|
<>
|
|
<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={settings.integration_email_api_token}
|
|
onChange={(e) => setSettings({ ...settings, integration_email_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={settings.integration_email_from_name}
|
|
onChange={(e) => setSettings({ ...settings, integration_email_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={settings.integration_email_from_email}
|
|
onChange={(e) => setSettings({ ...settings, integration_email_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 className="flex gap-4 pt-4 border-t">
|
|
<Input
|
|
type="email"
|
|
value={testEmail}
|
|
onChange={(e) => setTestEmail(e.target.value)}
|
|
placeholder="Email uji coba"
|
|
className="border-2 flex-1"
|
|
/>
|
|
<Button variant="outline" onClick={sendTestEmail} className="border-2 flex-1" disabled={sendingTest}>
|
|
<Send className="w-4 h-4 mr-2" />
|
|
{sendingTest ? 'Mengirim...' : 'Kirim Email Uji Coba'}
|
|
</Button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Public Links */}
|
|
<Card className="border-2 border-border">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<LinkIcon className="w-5 h-5" />
|
|
Link Publik
|
|
</CardTitle>
|
|
<CardDescription>
|
|
URL untuk halaman legal
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>URL Kebijakan Privasi</Label>
|
|
<Input
|
|
value={settings.integration_privacy_url}
|
|
onChange={(e) => setSettings({ ...settings, integration_privacy_url: e.target.value })}
|
|
placeholder="/privacy"
|
|
className="border-2"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>URL Syarat & Ketentuan</Label>
|
|
<Input
|
|
value={settings.integration_terms_url}
|
|
onChange={(e) => setSettings({ ...settings, integration_terms_url: e.target.value })}
|
|
placeholder="/terms"
|
|
className="border-2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
Default: halaman internal. Bisa diganti dengan URL eksternal.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className="flex gap-4 pt-4 border-t-2 border-border">
|
|
<Button onClick={saveSettings} disabled={saving} className="shadow-sm flex-1">
|
|
{saving ? 'Menyimpan...' : 'Simpan Semua Pengaturan'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|