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

308 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 {
ArrowLeft,
EllipsisVertical,
Gift,
HandCoins,
Lock,
Pencil,
ShoppingBag,
Trash2,
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
import { PageHeader } from "@/components/page-header";
import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { tr } from "@/i18n/tr";
import { ASSETS, formatAssetQuantity } from "@/shared/assets";
import { formatDate, formatMoney } from "@/shared/format";
import { isInflow, type MovementKind } from "@/shared/portfolio";
import { deleteAssetAction, deleteMovementAction } from "../actions";
import type { AssetDetail, MovementRow } from "../service";
import { AssetFormDialog, MovementDialog } from "./asset-dialogs";
import { ProfitLoss, Stat } from "./assets-view";
const t = tr.assets;
const d = (ymd: string) => formatDate(`${ymd}T12:00:00+03:00`);
type Pending =
| { kind: "edit" }
| { kind: "movement"; movement: MovementKind }
| { kind: "deleteMovement"; row: MovementRow }
| { kind: "deleteAsset" }
| null;
/** /birikim/:id — özet, hareket ekleme ve hareket geçmişi. */
export function AssetDetailView({ data }: { data: AssetDetail }) {
const router = useRouter();
const [pending, setPending] = useState<Pending>(null);
const close = () => setPending(null);
const s = data.summary;
const unit = ASSETS[data.assetKey].unit;
return (
<div className="flex flex-col gap-4">
<Button variant="ghost" size="sm" asChild className="-ml-2 w-fit">
<Link href="/birikim">
<ArrowLeft />
{t.back}
</Link>
</Button>
<PageHeader
title={data.name}
description={ASSETS[data.assetKey].label}
actions={
data.canWrite ? (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button size="icon" variant="outline" aria-label={tr.recurring.actions.more}>
<EllipsisVertical />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={() => setPending({ kind: "edit" })}>
<Pencil />
{tr.common.edit}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
disabled={data.movementCount > 0}
onSelect={() => setPending({ kind: "deleteAsset" })}
>
<Trash2 />
{t.delete}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : null
}
/>
{data.isPrivate ? (
<Badge variant="outline" className="-mt-2 w-fit gap-1">
<Lock className="size-3" />
{t.privateBadge}
</Badge>
) : null}
<Card size="sm">
<CardContent className="flex flex-col gap-3">
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<Stat label={t.stats.quantity}>{formatAssetQuantity(data.assetKey, s.quantity)}</Stat>
<Stat label={t.stats.avgCost}>
{s.avgCostMinor !== null
? `${formatMoney(s.avgCostMinor)} ${t.stats.perUnit(unit)}`
: "—"}
</Stat>
<Stat label={t.stats.cost}>{formatMoney(s.costMinor)}</Stat>
<Stat label={t.stats.currentValue}>
{s.currentValueMinor !== null ? formatMoney(s.currentValueMinor) : t.stats.noPrice}
</Stat>
<Stat label={t.stats.unrealized}>
<ProfitLoss minor={s.unrealizedMinor} bp={s.unrealizedBp} />
</Stat>
<Stat label={t.stats.realized}>
<ProfitLoss minor={s.realizedMinor} />
</Stat>
<Stat label={t.stats.invested}>{formatMoney(s.investedMinor)}</Stat>
<Stat label={t.stats.proceeds}>{formatMoney(s.proceedsMinor)}</Stat>
</div>
{data.price ? (
<p className="text-xs text-muted-foreground">
{t.stats.price(formatMoney(data.price.buyMinor), d(data.price.date))} ·{" "}
<Link href="/fiyatlar" className="underline-offset-2 hover:underline">
{t.prices}
</Link>
</p>
) : null}
{data.storage ? (
<p className="text-sm">
<span className="text-muted-foreground">{t.movement.storage}:</span> {data.storage}
</p>
) : null}
{data.note ? <p className="text-sm text-muted-foreground">{data.note}</p> : null}
{data.canWrite ? (
<div className="flex flex-wrap justify-end gap-2">
<Button
size="sm"
variant="outline"
onClick={() => setPending({ kind: "movement", movement: "gift_in" })}
>
<Gift />
{t.kinds.gift_in}
</Button>
{s.quantity > 0 ? (
<Button
size="sm"
variant="outline"
onClick={() => setPending({ kind: "movement", movement: "sell" })}
>
<HandCoins />
{t.kinds.sell}
</Button>
) : null}
<Button size="sm" onClick={() => setPending({ kind: "movement", movement: "buy" })}>
<ShoppingBag />
{t.kinds.buy}
</Button>
</div>
) : null}
</CardContent>
</Card>
<section className="flex flex-col gap-2" aria-labelledby="movements-title">
<h2 id="movements-title" className="text-base font-medium">
{t.history}
</h2>
{data.movements.length === 0 ? (
<p className="rounded-lg border border-dashed p-6 text-center text-sm text-muted-foreground">
{t.noMovements}
</p>
) : (
<ul className="divide-y rounded-xl border bg-card">
{data.movements.map((row) => (
<MovementItem
key={row.id}
row={row}
data={data}
canWrite={data.canWrite}
onDelete={() => setPending({ kind: "deleteMovement", row })}
/>
))}
</ul>
)}
</section>
{pending?.kind === "edit" ? <AssetFormDialog item={data} onClose={close} /> : null}
{pending?.kind === "movement" ? (
<MovementDialog item={data} initialKind={pending.movement} onClose={close} />
) : null}
<AlertDialog
open={pending?.kind === "deleteMovement" || pending?.kind === "deleteAsset"}
onOpenChange={(o) => !o && close()}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{pending?.kind === "deleteAsset" ? t.deleteTitle : t.deleteMovementTitle}
</AlertDialogTitle>
<AlertDialogDescription>
{pending?.kind === "deleteAsset" ? t.deleteDescription : t.deleteMovementDescription}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{tr.common.cancel}</AlertDialogCancel>
<Button
variant="destructive"
onClick={async () => {
if (pending?.kind === "deleteAsset") {
const r = await deleteAssetAction(data.id);
close();
if (r.ok) {
toast.success(t.toast.deleted);
router.push("/birikim");
} else toast.error(r.error);
} else if (pending?.kind === "deleteMovement") {
const r = await deleteMovementAction(pending.row.id);
close();
if (r.ok) toast.success(t.toast.movementDeleted);
else toast.error(r.error);
}
}}
>
{pending?.kind === "deleteAsset" ? t.delete : t.deleteMovement}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
function MovementItem({
row,
data,
canWrite,
onDelete,
}: {
row: MovementRow;
data: AssetDetail;
canWrite: boolean;
onDelete: () => void;
}) {
const inflow = isInflow(row.kind);
const qty = formatAssetQuantity(data.assetKey, row.quantity);
const unit = ASSETS[data.assetKey].unit;
return (
<li className="flex items-start gap-3 p-3">
<div className="min-w-0 flex-1">
<p className="flex flex-wrap items-center gap-x-2 text-sm font-medium">
<span>{t.kinds[row.kind]}</span>
<span className={inflow ? "text-income" : "text-expense"}>
{inflow ? "+" : "−"}
{qty}
</span>
</p>
<p className="text-xs text-muted-foreground">
{d(row.date)}
{row.unitPriceMinor !== null
? ` · ${formatMoney(row.unitPriceMinor)} ${t.stats.perUnit(unit)}`
: ""}
{row.account ? ` · ${row.account}` : ""}
{row.storage ? ` · ${row.storage}` : ""}
</p>
{row.note ? <p className="text-xs text-muted-foreground">{row.note}</p> : null}
<p className="text-xs text-muted-foreground">
{t.table.balance}: {formatAssetQuantity(data.assetKey, row.quantityAfter)}
</p>
</div>
<div className="flex shrink-0 flex-col items-end text-sm">
{row.kind !== "gift_out" ? (
<span className="tabular-nums">{formatMoney(row.amountMinor)}</span>
) : (
<span className="text-xs text-muted-foreground tabular-nums">
{t.stats.cost} {formatMoney(row.costOutMinor)}
</span>
)}
{row.kind === "sell" ? (
<span className="text-xs">
{t.table.result}: <ProfitLoss minor={row.realizedMinor} />
</span>
) : null}
</div>
{canWrite && !row.fromInKind ? (
<Button
size="icon-sm"
variant="ghost"
aria-label={t.deleteMovement}
onClick={onDelete}
className="shrink-0"
>
<Trash2 />
</Button>
) : null}
</li>
);
}