"use client"; import { zodResolver } from "@hookform/resolvers/zod"; import { ChevronDown, Loader2 } from "lucide-react"; import { useEffect, useState } from "react"; import { Controller, useForm, useWatch, type Control, type Resolver } from "react-hook-form"; import { toast } from "sonner"; import { IconPicker, SwatchPicker } from "@/components/appearance/pickers"; import { MoneyInput } from "@/components/money-input"; import { ResponsiveDialog } from "@/components/responsive-dialog"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSet, } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { tr } from "@/i18n/tr"; import { ACCOUNT_TYPES, DUE_RULE_KINDS, MEAL_CARD_PROVIDERS, type AccountType, } from "@/shared/accounts"; import type { IconKey } from "@/shared/appearance"; import { todayIstanbul } from "@/shared/dates"; import { formatMoneyInput } from "@/shared/money"; import { accountFormSchema, type AccountForm, type AccountFormInput, } from "@/shared/schemas/account"; import { createAccountAction, updateAccountAction } from "../actions"; import type { AccountView } from "../service"; const t = tr.accounts; const f = t.fields; const NONE = "__none__"; const DAYS = Array.from({ length: 31 }, (_, i) => i + 1); const DEFAULT_ICON: Record = { cash: "wallet", bank: "landmark", savings: "piggy-bank", credit_card: "credit-card", meal_card: "utensils", }; type FormValues = { name: string; type: AccountType; institution: string; mealCardProvider: AccountFormInput["mealCardProvider"]; openingBalance: string; openingDate: string; color: AccountFormInput["color"]; icon: AccountFormInput["icon"]; ownerUserId: string; isPrivate: boolean; overdraftEnabled: boolean; overdraftLimit: string; overdraftMonthlyRate: string; overdraftStatementDay: string; cardLimit: string; cardStatementDay: string; cardDueKind: AccountFormInput["cardDueKind"]; cardDueValue: string; cardPaymentAccountId: string; cardMinimumRate: string; }; const money = (minor: number | null | undefined) => (minor ? formatMoneyInput(minor) : ""); function toValues(a?: AccountView): FormValues { if (!a) { return { name: "", type: "bank", institution: "", mealCardProvider: null, openingBalance: "", openingDate: todayIstanbul(), color: "blue", icon: "landmark", ownerUserId: "", isPrivate: false, overdraftEnabled: false, overdraftLimit: "", overdraftMonthlyRate: "", overdraftStatementDay: "", cardLimit: "", cardStatementDay: "1", cardDueKind: "daysAfter", cardDueValue: "10", cardPaymentAccountId: "", cardMinimumRate: "", }; } const isCard = a.type === "credit_card"; return { name: a.name, type: a.type, institution: a.institution, mealCardProvider: a.mealCardProvider, openingBalance: a.openingBalanceMinor === null ? "" : money(isCard ? Math.abs(a.openingBalanceMinor) : a.openingBalanceMinor), openingDate: a.openingDate, color: a.color, icon: a.icon, ownerUserId: a.ownerUserId ?? "", isPrivate: a.isPrivate, overdraftEnabled: Boolean(a.overdraft), overdraftLimit: money(a.overdraft?.limitMinor), overdraftMonthlyRate: a.overdraft?.monthlyRateBp ? formatMoneyInput(a.overdraft.monthlyRateBp) : "", overdraftStatementDay: a.overdraft?.statementDay ? String(a.overdraft.statementDay) : "", cardLimit: money(a.creditCard?.limitMinor), cardStatementDay: String(a.creditCard?.statementDay ?? 1), cardDueKind: a.creditCard?.dueRule.kind ?? "daysAfter", cardDueValue: String(a.creditCard?.dueRule.value ?? 10), cardPaymentAccountId: a.creditCard?.paymentAccountId ?? "", cardMinimumRate: a.creditCard?.minimumPaymentRateBp != null ? formatMoneyInput(a.creditCard.minimumPaymentRateBp) : "", }; } function DaySelect({ id, value, onChange, max = 31, allowEmpty, }: { id: string; value: string; onChange: (v: string) => void; max?: number; allowEmpty?: boolean; }) { return ( ); } function MoneyField({ control, name, label, hint, allowNegative, disabled, }: { control: Control; name: "openingBalance" | "overdraftLimit" | "cardLimit"; label: string; hint?: string; allowNegative?: boolean; disabled?: boolean; }) { return ( ( {label} {hint ? {hint} : null} {fieldState.invalid ? : null} )} /> ); } export function AccountFormDialog({ open, account, members, paymentAccounts, onOpenChange, }: { open: boolean; account?: AccountView; members: { id: string; name: string }[]; paymentAccounts: { id: string; name: string }[]; onOpenChange: (open: boolean) => void; }) { const isEdit = Boolean(account); const balanceHidden = isEdit && account!.openingBalanceMinor === null; const form = useForm({ // FormValues, şemanın giriş şekliyle birebir (sayısal alanlar string olarak girilir, zod çevirir). resolver: zodResolver(accountFormSchema) as unknown as Resolver< FormValues, unknown, AccountForm >, defaultValues: toValues(), }); const [advancedOpen, setAdvancedOpen] = useState(false); useEffect(() => { if (open) form.reset(toValues(account)); }, [open, account, form]); function handleOpenChange(next: boolean) { if (!next) setAdvancedOpen(false); onOpenChange(next); } const type = useWatch({ control: form.control, name: "type" }); const color = useWatch({ control: form.control, name: "color" }); const overdraftEnabled = useWatch({ control: form.control, name: "overdraftEnabled" }); const dueKind = useWatch({ control: form.control, name: "cardDueKind" }); const isCard = type === "credit_card"; /** Ham form değerleri gönderilir; sunucu aynı şemayla ayrıştırır. */ async function onSubmit() { const values = form.getValues(); // Gizli bakiyede sunucu mevcut değeri korur; doğrulamadan geçmesi için 0 gönderilir. const payload = balanceHidden ? { ...values, openingBalance: "0" } : values; const result = account ? await updateAccountAction(account.id, payload) : await createAccountAction(payload); if (result.ok) { toast.success(account ? t.updated(values.name) : t.created(values.name)); handleOpenChange(false); return; } let advancedError = false; for (const [name, message] of Object.entries(result.fieldErrors ?? {})) { if (!message) continue; form.setError(name as keyof FormValues, { message }); if (/^(overdraft|ownerUserId|isPrivate)/.test(name)) advancedError = true; } if (advancedError) setAdvancedOpen(true); if (!result.fieldErrors) form.setError("root", { message: result.error }); } return ( } >
( {f.type} {isEdit ? {f.typeLockedHint} : null} )} /> ( {f.name} {fieldState.invalid ? : null} )} /> {type === "meal_card" ? ( ( {f.provider} {fieldState.invalid ? : null} )} /> ) : type !== "cash" ? ( ( {f.institution} )} /> ) : null}
( {f.openingDate} {fieldState.invalid ? : null} )} />
{isCard ? (
{f.card} ( {f.cardStatementDay} {f.cardStatementDayHint} )} />
( {f.cardDueKind} )} /> ( {dueKind === "daysAfter" ? f.cardDueValueDays : f.cardDueValueDay} {fieldState.invalid ? : null} )} />
( {f.cardPaymentAccount} {fieldState.invalid ? : null} )} /> ( {f.cardMinimumRate} {f.cardMinimumRateHint} {fieldState.invalid ? : null} )} />
) : null} {type === "bank" ? (
{f.overdraft} ( field.onChange(v === true)} /> {f.overdraftEnabled} )} /> {overdraftEnabled ? ( <>
( {f.overdraftRate} {fieldState.invalid ? ( ) : null} )} /> ( {f.overdraftStatementDay} )} />
) : null}
) : null} ( {f.owner} {fieldState.invalid ? : null} )} /> ( field.onChange(v === true)} /> {f.isPrivate} )} /> ( {tr.categories.color} )} /> ( {tr.categories.icon} )} />
{form.formState.errors.root ? ( {form.formState.errors.root.message} ) : null}
); }