mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
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.
This commit is contained in:
1 parent
1c57da7d44
commit
96c99f4dfa
6 files changed
+234
-286
No files matched your search
@@ -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 (
|
||||
<Dialog open={open} onOpenChange={(o) => !busy && onOpenChange(o)}>
|
||||
<DialogContent data-slot="ledger-add-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t("medications.detail.verlauf.addDialog.title")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("medications.detail.verlauf.addDialog.description")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ResponsiveSheet
|
||||
open={open}
|
||||
onOpenChange={(o) => !busy && onOpenChange(o)}
|
||||
title={t("medications.detail.verlauf.addDialog.title")}
|
||||
description={t("medications.detail.verlauf.addDialog.description")}
|
||||
footer={
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={busy}
|
||||
>
|
||||
{t("medications.detail.verlauf.addDialog.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
form={formId}
|
||||
disabled={busy}
|
||||
aria-busy={busy || undefined}
|
||||
data-slot="ledger-add-submit"
|
||||
>
|
||||
{busy && (
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("medications.detail.verlauf.addDialog.submit")}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{/* v1.16.4 — a real form so Enter in the datetime field submits;
|
||||
the buttons carry explicit types so cancel never submits. The
|
||||
nested nudge dialog renders through a portal, so its buttons
|
||||
never land inside this form. */}
|
||||
the buttons carry explicit types so cancel never submits. */}
|
||||
<form
|
||||
id={formId}
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -263,49 +275,17 @@ export function LedgerAddDialog({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={busy}
|
||||
>
|
||||
{t("medications.detail.verlauf.addDialog.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
aria-busy={busy || undefined}
|
||||
data-slot="ledger-add-submit"
|
||||
>
|
||||
{busy && (
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("medications.detail.verlauf.addDialog.submit")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
{/* The "diesem Slot zuordnen?" nudge — pin onto the slot, or keep the
|
||||
take as a standalone ad-hoc entry. */}
|
||||
{nudgeOpen && nearest && (
|
||||
<Dialog
|
||||
<ResponsiveSheet
|
||||
open={nudgeOpen}
|
||||
onOpenChange={(o) => !busy && setNudgeOpen(o)}
|
||||
>
|
||||
<DialogContent data-slot="ledger-add-nudge-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t("medications.detail.verlauf.nudge.title")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("medications.detail.verlauf.nudge.body", {
|
||||
slot: nearest.slotHm,
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
title={t("medications.detail.verlauf.nudge.title")}
|
||||
footer={
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => void write(false)}
|
||||
@@ -325,11 +305,16 @@ export function LedgerAddDialog({
|
||||
)}
|
||||
{t("medications.detail.verlauf.nudge.pin")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("medications.detail.verlauf.nudge.body", {
|
||||
slot: nearest.slotHm,
|
||||
})}
|
||||
</p>
|
||||
</ResponsiveSheet>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</ResponsiveSheet>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<Dialog open={!!event} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent data-slot="intake-edit-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t("medications.detail.intake.edit.dialogTitle")}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<ResponsiveSheet
|
||||
open={!!event}
|
||||
onOpenChange={(open) => !open && onClose()}
|
||||
title={t("medications.detail.intake.edit.dialogTitle")}
|
||||
footer={
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
disabled={busy}
|
||||
>
|
||||
{t("medications.detail.intake.edit.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
form={formId}
|
||||
disabled={busy}
|
||||
aria-busy={busy || undefined}
|
||||
>
|
||||
{busy && (
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("medications.detail.intake.edit.save")}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{/* v1.16.4 — a real form so Enter in the datetime field submits;
|
||||
the buttons carry explicit types so cancel never submits. */}
|
||||
<form
|
||||
id={formId}
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -208,24 +225,7 @@ function IntakeEditDialogBody({
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
disabled={busy}
|
||||
>
|
||||
{t("medications.detail.intake.edit.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" disabled={busy} aria-busy={busy || undefined}>
|
||||
{busy && (
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("medications.detail.intake.edit.save")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</ResponsiveSheet>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<Dialog open={!!medicationId} onOpenChange={() => handleClose()}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("medications.importIntakes")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<ResponsiveSheet
|
||||
open={!!medicationId}
|
||||
onOpenChange={() => handleClose()}
|
||||
title={t("medications.importIntakes")}
|
||||
contentWidth="lg"
|
||||
footer={
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="mr-auto h-9 w-9"
|
||||
disabled={importing}
|
||||
aria-label={t("common.moreOptions")}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={resetImportForm}>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
{t("common.reset")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={importing}
|
||||
className="min-h-11 sm:min-h-9"
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
disabled={importing || !jsonText.trim()}
|
||||
aria-busy={importing || undefined}
|
||||
className="min-h-11 sm:min-h-9"
|
||||
>
|
||||
{importing ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Upload className="h-4 w-4" />
|
||||
)}
|
||||
{t("common.import")}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("medications.importDescription")}
|
||||
</p>
|
||||
@@ -194,54 +235,7 @@ export function IntakeImportDialog({
|
||||
{result}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9"
|
||||
disabled={importing}
|
||||
aria-label={t("common.moreOptions")}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={resetImportForm}>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
{t("common.reset")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={importing}
|
||||
className="min-h-11 sm:min-h-9"
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
disabled={importing || !jsonText.trim()}
|
||||
aria-busy={importing || undefined}
|
||||
className="min-h-11 sm:min-h-9"
|
||||
>
|
||||
{importing ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Upload className="h-4 w-4" />
|
||||
)}
|
||||
{t("common.import")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</ResponsiveSheet>
|
||||
);
|
||||
}
|
||||
@@ -4,14 +4,7 @@ import { useState } from "react";
|
||||
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { InjectionSitePicker } from "@/components/medications/injection-site-picker";
|
||||
import {
|
||||
@@ -85,7 +78,7 @@ export function LogInjectionSiteDialog({
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
<ResponsiveSheet
|
||||
open={open}
|
||||
onOpenChange={(next) => {
|
||||
// Hold the dialog while the PATCH is in flight so a backdrop tap /
|
||||
@@ -93,18 +86,31 @@ export function LogInjectionSiteDialog({
|
||||
if (submitting) return;
|
||||
if (!next) onSkip();
|
||||
}}
|
||||
title={t("medications.logInjectionSiteTitle")}
|
||||
description={t("medications.logInjectionSiteDescription", {
|
||||
name: medicationName,
|
||||
})}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="outline" onClick={onSkip} disabled={submitting}>
|
||||
{t("medications.logInjectionSiteSkip")}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={selected === null || submitting}
|
||||
aria-busy={submitting || undefined}
|
||||
onClick={() => {
|
||||
void handleConfirm();
|
||||
}}
|
||||
>
|
||||
{submitting ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : null}
|
||||
{t("medications.logInjectionSiteConfirm")}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("medications.logInjectionSiteTitle")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("medications.logInjectionSiteDescription", {
|
||||
name: medicationName,
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{allowed.length === 0 ? (
|
||||
{allowed.length === 0 ? (
|
||||
<p className="text-muted-foreground py-4 text-center text-sm">
|
||||
{t("medications.logInjectionSiteNoneAvailable")}
|
||||
</p>
|
||||
@@ -161,25 +167,6 @@ export function LogInjectionSiteDialog({
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-2">
|
||||
<Button variant="outline" onClick={onSkip} disabled={submitting}>
|
||||
{t("medications.logInjectionSiteSkip")}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={selected === null || submitting}
|
||||
aria-busy={submitting || undefined}
|
||||
onClick={() => {
|
||||
void handleConfirm();
|
||||
}}
|
||||
>
|
||||
{submitting ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : null}
|
||||
{t("medications.logInjectionSiteConfirm")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</ResponsiveSheet>
|
||||
);
|
||||
}
|
||||
@@ -19,20 +19,13 @@
|
||||
* the form state + the medication/slot pickers.
|
||||
*/
|
||||
|
||||
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 { Input } from "@/components/ui/input";
|
||||
import { DateTimeField } from "@/components/ui/date-time-field";
|
||||
@@ -119,6 +112,7 @@ export function LogIntakeDialog({
|
||||
}: LogIntakeDialogProps) {
|
||||
const { t } = useTranslations();
|
||||
const queryClient = useQueryClient();
|
||||
const formId = useId();
|
||||
|
||||
const [medicationId, setMedicationId] = useState<string>(
|
||||
() => medications[0]?.id ?? "",
|
||||
@@ -208,18 +202,39 @@ export function LogIntakeDialog({
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(o) => !busy && onOpenChange(o)}>
|
||||
<DialogContent data-slot="log-intake-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("medications.logIntake.dialogTitle")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("medications.logIntake.dialogDescription")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ResponsiveSheet
|
||||
open={open}
|
||||
onOpenChange={(o) => !busy && onOpenChange(o)}
|
||||
title={t("medications.logIntake.dialogTitle")}
|
||||
description={t("medications.logIntake.dialogDescription")}
|
||||
footer={
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={busy}
|
||||
>
|
||||
{t("medications.logIntake.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
form={formId}
|
||||
disabled={busy || medications.length === 0 || !medicationId}
|
||||
aria-busy={busy || undefined}
|
||||
>
|
||||
{busy && (
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("medications.logIntake.submit")}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{/* v1.16.4 — a real form so Enter in the dose / datetime fields
|
||||
submits; mirrors the intake-edit and dose-history-add dialogs. */}
|
||||
<form
|
||||
id={formId}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (busy || medications.length === 0 || !medicationId) return;
|
||||
@@ -326,29 +341,7 @@ export function LogIntakeDialog({
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={busy}
|
||||
>
|
||||
{t("medications.logIntake.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={busy || medications.length === 0 || !medicationId}
|
||||
aria-busy={busy || undefined}
|
||||
>
|
||||
{busy && (
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
)}
|
||||
{t("medications.logIntake.submit")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</ResponsiveSheet>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<Dialog
|
||||
<ResponsiveSheet
|
||||
open={open}
|
||||
onOpenChange={(next) => {
|
||||
// Refuse to close while the take loop is in flight.
|
||||
if (submitting && !next) return;
|
||||
onOpenChange(next);
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("medications.takeAllDue.dialogTitle")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("medications.takeAllDue.dialogDescription")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ul className="space-y-2" data-testid="take-all-due-list">
|
||||
{dueMedications.map((med) => (
|
||||
<li
|
||||
key={med.id}
|
||||
className="flex items-baseline justify-between gap-3 text-sm"
|
||||
>
|
||||
<span className="min-w-0 truncate">
|
||||
<span className="font-medium">{med.name}</span>
|
||||
{med.dose ? (
|
||||
<span className="text-muted-foreground">
|
||||
{" "}
|
||||
— {formatDose(med.dose, t)}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
{med.window ? (
|
||||
<span className="text-muted-foreground shrink-0 text-xs tabular-nums">
|
||||
{formatTimeWindowRange(
|
||||
med.window.start,
|
||||
med.window.end,
|
||||
locale,
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<DialogFooter>
|
||||
title={t("medications.takeAllDue.dialogTitle")}
|
||||
description={t("medications.takeAllDue.dialogDescription")}
|
||||
footer={
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -136,8 +97,36 @@ export function TakeAllDueDialog({
|
||||
count: dueMedications.length,
|
||||
})}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ul className="space-y-2" data-testid="take-all-due-list">
|
||||
{dueMedications.map((med) => (
|
||||
<li
|
||||
key={med.id}
|
||||
className="flex items-baseline justify-between gap-3 text-sm"
|
||||
>
|
||||
<span className="min-w-0 truncate">
|
||||
<span className="font-medium">{med.name}</span>
|
||||
{med.dose ? (
|
||||
<span className="text-muted-foreground">
|
||||
{" "}
|
||||
— {formatDose(med.dose, t)}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
{med.window ? (
|
||||
<span className="text-muted-foreground shrink-0 text-xs tabular-nums">
|
||||
{formatTimeWindowRange(
|
||||
med.window.start,
|
||||
med.window.end,
|
||||
locale,
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</ResponsiveSheet>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user