// SnapshotEditor — groups the active accounts by balance category and // renders one `SnapshotLineRow` per account. // // Each row dispatches its variant on the account's OWN `account.kind` (#213) // inside `SnapshotLineRow` (simple → scalar value; detailed → holdings basket). // The editor itself only carries the values/holdings down and the change // handlers up. import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import type { BalanceAccountWithCategory, BalanceCategory, BalanceSecurity, } from "../../shared/types"; import type { HoldingDraft } from "../../hooks/useSnapshotEditor"; import type { HoldingColumnMapping } from "../../utils/csvAutoDetect"; import type { SecurityPick } from "./SecurityPicker"; import SnapshotLineRow from "./SnapshotLineRow"; import { renderCategoryLabelFromCategory } from "../../utils/renderCategoryLabel"; interface Props { accounts: BalanceAccountWithCategory[]; categories: BalanceCategory[]; /** account_id → string-typed value (simple accounts). */ values: Record; /** account_id → holdings basket (detailed accounts, #213). */ holdings: Record; /** Securities catalogue for the SecurityPicker autocomplete (#214). */ securities: BalanceSecurity[]; onValueChange: (accountId: number, next: string) => void; onAddHolding: (accountId: number, assetType?: "stock" | "crypto") => void; onRemoveHolding: (accountId: number, rowId: string) => void; onHoldingFieldChange: ( accountId: number, rowId: string, field: keyof Omit, value: string ) => void; /** Apply a SecurityPicker selection to a holding row (#214). */ onHoldingSecurityPick: ( accountId: number, rowId: string, pick: SecurityPick ) => void; /** Import a batch of holdings from a CSV into a detailed account (#245). */ onImportHoldings: ( accountId: number, rows: string[][], mapping: HoldingColumnMapping ) => void; disabled?: boolean; /** Snapshot date (YYYY-MM-DD) — forwarded to PriceFetchControl (Issue #158). */ snapshotDate?: string; } export default function SnapshotEditor({ accounts, categories, values, holdings, securities, onValueChange, onAddHolding, onRemoveHolding, onHoldingFieldChange, onHoldingSecurityPick, onImportHoldings, disabled, snapshotDate, }: Props) { const { t } = useTranslation(); // Group accounts by their category, preserving the categories' sort_order // first then the account name within each group. const groups = useMemo(() => { const byCategory = new Map(); for (const acc of accounts) { const list = byCategory.get(acc.balance_category_id) ?? []; list.push(acc); byCategory.set(acc.balance_category_id, list); } const sortedCategories = [...categories].sort( (a, b) => a.sort_order - b.sort_order || a.key.localeCompare(b.key) ); return sortedCategories .map((cat) => ({ category: cat, accounts: (byCategory.get(cat.id) ?? []).sort((a, b) => a.name.localeCompare(b.name) ), })) .filter((group) => group.accounts.length > 0); }, [accounts, categories]); if (accounts.length === 0) { return (
{t("balance.snapshot.editor.empty")}
); } return (
{groups.map(({ category, accounts: catAccounts }) => (

{renderCategoryLabelFromCategory(category, t)}

{catAccounts.map((acc) => ( onValueChange(acc.id, next)} onAddHolding={() => onAddHolding(acc.id, acc.category_asset_type ?? "stock") } onRemoveHolding={(rowId) => onRemoveHolding(acc.id, rowId)} onHoldingFieldChange={(rowId, field, value) => onHoldingFieldChange(acc.id, rowId, field, value) } onHoldingSecurityPick={(rowId, pick) => onHoldingSecurityPick(acc.id, rowId, pick) } onImportHoldings={(rows, mapping) => onImportHoldings(acc.id, rows, mapping) } disabled={disabled} snapshotDate={snapshotDate} /> ))}
))}
); }