- AccountForm now exposes a 'category' variant with a kind selector (simple | priced); the legacy 'account' variant is unchanged modulo the new symbol-required-for-priced UI guard. - SnapshotLineRow dispatches on account.category_kind: * simple variant unchanged from #146 * priced variant: quantity + unit_price inputs + read-only computed value rendered live (qty × price, 2 decimals) + [Manuel] attribution tag - useSnapshotEditor extends state with pricedValues map, exposes setLineQuantity / setLineUnitPrice handlers, prefill copies quantity but leaves unit_price blank (per spec-decisions row), save() builds mixed simple+priced batches. - SnapshotEditor + SnapshotEditPage thread the new priced state. - Total line at the top of SnapshotEditPage now sums simple + priced contributions live as the user types. Refs #140 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
106 lines
3.6 KiB
TypeScript
106 lines
3.6 KiB
TypeScript
// SnapshotEditor — groups the active accounts by balance category and
|
|
// renders one `SnapshotLineRow` per account.
|
|
//
|
|
// Both `simple` and `priced` variants are dispatched by `account.category_kind`
|
|
// inside `SnapshotLineRow`. The editor itself only carries the values down
|
|
// and the change handlers up.
|
|
|
|
import { useMemo } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import type {
|
|
BalanceAccountWithCategory,
|
|
BalanceCategory,
|
|
} from "../../shared/types";
|
|
import type { PricedEntry } from "../../hooks/useSnapshotEditor";
|
|
import SnapshotLineRow from "./SnapshotLineRow";
|
|
|
|
interface Props {
|
|
accounts: BalanceAccountWithCategory[];
|
|
categories: BalanceCategory[];
|
|
/** account_id → string-typed value (simple kind). */
|
|
values: Record<number, string>;
|
|
/** account_id → {quantity, unit_price} strings (priced kind). */
|
|
pricedValues: Record<number, PricedEntry>;
|
|
onValueChange: (accountId: number, next: string) => void;
|
|
onQuantityChange: (accountId: number, next: string) => void;
|
|
onUnitPriceChange: (accountId: number, next: string) => void;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export default function SnapshotEditor({
|
|
accounts,
|
|
categories,
|
|
values,
|
|
pricedValues,
|
|
onValueChange,
|
|
onQuantityChange,
|
|
onUnitPriceChange,
|
|
disabled,
|
|
}: 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<number, BalanceAccountWithCategory[]>();
|
|
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 (
|
|
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] p-8 text-center text-[var(--muted-foreground)]">
|
|
{t("balance.snapshot.editor.empty")}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
{groups.map(({ category, accounts: catAccounts }) => (
|
|
<div
|
|
key={category.id}
|
|
className="bg-[var(--card)] rounded-xl border border-[var(--border)] overflow-hidden"
|
|
>
|
|
<div className="px-4 py-2 bg-[var(--muted)] border-b border-[var(--border)]">
|
|
<h3 className="text-sm font-semibold">
|
|
{t(category.i18n_key, { defaultValue: category.key })}
|
|
</h3>
|
|
</div>
|
|
<div className="px-4">
|
|
{catAccounts.map((acc) => {
|
|
const priced = pricedValues[acc.id];
|
|
return (
|
|
<SnapshotLineRow
|
|
key={acc.id}
|
|
account={acc}
|
|
value={values[acc.id] ?? ""}
|
|
quantityValue={priced?.quantity ?? ""}
|
|
unitPriceValue={priced?.unit_price ?? ""}
|
|
onChange={(next) => onValueChange(acc.id, next)}
|
|
onQuantityChange={(next) => onQuantityChange(acc.id, next)}
|
|
onUnitPriceChange={(next) => onUnitPriceChange(acc.id, next)}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|