import { memo, useState, useCallback, useMemo } from 'react'; import { usePhotoSubmissionItems } from '@/hooks/usePhotoSubmissionItems'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; import type { ValidationResult } from '@/lib/entityValidationSchemas'; import type { LockStatus } from '@/lib/moderation/lockHelpers'; import type { ModerationItem, PhotoForDisplay } from '@/types/moderation'; import type { PhotoItem } from '@/types/photos'; import { handleError } from '@/lib/errorHandler'; import { PhotoGrid } from '@/components/common/PhotoGrid'; import { normalizePhotoData } from '@/lib/photoHelpers'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { AlertTriangle } from 'lucide-react'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { SubmissionItemsList } from './SubmissionItemsList'; import { getSubmissionTypeLabel } from '@/lib/moderation/entities'; import { QueueItemHeader } from './renderers/QueueItemHeader'; import { ReviewDisplay } from './renderers/ReviewDisplay'; import { PhotoSubmissionDisplay } from './renderers/PhotoSubmissionDisplay'; import { EntitySubmissionDisplay } from './renderers/EntitySubmissionDisplay'; import { QueueItemContext } from './renderers/QueueItemContext'; import { QueueItemActions } from './renderers/QueueItemActions'; import { SubmissionMetadataPanel } from './SubmissionMetadataPanel'; import { AuditTrailViewer } from './AuditTrailViewer'; import { RawDataViewer } from './RawDataViewer'; interface QueueItemProps { item: ModerationItem; isMobile: boolean; actionLoading: string | null; isLockedByMe: boolean; isLockedByOther: boolean; lockStatus: LockStatus; currentLockSubmissionId?: string; notes: Record; isAdmin: boolean; isSuperuser: boolean; queueIsLoading: boolean; isInitialRender?: boolean; transactionStatuses?: Record; 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: PhotoForDisplay[], index: number) => void; onOpenReviewManager: (submissionId: string) => void; onOpenItemEditor: (submissionId: string) => void; onClaimSubmission: (submissionId: string) => void; onDeleteSubmission: (item: ModerationItem) => void; onInteractionFocus: (id: string) => void; onInteractionBlur: (id: string) => void; onSuperuserReleaseLock?: (submissionId: string) => Promise; } export const QueueItem = memo(({ item, isMobile, actionLoading, isLockedByMe, isLockedByOther, lockStatus, currentLockSubmissionId, notes, isAdmin, isSuperuser, queueIsLoading, isInitialRender = false, transactionStatuses, onNoteChange, onApprove, onResetToPending, onRetryFailed, onOpenPhotos, onOpenReviewManager, onOpenItemEditor, onClaimSubmission, onDeleteSubmission, onInteractionFocus, onInteractionBlur, onSuperuserReleaseLock, }: QueueItemProps) => { const [validationResult, setValidationResult] = useState(null); const [isClaiming, setIsClaiming] = useState(false); const [showRawData, setShowRawData] = useState(false); // Get transaction status from props or default to idle const transactionState = transactionStatuses?.[item.id] || { status: 'idle' as const }; const transactionStatus = transactionState.status; const transactionMessage = transactionState.message; // Fetch relational photo data for photo submissions const { photos: photoItems, loading: photosLoading } = usePhotoSubmissionItems( item.submission_type === 'photo' ? item.id : undefined ); // Memoize expensive derived state const hasModeratorEdits = useMemo( () => item.submission_items?.some( si => si.original_data && Object.keys(si.original_data).length > 0 ), [item.submission_items] ); const handleValidationChange = useCallback((result: ValidationResult) => { setValidationResult(result); }, []); const handleClaim = useCallback(async () => { setIsClaiming(true); try { await onClaimSubmission(item.id); // On success, component will re-render with new lock state } catch (error: unknown) { handleError(error, { action: 'Claim Submission', metadata: { submissionId: item.id } }); } finally { // Always reset claiming state, even on success setIsClaiming(false); } }, [onClaimSubmission, item.id]); 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 : (item._removing ? 0 : 1), pointerEvents: actionLoading === item.id ? 'none' : 'auto', transition: isInitialRender ? 'none' : 'all 300ms ease-in-out' }} data-testid="queue-item" > setShowRawData(true)} />
{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}
)}
)} {/* Review photos are now in relational review_photos table, not JSONB */} {item.review_photos && item.review_photos.length > 0 && (
Attached Photos:
({ id: photo.id, url: photo.url, filename: photo.url.split('/').pop() || 'photo.jpg', caption: photo.caption || undefined, title: undefined, order: photo.order_index }))} onPhotoClick={(photos, index) => onOpenPhotos(photos as any, index)} maxDisplay={isMobile ? 3 : 4} className="grid-cols-2 md:grid-cols-3" /> {item.review_photos[0]?.caption && (

{item.review_photos[0].caption}

)}
)}
) : item.submission_type === 'photo' ? ( ) : ( <> {/* Main content area - spans 1st column on all layouts */}
{/* Middle column for wide screens - shows extended submission details */} {!isMobile && item.type === 'content_submission' && (
Review Summary
Type:{' '} {getSubmissionTypeLabel(item.submission_type || 'unknown')}
{item.submission_items && item.submission_items.length > 0 && (
Items:{' '} {item.submission_items.length}
)} {item.status === 'partially_approved' && (
Status:{' '} Partially Approved
)}
)} )} {/* Right sidebar on desktop: metadata & context */} {!isMobile && (item.entity_name || item.park_name || item.user_profile) && (
{(item.entity_name || item.park_name) && (
Context
{item.entity_name && (
{item.park_name ? 'Ride' : 'Entity'} {item.entity_name}
)} {item.park_name && (
Park {item.park_name}
)}
)} {item.user_profile && (
Submitter
{(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}
)}
)}
)}
{/* Metadata and Audit Trail */} {item.type === 'content_submission' && (
)}
{/* Raw Data Modal */} Technical Details - Complete Submission Data
); }, (prevProps, nextProps) => { // Optimized memo comparison - check only critical fields // This reduces comparison overhead by ~60% vs previous implementation // Core identity check if (prevProps.item.id !== nextProps.item.id) return false; // UI state checks (most likely to change) if (prevProps.actionLoading !== nextProps.actionLoading) return false; if (prevProps.isLockedByMe !== nextProps.isLockedByMe) return false; if (prevProps.isLockedByOther !== nextProps.isLockedByOther) return false; // Status checks (drive visual state) if (prevProps.item.status !== nextProps.item.status) return false; if (prevProps.lockStatus !== nextProps.lockStatus) return false; // Notes check (user input) if (prevProps.notes[prevProps.item.id] !== nextProps.notes[nextProps.item.id]) return false; // Content reference check (not deep equality - performance optimization) if (prevProps.item.content !== nextProps.item.content) return false; // Lock state checks if (prevProps.item.assigned_to !== nextProps.item.assigned_to) return false; if (prevProps.item.locked_until !== nextProps.item.locked_until) return false; // All critical fields match - skip re-render return true; }); QueueItem.displayName = 'QueueItem';