BeatSense / frontend /src /components /dashboard /dashboard-client.tsx
devrup404's picture
BeatSense: unified FastAPI + Next.js deployment
f4102c2
Raw
History Blame Contribute Delete
7.49 kB
"use client";
import Link from "next/link";
import { useQuery } from "@tanstack/react-query";
import {
Activity,
AlertTriangle,
CalendarDays,
HeartPulse,
TrendingUp,
} from "lucide-react";
import {
PieChart,
Pie,
Cell,
ResponsiveContainer,
Legend,
Tooltip,
} from "recharts";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { api } from "@/lib/api";
import { useT } from "@/i18n/context";
export function DashboardClient() {
const { dict, locale } = useT();
const stats = useQuery({ queryKey: ["stats"], queryFn: api.stats });
const history = useQuery({ queryKey: ["history"], queryFn: api.history });
const s = stats.data ?? {
total: 0,
high_risk: 0,
low_risk: 0,
high_risk_pct: 0,
today: 0,
};
const recent = (history.data ?? []).slice(-8).reverse();
const pieData = [
{ name: dict.dashboard.kpi.lowRisk, value: s.low_risk, color: "var(--success)" },
{ name: dict.dashboard.kpi.highRisk, value: s.high_risk, color: "var(--destructive)" },
];
return (
<div className="grid gap-6">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<KpiCard
icon={<Activity className="h-4 w-4" />}
label={dict.dashboard.kpi.totalAssessments}
value={s.total.toString()}
/>
<KpiCard
icon={<CalendarDays className="h-4 w-4" />}
label={dict.dashboard.kpi.todayAssessments}
value={s.today.toString()}
/>
<KpiCard
icon={<AlertTriangle className="h-4 w-4" />}
label={dict.dashboard.kpi.highRiskPct}
value={`${s.high_risk_pct}%`}
tone={s.high_risk_pct >= 40 ? "danger" : "default"}
/>
<KpiCard
icon={<TrendingUp className="h-4 w-4" />}
label={dict.dashboard.kpi.highRisk}
value={s.high_risk.toString()}
tone="danger"
/>
</div>
<div className="grid gap-6 lg:grid-cols-3">
<Card className="lg:col-span-2">
<CardContent className="p-6">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
{dict.dashboard.recentTitle}
</h2>
<Button asChild size="sm">
<Link href={`/${locale}/new-evaluation`}>
<HeartPulse className="h-4 w-4" />
{dict.dashboard.quickAction}
</Link>
</Button>
</div>
{recent.length === 0 ? (
<div className="rounded-md border border-dashed border-border py-10 text-center text-sm text-muted-foreground">
{dict.dashboard.recentEmpty}
</div>
) : (
<div className="overflow-hidden rounded-md border border-border">
<table className="w-full text-sm">
<thead className="bg-muted/40 text-xs uppercase tracking-wide text-muted-foreground">
<tr>
<th className="px-3 py-2 text-left">{dict.history.columns.patientId}</th>
<th className="px-3 py-2 text-left">{dict.history.columns.date}</th>
<th className="px-3 py-2 text-left">{dict.history.columns.age}</th>
<th className="px-3 py-2 text-left">{dict.history.columns.sex}</th>
<th className="px-3 py-2 text-right">{dict.history.columns.probability}</th>
<th className="px-3 py-2 text-right">{dict.history.columns.risk}</th>
</tr>
</thead>
<tbody>
{recent.map((r, i) => (
<tr
key={`${r.patient_id}-${i}`}
className="border-t border-border"
>
<td className="px-3 py-2 font-mono text-xs">{r.patient_id}</td>
<td className="px-3 py-2">{r.date} · {r.time}</td>
<td className="px-3 py-2">{r.age}</td>
<td className="px-3 py-2">{r.sex}</td>
<td className="px-3 py-2 text-right font-semibold">
{r.probability.toFixed(1)}%
</td>
<td className="px-3 py-2 text-right">
<Badge variant={r.risk === "ALTO" ? "destructive" : "success"}>
{r.risk === "ALTO"
? dict.common.high
: dict.common.low}
</Badge>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wide text-muted-foreground">
{dict.dashboard.riskDistribution}
</h2>
<div className="h-64">
{s.total === 0 ? (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
{dict.common.noData}
</div>
) : (
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={pieData}
dataKey="value"
nameKey="name"
innerRadius={50}
outerRadius={80}
paddingAngle={2}
>
{pieData.map((d, i) => (
<Cell key={i} fill={d.color} />
))}
</Pie>
<Tooltip
contentStyle={{
background: "var(--popover)",
border: "1px solid var(--border)",
borderRadius: 8,
fontSize: 12,
}}
/>
<Legend wrapperStyle={{ fontSize: 12 }} />
</PieChart>
</ResponsiveContainer>
)}
</div>
</CardContent>
</Card>
</div>
</div>
);
}
function KpiCard({
icon,
label,
value,
tone = "default",
}: {
icon: React.ReactNode;
label: string;
value: string;
tone?: "default" | "danger";
}) {
return (
<Card>
<CardContent className="flex items-center justify-between gap-4 p-5">
<div>
<div className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
{label}
</div>
<div
className={`mt-1 text-3xl font-bold ${
tone === "danger" ? "text-destructive" : "text-foreground"
}`}
>
{value}
</div>
</div>
<div
className={`flex h-11 w-11 items-center justify-center rounded-lg ${
tone === "danger"
? "bg-destructive/10 text-destructive"
: "bg-primary/10 text-primary"
}`}
>
{icon}
</div>
</CardContent>
</Card>
);
}