Code edited in Lovable Code Editor

This commit is contained in:
gpt-engineer-app[bot]
2025-10-13 00:07:47 +00:00
parent 43d36a97e0
commit 281b30bd65

View File

@@ -1,24 +1,18 @@
import { useState, useCallback, useRef, useEffect } from 'react'; import { useState, useCallback, useRef, useEffect } from "react";
import { supabase } from '@/integrations/supabase/client'; import { supabase } from "@/integrations/supabase/client";
import { useToast } from '@/hooks/use-toast'; import { useToast } from "@/hooks/use-toast";
import type { User } from '@supabase/supabase-js'; import type { User } from "@supabase/supabase-js";
import { import {
useEntityCache, useEntityCache,
useProfileCache, useProfileCache,
useModerationFilters, useModerationFilters,
useModerationSort, useModerationSort,
usePagination, usePagination,
useRealtimeSubscriptions useRealtimeSubscriptions,
} from './index'; } from "./index";
import { useModerationQueue } from '@/hooks/useModerationQueue'; import { useModerationQueue } from "@/hooks/useModerationQueue";
import { smartMergeArray } from '@/lib/smartStateUpdate'; import { smartMergeArray } from "@/lib/smartStateUpdate";
import type { import type { ModerationItem, EntityFilter, StatusFilter, LoadingState, SortConfig } from "@/types/moderation";
ModerationItem,
EntityFilter,
StatusFilter,
LoadingState,
SortConfig
} from '@/types/moderation';
/** /**
* Configuration for useModerationQueueManager * Configuration for useModerationQueueManager
@@ -27,11 +21,11 @@ export interface ModerationQueueManagerConfig {
user: User | null; user: User | null;
isAdmin: boolean; isAdmin: boolean;
isSuperuser: boolean; isSuperuser: boolean;
toast: ReturnType<typeof useToast>['toast']; toast: ReturnType<typeof useToast>["toast"];
settings: { settings: {
refreshMode: 'auto' | 'manual'; refreshMode: "auto" | "manual";
pollInterval: number; pollInterval: number;
refreshStrategy: 'notify' | 'merge' | 'replace'; refreshStrategy: "notify" | "merge" | "replace";
preserveInteraction: boolean; preserveInteraction: boolean;
useRealtimeQueue: boolean; useRealtimeQueue: boolean;
refreshOnTabVisible: boolean; refreshOnTabVisible: boolean;
@@ -64,11 +58,7 @@ export interface ModerationQueueManager {
// Actions // Actions
refresh: () => void; refresh: () => void;
performAction: ( performAction: (item: ModerationItem, action: "approved" | "rejected", moderatorNotes?: string) => Promise<void>;
item: ModerationItem,
action: 'approved' | 'rejected',
moderatorNotes?: string
) => Promise<void>;
deleteSubmission: (item: ModerationItem) => Promise<void>; deleteSubmission: (item: ModerationItem) => Promise<void>;
resetToPending: (item: ModerationItem) => Promise<void>; resetToPending: (item: ModerationItem) => Promise<void>;
retryFailedItems: (item: ModerationItem) => Promise<void>; retryFailedItems: (item: ModerationItem) => Promise<void>;
@@ -82,19 +72,17 @@ export interface ModerationQueueManager {
* Orchestrator hook for moderation queue management * Orchestrator hook for moderation queue management
* Consolidates all queue-related logic into a single hook * Consolidates all queue-related logic into a single hook
*/ */
export function useModerationQueueManager( export function useModerationQueueManager(config: ModerationQueueManagerConfig): ModerationQueueManager {
config: ModerationQueueManagerConfig
): ModerationQueueManager {
const { user, isAdmin, isSuperuser, toast, settings } = config; const { user, isAdmin, isSuperuser, toast, settings } = config;
// Initialize sub-hooks // Initialize sub-hooks
const filters = useModerationFilters({ const filters = useModerationFilters({
initialEntityFilter: 'all', initialEntityFilter: "all",
initialStatusFilter: 'pending', initialStatusFilter: "pending",
initialTab: 'mainQueue', initialTab: "mainQueue",
debounceDelay: 300, debounceDelay: 300,
persist: true, persist: true,
storageKey: 'moderationQueue_filters' storageKey: "moderationQueue_filters",
}); });
const pagination = usePagination({ const pagination = usePagination({
@@ -103,18 +91,18 @@ export function useModerationQueueManager(
persist: false, persist: false,
onPageChange: (page) => { onPageChange: (page) => {
if (page > 1) { if (page > 1) {
setLoadingState('loading'); setLoadingState("loading");
} }
}, },
onPageSizeChange: () => { onPageSizeChange: () => {
setLoadingState('loading'); setLoadingState("loading");
} },
}); });
const sort = useModerationSort({ const sort = useModerationSort({
initialConfig: { field: 'created_at', direction: 'asc' }, initialConfig: { field: "created_at", direction: "asc" },
persist: true, persist: true,
storageKey: 'moderationQueue_sortConfig' storageKey: "moderationQueue_sortConfig",
}); });
const queue = useModerationQueue(); const queue = useModerationQueue();
@@ -123,7 +111,7 @@ export function useModerationQueueManager(
// Core state // Core state
const [items, setItems] = useState<ModerationItem[]>([]); const [items, setItems] = useState<ModerationItem[]>([]);
const [loadingState, setLoadingState] = useState<LoadingState>('initial'); const [loadingState, setLoadingState] = useState<LoadingState>("initial");
const [actionLoading, setActionLoading] = useState<string | null>(null); const [actionLoading, setActionLoading] = useState<string | null>(null);
const [interactingWith, setInteractingWith] = useState<Set<string>>(new Set()); const [interactingWith, setInteractingWith] = useState<Set<string>>(new Set());
const [pendingNewItems, setPendingNewItems] = useState<ModerationItem[]>([]); const [pendingNewItems, setPendingNewItems] = useState<ModerationItem[]>([]);
@@ -155,30 +143,31 @@ export function useModerationQueueManager(
/** /**
* Fetch queue items from database * Fetch queue items from database
*/ */
const fetchItems = useCallback(async (silent = false) => { const fetchItems = useCallback(
async (silent = false) => {
if (!user) return; if (!user) return;
// Get caller info // Get caller info
const callerStack = new Error().stack; const callerStack = new Error().stack;
const callerLine = callerStack?.split('\n')[2]?.trim(); const callerLine = callerStack?.split("\n")[2]?.trim();
console.log('🔄 [FETCH ITEMS] Called', { console.log("🔄 [FETCH ITEMS] Called", {
silent, silent,
pauseFetchingRef: pauseFetchingRef.current, pauseFetchingRef: pauseFetchingRef.current,
documentHidden: document.hidden, documentHidden: document.hidden,
caller: callerLine, caller: callerLine,
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
// Check if fetching is paused (controlled by visibility handler if enabled) // Check if fetching is paused (controlled by visibility handler if enabled)
if (pauseFetchingRef.current) { if (pauseFetchingRef.current) {
console.log('⏸️ Fetch paused by pauseFetchingRef'); console.log("⏸️ Fetch paused by pauseFetchingRef");
return; return;
} }
// Prevent concurrent calls // Prevent concurrent calls
if (fetchInProgressRef.current) { if (fetchInProgressRef.current) {
console.log('⚠️ Fetch already in progress, skipping'); console.log("⚠️ Fetch already in progress, skipping");
return; return;
} }
@@ -193,25 +182,26 @@ export function useModerationQueueManager(
fetchInProgressRef.current = true; fetchInProgressRef.current = true;
lastFetchTimeRef.current = now; lastFetchTimeRef.current = now;
console.log('🔍 fetchItems called:', { console.log("🔍 fetchItems called:", {
entityFilter: filters.debouncedEntityFilter, entityFilter: filters.debouncedEntityFilter,
statusFilter: filters.debouncedStatusFilter, statusFilter: filters.debouncedStatusFilter,
silent, silent,
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
try { try {
// Set loading states // Set loading states
if (!silent) { if (!silent) {
setLoadingState('loading'); setLoadingState("loading");
} else { } else {
setLoadingState('refreshing'); setLoadingState("refreshing");
} }
// Build base query // Build base query
let submissionsQuery = supabase let submissionsQuery = supabase
.from('content_submissions') .from("content_submissions")
.select(` .select(
`
id, id,
submission_type, submission_type,
status, status,
@@ -230,50 +220,51 @@ export function useModerationQueueManager(
item_data, item_data,
status status
) )
`) `,
.order('escalated', { ascending: false }) )
.order('created_at', { ascending: true }); .order("escalated", { ascending: false })
.order("created_at", { ascending: true });
// Apply tab-based status filtering // Apply tab-based status filtering
const tab = filters.activeTab; const tab = filters.activeTab;
const statusFilter = filters.debouncedStatusFilter; const statusFilter = filters.debouncedStatusFilter;
const entityFilter = filters.debouncedEntityFilter; const entityFilter = filters.debouncedEntityFilter;
if (tab === 'mainQueue') { if (tab === "mainQueue") {
if (statusFilter === 'all') { if (statusFilter === "all") {
submissionsQuery = submissionsQuery.in('status', ['pending', 'flagged', 'partially_approved']); submissionsQuery = submissionsQuery.in("status", ["pending", "flagged", "partially_approved"]);
} else if (statusFilter === 'pending') { } else if (statusFilter === "pending") {
submissionsQuery = submissionsQuery.in('status', ['pending', 'partially_approved']); submissionsQuery = submissionsQuery.in("status", ["pending", "partially_approved"]);
} else { } else {
submissionsQuery = submissionsQuery.eq('status', statusFilter); submissionsQuery = submissionsQuery.eq("status", statusFilter);
} }
} else { } else {
if (statusFilter === 'all') { if (statusFilter === "all") {
submissionsQuery = submissionsQuery.in('status', ['approved', 'rejected']); submissionsQuery = submissionsQuery.in("status", ["approved", "rejected"]);
} else { } else {
submissionsQuery = submissionsQuery.eq('status', statusFilter); submissionsQuery = submissionsQuery.eq("status", statusFilter);
} }
} }
// Apply entity type filter // Apply entity type filter
if (entityFilter === 'photos') { if (entityFilter === "photos") {
submissionsQuery = submissionsQuery.eq('submission_type', 'photo'); submissionsQuery = submissionsQuery.eq("submission_type", "photo");
} else if (entityFilter === 'submissions') { } else if (entityFilter === "submissions") {
submissionsQuery = submissionsQuery.neq('submission_type', 'photo'); submissionsQuery = submissionsQuery.neq("submission_type", "photo");
} }
// Apply access control // Apply access control
if (!isAdmin && !isSuperuser) { if (!isAdmin && !isSuperuser) {
const now = new Date().toISOString(); const now = new Date().toISOString();
submissionsQuery = submissionsQuery.or( submissionsQuery = submissionsQuery.or(
`assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${user.id}` `assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${user.id}`,
); );
} }
// Get total count // Get total count
const { count } = await supabase const { count } = await supabase
.from('content_submissions') .from("content_submissions")
.select('*', { count: 'exact', head: true }) .select("*", { count: "exact", head: true })
.match(submissionsQuery as any); .match(submissionsQuery as any);
pagination.setTotalCount(count || 0); pagination.setTotalCount(count || 0);
@@ -288,10 +279,12 @@ export function useModerationQueueManager(
if (submissionsError) throw submissionsError; if (submissionsError) throw submissionsError;
// Fetch related profiles and entities // Fetch related profiles and entities
const userIds = [...new Set([ const userIds = [
...(submissions?.map(s => s.user_id) || []), ...new Set([
...(submissions?.map(s => s.reviewer_id).filter(Boolean) || []) ...(submissions?.map((s) => s.user_id) || []),
])]; ...(submissions?.map((s) => s.reviewer_id).filter(Boolean) || []),
]),
];
if (userIds.length > 0) { if (userIds.length > 0) {
await profileCache.bulkFetch(userIds); await profileCache.bulkFetch(userIds);
@@ -303,26 +296,30 @@ export function useModerationQueueManager(
} }
// Map to ModerationItems // Map to ModerationItems
const moderationItems: ModerationItem[] = submissions?.map(submission => { const moderationItems: ModerationItem[] =
submissions?.map((submission) => {
const content = submission.content as any; const content = submission.content as any;
let entityName = content?.name || 'Unknown'; let entityName = content?.name || "Unknown";
let parkName: string | undefined; let parkName: string | undefined;
// Resolve entity names from cache // Resolve entity names from cache
if (submission.submission_type === 'ride' && content?.entity_id) { if (submission.submission_type === "ride" && content?.entity_id) {
const ride = entityCache.getCached('rides', content.entity_id); const ride = entityCache.getCached("rides", content.entity_id);
if (ride) { if (ride) {
entityName = ride.name; entityName = ride.name;
if (ride.park_id) { if (ride.park_id) {
const park = entityCache.getCached('parks', ride.park_id); const park = entityCache.getCached("parks", ride.park_id);
if (park) parkName = park.name; if (park) parkName = park.name;
} }
} }
} else if (submission.submission_type === 'park' && content?.entity_id) { } else if (submission.submission_type === "park" && content?.entity_id) {
const park = entityCache.getCached('parks', content.entity_id); const park = entityCache.getCached("parks", content.entity_id);
if (park) entityName = park.name; if (park) entityName = park.name;
} else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(submission.submission_type) && content?.entity_id) { } else if (
const company = entityCache.getCached('companies', content.entity_id); ["manufacturer", "operator", "designer", "property_owner"].includes(submission.submission_type) &&
content?.entity_id
) {
const company = entityCache.getCached("companies", content.entity_id);
if (company) entityName = company.name; if (company) entityName = company.name;
} }
@@ -331,7 +328,7 @@ export function useModerationQueueManager(
return { return {
id: submission.id, id: submission.id,
type: 'content_submission', type: "content_submission",
content: submission.content, content: submission.content,
created_at: submission.created_at, created_at: submission.created_at,
user_id: submission.user_id, user_id: submission.user_id,
@@ -357,18 +354,18 @@ export function useModerationQueueManager(
if (silent) { if (silent) {
// Background polling: detect new submissions // Background polling: detect new submissions
const currentDisplayedIds = new Set(itemsRef.current.map(item => item.id)); const currentDisplayedIds = new Set(itemsRef.current.map((item) => item.id));
const newSubmissions = moderationItems.filter(item => !currentDisplayedIds.has(item.id)); const newSubmissions = moderationItems.filter((item) => !currentDisplayedIds.has(item.id));
if (newSubmissions.length > 0) { if (newSubmissions.length > 0) {
console.log('🆕 Detected new submissions:', newSubmissions.length); console.log("🆕 Detected new submissions:", newSubmissions.length);
setPendingNewItems(prev => { setPendingNewItems((prev) => {
const existingIds = new Set(prev.map(p => p.id)); const existingIds = new Set(prev.map((p) => p.id));
const uniqueNew = newSubmissions.filter(item => !existingIds.has(item.id)); const uniqueNew = newSubmissions.filter((item) => !existingIds.has(item.id));
if (uniqueNew.length > 0) { if (uniqueNew.length > 0) {
setNewItemsCount(prev => prev + uniqueNew.length); setNewItemsCount((prev) => prev + uniqueNew.length);
} }
return [...prev, ...uniqueNew]; return [...prev, ...uniqueNew];
@@ -377,32 +374,40 @@ export function useModerationQueueManager(
// Apply refresh strategy // Apply refresh strategy
switch (currentRefreshStrategy) { switch (currentRefreshStrategy) {
case 'notify': case "notify":
console.log('✅ Queue frozen (notify mode)'); console.log("✅ Queue frozen (notify mode)");
break; break;
case 'merge': case "merge":
if (newSubmissions.length > 0) { if (newSubmissions.length > 0) {
const currentIds = new Set(itemsRef.current.map(item => item.id)); const currentIds = new Set(itemsRef.current.map((item) => item.id));
const trulyNewSubmissions = newSubmissions.filter(item => !currentIds.has(item.id)); const trulyNewSubmissions = newSubmissions.filter((item) => !currentIds.has(item.id));
if (trulyNewSubmissions.length > 0) { if (trulyNewSubmissions.length > 0) {
setItems(prev => [...prev, ...trulyNewSubmissions]); setItems((prev) => [...prev, ...trulyNewSubmissions]);
console.log('🔀 Queue merged - added', trulyNewSubmissions.length, 'items'); console.log("🔀 Queue merged - added", trulyNewSubmissions.length, "items");
} }
} }
break; break;
case 'replace': case "replace":
const mergeResult = smartMergeArray(itemsRef.current, moderationItems, { const mergeResult = smartMergeArray(itemsRef.current, moderationItems, {
compareFields: ['status', 'content', 'reviewed_at', 'reviewed_by', 'reviewer_notes', 'assigned_to', 'locked_until'], compareFields: [
"status",
"content",
"reviewed_at",
"reviewed_by",
"reviewer_notes",
"assigned_to",
"locked_until",
],
preserveOrder: false, preserveOrder: false,
addToTop: false, addToTop: false,
}); });
if (mergeResult.hasChanges) { if (mergeResult.hasChanges) {
setItems(mergeResult.items); setItems(mergeResult.items);
console.log('🔄 Queue updated (replace mode)'); console.log("🔄 Queue updated (replace mode)");
} }
if (!currentPreserveInteraction) { if (!currentPreserveInteraction) {
@@ -414,32 +419,41 @@ export function useModerationQueueManager(
} else { } else {
// Manual refresh // Manual refresh
const mergeResult = smartMergeArray(itemsRef.current, moderationItems, { const mergeResult = smartMergeArray(itemsRef.current, moderationItems, {
compareFields: ['status', 'content', 'reviewed_at', 'reviewed_by', 'reviewer_notes', 'assigned_to', 'locked_until'], compareFields: [
"status",
"content",
"reviewed_at",
"reviewed_by",
"reviewer_notes",
"assigned_to",
"locked_until",
],
preserveOrder: false, preserveOrder: false,
addToTop: false, addToTop: false,
}); });
if (mergeResult.hasChanges) { if (mergeResult.hasChanges) {
setItems(mergeResult.items); setItems(mergeResult.items);
console.log('🔄 Queue updated (manual refresh)'); console.log("🔄 Queue updated (manual refresh)");
} }
setPendingNewItems([]); setPendingNewItems([]);
setNewItemsCount(0); setNewItemsCount(0);
} }
} catch (error: any) { } catch (error: any) {
console.error('Error fetching moderation items:', error); console.error("Error fetching moderation items:", error);
toast({ toast({
title: 'Error', title: "Error",
description: error.message || 'Failed to fetch moderation queue', description: error.message || "Failed to fetch moderation queue",
variant: 'destructive', variant: "destructive",
}); });
} finally { } finally {
fetchInProgressRef.current = false; fetchInProgressRef.current = false;
setLoadingState('ready'); setLoadingState("ready");
} }
}, [user, isAdmin, isSuperuser, filters, pagination, profileCache, entityCache, toast]); },
[user, isAdmin, isSuperuser, filters, pagination, profileCache, entityCache, toast],
);
// Store fetchItems in ref to avoid re-creating visibility listener // Store fetchItems in ref to avoid re-creating visibility listener
useEffect(() => { useEffect(() => {
@@ -451,10 +465,10 @@ export function useModerationQueueManager(
*/ */
const showNewItems = useCallback(() => { const showNewItems = useCallback(() => {
if (pendingNewItems.length > 0) { if (pendingNewItems.length > 0) {
setItems(prev => [...pendingNewItems, ...prev]); setItems((prev) => [...pendingNewItems, ...prev]);
setPendingNewItems([]); setPendingNewItems([]);
setNewItemsCount(0); setNewItemsCount(0);
console.log('✅ New items merged into queue:', pendingNewItems.length); console.log("✅ New items merged into queue:", pendingNewItems.length);
} }
}, [pendingNewItems]); }, [pendingNewItems]);
@@ -462,7 +476,7 @@ export function useModerationQueueManager(
* Mark an item as being interacted with (prevents realtime updates) * Mark an item as being interacted with (prevents realtime updates)
*/ */
const markInteracting = useCallback((id: string, interacting: boolean) => { const markInteracting = useCallback((id: string, interacting: boolean) => {
setInteractingWith(prev => { setInteractingWith((prev) => {
const next = new Set(prev); const next = new Set(prev);
if (interacting) { if (interacting) {
next.add(id); next.add(id);
@@ -476,26 +490,22 @@ export function useModerationQueueManager(
/** /**
* Perform moderation action (approve/reject) * Perform moderation action (approve/reject)
*/ */
const performAction = useCallback(async ( const performAction = useCallback(
item: ModerationItem, async (item: ModerationItem, action: "approved" | "rejected", moderatorNotes?: string) => {
action: 'approved' | 'rejected',
moderatorNotes?: string
) => {
if (actionLoading === item.id) return; if (actionLoading === item.id) return;
setActionLoading(item.id); setActionLoading(item.id);
// Optimistic update // Optimistic update
const shouldRemove = (filters.statusFilter === 'pending' || filters.statusFilter === 'flagged') && const shouldRemove =
(action === 'approved' || action === 'rejected'); (filters.statusFilter === "pending" || filters.statusFilter === "flagged") &&
(action === "approved" || action === "rejected");
if (shouldRemove) { if (shouldRemove) {
setItems(prev => prev.map(i => setItems((prev) => prev.map((i) => (i.id === item.id ? { ...i, _removing: true } : i)));
i.id === item.id ? { ...i, _removing: true } : i
));
setTimeout(() => { setTimeout(() => {
setItems(prev => prev.filter(i => i.id !== item.id)); setItems((prev) => prev.filter((i) => i.id !== item.id));
recentlyRemovedRef.current.add(item.id); recentlyRemovedRef.current.add(item.id);
setTimeout(() => recentlyRemovedRef.current.delete(item.id), 10000); setTimeout(() => recentlyRemovedRef.current.delete(item.id), 10000);
}, 300); }, 300);
@@ -508,18 +518,15 @@ export function useModerationQueueManager(
try { try {
// Handle photo submissions // Handle photo submissions
if (action === 'approved' && item.submission_type === 'photo') { if (action === "approved" && item.submission_type === "photo") {
const { data: photoSubmission } = await supabase const { data: photoSubmission } = await supabase
.from('photo_submissions') .from("photo_submissions")
.select(`*, items:photo_submission_items(*), submission:content_submissions!inner(user_id)`) .select(`*, items:photo_submission_items(*), submission:content_submissions!inner(user_id)`)
.eq('submission_id', item.id) .eq("submission_id", item.id)
.single(); .single();
if (photoSubmission && photoSubmission.items) { if (photoSubmission && photoSubmission.items) {
const { data: existingPhotos } = await supabase const { data: existingPhotos } = await supabase.from("photos").select("id").eq("submission_id", item.id);
.from('photos')
.select('id')
.eq('submission_id', item.id);
if (!existingPhotos || existingPhotos.length === 0) { if (!existingPhotos || existingPhotos.length === 0) {
const photoRecords = photoSubmission.items.map((photoItem: any) => ({ const photoRecords = photoSubmission.items.map((photoItem: any) => ({
@@ -537,25 +544,25 @@ export function useModerationQueueManager(
approved_at: new Date().toISOString(), approved_at: new Date().toISOString(),
})); }));
await supabase.from('photos').insert(photoRecords); await supabase.from("photos").insert(photoRecords);
} }
} }
} }
// Check for submission items // Check for submission items
const { data: submissionItems } = await supabase const { data: submissionItems } = await supabase
.from('submission_items') .from("submission_items")
.select('id, status') .select("id, status")
.eq('submission_id', item.id) .eq("submission_id", item.id)
.in('status', ['pending', 'rejected']); .in("status", ["pending", "rejected"]);
if (submissionItems && submissionItems.length > 0) { if (submissionItems && submissionItems.length > 0) {
if (action === 'approved') { if (action === "approved") {
await supabase.functions.invoke('process-selective-approval', { await supabase.functions.invoke("process-selective-approval", {
body: { body: {
itemIds: submissionItems.map(i => i.id), itemIds: submissionItems.map((i) => i.id),
submissionId: item.id submissionId: item.id,
} },
}); });
toast({ toast({
@@ -563,24 +570,24 @@ export function useModerationQueueManager(
description: `Successfully processed ${submissionItems.length} item(s)`, description: `Successfully processed ${submissionItems.length} item(s)`,
}); });
return; return;
} else if (action === 'rejected') { } else if (action === "rejected") {
await supabase await supabase
.from('submission_items') .from("submission_items")
.update({ .update({
status: 'rejected', status: "rejected",
rejection_reason: moderatorNotes || 'Parent submission rejected', rejection_reason: moderatorNotes || "Parent submission rejected",
updated_at: new Date().toISOString() updated_at: new Date().toISOString(),
}) })
.eq('submission_id', item.id) .eq("submission_id", item.id)
.eq('status', 'pending'); .eq("status", "pending");
} }
} }
// Standard update // Standard update
const table = item.type === 'review' ? 'reviews' : 'content_submissions'; const table = item.type === "review" ? "reviews" : "content_submissions";
const statusField = item.type === 'review' ? 'moderation_status' : 'status'; const statusField = item.type === "review" ? "moderation_status" : "status";
const timestampField = item.type === 'review' ? 'moderated_at' : 'reviewed_at'; const timestampField = item.type === "review" ? "moderated_at" : "reviewed_at";
const reviewerField = item.type === 'review' ? 'moderated_by' : 'reviewer_id'; const reviewerField = item.type === "review" ? "moderated_by" : "reviewer_id";
const updateData: any = { const updateData: any = {
[statusField]: action, [statusField]: action,
@@ -595,10 +602,7 @@ export function useModerationQueueManager(
updateData.reviewer_notes = moderatorNotes; updateData.reviewer_notes = moderatorNotes;
} }
const { error } = await supabase const { error } = await supabase.from(table).update(updateData).eq("id", item.id);
.from(table)
.update(updateData)
.eq('id', item.id);
if (error) throw error; if (error) throw error;
@@ -606,15 +610,14 @@ export function useModerationQueueManager(
title: `Content ${action}`, title: `Content ${action}`,
description: `The ${item.type} has been ${action}`, description: `The ${item.type} has been ${action}`,
}); });
} catch (error: any) { } catch (error: any) {
console.error('Error moderating content:', error); console.error("Error moderating content:", error);
// Revert optimistic update // Revert optimistic update
setItems(prev => { setItems((prev) => {
const exists = prev.find(i => i.id === item.id); const exists = prev.find((i) => i.id === item.id);
if (exists) { if (exists) {
return prev.map(i => i.id === item.id ? item : i); return prev.map((i) => (i.id === item.id ? item : i));
} else { } else {
return [...prev, item]; return [...prev, item];
} }
@@ -628,23 +631,23 @@ export function useModerationQueueManager(
} finally { } finally {
setActionLoading(null); setActionLoading(null);
} }
}, [actionLoading, filters.statusFilter, queue, user, toast]); },
[actionLoading, filters.statusFilter, queue, user, toast],
);
/** /**
* Delete a submission permanently * Delete a submission permanently
*/ */
const deleteSubmission = useCallback(async (item: ModerationItem) => { const deleteSubmission = useCallback(
if (item.type !== 'content_submission') return; async (item: ModerationItem) => {
if (item.type !== "content_submission") return;
if (actionLoading === item.id) return; if (actionLoading === item.id) return;
setActionLoading(item.id); setActionLoading(item.id);
setItems(prev => prev.filter(i => i.id !== item.id)); setItems((prev) => prev.filter((i) => i.id !== item.id));
try { try {
const { error } = await supabase const { error } = await supabase.from("content_submissions").delete().eq("id", item.id);
.from('content_submissions')
.delete()
.eq('id', item.id);
if (error) throw error; if (error) throw error;
@@ -652,12 +655,11 @@ export function useModerationQueueManager(
title: "Submission deleted", title: "Submission deleted",
description: "The submission has been permanently deleted", description: "The submission has been permanently deleted",
}); });
} catch (error: any) { } catch (error: any) {
console.error('Error deleting submission:', error); console.error("Error deleting submission:", error);
setItems(prev => { setItems((prev) => {
if (prev.some(i => i.id === item.id)) return prev; if (prev.some((i) => i.id === item.id)) return prev;
return [...prev, item]; return [...prev, item];
}); });
@@ -669,16 +671,19 @@ export function useModerationQueueManager(
} finally { } finally {
setActionLoading(null); setActionLoading(null);
} }
}, [actionLoading, toast]); },
[actionLoading, toast],
);
/** /**
* Reset submission to pending status * Reset submission to pending status
*/ */
const resetToPending = useCallback(async (item: ModerationItem) => { const resetToPending = useCallback(
async (item: ModerationItem) => {
setActionLoading(item.id); setActionLoading(item.id);
try { try {
const { resetRejectedItemsToPending } = await import('@/lib/submissionItemsService'); const { resetRejectedItemsToPending } = await import("@/lib/submissionItemsService");
await resetRejectedItemsToPending(item.id); await resetRejectedItemsToPending(item.id);
toast({ toast({
@@ -686,9 +691,9 @@ export function useModerationQueueManager(
description: "Submission and all items have been reset to pending status", description: "Submission and all items have been reset to pending status",
}); });
setItems(prev => prev.filter(i => i.id !== item.id)); setItems((prev) => prev.filter((i) => i.id !== item.id));
} catch (error: any) { } catch (error: any) {
console.error('Error resetting submission:', error); console.error("Error resetting submission:", error);
toast({ toast({
title: "Reset Failed", title: "Reset Failed",
description: error.message, description: error.message,
@@ -697,23 +702,25 @@ export function useModerationQueueManager(
} finally { } finally {
setActionLoading(null); setActionLoading(null);
} }
}, [toast]); },
[toast],
);
/** /**
* Retry failed items in a submission * Retry failed items in a submission
*/ */
const retryFailedItems = useCallback(async (item: ModerationItem) => { const retryFailedItems = useCallback(
async (item: ModerationItem) => {
setActionLoading(item.id); setActionLoading(item.id);
const shouldRemove = ( const shouldRemove =
filters.statusFilter === 'pending' || filters.statusFilter === "pending" ||
filters.statusFilter === 'flagged' || filters.statusFilter === "flagged" ||
filters.statusFilter === 'partially_approved' filters.statusFilter === "partially_approved";
);
if (shouldRemove) { if (shouldRemove) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
setItems(prev => prev.filter(i => i.id !== item.id)); setItems((prev) => prev.filter((i) => i.id !== item.id));
recentlyRemovedRef.current.add(item.id); recentlyRemovedRef.current.add(item.id);
setTimeout(() => recentlyRemovedRef.current.delete(item.id), 10000); setTimeout(() => recentlyRemovedRef.current.delete(item.id), 10000);
}); });
@@ -721,10 +728,10 @@ export function useModerationQueueManager(
try { try {
const { data: failedItems } = await supabase const { data: failedItems } = await supabase
.from('submission_items') .from("submission_items")
.select('id') .select("id")
.eq('submission_id', item.id) .eq("submission_id", item.id)
.eq('status', 'rejected'); .eq("status", "rejected");
if (!failedItems || failedItems.length === 0) { if (!failedItems || failedItems.length === 0) {
toast({ toast({
@@ -734,20 +741,19 @@ export function useModerationQueueManager(
return; return;
} }
await supabase.functions.invoke('process-selective-approval', { await supabase.functions.invoke("process-selective-approval", {
body: { body: {
itemIds: failedItems.map(i => i.id), itemIds: failedItems.map((i) => i.id),
submissionId: item.id submissionId: item.id,
} },
}); });
toast({ toast({
title: "Retry Complete", title: "Retry Complete",
description: `Processed ${failedItems.length} failed item(s)`, description: `Processed ${failedItems.length} failed item(s)`,
}); });
} catch (error: any) { } catch (error: any) {
console.error('Error retrying failed items:', error); console.error("Error retrying failed items:", error);
toast({ toast({
title: "Retry Failed", title: "Retry Failed",
description: error.message, description: error.message,
@@ -756,7 +762,9 @@ export function useModerationQueueManager(
} finally { } finally {
setActionLoading(null); setActionLoading(null);
} }
}, [filters.statusFilter, toast]); },
[filters.statusFilter, toast],
);
// Initial fetch on mount // Initial fetch on mount
useEffect(() => { useEffect(() => {
@@ -788,19 +796,19 @@ export function useModerationQueueManager(
// Polling effect (when realtime disabled) // Polling effect (when realtime disabled)
useEffect(() => { useEffect(() => {
if (!user || settings.refreshMode !== 'auto' || loadingState === 'initial' || settings.useRealtimeQueue) { if (!user || settings.refreshMode !== "auto" || loadingState === "initial" || settings.useRealtimeQueue) {
return; return;
} }
console.log('⚠️ Polling ENABLED - interval:', settings.pollInterval); console.log("⚠️ Polling ENABLED - interval:", settings.pollInterval);
const interval = setInterval(() => { const interval = setInterval(() => {
console.log('🔄 Polling refresh triggered'); console.log("🔄 Polling refresh triggered");
fetchItemsRef.current?.(true); fetchItemsRef.current?.(true);
}, settings.pollInterval); }, settings.pollInterval);
return () => { return () => {
clearInterval(interval); clearInterval(interval);
console.log('🛑 Polling stopped'); console.log("🛑 Polling stopped");
}; };
}, [user, settings.refreshMode, settings.pollInterval, loadingState, settings.useRealtimeQueue]); }, [user, settings.refreshMode, settings.pollInterval, loadingState, settings.useRealtimeQueue]);
@@ -809,60 +817,60 @@ export function useModerationQueueManager(
// HARD CHECK: Explicit boolean comparison to prevent any truthy coercion // HARD CHECK: Explicit boolean comparison to prevent any truthy coercion
const isEnabled = settings.refreshOnTabVisible === true; const isEnabled = settings.refreshOnTabVisible === true;
console.log('🔍 [VISIBILITY EFFECT] Hard check', { console.log("🔍 [VISIBILITY EFFECT] Hard check", {
refreshOnTabVisible: settings.refreshOnTabVisible, refreshOnTabVisible: settings.refreshOnTabVisible,
typeOf: typeof settings.refreshOnTabVisible, typeOf: typeof settings.refreshOnTabVisible,
isEnabled, isEnabled,
willAttachListener: isEnabled, willAttachListener: isEnabled,
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
// Early return if feature is disabled // Early return if feature is disabled
if (!isEnabled) { if (!isEnabled) {
console.log(' ✅ Feature DISABLED - skipping all visibility logic'); console.log(" ✅ Feature DISABLED - skipping all visibility logic");
console.log(' ✅ Tab focus will NOT trigger refreshes'); console.log(" ✅ Tab focus will NOT trigger refreshes");
// Cleanup: ensure no lingering handlers // Cleanup: ensure no lingering handlers
return () => { return () => {
console.log(' 🧹 Cleanup: Ensuring no visibility listeners exist'); console.log(" 🧹 Cleanup: Ensuring no visibility listeners exist");
}; };
} }
console.error(' ❌ Setting is TRUE - listener WILL be attached'); console.error(" ❌ Setting is TRUE - listener WILL be attached");
console.error(' ❌ THIS MEANS TAB FOCUS **WILL** TRIGGER REFRESHES'); console.error(" ❌ THIS MEANS TAB FOCUS **WILL** TRIGGER REFRESHES");
console.error(' ⚠️ If you disabled this setting, it is NOT working properly'); console.error(" ⚠️ If you disabled this setting, it is NOT working properly");
const handleVisibilityChange = () => { const handleVisibilityChange = () => {
// Double-check setting before doing anything (defensive check) // Double-check setting before doing anything (defensive check)
if (!settings.refreshOnTabVisible) { if (!settings.refreshOnTabVisible) {
console.log('⚠️ Visibility handler called but setting is disabled - ignoring'); console.log("⚠️ Visibility handler called but setting is disabled - ignoring");
return; return;
} }
if (document.hidden) { if (document.hidden) {
console.log('👁️ [VISIBILITY HANDLER] Tab hidden - pausing fetches'); console.log("👁️ [VISIBILITY HANDLER] Tab hidden - pausing fetches");
pauseFetchingRef.current = true; pauseFetchingRef.current = true;
} else { } else {
console.error('👁️ [VISIBILITY HANDLER] Tab visible - THIS IS WHERE THE REFRESH HAPPENS'); console.error("👁️ [VISIBILITY HANDLER] Tab visible - THIS IS WHERE THE REFRESH HAPPENS");
console.error(' 🔴 TAB FOCUS REFRESH TRIGGERED HERE'); console.error(" 🔴 TAB FOCUS REFRESH TRIGGERED HERE");
console.error(' 📍 Stack trace below:'); console.error(" 📍 Stack trace below:");
console.trace(); console.trace();
pauseFetchingRef.current = false; pauseFetchingRef.current = false;
if (initialFetchCompleteRef.current && !isMountingRef.current && fetchItemsRef.current) { if (initialFetchCompleteRef.current && !isMountingRef.current && fetchItemsRef.current) {
console.error(' ➡️ Calling fetchItems(true) NOW'); console.error(" ➡️ Calling fetchItems(true) NOW");
fetchItemsRef.current(true); fetchItemsRef.current(true);
} else { } else {
console.log(' ⏭️ Skipping refresh (initial fetch not complete or mounting)'); console.log(" ⏭️ Skipping refresh (initial fetch not complete or mounting)");
} }
} }
}; };
document.addEventListener('visibilitychange', handleVisibilityChange); document.addEventListener("visibilitychange", handleVisibilityChange);
return () => { return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange); document.removeEventListener("visibilitychange", handleVisibilityChange);
console.log('🧹 Visibility listener removed'); console.log("🧹 Visibility listener removed");
}; };
}, [settings.refreshOnTabVisible]); }, [settings.refreshOnTabVisible]);
@@ -876,14 +884,14 @@ export function useModerationQueueManager(
onNewItem: (item: ModerationItem) => { onNewItem: (item: ModerationItem) => {
if (recentlyRemovedRef.current.has(item.id)) return; if (recentlyRemovedRef.current.has(item.id)) return;
setPendingNewItems(prev => { setPendingNewItems((prev) => {
if (prev.some(p => p.id === item.id)) return prev; if (prev.some((p) => p.id === item.id)) return prev;
return [...prev, item]; return [...prev, item];
}); });
setNewItemsCount(prev => prev + 1); setNewItemsCount((prev) => prev + 1);
toast({ toast({
title: '🆕 New Submission', title: "🆕 New Submission",
description: `${item.submission_type} - ${item.entity_name}`, description: `${item.submission_type} - ${item.entity_name}`,
}); });
}, },
@@ -892,12 +900,12 @@ export function useModerationQueueManager(
if (interactingWith.has(item.id)) return; if (interactingWith.has(item.id)) return;
if (shouldRemove) { if (shouldRemove) {
setItems(prev => prev.filter(i => i.id !== item.id)); setItems((prev) => prev.filter((i) => i.id !== item.id));
} else { } else {
setItems(prev => { setItems((prev) => {
const exists = prev.some(i => i.id === item.id); const exists = prev.some((i) => i.id === item.id);
if (exists) { if (exists) {
return prev.map(i => i.id === item.id ? item : i); return prev.map((i) => (i.id === item.id ? item : i));
} else { } else {
return [item, ...prev]; return [item, ...prev];
} }
@@ -905,7 +913,7 @@ export function useModerationQueueManager(
} }
}, },
onItemRemoved: (itemId: string) => { onItemRemoved: (itemId: string) => {
setItems(prev => prev.filter(i => i.id !== itemId)); setItems((prev) => prev.filter((i) => i.id !== itemId));
}, },
entityCache, entityCache,
profileCache, profileCache,
@@ -927,7 +935,9 @@ export function useModerationQueueManager(
showNewItems, showNewItems,
interactingWith, interactingWith,
markInteracting, markInteracting,
refresh: async () => { await fetchItems(false); }, refresh: async () => {
await fetchItems(false);
},
performAction, performAction,
deleteSubmission, deleteSubmission,
resetToPending, resetToPending,