Files
butcem/src/features/assets/components/asset-dialogs.tsx
T

316 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { toast } from "sonner";
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 { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useAccounts } from "@/features/in-kind/components/in-kind-section";
import { tr } from "@/i18n/tr";
import { ASSET_KEYS, ASSETS, formatAssetQuantity, type AssetKey } from "@/shared/assets";
import { todayIstanbul } from "@/shared/dates";
import { MOVEMENT_KINDS, type MovementKind } from "@/shared/portfolio";
import { addMovementAction, saveAssetAction } from "../actions";
import type { AssetView } from "../service";
const t = tr.assets;
type Errors = Record<string, string | undefined>;
const NONE = "__none__";
function Footer({ busy, formId, onClose }: { busy: boolean; formId: string; onClose: () => void }) {
return (
<>
<Button variant="outline" type="button" onClick={onClose}>
{tr.common.cancel}
</Button>
<Button type="submit" form={formId} disabled={busy}>
{tr.common.save}
</Button>
</>
);
}
/** Yeni varlık / varlığı düzenle. */
export function AssetFormDialog({
item,
onClose,
onCreated,
}: {
item: AssetView | null;
onClose: () => void;
onCreated?: (id: string) => void;
}) {
const [v, setV] = useState({
assetKey: (item?.assetKey ?? "gram_gold") as AssetKey,
name: item?.name ?? "",
note: item?.note ?? "",
isPrivate: item?.isPrivate ?? false,
});
const [errors, setErrors] = useState<Errors>({});
const [busy, setBusy] = useState(false);
const set = <K extends keyof typeof v>(k: K, x: (typeof v)[K]) => setV((s) => ({ ...s, [k]: x }));
const locked = Boolean(item?.movementCount);
async function submit(e: React.FormEvent) {
e.preventDefault();
setBusy(true);
const r = await saveAssetAction(item?.id ?? null, {
...v,
name: v.name.trim() || ASSETS[v.assetKey].label,
});
setBusy(false);
if (r.ok) {
toast.success(t.toast.saved);
onClose();
if (!item) onCreated?.((r.data as { id: string }).id);
} else setErrors({ ...r.fieldErrors, root: r.fieldErrors ? undefined : r.error });
}
const f = t.fields;
return (
<ResponsiveDialog
open
onOpenChange={(o) => !o && onClose()}
title={item ? t.editTitle : t.createTitle}
footer={<Footer busy={busy} formId="asset-form" onClose={onClose} />}
>
<form id="asset-form" onSubmit={submit} noValidate>
<FieldGroup>
<Field data-invalid={Boolean(errors.assetKey)}>
<FieldLabel htmlFor="as-key">{f.asset}</FieldLabel>
<Select
value={v.assetKey}
onValueChange={(x) => set("assetKey", x as AssetKey)}
disabled={locked}
>
<SelectTrigger id="as-key" className="w-full">
<SelectValue>{ASSETS[v.assetKey].label}</SelectValue>
</SelectTrigger>
<SelectContent>
{ASSET_KEYS.map((k) => (
<SelectItem key={k} value={k}>
{ASSETS[k].label}
</SelectItem>
))}
</SelectContent>
</Select>
{errors.assetKey ? <FieldError>{errors.assetKey}</FieldError> : null}
</Field>
<Field data-invalid={Boolean(errors.name)}>
<FieldLabel htmlFor="as-name">{f.name}</FieldLabel>
<Input
id="as-name"
value={v.name}
placeholder={ASSETS[v.assetKey].label}
onChange={(e) => set("name", e.target.value)}
aria-invalid={Boolean(errors.name)}
/>
{errors.name ? <FieldError>{errors.name}</FieldError> : null}
</Field>
<Field>
<FieldLabel htmlFor="as-note">{f.note}</FieldLabel>
<Input id="as-note" value={v.note} onChange={(e) => set("note", e.target.value)} />
</Field>
<label className="flex items-start gap-2 text-sm">
<Checkbox
checked={v.isPrivate}
onCheckedChange={(x) => set("isPrivate", x === true)}
className="mt-0.5"
/>
<span>
{f.isPrivate}
<span className="block text-xs text-muted-foreground">{f.isPrivateHint}</span>
</span>
</label>
{errors.root ? <FieldError>{errors.root}</FieldError> : null}
</FieldGroup>
</form>
</ResponsiveDialog>
);
}
/** Alım, gelen hediye, satış veya çıkış hareketi. */
export function MovementDialog({
item,
initialKind,
onClose,
}: {
item: AssetView;
initialKind: MovementKind;
onClose: () => void;
}) {
const accounts = useAccounts();
const [kind, setKind] = useState<MovementKind>(initialKind);
const [v, setV] = useState({
date: todayIstanbul(),
quantity: "",
amount: "",
unitPrice: "",
accountId: "",
storage: item.storage,
note: "",
});
const [errors, setErrors] = useState<Errors>({});
const [busy, setBusy] = useState(false);
const set = <K extends keyof typeof v>(k: K, x: (typeof v)[K]) => setV((s) => ({ ...s, [k]: x }));
const withAccount = kind === "buy" || kind === "sell";
const priced = kind !== "gift_out";
const inflow = kind === "buy" || kind === "gift_in";
async function submit(e: React.FormEvent) {
e.preventDefault();
setBusy(true);
const r = await addMovementAction(item.id, {
kind,
date: v.date,
quantity: v.quantity,
amount: priced ? v.amount : "",
unitPrice: priced ? v.unitPrice : "",
accountId: withAccount ? v.accountId : "",
storage: inflow ? v.storage : "",
note: v.note,
});
setBusy(false);
if (r.ok) {
toast.success(t.toast.movement);
onClose();
} else setErrors({ ...r.fieldErrors, root: r.fieldErrors ? undefined : r.error });
}
const m = t.movement;
const unit = ASSETS[item.assetKey].unit;
return (
<ResponsiveDialog
open
onOpenChange={(o) => !o && onClose()}
title={m.title(t.kinds[kind], item.name)}
description={m.held(formatAssetQuantity(item.assetKey, item.summary.quantity))}
footer={<Footer busy={busy} formId="movement-form" onClose={onClose} />}
>
<form id="movement-form" onSubmit={submit} noValidate>
<FieldGroup>
<Tabs
value={kind}
onValueChange={(x) => {
setKind(x as MovementKind);
setErrors({});
}}
>
<TabsList className="grid h-auto w-full grid-cols-2 sm:grid-cols-4">
{MOVEMENT_KINDS.map((k) => (
<TabsTrigger key={k} value={k}>
{t.kinds[k]}
</TabsTrigger>
))}
</TabsList>
</Tabs>
<p className="-mt-2 text-xs text-muted-foreground">{t.kindHints[kind]}</p>
<div className="grid grid-cols-2 gap-3">
<Field data-invalid={Boolean(errors.quantity)}>
<FieldLabel htmlFor="mv-qty">
{m.quantity} ({unit})
</FieldLabel>
<Input
id="mv-qty"
inputMode="decimal"
value={v.quantity}
onChange={(e) => set("quantity", e.target.value)}
className="text-right tabular-nums"
aria-invalid={Boolean(errors.quantity)}
autoFocus
/>
{errors.quantity ? <FieldError>{errors.quantity}</FieldError> : null}
</Field>
<Field data-invalid={Boolean(errors.date)}>
<FieldLabel htmlFor="mv-date">{m.date}</FieldLabel>
<Input
id="mv-date"
type="date"
value={v.date}
max={todayIstanbul()}
onChange={(e) => set("date", e.target.value)}
/>
{errors.date ? <FieldError>{errors.date}</FieldError> : null}
</Field>
</div>
{priced ? (
<>
<div className="grid grid-cols-2 gap-3">
<Field data-invalid={Boolean(errors.amount)}>
<FieldLabel htmlFor="mv-amount">{m.amount}</FieldLabel>
<MoneyInput id="mv-amount" value={v.amount} onChange={(x) => set("amount", x)} />
{errors.amount ? <FieldError>{errors.amount}</FieldError> : null}
</Field>
<Field data-invalid={Boolean(errors.unitPrice)}>
<FieldLabel htmlFor="mv-unit">{m.unitPrice}</FieldLabel>
<MoneyInput
id="mv-unit"
value={v.unitPrice}
onChange={(x) => set("unitPrice", x)}
/>
{errors.unitPrice ? <FieldError>{errors.unitPrice}</FieldError> : null}
</Field>
</div>
<FieldDescription className="-mt-2">
{kind === "gift_in" ? m.giftValueHint : m.priceHint}
</FieldDescription>
</>
) : null}
{withAccount ? (
<Field data-invalid={Boolean(errors.accountId)}>
<FieldLabel htmlFor="mv-account">{m.account}</FieldLabel>
<Select
value={v.accountId || NONE}
onValueChange={(x) => set("accountId", x === NONE ? "" : x)}
>
<SelectTrigger id="mv-account" className="w-full">
<SelectValue>
{accounts?.find((a) => a.id === v.accountId)?.name ?? m.accountNone}
</SelectValue>
</SelectTrigger>
<SelectContent className="max-h-72">
<SelectItem value={NONE}>{m.accountNone}</SelectItem>
{(accounts ?? []).map((a) => (
<SelectItem key={a.id} value={a.id}>
{a.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FieldDescription>{m.accountHint}</FieldDescription>
{errors.accountId ? <FieldError>{errors.accountId}</FieldError> : null}
</Field>
) : null}
{inflow ? (
<Field>
<FieldLabel htmlFor="mv-storage">{m.storage}</FieldLabel>
<Input
id="mv-storage"
value={v.storage}
placeholder={m.storagePlaceholder}
onChange={(e) => set("storage", e.target.value)}
/>
</Field>
) : null}
<Field>
<FieldLabel htmlFor="mv-note">{m.note}</FieldLabel>
<Input id="mv-note" value={v.note} onChange={(e) => set("note", e.target.value)} />
</Field>
{errors.root ? <FieldError>{errors.root}</FieldError> : null}
</FieldGroup>
</form>
</ResponsiveDialog>
);
}