diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index 82db91aa..be7c54ae 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -5,6 +5,15 @@ import { Badge } from '@/components/ui/badge'; import { Card, CardContent } from '@/components/ui/card'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from '@/components/ui/pagination'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { supabase } from '@/integrations/supabase/client'; import { useToast } from '@/hooks/use-toast'; @@ -109,6 +118,12 @@ export const ModerationQueue = forwardRef((props, ref) => { const isMountingRef = useRef(true); const initialFetchCompleteRef = useRef(false); const FETCH_COOLDOWN_MS = 1000; + + // Pagination state + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(25); + const [totalCount, setTotalCount] = useState(0); + const totalPages = Math.ceil(totalCount / pageSize); // Get admin settings for polling configuration const { @@ -247,22 +262,59 @@ export const ModerationQueue = forwardRef((props, ref) => { submissionsQuery = submissionsQuery.neq('submission_type', 'photo'); } - // Always fetch ALL pending/partially_approved submissions - // Let ID-based tracking determine what's "new" instead of timestamp filtering - // CRM-style claim filtering: moderators only see unclaimed OR self-assigned submissions // Admins see all submissions if (!isAdmin && !isSuperuser) { const now = new Date().toISOString(); - // Show submissions that are: - // 1. Unclaimed (assigned_to is null) - // 2. Have expired locks (locked_until < now) - // 3. Are assigned to current user submissionsQuery = submissionsQuery.or( `assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${user.id}` ); } + // Get total count for pagination + const countQuery = supabase + .from('content_submissions') + .select('*', { count: 'exact', head: true }); + + // Apply same filters to count query + let countQueryWithFilters = countQuery; + if (tab === 'mainQueue') { + if (statusFilter === 'all') { + countQueryWithFilters = countQueryWithFilters.in('status', ['pending', 'flagged', 'partially_approved']); + } else if (statusFilter === 'pending') { + countQueryWithFilters = countQueryWithFilters.in('status', ['pending', 'partially_approved']); + } else { + countQueryWithFilters = countQueryWithFilters.eq('status', statusFilter); + } + } else { + if (statusFilter === 'all') { + countQueryWithFilters = countQueryWithFilters.in('status', ['approved', 'rejected']); + } else { + countQueryWithFilters = countQueryWithFilters.eq('status', statusFilter); + } + } + + if (entityFilter === 'photos') { + countQueryWithFilters = countQueryWithFilters.eq('submission_type', 'photo'); + } else if (entityFilter === 'submissions') { + countQueryWithFilters = countQueryWithFilters.neq('submission_type', 'photo'); + } + + if (!isAdmin && !isSuperuser) { + const now = new Date().toISOString(); + countQueryWithFilters = countQueryWithFilters.or( + `assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${user.id}` + ); + } + + const { count } = await countQueryWithFilters; + setTotalCount(count || 0); + + // Apply pagination range + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize - 1; + submissionsQuery = submissionsQuery.range(startIndex, endIndex); + const { data: submissions, error: submissionsError } = await submissionsQuery; if (submissionsError) throw submissionsError; @@ -2013,6 +2065,117 @@ export const ModerationQueue = forwardRef((props, ref) => { {/* Queue Content */} + + {/* Pagination Controls */} + {totalPages > 1 && !loading && ( +
+
+ + Showing {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, totalCount)} of {totalCount} items + + {!isMobile && ( + <> + + + + )} +
+ + {isMobile ? ( +
+ + + Page {currentPage} of {totalPages} + + +
+ ) : ( + + + + setCurrentPage(p => Math.max(1, p - 1))} + className={currentPage === 1 ? 'pointer-events-none opacity-50' : 'cursor-pointer'} + /> + + + {currentPage > 3 && ( + <> + + setCurrentPage(1)} isActive={currentPage === 1}> + 1 + + + {currentPage > 4 && } + + )} + + {Array.from({ length: totalPages }, (_, i) => i + 1) + .filter(page => page >= currentPage - 2 && page <= currentPage + 2) + .map(page => ( + + setCurrentPage(page)} + isActive={currentPage === page} + > + {page} + + + )) + } + + {currentPage < totalPages - 2 && ( + <> + {currentPage < totalPages - 3 && } + + setCurrentPage(totalPages)} isActive={currentPage === totalPages}> + {totalPages} + + + + )} + + + setCurrentPage(p => Math.min(totalPages, p + 1))} + className={currentPage === totalPages ? 'pointer-events-none opacity-50' : 'cursor-pointer'} + /> + + + + )} +
+ )} {/* Photo Modal */} ((props, ref) => { + const isMobile = useIsMobile(); const [reports, setReports] = useState([]); const [loading, setLoading] = useState(true); const [isInitialLoad, setIsInitialLoad] = useState(true); @@ -53,6 +65,12 @@ export const ReportsQueue = forwardRef((props, ref) => { const [newReportsCount, setNewReportsCount] = useState(0); const { toast } = useToast(); const { user } = useAuth(); + + // Pagination state + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(25); + const [totalCount, setTotalCount] = useState(0); + const totalPages = Math.ceil(totalCount / pageSize); // Get admin settings for polling configuration const { @@ -76,6 +94,18 @@ export const ReportsQueue = forwardRef((props, ref) => { setLoading(true); } + // Get total count + const { count } = await supabase + .from('reports') + .select('*', { count: 'exact', head: true }) + .eq('status', 'pending'); + + setTotalCount(count || 0); + + // Apply pagination + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize - 1; + const { data, error } = await supabase .from('reports') .select(` @@ -89,7 +119,8 @@ export const ReportsQueue = forwardRef((props, ref) => { reporter_id `) .eq('status', 'pending') - .order('created_at', { ascending: true }); + .order('created_at', { ascending: true }) + .range(startIndex, endIndex); if (error) throw error; @@ -205,7 +236,14 @@ export const ReportsQueue = forwardRef((props, ref) => { }); // Remove report from queue - setReports(prev => prev.filter(r => r.id !== reportId)); + setReports(prev => { + const newReports = prev.filter(r => r.id !== reportId); + // If last item on page and not page 1, go to previous page + if (newReports.length === 0 && currentPage > 1) { + setCurrentPage(prev => prev - 1); + } + return newReports; + }); } catch (error) { console.error('Error updating report:', error); toast({ @@ -346,6 +384,117 @@ export const ReportsQueue = forwardRef((props, ref) => { ))} + + {/* Pagination Controls */} + {totalPages > 1 && !loading && ( +
+
+ + Showing {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, totalCount)} of {totalCount} reports + + {!isMobile && ( + <> + + + + )} +
+ + {isMobile ? ( +
+ + + Page {currentPage} of {totalPages} + + +
+ ) : ( + + + + setCurrentPage(p => Math.max(1, p - 1))} + className={currentPage === 1 ? 'pointer-events-none opacity-50' : 'cursor-pointer'} + /> + + + {currentPage > 3 && ( + <> + + setCurrentPage(1)} isActive={currentPage === 1}> + 1 + + + {currentPage > 4 && } + + )} + + {Array.from({ length: totalPages }, (_, i) => i + 1) + .filter(page => page >= currentPage - 2 && page <= currentPage + 2) + .map(page => ( + + setCurrentPage(page)} + isActive={currentPage === page} + > + {page} + + + )) + } + + {currentPage < totalPages - 2 && ( + <> + {currentPage < totalPages - 3 && } + + setCurrentPage(totalPages)} isActive={currentPage === totalPages}> + {totalPages} + + + + )} + + + setCurrentPage(p => Math.min(totalPages, p + 1))} + className={currentPage === totalPages ? 'pointer-events-none opacity-50' : 'cursor-pointer'} + /> + + + + )} +
+ )} ); }); \ No newline at end of file