import { useState, useEffect, useCallback, useRef } from 'react'; import { supabase } from '@/integrations/supabase/client'; import { useAuth } from './useAuth'; import { useToast } from './use-toast'; interface QueuedSubmission { submission_id: string; submission_type: string; waiting_time: string; // PostgreSQL interval format } interface LockState { submissionId: string; expiresAt: Date; autoReleaseTimer?: NodeJS.Timeout; } interface QueueStats { pendingCount: number; assignedToMe: number; avgWaitHours: number; } export const useModerationQueue = () => { const [currentLock, setCurrentLock] = useState(null); const [queueStats, setQueueStats] = useState(null); const [isLoading, setIsLoading] = useState(false); const lockTimerRef = useRef(null); const { user } = useAuth(); const { toast } = useToast(); // Auto-release expired locks periodically useEffect(() => { if (!user) return; // Call release_expired_locks every 2 minutes const releaseInterval = setInterval(async () => { try { await supabase.rpc('release_expired_locks'); } catch (error) { console.error('Error auto-releasing expired locks:', error); } }, 120000); // 2 minutes return () => clearInterval(releaseInterval); }, [user]); // Fetch queue statistics const fetchStats = useCallback(async () => { if (!user) return; try { const { data: slaData } = await supabase .from('moderation_sla_metrics') .select('*'); const { count: assignedCount } = await supabase .from('content_submissions') .select('id', { count: 'exact', head: true }) .eq('assigned_to', user.id) .gt('locked_until', new Date().toISOString()); if (slaData) { const totals = slaData.reduce( (acc, row) => ({ pendingCount: acc.pendingCount + (row.pending_count || 0), avgWaitHours: acc.avgWaitHours + (row.avg_wait_hours || 0), }), { pendingCount: 0, avgWaitHours: 0 } ); setQueueStats({ pendingCount: totals.pendingCount, assignedToMe: assignedCount || 0, avgWaitHours: slaData.length > 0 ? totals.avgWaitHours / slaData.length : 0, }); } } catch (error) { console.error('Error fetching queue stats:', error); } }, [user]); // Fetch stats on mount only (realtime updates handled by useModerationStats) useEffect(() => { fetchStats(); }, [fetchStats]); // Start countdown timer for lock expiry const startLockTimer = useCallback((expiresAt: Date) => { if (lockTimerRef.current) { clearInterval(lockTimerRef.current); } lockTimerRef.current = setInterval(() => { const now = new Date(); const timeLeft = expiresAt.getTime() - now.getTime(); if (timeLeft <= 0) { // Lock expired setCurrentLock(null); if (lockTimerRef.current) { clearInterval(lockTimerRef.current); } toast({ title: 'Lock Expired', description: 'Your submission lock has expired', variant: 'destructive', }); } }, 1000); // Check every second }, [toast]); // Clean up timer on unmount useEffect(() => { return () => { if (lockTimerRef.current) { clearInterval(lockTimerRef.current); } }; }, []); // Claim next submission from queue const claimNext = useCallback(async (): Promise => { if (!user?.id) { toast({ title: 'Authentication Required', description: 'You must be logged in to claim submissions', variant: 'destructive', }); return null; } setIsLoading(true); try { const { data, error } = await supabase.rpc('claim_next_submission', { moderator_id: user.id, lock_duration: '15 minutes', }); if (error) throw error; if (!data || data.length === 0) { toast({ title: 'Queue Empty', description: 'No submissions available to review', }); return null; } const claimed = data[0] as QueuedSubmission; const expiresAt = new Date(Date.now() + 15 * 60 * 1000); setCurrentLock({ submissionId: claimed.submission_id, expiresAt, }); startLockTimer(expiresAt); fetchStats(); toast({ title: 'Submission Claimed', description: `${claimed.submission_type} submission (waiting ${formatInterval(claimed.waiting_time)})`, }); return claimed.submission_id; } catch (error: any) { console.error('Error claiming submission:', error); toast({ title: 'Error', description: error.message || 'Failed to claim submission', variant: 'destructive', }); return null; } finally { setIsLoading(false); } }, [user, toast, startLockTimer, fetchStats]); // Extend current lock const extendLock = useCallback(async (submissionId: string): Promise => { if (!user?.id) return false; setIsLoading(true); try { const { data, error } = await supabase.rpc('extend_submission_lock', { submission_id: submissionId, moderator_id: user.id, extension_duration: '15 minutes', }); if (error) throw error; if (data) { const newExpiresAt = new Date(data); setCurrentLock((prev) => prev?.submissionId === submissionId ? { ...prev, expiresAt: newExpiresAt } : prev ); startLockTimer(newExpiresAt); toast({ title: 'Lock Extended', description: 'Lock extended for 15 more minutes', }); return true; } return false; } catch (error: any) { console.error('Error extending lock:', error); toast({ title: 'Error', description: error.message || 'Failed to extend lock', variant: 'destructive', }); return false; } finally { setIsLoading(false); } }, [user, toast, startLockTimer]); // Release lock (manual or on completion) const releaseLock = useCallback(async (submissionId: string): Promise => { if (!user?.id) return false; try { const { data, error } = await supabase.rpc('release_submission_lock', { submission_id: submissionId, moderator_id: user.id, }); if (error) throw error; if (data) { setCurrentLock((prev) => prev?.submissionId === submissionId ? null : prev ); if (lockTimerRef.current) { clearInterval(lockTimerRef.current); } fetchStats(); return true; } return false; } catch (error: any) { console.error('Error releasing lock:', error); return false; } }, [user, fetchStats]); // Get time remaining on current lock const getTimeRemaining = useCallback((): number | null => { if (!currentLock) return null; return Math.max(0, currentLock.expiresAt.getTime() - Date.now()); }, [currentLock]); // Escalate submission const escalateSubmission = useCallback(async (submissionId: string, reason: string): Promise => { if (!user?.id) return false; setIsLoading(true); try { const { error } = await supabase .from('content_submissions') .update({ escalated: true, escalated_at: new Date().toISOString(), escalated_by: user.id, escalation_reason: reason, }) .eq('id', submissionId); if (error) throw error; toast({ title: 'Submission Escalated', description: 'This submission has been marked as high priority', }); fetchStats(); return true; } catch (error: any) { console.error('Error escalating submission:', error); toast({ title: 'Error', description: error.message || 'Failed to escalate submission', variant: 'destructive', }); return false; } finally { setIsLoading(false); } }, [user, toast, fetchStats]); // Claim a specific submission (CRM-style claim any) const claimSubmission = useCallback(async (submissionId: string): Promise => { if (!user?.id) { toast({ title: 'Authentication Required', description: 'You must be logged in to claim submissions', variant: 'destructive', }); return false; } setIsLoading(true); try { const expiresAt = new Date(Date.now() + 15 * 60 * 1000); const { error } = await supabase .from('content_submissions') .update({ assigned_to: user.id, assigned_at: new Date().toISOString(), locked_until: expiresAt.toISOString(), }) .eq('id', submissionId) .or(`assigned_to.is.null,locked_until.lt.${new Date().toISOString()}`); // Only if unclaimed or lock expired if (error) throw error; setCurrentLock({ submissionId, expiresAt, }); startLockTimer(expiresAt); fetchStats(); toast({ title: 'Submission Claimed', description: 'You now have 15 minutes to review this submission', }); return true; } catch (error: any) { console.error('Error claiming submission:', error); toast({ title: 'Error', description: error.message || 'Failed to claim submission', variant: 'destructive', }); return false; } finally { setIsLoading(false); } }, [user, toast, startLockTimer, fetchStats]); // Reassign submission const reassignSubmission = useCallback(async (submissionId: string, newModeratorId: string): Promise => { if (!user?.id) return false; setIsLoading(true); try { const { error } = await supabase .from('content_submissions') .update({ assigned_to: newModeratorId, assigned_at: new Date().toISOString(), locked_until: new Date(Date.now() + 15 * 60 * 1000).toISOString(), }) .eq('id', submissionId); if (error) throw error; // If this was our lock, clear it if (currentLock?.submissionId === submissionId) { setCurrentLock(null); if (lockTimerRef.current) { clearInterval(lockTimerRef.current); } } toast({ title: 'Submission Reassigned', description: 'The submission has been assigned to another moderator', }); fetchStats(); return true; } catch (error: any) { console.error('Error reassigning submission:', error); toast({ title: 'Error', description: error.message || 'Failed to reassign submission', variant: 'destructive', }); return false; } finally { setIsLoading(false); } }, [user, currentLock, toast, fetchStats]); // Check if submission is locked by current user const isLockedByMe = useCallback((submissionId: string): boolean => { return currentLock?.submissionId === submissionId; }, [currentLock]); // Check if submission is locked by another moderator const isLockedByOther = useCallback((submissionId: string, assignedTo: string | null, lockedUntil: string | null): boolean => { if (!assignedTo || !lockedUntil) return false; if (user?.id === assignedTo) return false; // It's our lock return new Date(lockedUntil) > new Date(); // Lock is still active }, [user]); // Get lock progress percentage (0-100) const getLockProgress = useCallback((): number => { const timeLeft = getTimeRemaining(); if (timeLeft === null) return 0; const totalTime = 15 * 60 * 1000; // 15 minutes in ms return Math.max(0, Math.min(100, (timeLeft / totalTime) * 100)); }, [getTimeRemaining]); // Auto-release lock after moderation action const releaseAfterAction = useCallback(async (submissionId: string, action: 'approved' | 'rejected'): Promise => { if (currentLock?.submissionId === submissionId) { await releaseLock(submissionId); console.log(`🔓 Auto-released lock after ${action} action`); } }, [currentLock, releaseLock]); return { currentLock, queueStats, isLoading, claimNext, claimSubmission, extendLock, releaseLock, getTimeRemaining, escalateSubmission, reassignSubmission, refreshStats: fetchStats, // New helpers isLockedByMe, isLockedByOther, getLockProgress, releaseAfterAction, }; }; // Helper to format PostgreSQL interval function formatInterval(interval: string): string { const match = interval.match(/(\d+):(\d+):(\d+)/); if (!match) return interval; const hours = parseInt(match[1]); const minutes = parseInt(match[2]); if (hours > 24) { const days = Math.floor(hours / 24); return `${days}d ${hours % 24}h`; } else if (hours > 0) { return `${hours}h ${minutes}m`; } else { return `${minutes}m`; } }