fix(checkups): offer the record-a-visit action only for real visits

The file-a-visit affordance painted on every checkup reminder, including
the self-measurement ones (weigh-in, blood pressure, a screening
check-in) where there is no appointment and 'you went' is never a
reading. Gate both the card and the dense-list branch on the same
free-text predicate the mark-done action already uses: a reminder with a
measurement type is satisfied in the app, a free-text reminder stands for
a practice visit. The server states the same boundary in resolve.ts.
This commit is contained in:
Marc-André Bombeck committed 2026-08-09 06:48:46 +02:00
1 parent 4335b38e02
commit 9aefce1625
3 files changed
+101 -15

No files matched your search

@@ -277,6 +277,83 @@ describe("<VorsorgeSection> loading + empty", () => {
expect(html).toContain("text-warning");
});
it("offers the file-visit action on a free-text appointment reminder", () => {
// A free-text reminder (no measurementType) stands for a real practice
// visit: "you went" is only ever recorded by filing a visit, so the
// affordance belongs here.
const reminder: MeasurementReminder = {
id: "appointment",
label: "Dentist appointment",
measurementType: null,
intervalDays: 365,
rrule: null,
anchorDate: null,
endsOn: null,
origin: "VORSORGE",
nextDueAt: null,
notifyHour: 9,
location: null,
lastSatisfiedAt: null,
enabled: true,
createdAt: "2030-01-01T00:00:00.000Z",
updatedAt: "2030-01-01T00:00:00.000Z",
} as MeasurementReminder;
remindersMock.mockReturnValue({ data: [reminder], isLoading: false });
const html = render(<VorsorgeSection />);
expect(html).toContain('data-slot="vorsorge-file-visit"');
});
it("hides the file-visit action on a self-measurement reminder", () => {
// A typed reminder (Gewicht, Blutdruck, a screening check-in) is
// satisfied by a reading or a check-in inside the app — there is no
// appointment behind it, so the visit affordance must not appear.
const reminder: MeasurementReminder = {
id: "self-measure",
label: "Measure blood pressure",
measurementType: "BLOOD_PRESSURE_SYS",
intervalDays: 7,
rrule: null,
anchorDate: null,
endsOn: null,
origin: "VORSORGE",
nextDueAt: null,
notifyHour: 9,
location: null,
lastSatisfiedAt: null,
enabled: true,
createdAt: "2030-01-01T00:00:00.000Z",
updatedAt: "2030-01-01T00:00:00.000Z",
} as MeasurementReminder;
remindersMock.mockReturnValue({ data: [reminder], isLoading: false });
const html = render(<VorsorgeSection />);
expect(html).not.toContain('data-slot="vorsorge-file-visit"');
});
it("hides the file-visit action on a screening check-in reminder", () => {
// A screening reminder (PHQ-9 / GAD-7) is a self-administered check-in,
// not a practice visit.
const reminder: MeasurementReminder = {
id: "screening-visit",
label: "PHQ-9 check-in",
measurementType: "PHQ9_SCORE",
intervalDays: 28,
rrule: null,
anchorDate: null,
endsOn: null,
origin: "VORSORGE",
nextDueAt: null,
notifyHour: 9,
location: null,
lastSatisfiedAt: null,
enabled: true,
createdAt: "2030-01-01T00:00:00.000Z",
updatedAt: "2030-01-01T00:00:00.000Z",
} as MeasurementReminder;
remindersMock.mockReturnValue({ data: [reminder], isLoading: false });
const html = render(<VorsorgeSection />);
expect(html).not.toContain('data-slot="vorsorge-file-visit"');
});
it("translates a COACH-origin label i18n key and shows the neutral badge", () => {
const reminder: MeasurementReminder = {
id: "c1",
@@ -940,19 +940,26 @@ function VorsorgeCard({
* checkbox uses, so a person who does both gets one satisfaction and no
* error.
*/
const fileVisitButton = canManage ? (
<Button
type="button"
variant="outline"
className="min-h-11 w-full"
data-slot="vorsorge-file-visit"
onClick={onFileVisit}
disabled={busy}
>
<CalendarPlus className="h-4 w-4" />
{t("measurementReminders.fileVisit")}
</Button>
) : null;
// The file-visit action belongs only to a free-text reminder — a real
// practice visit, where "you went" is recorded by filing a visit. A typed
// reminder (Gewicht, Blutdruck, a screening check-in) is satisfied by a
// reading or a check-in inside the app, so it carries no visit affordance.
// `resolve.ts` states the same boundary server-side: an appointment reminder
// is free-text by construction.
const fileVisitButton =
canManage && !isLinked ? (
<Button
type="button"
variant="outline"
className="min-h-11 w-full"
data-slot="vorsorge-file-visit"
onClick={onFileVisit}
disabled={busy}
>
<CalendarPlus className="h-4 w-4" />
{t("measurementReminders.fileVisit")}
</Button>
) : null;
const primaryButton = canManage ? (
<Button
@@ -1047,8 +1054,9 @@ function VorsorgeCard({
) : null}
{/* Icon-only in the dense list, labelled in the cards branch —
the same action either way, never one branch offering what
the other withholds. */}
{canManage ? (
the other withholds. Free-text reminders only: a typed /
screening reminder has no practice visit behind it. */}
{canManage && !isLinked ? (
<Button
type="button"
variant="outline"