import { useState, useCallback, useRef, useEffect, useMemo } from "react"; import { supabase } from "@/lib/supabaseClient"; import { useToast } from "@/hooks/use-toast"; import { useAuth } from "@/hooks/useAuth"; import { logger } from "@/lib/logger"; import { getErrorMessage } from "@/lib/errorHandler"; import { invokeWithTracking } from "@/lib/edgeFunctionTracking"; import { MODERATION_CONSTANTS } from "@/lib/moderation/constants"; import { useQueryClient } from '@tanstack/react-query'; import type { User } from "@supabase/supabase-js"; import { useEntityCache, useProfileCache, useModerationFilters, usePagination, useRealtimeSubscriptions, useQueueQuery, } from "./index"; import { useModerationQueue } from "@/hooks/useModerationQueue"; import { useModerationActions } from "./useModerationActions"; import type { ModerationItem, EntityFilter, StatusFilter, LoadingState } from "@/types/moderation"; interface ModerationStats { pendingSubmissions: number; openReports: number; flaggedContent: number; } /** * Configuration for useModerationQueueManager */ export interface ModerationQueueManagerConfig { user: User | null; isAdmin: boolean; isSuperuser: boolean; toast: ReturnType["toast"]; optimisticallyUpdateStats?: (delta: Partial) => void; settings: { refreshMode: "auto" | "manual"; pollInterval: number; refreshStrategy: "notify" | "merge" | "replace"; preserveInteraction: boolean; useRealtimeQueue: boolean; }; } /** * Return type for useModerationQueueManager */ export interface ModerationQueueManager { // State items: ModerationItem[]; loadingState: LoadingState; actionLoading: string | null; // Sub-hooks (exposed for granular control) filters: ReturnType; pagination: ReturnType; queue: ReturnType; // Realtime newItemsCount: number; pendingNewItems: ModerationItem[]; showNewItems: () => void; // Interaction tracking interactingWith: Set; markInteracting: (id: string, interacting: boolean) => void; // Actions refresh: () => void; performAction: (item: ModerationItem, action: "approved" | "rejected", moderatorNotes?: string) => Promise; deleteSubmission: (item: ModerationItem) => Promise; resetToPending: (item: ModerationItem) => Promise; retryFailedItems: (item: ModerationItem) => Promise; // Caches (for QueueItem enrichment) entityCache: ReturnType; profileCache: ReturnType; } /** * Orchestrator hook for moderation queue management * Consolidates all queue-related logic into a single hook */ export function useModerationQueueManager(config: ModerationQueueManagerConfig): ModerationQueueManager { logger.log('🚀 [QUEUE MANAGER] Hook mounting/rendering', { hasUser: !!config.user, isAdmin: config.isAdmin, timestamp: new Date().toISOString() }); const { user, isAdmin, isSuperuser, toast, optimisticallyUpdateStats, settings } = config; const queryClient = useQueryClient(); const { aal } = useAuth(); // Debug AAL status useEffect(() => { logger.log('🔐 [QUEUE MANAGER] AAL Status:', { aal, isNull: aal === null, isAal1: aal === 'aal1', isAal2: aal === 'aal2', timestamp: new Date().toISOString() }); }, [aal]); // Initialize sub-hooks const filters = useModerationFilters({ initialEntityFilter: "all", initialStatusFilter: "pending", initialTab: "mainQueue", debounceDelay: 300, persist: true, storageKey: "moderationQueue_filters", }); // Memoize filters object for realtime subscriptions to prevent reconnections const realtimeFilters = useMemo(() => ({ entityFilter: filters.debouncedEntityFilter, statusFilter: filters.debouncedStatusFilter, }), [filters.debouncedEntityFilter, filters.debouncedStatusFilter]); const pagination = usePagination({ initialPage: 1, initialPageSize: 25, persist: false, onPageChange: (page) => { if (page > 1) { setLoadingState("loading"); } }, onPageSizeChange: () => { setLoadingState("loading"); }, }); // Use a stable callback via ref to prevent excessive re-renders const lockStateChangeHandlerRef = useRef<() => void>(); const queue = useModerationQueue({ onLockStateChange: useCallback(() => { lockStateChangeHandlerRef.current?.(); }, []) }); const entityCache = useEntityCache(); const profileCache = useProfileCache(); // Core state const [items, setItems] = useState([]); const [loadingState, setLoadingState] = useState("initial"); const [actionLoading, setActionLoading] = useState(null); const [interactingWith, setInteractingWith] = useState>(new Set()); const [pendingNewItems, setPendingNewItems] = useState([]); const [newItemsCount, setNewItemsCount] = useState(0); // Refs for tracking const recentlyRemovedRef = useRef>(new Set()); const initialFetchCompleteRef = useRef(false); const isMountingRef = useRef(true); /** * Replace manual fetching with TanStack Query * Use direct state values for stable query keys */ const queueQuery = useQueueQuery({ userId: user?.id, isAdmin, isSuperuser, entityFilter: filters.debouncedEntityFilter, statusFilter: filters.debouncedStatusFilter, tab: filters.activeTab, currentPage: pagination.currentPage, pageSize: pagination.pageSize, sortConfig: filters.debouncedSortConfig, enabled: !!user, }); // Update the lock state change handler ref whenever queueQuery changes lockStateChangeHandlerRef.current = () => { logger.log('🔄 Lock state changed, invalidating queue cache'); queueQuery.invalidate(); setLoadingState(prev => prev === "loading" ? "ready" : prev); }; // Update items when query data changes useEffect(() => { if (queueQuery.items) { setItems(queueQuery.items); logger.log('✅ Queue items updated from TanStack Query:', queueQuery.items.length); } }, [queueQuery.items]); // Update loading state based on query status useEffect(() => { if (queueQuery.isLoading) { setLoadingState('loading'); } else if (queueQuery.isRefreshing) { setLoadingState('refreshing'); } else { setLoadingState('ready'); } }, [queueQuery.isLoading, queueQuery.isRefreshing]); // Show error toast when query fails useEffect(() => { if (queueQuery.error) { // Error already captured by TanStack Query toast({ variant: 'destructive', title: 'Failed to Load Queue', description: queueQuery.error.message || 'An error occurred while fetching the moderation queue.', }); } }, [queueQuery.error, toast]); // Extract stable callback to prevent infinite loop const { setTotalCount } = pagination; // Update total count for pagination useEffect(() => { setTotalCount(queueQuery.totalCount); }, [queueQuery.totalCount, setTotalCount]); // Mark initial fetch as complete useEffect(() => { if (!queueQuery.isLoading && !initialFetchCompleteRef.current) { initialFetchCompleteRef.current = true; logger.log('✅ Initial queue fetch complete'); } }, [queueQuery.isLoading]); /** * Manual refresh function */ const refresh = useCallback(async () => { logger.log('🔄 Manual refresh triggered'); await queueQuery.refetch(); }, [queueQuery]); /** * Show pending new items by invalidating query */ const showNewItems = useCallback(async () => { logger.log('✅ Showing new items via query invalidation'); await queueQuery.invalidate(); setPendingNewItems([]); setNewItemsCount(0); }, [queueQuery]); /** * Mark an item as being interacted with (prevents realtime updates) */ const markInteracting = useCallback((id: string, interacting: boolean) => { setInteractingWith((prev) => { const next = new Set(prev); if (interacting) { next.add(id); } else { next.delete(id); } return next; }); }, []); /** * Use validated action handler from useModerationActions */ const moderationActions = useModerationActions({ user, onActionStart: setActionLoading, onActionComplete: () => { setActionLoading(null); refresh(); queue.refreshStats(); }, currentLockSubmissionId: queue.currentLock?.submissionId, }); /** * Perform moderation action (approve/reject) - delegates to validated handler */ const performAction = useCallback( async (item: ModerationItem, action: "approved" | "rejected", moderatorNotes?: string) => { // Release lock if held if (queue.currentLock?.submissionId === item.id) { await queue.releaseLock(item.id, true); } // Use validated action handler await moderationActions.performAction(item, action, moderatorNotes); }, [moderationActions, queue] ); /** * Delete a submission permanently */ const deleteSubmission = useCallback( async (item: ModerationItem) => { if (item.type !== "content_submission") return; if (actionLoading === item.id) return; setActionLoading(item.id); setItems((prev) => prev.filter((i) => i.id !== item.id)); try { const { error } = await supabase.from("content_submissions").delete().eq("id", item.id); if (error) throw error; toast({ title: "Submission deleted", description: "The submission has been permanently deleted", }); // Refresh stats to update counts queue.refreshStats(); } catch (error: unknown) { const errorMsg = getErrorMessage(error); // Silent - operation handled optimistically setItems((prev) => { if (prev.some((i) => i.id === item.id)) return prev; return [...prev, item]; }); toast({ title: "Error", description: "Failed to delete submission", variant: "destructive", }); } finally { setActionLoading(null); } }, [actionLoading, toast], ); /** * Reset submission to pending status */ const resetToPending = useCallback( async (item: ModerationItem) => { setActionLoading(item.id); try { const { resetRejectedItemsToPending } = await import("@/lib/submissionItemsService"); await resetRejectedItemsToPending(item.id); toast({ title: "Reset Complete", description: "Submission and all items have been reset to pending status", }); // Refresh stats to update counts queue.refreshStats(); setItems((prev) => prev.filter((i) => i.id !== item.id)); } catch (error: unknown) { const errorMsg = getErrorMessage(error); // Silent - operation handled optimistically toast({ title: "Reset Failed", description: errorMsg, variant: "destructive", }); } finally { setActionLoading(null); } }, [toast], ); /** * Retry failed items in a submission */ const retryFailedItems = useCallback( async (item: ModerationItem) => { setActionLoading(item.id); const shouldRemove = filters.statusFilter === "pending" || filters.statusFilter === "flagged" || filters.statusFilter === "partially_approved"; if (shouldRemove) { requestAnimationFrame(() => { setItems((prev) => prev.filter((i) => i.id !== item.id)); recentlyRemovedRef.current.add(item.id); setTimeout(() => recentlyRemovedRef.current.delete(item.id), 10000); }); } try { const { data: failedItems } = await supabase .from("submission_items") .select("id") .eq("submission_id", item.id) .eq("status", "rejected"); if (!failedItems || failedItems.length === 0) { toast({ title: "No Failed Items", description: "All items have been processed successfully", }); return; } const { data, error, requestId } = await invokeWithTracking( "process-selective-approval", { itemIds: failedItems.map((i) => i.id), submissionId: item.id, }, user?.id ); if (error) throw error; toast({ title: "Retry Complete", description: `Processed ${failedItems.length} failed item(s)${requestId ? ` (Request: ${requestId.substring(0, 8)})` : ""}`, }); // Refresh stats to update counts queue.refreshStats(); } catch (error: unknown) { const errorMsg = getErrorMessage(error); // Silent - operation handled optimistically toast({ title: "Retry Failed", description: errorMsg, variant: "destructive", }); } finally { setActionLoading(null); } }, [filters.statusFilter, toast], ); // Extract stable callbacks to prevent infinite loop in effects const { invalidate: invalidateQuery } = queueQuery; const { reset: resetPagination } = pagination; // Mark initial fetch as complete when query loads useEffect(() => { if (!queueQuery.isLoading && !initialFetchCompleteRef.current) { initialFetchCompleteRef.current = true; isMountingRef.current = false; logger.log('✅ Initial queue fetch complete'); } }, [queueQuery.isLoading]); // Invalidate query when filters or sort changes (OPTIMIZED) useEffect(() => { if ( !user || !initialFetchCompleteRef.current || isMountingRef.current ) return; logger.log('🔄 Filters/sort changed, invalidating query'); resetPagination(); invalidateQuery(); }, [ filters.debouncedEntityFilter, filters.debouncedStatusFilter, filters.debouncedSortConfig.field, filters.debouncedSortConfig.direction, user, invalidateQuery, resetPagination ]); // Polling effect (when realtime disabled) - MUTUALLY EXCLUSIVE useEffect(() => { const shouldPoll = settings.refreshMode === 'auto' && !settings.useRealtimeQueue && loadingState !== 'initial' && !!user; if (!shouldPoll) { return; } logger.log("⚠️ Polling ENABLED - interval:", settings.pollInterval); const interval = setInterval(() => { logger.log("🔄 Polling refresh triggered"); queueQuery.refetch(); }, settings.pollInterval); return () => { clearInterval(interval); logger.log("🛑 Polling stopped"); }; }, [user, settings.refreshMode, settings.pollInterval, loadingState, settings.useRealtimeQueue, queueQuery.refetch]); // Initialize realtime subscriptions useRealtimeSubscriptions({ enabled: settings.useRealtimeQueue && !!user, filters: realtimeFilters, onNewItem: (item: ModerationItem) => { if (recentlyRemovedRef.current.has(item.id)) return; setPendingNewItems((prev) => { if (prev.some((p) => p.id === item.id)) return prev; return [...prev, item]; }); setNewItemsCount((prev) => prev + 1); toast({ title: "🆕 New Submission", description: `${item.submission_type} - ${item.entity_name}`, }); }, onUpdateItem: (item: ModerationItem, shouldRemove: boolean) => { if (recentlyRemovedRef.current.has(item.id)) return; if (interactingWith.has(item.id)) return; // Only track removals for optimistic update protection if (shouldRemove && !recentlyRemovedRef.current.has(item.id)) { recentlyRemovedRef.current.add(item.id); setTimeout(() => recentlyRemovedRef.current.delete(item.id), MODERATION_CONSTANTS.REALTIME_OPTIMISTIC_REMOVAL_TIMEOUT); } // TanStack Query handles actual state updates via invalidation }, onItemRemoved: (itemId: string) => { // Track for optimistic update protection recentlyRemovedRef.current.add(itemId); setTimeout(() => recentlyRemovedRef.current.delete(itemId), MODERATION_CONSTANTS.REALTIME_OPTIMISTIC_REMOVAL_TIMEOUT); // TanStack Query handles removal via invalidation }, entityCache, profileCache, recentlyRemovedIds: recentlyRemovedRef.current, interactingWithIds: interactingWith, }); return { items, loadingState, actionLoading, filters, pagination, queue, newItemsCount, pendingNewItems, showNewItems, interactingWith, markInteracting, refresh, performAction, deleteSubmission, resetToPending, retryFailedItems, entityCache, profileCache, }; }