import { useState, useEffect } from 'react'; import { useToast } from '@/hooks/use-toast'; import { useUserRole } from '@/hooks/useUserRole'; import { useAuth } from '@/hooks/useAuth'; import { handleError } from '@/lib/errorHandler'; import { fetchSubmissionItems, buildDependencyTree, detectDependencyConflicts, approveSubmissionItems, rejectSubmissionItems, escalateSubmission, type SubmissionItemWithDeps, type DependencyConflict } from '@/lib/submissionItemsService'; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '@/components/ui/sheet'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { AlertCircle, CheckCircle2, XCircle, Edit, Network, ArrowUp } from 'lucide-react'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { ScrollArea } from '@/components/ui/scroll-area'; import { useIsMobile } from '@/hooks/use-mobile'; import { ItemReviewCard } from './ItemReviewCard'; import { DependencyVisualizer } from './DependencyVisualizer'; import { ConflictResolutionDialog } from './ConflictResolutionDialog'; import { EscalationDialog } from './EscalationDialog'; import { RejectionDialog } from './RejectionDialog'; import { ItemEditDialog } from './ItemEditDialog'; import { ValidationBlockerDialog } from './ValidationBlockerDialog'; import { WarningConfirmDialog } from './WarningConfirmDialog'; import { validateMultipleItems, ValidationResult } from '@/lib/entityValidationSchemas'; interface SubmissionReviewManagerProps { submissionId: string; open: boolean; onOpenChange: (open: boolean) => void; onComplete: () => void; } export function SubmissionReviewManager({ submissionId, open, onOpenChange, onComplete }: SubmissionReviewManagerProps) { const [items, setItems] = useState([]); const [selectedItemIds, setSelectedItemIds] = useState>(new Set()); const [conflicts, setConflicts] = useState([]); const [loading, setLoading] = useState(false); const [showConflictDialog, setShowConflictDialog] = useState(false); const [showEscalationDialog, setShowEscalationDialog] = useState(false); const [showRejectionDialog, setShowRejectionDialog] = useState(false); const [showEditDialog, setShowEditDialog] = useState(false); const [editingItem, setEditingItem] = useState(null); const [activeTab, setActiveTab] = useState<'items' | 'dependencies'>('items'); const [submissionType, setSubmissionType] = useState('submission'); const [showValidationBlockerDialog, setShowValidationBlockerDialog] = useState(false); const [showWarningConfirmDialog, setShowWarningConfirmDialog] = useState(false); const [validationResults, setValidationResults] = useState>(new Map()); const [userConfirmedWarnings, setUserConfirmedWarnings] = useState(false); const [hasBlockingErrors, setHasBlockingErrors] = useState(false); const [globalValidationKey, setGlobalValidationKey] = useState(0); const { toast } = useToast(); const { isAdmin, isSuperuser } = useUserRole(); const { user } = useAuth(); const isMobile = useIsMobile(); const Container = isMobile ? Sheet : Dialog; useEffect(() => { if (open && submissionId) { loadSubmissionItems(); } }, [open, submissionId]); const loadSubmissionItems = async () => { setLoading(true); try { const { supabase } = await import('@/integrations/supabase/client'); // Fetch submission type const { data: submission } = await supabase .from('content_submissions') .select('submission_type') .eq('id', submissionId) .single(); if (submission) { setSubmissionType(submission.submission_type || 'submission'); } const fetchedItems = await fetchSubmissionItems(submissionId); // Protection 2: Detect empty submissions if (!fetchedItems || fetchedItems.length === 0) { setItems([]); return; } const itemsWithDeps = buildDependencyTree(fetchedItems); setItems(itemsWithDeps); // Auto-select pending items const pendingIds = fetchedItems .filter(item => item.status === 'pending') .map(item => item.id); setSelectedItemIds(new Set(pendingIds)); } catch (error: unknown) { handleError(error, { action: 'Load Submission Items', userId: user?.id, metadata: { submissionId, submissionType } }); } finally { setLoading(false); } }; const toggleItemSelection = (itemId: string) => { setSelectedItemIds(prev => { const next = new Set(prev); if (next.has(itemId)) { next.delete(itemId); } else { next.add(itemId); } // Clear blocking errors and warning confirmation when selection changes setHasBlockingErrors(false); setValidationResults(new Map()); setUserConfirmedWarnings(false); return next; }); }; const handleCheckConflicts = async () => { setLoading(true); try { const detectedConflicts = await detectDependencyConflicts(items, Array.from(selectedItemIds)); setConflicts(detectedConflicts); if (detectedConflicts.length > 0) { setShowConflictDialog(true); } else { // No conflicts, proceed with approval handleApprove(); } } catch (error: unknown) { handleError(error, { action: 'Check Dependency Conflicts', userId: user?.id, metadata: { submissionId, selectedCount: selectedItemIds.size } }); } finally { setLoading(false); } }; const handleApprove = async () => { if (!user?.id) { toast({ title: 'Authentication Required', description: 'You must be logged in to approve items', variant: 'destructive', }); return; } const selectedItems = items.filter(item => selectedItemIds.has(item.id)); setLoading(true); try { // Run validation on all selected items const validationResultsMap = await validateMultipleItems( selectedItems.map(item => ({ item_type: item.item_type, item_data: item.item_data, id: item.id })) ); setValidationResults(validationResultsMap); // Check for blocking errors const itemsWithBlockingErrors = selectedItems.filter(item => { const result = validationResultsMap.get(item.id); return result && result.blockingErrors.length > 0; }); // CRITICAL: Blocking errors can NEVER be bypassed, regardless of warnings if (itemsWithBlockingErrors.length > 0) { setHasBlockingErrors(true); setShowValidationBlockerDialog(true); setLoading(false); return; // Block approval } // Check for warnings const itemsWithWarnings = selectedItems.filter(item => { const result = validationResultsMap.get(item.id); return result && result.warnings.length > 0; }); if (itemsWithWarnings.length > 0 && !userConfirmedWarnings) { setShowWarningConfirmDialog(true); setLoading(false); return; // Ask for confirmation } // Proceed with approval const { supabase } = await import('@/integrations/supabase/client'); // Call the edge function for backend processing const { data, error } = await supabase.functions.invoke('process-selective-approval', { body: { itemIds: Array.from(selectedItemIds), submissionId } }); if (error) { throw new Error(error.message || 'Failed to process approval'); } if (!data?.success) { throw new Error(data?.error || 'Approval processing failed'); } const successCount = data.results.filter((r: any) => r.success).length; const failCount = data.results.filter((r: any) => !r.success).length; const allFailed = failCount > 0 && successCount === 0; const someFailed = failCount > 0 && successCount > 0; toast({ title: allFailed ? 'Approval Failed' : someFailed ? 'Partial Approval' : 'Approval Complete', description: failCount > 0 ? `Approved ${successCount} item(s), ${failCount} failed` : `Successfully approved ${successCount} item(s)`, variant: allFailed ? 'destructive' : someFailed ? 'default' : 'default', }); // Reset warning confirmation state after approval setUserConfirmedWarnings(false); // If ALL items failed, don't close dialog - show errors if (allFailed) { setLoading(false); return; } onComplete(); onOpenChange(false); } catch (error: unknown) { handleError(error, { action: 'Approve Submission Items', userId: user?.id, metadata: { submissionId, itemCount: selectedItemIds.size, hasWarnings: userConfirmedWarnings, hasBlockingErrors } }); } finally { setLoading(false); } }; const handleRejectSelected = async () => { if (selectedItemIds.size === 0) { toast({ title: 'No Items Selected', description: 'Please select items to reject', variant: 'destructive', }); return; } if (!user?.id) { toast({ title: 'Authentication Required', description: 'You must be logged in to reject items', variant: 'destructive', }); return; } // Check if any selected items have dependents const selectedItems = items.filter(item => selectedItemIds.has(item.id)); const hasDependents = selectedItems.some(item => item.dependents && item.dependents.length > 0 ); setShowRejectionDialog(true); }; const handleReject = async (reason: string, cascade: boolean) => { if (!user?.id) return; setLoading(true); try { const selectedItems = items.filter(item => selectedItemIds.has(item.id)); await rejectSubmissionItems(selectedItems, reason, user.id, cascade); toast({ title: 'Items Rejected', description: `Successfully rejected ${selectedItems.length} item${selectedItems.length !== 1 ? 's' : ''}`, }); onComplete(); onOpenChange(false); } catch (error: unknown) { handleError(error, { action: 'Reject Submission Items', userId: user?.id, metadata: { submissionId, itemCount: selectedItemIds.size, cascade, reason: reason.substring(0, 100) } }); } finally { setLoading(false); } }; const handleEscalate = async (reason: string) => { if (!user?.id) { toast({ title: 'Authentication Required', description: 'You must be logged in to escalate submissions', variant: 'destructive', }); return; } setLoading(true); try { const { supabase } = await import('@/integrations/supabase/client'); // Call the escalation notification edge function const { data, error } = await supabase.functions.invoke('send-escalation-notification', { body: { submissionId, escalationReason: reason, escalatedBy: user.id } }); if (error) { console.error('Edge function error:', error); // Fallback to direct database update if email fails await escalateSubmission(submissionId, reason, user.id); toast({ title: 'Escalated (Email Failed)', description: 'Submission escalated but notification email failed to send', variant: 'default', }); } else { toast({ title: 'Escalated Successfully', description: 'Submission escalated and admin notified via email', }); } onComplete(); onOpenChange(false); } catch (error: unknown) { handleError(error, { action: 'Escalate Submission', userId: user?.id, metadata: { submissionId, reason: reason.substring(0, 100) } }); } finally { setLoading(false); } }; const handleEdit = (item: SubmissionItemWithDeps) => { setEditingItem(item); setShowEditDialog(true); }; const handleEditComplete = async () => { setShowEditDialog(false); setEditingItem(null); await loadSubmissionItems(); setGlobalValidationKey(prev => prev + 1); }; const handleItemStatusChange = async (itemId: string, status: 'approved' | 'rejected') => { if (!user?.id) { toast({ title: 'Authentication Required', description: 'You must be logged in to change item status', variant: 'destructive', }); return; } setLoading(true); try { if (status === 'approved') { const { supabase } = await import('@/integrations/supabase/client'); const { data, error } = await supabase.functions.invoke('process-selective-approval', { body: { itemIds: [itemId], submissionId } }); if (error || !data?.success) { throw new Error(error?.message || data?.error || 'Failed to approve item'); } toast({ title: 'Item Approved', description: 'Successfully approved the item', }); } else { const item = items.find(i => i.id === itemId); if (!item) { throw new Error('Item not found'); } await rejectSubmissionItems([item], 'Quick rejection from review', user.id, false); toast({ title: 'Item Rejected', description: 'Successfully rejected the item', }); } await loadSubmissionItems(); } catch (error: unknown) { handleError(error, { action: `${status === 'approved' ? 'Approve' : 'Reject'} Item`, userId: user?.id, metadata: { submissionId, itemId, status } }); } finally { setLoading(false); } }; const pendingCount = items.filter(item => item.status === 'pending').length; const selectedCount = selectedItemIds.size; return ( <> {isMobile ? ( Review Submission {pendingCount} pending item(s) • {selectedCount} selected ) : ( Review Submission {pendingCount} pending item(s) • {selectedCount} selected )} { await loadSubmissionItems(); await handleApprove(); }} /> selectedItemIds.has(item.id)).some(item => item.dependents && item.dependents.length > 0 )} onReject={handleReject} /> setShowValidationBlockerDialog(false)} blockingErrors={Array.from(validationResults.values()).flatMap(r => r.blockingErrors)} itemNames={items.filter(i => selectedItemIds.has(i.id)).map(i => i.item_data?.name || i.item_type.replace('_', ' ') )} /> setShowWarningConfirmDialog(false)} onProceed={() => { setUserConfirmedWarnings(true); setShowWarningConfirmDialog(false); handleApprove(); }} warnings={Array.from(validationResults.values()).flatMap(r => r.warnings)} itemNames={items.filter(i => selectedItemIds.has(i.id)).map(i => i.item_data?.name || i.item_type.replace('_', ' ') )} /> ); function ReviewContent() { // Protection 2: UI detection of empty submissions if (items.length === 0 && !loading) { return ( This submission has no items and appears to be corrupted or incomplete. This usually happens when the submission creation process was interrupted.
); } return (
{ if (v === 'items' || v === 'dependencies') { setActiveTab(v); } }} className="flex-1 flex flex-col" > Items ({items.length}) Dependencies
{items.length === 0 && !loading && ( No items found in this submission )} {items.map((item) => (
toggleItemSelection(item.id)} disabled={item.status !== 'pending'} /> handleEdit(item)} onStatusChange={async () => { // Status changes handled via approve/reject actions await loadSubmissionItems(); }} submissionId={submissionId} />
))}
{/* Blocking error alert */} {hasBlockingErrors && ( Cannot approve: Selected items have validation errors that must be fixed first. )} {/* Action buttons */}
); } }