diff --git a/src/shared/goal.ts b/src/shared/goal.ts index 804eb73..1ea27b7 100644 --- a/src/shared/goal.ts +++ b/src/shared/goal.ts @@ -86,6 +86,8 @@ export interface GoalStatus { open: number | null; /** Fewer days left than check-ins needed. The target is never lowered. */ shortfall: boolean; + /** A task's days left in its period from today, ticked or not — what a cycle's chip shows. */ + daysLeft?: number | undefined; } /** Bounds a walk over keys when the end is unknown; matches daily.ts. */ @@ -207,7 +209,7 @@ export function goalSummary(goal: Goal | undefined): string { /** The Dailies chip's short count. */ export function goalCount(s: GoalStatus): string { // "cy" means nothing; the days left in a cycle say where it is. - if (s.scope === "cycle") return `${num(s.count)}/${num(s.required)} · ${s.open ?? 0}d`; + if (s.scope === "cycle") return `${num(s.count)}/${num(s.required)} · ${s.daysLeft ?? s.open ?? 0}d`; const unit = s.scope === "week" ? " wk" : s.scope === "month" ? " mo" : ""; return `${num(s.count)}/${num(s.required)}${unit}`; } diff --git a/src/shared/task.ts b/src/shared/task.ts index aa183fb..8c964bd 100644 --- a/src/shared/task.ts +++ b/src/shared/task.ts @@ -87,7 +87,7 @@ export function taskStatus(input: { return { scope, measure: "amount", periodStart: start, required, count, met: count >= required, needed: Math.max(0, required - count), - open: left.length, shortfall: false, + open: left.length, shortfall: false, daysLeft: left.length, }; } @@ -100,6 +100,7 @@ export function taskStatus(input: { return { scope, measure: "days", periodStart: start, required, count, met: count >= required, needed, open, shortfall: needed > open, + daysLeft: left.length, }; } diff --git a/test/task.test.ts b/test/task.test.ts index 5c1f5a5..e7aca8b 100644 --- a/test/task.test.ts +++ b/test/task.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; import { RESERVED_ID_SEGMENTS } from "../src/shared/custom.ts"; import { nextResetMs } from "../src/shared/daily.ts"; +import { goalCount } from "../src/shared/goal.ts"; import { mintTaskId, TaskSchema, taskCaption, taskStatus, type Task } from "../src/shared/task.ts"; /** @@ -171,3 +172,14 @@ describe("a task on another server's clock", () => { expect(after.periodStart).toBe("2026-10-08"); }); }); + +describe("a cycle chip's days left", () => { + test("is the days left in the cycle, ticked or not", () => { + const t = task({ period: { unit: "cycle", days: 14, from: "2026-10-16" }, measure: { kind: "days", required: 1 } }); + const at = (logged: string[], day: string) => + goalCount(taskStatus({ task: t, region: "europe", now: noon(day), logged })); + expect(at([], "2026-10-20")).toBe("0/1 · 10d"); + expect(at(["2026-10-20"], "2026-10-20")).toBe("1/1 · 10d"); + expect(at(["2026-10-29"], "2026-10-29")).toBe("1/1 · 1d"); + }); +});