feat: obsolete medication archiving, start date, and UI improvements (#215)

* feat: obsolete medication archiving, start date, and UI improvements

- Add soft-archive (obsolete) for medications with dedicated section and toggle
- Add medication start date field with date picker and validation
- Add obsolete/reactivate API endpoints with proper auth
- Filter obsolete meds from schedule, coverage, planner, and notifications
- Improve UserFilterModal with intake schedules, stock badges, and click-to-open
- Improve dashboard taken-by badges with per-intake bell icons
- Add Escape key support to ConfirmModal and MobileEditModal
- Fix Lightbox close button positioning near image
- Add read-only mode support for MobileEditModal
- DB migrations: 0008 (is_obsolete, obsolete_at), 0009 (medication_start_date)
- All user-facing text uses i18n keys (en + de)

* test: fix tests for obsolete medications and UI changes

- Backend: add is_obsolete, obsolete_at, medication_start_date columns to test schemas
- Backend: add test medication inserts in planner tests for active-med filtering
- Frontend: update useMedications URL to include includeObsolete param
- Frontend: fix MobileEditModal selectors and validation assertions
- Frontend: add onClearUser prop to UserFilterModal test renders
- Frontend: fix MedicationsPage and DashboardPage test assertions
This commit is contained in:
Daniel Volz
2026-02-15 23:23:38 +01:00
committed by GitHub
parent c47a35d642
commit 4b697374f6
38 changed files with 2042 additions and 907 deletions
@@ -14,9 +14,16 @@ const defaultForm: FormState = {
looseTablets: "0",
totalPills: "",
pillWeightMg: "",
doseUnit: "mg",
medicationStartDate: "",
expiryDate: "",
notes: "",
intakeRemindersEnabled: false,
prescriptionEnabled: false,
prescriptionAuthorizedRefills: "",
prescriptionRemainingRefills: "",
prescriptionLowRefillThreshold: "1",
prescriptionExpiryDate: "",
blisters: [
{
usage: "1",
@@ -47,6 +54,8 @@ const defaultProps = {
formSaved: false,
formChanged: false,
hasValidationErrors: false,
dateConsistencyError: null,
readOnlyMode: false,
takenByInput: "",
onTakenByInputChange: vi.fn(),
existingPeople: [],
@@ -108,7 +117,7 @@ describe("MobileEditModal", () => {
it("renders close button", () => {
render(<MobileEditModal {...defaultProps} />);
const closeBtn = document.querySelector(".modal-close");
const closeBtn = document.querySelector(".btn-nav");
expect(closeBtn).toBeInTheDocument();
});
@@ -116,7 +125,7 @@ describe("MobileEditModal", () => {
const onClose = vi.fn();
render(<MobileEditModal {...defaultProps} onClose={onClose} />);
const closeBtn = document.querySelector(".modal-close");
const closeBtn = document.querySelector(".btn-nav");
if (closeBtn) {
fireEvent.click(closeBtn);
}
@@ -191,7 +200,7 @@ describe("MobileEditModal", () => {
render(<MobileEditModal {...defaultProps} hasValidationErrors={true} />);
const saveBtn = document.querySelector('button[type="submit"]') as HTMLButtonElement;
expect(saveBtn).toBeDisabled();
expect(saveBtn).toHaveClass("has-validation-error");
});
it("renders add intake button", () => {
@@ -45,6 +45,7 @@ describe("UserFilterModal", () => {
coverage={{ all: [mockCoverage] }}
settings={defaultSettings}
onClose={onClose}
onClearUser={vi.fn()}
onOpenMedDetail={onOpenMedDetail}
/>
);
@@ -63,6 +64,7 @@ describe("UserFilterModal", () => {
coverage={{ all: [mockCoverage] }}
settings={defaultSettings}
onClose={onClose}
onClearUser={vi.fn()}
onOpenMedDetail={onOpenMedDetail}
/>
);
@@ -81,6 +83,7 @@ describe("UserFilterModal", () => {
coverage={{ all: [mockCoverage] }}
settings={defaultSettings}
onClose={onClose}
onClearUser={vi.fn()}
onOpenMedDetail={onOpenMedDetail}
/>
);
@@ -100,6 +103,7 @@ describe("UserFilterModal", () => {
coverage={{ all: [mockCoverage] }}
settings={defaultSettings}
onClose={onClose}
onClearUser={vi.fn()}
onOpenMedDetail={onOpenMedDetail}
/>
);
@@ -118,6 +122,7 @@ describe("UserFilterModal", () => {
coverage={{ all: [mockCoverage] }}
settings={defaultSettings}
onClose={onClose}
onClearUser={vi.fn()}
onOpenMedDetail={onOpenMedDetail}
/>
);
@@ -136,6 +141,7 @@ describe("UserFilterModal", () => {
coverage={{ all: [mockCoverage] }}
settings={defaultSettings}
onClose={onClose}
onClearUser={vi.fn()}
onOpenMedDetail={onOpenMedDetail}
/>
);
@@ -154,6 +160,7 @@ describe("UserFilterModal", () => {
coverage={{ all: [mockCoverage] }}
settings={defaultSettings}
onClose={onClose}
onClearUser={vi.fn()}
onOpenMedDetail={onOpenMedDetail}
/>
);
@@ -175,6 +182,7 @@ describe("UserFilterModal", () => {
coverage={{ all: [mockCoverage] }}
settings={defaultSettings}
onClose={onClose}
onClearUser={vi.fn()}
onOpenMedDetail={onOpenMedDetail}
/>
);
@@ -187,8 +195,9 @@ describe("UserFilterModal", () => {
expect(onClose).toHaveBeenCalledTimes(1);
});
it("calls onClose and onOpenMedDetail when medication clicked", () => {
it("calls onClearUser and onOpenMedDetail when medication clicked", () => {
const onClose = vi.fn();
const onClearUser = vi.fn();
const onOpenMedDetail = vi.fn();
render(
@@ -198,6 +207,7 @@ describe("UserFilterModal", () => {
coverage={{ all: [mockCoverage] }}
settings={defaultSettings}
onClose={onClose}
onClearUser={onClearUser}
onOpenMedDetail={onOpenMedDetail}
/>
);
@@ -207,7 +217,7 @@ describe("UserFilterModal", () => {
fireEvent.click(medItem);
}
expect(onClose).toHaveBeenCalledTimes(1);
expect(onClearUser).toHaveBeenCalledTimes(1);
expect(onOpenMedDetail).toHaveBeenCalledWith(mockMedication);
});
@@ -222,6 +232,7 @@ describe("UserFilterModal", () => {
coverage={{ all: [mockCoverage] }}
settings={defaultSettings}
onClose={onClose}
onClearUser={vi.fn()}
onOpenMedDetail={onOpenMedDetail}
/>
);
@@ -243,6 +254,7 @@ describe("UserFilterModal", () => {
coverage={{ all: [mockCoverage] }}
settings={defaultSettings}
onClose={onClose}
onClearUser={vi.fn()}
onOpenMedDetail={onOpenMedDetail}
/>
);
@@ -272,6 +284,7 @@ describe("UserFilterModal", () => {
coverage={{ all: [] }}
settings={defaultSettings}
onClose={onClose}
onClearUser={vi.fn()}
onOpenMedDetail={onOpenMedDetail}
/>
);
@@ -44,7 +44,7 @@ describe("useMedications", () => {
expect(result.current.meds).toEqual(mockMeds);
});
expect(fetch).toHaveBeenCalledWith("/api/medications", { credentials: "include" });
expect(fetch).toHaveBeenCalledWith("/api/medications?includeObsolete=true", { credentials: "include" });
});
it("handles API error gracefully", async () => {
@@ -123,7 +123,7 @@ describe("useMedications", () => {
});
expect(fetch).toHaveBeenCalledWith("/api/medications/5", { method: "DELETE", credentials: "include" });
expect(fetch).toHaveBeenCalledWith("/api/medications", { credentials: "include" });
expect(fetch).toHaveBeenCalledWith("/api/medications?includeObsolete=true", { credentials: "include" });
expect(mockResetForm).toHaveBeenCalled();
});
@@ -607,10 +607,7 @@ describe("DashboardPage with medications", () => {
</MemoryRouter>
);
// Aspirin has intakeRemindersEnabled and notes
const reminderIcons = document.querySelectorAll(".reminder-icon");
expect(reminderIcons.length).toBeGreaterThan(0);
// Aspirin has notes
const notesIcons = document.querySelectorAll(".notes-icon");
expect(notesIcons.length).toBeGreaterThan(0);
});
@@ -83,6 +83,8 @@ const createMockFormHook = (overrides = {}) => ({
prescriptionRemainingRefills: "",
prescriptionLowRefillThreshold: "1",
prescriptionExpiryDate: "",
medicationStartDate: "",
doseUnit: "mg" as const,
},
setForm: vi.fn(),
editingId: null,
@@ -132,6 +134,10 @@ vi.mock("../../context", () => ({
}),
}));
vi.mock("../../components/Auth", () => ({
useAuth: () => ({ user: { id: 1, username: "testuser" }, isAuthenticated: true }),
}));
function renderPage() {
render(
<MemoryRouter>
@@ -156,7 +162,8 @@ describe("MedicationsPage", () => {
it("renders list-first view with new button", () => {
renderPage();
expect(screen.getByText(/medications\.list\.title/i)).toBeInTheDocument();
expect(screen.getByText(/form\.newEntry/i)).toBeInTheDocument();
// Button text and form heading both contain "form.newEntry" in the DOM
expect(screen.getAllByText(/form\.newEntry/i).length).toBeGreaterThanOrEqual(1);
});
it("opens form after clicking new button", () => {