diff --git a/src/components/moderation/displays/RichParkDisplay.tsx b/src/components/moderation/displays/RichParkDisplay.tsx index ed81a473..8377e86e 100644 --- a/src/components/moderation/displays/RichParkDisplay.tsx +++ b/src/components/moderation/displays/RichParkDisplay.tsx @@ -32,9 +32,14 @@ export function RichParkDisplay({ data, actionType, showAllFields = true }: Rich .single(); setLocation(locationData); } - // Otherwise use temp_location_data (for new submissions) - else if (data.temp_location_data) { - setLocation(data.temp_location_data); + // Otherwise fetch from park_submission_locations (for new submissions) + else if (data.id) { + const { data: locationData } = await supabase + .from('park_submission_locations') + .select('*') + .eq('park_submission_id', data.id) + .maybeSingle(); + setLocation(locationData); } // Fetch operator @@ -59,7 +64,7 @@ export function RichParkDisplay({ data, actionType, showAllFields = true }: Rich }; fetchRelatedData(); - }, [data.location_id, data.temp_location_data, data.operator_id, data.property_owner_id]); + }, [data.location_id, data.id, data.operator_id, data.property_owner_id]); const getStatusColor = (status: string | undefined) => { if (!status) return 'bg-gray-500'; diff --git a/src/hooks/moderation/useModerationActions.ts b/src/hooks/moderation/useModerationActions.ts index e0af557b..8a766451 100644 --- a/src/hooks/moderation/useModerationActions.ts +++ b/src/hooks/moderation/useModerationActions.ts @@ -162,25 +162,24 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio }); // Transform to include item_data - const itemsWithData = fullItems.map(item => { + const itemsWithData = await Promise.all(fullItems.map(async item => { let itemData = {}; switch (item.item_type) { case 'park': { const parkSub = (item.park_submission as any) || {}; + let locationData = null; + if (parkSub?.id) { + const { data } = await supabase + .from('park_submission_locations') + .select('*') + .eq('park_submission_id', parkSub.id) + .maybeSingle(); + locationData = data; + } itemData = { ...parkSub, - // Transform temp_location_data → location for validation - location: parkSub.temp_location_data || undefined, - temp_location_data: undefined + location: locationData || undefined }; - - console.info('[Submission Flow] Transformed park data for validation', { - itemId: item.id, - hasLocation: !!parkSub.temp_location_data, - locationData: parkSub.temp_location_data, - transformedHasLocation: !!(itemData as any).location, - timestamp: new Date().toISOString() - }); break; } case 'ride': diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 26ed2f96..14b06a44 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -2006,6 +2006,65 @@ export type Database = { }, ] } + park_submission_locations: { + Row: { + city: string | null + country: string + created_at: string + display_name: string | null + id: string + latitude: number | null + longitude: number | null + name: string + park_submission_id: string + postal_code: string | null + state_province: string | null + street_address: string | null + timezone: string | null + updated_at: string + } + Insert: { + city?: string | null + country: string + created_at?: string + display_name?: string | null + id?: string + latitude?: number | null + longitude?: number | null + name: string + park_submission_id: string + postal_code?: string | null + state_province?: string | null + street_address?: string | null + timezone?: string | null + updated_at?: string + } + Update: { + city?: string | null + country?: string + created_at?: string + display_name?: string | null + id?: string + latitude?: number | null + longitude?: number | null + name?: string + park_submission_id?: string + postal_code?: string | null + state_province?: string | null + street_address?: string | null + timezone?: string | null + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "park_submission_locations_park_submission_id_fkey" + columns: ["park_submission_id"] + isOneToOne: false + referencedRelation: "park_submissions" + referencedColumns: ["id"] + }, + ] + } park_submissions: { Row: { banner_image_id: string | null @@ -2029,7 +2088,6 @@ export type Database = { slug: string status: string submission_id: string - temp_location_data: Json | null updated_at: string website_url: string | null } @@ -2055,7 +2113,6 @@ export type Database = { slug: string status?: string submission_id: string - temp_location_data?: Json | null updated_at?: string website_url?: string | null } @@ -2081,7 +2138,6 @@ export type Database = { slug?: string status?: string submission_id?: string - temp_location_data?: Json | null updated_at?: string website_url?: string | null } diff --git a/src/lib/entitySubmissionHelpers.ts b/src/lib/entitySubmissionHelpers.ts index 9d3b9a55..52b91972 100644 --- a/src/lib/entitySubmissionHelpers.ts +++ b/src/lib/entitySubmissionHelpers.ts @@ -362,22 +362,11 @@ async function submitCompositeCreation( images: primaryImages as unknown as Json }; - // Convert location object to temp_location_data for parks + // Store location reference for park submissions (will be created in relational table) if (uploadedPrimary.type === 'park' && uploadedPrimary.data.location) { - primaryData.temp_location_data = { - name: uploadedPrimary.data.location.name, - street_address: uploadedPrimary.data.location.street_address || null, - city: uploadedPrimary.data.location.city || null, - state_province: uploadedPrimary.data.location.state_province || null, - country: uploadedPrimary.data.location.country, - latitude: uploadedPrimary.data.location.latitude, - longitude: uploadedPrimary.data.location.longitude, - timezone: uploadedPrimary.data.location.timezone || null, - postal_code: uploadedPrimary.data.location.postal_code || null, - display_name: uploadedPrimary.data.location.display_name - }; + primaryData._temp_location = uploadedPrimary.data.location; delete primaryData.location; // Remove the original location object - console.log('[submitCompositeCreation] Converted location to temp_location_data:', primaryData.temp_location_data); + console.log('[submitCompositeCreation] Stored location for relational insert:', primaryData._temp_location); } // Map temporary IDs to order indices for foreign keys @@ -725,7 +714,7 @@ export async function submitParkCreation( name: data.name, hasLocation: !!data.location, hasLocationId: !!data.location_id, - temp_location_data: tempLocationData + hasLocationData: !!tempLocationData }); const { data: parkSubmission, error: parkSubmissionError } = await supabase @@ -745,7 +734,6 @@ export async function submitParkCreation( operator_id: data.operator_id || null, property_owner_id: data.property_owner_id || null, location_id: data.location_id || null, - temp_location_data: tempLocationData, banner_image_url: bannerImage?.url || data.banner_image_url || null, banner_image_id: bannerImage?.cloudflare_id || data.banner_image_id || null, card_image_url: cardImage?.url || data.card_image_url || null, @@ -756,6 +744,26 @@ export async function submitParkCreation( if (parkSubmissionError) throw parkSubmissionError; + // Create location in relational table if provided + if (tempLocationData) { + const { error: locationError } = await supabase + .from('park_submission_locations' as any) + .insert({ + park_submission_id: (parkSubmission as any).id, + ...tempLocationData + } as any); + + if (locationError) { + console.error('[submitParkCreation] Failed to create location:', locationError); + throw new Error(`Failed to save location data: ${locationError.message}`); + } + + console.info('[submitParkCreation] Created park_submission_location', { + parkSubmissionId: (parkSubmission as any).id, + locationName: tempLocationData.name + }); + } + // Create submission_items record linking to park_submissions const { error: itemError } = await supabase .from('submission_items') @@ -934,7 +942,6 @@ export async function submitParkUpdate( operator_id: changedFields.operator_id !== undefined ? changedFields.operator_id : existingPark.operator_id, property_owner_id: changedFields.property_owner_id !== undefined ? changedFields.property_owner_id : existingPark.property_owner_id, location_id: changedFields.location_id !== undefined ? changedFields.location_id : existingPark.location_id, - temp_location_data: tempLocationData, banner_image_url: changedFields.banner_image_url !== undefined ? changedFields.banner_image_url : existingPark.banner_image_url, banner_image_id: changedFields.banner_image_id !== undefined ? changedFields.banner_image_id : existingPark.banner_image_id, card_image_url: changedFields.card_image_url !== undefined ? changedFields.card_image_url : existingPark.card_image_url, @@ -945,6 +952,26 @@ export async function submitParkUpdate( if (parkSubmissionError) throw parkSubmissionError; + // Create location in relational table if provided + if (tempLocationData) { + const { error: locationError } = await supabase + .from('park_submission_locations' as any) + .insert({ + park_submission_id: (parkSubmission as any).id, + ...tempLocationData + } as any); + + if (locationError) { + console.error('[submitParkEdit] Failed to create location:', locationError); + throw new Error(`Failed to save location data: ${locationError.message}`); + } + + console.info('[submitParkEdit] Created park_submission_location', { + parkSubmissionId: (parkSubmission as any).id, + locationName: tempLocationData.name + }); + } + // ✅ Create submission_items referencing park_submission (no JSON data) const { error: itemError } = await supabase .from('submission_items') diff --git a/src/lib/submissionItemsService.ts b/src/lib/submissionItemsService.ts index f7675241..61379493 100644 --- a/src/lib/submissionItemsService.ts +++ b/src/lib/submissionItemsService.ts @@ -81,12 +81,21 @@ export async function fetchSubmissionItems(submissionId: string): Promise { action: 'approval_park_data_debug', itemId: item.id, hasLocationId: !!itemData.location_id, - hasTempLocationData: !!itemData.temp_location_data, - tempLocationDataKeys: itemData.temp_location_data ? Object.keys(itemData.temp_location_data) : [], + parkSubmissionId: itemData.id, parkSubmissionKeys: Object.keys((item as any).park_submission || {}), requestId: tracking.requestId }); @@ -1576,47 +1575,55 @@ function normalizeParkTypeValue(data: any): any { async function createPark(supabase: any, data: any): Promise { const submitterId = data._submitter_id; + const parkSubmissionId = data.id; // Store the park_submission.id for location lookup let uploadedPhotos: any[] = []; - // Create location if temp_location_data exists and location_id is missing - if (data.temp_location_data && !data.location_id) { - edgeLogger.info('Creating location from temp data', { - action: 'approval_create_location', - locationName: data.temp_location_data.name - }); - - const { data: newLocation, error: locationError } = await supabase - .from('locations') - .insert({ - name: data.temp_location_data.name, - street_address: data.temp_location_data.street_address || null, - city: data.temp_location_data.city, - state_province: data.temp_location_data.state_province, - country: data.temp_location_data.country, - latitude: data.temp_location_data.latitude, - longitude: data.temp_location_data.longitude, - timezone: data.temp_location_data.timezone, - postal_code: data.temp_location_data.postal_code - }) - .select('id') + + // Create location if park_submission_locations exists and location_id is missing + if (!data.location_id) { + // Try to fetch location from relational table + const { data: locationData, error: locationFetchError } = await supabase + .from('park_submission_locations') + .select('*') + .eq('park_submission_id', parkSubmissionId) .single(); - if (locationError) { - throw new Error(`Failed to create location: ${locationError.message}`); + if (locationData && !locationFetchError) { + edgeLogger.info('Creating location from relational table', { + action: 'approval_create_location', + locationName: locationData.name + }); + + const { data: newLocation, error: locationError } = await supabase + .from('locations') + .insert({ + name: locationData.name, + street_address: locationData.street_address || null, + city: locationData.city, + state_province: locationData.state_province, + country: locationData.country, + latitude: locationData.latitude, + longitude: locationData.longitude, + timezone: locationData.timezone, + postal_code: locationData.postal_code + }) + .select('id') + .single(); + + if (locationError) { + throw new Error(`Failed to create location: ${locationError.message}`); + } + + data.location_id = newLocation.id; + + edgeLogger.info('Location created successfully', { + action: 'approval_location_created', + locationId: newLocation.id, + locationName: locationData.name + }); } - - data.location_id = newLocation.id; - - edgeLogger.info('Location created successfully', { - action: 'approval_location_created', - locationId: newLocation.id, - locationName: data.temp_location_data.name - }); } - // Clean up temp data - delete data.temp_location_data; - // Transform images object if present if (data.images) { const { uploaded, banner_assignment, card_assignment } = data.images; @@ -1653,36 +1660,44 @@ async function createPark(supabase: any, data: any): Promise { parkId = data.park_id; delete data.park_id; // Remove ID from update data - // ✅ FIXED: Handle location updates from temp_location_data - if (data.temp_location_data && !data.location_id) { - edgeLogger.info('Creating location from temp data for update', { - action: 'approval_create_location_update', - locationName: data.temp_location_data.name - }); - - const { data: newLocation, error: locationError } = await supabase - .from('locations') - .insert({ - name: data.temp_location_data.name, - street_address: data.temp_location_data.street_address || null, - city: data.temp_location_data.city, - state_province: data.temp_location_data.state_province, - country: data.temp_location_data.country, - latitude: data.temp_location_data.latitude, - longitude: data.temp_location_data.longitude, - timezone: data.temp_location_data.timezone, - postal_code: data.temp_location_data.postal_code - }) - .select('id') + // ✅ FIXED: Handle location updates from park_submission_locations + if (!data.location_id) { + // Try to fetch location from relational table + const { data: locationData, error: locationFetchError } = await supabase + .from('park_submission_locations') + .select('*') + .eq('park_submission_id', parkSubmissionId) .single(); - if (locationError) { - throw new Error(`Failed to create location: ${locationError.message}`); + if (locationData && !locationFetchError) { + edgeLogger.info('Creating location from relational table for update', { + action: 'approval_create_location_update', + locationName: locationData.name + }); + + const { data: newLocation, error: locationError } = await supabase + .from('locations') + .insert({ + name: locationData.name, + street_address: locationData.street_address || null, + city: locationData.city, + state_province: locationData.state_province, + country: locationData.country, + latitude: locationData.latitude, + longitude: locationData.longitude, + timezone: locationData.timezone, + postal_code: locationData.postal_code + }) + .select('id') + .single(); + + if (locationError) { + throw new Error(`Failed to create location: ${locationError.message}`); + } + + data.location_id = newLocation.id; } - - data.location_id = newLocation.id; } - delete data.temp_location_data; const normalizedData = normalizeParkTypeValue(normalizeStatusValue(data)); const sanitizedData = sanitizeDateFields(normalizedData); diff --git a/supabase/migrations/20251106153950_f8708eee-106a-4cd6-ac14-6eb61d15554a.sql b/supabase/migrations/20251106153950_f8708eee-106a-4cd6-ac14-6eb61d15554a.sql new file mode 100644 index 00000000..c0bd3c65 --- /dev/null +++ b/supabase/migrations/20251106153950_f8708eee-106a-4cd6-ac14-6eb61d15554a.sql @@ -0,0 +1,122 @@ +-- Phase 1: Fix park_submissions.temp_location_data JSONB violation +-- Create relational table for temporary location data + +-- Create park_submission_locations table +CREATE TABLE IF NOT EXISTS public.park_submission_locations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + park_submission_id UUID NOT NULL REFERENCES public.park_submissions(id) ON DELETE CASCADE, + name TEXT NOT NULL, + street_address TEXT, + city TEXT, + state_province TEXT, + country TEXT NOT NULL, + postal_code TEXT, + latitude NUMERIC(10, 7), + longitude NUMERIC(10, 7), + timezone TEXT, + display_name TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Create indexes for performance +CREATE INDEX IF NOT EXISTS idx_park_submission_locations_submission + ON public.park_submission_locations(park_submission_id); + +CREATE INDEX IF NOT EXISTS idx_park_submission_locations_country + ON public.park_submission_locations(country); + +-- Enable RLS +ALTER TABLE public.park_submission_locations ENABLE ROW LEVEL SECURITY; + +-- RLS Policies (mirror park_submissions policies) +CREATE POLICY "Moderators can view all park submission locations" + ON public.park_submission_locations + FOR SELECT + TO authenticated + USING ( + is_moderator(auth.uid()) + AND ((NOT has_mfa_enabled(auth.uid())) OR has_aal2()) + ); + +CREATE POLICY "Users can view their own park submission locations" + ON public.park_submission_locations + FOR SELECT + TO authenticated + USING ( + EXISTS ( + SELECT 1 FROM content_submissions cs + INNER JOIN park_submissions ps ON ps.submission_id = cs.id + WHERE ps.id = park_submission_locations.park_submission_id + AND cs.user_id = auth.uid() + ) + ); + +CREATE POLICY "Users can insert park submission locations" + ON public.park_submission_locations + FOR INSERT + TO authenticated + WITH CHECK ( + EXISTS ( + SELECT 1 FROM content_submissions cs + INNER JOIN park_submissions ps ON ps.submission_id = cs.id + WHERE ps.id = park_submission_locations.park_submission_id + AND cs.user_id = auth.uid() + ) + AND NOT is_user_banned(auth.uid()) + ); + +CREATE POLICY "Moderators can update park submission locations" + ON public.park_submission_locations + FOR UPDATE + TO authenticated + USING ( + is_moderator(auth.uid()) + AND ((NOT has_mfa_enabled(auth.uid())) OR has_aal2()) + ); + +CREATE POLICY "Moderators can delete park submission locations" + ON public.park_submission_locations + FOR DELETE + TO authenticated + USING ( + is_moderator(auth.uid()) + AND ((NOT has_mfa_enabled(auth.uid())) OR has_aal2()) + ); + +-- Migrate existing temp_location_data to new table +INSERT INTO public.park_submission_locations ( + park_submission_id, + name, + street_address, + city, + state_province, + country, + postal_code, + latitude, + longitude, + timezone, + display_name +) +SELECT + id, + temp_location_data->>'name', + temp_location_data->>'street_address', + temp_location_data->>'city', + temp_location_data->>'state_province', + temp_location_data->>'country', + temp_location_data->>'postal_code', + (temp_location_data->>'latitude')::numeric, + (temp_location_data->>'longitude')::numeric, + temp_location_data->>'timezone', + temp_location_data->>'display_name' +FROM public.park_submissions +WHERE temp_location_data IS NOT NULL +AND temp_location_data->>'name' IS NOT NULL; + +-- Drop the JSONB column +ALTER TABLE public.park_submissions DROP COLUMN IF EXISTS temp_location_data; + +-- Add comment +COMMENT ON TABLE public.park_submission_locations IS +'Relational storage for park submission location data. Replaces temp_location_data JSONB column for proper queryability and data integrity.'; \ No newline at end of file