diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index 27805190..4715c40e 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -103,7 +103,6 @@ export const ModerationQueue = forwardRef((props, ref) => { const fetchInProgressRef = useRef(false); const itemsRef = useRef([]); const loadedIdsRef = useRef>(new Set()); - const lastFetchTimestampRef = useRef(null); // Get admin settings for polling configuration const { @@ -207,12 +206,8 @@ export const ModerationQueue = forwardRef((props, ref) => { submissionsQuery = submissionsQuery.neq('submission_type', 'photo'); } - // Silent polling: fetch submissions created OR updated since last poll - if (silent && lastFetchTimestampRef.current) { - submissionsQuery = submissionsQuery.or( - `submitted_at.gt.${lastFetchTimestampRef.current},updated_at.gt.${lastFetchTimestampRef.current}` - ); - } + // Always fetch ALL pending/partially_approved submissions + // Let ID-based tracking determine what's "new" instead of timestamp filtering // CRM-style claim filtering: moderators only see unclaimed OR self-assigned submissions // Admins see all submissions @@ -435,17 +430,10 @@ export const ModerationQueue = forwardRef((props, ref) => { const currentPreserveInteraction = preserveInteractionRef.current; if (silent) { - // Update timestamp using actual data timestamps, not current time - if (moderationItems.length > 0) { - const maxTimestamp = Math.max( - ...moderationItems.map(item => new Date(item.updated_at || item.created_at).getTime()) - ); - lastFetchTimestampRef.current = new Date(maxTimestamp).toISOString(); - } - - // Background polling: behavior controlled by admin settings - const currentLoadedIds = loadedIdsRef.current; - const newSubmissions = moderationItems.filter(item => !currentLoadedIds.has(item.id)); + // Background polling: detect new submissions by comparing IDs + // Use currently DISPLAYED items (itemsRef) not loadedIdsRef to avoid false positives + 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); @@ -458,6 +446,7 @@ export const ModerationQueue = forwardRef((props, ref) => { // Track these IDs as loaded to prevent re-counting on next poll if (uniqueNew.length > 0) { const newIds = uniqueNew.map(item => item.id); + const currentLoadedIds = loadedIdsRef.current; loadedIdsRef.current = new Set([...currentLoadedIds, ...newIds]); setNewItemsCount(prev => prev + uniqueNew.length); } @@ -508,11 +497,15 @@ export const ModerationQueue = forwardRef((props, ref) => { } else { // Normal fetch: Load all items and reset pending console.log('🔄 Manual refresh - replacing entire queue'); - lastFetchTimestampRef.current = null; // Reset timestamp to fetch everything next time setItems(moderationItems); setPendingNewItems([]); setNewItemsCount(0); - console.log('📋 Queue loaded with', moderationItems.length, 'submissions'); + + // Initialize loadedIdsRef on first load or manual refresh + if (loadedIdsRef.current.size === 0 || !silent) { + loadedIdsRef.current = new Set(moderationItems.map(item => item.id)); + console.log('📋 Queue loaded - tracking', loadedIdsRef.current.size, 'submissions'); + } } } catch (error: any) { diff --git a/supabase/migrations/20251009133219_ad66553a-d991-4684-bdc6-5a7548dd12a2.sql b/supabase/migrations/20251009133219_ad66553a-d991-4684-bdc6-5a7548dd12a2.sql new file mode 100644 index 00000000..b9d67d7b --- /dev/null +++ b/supabase/migrations/20251009133219_ad66553a-d991-4684-bdc6-5a7548dd12a2.sql @@ -0,0 +1,32 @@ +-- Fix the update trigger to only update updated_at when actual content changes +-- This prevents false "new submission" detection in the moderation queue + +-- Drop and recreate the trigger function to add proper change detection +DROP TRIGGER IF EXISTS update_content_submissions_updated_at ON public.content_submissions; +DROP FUNCTION IF EXISTS public.update_content_submissions_updated_at(); + +CREATE OR REPLACE FUNCTION public.update_content_submissions_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + -- Only update updated_at if actual content has changed + -- Ignore changes to: updated_at, assigned_to, assigned_at, locked_until, priority + IF ( + NEW.content IS DISTINCT FROM OLD.content OR + NEW.status IS DISTINCT FROM OLD.status OR + NEW.reviewer_id IS DISTINCT FROM OLD.reviewer_id OR + NEW.reviewer_notes IS DISTINCT FROM OLD.reviewer_notes OR + NEW.escalated IS DISTINCT FROM OLD.escalated OR + NEW.escalation_reason IS DISTINCT FROM OLD.escalation_reason OR + NEW.approval_mode IS DISTINCT FROM OLD.approval_mode + ) THEN + NEW.updated_at = NOW(); + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER update_content_submissions_updated_at + BEFORE UPDATE ON public.content_submissions + FOR EACH ROW + EXECUTE FUNCTION public.update_content_submissions_updated_at(); \ No newline at end of file