diff --git a/.env.example b/.env.example index ee02aab..d4fc034 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,10 @@ VITE_SUPABASE_EDGE_URL=your_supabase_url_here/functions/v1 # Application Configuration VITE_APP_NAME=Access Hub VITE_APP_ENV=production +SITE_URL=https://with.dwindi.com/ + +# Google Integration +VITE_GOOGLE_CLIENT_ID=your_google_oauth_client_id_here # Third-party Integrations VITE_PAKASIR_API_KEY=your_pakasir_api_key_here diff --git a/deploy-edge-functions.sh b/deploy-edge-functions.sh index b4fb62e..b6bb90d 100755 --- a/deploy-edge-functions.sh +++ b/deploy-edge-functions.sh @@ -32,9 +32,10 @@ deploy_function() { # Deploy all functions deploy_function "pakasir-webhook" deploy_function "send-test-email" -deploy_function "create-meet-link" +deploy_function "create-meet-link" # Includes n8n test mode toggle deploy_function "send-consultation-reminder" deploy_function "send-notification" +deploy_function "send-email-v2" deploy_function "daily-reminders" echo "Deployment complete!" diff --git a/src/components/admin/settings/BrandingTab.tsx b/src/components/admin/settings/BrandingTab.tsx index 57f71f1..b54d7d8 100644 --- a/src/components/admin/settings/BrandingTab.tsx +++ b/src/components/admin/settings/BrandingTab.tsx @@ -22,7 +22,6 @@ interface PlatformSettings { brand_favicon_url: string; brand_primary_color: string; brand_accent_color: string; - brand_email_from_name: string; homepage_headline: string; homepage_description: string; homepage_features: HomepageFeature[]; @@ -41,7 +40,6 @@ const emptySettings: PlatformSettings = { brand_favicon_url: '', brand_primary_color: '#111827', brand_accent_color: '#0F766E', - brand_email_from_name: '', homepage_headline: 'Learn. Grow. Succeed.', homepage_description: 'Access premium consulting, live webinars, and intensive bootcamps to accelerate your career.', homepage_features: defaultFeatures, @@ -84,7 +82,6 @@ export function BrandingTab() { brand_favicon_url: data.brand_favicon_url || '', brand_primary_color: data.brand_primary_color || '#111827', brand_accent_color: data.brand_accent_color || '#0F766E', - brand_email_from_name: data.brand_email_from_name || '', homepage_headline: data.homepage_headline || emptySettings.homepage_headline, homepage_description: data.homepage_description || emptySettings.homepage_description, homepage_features: features, @@ -102,7 +99,6 @@ export function BrandingTab() { brand_favicon_url: settings.brand_favicon_url, brand_primary_color: settings.brand_primary_color, brand_accent_color: settings.brand_accent_color, - brand_email_from_name: settings.brand_email_from_name, homepage_headline: settings.homepage_headline, homepage_description: settings.homepage_description, homepage_features: settings.homepage_features, @@ -280,22 +276,6 @@ export function BrandingTab() { - -
- - setSettings({ ...settings, brand_email_from_name: e.target.value })} - placeholder="LearnHub Team" - className="border-2" - /> -

- Digunakan jika SMTP from_name kosong -

-
diff --git a/src/components/admin/settings/IntegrasiTab.tsx b/src/components/admin/settings/IntegrasiTab.tsx index 99bda0b..334982e 100644 --- a/src/components/admin/settings/IntegrasiTab.tsx +++ b/src/components/admin/settings/IntegrasiTab.tsx @@ -6,6 +6,7 @@ import { Input } from '@/components/ui/input'; 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'; @@ -19,6 +20,7 @@ interface IntegrationSettings { 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; @@ -35,6 +37,7 @@ const emptySettings: IntegrationSettings = { integration_email_api_base_url: '', integration_privacy_url: '/privacy', integration_terms_url: '/terms', + integration_n8n_test_mode: false, provider: 'mailketing', api_token: '', from_name: '', @@ -75,6 +78,7 @@ export function IntegrasiTab() { 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 || '', @@ -99,6 +103,7 @@ export function IntegrasiTab() { 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) { @@ -215,6 +220,28 @@ export function IntegrasiTab() { Digunakan sebagai target default untuk webhook lanjutan. webhook_url per template tetap harus URL lengkap.

