From ba3be7ba9b5ddb2c8516001ea0926c67c2256220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 6 Aug 2026 04:18:05 +0200 Subject: [PATCH] feat: distinguish managed record access --- .../dashboard/looking-after-card.tsx | 90 ++++++++++--------- .../layout/account-switcher-menu.tsx | 73 ++++++++++----- .../layout/shared-record-banner.tsx | 36 +++++--- src/lib/navigation/record-presentation.ts | 30 +++++++ src/lib/navigation/shared-record.ts | 6 +- 5 files changed, 157 insertions(+), 78 deletions(-) create mode 100644 src/lib/navigation/record-presentation.ts diff --git a/src/components/dashboard/looking-after-card.tsx b/src/components/dashboard/looking-after-card.tsx index c95d6f94b..df251ef40 100644 --- a/src/components/dashboard/looking-after-card.tsx +++ b/src/components/dashboard/looking-after-card.tsx @@ -7,6 +7,10 @@ import { TileHeader } from "@/components/insights/tile-header"; import { useAccountSwitch } from "@/hooks/use-account-switch"; import { useAccountOnceMounted } from "@/hooks/use-auth"; import { useTranslations } from "@/lib/i18n/context"; +import { + resolveRecordPresentation, + type RecordPresentation, +} from "@/lib/navigation/record-presentation"; import { accountLabel, type AccountAccessEntry, @@ -77,42 +81,48 @@ export function LookingAfterCard() { {t("recordSharing.lookingAfter.description")}

@@ -131,22 +141,22 @@ export function LookingAfterCard() { * clothes. */ function levelLabel( - entry: AccountAccessEntry, + presentation: RecordPresentation, t: (key: string) => string, ): string { - if (entry.level === "manage") { + if (presentation.access === "manage") { return t("recordSharing.row.canManage"); } - return entry.canWrite + return presentation.access === "view-and-add" ? t("recordSharing.lookingAfter.levelWrite") : t("recordSharing.lookingAfter.levelRead"); } function recordKindLabel( - entry: AccountAccessEntry, + presentation: RecordPresentation, t: (key: string) => string, ): string { - return entry.recordKind === "managed" + return presentation.recordKind === "managed" ? t("recordSharing.lookingAfter.kindManaged") : t("recordSharing.lookingAfter.kindShared"); } diff --git a/src/components/layout/account-switcher-menu.tsx b/src/components/layout/account-switcher-menu.tsx index e132d385c..e3a3f5513 100644 --- a/src/components/layout/account-switcher-menu.tsx +++ b/src/components/layout/account-switcher-menu.tsx @@ -12,6 +12,7 @@ import { import { useAccountSwitch } from "@/hooks/use-account-switch"; import { useAuth } from "@/hooks/use-auth"; import { useTranslations } from "@/lib/i18n/context"; +import { resolveRecordPresentation } from "@/lib/navigation/record-presentation"; import { accountLabel } from "@/lib/sharing/account-access-view"; /** @@ -79,32 +80,56 @@ export function AccountSwitcherMenuItems() { ✓ )} - {access.accounts.map((account) => ( - switchAccount.mutate(account.accountId)} - > -
- {accountLabel(account)} - {/* Meta, so muted: what the grant lets this person do, in the - words the server resolved rather than a level code. */} - - {account.canWrite - ? t("recordSharing.switcher.canWrite") - : t("recordSharing.switcher.readOnly")} - -
- {activeId === account.accountId && ( - - )} -
- ))} + {access.accounts.map((account) => { + const presentation = resolveRecordPresentation(account); + + return ( + switchAccount.mutate(account.accountId)} + > +
+ {accountLabel(account)} + + {accessLabel(presentation.access, t)} + + + {recordKindLabel(presentation.recordKind, t)} + +
+ {activeId === account.accountId && ( + + )} +
+ ); + })} ); } + +function accessLabel( + access: ReturnType["access"], + t: ReturnType["t"], +): string { + if (access === "manage") return t("recordSharing.row.canManage"); + return access === "view-and-add" + ? t("recordSharing.switcher.canWrite") + : t("recordSharing.switcher.readOnly"); +} + +function recordKindLabel( + recordKind: ReturnType["recordKind"], + t: ReturnType["t"], +): string { + return recordKind === "managed" + ? t("recordSharing.lookingAfter.kindManaged") + : t("recordSharing.lookingAfter.kindShared"); +} diff --git a/src/components/layout/shared-record-banner.tsx b/src/components/layout/shared-record-banner.tsx index 63651d3ef..3d8744abb 100644 --- a/src/components/layout/shared-record-banner.tsx +++ b/src/components/layout/shared-record-banner.tsx @@ -5,6 +5,7 @@ import { Eye, Loader2, LogOut } from "lucide-react"; import { useAccountSwitch } from "@/hooks/use-account-switch"; import { useAuth } from "@/hooks/use-auth"; import { useTranslations } from "@/lib/i18n/context"; +import { resolveRecordPresentation } from "@/lib/navigation/record-presentation"; import { accountLabel } from "@/lib/sharing/account-access-view"; /** @@ -59,12 +60,15 @@ export function SharedRecordBanner() { if (!active) return null; const name = accountLabel(active); + const presentation = resolveRecordPresentation(active); return (
{t("recordSharing.banner.viewing", { name })} {" "} - {/* Resolved server-side, and it says something at BOTH levels now. - v1.36.1 is the release that made `canWrite` true, and until this - line changed the write case rendered nothing at all: a delegate who - could add to somebody's health record was told only whose record it - was. That matters more than the read case, not less, and the rest of - the product leans on it — a control the record refuses is absent - rather than disabled, on the stated grounds that the banner names - the mode globally. It has to actually do that. */} - {active.canWrite - ? t("recordSharing.banner.canAdd") - : t("recordSharing.banner.readOnly")} + {recordKindLabel(presentation.recordKind, t)}.{" "} + {accessLabel(presentation.access, t)}