diff --git a/src/components/moderation/ConflictResolutionDialog.tsx b/src/components/moderation/ConflictResolutionDialog.tsx new file mode 100644 index 00000000..2a8177f5 --- /dev/null +++ b/src/components/moderation/ConflictResolutionDialog.tsx @@ -0,0 +1,96 @@ +import { useState } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Label } from '@/components/ui/label'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { AlertCircle } from 'lucide-react'; +import { type DependencyConflict, type SubmissionItemWithDeps } from '@/lib/submissionItemsService'; + +interface ConflictResolutionDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + conflicts: DependencyConflict[]; + items: SubmissionItemWithDeps[]; + onResolve: () => void; +} + +export function ConflictResolutionDialog({ + open, + onOpenChange, + conflicts, + items, + onResolve, +}: ConflictResolutionDialogProps) { + const [resolutions, setResolutions] = useState>({}); + + const handleResolutionChange = (itemId: string, action: string) => { + setResolutions(prev => ({ ...prev, [itemId]: action })); + }; + + const allConflictsResolved = conflicts.every( + conflict => resolutions[conflict.itemId] + ); + + const handleApply = () => { + // TODO: Apply resolutions + onResolve(); + onOpenChange(false); + }; + + return ( + + + + Resolve Dependency Conflicts + + {conflicts.length} conflict(s) found. Choose how to resolve each one. + + + +
+ {conflicts.map((conflict) => { + const item = items.find(i => i.id === conflict.itemId); + + return ( +
+ + + +

+ {item?.item_type.replace('_', ' ').toUpperCase()}: {item?.item_data.name} +

+

{conflict.message}

+
+
+ + handleResolutionChange(conflict.itemId, value)} + > + {conflict.suggestions.map((suggestion, idx) => ( +
+ + +
+ ))} +
+
+ ); + })} +
+ + + + + +
+
+ ); +} diff --git a/src/components/moderation/DependencyVisualizer.tsx b/src/components/moderation/DependencyVisualizer.tsx new file mode 100644 index 00000000..513f39d4 --- /dev/null +++ b/src/components/moderation/DependencyVisualizer.tsx @@ -0,0 +1,118 @@ +import { useMemo } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { ArrowDown, AlertCircle } from 'lucide-react'; +import { type SubmissionItemWithDeps } from '@/lib/submissionItemsService'; +import { Alert, AlertDescription } from '@/components/ui/alert'; + +interface DependencyVisualizerProps { + items: SubmissionItemWithDeps[]; + selectedIds: Set; +} + +export function DependencyVisualizer({ items, selectedIds }: DependencyVisualizerProps) { + const dependencyLevels = useMemo(() => { + const levels: SubmissionItemWithDeps[][] = []; + const visited = new Set(); + + const getRootItems = () => items.filter(item => !item.depends_on); + + const addLevel = (currentItems: SubmissionItemWithDeps[]) => { + if (currentItems.length === 0) return; + + const nextLevel: SubmissionItemWithDeps[] = []; + currentItems.forEach(item => { + if (!visited.has(item.id)) { + visited.add(item.id); + if (item.dependents) { + nextLevel.push(...item.dependents); + } + } + }); + + levels.push(currentItems); + addLevel(nextLevel); + }; + + addLevel(getRootItems()); + return levels; + }, [items]); + + const hasCircularDependency = items.length > 0 && dependencyLevels.flat().length !== items.length; + + return ( +
+ {hasCircularDependency && ( + + + + Circular dependency detected! This submission needs admin review. + + + )} + + {dependencyLevels.length === 0 && ( + + + No dependencies found in this submission + + + )} + + {dependencyLevels.map((level, levelIdx) => ( +
+
+

+ Level {levelIdx + 1} +

+
+
+ +
+ {level.map((item) => ( + + +
+ + {item.item_type.replace('_', ' ').toUpperCase()} + + + {item.status} + +
+
+ +

+ {item.item_data.name || 'Unnamed'} +

+ {item.dependents && item.dependents.length > 0 && ( +

+ Has {item.dependents.length} dependent(s) +

+ )} +
+
+ ))} +
+ + {levelIdx < dependencyLevels.length - 1 && ( +
+ +
+ )} +
+ ))} +
+ ); +} diff --git a/src/components/moderation/EscalationDialog.tsx b/src/components/moderation/EscalationDialog.tsx new file mode 100644 index 00000000..b6d84135 --- /dev/null +++ b/src/components/moderation/EscalationDialog.tsx @@ -0,0 +1,93 @@ +import { useState } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Textarea } from '@/components/ui/textarea'; +import { Label } from '@/components/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; + +interface EscalationDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onEscalate: (reason: string) => void; +} + +const escalationReasons = [ + 'Complex dependency issue', + 'Potential policy violation', + 'Unclear submission content', + 'Requires admin judgment', + 'Technical issue', + 'Other', +]; + +export function EscalationDialog({ + open, + onOpenChange, + onEscalate, +}: EscalationDialogProps) { + const [selectedReason, setSelectedReason] = useState(''); + const [additionalNotes, setAdditionalNotes] = useState(''); + + const handleEscalate = () => { + const reason = selectedReason === 'Other' + ? additionalNotes + : `${selectedReason}${additionalNotes ? ': ' + additionalNotes : ''}`; + + onEscalate(reason); + onOpenChange(false); + + // Reset form + setSelectedReason(''); + setAdditionalNotes(''); + }; + + return ( + + + + Escalate to Admin + + This submission will be flagged for admin review. Please provide a reason. + + + +
+
+ + +
+ +
+ +