feat(accounts): hesaplar (KMH, kredi kartı, yemek kartı, yalnızca ben)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,678 @@
|
||||
"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<AccountType, IconKey> = {
|
||||
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;
|
||||
};
|
||||
|
||||
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: "",
|
||||
};
|
||||
}
|
||||
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 ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
function DaySelect({
|
||||
id,
|
||||
value,
|
||||
onChange,
|
||||
max = 31,
|
||||
allowEmpty,
|
||||
}: {
|
||||
id: string;
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
max?: number;
|
||||
allowEmpty?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Select value={value || NONE} onValueChange={(v) => onChange(v === NONE ? "" : v)}>
|
||||
<SelectTrigger id={id} className="w-28">
|
||||
<SelectValue>{value || "—"}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-64">
|
||||
{allowEmpty ? <SelectItem value={NONE}>—</SelectItem> : null}
|
||||
{DAYS.slice(0, max).map((d) => (
|
||||
<SelectItem key={d} value={String(d)}>
|
||||
{d}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
function MoneyField({
|
||||
control,
|
||||
name,
|
||||
label,
|
||||
hint,
|
||||
allowNegative,
|
||||
disabled,
|
||||
}: {
|
||||
control: Control<FormValues, unknown, AccountForm>;
|
||||
name: "openingBalance" | "overdraftLimit" | "cardLimit";
|
||||
label: string;
|
||||
hint?: string;
|
||||
allowNegative?: boolean;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={`acc-${name}`}>{label}</FieldLabel>
|
||||
<MoneyInput
|
||||
id={`acc-${name}`}
|
||||
name={field.name}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
allowNegative={allowNegative}
|
||||
disabled={disabled}
|
||||
placeholder={disabled ? t.hidden : "0,00"}
|
||||
aria-invalid={fieldState.invalid}
|
||||
aria-describedby={hint ? `acc-${name}-hint` : undefined}
|
||||
/>
|
||||
{hint ? <FieldDescription id={`acc-${name}-hint`}>{hint}</FieldDescription> : null}
|
||||
{fieldState.invalid ? <FieldError errors={[fieldState.error]} /> : null}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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, unknown, AccountForm>({
|
||||
// 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 (
|
||||
<ResponsiveDialog
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
title={isEdit ? t.editTitle : t.createTitle}
|
||||
description={t.formDescription}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="outline" type="button" onClick={() => handleOpenChange(false)}>
|
||||
{tr.common.cancel}
|
||||
</Button>
|
||||
<Button type="submit" form="account-form" disabled={form.formState.isSubmitting}>
|
||||
{form.formState.isSubmitting ? <Loader2 className="animate-spin" /> : null}
|
||||
{isEdit ? tr.common.save : tr.common.create}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<form id="account-form" onSubmit={form.handleSubmit(onSubmit)} noValidate>
|
||||
<FieldGroup>
|
||||
<Controller
|
||||
name="type"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<Field>
|
||||
<FieldLabel htmlFor="acc-type">{f.type}</FieldLabel>
|
||||
<Select
|
||||
value={field.value}
|
||||
disabled={isEdit}
|
||||
onValueChange={(v) => {
|
||||
field.onChange(v);
|
||||
if (!isEdit) form.setValue("icon", DEFAULT_ICON[v as AccountType]);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="acc-type" className="w-full">
|
||||
<SelectValue>{t.types[field.value]}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACCOUNT_TYPES.map((x) => (
|
||||
<SelectItem key={x} value={x}>
|
||||
{t.types[x]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{isEdit ? <FieldDescription>{f.typeLockedHint}</FieldDescription> : null}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="name"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor="acc-name">{f.name}</FieldLabel>
|
||||
<Input {...field} id="acc-name" aria-invalid={fieldState.invalid} />
|
||||
{fieldState.invalid ? <FieldError errors={[fieldState.error]} /> : null}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
{type === "meal_card" ? (
|
||||
<Controller
|
||||
name="mealCardProvider"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor="acc-provider">{f.provider}</FieldLabel>
|
||||
<Select value={field.value ?? undefined} onValueChange={field.onChange}>
|
||||
<SelectTrigger
|
||||
id="acc-provider"
|
||||
className="w-full"
|
||||
aria-invalid={fieldState.invalid}
|
||||
>
|
||||
<SelectValue placeholder={f.provider}>
|
||||
{field.value ? t.providers[field.value] : null}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{MEAL_CARD_PROVIDERS.map((p) => (
|
||||
<SelectItem key={p} value={p}>
|
||||
{t.providers[p]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{fieldState.invalid ? <FieldError errors={[fieldState.error]} /> : null}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
) : type !== "cash" ? (
|
||||
<Controller
|
||||
name="institution"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<Field>
|
||||
<FieldLabel htmlFor="acc-institution">{f.institution}</FieldLabel>
|
||||
<Input {...field} id="acc-institution" placeholder={f.institutionPlaceholder} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<MoneyField
|
||||
control={form.control}
|
||||
name="openingBalance"
|
||||
label={isCard ? f.openingDebt : f.openingBalance}
|
||||
hint={
|
||||
balanceHidden
|
||||
? t.hiddenHint
|
||||
: isCard
|
||||
? f.openingDebtHint
|
||||
: type === "bank"
|
||||
? f.openingBalanceHint
|
||||
: undefined
|
||||
}
|
||||
allowNegative={type === "bank"}
|
||||
disabled={balanceHidden}
|
||||
/>
|
||||
<Controller
|
||||
name="openingDate"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor="acc-date">{f.openingDate}</FieldLabel>
|
||||
<Input {...field} id="acc-date" type="date" aria-invalid={fieldState.invalid} />
|
||||
{fieldState.invalid ? <FieldError errors={[fieldState.error]} /> : null}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isCard ? (
|
||||
<FieldSet>
|
||||
<FieldLegend>{f.card}</FieldLegend>
|
||||
<FieldGroup>
|
||||
<MoneyField control={form.control} name="cardLimit" label={f.cardLimit} />
|
||||
<Controller
|
||||
name="cardStatementDay"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<Field>
|
||||
<FieldLabel htmlFor="acc-card-statement">{f.cardStatementDay}</FieldLabel>
|
||||
<DaySelect
|
||||
id="acc-card-statement"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
<FieldDescription>{f.cardStatementDayHint}</FieldDescription>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<Controller
|
||||
name="cardDueKind"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<Field>
|
||||
<FieldLabel htmlFor="acc-card-due-kind">{f.cardDueKind}</FieldLabel>
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger id="acc-card-due-kind" className="w-full">
|
||||
<SelectValue>{f.dueKinds[field.value]}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DUE_RULE_KINDS.map((k) => (
|
||||
<SelectItem key={k} value={k}>
|
||||
{f.dueKinds[k]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="cardDueValue"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor="acc-card-due-value">
|
||||
{dueKind === "daysAfter" ? f.cardDueValueDays : f.cardDueValueDay}
|
||||
</FieldLabel>
|
||||
<DaySelect
|
||||
id="acc-card-due-value"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
max={dueKind === "daysAfter" ? 30 : 31}
|
||||
/>
|
||||
{fieldState.invalid ? <FieldError errors={[fieldState.error]} /> : null}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
name="cardPaymentAccountId"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor="acc-card-payer">{f.cardPaymentAccount}</FieldLabel>
|
||||
<Select
|
||||
value={field.value || NONE}
|
||||
onValueChange={(v) => field.onChange(v === NONE ? "" : v)}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="acc-card-payer"
|
||||
className="w-full"
|
||||
aria-invalid={fieldState.invalid}
|
||||
>
|
||||
<SelectValue>
|
||||
{paymentAccounts.find((p) => p.id === field.value)?.name ??
|
||||
f.cardPaymentAccountNone}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NONE}>{f.cardPaymentAccountNone}</SelectItem>
|
||||
{paymentAccounts
|
||||
.filter((p) => p.id !== account?.id)
|
||||
.map((p) => (
|
||||
<SelectItem key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{fieldState.invalid ? <FieldError errors={[fieldState.error]} /> : null}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
) : null}
|
||||
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button type="button" variant="ghost" className="-ml-2 w-fit">
|
||||
<ChevronDown
|
||||
className={
|
||||
advancedOpen ? "rotate-180 transition-transform" : "transition-transform"
|
||||
}
|
||||
/>
|
||||
{f.advanced}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-3">
|
||||
<FieldGroup>
|
||||
{type === "bank" ? (
|
||||
<FieldSet>
|
||||
<FieldLegend>{f.overdraft}</FieldLegend>
|
||||
<FieldGroup>
|
||||
<Controller
|
||||
name="overdraftEnabled"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
id="acc-od"
|
||||
checked={field.value}
|
||||
onCheckedChange={(v) => field.onChange(v === true)}
|
||||
/>
|
||||
<FieldLabel htmlFor="acc-od" className="font-normal">
|
||||
{f.overdraftEnabled}
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
{overdraftEnabled ? (
|
||||
<>
|
||||
<MoneyField
|
||||
control={form.control}
|
||||
name="overdraftLimit"
|
||||
label={f.overdraftLimit}
|
||||
/>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<Controller
|
||||
name="overdraftMonthlyRate"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor="acc-od-rate">{f.overdraftRate}</FieldLabel>
|
||||
<Input
|
||||
{...field}
|
||||
id="acc-od-rate"
|
||||
inputMode="decimal"
|
||||
placeholder="4,25"
|
||||
className="w-28 tabular-nums"
|
||||
aria-invalid={fieldState.invalid}
|
||||
/>
|
||||
{fieldState.invalid ? (
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
) : null}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="overdraftStatementDay"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<Field>
|
||||
<FieldLabel htmlFor="acc-od-day">
|
||||
{f.overdraftStatementDay}
|
||||
</FieldLabel>
|
||||
<DaySelect
|
||||
id="acc-od-day"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
allowEmpty
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
) : null}
|
||||
|
||||
<Controller
|
||||
name="ownerUserId"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor="acc-owner">{f.owner}</FieldLabel>
|
||||
<Select
|
||||
value={field.value || NONE}
|
||||
onValueChange={(v) => field.onChange(v === NONE ? "" : v)}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="acc-owner"
|
||||
className="w-full"
|
||||
aria-invalid={fieldState.invalid}
|
||||
>
|
||||
<SelectValue>
|
||||
{members.find((m) => m.id === field.value)?.name ?? f.ownerNone}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NONE}>{f.ownerNone}</SelectItem>
|
||||
{members.map((m) => (
|
||||
<SelectItem key={m.id} value={m.id}>
|
||||
{m.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{fieldState.invalid ? <FieldError errors={[fieldState.error]} /> : null}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="isPrivate"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
id="acc-private"
|
||||
checked={field.value}
|
||||
onCheckedChange={(v) => field.onChange(v === true)}
|
||||
/>
|
||||
<FieldLabel htmlFor="acc-private" className="font-normal">
|
||||
{f.isPrivate}
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="color"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<Field>
|
||||
<FieldLabel id="acc-color-label">{tr.categories.color}</FieldLabel>
|
||||
<SwatchPicker
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
labelledBy="acc-color-label"
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="icon"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<Field>
|
||||
<FieldLabel id="acc-icon-label">{tr.categories.icon}</FieldLabel>
|
||||
<IconPicker
|
||||
value={field.value}
|
||||
color={color}
|
||||
onChange={field.onChange}
|
||||
labelledBy="acc-icon-label"
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</FieldGroup>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
{form.formState.errors.root ? (
|
||||
<FieldError>{form.formState.errors.root.message}</FieldError>
|
||||
) : null}
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user