dstpabuaran.com/resources/js/pages/admin/finance/payroll-period/show.tsx
Yoga Pangestu 166419134f Refactor component imports for consistency and organization
- Updated import paths for various components to align with new directory structure.
- Changed imports from 'row-actions', 'confirm-dialog', 'image-preview-button', and 'file-upload' to their respective new locations in 'data-display', 'dialogs', and 'inputs'.
- Adjusted imports in multiple pages including purchase, restock, transaction, category, customer, product, raw-material, supplier, roles, and settings.
- Ensured all relevant components are imported from their new locations to maintain functionality.
2026-08-07 13:48:46 +07:00

384 lines
17 KiB
TypeScript

import { Form, Head, Link, router } from '@inertiajs/react';
import { ArrowLeft } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
import { ConfirmDialog } from '@/components/dialogs';
import { DataTable } from '@/components/data-display';
import { InputError } from '@/components/ui';
import { RupiahInput } from '@/components/inputs';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { useCan } from '@/hooks/use-can';
import { MONTH_NAMES } from '@/lib/constants';
import { formatCurrency } from '@/lib/utils';
import { destroy as adjustmentDestroy } from '@/routes/admin/finance/payroll-adjustments';
import { index as payrollPeriodsIndex } from '@/routes/admin/finance/payroll-periods';
import {
cancel as payrollCancel,
pay as payrollPay,
} from '@/routes/admin/finance/payrolls';
import { store as adjustmentStore } from '@/routes/admin/finance/payrolls/adjustments';
import type { Payroll, PayrollAdjustment } from './show-columns';
import { createPayrollColumns } from './show-columns';
type Props = {
payrollPeriod: {
id: number;
year: number;
month: number;
status: string;
payrolls: Payroll[];
};
};
export default function PayrollPeriodShow({ payrollPeriod }: Props) {
const { can, hasAnyRole } = useCan();
const canViewAll = hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko']);
const [paying, setPaying] = useState<Payroll | null>(null);
const [cancelling, setCancelling] = useState<Payroll | null>(null);
const [addingAdjustment, setAddingAdjustment] = useState<Payroll | null>(
null,
);
const [deletingAdjustment, setDeletingAdjustment] = useState<{
adjustment: PayrollAdjustment;
payrollId: number;
} | null>(null);
const [adjustmentType, setAdjustmentType] = useState<string>('bonus');
function handlePay() {
if (!paying) {
return;
}
router.post(
payrollPay(paying.id),
{},
{
onSuccess: () => setPaying(null),
},
);
}
function handleCancel() {
if (!cancelling) {
return;
}
router.post(
payrollCancel(cancelling.id),
{},
{
onSuccess: () => setCancelling(null),
},
);
}
function handleDeleteAdjustment() {
if (!deletingAdjustment) {
return;
}
router.delete(adjustmentDestroy(deletingAdjustment.adjustment.id), {
onSuccess: () => setDeletingAdjustment(null),
});
}
const columns = createPayrollColumns({
handlePay: (payroll) => setPaying(payroll),
handleCancel: (payroll) => setCancelling(payroll),
handleAddAdjustment: (payroll) => {
setAddingAdjustment(payroll);
setAdjustmentType('bonus');
},
handleDeleteAdjustment: (adjustment, payrollId) => {
setDeletingAdjustment({ adjustment, payrollId });
},
can,
});
const activePayrolls = payrollPeriod.payrolls.filter(
(p) => p.status !== 'cancelled',
);
const totalBaseSalary = activePayrolls.reduce(
(sum, p) => sum + p.base_salary,
0,
);
const totalBonus = activePayrolls.reduce(
(sum, p) => sum + p.bonus_amount,
0,
);
const totalDeduction = activePayrolls.reduce(
(sum, p) => sum + p.deduction_amount,
0,
);
const totalAmount = activePayrolls.reduce(
(sum, p) => sum + p.total_amount,
0,
);
return (
<>
<Head
title={`Gaji - ${MONTH_NAMES[payrollPeriod.month]} ${payrollPeriod.year}`}
/>
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-semibold tracking-tight">
Gaji {MONTH_NAMES[payrollPeriod.month]}{' '}
{payrollPeriod.year}
</h2>
{canViewAll && (
<p className="text-sm text-muted-foreground">
{payrollPeriod.payrolls.length} karyawan &middot;
Status:{' '}
{payrollPeriod.status === 'open'
? 'Terbuka'
: 'Ditutup'}
</p>
)}
</div>
<Button asChild variant="outline">
<Link href={payrollPeriodsIndex.url()}>
<ArrowLeft className="h-4 w-4" />
Kembali
</Link>
</Button>
</div>
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
<div className="rounded-lg border p-4">
<p className="text-sm text-muted-foreground">
Total Gaji Pokok
</p>
<p className="text-lg font-semibold">
{formatCurrency(totalBaseSalary)}
</p>
</div>
<div className="rounded-lg border p-4">
<p className="text-sm text-muted-foreground">
Total Bonus
</p>
<p className="text-lg font-semibold text-green-600">
{formatCurrency(totalBonus)}
</p>
</div>
<div className="rounded-lg border p-4">
<p className="text-sm text-muted-foreground">
Total Potongan
</p>
<p className="text-lg font-semibold text-red-600">
{formatCurrency(totalDeduction)}
</p>
</div>
<div className="rounded-lg border p-4">
<p className="text-sm text-muted-foreground">
Total Gaji
</p>
<p className="text-lg font-semibold">
{formatCurrency(totalAmount)}
</p>
</div>
</div>
<DataTable
columns={columns}
data={payrollPeriod.payrolls}
searchKey="employee_name"
emptyText="Belum ada data gaji."
/>
{/* Dialog Tambah Penyesuaian */}
<Dialog
open={addingAdjustment !== null}
onOpenChange={(open) => {
if (!open) {
setAddingAdjustment(null);
setAdjustmentType('bonus');
}
}}
>
<DialogContent>
{addingAdjustment && (
<Form
action={adjustmentStore(addingAdjustment.id)}
resetOnSuccess
onSuccess={() => {
setAddingAdjustment(null);
setAdjustmentType('bonus');
}}
onError={() => {
toast.error('Terjadi kesalahan saat menyimpan data. Silakan periksa kembali input Anda.');
}}
>
{({ errors, processing }) => (
<>
<DialogHeader>
<DialogTitle>
Tambah Penyesuaian
</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label>
Jenis{' '}
<span className="text-destructive">
*
</span>
</Label>
<input
type="hidden"
name="type"
value={adjustmentType}
/>
<RadioGroup
value={adjustmentType}
onValueChange={
setAdjustmentType
}
className="flex gap-4"
>
<div className="flex items-center gap-2">
<RadioGroupItem
value="bonus"
id="bonus"
/>
<Label
htmlFor="bonus"
className="font-normal"
>
Bonus
</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem
value="deduction"
id="deduction"
/>
<Label
htmlFor="deduction"
className="font-normal"
>
Potongan
</Label>
</div>
</RadioGroup>
<InputError
message={errors.type}
/>
</div>
<div className="grid gap-2">
<Label>
Jumlah{' '}
<span className="text-destructive">
*
</span>
</Label>
<RupiahInput
name="amount"
/>
<InputError
message={errors.amount}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="adjustment-description">
Keterangan{' '}
<span className="text-destructive">
*
</span>
</Label>
<Input
id="adjustment-description"
name="description"
placeholder="Masukkan keterangan"
/>
<InputError
message={errors.description}
/>
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() =>
setAddingAdjustment(null)
}
>
Batal
</Button>
<Button
type="submit"
disabled={processing}
>
{processing
? 'Menyimpan...'
: 'Simpan'}
</Button>
</DialogFooter>
</>
)}
</Form>
)}
</DialogContent>
</Dialog>
{/* Dialog Konfirmasi Bayar */}
<ConfirmDialog
open={paying !== null}
onOpenChange={(open) => {
if (!open) {
setPaying(null);
}
}}
title="Tandai Dibayar"
description={`Apakah Anda yakin ingin menandai gaji "${paying?.employee?.user?.user_profile?.full_name}" sebesar ${formatCurrency(paying?.total_amount ?? 0)} sebagai sudah dibayar? Penyesuaian tidak dapat ditambahkan setelah dibayar.`}
confirmLabel="Bayar"
onConfirm={handlePay}
/>
{/* Dialog Konfirmasi Batalkan */}
<ConfirmDialog
open={cancelling !== null}
onOpenChange={(open) => {
if (!open) {
setCancelling(null);
}
}}
title="Batalkan Gaji"
description={`Apakah Anda yakin ingin membatalkan gaji "${cancelling?.employee?.user?.user_profile?.full_name}"?`}
confirmLabel="Batalkan"
onConfirm={handleCancel}
/>
{/* Dialog Konfirmasi Hapus Penyesuaian */}
<ConfirmDialog
open={deletingAdjustment !== null}
onOpenChange={(open) => {
if (!open) {
setDeletingAdjustment(null);
}
}}
title="Hapus Penyesuaian"
description={`Apakah Anda yakin ingin menghapus penyesuaian "${deletingAdjustment?.adjustment.description}" sebesar ${formatCurrency(deletingAdjustment?.adjustment.amount ?? 0)}?`}
confirmLabel="Hapus"
onConfirm={handleDeleteAdjustment}
/>
</div>
</>
);
}