vault-video-processor / src /features /admin /components /analysis-history.tsx
dvijaykrishnan's picture
feat: Implement admin moderation history, audit logging for detection actions, and a new admin navigation and opportunity hub.
5480d48
Raw
History Blame Contribute Delete
5.7 kB
"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>
);
}