stock-portfolio_byQwen3.6/src/components/transactions/add-transaction-dialog.tsx

327 lines
11 KiB
TypeScript

'use client';
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,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Plus } from 'lucide-react';
import { toast } from 'sonner';
import { createTransaction } from '@/actions/transaction';
import { formatDateForDatetimeLocal, parseDateTimeLocalToUTC_v2 } from '@/libs/utils';
const addTransactionSchema = z.object({
assetId: z.string().uuid('请选择一个资产'),
txType: z.enum(['BUY', 'SELL', 'DIVIDEND', 'AIRDROP', 'FEE']),
quantity: z.string().regex(/^-?\d+(\.\d+)?$/, '数量必须是数字字符串'),
price: z.string().regex(/^-?\d+(\.\d+)?$/, '价格必须是数字字符串'),
fee: z.string().regex(/^-?\d+(\.\d+)?$/, '手续费必须是数字字符串'),
txCurrency: z.string().min(1, '交易币种不能为空'),
executedAt: z.date(),
});
type AddTransactionForm = z.infer<typeof addTransactionSchema>;
interface Asset {
id: string;
symbol: string;
name: string | null;
type: string;
baseCurrency: string;
exchange: string | null;
}
interface AddTransactionDialogProps {
assets: Asset[];
}
const txTypeLabels: Record<string, string> = {
BUY: '买入 (BUY)',
SELL: '卖出 (SELL)',
DIVIDEND: '分红 (DIVIDEND)',
AIRDROP: '空投 (AIRDROP)',
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();
const router = useRouter();
const form = useForm<AddTransactionForm>({
resolver: zodResolver(addTransactionSchema),
defaultValues: {
assetId: '',
txType: 'BUY' as const,
quantity: '',
price: '',
fee: '0',
txCurrency: 'USD',
executedAt: new Date(),
},
});
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({
assetId: values.assetId,
txType: values.txType,
quantity: String(values.quantity),
price: String(values.price),
fee: String(values.fee),
txCurrency: values.txCurrency,
executedAt: values.executedAt,
exchangeRate: '1',
});
if (result.success) {
toast.success('流水记录成功');
setOpen(false);
form.reset();
router.refresh();
}
});
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="assetId"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="选择资产" />
</SelectTrigger>
</FormControl>
<SelectContent>
{assets.length === 0 ? (
<SelectItem value="__none__"></SelectItem>
) : (
assets.map((asset) => (
<SelectItem key={asset.id} value={asset.id}>
{asset.symbol}{asset.name ? ` (${asset.name})` : ''}
</SelectItem>
))
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="txType"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="选择交易类型" />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.entries(txTypeLabels).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="quantity"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="0.001234567890123456" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="price"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="0.00" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="fee"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="0" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="txCurrency"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<Select disabled onValueChange={field.onChange} defaultValue={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={form.control}
name="executedAt"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
type="datetime-local"
{...field}
onChange={(e) => {
const value = e.target.value;
if (!value || value.trim() === '') {
field.onChange(null);
return;
}
const [datePart, timePart] = value.split('T');
if (!datePart) {
field.onChange(null);
return;
}
const [yearStr, monthStr, dayStr] = datePart.split('-');
const year = parseInt(yearStr, 10);
const month = parseInt(monthStr, 10) - 1;
const day = parseInt(dayStr, 10);
if (isNaN(year) || isNaN(month) || isNaN(day)) {
field.onChange(null);
return;
}
let hours = 0;
let minutes = 0;
if (timePart) {
const timeParts = timePart.split(':');
hours = parseInt(timeParts[0], 10) || 0;
minutes = parseInt(timeParts[1], 10) || 0;
}
const parsedDate = parseDateTimeLocalToUTC_v2(year, month, day, hours, minutes);
if (isNaN(parsedDate.getTime())) {
field.onChange(null);
return;
}
field.onChange(parsedDate);
}}
value={
field.value
? formatDateForDatetimeLocal(field.value)
: ''
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="submit" disabled={isPending}>
{isPending ? '提交中...' : '确认添加'}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}