Summary
The task detail modal's due date is currently a single type="datetime-local" input (frontend/src/components/TasksPanel/TasksPanel.tsx:160-171), backed by Task.due_at — a single DateTimeField (progression/models.py:1062) with no separate date/time concept. Split this into two independent inputs — a date field and a time field — with the following behavior:
- Time set, no date set → default the date to today.
- Date set, no time set → the time field should visibly show as unset in the UI, but the value persisted to
due_at on the backend should be end-of-day, 23:59 (on the selected date), not midnight.
Current implementation
- Frontend: one
<input type="datetime-local">, converted via toDatetimeLocalValue/fromDatetimeLocalValue (frontend/src/utils/formatUtils.ts:106-126), committed on blur straight to due_at.
- Backend:
due_at = models.DateTimeField(null=True, blank=True) (progression/models.py:1062), exposed as-is through TaskSerializer (progression/serializers.py:355-384) with no split representation and no date/time-specific validation or defaulting.
Proposed approach
Frontend
- Replace the single
datetime-local input with two inputs: type="date" and type="time".
- When the time input gets a value and the date input is empty, default the date input to today (local) before committing.
- When only the date input has a value, leave the time input visually empty/unset, but send
23:59 for that date as the effective time when constructing the due_at payload.
- Add/adjust helpers near
toDatetimeLocalValue/fromDatetimeLocalValue in formatUtils.ts (or new ones) to convert between due_at and the split date/time pair, including the defaulting rules above. Update formatUtils.test.ts accordingly.
Backend
- Decide whether the 23:59-when-date-only rule is enforced only on the frontend (simplest — it's already sending a full ISO datetime for
due_at either way) or also defensively in TaskSerializer.validate. Frontend-only is likely sufficient since due_at remains a single DateTimeField; call this out explicitly in the PR either way so it's a deliberate choice, not an oversight.
Notes
Summary
The task detail modal's due date is currently a single
type="datetime-local"input (frontend/src/components/TasksPanel/TasksPanel.tsx:160-171), backed byTask.due_at— a singleDateTimeField(progression/models.py:1062) with no separate date/time concept. Split this into two independent inputs — a date field and a time field — with the following behavior:due_aton the backend should be end-of-day,23:59(on the selected date), not midnight.Current implementation
<input type="datetime-local">, converted viatoDatetimeLocalValue/fromDatetimeLocalValue(frontend/src/utils/formatUtils.ts:106-126), committed on blur straight todue_at.due_at = models.DateTimeField(null=True, blank=True)(progression/models.py:1062), exposed as-is throughTaskSerializer(progression/serializers.py:355-384) with no split representation and no date/time-specific validation or defaulting.Proposed approach
Frontend
datetime-localinput with two inputs:type="date"andtype="time".23:59for that date as the effective time when constructing thedue_atpayload.toDatetimeLocalValue/fromDatetimeLocalValueinformatUtils.ts(or new ones) to convert betweendue_atand the split date/time pair, including the defaulting rules above. UpdateformatUtils.test.tsaccordingly.Backend
due_ateither way) or also defensively inTaskSerializer.validate. Frontend-only is likely sufficient sincedue_atremains a singleDateTimeField; call this out explicitly in the PR either way so it's a deliberate choice, not an oversight.Notes
isOverdue(formatUtils.ts:128-133) andformatDueAt(formatUtils.ts:78-104, see Extend formatDueAt with weeks/months granularity for future and past due dates #760) both consumedue_atdirectly and shouldn't need changes as long as the split fields keep resolving to a single ISOdue_atvalue.