diff --git a/src/app/(app)/birikim/[id]/page.tsx b/src/app/(app)/birikim/[id]/page.tsx index 3f5c7f2..2e67999 100644 --- a/src/app/(app)/birikim/[id]/page.tsx +++ b/src/app/(app)/birikim/[id]/page.tsx @@ -1,17 +1,36 @@ import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { AssetDetailView } from "@/features/assets/components/asset-detail-view"; -import { getAsset } from "@/features/assets/service"; +import { PeriodReportView } from "@/features/assets/components/period-report"; +import { + getAsset, + getPeriodReport, + REPORT_RANGES, + type ReportRange, +} from "@/features/assets/service"; import { tr } from "@/i18n/tr"; import { requireActiveHousehold } from "@/lib/dal"; export const metadata: Metadata = { title: tr.assets.history }; -/** /birikim/:id — varlık hareketleri (gizli varlık yalnızca sahibine; aksi 404). */ -export default async function AssetPage({ params }: PageProps<"/birikim/[id]">) { +/** /birikim/:id — varlık hareketleri ve dönemsel rapor (gizli varlık yalnızca sahibine; aksi 404). */ +export default async function AssetPage({ params, searchParams }: PageProps<"/birikim/[id]">) { const { user, household } = await requireActiveHousehold("viewer"); const { id } = await params; - const data = await getAsset(user.id, household.id, id); - if (!data) notFound(); - return ; + const donem = (await searchParams).donem; + const range: ReportRange = REPORT_RANGES.includes(donem as ReportRange) + ? (donem as ReportRange) + : "6"; + const [data, report] = await Promise.all([ + getAsset(user.id, household.id, id), + getPeriodReport(user.id, household.id, range, id), + ]); + if (!data || !report) notFound(); + return ( + } + /> + ); } diff --git a/src/app/(app)/birikim/page.tsx b/src/app/(app)/birikim/page.tsx index 5372c00..ef7d36f 100644 --- a/src/app/(app)/birikim/page.tsx +++ b/src/app/(app)/birikim/page.tsx @@ -2,21 +2,45 @@ import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { PageHeader } from "@/components/page-header"; import { AssetsView } from "@/features/assets/components/assets-view"; -import { listAssets } from "@/features/assets/service"; +import { PeriodReportView } from "@/features/assets/components/period-report"; +import { + getPeriodReport, + listAssets, + REPORT_RANGES, + type ReportRange, +} from "@/features/assets/service"; +import { NetWorthCard } from "@/features/net-worth/components/net-worth-card"; +import { getNetWorth } from "@/features/net-worth/service"; import { tr } from "@/i18n/tr"; import { requireActiveHousehold } from "@/lib/dal"; export const metadata: Metadata = { title: tr.assets.title }; -/** /birikim — altın, döviz ve ziynet varlıkları; ortalama maliyet ve kâr/zarar. */ -export default async function AssetsPage() { +function parseRange(value: string | string[] | undefined): ReportRange { + return REPORT_RANGES.includes(value as ReportRange) ? (value as ReportRange) : "6"; +} + +/** /birikim — net varlık, varlıklar (ortalama maliyet, K/Z) ve dönemsel rapor. */ +export default async function AssetsPage({ searchParams }: PageProps<"/birikim">) { const { user, household } = await requireActiveHousehold("viewer"); - const data = await listAssets(user.id, household.id); - if (!data) notFound(); + const range = parseRange((await searchParams).donem); + const [data, report, netWorth] = await Promise.all([ + listAssets(user.id, household.id), + getPeriodReport(user.id, household.id, range), + getNetWorth(user.id, household.id), + ]); + if (!data || !report || !netWorth) notFound(); return ( <> +
+

+ {tr.assets.netWorth.title} +

+ +
+ ); } diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index 4f6aae1..fb0f8c2 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -1,9 +1,11 @@ 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 { ContactsDueBanner } from "@/features/contacts/components/due-banner"; +import { getNetWorth } from "@/features/net-worth/service"; import { OverdueBanner } from "@/features/recurring/components/overdue-banner"; import { tr } from "@/i18n/tr"; import { requireActiveHousehold } from "@/lib/dal"; @@ -16,22 +18,27 @@ type SummaryCard = { key: keyof typeof tr.dashboard.cards; tone?: string; amountMinor: number | null; + href?: string; }; -/** FAZ 14: gerçek özet verisi (gelir, gider, bakiye, kart borcu, net varlık). */ -async function getSummary(): Promise { +/** + * Özet kartları. Bakiye, kart borcu ve net varlık gerçek (Faz 9); bu ayın gelir/gider/kalan + * kartları Faz 14'te dolar. + */ +async function getSummary(userId: string, householdId: string): Promise { + const nw = await getNetWorth(userId, householdId); return [ { key: "monthIncome", tone: "text-income", amountMinor: null }, { key: "monthExpense", tone: "text-expense", amountMinor: null }, { key: "monthRemaining", tone: "text-budget", amountMinor: null }, - { key: "totalBalance", amountMinor: null }, - { key: "cardDebt", tone: "text-expense", amountMinor: null }, - { key: "netWorth", amountMinor: 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() { - const cards = await getSummary(); +async function SummaryCards({ userId, householdId }: { userId: string; householdId: string }) { + const cards = await getSummary(userId, householdId); return (
{cards.map((c) => ( @@ -39,7 +46,15 @@ async function SummaryCards() { {tr.dashboard.cards[c.key]} - {c.amountMinor === null ? "—" : formatMoney(c.amountMinor)} + {c.amountMinor === null ? ( + "—" + ) : c.href ? ( + + {formatMoney(c.amountMinor)} + + ) : ( + formatMoney(c.amountMinor) + )} {c.amountMinor === null ? ( @@ -68,7 +83,7 @@ export default async function DashboardPage() { }> - + ); diff --git a/src/components/layout/nav-config.ts b/src/components/layout/nav-config.ts index f4251e3..36571a8 100644 --- a/src/components/layout/nav-config.ts +++ b/src/components/layout/nav-config.ts @@ -27,7 +27,7 @@ export type NavItem = { /** Şu an tamamlanmış en yüksek faz — sayfalar eklendikçe artar. */ /** Alt fazlar ondalıkla: 6.1 = Faz 6a. */ -export const CURRENT_PHASE = 9.1; +export const CURRENT_PHASE = 9.2; export const isAvailable = (item: NavItem) => item.phase <= CURRENT_PHASE; diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx new file mode 100644 index 0000000..1bc077d --- /dev/null +++ b/src/components/ui/chart.tsx @@ -0,0 +1,338 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; +import * as RechartsPrimitive from "recharts"; +import type { TooltipValueType } from "recharts"; + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const; + +const INITIAL_DIMENSION = { width: 320, height: 200 } as const; +type TooltipNameType = number | string; + +export type ChartConfig = Record< + string, + { + label?: React.ReactNode; + icon?: React.ComponentType; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +>; + +type ChartContextProps = { + config: ChartConfig; +}; + +const ChartContext = React.createContext(null); + +function useChart() { + const context = React.useContext(ChartContext); + + if (!context) { + throw new Error("useChart must be used within a "); + } + + return context; +} + +function ChartContainer({ + id, + className, + children, + config, + initialDimension = INITIAL_DIMENSION, + ...props +}: React.ComponentProps<"div"> & { + config: ChartConfig; + children: React.ComponentProps["children"]; + initialDimension?: { + width: number; + height: number; + }; +}) { + const uniqueId = React.useId(); + const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}`; + + return ( + +
+ + + {children} + +
+
+ ); +} + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter(([, config]) => config.theme ?? config.color); + + if (!colorConfig.length) { + return null; + } + + return ( +