import { useState, useEffect } from 'react'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent } from '@/components/ui/card'; import { supabase } from '@/lib/supabaseClient'; import { Image as ImageIcon } from 'lucide-react'; import { PhotoModal } from './PhotoModal'; import { handleError, getErrorMessage } from '@/lib/errorHandler'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { AlertCircle } from 'lucide-react'; interface EntityEditPreviewProps { submissionId: string; entityType: string; entityName?: string; } /** * Deep equality check for detecting changes in nested objects/arrays */ const deepEqual = >(a: T, b: T): boolean => { // Handle null/undefined cases if (a === b) return true; if (a == null || b == null) return false; if (typeof a !== typeof b) return false; // Handle primitives and functions if (typeof a !== 'object') return a === b; // Handle arrays if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; return a.every((item, index) => deepEqual(item as Record, b[index] as Record)); } // One is array, other is not if (Array.isArray(a) !== Array.isArray(b)) return false; // Handle objects const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; return keysA.every(key => { const valueA = a[key]; const valueB = b[key]; if (typeof valueA === 'object' && valueA !== null && typeof valueB === 'object' && valueB !== null) { return deepEqual(valueA as Record, valueB as Record); } return valueA === valueB; }); }; interface ImageAssignments { uploaded: Array<{ url: string; cloudflare_id: string; }>; banner_assignment: number | null; card_assignment: number | null; } interface SubmissionItemData { id: string; item_data: Record; original_data?: Record; } export const EntityEditPreview = ({ submissionId, entityType, entityName }: EntityEditPreviewProps) => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [itemData, setItemData] = useState | null>(null); const [originalData, setOriginalData] = useState | null>(null); const [changedFields, setChangedFields] = useState([]); const [bannerImageUrl, setBannerImageUrl] = useState(null); const [cardImageUrl, setCardImageUrl] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedImageIndex, setSelectedImageIndex] = useState(0); const [isPhotoOperation, setIsPhotoOperation] = useState(false); useEffect(() => { fetchSubmissionItems(); }, [submissionId]); const fetchSubmissionItems = async () => { try { setLoading(true); // Fetch items with relational data const { data: items, error } = await supabase .from('submission_items') .select(` *, park_submission:park_submissions!submission_items_park_submission_id_fkey(*), ride_submission:ride_submissions!submission_items_ride_submission_id_fkey(*), photo_submission:photo_submissions!submission_items_photo_submission_id_fkey( *, photo_items:photo_submission_items(*) ) `) .eq('submission_id', submissionId) .order('order_index', { ascending: true }); if (error) throw error; if (items && items.length > 0) { const firstItem = items[0]; // Transform relational data to item_data format let itemDataObj: Record = {}; switch (firstItem.item_type) { case 'park': itemDataObj = (firstItem as any).park_submission || {}; break; case 'ride': itemDataObj = (firstItem as any).ride_submission || {}; break; case 'photo': itemDataObj = { ...(firstItem as any).photo_submission, photos: (firstItem as any).photo_submission?.photo_items || [] }; break; default: itemDataObj = {}; } setItemData(itemDataObj); setOriginalData(null); // Original data not used in new relational model // Check for photo edit/delete operations if (firstItem.item_type === 'photo_edit' || firstItem.item_type === 'photo_delete') { setIsPhotoOperation(true); if (firstItem.item_type === 'photo_edit') { setChangedFields(['caption']); } return; } // Parse changed fields const changed: string[] = []; const data = itemDataObj as Record; // Check for image changes if (data.images && typeof data.images === 'object') { const images = data.images as { uploaded?: Array<{ url: string; cloudflare_id: string }>; banner_assignment?: number | null; card_assignment?: number | null; }; // Safety check: verify uploaded array exists and is valid if (!images.uploaded || !Array.isArray(images.uploaded)) { // Invalid images data structure, skip image processing return; } // Extract banner image if (images.banner_assignment !== null && images.banner_assignment !== undefined) { // Safety check: verify index is within bounds if (images.banner_assignment >= 0 && images.banner_assignment < images.uploaded.length) { const bannerImg = images.uploaded[images.banner_assignment]; // Validate nested image data if (bannerImg && bannerImg.url) { setBannerImageUrl(bannerImg.url); changed.push('banner_image'); } } } // Extract card image if (images.card_assignment !== null && images.card_assignment !== undefined) { // Safety check: verify index is within bounds if (images.card_assignment >= 0 && images.card_assignment < images.uploaded.length) { const cardImg = images.uploaded[images.card_assignment]; // Validate nested image data if (cardImg && cardImg.url) { setCardImageUrl(cardImg.url); changed.push('card_image'); } } } } // Check for other field changes by comparing with original_data // Note: In new relational model, we don't track original_data at item level // Field changes are determined by comparing current vs approved entity data if (itemDataObj) { const excludeFields = ['images', 'updated_at', 'created_at', 'id']; Object.keys(itemDataObj).forEach(key => { if (!excludeFields.includes(key) && itemDataObj[key] !== null && itemDataObj[key] !== undefined) { changed.push(key); } }); } setChangedFields(changed); } } catch (error: unknown) { const errorMsg = getErrorMessage(error); handleError(error, { action: 'Load Submission Preview', metadata: { submissionId, entityType } }); setError(errorMsg); } finally { setLoading(false); } }; if (loading) { return (
Loading preview...
); } if (error) { return ( {error} ); } if (!itemData) { return (
No preview available
); } // Handle photo edit/delete operations if (isPhotoOperation) { const isEdit = changedFields.includes('caption'); return (
Photo {isEdit ? 'Edit' : 'Delete'}
{(itemData?.cloudflare_image_url && typeof itemData.cloudflare_image_url === 'string' && ( Photo to be modified )) as React.ReactNode} {isEdit && (
Old caption: {(originalData?.caption as string) || No caption}
New caption: {(itemData?.new_caption as string) || No caption}
)} {(!isEdit && itemData?.reason && typeof itemData.reason === 'string' && (
Reason: {itemData.reason}
)) as React.ReactNode}
Click "Review Items" for full details
); } // Build photos array for modal const photos: Array<{ id: string; url: string; caption: string | null }> = []; if (bannerImageUrl) { photos.push({ id: 'banner', url: `${bannerImageUrl}`, caption: 'New Banner Image' }); } if (cardImageUrl) { photos.push({ id: 'card', url: `${cardImageUrl}`, caption: 'New Card Image' }); } return (
{entityType} Edit
{entityName && (
{entityName}
)} {changedFields.length > 0 && (
Changed fields: {changedFields.map(field => field.replace(/_/g, ' ')).join(', ')}
)} {(bannerImageUrl || cardImageUrl) && (
Image Changes:
{bannerImageUrl && ( { setSelectedImageIndex(0); setIsModalOpen(true); }}> New banner
Banner
)} {cardImageUrl && ( { setSelectedImageIndex(bannerImageUrl ? 1 : 0); setIsModalOpen(true); }}> New card
Card
)}
)}
Click "Review Items" for full details
({ ...photo, caption: photo.caption ?? undefined }))} initialIndex={selectedImageIndex} isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
); };