// AccountForm — variant=account (Issue #138 / Bilan #1a). // // The category variant lands in Issue #140 (Bilan #2) when the priced-kind // switch becomes available. For now this component focuses on creating / // editing a `balance_account` record bound to an existing category. import { FormEvent, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import type { BalanceAccount, BalanceCategory, } from "../../shared/types"; import type { CreateBalanceAccountInput, UpdateBalanceAccountInput, } from "../../services/balance.service"; export interface AccountFormValues { balance_category_id: number; name: string; symbol: string; notes: string; } interface Props { /** When provided, the form is in edit mode; otherwise creation. */ initialAccount?: BalanceAccount | null; categories: BalanceCategory[]; isSaving: boolean; onSubmit: ( values: CreateBalanceAccountInput | UpdateBalanceAccountInput ) => Promise | void; onCancel: () => void; } function defaultValues( initial: BalanceAccount | null | undefined, categories: BalanceCategory[] ): AccountFormValues { if (initial) { return { balance_category_id: initial.balance_category_id, name: initial.name, symbol: initial.symbol ?? "", notes: initial.notes ?? "", }; } // First active category as a sane default const first = categories.find((c) => c.is_active) ?? categories[0]; return { balance_category_id: first?.id ?? 0, name: "", symbol: "", notes: "", }; } export default function AccountForm({ initialAccount, categories, isSaving, onSubmit, onCancel, }: Props) { const { t } = useTranslation(); const [values, setValues] = useState(() => defaultValues(initialAccount, categories) ); const [touched, setTouched] = useState(false); // Reset form when target account changes (edit different row). useEffect(() => { setValues(defaultValues(initialAccount, categories)); setTouched(false); }, [initialAccount, categories]); const isEditing = !!initialAccount; const selectedCategory = categories.find( (c) => c.id === values.balance_category_id ); const isPriced = selectedCategory?.kind === "priced"; const trimmedName = values.name.trim(); const nameInvalid = touched && trimmedName.length === 0; const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setTouched(true); if (!trimmedName) return; const payload: CreateBalanceAccountInput = { balance_category_id: values.balance_category_id, name: trimmedName, symbol: values.symbol.trim() || null, notes: values.notes.trim() || null, }; if (isEditing) { const updatePayload: UpdateBalanceAccountInput = { balance_category_id: payload.balance_category_id, name: payload.name, symbol: payload.symbol, notes: payload.notes, }; await onSubmit(updatePayload); } else { await onSubmit(payload); } }; const renderCategoryLabel = (cat: BalanceCategory) => t(cat.i18n_key, { defaultValue: cat.key }); return (
setValues({ ...values, name: e.target.value })} onBlur={() => setTouched(true)} className={`w-full px-3 py-2 rounded-lg border bg-[var(--card)] text-sm focus:outline-none focus:ring-1 focus:ring-[var(--primary)] ${ nameInvalid ? "border-[var(--negative)]" : "border-[var(--border)]" }`} autoFocus autoComplete="off" /> {nameInvalid && (

{t("balance.account.form.nameRequired")}

)}
setValues({ ...values, symbol: e.target.value })} placeholder={ isPriced ? t("balance.account.form.symbolPlaceholderPriced") : t("balance.account.form.symbolPlaceholderSimple") } className="w-full px-3 py-2 rounded-lg border border-[var(--border)] bg-[var(--card)] text-sm focus:outline-none focus:ring-1 focus:ring-[var(--primary)]" autoComplete="off" />