import { useState, useEffect } from 'react'; import { CheckCircle, XCircle, Eye, Calendar, User, Filter, MessageSquare, FileText, Image, X, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { supabase } from '@/integrations/supabase/client'; import { useToast } from '@/hooks/use-toast'; import { useUserRole } from '@/hooks/useUserRole'; import { format } from 'date-fns'; interface ModerationItem { id: string; type: 'review' | 'content_submission'; content: any; created_at: string; user_id: string; status: string; submission_type?: string; user_profile?: { username: string; display_name?: string; }; } type EntityFilter = 'all' | 'reviews' | 'submissions' | 'photos'; type StatusFilter = 'all' | 'pending' | 'flagged' | 'approved' | 'rejected'; export function ModerationQueue() { const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [actionLoading, setActionLoading] = useState(null); const [notes, setNotes] = useState>({}); const [activeEntityFilter, setActiveEntityFilter] = useState('all'); const [activeStatusFilter, setActiveStatusFilter] = useState('pending'); const { toast } = useToast(); const { isAdmin, isSuperuser } = useUserRole(); const fetchItems = async (entityFilter: EntityFilter = 'all', statusFilter: StatusFilter = 'pending') => { try { setLoading(true); let reviewStatuses: string[] = []; let submissionStatuses: string[] = []; // Define status filters switch (statusFilter) { case 'all': reviewStatuses = ['pending', 'flagged', 'approved', 'rejected']; submissionStatuses = ['pending', 'approved', 'rejected']; break; case 'pending': reviewStatuses = ['pending']; submissionStatuses = ['pending']; break; case 'flagged': reviewStatuses = ['flagged']; submissionStatuses = []; // Content submissions don't have flagged status break; case 'approved': reviewStatuses = ['approved']; submissionStatuses = ['approved']; break; case 'rejected': reviewStatuses = ['rejected']; submissionStatuses = ['rejected']; break; default: reviewStatuses = ['pending', 'flagged']; submissionStatuses = ['pending']; } // Fetch reviews let reviews = []; if ((entityFilter === 'all' || entityFilter === 'reviews') && reviewStatuses.length > 0) { const { data: reviewsData, error: reviewsError } = await supabase .from('reviews') .select(` id, title, content, rating, created_at, user_id, moderation_status, photos `) .in('moderation_status', reviewStatuses) .order('created_at', { ascending: false }); if (reviewsError) throw reviewsError; reviews = reviewsData || []; } // Fetch content submissions let submissions = []; if ((entityFilter === 'all' || entityFilter === 'submissions' || entityFilter === 'photos') && submissionStatuses.length > 0) { let query = supabase .from('content_submissions') .select(` id, content, submission_type, created_at, user_id, status `) .in('status', submissionStatuses); // Filter by submission type for photos if (entityFilter === 'photos') { query = query.eq('submission_type', 'photo'); } else if (entityFilter === 'submissions') { query = query.neq('submission_type', 'photo'); } const { data: submissionsData, error: submissionsError } = await query .order('created_at', { ascending: false }); if (submissionsError) throw submissionsError; submissions = submissionsData || []; } // Get unique user IDs to fetch profiles const userIds = [ ...reviews.map(r => r.user_id), ...submissions.map(s => s.user_id) ]; // Fetch profiles for all users const { data: profiles } = await supabase .from('profiles') .select('user_id, username, display_name') .in('user_id', userIds); const profileMap = new Map(profiles?.map(p => [p.user_id, p]) || []); // Combine and format items const formattedItems: ModerationItem[] = [ ...reviews.map(review => ({ id: review.id, type: 'review' as const, content: review, created_at: review.created_at, user_id: review.user_id, status: review.moderation_status, user_profile: profileMap.get(review.user_id), })), ...submissions.map(submission => ({ id: submission.id, type: 'content_submission' as const, content: submission, created_at: submission.created_at, user_id: submission.user_id, status: submission.status, submission_type: submission.submission_type, user_profile: profileMap.get(submission.user_id), })), ]; // Sort by creation date (newest first for better UX) formattedItems.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); setItems(formattedItems); } catch (error) { console.error('Error fetching moderation items:', error); toast({ title: "Error", description: "Failed to load moderation queue", variant: "destructive", }); } finally { setLoading(false); } }; useEffect(() => { fetchItems(activeEntityFilter, activeStatusFilter); }, [activeEntityFilter, activeStatusFilter]); const handleModerationAction = async ( item: ModerationItem, action: 'approved' | 'rejected', moderatorNotes?: string ) => { // Prevent multiple clicks on the same item if (actionLoading === item.id) { console.log('Action already in progress for item:', item.id); return; } setActionLoading(item.id); try { const table = item.type === 'review' ? 'reviews' : 'content_submissions'; const statusField = item.type === 'review' ? 'moderation_status' : 'status'; // Use correct timestamp column name based on table const timestampField = item.type === 'review' ? 'moderated_at' : 'reviewed_at'; const reviewerField = item.type === 'review' ? 'moderated_by' : 'reviewer_id'; const updateData: any = { [statusField]: action, [timestampField]: new Date().toISOString(), }; // Get current user ID for reviewer tracking const { data: { user } } = await supabase.auth.getUser(); if (user) { updateData[reviewerField] = user.id; } if (moderatorNotes) { updateData.reviewer_notes = moderatorNotes; } console.log('Updating item:', item.id, 'with data:', updateData, 'table:', table); const { error, data } = await supabase .from(table) .update(updateData) .eq('id', item.id) .select(); if (error) { console.error('Database update error:', error); throw error; } console.log('Update response:', { data, rowsAffected: data?.length }); // Check if the update actually affected any rows if (!data || data.length === 0) { console.error('No rows were updated. This might be due to RLS policies or the item not existing.'); throw new Error('Failed to update item - no rows affected. You might not have permission to moderate this content.'); } console.log('Update successful, rows affected:', data.length); toast({ title: `Content ${action}`, description: `The ${item.type} has been ${action}`, }); // Only update local state if the database update was successful setItems(prev => prev.map(i => i.id === item.id ? { ...i, status: action } : i )); // Clear notes only after successful update setNotes(prev => { const newNotes = { ...prev }; delete newNotes[item.id]; return newNotes; }); // Only refresh if we're viewing a filter that should no longer show this item if ((activeStatusFilter === 'pending' && (action === 'approved' || action === 'rejected')) || (activeStatusFilter === 'flagged' && (action === 'approved' || action === 'rejected'))) { console.log('Item no longer matches filter, removing from view'); } } catch (error: any) { console.error('Error moderating content:', error); // Revert any optimistic updates setItems(prev => prev.map(i => i.id === item.id ? { ...i, status: item.status } // Revert to original status : i )); toast({ title: "Error", description: error.message || `Failed to ${action} content`, variant: "destructive", }); } finally { setActionLoading(null); } }; const handleDeleteSubmission = async (item: ModerationItem) => { if (item.type !== 'content_submission') return; setActionLoading(item.id); try { // Step 1: Extract photo IDs from the submission content const photoIds: string[] = []; if (item.content?.photos && Array.isArray(item.content.photos)) { for (const photo of item.content.photos) { if (photo.imageId) { photoIds.push(photo.imageId); } } } // Step 2: Delete photos from Cloudflare Images (if any) if (photoIds.length > 0) { const deletePromises = photoIds.map(async (imageId) => { try { await supabase.functions.invoke('upload-image', { method: 'DELETE', body: { imageId } }); } catch (deleteError) { console.warn(`Failed to delete photo ${imageId} from Cloudflare:`, deleteError); // Continue with other deletions - don't fail the entire operation } }); // Execute all photo deletions in parallel await Promise.allSettled(deletePromises); } // Step 3: Delete the submission from the database const { error } = await supabase .from('content_submissions') .delete() .eq('id', item.id); if (error) throw error; toast({ title: "Submission deleted", description: `The submission and ${photoIds.length > 0 ? `${photoIds.length} associated photo(s) have` : 'has'} been permanently deleted`, }); // Remove item from the current view setItems(prev => prev.filter(i => i.id !== item.id)); } catch (error) { console.error('Error deleting submission:', error); toast({ title: "Error", description: "Failed to delete submission", variant: "destructive", }); } finally { setActionLoading(null); } }; const getStatusBadgeVariant = (status: string) => { switch (status) { case 'pending': return 'secondary'; case 'flagged': return 'destructive'; case 'approved': return 'default'; case 'rejected': return 'outline'; default: return 'secondary'; } }; const getEmptyStateMessage = (entityFilter: EntityFilter, statusFilter: StatusFilter) => { const entityLabel = entityFilter === 'all' ? 'items' : entityFilter === 'reviews' ? 'reviews' : entityFilter === 'photos' ? 'photos' : 'submissions'; switch (statusFilter) { case 'pending': return `No pending ${entityLabel} require moderation at this time.`; case 'flagged': return `No flagged ${entityLabel} found.`; case 'approved': return `No approved ${entityLabel} found.`; case 'rejected': return `No rejected ${entityLabel} found.`; case 'all': return `No ${entityLabel} found.`; default: return `No ${entityLabel} found for the selected filter.`; } }; const QueueContent = () => { if (loading) { return (
); } if (items.length === 0) { return (

No items found

{getEmptyStateMessage(activeEntityFilter, activeStatusFilter)}

); } return (
{items.map((item) => (
{item.type === 'review' ? ( <> Review ) : item.submission_type === 'photo' ? ( <> Photo ) : ( <> Submission )} {item.status.charAt(0).toUpperCase() + item.status.slice(1)}
{format(new Date(item.created_at), 'MMM d, yyyy HH:mm')}
{item.user_profile && (
{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
{item.content.photos && item.content.photos.length > 0 && (
Attached Photos:
{item.content.photos.map((photo: any, index: number) => (
{`Review
))}
)}
) : item.submission_type === 'photo' ? (
Photo Submission
{/* Submission Title */} {item.content.title && (
Title:

{item.content.title}

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

{item.content.caption}

)} {/* Photos */} {item.content.photos && item.content.photos.length > 0 ? (
Photos ({item.content.photos.length}):
{item.content.photos.map((photo: any, index: number) => (
{`Photo
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.context && (
Context: {item.content.context}
{item.content.ride_id && (
Ride ID: {item.content.ride_id}
)} {item.content.park_id && (
Park ID: {item.content.park_id}
)}
)}
) : (
Type: {item.content.submission_type}
                      {JSON.stringify(item.content.content, null, 2)}
                    
)}
{/* Action buttons based on status */} {(item.status === 'pending' || item.status === 'flagged') && ( <>