incr/resources/js/components/Transactions/AddPurchaseForm.tsx
2025-08-01 00:56:26 +02:00

178 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import InputError from '@/components/InputError';
import { useForm } from '@inertiajs/react';
import { LoaderCircle } from 'lucide-react';
import { FormEventHandler, useEffect, useState } from 'react';
import ComponentTitle from '@/components/ui/ComponentTitle';
interface PurchaseFormData {
date: string;
shares: string;
price_per_share: string;
total_cost: string;
[key: string]: string;
}
interface AddPurchaseFormProps {
onSuccess?: () => void;
onCancel?: () => void;
}
interface PurchaseSummary {
total_shares: number;
total_investment: number;
average_cost_per_share: number;
}
export default function AddPurchaseForm({ onSuccess, onCancel }: AddPurchaseFormProps) {
const { data, setData, post, processing, errors, reset } = useForm<PurchaseFormData>({
date: new Date().toISOString().split('T')[0], // Today's date in YYYY-MM-DD format
shares: '',
price_per_share: '',
total_cost: '',
});
const [currentHoldings, setCurrentHoldings] = useState<PurchaseSummary | null>(null);
// Load existing holdings data on mount
useEffect(() => {
const fetchCurrentHoldings = async () => {
try {
const response = await fetch('/purchases/summary');
if (response.ok) {
const summary = await response.json();
setCurrentHoldings(summary);
}
} catch (error) {
console.error('Failed to fetch current holdings:', error);
}
};
fetchCurrentHoldings();
}, []);
// Auto-calculate total cost when shares or price changes
useEffect(() => {
if (data.shares && data.price_per_share) {
const shares = parseFloat(data.shares);
const pricePerShare = parseFloat(data.price_per_share);
if (!isNaN(shares) && !isNaN(pricePerShare)) {
const totalCost = (shares * pricePerShare).toFixed(2);
setData('total_cost', totalCost);
}
}
}, [data.shares, data.price_per_share, setData]);
const submit: FormEventHandler = (e) => {
e.preventDefault();
post(route('purchases.store'), {
onSuccess: () => {
reset();
setData('date', new Date().toISOString().split('T')[0]);
if (onSuccess) {
onSuccess();
}
},
});
};
return (
<div className="w-full">
<div className="space-y-4">
<ComponentTitle>ADD PURCHASE</ComponentTitle>
{currentHoldings && currentHoldings.total_shares > 0 && (
<p className="text-sm text-red-400/60 font-mono">
[CURRENT] {currentHoldings.total_shares.toFixed(6)} shares {currentHoldings.total_investment.toFixed(2)} invested
</p>
)}
<form onSubmit={submit} className="space-y-4">
<div>
<Label htmlFor="date" className="text-red-400 font-mono text-xs uppercase tracking-wider">&gt; Purchase Date</Label>
<Input
id="date"
type="date"
value={data.date}
onChange={(e) => setData('date', e.target.value)}
max={new Date().toISOString().split('T')[0]}
className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none transition-all glow-red"
/>
<InputError message={errors.date} />
</div>
<div>
<Label htmlFor="shares" className="text-red-400 font-mono text-xs uppercase tracking-wider">&gt; Number of Shares</Label>
<Input
id="shares"
type="number"
step="0.000001"
min="0"
placeholder="1.234567"
value={data.shares}
onChange={(e) => setData('shares', e.target.value)}
className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none placeholder:text-red-400/40 transition-all glow-red"
/>
<InputError message={errors.shares} />
</div>
<div>
<Label htmlFor="price_per_share" className="text-red-400 font-mono text-xs uppercase tracking-wider">&gt; Price per Share ()</Label>
<Input
id="price_per_share"
type="number"
step="0.01"
min="0"
placeholder="123.45"
value={data.price_per_share}
onChange={(e) => setData('price_per_share', e.target.value)}
className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none placeholder:text-red-400/40 transition-all glow-red"
/>
<InputError message={errors.price_per_share} />
</div>
<div>
<Label htmlFor="total_cost" className="text-red-400 font-mono text-xs uppercase tracking-wider">&gt; Total Cost ()</Label>
<Input
id="total_cost"
type="number"
step="0.01"
min="0"
placeholder="1234.56"
value={data.total_cost}
onChange={(e) => setData('total_cost', e.target.value)}
className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none placeholder:text-red-400/40 transition-all glow-red"
/>
<p className="text-xs text-red-400/60 mt-1 font-mono">
[AUTO-CALC] shares × price
</p>
<InputError message={errors.total_cost} />
</div>
<div className="flex gap-3 pt-2">
<Button
type="submit"
disabled={processing}
className="flex-1 bg-red-500 hover:bg-red-500 text-black font-mono text-sm font-bold border-red-500 rounded-none border-2 uppercase tracking-wider transition-all glow-red"
>
{processing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
[EXECUTE]
</Button>
{onCancel && (
<Button
type="button"
variant="outline"
onClick={onCancel}
className="flex-1 bg-black border-red-500 text-red-400 hover:bg-red-950 hover:text-red-300 font-mono text-sm font-bold rounded-none border-2 uppercase tracking-wider transition-all glow-red"
>
[ABORT]
</Button>
)}
</div>
</form>
</div>
</div>
);
}