cc624af69f
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
707 lines
26 KiB
TypeScript
707 lines
26 KiB
TypeScript
"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;
|
||
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 (
|
||
<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>
|
||
)}
|
||
/>
|
||
<Controller
|
||
name="cardMinimumRate"
|
||
control={form.control}
|
||
render={({ field, fieldState }) => (
|
||
<Field data-invalid={fieldState.invalid}>
|
||
<FieldLabel htmlFor="acc-card-min">{f.cardMinimumRate}</FieldLabel>
|
||
<Input
|
||
{...field}
|
||
id="acc-card-min"
|
||
inputMode="decimal"
|
||
placeholder="20"
|
||
className="w-28 tabular-nums"
|
||
aria-describedby="acc-card-min-hint"
|
||
aria-invalid={fieldState.invalid}
|
||
/>
|
||
<FieldDescription id="acc-card-min-hint">
|
||
{f.cardMinimumRateHint}
|
||
</FieldDescription>
|
||
{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>
|
||
);
|
||
}
|