mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 12:31:13 -05:00
695 lines
22 KiB
TypeScript
695 lines
22 KiB
TypeScript
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<SubmissionItemWithDeps[]>([]);
|
|
const [selectedItemIds, setSelectedItemIds] = useState<Set<string>>(new Set());
|
|
const [conflicts, setConflicts] = useState<DependencyConflict[]>([]);
|
|
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<SubmissionItemWithDeps | null>(null);
|
|
const [activeTab, setActiveTab] = useState<'items' | 'dependencies'>('items');
|
|
const [submissionType, setSubmissionType] = useState<string>('submission');
|
|
const [showValidationBlockerDialog, setShowValidationBlockerDialog] = useState(false);
|
|
const [showWarningConfirmDialog, setShowWarningConfirmDialog] = useState(false);
|
|
const [validationResults, setValidationResults] = useState<Map<string, ValidationResult>>(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 (
|
|
<>
|
|
<Container open={open} onOpenChange={onOpenChange}>
|
|
{isMobile ? (
|
|
<SheetContent side="bottom" className="h-[90vh] overflow-y-auto">
|
|
<SheetHeader>
|
|
<SheetTitle>Review Submission</SheetTitle>
|
|
<SheetDescription>
|
|
{pendingCount} pending item(s) • {selectedCount} selected
|
|
</SheetDescription>
|
|
</SheetHeader>
|
|
<ReviewContent />
|
|
</SheetContent>
|
|
) : (
|
|
<DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>Review Submission</DialogTitle>
|
|
<DialogDescription>
|
|
{pendingCount} pending item(s) • {selectedCount} selected
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<ReviewContent />
|
|
</DialogContent>
|
|
)}
|
|
</Container>
|
|
|
|
<ConflictResolutionDialog
|
|
open={showConflictDialog}
|
|
onOpenChange={setShowConflictDialog}
|
|
conflicts={conflicts}
|
|
items={items}
|
|
onResolve={async () => {
|
|
await loadSubmissionItems();
|
|
await handleApprove();
|
|
}}
|
|
/>
|
|
|
|
<EscalationDialog
|
|
open={showEscalationDialog}
|
|
onOpenChange={setShowEscalationDialog}
|
|
onEscalate={handleEscalate}
|
|
submissionType={submissionType}
|
|
/>
|
|
|
|
<RejectionDialog
|
|
open={showRejectionDialog}
|
|
onOpenChange={setShowRejectionDialog}
|
|
itemCount={selectedItemIds.size}
|
|
hasDependents={items.filter(item => selectedItemIds.has(item.id)).some(item =>
|
|
item.dependents && item.dependents.length > 0
|
|
)}
|
|
onReject={handleReject}
|
|
/>
|
|
|
|
<ItemEditDialog
|
|
item={editingItem}
|
|
open={showEditDialog}
|
|
onOpenChange={setShowEditDialog}
|
|
onComplete={handleEditComplete}
|
|
/>
|
|
|
|
<ValidationBlockerDialog
|
|
open={showValidationBlockerDialog}
|
|
onClose={() => 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('_', ' ')
|
|
)}
|
|
/>
|
|
|
|
<WarningConfirmDialog
|
|
open={showWarningConfirmDialog}
|
|
onClose={() => 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 (
|
|
<Alert variant="destructive" className="my-4">
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription>
|
|
This submission has no items and appears to be corrupted or incomplete.
|
|
This usually happens when the submission creation process was interrupted.
|
|
<div className="mt-2 flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={async () => {
|
|
try {
|
|
const { supabase } = await import('@/integrations/supabase/client');
|
|
await supabase
|
|
.from('content_submissions')
|
|
.delete()
|
|
.eq('id', submissionId);
|
|
|
|
toast({
|
|
title: 'Submission Archived',
|
|
description: 'The corrupted submission has been removed',
|
|
});
|
|
onComplete();
|
|
onOpenChange(false);
|
|
} catch (error: unknown) {
|
|
handleError(error, {
|
|
action: 'Archive Corrupted Submission',
|
|
userId: user?.id,
|
|
metadata: { submissionId }
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
Archive Submission
|
|
</Button>
|
|
</div>
|
|
</AlertDescription>
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4 h-full">
|
|
<Tabs
|
|
value={activeTab}
|
|
onValueChange={(v) => {
|
|
if (v === 'items' || v === 'dependencies') {
|
|
setActiveTab(v);
|
|
}
|
|
}}
|
|
className="flex-1 flex flex-col"
|
|
>
|
|
<TabsList className="grid w-full grid-cols-2">
|
|
<TabsTrigger value="items">
|
|
<CheckCircle2 className="w-4 h-4 mr-2" />
|
|
Items ({items.length})
|
|
</TabsTrigger>
|
|
<TabsTrigger value="dependencies">
|
|
<Network className="w-4 h-4 mr-2" />
|
|
Dependencies
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="items" className="flex-1 overflow-hidden">
|
|
<ScrollArea className="h-full pr-4">
|
|
<div className="space-y-4">
|
|
{items.length === 0 && !loading && (
|
|
<Alert>
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription>
|
|
No items found in this submission
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{items.map((item) => (
|
|
<div key={item.id} className="flex gap-3 items-start">
|
|
<Checkbox
|
|
checked={selectedItemIds.has(item.id)}
|
|
onCheckedChange={() => toggleItemSelection(item.id)}
|
|
disabled={item.status !== 'pending'}
|
|
/>
|
|
<ItemReviewCard
|
|
item={item}
|
|
onEdit={() => handleEdit(item)}
|
|
onStatusChange={async () => {
|
|
// Status changes handled via approve/reject actions
|
|
await loadSubmissionItems();
|
|
}}
|
|
submissionId={submissionId}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="dependencies" className="flex-1 overflow-hidden">
|
|
<DependencyVisualizer items={items} selectedIds={selectedItemIds} />
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
{/* Blocking error alert */}
|
|
{hasBlockingErrors && (
|
|
<Alert variant="destructive">
|
|
<AlertCircle className="w-4 h-4" />
|
|
<AlertDescription>
|
|
Cannot approve: Selected items have validation errors that must be fixed first.
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Action buttons */}
|
|
<div className="flex flex-col sm:flex-row gap-2 pt-4 border-t">
|
|
<Button
|
|
onClick={handleCheckConflicts}
|
|
disabled={selectedCount === 0 || loading || hasBlockingErrors}
|
|
className="flex-1"
|
|
>
|
|
<CheckCircle2 className="w-4 h-4 mr-2" />
|
|
Approve Selected ({selectedCount})
|
|
</Button>
|
|
|
|
<Button
|
|
onClick={handleRejectSelected}
|
|
disabled={selectedCount === 0 || loading}
|
|
variant="destructive"
|
|
className="flex-1"
|
|
>
|
|
<XCircle className="w-4 h-4 mr-2" />
|
|
Reject Selected
|
|
</Button>
|
|
|
|
<Button
|
|
onClick={() => setShowEscalationDialog(true)}
|
|
variant="outline"
|
|
disabled={loading}
|
|
>
|
|
<ArrowUp className="w-4 h-4 mr-2" />
|
|
Escalate
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}
|