From 91ad5ec1155202ec58845c3956505419921ffe33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 10 May 2026 17:42:53 +0200 Subject: [PATCH] feat(insights): mount Health Score panel in hero strip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hero strip's right side, empty by design since B1, now paints the when `analytics?.healthScore` is non-null. Layout flips from `flex-col` to `flex-row` on `lg+` so the panel sits beside the title block on desktop and stacks below on mobile. The `onAskCoach` prop signature picks up an optional `prefill` argument so the same handler powers both surfaces — the action-row button passes nothing (drawer opens blank), the score panel passes "Why is my health score X out of 100?". One drawer instance, one state pair, one consistent UX. Tests: 5 new hero-strip cases pin the score panel slot, the lg-row layout switch, and the conditional ask-coach button mounting. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/insights/page.tsx | 9 +- .../insights/__tests__/hero-strip.test.tsx | 98 +++++++++++++++++++ src/components/insights/hero-strip.tsx | 60 +++++++++++- 3 files changed, 162 insertions(+), 5 deletions(-) diff --git a/src/app/insights/page.tsx b/src/app/insights/page.tsx index f717afa7c..50017c932 100644 --- a/src/app/insights/page.tsx +++ b/src/app/insights/page.tsx @@ -942,8 +942,12 @@ export default function InsightsPage() { userName={heroGreetingName} onRegenerate={advisor.regenerate} regenerating={advisor.isRegenerating} - onAskCoach={() => { - setCoachPrefill(null); + onAskCoach={(prefill?: string) => { + // The action-row button passes no prefill (drawer opens blank); + // the Health Score panel passes a score-aware question. Both + // share the same drawer state so the user only ever sees one + // drawer instance. + setCoachPrefill(prefill ?? null); setCoachOpen(true); }} onPickPrompt={(prompt) => { @@ -951,6 +955,7 @@ export default function InsightsPage() { setCoachOpen(true); }} weeklyReportReady={weeklyReportReady} + healthScore={analytics?.healthScore ?? undefined} /> ", () => { expect(html).toContain("Lesen"); expect(html).toContain("Als PDF"); }); + + // ── B5 — Health Score panel ──────────────────────────────────────── + it("does NOT render the Health Score panel when healthScore is omitted", () => { + const html = render(); + expect(html).not.toContain('data-slot="health-score-card"'); + }); + + it("renders the Health Score panel when the score is supplied", () => { + const html = render( + , + ); + expect(html).toMatch(/data-slot="health-score-card"/); + expect(html).toMatch(/data-band="green"/); + expect(html).toContain(">86<"); + }); + + it("forwards onAskCoach into the Health Score panel only when both are supplied", () => { + const html = render( + {}} + />, + ); + // HSC panel renders its own Ask-the-Coach button with the score- + // aware label (the action-row button uses the shorter "Ask the + // coach" copy). The HSC button mirrors the panel's slot. + expect(html).toMatch(/data-slot="health-score-card-ask-coach"/); + }); + + it("hides the Health Score panel's Ask button when no onAskCoach is supplied", () => { + const html = render( + , + ); + expect(html).not.toContain('data-slot="health-score-card-ask-coach"'); + }); + + it("uses the lg row layout when healthScore is supplied (smoke test on container class)", () => { + const html = render( + , + ); + // The wrapper picks up the `lg:flex-row` modifier when the score + // is present so the panel sits beside the title block on desktop. + expect(html).toContain("lg:flex-row"); + }); }); diff --git a/src/components/insights/hero-strip.tsx b/src/components/insights/hero-strip.tsx index d9634fe12..04c140695 100644 --- a/src/components/insights/hero-strip.tsx +++ b/src/components/insights/hero-strip.tsx @@ -14,6 +14,10 @@ import { Button } from "@/components/ui/button"; import { useTranslations } from "@/lib/i18n/context"; import { cn } from "@/lib/utils"; import { SuggestedPrompts } from "./suggested-prompts"; +import { + HealthScoreCard, + type HealthScoreCardProps, +} from "./health-score-card"; import type { DailyBriefing as DailyBriefingPayload } from "@/lib/ai/schema"; /** @@ -67,8 +71,14 @@ interface HeroStripProps { * the button is enabled and the coming-soon tooltip drops; clicking * opens the drawer (B2b). When omitted the button stays disabled — * see the v1.4.20 phase B1 dispatch where the drawer was deferred. + * + * v1.4.20 phase B5 — the same handler powers the Health Score + * panel's "Ask the Coach" button, with an optional `prefill` string + * that opens the drawer with a score-aware question. The action-row + * button calls it without an argument (drawer opens blank); the HSC + * panel calls it with "Why is my health score X out of 100?". */ - onAskCoach?: () => void; + onAskCoach?: (prefill?: string) => void; /** * v1.4.20 phase B4 — when the cached AI payload carries a fresh * weeklyReport block, the parent passes this so the hero paints a @@ -84,6 +94,20 @@ interface HeroStripProps { * Defaults to `new Date()`. Production callers omit this. */ now?: Date; + /** + * v1.4.20 phase B5 — Personal Health Score panel data. When supplied + * the right side of the hero band paints the score card. The + * `onAskCoach` handler is intentionally re-used from the action-row + * "Ask the coach" button — same drawer state, different prefill + * string. When the parent omits the field the right side stays empty + * (current B1 behaviour). + */ + healthScore?: { + score: HealthScoreCardProps["score"]; + band: HealthScoreCardProps["band"]; + components: HealthScoreCardProps["components"]; + delta: HealthScoreCardProps["delta"]; + } | null; } /** @@ -133,6 +157,7 @@ export function HeroStrip({ onAskCoach, weeklyReportReady, now, + healthScore, }: HeroStripProps) { const { t } = useTranslations(); const greetingKey = resolveGreetingKey(now ?? new Date()); @@ -152,7 +177,21 @@ export function HeroStrip({ "relative overflow-hidden rounded-xl px-4 py-5 sm:px-6 sm:py-6", )} > -
+ {/* + * v1.4.20 phase B5 — split layout. On `lg+` the title block sits + * left, the Health Score panel sits right. On ` +
onAskCoach() : undefined} disabled={!onAskCoach} title={onAskCoach ? undefined : comingSoon} data-slot="insights-hero-strip-action-coach" @@ -272,6 +311,21 @@ export function HeroStrip({ onPick={onPickPrompt ?? (() => undefined)} />
+
+ + {healthScore && ( + onAskCoach(prefill) + : undefined + } + /> + )}
);