diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index 3fdaec8e..be1b084a 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -776,6 +776,22 @@ export const ModerationQueue = forwardRef((props, ref) => { const handleRetryFailedItems = async (item: ModerationItem) => { setActionLoading(item.id); + + // Optimistic UI update - remove from queue immediately + const shouldRemove = ( + activeStatusFilter === 'pending' || + activeStatusFilter === 'flagged' || + activeStatusFilter === 'partially_approved' + ); + + if (shouldRemove) { + requestAnimationFrame(() => { + setItems(prev => prev.filter(i => i.id !== item.id)); + recentlyRemovedRef.current.add(item.id); + setTimeout(() => recentlyRemovedRef.current.delete(item.id), 3000); + }); + } + try { // Fetch failed/rejected submission items const { data: failedItems, error: fetchError } = await supabase @@ -812,7 +828,6 @@ export const ModerationQueue = forwardRef((props, ref) => { description: `Processed ${failedItems.length} failed item(s)`, }); - // No refresh needed - item already updated optimistically } catch (error: any) { console.error('Error retrying failed items:', error); toast({ diff --git a/supabase/functions/process-selective-approval/index.ts b/supabase/functions/process-selective-approval/index.ts index 8c6b3e42..bf8c2736 100644 --- a/supabase/functions/process-selective-approval/index.ts +++ b/supabase/functions/process-selective-approval/index.ts @@ -197,6 +197,7 @@ serve(async (req) => { itemType: string; success: boolean; error?: string; + isDependencyFailure?: boolean; }> = []; // Process items in order @@ -285,23 +286,71 @@ serve(async (req) => { console.log(`Successfully approved item ${item.id} -> entity ${entityId}`); } catch (error) { console.error(`Error processing item ${item.id}:`, error); + + const isDependencyError = error instanceof Error && ( + error.message.includes('Missing dependency') || + error.message.includes('depends on') || + error.message.includes('Circular dependency') + ); + approvalResults.push({ itemId: item.id, itemType: item.item_type, success: false, - error: error instanceof Error ? error.message : 'Unknown error' + error: error instanceof Error ? error.message : 'Unknown error', + isDependencyFailure: isDependencyError }); + + // Mark item as rejected in submission_items + const { error: markRejectedError } = await supabase + .from('submission_items') + .update({ + status: 'rejected', + rejection_reason: error instanceof Error ? error.message : 'Unknown error', + updated_at: new Date().toISOString() + }) + .eq('id', item.id); + + if (markRejectedError) { + console.error(`Failed to mark item ${item.id} as rejected:`, markRejectedError); + } } } - // Update submission status + // Check if any failures were dependency-related + const hasDependencyFailure = approvalResults.some(r => + !r.success && r.isDependencyFailure + ); + const allApproved = approvalResults.every(r => r.success); + const someApproved = approvalResults.some(r => r.success); + const allFailed = approvalResults.every(r => !r.success); + + // Determine final status: + // - If dependency validation failed: keep pending for escalation + // - If all approved: approved + // - If some approved: partially_approved + // - If all failed but no dependency issues: rejected (can retry) + const finalStatus = hasDependencyFailure && !someApproved + ? 'pending' // Keep pending for escalation only + : allApproved + ? 'approved' + : allFailed + ? 'rejected' // Total failure, allow retry + : 'partially_approved'; // Mixed results + + const reviewerNotes = hasDependencyFailure && !someApproved + ? 'Submission has unresolved dependencies. Escalation required.' + : undefined; + const { error: updateError } = await supabase .from('content_submissions') .update({ - status: allApproved ? 'approved' : 'partially_approved', + status: finalStatus, reviewer_id: authenticatedUserId, - reviewed_at: new Date().toISOString() + reviewed_at: new Date().toISOString(), + reviewer_notes: reviewerNotes, + escalated: hasDependencyFailure && !someApproved ? true : undefined }) .eq('id', submissionId); @@ -313,7 +362,7 @@ serve(async (req) => { JSON.stringify({ success: true, results: approvalResults, - submissionStatus: allApproved ? 'approved' : 'partially_approved' + submissionStatus: finalStatus }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); @@ -335,7 +384,10 @@ function topologicalSort(items: any[]): any[] { const visit = (item: any) => { if (visited.has(item.id)) return; if (visiting.has(item.id)) { - throw new Error(`Circular dependency detected for item ${item.id}`); + throw new Error( + `Circular dependency detected: item ${item.id} (${item.item_type}) ` + + `creates a dependency loop. This submission requires escalation.` + ); } visiting.add(item.id); @@ -343,7 +395,11 @@ function topologicalSort(items: any[]): any[] { if (item.depends_on) { const parent = items.find(i => i.id === item.depends_on); if (!parent) { - throw new Error(`Missing dependency: item ${item.id} depends on ${item.depends_on} which is not in the submission`); + throw new Error( + `Missing dependency: item ${item.id} (${item.item_type}) ` + + `depends on ${item.depends_on} which is not in this submission or has not been approved. ` + + `This submission requires escalation.` + ); } visit(parent); }