mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
feat(dashboard): show today's dose tally on the medication hero ring
The MED_COMPLIANCE hero ring carried the pooled 7-day adherence percentage — a mystery number next to today-focused scores. It now answers the question the hero poses: how many of today's doses are done. The resolver reads takenToday/scheduledToday off the snapshot's own medsToday block (one shared builder call, no second tally); score stays the rounded 0..100 progress for wire compatibility and a new additive doses field carries the taken/scheduled pair. No dose scheduled today means no ring, and the band is progress semantics — green once everything is taken, yellow while doses remain, never red (a pending morning dose is not an alert state). The web ring renders the tally (1/3) with a doses label over the ring system's constant green arc — the same green family the compliance surfaces in Insights paint — instead of a band gradient; the derived rings keep their exact insights hues. ScoreRing gains documented valueText/ariaLabel overrides for tally-shaped centres. OpenAPI regenerated; labels land in all six locales.
This commit is contained in:
1 parent
9b4f3ce56d
commit
47d638cf02
16 files changed
+236
-287
No files matched your search
@@ -375,7 +375,12 @@ describe("<DashboardHero> — ring row (v1.27.7)", () => {
|
||||
healthScore: { score: 82, band: "green", delta: 2 },
|
||||
scoreRings: [
|
||||
{ id: "READINESS", score: 71, band: "green" },
|
||||
{ id: "MED_COMPLIANCE", score: 95, band: "green" },
|
||||
{
|
||||
id: "MED_COMPLIANCE",
|
||||
score: 33,
|
||||
band: "yellow",
|
||||
doses: { taken: 1, scheduled: 3 },
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -389,9 +394,14 @@ describe("<DashboardHero> — ring row (v1.27.7)", () => {
|
||||
const rings = html.match(/data-slot="score-ring"/g) ?? [];
|
||||
expect(rings).toHaveLength(3);
|
||||
// Labels (de locale): the derived ring reuses its wellness-strip
|
||||
// title, the adherence ring the medications adherence label.
|
||||
// title; the dose ring names today's tally and shows it as
|
||||
// taken/scheduled over the constant green arc — never the band
|
||||
// gradient (a pending morning dose is not an alert state).
|
||||
expect(html).toContain("Bereitschaft");
|
||||
expect(html).toContain("Therapietreue");
|
||||
expect(html).toContain("Dosen heute");
|
||||
expect(html).toContain(">1/3<");
|
||||
expect(html).toContain("var(--ring-green-from)");
|
||||
expect(html).not.toContain("var(--ring-yellow-from)");
|
||||
});
|
||||
|
||||
it("no dose text row renders anymore — the ring row carries the slot", () => {
|
||||
|
||||
@@ -99,10 +99,12 @@ const CTA_LABEL_KEY: Partial<Record<DashboardVerdictVariant, string>> = {
|
||||
|
||||
/**
|
||||
* Per-ring hue for the selected score rings — the wellness-strip
|
||||
* vocabulary. MED_COMPLIANCE deliberately carries NO hue: the adherence
|
||||
* ring falls back to the ring's band gradient (green / yellow / red
|
||||
* semantic tokens), because for adherence the band IS the message —
|
||||
* exactly the classification colouring the targets tile uses.
|
||||
* vocabulary, so every hero ring paints EXACTLY the hue its insights
|
||||
* sibling paints. MED_COMPLIANCE carries no per-metric hue entry: the
|
||||
* dose ring pins the ring system's green arc (`band="green"`, constant)
|
||||
* — the same green family the medication compliance surfaces in
|
||||
* Insights paint for a taken dose — instead of a band gradient that
|
||||
* would flash yellow/red over pending morning doses.
|
||||
*/
|
||||
const RING_HUE_BY_ID: Partial<Record<ScoreRingId, RingHue>> = {
|
||||
READINESS: "readiness",
|
||||
@@ -110,12 +112,13 @@ const RING_HUE_BY_ID: Partial<Record<ScoreRingId, RingHue>> = {
|
||||
SLEEP_SCORE: "sleep",
|
||||
};
|
||||
|
||||
/** Per-ring label key — reuses the existing score / adherence labels. */
|
||||
/** Per-ring label key — reuses the existing score labels; the dose ring
|
||||
* names today's tally. */
|
||||
const RING_LABEL_KEY: Record<ScoreRingId, string> = {
|
||||
READINESS: "insights.derived.composite.READINESS.title",
|
||||
RECOVERY_SCORE: "insights.derived.scores.recovery",
|
||||
SLEEP_SCORE: "insights.derived.composite.SLEEP_SCORE.title",
|
||||
MED_COMPLIANCE: "medications.compliance",
|
||||
MED_COMPLIANCE: "dashboard.hero.ringDoses",
|
||||
};
|
||||
|
||||
export function DashboardHero({
|
||||
@@ -295,14 +298,33 @@ export function DashboardHero({
|
||||
data-ring={ring.id}
|
||||
className="flex shrink-0 items-center"
|
||||
>
|
||||
<ScoreRing
|
||||
score={ring.score}
|
||||
band={ring.band}
|
||||
size="sm"
|
||||
flat
|
||||
hue={RING_HUE_BY_ID[ring.id]}
|
||||
label={t(RING_LABEL_KEY[ring.id])}
|
||||
/>
|
||||
{/* Dose ring — today's tally ("1/3") over the constant
|
||||
green arc; the arc still sweeps on the 0..100
|
||||
progress `score`. Falls through to the score render
|
||||
when a cached pre-doses snapshot carries no tally. */}
|
||||
{ring.id === "MED_COMPLIANCE" && ring.doses ? (
|
||||
<ScoreRing
|
||||
score={ring.score}
|
||||
band="green"
|
||||
valueText={`${ring.doses.taken}/${ring.doses.scheduled}`}
|
||||
ariaLabel={t("dashboard.hero.ringDosesAria", {
|
||||
taken: ring.doses.taken,
|
||||
scheduled: ring.doses.scheduled,
|
||||
})}
|
||||
size="sm"
|
||||
flat
|
||||
label={t(RING_LABEL_KEY[ring.id])}
|
||||
/>
|
||||
) : (
|
||||
<ScoreRing
|
||||
score={ring.score}
|
||||
band={ring.band}
|
||||
size="sm"
|
||||
flat
|
||||
hue={RING_HUE_BY_ID[ring.id]}
|
||||
label={t(RING_LABEL_KEY[ring.id])}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<ScoreRing
|
||||
|
||||
@@ -103,6 +103,16 @@ export interface ScoreRingProps {
|
||||
* + tests don't break.
|
||||
*/
|
||||
variant?: "band" | "onGradient";
|
||||
/**
|
||||
* v1.27.x — optional centre-text override for rings whose number is a
|
||||
* tally rather than a score (the hero dose ring's "1/3"). The arc still
|
||||
* sweeps on `score` (the 0..100 progress); only the displayed text
|
||||
* changes, and the count-up is skipped. Pair with `ariaLabel` — the
|
||||
* default announcement reads score + band, which is wrong for a tally.
|
||||
*/
|
||||
valueText?: string;
|
||||
/** Optional aria-label override (see `valueText`). */
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -116,6 +126,8 @@ export function ScoreRing({
|
||||
delayMs = 0,
|
||||
baseline,
|
||||
flat = false,
|
||||
valueText,
|
||||
ariaLabel: ariaLabelOverride,
|
||||
className,
|
||||
}: ScoreRingProps) {
|
||||
const { t } = useTranslations();
|
||||
@@ -162,12 +174,14 @@ export function ScoreRing({
|
||||
});
|
||||
const displayedRounded = Math.round(displayed);
|
||||
|
||||
const ariaLabel = hasScore
|
||||
? t("insights.derived.scoreRing.aria", {
|
||||
score: Math.round(clamped),
|
||||
band: t(`insights.derived.scoreRing.band.${resolvedBand}`),
|
||||
})
|
||||
: t("insights.derived.scoreRing.ariaProvisional");
|
||||
const ariaLabel =
|
||||
ariaLabelOverride ??
|
||||
(hasScore
|
||||
? t("insights.derived.scoreRing.aria", {
|
||||
score: Math.round(clamped),
|
||||
band: t(`insights.derived.scoreRing.band.${resolvedBand}`),
|
||||
})
|
||||
: t("insights.derived.scoreRing.ariaProvisional"));
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -259,7 +273,7 @@ export function ScoreRing({
|
||||
hasScore ? "text-foreground" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{hasScore ? displayedRounded : "—"}
|
||||
{hasScore ? (valueText ?? displayedRounded) : "—"}
|
||||
</span>
|
||||
{label ? (
|
||||
<span className={cn("text-muted-foreground", dims.labelClass)}>
|
||||
|
||||
@@ -158,15 +158,15 @@ const WIDGET_LABEL_KEYS: Record<DashboardWidgetId, string> = {
|
||||
|
||||
/**
|
||||
* v1.27.7 — option labels for the hero score-ring picker. The three
|
||||
* derived rings reuse their wellness-strip titles; the adherence ring
|
||||
* reuses the existing "adherence (7 days)" label so the picker names
|
||||
* the exact window the ring shows.
|
||||
* derived rings reuse their wellness-strip titles; the dose ring names
|
||||
* today's tally (the exact thing the ring shows since it moved off the
|
||||
* 7-day adherence percentage).
|
||||
*/
|
||||
const SCORE_RING_LABEL_KEYS: Record<ScoreRingId, string> = {
|
||||
READINESS: "insights.derived.composite.READINESS.title",
|
||||
RECOVERY_SCORE: "insights.derived.scores.recovery",
|
||||
SLEEP_SCORE: "insights.derived.composite.SLEEP_SCORE.title",
|
||||
MED_COMPLIANCE: "dashboard.compliance7d",
|
||||
MED_COMPLIANCE: "dashboard.hero.ringDoses",
|
||||
};
|
||||
|
||||
export function DashboardLayoutSection({ id }: { id: string }) {
|
||||
|
||||
Reference in new issue
Block a user