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 (
+
+ );
+ }
+
+ if (orientation === "right") {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ },
+ };
+
+ const mergedComponents = {
+ ...defaultComponents,
+ ...userComponents,
+ };
+
+ const dayPickerProps = {
+ className: cn(
+ "w-fit [--cell-size:--spacing(10)] sm:[--cell-size:--spacing(9)]",
+ className,
+ ),
+ classNames: mergedClassNames,
+ components: mergedComponents,
+ "data-slot": "calendar",
+ formatters: {
+ formatMonthDropdown: (date: Date) =>
+ date.toLocaleString("default", { month: "short" }),
+ } as React.ComponentProps["formatters"],
+ mode,
+ showOutsideDays,
+ ...props,
+ };
+
+ return (
+ )}
+ />
+ );
+}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index ad4b465..ca21295 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -33,6 +33,7 @@
"next": "16.2.6",
"next-themes": "^0.4.6",
"react": "19.2.4",
+ "react-day-picker": "^10.0.1",
"react-dom": "19.2.4",
"react-i18next": "^17.0.8",
"react-jsx-parser": "^2.4.1",
@@ -757,6 +758,12 @@
"integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==",
"license": "Apache-2.0"
},
+ "node_modules/@date-fns/tz": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.5.0.tgz",
+ "integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==",
+ "license": "MIT"
+ },
"node_modules/@dotenvx/dotenvx": {
"version": "1.69.2",
"resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.69.2.tgz",
@@ -6170,6 +6177,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/date-fns": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz",
+ "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
"node_modules/dayjs": {
"version": "1.11.21",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
@@ -12057,6 +12074,32 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-day-picker": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-10.0.1.tgz",
+ "integrity": "sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w==",
+ "license": "MIT",
+ "dependencies": {
+ "@date-fns/tz": "^1.4.1",
+ "date-fns": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/sponsors/gpbl"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8.0",
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-dom": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index ff07095..25ce945 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -34,6 +34,7 @@
"next": "16.2.6",
"next-themes": "^0.4.6",
"react": "19.2.4",
+ "react-day-picker": "^10.0.1",
"react-dom": "19.2.4",
"react-i18next": "^17.0.8",
"react-jsx-parser": "^2.4.1",