feat(detail): let an event's goal be points, entered in the sheet

Some events want a number of points rather than a number of days. The
goal control gains Days and Points. Switching period keeps the measure
and the number, and the sheet keeps this period's number editable in
place. A target is capped where the schema reads it back, so a huge
number can't silently become no goal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-13 13:46:13 +02:00
1 parent 595ae55f4a
commit d0b295c02f
5 files changed
+196 -13

No files matched your search

+20
View File
@@ -785,6 +785,8 @@ describe("the derived-boundary note", () => {
dailyDays: [],
goal: undefined,
hasStoredGoal: false,
amounts: {},
onSetAmount: noop,
onDaily: noop,
onGoal: noop,
onToggleDay: noop,
@@ -872,6 +874,24 @@ describe("the derived-boundary note", () => {
expect(sheet("done")).not.toContain("Mark event done");
});
test("a points goal shows this week's number, editable in place", () => {
const html = renderToStaticMarkup(
<GameMetaProvider value={(id) => metaFor(id, GAMES)}>
<EventDetail
{...detailProps}
daily
goal={{ period: "week", measure: "amount", target: 2100, weekStartsOn: 1 }}
hasStoredGoal
amounts={{ "2026-08-24": { value: 1900, at: AT } }}
row={{ event: PARSED, clock: clockFor(PARSED, detailProps.region, NOW) }}
/>
</GameMetaProvider>,
);
expect(html).toContain("1,900 of 2,100 this week · 200 to go");
expect(html).toContain('value="1900"');
expect(html).toContain("/ 2,100");
});
test("'not daily' warns it removes a goal even when this build cannot read it", () => {
const html = renderToStaticMarkup(
<GameMetaProvider value={(id) => metaFor(id, GAMES)}>
+35 -1
View File
@@ -17,7 +17,12 @@ import {
} from "../src/client/components/Dailies.tsx";
import type { GoalStatus } from "../src/shared/goal.ts";
import { Welcome } from "../src/client/components/Welcome.tsx";
import { chooseGoal, GoalControl, parseDraft } from "../src/client/components/GoalControl.tsx";
import {
chooseGoal,
chooseMeasure,
GoalControl,
parseDraft,
} from "../src/client/components/GoalControl.tsx";
import { AmountEditor, parseAmount } from "../src/client/components/AmountEditor.tsx";
import type { Task } from "../src/shared/task.ts";
import { DailyChecklist } from "../src/client/components/DailyChecklist.tsx";
@@ -1423,3 +1428,32 @@ describe("AmountEditor", () => {
expect(parseAmount("-5")).toBeNull();
});
});
describe("GoalControl with points", () => {
const points = { period: "week", measure: "amount", target: 2100, weekStartsOn: 3 } as const;
test("switching period keeps the measure and the number", () => {
expect(chooseGoal("event", points, 1, 84)).toEqual({
period: "event", measure: "amount", target: 2100,
});
expect(chooseGoal("week", { period: "event", measure: "amount", target: 50 }, 2, 84)).toEqual({
period: "week", measure: "amount", target: 50, weekStartsOn: 2,
});
});
test("switching measure keeps the period", () => {
const days = { period: "week", measure: "days", required: 5, weekStartsOn: 3 } as const;
expect(chooseMeasure("amount", days, 84)).toEqual({ ...points, target: 100 });
expect(chooseMeasure("days", points, 84)).toEqual({ ...days, required: 5 });
expect(chooseMeasure("days", days, 84)).toBeNull();
});
test("a points goal shows its target and the measure toggle", () => {
const html = render(
<GoalControl goal={points} defaultWeekStartsOn={1} maxDays={84} onChange={() => {}} />,
);
expect(html).toMatch(/aria-pressed="true"[^>]*>Points</);
expect(html).toContain('value="2100"');
expect(html).toContain("points each week, weeks start");
});
});