From 58215f33380dc756364fa711ea6daa7937cd12fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 23 Oct 2026 12:41:46 +0200 Subject: [PATCH] refactor(medications): split the dose-history ledger into its hook, row and dialogs The ledger file held two components of 404 and 314 lines. It now reads: - dose-history/use-dose-history-ledger: the window read, summary, day groups and the optimistic mark, plus the owner's edits (toggle, pin, release, delete) and one reconcile step after every write - dose-history/ledger-row-item: the row and, apart from it, the owner's row menu - dose-history/ledger-summary-chart and ledger-confirms dose-history-ledger.tsx keeps the ledger layout. Markup and behaviour are unchanged; the guards name the modules that now hold what they check. --- .../baselines/source-file-lines.json | 5 +- .../baselines/source-function-lines.json | 6 +- .../baselines/version-stamp-comments.json | 5 +- .../daily-reads-refetch-guard.test.ts | 2 +- ...ate-order-explicit-formatter-guard.test.ts | 2 +- .../destructive-control-guard.test.ts | 3 +- .../intake-toast-single-decision.test.ts | 2 +- .../success-affordance-guard.test.ts | 9 +- .../delegated-write-affordances.test.tsx | 2 +- .../medications/dose-history-ledger.tsx | 875 +----------------- .../dose-history/ledger-confirms.tsx | 101 ++ .../dose-history/ledger-row-item.tsx | 451 +++++++++ .../dose-history/ledger-summary-chart.tsx | 68 ++ .../dose-history/use-dose-history-ledger.ts | 326 +++++++ 14 files changed, 1008 insertions(+), 849 deletions(-) create mode 100644 src/components/medications/dose-history/ledger-confirms.tsx create mode 100644 src/components/medications/dose-history/ledger-row-item.tsx create mode 100644 src/components/medications/dose-history/ledger-summary-chart.tsx create mode 100644 src/components/medications/dose-history/use-dose-history-ledger.ts diff --git a/src/__tests__/baselines/source-file-lines.json b/src/__tests__/baselines/source-file-lines.json index 2bee410d8..a6acffeed 100644 --- a/src/__tests__/baselines/source-file-lines.json +++ b/src/__tests__/baselines/source-file-lines.json @@ -1,7 +1,7 @@ { "$comment": "Production files over 800 lines and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.", - "count": 118, - "total": 136542, + "count": 117, + "total": 135490, "entries": { "app/api/dashboard/summary/route.ts": 1032, "app/api/documents/inbound/route.ts": 811, @@ -26,7 +26,6 @@ "components/measurement-reminders/vorsorge-section.tsx": 1536, "components/measurements/measurement-form.tsx": 815, "components/medications/detail/medication-detail-tabs.tsx": 1160, - "components/medications/dose-history-ledger.tsx": 1052, "components/medications/medication-table.tsx": 853, "components/medications/wizard/wizard-payload.ts": 1414, "components/mood/mood-list.tsx": 1408, diff --git a/src/__tests__/baselines/source-function-lines.json b/src/__tests__/baselines/source-function-lines.json index 93dcdaa32..ba2445663 100644 --- a/src/__tests__/baselines/source-function-lines.json +++ b/src/__tests__/baselines/source-function-lines.json @@ -1,7 +1,7 @@ { "$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.", - "count": 405, - "total": 140305, + "count": 403, + "total": 139587, "entries": { "app/achievements/page.tsx :: AchievementsPage": 206, "app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 314, @@ -183,8 +183,6 @@ "components/medications/detail/efficacy/efficacy-chart.tsx :: EfficacyChart": 361, "components/medications/detail/medication-detail-tabs.tsx :: MedicationDetailTabs": 771, "components/medications/dose-history-add-dialog.tsx :: LedgerAddDialog": 223, - "components/medications/dose-history-ledger.tsx :: DoseHistoryLedger": 404, - "components/medications/dose-history-ledger.tsx :: LedgerRowItem": 314, "components/medications/dose-strength-curve.tsx :: DoseStrengthCurve": 266, "components/medications/glp1-medication-card.tsx :: Glp1MedicationCard": 405, "components/medications/injection-site-picker.tsx :: InjectionSitePicker": 287, diff --git a/src/__tests__/baselines/version-stamp-comments.json b/src/__tests__/baselines/version-stamp-comments.json index e546e423d..a8a35605e 100644 --- a/src/__tests__/baselines/version-stamp-comments.json +++ b/src/__tests__/baselines/version-stamp-comments.json @@ -1,7 +1,7 @@ { "$comment": "Comment lines that open with a release version, per file under src/. Maintained by src/__tests__/version-stamp-ratchet.test.ts.", - "count": 2950, - "total": 10294, + "count": 2949, + "total": 10287, "entries": { "__tests__/account-payload-consumer-guard.test.ts": 2, "__tests__/acting-account-boundary-guard.test.ts": 7, @@ -1171,7 +1171,6 @@ "components/medications/detail/medication-detail-tabs.tsx": 28, "components/medications/dose-history-add-dialog.tsx": 2, "components/medications/dose-history-ledger-compute.ts": 5, - "components/medications/dose-history-ledger.tsx": 7, "components/medications/dose-strength-curve.tsx": 2, "components/medications/drug-level-chart.tsx": 2, "components/medications/glp1-medication-card.tsx": 36, diff --git a/src/__tests__/daily-reads-refetch-guard.test.ts b/src/__tests__/daily-reads-refetch-guard.test.ts index 97bfa839b..0102123dc 100644 --- a/src/__tests__/daily-reads-refetch-guard.test.ts +++ b/src/__tests__/daily-reads-refetch-guard.test.ts @@ -242,7 +242,7 @@ describe("daily-reads refetch guard", () => { expect(read(rel)).toMatch(/invalidateMedicationReads\(/); } const pairInPlace = [ - "components/medications/dose-history-ledger.tsx", + "components/medications/dose-history/use-dose-history-ledger.ts", "components/medications/dose-history-add-dialog.tsx", ]; for (const rel of pairInPlace) { diff --git a/src/__tests__/date-order-explicit-formatter-guard.test.ts b/src/__tests__/date-order-explicit-formatter-guard.test.ts index 8d479de9c..908784bbc 100644 --- a/src/__tests__/date-order-explicit-formatter-guard.test.ts +++ b/src/__tests__/date-order-explicit-formatter-guard.test.ts @@ -252,7 +252,7 @@ const RAW_ALLOWLIST: ReadonlyArray<{ file: string; why: string }> = [ why: "The `yyyy-MM-dd` key the ledger groups doses by. The heading a reader sees is rendered separately, through the preference-aware `formatDateWithWeekdaySmart`.", }, { - file: "src/components/medications/dose-history-ledger.tsx", + file: "src/components/medications/dose-history/use-dose-history-ledger.ts", why: "The same key shape for `today`, compared against those buckets. A comparison, not a label.", }, { diff --git a/src/__tests__/destructive-control-guard.test.ts b/src/__tests__/destructive-control-guard.test.ts index 023c98402..3e5f7f9b6 100644 --- a/src/__tests__/destructive-control-guard.test.ts +++ b/src/__tests__/destructive-control-guard.test.ts @@ -524,8 +524,9 @@ const REGISTRY: DestructiveEntry[] = [ confirm: ["AlertDialog"], }, { - file: "components/medications/dose-history-ledger.tsx", + file: "components/medications/dose-history/use-dose-history-ledger.ts", destroys: "one intake event", + triggers: ["components/medications/dose-history/ledger-confirms.tsx"], recovery: "tombstoned-no-restore", confirm: ["AlertDialog"], }, diff --git a/src/__tests__/intake-toast-single-decision.test.ts b/src/__tests__/intake-toast-single-decision.test.ts index 39e73fbb1..0c3927a20 100644 --- a/src/__tests__/intake-toast-single-decision.test.ts +++ b/src/__tests__/intake-toast-single-decision.test.ts @@ -151,7 +151,7 @@ describe("the intake confirmation is decided in one place", () => { expect(posting.length).toBeGreaterThan(2); for (const rel of [ "components/medications/use-medication-intake.ts", - "components/medications/dose-history-ledger.tsx", + "components/medications/dose-history/use-dose-history-ledger.ts", "components/dashboard/medication-intake-quick-add.tsx", "components/medications/dose-history-add-dialog.tsx", ]) { diff --git a/src/__tests__/success-affordance-guard.test.ts b/src/__tests__/success-affordance-guard.test.ts index 335674dfe..e0515bbe4 100644 --- a/src/__tests__/success-affordance-guard.test.ts +++ b/src/__tests__/success-affordance-guard.test.ts @@ -248,8 +248,13 @@ const PINNED_AFFORDANCES: Record< "src/components/medications/dose-history-add-dialog.tsx": { "toast.success": 2, }, - "src/components/medications/dose-history-ledger.tsx": { - "text-success": 2, + "src/components/medications/dose-history/ledger-row-item.tsx": { + "text-success": 1, + }, + "src/components/medications/dose-history/ledger-summary-chart.tsx": { + "text-success": 1, + }, + "src/components/medications/dose-history/use-dose-history-ledger.ts": { "toast.success": 4, }, "src/components/medications/intake-edit-dialog.tsx": { "toast.success": 1 }, diff --git a/src/components/__tests__/delegated-write-affordances.test.tsx b/src/components/__tests__/delegated-write-affordances.test.tsx index 21ea97e13..7517ab847 100644 --- a/src/components/__tests__/delegated-write-affordances.test.tsx +++ b/src/components/__tests__/delegated-write-affordances.test.tsx @@ -185,7 +185,7 @@ import { admittedQuickEntry } from "@/components/dashboard/quick-entry-sheets"; import { TodayHero } from "@/components/daily/today-hero"; import { VorsorgeDashboardCard } from "@/components/measurement-reminders/vorsorge-dashboard-card"; import { EpisodeDocumentsCard } from "@/components/documents/episode-documents-card"; -import { LedgerRowItem } from "@/components/medications/dose-history-ledger"; +import { LedgerRowItem } from "@/components/medications/dose-history/ledger-row-item"; import { VaccinationsView } from "@/components/vaccinations/vaccinations-view"; import { VisitsSection } from "@/components/encounters/visits-section"; import type { EncounterDTO } from "@/lib/encounters/dto"; diff --git a/src/components/medications/dose-history-ledger.tsx b/src/components/medications/dose-history-ledger.tsx index 1df7caeb2..dd39a842a 100644 --- a/src/components/medications/dose-history-ledger.tsx +++ b/src/components/medications/dose-history-ledger.tsx @@ -1,7 +1,7 @@ "use client"; /** - * v1.15.18 WE — the Verlauf tab's dose-history ledger. + * The Verlauf tab's dose-history ledger. * * Renders the server-minted ledger (`GET /api/medications/[id]/dose-history`): * EVERY expected slot with its status (pünktlich / verspätet / übersprungen / @@ -24,13 +24,13 @@ * (incl. backdated / off-schedule); an off-window take surfaces the * "diesem Slot zuordnen?" nudge, pinning it onto a chosen slot via * `forceSlotInstant` rather than orphaning it to an ad-hoc row; - * - v1.15.20: an ad-hoc row shows WHEN the dose would have been due + * - an ad-hoc row shows WHEN the dose would have been due * (`nearestSlot` subline) and — when that slot is still unserved — a * kebab "Slot … zuordnen" action that pins the take onto it after a * confirm that names the consequence ("zählt als verspätet"). A pinned * slot row carries a quiet "zugeordnet" badge and the inverse - * "Zuordnung lösen" kebab action (`forceSlotInstant: null`). v1.16.0: - * the released row keeps USER_PIN provenance server-side (the dedup + * "Zuordnung lösen" kebab action (`forceSlotInstant: null`). The + * released row keeps USER_PIN provenance server-side (the dedup * leaves it standalone) and reads as a plain ad-hoc row here — no * badge, since it is no longer bound to a slot. * @@ -39,85 +39,27 @@ * the ledger uses the semantic feedback vocabulary on the row, not the card. */ -import { DayLink } from "@/components/day/day-link"; -import { - useActiveRecordName, - useRecordCapabilities, -} from "@/hooks/use-record-capabilities"; -import { useCallback, useMemo, useState } from "react"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { - AlertCircle, - AlertTriangle, - Check, - CircleCheck, - Link2, - Loader2, - MoreVertical, - Pencil, - Plus, - SkipForward, - Trash2, - Unlink, -} from "lucide-react"; -import { toast } from "sonner"; -import { showErrorToast } from "@/lib/errors/error-toast"; +import { useState } from "react"; +import { Loader2, Plus } from "lucide-react"; +import { DayLink } from "@/components/day/day-link"; +import { LedgerAddDialog } from "@/components/medications/dose-history-add-dialog"; +import { IntakeEditDialog } from "@/components/medications/intake-edit-dialog"; import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; -import { useAuth } from "@/hooks/use-auth"; -import { - useTranslations, - useFormatters, - useDateFormatPreference, -} from "@/lib/i18n/context"; +import { EmptyState } from "@/components/ui/empty-state"; +import { QueryErrorRow } from "@/components/ui/query-error-row"; +import { useRecordCapabilities } from "@/hooks/use-record-capabilities"; import { formatDateWithWeekdaySmart } from "@/lib/date-format"; import { - invalidateKeys, - medicationDependentKeys, - queryKeys, - refetchInactiveDailyReads, -} from "@/lib/query-keys"; -import type { DoseHistoryStatus } from "@/lib/medications/scheduling/dose-history"; -import { - intakeToastOptions, - runUndoIntake, -} from "@/components/medications/use-medication-intake"; -import { IntakeEditDialog } from "@/components/medications/intake-edit-dialog"; -import { LedgerAddDialog } from "@/components/medications/dose-history-add-dialog"; -import { - applyOptimisticSlotMark, - complianceFromLedger, - formatSlotDelta, - groupLedgerByDay, - isSlotActionable, - slotMarkRequestBody, - type LedgerPayload, - type LedgerRow, -} from "@/components/medications/dose-history-ledger-compute"; -import { apiDelete, apiGet, apiPost, apiPut } from "@/lib/api/api-fetch"; -import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; -import { QueryErrorRow } from "@/components/ui/query-error-row"; -import { EmptyState } from "@/components/ui/empty-state"; + useDateFormatPreference, + useFormatters, + useTranslations, +} from "@/lib/i18n/context"; -const DAY_MS = 24 * 60 * 60 * 1000; +import { DeleteRowConfirm, PinConfirm } from "./dose-history/ledger-confirms"; +import { LedgerRowItem } from "./dose-history/ledger-row-item"; +import { LedgerSummaryChart } from "./dose-history/ledger-summary-chart"; +import { useDoseHistoryLedger } from "./dose-history/use-dose-history-ledger"; /** A schedule slot the add dialog offers for pinning. */ export interface LedgerSchedule { @@ -135,36 +77,6 @@ export interface DoseHistoryLedgerProps { windowDays?: number; } -/** Status → semantic colour token + glyph. Mirrors the card status pill's - * success / warning / destructive ramp so the surface reads as one vocabulary; - * skipped + upcoming are calm muted, ad-hoc is the neutral-positive accent. */ -const STATUS_TONE: Record = { - taken_on_time: "text-success", - taken_late: "text-warning", - skipped: "text-muted-foreground", - missed: "text-destructive", - upcoming: "text-muted-foreground", - ad_hoc: "text-primary", -}; - -function StatusGlyph({ status }: { status: DoseHistoryStatus }) { - const cls = "size-3.5 shrink-0"; - switch (status) { - case "taken_on_time": - return