fix(labs): name a merge in the owner's activity feed and toast it through the outcome module

A merge by a record manager now reads as its own sentence in the owner's
activity feed, the merged toast goes through the written-outcome toast,
and the export test doubles carry the alias and merge tables.
This commit is contained in:
Marc-André Bombeck committed 2026-10-15 23:03:45 +02:00
1 parent e65ada2961
commit e112e05a58
13 files changed
+52 -25

No files matched your search

+2 -1
View File
@@ -12061,7 +12061,8 @@
"customVaccineDelete": "{name} hat einen eigenen Impfstoff entfernt",
"lifeEventCreate": "{name} hat ein Lebensereignis hinzugefügt",
"lifeEventUpdate": "{name} hat ein Lebensereignis geändert",
"lifeEventDelete": "{name} hat ein Lebensereignis entfernt"
"lifeEventDelete": "{name} hat ein Lebensereignis entfernt",
"biomarkerMerge": "{name} hat Biomarker zusammengeführt"
},
"section": {
"measurements": {
+2 -1
View File
@@ -12061,7 +12061,8 @@
"customVaccineDelete": "{name} removed an own vaccine",
"lifeEventCreate": "{name} added a life event",
"lifeEventUpdate": "{name} changed a life event",
"lifeEventDelete": "{name} removed a life event"
"lifeEventDelete": "{name} removed a life event",
"biomarkerMerge": "{name} merged biomarkers"
},
"section": {
"measurements": {
+2 -1
View File
@@ -12061,7 +12061,8 @@
"customVaccineDelete": "{name} quitó una vacuna propia",
"lifeEventCreate": "{name} añadió un acontecimiento vital",
"lifeEventUpdate": "{name} cambió un acontecimiento vital",
"lifeEventDelete": "{name} eliminó un acontecimiento vital"
"lifeEventDelete": "{name} eliminó un acontecimiento vital",
"biomarkerMerge": "{name} combinó biomarcadores"
},
"section": {
"measurements": {
+2 -1
View File
@@ -12061,7 +12061,8 @@
"customVaccineDelete": "{name} a retiré un vaccin personnel",
"lifeEventCreate": "{name} a ajouté un événement de vie",
"lifeEventUpdate": "{name} a modifié un événement de vie",
"lifeEventDelete": "{name} a supprimé un événement de vie"
"lifeEventDelete": "{name} a supprimé un événement de vie",
"biomarkerMerge": "{name} a fusionné des biomarqueurs"
},
"section": {
"measurements": {
+2 -1
View File
@@ -12061,7 +12061,8 @@
"customVaccineDelete": "{name} ha rimosso un vaccino personale",
"lifeEventCreate": "{name} ha aggiunto un evento della vita",
"lifeEventUpdate": "{name} ha modificato un evento della vita",
"lifeEventDelete": "{name} ha rimosso un evento della vita"
"lifeEventDelete": "{name} ha rimosso un evento della vita",
"biomarkerMerge": "{name} ha unito dei biomarcatori"
},
"section": {
"measurements": {
+2 -1
View File
@@ -12061,7 +12061,8 @@
"customVaccineDelete": "{name} 님이 직접 추가한 백신을 삭제했어요",
"lifeEventCreate": "{name} 님이 삶의 사건을 추가했어요",
"lifeEventUpdate": "{name} 님이 삶의 사건을 수정했어요",
"lifeEventDelete": "{name} 님이 삶의 사건을 삭제했어요"
"lifeEventDelete": "{name} 님이 삶의 사건을 삭제했어요",
"biomarkerMerge": "{name} 님이 바이오마커를 합쳤어요"
},
"section": {
"measurements": {
+2 -1
View File
@@ -12061,7 +12061,8 @@
"customVaccineDelete": "{name} usunął własną szczepionkę",
"lifeEventCreate": "{name} dodał wydarzenie życiowe",
"lifeEventUpdate": "{name} zmienił wydarzenie życiowe",
"lifeEventDelete": "{name} usunął wydarzenie życiowe"
"lifeEventDelete": "{name} usunął wydarzenie życiowe",
"biomarkerMerge": "{name} scalił biomarkery"
},
"section": {
"measurements": {
@@ -86,6 +86,8 @@ vi.mock("@/lib/db", () => ({
// helper now also reads (`buildRecordsBackupSection`).
labResult: { findMany: vi.fn() },
biomarker: { findMany: vi.fn() },
biomarkerAlias: { findMany: vi.fn() },
biomarkerMerge: { findMany: vi.fn() },
illnessEpisode: { findMany: vi.fn() },
allergy: { findMany: vi.fn() },
familyHistoryEntry: { findMany: vi.fn() },
@@ -135,6 +137,8 @@ beforeEach(() => {
// `resetAllMocks` clears the inline defaults, and the full-backup payload
// builder reads these unconditionally.
vi.mocked(prisma.nutrientIntakeDay.findMany).mockResolvedValue([] as never);
vi.mocked(prisma.biomarkerAlias.findMany).mockResolvedValue([] as never);
vi.mocked(prisma.biomarkerMerge.findMany).mockResolvedValue([] as never);
vi.mocked(prisma.medicationCategoryLabel.findMany).mockResolvedValue(
[] as never,
);
@@ -67,6 +67,8 @@ vi.mock("@/lib/db", () => ({
cycleDayLog: { findMany: vi.fn() },
labResult: { findMany: vi.fn() },
biomarker: { findMany: vi.fn() },
biomarkerAlias: { findMany: vi.fn() },
biomarkerMerge: { findMany: vi.fn() },
allergy: { findMany: vi.fn() },
familyHistoryEntry: { findMany: vi.fn() },
workout: { findMany: vi.fn() },
@@ -205,6 +207,8 @@ beforeEach(() => {
vi.mocked(prisma.cycleDayLog.findMany).mockResolvedValue([] as never);
vi.mocked(prisma.labResult.findMany).mockResolvedValue([] as never);
vi.mocked(prisma.biomarker.findMany).mockResolvedValue([] as never);
vi.mocked(prisma.biomarkerAlias.findMany).mockResolvedValue([] as never);
vi.mocked(prisma.biomarkerMerge.findMany).mockResolvedValue([] as never);
vi.mocked(prisma.allergy.findMany).mockResolvedValue([] as never);
vi.mocked(prisma.familyHistoryEntry.findMany).mockResolvedValue([] as never);
vi.mocked(prisma.workout.findMany).mockResolvedValue([] as never);
@@ -77,6 +77,8 @@ vi.mock("@/lib/db", () => ({
// builder now also reads (`buildRecordsBackupSection`).
labResult: { findMany: vi.fn().mockResolvedValue([]) },
biomarker: { findMany: vi.fn().mockResolvedValue([]) },
biomarkerAlias: { findMany: vi.fn().mockResolvedValue([]) },
biomarkerMerge: { findMany: vi.fn().mockResolvedValue([]) },
illnessEpisode: { findMany: vi.fn().mockResolvedValue([]) },
allergy: { findMany: vi.fn().mockResolvedValue([]) },
familyHistoryEntry: { findMany: vi.fn().mockResolvedValue([]) },
@@ -16,6 +16,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { Loader2 } from "lucide-react";
import { toastWrittenOutcome } from "@/components/outcome/outcome-toast";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
@@ -71,7 +72,8 @@ export function BiomarkerMergeSheet({
setPreview(result);
return;
}
toast.success(
toastWrittenOutcome(
"success",
tCount("labs.merge.doneToast", result.movedReadings, {
name: result.target.name,
}),
+2
View File
@@ -179,6 +179,8 @@ export function recordActivityVerbLine(
return t("recordSharing.activityVerb.biomarkerUpdate", { name });
case "biomarker.delete":
return t("recordSharing.activityVerb.biomarkerDelete", { name });
case "labs.biomarker.merge":
return t("recordSharing.activityVerb.biomarkerMerge", { name });
case "allergy.update":
return t("recordSharing.activityVerb.allergyUpdate", { name });
+23 -17
View File
@@ -97,10 +97,9 @@ async function merge(
body: { sourceIds: string[]; dryRun?: boolean },
) {
const { POST } = await import("@/app/api/biomarkers/[id]/merge/route");
return POST(
json(body, `http://localhost/api/biomarkers/${targetId}/merge`),
{ params: Promise.resolve({ id: targetId }) },
);
return POST(json(body, `http://localhost/api/biomarkers/${targetId}/merge`), {
params: Promise.resolve({ id: targetId }),
});
}
async function tableCounts() {
@@ -172,9 +171,8 @@ describe("POST /api/biomarkers/{id}/merge", () => {
expect(detail.data.meta.total).toBe(42);
// The doctor report: one line, counted 42, under the marker's name.
const { loadLabResults } = await import(
"@/lib/doctor-report/clinical-records"
);
const { loadLabResults } =
await import("@/lib/doctor-report/clinical-records");
const report = await loadLabResults(USER_ID, daysAgo(1000), new Date());
expect(report).toHaveLength(1);
expect(report![0]).toMatchObject({
@@ -206,14 +204,18 @@ describe("POST /api/biomarkers/{id}/merge", () => {
if (!page.nextCursor) break;
offset += page.readings!.length;
}
expect(total.reduce((a, b) => a + b, 0), analyte).toBe(42);
expect(
total.reduce((a, b) => a + b, 0),
analyte,
).toBe(42);
}
// The source is gone, its old id leads to the target, and the reading's
// printed name survived.
const prisma = getPrismaClient();
expect(await prisma.biomarker.findUnique({ where: { id: source.id } }))
.toBeNull();
expect(
await prisma.biomarker.findUnique({ where: { id: source.id } }),
).toBeNull();
const { GET: getMarker } = await import("@/app/api/biomarkers/[id]/route");
const stale = await getMarker(
new NextRequest(`http://localhost/api/biomarkers/${source.id}`),
@@ -442,9 +444,11 @@ describe("POST /api/biomarkers/{id}/merge", () => {
"Ferr",
"Ferritin (Serum)",
]);
expect(await getPrismaClient().labResult.count({
where: { biomarkerId: c.id },
})).toBe(3);
expect(
await getPrismaClient().labResult.count({
where: { biomarkerId: c.id },
}),
).toBe(3);
});
});
@@ -469,7 +473,9 @@ describe("aliases on the marker", () => {
},
});
const res = await put(alp.id, { aliases: ["Alk.P", "AP", "alkaline phosphatase"] });
const res = await put(alp.id, {
aliases: ["Alk.P", "AP", "alkaline phosphatase"],
});
expect(res.status).toBe(200);
expect((await res.json()).data.aliases).toEqual(["Alk.P", "AP"]);
const rows = await prisma.biomarkerAlias.findMany({
@@ -712,9 +718,9 @@ describe("a spelling resolves on its own once chosen", () => {
where: { id: ref.recordId },
});
expect(reading.biomarkerId).toBe(alp.id);
expect(
(await prisma.biomarkerAlias.findFirstOrThrow({})).name,
).toBe("Alk. Phosph.");
expect((await prisma.biomarkerAlias.findFirstOrThrow({})).name).toBe(
"Alk. Phosph.",
);
const again = serialiseFact(fact, await loadMarkerIndex(prisma, USER_ID));
expect(again).toMatchObject({ biomarkerId: alp.id, matchKind: "alias" });