fix: use has_time flag for due date time tracking

Replace the hours==0 && minutes==0 heuristic with an explicit has_time
bool field on Task. Existing files without the field deserialize as false
(date-only), preserving current behavior. Frontend components pass and
receive has_time through DateTimePicker's onchange callback.
This commit is contained in:
Tristan Michael 2026-04-01 01:04:01 -07:00
parent 970ed9aa1d
commit 72475a552a
6 changed files with 27 additions and 12 deletions

View file

@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
let { value = null, onchange, onclose }: { let { value = null, has_time = false, onchange, onclose }: {
value: string | null; value: string | null;
onchange: (iso: string | null) => void; has_time: boolean;
onchange: (iso: string | null, has_time: boolean) => void;
onclose: () => void; onclose: () => void;
} = $props(); } = $props();
@ -12,7 +13,7 @@
let viewYear = $state(existing ? existing.getFullYear() : now.getFullYear()); let viewYear = $state(existing ? existing.getFullYear() : now.getFullYear());
let viewMonth = $state(existing ? existing.getMonth() : now.getMonth()); let viewMonth = $state(existing ? existing.getMonth() : now.getMonth());
let selectedDay = $state(existing ? existing.getDate() : now.getDate()); let selectedDay = $state(existing ? existing.getDate() : now.getDate());
let includeTime = $state(existing ? (existing.getHours() !== 0 || existing.getMinutes() !== 0) : false); let includeTime = $state(has_time);
let selectedHour = $state(existing ? existing.getHours() : now.getHours()); let selectedHour = $state(existing ? existing.getHours() : now.getHours());
let selectedMinute = $state(existing ? existing.getMinutes() : 0); let selectedMinute = $state(existing ? existing.getMinutes() : 0);
let visible = $state(false); let visible = $state(false);
@ -66,12 +67,12 @@
const h = includeTime ? selectedHour : 0; const h = includeTime ? selectedHour : 0;
const m = includeTime ? selectedMinute : 0; const m = includeTime ? selectedMinute : 0;
const iso = new Date(viewYear, viewMonth, selectedDay, h, m).toISOString(); const iso = new Date(viewYear, viewMonth, selectedDay, h, m).toISOString();
onchange(iso); onchange(iso, includeTime);
dismiss(); dismiss();
} }
function clear() { function clear() {
onchange(null); onchange(null, false);
dismiss(); dismiss();
} }
</script> </script>

View file

