mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
feat(coach): close the plan check-in loop on the Today rail
Every accepted coach plan now earns a gentle weekly check-in. Confirming a plan (proposed → active) without an explicit review date defaults one to a week out, so a plan is always reviewed, not only the coach's own experiments. The daily digest emits a single `coach_checkin` rail item when a standing plan's review has come due — its pinned review date, or a defaulted activation + 7 days for plans that predate it, or the read-back state a reviewed plan carries. Capped at one check-in a day across all plans (earliest-due wins); the rest wait for a following day. It reads existing plan state only — no fresh generation, nothing warms on mount, and it is gated on the coach module. The card offers three one-tap actions through the existing plan-lifecycle route: keep it (re-arm for another week), adjust it (open the coach), or let it go (a guilt-free, respected end — the calm inversion of a streak). An ignored check-in sits quiet and stops resurfacing after about two cycles; its status is never changed behind the user's back. Copy lands in all six locales.
This commit is contained in:
1 parent
7c96abc8ec
commit
b7a6b4c898
15 files changed
+643
-32
No files matched your search
@@ -1,14 +1,20 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { TodayHero } from "../today-hero";
|
||||
import type { DailyDigest } from "@/lib/daily/digest";
|
||||
import type { PriorityItem } from "@/lib/daily/priority-item";
|
||||
|
||||
// The hero now wires the coach check-in card's keep / let-go taps through
|
||||
// `useCoachCheckinAction`, so it needs a QueryClient in the tree.
|
||||
function render(node: React.ReactNode, locale: "en" | "de" = "en") {
|
||||
const client = new QueryClient();
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale={locale}>{node}</I18nProvider>,
|
||||
<QueryClientProvider client={client}>
|
||||
<I18nProvider initialLocale={locale}>{node}</I18nProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,9 +36,14 @@ import { ScoreRing } from "@/components/insights/derived/score-ring";
|
||||
import type { ScoreBand } from "@/components/insights/derived/band-tokens";
|
||||
import { ProseBlocks } from "@/components/insights/prose-blocks";
|
||||
import { PriorityCard } from "@/components/daily/priority-card";
|
||||
import { useCoachCheckinAction } from "@/hooks/use-coach-checkin";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { DailyDigest } from "@/lib/daily/digest";
|
||||
import {
|
||||
COACH_CHECKIN_KEEP_INTENT,
|
||||
COACH_CHECKIN_LETGO_INTENT,
|
||||
type DailyDigest,
|
||||
} from "@/lib/daily/digest";
|
||||
|
||||
/** Format the server-computed score delta as a signed, muted chip string. */
|
||||
function formatDelta(
|
||||
@@ -53,6 +58,21 @@ function formatDelta(
|
||||
|
||||
export function TodayHero({ digest }: { digest: DailyDigest }) {
|
||||
const { t } = useTranslations();
|
||||
const { keep, letGo } = useCoachCheckinAction();
|
||||
|
||||
// The coach check-in card's keep / let-go intents carry the plan id after the
|
||||
// ":" (a closed two-intent allowlist); adjust is an href handled by the card
|
||||
// as a <Link>, so it never lands here. Every other rail kind is pure
|
||||
// navigation (dose / sync / preventive), so their taps never call this.
|
||||
const handleAction = (intent: string) => {
|
||||
const keepPrefix = `${COACH_CHECKIN_KEEP_INTENT}:`;
|
||||
const letGoPrefix = `${COACH_CHECKIN_LETGO_INTENT}:`;
|
||||
if (intent.startsWith(keepPrefix)) {
|
||||
keep.mutate(intent.slice(keepPrefix.length));
|
||||
} else if (intent.startsWith(letGoPrefix)) {
|
||||
letGo.mutate(intent.slice(letGoPrefix.length));
|
||||
}
|
||||
};
|
||||
|
||||
const hasScore = digest.score !== null;
|
||||
const hasItems = digest.worthALook.length > 0;
|
||||
@@ -174,7 +194,11 @@ export function TodayHero({ digest }: { digest: DailyDigest }) {
|
||||
</h2>
|
||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{digest.worthALook.map((item, i) => (
|
||||
<PriorityCard key={`${item.kind}-${i}`} item={item} />
|
||||
<PriorityCard
|
||||
key={`${item.kind}-${i}`}
|
||||
item={item}
|
||||
onAction={handleAction}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user