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

274 lines
13 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 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 &quot;Tambah Komponen&quot; 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>
);
}