mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 14:11:12 -05:00
Reverted to commit 026a4e9362
This commit is contained in:
@@ -8,7 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|||||||
interface EscalationDialogProps {
|
interface EscalationDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onEscalate: (reason: string, additionalNotes?: string) => void;
|
onEscalate: (reason: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const escalationReasons = [
|
const escalationReasons = [
|
||||||
@@ -31,9 +31,9 @@ export function EscalationDialog({
|
|||||||
const handleEscalate = () => {
|
const handleEscalate = () => {
|
||||||
const reason = selectedReason === 'Other'
|
const reason = selectedReason === 'Other'
|
||||||
? additionalNotes
|
? additionalNotes
|
||||||
: selectedReason;
|
: `${selectedReason}${additionalNotes ? ': ' + additionalNotes : ''}`;
|
||||||
|
|
||||||
onEscalate(reason, selectedReason !== 'Other' ? additionalNotes : undefined);
|
onEscalate(reason);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
|
|
||||||
// Reset form
|
// Reset form
|
||||||
|
|||||||
@@ -150,38 +150,13 @@ export function SubmissionReviewManager({
|
|||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// Use edge function for complex approval processing
|
const selectedItems = items.filter(item => selectedItemIds.has(item.id));
|
||||||
const { supabase } = await import('@/integrations/supabase/client');
|
await approveSubmissionItems(selectedItems, user.id);
|
||||||
|
|
||||||
const { data, error } = await supabase.functions.invoke('process-selective-approval', {
|
|
||||||
body: {
|
|
||||||
submissionId,
|
|
||||||
selectedItemIds: Array.from(selectedItemIds),
|
|
||||||
moderatorId: user.id,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
const result = data as {
|
|
||||||
success: boolean;
|
|
||||||
processedItems: any[];
|
|
||||||
successCount: number;
|
|
||||||
failureCount: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (result.failureCount > 0) {
|
|
||||||
toast({
|
|
||||||
title: 'Partial Success',
|
|
||||||
description: `Approved ${result.successCount} of ${result.processedItems.length} item(s). ${result.failureCount} failed.`,
|
|
||||||
variant: 'default',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
description: `Approved ${result.successCount} item(s)`,
|
description: `Approved ${selectedItems.length} item(s)`,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
onComplete();
|
onComplete();
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
@@ -251,7 +226,7 @@ export function SubmissionReviewManager({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEscalate = async (reason: string, additionalNotes?: string) => {
|
const handleEscalate = async (reason: string) => {
|
||||||
if (!user?.id) {
|
if (!user?.id) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Authentication Required',
|
title: 'Authentication Required',
|
||||||
@@ -263,35 +238,12 @@ export function SubmissionReviewManager({
|
|||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// Update submission escalation status
|
|
||||||
await escalateSubmission(submissionId, reason, user.id);
|
await escalateSubmission(submissionId, reason, user.id);
|
||||||
|
|
||||||
// Send email notifications via edge function
|
|
||||||
const { supabase } = await import('@/integrations/supabase/client');
|
|
||||||
|
|
||||||
const { data, error: emailError } = await supabase.functions.invoke('send-escalation-notification', {
|
|
||||||
body: {
|
|
||||||
submissionId,
|
|
||||||
escalationReason: reason,
|
|
||||||
escalatedBy: user.id,
|
|
||||||
additionalNotes,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (emailError) {
|
|
||||||
console.error('Failed to send escalation emails:', emailError);
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Escalated',
|
title: 'Escalated',
|
||||||
description: 'Submission escalated but email notifications failed',
|
description: 'Submission escalated to admin for review',
|
||||||
variant: 'default',
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
const result = data as { emailsSent: number; totalRecipients: number };
|
|
||||||
toast({
|
|
||||||
title: 'Escalated',
|
|
||||||
description: `Submission escalated to admin. ${result.emailsSent} of ${result.totalRecipients} admin(s) notified.`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onComplete();
|
onComplete();
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
|
|||||||
@@ -130,8 +130,6 @@ export type Database = {
|
|||||||
approval_mode: string | null
|
approval_mode: string | null
|
||||||
content: Json
|
content: Json
|
||||||
created_at: string
|
created_at: string
|
||||||
escalated: boolean | null
|
|
||||||
escalated_at: string | null
|
|
||||||
escalated_by: string | null
|
escalated_by: string | null
|
||||||
escalation_reason: string | null
|
escalation_reason: string | null
|
||||||
id: string
|
id: string
|
||||||
@@ -148,8 +146,6 @@ export type Database = {
|
|||||||
approval_mode?: string | null
|
approval_mode?: string | null
|
||||||
content: Json
|
content: Json
|
||||||
created_at?: string
|
created_at?: string
|
||||||
escalated?: boolean | null
|
|
||||||
escalated_at?: string | null
|
|
||||||
escalated_by?: string | null
|
escalated_by?: string | null
|
||||||
escalation_reason?: string | null
|
escalation_reason?: string | null
|
||||||
id?: string
|
id?: string
|
||||||
@@ -166,8 +162,6 @@ export type Database = {
|
|||||||
approval_mode?: string | null
|
approval_mode?: string | null
|
||||||
content?: Json
|
content?: Json
|
||||||
created_at?: string
|
created_at?: string
|
||||||
escalated?: boolean | null
|
|
||||||
escalated_at?: string | null
|
|
||||||
escalated_by?: string | null
|
escalated_by?: string | null
|
||||||
escalation_reason?: string | null
|
escalation_reason?: string | null
|
||||||
id?: string
|
id?: string
|
||||||
@@ -190,36 +184,6 @@ export type Database = {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
email_aliases: {
|
|
||||||
Row: {
|
|
||||||
created_at: string
|
|
||||||
description: string | null
|
|
||||||
email: string
|
|
||||||
id: string
|
|
||||||
key: string
|
|
||||||
owner_id: string | null
|
|
||||||
updated_at: string
|
|
||||||
}
|
|
||||||
Insert: {
|
|
||||||
created_at?: string
|
|
||||||
description?: string | null
|
|
||||||
email: string
|
|
||||||
id?: string
|
|
||||||
key: string
|
|
||||||
owner_id?: string | null
|
|
||||||
updated_at?: string
|
|
||||||
}
|
|
||||||
Update: {
|
|
||||||
created_at?: string
|
|
||||||
description?: string | null
|
|
||||||
email?: string
|
|
||||||
id?: string
|
|
||||||
key?: string
|
|
||||||
owner_id?: string | null
|
|
||||||
updated_at?: string
|
|
||||||
}
|
|
||||||
Relationships: []
|
|
||||||
}
|
|
||||||
locations: {
|
locations: {
|
||||||
Row: {
|
Row: {
|
||||||
city: string | null
|
city: string | null
|
||||||
|
|||||||
@@ -4,7 +4,3 @@ project_id = "ydvtmnrszybqnbcqbdcy"
|
|||||||
verify_jwt = false
|
verify_jwt = false
|
||||||
|
|
||||||
[functions.upload-image]
|
[functions.upload-image]
|
||||||
|
|
||||||
[functions.process-selective-approval]
|
|
||||||
|
|
||||||
[functions.send-escalation-notification]
|
|
||||||
@@ -1,319 +0,0 @@
|
|||||||
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 ProcessApprovalRequest {
|
|
||||||
submissionId: string;
|
|
||||||
selectedItemIds: string[];
|
|
||||||
moderatorId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProcessedItem {
|
|
||||||
id: string;
|
|
||||||
entityType: string;
|
|
||||||
entityId: string | null;
|
|
||||||
status: 'success' | 'failed';
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
serve(async (req) => {
|
|
||||||
if (req.method === 'OPTIONS') {
|
|
||||||
return new Response(null, { headers: corsHeaders });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const supabaseClient = createClient(
|
|
||||||
Deno.env.get('SUPABASE_URL') ?? '',
|
|
||||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '',
|
|
||||||
{
|
|
||||||
auth: {
|
|
||||||
autoRefreshToken: false,
|
|
||||||
persistSession: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const authHeader = req.headers.get('Authorization');
|
|
||||||
if (!authHeader) {
|
|
||||||
throw new Error('No authorization header');
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = authHeader.replace('Bearer ', '');
|
|
||||||
const { data: { user }, error: authError } = await supabaseClient.auth.getUser(token);
|
|
||||||
|
|
||||||
if (authError || !user) {
|
|
||||||
throw new Error('Unauthorized');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify user is moderator or admin
|
|
||||||
const { data: userRoles } = await supabaseClient
|
|
||||||
.from('user_roles')
|
|
||||||
.select('role')
|
|
||||||
.eq('user_id', user.id);
|
|
||||||
|
|
||||||
const isModerator = userRoles?.some(r =>
|
|
||||||
['moderator', 'admin', 'superuser'].includes(r.role)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isModerator) {
|
|
||||||
throw new Error('Insufficient permissions');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { submissionId, selectedItemIds, moderatorId }: ProcessApprovalRequest = await req.json();
|
|
||||||
|
|
||||||
console.log('Processing selective approval:', { submissionId, selectedItemIds, moderatorId });
|
|
||||||
|
|
||||||
// Fetch all selected items with their dependencies
|
|
||||||
const { data: items, error: itemsError } = await supabaseClient
|
|
||||||
.from('submission_items')
|
|
||||||
.select('*')
|
|
||||||
.in('id', selectedItemIds)
|
|
||||||
.eq('submission_id', submissionId);
|
|
||||||
|
|
||||||
if (itemsError) {
|
|
||||||
throw new Error(`Failed to fetch items: ${itemsError.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Topological sort to process items in correct order
|
|
||||||
const sortedItems = topologicalSort(items || []);
|
|
||||||
const processedItems: ProcessedItem[] = [];
|
|
||||||
const entityMap = new Map<string, string>(); // temp_id -> real_id mapping
|
|
||||||
|
|
||||||
// Process each item in dependency order
|
|
||||||
for (const item of sortedItems) {
|
|
||||||
try {
|
|
||||||
let entityId: string | null = null;
|
|
||||||
|
|
||||||
// Resolve parent dependency if exists
|
|
||||||
if (item.depends_on && entityMap.has(item.depends_on)) {
|
|
||||||
item.item_data.parent_id = entityMap.get(item.depends_on);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the entity based on type
|
|
||||||
switch (item.item_type) {
|
|
||||||
case 'manufacturer':
|
|
||||||
const { data: manufacturer, error: mfgError } = await supabaseClient
|
|
||||||
.from('companies')
|
|
||||||
.insert({
|
|
||||||
name: item.item_data.name,
|
|
||||||
slug: item.item_data.slug,
|
|
||||||
company_type: 'MANUFACTURER',
|
|
||||||
description: item.item_data.description,
|
|
||||||
founded_year: item.item_data.founded_year,
|
|
||||||
headquarters_location: item.item_data.headquarters_location,
|
|
||||||
website_url: item.item_data.website_url,
|
|
||||||
})
|
|
||||||
.select('id')
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (mfgError) throw mfgError;
|
|
||||||
entityId = manufacturer.id;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'designer':
|
|
||||||
const { data: designer, error: designerError } = await supabaseClient
|
|
||||||
.from('companies')
|
|
||||||
.insert({
|
|
||||||
name: item.item_data.name,
|
|
||||||
slug: item.item_data.slug,
|
|
||||||
company_type: 'DESIGNER',
|
|
||||||
description: item.item_data.description,
|
|
||||||
founded_year: item.item_data.founded_year,
|
|
||||||
headquarters_location: item.item_data.headquarters_location,
|
|
||||||
website_url: item.item_data.website_url,
|
|
||||||
})
|
|
||||||
.select('id')
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (designerError) throw designerError;
|
|
||||||
entityId = designer.id;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'ride_model':
|
|
||||||
const { data: model, error: modelError } = await supabaseClient
|
|
||||||
.from('ride_models')
|
|
||||||
.insert({
|
|
||||||
name: item.item_data.name,
|
|
||||||
slug: item.item_data.slug,
|
|
||||||
manufacturer_id: item.item_data.manufacturer_id || item.item_data.parent_id,
|
|
||||||
description: item.item_data.description,
|
|
||||||
model_type: item.item_data.model_type,
|
|
||||||
})
|
|
||||||
.select('id')
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (modelError) throw modelError;
|
|
||||||
entityId = model.id;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'ride':
|
|
||||||
const { data: ride, error: rideError } = await supabaseClient
|
|
||||||
.from('rides')
|
|
||||||
.insert({
|
|
||||||
name: item.item_data.name,
|
|
||||||
slug: item.item_data.slug,
|
|
||||||
park_id: item.item_data.park_id,
|
|
||||||
manufacturer_id: item.item_data.manufacturer_id,
|
|
||||||
designer_id: item.item_data.designer_id,
|
|
||||||
model_id: item.item_data.model_id || item.item_data.parent_id,
|
|
||||||
ride_type: item.item_data.ride_type,
|
|
||||||
status: item.item_data.status,
|
|
||||||
opening_date: item.item_data.opening_date,
|
|
||||||
closing_date: item.item_data.closing_date,
|
|
||||||
description: item.item_data.description,
|
|
||||||
})
|
|
||||||
.select('id')
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (rideError) throw rideError;
|
|
||||||
entityId = ride.id;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'park':
|
|
||||||
const { data: park, error: parkError } = await supabaseClient
|
|
||||||
.from('parks')
|
|
||||||
.insert({
|
|
||||||
name: item.item_data.name,
|
|
||||||
slug: item.item_data.slug,
|
|
||||||
operator_id: item.item_data.operator_id,
|
|
||||||
property_owner_id: item.item_data.property_owner_id,
|
|
||||||
location_id: item.item_data.location_id,
|
|
||||||
status: item.item_data.status,
|
|
||||||
opening_date: item.item_data.opening_date,
|
|
||||||
closing_date: item.item_data.closing_date,
|
|
||||||
description: item.item_data.description,
|
|
||||||
website_url: item.item_data.website_url,
|
|
||||||
})
|
|
||||||
.select('id')
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (parkError) throw parkError;
|
|
||||||
entityId = park.id;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown item type: ${item.item_type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update submission item status
|
|
||||||
await supabaseClient
|
|
||||||
.from('submission_items')
|
|
||||||
.update({
|
|
||||||
status: 'approved',
|
|
||||||
entity_id: entityId,
|
|
||||||
reviewed_by: moderatorId,
|
|
||||||
reviewed_at: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
.eq('id', item.id);
|
|
||||||
|
|
||||||
// Store mapping for dependent items
|
|
||||||
entityMap.set(item.id, entityId!);
|
|
||||||
|
|
||||||
processedItems.push({
|
|
||||||
id: item.id,
|
|
||||||
entityType: item.item_type,
|
|
||||||
entityId,
|
|
||||||
status: 'success',
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Successfully processed ${item.item_type} ${item.id} -> ${entityId}`);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`Failed to process item ${item.id}:`, error);
|
|
||||||
|
|
||||||
// Mark item as failed
|
|
||||||
await supabaseClient
|
|
||||||
.from('submission_items')
|
|
||||||
.update({
|
|
||||||
status: 'rejected',
|
|
||||||
rejection_reason: error.message,
|
|
||||||
reviewed_by: moderatorId,
|
|
||||||
reviewed_at: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
.eq('id', item.id);
|
|
||||||
|
|
||||||
processedItems.push({
|
|
||||||
id: item.id,
|
|
||||||
entityType: item.item_type,
|
|
||||||
entityId: null,
|
|
||||||
status: 'failed',
|
|
||||||
error: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update submission status if all items are processed
|
|
||||||
const { data: remainingItems } = await supabaseClient
|
|
||||||
.from('submission_items')
|
|
||||||
.select('id')
|
|
||||||
.eq('submission_id', submissionId)
|
|
||||||
.eq('status', 'pending');
|
|
||||||
|
|
||||||
if (!remainingItems || remainingItems.length === 0) {
|
|
||||||
await supabaseClient
|
|
||||||
.from('content_submissions')
|
|
||||||
.update({
|
|
||||||
status: 'approved',
|
|
||||||
reviewed_at: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
.eq('id', submissionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
success: true,
|
|
||||||
processedItems,
|
|
||||||
totalProcessed: processedItems.length,
|
|
||||||
successCount: processedItems.filter(p => p.status === 'success').length,
|
|
||||||
failureCount: processedItems.filter(p => p.status === 'failed').length,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
||||||
status: 200,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error in process-selective-approval:', error);
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({ error: error.message }),
|
|
||||||
{
|
|
||||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
||||||
status: 500,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Topological sort helper
|
|
||||||
function topologicalSort(items: any[]): any[] {
|
|
||||||
const sorted: any[] = [];
|
|
||||||
const visited = new Set<string>();
|
|
||||||
const visiting = new Set<string>();
|
|
||||||
|
|
||||||
function visit(item: any) {
|
|
||||||
if (visited.has(item.id)) return;
|
|
||||||
if (visiting.has(item.id)) {
|
|
||||||
throw new Error('Circular dependency detected');
|
|
||||||
}
|
|
||||||
|
|
||||||
visiting.add(item.id);
|
|
||||||
|
|
||||||
// Visit dependencies first
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
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;
|
|
||||||
additionalNotes?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
serve(async (req) => {
|
|
||||||
if (req.method === 'OPTIONS') {
|
|
||||||
return new Response(null, { headers: corsHeaders });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const supabaseClient = createClient(
|
|
||||||
Deno.env.get('SUPABASE_URL') ?? '',
|
|
||||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '',
|
|
||||||
{
|
|
||||||
auth: {
|
|
||||||
autoRefreshToken: false,
|
|
||||||
persistSession: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { submissionId, escalationReason, escalatedBy, additionalNotes }: EscalationRequest = await req.json();
|
|
||||||
|
|
||||||
console.log('Processing escalation notification:', { submissionId, escalationReason, escalatedBy });
|
|
||||||
|
|
||||||
// Get submission details
|
|
||||||
const { data: submission, error: submissionError } = await supabaseClient
|
|
||||||
.from('content_submissions')
|
|
||||||
.select(`
|
|
||||||
*,
|
|
||||||
submitter:profiles!content_submissions_user_id_fkey(username, display_name)
|
|
||||||
`)
|
|
||||||
.eq('id', submissionId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (submissionError || !submission) {
|
|
||||||
throw new Error('Submission not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get escalated by user details
|
|
||||||
const { data: escalator, error: escalatorError } = await supabaseClient
|
|
||||||
.from('profiles')
|
|
||||||
.select('username, display_name')
|
|
||||||
.eq('user_id', escalatedBy)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
// Get all admin/superuser emails
|
|
||||||
const { data: adminUsers, error: adminError } = await supabaseClient
|
|
||||||
.from('user_roles')
|
|
||||||
.select('user_id, profiles!inner(user_id, display_name, username)')
|
|
||||||
.in('role', ['admin', 'superuser']);
|
|
||||||
|
|
||||||
if (adminError || !adminUsers || adminUsers.length === 0) {
|
|
||||||
throw new Error('No admin users found to notify');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get email addresses for admins
|
|
||||||
const adminUserIds = adminUsers.map(u => u.user_id);
|
|
||||||
const { data: authUsers, error: authError } = await supabaseClient.auth.admin.listUsers();
|
|
||||||
|
|
||||||
if (authError) {
|
|
||||||
throw new Error('Failed to fetch admin emails');
|
|
||||||
}
|
|
||||||
|
|
||||||
const adminEmails = authUsers.users
|
|
||||||
.filter(u => adminUserIds.includes(u.id))
|
|
||||||
.map(u => u.email)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
if (adminEmails.length === 0) {
|
|
||||||
throw new Error('No admin emails found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get submission items for context
|
|
||||||
const { data: items } = await supabaseClient
|
|
||||||
.from('submission_items')
|
|
||||||
.select('item_type, item_data')
|
|
||||||
.eq('submission_id', submissionId);
|
|
||||||
|
|
||||||
const itemsSummary = items?.map(item =>
|
|
||||||
`${item.item_type}: ${item.item_data.name || 'Unnamed'}`
|
|
||||||
).join(', ') || 'No items';
|
|
||||||
|
|
||||||
// Prepare email content
|
|
||||||
const emailSubject = `🚨 Escalated Submission: ${submission.title || submissionId}`;
|
|
||||||
const emailHtml = `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<style>
|
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
||||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
||||||
.header { background: #ef4444; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
|
|
||||||
.content { background: #f9fafb; padding: 30px; border: 1px solid #e5e7eb; border-top: none; }
|
|
||||||
.section { margin-bottom: 20px; }
|
|
||||||
.label { font-weight: 600; color: #6b7280; margin-bottom: 5px; }
|
|
||||||
.value { background: white; padding: 10px; border-radius: 4px; border: 1px solid #e5e7eb; }
|
|
||||||
.reason { background: #fef2f2; padding: 15px; border-left: 4px solid #ef4444; margin: 15px 0; }
|
|
||||||
.button { display: inline-block; background: #3b82f6; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin-top: 20px; }
|
|
||||||
.footer { text-align: center; color: #6b7280; font-size: 12px; margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="header">
|
|
||||||
<h1 style="margin: 0; font-size: 24px;">⚠️ Submission Escalated</h1>
|
|
||||||
<p style="margin: 10px 0 0 0; opacity: 0.9;">Immediate attention required</p>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<div class="section">
|
|
||||||
<div class="label">Submission Title</div>
|
|
||||||
<div class="value">${submission.title || 'Untitled Submission'}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<div class="label">Submitted By</div>
|
|
||||||
<div class="value">${submission.submitter?.display_name || submission.submitter?.username || 'Unknown'}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<div class="label">Items in Submission</div>
|
|
||||||
<div class="value">${itemsSummary}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="reason">
|
|
||||||
<div class="label">Escalation Reason</div>
|
|
||||||
<div style="margin-top: 10px; font-size: 15px;">${escalationReason}</div>
|
|
||||||
${additionalNotes ? `<div style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #fca5a5;"><strong>Additional Notes:</strong><br>${additionalNotes}</div>` : ''}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<div class="label">Escalated By</div>
|
|
||||||
<div class="value">${escalator?.display_name || escalator?.username || 'System'}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<div class="label">Submission ID</div>
|
|
||||||
<div class="value" style="font-family: monospace; font-size: 12px;">${submissionId}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a href="${Deno.env.get('SUPABASE_URL')?.replace('supabase.co', 'lovableproject.com')}/admin?tab=moderation&submission=${submissionId}" class="button">
|
|
||||||
Review Submission →
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
<p>This is an automated notification from the Moderation System</p>
|
|
||||||
<p>To adjust your notification preferences, visit your admin settings</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const emailText = `
|
|
||||||
ESCALATED SUBMISSION - IMMEDIATE ATTENTION REQUIRED
|
|
||||||
|
|
||||||
Submission: ${submission.title || 'Untitled'}
|
|
||||||
Submitted By: ${submission.submitter?.display_name || submission.submitter?.username || 'Unknown'}
|
|
||||||
|
|
||||||
Items: ${itemsSummary}
|
|
||||||
|
|
||||||
ESCALATION REASON:
|
|
||||||
${escalationReason}
|
|
||||||
|
|
||||||
${additionalNotes ? `Additional Notes:\n${additionalNotes}\n` : ''}
|
|
||||||
|
|
||||||
Escalated By: ${escalator?.display_name || escalator?.username || 'System'}
|
|
||||||
|
|
||||||
Submission ID: ${submissionId}
|
|
||||||
|
|
||||||
Review at: ${Deno.env.get('SUPABASE_URL')?.replace('supabase.co', 'lovableproject.com')}/admin?tab=moderation&submission=${submissionId}
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
// Send emails to all admins using forwardemail_proxy
|
|
||||||
const emailResults = [];
|
|
||||||
|
|
||||||
for (const adminEmail of adminEmails) {
|
|
||||||
try {
|
|
||||||
const { data: emailData, error: emailError } = await supabaseClient.functions.invoke('forwardemail_proxy', {
|
|
||||||
body: {
|
|
||||||
to: adminEmail,
|
|
||||||
subject: emailSubject,
|
|
||||||
html: emailHtml,
|
|
||||||
text: emailText,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (emailError) {
|
|
||||||
console.error(`Failed to send email to ${adminEmail}:`, emailError);
|
|
||||||
emailResults.push({ email: adminEmail, status: 'failed', error: emailError.message });
|
|
||||||
} else {
|
|
||||||
console.log(`Email sent successfully to ${adminEmail}`);
|
|
||||||
emailResults.push({ email: adminEmail, status: 'sent' });
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`Exception sending email to ${adminEmail}:`, error);
|
|
||||||
emailResults.push({ email: adminEmail, status: 'failed', error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log escalation
|
|
||||||
await supabaseClient
|
|
||||||
.from('content_submissions')
|
|
||||||
.update({
|
|
||||||
escalated: true,
|
|
||||||
escalation_reason: escalationReason,
|
|
||||||
escalated_at: new Date().toISOString(),
|
|
||||||
escalated_by: escalatedBy,
|
|
||||||
})
|
|
||||||
.eq('id', submissionId);
|
|
||||||
|
|
||||||
const successCount = emailResults.filter(r => r.status === 'sent').length;
|
|
||||||
const failureCount = emailResults.filter(r => r.status === 'failed').length;
|
|
||||||
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
success: true,
|
|
||||||
emailsSent: successCount,
|
|
||||||
totalRecipients: adminEmails.length,
|
|
||||||
failures: failureCount,
|
|
||||||
results: emailResults,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
||||||
status: 200,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error in send-escalation-notification:', error);
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({ error: error.message }),
|
|
||||||
{
|
|
||||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
||||||
status: 500,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
-- Add escalation tracking fields to content_submissions table
|
|
||||||
ALTER TABLE public.content_submissions
|
|
||||||
ADD COLUMN IF NOT EXISTS escalated BOOLEAN DEFAULT FALSE,
|
|
||||||
ADD COLUMN IF NOT EXISTS escalation_reason TEXT,
|
|
||||||
ADD COLUMN IF NOT EXISTS escalated_at TIMESTAMP WITH TIME ZONE,
|
|
||||||
ADD COLUMN IF NOT EXISTS escalated_by UUID REFERENCES auth.users(id);
|
|
||||||
|
|
||||||
-- Add index for escalated submissions
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_content_submissions_escalated
|
|
||||||
ON public.content_submissions(escalated)
|
|
||||||
WHERE escalated = TRUE;
|
|
||||||
|
|
||||||
-- Add index for escalated_by
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_content_submissions_escalated_by
|
|
||||||
ON public.content_submissions(escalated_by);
|
|
||||||
|
|
||||||
-- Add comments for documentation
|
|
||||||
COMMENT ON COLUMN public.content_submissions.escalated IS 'Whether this submission has been escalated to admin';
|
|
||||||
COMMENT ON COLUMN public.content_submissions.escalation_reason IS 'Reason for escalation';
|
|
||||||
COMMENT ON COLUMN public.content_submissions.escalated_at IS 'Timestamp when submission was escalated';
|
|
||||||
COMMENT ON COLUMN public.content_submissions.escalated_by IS 'User who escalated the submission';
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
-- The content_submissions table already has RLS policies defined
|
|
||||||
-- We just need to ensure RLS is enabled (it should already be enabled from previous migrations)
|
|
||||||
-- This migration verifies RLS is enabled
|
|
||||||
|
|
||||||
-- Verify RLS is enabled on content_submissions
|
|
||||||
ALTER TABLE public.content_submissions ENABLE ROW LEVEL SECURITY;
|
|
||||||
|
|
||||||
-- Verify all existing RLS policies are in place
|
|
||||||
-- (They should already exist from previous migrations, but we document them here)
|
|
||||||
|
|
||||||
-- Users can create submissions (already exists)
|
|
||||||
-- Users can view their own submissions (already exists)
|
|
||||||
-- Moderators can view all content submissions (already exists)
|
|
||||||
-- Moderators can update content submissions (already exists)
|
|
||||||
-- Moderators can delete content submissions (already exists)
|
|
||||||
Reference in New Issue
Block a user