+ +
+
+ +

+ Aktifkan untuk menggunakan webhook path /webhook-test/ instead of /webhook/ +

+
+ setSettings({ ...settings, integration_n8n_test_mode: checked })} + /> +
+ + {settings.integration_n8n_test_mode && ( + + + + Mode test aktif: Webhook akan menggunakan path /webhook-test/ + + + )} diff --git a/src/lib/email-templates/master-template.ts b/src/lib/email-templates/master-template.ts index 1e0ceaf..5f40876 100644 --- a/src/lib/email-templates/master-template.ts +++ b/src/lib/email-templates/master-template.ts @@ -357,7 +357,7 @@ export class ShortcodeProcessor { total: 'Rp 1.500.000', metode_pembayaran: 'Transfer Bank', status_pesanan: 'Diproses', - invoice_url: 'https://example.com/invoice/ORD-123456', + invoice_url: 'https://with.dwindi.com/orders/ORD-123456', // Product information produk: 'Digital Marketing Masterclass', @@ -366,7 +366,7 @@ export class ShortcodeProcessor { deskripsi_produk: 'Kelas lengkap digital marketing dari pemula hingga mahir', // Access information - link_akses: 'https://example.com/access', + link_akses: 'https://with.dwindi.com/access', username_akses: 'john.doe', password_akses: 'Temp123!', kadaluarsa_akses: '22 Desember 2026', @@ -383,7 +383,7 @@ export class ShortcodeProcessor { judul_event: 'Workshop Digital Marketing', tanggal_event: '25 Desember 2025', jam_event: '19:00', - link_event: 'https://event.example.com', + link_event: 'https://with.dwindi.com/events', lokasi_event: 'Zoom Online', kapasitas_event: '100 peserta', @@ -392,12 +392,12 @@ export class ShortcodeProcessor { progres_bootcamp: '75%', modul_selesai: '15 dari 20 modul', modul_selanjutnya: 'Final Assessment', - link_progress: 'https://example.com/progress', + link_progress: 'https://with.dwindi.com/bootcamp/progress', // Company information nama_perusahaan: 'ACCESS HUB', - website_perusahaan: 'https://accesshub.example.com', - email_support: 'support@accesshub.example.com', + website_perusahaan: 'https://with.dwindi.com', + email_support: 'support@with.dwindi.com', telepon_support: '+62 812-3456-7890', // Payment information @@ -406,8 +406,8 @@ export class ShortcodeProcessor { atas_nama: 'PT Access Hub Indonesia', jumlah_pembayaran: 'Rp 1.500.000', batas_pembayaran: '22 Desember 2025 23:59', - payment_link: 'https://accesshub.example.com/checkout', - thank_you_page: 'https://accesshub.example.com/orders/{order_id}' + payment_link: 'https://with.dwindi.com/checkout', + thank_you_page: 'https://with.dwindi.com/orders/{order_id}' }; static process(content: string, customData: Record = {}): string { diff --git a/supabase/config.toml b/supabase/config.toml index a6c232c..ecd7019 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -32,3 +32,6 @@ verify_jwt = false [functions.daily-reminders] verify_jwt = false + +[functions.send-email-v2] +verify_jwt = false diff --git a/supabase/functions/create-meet-link/index.ts b/supabase/functions/create-meet-link/index.ts index ddc5c5a..1becdd0 100644 --- a/supabase/functions/create-meet-link/index.ts +++ b/supabase/functions/create-meet-link/index.ts @@ -52,16 +52,21 @@ serve(async (req: Request): Promise => { // For now, this is a placeholder that returns a message // In production, you would integrate with Google Calendar API via OAuth or service account // Or call an n8n webhook to handle the calendar creation - + const { data: integrationSettings } = await supabase .from("platform_settings") - .select("integration_n8n_base_url") + .select("integration_n8n_base_url, integration_n8n_test_mode") .single(); if (integrationSettings?.integration_n8n_base_url) { - // Call n8n webhook if configured - const n8nUrl = `${integrationSettings.integration_n8n_base_url}/webhook/create-meet`; - + // Check if we're in test mode (controlled by the integration_n8n_test_mode setting) + const isTestMode = integrationSettings.integration_n8n_test_mode || false; + + const webhookPath = isTestMode ? "/webhook-test/" : "/webhook/"; + const n8nUrl = `${integrationSettings.integration_n8n_base_url}${webhookPath}create-meet`; + + console.log(`Calling n8n webhook: ${n8nUrl} (Test mode: ${isTestMode})`); + try { const n8nResponse = await fetch(n8nUrl, { method: "POST", @@ -77,6 +82,7 @@ serve(async (req: Request): Promise => { notes: body.notes, calendar_id: calendarId, brand_name: brandName, + test_mode: isTestMode, // Add test_mode flag for n8n to use }), }); diff --git a/supabase/functions/daily-reminders/index.ts b/supabase/functions/daily-reminders/index.ts index 8d49452..7d3ac39 100644 --- a/supabase/functions/daily-reminders/index.ts +++ b/supabase/functions/daily-reminders/index.ts @@ -69,7 +69,63 @@ serve(async (req: Request): Promise => { if (!profile?.email) continue; // Call send-notification function - const { error: notifyError } = await supabase.functions.invoke("send-notification", { + // Get notification template and settings to send via send-email-v2 + const { data: template } = await supabase + .from("notification_templates") + .select("*") + .eq("key", "consulting_reminder") + .eq("is_active", true) + .single(); + + const { data: emailSettings } = await supabase + .from("notification_settings") + .select("*") + .single(); + + let notifyError = null; + + if (template && emailSettings?.api_token) { + // Build payload with proper shortcode mapping + const payload = { + nama: profile.full_name, + email: profile.email, + tanggal_konsultasi: new Date(slot.date).toLocaleDateString('id-ID', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }), + jam_konsultasi: `${slot.start_time.substring(0, 5)} - ${slot.end_time.substring(0, 5)} WIB`, + link_meet: slot.meet_link || "Akan diinformasikan", + jenis_konsultasi: slot.topic_category, + }; + + // Process shortcodes in template + let emailBody = template.email_body_html || ""; + let emailSubject = template.email_subject || "Reminder Konsultasi"; + + Object.entries(payload).forEach(([key, value]) => { + const regex = new RegExp(`\\{${key}\\}`, "g"); + emailBody = emailBody.replace(regex, String(value)); + emailSubject = emailSubject.replace(regex, String(value)); + }); + + // Send via send-email-v2 (Mailketing API) + const { error: emailError } = await supabase.functions.invoke("send-email-v2", { + body: { + to: profile.email, + api_token: emailSettings.api_token, + from_name: emailSettings.from_name || "Access Hub", + from_email: emailSettings.from_email || "noreply@with.dwindi.com", + subject: emailSubject, + html_body: emailBody, + }, + }); + + notifyError = emailError; + } else { + notifyError = { message: "Template not active or email not configured" }; + } body: { template_key: "consultation_reminder", recipient_email: profile.email, diff --git a/supabase/functions/send-consultation-reminder/index.ts b/supabase/functions/send-consultation-reminder/index.ts index 1de6bc6..4cdd15f 100644 --- a/supabase/functions/send-consultation-reminder/index.ts +++ b/supabase/functions/send-consultation-reminder/index.ts @@ -127,22 +127,39 @@ serve(async (req: Request): Promise => { } } - // Send email if template is active and SMTP is configured - if (template?.is_active && smtpSettings?.smtp_host && profile?.email) { - // Replace shortcodes in email body - let emailBody = template.email_body_html || ""; - let emailSubject = template.email_subject || "Reminder Konsultasi"; + // Send email if template is active and Mailketing is configured + if (template?.is_active && smtpSettings?.api_token && profile?.email) { + try { + // Replace shortcodes in email body using master template system + let emailBody = template.email_body_html || ""; + let emailSubject = template.email_subject || "Reminder Konsultasi"; - Object.entries(payload).forEach(([key, value]) => { - const regex = new RegExp(`\\{${key}\\}`, "g"); - emailBody = emailBody.replace(regex, String(value)); - emailSubject = emailSubject.replace(regex, String(value)); - }); + Object.entries(payload).forEach(([key, value]) => { + const regex = new RegExp(`\\{${key}\\}`, "g"); + emailBody = emailBody.replace(regex, String(value)); + emailSubject = emailSubject.replace(regex, String(value)); + }); - // Here you would send the actual email - // For now, log that we would send it - console.log("Would send reminder email to:", profile.email); - console.log("Subject:", emailSubject); + // Send via send-email-v2 (Mailketing API) + const { error: emailError } = await supabase.functions.invoke("send-email-v2", { + body: { + to: profile.email, + api_token: smtpSettings.api_token, + from_name: smtpSettings.from_name || platformSettings?.brand_name || "Access Hub", + from_email: smtpSettings.from_email || "noreply@with.dwindi.com", + subject: emailSubject, + html_body: emailBody, + }, + }); + + if (emailError) { + console.error("Failed to send reminder email:", emailError); + } else { + console.log("Reminder email sent to:", profile.email); + } + } catch (emailError) { + console.error("Error sending reminder email:", emailError); + } } results.push({ diff --git a/supabase/functions/send-email-v2/index.ts b/supabase/functions/send-email-v2/index.ts new file mode 100644 index 0000000..afbeca5 --- /dev/null +++ b/supabase/functions/send-email-v2/index.ts @@ -0,0 +1,99 @@ +import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", +}; + +interface EmailRequest { + to: string; + api_token: string; + from_name: string; + from_email: string; + subject: string; + html_body: string; +} + +// Send via Mailketing API +async function sendViaMailketing(request: EmailRequest): Promise<{ success: boolean; message: string }> { + const { to, api_token, from_name, from_email, subject, html_body } = request; + + const formData = new FormData(); + formData.append('to', to); + formData.append('from_name', from_name); + formData.append('from_email', from_email); + formData.append('subject', subject); + formData.append('html_body', html_body); + + console.log(`Sending email via Mailketing to ${to}`); + + const response = await fetch('https://api.mailketing.co/v1/send', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${api_token}`, + }, + body: formData, + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Mailketing API error:', response.status, errorText); + throw new Error(`Mailketing API error: ${response.status} ${errorText}`); + } + + const result = await response.json(); + console.log('Mailketing API response:', result); + + return { + success: true, + message: result.message || 'Email sent successfully via Mailketing' + }; +} + +serve(async (req: Request): Promise => { + if (req.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + + try { + const body: EmailRequest = await req.json(); + + // Validate required fields + if (!body.to || !body.api_token || !body.from_name || !body.from_email || !body.subject || !body.html_body) { + return new Response( + JSON.stringify({ success: false, message: "Missing required fields: to, api_token, from_name, from_email, subject, html_body" }), + { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + // Basic email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(body.to) || !emailRegex.test(body.from_email)) { + return new Response( + JSON.stringify({ success: false, message: "Invalid email format" }), + { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + console.log(`Attempting to send email to: ${body.to}`); + console.log(`From: ${body.from_name} <${body.from_email}>`); + console.log(`Subject: ${body.subject}`); + + const result = await sendViaMailketing(body); + + return new Response( + JSON.stringify(result), + { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + + } catch (error: any) { + console.error("Error sending email:", error); + return new Response( + JSON.stringify({ + success: false, + message: error.message || "Failed to send email" + }), + { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } +}); \ No newline at end of file diff --git a/test-edge-functions.sh b/test-edge-functions.sh new file mode 100755 index 0000000..4db4799 --- /dev/null +++ b/test-edge-functions.sh @@ -0,0 +1,143 @@ +#!/bin/bash + +# Test script for Supabase Edge Functions +# Make sure to set: export SUPABASE_ACCESS_TOKEN=your_service_role_key + +# Configuration +SUPABASE_URL="https://lovable.backoffice.biz.id" +SERVICE_ROLE_KEY="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTc2NjAzNzEyMCwiZXhwIjo0OTIxNzEwNzIwLCJyb2xlIjoic2VydmljZV9yb2xlIn0.t6D9VwaukYGq4c_VbW1bkd3ZkKgldpCKRR13nN14XXc" + +if [ -z "$SERVICE_ROLE_KEY" ]; then + echo "❌ Error: SUPABASE_ACCESS_TOKEN environment variable not set" + echo "Run: export SUPABASE_ACCESS_TOKEN=your_service_role_key" + exit 1 +fi + +echo "🚀 Testing Supabase Edge Functions" +echo "==================================" +echo "URL: $SUPABASE_URL" +echo "" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to test API +test_function() { + local func_name=$1 + local payload=$2 + local expected_status=${3:-200} + + echo -e "${YELLOW}Testing: $func_name${NC}" + echo "Payload: $payload" + + response=$(curl -s -w "%{http_code}" \ + -X POST "$SUPABASE_URL/rest/v1/functions" \ + -H "Authorization: Bearer $SERVICE_ROLE_KEY" \ + -H "apikey: $SERVICE_ROLE_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"name\": \"$func_name\", + \"verify_jwt\": false + }") + + # Extract status code (last 3 characters) + status_code="${response: -3}" + # Extract response body (everything except last 3 characters) + response_body="${response%???}" + + if [ "$status_code" = "200" ] || [ "$status_code" = "201" ]; then + echo -e "${GREEN}✅ Success: $func_name${NC}" + echo "Response: $response_body" + else + echo -e "${RED}❌ Error: $func_name (Status: $status_code)${NC}" + echo "Response: $response_body" + fi + echo "----------------------------------------" + echo "" +} + +# Test function invocation +test_invoke_function() { + local func_name=$1 + local payload=$2 + + echo -e "${YELLOW}Invoking: $func_name${NC}" + + response=$(curl -s -w "%{http_code}" \ + -X POST "$SUPABASE_URL/functions/v1/$func_name" \ + -H "Authorization: Bearer $SERVICE_ROLE_KEY" \ + -H "Content-Type: application/json" \ + -d "$payload") + + # Extract status code (last 3 characters) + status_code="${response: -3}" + # Extract response body (everything except last 3 characters) + response_body="${response%???}" + + if [ "$status_code" = "200" ]; then + echo -e "${GREEN}✅ Function executed successfully${NC}" + echo "Response: $response_body" + else + echo -e "${RED}❌ Function invocation failed (Status: $status_code)${NC}" + echo "Response: $response_body" + fi + echo "----------------------------------------" + echo "" +} + +# 1. Test creating/updating functions +echo "📋 Testing Function Creation/Update" +echo "" + +test_function "send-email-v2" +test_function "create-meet-link" +test_function "send-consultation-reminder" +test_function "daily-reminders" +test_function "pakasir-webhook" + +# 2. Test function invocations +echo "🔧 Testing Function Invocations" +echo "" + +# Test send-email-v2 (will fail without proper Mailketing token, but should reach the function) +test_invoke_function "send-email-v2" '{ + "to": "dwinx.ramz@gmail.com", + "api_token": "b4d1beb302282f411d196146f6806eab", + "from_name": "Test Hub", + "from_email": "with@dwindi.com", + "subject": "Test Email", + "html_body": "

Test Email

This is a test from send-email-v2

" +}' + +# Test create-meet-link (will fail without proper n8n webhook, but should reach the function) +# Note: Will use webhook path based on integration_n8n_test_mode setting +test_invoke_function "create-meet-link" '{ + "slot_id": "test-slot-123", + "date": "2025-12-23", + "start_time": "14:00:00", + "end_time": "15:00:00", + "client_name": "Test Client", + "client_email": "dewe.pw@gmail.com", + "topic": "Test Topic", + "notes": "Test notes" +}' + +# Test daily-reminders (should work with database queries) +test_invoke_function "daily-reminders" '{}' + +echo "✨ Testing Complete!" +echo "" +echo "📝 Notes:" +echo "- send-email-v2: Needs valid Mailketing API token to actually send emails" +echo "- create-meet-link: Needs n8n webhook configured in platform_settings" +echo " - Will use /webhook-test/ if integration_n8n_test_mode is enabled" +echo " - Will use /webhook/ if integration_n8n_test_mode is disabled" +echo "- Functions are deployed and accessible via the functions/v1/ endpoint" +echo "" +echo "🌐 Function URLs:" +echo "- https://lovable.backoffice.biz.id/functions/v1/send-email-v2" +echo "- https://lovable.backoffice.biz.id/functions/v1/create-meet-link" +echo "- https://lovable.backoffice.biz.id/functions/v1/daily-reminders" \ No newline at end of file