feat(ui): 接入股票名称展示,实装表单币种自动绑定及编辑删除功能

This commit is contained in:
kennethcheng 2026-04-28 12:51:20 +08:00
parent 7cd084d4b3
commit ea57b4629a
9 changed files with 655 additions and 155 deletions

View File

@ -0,0 +1,260 @@
'use client';
import { useState, useTransition } from 'react';
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Edit, Trash2 } from 'lucide-react';
import Big from 'big.js';
import { updateAsset, deleteAsset } from '@/actions/asset';
import { AddAssetDialog } from '@/components/assets/add-asset-dialog';
import { SyncButton } from '@/components/assets/sync-button';
interface Asset {
id: string;
symbol: string;
name: string | null;
type: string;
exchange: string | null;
baseCurrency: string;
latestPrice: string;
createdAt: Date | null;
}
export default function AssetsPageClient({ assets }: { assets: Asset[] }) {
const [isPending, startTransition] = useTransition();
const [deleteTarget, setDeleteTarget] = useState<Asset | null>(null);
const [editTarget, setEditTarget] = useState<Asset | null>(null);
const typeLabels: Record<string, string> = {
STOCK: '股票',
CRYPTO: '加密貨幣',
CASH: '現金',
};
const editForm = useForm<{ symbol: string; name: string; baseCurrency: string }>({
resolver: zodResolver(z.object({
symbol: z.string().min(1, '资产代码不能为空'),
name: z.string(),
baseCurrency: z.string().min(2, '基础币种至少2个字符'),
})),
defaultValues: { symbol: '', name: '', baseCurrency: '' },
});
function handleEditClick(asset: Asset) {
setEditTarget(asset);
editForm.reset({ symbol: asset.symbol, name: asset.name || '', baseCurrency: asset.baseCurrency });
}
function handleEditSubmit(values: { symbol: string; name: string; baseCurrency: string }) {
if (!editTarget) return;
startTransition(async () => {
const result = await updateAsset({
id: editTarget.id,
symbol: values.symbol,
baseCurrency: values.baseCurrency,
});
if (result.success) {
setEditTarget(null);
editForm.reset();
window.location.reload();
}
});
}
function handleDelete(asset: Asset) {
startTransition(async () => {
await deleteAsset(asset.id);
setDeleteTarget(null);
window.location.reload();
});
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold"></h1>
<div className="flex gap-2">
<SyncButton />
<AddAssetDialog />
</div>
</div>
<div className="rounded-md border">
<Table>
<TableCaption></TableCaption>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead> (Latest Price)</TableHead>
<TableHead></TableHead>
<TableHead className="w-[100px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{assets.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center text-muted-foreground">
"添加资产"
</TableCell>
</TableRow>
) : (
assets.map((asset) => (
<TableRow key={asset.id}>
<TableCell className="font-medium">{asset.name || '-'}</TableCell>
<TableCell>{asset.symbol}</TableCell>
<TableCell>{typeLabels[asset.type] || asset.type}</TableCell>
<TableCell>{asset.baseCurrency}</TableCell>
<TableCell>{asset.latestPrice ? new Big(asset.latestPrice).toString() : '-'}</TableCell>
<TableCell>
{asset.createdAt
? new Date(asset.createdAt).toLocaleString('zh-CN')
: '-'}
</TableCell>
<TableCell>
<div className="flex gap-1">
<Button
variant="ghost"
size="icon"
onClick={() => handleEditClick(asset)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => setDeleteTarget(asset)}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
<Dialog open={!!editTarget} onOpenChange={(open) => !open && setEditTarget(null)}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<Form {...editForm}>
<form onSubmit={editForm.handleSubmit(handleEditSubmit)} className="space-y-4">
<FormField
control={editForm.control}
name="symbol"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="可选" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="baseCurrency"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="submit" disabled={isPending}>
{isPending ? '保存中...' : '保存'}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
<Dialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
<DialogContent className="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
<strong>{deleteTarget?.symbol}</strong>
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={() => setDeleteTarget(null)}
>
</Button>
<Button
variant="destructive"
onClick={() => deleteTarget && handleDelete(deleteTarget)}
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -1,73 +1,7 @@
import { getAssets } from '@/actions/asset';
import { AddAssetDialog } from '@/components/assets/add-asset-dialog';
import { SyncButton } from '@/components/assets/sync-button';
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import Big from 'big.js';
import AssetsPageClient from './assets-client';
export default async function AssetsPage() {
const assets = await getAssets();
const typeLabels: Record<string, string> = {
STOCK: '股票',
CRYPTO: '加密貨幣',
CASH: '現金',
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold"></h1>
<div className="flex gap-2">
<SyncButton />
<AddAssetDialog />
</div>
</div>
<div className="rounded-md border">
<Table>
<TableCaption></TableCaption>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead> (Latest Price)</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{assets.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground">
"添加資產"
</TableCell>
</TableRow>
) : (
assets.map((asset) => (
<TableRow key={asset.id}>
<TableCell className="font-medium">{asset.symbol}</TableCell>
<TableCell>{typeLabels[asset.type] || asset.type}</TableCell>
<TableCell>{asset.baseCurrency}</TableCell>
<TableCell>{asset.latestPrice ? new Big(asset.latestPrice).toString() : '-'}</TableCell>
<TableCell>
{asset.createdAt
? new Date(asset.createdAt).toLocaleString('zh-CN')
: '-'}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
);
return <AssetsPageClient assets={assets} />;
}

View File

@ -71,7 +71,10 @@ export default async function DashboardPage() {
<Card key={pos.assetId}>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>{pos.symbol}</span>
<div className="flex flex-col">
<span>{pos.name || pos.symbol}</span>
<span className="text-sm font-normal text-muted-foreground">{pos.symbol}</span>
</div>
<span className="text-sm font-normal text-muted-foreground">
{pos.type}
</span>

View File

@ -1,16 +1,6 @@
import { getAssets } from '@/actions/asset';
import { getTransactions } from '@/actions/transaction';
import { AddTransactionDialog } from '@/components/transactions/add-transaction-dialog';
import { formatAmount, formatQuantity } from '@/lib/formatters';
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import TransactionsPageClient from './transactions-client';
export default async function TransactionsPage() {
const [assets, transactions] = await Promise.all([
@ -18,74 +8,5 @@ export default async function TransactionsPage() {
getTransactions(),
]);
const typeLabels: Record<string, string> = {
BUY: '买入',
SELL: '卖出',
DIVIDEND: '分红',
AIRDROP: '空投',
FEE: '手续费',
};
const assetMap = new Map(assets.map((a) => [a.id, { symbol: a.symbol, type: a.type }]));
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold"></h1>
<AddTransactionDialog assets={assets} />
</div>
<div className="rounded-md border">
<Table>
<TableCaption>
{transactions.length}
</TableCaption>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{transactions.length === 0 ? (
<TableRow>
<TableCell
colSpan={7}
className="text-center text-muted-foreground"
>
"添加流水"
</TableCell>
</TableRow>
) : (
transactions.map((tx) => {
const assetInfo = assetMap.get(tx.assetId);
const symbol = assetInfo?.symbol || tx.assetId;
const assetType = assetInfo?.type || 'CASH';
return (
<TableRow key={tx.id}>
<TableCell className="font-medium">{symbol}</TableCell>
<TableCell>{typeLabels[tx.txType] || tx.txType}</TableCell>
<TableCell>{formatQuantity(tx.quantity, assetType)}</TableCell>
<TableCell>{formatAmount(tx.price)}</TableCell>
<TableCell>{formatAmount(tx.fee)}</TableCell>
<TableCell>{tx.txCurrency}</TableCell>
<TableCell>
{tx.executedAt
? new Date(tx.executedAt).toLocaleString('zh-CN')
: '-'}
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</div>
</div>
);
return <TransactionsPageClient assets={assets} transactions={transactions} />;
}

View File

@ -0,0 +1,338 @@
'use client';
import { useState, useTransition } from 'react';
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Edit, Trash2 } from 'lucide-react';
import { formatAmount, formatQuantity } from '@/lib/formatters';
import { deleteTransaction, updateTransaction } from '@/actions/transaction';
import { AddTransactionDialog } from '@/components/transactions/add-transaction-dialog';
interface Asset {
id: string;
symbol: string;
name: string | null;
type: string;
baseCurrency: string;
}
interface Transaction {
id: string;
assetId: string;
txType: string;
quantity: string;
price: string;
fee: string;
txCurrency: string;
executedAt: Date;
}
export default function TransactionsPageClient({
assets,
transactions,
}: {
assets: Asset[];
transactions: Transaction[];
}) {
const [isPending, startTransition] = useTransition();
const [deleteTarget, setDeleteTarget] = useState<Transaction | null>(null);
const [editTarget, setEditTarget] = useState<Transaction | null>(null);
const typeLabels: Record<string, string> = {
BUY: '买入',
SELL: '卖出',
DIVIDEND: '分红',
AIRDROP: '空投',
FEE: '手续费',
};
const assetMap = new Map(assets.map((a) => [a.id, { symbol: a.symbol, name: a.name, type: a.type }]));
const editForm = useForm<{ quantity: string; price: string; fee: string; txCurrency: string; executedAt: string }>({
resolver: zodResolver(z.object({
quantity: z.string().regex(/^-?\d+(\.\d+)?$/, '数量必须是数字'),
price: z.string().regex(/^-?\d+(\.\d+)?$/, '价格必须是数字'),
fee: z.string().regex(/^-?\d+(\.\d+)?$/, '手续费必须是数字').default('0'),
txCurrency: z.string().min(1, '交易币种不能为空'),
executedAt: z.string(),
})),
defaultValues: { quantity: '', price: '', fee: '0', txCurrency: 'USD', executedAt: '' },
});
function handleEditClick(tx: Transaction) {
setEditTarget(tx);
editForm.reset({
quantity: tx.quantity.toString(),
price: tx.price.toString(),
fee: tx.fee.toString(),
txCurrency: tx.txCurrency,
executedAt: tx.executedAt
? new Date(tx.executedAt).toISOString().slice(0, 16)
: '',
});
}
function handleEditSubmit(values: { quantity: string; price: string; fee: string; txCurrency: string; executedAt: string }) {
if (!editTarget) return;
startTransition(async () => {
const result = await updateTransaction({
id: editTarget.id,
quantity: values.quantity,
price: values.price,
fee: values.fee,
txCurrency: values.txCurrency,
executedAt: new Date(values.executedAt),
});
if (result.success) {
setEditTarget(null);
editForm.reset();
window.location.reload();
}
});
}
function handleDelete(tx: Transaction) {
startTransition(async () => {
await deleteTransaction(tx.id);
setDeleteTarget(null);
window.location.reload();
});
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold"></h1>
<AddTransactionDialog assets={assets} />
</div>
<div className="rounded-md border">
<Table>
<TableCaption>
{transactions.length}
</TableCaption>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="w-[100px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{transactions.length === 0 ? (
<TableRow>
<TableCell
colSpan={9}
className="text-center text-muted-foreground"
>
"添加流水"
</TableCell>
</TableRow>
) : (
transactions.map((tx) => {
const assetInfo = assetMap.get(tx.assetId);
const symbol = assetInfo?.symbol || tx.assetId;
const name = assetInfo?.name || null;
const assetType = assetInfo?.type || 'CASH';
return (
<TableRow key={tx.id}>
<TableCell className="font-medium">{name || '-'}</TableCell>
<TableCell>{symbol}</TableCell>
<TableCell>{typeLabels[tx.txType] || tx.txType}</TableCell>
<TableCell>{formatQuantity(tx.quantity, assetType)}</TableCell>
<TableCell>{formatAmount(tx.price)}</TableCell>
<TableCell>{formatAmount(tx.fee)}</TableCell>
<TableCell>{tx.txCurrency}</TableCell>
<TableCell>
{tx.executedAt
? new Date(tx.executedAt).toLocaleString('zh-CN')
: '-'}
</TableCell>
<TableCell>
<div className="flex gap-1">
<Button
variant="ghost"
size="icon"
onClick={() => handleEditClick(tx)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => setDeleteTarget(tx)}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</div>
<Dialog open={!!editTarget} onOpenChange={(open) => !open && setEditTarget(null)}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<Form {...editForm}>
<form onSubmit={editForm.handleSubmit(handleEditSubmit)} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<FormField
control={editForm.control}
name="quantity"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="price"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={editForm.control}
name="fee"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="txCurrency"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="选择币种" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="USD">USD</SelectItem>
<SelectItem value="CNY">CNY</SelectItem>
<SelectItem value="HKD">HKD</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={editForm.control}
name="executedAt"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input type="datetime-local" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="submit" disabled={isPending}>
{isPending ? '保存中...' : '保存'}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
<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)}
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -37,8 +37,9 @@ export async function syncAllStockPrices() {
const latestPrice = dataArr[3];
if (latestPrice && !isNaN(Number(latestPrice)) && Number(latestPrice) > 0) {
const stockName = dataArr[1] || null;
await db.update(assets)
.set({ latestPrice: latestPrice })
.set({ latestPrice: latestPrice, name: stockName })
.where(eq(assets.id, asset.id));
successCount++;
}

View File

@ -8,6 +8,7 @@ import { desc, eq } from 'drizzle-orm';
interface Position {
assetId: string;
symbol: string;
name: string | null;
type: string;
quantity: string;
baseCurrency: string;
@ -82,6 +83,7 @@ export async function getPortfolioPositions(): Promise<Position[]> {
txCurrency: transactions.txCurrency,
assetId: transactions.assetId,
assetSymbol: assets.symbol,
assetName: assets.name,
assetType: assets.type,
assetBaseCurrency: assets.baseCurrency,
assetLatestPrice: assets.latestPrice,
@ -101,6 +103,7 @@ export async function getPortfolioPositions(): Promise<Position[]> {
const holdings = new Map<string, {
assetId: string;
symbol: string;
name: string | null;
type: string;
quantity: Big;
baseCurrency: string;
@ -117,6 +120,7 @@ export async function getPortfolioPositions(): Promise<Position[]> {
holdings.set(tx.assetId, {
assetId: tx.assetId,
symbol: tx.assetSymbol || tx.assetId,
name: tx.assetName,
type: tx.assetType || 'CASH',
quantity: new Big('0'),
baseCurrency: tx.assetBaseCurrency || '',
@ -190,6 +194,7 @@ export async function getPortfolioPositions(): Promise<Position[]> {
result.push({
assetId: holding.assetId,
symbol: holding.symbol,
name: holding.name,
type: holding.type,
quantity: holding.quantity.toString(),
baseCurrency: holding.baseCurrency,

View File

@ -1,10 +1,11 @@
'use client';
import { useState, useTransition } from 'react';
import { useState, useTransition, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useRouter } from 'next/navigation';
import { useWatch } from 'react-hook-form';
import {
Dialog,
DialogContent,
@ -61,10 +62,26 @@ export function AddAssetDialog() {
symbol: '',
type: 'STOCK' as const,
exchange: 'US',
baseCurrency: '',
baseCurrency: 'USD',
},
});
const exchangeValue = useWatch({ control: form.control, name: 'exchange' });
useEffect(() => {
if (!exchangeValue) return;
const currencyMap: Record<string, string> = {
'US': 'USD',
'HKEX': 'HKD',
'SSE': 'CNY',
'SZSE': 'CNY',
};
const currency = currencyMap[exchangeValue];
if (currency) {
form.setValue('baseCurrency', currency, { shouldValidate: true });
}
}, [exchangeValue, form]);
function onSubmit(values: AddAssetForm) {
startTransition(async () => {
const result = await createAsset(values);

View File

@ -1,10 +1,11 @@
'use client';
import { useState, useTransition } from 'react';
import { useState, useTransition, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useRouter } from 'next/navigation';
import { useWatch } from 'react-hook-form';
import {
Dialog,
DialogContent,
@ -49,8 +50,10 @@ type AddTransactionForm = z.infer<typeof addTransactionSchema>;
interface Asset {
id: string;
symbol: string;
name: string | null;
type: string;
baseCurrency: string;
exchange: string | null;
}
interface AddTransactionDialogProps {
@ -65,6 +68,13 @@ const txTypeLabels: Record<string, string> = {
FEE: '手续费 (FEE)',
};
const exchangeToCurrencyMap: Record<string, string> = {
'US': 'USD',
'HKEX': 'HKD',
'SSE': 'CNY',
'SZSE': 'CNY',
};
export function AddTransactionDialog({ assets }: AddTransactionDialogProps) {
const [open, setOpen] = useState(false);
const [isPending, startTransition] = useTransition();
@ -83,6 +93,17 @@ export function AddTransactionDialog({ assets }: AddTransactionDialogProps) {
},
});
const selectedAssetId = useWatch({ control: form.control, name: 'assetId' });
useEffect(() => {
if (!selectedAssetId) return;
const selectedAsset = assets.find((a) => a.id === selectedAssetId);
if (selectedAsset) {
const currency = exchangeToCurrencyMap[selectedAsset.exchange || ''] || selectedAsset.baseCurrency || 'USD';
form.setValue('txCurrency', currency, { shouldValidate: true });
}
}, [selectedAssetId, assets, form]);
function onSubmit(values: AddTransactionForm) {
startTransition(async () => {
const result = await createTransaction({
@ -136,7 +157,7 @@ export function AddTransactionDialog({ assets }: AddTransactionDialogProps) {
) : (
assets.map((asset) => (
<SelectItem key={asset.id} value={asset.id}>
{asset.symbol}
{asset.symbol}{asset.name ? ` (${asset.name})` : ''}
</SelectItem>
))
)}