diff --git a/src/components/moderation/SubmissionItemsList.tsx b/src/components/moderation/SubmissionItemsList.tsx index 6256c706..b9bac9f5 100644 --- a/src/components/moderation/SubmissionItemsList.tsx +++ b/src/components/moderation/SubmissionItemsList.tsx @@ -6,6 +6,7 @@ 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'; @@ -13,6 +14,7 @@ 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'; @@ -270,6 +272,29 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({ ); } + if ((item.item_type === 'milestone' || item.item_type === 'timeline_event') && entityData) { + return ( + <> + {itemMetadata} + +
+
+ All Fields (Detailed View) +
+ +
+ + ); + } + // Fallback to SubmissionChangesDisplay return ( <> diff --git a/src/components/moderation/TimelineEventPreview.tsx b/src/components/moderation/TimelineEventPreview.tsx index 582a4fec..fe597702 100644 --- a/src/components/moderation/TimelineEventPreview.tsx +++ b/src/components/moderation/TimelineEventPreview.tsx @@ -1,38 +1,93 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Calendar, Tag } from 'lucide-react'; +import { Calendar, Tag, Building2, MapPin } from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; +import { FlexibleDateDisplay } from '@/components/ui/flexible-date-display'; import type { TimelineSubmissionData } from '@/types/timeline'; +import { useEffect, useState } from 'react'; +import { supabase } from '@/lib/supabaseClient'; interface TimelineEventPreviewProps { data: TimelineSubmissionData; } export function TimelineEventPreview({ data }: TimelineEventPreviewProps) { + const [entityName, setEntityName] = useState(null); + + useEffect(() => { + if (!data?.entity_id || !data?.entity_type) return; + + const fetchEntityName = async () => { + const table = data.entity_type === 'park' ? 'parks' : 'rides'; + const { data: entity } = await supabase + .from(table) + .select('name') + .eq('id', data.entity_id) + .single(); + setEntityName(entity?.name || null); + }; + + fetchEntityName(); + }, [data?.entity_id, data?.entity_type]); + const formatEventType = (type: string) => { return type.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()); }; + + const getEventTypeColor = (type: string) => { + const colors: Record = { + opening: 'bg-green-600', + closure: 'bg-red-600', + reopening: 'bg-blue-600', + renovation: 'bg-purple-600', + expansion: 'bg-indigo-600', + acquisition: 'bg-amber-600', + name_change: 'bg-cyan-600', + operator_change: 'bg-orange-600', + owner_change: 'bg-orange-600', + location_change: 'bg-pink-600', + status_change: 'bg-yellow-600', + milestone: 'bg-emerald-600', + }; + return colors[type] || 'bg-gray-600'; + }; return ( - - Timeline Event: {data.title} + + {data.title} +
+ + {formatEventType(data.event_type)} + + + {data.entity_type} + +
+ {entityName && ( +
+ + Entity: + {entityName} +
+ )} +
- Event Type: -

- {formatEventType(data.event_type)} -

-
-
- Date: -

+ Event Date: +

- {new Date(data.event_date).toLocaleDateString()} - ({data.event_date_precision}) + +

+

+ Precision: {data.event_date_precision}

