diff --git a/src/components/admin/RideDataBackfill.tsx b/src/components/admin/RideDataBackfill.tsx new file mode 100644 index 00000000..064ee077 --- /dev/null +++ b/src/components/admin/RideDataBackfill.tsx @@ -0,0 +1,110 @@ +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { supabase } from '@/lib/supabaseClient'; +import { Hammer, AlertCircle, CheckCircle2 } from 'lucide-react'; +import { useToast } from '@/hooks/use-toast'; + +export function RideDataBackfill() { + const [isRunning, setIsRunning] = useState(false); + const [result, setResult] = useState<{ + success: boolean; + rides_updated: number; + manufacturer_added: number; + designer_added: number; + ride_model_added: number; + } | null>(null); + const [error, setError] = useState(null); + const { toast } = useToast(); + + const handleBackfill = async () => { + setIsRunning(true); + setError(null); + setResult(null); + + try { + const { data, error: invokeError } = await supabase.functions.invoke( + 'backfill-ride-data' + ); + + if (invokeError) throw invokeError; + + setResult(data); + + const updates: string[] = []; + if (data.manufacturer_added > 0) updates.push(`${data.manufacturer_added} manufacturers`); + if (data.designer_added > 0) updates.push(`${data.designer_added} designers`); + if (data.ride_model_added > 0) updates.push(`${data.ride_model_added} ride models`); + + toast({ + title: 'Backfill Complete', + description: `Updated ${data.rides_updated} rides: ${updates.join(', ')}`, + }); + } catch (err: any) { + const errorMessage = err.message || 'Failed to run backfill'; + setError(errorMessage); + toast({ + title: 'Backfill Failed', + description: errorMessage, + variant: 'destructive', + }); + } finally { + setIsRunning(false); + } + }; + + return ( + + + + + Ride Data Backfill + + + Backfill missing manufacturer, designer, and ride model data for approved rides from their submission data + + + + + + + This tool will find rides missing manufacturer, designer, or ride model information and populate them using data from their approved submissions. Useful for fixing rides that were approved before relationship data was properly handled. + + + + {result && ( + + + +
Backfill completed successfully!
+
+
Rides updated: {result.rides_updated}
+
Manufacturers added: {result.manufacturer_added}
+
Designers added: {result.designer_added}
+
Ride models added: {result.ride_model_added}
+
+
+
+ )} + + {error && ( + + + {error} + + )} + + +
+
+ ); +} diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 2c60255b..4bec8ea0 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -6633,6 +6633,7 @@ export type Database = { Returns: undefined } backfill_park_locations: { Args: never; Returns: Json } + backfill_ride_data: { Args: never; Returns: Json } backfill_sort_orders: { Args: never; Returns: undefined } block_aal1_with_mfa: { Args: never; Returns: boolean } can_approve_submission_item: { diff --git a/src/pages/AdminSettings.tsx b/src/pages/AdminSettings.tsx index 65ac86f0..146acb56 100644 --- a/src/pages/AdminSettings.tsx +++ b/src/pages/AdminSettings.tsx @@ -15,6 +15,7 @@ import { NovuMigrationUtility } from '@/components/admin/NovuMigrationUtility'; import { TestDataGenerator } from '@/components/admin/TestDataGenerator'; import { IntegrationTestRunner } from '@/components/admin/IntegrationTestRunner'; import { ParkLocationBackfill } from '@/components/admin/ParkLocationBackfill'; +import { RideDataBackfill } from '@/components/admin/RideDataBackfill'; import { Loader2, Save, Clock, Users, Bell, Shield, Settings, Trash2, Plug, AlertTriangle, Lock, TestTube, RefreshCw, Info, AlertCircle, Database } from 'lucide-react'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; @@ -938,6 +939,7 @@ export default function AdminSettings() { + diff --git a/supabase/functions/backfill-ride-data/index.ts b/supabase/functions/backfill-ride-data/index.ts new file mode 100644 index 00000000..d6ca0fbc --- /dev/null +++ b/supabase/functions/backfill-ride-data/index.ts @@ -0,0 +1,54 @@ +import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts'; +import { edgeLogger } from '../_shared/logger.ts'; + +export default createEdgeFunction( + { + name: 'backfill-ride-data', + requireAuth: true, + }, + async (req, context, supabase) => { + edgeLogger.info('Starting ride data backfill', { requestId: context.requestId }); + + // Check if user is superuser + const { data: profile, error: profileError } = await supabase + .from('user_profiles') + .select('role') + .eq('id', context.user.id) + .single(); + + if (profileError || profile?.role !== 'superuser') { + edgeLogger.warn('Unauthorized backfill attempt', { + userId: context.user.id, + requestId: context.requestId + }); + return new Response( + JSON.stringify({ error: 'Unauthorized: Superuser access required' }), + { status: 403, headers: { 'Content-Type': 'application/json' } } + ); + } + + // Execute the backfill function + const { data, error } = await supabase.rpc('backfill_ride_data'); + + if (error) { + edgeLogger.error('Error running ride data backfill', { + error, + requestId: context.requestId + }); + throw error; + } + + edgeLogger.info('Ride data backfill completed', { + results: data, + requestId: context.requestId + }); + + return new Response( + JSON.stringify({ + success: true, + ...data, + }), + { headers: { 'Content-Type': 'application/json' } } + ); + } +); diff --git a/supabase/migrations/20251111154341_28e46157-f827-4caa-a9df-14e7ba895d59.sql b/supabase/migrations/20251111154341_28e46157-f827-4caa-a9df-14e7ba895d59.sql new file mode 100644 index 00000000..3095b8d8 --- /dev/null +++ b/supabase/migrations/20251111154341_28e46157-f827-4caa-a9df-14e7ba895d59.sql @@ -0,0 +1,92 @@ +-- Function to backfill missing ride data from submission data +CREATE OR REPLACE FUNCTION backfill_ride_data() +RETURNS jsonb +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + v_rides_updated INTEGER := 0; + v_manufacturer_added INTEGER := 0; + v_designer_added INTEGER := 0; + v_ride_model_added INTEGER := 0; + v_ride RECORD; + v_submission RECORD; +BEGIN + -- Find rides with missing manufacturer, designer, or ride_model that have approved submissions + FOR v_ride IN + SELECT DISTINCT r.id, r.name, r.slug, r.manufacturer_id, r.designer_id, r.ride_model_id + FROM rides r + WHERE r.manufacturer_id IS NULL + OR r.designer_id IS NULL + OR r.ride_model_id IS NULL + LOOP + -- Find the most recent approved submission for this ride with the missing data + SELECT + rs.manufacturer_id, + rs.designer_id, + rs.ride_model_id + INTO v_submission + FROM ride_submissions rs + JOIN content_submissions cs ON cs.id = rs.submission_id + WHERE rs.ride_id = v_ride.id + AND cs.status = 'approved' + AND ( + (v_ride.manufacturer_id IS NULL AND rs.manufacturer_id IS NOT NULL) OR + (v_ride.designer_id IS NULL AND rs.designer_id IS NOT NULL) OR + (v_ride.ride_model_id IS NULL AND rs.ride_model_id IS NOT NULL) + ) + ORDER BY cs.created_at DESC + LIMIT 1; + + -- If we found submission data, update the ride + IF FOUND THEN + DECLARE + v_updated BOOLEAN := FALSE; + BEGIN + -- Update manufacturer if missing + IF v_ride.manufacturer_id IS NULL AND v_submission.manufacturer_id IS NOT NULL THEN + UPDATE rides + SET manufacturer_id = v_submission.manufacturer_id + WHERE id = v_ride.id; + v_manufacturer_added := v_manufacturer_added + 1; + v_updated := TRUE; + RAISE NOTICE 'Added manufacturer for ride: % (id: %)', v_ride.name, v_ride.id; + END IF; + + -- Update designer if missing + IF v_ride.designer_id IS NULL AND v_submission.designer_id IS NOT NULL THEN + UPDATE rides + SET designer_id = v_submission.designer_id + WHERE id = v_ride.id; + v_designer_added := v_designer_added + 1; + v_updated := TRUE; + RAISE NOTICE 'Added designer for ride: % (id: %)', v_ride.name, v_ride.id; + END IF; + + -- Update ride_model if missing + IF v_ride.ride_model_id IS NULL AND v_submission.ride_model_id IS NOT NULL THEN + UPDATE rides + SET ride_model_id = v_submission.ride_model_id + WHERE id = v_ride.id; + v_ride_model_added := v_ride_model_added + 1; + v_updated := TRUE; + RAISE NOTICE 'Added ride model for ride: % (id: %)', v_ride.name, v_ride.id; + END IF; + + IF v_updated THEN + v_rides_updated := v_rides_updated + 1; + END IF; + END; + END IF; + END LOOP; + + RETURN jsonb_build_object( + 'success', true, + 'rides_updated', v_rides_updated, + 'manufacturer_added', v_manufacturer_added, + 'designer_added', v_designer_added, + 'ride_model_added', v_ride_model_added + ); +END; +$$; \ No newline at end of file