mirror of
https://github.com/temetro/temetro.git
synced 2026-08-21 23:47:14 +00:00
frontend: COSS checkbox/table + fewer toolbar/sheet buttons
- Invoice dialog: the Back-date / Due-date toggles now use the COSS Checkbox instead of raw, misshapen native checkboxes. - Patients page: rebuilt the list as a COSS Table in a CardFrame, and moved "Import from a patient app" into a ⋯ overflow menu so the toolbar keeps one primary "Add patient" CTA. - Patient detail sheet: collapsed the five header buttons + delete into a primary Edit action plus a ⋯ More menu (Download summary, Record visit, Transfer, Push to wallet, and a destructive Delete). - Added the COSS table + checkbox primitives; new i18n keys across all locales. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
type LucideIcon,
|
||||
ListTodo,
|
||||
Mic,
|
||||
MoreHorizontal,
|
||||
Network,
|
||||
NotebookPen,
|
||||
Pencil,
|
||||
@@ -29,6 +30,13 @@ import {
|
||||
import { printPatientSummary } from "@/lib/patient-pdf";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuPopup,
|
||||
MenuSeparator,
|
||||
MenuTrigger,
|
||||
} from "@/components/ui/menu";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@@ -315,63 +323,63 @@ export function PatientDetail({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Actions — their own wrapping row beneath the identity. */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
onClick={() => printPatientSummary(patient, t)}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<FileDown className="size-4" />
|
||||
{t("patientCard.exportPdf")}
|
||||
</Button>
|
||||
{onTransfer && (
|
||||
<Button
|
||||
onClick={onTransfer}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<ArrowLeftRight className="size-4" />
|
||||
{t("patients.transfer.action")}
|
||||
</Button>
|
||||
)}
|
||||
{onScribe && (
|
||||
<Button onClick={onScribe} size="sm" type="button" variant="outline">
|
||||
<Mic className="size-4" />
|
||||
{t("scribe.recordVisit")}
|
||||
</Button>
|
||||
)}
|
||||
{/* Actions — one primary (Edit) with the rest tucked into an overflow
|
||||
menu so the header stays uncluttered. */}
|
||||
<div className="flex items-center gap-2">
|
||||
{onEdit && (
|
||||
<Button onClick={onEdit} size="sm" type="button" variant="outline">
|
||||
<Pencil className="size-4" />
|
||||
{t("patientCard.edit")}
|
||||
</Button>
|
||||
)}
|
||||
{onWalletPush && (
|
||||
<Button
|
||||
onClick={onWalletPush}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
<Menu>
|
||||
<MenuTrigger
|
||||
render={
|
||||
<Button
|
||||
aria-label={t("patientCard.moreActions")}
|
||||
className="ms-auto"
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Send className="size-4" />
|
||||
{t("walletPush.action")}
|
||||
</Button>
|
||||
)}
|
||||
{onDelete && (
|
||||
<Button
|
||||
aria-label={t("patients.delete.action")}
|
||||
className="ms-auto"
|
||||
onClick={onDelete}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="destructive"
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
<MoreHorizontal className="size-4" />
|
||||
</MenuTrigger>
|
||||
<MenuPopup align="end">
|
||||
<MenuItem onClick={() => printPatientSummary(patient, t)}>
|
||||
<FileDown className="size-4" />
|
||||
{t("patientCard.exportPdf")}
|
||||
</MenuItem>
|
||||
{onScribe && (
|
||||
<MenuItem onClick={onScribe}>
|
||||
<Mic className="size-4" />
|
||||
{t("scribe.recordVisit")}
|
||||
</MenuItem>
|
||||
)}
|
||||
{onTransfer && (
|
||||
<MenuItem onClick={onTransfer}>
|
||||
<ArrowLeftRight className="size-4" />
|
||||
{t("patients.transfer.action")}
|
||||
</MenuItem>
|
||||
)}
|
||||
{onWalletPush && (
|
||||
<MenuItem onClick={onWalletPush}>
|
||||
<Send className="size-4" />
|
||||
{t("walletPush.action")}
|
||||
</MenuItem>
|
||||
)}
|
||||
{onDelete && (
|
||||
<>
|
||||
<MenuSeparator />
|
||||
<MenuItem onClick={onDelete} variant="destructive">
|
||||
<Trash2 className="size-4" />
|
||||
{t("patients.delete.action")}
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</MenuPopup>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Plus, Search, Smartphone } from "lucide-react";
|
||||
import { MoreHorizontal, Plus, Search, Smartphone } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -11,8 +11,15 @@ import { ImportFromWalletDialog } from "@/components/patients/import-from-wallet
|
||||
import { PatientDetailSheet } from "@/components/patients/patient-detail-sheet";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardFrame } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ListPagination } from "@/components/ui/list-pagination";
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuPopup,
|
||||
MenuTrigger,
|
||||
} from "@/components/ui/menu";
|
||||
import {
|
||||
Select,
|
||||
SelectItem,
|
||||
@@ -20,6 +27,14 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { listPatients, type Patient } from "@/lib/patients";
|
||||
|
||||
type StatusFilter = "all" | Patient["status"];
|
||||
@@ -181,15 +196,6 @@ export function PatientsView() {
|
||||
))}
|
||||
</SelectPopup>
|
||||
</Select>
|
||||
<Button
|
||||
className="rounded-3xl"
|
||||
onClick={() => setImportOpen(true)}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<Smartphone className="size-4" />
|
||||
{t("patients.importFromApp")}
|
||||
</Button>
|
||||
<Button
|
||||
className="rounded-3xl"
|
||||
onClick={() => {
|
||||
@@ -201,58 +207,88 @@ export function PatientsView() {
|
||||
<Plus className="size-4" />
|
||||
{t("patients.add")}
|
||||
</Button>
|
||||
{/* Secondary actions tuck into an overflow menu so the toolbar keeps a
|
||||
single primary CTA. */}
|
||||
<Menu>
|
||||
<MenuTrigger
|
||||
render={
|
||||
<Button
|
||||
aria-label={t("patients.moreActions")}
|
||||
className="rounded-full"
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="outline"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
</MenuTrigger>
|
||||
<MenuPopup align="end">
|
||||
<MenuItem onClick={() => setImportOpen(true)}>
|
||||
<Smartphone className="size-4" />
|
||||
{t("patients.importFromApp")}
|
||||
</MenuItem>
|
||||
</MenuPopup>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 overflow-hidden rounded-2xl border border-border bg-card/30">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-border border-b text-start text-xs text-muted-foreground uppercase">
|
||||
<th className="px-4 py-3 font-medium">{t("patients.columns.name")}</th>
|
||||
<th className="px-4 py-3 font-medium">{t("patients.columns.mrn")}</th>
|
||||
<th className="px-4 py-3 font-medium">
|
||||
<CardFrame className="mt-8 w-full">
|
||||
<Table variant="card">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="ps-4 text-xs uppercase">
|
||||
{t("patients.columns.name")}
|
||||
</TableHead>
|
||||
<TableHead className="text-xs uppercase">
|
||||
{t("patients.columns.mrn")}
|
||||
</TableHead>
|
||||
<TableHead className="text-xs uppercase">
|
||||
{t("patients.columns.ageSex")}
|
||||
</th>
|
||||
<th className="px-4 py-3 font-medium">
|
||||
</TableHead>
|
||||
<TableHead className="text-xs uppercase">
|
||||
{t("patients.columns.status")}
|
||||
</th>
|
||||
<th className="px-4 py-3 font-medium">
|
||||
</TableHead>
|
||||
<TableHead className="text-xs uppercase">
|
||||
{t("patients.columns.lastSeen")}
|
||||
</th>
|
||||
<th className="px-4 py-3 font-medium">
|
||||
</TableHead>
|
||||
<TableHead className="pe-4 text-xs uppercase">
|
||||
{t("patients.columns.allergies")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td
|
||||
className="px-4 py-10 text-center text-muted-foreground"
|
||||
<TableRow>
|
||||
<TableCell
|
||||
className="py-10 text-center text-muted-foreground"
|
||||
colSpan={6}
|
||||
>
|
||||
{t("patients.loading")}
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : loadError ? (
|
||||
<tr>
|
||||
<td className="px-4 py-10 text-center text-destructive" colSpan={6}>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
className="py-10 text-center text-destructive"
|
||||
colSpan={6}
|
||||
>
|
||||
{loadError}
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : patients.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
className="px-4 py-10 text-center text-muted-foreground"
|
||||
<TableRow>
|
||||
<TableCell
|
||||
className="py-10 text-center text-muted-foreground"
|
||||
colSpan={6}
|
||||
>
|
||||
{t("patients.empty")}
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
pageRows.map((p) => (
|
||||
<tr
|
||||
className="cursor-pointer border-border/50 border-b transition-colors last:border-0 hover:bg-accent/50"
|
||||
<TableRow
|
||||
className="cursor-pointer"
|
||||
key={p.fileNumber}
|
||||
onClick={() => open(p.fileNumber)}
|
||||
onKeyDown={(event) => {
|
||||
@@ -264,7 +300,7 @@ export function PatientsView() {
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<td className="px-4 py-3 font-medium text-foreground">
|
||||
<TableCell className="ps-4 py-3 font-medium text-foreground">
|
||||
<span className="flex items-center gap-2">
|
||||
{p.name}
|
||||
<AiBadge source={p.source} />
|
||||
@@ -274,30 +310,30 @@ export function PatientsView() {
|
||||
</Badge>
|
||||
) : null}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
</TableCell>
|
||||
<TableCell className="py-3 text-muted-foreground">
|
||||
{p.fileNumber}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
</TableCell>
|
||||
<TableCell className="py-3 text-muted-foreground">
|
||||
{p.age} · {p.sex}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
</TableCell>
|
||||
<TableCell className="py-3">
|
||||
<Badge variant={statusVariant[p.status]}>
|
||||
{t(`patients.status.${p.status}`)}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
</TableCell>
|
||||
<TableCell className="py-3 text-muted-foreground">
|
||||
{p.encounters[0]?.date ?? "—"}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
</TableCell>
|
||||
<TableCell className="pe-4 py-3 text-muted-foreground">
|
||||
{p.allergies.length || "—"}
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardFrame>
|
||||
|
||||
{!loading && !loadError ? (
|
||||
<ListPagination
|
||||
|
||||
Reference in New Issue
Block a user