@@ -45,6 +100,20 @@ export function TimelineEventPreview({ data }: TimelineEventPreviewProps) { )} + + {(data.from_entity_id || data.to_entity_id) && ( +
+ + Related entities: {data.from_entity_id ? 'From entity' : ''} {data.to_entity_id ? 'To entity' : ''} +
+ )} + + {(data.from_location_id || data.to_location_id) && ( +
+ + Location change involved +
+ )} {data.description && (
diff --git a/src/components/moderation/displays/RichTimelineEventDisplay.tsx b/src/components/moderation/displays/RichTimelineEventDisplay.tsx new file mode 100644 index 00000000..ab715731 --- /dev/null +++ b/src/components/moderation/displays/RichTimelineEventDisplay.tsx @@ -0,0 +1,266 @@ +import { Calendar, Tag, ArrowRight, MapPin, Building2, Clock } from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; +import { FlexibleDateDisplay } from '@/components/ui/flexible-date-display'; +import type { TimelineSubmissionData } from '@/types/timeline'; +import { useEffect, useState } from 'react'; +import { supabase } from '@/lib/supabaseClient'; + +interface RichTimelineEventDisplayProps { + data: TimelineSubmissionData; + actionType: 'create' | 'edit' | 'delete'; +} + +export function RichTimelineEventDisplay({ data, actionType }: RichTimelineEventDisplayProps) { + const [entityName, setEntityName] = useState(null); + const [parkContext, setParkContext] = useState(null); + const [fromEntity, setFromEntity] = useState(null); + const [toEntity, setToEntity] = useState(null); + const [fromLocation, setFromLocation] = useState(null); + const [toLocation, setToLocation] = useState(null); + + useEffect(() => { + if (!data) return; + + const fetchRelatedData = async () => { + // Fetch the main entity this timeline event is for + if (data.entity_id && data.entity_type) { + if (data.entity_type === 'park') { + const { data: park } = await supabase + .from('parks') + .select('name') + .eq('id', data.entity_id) + .single(); + setEntityName(park?.name || null); + } else if (data.entity_type === 'ride') { + const { data: ride } = await supabase + .from('rides') + .select('name, park:parks(name)') + .eq('id', data.entity_id) + .single(); + setEntityName(ride?.name || null); + setParkContext((ride?.park as any)?.name || null); + } + } + + // Fetch from/to entities for relational changes + if (data.from_entity_id) { + const { data: entity } = await supabase + .from('companies') + .select('name') + .eq('id', data.from_entity_id) + .single(); + setFromEntity(entity?.name || null); + } + + if (data.to_entity_id) { + const { data: entity } = await supabase + .from('companies') + .select('name') + .eq('id', data.to_entity_id) + .single(); + setToEntity(entity?.name || null); + } + + // Fetch from/to locations for location changes + if (data.from_location_id) { + const { data: loc } = await supabase + .from('locations') + .select('*') + .eq('id', data.from_location_id) + .single(); + setFromLocation(loc); + } + + if (data.to_location_id) { + const { data: loc } = await supabase + .from('locations') + .select('*') + .eq('id', data.to_location_id) + .single(); + setToLocation(loc); + } + }; + + fetchRelatedData(); + }, [data.entity_id, data.entity_type, data.from_entity_id, data.to_entity_id, data.from_location_id, data.to_location_id]); + + const formatEventType = (type: string) => { + return type.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()); + }; + + const getEventTypeColor = (type: string) => { + switch (type) { + case 'opening': return 'bg-green-600'; + case 'closure': return 'bg-red-600'; + case 'reopening': return 'bg-blue-600'; + case 'renovation': return 'bg-purple-600'; + case 'expansion': return 'bg-indigo-600'; + case 'acquisition': return 'bg-amber-600'; + case 'name_change': return 'bg-cyan-600'; + case 'operator_change': + case 'owner_change': return 'bg-orange-600'; + case 'location_change': return 'bg-pink-600'; + case 'status_change': return 'bg-yellow-600'; + case 'milestone': return 'bg-emerald-600'; + default: return 'bg-gray-600'; + } + }; + + const getPrecisionIcon = (precision: string) => { + switch (precision) { + case 'day': return '📅'; + case 'month': return '📆'; + case 'year': return '🗓️'; + default: return '📅'; + } + }; + + const formatLocation = (loc: any) => { + if (!loc) return null; + const parts = [loc.city, loc.state_province, loc.country].filter(Boolean); + return parts.join(', '); + }; + + return ( +
+ {/* Header Section */} +
+
+ +
+
+

{data.title}

+
+ + {formatEventType(data.event_type)} + + {actionType === 'create' && ( + New Event + )} + {actionType === 'edit' && ( + Edit Event + )} + {actionType === 'delete' && ( + Delete Event + )} +
+
+
+ + + + {/* Entity Context Section */} +
+
+ + Event For: + + {entityName || 'Loading...'} + + {data.entity_type} + + +
+ + {parkContext && ( +
+ + Park: + {parkContext} +
+ )} +
+ + + + {/* Event Date Section */} +
+
+ + Event Date: +
+
+ {getPrecisionIcon(data.event_date_precision)} +
+
+ +
+
+ Precision: {data.event_date_precision} +
+
+
+
+ + {/* Change Details Section */} + {(data.from_value || data.to_value || fromEntity || toEntity) && ( + <> + +
+
Change Details:
+
+
+
From
+
+ {fromEntity || data.from_value || '—'} +
+
+ +
+
To
+
+ {toEntity || data.to_value || '—'} +
+
+
+
+ + )} + + {/* Location Change Section */} + {(fromLocation || toLocation) && ( + <> + +
+
+ + Location Change: +
+
+
+
From
+
+ {formatLocation(fromLocation) || '—'} +
+
+ +
+
To
+
+ {formatLocation(toLocation) || '—'} +
+
+
+
+ + )} + + {/* Description Section */} + {data.description && ( + <> + +
+
Description:
+

+ {data.description} +

+
+ + )} +
+ ); +}