diff --git a/src/components/medications/detail/medication-detail-tabs.tsx b/src/components/medications/detail/medication-detail-tabs.tsx index 39a7a8b7b..9cefc459a 100644 --- a/src/components/medications/detail/medication-detail-tabs.tsx +++ b/src/components/medications/detail/medication-detail-tabs.tsx @@ -923,11 +923,7 @@ export function MedicationDetailTabs({ defaultOpen={false} >
- +
{/* v1.40 (#1024) — the courses: when it was taken, and a new course once the last one has ended. */} diff --git a/src/components/medications/sections/__tests__/destructive-zone-section.test.tsx b/src/components/medications/sections/__tests__/destructive-zone-section.test.tsx new file mode 100644 index 000000000..5992803ea --- /dev/null +++ b/src/components/medications/sections/__tests__/destructive-zone-section.test.tsx @@ -0,0 +1,93 @@ +import { describe, expect, it, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +import { I18nProvider } from "@/lib/i18n/context"; +import { updateMedicationSchema } from "@/lib/validations/medication/create-update"; + +import { + canEndCourse, + endCourseBody, + LifecycleManageBody, +} from "../destructive-zone-section"; + +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ + user: { id: "test-user", username: "tester", role: "USER" }, + isAuthenticated: true, + isLoading: false, + }), +})); + +/** + * "End medication" sent `new Date().toISOString()`, a full instant, against an + * `endsOn` that only takes `YYYY-MM-DD`, so every press answered 422 and the + * toast said the medication could not be ended. The body now carries the + * person's own calendar day. + */ +describe("endCourseBody", () => { + it("is a date the update schema accepts", () => { + const body = endCourseBody( + new Date("2026-10-21T09:30:00.000Z"), + "Europe/Berlin", + ); + expect(body).toEqual({ endsOn: "2026-10-21" }); + expect(updateMedicationSchema.safeParse(body).success).toBe(true); + }); + + it("is the person's day, not the UTC one", () => { + // 23:30 UTC is already the next day in Warsaw and still the same day in + // New York. + const lateUtc = new Date("2026-10-20T23:30:00.000Z"); + expect(endCourseBody(lateUtc, "Europe/Warsaw").endsOn).toBe("2026-10-21"); + expect(endCourseBody(lateUtc, "America/New_York").endsOn).toBe( + "2026-10-20", + ); + }); + + it("is what the update schema refused before: a full instant is not a date", () => { + expect( + updateMedicationSchema.safeParse({ + endsOn: new Date("2026-10-21T09:30:00.000Z").toISOString(), + }).success, + ).toBe(false); + }); +}); + +/** + * Ending writes today as the end of the latest course. On a course that has + * already ended that would reopen it up to today, and on one that has not + * started it would become a one-day course today, so End is only offered while + * the course runs. A medication with no dates is CURRENT. + */ +describe("End is offered only for a running course", () => { + function render(courseStatus?: "UPCOMING" | "CURRENT" | "ENDED") { + return renderToStaticMarkup( + + + + + , + ); + } + + it.each([ + ["CURRENT", true], + [undefined, true], + ["ENDED", false], + ["UPCOMING", false], + ] as const)("course %s → End shown: %s", (status, shown) => { + expect(canEndCourse(status)).toBe(shown); + const html = render(status); + expect(html.includes('data-slot="destructive-zone-end"')).toBe(shown); + // Pausing stays available whatever the course. + expect(html).toContain('id="medication-detail-pause-switch"'); + }); +}); diff --git a/src/components/medications/sections/destructive-zone-section.tsx b/src/components/medications/sections/destructive-zone-section.tsx index b8e247d76..ae1c193c2 100644 --- a/src/components/medications/sections/destructive-zone-section.tsx +++ b/src/components/medications/sections/destructive-zone-section.tsx @@ -13,7 +13,7 @@ * - Tier 1 Pausieren: PUT `{ active: false | true }`. Server derives * `pausedAt`. Switch wraps in `