192 lines
8.8 KiB
TypeScript
192 lines
8.8 KiB
TypeScript
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog"
|
|
import { Field, FieldGroup } from "@/components/ui/field"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Switch } from '@/components/ui/switch';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { Payroll } from '@/types';
|
|
import { useForm } from '@inertiajs/react';
|
|
import { useEffect } from 'react';
|
|
import payrollRoutes from '@/routes/payroll';
|
|
import { toast } from 'sonner';
|
|
import { Plus, Trash2 } from 'lucide-react';
|
|
import { NumericFormat } from 'react-number-format';
|
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
|
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from '@/components/ui/empty';
|
|
|
|
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="flex items-center justify-between border-b pb-2 mb-4">
|
|
<h3 className="text-sm font-medium">Bonus & Potongan</h3>
|
|
<Button type="button" variant="outline" size="sm" onClick={addAdjustment}>
|
|
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="flex gap-3 items-start border-l-2 border-primary/20 pl-4 py-2 mb-4 group relative">
|
|
<div className="flex flex-col gap-3 flex-1">
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<Label className="text-[10px] uppercase mb-1">Jenis</Label>
|
|
<RadioGroup
|
|
value={adj.type}
|
|
onValueChange={(val) => updateAdjustment(index, 'type', val)}
|
|
className="flex items-center gap-6 w-fit"
|
|
>
|
|
<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="text-[10px] uppercase mb-1">Jumlah</Label>
|
|
<NumericFormat
|
|
id="amount"
|
|
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="text-[10px] uppercase mb-1">Keterangan</Label>
|
|
<Input
|
|
value={adj.description}
|
|
onChange={e =>
|
|
updateAdjustment(index, 'description', e.target.value)
|
|
}
|
|
placeholder="Contoh: Lembur"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="text-red-500 mt-6"
|
|
onClick={() => removeAdjustment(index)}
|
|
>
|
|
<Trash2 className="size-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</FieldGroup>
|
|
</div>
|
|
|
|
<DialogFooter className="mt-6">
|
|
<Button type="button" variant="outline" onClick={handleClose}>Batal</Button>
|
|
<Button type="submit" disabled={processing}>
|
|
{processing ? 'Menyimpan...' : 'Simpan'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|