BeatSense / frontend /src /components /patients /patients-table.tsx
devrup404's picture
BeatSense: unified FastAPI + Next.js deployment
f4102c2
Raw
History Blame Contribute Delete
6.8 kB
"use client";
import { useMemo, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Download, Search } from "lucide-react";
import { api, type HistoryEntry } from "@/lib/api";
import { useT } from "@/i18n/context";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
type Filter = "all" | "high" | "low";
export function PatientsTable() {
const { dict } = useT();
const [query, setQuery] = useState("");
const [filter, setFilter] = useState<Filter>("all");
const { data } = useQuery({ queryKey: ["history"], queryFn: api.history });
const rows = useMemo(() => {
let r = [...(data ?? [])].reverse();
if (filter === "high") r = r.filter((x) => x.risk === "ALTO");
if (filter === "low") r = r.filter((x) => x.risk !== "ALTO");
if (query.trim())
r = r.filter((x) =>
x.patient_id.toLowerCase().includes(query.toLowerCase())
);
return r;
}, [data, query, filter]);
function exportCsv() {
const headers = [
"Date","Time","Patient ID","Age","Sex","Max HR","Fasting BS","Exercise Angina","Chest Pain","ST Slope","Probability","Risk",
];
const lines = [
headers.join(","),
...rows.map((r: HistoryEntry) =>
[
r.date,
r.time,
r.patient_id,
r.age,
r.sex,
r.max_hr,
r.fasting_bs,
r.exercise_angina,
r.chest_pain_type,
r.st_slope,
r.probability,
r.risk,
].join(",")
),
];
const blob = new Blob([lines.join("\n")], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `beatsense-patients-${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
URL.revokeObjectURL(url);
}
return (
<Card>
<CardContent className="space-y-4 p-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={dict.history.search}
className="h-9 w-72 rounded-md border border-input bg-background pl-9 pr-3 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
</div>
<div className="flex items-center gap-2">
<FilterButton
label={dict.history.filterAll}
active={filter === "all"}
onClick={() => setFilter("all")}
/>
<FilterButton
label={dict.history.filterHigh}
active={filter === "high"}
onClick={() => setFilter("high")}
/>
<FilterButton
label={dict.history.filterLow}
active={filter === "low"}
onClick={() => setFilter("low")}
/>
<Button variant="outline" size="sm" onClick={exportCsv}>
<Download className="h-4 w-4" />
{dict.common.export}
</Button>
</div>
</div>
<div className="overflow-x-auto 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>{dict.history.columns.date}</Th>
<Th>{dict.history.columns.time}</Th>
<Th>{dict.history.columns.patientId}</Th>
<Th className="text-right">{dict.history.columns.age}</Th>
<Th>{dict.history.columns.sex}</Th>
<Th className="text-right">{dict.history.columns.maxHr}</Th>
<Th>{dict.history.columns.fastingBs}</Th>
<Th>{dict.history.columns.exerciseAngina}</Th>
<Th>{dict.history.columns.chestPain}</Th>
<Th>{dict.history.columns.stSlope}</Th>
<Th className="text-right">{dict.history.columns.probability}</Th>
<Th className="text-right">{dict.history.columns.risk}</Th>
</tr>
</thead>
<tbody>
{rows.length === 0 ? (
<tr>
<td
colSpan={12}
className="py-10 text-center text-muted-foreground"
>
{dict.common.noData}
</td>
</tr>
) : (
rows.map((r, i) => (
<tr key={`${r.patient_id}-${i}`} className="border-t border-border hover:bg-muted/20">
<Td>{r.date}</Td>
<Td>{r.time}</Td>
<Td className="font-mono text-xs">{r.patient_id}</Td>
<Td className="text-right">{r.age}</Td>
<Td>{r.sex}</Td>
<Td className="text-right">{r.max_hr}</Td>
<Td>{r.fasting_bs}</Td>
<Td>{r.exercise_angina}</Td>
<Td>{r.chest_pain_type}</Td>
<Td>{r.st_slope}</Td>
<Td className="text-right font-semibold">
{r.probability.toFixed(1)}%
</Td>
<Td className="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>
);
}
function FilterButton({
label,
active,
onClick,
}: {
label: string;
active: boolean;
onClick: () => void;
}) {
return (
<button
onClick={onClick}
className={`h-9 rounded-md border px-3 text-xs font-medium transition-colors ${
active
? "border-primary bg-primary/10 text-primary"
: "border-border bg-card text-muted-foreground hover:text-foreground"
}`}
>
{label}
</button>
);
}
function Th({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<th className={`px-3 py-2 text-left ${className ?? ""}`}>{children}</th>
);
}
function Td({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return <td className={`px-3 py-2 ${className ?? ""}`}>{children}</td>;
}