diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 1fa1f12..2b2ed77 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,14 +1,53 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { getPortfolioPositions } from '@/actions/portfolio'; +import { formatQuantity } from '@/lib/formatters'; + +export default async function DashboardPage() { + const positions = await getPortfolioPositions(); -export default function DashboardPage() { return ( - - - 欢迎来到 Omniledger - - +
+
+

欢迎来到 Omniledger

您的跨界记账中枢。

- - +
+ +
+ {positions.length === 0 ? ( + + +

暂无持仓,请先添加资产和交易记录。

+
+
+ ) : ( + positions.map((pos) => ( + + + + {pos.symbol} + + {pos.type} + + + + +
+
+ 持仓数量 + + {formatQuantity(pos.quantity, pos.type)} + +
+
+ 结算币种 + {pos.baseCurrency} +
+
+
+
+ )) + )} +
+
); -} \ No newline at end of file +} diff --git a/src/actions/portfolio.ts b/src/actions/portfolio.ts new file mode 100644 index 0000000..69ccae4 --- /dev/null +++ b/src/actions/portfolio.ts @@ -0,0 +1,68 @@ +'use server'; + +import { db } from '@/db'; +import { transactions, assets } from '@/db/schema'; +import Big from 'big.js'; +import { desc } from 'drizzle-orm'; + +export async function getPortfolioPositions() { + const allTransactions = await db + .select({ + txType: transactions.txType, + quantity: transactions.quantity, + assetId: transactions.assetId, + assetSymbol: assets.symbol, + assetType: assets.type, + assetBaseCurrency: assets.baseCurrency, + }) + .from(transactions) + .leftJoin(assets, assets.id.eq(transactions.assetId)) + .orderBy(desc(transactions.executedAt)); + + const holdings = new Map(); + + for (const tx of allTransactions) { + if (!tx.assetId) continue; + + const existing = holdings.get(tx.assetId); + if (!existing) { + holdings.set(tx.assetId, { + assetId: tx.assetId, + symbol: tx.assetSymbol || tx.assetId, + type: tx.assetType || 'CASH', + quantity: new Big('0'), + baseCurrency: tx.assetBaseCurrency || '', + }); + } + + const holding = holdings.get(tx.assetId)!; + + if (tx.txType === 'BUY' || tx.txType === 'AIRDROP') { + holding.quantity = holding.quantity.plus(tx.quantity); + } else if (tx.txType === 'SELL') { + holding.quantity = holding.quantity.minus(tx.quantity); + } else if (tx.txType === 'DIVIDEND') { + holding.quantity = holding.quantity.plus(tx.quantity); + } + } + + const result = []; + for (const [_, holding] of holdings) { + if (holding.quantity.lte(0)) continue; + result.push({ + assetId: holding.assetId, + symbol: holding.symbol, + type: holding.type, + quantity: holding.quantity.toString(), + baseCurrency: holding.baseCurrency, + }); + } + + return result; +}