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

331 lines
14 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, Link, router } from '@inertiajs/react';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import AppLayout from '@/layouts/app-layout';
import { Printer, Building2, CalendarDays, Hash, Pencil, CheckCircle2, Clock } 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';
created_at: string;
employee: {
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('-');
const date = new Date(Number(year), Number(month) - 1, 1);
return date.toLocaleDateString('id-ID', { month: 'long', year: 'numeric' });
}
function InfoCell({ label, value }: { label: string; value: string }) {
return (
<div>
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">{label}</p>
<p className="mt-0.5 text-sm font-semibold text-foreground">{value}</p>
</div>
);
}
function LineItem({
label,
value,
type = 'neutral',
}: {
label: string;
value: string;
type?: 'neutral' | 'bonus' | 'deduction';
}) {
const valueClass =
type === 'bonus'
? 'text-emerald-600 font-medium'
: type === 'deduction'
? 'text-red-500 font-medium'
: 'font-medium';
const prefix = type === 'bonus' ? '+ ' : type === 'deduction' ? ' ' : '';
return (
<div className="flex items-center justify-between py-2">
<span className="text-sm text-muted-foreground">{label}</span>
<span className={`text-sm ${valueClass}`}>
{prefix}{value}
</span>
</div>
);
}
function StatusBadge({ status }: { status: 'pending' | 'paid' }) {
return status === 'paid' ? (
<Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100 border-emerald-200 gap-1">
<CheckCircle2 className="h-3 w-3" />
Telah Dikirim
</Badge>
) : (
<Badge className="bg-amber-100 text-amber-700 hover:bg-amber-100 border-amber-200 gap-1">
<Clock className="h-3 w-3" />
Menunggu
</Badge>
);
}
export default function Show({ payroll }: PageProps) {
const bonuses = payroll.details.filter((d) => d.type === 'bonus');
const deductions = payroll.details.filter((d) => d.type === 'deduction');
const totalBonus = bonuses.reduce((s, d) => s + d.amount, 0);
const totalDeduction = deductions.reduce((s, d) => s + d.amount, 0);
const [toggling, setToggling] = useState(false);
const toggleStatus = () => {
const newStatus = payroll.status === 'paid' ? 'pending' : 'paid';
setToggling(true);
router.patch(
`/admin/payrolls/${payroll.id}/status`,
{ status: newStatus },
{ onFinish: () => setToggling(false) },
);
};
return (
<AppLayout>
<Head title={`Slip Gaji — ${payroll.employee.name}`} />
<div className="mx-auto max-w-2xl p-4 md:p-8 print:p-0 print:max-w-none">
{/* Toolbar — tersembunyi saat print */}
<div className="mb-5 flex items-center justify-between gap-2 print:hidden">
<Button variant="outline" size="sm" asChild>
<Link href="/admin/payrolls"> Kembali</Link>
</Button>
<div className="flex items-center gap-2">
{payroll.status !== 'paid' && (
<>
<Button
variant="outline"
size="sm"
onClick={toggleStatus}
disabled={toggling}
className="gap-1.5"
>
<CheckCircle2 className="h-3.5 w-3.5" /> Tandai Terkirim
</Button>
<Button
variant="outline"
size="sm"
className="gap-1.5"
asChild
>
<Link href={`/admin/payrolls/${payroll.id}/edit`}>
<Pencil className="h-3.5 w-3.5" />
Edit
</Link>
</Button>
</>
)}
<Button
variant="outline"
size="sm"
className="gap-1.5"
onClick={() => window.print()}
>
<Printer className="h-3.5 w-3.5" />
Cetak
</Button>
</div>
</div>
{/* ── Slip Card ── */}
<Card className="overflow-hidden border shadow-sm print:shadow-none print:border-none print:rounded-none">
{/* ── HEADER ── */}
<div className="border-b bg-zinc-50 px-6 py-5 print:break-inside-avoid">
<div className="flex items-start justify-between">
{/* Nama Perusahaan */}
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10">
<Building2 className="h-5 w-5 text-primary" />
</div>
<div>
<p className="text-sm font-bold leading-none text-foreground">
PT. HRIS Nusantara
</p>
<p className="mt-0.5 text-xs text-muted-foreground">
Slip Gaji Karyawan
</p>
</div>
</div>
{/* Meta slip */}
<div className="text-right">
<div className="flex items-center justify-end gap-1.5 text-xs text-muted-foreground">
<Hash className="h-3 w-3" />
<span className="font-mono font-medium">
{String(payroll.id).padStart(5, '0')}
</span>
</div>
<div className="mt-1 flex items-center justify-end gap-1.5 text-xs text-muted-foreground">
<CalendarDays className="h-3 w-3" />
<span>{payroll.created_at}</span>
</div>
<div className="mt-2">
<StatusBadge status={payroll.status} />
</div>
</div>
</div>
</div>
<CardContent className="p-0">
{/* ── INFO KARYAWAN ── */}
<div className="border-b px-6 py-5 print:break-inside-avoid">
<p className="mb-3 text-[11px] font-semibold uppercase tracking-widest text-muted-foreground">
Informasi Karyawan
</p>
<div className="grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-4">
<InfoCell label="Nama" value={payroll.employee.name} />
<InfoCell label="NIP" value={payroll.employee.nip} />
<InfoCell label="Jabatan" value={payroll.employee.position ?? '-'} />
<InfoCell label="Departemen" value={payroll.employee.department ?? '-'} />
</div>
</div>
{/* ── PERIODE ── */}
<div className="flex items-center justify-between border-b bg-muted/30 px-6 py-3 print:break-inside-avoid">
<p className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">
Periode Penggajian
</p>
<p className="text-sm font-bold text-foreground">
{formatPeriod(payroll.period)}
</p>
</div>
{/* ── RINCIAN GAJI ── */}
<div className="px-6 py-5 space-y-1 print:break-inside-avoid">
{/* Gaji Pokok */}
<p className="mb-1 text-[11px] font-semibold uppercase tracking-widest text-muted-foreground">
Komponen Gaji
</p>
<LineItem
label="Gaji Pokok"
value={formatRupiah(payroll.basic_salary)}
/>
{/* Bonus */}
{bonuses.length > 0 && (
<>
<Separator className="my-2" />
<p className="pb-0.5 text-[11px] font-semibold uppercase tracking-widest text-emerald-600">
Tambahan
</p>
{bonuses.map((item, i) => (
<LineItem
key={i}
label={item.name}
value={formatRupiah(item.amount)}
type="bonus"
/>
))}
<div className="flex justify-between pt-1 text-xs">
<span className="text-muted-foreground">Subtotal tambahan</span>
<span className="font-semibold text-emerald-600">
+ {formatRupiah(totalBonus)}
</span>
</div>
</>
)}
{/* Potongan */}
{deductions.length > 0 && (
<>
<Separator className="my-2" />
<p className="pb-0.5 text-[11px] font-semibold uppercase tracking-widest text-red-500">
Potongan
</p>
{deductions.map((item, i) => (
<LineItem
key={i}
label={item.name}
value={formatRupiah(item.amount)}
type="deduction"
/>
))}
<div className="flex justify-between pt-1 text-xs">
<span className="text-muted-foreground">Subtotal potongan</span>
<span className="font-semibold text-red-500">
{formatRupiah(totalDeduction)}
</span>
</div>
</>
)}
</div>
{/* ── TOTAL GAJI BERSIH ── */}
<div className="border-t bg-zinc-50 px-6 py-5 print:break-inside-avoid">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-semibold text-foreground">
Total Gaji Bersih
</p>
<p className="text-xs text-muted-foreground">
Gaji Pokok
{bonuses.length > 0 && ' + Tambahan'}
{deductions.length > 0 && ' Potongan'}
</p>
</div>
<p className="text-2xl font-bold tracking-tight text-foreground">
{formatRupiah(payroll.net_salary)}
</p>
</div>
</div>
{/* ── FOOTER ── */}
<div className="border-t px-6 py-3">
<p className="text-center text-[11px] text-muted-foreground">
Dokumen ini diterbitkan secara otomatis oleh sistem HRIS dan sah tanpa tanda tangan basah.
</p>
</div>
</CardContent>
</Card>
</div>
</AppLayout>
);
}