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:
Khalid Abdi
2026-07-13 20:22:36 +03:00
parent 17209a2cf1
commit b299501ab2
18 changed files with 421 additions and 205 deletions
+55 -58
View File
@@ -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")}>
+32 -24
View File
@@ -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>