mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 15:31:12 -05:00
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.
This commit is contained in:
116
src/components/admin/CompanyDataBackfill.tsx
Normal file
116
src/components/admin/CompanyDataBackfill.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6632,6 +6632,7 @@ export type Database = {
|
|||||||
Args: { target_user_id: string }
|
Args: { target_user_id: string }
|
||||||
Returns: undefined
|
Returns: undefined
|
||||||
}
|
}
|
||||||
|
backfill_company_data: { Args: never; Returns: Json }
|
||||||
backfill_park_locations: { Args: never; Returns: Json }
|
backfill_park_locations: { Args: never; Returns: Json }
|
||||||
backfill_ride_data: { Args: never; Returns: Json }
|
backfill_ride_data: { Args: never; Returns: Json }
|
||||||
backfill_sort_orders: { Args: never; Returns: undefined }
|
backfill_sort_orders: { Args: never; Returns: undefined }
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { TestDataGenerator } from '@/components/admin/TestDataGenerator';
|
|||||||
import { IntegrationTestRunner } from '@/components/admin/IntegrationTestRunner';
|
import { IntegrationTestRunner } from '@/components/admin/IntegrationTestRunner';
|
||||||
import { ParkLocationBackfill } from '@/components/admin/ParkLocationBackfill';
|
import { ParkLocationBackfill } from '@/components/admin/ParkLocationBackfill';
|
||||||
import { RideDataBackfill } from '@/components/admin/RideDataBackfill';
|
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 { Loader2, Save, Clock, Users, Bell, Shield, Settings, Trash2, Plug, AlertTriangle, Lock, TestTube, RefreshCw, Info, AlertCircle, Database } from 'lucide-react';
|
||||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||||
|
|
||||||
@@ -940,6 +941,7 @@ export default function AdminSettings() {
|
|||||||
|
|
||||||
<ParkLocationBackfill />
|
<ParkLocationBackfill />
|
||||||
<RideDataBackfill />
|
<RideDataBackfill />
|
||||||
|
<CompanyDataBackfill />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
|||||||
54
supabase/functions/backfill-company-data/index.ts
Normal file
54
supabase/functions/backfill-company-data/index.ts
Normal 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' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -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;
|
||||||
|
$$;
|
||||||
Reference in New Issue
Block a user