mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
feat(vorsorge): make the mental-wellbeing screening plannable
PHQ9_SCORE / GAD7_SCORE join the Vorsorge reminder type allow-list as their own "Seelisches Wohlbefinden" group, with a 4-week default cadence (both instruments cover the last two weeks; 28 joins the interval presets). The reminders ride the existing activity-aware engine unchanged: they fire only when overdue, and a completed check-in auto-satisfies them — the assessment route now kicks the eventful reminder-satisfy worker, which resolves against the server-written score row; the 15-min cron stays the idempotent safety-net. The screening reminder's primary action opens the check-in page (a score is never typed in), and the dispatcher gates the reminder on the opt-in mental-health module so a disabled module never nags. OpenAPI spec regenerated for the widened MeasurementReminderType enum.
This commit is contained in:
1 parent
b6aedfeb61
commit
01beabb99b
16 files changed
+256
-30
No files matched your search
@@ -14,6 +14,12 @@ import type { MeasurementReminder } from "@/hooks/use-measurement-reminders";
|
||||
* `<EmptyState>` with an add action. These tests pin both.
|
||||
*/
|
||||
|
||||
// v1.27.6 — a screening reminder's primary action navigates to the
|
||||
// check-in page via the app router, which the SSR harness doesn't mount.
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ replace: vi.fn(), push: vi.fn() }),
|
||||
}));
|
||||
|
||||
const remindersMock = vi.fn();
|
||||
vi.mock("@/hooks/use-measurement-reminders", () => ({
|
||||
useMeasurementReminders: () => remindersMock(),
|
||||
@@ -136,6 +142,32 @@ describe("<VorsorgeSection> loading + empty", () => {
|
||||
expect(html).not.toContain("Log value");
|
||||
});
|
||||
|
||||
it("renders a 'Start check-in' action for a screening reminder (v1.27.6)", () => {
|
||||
// A PHQ-9 / GAD-7 reminder routes to the check-in page — the score is
|
||||
// never typed in, so the numeric "Measure now" wording would mislead.
|
||||
const reminder: MeasurementReminder = {
|
||||
id: "screening",
|
||||
label: "PHQ-9 check-in",
|
||||
measurementType: "PHQ9_SCORE",
|
||||
intervalDays: 28,
|
||||
rrule: null,
|
||||
anchorDate: null,
|
||||
endsOn: null,
|
||||
origin: "VORSORGE",
|
||||
nextDueAt: null,
|
||||
notifyHour: 9,
|
||||
location: null,
|
||||
lastSatisfiedAt: null,
|
||||
enabled: true,
|
||||
createdAt: "2030-01-01T00:00:00.000Z",
|
||||
updatedAt: "2030-01-01T00:00:00.000Z",
|
||||
} as MeasurementReminder;
|
||||
remindersMock.mockReturnValue({ data: [reminder], isLoading: false });
|
||||
const html = render(<VorsorgeSection />);
|
||||
expect(html).toContain("Start check-in");
|
||||
expect(html).not.toContain("Measure now");
|
||||
});
|
||||
|
||||
it("reads a same-day due time as 'today' (calendar-day delta, not rolling 24h)", () => {
|
||||
// v1.18.9 recon — a reminder whose nextDue falls on the SAME local
|
||||
// calendar day must read "Due today" with the green status hue, even when
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { CalendarClock, CheckCircle2, Plus } from "lucide-react";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
@@ -26,6 +27,7 @@ import { ListRow } from "@/components/ui/list-row";
|
||||
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { MeasurementForm } from "@/components/measurements/measurement-form";
|
||||
import { isScreeningReminderType } from "@/lib/validations/measurement-reminders";
|
||||
import {
|
||||
useMeasurementReminders,
|
||||
useMeasurementReminderMutations,
|
||||
@@ -60,6 +62,7 @@ function resolveLabel(
|
||||
|
||||
export function VorsorgeDashboardCard() {
|
||||
const { t } = useTranslations();
|
||||
const router = useRouter();
|
||||
const { data: reminders, isLoading } = useMeasurementReminders();
|
||||
const { satisfy } = useMeasurementReminderMutations();
|
||||
const [now] = useState(() => Date.now());
|
||||
@@ -74,8 +77,12 @@ export function VorsorgeDashboardCard() {
|
||||
.filter((r) => r.enabled)
|
||||
.slice(0, SUMMARY_LIMIT);
|
||||
|
||||
// v1.27.6 — a screening reminder routes to the check-in page (a score is
|
||||
// never typed in); completing the test auto-satisfies the reminder.
|
||||
function onPrimaryAction(reminder: MeasurementReminder) {
|
||||
if (reminder.measurementType) {
|
||||
if (isScreeningReminderType(reminder.measurementType)) {
|
||||
router.push("/mental-wellbeing");
|
||||
} else if (reminder.measurementType) {
|
||||
setCapturing(reminder);
|
||||
} else {
|
||||
satisfy.mutate(reminder.id);
|
||||
@@ -119,6 +126,9 @@ export function VorsorgeDashboardCard() {
|
||||
{upcoming.map((reminder) => {
|
||||
const due = relativeDueKey(reminder.nextDueAt, now);
|
||||
const isLinked = reminder.measurementType != null;
|
||||
const isScreening = isScreeningReminderType(
|
||||
reminder.measurementType,
|
||||
);
|
||||
return (
|
||||
<ListRow
|
||||
key={reminder.id}
|
||||
@@ -146,7 +156,11 @@ export function VorsorgeDashboardCard() {
|
||||
{isLinked ? (
|
||||
<>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t("measurementReminders.captureValue")}
|
||||
{t(
|
||||
isScreening
|
||||
? "measurementReminders.startCheckIn"
|
||||
: "measurementReminders.captureValue",
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
*/
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Activity,
|
||||
CalendarClock,
|
||||
@@ -76,6 +77,7 @@ import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { MeasurementForm } from "@/components/measurements/measurement-form";
|
||||
import { MedicationCardHeader } from "@/components/medications/medication-card-header";
|
||||
import { isScreeningReminderType } from "@/lib/validations/measurement-reminders";
|
||||
import {
|
||||
useMeasurementReminders,
|
||||
useMeasurementReminderMutations,
|
||||
@@ -113,9 +115,23 @@ const TYPE_GROUPS = [
|
||||
"BODY_MASS_INDEX",
|
||||
],
|
||||
},
|
||||
// v1.27.6 — the mental-wellbeing screenings become plannable like any
|
||||
// other Vorsorge item. Auto-resolve rides the server-written PHQ9_SCORE /
|
||||
// GAD7_SCORE row a completed check-in produces.
|
||||
{
|
||||
group: "mentalWellbeing",
|
||||
types: ["PHQ9_SCORE", "GAD7_SCORE"],
|
||||
},
|
||||
] as const;
|
||||
|
||||
const INTERVAL_PRESETS = [7, 14, 30, 90, 180, 365] as const;
|
||||
// v1.27.6 — screening types get a dedicated primary action (the check-in
|
||||
// page, not the numeric MeasurementForm) and a 4-week default cadence
|
||||
// (both instruments cover the last two weeks).
|
||||
const MENTAL_SCREENING_DEFAULT_INTERVAL = "28";
|
||||
|
||||
// 28 joined in v1.27.6 as the screening default (every 4 weeks); 30 stays
|
||||
// so existing monthly reminders keep resolving onto a preset.
|
||||
const INTERVAL_PRESETS = [7, 14, 28, 30, 90, 180, 365] as const;
|
||||
// v1.18.2 — the sentinel the cadence picker writes when the user wants a
|
||||
// free interval or an RFC-5545 RRULE instead of a fixed preset.
|
||||
const CADENCE_CUSTOM = "custom";
|
||||
@@ -220,6 +236,7 @@ export function VorsorgeSection({
|
||||
variant?: "settings" | "page";
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
const router = useRouter();
|
||||
const {
|
||||
data: reminders,
|
||||
isLoading,
|
||||
@@ -330,9 +347,14 @@ export function VorsorgeSection({
|
||||
|
||||
// The mark-done action. A free-text / self-planned exam satisfies
|
||||
// silently; a measurement-linked reminder opens the real value-entry
|
||||
// form so the user logs an actual reading.
|
||||
// form so the user logs an actual reading. A screening reminder
|
||||
// (v1.27.6) routes to the check-in page instead — the score is never
|
||||
// typed in, it falls out of a completed test, which then auto-satisfies
|
||||
// the reminder through the eventful worker.
|
||||
function onPrimaryAction(reminder: MeasurementReminder) {
|
||||
if (reminder.measurementType) {
|
||||
if (isScreeningReminderType(reminder.measurementType)) {
|
||||
router.push("/mental-wellbeing");
|
||||
} else if (reminder.measurementType) {
|
||||
setCapturing(reminder);
|
||||
} else {
|
||||
satisfy.mutate(reminder.id);
|
||||
@@ -495,9 +517,19 @@ export function VorsorgeSection({
|
||||
<NativeSelect
|
||||
id="vorsorge-type"
|
||||
value={form.measurementType}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, measurementType: e.target.value }))
|
||||
}
|
||||
onChange={(e) => {
|
||||
const nextType = e.target.value;
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
measurementType: nextType,
|
||||
// v1.27.6 — a screening covers the last two weeks, so
|
||||
// switching onto a screening type defaults the cadence
|
||||
// to every 4 weeks (still freely changeable).
|
||||
cadenceChoice: isScreeningReminderType(nextType)
|
||||
? MENTAL_SCREENING_DEFAULT_INTERVAL
|
||||
: f.cadenceChoice,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{TYPE_GROUPS.map(({ group, types }) => (
|
||||
<optgroup
|
||||
@@ -784,6 +816,9 @@ function VorsorgeCard({
|
||||
: "";
|
||||
const isCoach = reminder.origin === "COACH";
|
||||
const isLinked = reminder.measurementType != null;
|
||||
// v1.27.6 — a screening reminder's action is "start the check-in" (it
|
||||
// routes to the check-in page), never the numeric value-entry wording.
|
||||
const isScreening = isScreeningReminderType(reminder.measurementType);
|
||||
// Due now / overdue ⇒ the action button takes the green "do it now" tone.
|
||||
// The CARD stays neutral (no tint) per the project rule — only the action
|
||||
// button goes green; the surface follows the medication-card grammar.
|
||||
@@ -891,7 +926,11 @@ function VorsorgeCard({
|
||||
{isLinked ? (
|
||||
<>
|
||||
<Activity className="h-4 w-4" />
|
||||
{t("measurementReminders.measureNow")}
|
||||
{t(
|
||||
isScreening
|
||||
? "measurementReminders.startCheckIn"
|
||||
: "measurementReminders.measureNow",
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -958,7 +997,11 @@ function VorsorgeCard({
|
||||
disabled={busy}
|
||||
>
|
||||
{isLinked
|
||||
? t("measurementReminders.measureNow")
|
||||
? t(
|
||||
isScreening
|
||||
? "measurementReminders.startCheckIn"
|
||||
: "measurementReminders.measureNow",
|
||||
)
|
||||
: t("measurementReminders.markDone")}
|
||||
</Button>
|
||||
{headerActions}
|
||||
|
||||
Reference in new issue
Block a user