dress/resources/js/pages/admin/finance/payroll/index.tsx

213 lines
9.5 KiB
TypeScript

import { Head } from '@inertiajs/react';
import type { Payroll } from '@/types';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Trash2, Sparkles, Plus, X } from 'lucide-react';
import { DataTable } from '@/components/data-table';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { usePayrollIndex } from './hooks/use-payroll-index';
import { Badge } from '@/components/ui/badge';
import { getColumns } from './partials/columns';
import { PayrollFormModal } from './partials/payroll-form-modal';
export default function PayrollIndex({
payrolls,
currentMonthPayrolls,
}: {
payrolls: Payroll[],
currentMonthPayrolls: Payroll[],
}) {
const {
isFormOpen,
selectedPayroll,
isDeleteDialogOpen,
isBulkDeleteDialogOpen,
payrollToDelete,
rowsToDelete,
rowSelection,
isGenerating,
setRowSelection,
setRowsToDelete,
setIsDeleteDialogOpen,
setIsBulkDeleteDialogOpen,
onGenerate,
onEdit,
onDelete,
confirmDelete,
confirmBulkDelete,
onTogglePaid,
closeForm,
} = usePayrollIndex();
const columns = getColumns({ onEdit, onDelete, onTogglePaid });
return (
<div className="flex flex-col gap-6 p-6">
<Head title="Penggajian" />
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Penggajian</h1>
</div>
<Button onClick={onGenerate} disabled={isGenerating} className="gap-2">
{isGenerating ? <Sparkles className="size-4 animate-spin" /> : <Sparkles className="size-4" />}
Generate
</Button>
</div>
<div className="flex flex-col gap-3">
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{currentMonthPayrolls.map((payroll) => (
<Card
key={payroll.id}
className="border-none shadow-lg bg-card/50 backdrop-blur-sm hover:scale-[1.02] transition-transform cursor-pointer"
onClick={() => onEdit(payroll)}
>
<CardContent className="p-4">
<div className="flex justify-between items-start mb-3">
<div className="flex flex-col">
<span className="font-bold text-sm line-clamp-1">{payroll.user?.name}</span>
<span className="text-[10px] text-muted-foreground">{payroll.period_month_formatted}</span>
</div>
<Badge variant={payroll.is_paid ? 'default' : 'outline'} className={payroll.is_paid ? 'bg-green-500 hover:bg-green-600 text-[10px]' : 'text-[10px]'}>
{payroll.is_paid ? 'Lunas' : 'Pending'}
</Badge>
</div>
<div className="space-y-1">
<div className="flex justify-between text-[10px] text-muted-foreground">
<span>Gaji Pokok</span>
<span>{payroll.base_salary_formatted}</span>
</div>
{payroll.adjustments?.map((adj, idx) => (
<div key={idx} className="flex justify-between text-[10px] text-muted-foreground italic">
<span>{adj.description}</span>
<span className={adj.type === 'bonus' ? 'text-green-600' : 'text-red-500'}>
{adj.type === 'bonus' ? '+' : '-'}{adj.amount_formatted}
</span>
</div>
))}
<div className="flex justify-between text-[10px] text-muted-foreground pt-1 border-t">
<span>Gaji Bersih</span>
<span className="font-medium text-foreground">{payroll.total_salary_formatted}</span>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
<PayrollFormModal
isOpen={isFormOpen}
onClose={closeForm}
payroll={selectedPayroll}
/>
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm p-5">
<CardContent className="p-0">
<DataTable
columns={columns}
data={payrolls}
rowSelection={rowSelection}
onRowSelectionChange={setRowSelection}
filters={[
{
columnId: 'is_paid',
title: 'Status',
options: [
{ label: 'Dibayar', value: 'true' },
{ label: 'Pending', value: 'false' },
]
}
]}
bulkActions={[
{
label: 'Hapus Terpilih',
onClick: (rows) => {
setRowsToDelete(rows);
setIsBulkDeleteDialogOpen(true);
},
icon: Trash2,
variant: 'destructive'
},
]}
/>
</CardContent>
</Card>
{/* Single Delete Confirmation */}
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<AlertDialogContent size="default">
<AlertDialogHeader>
<AlertDialogMedia className="bg-destructive/10 text-destructive dark:bg-destructive/20 dark:text-destructive">
<Trash2 className="size-5" />
</AlertDialogMedia>
<AlertDialogTitle>Hapus data penggajian?</AlertDialogTitle>
<AlertDialogDescription>
Tindakan ini tidak dapat dibatalkan. Data penggajian untuk <strong>{payrollToDelete?.user?.name}</strong> periode <strong>{payrollToDelete?.period_month}</strong> akan dihapus secara permanen.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel variant="outline" className="gap-2">
<X className="size-4" />
Batal
</AlertDialogCancel>
<AlertDialogAction onClick={confirmDelete} variant="destructive" className="gap-2">
<Trash2 className="size-4" />
Hapus
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Bulk Delete Confirmation */}
<AlertDialog open={isBulkDeleteDialogOpen} onOpenChange={setIsBulkDeleteDialogOpen}>
<AlertDialogContent size="default">
<AlertDialogHeader>
<AlertDialogMedia className="bg-destructive/10 text-destructive dark:bg-destructive/20 dark:text-destructive">
<Trash2 className="size-5" />
</AlertDialogMedia>
<AlertDialogTitle>Hapus {rowsToDelete.length} data penggajian?</AlertDialogTitle>
<AlertDialogDescription>
Tindakan ini tidak dapat dibatalkan. <strong>{rowsToDelete.length}</strong> item yang terpilih akan dihapus secara permanen.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel variant="outline" className="gap-2">
<X className="size-4" />
Batal
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmBulkDelete}
variant="destructive"
className="gap-2"
>
<Trash2 className="size-4" />
Hapus
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
PayrollIndex.layout = {
breadcrumbs: [
{
title: 'Keuangan',
},
],
};