Compare commits

...

3 Commits

Author SHA1 Message Date
gpt-engineer-app[bot]
664c894bb1 Add Company Backfill Tool
Implement edge function and UI to backfill missing company data from submissions, including admin trigger and result reporting, and wire into admin settings.
2025-11-11 15:48:49 +00:00
gpt-engineer-app[bot]
314db65591 Backfill Ride Data
Add edge function and UI for ride data backfill, integrating admin UI to trigger backfill, plus supporting types and wiring to backfill_ride_data function. Includes RideDataBackfill component, admin page integration, and initial server-side function scaffold for updating rides from submissions.
2025-11-11 15:45:14 +00:00
gpt-engineer-app[bot]
d48e95ee7c Connect to Park Location Backfill UI 2025-11-11 15:40:48 +00:00
10 changed files with 737 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
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 { Building2, AlertCircle, CheckCircle2 } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
export function CompanyDataBackfill() {
const [isRunning, setIsRunning] = useState(false);
const [result, setResult] = useState<{
success: boolean;
companies_updated: number;
headquarters_added: number;
website_added: number;
founded_year_added: number;
description_added: number;
logo_added: number;
} | null>(null);
const [error, setError] = useState<string | null>(null);
const { toast } = useToast();
const handleBackfill = async () => {
setIsRunning(true);
setError(null);
setResult(null);
try {
const { data, error: invokeError } = await supabase.functions.invoke(
'backfill-company-data'
);
if (invokeError) throw invokeError;
setResult(data);
const updates: string[] = [];
if (data.headquarters_added > 0) updates.push(`${data.headquarters_added} headquarters`);
if (data.website_added > 0) updates.push(`${data.website_added} websites`);
if (data.founded_year_added > 0) updates.push(`${data.founded_year_added} founding years`);
if (data.description_added > 0) updates.push(`${data.description_added} descriptions`);
if (data.logo_added > 0) updates.push(`${data.logo_added} logos`);
toast({
title: 'Backfill Complete',
description: `Updated ${data.companies_updated} companies: ${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 (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Building2 className="w-5 h-5" />
Company Data Backfill
</CardTitle>
<CardDescription>
Backfill missing headquarters, website, founding year, description, and logo data for companies from their submission data
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
This tool will find companies (operators, manufacturers, designers) missing basic information and populate them using data from their approved submissions. Useful for fixing companies that were approved before all fields were properly handled.
</AlertDescription>
</Alert>
{result && (
<Alert className="border-green-200 bg-green-50 dark:bg-green-950 dark:border-green-800">
<CheckCircle2 className="h-4 w-4 text-green-600 dark:text-green-400" />
<AlertDescription className="text-green-900 dark:text-green-100">
<div className="font-medium">Backfill completed successfully!</div>
<div className="mt-2 space-y-1">
<div>Companies updated: {result.companies_updated}</div>
<div>Headquarters added: {result.headquarters_added}</div>
<div>Websites added: {result.website_added}</div>
<div>Founding years added: {result.founded_year_added}</div>
<div>Descriptions added: {result.description_added}</div>
<div>Logos added: {result.logo_added}</div>
</div>
</AlertDescription>
</Alert>
)}
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button
onClick={handleBackfill}
disabled={isRunning}
className="w-full"
trackingLabel="run-company-data-backfill"
>
<Building2 className="w-4 h-4 mr-2" />
{isRunning ? 'Running Backfill...' : 'Run Company Data Backfill'}
</Button>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,100 @@
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 { MapPin, AlertCircle, CheckCircle2 } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
export function ParkLocationBackfill() {
const [isRunning, setIsRunning] = useState(false);
const [result, setResult] = useState<{
success: boolean;
parks_updated: number;
locations_created: number;
} | null>(null);
const [error, setError] = useState<string | null>(null);
const { toast } = useToast();
const handleBackfill = async () => {
setIsRunning(true);
setError(null);
setResult(null);
try {
const { data, error: invokeError } = await supabase.functions.invoke(
'backfill-park-locations'
);
if (invokeError) throw invokeError;
setResult(data);
toast({
title: 'Backfill Complete',
description: `Updated ${data.parks_updated} parks with ${data.locations_created} new locations`,
});
} 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 (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MapPin className="w-5 h-5" />
Park Location Backfill
</CardTitle>
<CardDescription>
Backfill missing location data for approved parks from their submission data
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
This tool will find parks without location data and populate them using the location information from their approved submissions. This is useful for fixing parks that were approved before the location creation fix was implemented.
</AlertDescription>
</Alert>
{result && (
<Alert className="border-green-200 bg-green-50 dark:bg-green-950 dark:border-green-800">
<CheckCircle2 className="h-4 w-4 text-green-600 dark:text-green-400" />
<AlertDescription className="text-green-900 dark:text-green-100">
<div className="font-medium">Backfill completed successfully!</div>
<div className="mt-2 space-y-1">
<div>Parks updated: {result.parks_updated}</div>
<div>Locations created: {result.locations_created}</div>
</div>
</AlertDescription>
</Alert>
)}
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button
onClick={handleBackfill}
disabled={isRunning}
className="w-full"
trackingLabel="run-park-location-backfill"
>
<MapPin className="w-4 h-4 mr-2" />
{isRunning ? 'Running Backfill...' : 'Run Location Backfill'}
</Button>
</CardContent>
</Card>
);
}

View File

@@ -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<string | null>(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 (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Hammer className="w-5 h-5" />
Ride Data Backfill
</CardTitle>
<CardDescription>
Backfill missing manufacturer, designer, and ride model data for approved rides from their submission data
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
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.
</AlertDescription>
</Alert>
{result && (
<Alert className="border-green-200 bg-green-50 dark:bg-green-950 dark:border-green-800">
<CheckCircle2 className="h-4 w-4 text-green-600 dark:text-green-400" />
<AlertDescription className="text-green-900 dark:text-green-100">
<div className="font-medium">Backfill completed successfully!</div>
<div className="mt-2 space-y-1">
<div>Rides updated: {result.rides_updated}</div>
<div>Manufacturers added: {result.manufacturer_added}</div>
<div>Designers added: {result.designer_added}</div>
<div>Ride models added: {result.ride_model_added}</div>
</div>
</AlertDescription>
</Alert>
)}
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button
onClick={handleBackfill}
disabled={isRunning}
className="w-full"
trackingLabel="run-ride-data-backfill"
>
<Hammer className="w-4 h-4 mr-2" />
{isRunning ? 'Running Backfill...' : 'Run Ride Data Backfill'}
</Button>
</CardContent>
</Card>
);
}

View File

@@ -6632,7 +6632,9 @@ export type Database = {
Args: { target_user_id: string }
Returns: undefined
}
backfill_company_data: { Args: never; Returns: Json }
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: {

View File

@@ -14,6 +14,9 @@ import { useAdminSettings } from '@/hooks/useAdminSettings';
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 { CompanyDataBackfill } from '@/components/admin/CompanyDataBackfill';
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';
@@ -935,6 +938,10 @@ export default function AdminSettings() {
)}
</CardContent>
</Card>
<ParkLocationBackfill />
<RideDataBackfill />
<CompanyDataBackfill />
</div>
</TabsContent>

View File

@@ -0,0 +1,54 @@
import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts';
import { edgeLogger } from '../_shared/logger.ts';
export default createEdgeFunction(
{
name: 'backfill-company-data',
requireAuth: true,
},
async (req, context, supabase) => {
edgeLogger.info('Starting company 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_company_data');
if (error) {
edgeLogger.error('Error running company data backfill', {
error,
requestId: context.requestId
});
throw error;
}
edgeLogger.info('Company data backfill completed', {
results: data,
requestId: context.requestId
});
return new Response(
JSON.stringify({
success: true,
...data,
}),
{ headers: { 'Content-Type': 'application/json' } }
);
}
);

View File

@@ -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' } }
);
}
);

View File

@@ -0,0 +1,78 @@
-- Fix search_path for backfill_park_locations function
CREATE OR REPLACE FUNCTION backfill_park_locations()
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_parks_updated INTEGER := 0;
v_locations_created INTEGER := 0;
v_park RECORD;
v_submission RECORD;
v_location_id UUID;
BEGIN
-- Find parks without locations that have approved submissions with location data
FOR v_park IN
SELECT DISTINCT p.id, p.name, p.slug
FROM parks p
WHERE p.location_id IS NULL
LOOP
-- Find the most recent approved submission for this park with location data
SELECT
psl.country,
psl.state_province,
psl.city,
psl.street_address,
psl.postal_code,
psl.latitude,
psl.longitude
INTO v_submission
FROM park_submissions ps
JOIN park_submission_locations psl ON ps.id = psl.park_submission_id
WHERE ps.park_id = v_park.id
AND ps.status = 'approved'
AND psl.country IS NOT NULL
ORDER BY ps.created_at DESC
LIMIT 1;
-- If we found location data, create a location record
IF FOUND THEN
INSERT INTO locations (
country,
state_province,
city,
street_address,
postal_code,
latitude,
longitude
) VALUES (
v_submission.country,
v_submission.state_province,
v_submission.city,
v_submission.street_address,
v_submission.postal_code,
v_submission.latitude,
v_submission.longitude
)
RETURNING id INTO v_location_id;
-- Update the park with the new location
UPDATE parks
SET location_id = v_location_id
WHERE id = v_park.id;
v_parks_updated := v_parks_updated + 1;
v_locations_created := v_locations_created + 1;
RAISE NOTICE 'Backfilled location for park: % (id: %)', v_park.name, v_park.id;
END IF;
END LOOP;
RETURN jsonb_build_object(
'success', true,
'parks_updated', v_parks_updated,
'locations_created', v_locations_created
);
END;
$$;

View File

@@ -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;
$$;

View File

@@ -0,0 +1,124 @@
-- Function to backfill missing company data from submission data
CREATE OR REPLACE FUNCTION backfill_company_data()
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_companies_updated INTEGER := 0;
v_headquarters_added INTEGER := 0;
v_website_added INTEGER := 0;
v_founded_year_added INTEGER := 0;
v_description_added INTEGER := 0;
v_logo_added INTEGER := 0;
v_company RECORD;
v_submission RECORD;
BEGIN
-- Find companies with missing data that have approved submissions
FOR v_company IN
SELECT DISTINCT c.id, c.name, c.slug,
c.headquarters_location, c.website_url, c.founded_year,
c.description, c.logo_url
FROM companies c
WHERE c.headquarters_location IS NULL
OR c.website_url IS NULL
OR c.founded_year IS NULL
OR c.description IS NULL
OR c.logo_url IS NULL
LOOP
-- Find the most recent approved submission for this company with the missing data
SELECT
cs.headquarters_location,
cs.website_url,
cs.founded_year,
cs.description,
cs.logo_url
INTO v_submission
FROM company_submissions cs
JOIN content_submissions sub ON sub.id = cs.submission_id
WHERE cs.company_id = v_company.id
AND sub.status = 'approved'
AND (
(v_company.headquarters_location IS NULL AND cs.headquarters_location IS NOT NULL) OR
(v_company.website_url IS NULL AND cs.website_url IS NOT NULL) OR
(v_company.founded_year IS NULL AND cs.founded_year IS NOT NULL) OR
(v_company.description IS NULL AND cs.description IS NOT NULL) OR
(v_company.logo_url IS NULL AND cs.logo_url IS NOT NULL)
)
ORDER BY sub.created_at DESC
LIMIT 1;
-- If we found submission data, update the company
IF FOUND THEN
DECLARE
v_updated BOOLEAN := FALSE;
BEGIN
-- Update headquarters_location if missing
IF v_company.headquarters_location IS NULL AND v_submission.headquarters_location IS NOT NULL THEN
UPDATE companies
SET headquarters_location = v_submission.headquarters_location
WHERE id = v_company.id;
v_headquarters_added := v_headquarters_added + 1;
v_updated := TRUE;
RAISE NOTICE 'Added headquarters for company: % (id: %)', v_company.name, v_company.id;
END IF;
-- Update website_url if missing
IF v_company.website_url IS NULL AND v_submission.website_url IS NOT NULL THEN
UPDATE companies
SET website_url = v_submission.website_url
WHERE id = v_company.id;
v_website_added := v_website_added + 1;
v_updated := TRUE;
RAISE NOTICE 'Added website for company: % (id: %)', v_company.name, v_company.id;
END IF;
-- Update founded_year if missing
IF v_company.founded_year IS NULL AND v_submission.founded_year IS NOT NULL THEN
UPDATE companies
SET founded_year = v_submission.founded_year
WHERE id = v_company.id;
v_founded_year_added := v_founded_year_added + 1;
v_updated := TRUE;
RAISE NOTICE 'Added founded year for company: % (id: %)', v_company.name, v_company.id;
END IF;
-- Update description if missing
IF v_company.description IS NULL AND v_submission.description IS NOT NULL THEN
UPDATE companies
SET description = v_submission.description
WHERE id = v_company.id;
v_description_added := v_description_added + 1;
v_updated := TRUE;
RAISE NOTICE 'Added description for company: % (id: %)', v_company.name, v_company.id;
END IF;
-- Update logo_url if missing
IF v_company.logo_url IS NULL AND v_submission.logo_url IS NOT NULL THEN
UPDATE companies
SET logo_url = v_submission.logo_url
WHERE id = v_company.id;
v_logo_added := v_logo_added + 1;
v_updated := TRUE;
RAISE NOTICE 'Added logo for company: % (id: %)', v_company.name, v_company.id;
END IF;
IF v_updated THEN
v_companies_updated := v_companies_updated + 1;
END IF;
END;
END IF;
END LOOP;
RETURN jsonb_build_object(
'success', true,
'companies_updated', v_companies_updated,
'headquarters_added', v_headquarters_added,
'website_added', v_website_added,
'founded_year_added', v_founded_year_added,
'description_added', v_description_added,
'logo_added', v_logo_added
);
END;
$$;