Files
pitchfast/components/audits/audits-board.tsx

200 lines
6.5 KiB
TypeScript

"use client";
import { useMemo } from "react";
import { useQuery } from "convex/react";
import { FunctionReturnType } from "convex/server";
import { Activity, Files, SquarePen } from "lucide-react";
import Link from "next/link";
import { api } from "@/convex/_generated/api";
import { Skeleton } from "@/components/ui/skeleton";
import { Badge } from "@/components/ui/badge";
type AuditDashboardRowsResult = FunctionReturnType<typeof api.audits.listDashboardRows>;
type AuditRow = Extract<NonNullable<AuditDashboardRowsResult>[number], { kind: "audit" }>;
type AuditDashboardRow = NonNullable<AuditDashboardRowsResult>[number];
const statusText: Record<string, string> = {
draft: "Entwurf",
approved: "Freigegeben",
published: "Veröffentlicht",
deactivated: "Deaktiviert",
};
const fallbackStatus = "Unbekannt";
const generationStageText: Record<string, string> = {
audit_generation: "Audit-Generierung",
classification: "Klassifikation",
multimodalAudit: "Multimodale Analyse",
germanCopy: "Deutsche Texte",
qualityReview: "Qualitätsprüfung",
};
function formatPageCount(pageCount: number) {
return `${pageCount} Seite${pageCount === 1 ? "" : "n"}`;
}
function getStatusLabel(status: AuditRow["status"]) {
return statusText[status] ?? fallbackStatus;
}
function getGenerationStatusLabel(
row: Extract<AuditDashboardRow, { kind: "generation" }>,
) {
if (row.status === "pending") {
return "Wartet auf Start";
}
if (row.status === "failed") {
return "Fehlgeschlagen";
}
if (row.status === "canceled") {
return "Abgebrochen";
}
if (row.status === "succeeded") {
return "Wartet auf finales Audit";
}
return "Generierung läuft";
}
function getStageLabel(stage: string) {
return generationStageText[stage] ?? stage;
}
function AuditsBoardLoading() {
return (
<section className="space-y-4">
<header className="space-y-2">
<p className="text-sm text-muted-foreground">Interne Audit-Übersicht</p>
<h1 className="text-2xl font-semibold tracking-normal">Audits</h1>
<p className="text-sm text-muted-foreground">Audits werden geladen...</p>
</header>
<div className="rounded-lg border">
<div className="grid gap-2 p-3">
{Array.from({ length: 4 }, (_, index) => (
<Skeleton className="h-20 rounded-md" key={index} />
))}
</div>
</div>
</section>
);
}
export function AuditsBoard() {
const dashboardRows = useQuery(api.audits.listDashboardRows, { limit: 100 });
const rows = useMemo(() => {
if (!dashboardRows) {
return [];
}
return [...dashboardRows].sort((a, b) => b.updatedAt - a.updatedAt);
}, [dashboardRows]);
if (dashboardRows === undefined) {
return <AuditsBoardLoading />;
}
if (rows.length === 0) {
return (
<section className="space-y-4">
<header className="space-y-2">
<p className="text-sm text-muted-foreground">Interne Audit-Übersicht</p>
<h1 className="text-2xl font-semibold tracking-normal">Audits</h1>
</header>
<article className="rounded-lg border p-4">
<h2 className="text-sm font-medium">Noch keine Audits</h2>
<p className="mt-1 text-sm text-muted-foreground">
Sobald neue Audits oder laufende Audit-Generierungen angelegt
wurden, erscheinen sie hier als kompakte Zeilen.
</p>
</article>
</section>
);
}
return (
<section className="space-y-4">
<header className="space-y-2">
<p className="text-sm text-muted-foreground">Interne Audit-Übersicht</p>
<h1 className="text-2xl font-semibold tracking-normal">Audits</h1>
</header>
<section className="space-y-2 overflow-x-auto">
<div className="grid min-w-[840px] grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_170px_150px_auto] gap-2 rounded-md border bg-muted/40 px-3 py-2 text-xs font-medium text-muted-foreground">
<span>Slug</span>
<span>Domain</span>
<span>Status</span>
<span>Seiten/Phase</span>
<span className="text-right">Aktion</span>
</div>
<div className="space-y-2">
{rows.map((row: AuditDashboardRow) => (
<article
className="grid min-w-[840px] grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_170px_150px_auto] items-center gap-2 rounded-lg border px-3 py-2 text-sm"
key={row.id}
>
<div className="min-w-0">
<p className="truncate font-medium">{row.title}</p>
{row.kind === "generation" ? (
<p className="truncate text-xs text-muted-foreground">
Run {row.runId}
</p>
) : null}
</div>
<p className="truncate text-muted-foreground">{row.checkedDomain}</p>
<Badge
className="h-auto min-h-6 justify-center whitespace-normal text-center"
variant="secondary"
>
{row.kind === "audit"
? getStatusLabel(row.status)
: getGenerationStatusLabel(row)}
</Badge>
<p className="text-muted-foreground">
<span className="inline-flex items-center gap-1">
{row.kind === "audit" ? (
<>
<Files className="size-3.5" />
{formatPageCount(row.pageCount)}
</>
) : (
<>
<Activity className="size-3.5" />
{getStageLabel(row.latestStage)}
</>
)}
</span>
{row.kind === "generation" && row.errorSummary ? (
<span className="mt-1 block truncate text-xs">
{row.errorSummary}
</span>
) : null}
</p>
<div className="flex justify-end">
{row.kind === "audit" ? (
<Link
className="inline-flex min-h-8 items-center gap-1 text-sm text-primary"
href={row.detailHref}
>
<SquarePen className="size-4" />
Öffnen
</Link>
) : (
<span className="text-sm text-muted-foreground">Pipeline</span>
)}
</div>
</article>
))}
</div>
</section>
</section>
);
}