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(); $action();
Inertia::flash('toast', ['type' => 'success', 'message' => $successMessage]); Inertia::flash('toast', ['type' => 'success', 'message' => $successMessage]);
return to_route($redirectRoute); return to_route($redirectRoute, $parameters);
} catch (ValidationException $e) { } catch (ValidationException $e) {
$firstError = collect($e->errors())->flatten()->first(); $firstError = collect($e->errors())->flatten()->first();
Inertia::flash('toast', ['type' => 'error', 'message' => $firstError ?? 'Terjadi kesalahan.']); Inertia::flash('toast', ['type' => 'error', 'message' => $firstError ?? 'Terjadi kesalahan.']);

View File

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

View File

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

View File

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

View File

@ -40,7 +40,8 @@ type Props = {
}; };
export default function PayrollPeriodShow({ payrollPeriod }: 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 [paying, setPaying] = useState<Payroll | null>(null);
const [cancelling, setCancelling] = useState<Payroll | null>(null); const [cancelling, setCancelling] = useState<Payroll | null>(null);
const [addingAdjustment, setAddingAdjustment] = useState<Payroll | null>( const [addingAdjustment, setAddingAdjustment] = useState<Payroll | null>(
@ -103,19 +104,23 @@ export default function PayrollPeriodShow({ payrollPeriod }: Props) {
can, 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, (sum, p) => sum + p.base_salary,
0, 0,
); );
const totalBonus = payrollPeriod.payrolls.reduce( const totalBonus = activePayrolls.reduce(
(sum, p) => sum + p.bonus_amount, (sum, p) => sum + p.bonus_amount,
0, 0,
); );
const totalDeduction = payrollPeriod.payrolls.reduce( const totalDeduction = activePayrolls.reduce(
(sum, p) => sum + p.deduction_amount, (sum, p) => sum + p.deduction_amount,
0, 0,
); );
const totalAmount = payrollPeriod.payrolls.reduce( const totalAmount = activePayrolls.reduce(
(sum, p) => sum + p.total_amount, (sum, p) => sum + p.total_amount,
0, 0,
); );
@ -133,13 +138,15 @@ export default function PayrollPeriodShow({ payrollPeriod }: Props) {
Gaji {MONTH_NAMES[payrollPeriod.month]}{' '} Gaji {MONTH_NAMES[payrollPeriod.month]}{' '}
{payrollPeriod.year} {payrollPeriod.year}
</h2> </h2>
<p className="text-sm text-muted-foreground"> {canViewAll && (
{payrollPeriod.payrolls.length} karyawan &middot; <p className="text-sm text-muted-foreground">
Status:{' '} {payrollPeriod.payrolls.length} karyawan &middot;
{payrollPeriod.status === 'open' Status:{' '}
? 'Terbuka' {payrollPeriod.status === 'open'
: 'Ditutup'} ? 'Terbuka'
</p> : 'Ditutup'}
</p>
)}
</div> </div>
<Button asChild variant="outline"> <Button asChild variant="outline">
<Link href={payrollPeriodsIndex.url()}> <Link href={payrollPeriodsIndex.url()}>