From 2914832ed87a4c3c94942536a2348043cbeca889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 21 Oct 2026 12:38:08 +0200 Subject: [PATCH] fix(visits): keep the appointment rail action's wire tokens for the shipped app The shipped iOS app routes the rail by intent and resolves the label from its own catalogue; a new `visit.view` intent would leave its tap on an appointment inert. The upcoming-visit action keeps `checkup.view` and `daily.action.viewCheckups` on the wire and only its `href` moves to the visit. The web card names what that link opens: "View appointment", or "View appointments" for the visits list. --- .../daily/__tests__/priority-card.test.tsx | 31 +++++++++++++++++++ src/components/daily/priority-card.tsx | 21 ++++++++++++- src/components/daily/today-hero.tsx | 4 +-- src/lib/daily/__tests__/digest.test.ts | 17 +++++----- src/lib/daily/digest.ts | 20 ++++++------ 5 files changed, 73 insertions(+), 20 deletions(-) diff --git a/src/components/daily/__tests__/priority-card.test.tsx b/src/components/daily/__tests__/priority-card.test.tsx index c3a6a5df0..1aff76415 100644 --- a/src/components/daily/__tests__/priority-card.test.tsx +++ b/src/components/daily/__tests__/priority-card.test.tsx @@ -161,6 +161,37 @@ describe("", () => { expect(html).toContain("View check-ups"); }); + // An appointment keeps the check-up label on the wire for the shipped iOS + // app; the web names what the link opens. The check-up item above is the + // negative control. + it("names an appointment's action for the visit it opens", () => { + const action = (href: string) => ({ + labelKey: "daily.action.viewCheckups", + intent: "checkup.view", + href, + }); + const one = render( + , + ); + expect(one).toContain("View appointment"); + expect(one).not.toContain("View check-ups"); + expect(one).toContain('href="/checkups?visit=v1"'); + const several = render( + , + ); + expect(several).toContain("View appointments"); + }); + describe("dismiss affordance — observational kinds only", () => { const DISMISSIBLE: PriorityItemKind[] = [ "milestone", diff --git a/src/components/daily/priority-card.tsx b/src/components/daily/priority-card.tsx index 6f47ec2c1..5d1d5962d 100644 --- a/src/components/daily/priority-card.tsx +++ b/src/components/daily/priority-card.tsx @@ -36,6 +36,7 @@ import { Card, CardContent } from "@/components/ui/card"; import { TileHeader } from "@/components/insights/tile-header"; import { ProseBlocks } from "@/components/insights/prose-blocks"; import { Button, buttonVariants } from "@/components/ui/button"; +import { VISITS_PATH } from "@/lib/encounters/visit-links"; import { useTranslations } from "@/lib/i18n/context"; import { cn } from "@/lib/utils"; import { @@ -205,7 +206,7 @@ export function PriorityCard({ ) : onAction ? ( , + action: Pick, +): string { + if (item.kind !== "upcoming_visit" || !action.href) return action.labelKey; + return action.href === VISITS_PATH + ? "daily.action.viewAppointments" + : "daily.action.viewAppointment"; +} + function ActionLink({ href, label }: { href: string; label: string }) { return ( ` to its existing destination * by construction; S2 invents no new backend action. * diff --git a/src/lib/daily/__tests__/digest.test.ts b/src/lib/daily/__tests__/digest.test.ts index 01ce5ce76..33263b9ca 100644 --- a/src/lib/daily/__tests__/digest.test.ts +++ b/src/lib/daily/__tests__/digest.test.ts @@ -1507,16 +1507,17 @@ describe("buildDailyDigest — due check-ups and today's visits hold their place ).toEqual(["Dr. Weiss today", "Dentist tomorrow"]); }); - // The tap opens where the appointment lives. The item used to borrow the - // check-up item's action whole (label, intent and the bare `/checkups`), - // which opens the preventive-care tab an appointment is never on. - it("opens the visit itself, with its own label and intent", () => { + // The tap opens where the appointment lives: the bare `/checkups` it carried + // opens the preventive-care tab an appointment is never on. Label and intent + // stay the check-up item's on the wire, for the shipped iOS app; the web + // card relabels (`priority-card.test.tsx`). + it("opens the visit itself, keeping the intent the app routes", () => { const d = buildDailyDigest(input({ upcomingVisits: [visit()] }), t); const item = d.worthALook.find((i) => i.kind === "upcoming_visit"); expect(item?.actions).toEqual([ { - labelKey: "daily.action.viewAppointment", - intent: "visit.view", + labelKey: "daily.action.viewCheckups", + intent: "checkup.view", href: "/checkups?visit=v1", }, ]); @@ -1535,8 +1536,8 @@ describe("buildDailyDigest — due check-ups and today's visits hold their place const item = d.worthALook.find((i) => i.kind === "upcoming_visit"); expect(item?.actions).toEqual([ { - labelKey: "daily.action.viewAppointments", - intent: "visit.view", + labelKey: "daily.action.viewCheckups", + intent: "checkup.view", href: "/checkups?view=visits", }, ]); diff --git a/src/lib/daily/digest.ts b/src/lib/daily/digest.ts index 9f7222a57..5d837ba28 100644 --- a/src/lib/daily/digest.ts +++ b/src/lib/daily/digest.ts @@ -681,10 +681,9 @@ function buildPreventiveCareItem( * visit's own status field with a second place to set it. * * The action opens where the appointment lives: the visit itself, or the - * visits list for a day with several. It is `visit.view` and not the checkup - * item's `checkup.view`: the two kinds used to share that intent, its label - * and its bare `/checkups`, which sent every tap on an appointment to the - * preventive-care list the appointment is not on. + * visits list for a day with several. The bare `/checkups` it used to carry + * sent every tap on an appointment to the preventive-care list the + * appointment is not on. */ function buildUpcomingVisitItems( visits: readonly DailyDigestUpcomingVisit[], @@ -713,11 +712,14 @@ function buildUpcomingVisitItems( status: "info", actions: [ { - labelKey: - group.length === 1 - ? "daily.action.viewAppointment" - : "daily.action.viewAppointments", - intent: "visit.view", + // The wire keeps the check-up item's label and intent: the shipped + // iOS app routes `checkup.view` and knows no other token for this + // item, so a new intent would leave its tap inert. The web card + // relabels it from the kind (`actionLabelKey` in + // `priority-card.tsx`); the switch to `visit.view` waits for an + // app that understands it. + labelKey: "daily.action.viewCheckups", + intent: "checkup.view", href: visitsPath(group.map((visit) => visit.id)), }, ],