Files
butcem/src/features/in-kind/components/prices-view.tsx
T

236 lines
8.5 KiB
TypeScript

"use client";
import { ChevronDown, Plus } from "lucide-react";
import { Fragment, 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 { 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 { tr } from "@/i18n/tr";
import { cn } from "@/lib/utils";
import { ASSET_KEYS, ASSETS, type AssetKey } from "@/shared/assets";
import { todayIstanbul } from "@/shared/dates";
import { formatDate, formatMoney } from "@/shared/format";
import { formatMoneyInput } from "@/shared/money";
import { savePriceAction } from "../actions";
import type { PriceRow } from "../service";
const t = tr.prices;
const d = (ymd: string) => formatDate(`${ymd}T12:00:00+03:00`);
export function PricesView({ rows, canWrite }: { rows: PriceRow[]; canWrite: boolean }) {
const [editing, setEditing] = useState<AssetKey | null>(null);
const [openRow, setOpenRow] = useState<AssetKey | null>(null);
return (
<div className="flex flex-col gap-4">
{canWrite ? (
<div className="flex justify-end">
<Button onClick={() => setEditing("gram_gold")}>
<Plus />
{t.add}
</Button>
</div>
) : null}
<div className="overflow-x-auto rounded-xl border bg-card">
<table className="w-full text-sm">
<thead className="text-xs text-muted-foreground">
<tr className="border-b">
<th className="p-3 text-left font-normal">{t.asset}</th>
<th className="p-3 text-right font-normal">{t.buy}</th>
<th className="p-3 text-right font-normal">{t.sell}</th>
<th className="p-3 text-right font-normal">{t.date}</th>
<th className="w-10" />
</tr>
</thead>
<tbody className="divide-y tabular-nums">
{rows.map((r) => (
<Fragment key={r.assetKey}>
<tr>
<td className="p-3">
<button
type="button"
className="flex items-center gap-1 text-left font-medium disabled:cursor-default"
onClick={() => setOpenRow(openRow === r.assetKey ? null : r.assetKey)}
disabled={r.history.length < 2}
aria-expanded={openRow === r.assetKey}
>
{r.history.length > 1 ? (
<ChevronDown
className={cn(
"size-4 transition-transform",
openRow === r.assetKey && "rotate-180",
)}
/>
) : null}
{ASSETS[r.assetKey].label}
<span className="text-xs font-normal text-muted-foreground">
{t.perUnit(ASSETS[r.assetKey].unit)}
</span>
</button>
</td>
<td className="p-3 text-right">
{r.latest ? formatMoney(r.latest.buyMinor) : "—"}
</td>
<td className="p-3 text-right">
{r.latest ? formatMoney(r.latest.sellMinor) : "—"}
</td>
<td className="p-3 text-right text-muted-foreground">
{r.latest ? d(r.latest.date) : t.noPrice}
</td>
<td className="p-1 text-right">
{canWrite ? (
<Button
size="icon-sm"
variant="ghost"
onClick={() => setEditing(r.assetKey)}
aria-label={`${t.add}: ${ASSETS[r.assetKey].label}`}
>
<Plus />
</Button>
) : null}
</td>
</tr>
{openRow === r.assetKey
? r.history.slice(1).map((h) => (
<tr key={h.date} className="bg-muted/30 text-muted-foreground">
<td className="p-2 pl-9 text-xs">{t.history}</td>
<td className="p-2 text-right">{formatMoney(h.buyMinor)}</td>
<td className="p-2 text-right">{formatMoney(h.sellMinor)}</td>
<td className="p-2 text-right">{d(h.date)}</td>
<td />
</tr>
))
: null}
</Fragment>
))}
</tbody>
</table>
</div>
{editing ? (
<PriceForm
key={editing}
assetKey={editing}
latest={rows.find((r) => r.assetKey === editing)?.latest ?? null}
onClose={() => setEditing(null)}
/>
) : null}
</div>
);
}
function PriceForm({
assetKey: initial,
latest,
onClose,
}: {
assetKey: AssetKey;
latest: PriceRow["latest"];
onClose: () => void;
}) {
const [v, setV] = useState({
assetKey: initial,
date: todayIstanbul(),
buy: latest ? formatMoneyInput(latest.buyMinor) : "",
sell: latest ? formatMoneyInput(latest.sellMinor) : "",
});
const [errors, setErrors] = useState<Record<string, string | undefined>>({});
const [busy, setBusy] = useState(false);
async function submit(e: React.FormEvent) {
e.preventDefault();
setBusy(true);
const r = await savePriceAction(v);
setBusy(false);
if (r.ok) {
toast.success(t.saved);
onClose();
} else setErrors({ ...r.fieldErrors, root: r.fieldErrors ? undefined : r.error });
}
return (
<ResponsiveDialog
open
onOpenChange={(o) => !o && onClose()}
title={t.add}
footer={
<>
<Button variant="outline" type="button" onClick={onClose}>
{tr.common.cancel}
</Button>
<Button type="submit" form="price-form" disabled={busy}>
{tr.common.save}
</Button>
</>
}
>
<form id="price-form" onSubmit={submit} noValidate>
<FieldGroup>
<div className="grid grid-cols-[1fr_auto] gap-3">
<Field>
<FieldLabel htmlFor="pr-asset">{t.asset}</FieldLabel>
<Select
value={v.assetKey}
onValueChange={(x) => setV((s) => ({ ...s, assetKey: x as AssetKey }))}
>
<SelectTrigger id="pr-asset" 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>
</Field>
<Field data-invalid={Boolean(errors.date)}>
<FieldLabel htmlFor="pr-date">{t.date}</FieldLabel>
<Input
id="pr-date"
type="date"
value={v.date}
max={todayIstanbul()}
onChange={(e) => setV((s) => ({ ...s, date: e.target.value }))}
/>
{errors.date ? <FieldError>{errors.date}</FieldError> : null}
</Field>
</div>
<div className="grid grid-cols-2 gap-3">
<Field data-invalid={Boolean(errors.buy)}>
<FieldLabel htmlFor="pr-buy">{t.buy}</FieldLabel>
<MoneyInput
id="pr-buy"
value={v.buy}
onChange={(x) => setV((s) => ({ ...s, buy: x }))}
autoFocus
/>
{errors.buy ? <FieldError>{errors.buy}</FieldError> : null}
</Field>
<Field>
<FieldLabel htmlFor="pr-sell">{t.sell}</FieldLabel>
<MoneyInput
id="pr-sell"
value={v.sell}
onChange={(x) => setV((s) => ({ ...s, sell: x }))}
/>
</Field>
</div>
<FieldDescription className="-mt-2">{t.sellHint}</FieldDescription>
{errors.root ? <FieldError>{errors.root}</FieldError> : null}
</FieldGroup>
</form>
</ResponsiveDialog>
);
}