mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:11:13 -05:00
Refactor: Implement milestone moderation fixes
This commit is contained in:
@@ -6,7 +6,9 @@ import { PhotoAdditionPreview, PhotoEditPreview, PhotoDeletionPreview } from './
|
|||||||
import { detectChanges, type ChangesSummary } from '@/lib/submissionChangeDetection';
|
import { detectChanges, type ChangesSummary } from '@/lib/submissionChangeDetection';
|
||||||
import type { SubmissionItemData } from '@/types/submissions';
|
import type { SubmissionItemData } from '@/types/submissions';
|
||||||
import type { SubmissionItemWithDeps } from '@/lib/submissionItemsService';
|
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 {
|
interface SubmissionChangesDisplayProps {
|
||||||
item: SubmissionItemData | SubmissionItemWithDeps | { item_data?: any; original_data?: any; item_type: string; action_type?: 'create' | 'edit' | 'delete' };
|
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':
|
||||||
case 'photo_edit':
|
case 'photo_edit':
|
||||||
case 'photo_delete': return <ImageIcon className={iconClass} />;
|
case 'photo_delete': return <ImageIcon className={iconClass} />;
|
||||||
|
case 'milestone':
|
||||||
|
case 'timeline_event': return <Calendar className={iconClass} />;
|
||||||
default: return <MapPin className={iconClass} />;
|
default: return <MapPin className={iconClass} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -111,6 +115,35 @@ export function SubmissionChangesDisplay({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
{getEntityIcon()}
|
||||||
|
<span className="font-medium">{milestoneData.title}</span>
|
||||||
|
{getActionBadge()}
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{eventType}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground flex items-center gap-2">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
{eventDate}
|
||||||
|
{milestoneData.from_value && milestoneData.to_value && (
|
||||||
|
<span className="ml-1">
|
||||||
|
• {milestoneData.from_value} → {milestoneData.to_value}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
@@ -193,6 +226,23 @@ export function SubmissionChangesDisplay({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getEntityIcon()}
|
||||||
|
<h3 className="text-lg font-semibold">{milestoneData.title}</h3>
|
||||||
|
{getActionBadge()}
|
||||||
|
<Badge variant="secondary">Timeline Event</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TimelineEventPreview data={milestoneData} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Detailed view for other items
|
// Detailed view for other items
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1167,30 +1167,7 @@ export async function submitTimelineEventUpdate(
|
|||||||
throw new Error('Failed to fetch original timeline event');
|
throw new Error('Failed to fetch original timeline event');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create submission
|
// Prepare item data
|
||||||
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
|
|
||||||
const itemData: Record<string, any> = {
|
const itemData: Record<string, any> = {
|
||||||
entity_type: originalEvent.entity_type,
|
entity_type: originalEvent.entity_type,
|
||||||
entity_id: originalEvent.entity_id,
|
entity_id: originalEvent.entity_id,
|
||||||
@@ -1205,28 +1182,41 @@ export async function submitTimelineEventUpdate(
|
|||||||
to_entity_id: data.to_entity_id,
|
to_entity_id: data.to_entity_id,
|
||||||
from_location_id: data.from_location_id,
|
from_location_id: data.from_location_id,
|
||||||
to_location_id: data.to_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
|
// Use atomic RPC function to create submission and item together
|
||||||
.from('submission_items')
|
const { data: result, error: rpcError } = await supabase.rpc(
|
||||||
.insert({
|
'create_submission_with_items',
|
||||||
submission_id: submission.id,
|
{
|
||||||
item_type: 'milestone',
|
p_user_id: userId,
|
||||||
action_type: 'edit',
|
p_submission_type: 'milestone',
|
||||||
item_data: itemData as unknown as Json,
|
p_content: {
|
||||||
original_data: originalEvent as unknown as Json,
|
action: 'edit',
|
||||||
status: 'pending',
|
event_id: eventId,
|
||||||
order_index: 0,
|
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) {
|
if (rpcError || !result) {
|
||||||
throw new Error('Failed to submit timeline event update item');
|
console.error('Failed to create timeline event update:', rpcError);
|
||||||
|
throw new Error('Failed to submit timeline event update');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
submitted: true,
|
submitted: true,
|
||||||
submissionId: submission.id,
|
submissionId: result,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'),
|
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('')),
|
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_type: z.string().min(1, 'Event type is required'),
|
||||||
event_date: z.string().min(1, 'Event date is required'),
|
event_date: z.string().min(1, 'Event date is required').refine((val) => {
|
||||||
event_date_precision: z.enum(['day', 'month', 'year']).optional(),
|
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_type: z.string().min(1, 'Entity type is required'),
|
||||||
entity_id: z.string().uuid('Invalid entity ID'),
|
entity_id: z.string().uuid('Invalid entity ID'),
|
||||||
is_public: z.boolean().optional(),
|
is_public: z.boolean().optional(),
|
||||||
display_order: z.number().optional(),
|
display_order: z.number().optional(),
|
||||||
from_value: z.string().optional(),
|
from_value: z.string().trim().max(200).optional().or(z.literal('')),
|
||||||
to_value: z.string().optional(),
|
to_value: z.string().trim().max(200).optional().or(z.literal('')),
|
||||||
from_entity_id: z.string().uuid().optional().nullable(),
|
from_entity_id: z.string().uuid().optional().nullable(),
|
||||||
to_entity_id: z.string().uuid().optional().nullable(),
|
to_entity_id: z.string().uuid().optional().nullable(),
|
||||||
from_location_id: z.string().uuid().optional().nullable(),
|
from_location_id: z.string().uuid().optional().nullable(),
|
||||||
to_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'],
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -212,6 +212,9 @@ export function getSubmissionTypeLabel(submissionType: string): string {
|
|||||||
property_owner: 'Property Owner',
|
property_owner: 'Property Owner',
|
||||||
ride_model: 'Ride Model',
|
ride_model: 'Ride Model',
|
||||||
photo: 'Photo',
|
photo: 'Photo',
|
||||||
|
photo_delete: 'Photo Deletion',
|
||||||
|
milestone: 'Timeline Event',
|
||||||
|
timeline_event: 'Timeline Event',
|
||||||
review: 'Review',
|
review: 'Review',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user