dstpabuaran.com/resources/js/pages/admin/finance/payroll-period/show.tsx
Yoga Pangestu 50b2be68ac Add error handling with toast notifications for form submissions
- Integrated toast notifications to display error messages when form submissions fail across various components, enhancing user feedback and experience.
- Updated components including DeleteUser, FormDialog, ManageTwoFactor, TwoFactorRecoveryCodes, TwoFactorSetupModal, PayrollPeriodShow, EmployeeCreate, EmployeeEdit, CuttingCreate, CuttingEdit, PurchaseCreate, PurchaseEdit, RestockCreate, RestockEdit, TransactionCreate, TransactionEdit, Category management, Product management, Raw Material management, Role management, Settings, and Authentication pages.
2026-08-07 08:50:11 +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/confirm-dialog';
import { DataTable } from '@/components/data-table';
import InputError from '@/components/input-error';
import { RupiahInput } from '@/components/rupiah-input';
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"
searchPlaceholder="Cari karyawan..."
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>
</>
);
}