From 96c99f4dfab272919252b2a399e2047fdf56d098 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Thu, 9 Jul 2026 00:13:08 +0200
Subject: [PATCH] fix(medications): render intake dialogs as bottom sheets on
mobile
Port the six medication dialogs (log-intake, intake-edit, dose-history-add,
take-all-due, log-injection-site, intake-import) from the centred Dialog to
ResponsiveSheet with the sticky-footer slot, matching every other module's
add/edit form. Form-backed dialogs associate their footer submit button via a
useId form id so Enter-to-submit and the footer button share one form; the
nested slot-nudge confirm rides the same sheet. Primary actions now sit within
thumb reach on phones.
---
.../medications/dose-history-add-dialog.tsx | 113 ++++++++---------
.../medications/intake-edit-dialog.tsx | 66 +++++-----
.../medications/intake-import-dialog.tsx | 114 +++++++++---------
.../medications/log-injection-site-dialog.tsx | 67 +++++-----
.../medications/log-intake-dialog.tsx | 75 ++++++------
.../medications/take-all-due-dialog.tsx | 85 ++++++-------
6 files changed, 234 insertions(+), 286 deletions(-)
diff --git a/src/components/medications/dose-history-add-dialog.tsx b/src/components/medications/dose-history-add-dialog.tsx
index 49587bea0..c2b332f24 100644
--- a/src/components/medications/dose-history-add-dialog.tsx
+++ b/src/components/medications/dose-history-add-dialog.tsx
@@ -17,20 +17,13 @@
* ad-hoc (no `scheduledFor`); an exact slot match pins directly with no nudge.
*/
-import { useMemo, useState } from "react";
+import { useId, useMemo, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { Loader2 } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
+import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { Label } from "@/components/ui/label";
import { DateTimeField } from "@/components/ui/date-time-field";
import { Switch } from "@/components/ui/switch";
@@ -135,6 +128,7 @@ export function LedgerAddDialog({
}: LedgerAddDialogProps) {
const { t } = useTranslations();
const queryClient = useQueryClient();
+ const formId = useId();
const [takenAt, setTakenAt] = useState(() => toDateTimeLocal(new Date()));
const [skipped, setSkipped] = useState(false);
@@ -200,22 +194,40 @@ export function LedgerAddDialog({
}
return (
-
)}
-
-
-
-
-
{/* The "diesem Slot zuordnen?" nudge — pin onto the slot, or keep the
take as a standalone ad-hoc entry. */}
{nudgeOpen && nearest && (
-
+ >
+ }
+ >
+
+ {t("medications.detail.verlauf.nudge.body", {
+ slot: nearest.slotHm,
+ })}
+
+
)}
-
-
+
);
}
diff --git a/src/components/medications/intake-edit-dialog.tsx b/src/components/medications/intake-edit-dialog.tsx
index e01ab2feb..59489a485 100644
--- a/src/components/medications/intake-edit-dialog.tsx
+++ b/src/components/medications/intake-edit-dialog.tsx
@@ -21,19 +21,13 @@
* persisted it (audit LOW-10), so the dialog no longer pretends.
*/
-import { useState } from "react";
+import { useId, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
+import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { DateTimeField } from "@/components/ui/date-time-field";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
@@ -107,6 +101,7 @@ function IntakeEditDialogBody({
const { t } = useTranslations();
const fmt = useFormatters();
const queryClient = useQueryClient();
+ const formId = useId();
const [takenAt, setTakenAt] = useState(() =>
toDateTimeLocal(event?.takenAt ?? null),
);
@@ -145,16 +140,38 @@ function IntakeEditDialogBody({
}
return (
-
+
);
}
diff --git a/src/components/medications/intake-import-dialog.tsx b/src/components/medications/intake-import-dialog.tsx
index 591bf85eb..62589547f 100644
--- a/src/components/medications/intake-import-dialog.tsx
+++ b/src/components/medications/intake-import-dialog.tsx
@@ -22,12 +22,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { Loader2, MoreHorizontal, RotateCcw, Upload } from "lucide-react";
import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
+import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import {
DropdownMenu,
DropdownMenuContent,
@@ -143,12 +138,58 @@ export function IntakeImportDialog({
}
return (
-
+
);
}
diff --git a/src/components/medications/take-all-due-dialog.tsx b/src/components/medications/take-all-due-dialog.tsx
index 7a2913d31..3954d2760 100644
--- a/src/components/medications/take-all-due-dialog.tsx
+++ b/src/components/medications/take-all-due-dialog.tsx
@@ -19,14 +19,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { CheckCheck, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
+import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { useTranslations } from "@/lib/i18n/context";
import { formatDose } from "@/lib/medications/format-dose";
import { formatTimeWindowRange } from "@/lib/time-window-format";
@@ -65,49 +58,17 @@ export function TakeAllDueDialog({
}
return (
- {
// Refuse to close while the take loop is in flight.
if (submitting && !next) return;
onOpenChange(next);
}}
- >
-
-
- {t("medications.takeAllDue.dialogTitle")}
-
- {t("medications.takeAllDue.dialogDescription")}
-
-
-
- {dueMedications.map((med) => (
- -
-
- {med.name}
- {med.dose ? (
-
- {" "}
- — {formatDose(med.dose, t)}
-
- ) : null}
-
- {med.window ? (
-
- {formatTimeWindowRange(
- med.window.start,
- med.window.end,
- locale,
- )}
-
- ) : null}
-
- ))}
-
-
+ title={t("medications.takeAllDue.dialogTitle")}
+ description={t("medications.takeAllDue.dialogDescription")}
+ footer={
+ <>
-
-
+ >
+ }
+ >
+
+ {dueMedications.map((med) => (
+ -
+
+ {med.name}
+ {med.dose ? (
+
+ {" "}
+ — {formatDose(med.dose, t)}
+
+ ) : null}
+
+ {med.window ? (
+
+ {formatTimeWindowRange(
+ med.window.start,
+ med.window.end,
+ locale,
+ )}
+
+ ) : null}
+
+ ))}
+
+
);
}