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:
Marc-André Bombeck committed 2026-07-16 22:30:46 +02:00
1 parent 7c96abc8ec
commit b7a6b4c898
15 files changed
+643 -32

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
"use client";
/**
* S3 — the coach check-in card's keep / let-go actions (§2.3).
*
* The Today rail surfaces a `coach_checkin` `PriorityItem` whose two mutating
* one-tap actions close the coach loop through the EXISTING plan-lifecycle
* route (`PATCH /api/coach/plans/[id]`) — no new backend surface:
* - keep → re-arm: status `active` + `reviewDate` pushed out one more cycle,
* so the check-in comes back in a week rather than the same day;
* - letGo → guilt-free retirement: status `abandoned`, a terminal, respected
* outcome (the calm inversion of a streak). The card never nags.
*
* "Adjust" is navigation (an `href` into the coach) handled by `PriorityCard`
* as a `<Link>` — it never reaches this hook.
*
* Both invalidate the daily digest (so the acted-on card leaves the rail) and
* every plans-list slot (so the /coach/plans ledger stays in step). Keys route
* through the centralised factory — no bare arrays.
*/
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiPatch } from "@/lib/api/api-fetch";
import { queryKeys } from "@/lib/query-keys";
import { COACH_CHECKIN_REVIEW_DAYS } from "@/lib/daily/digest";
export function useCoachCheckinAction() {
const queryClient = useQueryClient();
const invalidate = () => {
void queryClient.invalidateQueries({ queryKey: queryKeys.dailyDigest() });
void queryClient.invalidateQueries({ queryKey: queryKeys.coachPlansAll() });
};
const keep = useMutation({
mutationFn: async (planId: string) => {
const reviewDate = new Date(
Date.now() + COACH_CHECKIN_REVIEW_DAYS * 86_400_000,
).toISOString();
await apiPatch(`/api/coach/plans/${encodeURIComponent(planId)}`, {
status: "active",
reviewDate,
});
return planId;
},
onSuccess: invalidate,
});
const letGo = useMutation({
mutationFn: async (planId: string) => {
await apiPatch(`/api/coach/plans/${encodeURIComponent(planId)}`, {
status: "abandoned",
});
return planId;
},
onSuccess: invalidate,
});
return { keep, letGo };
}