mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 21:31:12 -05:00
feat: Implement enhanced moderation system
This commit is contained in:
315
src/components/moderation/SubmissionReviewManager.tsx
Normal file
315
src/components/moderation/SubmissionReviewManager.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user