TIFNJK_E41222887/resources/js/pages/admin/payrolls/create.tsx

388 lines
19 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 { Head, useForm, Link } from '@inertiajs/react';
import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import AppLayout from '@/layouts/app-layout';
import { PlusCircle, Trash2 } from 'lucide-react';
interface Position {
id: number;
name: string;
basic_salary: number;
}
interface Department {
name: string;
}
interface Employee {
id: number;
name: string;
nip: string;
position: Position;
department: Department;
}
interface DetailItem {
name: string;
type: 'bonus' | 'deduction';
amount: number;
}
interface PageProps {
employees: Employee[];
}
function formatRupiah(value: number): string {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(value);
}
export default function Create({ employees }: PageProps) {
const { data, setData, post, processing, errors } = useForm<{
employee_id: string;
period: string;
basic_salary: number;
details: DetailItem[];
}>({
employee_id: '',
period: '',
basic_salary: 0,
details: [],
});
const [selectedEmployee, setSelectedEmployee] = useState<Employee | null>(null);
// Kalkulasi net salary
const netSalary = data.details.reduce((acc, item) => {
return item.type === 'bonus'
? acc + (item.amount || 0)
: acc - (item.amount || 0);
}, data.basic_salary);
// Set basic_salary otomatis
const handleEmployeeChange = (value: string) => {
const emp = employees.find((e) => String(e.id) === value) ?? null;
setSelectedEmployee(emp);
setData((prev) => ({
...prev,
employee_id: value,
basic_salary: emp?.position?.basic_salary ?? 0,
}));
};
// Tambah baris
const addDetail = () => {
setData('details', [
...data.details,
{ name: '', type: 'bonus', amount: 0 },
]);
};
// Update baris
const updateDetail = (index: number, field: keyof DetailItem, value: string | number) => {
const updated = [...data.details];
// @ts-expect-error dynamic field assignment
updated[index][field] = field === 'amount' ? Number(value) : value;
setData('details', updated);
};
// Hapus baris
const removeDetail = (index: number) => {
setData('details', data.details.filter((_, i) => i !== index));
};
const submit = (e: React.FormEvent) => {
e.preventDefault();
post('/admin/payrolls');
};
return (
<AppLayout>
<Head title="Generate Payroll" />
<div className="mx-auto max-w-4xl p-4 md:p-8">
{/* Back button */}
<Button variant="outline" asChild className="mb-6">
<Link href="/admin/payrolls"> Kembali</Link>
</Button>
<Card>
<CardHeader>
<CardTitle>Form Generate Payroll</CardTitle>
<p className="text-sm text-muted-foreground">
Pilih karyawan dan periode, lalu tambahkan komponen bonus/potongan jika diperlukan.
</p>
</CardHeader>
<Separator />
<CardContent className="pt-6">
<form onSubmit={submit} className="space-y-8">
{/* ── SEKSI 1: Informasi Utama ── */}
<div className="space-y-4">
<h3 className="flex items-center gap-2 text-lg font-medium">
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/10 text-xs text-primary">
1
</span>
Informasi Utama
</h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{/* Pilih Karyawan */}
<div className="space-y-2">
<Label>Karyawan</Label>
<Select onValueChange={handleEmployeeChange}>
<SelectTrigger>
<SelectValue placeholder="Pilih Karyawan..." />
</SelectTrigger>
<SelectContent>
{employees.map((emp) => (
<SelectItem key={emp.id} value={String(emp.id)}>
{emp.name}{' '}
<span className="text-muted-foreground">
({emp.nip})
</span>
</SelectItem>
))}
</SelectContent>
</Select>
{errors.employee_id && (
<p className="text-xs text-red-500">{errors.employee_id}</p>
)}
</div>
{/* Periode */}
<div className="space-y-2">
<Label>Periode Gaji</Label>
<Input
type="month"
value={data.period}
onChange={(e) => setData('period', e.target.value)}
/>
{errors.period && (
<p className="text-xs text-red-500">{errors.period}</p>
)}
</div>
</div>
{/* Info Karyawan Terpilih */}
{selectedEmployee && (
<div className="rounded-lg border bg-muted/40 p-4 text-sm">
<div className="grid grid-cols-2 gap-2">
<div>
<span className="text-muted-foreground">Departemen:</span>{' '}
<span className="font-medium">
{selectedEmployee.department.name}
</span>
</div>
<div>
<span className="text-muted-foreground">Jabatan:</span>{' '}
<span className="font-medium">
{selectedEmployee.position.name}
</span>
</div>
</div>
</div>
)}
</div>
<Separator />
{/* ── SEKSI 2: Gaji Pokok (Auto-fill) ── */}
<div className="space-y-4">
<h3 className="flex items-center gap-2 text-lg font-medium">
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/10 text-xs text-primary">
2
</span>
Gaji Pokok
</h3>
<div className="max-w-xs space-y-2">
<Label>
Gaji Pokok{' '}
<span className="text-xs text-muted-foreground">
(otomatis dari jabatan)
</span>
</Label>
<Input
type="number"
min={0}
value={data.basic_salary}
onChange={(e) =>
setData('basic_salary', Number(e.target.value))
}
className="font-medium"
/>
{errors.basic_salary && (
<p className="text-xs text-red-500">{errors.basic_salary}</p>
)}
</div>
</div>
<Separator />
{/* ── SEKSI 3: Komponen Bonus / Potongan ── */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="flex items-center gap-2 text-lg font-medium">
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/10 text-xs text-primary">
3
</span>
Komponen Gaji
</h3>
<Button
type="button"
variant="outline"
size="sm"
onClick={addDetail}
className="gap-1.5"
>
<PlusCircle className="h-4 w-4" />
Tambah Komponen
</Button>
</div>
{data.details.length === 0 ? (
<p className="py-4 text-center text-sm text-muted-foreground">
Belum ada komponen tambahan. Klik &quot;Tambah Komponen&quot; untuk menambahkan bonus atau potongan.
</p>
) : (
<div className="space-y-3">
{/* Header kolom */}
<div className="grid grid-cols-[1fr_140px_160px_40px] gap-3 px-1">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Nama Komponen</span>
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Tipe</span>
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Nominal (Rp)</span>
<span />
</div>
{data.details.map((item, index) => (
<div key={index} className="space-y-1">
<div
className="grid grid-cols-[1fr_140px_160px_40px] items-center gap-3"
>
{/* Nama Komponen */}
<Input
placeholder="cth: Bonus Kinerja"
value={item.name}
onChange={(e) =>
updateDetail(index, 'name', e.target.value)
}
/>
{/* Tipe */}
<Select
value={item.type}
onValueChange={(val) =>
updateDetail(index, 'type', val)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="bonus">Bonus</SelectItem>
<SelectItem value="deduction">Potongan</SelectItem>
</SelectContent>
</Select>
{/* Nominal */}
<Input
type="number"
min={0}
placeholder="0"
value={item.amount || ''}
onChange={(e) =>
updateDetail(index, 'amount', e.target.value)
}
/>
{/* Hapus */}
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => removeDetail(index)}
className="text-muted-foreground hover:text-red-500"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
{/* Error per baris detail */}
<div className="px-1">
{errors[`details.${index}.name`] && (
<p className="text-xs text-red-500">{errors[`details.${index}.name`]}</p>
)}
{errors[`details.${index}.type`] && (
<p className="text-xs text-red-500">{errors[`details.${index}.type`]}</p>
)}
{errors[`details.${index}.amount`] && (
<p className="text-xs text-red-500">{errors[`details.${index}.amount`]}</p>
)}
</div>
</div>
))}
</div>
)}
</div>
<Separator />
{/* ── RINGKASAN GAJI BERSIH ── */}
<div className="rounded-lg border bg-primary/5 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Total Gaji Bersih (Estimasi)</p>
<p className="mt-0.5 text-xs text-muted-foreground">
Gaji Pokok + Bonus Potongan
</p>
</div>
<p className="text-2xl font-semibold text-primary">
{formatRupiah(Math.max(0, netSalary))}
</p>
</div>
</div>
{/* ── TOMBOL AKSI ── */}
<div className="flex justify-end gap-4 pt-2">
<Button type="button" variant="ghost" asChild>
<Link href="/admin/payrolls">Batal</Link>
</Button>
<Button
type="submit"
disabled={processing || !data.employee_id || !data.period}
className="px-8"
>
{processing ? 'Menyimpan...' : 'Generate Payroll'}
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
</AppLayout>
);
}