dress/resources/js/pages/admin/finance/expense/partials/expense-form-modal.tsx

203 lines
8.0 KiB
TypeScript

import { useForm, router } from '@inertiajs/react';
import { ImagePlus, X, Save, Loader } from 'lucide-react';
import { useEffect, useState, useRef } from 'react';
import { NumericFormat } from 'react-number-format';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Field, FieldError, FieldGroup } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import expenseRoutes from '@/routes/expense';
import type { Expense } from '@/types';
interface ExpenseFormModalProps {
isOpen: boolean;
onClose: () => void;
expense: Expense | null;
}
export function ExpenseFormModal({ isOpen, onClose, expense }: ExpenseFormModalProps) {
const isEditing = !!expense;
const fileInputRef = useRef<HTMLInputElement>(null);
const [imagePreview, setImagePreview] = useState<string | null>(null);
const { data, setData, post, processing, errors, reset, clearErrors } = useForm<{
name: string;
amount: string;
image: File | null;
}>({
name: '',
amount: '',
image: null,
});
useEffect(() => {
if (expense) {
setData({
name: expense.name,
amount: expense.amount.toString(),
image: null,
});
setImagePreview(expense.proof_url || null);
} else {
reset();
setImagePreview(null);
}
}, [expense]);
const handleClose = () => {
onClose();
setTimeout(() => {
reset();
setImagePreview(null);
clearErrors();
}, 200);
};
const onImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setData('image', file);
const reader = new FileReader();
reader.onloadend = () => {
setImagePreview(reader.result as string);
};
reader.readAsDataURL(file);
}
};
const removeImage = () => {
setData('image', null);
setImagePreview(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isEditing && expense) {
router.post(expenseRoutes.update(expense.id).url, {
...data,
_method: 'PATCH',
}, {
onSuccess: (response: any) => {
toast.success(response.props.flash.success);
handleClose();
},
});
} else {
post(expenseRoutes.store().url, {
onSuccess: (response: any) => {
toast.success(response.props.flash.success);
handleClose();
},
});
}
};
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
<DialogContent>
<DialogHeader>
<DialogTitle>{isEditing ? 'Ubah Pengeluaran' : 'Tambah Pengeluaran'}</DialogTitle>
</DialogHeader>
<form onSubmit={onSubmit}>
<FieldGroup>
<Field>
<Label htmlFor="name" required>Nama</Label>
<Input
id="name"
name="name"
value={data.name}
onChange={e => setData('name', e.target.value)}
autoComplete='off'
placeholder='Contoh: Bayar Listrik'
maxLength={100}
/>
<FieldError error={errors.name} label="Nama" className="text-xs" />
</Field>
<Field>
<Label htmlFor="amount" required>Nominal</Label>
<NumericFormat
id="amount"
customInput={Input}
thousandSeparator="."
decimalSeparator=","
prefix="Rp "
value={data.amount}
onValueChange={(values) => {
setData('amount', values.value)
}}
placeholder="Rp 0"
autoComplete='off'
/>
<FieldError error={errors.amount} label="Nominal" className="text-xs" />
</Field>
<Field>
<Label>Bukti</Label>
<div className="mt-2">
{imagePreview ? (
<div className="relative inline-block">
<img
src={imagePreview}
alt="Preview"
className="object-cover rounded-lg border shadow-sm"
/>
<button
type="button"
onClick={removeImage}
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 shadow-md hover:bg-red-600 transition-colors"
>
<X className="h-4 w-4" />
</button>
</div>
) : (
<div
onClick={() => fileInputRef.current?.click()}
className="h-40 w-full flex flex-col items-center justify-center border-2 border-dashed rounded-lg cursor-pointer hover:bg-accent/50 transition-colors"
>
<ImagePlus className="h-8 w-8 text-muted-foreground mb-2" />
<span className="text-sm text-muted-foreground font-medium">Klik untuk upload bukti</span>
<span className="text-xs text-muted-foreground mt-1">PNG, JPG up to 5MB</span>
</div>
)}
<input
type="file"
ref={fileInputRef}
className="hidden"
accept="image/*"
onChange={onImageChange}
/>
</div>
<FieldError error={errors.image} label="Bukti" className="text-xs mt-1" />
</Field>
</FieldGroup>
<DialogFooter className="mt-6">
<Button type="button" variant="outline" className="gap-2" onClick={handleClose}>
<X className="size-4" />
Batal
</Button>
<Button type="submit" className="gap-2" disabled={processing}>
{processing ? <Loader className="size-4 animate-spin" /> : <Save className="size-4" />}
{processing ? 'Menyimpan...' : 'Simpan'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}