diff --git a/frontend/components/chat/patient-form-dialog.tsx b/frontend/components/chat/patient-form-dialog.tsx index 184eb38..742cfbc 100644 --- a/frontend/components/chat/patient-form-dialog.tsx +++ b/frontend/components/chat/patient-form-dialog.tsx @@ -1,9 +1,15 @@ "use client"; -import { Plus, RefreshCw, X } from "lucide-react"; +import { CalendarIcon, Plus, RefreshCw, X } from "lucide-react"; import { type FormEvent, type ReactNode, useState } from "react"; import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; +import { + Popover, + PopoverPopup, + PopoverTrigger, +} from "@/components/ui/popover"; import { Dialog, DialogClose, @@ -112,13 +118,76 @@ function initialsFromName(name: string): string { .toUpperCase(); } -const today = () => - new Date().toLocaleDateString("en-US", { +const formatDate = (date: Date) => + date.toLocaleDateString("en-US", { month: "short", day: "2-digit", year: "numeric", }); +const today = () => formatDate(new Date()); + +// Patient dates are stored as formatted strings (e.g. "Jun 02, 2026"); parse one +// back to a Date so the calendar can highlight the current selection. +function parseDate(value: string): Date | undefined { + const trimmed = value.trim(); + if (!trimmed) { + return undefined; + } + const parsed = new Date(trimmed); + return Number.isNaN(parsed.getTime()) ? undefined : parsed; +} + +// Calendar-backed date field that reads/writes the same formatted string the rest +// of the form uses. +function DatePicker({ + value, + onChange, + ariaLabel, + placeholder = "Pick a date", + className, +}: { + value: string; + onChange: (value: string) => void; + ariaLabel: string; + placeholder?: string; + className?: string; +}) { + const [open, setOpen] = useState(false); + + return ( + + + } + > + + {value || placeholder} + + + { + onChange(date ? formatDate(date) : ""); + setOpen(false); + }} + selected={parseDate(value)} + /> + + + ); +} + export function PatientFormDialog({ open, onOpenChange, @@ -429,10 +498,10 @@ export function PatientFormDialog({ placeholder="Diagnosis" value={row.label} /> - set({ since: event.target.value })} + set({ since })} placeholder="Since" value={row.since} /> @@ -488,10 +557,10 @@ export function PatientFormDialog({ placeholder="Type" value={row.type} /> - set({ date: event.target.value })} + set({ date })} placeholder="Date" value={row.date} /> diff --git a/frontend/components/ui/calendar.tsx b/frontend/components/ui/calendar.tsx new file mode 100644 index 0000000..75a75af --- /dev/null +++ b/frontend/components/ui/calendar.tsx @@ -0,0 +1,139 @@ +"use client"; + +import { + ChevronLeftIcon, + ChevronRightIcon, + ChevronsUpDownIcon, +} from "lucide-react"; +import type * as React from "react"; +import { DayPicker } from "react-day-picker"; +import { cn } from "@/lib/utils"; + +const buttonClassNames = + "relative flex size-(--cell-size) text-base sm:text-sm items-center justify-center rounded-lg text-foreground not-in-data-selected:hover:bg-accent disabled:pointer-events-none disabled:opacity-64 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"; + +export function Calendar({ + className, + classNames, + showOutsideDays = true, + components: userComponents, + mode = "single", + ...props +}: React.ComponentProps): React.ReactElement { + const defaultClassNames = { + button_next: buttonClassNames, + button_previous: buttonClassNames, + caption_label: + "text-base sm:text-sm font-medium flex items-center gap-2 h-full", + day: "size-(--cell-size) text-sm py-px", + day_button: cn( + buttonClassNames, + "in-data-disabled:pointer-events-none in-[.range-middle]:rounded-none in-[.range-end:not(.range-start)]:rounded-s-none in-[.range-start:not(.range-end)]:rounded-e-none in-[.range-middle]:in-data-selected:bg-accent in-data-selected:bg-primary in-[.range-middle]:in-data-selected:text-foreground in-data-disabled:text-muted-foreground/72 in-data-outside:text-muted-foreground/72 in-data-selected:in-data-outside:text-primary-foreground in-data-selected:text-primary-foreground in-data-disabled:line-through outline-none in-[[data-selected]:not(.range-middle)]:transition-[color,background-color,border-radius,box-shadow] focus-visible:z-1 focus-visible:ring-[3px] focus-visible:ring-ring/50", + ), + dropdown: "absolute bg-popover inset-0 opacity-0", + dropdown_root: + "relative has-focus:border-ring has-focus:ring-ring/50 has-focus:ring-[3px] border border-input shadow-xs/5 rounded-lg px-[calc(--spacing(3)-1px)] h-9 sm:h-8 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-me-1", + dropdowns: + "w-full flex items-center text-base sm:text-sm justify-center h-(--cell-size) gap-1.5 *:[span]:font-medium", + hidden: "invisible", + month: "w-full", + month_caption: + "relative mx-(--cell-size) px-1 mb-1 flex h-(--cell-size) items-center justify-center z-2", + months: "relative flex flex-col sm:flex-row gap-2", + nav: "absolute top-0 flex w-full justify-between z-1", + outside: + "text-muted-foreground data-selected:bg-accent/50 data-selected:text-muted-foreground", + range_end: "range-end", + range_middle: "range-middle", + range_start: "range-start", + today: + "*:after:pointer-events-none *:after:absolute *:after:bottom-1 *:after:start-1/2 *:after:z-1 *:after:size-[3px] *:after:-translate-x-1/2 *:after:rounded-full *:after:bg-primary [&[data-selected]:not(.range-middle)>*]:after:bg-background [&[data-disabled]>*]:after:bg-foreground/30 *:after:transition-colors", + week_number: + "size-(--cell-size) p-0 text-xs font-medium text-muted-foreground/72", + weekday: + "size-(--cell-size) p-0 text-xs font-medium text-muted-foreground/72", + }; + const mergedClassNames: typeof defaultClassNames = Object.keys( + defaultClassNames, + ).reduce( + (acc, key) => { + const userClass = classNames?.[key as keyof typeof classNames]; + const baseClass = + defaultClassNames[key as keyof typeof defaultClassNames]; + + acc[key as keyof typeof defaultClassNames] = userClass + ? cn(baseClass, userClass) + : baseClass; + + return acc; + }, + { ...defaultClassNames } as typeof defaultClassNames, + ); + + const defaultComponents = { + Chevron: ({ + className, + orientation, + ...props + }: { + className?: string; + orientation?: "left" | "right" | "up" | "down"; + }): React.ReactElement => { + if (orientation === "left") { + return ( +