mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 09:11:12 -05:00
Refactor moderation data fetching
This commit is contained in:
@@ -48,125 +48,21 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
|
|||||||
}
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// Fetch submission items with entity data
|
// Use database function to fetch submission items with entity data in one query
|
||||||
// Note: entity_data is pre-loaded from the view, but we need to fetch from raw table
|
// This eliminates N+1 query problem and properly handles RLS/AAL2 checks
|
||||||
// when this component is used standalone
|
|
||||||
const { data: itemsData, error: itemsError } = await supabase
|
const { data: itemsData, error: itemsError } = await supabase
|
||||||
.from('submission_items')
|
.rpc('get_submission_items_with_entities', {
|
||||||
.select('*')
|
p_submission_id: submissionId
|
||||||
.eq('submission_id', submissionId)
|
});
|
||||||
.order('order_index');
|
|
||||||
|
|
||||||
if (itemsError) throw itemsError;
|
if (itemsError) throw itemsError;
|
||||||
|
|
||||||
// Fetch entity data for each item based on item_type and typed FK columns
|
// Transform to expected format
|
||||||
const transformedItems = await Promise.all(
|
const transformedItems = (itemsData || []).map((item: any) => ({
|
||||||
(itemsData || []).map(async (item) => {
|
...item,
|
||||||
// Determine which FK column to use based on item_type
|
item_data: item.entity_data || {},
|
||||||
let itemDataId: string | null = null;
|
entity_data: item.entity_data
|
||||||
switch (item.item_type) {
|
}));
|
||||||
case 'park':
|
|
||||||
itemDataId = item.park_submission_id;
|
|
||||||
break;
|
|
||||||
case 'ride':
|
|
||||||
itemDataId = item.ride_submission_id;
|
|
||||||
break;
|
|
||||||
case 'photo':
|
|
||||||
case 'photo_edit':
|
|
||||||
case 'photo_delete':
|
|
||||||
itemDataId = item.photo_submission_id;
|
|
||||||
break;
|
|
||||||
case 'manufacturer':
|
|
||||||
case 'operator':
|
|
||||||
case 'designer':
|
|
||||||
case 'property_owner':
|
|
||||||
itemDataId = item.company_submission_id;
|
|
||||||
break;
|
|
||||||
case 'ride_model':
|
|
||||||
itemDataId = item.ride_model_submission_id;
|
|
||||||
break;
|
|
||||||
case 'milestone':
|
|
||||||
case 'timeline_event':
|
|
||||||
itemDataId = item.timeline_event_submission_id;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!itemDataId) {
|
|
||||||
return { ...item, item_data: {}, entity_data: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let entityData = null;
|
|
||||||
|
|
||||||
// Fetch from appropriate table based on item_type
|
|
||||||
switch (item.item_type) {
|
|
||||||
case 'park': {
|
|
||||||
const { data } = await supabase
|
|
||||||
.from('park_submissions')
|
|
||||||
.select('*')
|
|
||||||
.eq('id', itemDataId)
|
|
||||||
.maybeSingle();
|
|
||||||
entityData = data as any;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'ride': {
|
|
||||||
const { data } = await supabase
|
|
||||||
.from('ride_submissions')
|
|
||||||
.select('*')
|
|
||||||
.eq('id', itemDataId)
|
|
||||||
.maybeSingle();
|
|
||||||
entityData = data as any;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'manufacturer':
|
|
||||||
case 'operator':
|
|
||||||
case 'designer':
|
|
||||||
case 'property_owner': {
|
|
||||||
const { data } = await supabase
|
|
||||||
.from('company_submissions')
|
|
||||||
.select('*')
|
|
||||||
.eq('id', itemDataId)
|
|
||||||
.maybeSingle();
|
|
||||||
entityData = data as any;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'photo': {
|
|
||||||
const { data } = await supabase
|
|
||||||
.from('photo_submissions')
|
|
||||||
.select('*')
|
|
||||||
.eq('id', itemDataId)
|
|
||||||
.maybeSingle();
|
|
||||||
entityData = data as any;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'ride_model': {
|
|
||||||
const { data } = await supabase
|
|
||||||
.from('ride_model_submissions')
|
|
||||||
.select('*')
|
|
||||||
.eq('id', itemDataId)
|
|
||||||
.maybeSingle();
|
|
||||||
entityData = data as any;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
entityData = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
item_data: entityData || {},
|
|
||||||
entity_data: entityData
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn('Failed to fetch entity data for item', {
|
|
||||||
itemId: item.id,
|
|
||||||
itemType: item.item_type,
|
|
||||||
error: getErrorMessage(err)
|
|
||||||
});
|
|
||||||
return { ...item, item_data: {}, entity_data: null };
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check for photo submissions (using array query to avoid 406)
|
// Check for photo submissions (using array query to avoid 406)
|
||||||
const { data: photoData, error: photoError } = await supabase
|
const { data: photoData, error: photoError } = await supabase
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ interface RichCompanyDisplayProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function RichCompanyDisplay({ data, actionType, showAllFields = true }: RichCompanyDisplayProps) {
|
export function RichCompanyDisplay({ data, actionType, showAllFields = true }: RichCompanyDisplayProps) {
|
||||||
const getCompanyTypeColor = (type: string) => {
|
const getCompanyTypeColor = (type: string | undefined) => {
|
||||||
|
if (!type) return 'bg-gray-500';
|
||||||
switch (type.toLowerCase()) {
|
switch (type.toLowerCase()) {
|
||||||
case 'manufacturer': return 'bg-blue-500';
|
case 'manufacturer': return 'bg-blue-500';
|
||||||
case 'operator': return 'bg-green-500';
|
case 'operator': return 'bg-green-500';
|
||||||
@@ -31,7 +32,7 @@ export function RichCompanyDisplay({ data, actionType, showAllFields = true }: R
|
|||||||
<h3 className="text-xl font-bold text-foreground truncate">{data.name}</h3>
|
<h3 className="text-xl font-bold text-foreground truncate">{data.name}</h3>
|
||||||
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
||||||
<Badge className={`${getCompanyTypeColor(data.company_type)} text-white text-xs`}>
|
<Badge className={`${getCompanyTypeColor(data.company_type)} text-white text-xs`}>
|
||||||
{data.company_type?.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
{(data.company_type || 'Unknown')?.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||||
</Badge>
|
</Badge>
|
||||||
{data.person_type && (
|
{data.person_type && (
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
|
|||||||
@@ -5612,6 +5612,29 @@ export type Database = {
|
|||||||
Args: { p_item_data_id: string; p_item_type: string }
|
Args: { p_item_data_id: string; p_item_type: string }
|
||||||
Returns: Json
|
Returns: Json
|
||||||
}
|
}
|
||||||
|
get_submission_items_with_entities: {
|
||||||
|
Args: { p_submission_id: string }
|
||||||
|
Returns: {
|
||||||
|
action_type: string
|
||||||
|
approved_by: string
|
||||||
|
company_submission_id: string
|
||||||
|
created_at: string
|
||||||
|
depends_on: string
|
||||||
|
entity_data: Json
|
||||||
|
id: string
|
||||||
|
item_type: string
|
||||||
|
order_index: number
|
||||||
|
park_submission_id: string
|
||||||
|
photo_submission_id: string
|
||||||
|
rejected_by: string
|
||||||
|
ride_model_submission_id: string
|
||||||
|
ride_submission_id: string
|
||||||
|
status: string
|
||||||
|
submission_id: string
|
||||||
|
timeline_event_submission_id: string
|
||||||
|
updated_at: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
get_user_management_permissions: {
|
get_user_management_permissions: {
|
||||||
Args: { _user_id: string }
|
Args: { _user_id: string }
|
||||||
Returns: Json
|
Returns: Json
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
-- Create function to fetch submission items with entity data
|
||||||
|
-- This replaces the N individual queries with a single efficient query
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION get_submission_items_with_entities(p_submission_id uuid)
|
||||||
|
RETURNS TABLE (
|
||||||
|
id uuid,
|
||||||
|
submission_id uuid,
|
||||||
|
item_type text,
|
||||||
|
action_type text,
|
||||||
|
status text,
|
||||||
|
order_index integer,
|
||||||
|
depends_on uuid,
|
||||||
|
park_submission_id uuid,
|
||||||
|
ride_submission_id uuid,
|
||||||
|
company_submission_id uuid,
|
||||||
|
photo_submission_id uuid,
|
||||||
|
ride_model_submission_id uuid,
|
||||||
|
timeline_event_submission_id uuid,
|
||||||
|
approved_by uuid,
|
||||||
|
rejected_by uuid,
|
||||||
|
created_at timestamptz,
|
||||||
|
updated_at timestamptz,
|
||||||
|
entity_data jsonb
|
||||||
|
)
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
si.id,
|
||||||
|
si.submission_id,
|
||||||
|
si.item_type,
|
||||||
|
si.action_type,
|
||||||
|
si.status,
|
||||||
|
si.order_index,
|
||||||
|
si.depends_on,
|
||||||
|
si.park_submission_id,
|
||||||
|
si.ride_submission_id,
|
||||||
|
si.company_submission_id,
|
||||||
|
si.photo_submission_id,
|
||||||
|
si.ride_model_submission_id,
|
||||||
|
si.timeline_event_submission_id,
|
||||||
|
si.approved_by,
|
||||||
|
si.rejected_by,
|
||||||
|
si.created_at,
|
||||||
|
si.updated_at,
|
||||||
|
-- Join entity data based on item_type
|
||||||
|
CASE
|
||||||
|
WHEN si.item_type = 'park' THEN
|
||||||
|
(SELECT to_jsonb(ps.*) FROM park_submissions ps WHERE ps.id = si.park_submission_id)
|
||||||
|
WHEN si.item_type = 'ride' THEN
|
||||||
|
(SELECT to_jsonb(rs.*) FROM ride_submissions rs WHERE rs.id = si.ride_submission_id)
|
||||||
|
WHEN si.item_type IN ('manufacturer', 'operator', 'designer', 'property_owner') THEN
|
||||||
|
(SELECT to_jsonb(cs.*) FROM company_submissions cs WHERE cs.id = si.company_submission_id)
|
||||||
|
WHEN si.item_type IN ('photo', 'photo_edit', 'photo_delete') THEN
|
||||||
|
(SELECT to_jsonb(phs.*) FROM photo_submissions phs WHERE phs.id = si.photo_submission_id)
|
||||||
|
WHEN si.item_type = 'ride_model' THEN
|
||||||
|
(SELECT to_jsonb(rms.*) FROM ride_model_submissions rms WHERE rms.id = si.ride_model_submission_id)
|
||||||
|
ELSE NULL
|
||||||
|
END AS entity_data
|
||||||
|
FROM submission_items si
|
||||||
|
WHERE si.submission_id = p_submission_id
|
||||||
|
ORDER BY si.order_index;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION get_submission_items_with_entities IS
|
||||||
|
'Fetch submission items with their entity data in a single query. Uses SECURITY DEFINER to access submission tables with proper RLS context from the parent content_submissions access.';
|
||||||
|
|
||||||
|
-- Grant execute to authenticated users
|
||||||
|
GRANT EXECUTE ON FUNCTION get_submission_items_with_entities(uuid) TO authenticated;
|
||||||
Reference in New Issue
Block a user