Improve audit pipeline and outreach review
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { useQuery } from "convex/react";
|
||||
import { FunctionReturnType } from "convex/server";
|
||||
@@ -10,10 +10,21 @@ import Link from "next/link";
|
||||
import { api } from "@/convex/_generated/api";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
type AuditDashboardRowsResult = FunctionReturnType<typeof api.audits.listDashboardRows>;
|
||||
type AuditRow = Extract<NonNullable<AuditDashboardRowsResult>[number], { kind: "audit" }>;
|
||||
type AuditRow = Extract<
|
||||
NonNullable<AuditDashboardRowsResult>[number],
|
||||
{ kind: "audit" }
|
||||
>;
|
||||
type AuditDashboardRow = NonNullable<AuditDashboardRowsResult>[number];
|
||||
type AuditStatusFilter = "all" | "audit" | "generation" | "failed";
|
||||
|
||||
const statusText: Record<string, string> = {
|
||||
draft: "Entwurf",
|
||||
@@ -74,12 +85,10 @@ function AuditsBoardLoading() {
|
||||
<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 className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{Array.from({ length: 4 }, (_, index) => (
|
||||
<Skeleton className="h-40 rounded-lg" key={index} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -87,6 +96,7 @@ function AuditsBoardLoading() {
|
||||
|
||||
export function AuditsBoard() {
|
||||
const dashboardRows = useQuery(api.audits.listDashboardRows, { limit: 100 });
|
||||
const [activeFilter, setActiveFilter] = useState<AuditStatusFilter>("all");
|
||||
const rows = useMemo(() => {
|
||||
if (!dashboardRows) {
|
||||
return [];
|
||||
@@ -94,6 +104,43 @@ export function AuditsBoard() {
|
||||
|
||||
return [...dashboardRows].sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
}, [dashboardRows]);
|
||||
const statusCounts = useMemo(() => {
|
||||
return {
|
||||
all: rows.length,
|
||||
audit: rows.filter((row) => row.kind === "audit").length,
|
||||
generation: rows.filter((row) => row.kind === "generation").length,
|
||||
failed: rows.filter(
|
||||
(row) => row.kind === "generation" && row.status === "failed",
|
||||
).length,
|
||||
};
|
||||
}, [rows]);
|
||||
const visibleRows = useMemo(() => {
|
||||
if (activeFilter === "audit") {
|
||||
return rows.filter((row) => row.kind === "audit");
|
||||
}
|
||||
|
||||
if (activeFilter === "generation") {
|
||||
return rows.filter((row) => row.kind === "generation");
|
||||
}
|
||||
|
||||
if (activeFilter === "failed") {
|
||||
return rows.filter(
|
||||
(row) => row.kind === "generation" && row.status === "failed",
|
||||
);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}, [activeFilter, rows]);
|
||||
const auditStatusFilters: Array<{
|
||||
label: string;
|
||||
value: AuditStatusFilter;
|
||||
count: number;
|
||||
}> = [
|
||||
{ label: "Alle", value: "all", count: statusCounts.all },
|
||||
{ label: "Audits", value: "audit", count: statusCounts.audit },
|
||||
{ label: "Pipeline", value: "generation", count: statusCounts.generation },
|
||||
{ label: "Fehlgeschlagen", value: "failed", count: statusCounts.failed },
|
||||
];
|
||||
|
||||
if (dashboardRows === undefined) {
|
||||
return <AuditsBoardLoading />;
|
||||
@@ -107,13 +154,15 @@ export function AuditsBoard() {
|
||||
<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>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2 className="text-sm font-medium">Noch keine Audits</h2>
|
||||
<CardDescription>
|
||||
Sobald neue Audits oder laufende Audit-Generierungen angelegt
|
||||
wurden, erscheinen sie hier als kompakte Cards.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -125,74 +174,118 @@ export function AuditsBoard() {
|
||||
<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="flex flex-wrap gap-2" aria-label="Audit-Filter">
|
||||
{auditStatusFilters.map((filter) => (
|
||||
<button
|
||||
aria-pressed={activeFilter === filter.value}
|
||||
className="inline-flex min-h-8 items-center gap-2 rounded-md border px-3 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted aria-pressed:border-foreground aria-pressed:text-foreground"
|
||||
key={filter.value}
|
||||
onClick={() => setActiveFilter(filter.value)}
|
||||
type="button"
|
||||
>
|
||||
{filter.label}
|
||||
<Badge variant="secondary">{filter.count}</Badge>
|
||||
</button>
|
||||
))}
|
||||
</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"
|
||||
<section
|
||||
className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3"
|
||||
aria-label="Audit-Cards"
|
||||
>
|
||||
{visibleRows.map((row: AuditDashboardRow) => {
|
||||
const rowTitleId = `audit-row-title-${row.id}`;
|
||||
|
||||
return (
|
||||
<Card
|
||||
aria-labelledby={rowTitleId}
|
||||
className="flex min-w-0 flex-col"
|
||||
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">
|
||||
<CardHeader className="gap-3">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<CardDescription>
|
||||
{row.kind === "audit" ? "Audit" : "Pipeline"}
|
||||
</CardDescription>
|
||||
<CardTitle className="mt-1 break-words text-base" id={rowTitleId}>
|
||||
{row.title}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<Badge variant={row.kind === "audit" ? "secondary" : "outline"}>
|
||||
{row.kind === "audit"
|
||||
? getStatusLabel(row.status)
|
||||
: getGenerationStatusLabel(row)}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-1 flex-col gap-4">
|
||||
<div className="grid gap-3 text-sm">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-medium text-muted-foreground">Domain</p>
|
||||
<p className="mt-1 break-all">{row.checkedDomain}</p>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
{row.kind === "audit" ? "Seiten" : "Phase"}
|
||||
</p>
|
||||
<p className="mt-1 inline-flex items-center gap-1 text-muted-foreground">
|
||||
{row.kind === "audit" ? (
|
||||
<>
|
||||
<Files className="size-3.5" aria-hidden="true" />
|
||||
{formatPageCount(row.pageCount)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Activity className="size-3.5" aria-hidden="true" />
|
||||
{getStageLabel(row.latestStage)}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-medium text-muted-foreground">Slug</p>
|
||||
<p className="mt-1 break-words text-muted-foreground">
|
||||
{row.kind === "generation" ? `Run ${row.runId}` : row.title}
|
||||
</p>
|
||||
</div>
|
||||
{row.kind === "generation" && row.errorSummary ? (
|
||||
<p className="break-words rounded-md border bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
|
||||
{row.errorSummary}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex justify-end">
|
||||
{row.kind === "audit" ? (
|
||||
<>
|
||||
<Files className="size-3.5" />
|
||||
{formatPageCount(row.pageCount)}
|
||||
</>
|
||||
<Link
|
||||
className="inline-flex min-h-8 items-center gap-1 rounded-md px-2 text-sm text-primary hover:bg-muted"
|
||||
href={row.detailHref}
|
||||
>
|
||||
<SquarePen className="size-4" aria-hidden="true" />
|
||||
Öffnen
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<Activity className="size-3.5" />
|
||||
{getStageLabel(row.latestStage)}
|
||||
</>
|
||||
<span className="inline-flex min-h-8 items-center text-sm text-muted-foreground">
|
||||
Pipeline läuft
|
||||
</span>
|
||||
)}
|
||||
</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>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
{visibleRows.length === 0 ? (
|
||||
<Card className="sm:col-span-2 xl:col-span-3">
|
||||
<CardHeader>
|
||||
<CardTitle>Keine Treffer</CardTitle>
|
||||
<CardDescription>
|
||||
Für diesen Filter gibt es aktuell keine Audit-Einträge.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
) : null}
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user