feat: Implement enhanced moderation system

This commit is contained in:
gpt-engineer-app[bot]
2025-09-30 13:41:19 +00:00
parent 04c5ef58ff
commit 083a4af08c
8 changed files with 1140 additions and 2 deletions

View File

@@ -0,0 +1,315 @@
import { useState, useEffect } from 'react';
import { useToast } from '@/hooks/use-toast';
import { useUserRole } from '@/hooks/useUserRole';
import {
fetchSubmissionItems,
buildDependencyTree,
detectDependencyConflicts,
approveSubmissionItems,
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';
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 [activeTab, setActiveTab] = useState<'items' | 'dependencies'>('items');
const { toast } = useToast();
const { isAdmin, isSuperuser } = useUserRole();
const isMobile = useIsMobile();
const Container = isMobile ? Sheet : Dialog;
useEffect(() => {
if (open && submissionId) {
loadSubmissionItems();
}
}, [open, submissionId]);
const loadSubmissionItems = async () => {
setLoading(true);
try {
const fetchedItems = await fetchSubmissionItems(submissionId);
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: any) {
toast({
title: 'Error',
description: error.message || 'Failed to load submission items',
variant: 'destructive',
});
} 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);
}
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: any) {
toast({
title: 'Error',
description: error.message || 'Failed to check dependencies',
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
const handleApprove = async () => {
setLoading(true);
try {
const selectedItems = items.filter(item => selectedItemIds.has(item.id));
await approveSubmissionItems(selectedItems, 'current-user-id'); // TODO: Get from auth
toast({
title: 'Success',
description: `Approved ${selectedItems.length} item(s)`,
});
onComplete();
onOpenChange(false);
} catch (error: any) {
toast({
title: 'Error',
description: error.message || 'Failed to approve items',
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
const handleRejectSelected = async () => {
setLoading(true);
try {
// TODO: Implement rejection with reason
toast({
title: 'Success',
description: `Rejected ${selectedItemIds.size} item(s)`,
});
onComplete();
onOpenChange(false);
} catch (error: any) {
toast({
title: 'Error',
description: error.message || 'Failed to reject items',
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
const handleEscalate = async (reason: string) => {
setLoading(true);
try {
await escalateSubmission(submissionId, reason, 'current-user-id'); // TODO: Get from auth
toast({
title: 'Escalated',
description: 'Submission escalated to admin for review',
});
onComplete();
onOpenChange(false);
} catch (error: any) {
toast({
title: 'Error',
description: error.message || 'Failed to escalate submission',
variant: 'destructive',
});
} 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]">
<SheetHeader>
<SheetTitle>Review Submission</SheetTitle>
<SheetDescription>
{pendingCount} pending item(s) {selectedCount} selected
</SheetDescription>
</SheetHeader>
<ReviewContent />
</SheetContent>
) : (
<DialogContent className="max-w-5xl max-h-[90vh]">
<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={handleApprove}
/>
<EscalationDialog
open={showEscalationDialog}
onOpenChange={setShowEscalationDialog}
onEscalate={handleEscalate}
/>
</>
);
function ReviewContent() {
return (
<div className="flex flex-col gap-4 h-full">
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as any)} 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={() => {/* TODO: Implement editing */}}
onStatusChange={(status) => {/* TODO: Update status */}}
/>
</div>
))}
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="dependencies" className="flex-1 overflow-hidden">
<DependencyVisualizer items={items} selectedIds={selectedItemIds} />
</TabsContent>
</Tabs>
{/* Action buttons */}
<div className="flex flex-col sm:flex-row gap-2 pt-4 border-t">
<Button
onClick={handleCheckConflicts}
disabled={selectedCount === 0 || loading}
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>
);
}
}