import { memo, useState } from 'react'; import { CheckCircle, XCircle, Eye, Calendar, MessageSquare, FileText, Image, ListTree, RefreshCw, AlertCircle, Lock, Trash2, AlertTriangle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { format } from 'date-fns'; import { SubmissionItemsList } from './SubmissionItemsList'; import { MeasurementDisplay } from '@/components/ui/measurement-display'; interface ModerationItem { id: string; type: 'review' | 'content_submission'; content: any; created_at: string; updated_at?: string; user_id: string; status: string; submission_type?: string; user_profile?: { username: string; display_name?: string; avatar_url?: string; }; entity_name?: string; park_name?: string; reviewed_at?: string; reviewed_by?: string; reviewer_notes?: string; reviewer_profile?: { username: string; display_name?: string; avatar_url?: string; }; escalated?: boolean; assigned_to?: string; locked_until?: string; } import { ValidationSummary } from './ValidationSummary'; import type { ValidationResult } from '@/lib/entityValidationSchemas'; interface QueueItemProps { item: ModerationItem; isMobile: boolean; actionLoading: string | null; lockedSubmissions: Set; currentLockSubmissionId?: string; notes: Record; isAdmin: boolean; isSuperuser: boolean; queueIsLoading: boolean; onNoteChange: (id: string, value: string) => void; onApprove: (item: ModerationItem, action: 'approved' | 'rejected', notes?: string) => void; onResetToPending: (item: ModerationItem) => void; onRetryFailed: (item: ModerationItem) => void; onOpenPhotos: (photos: any[], index: number) => void; onOpenReviewManager: (submissionId: string) => void; onClaimSubmission: (submissionId: string) => void; onDeleteSubmission: (item: ModerationItem) => void; onInteractionFocus: (id: string) => void; onInteractionBlur: (id: string) => void; } const getStatusBadgeVariant = (status: string): "default" | "secondary" | "destructive" | "outline" => { switch (status) { case 'pending': return 'default'; case 'approved': return 'secondary'; case 'rejected': return 'destructive'; case 'flagged': return 'destructive'; case 'partially_approved': return 'outline'; default: return 'outline'; } }; export const QueueItem = memo(({ item, isMobile, actionLoading, lockedSubmissions, currentLockSubmissionId, notes, isAdmin, isSuperuser, queueIsLoading, onNoteChange, onApprove, onResetToPending, onRetryFailed, onOpenPhotos, onOpenReviewManager, onClaimSubmission, onDeleteSubmission, onInteractionFocus, onInteractionBlur }: QueueItemProps) => { const [validationResult, setValidationResult] = useState(null); return ( 0 ? 'border-l-red-600' : item.status === 'flagged' ? 'border-l-red-500' : item.status === 'approved' ? 'border-l-green-500' : item.status === 'rejected' ? 'border-l-red-400' : item.status === 'partially_approved' ? 'border-l-yellow-500' : 'border-l-amber-500' }`} style={{ opacity: actionLoading === item.id ? 0.5 : 1, pointerEvents: actionLoading === item.id ? 'none' : 'auto' }} >
{item.type === 'review' ? ( <> Review ) : item.submission_type === 'photo' ? ( <> Photo ) : ( <> Submission )} {item.status === 'partially_approved' ? 'Partially Approved' : item.status.charAt(0).toUpperCase() + item.status.slice(1)} {item.status === 'partially_approved' && ( Needs Retry )} {lockedSubmissions.has(item.id) && item.type === 'content_submission' && ( Locked by Another Moderator )} {currentLockSubmissionId === item.id && item.type === 'content_submission' && ( Claimed by You )} {item.submission_type && ( )}
{format(new Date(item.created_at), isMobile ? 'MMM d, yyyy' : 'MMM d, yyyy HH:mm')}
{item.user_profile && (
{(item.user_profile.display_name || item.user_profile.username)?.slice(0, 2).toUpperCase()}
{item.user_profile.display_name || item.user_profile.username} {item.user_profile.display_name && ( @{item.user_profile.username} )}
)}
{item.type === 'review' ? (
{item.content.title && (

{item.content.title}

)} {item.content.content && (

{item.content.content}

)}
Rating: {item.content.rating}/5
{/* Entity Names for Reviews */} {(item.entity_name || item.park_name) && (
{item.entity_name && (
{item.park_name ? 'Ride:' : 'Park:'} {item.entity_name}
)} {item.park_name && (
Park: {item.park_name}
)}
)} {item.content.photos && item.content.photos.length > 0 && (
Attached Photos:
{item.content.photos.map((photo: any, index: number) => (
{ onOpenPhotos(item.content.photos.map((p: any, i: number) => ({ id: `${item.id}-${i}`, url: p.url, filename: `Review photo ${i + 1}`, caption: p.caption })), index); }}> {`Review { console.error('Failed to load review photo:', photo.url); (e.target as HTMLImageElement).style.display = 'none'; }} />
))}
)}
) : item.submission_type === 'photo' ? (
Photo Submission
{/* Submission Title */} {item.content.title && (
Title:

{item.content.title}

)} {/* Submission Caption */} {item.content.content?.caption && (
Caption:

{item.content.content.caption}

)} {/* Photos */} {item.content.content?.photos && item.content.content.photos.length > 0 ? (
Photos ({item.content.content.photos.length}):
{item.content.content.photos.map((photo: any, index: number) => (
{ onOpenPhotos(item.content.content.photos.map((p: any, i: number) => ({ id: `${item.id}-${i}`, url: p.url, filename: p.filename, caption: p.caption })), index); }}> {`Photo { console.error('Failed to load photo submission:', photo); const target = e.target as HTMLImageElement; target.style.display = 'none'; const parent = target.parentElement; if (parent) { // Create elements safely using DOM API to prevent XSS const errorContainer = document.createElement('div'); errorContainer.className = 'absolute inset-0 flex flex-col items-center justify-center text-destructive text-xs'; const errorIcon = document.createElement('div'); errorIcon.textContent = '⚠️ Image failed to load'; const urlDisplay = document.createElement('div'); urlDisplay.className = 'mt-1 font-mono text-xs break-all px-2'; // Use textContent to prevent XSS - it escapes HTML automatically urlDisplay.textContent = photo.url; errorContainer.appendChild(errorIcon); errorContainer.appendChild(urlDisplay); parent.appendChild(errorContainer); } }} />
URL: {photo.url}
Filename: {photo.filename || 'Unknown'}
Size: {photo.size ? `${Math.round(photo.size / 1024)} KB` : 'Unknown'}
Type: {photo.type || 'Unknown'}
{photo.caption && (
Caption:
{photo.caption}
)}
))}
) : (
No photos found in submission
)} {/* Context Information */} {item.content.content?.context && (
Context: {typeof item.content.content.context === 'object' ? (item.content.content.context.ride_id ? 'ride' : item.content.content.context.park_id ? 'park' : 'unknown') : item.content.content.context}
{item.entity_name && (
{(typeof item.content.content.context === 'object' ? (item.content.content.context.ride_id ? 'ride' : 'park') : item.content.content.context) === 'ride' ? 'Ride:' : 'Park:'} {item.entity_name}
)} {item.park_name && (typeof item.content.content.context === 'object' ? !!item.content.content.context.ride_id : item.content.content.context === 'ride') && (
Park: {item.park_name}
)}
)}
) : (
{/* Composite Submissions or Standard Submissions */}
)}
{/* Action buttons based on status */} {(item.status === 'pending' || item.status === 'flagged') && ( <> {/* Claim button for unclaimed submissions */} {!lockedSubmissions.has(item.id) && currentLockSubmissionId !== item.id && (
Unclaimed Submission
Claim this submission to lock it for 15 minutes while you review
)}