- 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.
384 lines
17 KiB
TypeScript
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 ·
|
|
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>
|
|
</>
|
|
);
|
|
}
|