diff --git a/src/app/labs/page.tsx b/src/app/labs/page.tsx
index 737a20f75..78adf9925 100644
--- a/src/app/labs/page.tsx
+++ b/src/app/labs/page.tsx
@@ -30,12 +30,18 @@ import { PullToRefreshIndicator } from "@/components/ui/pull-to-refresh-indicato
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { useAuth } from "@/hooks/use-auth";
import { useMounted } from "@/hooks/use-mounted";
+import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { usePullToRefresh } from "@/hooks/use-pull-to-refresh";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
export default function LabsPage() {
const { user, isAuthenticated, isLoading } = useAuth();
+ // v1.36.x — entering a lab result by hand is an admitted delegated write.
+ // Scanning a report is not: it files a document and commits through the OCR
+ // route, and neither is delegated. So a WRITE delegate keeps the manual add
+ // and loses the choice menu around it.
+ const { canAdd, canManage } = useRecordCapabilities();
const mounted = useMounted();
const router = useRouter();
const queryClient = useQueryClient();
@@ -94,27 +100,29 @@ export default function LabsPage() {
point, left of the primary Add and linking to the Labs settings
page (view, sort order, biomarker CRUD + reorder). Mirrors the
medication page header's wrench glyph + slot + 44px tap floor. */}
-
+
+
+
+
+ )}
{/* v1.18.10 — the "Add" action is a CHOICE when scanning is
available: scan a document (OCR) or add a value by hand. The scan
option only appears when the capability probe reports a usable
mode (vision, or local OCR opted-in for text-only providers), so
the surface stays simple for everyone else. When scanning is
unavailable, Add opens the manual form directly. */}
- {ocrCapability.data?.available ? (
+ {ocrCapability.data?.available && canManage ? (
- ) : (
+ ) : canAdd ? (
- )}
+ ) : null}
>
}
/>
diff --git a/src/app/measurements/page.tsx b/src/app/measurements/page.tsx
index 599915e8b..f6517480a 100644
--- a/src/app/measurements/page.tsx
+++ b/src/app/measurements/page.tsx
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
import { useMounted } from "@/hooks/use-mounted";
+import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { usePullToRefresh } from "@/hooks/use-pull-to-refresh";
import {
MeasurementForm,
@@ -35,6 +36,7 @@ export function resolveMeasurementReturnTo(
export default function MeasurementsPage() {
const { isAuthenticated, isLoading } = useAuth();
+ const { canAdd } = useRecordCapabilities();
const mounted = useMounted();
const router = useRouter();
const queryClient = useQueryClient();
@@ -149,17 +151,22 @@ export default function MeasurementsPage() {
}
description={t("measurements.subtitle")}
+ // v1.36.x — entering a reading is one of the verbs a WRITE grant
+ // admits, so this is the header action a delegate keeps. A read-only
+ // delegate loses it entirely rather than meeting a disabled one.
actions={
-
+ canAdd ? (
+
+ ) : null
}
/>
diff --git a/src/app/mood/page.tsx b/src/app/mood/page.tsx
index a98078fd6..cc9b87b29 100644
--- a/src/app/mood/page.tsx
+++ b/src/app/mood/page.tsx
@@ -4,6 +4,7 @@ import { useCallback, useState } from "react";
import Link from "next/link";
import { useQueryClient } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
+import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { useMounted } from "@/hooks/use-mounted";
import { usePullToRefresh } from "@/hooks/use-pull-to-refresh";
import { MoodForm } from "@/components/mood/mood-form";
@@ -20,6 +21,7 @@ import { useTranslations } from "@/lib/i18n/context";
export default function MoodPage() {
const { user, isAuthenticated, isLoading } = useAuth();
+ const { canManage } = useRecordCapabilities();
const mounted = useMounted();
const router = useRouter();
const queryClient = useQueryClient();
@@ -95,13 +97,18 @@ export default function MoodPage() {
-
+ {/* v1.36.x — a mood entry is not one of the verbs a grant
+ admits, so it stays with the account whose record it is. A
+ delegate at any level gets no add path here. */}
+ {canManage && (
+
+ )}
>
}
/>
diff --git a/src/components/custom-metrics/custom-metric-list.tsx b/src/components/custom-metrics/custom-metric-list.tsx
index b7669cc0c..01e3bafc0 100644
--- a/src/components/custom-metrics/custom-metric-list.tsx
+++ b/src/components/custom-metrics/custom-metric-list.tsx
@@ -1,5 +1,6 @@
"use client";
+import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
@@ -29,6 +30,14 @@ import type { CustomMetricDto, CustomMetricListResponse } from "./types";
*/
export function CustomMetricList() {
const { t } = useTranslations();
+ // v1.36.x — the section is dropped whole inside somebody else's record.
+ // Defining a metric is not a delegated verb, and every row here links to
+ // `/custom-metrics/{id}`, which sharing does not cover: the shell turns a
+ // delegate away at that route, so the rows would be doors into a wall.
+ // Logging a value against a custom metric IS admitted, but there is no
+ // reachable surface for it under a switch, and inventing one is a read-model
+ // change this does not make.
+ const { canManage } = useRecordCapabilities();
const router = useRouter();
const [addOpen, setAddOpen] = useState(false);
const [addFooterEl, setAddFooterEl] = useState(null);
@@ -47,6 +56,8 @@ export function CustomMetricList() {
router.push(`/custom-metrics/${saved.id}`);
}
+ if (!canManage) return null;
+
return (
void;
+ /**
+ * Open the day. Omitted inside somebody else's record: no grant level
+ * admits a cycle write, so the cells stay readable and stop being controls
+ * rather than opening a sheet whose every button the server would refuse.
+ */
+ onSelectDay?: (date: string) => void;
className?: string;
}
@@ -208,34 +213,8 @@ export function CycleCalendar({
? "UNGRADED"
: undefined;
- return (
-
- remove.mutate(doc.id)}
- disabled={remove.isPending}
- >
-
- {t("documents.detail.delete")}
-
+ {/* v1.36.x — deleting, sharing and re-filing a document are the
+ owner's alone. Inside somebody else's record the sheet reads
+ the document and offers the download, and the leading slot
+ simply has nothing in it. */}
+ {canManage ? (
+ remove.mutate(doc.id)}
+ disabled={remove.isPending}
+ >
+
+ {t("documents.detail.delete")}
+
+ ) : (
+
+ )}
) : (
{t("documents.detail.kindLabel")}
+ {/* Read-only rather than absent: the value IS the content
+ a delegate came to read, and a form field that shows it
+ without accepting a change is the honest rendering. */}
) : (
diff --git a/src/components/illness/illness-episode-detail.tsx b/src/components/illness/illness-episode-detail.tsx
index 79e4a8a17..98c444d23 100644
--- a/src/components/illness/illness-episode-detail.tsx
+++ b/src/components/illness/illness-episode-detail.tsx
@@ -8,6 +8,7 @@
* red-flag / pre-onset / nadir). Calm + retrospective — no prediction UI, no
* alarming colour.
*/
+import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { useState } from "react";
import { EpisodeDocumentsCard } from "@/components/documents/episode-documents-card";
@@ -36,6 +37,9 @@ function todayLocal(): string {
export function IllnessEpisodeDetail({ episodeId }: { episodeId: string }) {
const { t } = useTranslations();
+ // v1.36.x — logging a day, editing and resolving an episode are the
+ // owner's; a delegate reads the episode.
+ const { canManage } = useRecordCapabilities();
const fmt = useFormatters();
const {
data: episode,
@@ -93,26 +97,28 @@ export function IllnessEpisodeDetail({ episodeId }: { episodeId: string }) {
+ )}
);
@@ -308,6 +316,10 @@ function EpisodeGroup({
export function IllnessView() {
const { t } = useTranslations();
+ // v1.36.x — opening an illness entry is an admitted delegated write. Logging
+ // a day against one, editing it, resolving it and deleting it are not, so a
+ // delegate can start the record of an illness and nothing more.
+ const { canAdd, canManage } = useRecordCapabilities();
const {
data: episodes,
isLoading,
@@ -380,29 +392,33 @@ export function IllnessView() {
<>
{/* v1.18.6 (MOD-01) — wrench left of the primary Add, linking to the
Illness settings page (view + reorder). */}
-
-
-
-
-
+
+
+
+
+ )}
{/* v1.18.6 (MOD-02) — the add button reads "hinzufügen" like every
other module, not the bespoke "neue Episode". */}
- setNewOpen(true)}
- className="min-h-11 sm:min-h-9"
- >
-
- {t("common.add")}
-
+ {canAdd && (
+ setNewOpen(true)}
+ className="min-h-11 sm:min-h-9"
+ >
+
+ {t("common.add")}
+
+ )}
>
}
/>
@@ -458,10 +474,12 @@ export function IllnessView() {
description={t("illness.empty.body")}
ctaSize="lg"
action={
- setNewOpen(true)}>
-
- {t("illness.newEpisode")}
-
+ canAdd ? (
+ setNewOpen(true)}>
+
+ {t("illness.newEpisode")}
+
+ ) : undefined
}
/>
)}
diff --git a/src/components/labs/lab-biomarker-detail.tsx b/src/components/labs/lab-biomarker-detail.tsx
index 6985618a7..d57b42bd7 100644
--- a/src/components/labs/lab-biomarker-detail.tsx
+++ b/src/components/labs/lab-biomarker-detail.tsx
@@ -1,5 +1,6 @@
"use client";
+import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
@@ -93,6 +94,7 @@ const READINGS_PAGE_SIZE = 200;
*/
export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) {
const { user } = useAuth();
+ const { canAdd, canManage } = useRecordCapabilities();
const { t, locale } = useTranslations();
const router = useRouter();
const queryClient = useQueryClient();
@@ -346,17 +348,19 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) {
className="min-h-11 min-w-11 sm:min-h-9 sm:min-w-9"
iconClassName="h-4 w-4"
/>
- setEditOpen(true)}
- disabled={!marker}
- aria-label={t("labs.biomarker.edit")}
- title={t("labs.biomarker.edit")}
- >
-
-
+ {canManage && (
+ setEditOpen(true)}
+ disabled={!marker}
+ aria-label={t("labs.biomarker.edit")}
+ title={t("labs.biomarker.edit")}
+ >
+
+
+ )}
{/* "Show all readings" mirrors the metric sub-pages' ``
control. The full reading feed lives on
`/labs/[biomarkerId]/values`; the detail page keeps the
@@ -378,18 +382,22 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) {
) : null}
- setAddOpen(true)}
- // v1.18.10 (W10) — on the narrowest phones the h1 + Edit + Delete +
- // text "Add" button crowd the row and truncate the title hard.
- // Drop the Add button to icon-only under `sm` (label kept for
- // screen readers via `aria-label`); the full text returns at `sm+`.
- className="min-h-11 min-w-11 shrink-0 sm:min-h-9 sm:min-w-0"
- aria-label={t("labs.addResult")}
- >
-
- {t("labs.addResult")}
-
+ {/* v1.36.x — entering a result is an admitted delegated write;
+ renaming or removing the marker is not. */}
+ {canAdd && (
+ setAddOpen(true)}
+ // v1.18.10 (W10) — on the narrowest phones the h1 + Edit + Delete +
+ // text "Add" button crowd the row and truncate the title hard.
+ // Drop the Add button to icon-only under `sm` (label kept for
+ // screen readers via `aria-label`); the full text returns at `sm+`.
+ className="min-h-11 min-w-11 shrink-0 sm:min-h-9 sm:min-w-0"
+ aria-label={t("labs.addResult")}
+ >
+
+ {t("labs.addResult")}
+
+ )}
@@ -429,9 +437,11 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) {
title={t("labs.detail.emptyTitle")}
description={t("labs.detail.emptyDescription")}
action={
- setAddOpen(true)}>
- {t("labs.addResult")}
-
+ canAdd ? (
+ setAddOpen(true)}>
+ {t("labs.addResult")}
+
+ ) : undefined
}
/>
diff --git a/src/components/labs/lab-history-list.tsx b/src/components/labs/lab-history-list.tsx
index 5b547dd7a..329ce1561 100644
--- a/src/components/labs/lab-history-list.tsx
+++ b/src/components/labs/lab-history-list.tsx
@@ -1,5 +1,6 @@
"use client";
+import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { useMemo, useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Loader2, Pencil } from "lucide-react";
@@ -74,6 +75,7 @@ function parseDecimal(raw: string): number | null {
*/
export function LabHistoryList({ readings }: { readings: LabResultDto[] }) {
const { t } = useTranslations();
+ const { canManage } = useRecordCapabilities();
const queryClient = useQueryClient();
// Client-side column sort over the in-memory reading feed.
@@ -361,17 +363,19 @@ export function LabHistoryList({ readings }: { readings: LabResultDto[] }) {
- void openEdit(r)}
- aria-label={t("labs.editReading")}
- >
-
-
+ {canManage && (
+ void openEdit(r)}
+ aria-label={t("labs.editReading")}
+ >
+
+
+ )}
deleteMutation.mutate(r.id)}
title={t("labs.deleteConfirmTitle")}
diff --git a/src/components/labs/lab-list.tsx b/src/components/labs/lab-list.tsx
index b96f62136..99573ec0b 100644
--- a/src/components/labs/lab-list.tsx
+++ b/src/components/labs/lab-list.tsx
@@ -1,5 +1,6 @@
"use client";
+import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { useMemo } from "react";
import Link from "next/link";
import { useQuery } from "@tanstack/react-query";
@@ -77,6 +78,7 @@ function groupReadings(results: LabResultDto[]): MarkerGroup[] {
export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {
const { t } = useTranslations();
+ const { canAdd } = useRecordCapabilities();
const { prefs } = useModuleListPrefs("labs");
// v1.22 — a short, factual line under each marker heading describing what the
@@ -167,7 +169,7 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {
title={t("labs.emptyTitle")}
description={t("labs.emptyDescription")}
action={
- onAddFirst ? (
+ onAddFirst && canAdd ? (
{t("labs.addFirst")}
) : undefined
}
diff --git a/src/components/measurement-reminders/vorsorge-section.tsx b/src/components/measurement-reminders/vorsorge-section.tsx
index 941f66f29..3f0fa6413 100644
--- a/src/components/measurement-reminders/vorsorge-section.tsx
+++ b/src/components/measurement-reminders/vorsorge-section.tsx
@@ -28,6 +28,7 @@
* hosting the per-reminder enable/disable toggles (moved off the
* card so the card carries a single med-style kebab).
*/
+import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
@@ -219,6 +220,7 @@ export function VorsorgeSection({
variant?: "settings" | "page";
}) {
const { t } = useTranslations();
+ const { canManage } = useRecordCapabilities();
const router = useRouter();
const {
data: reminders,
@@ -359,7 +361,11 @@ export function VorsorgeSection({
// v1.18.6 (MOD-02) — the primary add affordance reads "hinzufügen" like
// every other module's add button.
- const addButton = (
+ // v1.36.x — a checkup reminder belongs to the person whose schedule it is,
+ // and no grant level admits creating, editing or satisfying one. Inside
+ // somebody else's record the schedule stays readable and every control that
+ // would change it is absent.
+ const addButton = canManage ? (
{t("common.add")}
- );
+ ) : null;
// v1.18.6 (MOD-01) — the wrench links to the Vorsorge settings page (view,
// reorder, per-reminder enable toggles), left of the Add button — the
// canonical medication-page pattern. The old kebab "Anpassen" sheet is
// gone (MOD-07); its toggles moved to the settings page.
- const wrenchButton = (
+ const wrenchButton = canManage ? (
- );
+ ) : null;
return (
@@ -709,9 +715,11 @@ export function VorsorgeSection({
title={t("measurementReminders.empty.title")}
description={t("measurementReminders.empty.description")}
action={
-
- {t("common.add")}
-
+ canManage ? (
+
+ {t("common.add")}
+
+ ) : undefined
}
/>
)}
@@ -787,6 +795,7 @@ function VorsorgeCard({
}) {
const { t } = useTranslations();
const fmt = useFormatters();
+ const { canManage } = useRecordCapabilities();
// Issue #490 — day-boundary zone for the relative "today / yesterday"
// bucket must match the zone `fmt.date` renders in (mirror → Berlin).
// The next-due phrase reads off the SAME zone as the last-done date below
@@ -855,7 +864,7 @@ function VorsorgeCard({
// Single med-style kebab: Edit + Delete. The Delete item is the shared
// confirm-on-delete control rendered as a full-width menu row.
- const headerActions = (
+ const headerActions = canManage ? (
- );
+ ) : null;
// The measure / mark-done action. One constant appearance in every state —
// no due-driven tint, no label swap (v1.27.5 retired the former green
// "do it now" wash; a button that changes colour by schedule state reads as
// a different control). Due-ness is communicated by the discreet coloured
// next-due text above.
- const primaryButton = (
+ const primaryButton = canManage ? (
)}
- );
+ ) : null;
const deleteDialog = (
@@ -1114,7 +1123,9 @@ function VorsorgeCard({
)}
{/* Bottom-pinned single primary action — green when due (MOD-06). */}
-
{primaryButton}
+ {primaryButton ? (
+
{primaryButton}
+ ) : null}
diff --git a/src/components/measurements/measurement-list.tsx b/src/components/measurements/measurement-list.tsx
index 3af1468de..912e9c5d7 100644
--- a/src/components/measurements/measurement-list.tsx
+++ b/src/components/measurements/measurement-list.tsx
@@ -1,8 +1,10 @@
"use client";
+import { accountLabel } from "@/lib/sharing/account-access-view";
import { useRouter } from "next/navigation";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
+import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { useIsMobile } from "@/hooks/use-is-mobile";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@@ -283,7 +285,20 @@ export function MeasurementList({
}: MeasurementListProps) {
const { t } = useTranslations();
const fmt = useFormatters();
- const { isAuthenticated } = useAuth();
+ const { isAuthenticated, user } = useAuth();
+ // v1.36.x — inside somebody else's record, only the add path survives:
+ // entering a reading is an admitted delegated write, editing and deleting
+ // one are not. The row controls are dropped rather than disabled.
+ const { canAdd, canManage, inSharedRecord, canWrite } =
+ useRecordCapabilities();
+ const activeRecord = user?.accountAccess?.active ?? null;
+ // Named only when the sentence below is owed: a delegate who may add but
+ // not change. In the caller's own record, and for a read-only delegate who
+ // was offered no add button to be surprised by, there is nothing to explain.
+ const delegateName =
+ inSharedRecord && canWrite && activeRecord
+ ? accountLabel(activeRecord)
+ : null;
const unitDisplay = useUnitDisplay();
// v1.32.26 — a single-reading row renders its value + unit in the user's
// preferred unit (kg↔lb, cm↔in, °C↔°F). A canonical row converts through the
@@ -928,38 +943,58 @@ export function MeasurementList({
// an outage never reads as "you have no measurements".
void refetch()} />
) : !data?.measurements?.length ? (
- // v1.4.15 phase-C5: replaces the bare-text empty rectangle.
- // Filter-aware copy distinguishes brand-new accounts ("no
- // measurements yet") from filtered-out lists ("no measurements
- // match this filter") and exposes the right CTA for each
- // case.
- }
- title={
- typeFilter === "ALL" || lockedType
- ? t("measurements.emptyTitle")
- : t("measurements.emptyFilteredTitle")
- }
- description={
- typeFilter === "ALL" || lockedType
- ? t("measurements.emptyDescription")
- : t("measurements.emptyFilteredDescription")
- }
- action={
- // v1.8.5 — a locked-type list has no "reset filter" path;
- // the metric is fixed by the route.
- typeFilter !== "ALL" && !lockedType ? (
- setTypeFilter("ALL")}>
- {t("measurements.emptyResetFilter")}
-
- ) : onAddFirst ? (
-
-
- {t("measurements.emptyAddFirst")}
-
- ) : undefined
- }
- />
+ <>
+ {/* v1.4.15 phase-C5: replaces the bare-text empty rectangle.
+ Filter-aware copy distinguishes brand-new accounts ("no
+ measurements yet") from filtered-out lists ("no measurements
+ match this filter") and exposes the right CTA for each case. */}
+ }
+ title={
+ typeFilter === "ALL" || lockedType
+ ? t("measurements.emptyTitle")
+ : t("measurements.emptyFilteredTitle")
+ }
+ description={
+ typeFilter === "ALL" || lockedType
+ ? t("measurements.emptyDescription")
+ : t("measurements.emptyFilteredDescription")
+ }
+ action={
+ // v1.8.5 — a locked-type list has no "reset filter" path;
+ // the metric is fixed by the route.
+ typeFilter !== "ALL" && !lockedType ? (
+ setTypeFilter("ALL")}
+ >
+ {t("measurements.emptyResetFilter")}
+
+ ) : onAddFirst && canAdd ? (
+
+
+ {t("measurements.emptyAddFirst")}
+
+ ) : undefined
+ }
+ />
+ {/* The one place in the app the asymmetry is spelled out. A
+ delegate who may add will look for the edit control beside the
+ row they just entered, so the list that carries the add button
+ says once, in one muted line, where the limit is. Every other
+ hidden control stays silent: the banner already names the mode,
+ and a chip beside each absent button would be noise. */}
+ {delegateName && (
+
+ )}
+ >
) : (
<>
{/* Desktop table — rendered only when not mobile (v1.28.42 H3). */}
@@ -978,19 +1013,23 @@ export function MeasurementList({
- {/* v1.15.13 — select-all-on-page header checkbox. */}
-
+ {/* v1.15.13 — select-all-on-page header checkbox.
+ v1.36.x — bulk delete is the owner's alone, so a
+ delegate gets no selection column. */}
+ {canManage && (
+
+ )}
{t("measurements.type")}
@@ -1051,7 +1090,7 @@ export function MeasurementList({
{/* Grouped/synthetic rows aren't individually
deletable, so they have no checkbox. */}
- {!isGrouped && (
+ {!isGrouped && canManage && (
onToggleRow(m.id)}
@@ -1178,15 +1217,17 @@ export function MeasurementList({
) : (
<>
- startEdit(m)}
- aria-label={t("common.edit")}
- >
-
-
+ {canManage && (
+ startEdit(m)}
+ aria-label={t("common.edit")}
+ >
+
+
+ )}
deleteMutation.mutate(m.id)
@@ -1247,7 +1288,7 @@ export function MeasurementList({
{/* v1.15.13 — multi-select checkbox in a 44px tap
target; absent for synthetic grouped rows. */}
- {!isGrouped && (
+ {!isGrouped && canManage && (
// v1.15.13 MEDIUM-1 kept the 16px Radix Checkbox
// (itself a ``) inside a
// 44px wrapper `` for WCAG 2.5.5 — but a
@@ -1375,14 +1416,16 @@ export function MeasurementList({
) : (
<>
- startEdit(m)}
- aria-label={t("common.edit")}
- >
-
-
+ {canManage && (
+ startEdit(m)}
+ aria-label={t("common.edit")}
+ >
+
+
+ )}
deleteMutation.mutate(m.id)}
diff --git a/src/components/mental-health/instrument-card.tsx b/src/components/mental-health/instrument-card.tsx
index 82642056b..4c3850b3a 100644
--- a/src/components/mental-health/instrument-card.tsx
+++ b/src/components/mental-health/instrument-card.tsx
@@ -39,6 +39,7 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
+import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { MedicationCardHeader } from "@/components/medications/medication-card-header";
import {
useFormatters,
@@ -71,6 +72,10 @@ export function InstrumentCard({
}) {
const { t, locale } = useTranslations();
const { date: formatDate } = useFormatters();
+ // v1.36.x — a screener is answered by the person it is about. No grant
+ // level admits it, so inside somebody else's record the card reads as
+ // history and carries no way to start one.
+ const { canManage } = useRecordCapabilities();
// Issue #490 — day-boundary zone for the relative "today / yesterday"
// bucket must match the zone `formatDate` renders in (mirror → Berlin).
const displayTz = useDisplayTimezone();
@@ -173,11 +178,13 @@ export function InstrumentCard({
{/* Bottom-pinned single primary action — the med-/Vorsorge card slot.
Nothing sits below it now: the attribution moved to the header's
info control, so the Start button stands alone with clean space. */}
-
-
- {t("mentalHealth.start")}
-
-
+ {canManage && (
+
+
+ {t("mentalHealth.start")}
+
+
+ )}
);
diff --git a/src/components/mental-health/instrument-detail.tsx b/src/components/mental-health/instrument-detail.tsx
index 385f2b40f..077309011 100644
--- a/src/components/mental-health/instrument-detail.tsx
+++ b/src/components/mental-health/instrument-detail.tsx
@@ -10,6 +10,7 @@
* landing stays a quiet card grid, so the trend is opt-in behind a
* deliberate click, never pushed at the moment someone arrives to check in.
*/
+import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { Button } from "@/components/ui/button";
import {
useFormatters,
@@ -35,6 +36,8 @@ export function InstrumentDetail({
}) {
const { t } = useTranslations();
const { date: formatDate } = useFormatters();
+ // v1.36.x — see the card: starting a screener is not a delegated verb.
+ const { canManage } = useRecordCapabilities();
// Issue #490 — day-boundary zone for the relative "today / yesterday"
// bucket must match the zone `formatDate` renders in (mirror → Berlin).
const displayTz = useDisplayTimezone();
@@ -73,14 +76,16 @@ export function InstrumentDetail({
{/* The Start action stays reachable here too — the detail is a calm
reading surface, not a dead end. */}
-
- {t("mentalHealth.start")}
-
+ {canManage && (
+
+ {t("mentalHealth.start")}
+
+ )}
diff --git a/src/components/mood/mood-list.tsx b/src/components/mood/mood-list.tsx
index 554b15cf3..7cd920d1c 100644
--- a/src/components/mood/mood-list.tsx
+++ b/src/components/mood/mood-list.tsx
@@ -2,6 +2,7 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
+import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { EmptyState } from "@/components/ui/empty-state";
@@ -139,6 +140,9 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
const { t } = useTranslations();
const fmt = useFormatters();
const { isAuthenticated } = useAuth();
+ // v1.36.x — mood is not a delegated verb. Inside somebody else's record the
+ // list stays readable and every control that would change it is absent.
+ const { canManage } = useRecordCapabilities();
const queryClient = useQueryClient();
const [moodFilter, setMoodFilterRaw] = useState("ALL");
// v1.15.13 — management-list source filter + optional date range.
@@ -618,7 +622,7 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
>
{t("mood.emptyResetFilter")}
- ) : onAddFirst ? (
+ ) : onAddFirst && canManage ? (
{t("mood.emptyAddFirst")}
@@ -644,18 +648,20 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
{/* v1.15.13 — select-all-on-page header checkbox. */}
-
+ {canManage && (
+
+ )}
- onToggleRow(entry.id)}
- aria-label={t("dataList.selectRow")}
- />
+ {canManage && (
+ onToggleRow(entry.id)}
+ aria-label={t("dataList.selectRow")}
+ />
+ )}
{entry.score}{" "}
@@ -737,15 +745,17 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
- startEdit(entry)}
- aria-label={t("common.edit")}
- >
-
-
+ {canManage && (
+ startEdit(entry)}
+ aria-label={t("common.edit")}
+ >
+
+
+ )}
deleteMutation.mutate(entry.id)}
title={t("mood.deleteConfirmTitle")}
@@ -789,14 +799,16 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
stays the single control and owns the 44px hit area
via an `after` hit-slop (clicks on a pseudo-element
hit-test against its host button). */}
-