import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { NativeSelect } from "@/components/ui/native-select"; import { useMutate } from "../hooks/useMutate.ts"; import { removeRuleFrom, setRuleFrom } from "../../store/actions.ts"; import { currentMonth } from "../../store/index.ts"; import type { Post, Rule } from "../../domain/types.ts"; interface Props { post: Post; } /** * Shape AND range, matching the import validator. `setRuleFrom` sorts the * series with `compareMonths`, which THROWS on a malformed MonthId; useMutate * would catch it and show an error, but a disabled button is the better * answer, and AGENTS.md requires guarding a throwing domain call at the site * that invokes it from a handler. */ const MONTH_ID = /^\d{4}-(0[1-9]|1[0-2])$/; /** * A post's allocation over time. Each row is "from this month, this rule", * effective until the next row takes over. Adding a row for a month that * already has one replaces it — `setRuleFrom` enforces that, and the form says * so rather than letting it look like an append. */ export function RuleHistory({ post }: Props) { const { mutate, error, clearError } = useMutate(); const [from, setFrom] = useState(currentMonth); const [kind, setKind] = useState("percentOfIncome"); // Held as text, not a number, so the field can be cleared and can hold a // half-typed "12." mid-entry. Parsed once, on submit. const [value, setValue] = useState(""); const versions = [...post.rules].sort((a, b) => a.from.localeCompare(b.from)); const replaces = post.rules.some((v) => v.from === from); const monthOk = MONTH_ID.test(from); const amount = Number(value); const valueOk = value.trim() !== "" && Number.isFinite(amount); function add() { const rule: Rule = kind === "fixed" ? { kind: "fixed", amount: { amount, currency: post.currency } } : { kind: "percentOfIncome", percent: amount }; clearError(); void mutate((draft) => setRuleFrom(draft, post.id, from, rule)); } return (
{post.rules.length === 0 ? (

Not budgeted. Until a rule starts, this post is allocated nothing. Spending still records without an overspend status.

) : ( {versions.map((version) => ( ))}
From Rule
{version.from} {version.rule.kind === "fixed" ? `${version.rule.amount.amount} ${version.rule.amount.currency}` : `${version.rule.percent}% of income`}
)}
{ const next = event.target.value; setFrom(next); }} /> { const next = event.target.value as Rule["kind"]; setKind(next); }} > { const next = event.target.value; setValue(next); }} /> {!monthOk && from.trim() !== "" && ( Needs a month as YYYY-MM, 01 to 12. )} {monthOk && replaces && ( {from} already has a rule; this replaces it. )}
{error &&

Could not save: {error}

}
); }