Files
thrilltrack-explorer/src-old/components/moderation/SubmissionItemsList.tsx

339 lines
11 KiB
TypeScript

import { useState, useEffect, memo } from 'react';
import { supabase } from '@/lib/supabaseClient';
import { SubmissionChangesDisplay } from './SubmissionChangesDisplay';
import { PhotoSubmissionDisplay } from './PhotoSubmissionDisplay';
import { RichParkDisplay } from './displays/RichParkDisplay';
import { RichRideDisplay } from './displays/RichRideDisplay';
import { RichCompanyDisplay } from './displays/RichCompanyDisplay';
import { RichRideModelDisplay } from './displays/RichRideModelDisplay';
import { RichTimelineEventDisplay } from './displays/RichTimelineEventDisplay';
import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import { AlertCircle, Loader2 } from 'lucide-react';
import { format } from 'date-fns';
import type { SubmissionItemData } from '@/types/submissions';
import type { ParkSubmissionData, RideSubmissionData, CompanySubmissionData, RideModelSubmissionData } from '@/types/submission-data';
import type { TimelineSubmissionData } from '@/types/timeline';
import { getErrorMessage, handleNonCriticalError } from '@/lib/errorHandler';
import { ModerationErrorBoundary } from '@/components/error/ModerationErrorBoundary';
interface SubmissionItemsListProps {
submissionId: string;
view?: 'summary' | 'detailed';
showImages?: boolean;
}
export const SubmissionItemsList = memo(function SubmissionItemsList({
submissionId,
view = 'summary',
showImages = true
}: SubmissionItemsListProps) {
const [items, setItems] = useState<SubmissionItemData[]>([]);
const [hasPhotos, setHasPhotos] = useState(false);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchSubmissionItems();
}, [submissionId]);
const fetchSubmissionItems = async () => {
try {
// Only show skeleton on initial load, show refreshing indicator on refresh
if (loading) {
setLoading(true);
} else {
setRefreshing(true);
}
setError(null);
// Use database function to fetch submission items with entity data in one query
// This eliminates N+1 query problem and properly handles RLS/AAL2 checks
const { data: itemsData, error: itemsError } = await supabase
.rpc('get_submission_items_with_entities', {
p_submission_id: submissionId
});
if (itemsError) throw itemsError;
// Transform to expected format with better null handling
const transformedItems = (itemsData || []).map((item: any) => {
// Ensure entity_data is at least an empty object, never null
const safeEntityData = item.entity_data && typeof item.entity_data === 'object'
? item.entity_data
: {};
return {
...item,
item_data: safeEntityData,
entity_data: item.entity_data // Keep original for debugging
};
});
// Check for photo submissions (using array query to avoid 406)
const { data: photoData, error: photoError } = await supabase
.from('photo_submissions')
.select('id')
.eq('submission_id', submissionId);
if (photoError) {
handleNonCriticalError(photoError, {
action: 'Check photo submissions',
metadata: { submissionId }
});
}
setItems(transformedItems as SubmissionItemData[]);
setHasPhotos(!!(photoData && photoData.length > 0));
} catch (err) {
handleNonCriticalError(err, {
action: 'Fetch submission items',
metadata: { submissionId }
});
setError('Failed to load submission details');
} finally {
setLoading(false);
setRefreshing(false);
}
};
if (loading) {
return (
<div className="flex flex-col gap-2">
<Skeleton className="h-16 w-full" />
{view === 'detailed' && <Skeleton className="h-32 w-full" />}
</div>
);
}
if (error) {
return (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
);
}
if (items.length === 0 && !hasPhotos) {
return (
<div className="text-sm text-muted-foreground">
No items found for this submission
</div>
);
}
// Render item with appropriate display component
const renderItem = (item: SubmissionItemData) => {
// SubmissionItemData from submissions.ts has item_data property
const entityData = item.item_data;
const actionType = item.action_type || 'create';
// Show item metadata (order_index, depends_on, timestamps, test data flag)
const itemMetadata = (
<div className="flex flex-wrap items-center gap-2 mb-2 text-xs text-muted-foreground">
<Badge variant="outline" className="font-mono">
#{item.order_index ?? 0}
</Badge>
{item.depends_on && (
<Badge variant="outline" className="text-xs">
Depends on: {item.depends_on.slice(0, 8)}...
</Badge>
)}
{(item as any).is_test_data && (
<Badge variant="outline" className="bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300">
Test Data
</Badge>
)}
{item.created_at && (
<span className="font-mono">
Created: {format(new Date(item.created_at), 'MMM d, HH:mm:ss')}
</span>
)}
{item.updated_at && item.updated_at !== item.created_at && (
<span className="font-mono">
Updated: {format(new Date(item.updated_at), 'MMM d, HH:mm:ss')}
</span>
)}
</div>
);
// Use summary view for compact display
if (view === 'summary') {
return (
<>
{itemMetadata}
<SubmissionChangesDisplay
item={item}
view={view}
showImages={showImages}
submissionId={submissionId}
/>
</>
);
}
// Use rich displays for detailed view - show BOTH rich display AND field-by-field changes
if (item.item_type === 'park' && entityData) {
return (
<>
{itemMetadata}
<RichParkDisplay
data={entityData as unknown as ParkSubmissionData}
actionType={actionType}
/>
<div className="mt-6 pt-6 border-t">
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">
All Fields (Detailed View)
</div>
<SubmissionChangesDisplay
item={item}
view="detailed"
showImages={showImages}
submissionId={submissionId}
/>
</div>
</>
);
}
if (item.item_type === 'ride' && entityData) {
return (
<>
{itemMetadata}
<RichRideDisplay
data={entityData as unknown as RideSubmissionData}
actionType={actionType}
/>
<div className="mt-6 pt-6 border-t">
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">
All Fields (Detailed View)
</div>
<SubmissionChangesDisplay
item={item}
view="detailed"
showImages={showImages}
submissionId={submissionId}
/>
</div>
</>
);
}
if ((['manufacturer', 'operator', 'designer', 'property_owner'] as const).some(type => type === item.item_type) && entityData) {
return (
<>
{itemMetadata}
<RichCompanyDisplay
data={entityData as unknown as CompanySubmissionData}
actionType={actionType}
/>
<div className="mt-6 pt-6 border-t">
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">
All Fields (Detailed View)
</div>
<SubmissionChangesDisplay
item={item}
view="detailed"
showImages={showImages}
submissionId={submissionId}
/>
</div>
</>
);
}
if (item.item_type === 'ride_model' && entityData) {
return (
<>
{itemMetadata}
<RichRideModelDisplay
data={entityData as unknown as RideModelSubmissionData}
actionType={actionType}
/>
<div className="mt-6 pt-6 border-t">
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">
All Fields (Detailed View)
</div>
<SubmissionChangesDisplay
item={item}
view="detailed"
showImages={showImages}
submissionId={submissionId}
/>
</div>
</>
);
}
if ((item.item_type === 'milestone' || item.item_type === 'timeline_event') && entityData) {
return (
<>
{itemMetadata}
<RichTimelineEventDisplay
data={entityData as unknown as TimelineSubmissionData}
actionType={actionType}
/>
<div className="mt-6 pt-6 border-t">
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">
All Fields (Detailed View)
</div>
<SubmissionChangesDisplay
item={item}
view="detailed"
showImages={showImages}
submissionId={submissionId}
/>
</div>
</>
);
}
// Fallback to SubmissionChangesDisplay
return (
<>
{itemMetadata}
<SubmissionChangesDisplay
item={item}
view={view}
showImages={showImages}
submissionId={submissionId}
/>
</>
);
};
return (
<ModerationErrorBoundary submissionId={submissionId}>
<div className={view === 'summary' ? 'flex flex-col gap-3' : 'flex flex-col gap-6'}>
{refreshing && (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" />
<span>Refreshing...</span>
</div>
)}
{/* Show regular submission items */}
{items.map((item) => (
<div key={item.id} className={view === 'summary' ? 'border-l-2 border-primary/20 pl-3' : ''}>
{renderItem(item)}
</div>
))}
{/* Show photo submission if exists */}
{hasPhotos && (
<div className={view === 'summary' ? 'border-l-2 border-primary/20 pl-3' : ''}>
<PhotoSubmissionDisplay submissionId={submissionId} />
</div>
)}
</div>
</ModerationErrorBoundary>
);
});