diff --git a/src/ui/components/PostTable.tsx b/src/ui/components/PostTable.tsx index 871d5a0..f541b08 100644 --- a/src/ui/components/PostTable.tsx +++ b/src/ui/components/PostTable.tsx @@ -2,6 +2,7 @@ import { Fragment } from "react"; import { Link } from "react-router"; import { Badge } from "@/components/ui/badge"; import { formatMoney, formatSignedMoney } from "../format.ts"; +import { ruleAt } from "../../domain/allocation.ts"; import type { MonthPostRow } from "../../domain/views.ts"; import type { PostMonthFigures } from "../../domain/fold.ts"; import type { Currency, MonthId } from "../../domain/types.ts"; @@ -10,6 +11,8 @@ interface Props { monthId: MonthId; baseCurrency: Currency; rows: MonthPostRow[]; + /** Omitted on read-only mounts; only the month view offers rule editing. */ + onChangeRule?: (postId: string) => void; } /** @@ -57,7 +60,7 @@ function CarryMeter({ figures }: { figures: PostMonthFigures }) { ); } -export function PostTable({ monthId, baseCurrency, rows }: Props) { +export function PostTable({ monthId, baseCurrency, rows, onChangeRule }: Props) { return (
@@ -87,11 +90,33 @@ export function PostTable({ monthId, baseCurrency, rows }: Props) { overridden )} + {/* A month can carry BOTH badges: a version starting in a + month the user also overrode is unusual but legal, and + hiding either would obscure why the number is what it + is. The override still wins the allocation. */} + {ruleAt(post, monthId)?.from === monthId && ( + + rule changes here + + )} + {onChangeRule && ( + + )}
(null); + const changingPost = changingRuleFor + ? (dataset.posts.find((p) => p.id === changingRuleFor) ?? null) + : null; return (
@@ -82,7 +89,25 @@ export function MonthRoute() { - + + + {changingPost && ( + setChangingRuleFor(null)} + /> + )}
@@ -132,3 +157,80 @@ export function MonthRoute() {
); } + +/** + * "15% from July" is a decision made while looking at July. This writes the + * same `setRuleFrom` action the Settings history editor uses, so the two + * surfaces cannot drift apart. + */ +function RuleFromMonth({ + post, + monthId, + initial, + onDone, +}: { + post: Post; + monthId: MonthId; + initial: Rule | undefined; + onDone: () => void; +}) { + const { mutate } = useMutate(); + // Pre-filled with what is currently in effect, so nudging 10% to 15% is an + // edit rather than re-entry. Empty when the post has no rule yet. Held as + // text so the field can be cleared mid-edit; parsed once, on apply. + const [kind, setKind] = useState(initial?.kind ?? "percentOfIncome"); + const [value, setValue] = useState( + initial === undefined + ? "" + : String(initial.kind === "fixed" ? initial.amount.amount : initial.percent), + ); + + const amount = Number(value); + const valueOk = value.trim() !== "" && Number.isFinite(amount); + + function apply() { + const rule: Rule = + kind === "fixed" + ? { kind: "fixed", amount: { amount, currency: post.currency } } + : { kind: "percentOfIncome", percent: amount }; + mutate((draft) => setRuleFrom(draft, post.id, monthId, rule)); + onDone(); + } + + return ( +
+ + {post.name}: allocate from {monthId} onward + + + { + const next = event.target.value; + setValue(next); + }} + /> + + +
+ ); +}