mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
feat(medications): side-effect logging on GLP-1 detail pages
Adds the /api/medications/[id]/side-effects collection + per-row endpoints (GET / POST + DELETE), the SideEffectsSection picker UI, and i18n across the six locales. Mounted on the medication detail page between the drug-level chart and the intake history; only the GLP-1 treatmentClass renders the section. The picker is category → entry chip → 5-Likert severity with an optional 280-char note. The server derives the category from the entry server-side — a client-supplied mismatch returns 422, so a NAUSEA row tagged as INJECTION_SITE never lands in the DB. POST is rate-limited 30/min/user (matching the inventory route); DELETE is allowed at any time because the rows are user-owned and non-clinical, with the audit trail preserving entry + severity + occurredAt so a forensic reconstruction stays possible. DE + EN copy hand-curated; FR / ES / IT / PL drafted from EN with EMA-EPAR-§4.8 terminology. 39 new tests cover the route contract (auth, ownership, validation, category mismatch, rate-limit) and the section's static-render shape (empty state, category badge, severity ladder, entry label, locale switch).
This commit is contained in:
1 parent
4e2b7be472
commit
7dfc212634
13 files changed
+1693
No files matched your search
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* v1.4.25 W19d — Zod validators for the side-effect API surface.
|
||||
*
|
||||
* The category supplied on the wire is sanity-checked against the
|
||||
* authoritative entry → category mapping in `./taxonomy.ts`; on
|
||||
* mismatch the route returns 422. This means a client cannot
|
||||
* accidentally (or maliciously) write a NAUSEA row stamped with the
|
||||
* INJECTION_SITE category and confuse the Coach aggregator.
|
||||
*
|
||||
* Severity is bounded 1-5 — the same range as the DB-level CHECK
|
||||
* constraint and the `SideEffectSeverity` type-guard. Notes are
|
||||
* capped to 280 chars (the chat-bubble length budget the timeline
|
||||
* renders inside).
|
||||
*/
|
||||
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const SIDE_EFFECT_CATEGORY_VALUES = [
|
||||
"GI",
|
||||
"METABOLIC",
|
||||
"INJECTION_SITE",
|
||||
"COGNITIVE",
|
||||
"GLP1_SPECIFIC",
|
||||
] as const;
|
||||
|
||||
export const SIDE_EFFECT_ENTRY_VALUES = [
|
||||
"NAUSEA",
|
||||
"VOMITING",
|
||||
"DIARRHEA",
|
||||
"CONSTIPATION",
|
||||
"ABDOMINAL_PAIN",
|
||||
"HYPOGLYCEMIA_SYMPTOMS",
|
||||
"DEHYDRATION",
|
||||
"ANOREXIA",
|
||||
"ELECTROLYTE_FATIGUE",
|
||||
"INJECTION_REDNESS",
|
||||
"INJECTION_SWELLING",
|
||||
"INJECTION_BRUISING",
|
||||
"INJECTION_INDURATION",
|
||||
"BRAIN_FOG",
|
||||
"DIZZINESS",
|
||||
"LOW_MOOD",
|
||||
"LOW_ENERGY",
|
||||
"EARLY_SATIETY",
|
||||
"GASTROPARESIS_LIKE",
|
||||
"DYSGEUSIA",
|
||||
"GALLBLADDER_DISCOMFORT",
|
||||
] as const;
|
||||
|
||||
export const SIDE_EFFECT_NOTES_MAX = 280;
|
||||
|
||||
export const createSideEffectSchema = z.object({
|
||||
category: z.enum(SIDE_EFFECT_CATEGORY_VALUES),
|
||||
entry: z.enum(SIDE_EFFECT_ENTRY_VALUES),
|
||||
severity: z.number().int().min(1).max(5),
|
||||
occurredAt: z.iso
|
||||
.datetime({ offset: true })
|
||||
.transform((s) => new Date(s))
|
||||
.optional(),
|
||||
notes: z.string().max(SIDE_EFFECT_NOTES_MAX).nullable().optional(),
|
||||
});
|
||||
|
||||
export const listSideEffectsSchema = z.object({
|
||||
from: z.iso
|
||||
.datetime({ offset: true })
|
||||
.transform((s) => new Date(s))
|
||||
.optional(),
|
||||
to: z.iso
|
||||
.datetime({ offset: true })
|
||||
.transform((s) => new Date(s))
|
||||
.optional(),
|
||||
limit: z.coerce.number().int().min(1).max(200).optional().default(50),
|
||||
});
|
||||
|
||||
export type CreateSideEffectInput = z.infer<typeof createSideEffectSchema>;
|
||||
export type ListSideEffectsInput = z.infer<typeof listSideEffectsSchema>;
|
||||
Reference in new issue
Block a user