Skip to content

Commit af86874

Browse files
felixeversclaude
andauthored
fix(web): render date-only due dates without a time component (#248)
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 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5b43bc3 commit af86874

5 files changed

Lines changed: 82 additions & 14 deletions

File tree

web/components/tables/TaskList.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,8 @@ export const TaskList = forwardRef<TaskListRef, TaskListProps>(({ tasks: initial
665665
<TaskRowRefreshingGate taskId={row.original.id}>
666666
<InTableDateTimeEditPopUp
667667
value={row.original.dueDate}
668+
flexible
669+
mode={DueDateUtils.isDateOnly(row.original.dueDate) ? 'date' : 'dateTime'}
668670
onUpdate={next => {
669671
if (next === row.original.dueDate) {
670672
return
@@ -685,6 +687,7 @@ export const TaskList = forwardRef<TaskListRef, TaskListProps>(({ tasks: initial
685687
<DateDisplay
686688
date={row.original.dueDate}
687689
mode="absolute"
690+
showTime={!DueDateUtils.isDateOnly(row.original.dueDate)}
688691
className={clsx(colorClass, 'truncate')}
689692
/>
690693
<Edit2 className="size-4 min-w-4 group-hover:block hidden"/>

web/components/tables/in-table-edit/InTableDateTimeEditPopUp.tsx

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ReactNode } from 'react'
22
import { useState, type ComponentProps } from 'react'
33
import type { ButtonProps } from '@helpwave/hightide'
4-
import { Button, DateTimeInput, PopUp, PopUpContext, PopUpOpener, PopUpRoot } from '@helpwave/hightide'
4+
import { Button, DateTimeInput, FlexibleDateTimeInput, PopUp, PopUpContext, PopUpOpener, PopUpRoot } from '@helpwave/hightide'
55
import { useTasksTranslation } from '@/i18n/useTasksTranslation'
66
import clsx from 'clsx'
77

@@ -21,6 +21,12 @@ type InTableDateTimeEditPopUpProps = {
2121
buttonProps?: ButtonProps,
2222
children: ReactNode,
2323
mode?: 'date' | 'dateTime',
24+
/**
25+
* When true, lets the user choose between a date-only and a date+time value via the
26+
* input's built in toggle. A date-only selection is stored at the end of the day so it
27+
* carries no meaningful time. `mode` is used as the initial mode.
28+
*/
29+
flexible?: boolean,
2430
dateTimeInputProps?: Omit<
2531
ComponentProps<typeof DateTimeInput>,
2632
'value' | 'onValueChange' | 'onEditComplete' | 'mode'
@@ -33,6 +39,7 @@ export function InTableDateTimeEditPopUp({
3339
buttonProps,
3440
children,
3541
mode = 'dateTime',
42+
flexible = false,
3643
dateTimeInputProps,
3744
options = { horizontalAlignment: 'afterStart', verticalAlignment: 'afterEnd' },
3845
className = 'p-2',
@@ -68,17 +75,31 @@ export function InTableDateTimeEditPopUp({
6875
}}
6976
</PopUpOpener>
7077
<PopUp options={options} className={clsx(className, 'flex-col-2 items-end')} onClick={e => e.stopPropagation()}>
71-
<DateTimeInput
72-
mode={mode}
73-
{...dateTimeInputProps}
74-
value={draft}
75-
onValueChange={next => {
76-
setDraft(next)
77-
}}
78-
onEditComplete={v => {
79-
setDraft(v)
80-
}}
81-
/>
78+
{flexible ? (
79+
<FlexibleDateTimeInput
80+
defaultMode={mode === 'dateTime' ? 'dateTime' : 'date'}
81+
{...dateTimeInputProps}
82+
value={draft}
83+
onValueChange={next => {
84+
setDraft(next)
85+
}}
86+
onEditComplete={v => {
87+
setDraft(v)
88+
}}
89+
/>
90+
) : (
91+
<DateTimeInput
92+
mode={mode}
93+
{...dateTimeInputProps}
94+
value={draft}
95+
onValueChange={next => {
96+
setDraft(next)
97+
}}
98+
onEditComplete={v => {
99+
setDraft(v)
100+
}}
101+
/>
102+
)}
82103
<PopUpContext.Consumer>
83104
{({ setIsOpen }) => (
84105
<div className="flex-row-2 justify-end items-center gap-x-2">

web/components/tasks/TaskCardView.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { AvatarStatusComponent } from '@/components/AvatarStatusComponent'
33
import { Clock, Combine, User, Users, Flag } from 'lucide-react'
44
import clsx from 'clsx'
55
import { DateDisplay } from '@/components/Date/DateDisplay'
6+
import { DueDateUtils } from '@/utils/dueDate'
67
import { LocationChipsBySetting } from '@/components/patients/LocationChipsBySetting'
78
import type { TaskViewModel } from '@/components/tables/TaskList'
89
import { useRouter } from 'next/router'
@@ -285,7 +286,7 @@ export const TaskCardView = ({ task, onToggleDone: _onToggleDone, onClick, showA
285286
{dueDate && (
286287
<div className={clsx('flex items-center gap-2 min-w-0', dueDateColorClass)}>
287288
<Clock className="size-4 shrink-0" />
288-
<DateDisplay date={dueDate} mode="absolute" showTime={true} />
289+
<DateDisplay date={dueDate} mode="absolute" showTime={!DueDateUtils.isDateOnly(dueDate)} />
289290
</div>
290291
)}
291292
</div>

web/utils/dueDate.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { DueDateUtils } from '@/utils/dueDate'
3+
4+
describe('DueDateUtils.isDateOnly', () => {
5+
it('returns true for a date-only due date (end of day sentinel)', () => {
6+
const date = new Date(2026, 5, 10, 23, 59, 59, 999)
7+
expect(DueDateUtils.isDateOnly(date)).toBe(true)
8+
})
9+
10+
it('returns false for a due date with a real time of day', () => {
11+
const date = new Date(2026, 5, 10, 9, 30, 0, 0)
12+
expect(DueDateUtils.isDateOnly(date)).toBe(false)
13+
})
14+
15+
it('returns false when the time is only partially matching the sentinel', () => {
16+
const date = new Date(2026, 5, 10, 23, 59, 59, 0)
17+
expect(DueDateUtils.isDateOnly(date)).toBe(false)
18+
})
19+
20+
it('returns false for null, undefined and invalid input', () => {
21+
expect(DueDateUtils.isDateOnly(null)).toBe(false)
22+
expect(DueDateUtils.isDateOnly(undefined)).toBe(false)
23+
expect(DueDateUtils.isDateOnly('not-a-date')).toBe(false)
24+
})
25+
})

web/utils/dueDate.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,22 @@
1+
// A "date only" due date (no time-of-day selected) is represented by fixing the time
2+
// component to the end of the day (23:59:59.999). This matches the sentinel used by
3+
// hightide's FlexibleDateTimeInput and lets us render such due dates without a
4+
// meaningless time component.
5+
const DATE_ONLY_HOURS = 23
6+
const DATE_ONLY_MINUTES = 59
7+
const DATE_ONLY_SECONDS = 59
8+
const DATE_ONLY_MILLISECONDS = 999
9+
110
export const DueDateUtils = {
11+
isDateOnly: (dueDate: Date | string | undefined | null): boolean => {
12+
if (!dueDate) return false
13+
const date = new Date(dueDate)
14+
if (Number.isNaN(date.getTime())) return false
15+
return date.getHours() === DATE_ONLY_HOURS
16+
&& date.getMinutes() === DATE_ONLY_MINUTES
17+
&& date.getSeconds() === DATE_ONLY_SECONDS
18+
&& date.getMilliseconds() === DATE_ONLY_MILLISECONDS
19+
},
220
isOverdue: (dueDate: Date | undefined | null, done: boolean): boolean => {
321
if (!dueDate || done) return false
422
return new Date(dueDate).getTime() < Date.now()
@@ -10,4 +28,4 @@ export const DueDateUtils = {
1028
const oneHour = 60 * 60 * 1000
1129
return dueTime > now && dueTime - now <= oneHour
1230
}
13-
}
31+
}

0 commit comments

Comments
 (0)