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

281 lines
12 KiB
TypeScript

import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyTitle,
} from '@/components/ui/empty';
import { FieldGroup } from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import payrollRoutes from '@/routes/payroll';
import type { Payroll } from '@/types';
import { useForm } from '@inertiajs/react';
import { Loader, Plus, Save, Trash2, X } from 'lucide-react';
import { useEffect } from 'react';
import { NumericFormat } from 'react-number-format';
import { toast } from 'sonner';
interface PayrollFormModalProps {
isOpen: boolean;
onClose: () => void;
payroll: Payroll | null;
}
export function PayrollFormModal({
isOpen,
onClose,
payroll,
}: PayrollFormModalProps) {
const { data, setData, patch, processing, errors, reset, clearErrors } =
useForm({
adjustments: [] as {
type: 'bonus' | 'deduction';
amount: number;
description: string;
}[],
});
useEffect(() => {
if (payroll) {
setData({
adjustments:
payroll.adjustments?.map((adj) => ({
type: adj.type,
amount: adj.amount,
description: adj.description,
})) || [],
});
} else {
reset();
}
}, [payroll]);
const handleClose = () => {
onClose();
setTimeout(() => {
reset();
clearErrors();
}, 200);
};
const addAdjustment = () => {
setData('adjustments', [
...data.adjustments,
{ type: 'bonus', amount: 0, description: '' },
]);
};
const removeAdjustment = (index: number) => {
const newAdjustments = [...data.adjustments];
newAdjustments.splice(index, 1);
setData('adjustments', newAdjustments);
};
const updateAdjustment = (index: number, key: string, value: any) => {
const newAdjustments = [...data.adjustments];
newAdjustments[index] = { ...newAdjustments[index], [key]: value };
setData('adjustments', newAdjustments);
};
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (payroll) {
patch(payrollRoutes.update(payroll.id).url, {
onSuccess: (response: any) => {
toast.success(response.props.flash.success);
handleClose();
},
});
}
};
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>
Penyesuaian Penggajian - {payroll?.user?.name}
</DialogTitle>
</DialogHeader>
<form onSubmit={onSubmit}>
<div className="max-h-[60vh] overflow-y-auto px-1 py-2">
<FieldGroup>
<div className="mb-4 flex items-center justify-between border-b pb-2">
<h3 className="text-sm font-medium">
Bonus & Potongan
</h3>
<Button
type="button"
variant="outline"
size="sm"
onClick={addAdjustment}
className="gap-2"
>
<Plus className="size-4" />
Tambah
</Button>
</div>
{data.adjustments.length === 0 && (
<Empty>
<EmptyHeader>
<EmptyTitle>Ooops...</EmptyTitle>
<EmptyDescription>
Tidak ada data yang ditemukan.
</EmptyDescription>
</EmptyHeader>
</Empty>
)}
{data.adjustments.map((adj, index) => (
<div
key={index}
className="group relative mb-4 flex items-start gap-3 border-l-2 border-primary/20 py-2 pl-4"
>
<div className="flex flex-1 flex-col gap-3">
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="mb-1 text-[10px] uppercase">
Jenis
</Label>
<RadioGroup
value={adj.type}
onValueChange={(val) =>
updateAdjustment(
index,
'type',
val,
)
}
className="flex w-fit items-center gap-6"
>
<div className="flex items-center gap-2">
<RadioGroupItem
value="bonus"
id={`bonus-${index}`}
/>
<Label
htmlFor={`bonus-${index}`}
>
Bonus
</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem
value="deduction"
id={`deduction-${index}`}
/>
<Label
htmlFor={`deduction-${index}`}
>
Potongan
</Label>
</div>
</RadioGroup>
</div>
<div>
<Label
className="mb-1 text-[10px] uppercase"
htmlFor={`amount-${index}`}
required
>
Jumlah
</Label>
<NumericFormat
id={`amount-${index}`}
customInput={Input}
thousandSeparator="."
decimalSeparator=","
prefix="Rp "
value={adj.amount}
onValueChange={(values) => {
updateAdjustment(
index,
'amount',
values.value,
);
}}
placeholder="Rp 0"
autoComplete="off"
/>
</div>
</div>
<div>
<Label
className="mb-1 text-[10px] uppercase"
htmlFor={`description-${index}`}
required
>
Keterangan
</Label>
<Input
id={`description-${index}`}
value={adj.description}
onChange={(e) =>
updateAdjustment(
index,
'description',
e.target.value,
)
}
placeholder="Contoh: Lembur"
autoComplete="off"
/>
</div>
</div>
<Button
type="button"
variant="ghost"
size="icon"
className="mt-6 text-red-500"
onClick={() => removeAdjustment(index)}
>
<Trash2 className="size-4" />
</Button>
</div>
))}
</FieldGroup>
</div>
<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>
);
}