mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +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
@@ -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 };
|
||||
}
|
||||
Reference in new issue
Block a user