From d076df9d72cce2e2a5e46b23ddbd2793e1a534d0 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sun, 14 Jun 2026 14:38:38 +0700 Subject: [PATCH] feat: implement CRUD functionality for payroll adjustments including update and delete operations --- app/Enums/Permission.php | 7 +- app/Enums/Role.php | 4 - .../Admin/Finance/PayrollController.php | 42 ++-- .../Admin/Finance/PayrollPeriodController.php | 13 -- app/Models/Payroll.php | 10 - app/Services/Finance/PayrollService.php | 164 +++++++++------ .../payroll/PayrollAdjustmentModal.vue | 190 +++++++++++------ .../finance/payroll/data-table-actions.vue | 40 +--- .../js/pages/admin/finance/payroll/Index.vue | 192 ++++++++++-------- resources/js/types/payroll.ts | 22 +- routes/web.php | 16 +- 11 files changed, 405 insertions(+), 295 deletions(-) diff --git a/app/Enums/Permission.php b/app/Enums/Permission.php index 769a215..e838ee2 100644 --- a/app/Enums/Permission.php +++ b/app/Enums/Permission.php @@ -94,9 +94,7 @@ enum Permission: string case EMPLOYEE_ADVANCES_PAY = 'employee-advances.pay'; case PAYROLL_VIEW = 'payroll.view'; - case PAYROLL_PAY = 'payroll.pay'; case PAYROLL_ADJUST = 'payroll.adjust'; - case PAYROLL_CLOSE = 'payroll.close'; case SETTINGS_VIEW = 'settings.view'; case SETTINGS_UPDATE = 'settings.update'; @@ -197,9 +195,7 @@ public function label(): string self::EMPLOYEE_ADVANCES_PAY => 'Pelunasi Kasbon', self::PAYROLL_VIEW => 'Lihat Gaji', - self::PAYROLL_PAY => 'Bayar Gaji', self::PAYROLL_ADJUST => 'Sesuaikan Gaji', - self::PAYROLL_CLOSE => 'Tutup Periode Gaji', self::SETTINGS_VIEW => 'Lihat Pengaturan Aplikasi', self::SETTINGS_UPDATE => 'Ubah Pengaturan Aplikasi', @@ -247,8 +243,7 @@ public function group(): string self::EMPLOYEE_ADVANCES_VIEW, self::EMPLOYEE_ADVANCES_CREATE, self::EMPLOYEE_ADVANCES_UPDATE, self::EMPLOYEE_ADVANCES_DELETE, self::EMPLOYEE_ADVANCES_VERIFY, self::EMPLOYEE_ADVANCES_PAY => 'Kasbon', - self::PAYROLL_VIEW, self::PAYROLL_PAY, self::PAYROLL_ADJUST, - self::PAYROLL_CLOSE => 'Gaji', + self::PAYROLL_VIEW, self::PAYROLL_ADJUST => 'Gaji', self::SETTINGS_VIEW, self::SETTINGS_UPDATE => 'Pengaturan Aplikasi', self::ACTIVITY_LOGS_VIEW => 'Log Aktivitas', self::ROLES_VIEW, self::ROLES_CREATE, self::ROLES_UPDATE, diff --git a/app/Enums/Role.php b/app/Enums/Role.php index de1fe15..2d1e25c 100644 --- a/app/Enums/Role.php +++ b/app/Enums/Role.php @@ -142,9 +142,7 @@ public function permissions(): array Permission::EMPLOYEE_ADVANCES_PAY, Permission::PAYROLL_VIEW, - Permission::PAYROLL_PAY, Permission::PAYROLL_ADJUST, - Permission::PAYROLL_CLOSE, ], self::ADMIN_BAHAN_BAKU => [ @@ -208,9 +206,7 @@ public function permissions(): array Permission::EMPLOYEE_ADVANCES_PAY, Permission::PAYROLL_VIEW, - Permission::PAYROLL_PAY, Permission::PAYROLL_ADJUST, - Permission::PAYROLL_CLOSE, ], self::MARKETING => [ diff --git a/app/Http/Controllers/Admin/Finance/PayrollController.php b/app/Http/Controllers/Admin/Finance/PayrollController.php index 1aa93a9..78e8440 100644 --- a/app/Http/Controllers/Admin/Finance/PayrollController.php +++ b/app/Http/Controllers/Admin/Finance/PayrollController.php @@ -6,6 +6,7 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Admin\Finance\PayrollAdjustmentRequest; use App\Models\Payroll; +use App\Models\PayrollAdjustment; use App\Services\Finance\PayrollService; use Illuminate\Http\RedirectResponse; @@ -17,23 +18,12 @@ public function __construct( private readonly PayrollService $payrollService, ) {} - public function pay(Payroll $payroll): RedirectResponse - { - $this->payrollService->pay($payroll, auth()->user()); - - $this->flashSuccess('Gaji berhasil dibayar.'); - - return redirect()->route('admin.finance.payroll.index', [ - 'period_id' => $payroll->payroll_period_id, - ]); - } - public function storeAdjustment(PayrollAdjustmentRequest $request, Payroll $payroll): RedirectResponse { $this->payrollService->addAdjustment( $payroll, $request->validated(), - auth()->user(), + $request->user(), ); $this->flashSuccess('Penyesuaian gaji berhasil ditambahkan.'); @@ -42,4 +32,32 @@ public function storeAdjustment(PayrollAdjustmentRequest $request, Payroll $payr 'period_id' => $payroll->payroll_period_id, ]); } + + public function updateAdjustment(PayrollAdjustmentRequest $request, PayrollAdjustment $payrollAdjustment): RedirectResponse + { + $this->payrollService->updateAdjustment( + $payrollAdjustment, + $request->validated(), + $request->user(), + ); + + $this->flashSuccess('Penyesuaian gaji berhasil diperbarui.'); + + return redirect()->route('admin.finance.payroll.index', [ + 'period_id' => $payrollAdjustment->payroll->payroll_period_id, + ]); + } + + public function destroyAdjustment(PayrollAdjustment $payrollAdjustment): RedirectResponse + { + $payroll = $payrollAdjustment->payroll; + + $this->payrollService->deleteAdjustment($payrollAdjustment); + + $this->flashSuccess('Penyesuaian gaji berhasil dihapus.'); + + return redirect()->route('admin.finance.payroll.index', [ + 'period_id' => $payroll->payroll_period_id, + ]); + } } diff --git a/app/Http/Controllers/Admin/Finance/PayrollPeriodController.php b/app/Http/Controllers/Admin/Finance/PayrollPeriodController.php index c2b22ae..274bb37 100644 --- a/app/Http/Controllers/Admin/Finance/PayrollPeriodController.php +++ b/app/Http/Controllers/Admin/Finance/PayrollPeriodController.php @@ -6,9 +6,7 @@ use App\Http\Controllers\Concerns\FlashesEntityMessage; use App\Http\Controllers\Concerns\ParsesDataTableQuery; use App\Http\Controllers\Controller; -use App\Models\PayrollPeriod; use App\Services\Finance\PayrollService; -use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Inertia\Inertia; use Inertia\Response; @@ -44,15 +42,4 @@ public function index(Request $request): Response ), ]); } - - public function close(PayrollPeriod $payrollPeriod): RedirectResponse - { - $this->payrollService->closePeriod($payrollPeriod, auth()->user()); - - $this->flashSuccess('Periode gaji berhasil ditutup.'); - - return redirect()->route('admin.finance.payroll.index', [ - 'period_id' => $payrollPeriod->id, - ]); - } } diff --git a/app/Models/Payroll.php b/app/Models/Payroll.php index cc72dfd..cf96ce3 100644 --- a/app/Models/Payroll.php +++ b/app/Models/Payroll.php @@ -23,7 +23,6 @@ 'status_label', 'employee_name', 'paid_at_formatted', - 'can_pay', 'can_adjust', ])] class Payroll extends Model @@ -91,15 +90,6 @@ public function canAdjust(): Attribute ); } - public function canPay(): Attribute - { - return Attribute::make( - get: fn () => $this->status === PayrollStatus::UNPAID - && $this->relationLoaded('payrollPeriod') - && $this->payrollPeriod?->isOpen(), - ); - } - public function deductionAmountFormatted(): Attribute { return Attribute::make( diff --git a/app/Services/Finance/PayrollService.php b/app/Services/Finance/PayrollService.php index 9949f22..4c4cee0 100644 --- a/app/Services/Finance/PayrollService.php +++ b/app/Services/Finance/PayrollService.php @@ -10,6 +10,7 @@ use App\Models\Employee; use App\Models\EmployeeAdvance; use App\Models\Payroll; +use App\Models\PayrollAdjustment; use App\Models\PayrollPeriod; use App\Models\User; use App\Services\System\PushNotificationService; @@ -56,30 +57,24 @@ public function resolvePeriod(?int $periodId): ?PayrollPeriod } /** - * @return array{total_total_amount: int, total_total_amount_formatted: string, unpaid_count: int, paid_count: int} + * @return array{total_amount: int, total_amount_formatted: string, total_count: int, status: string, status_label: string} */ public function periodSummary(PayrollPeriod $period): array { - $totalTotalAmount = (int) Payroll::query() + $totalAmount = (int) Payroll::query() ->where('payroll_period_id', $period->id) - ->where('status', PayrollStatus::UNPAID) ->sum('total_amount'); - $unpaidCount = Payroll::query() + $totalCount = Payroll::query() ->where('payroll_period_id', $period->id) - ->where('status', PayrollStatus::UNPAID) - ->count(); - - $paidCount = Payroll::query() - ->where('payroll_period_id', $period->id) - ->where('status', PayrollStatus::PAID) ->count(); return [ - 'total_total_amount' => $totalTotalAmount, - 'total_total_amount_formatted' => 'Rp '.number_format($totalTotalAmount, 0, ',', '.'), - 'unpaid_count' => $unpaidCount, - 'paid_count' => $paidCount, + 'total_amount' => $totalAmount, + 'total_amount_formatted' => 'Rp '.number_format($totalAmount, 0, ',', '.'), + 'total_count' => $totalCount, + 'status' => $period->status->value, + 'status_label' => $period->status->label(), ]; } @@ -89,7 +84,7 @@ public function periodSummary(PayrollPeriod $period): array public function paginateForPeriod(PayrollPeriod $period, array $tableQuery): LengthAwarePaginator { $query = Payroll::query() - ->with(['employee.user.profile', 'payrollPeriod']) + ->with(['employee.user.profile', 'payrollPeriod', 'adjustments.createdBy.profile']) ->where('payroll_period_id', $period->id) ->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void { $search = $tableQuery['search']; @@ -108,18 +103,37 @@ public function paginateForPeriod(PayrollPeriod $period, array $tableQuery): Len public function openCurrentPeriod(?User $closedBy = null): PayrollPeriod { - $period = DB::transaction(function () use ($closedBy): PayrollPeriod { + $user = $closedBy + ?? auth()->user() + ?? User::query()->whereHas('roles', fn ($q) => $q->whereIn('name', [Role::DEVELOPER->value, Role::OWNER->value]))->first() + ?? User::query()->first(); + + $period = DB::transaction(function () use ($user): PayrollPeriod { $now = now(); $year = $now->year; $month = $now->month; - PayrollPeriod::query() + $openPeriods = PayrollPeriod::query() ->where('status', PayrollPeriodStatus::OPEN) - ->update([ - 'status' => PayrollPeriodStatus::CLOSED, - 'closed_at' => now(), - 'closed_by_id' => $closedBy?->id, - ]); + ->get(); + + foreach ($openPeriods as $oldPeriod) { + $unpaidPayrolls = Payroll::query() + ->where('payroll_period_id', $oldPeriod->id) + ->where('status', PayrollStatus::UNPAID) + ->get(); + + foreach ($unpaidPayrolls as $payroll) { + if ($user) { + $this->pay($payroll, $user); + } + } + + $oldPeriod->status = PayrollPeriodStatus::CLOSED; + $oldPeriod->closed_at = now(); + $oldPeriod->closed_by_id = $user?->id; + $oldPeriod->save(); + } $period = PayrollPeriod::query() ->where('year', $year) @@ -144,47 +158,9 @@ public function openCurrentPeriod(?User $closedBy = null): PayrollPeriod return $period->fresh(); }); - $this->pushNotificationService->sendToAll( - '📊 Periode Gaji Baru Dibuka', - "Periode gaji untuk {$period->period_label} telah dibuka.", - '/admin/finance/payrolls', - ); - return $period; } - public function closePeriod(PayrollPeriod $period, User $user): void - { - if (! $period->isOpen()) { - throw ValidationException::withMessages([ - 'payroll_period' => 'Periode gaji ini sudah ditutup.', - ]); - } - - $unpaidCount = Payroll::query() - ->where('payroll_period_id', $period->id) - ->where('status', PayrollStatus::UNPAID) - ->where('total_amount', '>', 0) - ->count(); - - if ($unpaidCount > 0) { - throw ValidationException::withMessages([ - 'payroll_period' => 'Masih ada gaji yang belum dibayar.', - ]); - } - - $period->status = PayrollPeriodStatus::CLOSED; - $period->closed_at = now(); - $period->closed_by_id = $user->id; - $period->save(); - - $this->pushNotificationService->sendToAll( - '📊 Periode Gaji Ditutup', - "Periode gaji untuk {$period->period_label} telah ditutup.", - '/admin/finance/payrolls', - ); - } - public function generatePayrollsForPeriod(PayrollPeriod $period): void { $employees = $this->payrollEligibleEmployees($period); @@ -220,7 +196,7 @@ public function generatePayrollsForPeriod(PayrollPeriod $period): void */ public function addAdjustment(Payroll $payroll, array $validated, User $user): void { - $payroll->loadMissing('payrollPeriod'); + $payroll->loadMissing(['payrollPeriod', 'employee.user']); if (! $payroll->can_adjust) { throw ValidationException::withMessages([ @@ -240,6 +216,72 @@ public function addAdjustment(Payroll $payroll, array $validated, User $user): v $payroll->recalculateAmounts(); $payroll->save(); }); + + if ($payroll->employee?->user_id) { + $typeLabel = PayrollAdjustmentType::from($validated['type'])->label(); + $formattedAmount = 'Rp '.number_format($validated['amount'], 0, ',', '.'); + $this->pushNotificationService->sendToUser( + "📊 Penyesuaian Gaji: {$typeLabel}", + "Gaji periode {$payroll->payrollPeriod->period_label} disesuaikan: {$typeLabel} sebesar {$formattedAmount} ({$validated['description']}).", + $payroll->employee->user_id, + '/admin/finance/payrolls', + ); + } + } + + public function updateAdjustment(PayrollAdjustment $adjustment, array $validated, User $user): void + { + $payroll = $adjustment->payroll; + $payroll->loadMissing(['payrollPeriod', 'employee.user']); + + if (! $payroll->can_adjust) { + throw ValidationException::withMessages([ + 'payroll' => 'Penyesuaian hanya dapat diubah pada gaji yang belum dibayar di periode terbuka.', + ]); + } + + DB::transaction(function () use ($payroll, $adjustment, $validated): void { + $adjustment->update([ + 'type' => PayrollAdjustmentType::from($validated['type']), + 'amount' => (int) $validated['amount'], + 'description' => $validated['description'], + ]); + + $payroll->load('adjustments'); + $payroll->recalculateAmounts(); + $payroll->save(); + }); + + if ($payroll->employee?->user_id) { + $typeLabel = PayrollAdjustmentType::from($validated['type'])->label(); + $formattedAmount = 'Rp '.number_format($validated['amount'], 0, ',', '.'); + $this->pushNotificationService->sendToUser( + "📊 Penyesuaian Gaji Diperbarui: {$typeLabel}", + "Penyesuaian gaji Anda untuk periode {$payroll->payrollPeriod->period_label} diperbarui: {$typeLabel} menjadi {$formattedAmount} ({$validated['description']}).", + $payroll->employee->user_id, + '/admin/finance/payrolls', + ); + } + } + + public function deleteAdjustment(PayrollAdjustment $adjustment): void + { + $payroll = $adjustment->payroll; + $payroll->loadMissing(['payrollPeriod', 'employee.user']); + + if (! $payroll->can_adjust) { + throw ValidationException::withMessages([ + 'payroll' => 'Penyesuaian hanya dapat dihapus pada gaji yang belum dibayar di periode terbuka.', + ]); + } + + DB::transaction(function () use ($payroll, $adjustment): void { + $adjustment->delete(); + + $payroll->load('adjustments'); + $payroll->recalculateAmounts(); + $payroll->save(); + }); } public function pay(Payroll $payroll, User $user): void diff --git a/resources/js/components/admin/finance/payroll/PayrollAdjustmentModal.vue b/resources/js/components/admin/finance/payroll/PayrollAdjustmentModal.vue index ecea3bb..b69d8bd 100644 --- a/resources/js/components/admin/finance/payroll/PayrollAdjustmentModal.vue +++ b/resources/js/components/admin/finance/payroll/PayrollAdjustmentModal.vue @@ -1,14 +1,13 @@