Repeats, estimates, weekly review: completing a repeating task spawns the next occurrence; estimate chips + quick-add tokens (~30m, monthly); review screen with one-tap triage; server gains repeat column and applies its idempotent schema at boot

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 00:06:39 -07:00
parent 6ccd417f2a
commit 4ef3f8c63c
22 changed files with 754 additions and 93 deletions

51
app/src/lib/dates.ts Normal file
View File

@@ -0,0 +1,51 @@
// Date helpers for human-friendly display. All task dates are local ISO
// "YYYY-MM-DD" strings; comparisons are lexicographic.
const DAY_MS = 86_400_000;
export function todayIso(): string {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
export function fromIso(s: string): Date {
const [y, m, d] = s.split("-").map(Number);
return new Date(y, m - 1, d);
}
export function toIso(d: Date): string {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
/** Deferred = hidden from action lists until the date arrives. */
export function isDeferred(deferDate: string | null, today = todayIso()): boolean {
return !!deferDate && deferDate > today;
}
export function daysFromToday(dateIso: string): number {
return Math.round((fromIso(dateIso).getTime() - fromIso(todayIso()).getTime()) / DAY_MS);
}
/** "Today" / "Tomorrow" / "Mon 18 Aug" — no ISO date math for the reader. */
export function friendlyDate(dateIso: string): string {
const diff = daysFromToday(dateIso);
if (diff === 0) return "Today";
if (diff === 1) return "Tomorrow";
if (diff === -1) return "Yesterday";
const d = fromIso(dateIso);
const wd = d.toLocaleDateString("en-AU", { weekday: "short" });
const mo = d.toLocaleDateString("en-AU", { month: "short" });
const yr = d.getFullYear() !== new Date().getFullYear() ? ` ${d.getFullYear()}` : "";
return `${wd} ${d.getDate()} ${mo}${yr}`;
}
export type DueTone = "overdue" | "today" | "later";
/** Row label for a due date, with a tone the UI can color. */
export function dueMeta(dueIso: string): { label: string; tone: DueTone } {
const diff = daysFromToday(dueIso);
if (diff < 0) return { label: diff === -1 ? "1d overdue" : `${-diff}d overdue`, tone: "overdue" };
if (diff === 0) return { label: "due today", tone: "today" };
if (diff === 1) return { label: "due tomorrow", tone: "later" };
return { label: `due ${friendlyDate(dueIso)}`, tone: "later" };
}