fix(anamnesis): one newest AUDIT-C entry, scored against the wording offered

The stored language is the wording the read route offers the account;
a request naming another wording, or a takenAt more than five minutes
ahead, is refused. Latest, offerDue, the Coach block and the doctor
report all read the newest entry whose answers open.

The old one-word alcohol pattern gives way to an AUDIT-C entry in the
Coach block, and the alcohol section lists it with a remove action.
This commit is contained in:
Marc-André Bombeck committed 2026-10-16 05:36:42 +02:00
1 parent e9a0b3d959
commit b99b70127d
17 files changed
+515 -87

No files matched your search

@@ -0,0 +1,73 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
const state = vi.hoisted(() => ({
facts: null as unknown,
}));
vi.mock("@tanstack/react-query", () => ({
useQuery: () => ({ data: state.facts }),
useQueryClient: () => ({ invalidateQueries: vi.fn() }),
useMutation: () => ({ mutate: vi.fn(), isPending: false }),
}));
vi.mock("@/lib/api/api-fetch", () => ({
apiGet: vi.fn(),
apiPost: vi.fn(),
apiDelete: vi.fn(),
}));
vi.mock("@/components/outcome/outcome-toast", () => ({
toastWrittenOutcome: vi.fn(),
}));
import { I18nProvider } from "@/lib/i18n/context";
import { auditCInstrument } from "@/lib/mental-health/audit-c";
import { AuditCPanel } from "../audit-c-panel";
function render() {
return renderToStaticMarkup(
<I18nProvider initialLocale="en">
<AuditCPanel
data={{
latest: null,
history: [],
offerDue: true,
instrument: auditCInstrument("en"),
}}
onChanged={() => {}}
/>
</I18nProvider>,
);
}
beforeEach(() => {
state.facts = null;
});
describe("the old alcohol pattern in the alcohol section", () => {
it("lists the stored pattern with a way to remove it", () => {
state.facts = {
current: {
SMOKING_STATUS: null,
SHIFT_SCHEDULE: null,
ALCOHOL_PATTERN: { id: "fact-1", value: "WEEKLY" },
},
history: [],
};
const html = render();
expect(html).toContain('data-slot="audit-c-legacy-pattern"');
expect(html).toContain("Earlier answer: Weekly");
expect(html).toContain("audit-c-legacy-remove");
});
it("shows nothing when there is no stored pattern", () => {
state.facts = {
current: {
SMOKING_STATUS: null,
SHIFT_SCHEDULE: null,
ALCOHOL_PATTERN: null,
},
history: [],
};
expect(render()).not.toContain("audit-c-legacy-pattern");
});
});
+66 -2
View File
@@ -10,7 +10,7 @@
* and its source are the server's too; nothing is scored here.
*/
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Loader2, Trash2 } from "lucide-react";
import { toastWrittenOutcome } from "@/components/outcome/outcome-toast";
@@ -18,9 +18,10 @@ import { SettingsCardActions } from "@/components/settings/_card-actions";
import { Button } from "@/components/ui/button";
import { ConfirmButton } from "@/components/ui/confirm-button";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { apiDelete, apiPost } from "@/lib/api/api-fetch";
import { apiDelete, apiGet, apiPost } from "@/lib/api/api-fetch";
import { showErrorToast } from "@/lib/errors/error-toast";
import { useFormatters, useTranslations } from "@/lib/i18n/context";
import type { HealthProfileFactsPayload } from "@/lib/profile/health-facts";
import type { LifeCircumstancesDto } from "@/lib/profile/life-circumstances";
import { queryKeys } from "@/lib/query-keys";
@@ -76,6 +77,34 @@ export function AuditCPanel({
const complete = answers.every((answer) => answer !== null);
// The one-word alcohol pattern asked before AUDIT-C. It still lives in the
// record, so it is shown here, where alcohol now lives, with a way out.
// The Coach stops reading it once an AUDIT-C entry exists.
const queryClient = useQueryClient();
const facts = useQuery({
queryKey: queryKeys.healthProfileFacts(),
queryFn: () => apiGet<HealthProfileFactsPayload>("/api/anamnesis/facts"),
});
const legacy = facts.data?.current.ALCOHOL_PATTERN ?? null;
const removeLegacy = useMutation({
mutationKey: queryKeys.healthProfileFacts(),
mutationFn: (id: string) => apiDelete(`/api/anamnesis/facts/${id}`),
onSuccess: () => {
toastWrittenOutcome(
"success",
t("records.life.auditC.legacyRemovedToast"),
);
void queryClient.invalidateQueries({
queryKey: queryKeys.healthProfileFacts(),
});
onChanged();
},
onError: (error) =>
showErrorToast(t("records.life.auditC.legacyRemoveError"), {
cause: error,
}),
});
return (
<div className="space-y-4" data-slot="audit-c-panel">
<p className="text-sm">{t("records.life.auditC.intro")}</p>
@@ -154,6 +183,41 @@ export function AuditCPanel({
</div>
) : null}
{legacy?.value ? (
<div
className="flex items-start justify-between gap-2"
data-slot="audit-c-legacy-pattern"
>
<div className="space-y-1">
<p className="text-sm">
{t("records.life.auditC.legacyLabel", {
value: t(
`records.profileFacts.values.ALCOHOL_PATTERN.${legacy.value}`,
),
})}
</p>
<p className="text-muted-foreground text-xs">
{t("records.life.auditC.legacyHint")}
</p>
</div>
<ConfirmButton
label={t("records.life.auditC.legacyRemove")}
ariaLabel={t("records.life.auditC.legacyRemove")}
title={t("records.life.auditC.legacyRemoveConfirmTitle")}
body={t("records.life.auditC.legacyRemoveConfirmBody")}
confirmLabel={t("records.life.auditC.legacyRemove")}
onConfirm={() => removeLegacy.mutate(legacy.id)}
pending={removeLegacy.isPending}
disabled={removeLegacy.isPending}
variant="ghost"
size="icon"
className="text-destructive min-h-11 sm:min-h-9"
icon={<Trash2 className="size-4" aria-hidden />}
slot="audit-c-legacy-remove"
/>
</div>
) : null}
{data.offerDue ? (
<p className="text-muted-foreground text-xs">
{t("records.life.auditC.offerDue")}