150 lines
4.9 KiB
TypeScript
150 lines
4.9 KiB
TypeScript
import {useState, useEffect} from 'react';
|
|
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog';
|
|
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select';
|
|
import {Button} from '@/components/ui/button';
|
|
import {Input} from '@/components/ui/input';
|
|
import {Label} from '@/components/ui/label';
|
|
import {useAppDispatch, updateAsset, removeAsset, type Asset} from '@/store';
|
|
import {validatePositiveNumber, validateRequired} from '@/lib/validation';
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
asset: Asset | null;
|
|
}
|
|
|
|
const assetTypes = ['cash', 'investment', 'property', 'vehicle', 'other'] as const;
|
|
|
|
export default function EditAssetDialog({open, onOpenChange, asset}: Props) {
|
|
const dispatch = useAppDispatch();
|
|
const [form, setForm] = useState({name: '', type: '', value: ''});
|
|
const [errors, setErrors] = useState({name: '', value: ''});
|
|
|
|
useEffect(() => {
|
|
if (asset) {
|
|
setForm({
|
|
name: asset.name,
|
|
type: asset.type,
|
|
value: asset.value.toString()
|
|
});
|
|
setErrors({name: '', value: ''});
|
|
}
|
|
}, [asset]);
|
|
|
|
const validate = (): boolean => {
|
|
const newErrors = {name: '', value: ''};
|
|
let isValid = true;
|
|
|
|
if (!validateRequired(form.name)) {
|
|
newErrors.name = 'Name is required';
|
|
isValid = false;
|
|
}
|
|
|
|
const valueNum = validatePositiveNumber(form.value);
|
|
if (valueNum === null) {
|
|
newErrors.value = 'Please enter a valid positive number';
|
|
isValid = false;
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
return isValid;
|
|
};
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!asset || !validate()) return;
|
|
|
|
const valueNum = validatePositiveNumber(form.value);
|
|
if (valueNum === null) return;
|
|
|
|
dispatch(
|
|
updateAsset({
|
|
id: asset.id,
|
|
name: form.name.trim(),
|
|
type: form.type as (typeof assetTypes)[number],
|
|
value: valueNum,
|
|
updatedAt: new Date().toISOString()
|
|
})
|
|
);
|
|
onOpenChange(false);
|
|
};
|
|
|
|
const handleDelete = () => {
|
|
if (!asset) return;
|
|
if (confirm(`Are you sure you want to delete "${asset.name}"?`)) {
|
|
dispatch(removeAsset(asset.id));
|
|
onOpenChange(false);
|
|
}
|
|
};
|
|
|
|
if (!asset) return null;
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="card-elevated sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>Edit Asset</DialogTitle>
|
|
<DialogDescription>Update asset details or delete this asset</DialogDescription>
|
|
</DialogHeader>
|
|
<form onSubmit={handleSubmit}>
|
|
<div className="grid gap-4 py-4">
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="edit-name">Name</Label>
|
|
<Input
|
|
id="edit-name"
|
|
placeholder="e.g., Chase Savings"
|
|
value={form.name}
|
|
onChange={e => setForm({...form, name: e.target.value})}
|
|
className="input-depth"
|
|
required
|
|
/>
|
|
{errors.name && <p className="text-xs text-red-400">{errors.name}</p>}
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label>Type</Label>
|
|
<Select value={form.type} onValueChange={v => setForm({...form, type: v})} required>
|
|
<SelectTrigger className="input-depth">
|
|
<SelectValue placeholder="Select type" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{assetTypes.map(t => (
|
|
<SelectItem key={t} value={t} className="capitalize">
|
|
{t}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="edit-value">Value</Label>
|
|
<Input
|
|
id="edit-value"
|
|
type="number"
|
|
step="0.01"
|
|
min="0"
|
|
placeholder="0.00"
|
|
value={form.value}
|
|
onChange={e => setForm({...form, value: e.target.value})}
|
|
className="input-depth"
|
|
required
|
|
/>
|
|
{errors.value && <p className="text-xs text-red-400">{errors.value}</p>}
|
|
</div>
|
|
</div>
|
|
<DialogFooter className="flex justify-between sm:justify-between">
|
|
<Button type="button" variant="destructive" onClick={handleDelete} size="sm">
|
|
Delete
|
|
</Button>
|
|
<div className="flex gap-2">
|
|
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit">Save Changes</Button>
|
|
</div>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|