mirror of
https://github.com/temetro/temetro.git
synced 2026-08-27 19:06:52 +00:00
frontend+backend: patients/settings UI, AI Auto/Off modes, wallet doc push
- patients: move status filter to its own "Filter" row above the table - patient sheet: name + ⋯ menu on one left row; Edit moved into the ⋯ menu - settings: redesign sections with the COSS CardFrame surface (SettingsFrame) - AI: add Automatic (auto-pick provider) and Off modes; default to Automatic so a fresh install shows the setup banner until a provider is configured - AI: fix the setup banner never showing (defaulted Ollama URL counted as configured); add an "AI off" notice; add a fallback render for unknown chat data parts so cards never silently vanish - backend: include patient attachment metadata in the wallet record-update bundle so pushed documents reach the wallet - i18n: add mode/off/card keys across all five locales Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -303,6 +303,8 @@ export function PatientDetail({
|
||||
<AvatarFallback>{patient.initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
{/* Name, status, and the overflow menu share one left-aligned row;
|
||||
Edit now lives inside the menu rather than as a separate button. */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-semibold text-base text-foreground">
|
||||
{patient.name}
|
||||
@@ -310,6 +312,59 @@ export function PatientDetail({
|
||||
<Badge variant={statusVariant[patient.status]}>
|
||||
{t(`patients.status.${patient.status}`)}
|
||||
</Badge>
|
||||
<Menu>
|
||||
<MenuTrigger
|
||||
render={
|
||||
<Button
|
||||
aria-label={t("patientCard.moreActions")}
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
</MenuTrigger>
|
||||
<MenuPopup align="start">
|
||||
{onEdit && (
|
||||
<MenuItem onClick={onEdit}>
|
||||
<Pencil className="size-4" />
|
||||
{t("patientCard.edit")}
|
||||
</MenuItem>
|
||||
)}
|
||||
<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>
|
||||
<span className="text-muted-foreground text-sm">{idLine}</span>
|
||||
{patient.alerts.length > 0 && (
|
||||
@@ -323,64 +378,6 @@ export function PatientDetail({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* 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>
|
||||
)}
|
||||
<Menu>
|
||||
<MenuTrigger
|
||||
render={
|
||||
<Button
|
||||
aria-label={t("patientCard.moreActions")}
|
||||
className="ms-auto"
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<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>
|
||||
|
||||
<Section title={t("patientCard.overview")}>
|
||||
|
||||
@@ -173,29 +173,6 @@ export function PatientsView() {
|
||||
value={query}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
setStatusFilter((value ?? "all") as StatusFilter);
|
||||
setPage(1);
|
||||
}}
|
||||
value={statusFilter}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label={t("patients.filterStatus")}
|
||||
className="w-full sm:w-40"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectPopup>
|
||||
{STATUS_FILTERS.map((value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{value === "all"
|
||||
? t("patients.allStatuses")
|
||||
: t(`patients.status.${value}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectPopup>
|
||||
</Select>
|
||||
<Button
|
||||
className="rounded-3xl"
|
||||
onClick={() => {
|
||||
@@ -233,7 +210,38 @@ export function PatientsView() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardFrame className="mt-8 w-full">
|
||||
{/* Filter sits on its own row just above the table so the toolbar stays a
|
||||
clean title + search + primary actions cluster. */}
|
||||
<div className="mt-8 flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{t("patients.filterLabel")}
|
||||
</span>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
setStatusFilter((value ?? "all") as StatusFilter);
|
||||
setPage(1);
|
||||
}}
|
||||
value={statusFilter}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label={t("patients.filterStatus")}
|
||||
className="w-40"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectPopup>
|
||||
{STATUS_FILTERS.map((value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{value === "all"
|
||||
? t("patients.allStatuses")
|
||||
: t(`patients.status.${value}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectPopup>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<CardFrame className="mt-4 w-full">
|
||||
<Table variant="card">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
|
||||
Reference in New Issue
Block a user