Spaces:
Runtime error
Runtime error
feat: Implement admin moderation history, audit logging for detection actions, and a new admin navigation and opportunity hub.
5480d48 | "use client"; | |
| import { useEffect, useState } from "react"; | |
| import { getAnalysisHistory } from "../actions/analysis"; | |
| import { formatDistanceToNow } from "date-fns"; | |
| import { Card, CardContent } from "@/components/ui/card"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { User, Clock, CheckCircle2, XCircle, Settings, HelpCircle, Loader2 } from "lucide-react"; | |
| import { cn } from "@/lib/utils"; | |
| interface LogEntry { | |
| id: string; | |
| action: string; | |
| createdAt: string | Date; | |
| admin: { name: string | null; email: string }; | |
| detection: { objectName: string }; | |
| correctedValues: any; | |
| } | |
| export function AnalysisHistory({ videoInternalId }: { videoInternalId: string }) { | |
| const [logs, setLogs] = useState<LogEntry[]>([]); | |
| const [isLoading, setIsLoading] = useState(true); | |
| useEffect(() => { | |
| const fetch = async () => { | |
| try { | |
| const results = await getAnalysisHistory(videoInternalId); | |
| setLogs(results as any); | |
| } catch (err) { | |
| console.error("Failed to fetch history", err); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| fetch(); | |
| }, [videoInternalId]); | |
| if (isLoading) { | |
| return ( | |
| <div className="flex flex-col items-center justify-center p-12 text-center"> | |
| <Loader2 className="h-8 w-8 animate-spin text-muted-foreground mb-4" /> | |
| <p className="text-muted-foreground">Loading audit log...</p> | |
| </div> | |
| ); | |
| } | |
| if (logs.length === 0) { | |
| return ( | |
| <div className="flex flex-col items-center justify-center p-12 text-center border-2 border-dashed rounded-xl"> | |
| <Clock className="h-12 w-12 text-muted-foreground mb-4 opacity-20" /> | |
| <h3 className="font-semibold text-lg">No history yet</h3> | |
| <p className="text-muted-foreground max-w-xs mx-auto"> | |
| Actions taken on this video's objects will appear here. | |
| </p> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="space-y-4"> | |
| <div className="flex items-center justify-between px-1"> | |
| <h3 className="text-sm font-medium text-muted-foreground">Audit Trail ({logs.length})</h3> | |
| </div> | |
| <div className="relative space-y-0 before:absolute before:inset-0 before:ml-5 before:h-full before:w-0.5 before:-translate-x-px before:bg-gradient-to-b before:from-transparent before:via-border/50 before:to-transparent"> | |
| {logs.map((log) => { | |
| const isApprove = log.action === 'approved'; | |
| const isReject = log.action === 'rejected'; | |
| const isCorrect = log.action === 'corrected'; | |
| const isIncorrect = log.action === 'marked_incorrect'; | |
| return ( | |
| <div key={log.id} className="relative flex items-start gap-4 pb-8 last:pb-0"> | |
| <div className={cn( | |
| "flex h-10 w-10 shrink-0 items-center justify-center rounded-full border bg-background shadow-sm ring-4 ring-background z-10", | |
| isApprove ? "border-emerald-500/50 text-emerald-500" : | |
| isReject ? "border-rose-500/50 text-rose-500" : | |
| "border-amber-500/50 text-amber-500" | |
| )}> | |
| {isApprove && <CheckCircle2 className="h-5 w-5" />} | |
| {isReject && <XCircle className="h-5 w-5" />} | |
| {isCorrect && <Settings className="h-5 w-5" />} | |
| {isIncorrect && <HelpCircle className="h-5 w-5" />} | |
| </div> | |
| <Card className="flex-1 bg-card/40 backdrop-blur-md border-border/40"> | |
| <CardContent className="p-4"> | |
| <div className="flex items-start justify-between"> | |
| <div className="space-y-1"> | |
| <p className="text-sm"> | |
| <span className="font-semibold text-foreground">{log.admin.name || log.admin.email}</span> | |
| <span className="text-muted-foreground"> {log.action.replace('_', ' ')} </span> | |
| <span className="font-medium text-foreground">{log.detection.objectName}</span> | |
| </p> | |
| <div className="flex items-center gap-2 text-xs text-muted-foreground"> | |
| <Clock className="h-3 w-3" /> | |
| {formatDistanceToNow(new Date(log.createdAt), { addSuffix: true })} | |
| </div> | |
| </div> | |
| </div> | |
| {log.correctedValues?.notes && ( | |
| <div className="mt-3 p-2 rounded bg-muted/30 text-xs text-muted-foreground italic border-l-2 border-border"> | |
| "{log.correctedValues.notes}" | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| ); | |
| } | |