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:
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user