diff --git a/src/components/moderation/ActiveFiltersDisplay.tsx b/src/components/moderation/ActiveFiltersDisplay.tsx index e4800191..174a9b03 100644 --- a/src/components/moderation/ActiveFiltersDisplay.tsx +++ b/src/components/moderation/ActiveFiltersDisplay.tsx @@ -22,6 +22,7 @@ const getEntityFilterIcon = (filter: EntityFilter) => { const getSortFieldLabel = (field: SortField): string => { switch (field) { + case 'username': return 'Submitter'; case 'submission_type': return 'Type'; case 'escalated': return 'Escalated'; case 'status': return 'Status'; diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index 2340a517..bb308199 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -1,6 +1,5 @@ import { useState, useImperativeHandle, forwardRef, useMemo } from 'react'; import { Card, CardContent } from '@/components/ui/card'; -import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useToast } from '@/hooks/use-toast'; import { useUserRole } from '@/hooks/useUserRole'; import { useAuth } from '@/hooks/useAuth'; @@ -20,8 +19,7 @@ import { AutoRefreshIndicator } from './AutoRefreshIndicator'; import { NewItemsAlert } from './NewItemsAlert'; import { EmptyQueueState } from './EmptyQueueState'; import { QueuePagination } from './QueuePagination'; -import type { ModerationQueueRef, QueueTab } from '@/types/moderation'; -import { AlertTriangle, Clock, Archive } from 'lucide-react'; +import type { ModerationQueueRef } from '@/types/moderation'; export const ModerationQueue = forwardRef((props, ref) => { const isMobile = useIsMobile(); @@ -89,28 +87,6 @@ export const ModerationQueue = forwardRef((props, ref) => { return (
- {/* Queue Tabs */} - queueManager.filters.setActiveTab(value as QueueTab)} - className="w-full" - > - - - - Main Queue - - - - Escalated - - - - Archive - - - - {/* Queue Statistics & Lock Status */} {queueManager.queue.queueStats && ( diff --git a/src/components/moderation/QueueSortControls.tsx b/src/components/moderation/QueueSortControls.tsx index bcfa5de0..3e2dd352 100644 --- a/src/components/moderation/QueueSortControls.tsx +++ b/src/components/moderation/QueueSortControls.tsx @@ -15,6 +15,7 @@ interface QueueSortControlsProps { const getSortFieldLabel = (field: SortField): string => { switch (field) { case 'created_at': return 'Date Created'; + case 'username': return 'Submitter'; case 'submission_type': return 'Type'; case 'status': return 'Status'; case 'escalated': return 'Escalated'; @@ -57,6 +58,7 @@ export const QueueSortControls = ({ {getSortFieldLabel('created_at')} + {getSortFieldLabel('username')} {getSortFieldLabel('submission_type')} {getSortFieldLabel('status')} {getSortFieldLabel('escalated')} diff --git a/src/hooks/moderation/useModerationQueueManager.ts b/src/hooks/moderation/useModerationQueueManager.ts index 303402bf..3ea47967 100644 --- a/src/hooks/moderation/useModerationQueueManager.ts +++ b/src/hooks/moderation/useModerationQueueManager.ts @@ -128,8 +128,6 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig): const fetchItemsRef = useRef<((silent?: boolean) => Promise) | null>(null); const FETCH_COOLDOWN_MS = 1000; - const EFFECT_DEBOUNCE_MS = 50; // Short debounce to let all effects settle - const effectFetchTimerRef = useRef(null); // Store settings in refs to avoid re-creating fetchItems const settingsRef = useRef(settings); @@ -223,26 +221,9 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig): status ) `, - ); - - // Validate and log sort configuration - console.log('[Query] Sort config received:', { - field: sort.config.field, - direction: sort.config.direction, - ascending: sort.config.direction === 'asc' - }); - - const validSortFields = ['created_at', 'submission_type', 'status', 'escalated']; - let sortField = sort.config.field; - - if (!validSortFields.includes(sortField)) { - console.warn('[Query] Invalid sort field:', sortField, '- falling back to created_at'); - sortField = 'created_at'; - } - - // Apply sorting by user's chosen field only - submissionsQuery = submissionsQuery - .order(sortField, { ascending: sort.config.direction === 'asc' }); + ) + .order("escalated", { ascending: false }) + .order("created_at", { ascending: true }); // Apply tab-based status filtering const tab = filters.activeTab; @@ -250,20 +231,6 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig): const entityFilter = filters.debouncedEntityFilter; if (tab === "mainQueue") { - // Main queue: non-escalated pending items - submissionsQuery = submissionsQuery.eq("escalated", false); - - 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); - } - } else if (tab === "escalated") { - // Escalated queue: only escalated items - submissionsQuery = submissionsQuery.eq("escalated", true); - if (statusFilter === "all") { submissionsQuery = submissionsQuery.in("status", ["pending", "flagged", "partially_approved"]); } else if (statusFilter === "pending") { @@ -272,7 +239,6 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig): submissionsQuery = submissionsQuery.eq("status", statusFilter); } } else { - // Archive: completed items (non-escalated and escalated) if (statusFilter === "all") { submissionsQuery = submissionsQuery.in("status", ["approved", "rejected"]); } else { @@ -295,56 +261,12 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig): ); } - // Get total count - rebuild query with same filters - let countQuery = supabase + // Get total count + const { count } = await supabase .from("content_submissions") - .select("*", { count: "exact", head: true }); + .select("*", { count: "exact", head: true }) + .match(submissionsQuery as any); - // Apply the exact same filters as the main query - if (tab === "mainQueue") { - countQuery = countQuery.eq("escalated", false); - - if (statusFilter === "all") { - countQuery = countQuery.in("status", ["pending", "flagged", "partially_approved"]); - } else if (statusFilter === "pending") { - countQuery = countQuery.in("status", ["pending", "partially_approved"]); - } else { - countQuery = countQuery.eq("status", statusFilter); - } - } else if (tab === "escalated") { - countQuery = countQuery.eq("escalated", true); - - if (statusFilter === "all") { - countQuery = countQuery.in("status", ["pending", "flagged", "partially_approved"]); - } else if (statusFilter === "pending") { - countQuery = countQuery.in("status", ["pending", "partially_approved"]); - } else { - countQuery = countQuery.eq("status", statusFilter); - } - } else { - if (statusFilter === "all") { - countQuery = countQuery.in("status", ["approved", "rejected"]); - } else { - countQuery = countQuery.eq("status", statusFilter); - } - } - - // Apply entity type filter - if (entityFilter === "photos") { - countQuery = countQuery.eq("submission_type", "photo"); - } else if (entityFilter === "submissions") { - countQuery = countQuery.neq("submission_type", "photo"); - } - - // Apply access control - if (!isAdmin && !isSuperuser) { - const now = new Date().toISOString(); - countQuery = countQuery.or( - `assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${user.id}`, - ); - } - - const { count } = await countQuery; pagination.setTotalCount(count || 0); // Apply pagination @@ -530,7 +452,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig): setLoadingState("ready"); } }, - [user, isAdmin, isSuperuser, filters, pagination, sort, profileCache, entityCache, toast], + [user, isAdmin, isSuperuser, filters, pagination, profileCache, entityCache, toast], ); // Store fetchItems in ref to avoid re-creating visibility listener @@ -858,56 +780,20 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig): // eslint-disable-next-line react-hooks/exhaustive-deps }, [user?.id]); - // Debounced fetch for effects - prevents race conditions - const debouncedEffectFetch = useCallback(() => { - if (effectFetchTimerRef.current) { - clearTimeout(effectFetchTimerRef.current); - } - - effectFetchTimerRef.current = setTimeout(() => { - console.log('[Debounced Fetch] Executing after effects settled'); - fetchItemsRef.current?.(true); - }, EFFECT_DEBOUNCE_MS); - }, []); - - // Filter and tab changes trigger refetch + // Filter changes trigger refetch useEffect(() => { if (!user || !initialFetchCompleteRef.current || isMountingRef.current) return; - console.log('[Filter/Tab Change] Queuing debounced fetch'); pagination.reset(); - debouncedEffectFetch(); - }, [filters.activeTab, filters.debouncedEntityFilter, filters.debouncedStatusFilter, user, debouncedEffectFetch, pagination]); - - // Sort changes trigger refetch - useEffect(() => { - if (!user || !initialFetchCompleteRef.current || isMountingRef.current) { - return; - } - - console.log('[Sort Change] Queuing debounced fetch', { - field: sort.config.field, - direction: sort.config.direction - }); - pagination.reset(); - debouncedEffectFetch(); - }, [sort.config.field, sort.config.direction, user, pagination, debouncedEffectFetch]); + fetchItems(true); + }, [filters.debouncedEntityFilter, filters.debouncedStatusFilter]); // Pagination changes trigger refetch useEffect(() => { if (!user || !initialFetchCompleteRef.current || pagination.currentPage === 1) return; - debouncedEffectFetch(); - }, [pagination.currentPage, pagination.pageSize, debouncedEffectFetch]); - - // Cleanup effect timer on unmount - useEffect(() => { - return () => { - if (effectFetchTimerRef.current) { - clearTimeout(effectFetchTimerRef.current); - } - }; - }, []); + fetchItemsRef.current?.(true); + }, [pagination.currentPage, pagination.pageSize]); // Polling effect (when realtime disabled) useEffect(() => { diff --git a/src/lib/moderation/queries.ts b/src/lib/moderation/queries.ts index 264b689d..737b8723 100644 --- a/src/lib/moderation/queries.ts +++ b/src/lib/moderation/queries.ts @@ -73,7 +73,9 @@ export function buildSubmissionQuery( item_data, status ) - `); + `) + .order('escalated', { ascending: false }) + .order('created_at', { ascending: true }); // Apply tab-based status filtering if (tab === 'mainQueue') { diff --git a/src/lib/moderation/sorting.ts b/src/lib/moderation/sorting.ts index 61e9cba7..31a15512 100644 --- a/src/lib/moderation/sorting.ts +++ b/src/lib/moderation/sorting.ts @@ -27,6 +27,12 @@ export function sortModerationItems( comparison = new Date(a.created_at).getTime() - new Date(b.created_at).getTime(); break; + case 'username': + const usernameA = a.user_profile?.username || a.user_profile?.display_name || ''; + const usernameB = b.user_profile?.username || b.user_profile?.display_name || ''; + comparison = usernameA.localeCompare(usernameB); + break; + case 'submission_type': comparison = (a.submission_type || '').localeCompare(b.submission_type || ''); break; @@ -71,16 +77,7 @@ export function loadSortConfig(key: string = 'moderationQueue_sortConfig'): Sort try { const saved = localStorage.getItem(key); if (saved) { - const config = JSON.parse(saved); - - // Migrate old 'username' sort to 'created_at' - if (config.field === 'username') { - console.warn('[Sort] Migrating deprecated username sort to created_at'); - config.field = 'created_at'; - saveSortConfig(config, key); // Save the migrated config - } - - return config; + return JSON.parse(saved); } } catch (error) { console.error('Failed to load sort config:', error); @@ -126,6 +123,8 @@ export function getSortFieldLabel(field: SortField): string { switch (field) { case 'created_at': return 'Date Created'; + case 'username': + return 'Submitter'; case 'submission_type': return 'Type'; case 'status': diff --git a/src/types/moderation.ts b/src/types/moderation.ts index 17edbf32..68a059bf 100644 --- a/src/types/moderation.ts +++ b/src/types/moderation.ts @@ -97,12 +97,12 @@ export type StatusFilter = 'all' | 'pending' | 'partially_approved' | 'flagged' /** * Available tabs in the moderation interface */ -export type QueueTab = 'mainQueue' | 'escalated' | 'archive'; +export type QueueTab = 'mainQueue' | 'archive'; /** * Fields that can be used for sorting the moderation queue */ -export type SortField = 'created_at' | 'submission_type' | 'status' | 'escalated'; +export type SortField = 'created_at' | 'username' | 'submission_type' | 'status' | 'escalated'; /** * Direction for sorting (ascending or descending)