feat(ui): değer grafiği, net varlık kartı, dashboard kartları ve ödeme diyaloğunda varlık bağı

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-06 00:19:05 +03:00
parent 9a60085d14
commit b022c1c3b0
10 changed files with 825 additions and 36 deletions
@@ -16,6 +16,7 @@ import {
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
@@ -35,6 +36,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { getAssetOptionsAction, type AssetOption } from "@/features/assets/actions";
import { getTransactionFormOptionsAction } from "@/features/transactions/actions";
import { AccountSelect } from "@/features/transactions/components/transaction-form";
import type { TransactionFormOptions } from "@/features/transactions/service";
@@ -79,6 +81,21 @@ export function useAccounts() {
return options?.accounts ?? null;
}
/** Kaydın varlık türündeki birikim varlıkları (ödeme diyaloğu açıldığında). */
function useAssetOptions(assetKey: AssetKey) {
const [options, setOptions] = useState<AssetOption[] | null>(null);
useEffect(() => {
let cancelled = false;
getAssetOptionsAction().then((r) => {
if (!cancelled && r.ok) setOptions(r.data.filter((a) => a.assetKey === assetKey));
});
return () => {
cancelled = true;
};
}, [assetKey]);
return options;
}
function Stat({ label, value, tone }: { label: string; value: string; tone?: string }) {
return (
<div className="min-w-0">
@@ -602,16 +619,23 @@ function PaymentForm({ item, onClose }: { item: InKindView; onClose: () => void
});
const [errors, setErrors] = useState<Errors>({});
const [busy, setBusy] = useState(false);
const assets = useAssetOptions(item.assetKey);
const [link, setLink] = useState(false);
const [assetId, setAssetId] = useState("");
const linked = method === "asset" && link;
const linkAssetId = assetId || assets?.[0]?.id || "";
async function submit(e: React.FormEvent) {
e.preventDefault();
setBusy(true);
const amount = linked ? "" : v.amount;
const r = await recordPaymentAction(item.id, {
method,
date: v.date,
quantity: method === "cash" ? "" : v.quantity,
amount: v.amount,
accountId: v.amount ? v.accountId || accounts?.[0]?.id || "" : "",
amount,
accountId: amount ? v.accountId || accounts?.[0]?.id || "" : "",
assetId: linked ? linkAssetId : "",
note: v.note,
});
setBusy(false);
@@ -659,17 +683,19 @@ function PaymentForm({ item, onClose }: { item: InKindView; onClose: () => void
{errors.quantity ? <FieldError>{errors.quantity}</FieldError> : null}
</Field>
) : null}
<Field data-invalid={Boolean(errors.amount)}>
<FieldLabel htmlFor="pay-amount">
{method === "cash" ? f.amount : f.assetAmount}
</FieldLabel>
<MoneyInput
id="pay-amount"
value={v.amount}
onChange={(x) => setV((s) => ({ ...s, amount: x }))}
/>
{errors.amount ? <FieldError>{errors.amount}</FieldError> : null}
</Field>
{linked || (method === "asset" && !debt) ? null : (
<Field data-invalid={Boolean(errors.amount)}>
<FieldLabel htmlFor="pay-amount">
{method === "cash" ? f.amount : f.assetAmount}
</FieldLabel>
<MoneyInput
id="pay-amount"
value={v.amount}
onChange={(x) => setV((s) => ({ ...s, amount: x }))}
/>
{errors.amount ? <FieldError>{errors.amount}</FieldError> : null}
</Field>
)}
<Field>
<FieldLabel htmlFor="pay-date">{tr.transactions.fields.date}</FieldLabel>
<Input
@@ -684,7 +710,49 @@ function PaymentForm({ item, onClose }: { item: InKindView; onClose: () => void
{method === "cash" ? (
<FieldDescription className="-mt-2">{f.quantityHint}</FieldDescription>
) : null}
{method === "cash" || v.amount ? (
{method === "asset" ? (
<div className="flex flex-col gap-2 rounded-lg border p-3">
<label className="flex items-start gap-2 text-sm">
<Checkbox
checked={link}
onCheckedChange={(x) => setLink(x === true)}
disabled={!assets?.length}
className="mt-0.5"
/>
<span>
{debt ? t.link.debt : t.link.receivable}
<span className="block text-xs text-muted-foreground">
{assets && !assets.length
? t.link.none
: debt
? t.link.debtHint
: t.link.receivableHint}
</span>
</span>
</label>
{link && assets?.length ? (
<Field data-invalid={Boolean(errors.assetId)}>
<FieldLabel htmlFor="pay-asset" className="sr-only">
{t.link.asset}
</FieldLabel>
<Select value={linkAssetId} onValueChange={setAssetId}>
<SelectTrigger id="pay-asset" className="w-full">
<SelectValue>{assets.find((a) => a.id === linkAssetId)?.name}</SelectValue>
</SelectTrigger>
<SelectContent>
{assets.map((a) => (
<SelectItem key={a.id} value={a.id}>
{a.name} · {formatAssetQuantity(item.assetKey, a.quantity)}
</SelectItem>
))}
</SelectContent>
</Select>
{errors.assetId ? <FieldError>{errors.assetId}</FieldError> : null}
</Field>
) : null}
</div>
) : null}
{(method === "cash" || v.amount) && !linked ? (
<Field data-invalid={Boolean(errors.accountId)}>
<FieldLabel htmlFor="pay-account">{f.account}</FieldLabel>
<AccountSelect