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