Spaces:
Sleeping
Sleeping
| import { useForm } from "react-hook-form"; | |
| import { zodResolver } from "@hookform/resolvers/zod"; | |
| import * as z from "zod"; | |
| import { Button } from "../../../components/ui/button"; | |
| import { Input } from "../../../components/ui/input"; | |
| import { AmountInput } from "../../../components/ui/AmountInput"; | |
| import { Select } from "../../../components/ui/select"; | |
| import { Label } from "../../../components/ui/label"; | |
| import { cn } from "../../../lib/utils"; | |
| import type { Wallet } from "../../../types"; | |
| import { useEffect } from "react"; | |
| export const transactionSchema = z.object({ | |
| type: z.enum(['expense', 'income', 'transfer']), | |
| amount: z.coerce.number().positive("Amount must be positive"), | |
| currency: z.string().min(1, "Currency is required"), | |
| wallet_id: z.coerce.number().min(1, "Wallet is required"), | |
| to_wallet_id: z.coerce.number().optional().nullable(), | |
| category: z.string().optional(), | |
| note: z.string().optional() | |
| }).refine(data => { | |
| if (data.type === 'transfer' && !data.to_wallet_id) return false; | |
| return true; | |
| }, { | |
| message: "Destination wallet is required", | |
| path: ["to_wallet_id"] | |
| }).refine(data => { | |
| if (data.type === 'transfer' && data.wallet_id === data.to_wallet_id) return false; | |
| return true; | |
| }, { | |
| message: "Cannot transfer to same wallet", | |
| path: ["to_wallet_id"] | |
| }); | |
| export type TransactionFormValues = z.infer<typeof transactionSchema>; | |
| const CATEGORIES = ['Food', 'Transport', 'Utilities', 'Shopping', 'Entertainment', 'Health', 'Salary', 'Investment', 'Other']; | |
| export function TransactionForm({ | |
| wallets, | |
| onSubmit, | |
| isSubmitting, | |
| submitError | |
| }: { | |
| wallets: Wallet[], | |
| onSubmit: (values: TransactionFormValues) => void, | |
| isSubmitting: boolean, | |
| submitError: string | |
| }) { | |
| const form = useForm<TransactionFormValues>({ | |
| resolver: zodResolver(transactionSchema), | |
| defaultValues: { | |
| type: 'expense', | |
| amount: '' as any, | |
| currency: wallets[0]?.currency || 'USD', | |
| wallet_id: wallets[0]?.id || 0, | |
| category: 'Other', | |
| note: '' | |
| } | |
| }); | |
| const type = form.watch('type'); | |
| const walletId = form.watch('wallet_id'); | |
| useEffect(() => { | |
| const w = wallets.find(w => w.id === Number(walletId)); | |
| if (w) { | |
| form.setValue('currency', w.currency); | |
| } | |
| }, [walletId, wallets, form]); | |
| return ( | |
| <div className="glass-panel p-6 mb-8 border border-blue-500/30 shadow-blue-500/5 relative overflow-hidden"> | |
| <div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 to-indigo-500" /> | |
| <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> | |
| <div className="flex flex-wrap gap-2 md:gap-4 mb-4 md:mb-6"> | |
| {['expense', 'income', 'transfer'].map(t => ( | |
| <button | |
| key={t} type="button" | |
| onClick={() => form.setValue('type', t as any)} | |
| className={cn( | |
| "flex-1 min-w-[30%] py-2 rounded-lg font-medium capitalize border transition-all text-sm md:text-base", | |
| type === t | |
| ? "bg-blue-500/20 border-blue-500/50 text-blue-400" | |
| : "bg-white/5 border-white/10 text-neutral-400 hover:text-neutral-200" | |
| )} | |
| > | |
| {t} | |
| </button> | |
| ))} | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <Label>Amount</Label> | |
| <AmountInput {...form.register('amount')} /> | |
| {form.formState.errors.amount && <p className="text-rose-400 text-xs mt-1">{form.formState.errors.amount.message}</p>} | |
| </div> | |
| <div> | |
| <Label>Currency</Label> | |
| <Select {...form.register('currency')}> | |
| <option value="USD">USD</option> | |
| <option value="IQD">IQD</option> | |
| <option value="RMB">RMB</option> | |
| </Select> | |
| </div> | |
| <div> | |
| <Label>Wallet</Label> | |
| <Select {...form.register('wallet_id')}> | |
| {wallets.map(w => <option key={w.id} value={w.id}>{w.name} ({w.currency})</option>)} | |
| </Select> | |
| </div> | |
| {type === 'transfer' && ( | |
| <div> | |
| <Label>To Wallet</Label> | |
| <Select {...form.register('to_wallet_id')}> | |
| <option value="">Select Destination</option> | |
| {wallets.filter(w => w.id.toString() !== walletId.toString()).map(w => <option key={w.id} value={w.id}>{w.name}</option>)} | |
| </Select> | |
| {form.formState.errors.to_wallet_id && <p className="text-rose-400 text-xs mt-1">{form.formState.errors.to_wallet_id.message}</p>} | |
| </div> | |
| )} | |
| {type !== 'transfer' && ( | |
| <div> | |
| <Label>Category</Label> | |
| <Select {...form.register('category')}> | |
| {CATEGORIES.map(cat => <option key={cat} value={cat}>{cat}</option>)} | |
| </Select> | |
| </div> | |
| )} | |
| <div className={cn("col-span-2", type === 'transfer' && "md:col-span-2")}> | |
| <Label>Note (Optional)</Label> | |
| <Input type="text" {...form.register('note')} placeholder="e.g. Paid university fee" /> | |
| </div> | |
| </div> | |
| {submitError && ( | |
| <div className="bg-rose-500/10 border border-rose-500/20 text-rose-400 p-3 rounded-xl text-sm mb-4"> | |
| {submitError} | |
| </div> | |
| )} | |
| <div className="flex justify-end pt-2 md:pt-4"> | |
| <Button type="submit" disabled={isSubmitting} className="w-full md:w-auto"> | |
| {isSubmitting ? ( | |
| <span className="flex items-center gap-2"> | |
| <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> Saving... | |
| </span> | |
| ) : ( | |
| <>Save {type}</> | |
| )} | |
| </Button> | |
| </div> | |
| </form> | |
| </div> | |
| ); | |
| } | |