refactor: extract payroll card to reusable component and improve state persistence for toggle actions
This commit is contained in:
parent
b04dd959f6
commit
d6935d49d6
72
resources/js/components/cards/payroll-card.tsx
Normal file
72
resources/js/components/cards/payroll-card.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import type { Payroll } from '@/types';
|
||||||
|
|
||||||
|
export interface PayrollCardProps {
|
||||||
|
payroll: Payroll;
|
||||||
|
onClick?: () => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PayrollCard({ payroll, onClick, className }: PayrollCardProps) {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={cn(
|
||||||
|
'@container/card relative cursor-pointer hover:scale-[1.02] transition-transform shadow-lg bg-card/50 backdrop-blur-sm',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<CardHeader className="gap-3">
|
||||||
|
<div className="flex w-full items-start justify-between gap-3">
|
||||||
|
<CardDescription className="min-w-0 flex-1 break-words font-bold text-foreground leading-tight">
|
||||||
|
{payroll.user?.name}
|
||||||
|
</CardDescription>
|
||||||
|
<Badge
|
||||||
|
variant={payroll.is_paid ? 'default' : 'outline'}
|
||||||
|
className={cn(
|
||||||
|
'shrink-0 flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-bold sm:text-xs',
|
||||||
|
payroll.is_paid
|
||||||
|
? 'bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300'
|
||||||
|
: 'text-muted-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{payroll.is_paid ? 'Lunas' : 'Pending'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardTitle className="break-words text-xl sm:text-2xl font-bold tabular-nums tracking-tight">
|
||||||
|
{payroll.total_salary_formatted}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardFooter className="flex-col items-start gap-1.5 text-xs">
|
||||||
|
<div className="w-full space-y-1">
|
||||||
|
<div className="flex justify-between 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-muted-foreground italic">
|
||||||
|
<span>{adj.description}</span>
|
||||||
|
<span className={adj.type === 'bonus' ? 'text-green-600 font-medium' : 'text-red-500 font-medium'}>
|
||||||
|
{adj.type === 'bonus' ? '+' : '-'}{adj.amount_formatted}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="w-full pt-1.5 border-t mt-1.5 text-muted-foreground flex justify-between items-center">
|
||||||
|
<span>Periode</span>
|
||||||
|
<span className="font-semibold text-foreground">{payroll.period_month_formatted}</span>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
|
import payrollRoutes from '@/routes/payroll';
|
||||||
|
import type { Payroll } from '@/types';
|
||||||
import { router } from '@inertiajs/react';
|
import { router } from '@inertiajs/react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import payrollRoutes from '@/routes/payroll';
|
|
||||||
import type { Payroll } from '@/types';
|
|
||||||
|
|
||||||
export function usePayrollIndex() {
|
export function usePayrollIndex() {
|
||||||
const [isFormOpen, setIsFormOpen] = useState(false);
|
const [isFormOpen, setIsFormOpen] = useState(false);
|
||||||
@ -70,6 +70,8 @@ export function usePayrollIndex() {
|
|||||||
|
|
||||||
const onTogglePaid = (id: number) => {
|
const onTogglePaid = (id: number) => {
|
||||||
router.patch(payrollRoutes.togglePaid(id).url, {}, {
|
router.patch(payrollRoutes.togglePaid(id).url, {}, {
|
||||||
|
preserveState: true,
|
||||||
|
preserveScroll: true,
|
||||||
onSuccess: (response: any) => toast.success(response.props.flash.success),
|
onSuccess: (response: any) => toast.success(response.props.flash.success),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,25 +1,24 @@
|
|||||||
import { Head } from '@inertiajs/react';
|
|
||||||
import { Trash2, Sparkles } from 'lucide-react';
|
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
|
import { Head } from '@inertiajs/react';
|
||||||
|
import { Sparkles, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
|
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import type { Payroll } from '@/types';
|
import type { Payroll } from '@/types';
|
||||||
|
|
||||||
|
import { PayrollCard } from '@/components/cards/payroll-card';
|
||||||
|
import { usePermission } from '@/hooks/use-permission';
|
||||||
import { usePayrollIndex } from './hooks/use-payroll-index';
|
import { usePayrollIndex } from './hooks/use-payroll-index';
|
||||||
import { getColumns } from './partials/columns';
|
import { getColumns } from './partials/columns';
|
||||||
import { PayrollFormModal } from './partials/payroll-form-modal';
|
import { PayrollFormModal } from './partials/payroll-form-modal';
|
||||||
import { usePermission } from '@/hooks/use-permission';
|
|
||||||
|
|
||||||
export default function PayrollIndex({
|
export default function PayrollIndex({
|
||||||
payrolls,
|
payrolls,
|
||||||
currentMonthPayrolls,
|
currentMonthPayrolls,
|
||||||
}: {
|
}: {
|
||||||
payrolls: Payroll[],
|
payrolls: Payroll[];
|
||||||
currentMonthPayrolls: Payroll[],
|
currentMonthPayrolls: Payroll[];
|
||||||
}) {
|
}) {
|
||||||
const { hasPermission } = usePermission();
|
const { hasPermission } = usePermission();
|
||||||
const {
|
const {
|
||||||
@ -45,7 +44,10 @@ export default function PayrollIndex({
|
|||||||
} = usePayrollIndex();
|
} = usePayrollIndex();
|
||||||
|
|
||||||
const columns = getColumns({ onEdit, onDelete, onTogglePaid }).filter(
|
const columns = getColumns({ onEdit, onDelete, onTogglePaid }).filter(
|
||||||
(col) => col.id !== 'actions' || hasPermission('Edit:PayrollAdjustment') || hasPermission('Delete:Payroll')
|
(col) =>
|
||||||
|
col.id !== 'actions' ||
|
||||||
|
hasPermission('Edit:PayrollAdjustment') ||
|
||||||
|
hasPermission('Delete:Payroll'),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -54,11 +56,21 @@ export default function PayrollIndex({
|
|||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Penggajian</h1>
|
<h1 className="text-3xl font-bold tracking-tight">
|
||||||
|
Penggajian
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
{hasPermission('Generate:Payroll') && (
|
{hasPermission('Generate:Payroll') && (
|
||||||
<Button onClick={onGenerate} disabled={isGenerating} className="gap-2">
|
<Button
|
||||||
{isGenerating ? <Sparkles className="size-4 animate-spin" /> : <Sparkles className="size-4" />}
|
onClick={onGenerate}
|
||||||
|
disabled={isGenerating}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
{isGenerating ? (
|
||||||
|
<Sparkles className="size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Sparkles className="size-4" />
|
||||||
|
)}
|
||||||
Generate
|
Generate
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@ -67,45 +79,15 @@ export default function PayrollIndex({
|
|||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||||
{currentMonthPayrolls.map((payroll) => (
|
{currentMonthPayrolls.map((payroll) => (
|
||||||
<Card
|
<PayrollCard
|
||||||
key={payroll.id}
|
key={payroll.id}
|
||||||
className="border-none shadow-lg bg-card/50 backdrop-blur-sm hover:scale-[1.02] transition-transform cursor-pointer"
|
payroll={payroll}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (hasPermission('Edit:PayrollAdjustment')) {
|
if (hasPermission('Edit:PayrollAdjustment')) {
|
||||||
onEdit(payroll);
|
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>
|
||||||
</div>
|
</div>
|
||||||
@ -116,7 +98,7 @@ export default function PayrollIndex({
|
|||||||
payroll={selectedPayroll}
|
payroll={selectedPayroll}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm p-5">
|
<Card className="overflow-hidden border-none bg-card/50 p-5 shadow-lg backdrop-blur-sm">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@ -131,8 +113,8 @@ export default function PayrollIndex({
|
|||||||
options: [
|
options: [
|
||||||
{ label: 'Dibayar', value: 'true' },
|
{ label: 'Dibayar', value: 'true' },
|
||||||
{ label: 'Pending', value: 'false' },
|
{ label: 'Pending', value: 'false' },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
bulkActions={
|
bulkActions={
|
||||||
hasPermission('DeleteAny:Payroll')
|
hasPermission('DeleteAny:Payroll')
|
||||||
@ -160,7 +142,11 @@ export default function PayrollIndex({
|
|||||||
title="Hapus penggajian?"
|
title="Hapus penggajian?"
|
||||||
description={
|
description={
|
||||||
<>
|
<>
|
||||||
Tindakan ini tidak dapat dibatalkan. Penggajian <strong>{payrollToDelete?.period_month_formatted}</strong> akan dihapus secara permanen dan stok akan dikembalikan.
|
Tindakan ini tidak dapat dibatalkan. Penggajian{' '}
|
||||||
|
<strong>
|
||||||
|
{payrollToDelete?.period_month_formatted}
|
||||||
|
</strong>{' '}
|
||||||
|
akan dihapus secara permanen dan stok akan dikembalikan.
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
onDelete={confirmDelete}
|
onDelete={confirmDelete}
|
||||||
@ -173,7 +159,9 @@ export default function PayrollIndex({
|
|||||||
title={`Hapus ${rowsToDelete.length} penggajian?`}
|
title={`Hapus ${rowsToDelete.length} penggajian?`}
|
||||||
description={
|
description={
|
||||||
<>
|
<>
|
||||||
Tindakan ini tidak dapat dibatalkan. <strong>{rowsToDelete.length}</strong> item yang terpilih akan dihapus secara permanen.
|
Tindakan ini tidak dapat dibatalkan.{' '}
|
||||||
|
<strong>{rowsToDelete.length}</strong> item yang
|
||||||
|
terpilih akan dihapus secara permanen.
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
onDelete={confirmBulkDelete}
|
onDelete={confirmBulkDelete}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user