mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 20:51:17 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
563
src-old/hooks/moderation/useModerationQueueManager.ts
Normal file
563
src-old/hooks/moderation/useModerationQueueManager.ts
Normal file
@@ -0,0 +1,563 @@
|
||||
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<typeof useToast>["toast"];
|
||||
optimisticallyUpdateStats?: (delta: Partial<ModerationStats>) => 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<typeof useModerationFilters>;
|
||||
pagination: ReturnType<typeof usePagination>;
|
||||
queue: ReturnType<typeof useModerationQueue>;
|
||||
|
||||
// Realtime
|
||||
newItemsCount: number;
|
||||
pendingNewItems: ModerationItem[];
|
||||
showNewItems: () => void;
|
||||
|
||||
// Interaction tracking
|
||||
interactingWith: Set<string>;
|
||||
markInteracting: (id: string, interacting: boolean) => void;
|
||||
|
||||
// Actions
|
||||
refresh: () => void;
|
||||
performAction: (item: ModerationItem, action: "approved" | "rejected", moderatorNotes?: string) => Promise<void>;
|
||||
deleteSubmission: (item: ModerationItem) => Promise<void>;
|
||||
resetToPending: (item: ModerationItem) => Promise<void>;
|
||||
retryFailedItems: (item: ModerationItem) => Promise<void>;
|
||||
|
||||
// Caches (for QueueItem enrichment)
|
||||
entityCache: ReturnType<typeof useEntityCache>;
|
||||
profileCache: ReturnType<typeof useProfileCache>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<ModerationItem[]>([]);
|
||||
const [loadingState, setLoadingState] = useState<LoadingState>("initial");
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
||||
const [interactingWith, setInteractingWith] = useState<Set<string>>(new Set());
|
||||
const [pendingNewItems, setPendingNewItems] = useState<ModerationItem[]>([]);
|
||||
const [newItemsCount, setNewItemsCount] = useState(0);
|
||||
|
||||
// Refs for tracking
|
||||
const recentlyRemovedRef = useRef<Set<string>>(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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user