mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
feat(labs): add a hidden flag to the biomarker catalog
Add a `hidden` boolean to the Biomarker model (migration 0195_biomarker_hidden) so a marker the user no longer needs can be soft-removed instead of deleted: it leaves the active catalog and the lab-entry pickers but keeps its readings and canonical unit/range. Thread `hidden` through the DTO, the update schema, the PUT handler (field-by-field), and the list endpoint (visible markers first). Regen the OpenAPI contract.
This commit is contained in:
1 parent
42d49953f5
commit
e097e57c84
9 files changed
+54
-4
No files matched your search
@@ -6741,6 +6741,8 @@ paths:
|
||||
- type: string
|
||||
const: ""
|
||||
- type: "null"
|
||||
hidden:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: Updated biomarker.
|
||||
@@ -20212,6 +20214,8 @@ components:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
hidden:
|
||||
type: boolean
|
||||
createdAt:
|
||||
type: string
|
||||
updatedAt:
|
||||
@@ -20225,11 +20229,14 @@ components:
|
||||
- panel
|
||||
- hasContext
|
||||
- context
|
||||
- hidden
|
||||
- createdAt
|
||||
- updatedAt
|
||||
additionalProperties: false
|
||||
description: A user-scoped catalog marker. `context` is the decrypted per-marker note (or null); `hasContext` flags its
|
||||
presence. A `LabResult` linking this marker resolves its unit + reference bounds from here.
|
||||
presence. `hidden` drops the marker from the active catalog list and the lab-entry pickers without deleting it
|
||||
(its readings and unit/range survive). A `LabResult` linking this marker resolves its unit + reference bounds
|
||||
from here.
|
||||
CreateBiomarkerResponse:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-- v1.22.0 — Biomarker catalog: per-marker "hidden" flag.
|
||||
--
|
||||
-- A marker the user no longer needs is hidden rather than deleted: it leaves
|
||||
-- the active catalog list and the lab-entry pickers, but its readings and its
|
||||
-- canonical unit/reference-range definition survive (unhide restores it). A
|
||||
-- plain additive boolean column with a server-side default; no existing row is
|
||||
-- touched (every back-filled row defaults to visible). `IF NOT EXISTS` makes
|
||||
-- the rerun safe.
|
||||
ALTER TABLE "biomarkers" ADD COLUMN IF NOT EXISTS "hidden" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -3657,6 +3657,10 @@ model Biomarker {
|
||||
/// Optional panel grouping moved off the per-entry row, e.g.
|
||||
/// "Großes Blutbild". NULL = standalone marker.
|
||||
panel String?
|
||||
/// A marker the user no longer needs: dropped from the active catalog list
|
||||
/// and from the lab-entry pickers, but never deleted — its readings and
|
||||
/// canonical unit/range definition survive.
|
||||
hidden Boolean @default(false) @map("hidden")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ function serialiseBiomarker(row: {
|
||||
lowerBound: number | null;
|
||||
upperBound: number | null;
|
||||
panel: string | null;
|
||||
hidden: boolean;
|
||||
contextEncrypted: Uint8Array | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -54,6 +55,7 @@ function serialiseBiomarker(row: {
|
||||
context: row.contextEncrypted
|
||||
? decryptContextFromBytes(row.contextEncrypted)
|
||||
: null,
|
||||
hidden: row.hidden,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
};
|
||||
@@ -137,6 +139,7 @@ export const PUT = apiHandler(
|
||||
if (d.lowerBound !== undefined) data.lowerBound = d.lowerBound;
|
||||
if (d.upperBound !== undefined) data.upperBound = d.upperBound;
|
||||
if (d.panel !== undefined) data.panel = d.panel;
|
||||
if (d.hidden !== undefined) data.hidden = d.hidden;
|
||||
|
||||
// Inverted-range guard for a PARTIAL bound update. The schema refine only
|
||||
// fires when both bounds arrive together; moving a single bound past the
|
||||
|
||||
@@ -50,6 +50,7 @@ function serialiseBiomarker(row: {
|
||||
lowerBound: number | null;
|
||||
upperBound: number | null;
|
||||
panel: string | null;
|
||||
hidden: boolean;
|
||||
contextEncrypted: Uint8Array | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -63,6 +64,7 @@ function serialiseBiomarker(row: {
|
||||
panel: row.panel,
|
||||
hasContext: row.contextEncrypted !== null,
|
||||
context: decryptContextSoft(row.contextEncrypted),
|
||||
hidden: row.hidden,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
};
|
||||
@@ -71,9 +73,11 @@ function serialiseBiomarker(row: {
|
||||
export const GET = apiHandler(async () => {
|
||||
const { user } = await requireAuth();
|
||||
|
||||
// Visible markers first, then hidden — the manager renders them in two
|
||||
// sections and the lab-entry pickers drop the hidden tail client-side.
|
||||
const rows = await prisma.biomarker.findMany({
|
||||
where: { userId: user.id },
|
||||
orderBy: [{ name: "asc" }],
|
||||
orderBy: [{ hidden: "asc" }, { name: "asc" }],
|
||||
});
|
||||
|
||||
annotate({
|
||||
|
||||
@@ -42,6 +42,8 @@ export interface BiomarkerDto {
|
||||
panel: string | null;
|
||||
hasContext: boolean;
|
||||
context: string | null;
|
||||
/** v1.22 — a hidden marker drops from the active list + lab-entry pickers. */
|
||||
hidden: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ createBiomarkerSchema.meta({
|
||||
updateBiomarkerSchema.meta({
|
||||
id: "UpdateBiomarkerRequest",
|
||||
description:
|
||||
"Partial edit of a biomarker. An omitted key leaves the column untouched; an explicit `null` on `context` / `panel` / a bound clears it. A rename that collides with another of the caller's markers is rejected 409.",
|
||||
"Partial edit of a biomarker. An omitted key leaves the column untouched; an explicit `null` on `context` / `panel` / a bound clears it. Set `hidden` to drop / restore the marker in the active list + lab-entry pickers. A rename that collides with another of the caller's markers is rejected 409.",
|
||||
});
|
||||
|
||||
const biomarkerRow = z
|
||||
@@ -43,13 +43,14 @@ const biomarkerRow = z
|
||||
panel: z.string().nullable(),
|
||||
hasContext: z.boolean(),
|
||||
context: z.string().nullable(),
|
||||
hidden: z.boolean(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
.meta({
|
||||
id: "Biomarker",
|
||||
description:
|
||||
"A user-scoped catalog marker. `context` is the decrypted per-marker note (or null); `hasContext` flags its presence. A `LabResult` linking this marker resolves its unit + reference bounds from here.",
|
||||
"A user-scoped catalog marker. `context` is the decrypted per-marker note (or null); `hasContext` flags its presence. `hidden` drops the marker from the active catalog list and the lab-entry pickers without deleting it (its readings and unit/range survive). A `LabResult` linking this marker resolves its unit + reference bounds from here.",
|
||||
});
|
||||
|
||||
const listResponse = z
|
||||
|
||||
@@ -72,4 +72,21 @@ describe("updateBiomarkerSchema", () => {
|
||||
});
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts a hidden toggle", () => {
|
||||
const r = updateBiomarkerSchema.safeParse({ hidden: true });
|
||||
expect(r.success).toBe(true);
|
||||
if (r.success) expect(r.data.hidden).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves hidden undefined when omitted", () => {
|
||||
const r = updateBiomarkerSchema.safeParse({ unit: "mmol/L" });
|
||||
expect(r.success).toBe(true);
|
||||
if (r.success) expect(r.data.hidden).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects a non-boolean hidden", () => {
|
||||
const r = updateBiomarkerSchema.safeParse({ hidden: "yes" });
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -72,6 +72,9 @@ export const updateBiomarkerSchema = z
|
||||
upperBound: upperBound.or(z.null()),
|
||||
context: optionalContext.or(z.null()),
|
||||
panel: optionalPanel.or(z.null()),
|
||||
// v1.22 — hide / unhide a marker. An omitted key leaves visibility
|
||||
// untouched; `true` drops the marker from the active list + pickers.
|
||||
hidden: z.boolean().optional(),
|
||||
})
|
||||
.refine(
|
||||
(d) =>
|
||||
|
||||
Reference in new issue
Block a user