import { useEffect, useState, useRef, useCallback } from 'react'; import { supabase } from '@/lib/supabaseClient'; import { reportsService } from '@/services/reports'; // Type for submission realtime payload interface SubmissionPayload { status?: string; assigned_to?: string | null; locked_until?: string | null; escalated?: boolean; } interface ModerationStats { pendingSubmissions: number; openReports: number; flaggedContent: number; } interface UseModerationStatsOptions { onStatsChange?: (stats: ModerationStats) => void; enabled?: boolean; pollingEnabled?: boolean; pollingInterval?: number; realtimeEnabled?: boolean; } export const useModerationStats = (options: UseModerationStatsOptions = {}) => { const { onStatsChange, enabled = true, pollingEnabled = true, pollingInterval = 60000, // Reduced to 60 seconds realtimeEnabled = true } = options; const [stats, setStats] = useState({ pendingSubmissions: 0, openReports: 0, flaggedContent: 0, }); // Optimistic deltas for immediate UI updates const [optimisticDeltas, setOptimisticDeltas] = useState({ pendingSubmissions: 0, openReports: 0, flaggedContent: 0, }); const [isLoading, setIsLoading] = useState(true); const [isInitialLoad, setIsInitialLoad] = useState(true); const [lastUpdated, setLastUpdated] = useState(null); const onStatsChangeRef = useRef(onStatsChange); const statsDebounceRef = useRef(null); // Update ref when callback changes useEffect(() => { onStatsChangeRef.current = onStatsChange; }, [onStatsChange]); // Optimistic update function const optimisticallyUpdateStats = useCallback((delta: Partial) => { setOptimisticDeltas(prev => ({ pendingSubmissions: (prev.pendingSubmissions || 0) + (delta.pendingSubmissions || 0), openReports: (prev.openReports || 0) + (delta.openReports || 0), flaggedContent: (prev.flaggedContent || 0) + (delta.flaggedContent || 0), })); }, []); const fetchStats = useCallback(async (silent = false) => { if (!enabled) return; try { // Only show loading on initial load if (!silent) { setIsLoading(true); } // Fetch stats - use Django API for reports, Supabase for submissions and reviews const [submissionsResult, reportsStatsResult, reviewsResult] = await Promise.all([ supabase .from('content_submissions') .select('id', { count: 'exact', head: true }) .eq('status', 'pending'), reportsService.getStatistics(), supabase .from('reviews') .select('id', { count: 'exact', head: true }) .eq('moderation_status', 'flagged'), ]); const newStats = { pendingSubmissions: submissionsResult.count || 0, openReports: reportsStatsResult.success && reportsStatsResult.data ? reportsStatsResult.data.pending_reports : 0, flaggedContent: reviewsResult.count || 0, }; setStats(newStats); setLastUpdated(new Date()); onStatsChangeRef.current?.(newStats); // Clear optimistic deltas when real data arrives setOptimisticDeltas({ pendingSubmissions: 0, openReports: 0, flaggedContent: 0, }); } catch (error: unknown) { // Silent failure - stats refresh periodically in background // Error already captured for potential monitoring } finally { // Only clear loading if it was set if (!silent) { setIsLoading(false); } if (isInitialLoad) { setIsInitialLoad(false); } } }, [enabled, isInitialLoad]); // Initial fetch useEffect(() => { if (enabled) { fetchStats(false); // Show loading } }, [enabled, fetchStats]); // Debounced stats fetch to prevent rapid-fire updates const debouncedFetchStats = useCallback(() => { if (statsDebounceRef.current) { clearTimeout(statsDebounceRef.current); } statsDebounceRef.current = setTimeout(() => { fetchStats(true); // Silent refresh }, 2000); // 2 second debounce to reduce flashing }, [fetchStats]); // Realtime subscription - only for content_submissions and reviews // Reports use polling since Django API doesn't support realtime useEffect(() => { if (!enabled || !realtimeEnabled) return; const channel = supabase .channel('moderation-stats-realtime') // Listen to ALL events on content_submissions without filter // Manual filtering catches submissions leaving pending state .on('postgres_changes', { event: '*', schema: 'public', table: 'content_submissions' }, (payload) => { const oldData = payload.old as SubmissionPayload; const newData = payload.new as SubmissionPayload; const oldStatus = oldData?.status; const newStatus = newData?.status; const oldAssignedTo = oldData?.assigned_to; const newAssignedTo = newData?.assigned_to; const oldLockedUntil = oldData?.locked_until; const newLockedUntil = newData?.locked_until; // Only refresh if change affects pending count or assignments if ( payload.eventType === 'INSERT' && newStatus === 'pending' || payload.eventType === 'UPDATE' && (oldStatus === 'pending' || newStatus === 'pending') || payload.eventType === 'DELETE' && oldStatus === 'pending' || payload.eventType === 'UPDATE' && (oldAssignedTo !== newAssignedTo || oldLockedUntil !== newLockedUntil) ) { debouncedFetchStats(); } }) .on('postgres_changes', { event: '*', schema: 'public', table: 'reviews', filter: 'moderation_status=eq.flagged' }, debouncedFetchStats) .subscribe(); return () => { supabase.removeChannel(channel); if (statsDebounceRef.current) { clearTimeout(statsDebounceRef.current); } }; }, [enabled, realtimeEnabled, debouncedFetchStats]); // Polling (fallback when realtime is disabled OR always for reports since Django has no realtime) useEffect(() => { if (!enabled || !pollingEnabled || isInitialLoad) return; const interval = setInterval(() => { fetchStats(true); // Silent refresh }, pollingInterval); return () => { clearInterval(interval); }; }, [enabled, pollingEnabled, pollingInterval, fetchStats, isInitialLoad]); // Combine real stats with optimistic deltas for display const displayStats = { pendingSubmissions: Math.max(0, stats.pendingSubmissions + optimisticDeltas.pendingSubmissions), openReports: Math.max(0, stats.openReports + optimisticDeltas.openReports), flaggedContent: Math.max(0, stats.flaggedContent + optimisticDeltas.flaggedContent), }; return { stats: displayStats, refresh: fetchStats, optimisticallyUpdateStats, isLoading, lastUpdated }; };