fa70168622
- /alisveris, /alisveris/:id ve /alisveris/:id/mod sayfaları; odak ve 30 sn yenileme - stok önerileri paneli, kalem ekle/düzenle/taşı, mağazaya gruplu kontrol listesi - dashboard Alışveriş kartı, kenar menü, ürün tipi formunda hacimli işareti Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
121 lines
4.7 KiB
TypeScript
121 lines
4.7 KiB
TypeScript
import type { Metadata } from "next";
|
||
import Link from "next/link";
|
||
import { Suspense } from "react";
|
||
import { PageHeader } from "@/components/page-header";
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { SummaryCardsSkeleton } from "@/features/dashboard/components/summary-cards-skeleton";
|
||
import { DailyPlanCard } from "@/features/budgets/components/daily-plan-card";
|
||
import { BudgetWarningBanner } from "@/features/budgets/components/warning-banner";
|
||
import { getIncomePlan } from "@/features/budgets/plan";
|
||
import { getBudget } from "@/features/budgets/service";
|
||
import { ContactsDueBanner } from "@/features/contacts/components/due-banner";
|
||
import { getNetWorth } from "@/features/net-worth/service";
|
||
import { OverdueBanner } from "@/features/recurring/components/overdue-banner";
|
||
import { ShoppingCard } from "@/features/shopping/components/shopping-card";
|
||
import { LowStockCard } from "@/features/stock/components/low-stock-card";
|
||
import { tr } from "@/i18n/tr";
|
||
import { requireActiveHousehold } from "@/lib/dal";
|
||
import { cn } from "@/lib/utils";
|
||
import { formatMoney } from "@/shared/format";
|
||
|
||
export const metadata: Metadata = { title: tr.dashboard.title };
|
||
|
||
type SummaryCard = {
|
||
key: keyof typeof tr.dashboard.cards;
|
||
tone?: string;
|
||
amountMinor: number | null;
|
||
href?: string;
|
||
};
|
||
|
||
/**
|
||
* Özet kartları (içinde bulunulan bütçe ayı): gerçekleşen gelir, gider ve farkı; hesap bakiyesi,
|
||
* kart borcu ve net varlık.
|
||
*/
|
||
async function getSummary(userId: string, householdId: string): Promise<SummaryCard[]> {
|
||
const [nw, budget] = await Promise.all([
|
||
getNetWorth(userId, householdId),
|
||
getBudget(userId, householdId),
|
||
]);
|
||
const income = budget ? await getIncomePlan(userId, householdId, budget.month) : null;
|
||
const inc = income?.actualMinor ?? null;
|
||
const exp = budget?.totals.actualMinor ?? null;
|
||
return [
|
||
{ key: "monthIncome", tone: "text-income", amountMinor: inc, href: "/butce" },
|
||
{ key: "monthExpense", tone: "text-expense", amountMinor: exp, href: "/butce" },
|
||
{
|
||
key: "monthRemaining",
|
||
tone: "text-budget",
|
||
amountMinor: inc !== null && exp !== null ? inc - exp : null,
|
||
},
|
||
{ key: "totalBalance", amountMinor: nw?.accountsMinor ?? null, href: "/hesaplar" },
|
||
{ key: "cardDebt", tone: "text-expense", amountMinor: nw?.cardDebtMinor ?? null },
|
||
{ key: "netWorth", amountMinor: nw?.totalMinor ?? null, href: "/birikim" },
|
||
];
|
||
}
|
||
|
||
async function SummaryCards({ userId, householdId }: { userId: string; householdId: string }) {
|
||
const cards = await getSummary(userId, householdId);
|
||
return (
|
||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-3 xl:grid-cols-6">
|
||
{cards.map((c) => (
|
||
<Card key={c.key} size="sm">
|
||
<CardHeader>
|
||
<CardDescription>{tr.dashboard.cards[c.key]}</CardDescription>
|
||
<CardTitle className={cn("text-lg tabular-nums", c.tone)}>
|
||
{c.amountMinor === null ? (
|
||
"—"
|
||
) : c.href ? (
|
||
<Link href={c.href} className="hover:underline">
|
||
{formatMoney(c.amountMinor)}
|
||
</Link>
|
||
) : (
|
||
formatMoney(c.amountMinor)
|
||
)}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
{c.amountMinor === null ? (
|
||
<CardContent className="text-xs text-muted-foreground">
|
||
{tr.dashboard.noData}
|
||
</CardContent>
|
||
) : null}
|
||
</Card>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default async function DashboardPage() {
|
||
const { user, household } = await requireActiveHousehold("viewer");
|
||
return (
|
||
<>
|
||
<PageHeader
|
||
title={tr.dashboard.welcome(user.name.split(" ")[0] ?? user.name)}
|
||
description={`${household.name} · ${tr.dashboard.welcomeDescription}`}
|
||
/>
|
||
<Suspense fallback={null}>
|
||
<OverdueBanner userId={user.id} householdId={household.id} />
|
||
</Suspense>
|
||
<Suspense fallback={null}>
|
||
<BudgetWarningBanner userId={user.id} householdId={household.id} />
|
||
</Suspense>
|
||
<Suspense fallback={null}>
|
||
<ContactsDueBanner userId={user.id} householdId={household.id} />
|
||
</Suspense>
|
||
<Suspense fallback={<SummaryCardsSkeleton />}>
|
||
<SummaryCards userId={user.id} householdId={household.id} />
|
||
</Suspense>
|
||
<Suspense fallback={null}>
|
||
<DailyPlanCard userId={user.id} householdId={household.id} />
|
||
</Suspense>
|
||
<div className="grid gap-4 lg:grid-cols-2 lg:items-start">
|
||
<Suspense fallback={null}>
|
||
<LowStockCard userId={user.id} householdId={household.id} />
|
||
</Suspense>
|
||
<Suspense fallback={null}>
|
||
<ShoppingCard userId={user.id} householdId={household.id} />
|
||
</Suspense>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|