@ -10,6 +10,7 @@
let title = $state(""); let title = $state("");
let description = $state(""); let description = $state("");
let dueDate = $state<string | null>(null); let dueDate = $state<string | null>(null);
let dueDateHasTime = $state(false);
let inputEl = $state<HTMLInputElement | null>(null); let inputEl = $state<HTMLInputElement | null>(null);
let showDatePicker = $state(false); let showDatePicker = $state(false);
@ -17,11 +18,12 @@
if (!title.trim()) return; if (!title.trim()) return;
const created = await app.createTask(title.trim(), description.trim() || undefined); const created = await app.createTask(title.trim(), description.trim() || undefined);
if (dueDate && created) { if (dueDate && created) {
await app.updateTask({ ...created, due_date: dueDate, updated_at: new Date().toISOString() }); await app.updateTask({ ...created, due_date: dueDate, has_time: dueDateHasTime, updated_at: new Date().toISOString() });
} }
title = ""; title = "";
description = ""; description = "";
dueDate = null; dueDate = null;
dueDateHasTime = false;
newTaskState.open = false; newTaskState.open = false;
} }
@ -30,11 +32,13 @@
title = ""; title = "";
description = ""; description = "";
dueDate = null; dueDate = null;
dueDateHasTime = false;
showDatePicker = false; showDatePicker = false;
} }
function handleDateChange(iso: string | null) { function handleDateChange(iso: string | null, hasTime: boolean = false) {
dueDate = iso; dueDate = iso;
dueDateHasTime = hasTime;
} }
function formatDateChip(iso: string): string { function formatDateChip(iso: string): string {
@ -43,8 +47,7 @@
const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const day = dayNames[d.getDay()]; const day = dayNames[d.getDay()];
const pad = (n: number) => String(n).padStart(2, "0"); const pad = (n: number) => String(n).padStart(2, "0");
const hasTime = d.getHours() !== 0 || d.getMinutes() !== 0; const timePart = dueDateHasTime ? `, ${pad(d.getHours())}:${pad(d.getMinutes())}` : "";
const timePart = hasTime ? `, ${pad(d.getHours())}:${pad(d.getMinutes())}` : "";
if (d.toDateString() === today.toDateString()) return `Today${timePart}`; if (d.toDateString() === today.toDateString()) return `Today${timePart}`;
return `${day}, ${pad(d.getDate())}/${pad(d.getMonth() + 1)}${timePart}`; return `${day}, ${pad(d.getDate())}/${pad(d.getMonth() + 1)}${timePart}`;
} }
@ -137,6 +140,7 @@
{#if showDatePicker} {#if showDatePicker}
<DateTimePicker <DateTimePicker
value={dueDate} value={dueDate}
has_time={dueDateHasTime}
onchange={handleDateChange} onchange={handleDateChange}
onclose={() => (showDatePicker = false)} onclose={() => (showDatePicker = false)}
/> />

View file

@ -42,8 +42,8 @@
debouncedSave({ description }); debouncedSave({ description });
} }
function handleDateChange(iso: string | null) { function handleDateChange(iso: string | null, hasTime: boolean = false) {
app.updateTask({ ...task, due_date: iso, updated_at: new Date().toISOString() }); app.updateTask({ ...task, due_date: iso, has_time: hasTime, updated_at: new Date().toISOString() });
} }
async function handleToggle() { async function handleToggle() {
@ -79,7 +79,7 @@
const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const day = dayNames[d.getDay()]; const day = dayNames[d.getDay()];
const pad = (n: number) => String(n).padStart(2, "0"); const pad = (n: number) => String(n).padStart(2, "0");
const hasTime = d.getHours() !== 0 || d.getMinutes() !== 0; const hasTime = task.has_time;
const timePart = hasTime ? `, ${pad(d.getHours())}:${pad(d.getMinutes())}` : ""; const timePart = hasTime ? `, ${pad(d.getHours())}:${pad(d.getMinutes())}` : "";
if (d.toDateString() === today.toDateString()) return `Today${timePart}`; if (d.toDateString() === today.toDateString()) return `Today${timePart}`;
return `${day}, ${pad(d.getDate())}/${pad(d.getMonth() + 1)}${timePart}`; return `${day}, ${pad(d.getDate())}/${pad(d.getMonth() + 1)}${timePart}`;
@ -223,6 +223,7 @@
{#if showDatePicker} {#if showDatePicker}
<DateTimePicker <DateTimePicker
value={task.due_date} value={task.due_date}
has_time={task.has_time}
onchange={handleDateChange} onchange={handleDateChange}
onclose={() => (showDatePicker = false)} onclose={() => (showDatePicker = false)}
/> />

View file

@ -4,6 +4,7 @@ export interface Task {
description: string; description: string;
status: "backlog" | "completed"; status: "backlog" | "completed";
due_date: string | null; due_date: string | null;
has_time: boolean;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
parent_id: string | null; parent_id: string | null;

View file

@ -17,6 +17,8 @@ pub struct Task {
pub status: TaskStatus, pub status: TaskStatus,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub due_date: Option<DateTime<Utc>>, pub due_date: Option<DateTime<Utc>>,
#[serde(default)]
pub has_time: bool,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@ -32,6 +34,7 @@ impl Task {
description: String::new(), description: String::new(),
status: TaskStatus::Backlog, status: TaskStatus::Backlog,
due_date: None, due_date: None,
has_time: false,
created_at: now, created_at: now,
updated_at: now, updated_at: now,
parent_id: None, parent_id: None,

View file

@ -56,6 +56,8 @@ pub struct TaskFrontmatter {
pub status: TaskStatus, pub status: TaskStatus,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub due: Option<DateTime<Utc>>, pub due: Option<DateTime<Utc>>,
#[serde(default)]
pub has_time: bool,
pub created: DateTime<Utc>, pub created: DateTime<Utc>,
pub updated: DateTime<Utc>, pub updated: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@ -68,6 +70,7 @@ impl From<&Task> for TaskFrontmatter {
id: task.id, id: task.id,
status: task.status, status: task.status,
due: task.due_date, due: task.due_date,
has_time: task.has_time,
created: task.created_at, created: task.created_at,
updated: task.updated_at, updated: task.updated_at,
parent: task.parent_id, parent: task.parent_id,
@ -256,6 +259,7 @@ impl Storage for FileSystemStorage {
description, description,
status: frontmatter.status, status: frontmatter.status,
due_date: frontmatter.due, due_date: frontmatter.due,
has_time: frontmatter.has_time,
created_at: frontmatter.created, created_at: frontmatter.created,
updated_at: frontmatter.updated, updated_at: frontmatter.updated,
parent_id: frontmatter.parent, parent_id: frontmatter.parent,
@ -344,6 +348,7 @@ impl Storage for FileSystemStorage {
description, description,
status: frontmatter.status, status: frontmatter.status,
due_date: frontmatter.due, due_date: frontmatter.due,
has_time: frontmatter.has_time,
created_at: frontmatter.created, created_at: frontmatter.created,
updated_at: frontmatter.updated, updated_at: frontmatter.updated,
parent_id: frontmatter.parent, parent_id: frontmatter.parent,