c3c3c43bd1
- dönem seçici, tablolar, birim fiyat grafiği ve CSV indirme - rapor ana sayfasında yeni kartlar Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
251 lines
11 KiB
TypeScript
251 lines
11 KiB
TypeScript
import Link from "next/link";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { tr } from "@/i18n/tr";
|
||
import { cn } from "@/lib/utils";
|
||
import { formatMoney } from "@/shared/format";
|
||
import { formatAssetQuantity } from "@/shared/assets";
|
||
import type { AssetsReport, ContactsReport } from "../extra";
|
||
import { dayLabel } from "./report-frame";
|
||
|
||
const signed = (n: number) => `${n > 0 ? "+" : n < 0 ? "−" : ""}${formatMoney(Math.abs(n))}`;
|
||
const bp = (n: number | null) =>
|
||
n === null
|
||
? ""
|
||
: ` (${n < 0 ? "−" : "+"}%${(Math.abs(n) / 100).toLocaleString("tr-TR", { maximumFractionDigits: 1 })})`;
|
||
|
||
function Stat({ label, value, tone }: { label: string; value: string; tone?: string }) {
|
||
return (
|
||
<div className="rounded-xl border bg-card p-3">
|
||
<p className="text-xs text-muted-foreground">{label}</p>
|
||
<p className={cn("text-lg font-semibold tabular-nums", tone)}>{value}</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function ContactsReportView({ data }: { data: ContactsReport }) {
|
||
const t = tr.reports.contacts;
|
||
return (
|
||
<div className="flex flex-col gap-6">
|
||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||
<Stat label={t.owesMe} value={formatMoney(data.totals.owesMeMinor)} tone="text-income" />
|
||
<Stat label={t.iOwe} value={formatMoney(data.totals.iOweMinor)} tone="text-expense" />
|
||
<Stat
|
||
label={`${t.receivable} (${t.value.toLocaleLowerCase("tr-TR")})`}
|
||
value={formatMoney(data.inKindTotals.receivableMinor)}
|
||
/>
|
||
<Stat
|
||
label={`${t.debt} (${t.value.toLocaleLowerCase("tr-TR")})`}
|
||
value={formatMoney(data.inKindTotals.debtMinor)}
|
||
/>
|
||
</div>
|
||
|
||
<section aria-labelledby="cari-h" className="flex flex-col gap-2">
|
||
<h2 id="cari-h" className="font-medium">
|
||
{t.contact}
|
||
</h2>
|
||
{data.contacts.length ? (
|
||
<div className="overflow-x-auto rounded-xl border bg-card">
|
||
<table className="w-full min-w-[28rem] text-sm tabular-nums">
|
||
<thead className="border-b text-xs text-muted-foreground">
|
||
<tr>
|
||
<th className="px-3 py-2 text-left font-normal">{t.contact}</th>
|
||
<th className="px-3 py-2 text-left font-normal">{t.direction}</th>
|
||
<th className="px-3 py-2 text-right font-normal">{t.balance}</th>
|
||
<th className="px-3 py-2 text-right font-normal">{t.due}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y">
|
||
{data.contacts.map((c) => (
|
||
<tr key={c.id}>
|
||
<td className="px-3 py-1.5">
|
||
<Link href={`/cari/${c.id}`} className="hover:underline">
|
||
{c.name}
|
||
</Link>
|
||
</td>
|
||
<td className="px-3 py-1.5">{c.balanceMinor > 0 ? t.owesMe : t.iOwe}</td>
|
||
<td
|
||
className={cn(
|
||
"px-3 py-1.5 text-right font-medium",
|
||
c.balanceMinor > 0 ? "text-income" : "text-expense",
|
||
)}
|
||
>
|
||
{formatMoney(Math.abs(c.balanceMinor))}
|
||
</td>
|
||
<td className="px-3 py-1.5 text-right">
|
||
{c.dueDate ? dayLabel(c.dueDate) : "—"}
|
||
{t.dueStates[c.due] ? (
|
||
<Badge
|
||
variant="outline"
|
||
className={cn(
|
||
"ml-2",
|
||
c.due === "overdue" && "border-expense/50 text-expense",
|
||
)}
|
||
>
|
||
{t.dueStates[c.due]}
|
||
</Badge>
|
||
) : null}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
) : (
|
||
<p className="rounded-lg border border-dashed p-6 text-center text-sm text-muted-foreground">
|
||
{t.noContacts}
|
||
</p>
|
||
)}
|
||
</section>
|
||
|
||
<section aria-labelledby="ayni-h" className="flex flex-col gap-2">
|
||
<div>
|
||
<h2 id="ayni-h" className="font-medium">
|
||
{t.inKindTitle}
|
||
</h2>
|
||
<p className="text-xs text-muted-foreground">{t.inKindHint}</p>
|
||
</div>
|
||
{data.inKind.length ? (
|
||
<div className="overflow-x-auto rounded-xl border bg-card">
|
||
<table className="w-full min-w-[32rem] text-sm tabular-nums">
|
||
<thead className="border-b text-xs text-muted-foreground">
|
||
<tr>
|
||
<th className="px-3 py-2 text-left font-normal">{t.contact}</th>
|
||
<th className="px-3 py-2 text-left font-normal">{t.direction}</th>
|
||
<th className="px-3 py-2 text-right font-normal">{t.remaining}</th>
|
||
<th className="px-3 py-2 text-right font-normal">{t.value}</th>
|
||
<th className="px-3 py-2 text-right font-normal">{t.since}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y">
|
||
{data.inKind.map((d) => (
|
||
<tr key={d.id}>
|
||
<td className="px-3 py-1.5">
|
||
<Link href={`/cari/${d.contactId}`} className="hover:underline">
|
||
{d.contactName}
|
||
</Link>
|
||
</td>
|
||
<td className="px-3 py-1.5">
|
||
{d.direction === "receivable" ? t.receivable : t.debt}
|
||
</td>
|
||
<td className="px-3 py-1.5 text-right">{d.remainingLabel}</td>
|
||
<td className="px-3 py-1.5 text-right">
|
||
{d.valueMinor === null ? "—" : formatMoney(d.valueMinor)}
|
||
</td>
|
||
<td className="px-3 py-1.5 text-right">{dayLabel(d.date)}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
) : (
|
||
<p className="rounded-lg border border-dashed p-6 text-center text-sm text-muted-foreground">
|
||
{t.noInKind}
|
||
</p>
|
||
)}
|
||
{data.inKindTotals.unpriced ? (
|
||
<p className="text-xs text-muted-foreground">{t.unpriced(data.inKindTotals.unpriced)}</p>
|
||
) : null}
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function AssetsReportView({ data }: { data: AssetsReport }) {
|
||
const t = tr.reports.assets;
|
||
const p = data.period;
|
||
const tone = (n: number | null) =>
|
||
n === null ? "" : n < 0 ? "text-expense" : n > 0 ? "text-income" : "";
|
||
return (
|
||
<div className="flex flex-col gap-6">
|
||
<section aria-labelledby="period-h" className="flex flex-col gap-2">
|
||
<h2 id="period-h" className="font-medium">
|
||
{t.periodTitle}: {dayLabel(p.from)} – {dayLabel(p.to)}
|
||
</h2>
|
||
<div className="grid grid-cols-2 gap-3 md:grid-cols-5">
|
||
<Stat
|
||
label={t.startValue}
|
||
value={p.startValueMinor === null ? "—" : formatMoney(p.startValueMinor)}
|
||
/>
|
||
<Stat label={t.invested} value={formatMoney(p.investedMinor)} />
|
||
<Stat label={t.proceeds} value={formatMoney(p.proceedsMinor)} />
|
||
<Stat
|
||
label={t.endValue}
|
||
value={p.endValueMinor === null ? "—" : formatMoney(p.endValueMinor)}
|
||
/>
|
||
<Stat
|
||
label={t.profit}
|
||
value={p.profitMinor === null ? "—" : `${signed(p.profitMinor)}${bp(p.profitBp)}`}
|
||
tone={tone(p.profitMinor)}
|
||
/>
|
||
</div>
|
||
{p.excluded ? (
|
||
<p className="text-xs text-muted-foreground">{t.excluded(p.excluded)}</p>
|
||
) : null}
|
||
</section>
|
||
|
||
{data.items.length ? (
|
||
<div className="overflow-x-auto rounded-xl border bg-card">
|
||
<table className="w-full min-w-[40rem] text-sm tabular-nums">
|
||
<thead className="border-b text-xs text-muted-foreground">
|
||
<tr>
|
||
<th className="px-3 py-2 text-left font-normal">{t.asset}</th>
|
||
<th className="px-3 py-2 text-right font-normal">{t.quantity}</th>
|
||
<th className="px-3 py-2 text-right font-normal">{t.cost}</th>
|
||
<th className="px-3 py-2 text-right font-normal">{t.value}</th>
|
||
<th className="px-3 py-2 text-right font-normal">{t.unrealized}</th>
|
||
<th className="px-3 py-2 text-right font-normal">{t.realized}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y">
|
||
{data.items.map((a) => (
|
||
<tr key={a.id}>
|
||
<td className="px-3 py-1.5">
|
||
<Link href={`/birikim/${a.id}`} className="hover:underline">
|
||
{a.name || a.label}
|
||
</Link>
|
||
</td>
|
||
<td className="px-3 py-1.5 text-right">
|
||
{formatAssetQuantity(a.assetKey, a.summary.quantity)}
|
||
</td>
|
||
<td className="px-3 py-1.5 text-right">{formatMoney(a.summary.costMinor)}</td>
|
||
<td className="px-3 py-1.5 text-right">
|
||
{a.summary.currentValueMinor === null
|
||
? t.noPrice
|
||
: formatMoney(a.summary.currentValueMinor)}
|
||
</td>
|
||
<td className={cn("px-3 py-1.5 text-right", tone(a.summary.unrealizedMinor))}>
|
||
{a.summary.unrealizedMinor === null
|
||
? "—"
|
||
: `${signed(a.summary.unrealizedMinor)}${bp(a.summary.unrealizedBp)}`}
|
||
</td>
|
||
<td className={cn("px-3 py-1.5 text-right", tone(a.summary.realizedMinor))}>
|
||
{signed(a.summary.realizedMinor)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
<tfoot className="border-t font-medium">
|
||
<tr>
|
||
<td className="px-3 py-2">{t.total}</td>
|
||
<td />
|
||
<td className="px-3 py-2 text-right">{formatMoney(data.totals.costMinor)}</td>
|
||
<td className="px-3 py-2 text-right">{formatMoney(data.totals.valueMinor)}</td>
|
||
<td className={cn("px-3 py-2 text-right", tone(data.totals.unrealizedMinor))}>
|
||
{signed(data.totals.unrealizedMinor)}
|
||
</td>
|
||
<td className={cn("px-3 py-2 text-right", tone(data.totals.realizedMinor))}>
|
||
{signed(data.totals.realizedMinor)}
|
||
</td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
) : (
|
||
<p className="rounded-lg border border-dashed p-6 text-center text-sm text-muted-foreground">
|
||
{t.empty}
|
||
</p>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|