mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 09:31:13 -05:00
Code edited in Lovable Code Editor
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user