diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index 3534baaa..dc4fcdaf 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -120,8 +120,284 @@ export const ModerationQueue = forwardRef((props, ref) => { timestamp: new Date().toISOString() }); - // TODO: Function body was accidentally removed - needs restoration - console.error('fetchItems function body is missing!'); + try { + // Set loading states + if (!silent) { + setLoading(true); + } else { + setIsRefreshing(true); + } + + // Build base query for content submissions + let submissionsQuery = supabase + .from('content_submissions') + .select(` + id, + submission_type, + status, + content, + created_at, + user_id, + reviewed_at, + reviewer_id, + reviewer_notes, + escalated, + priority, + assigned_to, + locked_until + `) + .order('priority', { ascending: false }) + .order('created_at', { ascending: true }); + + // Apply status filter + if (statusFilter === 'all') { + submissionsQuery = submissionsQuery.in('status', ['pending', 'approved', 'rejected', 'partially_approved']); + } else if (statusFilter === 'pending') { + submissionsQuery = submissionsQuery.in('status', ['pending', 'partially_approved']); + } else { + submissionsQuery = submissionsQuery.eq('status', statusFilter); + } + + // Apply entity type filter + if (entityFilter === 'photos') { + submissionsQuery = submissionsQuery.eq('submission_type', 'photo'); + } else if (entityFilter === 'submissions') { + submissionsQuery = submissionsQuery.neq('submission_type', 'photo'); + } + + const { data: submissions, error: submissionsError } = await submissionsQuery; + + if (submissionsError) throw submissionsError; + + // Get user IDs and fetch user profiles + const userIds = submissions?.map(s => s.user_id).filter(Boolean) || []; + const reviewerIds = submissions?.map(s => s.reviewer_id).filter((id): id is string => !!id) || []; + const allUserIds = [...new Set([...userIds, ...reviewerIds])]; + + let userProfiles: any[] = []; + if (allUserIds.length > 0) { + const { data: profiles } = await supabase + .from('profiles') + .select('user_id, username, display_name, avatar_url') + .in('user_id', allUserIds); + + userProfiles = profiles || []; + } + + const userProfileMap = new Map(userProfiles.map(p => [p.user_id, p])); + + // Collect entity IDs for bulk fetching + const rideIds = new Set(); + const parkIds = new Set(); + const companyIds = new Set(); + const rideModelIds = new Set(); + + submissions?.forEach(submission => { + const content = submission.content as any; + if (content && typeof content === 'object') { + if (content.ride_id) rideIds.add(content.ride_id); + if (content.park_id) parkIds.add(content.park_id); + if (content.company_id) companyIds.add(content.company_id); + if (content.entity_id) { + if (submission.submission_type === 'ride') rideIds.add(content.entity_id); + if (submission.submission_type === 'park') parkIds.add(content.entity_id); + if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(submission.submission_type)) { + companyIds.add(content.entity_id); + } + } + if (content.manufacturer_id) companyIds.add(content.manufacturer_id); + if (content.designer_id) companyIds.add(content.designer_id); + if (content.operator_id) companyIds.add(content.operator_id); + if (content.property_owner_id) companyIds.add(content.property_owner_id); + if (content.ride_model_id) rideModelIds.add(content.ride_model_id); + } + }); + + // Fetch entities only if we don't have them cached or if they're new + const fetchPromises: Promise<{ type: string; data: any[] }>[] = []; + + if (rideIds.size > 0) { + const uncachedRideIds = Array.from(rideIds).filter(id => !entityCache.rides.has(id)); + if (uncachedRideIds.length > 0) { + fetchPromises.push( + Promise.resolve( + supabase + .from('rides') + .select('id, name, park_id') + .in('id', uncachedRideIds) + ).then(({ data }) => ({ type: 'rides', data: data || [] })) + ); + } + } + + if (parkIds.size > 0) { + const uncachedParkIds = Array.from(parkIds).filter(id => !entityCache.parks.has(id)); + if (uncachedParkIds.length > 0) { + fetchPromises.push( + Promise.resolve( + supabase + .from('parks') + .select('id, name') + .in('id', uncachedParkIds) + ).then(({ data }) => ({ type: 'parks', data: data || [] })) + ); + } + } + + if (companyIds.size > 0) { + const uncachedCompanyIds = Array.from(companyIds).filter(id => !entityCache.companies.has(id)); + if (uncachedCompanyIds.length > 0) { + fetchPromises.push( + Promise.resolve( + supabase + .from('companies') + .select('id, name') + .in('id', uncachedCompanyIds) + ).then(({ data }) => ({ type: 'companies', data: data || [] })) + ); + } + } + + // Fetch all uncached entities + const entityResults = await Promise.all(fetchPromises); + + // Update entity cache + entityResults.forEach(result => { + if (result.type === 'rides') { + result.data.forEach((ride: any) => { + entityCache.rides.set(ride.id, ride); + if (ride.park_id) parkIds.add(ride.park_id); + }); + } else if (result.type === 'parks') { + result.data.forEach((park: any) => { + entityCache.parks.set(park.id, park); + }); + } else if (result.type === 'companies') { + result.data.forEach((company: any) => { + entityCache.companies.set(company.id, company); + }); + } + }); + + // Helper function to create memo key + const createMemoKey = (submission: any): string => { + return JSON.stringify({ + id: submission.id, + status: submission.status, + content: submission.content, + reviewed_at: submission.reviewed_at, + reviewer_notes: submission.reviewer_notes, + }); + }; + + // Map submissions to moderation items with memoization + const moderationItems: ModerationItem[] = submissions?.map(submission => { + const memoKey = createMemoKey(submission); + const existingMemo = submissionMemo.get(submission.id); + + // Check if we can reuse the memoized item + if (existingMemo && createMemoKey(existingMemo) === memoKey) { + return existingMemo as ModerationItem; + } + + // Resolve entity name + const content = submission.content as any; + let entityName = content?.name || 'Unknown'; + let parkName: string | undefined; + + if (submission.submission_type === 'ride' && content?.entity_id) { + const ride = entityCache.rides.get(content.entity_id); + if (ride) { + entityName = ride.name; + if (ride.park_id) { + const park = entityCache.parks.get(ride.park_id); + if (park) parkName = park.name; + } + } + } else if (submission.submission_type === 'park' && content?.entity_id) { + const park = entityCache.parks.get(content.entity_id); + if (park) entityName = park.name; + } else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(submission.submission_type) && content?.entity_id) { + const company = entityCache.companies.get(content.entity_id); + if (company) entityName = company.name; + } else if (content?.ride_id) { + const ride = entityCache.rides.get(content.ride_id); + if (ride) { + entityName = ride.name; + if (ride.park_id) { + const park = entityCache.parks.get(ride.park_id); + if (park) parkName = park.name; + } + } + } else if (content?.park_id) { + const park = entityCache.parks.get(content.park_id); + if (park) parkName = park.name; + } + + const userProfile = userProfileMap.get(submission.user_id); + const reviewerProfile = submission.reviewer_id ? userProfileMap.get(submission.reviewer_id) : undefined; + + const item: ModerationItem = { + id: submission.id, + type: 'content_submission', + content: submission.content, + created_at: submission.created_at, + user_id: submission.user_id, + status: submission.status, + submission_type: submission.submission_type, + user_profile: userProfile ? { + username: userProfile.username, + display_name: userProfile.display_name, + avatar_url: userProfile.avatar_url, + } : undefined, + entity_name: entityName, + park_name: parkName, + reviewed_at: submission.reviewed_at, + reviewed_by: submission.reviewer_id, + reviewer_notes: submission.reviewer_notes, + reviewer_profile: reviewerProfile, + }; + + return item; + }) || []; + + // Update memoization cache + const newMemoMap = new Map(); + moderationItems.forEach(item => { + newMemoMap.set(item.id, item); + }); + setSubmissionMemo(newMemoMap); + + // Apply smart merge for state updates + const mergeResult = smartMergeArray(items, moderationItems, { + compareFields: ['status', 'reviewed_at', 'reviewer_notes'], + preserveOrder: silent && preserveInteraction, + addToTop: false, + }); + + if (!silent || mergeResult.hasChanges) { + setItems(mergeResult.items); + + // Track new items for toast notification + if (silent && mergeResult.changes.added.length > 0) { + setNewItemsCount(prev => prev + mergeResult.changes.added.length); + } else if (!silent) { + setNewItemsCount(0); + } + } + + } catch (error: any) { + console.error('Error fetching moderation items:', error); + toast({ + title: 'Error', + description: error.message || 'Failed to fetch moderation queue', + variant: 'destructive', + }); + } finally { + setLoading(false); + setIsRefreshing(false); + setIsInitialLoad(false); + } }; // Expose refresh method via ref