Files
meet-hub/src/components/admin/settings/IntegrasiTab.tsx
dwindown ee019ea767 Fix column name mismatch for service account JSON
- Change integration_google_service_account_json to google_service_account_json
- Matches actual database column name
- Remove schema cache workaround since column name now matches
- Update all frontend references to use correct column name

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-23 10:57:09 +07:00

574 lines
23 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_service_account_json?: string;
integration_email_provider: string;
integration_email_api_base_url: string;
integration_privacy_url: string;
integration_terms_url: string;
integration_n8n_test_mode: boolean;
// Mailketing specific settings
provider: 'mailketing' | 'smtp';
api_token: string;
from_name: string;
from_email: string;
}
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_privacy_url: '/privacy',
integration_terms_url: '/terms',
integration_n8n_test_mode: false,
provider: 'mailketing',
api_token: '',
from_name: '',
from_email: '',
};
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);
useEffect(() => {
fetchSettings();
}, []);
const fetchSettings = async () => {
const { data: platformData } = await supabase
.from('platform_settings')
.select('*')
.single();
// Fetch email provider settings from notification_settings
const { data: emailData } = await supabase
.from('notification_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_service_account_json: platformData.google_service_account_json || '',
integration_email_provider: platformData.integration_email_provider || 'mailketing',
integration_email_api_base_url: platformData.integration_email_api_base_url || '',
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,
// Email settings from notification_settings
provider: emailData?.provider || 'mailketing',
api_token: emailData?.api_token || '',
from_name: emailData?.from_name || platformData.brand_email_from_name || '',
from_email: emailData?.from_email || '',
});
}
setLoading(false);
};
const saveSettings = async () => {
setSaving(true);
try {
// Save platform 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_service_account_json: settings.google_service_account_json,
integration_email_provider: settings.integration_email_provider,
integration_email_api_base_url: settings.integration_email_api_base_url,
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 service account JSON separately via raw SQL
if (platformError.code === 'PGRST204' && settings.google_service_account_json) {
console.log('Schema cache error, using fallback RPC method');
const { error: rpcError } = await supabase.rpc('exec_sql', {
sql: `UPDATE platform_settings SET google_service_account_json = '${settings.google_service_account_json.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_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;
}
}
}
// Save email provider settings to notification_settings
const emailPayload = {
provider: settings.provider,
api_token: settings.api_token,
from_name: settings.from_name,
from_email: settings.from_email,
};
const { data: existingEmailSettings } = await supabase
.from('notification_settings')
.select('id')
.maybeSingle();
if (existingEmailSettings?.id) {
const { error: emailError } = await supabase
.from('notification_settings')
.update(emailPayload)
.eq('id', existingEmailSettings.id);
if (emailError) throw emailError;
} else {
const { error: emailError } = await supabase
.from('notification_settings')
.insert(emailPayload);
if (emailError) throw emailError;
}
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 {
const { data, error } = await supabase.functions.invoke('send-email-v2', {
body: {
to: testEmail,
api_token: settings.api_token,
from_name: settings.from_name,
from_email: settings.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 isEmailConfigured = settings.api_token && settings.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 Service Account JSON
</Label>
<Textarea
value={settings.google_service_account_json || ''}
onChange={(e) => setSettings({ ...settings, google_service_account_json: e.target.value })}
placeholder='{"type": "service_account", "project_id": "...", "private_key": "...", "client_email": "..."}'
className="min-h-[120px] font-mono text-sm border-2"
/>
<p className="text-sm text-muted-foreground">
Paste entire service account JSON from Google Cloud Console. Calendar must be shared with the service account email.
</p>
{settings.google_service_account_json && (
<Alert>
<AlertTriangle className="w-4 h-4" />
<AlertDescription>
Service account configured. Calendar ID: {settings.integration_google_calendar_id || 'Not set'}
</AlertDescription>
</Alert>
)}
</div>
<Button
variant="outline"
onClick={async () => {
if (!settings.integration_google_calendar_id || !settings.google_service_account_json) {
toast({ title: "Error", description: "Lengkapi Calendar ID dan Service Account JSON", variant: "destructive" });
return;
}
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" });
}
}}
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.provider}
onValueChange={(value: 'mailketing' | 'smtp') => setSettings({ ...settings, provider: value })}
>
<SelectTrigger className="border-2">
<SelectValue placeholder="Pilih provider email" />
</SelectTrigger>
<SelectContent>
<SelectItem value="mailketing">Mailketing</SelectItem>
<SelectItem value="smtp">SMTP (Legacy)</SelectItem>
</SelectContent>
</Select>
</div>
{settings.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.api_token}
onChange={(e) => setSettings({ ...settings, 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.from_name}
onChange={(e) => setSettings({ ...settings, 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.from_email}
onChange={(e) => setSettings({ ...settings, 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>
</>
)}
{settings.provider === 'smtp' && (
<div className="space-y-2">
<Label>API Base URL Provider Email</Label>
<Input
value={settings.integration_email_api_base_url}
onChange={(e) => setSettings({ ...settings, integration_email_api_base_url: e.target.value })}
placeholder="https://api.resend.com"
className="border-2"
/>
<p className="text-sm text-muted-foreground">
Konfigurasi SMTP masih di bagian Notifikasi
</p>
</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>
);
}