diff --git a/src/components/moderation/SubmissionChangesDisplay.tsx b/src/components/moderation/SubmissionChangesDisplay.tsx index 11ac2645..d0db5cb8 100644 --- a/src/components/moderation/SubmissionChangesDisplay.tsx +++ b/src/components/moderation/SubmissionChangesDisplay.tsx @@ -6,7 +6,9 @@ import { PhotoAdditionPreview, PhotoEditPreview, PhotoDeletionPreview } from './ import { detectChanges, type ChangesSummary } from '@/lib/submissionChangeDetection'; import type { SubmissionItemData } from '@/types/submissions'; import type { SubmissionItemWithDeps } from '@/lib/submissionItemsService'; -import { Building2, Train, MapPin, Building, User, ImageIcon, Trash2, Edit, Plus, AlertTriangle } from 'lucide-react'; +import { Building2, Train, MapPin, Building, User, ImageIcon, Trash2, Edit, Plus, AlertTriangle, Calendar } from 'lucide-react'; +import { TimelineEventPreview } from './TimelineEventPreview'; +import type { TimelineSubmissionData } from '@/types/timeline'; interface SubmissionChangesDisplayProps { item: SubmissionItemData | SubmissionItemWithDeps | { item_data?: any; original_data?: any; item_type: string; action_type?: 'create' | 'edit' | 'delete' }; @@ -62,6 +64,8 @@ export function SubmissionChangesDisplay({ case 'photo': case 'photo_edit': case 'photo_delete': return ; + case 'milestone': + case 'timeline_event': return ; default: return ; } }; @@ -111,6 +115,35 @@ export function SubmissionChangesDisplay({ ); } + + // Special compact display for milestone/timeline events + if (item.item_type === 'milestone' || item.item_type === 'timeline_event') { + const milestoneData = item.item_data as TimelineSubmissionData; + const eventType = milestoneData.event_type?.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()) || 'Event'; + const eventDate = milestoneData.event_date ? new Date(milestoneData.event_date).toLocaleDateString() : 'No date'; + + return ( +
+
+ {getEntityIcon()} + {milestoneData.title} + {getActionBadge()} + + {eventType} + +
+
+ + {eventDate} + {milestoneData.from_value && milestoneData.to_value && ( + + • {milestoneData.from_value} → {milestoneData.to_value} + + )} +
+
+ ); + } return (
@@ -193,6 +226,23 @@ export function SubmissionChangesDisplay({
); } + + // Detailed view - special handling for milestone/timeline events + if (item.item_type === 'milestone' || item.item_type === 'timeline_event') { + const milestoneData = item.item_data as TimelineSubmissionData; + return ( +
+
+ {getEntityIcon()} +

{milestoneData.title}

+ {getActionBadge()} + Timeline Event +
+ + +
+ ); + } // Detailed view for other items return ( diff --git a/src/lib/entitySubmissionHelpers.ts b/src/lib/entitySubmissionHelpers.ts index 31bc5635..753dad8a 100644 --- a/src/lib/entitySubmissionHelpers.ts +++ b/src/lib/entitySubmissionHelpers.ts @@ -1167,30 +1167,7 @@ export async function submitTimelineEventUpdate( throw new Error('Failed to fetch original timeline event'); } - // Create submission - const content: Json = { - action: 'edit', - event_id: eventId, - entity_type: originalEvent.entity_type, - }; - - const { data: submission, error: submissionError } = await supabase - .from('content_submissions') - .insert({ - user_id: userId, - submission_type: 'milestone', - content, - status: 'pending', - approval_mode: 'full', - }) - .select() - .single(); - - if (submissionError || !submission) { - throw new Error('Failed to create timeline event update submission'); - } - - // Create submission item + // Prepare item data const itemData: Record = { entity_type: originalEvent.entity_type, entity_id: originalEvent.entity_id, @@ -1205,28 +1182,41 @@ export async function submitTimelineEventUpdate( to_entity_id: data.to_entity_id, from_location_id: data.from_location_id, to_location_id: data.to_location_id, - is_public: true, // All timeline events are public + is_public: true, }; - const { error: itemError } = await supabase - .from('submission_items') - .insert({ - submission_id: submission.id, - item_type: 'milestone', - action_type: 'edit', - item_data: itemData as unknown as Json, - original_data: originalEvent as unknown as Json, - status: 'pending', - order_index: 0, - }); + // Use atomic RPC function to create submission and item together + const { data: result, error: rpcError } = await supabase.rpc( + 'create_submission_with_items', + { + p_user_id: userId, + p_submission_type: 'milestone', + p_content: { + action: 'edit', + event_id: eventId, + entity_type: originalEvent.entity_type, + } as unknown as Json, + p_items: [ + { + item_type: 'milestone', + action_type: 'edit', + item_data: itemData, + original_data: originalEvent, + status: 'pending', + order_index: 0, + } + ] as unknown as Json[], + } + ); - if (itemError) { - throw new Error('Failed to submit timeline event update item'); + if (rpcError || !result) { + console.error('Failed to create timeline event update:', rpcError); + throw new Error('Failed to submit timeline event update'); } return { submitted: true, - submissionId: submission.id, + submissionId: result, }; } diff --git a/src/lib/entityValidationSchemas.ts b/src/lib/entityValidationSchemas.ts index 1a83b1d3..9cff8abc 100644 --- a/src/lib/entityValidationSchemas.ts +++ b/src/lib/entityValidationSchemas.ts @@ -195,18 +195,34 @@ export const milestoneValidationSchema = z.object({ title: z.string().trim().min(1, 'Event title is required').max(200, 'Title must be less than 200 characters'), description: z.string().trim().max(2000, 'Description must be less than 2000 characters').optional().or(z.literal('')), event_type: z.string().min(1, 'Event type is required'), - event_date: z.string().min(1, 'Event date is required'), - event_date_precision: z.enum(['day', 'month', 'year']).optional(), + event_date: z.string().min(1, 'Event date is required').refine((val) => { + if (!val) return true; + const date = new Date(val); + const fiveYearsFromNow = new Date(); + fiveYearsFromNow.setFullYear(fiveYearsFromNow.getFullYear() + 5); + return date <= fiveYearsFromNow; + }, 'Event date cannot be more than 5 years in the future'), + event_date_precision: z.enum(['day', 'month', 'year']).optional().default('day'), entity_type: z.string().min(1, 'Entity type is required'), entity_id: z.string().uuid('Invalid entity ID'), is_public: z.boolean().optional(), display_order: z.number().optional(), - from_value: z.string().optional(), - to_value: z.string().optional(), + from_value: z.string().trim().max(200).optional().or(z.literal('')), + to_value: z.string().trim().max(200).optional().or(z.literal('')), from_entity_id: z.string().uuid().optional().nullable(), to_entity_id: z.string().uuid().optional().nullable(), from_location_id: z.string().uuid().optional().nullable(), to_location_id: z.string().uuid().optional().nullable(), +}).refine((data) => { + // For change events, require from_value or to_value + const changeEvents = ['name_change', 'operator_change', 'owner_change', 'location_change', 'status_change']; + if (changeEvents.includes(data.event_type)) { + return data.from_value || data.to_value || data.from_entity_id || data.to_entity_id || data.from_location_id || data.to_location_id; + } + return true; +}, { + message: 'Change events must specify what changed (from/to values or entity IDs)', + path: ['from_value'], }); // ============================================ diff --git a/src/lib/moderation/entities.ts b/src/lib/moderation/entities.ts index 41be69a3..79da128a 100644 --- a/src/lib/moderation/entities.ts +++ b/src/lib/moderation/entities.ts @@ -212,6 +212,9 @@ export function getSubmissionTypeLabel(submissionType: string): string { property_owner: 'Property Owner', ride_model: 'Ride Model', photo: 'Photo', + photo_delete: 'Photo Deletion', + milestone: 'Timeline Event', + timeline_event: 'Timeline Event', review: 'Review', };