fix: nest legacy bills and stamp builds with commit
Continuous Integration / lint-and-test (push) Successful in 13s
Continuous Integration / docker-build-and-push (push) Successful in 35s

This commit is contained in:
Lucas Winther committed 2026-09-30 14:35:29 +02:00
1 parent 85a21c58eb
commit f02c452d6c
17 files changed
+214 -27

No files matched your search

+25 -1
View File
@@ -6,7 +6,7 @@ import { act } from "react";
import { createRoot } from "react-dom/client";
import { MemoryRouter } from "react-router";
import { createSeedDataset } from "../../domain/seed.ts";
import { addBudgetFolder, addRecurringCost, confirmOccurrence } from "../../store/actions.ts";
import { addBudgetFolder, addRecurringCost, confirmOccurrence, placeRecurringUnderPost } from "../../store/actions.ts";
import { monthView } from "../../domain/views.ts";
import { store } from "../../store/index.ts";
import { PostTableLegend } from "./PostTable.tsx";
@@ -57,3 +57,27 @@ test("legend scopes base currency to post figures and identifies bill currencies
expect(legend.textContent).toContain("Projected subtracts bills not yet confirmed.");
expect(legend.textContent).not.toContain("All figures");
}));
test("bills beneath a post are hidden until its post is expanded", () => {
const data = createSeedDataset("2026-09");
const games = data.posts[0]!;
const bill = addRecurringCost(data, { name: "Star Rail Pass", folderId: null, archived: false, amount: { amount: 35, currency: "DKK" }, startDate: "2026-09", recurrence: { kind: "everyNMonths", n: 1 }, anchoring: "calendar", splitMode: "percent", splits: [{ postId: games.id, value: 100, absorbsRemainder: true }] });
placeRecurringUnderPost(data, bill.id, games.id);
const original = { get: store.get, subscribe: store.subscribe };
store.get = () => data;
store.subscribe = () => () => {};
const container = document.createElement("div"); document.body.append(container);
const root = createRoot(container);
try {
act(() => root.render(<MemoryRouter><BudgetTree monthId="2026-09" rows={monthView(data, "2026-09").rows} /></MemoryRouter>));
const controls = Array.from(container.querySelectorAll(`button[aria-label="Bills under ${games.name}"]`)) as HTMLButtonElement[];
expect(controls).toHaveLength(2);
expect(controls[0]!.getAttribute("aria-expanded")).toBe("false");
expect(container.querySelector(`[data-budget-item="${bill.id}"]`)).toBeNull();
act(() => controls[0]!.click());
expect(controls[0]!.getAttribute("aria-expanded")).toBe("true");
expect(container.querySelectorAll(`[data-budget-item="${bill.id}"]`)).toHaveLength(2);
} finally {
act(() => root.unmount()); container.remove(); Object.assign(store, original);
}
});
+4 -1
View File
@@ -37,7 +37,10 @@ export function BudgetTree({ monthId, rows, onChangeRule }: { monthId: MonthId;
const render = (nodes: BudgetNode[], mobile: boolean, depth = 0): ReactNode[] => nodes.flatMap(node => {
const indent = Math.min(depth, 4) * 16;
const id = node.kind === "folder" ? node.folder.id : node.kind === "post" ? node.row.post.id : node.kind === "purchase" ? node.purchase.id : node.cost.id;
if (node.kind === "post") return [mobile ? <MobilePostRow key={id} row={node.row} indent={indent} monthId={monthId} onChangeRule={onChangeRule} /> : <DesktopPostBody key={id} row={node.row} indent={indent} monthId={monthId} onChangeRule={onChangeRule} />, ...render(node.children, mobile, depth + 1)];
if (node.kind === "post") {
const rowProps = { row: node.row, indent, monthId, onChangeRule, ...(node.children.length ? { childrenExpanded: expanded.has(id), onToggleChildren: () => toggle(id) } : {}) };
return [mobile ? <MobilePostRow key={id} {...rowProps} /> : <DesktopPostBody key={id} {...rowProps} />, ...(expanded.has(id) ? render(node.children, mobile, depth + 1) : [])];
}
const name = node.kind === "folder" ? node.folder.name : node.kind === "purchase" ? node.purchase.description : node.cost.name;
const content = node.kind === "folder" ? folderHeading(node) : node.kind === "purchase" ? purchaseContent(node) : billContent(node);
const style = { paddingInlineStart: `${indent}px` };
+11 -3
View File
@@ -1,4 +1,5 @@
import { Link } from "react-router";
import { ChevronDown, ChevronRight } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { useMoneyFormat, type MoneyFormat } from "../hooks/useMoneyFormat.ts";
import { ruleAt } from "../../domain/allocation.ts";
@@ -211,14 +212,20 @@ export function PostTable({ monthId, rows, onChangeRule }: Props) {
);
}
interface PostRowProps { row: MonthPostRow; monthId: MonthId; onChangeRule?: (postId: string) => void; indent?: number; }
interface PostRowProps { row: MonthPostRow; monthId: MonthId; onChangeRule?: (postId: string) => void; indent?: number; childrenExpanded?: boolean; onToggleChildren?: () => void; }
export function DesktopPostBody({ row, monthId, onChangeRule, indent = 0 }: PostRowProps) {
function PostChildrenToggle({ postName, expanded, onToggle }: { postName: string; expanded: boolean; onToggle: () => void }) {
const Icon = expanded ? ChevronDown : ChevronRight;
return <button type="button" aria-label={`Bills under ${postName}`} aria-expanded={expanded} onClick={onToggle} className="mr-1 inline-flex size-6 align-middle items-center justify-center rounded-sm text-budget-ink-muted hover:text-budget-accent focus-visible:outline-2 focus-visible:outline-ring"><Icon size={16} aria-hidden="true" /></button>;
}
export function DesktopPostBody({ row, monthId, onChangeRule, indent = 0, childrenExpanded, onToggleChildren }: PostRowProps) {
const cells = postRowCells(row, useMoneyFormat());
return (
<tbody data-budget-item={row.post.id} data-budget-name={row.post.name} className="group">
<tr className="transition-colors group-hover:bg-accent/60">
<td className="pb-0 pt-3" style={{ paddingInlineStart: indent }}>
{onToggleChildren && <PostChildrenToggle postName={row.post.name} expanded={!!childrenExpanded} onToggle={onToggleChildren} />}
<PostName row={row} monthId={monthId} />
{onChangeRule && (
<ChangeRuleButton
@@ -257,12 +264,13 @@ export function DesktopPostBody({ row, monthId, onChangeRule, indent = 0 }: Post
);
}
export function MobilePostRow({ row, monthId, onChangeRule, indent = 0 }: PostRowProps) {
export function MobilePostRow({ row, monthId, onChangeRule, indent = 0, childrenExpanded, onToggleChildren }: PostRowProps) {
const cells = postRowCells(row, useMoneyFormat());
return (
<li data-budget-item={row.post.id} data-budget-name={row.post.name} className="space-y-1.5" style={{ paddingInlineStart: indent }}>
<div className="flex items-baseline justify-between gap-3 text-sm">
<span className="min-w-0">
{onToggleChildren && <PostChildrenToggle postName={row.post.name} expanded={!!childrenExpanded} onToggle={onToggleChildren} />}
<PostName row={row} monthId={monthId} />
</span>
<span