feat: enhance payroll period functionality with role-based visibility and improved data handling

This commit is contained in:
Yoga Pangestu 2026-08-06 13:30:04 +07:00
parent 54ad2edaf7
commit 8eb3e565ec
6 changed files with 95 additions and 46 deletions

View File

@ -15,7 +15,7 @@ protected function handleAction(callable $action, string $successMessage, string
$action();
Inertia::flash('toast', ['type' => 'success', 'message' => $successMessage]);
return to_route($redirectRoute);
return to_route($redirectRoute, $parameters);
} catch (ValidationException $e) {
$firstError = collect($e->errors())->flatten()->first();
Inertia::flash('toast', ['type' => 'error', 'message' => $firstError ?? 'Terjadi kesalahan.']);

View File

@ -17,19 +17,30 @@ class PayrollPeriodService
{
use HandlesCashTransactions;
private function canViewAll(): bool
{
return auth()->user()->hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko']);
}
public function getAll(array $filters = []): Collection
{
return PayrollPeriod::select(['id', 'year', 'month', 'status', 'closed_at', 'created_at'])
->withCount('payrolls')
->withSum('payrolls', 'total_amount')
->withSum('payrolls', 'bonus_amount')
->withSum('payrolls', 'deduction_amount')
->withCount(['payrolls as paid_count' => function ($q) {
$q->paid();
}])
->withCount(['payrolls as cancelled_count' => function ($q) {
$q->cancelled();
}])
->when(! $this->canViewAll(), function ($query) {
$query->withCount(['payrolls as payrolls_count' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))])
->withSum(['payrolls as payrolls_sum_total_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'total_amount')
->withSum(['payrolls as payrolls_sum_bonus_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'bonus_amount')
->withSum(['payrolls as payrolls_sum_deduction_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'deduction_amount')
->withCount(['payrolls as paid_count' => fn ($q) => $q->paid()->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))])
->withCount(['payrolls as cancelled_count' => fn ($q) => $q->cancelled()->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))]);
})
->when($this->canViewAll(), function ($query) {
$query->withCount('payrolls')
->withSum('payrolls', 'total_amount')
->withSum('payrolls', 'bonus_amount')
->withSum('payrolls', 'deduction_amount')
->withCount(['payrolls as paid_count' => fn ($q) => $q->paid()])
->withCount(['payrolls as cancelled_count' => fn ($q) => $q->cancelled()]);
})
->latest('year')
->latest('month')
->get();
@ -39,16 +50,22 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
{
return PayrollPeriod::query()
->select(['id', 'year', 'month', 'status', 'closed_at', 'created_at'])
->withCount('payrolls')
->withSum('payrolls', 'total_amount')
->withSum('payrolls', 'bonus_amount')
->withSum('payrolls', 'deduction_amount')
->withCount(['payrolls as paid_count' => function ($q) {
$q->paid();
}])
->withCount(['payrolls as cancelled_count' => function ($q) {
$q->cancelled();
}])
->when(! $this->canViewAll(), function ($query) {
$query->withCount(['payrolls as payrolls_count' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))])
->withSum(['payrolls as payrolls_sum_total_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'total_amount')
->withSum(['payrolls as payrolls_sum_bonus_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'bonus_amount')
->withSum(['payrolls as payrolls_sum_deduction_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'deduction_amount')
->withCount(['payrolls as paid_count' => fn ($q) => $q->paid()->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))])
->withCount(['payrolls as cancelled_count' => fn ($q) => $q->cancelled()->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))]);
})
->when($this->canViewAll(), function ($query) {
$query->withCount('payrolls')
->withSum('payrolls', 'total_amount')
->withSum('payrolls', 'bonus_amount')
->withSum('payrolls', 'deduction_amount')
->withCount(['payrolls as paid_count' => fn ($q) => $q->paid()])
->withCount(['payrolls as cancelled_count' => fn ($q) => $q->cancelled()]);
})
->when($search, fn ($q) => $q->where('year', 'like', "%{$search}%"))
->orderBy($sort, $direction)
->paginate($perPage);
@ -59,6 +76,7 @@ public function getDetail(PayrollPeriod $period): PayrollPeriod
return $period->load([
'payrolls' => function ($query) {
$query->with(['employee.user.userProfile', 'payrollAdjustments'])
->when(! $this->canViewAll(), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
->orderBy('id');
},
]);

View File

@ -50,14 +50,15 @@ type CreateColumnsParams = {
handleClose: (period: PayrollPeriod) => void;
handleReopen: (period: PayrollPeriod) => void;
can: (permission: string) => boolean;
canViewAll: boolean;
};
export function createPayrollPeriodColumns(
params: CreateColumnsParams,
): ColumnDef<PayrollPeriod>[] {
const { showUrl, handleClose, handleReopen, can } = params;
const { showUrl, handleClose, handleReopen, can, canViewAll } = params;
return [
const columns: ColumnDef<PayrollPeriod>[] = [
{
accessorKey: 'year',
header: () => <span>Periode</span>,
@ -67,7 +68,10 @@ export function createPayrollPeriodColumns(
</span>
),
},
{
];
if (canViewAll) {
columns.push({
accessorKey: 'payrolls_count',
header: () => <span>Jumlah Karyawan</span>,
cell: ({ row }) => (
@ -75,7 +79,10 @@ export function createPayrollPeriodColumns(
{row.getValue('payrolls_count') as number}
</span>
),
},
});
}
columns.push(
{
accessorKey: 'payrolls_sum_total_amount',
header: () => <span>Total Gaji</span>,
@ -131,7 +138,10 @@ export function createPayrollPeriodColumns(
);
},
},
{
);
if (canViewAll) {
columns.push({
id: 'payment_status',
header: () => <span>Status Bayar</span>,
cell: ({ row }) => {
@ -161,7 +171,10 @@ export function createPayrollPeriodColumns(
</div>
);
},
},
});
}
columns.push(
{
accessorKey: 'status',
header: () => <span>Status</span>,
@ -212,5 +225,7 @@ export function createPayrollPeriodColumns(
);
},
},
];
);
return columns;
}

View File

@ -27,7 +27,8 @@ type Props = {
};
export default function PayrollPeriodIndex({ payrollPeriods }: Props) {
const { can } = useCan();
const { can, hasAnyRole } = useCan();
const canViewAll = hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko']);
const [closing, setClosing] = useState<PayrollPeriod | null>(null);
const [reopening, setReopening] = useState<PayrollPeriod | null>(null);
@ -81,6 +82,7 @@ export default function PayrollPeriodIndex({ payrollPeriods }: Props) {
handleClose: (period) => setClosing(period),
handleReopen: (period) => setReopening(period),
can,
canViewAll,
});
return (

View File

@ -81,7 +81,9 @@ export function createPayrollColumns(
can,
} = params;
return [
const hasAnyPayrollAction = can('payroll.adjust') || can('payroll.pay') || can('payroll.cancel');
const columns: ColumnDef<Payroll>[] = [
{
id: 'employee_name',
header: () => <span>Nama Karyawan</span>,
@ -185,7 +187,7 @@ export function createPayrollColumns(
<span className="max-w-[100px] text-muted-foreground">
{adj.description}
</span>
{payroll.status === 'unpaid' && (
{can('payroll.adjust') && payroll.status === 'unpaid' && (
<button
onClick={() =>
handleDeleteAdjustment(
@ -204,7 +206,10 @@ export function createPayrollColumns(
);
},
},
{
];
if (hasAnyPayrollAction) {
columns.push({
id: 'actions',
header: () => <span className="block text-center">Aksi</span>,
meta: {
@ -249,6 +254,8 @@ export function createPayrollColumns(
/>
);
},
},
];
});
}
return columns;
}

View File

@ -40,7 +40,8 @@ type Props = {
};
export default function PayrollPeriodShow({ payrollPeriod }: Props) {
const { can } = useCan();
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>(
@ -103,19 +104,23 @@ export default function PayrollPeriodShow({ payrollPeriod }: Props) {
can,
});
const totalBaseSalary = payrollPeriod.payrolls.reduce(
const activePayrolls = payrollPeriod.payrolls.filter(
(p) => p.status !== 'cancelled',
);
const totalBaseSalary = activePayrolls.reduce(
(sum, p) => sum + p.base_salary,
0,
);
const totalBonus = payrollPeriod.payrolls.reduce(
const totalBonus = activePayrolls.reduce(
(sum, p) => sum + p.bonus_amount,
0,
);
const totalDeduction = payrollPeriod.payrolls.reduce(
const totalDeduction = activePayrolls.reduce(
(sum, p) => sum + p.deduction_amount,
0,
);
const totalAmount = payrollPeriod.payrolls.reduce(
const totalAmount = activePayrolls.reduce(
(sum, p) => sum + p.total_amount,
0,
);
@ -133,13 +138,15 @@ export default function PayrollPeriodShow({ payrollPeriod }: Props) {
Gaji {MONTH_NAMES[payrollPeriod.month]}{' '}
{payrollPeriod.year}
</h2>
<p className="text-sm text-muted-foreground">
{payrollPeriod.payrolls.length} karyawan &middot;
Status:{' '}
{payrollPeriod.status === 'open'
? 'Terbuka'
: 'Ditutup'}
</p>
{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()}>