feat(ui): 實裝資產流水下鑽明細表格及行內 CRUD 操作

This commit is contained in:
2026-04-29 00:21:16 +08:00
parent 1c6c36b147
commit 574d27968d
4 changed files with 394 additions and 60 deletions
+168 -60
View File
@@ -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>
);
}