mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
fix(medications): keep a medication's category when it is edited on the web
The add and edit wizard had no row for the thyroid, digestive, skin and sleep-aid categories, so such a medication opened on "Other" and saving any change, even a new dose time, rewrote its category to OTHER. The wizard now offers a row for every category the app knows, translated in all seven languages. An edit also sends the category and treatment class only when the category row was changed. A stored value the wizard cannot show, such as one a newer server or another client wrote, now survives an unrelated edit.
This commit is contained in:
1 parent
a224875ed9
commit
e94e9b94a8
10 files changed
+168
-6
No files matched your search
@@ -1569,6 +1569,10 @@
|
||||
"supplement": "Nahrungsergänzung",
|
||||
"antibiotic": "Antibiotikum",
|
||||
"mentalHealth": "Psychische Gesundheit",
|
||||
"thyroid": "Schilddrüse",
|
||||
"digestive": "Magen/Darm",
|
||||
"skin": "Hautpflege",
|
||||
"sleepAid": "Schlafmittel",
|
||||
"other": "Sonstiges"
|
||||
},
|
||||
"cadence": {
|
||||
|
||||
@@ -1569,6 +1569,10 @@
|
||||
"supplement": "Supplement",
|
||||
"antibiotic": "Antibiotic",
|
||||
"mentalHealth": "Mental health",
|
||||
"thyroid": "Thyroid",
|
||||
"digestive": "Gastrointestinal",
|
||||
"skin": "Skincare",
|
||||
"sleepAid": "Sleep aid",
|
||||
"other": "Other"
|
||||
},
|
||||
"cadence": {
|
||||
|
||||
@@ -1569,6 +1569,10 @@
|
||||
"supplement": "Suplemento",
|
||||
"antibiotic": "Antibiótico",
|
||||
"mentalHealth": "Salud mental",
|
||||
"thyroid": "Tiroides",
|
||||
"digestive": "Digestivo",
|
||||
"skin": "Cuidado de la piel",
|
||||
"sleepAid": "Para dormir",
|
||||
"other": "Otro"
|
||||
},
|
||||
"cadence": {
|
||||
|
||||
@@ -1569,6 +1569,10 @@
|
||||
"supplement": "Complément",
|
||||
"antibiotic": "Antibiotique",
|
||||
"mentalHealth": "Santé mentale",
|
||||
"thyroid": "Thyroïde",
|
||||
"digestive": "Digestif",
|
||||
"skin": "Soin de la peau",
|
||||
"sleepAid": "Aide au sommeil",
|
||||
"other": "Autre"
|
||||
},
|
||||
"cadence": {
|
||||
|
||||
@@ -1569,6 +1569,10 @@
|
||||
"supplement": "Integratore",
|
||||
"antibiotic": "Antibiotico",
|
||||
"mentalHealth": "Salute mentale",
|
||||
"thyroid": "Tiroide",
|
||||
"digestive": "Digestivo",
|
||||
"skin": "Cura della pelle",
|
||||
"sleepAid": "Per dormire",
|
||||
"other": "Altro"
|
||||
},
|
||||
"cadence": {
|
||||
|
||||
@@ -1569,6 +1569,10 @@
|
||||
"supplement": "보충제",
|
||||
"antibiotic": "항생제",
|
||||
"mentalHealth": "정신 건강",
|
||||
"thyroid": "갑상선",
|
||||
"digestive": "소화기",
|
||||
"skin": "피부",
|
||||
"sleepAid": "수면 보조",
|
||||
"other": "기타"
|
||||
},
|
||||
"cadence": {
|
||||
|
||||
@@ -1569,6 +1569,10 @@
|
||||
"supplement": "Suplement",
|
||||
"antibiotic": "Antybiotyk",
|
||||
"mentalHealth": "Zdrowie psychiczne",
|
||||
"thyroid": "Tarczyca",
|
||||
"digestive": "Trawienne",
|
||||
"skin": "Pielęgnacja skóry",
|
||||
"sleepAid": "Na sen",
|
||||
"other": "Inne"
|
||||
},
|
||||
"cadence": {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type WizardTreatmentRow,
|
||||
} from "@/components/medications/wizard/wizard-payload";
|
||||
import { encodeCadence } from "@/components/medications/scheduling/cadence-picker";
|
||||
import { MEDICATION_CATEGORY_VALUES } from "@/lib/validations/medication";
|
||||
import {
|
||||
type CadenceKind,
|
||||
DEFAULT_SUB_CONTROLS,
|
||||
@@ -335,6 +336,10 @@ describe("WIZARD_TREATMENT_MAPPING — Step 2 row → request body", () => {
|
||||
"supplement",
|
||||
"antibiotic",
|
||||
"mentalHealth",
|
||||
"thyroid",
|
||||
"digestive",
|
||||
"skin",
|
||||
"sleepAid",
|
||||
"other",
|
||||
];
|
||||
for (const row of rows) {
|
||||
@@ -356,6 +361,10 @@ describe("rowFromTreatment — reverse mapping for edit-hydration", () => {
|
||||
it("ANTIBIOTIC category maps to the antibiotic row", () => {
|
||||
expect(rowFromTreatment("GENERIC", "ANTIBIOTIC")).toBe("antibiotic");
|
||||
expect(rowFromTreatment("GENERIC", "MENTAL_HEALTH")).toBe("mentalHealth");
|
||||
expect(rowFromTreatment("GENERIC", "THYROID")).toBe("thyroid");
|
||||
expect(rowFromTreatment("GENERIC", "DIGESTIVE")).toBe("digestive");
|
||||
expect(rowFromTreatment("GENERIC", "SKIN")).toBe("skin");
|
||||
expect(rowFromTreatment("GENERIC", "SLEEP_AID")).toBe("sleepAid");
|
||||
});
|
||||
|
||||
it("unknown category falls back to 'other'", () => {
|
||||
@@ -1249,3 +1258,75 @@ describe("as-needed mode (v1.16.11, #316)", () => {
|
||||
expect(validateStep({ ...p, mode: null }, 5)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("category on edit (v1.39.4)", () => {
|
||||
function hydrate(category: string, treatmentClass = "GENERIC") {
|
||||
return hydrateWizardPayload({
|
||||
id: "m1",
|
||||
name: "Levothyroxine",
|
||||
dose: "50 µg",
|
||||
category,
|
||||
treatmentClass,
|
||||
notificationsEnabled: true,
|
||||
startsOn: null,
|
||||
endsOn: null,
|
||||
oneShot: false,
|
||||
schedules: [
|
||||
{
|
||||
windowStart: "07:00",
|
||||
windowEnd: "08:00",
|
||||
timesOfDay: ["07:00"],
|
||||
rrule: "FREQ=DAILY",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
it("gives every stored category a wizard row of its own", () => {
|
||||
const covered = new Set(
|
||||
Object.values(WIZARD_TREATMENT_MAPPING).map((m) => m.category),
|
||||
);
|
||||
for (const value of MEDICATION_CATEGORY_VALUES) {
|
||||
expect(covered.has(value), value).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["THYROID", "SKIN", "SLEEP_AID", "MENTAL_HEALTH"])(
|
||||
"an edit that leaves the row alone keeps %s",
|
||||
(category) => {
|
||||
const body = buildCreateBody(
|
||||
{ ...hydrate(category), name: "Renamed" },
|
||||
"edit",
|
||||
);
|
||||
expect("category" in body).toBe(false);
|
||||
expect("treatmentClass" in body).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("never overwrites a category the wizard has no row for", () => {
|
||||
const payload = hydrate("SOMETHING_NEWER");
|
||||
expect(payload.treatmentRow).toBe("other");
|
||||
const body = buildCreateBody(payload, "edit");
|
||||
expect("category" in body).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps a GLP-1 medication's clinical category on an untouched edit", () => {
|
||||
const body = buildCreateBody(hydrate("BLOOD_PRESSURE", "GLP1"), "edit");
|
||||
expect("category" in body).toBe(false);
|
||||
expect("treatmentClass" in body).toBe(false);
|
||||
});
|
||||
|
||||
it("sends the new category when the row changed", () => {
|
||||
const body = buildCreateBody(
|
||||
{ ...hydrate("THYROID"), treatmentRow: "sleepAid" },
|
||||
"edit",
|
||||
);
|
||||
expect(body.category).toBe("SLEEP_AID");
|
||||
expect(body.treatmentClass).toBe("GENERIC");
|
||||
});
|
||||
|
||||
it("always sends the category on create", () => {
|
||||
const body = buildCreateBody({ ...hydrate("SKIN") }, "create");
|
||||
expect(body.category).toBe("SKIN");
|
||||
});
|
||||
});
|
||||
@@ -3,9 +3,12 @@
|
||||
import {
|
||||
Activity,
|
||||
Apple,
|
||||
BedDouble,
|
||||
Brain,
|
||||
Droplet,
|
||||
Flame,
|
||||
Gauge,
|
||||
Hand,
|
||||
Leaf,
|
||||
type LucideIcon,
|
||||
type LucideProps,
|
||||
@@ -13,6 +16,7 @@ import {
|
||||
Stethoscope,
|
||||
Syringe,
|
||||
Tag,
|
||||
Utensils,
|
||||
Wind,
|
||||
} from "lucide-react";
|
||||
import type { ComponentType } from "react";
|
||||
@@ -37,6 +41,10 @@ const ROW_ICONS: Record<WizardTreatmentRow, ComponentType<LucideProps>> = {
|
||||
supplement: Leaf,
|
||||
antibiotic: ShieldCheck,
|
||||
mentalHealth: Brain,
|
||||
thyroid: Gauge,
|
||||
digestive: Utensils,
|
||||
skin: Hand,
|
||||
sleepAid: BedDouble,
|
||||
other: Tag as unknown as LucideIcon,
|
||||
};
|
||||
|
||||
|
||||
@@ -87,13 +87,20 @@ export type WizardTreatmentRow =
|
||||
| "supplement"
|
||||
| "antibiotic"
|
||||
| "mentalHealth"
|
||||
| "thyroid"
|
||||
| "digestive"
|
||||
| "skin"
|
||||
| "sleepAid"
|
||||
| "other";
|
||||
|
||||
/**
|
||||
* The taxonomy rows in their visible order (D-1 §3 Step 2): Blutdruck ·
|
||||
* Diabetes · Hormone · GLP-1-Injektion · Schmerz · Allergie · Vitamine ·
|
||||
* Nahrungsergänzung · Antibiotikum · Psychische Gesundheit (v1.39.4) ·
|
||||
* Sonstiges.
|
||||
* Nahrungsergänzung · Antibiotikum · Psychische Gesundheit · Schilddrüse ·
|
||||
* Verdauung · Haut · Schlafmittel · Sonstiges. v1.39.4 gave every stored
|
||||
* category a row; before, a thyroid, digestive, skin or sleep-aid
|
||||
* medication hydrated as
|
||||
* "Sonstiges" and an edit saved it back as OTHER.
|
||||
*/
|
||||
export const WIZARD_TREATMENT_ROWS: readonly WizardTreatmentRow[] = [
|
||||
"bloodPressure",
|
||||
@@ -106,6 +113,10 @@ export const WIZARD_TREATMENT_ROWS: readonly WizardTreatmentRow[] = [
|
||||
"supplement",
|
||||
"antibiotic",
|
||||
"mentalHealth",
|
||||
"thyroid",
|
||||
"digestive",
|
||||
"skin",
|
||||
"sleepAid",
|
||||
"other",
|
||||
];
|
||||
|
||||
@@ -131,6 +142,10 @@ export const WIZARD_TREATMENT_MAPPING: Record<
|
||||
supplement: { treatmentClass: "GENERIC", category: "SUPPLEMENT" },
|
||||
antibiotic: { treatmentClass: "GENERIC", category: "ANTIBIOTIC" },
|
||||
mentalHealth: { treatmentClass: "GENERIC", category: "MENTAL_HEALTH" },
|
||||
thyroid: { treatmentClass: "GENERIC", category: "THYROID" },
|
||||
digestive: { treatmentClass: "GENERIC", category: "DIGESTIVE" },
|
||||
skin: { treatmentClass: "GENERIC", category: "SKIN" },
|
||||
sleepAid: { treatmentClass: "GENERIC", category: "SLEEP_AID" },
|
||||
other: { treatmentClass: "GENERIC", category: "OTHER" },
|
||||
};
|
||||
|
||||
@@ -164,6 +179,14 @@ export function rowFromTreatment(
|
||||
return "antibiotic";
|
||||
case "MENTAL_HEALTH":
|
||||
return "mentalHealth";
|
||||
case "THYROID":
|
||||
return "thyroid";
|
||||
case "DIGESTIVE":
|
||||
return "digestive";
|
||||
case "SKIN":
|
||||
return "skin";
|
||||
case "SLEEP_AID":
|
||||
return "sleepAid";
|
||||
default:
|
||||
return "other";
|
||||
}
|
||||
@@ -226,6 +249,14 @@ export interface WizardPayload {
|
||||
doseAmount: string;
|
||||
doseUnit: string;
|
||||
treatmentRow: WizardTreatmentRow | null;
|
||||
/**
|
||||
* v1.39.4 — the row the edit hydrated to. An edit sends `category` and
|
||||
* `treatmentClass` only when `treatmentRow` differs from it, so a stored
|
||||
* category the wizard has no row for (a value a newer server or another
|
||||
* client wrote) is never overwritten by saving an unrelated change.
|
||||
* Absent on create.
|
||||
*/
|
||||
initialTreatmentRow?: WizardTreatmentRow | null;
|
||||
/**
|
||||
* v1.6.0 — route of administration. Drives the injection-site
|
||||
* rotation preview (shown when `INJECTION`) and, paired with
|
||||
@@ -597,8 +628,9 @@ export function firstInvalidIndex(
|
||||
export interface CreateMedicationBody {
|
||||
name: string;
|
||||
dose: string;
|
||||
category: MedicationCategoryValue;
|
||||
treatmentClass: MedicationTreatmentClass;
|
||||
/** Omitted on an edit that left the category row untouched (v1.39.4). */
|
||||
category?: MedicationCategoryValue;
|
||||
treatmentClass?: MedicationTreatmentClass;
|
||||
/** v1.6.0 — route of administration (ORAL | INJECTION | OTHER). */
|
||||
deliveryForm: MedicationDeliveryForm;
|
||||
/** v1.6.0 — doses per pen / vial. Omitted when inventory is off. */
|
||||
@@ -808,11 +840,20 @@ export function buildCreateBody(
|
||||
// reads 1), "1 1/2" and "1½" are 1.5, and a value the server would
|
||||
// refuse is null. Step 3 cannot be left while it is null.
|
||||
const parsedUnitsPerDose = parseUnitsPerDoseInput(committed.unitsPerDose);
|
||||
// v1.39.4 — an edit that did not touch the category row leaves the stored
|
||||
// category (and treatment class) alone rather than re-deriving them from
|
||||
// the row, which cannot represent every stored value.
|
||||
const keepStoredCategory =
|
||||
forMode === "edit" &&
|
||||
committed.initialTreatmentRow !== undefined &&
|
||||
committed.treatmentRow === committed.initialTreatmentRow;
|
||||
const body: CreateMedicationBody = {
|
||||
name: committed.name.trim(),
|
||||
dose,
|
||||
category: mapping.category,
|
||||
treatmentClass: mapping.treatmentClass,
|
||||
...(!keepStoredCategory && {
|
||||
category: mapping.category,
|
||||
treatmentClass: mapping.treatmentClass,
|
||||
}),
|
||||
deliveryForm: committed.deliveryForm,
|
||||
...(Number.isFinite(parsedDosesPerUnit) &&
|
||||
parsedDosesPerUnit >= 1 && {
|
||||
@@ -1194,6 +1235,10 @@ export function hydrateWizardPayload(
|
||||
doseAmount: parsedDose.amount,
|
||||
doseUnit: parsedDose.unit || base.doseUnit,
|
||||
treatmentRow: rowFromTreatment(initial.treatmentClass, initial.category),
|
||||
initialTreatmentRow: rowFromTreatment(
|
||||
initial.treatmentClass,
|
||||
initial.category,
|
||||
),
|
||||
deliveryForm: normaliseDeliveryForm(initial.deliveryForm),
|
||||
dosesPerUnit:
|
||||
typeof initial.dosesPerUnit === "number" && initial.dosesPerUnit >= 1
|
||||
|
||||
Reference in new issue
Block a user