mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 10:11:13 -05:00
Add email secrets
This commit is contained in:
@@ -150,17 +150,41 @@ export function SubmissionReviewManager({
|
|||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
const { supabase } = await import('@/integrations/supabase/client');
|
||||||
const selectedItems = items.filter(item => selectedItemIds.has(item.id));
|
const selectedItems = items.filter(item => selectedItemIds.has(item.id));
|
||||||
await approveSubmissionItems(selectedItems, user.id);
|
|
||||||
|
|
||||||
|
// Call the edge function for backend processing
|
||||||
|
const { data, error } = await supabase.functions.invoke('process-selective-approval', {
|
||||||
|
body: {
|
||||||
|
itemIds: Array.from(selectedItemIds),
|
||||||
|
userId: user.id,
|
||||||
|
submissionId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message || 'Failed to process approval');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data?.success) {
|
||||||
|
throw new Error(data?.error || 'Approval processing failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const successCount = data.results.filter((r: any) => r.success).length;
|
||||||
|
const failCount = data.results.filter((r: any) => !r.success).length;
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Success',
|
title: 'Approval Complete',
|
||||||
description: `Approved ${selectedItems.length} item(s)`,
|
description: failCount > 0
|
||||||
|
? `Approved ${successCount} item(s), ${failCount} failed`
|
||||||
|
: `Successfully approved ${successCount} item(s)`,
|
||||||
|
variant: failCount > 0 ? 'destructive' : 'default',
|
||||||
});
|
});
|
||||||
|
|
||||||
onComplete();
|
onComplete();
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
console.error('Error approving items:', error);
|
||||||
toast({
|
toast({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
description: error.message || 'Failed to approve items',
|
description: error.message || 'Failed to approve items',
|
||||||
@@ -238,16 +262,37 @@ export function SubmissionReviewManager({
|
|||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await escalateSubmission(submissionId, reason, user.id);
|
const { supabase } = await import('@/integrations/supabase/client');
|
||||||
|
|
||||||
toast({
|
// Call the escalation notification edge function
|
||||||
title: 'Escalated',
|
const { data, error } = await supabase.functions.invoke('send-escalation-notification', {
|
||||||
description: 'Submission escalated to admin for review',
|
body: {
|
||||||
|
submissionId,
|
||||||
|
escalationReason: reason,
|
||||||
|
escalatedBy: user.id
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Edge function error:', error);
|
||||||
|
// Fallback to direct database update if email fails
|
||||||
|
await escalateSubmission(submissionId, reason, user.id);
|
||||||
|
toast({
|
||||||
|
title: 'Escalated (Email Failed)',
|
||||||
|
description: 'Submission escalated but notification email failed to send',
|
||||||
|
variant: 'default',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: 'Escalated Successfully',
|
||||||
|
description: 'Submission escalated and admin notified via email',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onComplete();
|
onComplete();
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
console.error('Error escalating submission:', error);
|
||||||
toast({
|
toast({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
description: error.message || 'Failed to escalate submission',
|
description: error.message || 'Failed to escalate submission',
|
||||||
|
|||||||
@@ -3,4 +3,11 @@ project_id = "ydvtmnrszybqnbcqbdcy"
|
|||||||
[functions.detect-location]
|
[functions.detect-location]
|
||||||
verify_jwt = false
|
verify_jwt = false
|
||||||
|
|
||||||
[functions.upload-image]
|
[functions.upload-image]
|
||||||
|
verify_jwt = false
|
||||||
|
|
||||||
|
[functions.process-selective-approval]
|
||||||
|
verify_jwt = true
|
||||||
|
|
||||||
|
[functions.send-escalation-notification]
|
||||||
|
verify_jwt = true
|
||||||
271
supabase/functions/process-selective-approval/index.ts
Normal file
271
supabase/functions/process-selective-approval/index.ts
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||||
|
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||||
|
|
||||||
|
const corsHeaders = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ApprovalRequest {
|
||||||
|
itemIds: string[];
|
||||||
|
userId: string;
|
||||||
|
submissionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
serve(async (req) => {
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
return new Response(null, { headers: corsHeaders });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const supabase = createClient(
|
||||||
|
Deno.env.get('SUPABASE_URL') ?? '',
|
||||||
|
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const { itemIds, userId, submissionId }: ApprovalRequest = await req.json();
|
||||||
|
|
||||||
|
console.log('Processing selective approval:', { itemIds, userId, submissionId });
|
||||||
|
|
||||||
|
// Fetch all items for the submission
|
||||||
|
const { data: items, error: fetchError } = await supabase
|
||||||
|
.from('submission_items')
|
||||||
|
.select('*')
|
||||||
|
.in('id', itemIds);
|
||||||
|
|
||||||
|
if (fetchError) {
|
||||||
|
throw new Error(`Failed to fetch items: ${fetchError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Topologically sort items by dependencies
|
||||||
|
const sortedItems = topologicalSort(items);
|
||||||
|
const dependencyMap = new Map<string, string>();
|
||||||
|
const approvalResults = [];
|
||||||
|
|
||||||
|
// Process items in order
|
||||||
|
for (const item of sortedItems) {
|
||||||
|
try {
|
||||||
|
console.log(`Processing item ${item.id} of type ${item.item_type}`);
|
||||||
|
|
||||||
|
// Resolve dependencies in item data
|
||||||
|
const resolvedData = resolveDependencies(item.item_data, dependencyMap);
|
||||||
|
|
||||||
|
let entityId: string | null = null;
|
||||||
|
|
||||||
|
// Create entity based on type
|
||||||
|
switch (item.item_type) {
|
||||||
|
case 'park':
|
||||||
|
entityId = await createPark(supabase, resolvedData);
|
||||||
|
break;
|
||||||
|
case 'ride':
|
||||||
|
entityId = await createRide(supabase, resolvedData);
|
||||||
|
break;
|
||||||
|
case 'manufacturer':
|
||||||
|
case 'operator':
|
||||||
|
case 'property_owner':
|
||||||
|
case 'designer':
|
||||||
|
entityId = await createCompany(supabase, resolvedData, item.item_type);
|
||||||
|
break;
|
||||||
|
case 'ride_model':
|
||||||
|
entityId = await createRideModel(supabase, resolvedData);
|
||||||
|
break;
|
||||||
|
case 'photo':
|
||||||
|
await approvePhotos(supabase, resolvedData, item.id);
|
||||||
|
entityId = item.id; // Use item ID as entity ID for photos
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown item type: ${item.item_type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entityId) {
|
||||||
|
dependencyMap.set(item.id, entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update item status
|
||||||
|
const { error: updateError } = await supabase
|
||||||
|
.from('submission_items')
|
||||||
|
.update({
|
||||||
|
status: 'approved',
|
||||||
|
approved_entity_id: entityId,
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
})
|
||||||
|
.eq('id', item.id);
|
||||||
|
|
||||||
|
if (updateError) {
|
||||||
|
throw new Error(`Failed to update item status: ${updateError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalResults.push({
|
||||||
|
itemId: item.id,
|
||||||
|
entityId,
|
||||||
|
itemType: item.item_type,
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Successfully approved item ${item.id} -> entity ${entityId}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error processing item ${item.id}:`, error);
|
||||||
|
approvalResults.push({
|
||||||
|
itemId: item.id,
|
||||||
|
itemType: item.item_type,
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update submission status
|
||||||
|
const allApproved = approvalResults.every(r => r.success);
|
||||||
|
const { error: submissionError } = await supabase
|
||||||
|
.from('content_submissions')
|
||||||
|
.update({
|
||||||
|
status: allApproved ? 'approved' : 'partially_approved',
|
||||||
|
reviewer_id: userId,
|
||||||
|
reviewed_at: new Date().toISOString()
|
||||||
|
})
|
||||||
|
.eq('id', submissionId);
|
||||||
|
|
||||||
|
if (submissionError) {
|
||||||
|
console.error('Failed to update submission status:', submissionError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
results: approvalResults,
|
||||||
|
submissionStatus: allApproved ? 'approved' : 'partially_approved'
|
||||||
|
}),
|
||||||
|
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in process-selective-approval:', error);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: error.message }),
|
||||||
|
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function topologicalSort(items: any[]): any[] {
|
||||||
|
const sorted: any[] = [];
|
||||||
|
const visited = new Set<string>();
|
||||||
|
const visiting = new Set<string>();
|
||||||
|
|
||||||
|
const visit = (item: any) => {
|
||||||
|
if (visited.has(item.id)) return;
|
||||||
|
if (visiting.has(item.id)) {
|
||||||
|
throw new Error(`Circular dependency detected for item ${item.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
visiting.add(item.id);
|
||||||
|
|
||||||
|
if (item.depends_on) {
|
||||||
|
const parent = items.find(i => i.id === item.depends_on);
|
||||||
|
if (parent) {
|
||||||
|
visit(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visiting.delete(item.id);
|
||||||
|
visited.add(item.id);
|
||||||
|
sorted.push(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
items.forEach(item => visit(item));
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDependencies(data: any, dependencyMap: Map<string, string>): any {
|
||||||
|
if (typeof data !== 'object' || data === null) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return data.map(item => resolveDependencies(item, dependencyMap));
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved: any = {};
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
if (typeof value === 'string' && dependencyMap.has(value)) {
|
||||||
|
resolved[key] = dependencyMap.get(value);
|
||||||
|
} else {
|
||||||
|
resolved[key] = resolveDependencies(value, dependencyMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createPark(supabase: any, data: any): Promise<string> {
|
||||||
|
const { data: park, error } = await supabase
|
||||||
|
.from('parks')
|
||||||
|
.insert(data)
|
||||||
|
.select('id')
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw new Error(`Failed to create park: ${error.message}`);
|
||||||
|
return park.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createRide(supabase: any, data: any): Promise<string> {
|
||||||
|
const { data: ride, error } = await supabase
|
||||||
|
.from('rides')
|
||||||
|
.insert(data)
|
||||||
|
.select('id')
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw new Error(`Failed to create ride: ${error.message}`);
|
||||||
|
return ride.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createCompany(supabase: any, data: any, companyType: string): Promise<string> {
|
||||||
|
const companyData = { ...data, company_type: companyType };
|
||||||
|
const { data: company, error } = await supabase
|
||||||
|
.from('companies')
|
||||||
|
.insert(companyData)
|
||||||
|
.select('id')
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw new Error(`Failed to create company: ${error.message}`);
|
||||||
|
return company.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createRideModel(supabase: any, data: any): Promise<string> {
|
||||||
|
const { data: model, error } = await supabase
|
||||||
|
.from('ride_models')
|
||||||
|
.insert(data)
|
||||||
|
.select('id')
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw new Error(`Failed to create ride model: ${error.message}`);
|
||||||
|
return model.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function approvePhotos(supabase: any, data: any, submissionItemId: string): Promise<void> {
|
||||||
|
const photos = data.photos || [];
|
||||||
|
|
||||||
|
for (const photo of photos) {
|
||||||
|
const photoData = {
|
||||||
|
entity_id: data.entity_id,
|
||||||
|
entity_type: data.context,
|
||||||
|
cloudflare_image_id: extractImageId(photo.url),
|
||||||
|
cloudflare_image_url: photo.url,
|
||||||
|
title: photo.title,
|
||||||
|
caption: photo.caption,
|
||||||
|
date_taken: photo.date,
|
||||||
|
order_index: photo.order,
|
||||||
|
submission_id: submissionItemId
|
||||||
|
};
|
||||||
|
|
||||||
|
const { error } = await supabase.from('photos').insert(photoData);
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to insert photo:', error);
|
||||||
|
throw new Error(`Failed to insert photo: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractImageId(url: string): string {
|
||||||
|
const matches = url.match(/\/([^\/]+)\/public$/);
|
||||||
|
return matches ? matches[1] : url;
|
||||||
|
}
|
||||||
222
supabase/functions/send-escalation-notification/index.ts
Normal file
222
supabase/functions/send-escalation-notification/index.ts
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||||
|
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||||
|
|
||||||
|
const corsHeaders = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface EscalationRequest {
|
||||||
|
submissionId: string;
|
||||||
|
escalationReason: string;
|
||||||
|
escalatedBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
serve(async (req) => {
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
return new Response(null, { headers: corsHeaders });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const supabase = createClient(
|
||||||
|
Deno.env.get('SUPABASE_URL') ?? '',
|
||||||
|
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const { submissionId, escalationReason, escalatedBy }: EscalationRequest = await req.json();
|
||||||
|
|
||||||
|
console.log('Processing escalation notification:', { submissionId, escalationReason, escalatedBy });
|
||||||
|
|
||||||
|
// Fetch submission details
|
||||||
|
const { data: submission, error: submissionError } = await supabase
|
||||||
|
.from('content_submissions')
|
||||||
|
.select('*, profiles:user_id(username, display_name, id)')
|
||||||
|
.eq('id', submissionId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (submissionError || !submission) {
|
||||||
|
throw new Error(`Failed to fetch submission: ${submissionError?.message || 'Not found'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch escalator details
|
||||||
|
const { data: escalator, error: escalatorError } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('username, display_name')
|
||||||
|
.eq('user_id', escalatedBy)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (escalatorError) {
|
||||||
|
console.error('Failed to fetch escalator profile:', escalatorError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch submission items count
|
||||||
|
const { count: itemsCount, error: countError } = await supabase
|
||||||
|
.from('submission_items')
|
||||||
|
.select('*', { count: 'exact', head: true })
|
||||||
|
.eq('submission_id', submissionId);
|
||||||
|
|
||||||
|
if (countError) {
|
||||||
|
console.error('Failed to fetch items count:', countError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare email content
|
||||||
|
const escalatorName = escalator?.display_name || escalator?.username || 'Unknown User';
|
||||||
|
const submitterName = submission.profiles?.display_name || submission.profiles?.username || 'Unknown User';
|
||||||
|
const submissionType = submission.submission_type || 'Unknown';
|
||||||
|
const itemCount = itemsCount || 0;
|
||||||
|
|
||||||
|
const emailSubject = `🚨 Submission Escalated: ${submissionType} - ID: ${submissionId.substring(0, 8)}`;
|
||||||
|
|
||||||
|
const emailHtml = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||||
|
.header { background-color: #ef4444; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
|
||||||
|
.content { background-color: #f9fafb; padding: 20px; border: 1px solid #e5e7eb; }
|
||||||
|
.info-row { margin: 10px 0; padding: 10px; background: white; border-radius: 4px; }
|
||||||
|
.label { font-weight: bold; color: #374151; }
|
||||||
|
.value { color: #1f2937; }
|
||||||
|
.reason { background-color: #fef3c7; padding: 15px; border-left: 4px solid #f59e0b; margin: 15px 0; }
|
||||||
|
.footer { text-align: center; padding: 20px; color: #6b7280; font-size: 14px; }
|
||||||
|
.button { display: inline-block; padding: 12px 24px; background-color: #3b82f6; color: white; text-decoration: none; border-radius: 6px; margin: 15px 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1 style="margin: 0;">⚠️ Submission Escalated</h1>
|
||||||
|
<p style="margin: 5px 0 0 0;">Admin review required</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">Submission ID:</span>
|
||||||
|
<span class="value">${submissionId}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">Submission Type:</span>
|
||||||
|
<span class="value">${submissionType}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">Items Count:</span>
|
||||||
|
<span class="value">${itemCount}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">Submitted By:</span>
|
||||||
|
<span class="value">${submitterName}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">Escalated By:</span>
|
||||||
|
<span class="value">${escalatorName}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reason">
|
||||||
|
<strong>📝 Escalation Reason:</strong>
|
||||||
|
<p style="margin: 10px 0 0 0;">${escalationReason}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="${Deno.env.get('SUPABASE_URL')?.replace('.supabase.co', '.lovable.app')}/admin" class="button">
|
||||||
|
Review Submission →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>This is an automated notification from your moderation system.</p>
|
||||||
|
<p>Please review and take appropriate action.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const emailText = `
|
||||||
|
SUBMISSION ESCALATED - Admin Review Required
|
||||||
|
|
||||||
|
Submission ID: ${submissionId}
|
||||||
|
Submission Type: ${submissionType}
|
||||||
|
Items Count: ${itemCount}
|
||||||
|
Submitted By: ${submitterName}
|
||||||
|
Escalated By: ${escalatorName}
|
||||||
|
|
||||||
|
Escalation Reason:
|
||||||
|
${escalationReason}
|
||||||
|
|
||||||
|
Please review this submission in the admin panel.
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Send email via ForwardEmail API
|
||||||
|
const forwardEmailApiKey = Deno.env.get('FORWARDEMAIL_API_KEY');
|
||||||
|
const adminEmail = Deno.env.get('ADMIN_EMAIL_ADDRESS');
|
||||||
|
const fromEmail = Deno.env.get('FROM_EMAIL_ADDRESS');
|
||||||
|
|
||||||
|
if (!forwardEmailApiKey || !adminEmail || !fromEmail) {
|
||||||
|
throw new Error('Email configuration is incomplete. Please check environment variables.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailResponse = await fetch('https://api.forwardemail.net/v1/emails', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${forwardEmailApiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
from: fromEmail,
|
||||||
|
to: adminEmail,
|
||||||
|
subject: emailSubject,
|
||||||
|
html: emailHtml,
|
||||||
|
text: emailText,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!emailResponse.ok) {
|
||||||
|
const errorText = await emailResponse.text();
|
||||||
|
console.error('ForwardEmail API error:', errorText);
|
||||||
|
throw new Error(`Failed to send email: ${emailResponse.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailResult = await emailResponse.json();
|
||||||
|
console.log('Email sent successfully:', emailResult);
|
||||||
|
|
||||||
|
// Update submission with notification status
|
||||||
|
const { error: updateError } = await supabase
|
||||||
|
.from('content_submissions')
|
||||||
|
.update({
|
||||||
|
escalated: true,
|
||||||
|
escalated_at: new Date().toISOString(),
|
||||||
|
escalated_by: escalatedBy,
|
||||||
|
escalation_reason: escalationReason
|
||||||
|
})
|
||||||
|
.eq('id', submissionId);
|
||||||
|
|
||||||
|
if (updateError) {
|
||||||
|
console.error('Failed to update submission escalation status:', updateError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'Escalation notification sent successfully',
|
||||||
|
emailId: emailResult.id
|
||||||
|
}),
|
||||||
|
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in send-escalation-notification:', error);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: error.message,
|
||||||
|
details: 'Failed to send escalation notification'
|
||||||
|
}),
|
||||||
|
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user