feat(ui): kart özeti, KMH paneli, taksit seçimi ve kalan taksit iptali

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 11:19:17 +03:00
parent cc624af69f
commit 0ad52757ad
7 changed files with 512 additions and 9 deletions
@@ -42,9 +42,22 @@ import type { PayeeSuggestion, TransactionDetail, TransactionFormOptions } from
const t = tr.transactions;
const f = t.fields;
const INSTALLMENT_OPTIONS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 18, 24, 36];
export type TransactionFormMode =
| { kind: "create"; type: TransactionType }
| {
kind: "create";
type: TransactionType;
/** Ön doldurma (ör. kart borcu ödemesi, KMH faiz kaydı). */
prefill?: {
accountId?: string;
toAccountId?: string;
amountMinor?: number;
categoryId?: string;
date?: string;
description?: string;
};
}
| { kind: "edit"; transaction: TransactionDetail }
| { kind: "copy"; transaction: TransactionDetail };
@@ -62,22 +75,26 @@ type FormValues = {
tags: string;
/** Önizleme bilgisiyle ekler; sunucuya yalnızca kimlikleri gider. */
attachments: AttachmentInfo[];
/** Taksit sayısı (yalnızca yeni kredi kartı gideri). */
installments: string;
};
function toValues(mode: TransactionFormMode, options: TransactionFormOptions | null): FormValues {
if (mode.kind === "create") {
const p = mode.prefill ?? {};
return {
type: mode.type,
amount: "",
date: todayIstanbul(),
accountId: options?.accounts[0]?.id ?? "",
toAccountId: "",
categoryId: "",
amount: p.amountMinor ? formatMoneyInput(p.amountMinor) : "",
date: p.date ?? todayIstanbul(),
accountId: p.accountId ?? options?.accounts[0]?.id ?? "",
toAccountId: p.toAccountId ?? "",
categoryId: p.categoryId ?? "",
splits: [],
payee: "",
description: "",
description: p.description ?? "",
tags: "",
attachments: [],
installments: "1",
};
}
const x = mode.transaction;
@@ -98,6 +115,7 @@ function toValues(mode: TransactionFormMode, options: TransactionFormOptions | n
tags: x.tags.join(", "),
// Kopyada ekler taşınmaz (fiş o işleme aittir).
attachments: mode.kind === "edit" ? x.attachments : [],
installments: "1",
};
}
@@ -374,6 +392,13 @@ export function TransactionFormDialog({
}, [options, type, mode]);
const totalMinor = parseMoney(amount ?? "") ?? 0;
const selectedAccountType = options?.accounts.find((a) => a.id === accountId)?.type;
// Taksit: yeni (veya kopya) kredi kartı gideri, bölünmemiş.
const showInstallments =
mode?.kind !== "edit" &&
type === "expense" &&
selectedAccountType === "credit_card" &&
!splitMode;
const splitSum = (splitValues ?? []).reduce((a, s) => a + (parseMoney(s.amount ?? "") ?? 0), 0);
const remaining = totalMinor - splitSum;
@@ -448,6 +473,7 @@ export function TransactionFormDialog({
const { attachments, ...values } = form.getValues();
const payload = {
...values,
installments: showInstallments ? values.installments : "1",
attachmentIds: attachments.map((a) => a.id),
toAccountId: values.type === "transfer" ? values.toAccountId : "",
categoryId: values.type === "transfer" || values.splits.length ? "" : values.categoryId,
@@ -595,6 +621,42 @@ export function TransactionFormDialog({
</Field>
)}
/>
{showInstallments ? (
<Controller
name="installments"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="tx-installments">{f.installments}</FieldLabel>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger id="tx-installments" className="w-full">
<SelectValue>
{field.value === "1"
? f.singlePayment
: f.installmentCount(Number(field.value))}
</SelectValue>
</SelectTrigger>
<SelectContent className="max-h-72">
{INSTALLMENT_OPTIONS.map((n) => (
<SelectItem key={n} value={String(n)}>
{n === 1 ? f.singlePayment : f.installmentCount(n)}
</SelectItem>
))}
</SelectContent>
</Select>
{Number(field.value) > 1 && totalMinor > 0 ? (
<FieldDescription>
{f.installmentHint(
Number(field.value),
formatMoney(Math.ceil(totalMinor / Number(field.value))),
)}
</FieldDescription>
) : null}
{fieldState.invalid ? <FieldError errors={[fieldState.error]} /> : null}
</Field>
)}
/>
) : null}
{type === "transfer" ? (
<Controller
name="toAccountId"