Simpl-Resultat/src/components/balance/SnapshotEditor.tsx
le king fu 6288a3fe23 feat(balance): support priced kind in AccountForm + SnapshotLineRow
- 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>
2026-04-25 15:01:38 -04:00

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>
);
}