mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:11:13 -05:00
389 lines
12 KiB
TypeScript
389 lines
12 KiB
TypeScript
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> {
|
|
// Transform images object if present
|
|
if (data.images) {
|
|
const { uploaded, banner_assignment, card_assignment } = data.images;
|
|
|
|
if (uploaded && Array.isArray(uploaded)) {
|
|
// Assign banner image
|
|
if (banner_assignment !== undefined && uploaded[banner_assignment]) {
|
|
data.banner_image_id = uploaded[banner_assignment].cloudflare_id;
|
|
data.banner_image_url = uploaded[banner_assignment].url;
|
|
}
|
|
|
|
// Assign card image
|
|
if (card_assignment !== undefined && uploaded[card_assignment]) {
|
|
data.card_image_id = uploaded[card_assignment].cloudflare_id;
|
|
data.card_image_url = uploaded[card_assignment].url;
|
|
}
|
|
}
|
|
|
|
// Remove images object
|
|
delete data.images;
|
|
}
|
|
|
|
// Check if this is an edit (has park_id) or a new creation
|
|
if (data.park_id) {
|
|
console.log(`Updating existing park ${data.park_id}`);
|
|
const parkId = data.park_id;
|
|
delete data.park_id; // Remove ID from update data
|
|
|
|
const { error } = await supabase
|
|
.from('parks')
|
|
.update(data)
|
|
.eq('id', parkId);
|
|
|
|
if (error) throw new Error(`Failed to update park: ${error.message}`);
|
|
return parkId;
|
|
} else {
|
|
console.log('Creating new park');
|
|
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> {
|
|
// Transform images object if present
|
|
if (data.images) {
|
|
const { uploaded, banner_assignment, card_assignment } = data.images;
|
|
|
|
if (uploaded && Array.isArray(uploaded)) {
|
|
// Assign banner image
|
|
if (banner_assignment !== undefined && uploaded[banner_assignment]) {
|
|
data.banner_image_id = uploaded[banner_assignment].cloudflare_id;
|
|
data.banner_image_url = uploaded[banner_assignment].url;
|
|
}
|
|
|
|
// Assign card image
|
|
if (card_assignment !== undefined && uploaded[card_assignment]) {
|
|
data.card_image_id = uploaded[card_assignment].cloudflare_id;
|
|
data.card_image_url = uploaded[card_assignment].url;
|
|
}
|
|
}
|
|
|
|
// Remove images object
|
|
delete data.images;
|
|
}
|
|
|
|
// Check if this is an edit (has ride_id) or a new creation
|
|
if (data.ride_id) {
|
|
console.log(`Updating existing ride ${data.ride_id}`);
|
|
const rideId = data.ride_id;
|
|
delete data.ride_id; // Remove ID from update data
|
|
|
|
const { error } = await supabase
|
|
.from('rides')
|
|
.update(data)
|
|
.eq('id', rideId);
|
|
|
|
if (error) throw new Error(`Failed to update ride: ${error.message}`);
|
|
return rideId;
|
|
} else {
|
|
console.log('Creating new ride');
|
|
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> {
|
|
// Transform images object if present
|
|
if (data.images) {
|
|
const { uploaded, banner_assignment, card_assignment } = data.images;
|
|
|
|
if (uploaded && Array.isArray(uploaded)) {
|
|
// Assign banner image
|
|
if (banner_assignment !== undefined && uploaded[banner_assignment]) {
|
|
data.banner_image_id = uploaded[banner_assignment].cloudflare_id;
|
|
data.banner_image_url = uploaded[banner_assignment].url;
|
|
}
|
|
|
|
// Assign card image
|
|
if (card_assignment !== undefined && uploaded[card_assignment]) {
|
|
data.card_image_id = uploaded[card_assignment].cloudflare_id;
|
|
data.card_image_url = uploaded[card_assignment].url;
|
|
}
|
|
}
|
|
|
|
// Remove images object
|
|
delete data.images;
|
|
}
|
|
|
|
// Check if this is an edit (has company_id or id) or a new creation
|
|
const companyId = data.company_id || data.id;
|
|
|
|
if (companyId) {
|
|
console.log(`Updating existing company ${companyId}`);
|
|
const updateData = { ...data, company_type: companyType };
|
|
delete updateData.company_id;
|
|
delete updateData.id; // Remove ID from update data
|
|
|
|
const { error } = await supabase
|
|
.from('companies')
|
|
.update(updateData)
|
|
.eq('id', companyId);
|
|
|
|
if (error) throw new Error(`Failed to update company: ${error.message}`);
|
|
return companyId;
|
|
} else {
|
|
console.log('Creating new company');
|
|
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;
|
|
}
|