import { useState, type ReactNode } from "react"; import { Link } from "react-router"; import { ChevronDown, ChevronRight } from "lucide-react"; import { budgetTree, type BudgetNode } from "../../domain/budgetTree.ts"; import { createPostForPurchase } from "../../store/actions.ts"; import type { MonthId, Recurrence } from "../../domain/types.ts"; import type { MonthPostRow } from "../../domain/views.ts"; import { useDataset } from "../hooks/useDataset.ts"; import { useMoneyFormat } from "../hooks/useMoneyFormat.ts"; import { useMutate } from "../hooks/useMutate.ts"; import { DesktopPostBody, MobilePostRow } from "./PostTable.tsx"; import { PurchaseDialog } from "./PurchaseDialog.tsx"; function cadence(rule: Recurrence) { const unit = rule.kind === "everyNMonths" ? "month" : rule.kind === "everyNWeeks" ? "week" : "day"; return `Every ${rule.n} ${unit}${rule.n === 1 ? "" : "s"}`; } /** A mixed placement tree; only post rows carry envelope balances. */ export function BudgetTree({ monthId, rows, onChangeRule }: { monthId: MonthId; rows: MonthPostRow[]; onChangeRule?: (postId: string) => void }) { const dataset = useDataset(); const fmt = useMoneyFormat(); const { mutate, error } = useMutate(); const tree = budgetTree(dataset, monthId, rows); const [expanded, setExpanded] = useState>(() => new Set()); const toggle = (id: string) => setExpanded(previous => { const next = new Set(previous); if (next.has(id)) next.delete(id); else next.add(id); return next; }); const folderHeading = (node: Extract) => { const open = expanded.has(node.folder.id); const Icon = open ? ChevronDown : ChevronRight; return
Bills due {fmt.money(node.billsDue, dataset.settings.baseCurrency)}
; }; const billContent = (node: Extract) =>
{node.cost.name} {fmt.money(node.cost.amount.amount, node.cost.amount.currency)}Edit

{cadence(node.cost.recurrence)} · {node.cost.splits.length === 0 ? "Unassigned" : `Assigned to ${node.cost.splits.map(split => dataset.posts.find(post => post.id === split.postId)?.name).join(", ")}`} · {node.nextOccurrence ? `Next ${node.nextOccurrence.date}` : "Ended"}

; const purchaseContent = (node: Extract) =>
{node.purchase.description} {fmt.money(node.amount.amount, node.amount.currency)}{node.purchase.splits.length === 0 && }Edit} />
; 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") { const rowProps = { row: node.row, indent, monthId, onChangeRule, ...(node.children.length ? { childrenExpanded: expanded.has(id), onToggleChildren: () => toggle(id) } : {}) }; return [mobile ? : , ...(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` }; const item = mobile ?
  • {content}
  • : {content}; return [item, ...(node.kind === "folder" && expanded.has(id) ? render(node.children, mobile, depth + 1) : [])]; }); return <> {error &&

    Could not save: {error}

    }
    {["Budget item", "Carried in", "Allocated", "Spent", "Remaining", "Projected"].map((label, index) => )} {render(tree.roots, false)} {tree.unfiled.length > 0 && }{render(tree.unfiled, false)} {render(tree.unassigned, false, 1)} {tree.unassigned.length === 0 && }
    {label}
    Unfiled
    Unassigned
    No unassigned purchases this month.
      {render(tree.roots, true)}{tree.unfiled.length > 0 &&
    • Unfiled
    • }{render(tree.unfiled, true)}
    • Unassigned
    • {render(tree.unassigned, true, 1)}{tree.unassigned.length === 0 &&
    • No unassigned purchases this month.
    • }
    ; }