Implement realtime queue fix

This commit is contained in:
gpt-engineer-app[bot]
2025-10-09 13:54:39 +00:00
parent 57368eb309
commit 1d45294703
5 changed files with 200 additions and 11 deletions

View File

@@ -52,6 +52,9 @@ interface ModerationItem {
display_name?: string;
avatar_url?: string;
};
escalated?: boolean;
assigned_to?: string;
locked_until?: string;
}
type EntityFilter = 'all' | 'reviews' | 'submissions' | 'photos';
@@ -109,12 +112,14 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
getAdminPanelRefreshMode,
getAdminPanelPollInterval,
getAutoRefreshStrategy,
getPreserveInteractionState
getPreserveInteractionState,
getUseRealtimeQueue
} = useAdminSettings();
const refreshMode = getAdminPanelRefreshMode();
const pollInterval = getAdminPanelPollInterval();
const refreshStrategy = getAutoRefreshStrategy();
const preserveInteraction = getPreserveInteractionState();
const useRealtimeQueue = getUseRealtimeQueue();
// Store admin settings in refs to avoid triggering fetchItems recreation
const refreshStrategyRef = useRef(refreshStrategy);
@@ -552,9 +557,9 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedEntityFilter, debouncedStatusFilter, user]);
// Polling for auto-refresh
// Polling for auto-refresh (only if realtime is disabled)
useEffect(() => {
if (!user || refreshMode !== 'auto' || isInitialLoad) return;
if (!user || refreshMode !== 'auto' || isInitialLoad || useRealtimeQueue) return;
const interval = setInterval(() => {
fetchItems(filtersRef.current.entityFilter, filtersRef.current.statusFilter, true); // Silent refresh
@@ -564,7 +569,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
clearInterval(interval);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user, refreshMode, pollInterval, isInitialLoad]);
}, [user, refreshMode, pollInterval, isInitialLoad, useRealtimeQueue]);
// Real-time subscription for lock status
useEffect(() => {
@@ -611,6 +616,146 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
};
}, [user]);
// Real-time subscription for NEW submissions (replaces polling)
useEffect(() => {
if (!user || !useRealtimeQueue) return;
const channel = supabase
.channel('moderation-new-submissions')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'content_submissions',
},
async (payload) => {
const newSubmission = payload.new as any;
// Only process pending/partially_approved submissions
if (!['pending', 'partially_approved'].includes(newSubmission.status)) {
return;
}
// Apply entity filter
const matchesEntityFilter =
filtersRef.current.entityFilter === 'all' ||
(filtersRef.current.entityFilter === 'photos' && newSubmission.submission_type === 'photo') ||
(filtersRef.current.entityFilter === 'submissions' && newSubmission.submission_type !== 'photo');
// Apply status filter
const matchesStatusFilter =
filtersRef.current.statusFilter === 'all' ||
(filtersRef.current.statusFilter === 'pending' && ['pending', 'partially_approved'].includes(newSubmission.status)) ||
filtersRef.current.statusFilter === newSubmission.status;
if (matchesEntityFilter && matchesStatusFilter) {
console.log('🆕 NEW submission detected:', newSubmission.id);
// Fetch full submission details
try {
const { data: submission, error } = await supabase
.from('content_submissions')
.select(`
id, submission_type, status, content, created_at, user_id,
reviewed_at, reviewer_id, reviewer_notes, escalated, assigned_to, locked_until
`)
.eq('id', newSubmission.id)
.single();
if (error || !submission) {
console.error('Error fetching submission details:', error);
return;
}
// Fetch user profile
const { data: profile } = await supabase
.from('profiles')
.select('user_id, username, display_name, avatar_url')
.eq('user_id', submission.user_id)
.maybeSingle();
// Resolve entity name
const content = submission.content as any;
let entityName = content?.name || 'Unknown';
let parkName: string | undefined;
if (submission.submission_type === 'ride' && content?.entity_id) {
const { data: ride } = await supabase
.from('rides')
.select('name, park_id')
.eq('id', content.entity_id)
.maybeSingle();
if (ride) {
entityName = ride.name;
if (ride.park_id) {
const { data: park } = await supabase
.from('parks')
.select('name')
.eq('id', ride.park_id)
.maybeSingle();
if (park) parkName = park.name;
}
}
} else if (submission.submission_type === 'park' && content?.entity_id) {
const { data: park } = await supabase
.from('parks')
.select('name')
.eq('id', content.entity_id)
.maybeSingle();
if (park) entityName = park.name;
} else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(submission.submission_type) && content?.entity_id) {
const { data: company } = await supabase
.from('companies')
.select('name')
.eq('id', content.entity_id)
.maybeSingle();
if (company) entityName = company.name;
}
const fullItem: ModerationItem = {
id: submission.id,
type: 'content_submission',
content: submission.content,
created_at: submission.created_at,
user_id: submission.user_id,
status: submission.status,
submission_type: submission.submission_type,
user_profile: profile || undefined,
entity_name: entityName,
park_name: parkName,
reviewed_at: submission.reviewed_at || undefined,
reviewer_notes: submission.reviewer_notes || undefined,
escalated: submission.escalated,
assigned_to: submission.assigned_to || undefined,
locked_until: submission.locked_until || undefined,
};
// Add to pending items
setPendingNewItems(prev => {
if (prev.some(p => p.id === fullItem.id)) return prev;
return [...prev, fullItem];
});
setNewItemsCount(prev => prev + 1);
// Toast notification
toast({
title: '🆕 New Submission',
description: `${fullItem.submission_type} - ${fullItem.entity_name}`,
});
} catch (error) {
console.error('Error processing new submission:', error);
}
}
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [user, useRealtimeQueue, toast]);
const handleResetToPending = async (item: ModerationItem) => {
setActionLoading(item.id);
try {