From 506fbf49887189bae64d7b0a0d329ef5c8e69503 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 20:37:50 +0000 Subject: [PATCH] fix(web): render date-only due dates without a time component When a task is given a due date without a time, the value is stored at the end of the day (23:59:59.999) as a date-only sentinel. The task views, however, always rendered the due date with that time and the inline table editor forced a date+time mode, so a meaningless time appeared whenever only a date was selected (issue #215). Add `DueDateUtils.isDateOnly` to detect the sentinel and: - hide the time in the task list and task card due-date displays for date-only due dates - let the inline due-date editor toggle between date-only and date+time via FlexibleDateTimeInput, defaulting to the current value's granularity https://claude.ai/code/session_018XwCELPc4u4QsnxXqAhNa4 --- web/components/tables/TaskList.tsx | 3 ++ .../InTableDateTimeEditPopUp.tsx | 45 ++++++++++++++----- web/components/tasks/TaskCardView.tsx | 3 +- web/utils/dueDate.test.ts | 25 +++++++++++ web/utils/dueDate.ts | 20 ++++++++- 5 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 web/utils/dueDate.test.ts diff --git a/web/components/tables/TaskList.tsx b/web/components/tables/TaskList.tsx index 938a0d05..1fd338d0 100644 --- a/web/components/tables/TaskList.tsx +++ b/web/components/tables/TaskList.tsx @@ -665,6 +665,8 @@ export const TaskList = forwardRef(({ tasks: initial { if (next === row.original.dueDate) { return @@ -685,6 +687,7 @@ export const TaskList = forwardRef(({ tasks: initial diff --git a/web/components/tables/in-table-edit/InTableDateTimeEditPopUp.tsx b/web/components/tables/in-table-edit/InTableDateTimeEditPopUp.tsx index be771c7b..1933541a 100644 --- a/web/components/tables/in-table-edit/InTableDateTimeEditPopUp.tsx +++ b/web/components/tables/in-table-edit/InTableDateTimeEditPopUp.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from 'react' import { useState, type ComponentProps } from 'react' import type { ButtonProps } from '@helpwave/hightide' -import { Button, DateTimeInput, PopUp, PopUpContext, PopUpOpener, PopUpRoot } from '@helpwave/hightide' +import { Button, DateTimeInput, FlexibleDateTimeInput, PopUp, PopUpContext, PopUpOpener, PopUpRoot } from '@helpwave/hightide' import { useTasksTranslation } from '@/i18n/useTasksTranslation' import clsx from 'clsx' @@ -21,6 +21,12 @@ type InTableDateTimeEditPopUpProps = { buttonProps?: ButtonProps, children: ReactNode, mode?: 'date' | 'dateTime', + /** + * When true, lets the user choose between a date-only and a date+time value via the + * input's built in toggle. A date-only selection is stored at the end of the day so it + * carries no meaningful time. `mode` is used as the initial mode. + */ + flexible?: boolean, dateTimeInputProps?: Omit< ComponentProps, 'value' | 'onValueChange' | 'onEditComplete' | 'mode' @@ -33,6 +39,7 @@ export function InTableDateTimeEditPopUp({ buttonProps, children, mode = 'dateTime', + flexible = false, dateTimeInputProps, options = { horizontalAlignment: 'afterStart', verticalAlignment: 'afterEnd' }, className = 'p-2', @@ -68,17 +75,31 @@ export function InTableDateTimeEditPopUp({ }} e.stopPropagation()}> - { - setDraft(next) - }} - onEditComplete={v => { - setDraft(v) - }} - /> + {flexible ? ( + { + setDraft(next) + }} + onEditComplete={v => { + setDraft(v) + }} + /> + ) : ( + { + setDraft(next) + }} + onEditComplete={v => { + setDraft(v) + }} + /> + )} {({ setIsOpen }) => (
diff --git a/web/components/tasks/TaskCardView.tsx b/web/components/tasks/TaskCardView.tsx index 22876072..24a57c75 100644 --- a/web/components/tasks/TaskCardView.tsx +++ b/web/components/tasks/TaskCardView.tsx @@ -3,6 +3,7 @@ import { AvatarStatusComponent } from '@/components/AvatarStatusComponent' import { Clock, Combine, User, Users, Flag } from 'lucide-react' import clsx from 'clsx' import { DateDisplay } from '@/components/Date/DateDisplay' +import { DueDateUtils } from '@/utils/dueDate' import { LocationChipsBySetting } from '@/components/patients/LocationChipsBySetting' import type { TaskViewModel } from '@/components/tables/TaskList' import { useRouter } from 'next/router' @@ -285,7 +286,7 @@ export const TaskCardView = ({ task, onToggleDone: _onToggleDone, onClick, showA {dueDate && (
- +
)}
diff --git a/web/utils/dueDate.test.ts b/web/utils/dueDate.test.ts new file mode 100644 index 00000000..1df3e82f --- /dev/null +++ b/web/utils/dueDate.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest' +import { DueDateUtils } from '@/utils/dueDate' + +describe('DueDateUtils.isDateOnly', () => { + it('returns true for a date-only due date (end of day sentinel)', () => { + const date = new Date(2026, 5, 10, 23, 59, 59, 999) + expect(DueDateUtils.isDateOnly(date)).toBe(true) + }) + + it('returns false for a due date with a real time of day', () => { + const date = new Date(2026, 5, 10, 9, 30, 0, 0) + expect(DueDateUtils.isDateOnly(date)).toBe(false) + }) + + it('returns false when the time is only partially matching the sentinel', () => { + const date = new Date(2026, 5, 10, 23, 59, 59, 0) + expect(DueDateUtils.isDateOnly(date)).toBe(false) + }) + + it('returns false for null, undefined and invalid input', () => { + expect(DueDateUtils.isDateOnly(null)).toBe(false) + expect(DueDateUtils.isDateOnly(undefined)).toBe(false) + expect(DueDateUtils.isDateOnly('not-a-date')).toBe(false) + }) +}) diff --git a/web/utils/dueDate.ts b/web/utils/dueDate.ts index d2f7f705..5b284b91 100644 --- a/web/utils/dueDate.ts +++ b/web/utils/dueDate.ts @@ -1,4 +1,22 @@ +// A "date only" due date (no time-of-day selected) is represented by fixing the time +// component to the end of the day (23:59:59.999). This matches the sentinel used by +// hightide's FlexibleDateTimeInput and lets us render such due dates without a +// meaningless time component. +const DATE_ONLY_HOURS = 23 +const DATE_ONLY_MINUTES = 59 +const DATE_ONLY_SECONDS = 59 +const DATE_ONLY_MILLISECONDS = 999 + export const DueDateUtils = { + isDateOnly: (dueDate: Date | string | undefined | null): boolean => { + if (!dueDate) return false + const date = new Date(dueDate) + if (Number.isNaN(date.getTime())) return false + return date.getHours() === DATE_ONLY_HOURS + && date.getMinutes() === DATE_ONLY_MINUTES + && date.getSeconds() === DATE_ONLY_SECONDS + && date.getMilliseconds() === DATE_ONLY_MILLISECONDS + }, isOverdue: (dueDate: Date | undefined | null, done: boolean): boolean => { if (!dueDate || done) return false return new Date(dueDate).getTime() < Date.now() @@ -10,4 +28,4 @@ export const DueDateUtils = { const oneHour = 60 * 60 * 1000 return dueTime > now && dueTime - now <= oneHour } -} \ No newline at end of file +}