diff --git a/src/components/moderation/ItemReviewCard.tsx b/src/components/moderation/ItemReviewCard.tsx index 2dcd0a3b..8d42d41b 100644 --- a/src/components/moderation/ItemReviewCard.tsx +++ b/src/components/moderation/ItemReviewCard.tsx @@ -127,8 +127,7 @@ export function ItemReviewCard({ item, onEdit, onStatusChange, submissionId }: I { const { data, error } = await supabase .from('submission_items') - .select('*') + .select(` + *, + park_submission:park_submissions!item_data_id(*), + ride_submission:ride_submissions!item_data_id(*), + photo_submission:photo_submissions!item_data_id( + *, + photo_items:photo_submission_items(*) + ) + `) .eq('submission_id', submissionId) .order('order_index', { ascending: true }); if (error) throw error; - // Cast the data to the correct type - return (data || []).map(item => ({ - ...item, - status: item.status as 'pending' | 'approved' | 'rejected', - })) as SubmissionItemWithDeps[]; + // Transform data to include relational data as item_data + return (data || []).map(item => { + let item_data: unknown; + + switch (item.item_type) { + case 'park': + item_data = (item as any).park_submission; + break; + case 'ride': + item_data = (item as any).ride_submission; + break; + case 'photo': + item_data = { + ...(item as any).photo_submission, + photos: (item as any).photo_submission?.photo_items || [] + }; + break; + default: + item_data = null; + } + + return { + ...item, + item_data, + status: item.status as 'pending' | 'approved' | 'rejected', + }; + }) as SubmissionItemWithDeps[]; } /** @@ -167,23 +198,18 @@ export async function detectDependencyConflicts( /** * Update individual submission item status + * Note: item_data and original_data are read-only (managed via relational tables) */ export async function updateSubmissionItem( itemId: string, updates: Partial ): Promise { - // Cast unknown to Json for Supabase compatibility - const supabaseUpdates = { - ...updates, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - item_data: updates.item_data !== undefined ? updates.item_data as any : undefined, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - original_data: updates.original_data !== undefined ? updates.original_data as any : undefined, - }; + // Remove item_data and original_data from updates (managed via relational tables) + const { item_data, original_data, ...cleanUpdates } = updates; const { error } = await supabase .from('submission_items') - .update(supabaseUpdates) + .update(cleanUpdates) .eq('id', itemId); if (error) throw error; diff --git a/supabase/functions/process-selective-approval/index.ts b/supabase/functions/process-selective-approval/index.ts index 2da442ce..20c08957 100644 --- a/supabase/functions/process-selective-approval/index.ts +++ b/supabase/functions/process-selective-approval/index.ts @@ -343,7 +343,11 @@ serve(async (req) => { park_submission:park_submissions!item_data_id(*), ride_submission:ride_submissions!item_data_id(*), company_submission:company_submissions!item_data_id(*), - ride_model_submission:ride_model_submissions!item_data_id(*) + ride_model_submission:ride_model_submissions!item_data_id(*), + photo_submission:photo_submissions!item_data_id( + *, + photo_items:photo_submission_items(*) + ) `) .in('id', itemIds); @@ -429,8 +433,15 @@ serve(async (req) => { case 'ride_model': itemData = (item as any).ride_model_submission; break; + case 'photo': + // Combine photo_submission with its photo_items array + itemData = { + ...(item as any).photo_submission, + photos: (item as any).photo_submission?.photo_items || [] + }; + break; default: - // For photo/timeline items, fall back to item_data (these still use JSONB) + // For timeline/other items not yet migrated, fall back to item_data (JSONB) itemData = item.item_data; } diff --git a/supabase/migrations/20251103140627_cb03678b-7247-4b79-b155-2fd8cc1d4778.sql b/supabase/migrations/20251103140627_cb03678b-7247-4b79-b155-2fd8cc1d4778.sql new file mode 100644 index 00000000..098c46fd --- /dev/null +++ b/supabase/migrations/20251103140627_cb03678b-7247-4b79-b155-2fd8cc1d4778.sql @@ -0,0 +1,204 @@ +-- Phase 3: Migrate existing JSONB data to relational tables +-- This migration moves all existing item_data from submission_items into relational tables + +DO $$ +DECLARE + item_record RECORD; + new_park_sub_id UUID; + new_ride_sub_id UUID; + new_photo_sub_id UUID; +BEGIN + -- Migrate park submissions + FOR item_record IN + SELECT * FROM submission_items + WHERE item_type = 'park' + AND item_data IS NOT NULL + AND item_data_id IS NULL + LOOP + INSERT INTO park_submissions ( + submission_id, + name, + slug, + description, + park_type, + status, + location_id, + operator_id, + property_owner_id, + opening_date, + closing_date, + opening_date_precision, + closing_date_precision, + website_url, + phone, + email, + banner_image_url, + banner_image_id, + card_image_url, + card_image_id + ) + VALUES ( + item_record.submission_id, + COALESCE((item_record.item_data->>'name')::TEXT, 'Unnamed'), + COALESCE((item_record.item_data->>'slug')::TEXT, 'unnamed'), + (item_record.item_data->>'description')::TEXT, + COALESCE((item_record.item_data->>'park_type')::TEXT, 'theme_park'), + COALESCE((item_record.item_data->>'status')::TEXT, 'operating'), + (item_record.item_data->>'location_id')::UUID, + (item_record.item_data->>'operator_id')::UUID, + (item_record.item_data->>'property_owner_id')::UUID, + (item_record.item_data->>'opening_date')::DATE, + (item_record.item_data->>'closing_date')::DATE, + (item_record.item_data->>'opening_date_precision')::TEXT, + (item_record.item_data->>'closing_date_precision')::TEXT, + (item_record.item_data->>'website_url')::TEXT, + (item_record.item_data->>'phone')::TEXT, + (item_record.item_data->>'email')::TEXT, + (item_record.item_data->>'banner_image_url')::TEXT, + (item_record.item_data->>'banner_image_id')::TEXT, + (item_record.item_data->>'card_image_url')::TEXT, + (item_record.item_data->>'card_image_id')::TEXT + ) + RETURNING id INTO new_park_sub_id; + + UPDATE submission_items + SET item_data_id = new_park_sub_id + WHERE id = item_record.id; + + RAISE NOTICE 'Migrated park submission item %', item_record.id; + END LOOP; + + -- Migrate ride submissions + FOR item_record IN + SELECT * FROM submission_items + WHERE item_type = 'ride' + AND item_data IS NOT NULL + AND item_data_id IS NULL + LOOP + INSERT INTO ride_submissions ( + submission_id, + name, + slug, + description, + category, + status, + park_id, + manufacturer_id, + designer_id, + ride_model_id, + opening_date, + closing_date, + opening_date_precision, + closing_date_precision, + height_requirement_cm, + age_requirement, + max_speed_kmh, + duration_seconds, + capacity_per_hour, + gforce_max, + inversions_count, + length_meters, + height_meters, + drop_meters, + banner_image_url, + banner_image_id, + card_image_url, + card_image_id + ) + VALUES ( + item_record.submission_id, + COALESCE((item_record.item_data->>'name')::TEXT, 'Unnamed'), + COALESCE((item_record.item_data->>'slug')::TEXT, 'unnamed'), + (item_record.item_data->>'description')::TEXT, + COALESCE((item_record.item_data->>'category')::TEXT, 'other'), + COALESCE((item_record.item_data->>'status')::TEXT, 'operating'), + (item_record.item_data->>'park_id')::UUID, + (item_record.item_data->>'manufacturer_id')::UUID, + (item_record.item_data->>'designer_id')::UUID, + (item_record.item_data->>'ride_model_id')::UUID, + (item_record.item_data->>'opening_date')::DATE, + (item_record.item_data->>'closing_date')::DATE, + (item_record.item_data->>'opening_date_precision')::TEXT, + (item_record.item_data->>'closing_date_precision')::TEXT, + (item_record.item_data->>'height_requirement_cm')::INTEGER, + (item_record.item_data->>'age_requirement')::INTEGER, + (item_record.item_data->>'max_speed_kmh')::DECIMAL, + (item_record.item_data->>'duration_seconds')::INTEGER, + (item_record.item_data->>'capacity_per_hour')::INTEGER, + (item_record.item_data->>'gforce_max')::DECIMAL, + (item_record.item_data->>'inversions_count')::INTEGER, + (item_record.item_data->>'length_meters')::DECIMAL, + (item_record.item_data->>'height_meters')::DECIMAL, + (item_record.item_data->>'drop_meters')::DECIMAL, + (item_record.item_data->>'banner_image_url')::TEXT, + (item_record.item_data->>'banner_image_id')::TEXT, + (item_record.item_data->>'card_image_url')::TEXT, + (item_record.item_data->>'card_image_id')::TEXT + ) + RETURNING id INTO new_ride_sub_id; + + UPDATE submission_items + SET item_data_id = new_ride_sub_id + WHERE id = item_record.id; + + RAISE NOTICE 'Migrated ride submission item %', item_record.id; + END LOOP; + + -- Migrate photo submissions (check if already migrated) + FOR item_record IN + SELECT * FROM submission_items + WHERE item_type = 'photo' + AND item_data IS NOT NULL + AND item_data_id IS NULL + AND NOT EXISTS ( + SELECT 1 FROM photo_submissions + WHERE submission_id = item_record.submission_id + ) + LOOP + INSERT INTO photo_submissions ( + submission_id, + entity_type, + entity_id, + context + ) + VALUES ( + item_record.submission_id, + COALESCE((item_record.item_data->>'entity_type')::TEXT, (item_record.item_data->>'context')::TEXT, 'park'), + (item_record.item_data->>'entity_id')::UUID, + COALESCE((item_record.item_data->>'context')::TEXT, 'park') + ) + RETURNING id INTO new_photo_sub_id; + + -- Migrate individual photos + IF item_record.item_data ? 'photos' THEN + INSERT INTO photo_submission_items ( + photo_submission_id, + url, + caption, + title, + cloudflare_id, + order_index + ) + SELECT + new_photo_sub_id, + (photo->>'url')::TEXT, + (photo->>'caption')::TEXT, + (photo->>'title')::TEXT, + (photo->>'cloudflare_id')::TEXT, + COALESCE((photo->>'order')::INTEGER, (photo->>'order_index')::INTEGER, 0) + FROM jsonb_array_elements(item_record.item_data->'photos') AS photo; + END IF; + + UPDATE submission_items + SET item_data_id = new_photo_sub_id + WHERE id = item_record.id; + + RAISE NOTICE 'Migrated photo submission item %', item_record.id; + END LOOP; + + RAISE NOTICE 'Data migration complete'; +END $$; + +-- Add comments documenting the migration +COMMENT ON COLUMN submission_items.item_data IS 'DEPRECATED: Legacy JSONB column. All data migrated to relational tables. Use item_data_id foreign key instead. Will be dropped in next migration.'; +COMMENT ON COLUMN submission_items.original_data IS 'DEPRECATED: Legacy JSONB column for moderator edits. All data migrated to relational tables. Will be dropped in next migration.'; \ No newline at end of file