feat(ui): 實裝資產流水下鑽明細表格及行內 CRUD 操作
This commit is contained in:
+168
-60
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useTransition } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
Table,
|
||||
@@ -11,13 +11,24 @@ import {
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { getPortfolioSummary } from '@/actions/portfolio';
|
||||
import { getAssets } from '@/actions/asset';
|
||||
import { formatQuantity, formatAmount } from '@/lib/formatters';
|
||||
import AllocationChart from '@/components/dashboard/allocation-chart';
|
||||
import { SyncButton } from '@/components/assets/sync-button';
|
||||
import { AddTransactionDialog } from '@/components/transactions/add-transaction-dialog';
|
||||
import { ChevronDown, ChevronUp, Plus, Edit3 } from 'lucide-react';
|
||||
import { UpdateTransactionDialog } from '@/components/transactions/update-transaction-dialog';
|
||||
import { deleteTransaction } from '@/actions/transaction';
|
||||
import { ChevronDown, ChevronUp, Plus, Edit3, Trash2 } from 'lucide-react';
|
||||
import Big from 'big.js';
|
||||
|
||||
function getCurrencySymbol(currency: string): string {
|
||||
@@ -64,6 +75,9 @@ export default function DashboardPage() {
|
||||
const [expandedIds, setExpandedIds] = useState<Record<string, boolean>>({});
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [selectedAssetId, setSelectedAssetId] = useState<string>('');
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [updateTarget, setUpdateTarget] = useState<any>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadData() {
|
||||
@@ -88,6 +102,27 @@ export default function DashboardPage() {
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleUpdate = (tx: any) => {
|
||||
setUpdateTarget(tx);
|
||||
};
|
||||
|
||||
function handleUpdateSubmit() {
|
||||
setUpdateTarget(null);
|
||||
}
|
||||
|
||||
function handleDelete(tx: any) {
|
||||
startTransition(async () => {
|
||||
const result = await deleteTransaction(tx.id);
|
||||
if (result.success) {
|
||||
toast.success('交易記錄已刪除');
|
||||
setDeleteTarget(null);
|
||||
window.location.reload();
|
||||
} else if (result.error) {
|
||||
toast.error(result.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const formattedTotal = formatAmount(totalCnyValue);
|
||||
const formattedTotalPnl = formatAmount(totalPnlCny);
|
||||
const formattedUnrealized = formatAmount(unrealizedPnlCny);
|
||||
@@ -214,65 +249,108 @@ export default function DashboardPage() {
|
||||
</TableRow>
|
||||
{isExpanded && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="bg-muted/30 p-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-muted-foreground">交易記錄</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 px-2 text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOpenDialog(pos.assetId);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
新增記錄
|
||||
</Button>
|
||||
<TableCell colSpan={8} className="p-0">
|
||||
<div className="bg-muted/20 border-l-4 border-primary/20 p-3 ml-2">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between pb-2 border-b border-border/50">
|
||||
<span className="text-sm font-semibold text-foreground">流水明細</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 px-2 text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOpenDialog(pos.assetId);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
新增記錄
|
||||
</Button>
|
||||
</div>
|
||||
{pos.transactions && pos.transactions.length > 0 ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>交易日期</TableHead>
|
||||
<TableHead>類型</TableHead>
|
||||
<TableHead className="text-right">價格/數量</TableHead>
|
||||
<TableHead className="text-right">手續費</TableHead>
|
||||
<TableHead>備註</TableHead>
|
||||
<TableHead className="text-right w-[100px]">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{pos.transactions.map((tx: any) => {
|
||||
const txDate = tx.executedAt
|
||||
? new Date(tx.executedAt).toLocaleString('zh-TW', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
: '-';
|
||||
return (
|
||||
<TableRow key={tx.id} className="bg-muted/10">
|
||||
<TableCell className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
{txDate}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className={`text-xs font-medium px-1.5 py-0.5 rounded ${
|
||||
tx.txType === 'BUY' ? 'bg-red-100 text-red-700' :
|
||||
tx.txType === 'SELL' ? 'bg-green-100 text-green-700' :
|
||||
tx.txType === 'DIVIDEND' ? 'bg-blue-100 text-blue-700' :
|
||||
tx.txType === 'AIRDROP' ? 'bg-purple-100 text-purple-700' :
|
||||
'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
{tx.txType}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-xs">
|
||||
<div className="flex flex-col items-end">
|
||||
<span>量: {tx.quantity}</span>
|
||||
<span>價: {tx.price}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-xs">{tx.fee || '0'}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground">-</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 px-1.5 text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleUpdate(tx);
|
||||
}}
|
||||
>
|
||||
<Edit3 className="h-3 w-3 mr-0.5" />
|
||||
修改
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 px-1.5 text-xs text-red-500 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDeleteTarget(tx);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3 w-3 mr-0.5" />
|
||||
刪除
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground text-center py-3">暫無交易記錄</p>
|
||||
)}
|
||||
</div>
|
||||
{pos.transactions && pos.transactions.length > 0 ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>類型</TableHead>
|
||||
<TableHead className="text-right">數量</TableHead>
|
||||
<TableHead className="text-right">價格</TableHead>
|
||||
<TableHead className="text-right">手續費</TableHead>
|
||||
<TableHead className="text-right">幣種</TableHead>
|
||||
<TableHead className="text-right">日期</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{pos.transactions.map((tx: any, idx: number) => {
|
||||
const txDate = tx.executedAt
|
||||
? new Date(tx.executedAt).toLocaleString('zh-TW')
|
||||
: '-';
|
||||
return (
|
||||
<TableRow key={idx}>
|
||||
<TableCell>
|
||||
<span className={`text-xs font-medium px-1.5 py-0.5 rounded ${
|
||||
tx.txType === 'BUY' ? 'bg-red-100 text-red-700' :
|
||||
tx.txType === 'SELL' ? 'bg-green-100 text-green-700' :
|
||||
tx.txType === 'DIVIDEND' ? 'bg-blue-100 text-blue-700' :
|
||||
'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
{tx.txType}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{tx.quantity}</TableCell>
|
||||
<TableCell className="text-right">{tx.price}</TableCell>
|
||||
<TableCell className="text-right">{tx.fee || '0'}</TableCell>
|
||||
<TableCell className="text-right">{tx.txCurrency}</TableCell>
|
||||
<TableCell className="text-right text-xs text-muted-foreground">{txDate}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">暫無交易記錄</p>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -301,6 +379,36 @@ export default function DashboardPage() {
|
||||
onOpenChange={setDialogOpen}
|
||||
defaultAssetId={selectedAssetId}
|
||||
/>
|
||||
|
||||
<UpdateTransactionDialog
|
||||
open={!!updateTarget}
|
||||
onOpenChange={(open) => !open && setUpdateTarget(null)}
|
||||
transaction={updateTarget}
|
||||
onSuccess={handleUpdateSubmit}
|
||||
/>
|
||||
|
||||
<Dialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
||||
<DialogContent className="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>確認刪除</DialogTitle>
|
||||
<DialogDescription>
|
||||
確定要刪除這筆交易記錄嗎?此操作不可撤銷。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<Button variant="outline" onClick={() => setDeleteTarget(null)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => deleteTarget && handleDelete(deleteTarget)}
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? '刪除中...' : '刪除'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user