274 lines
13 KiB
TypeScript
274 lines
13 KiB
TypeScript
import { Head, useForm, Link } from '@inertiajs/react';
|
||
import React 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 DetailItem {
|
||
name: string;
|
||
type: 'bonus' | 'deduction';
|
||
amount: number;
|
||
}
|
||
|
||
interface PayrollData {
|
||
id: number;
|
||
period: string;
|
||
basic_salary: number;
|
||
details: DetailItem[];
|
||
net_salary: number;
|
||
status: 'pending' | 'paid';
|
||
employee: {
|
||
id: number;
|
||
name: string;
|
||
nip: string;
|
||
position: string;
|
||
department: string;
|
||
};
|
||
}
|
||
|
||
interface PageProps {
|
||
payroll: PayrollData;
|
||
}
|
||
|
||
|
||
|
||
function formatRupiah(value: number): string {
|
||
return new Intl.NumberFormat('id-ID', {
|
||
style: 'currency',
|
||
currency: 'IDR',
|
||
minimumFractionDigits: 0,
|
||
}).format(value);
|
||
}
|
||
|
||
function formatPeriod(period: string): string {
|
||
const [year, month] = period.split('-');
|
||
return new Date(Number(year), Number(month) - 1, 1)
|
||
.toLocaleDateString('id-ID', { month: 'long', year: 'numeric' });
|
||
}
|
||
|
||
|
||
|
||
export default function Edit({ payroll }: PageProps) {
|
||
const { data, setData, put, processing, errors } = useForm<{
|
||
basic_salary: number;
|
||
details: DetailItem[];
|
||
}>({
|
||
basic_salary: payroll.basic_salary,
|
||
details: payroll.details,
|
||
});
|
||
|
||
// Hitung net salary
|
||
const netSalary = data.details.reduce((acc, item) => {
|
||
return item.type === 'bonus'
|
||
? acc + (item.amount || 0)
|
||
: acc - (item.amount || 0);
|
||
}, data.basic_salary);
|
||
|
||
const addDetail = () => {
|
||
setData('details', [...data.details, { name: '', type: 'bonus', amount: 0 }]);
|
||
};
|
||
|
||
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);
|
||
};
|
||
|
||
const removeDetail = (index: number) => {
|
||
setData('details', data.details.filter((_, i) => i !== index));
|
||
};
|
||
|
||
const submit = (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
put(`/admin/payrolls/${payroll.id}`);
|
||
};
|
||
|
||
return (
|
||
<AppLayout>
|
||
<Head title={`Edit Payroll — ${payroll.employee.name}`} />
|
||
|
||
<div className="mx-auto max-w-4xl p-4 md:p-8">
|
||
<Button variant="outline" asChild className="mb-6">
|
||
<Link href={`/admin/payrolls/${payroll.id}`}>← Kembali ke Slip</Link>
|
||
</Button>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>Edit Payroll</CardTitle>
|
||
<p className="text-sm text-muted-foreground">
|
||
Revisi komponen gaji untuk periode ini. Karyawan dan periode tidak dapat diubah.
|
||
</p>
|
||
</CardHeader>
|
||
|
||
<Separator />
|
||
|
||
<CardContent className="pt-6">
|
||
{/* Info Payroll (read-only) */}
|
||
<div className="mb-6 grid grid-cols-2 gap-4 rounded-lg border bg-muted/40 p-4 text-sm sm:grid-cols-4">
|
||
<div>
|
||
<p className="text-xs text-muted-foreground">Karyawan</p>
|
||
<p className="font-semibold">{payroll.employee.name}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-muted-foreground">NIP</p>
|
||
<p className="font-semibold">{payroll.employee.nip}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-muted-foreground">Jabatan</p>
|
||
<p className="font-semibold">{payroll.employee.position ?? '-'}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-muted-foreground">Periode</p>
|
||
<p className="font-semibold">{formatPeriod(payroll.period)}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<form onSubmit={submit} className="space-y-8">
|
||
|
||
{/* ── Gaji Pokok ── */}
|
||
<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>
|
||
Gaji Pokok
|
||
</h3>
|
||
<div className="max-w-xs space-y-2">
|
||
<Label>Gaji Pokok (Rp)</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 />
|
||
|
||
{/* ── 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">2</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. Klik "Tambah Komponen" untuk menambah bonus atau potongan.
|
||
</p>
|
||
) : (
|
||
<div className="space-y-3">
|
||
<div className="grid grid-cols-[1fr_140px_160px_40px] gap-3 px-1">
|
||
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Nama Komponen</span>
|
||
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Tipe</span>
|
||
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground">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">
|
||
<Input
|
||
placeholder="cth: Bonus Kinerja"
|
||
value={item.name}
|
||
onChange={(e) => updateDetail(index, 'name', e.target.value)}
|
||
/>
|
||
<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>
|
||
<Input
|
||
type="number"
|
||
min={0}
|
||
placeholder="0"
|
||
value={item.amount || ''}
|
||
onChange={(e) => updateDetail(index, 'amount', e.target.value)}
|
||
/>
|
||
<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>
|
||
<div className="px-1">
|
||
{errors[`details.${index}.name` as keyof typeof errors] && (
|
||
<p className="text-xs text-red-500">{errors[`details.${index}.name` as keyof typeof errors]}</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/${payroll.id}`}>Batal</Link>
|
||
</Button>
|
||
<Button type="submit" disabled={processing} className="px-8">
|
||
{processing ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||
</Button>
|
||
</div>
|
||
|
||
</form>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</AppLayout>
|
||
);
|
||
}
|