incr/resources/js/components/Transactions/AddEntryForm.tsx
myrmidex 6e76ce9c68
All checks were successful
CI / ci (push) Successful in 14m47s
CI / build (push) Successful in 27s
34 - Frontend: AddEntryForm, generalize unit labels, update LedDisplay/StatsBox/ProgressBar/InlineForm
2026-05-02 18:33:41 +02:00

181 lines
8.1 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 EntryFormData {
date: string;
quantity: string;
unit_price: string;
total_cost: string;
[key: string]: string;
}
interface AddEntryFormProps {
unit?: string;
priceTrackingEnabled?: boolean;
onSuccess?: () => void;
onCancel?: () => void;
}
interface EntrySummary {
total_quantity: number;
total_cost: number;
average_cost_per_unit: number;
}
export default function AddEntryForm({ unit = 'units', priceTrackingEnabled = false, onSuccess, onCancel }: AddEntryFormProps) {
const { data, setData, post, processing, errors, reset } = useForm<EntryFormData>({
date: new Date().toISOString().split('T')[0],
quantity: '',
unit_price: '',
total_cost: '',
});
const [currentHoldings, setCurrentHoldings] = useState<EntrySummary | null>(null);
useEffect(() => {
const fetchSummary = async () => {
try {
const response = await fetch('/entries/summary');
if (response.ok) {
const summary = await response.json();
setCurrentHoldings(summary);
}
} catch (error) {
console.error('Failed to fetch entry summary:', error);
}
};
fetchSummary();
}, []);
// Auto-calculate total cost when quantity or unit_price changes
useEffect(() => {
if (data.quantity && data.unit_price) {
const quantity = parseFloat(data.quantity);
const unitPrice = parseFloat(data.unit_price);
if (!isNaN(quantity) && !isNaN(unitPrice)) {
setData('total_cost', (quantity * unitPrice).toFixed(2));
}
}
}, [data.quantity, data.unit_price, setData]);
const submit: FormEventHandler = (e) => {
e.preventDefault();
post(route('entries.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 ENTRY</ComponentTitle>
{currentHoldings && currentHoldings.total_quantity > 0 && (
<p className="text-sm text-red-400/60 font-mono">
[CURRENT] {currentHoldings.total_quantity.toFixed(6)} {unit}
{priceTrackingEnabled && ` • €${currentHoldings.total_cost.toFixed(2)} spent`}
</p>
)}
<form onSubmit={submit} className="space-y-4">
<div>
<Label htmlFor="date" className="text-red-400 font-mono text-xs uppercase tracking-wider">&gt; 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="quantity" className="text-red-400 font-mono text-xs uppercase tracking-wider">
&gt; Quantity ({unit})
</Label>
<Input
id="quantity"
type="number"
step="0.000001"
min="0"
placeholder="1.234567"
value={data.quantity}
onChange={(e) => setData('quantity', 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.quantity} />
</div>
{priceTrackingEnabled && (
<>
<div>
<Label htmlFor="unit_price" className="text-red-400 font-mono text-xs uppercase tracking-wider">&gt; Price per {unit} ()</Label>
<Input
id="unit_price"
type="number"
step="0.01"
min="0"
placeholder="123.45"
value={data.unit_price}
onChange={(e) => setData('unit_price', 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.unit_price} />
</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] quantity × 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>
);
}