whereIn('status', [EmployeeAdvanceStatus::APPROVED, EmployeeAdvanceStatus::PARTIALLY_PAID]) ->when(! $user->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]), fn (Builder $query) => $query->where('employee_id', $user->employee?->id ?? -1)); $outstandingAmount = (int) $query->selectRaw('SUM(amount - paid_amount) as remaining')->value('remaining'); $outstandingCount = $query->count(); return [ 'outstanding_amount' => $outstandingAmount, 'outstanding_amount_formatted' => 'Rp '.number_format($outstandingAmount, 0, ',', '.'), 'outstanding_count' => $outstandingCount, ]; } public function paginateForIndex(array $tableQuery, User $user, string $status = ''): LengthAwarePaginator { $query = EmployeeAdvance::query() ->with(['employee.user.profile', 'rejection', 'payments']) ->when(! $user->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]), function (Builder $query) use ($user): void { $employeeId = $user->employee?->id ?? -1; $query->where('employee_id', $employeeId); }) ->when($status !== '', fn (Builder $query) => $query->where('status', $status)) ->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void { $search = $tableQuery['search']; $query->where(function (Builder $query) use ($search): void { $query->where('description', 'like', "%{$search}%") ->orWhereHas('employee.user.profile', fn (Builder $query) => $query->where('full_name', 'like', "%{$search}%")) ->orWhereHas('employee.user', fn (Builder $query) => $query->where('username', 'like', "%{$search}%")) ->orWhereHas('rejection', fn (Builder $query) => $query->where('reason', 'like', "%{$search}%")); }); }); $this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']); return $query ->paginate(25) ->withQueryString(); } public function create(array $validated, User $user): void { $employee = $this->resolveAuthenticatedEmployee($user); $employeeAdvance = $this->runInTransaction( function () use ($validated, $employee): EmployeeAdvance { return EmployeeAdvance::create([ 'employee_id' => $employee->id, 'amount' => (int) $validated['amount'], 'description' => $validated['description'], 'due_date' => $validated['due_date'], 'status' => EmployeeAdvanceStatus::PENDING, ]); }, 'Gagal membuat pengajuan kasbon', ); $this->pushNotificationService->sendToRoles( '💰 Pengajuan Kasbon Baru', "Karyawan {$user->profile?->full_name} mengajukan kasbon sebesar {$employeeAdvance->amount_formatted} dengan keterangan: {$employeeAdvance->description}.", ['owner', 'developer', 'direktur'], route('admin.finance.employee_advances.index'), ); } public function update(EmployeeAdvance $employeeAdvance, array $validated, User $user): void { $this->runInTransaction( function () use ($employeeAdvance, $validated): void { $employeeAdvance->update([ 'amount' => (int) $validated['amount'], 'description' => $validated['description'], 'due_date' => $validated['due_date'], ]); }, 'Gagal memperbarui kasbon', ); $this->pushNotificationService->sendToRoles( '✏️ Kasbon Diperbarui', "Kasbon sebesar {$employeeAdvance->amount_formatted} telah diperbarui oleh {$user->profile?->full_name}.", ['owner', 'developer', 'direktur'], route('admin.finance.employee_advances.index'), ); } public function delete(EmployeeAdvance $employeeAdvance, User $user): void { $amountFormatted = $employeeAdvance->amount_formatted; $description = $employeeAdvance->description; $this->runInTransaction( function () use ($employeeAdvance): void { $employeeAdvance->delete(); }, 'Gagal menghapus kasbon', ); $this->pushNotificationService->sendToRoles( '🗑️ Kasbon Dihapus', "Kasbon sebesar {$amountFormatted} dengan keterangan {$description} telah dihapus.", ['owner', 'developer', 'direktur'], route('admin.finance.employee_advances.index'), ); } public function approve(EmployeeAdvance $employeeAdvance, User $user): void { $this->runInTransaction( function () use ($employeeAdvance, $user): void { $employeeAdvance->loadMissing('employee.user.profile'); $description = sprintf( 'Pencairan kasbon: %s', $employeeAdvance->employeeName, ); $cashTransaction = $this->cashService->recordOutgoing( $employeeAdvance, $employeeAdvance->amount, $description, $user, ); $employeeAdvance->update([ 'cash_transaction_id' => $cashTransaction->id, 'status' => EmployeeAdvanceStatus::APPROVED, 'verified_at' => now(), 'verified_by_id' => $user->id, ]); }, 'Gagal menyetujui kasbon', ); $employeeAdvance->loadMissing('employee.user'); if ($employeeAdvance->employee?->user_id) { $this->pushNotificationService->sendToUser( '💰 Kasbon Disetujui', "Pengajuan kasbon Anda sebesar {$employeeAdvance->amount_formatted} telah disetujui.", $employeeAdvance->employee->user_id, route('admin.finance.employee_advances.index'), ); } } public function reject(EmployeeAdvance $employeeAdvance, string $reason, User $user): void { $this->runInTransaction( function () use ($employeeAdvance, $user, $reason): void { $employeeAdvance->update([ 'status' => EmployeeAdvanceStatus::REJECTED, 'verified_at' => now(), 'verified_by_id' => $user->id, ]); $employeeAdvance->rejection()->create([ 'reason' => $reason, 'rejected_by_id' => $user->id, ]); }, 'Gagal menolak kasbon', ); $employeeAdvance->loadMissing('employee.user'); if ($employeeAdvance->employee?->user_id) { $this->pushNotificationService->sendToUser( '💰 Kasbon Ditolak', "Pengajuan kasbon Anda sebesar {$employeeAdvance->amount_formatted} ditolak dengan alasan: {$reason}.", $employeeAdvance->employee->user_id, route('admin.finance.employee_advances.index'), ); } } public function pay(EmployeeAdvance $employeeAdvance, User $user, ?int $payAmount = null): void { $this->runInTransaction( function () use ($employeeAdvance, $user, $payAmount): void { $employeeAdvance->loadMissing('employee.user.profile'); $remaining = $employeeAdvance->amount - $employeeAdvance->paid_amount; $amountToPay = $payAmount !== null ? min($payAmount, $remaining) : $remaining; $isFullPayment = ($employeeAdvance->paid_amount + $amountToPay) >= $employeeAdvance->amount; $description = sprintf( '%s kasbon: %s', $isFullPayment ? 'Pelunasan' : 'Pembayaran sebagian kasbon', $employeeAdvance->employeeName, ); $cashTransaction = $this->cashService->recordIncoming( $employeeAdvance, $amountToPay, $description, $user, ); $newPaidAmount = $employeeAdvance->paid_amount + $amountToPay; $employeeAdvance->update([ 'paid_amount' => $newPaidAmount, 'repayment_cash_transaction_id' => $isFullPayment ? $cashTransaction->id : $employeeAdvance->repayment_cash_transaction_id, 'paid_at' => $isFullPayment ? now() : $employeeAdvance->paid_at, 'paid_by_id' => $isFullPayment ? $user->id : $employeeAdvance->paid_by_id, 'status' => $isFullPayment ? EmployeeAdvanceStatus::PAID : EmployeeAdvanceStatus::PARTIALLY_PAID, ]); EmployeeAdvancePayment::create([ 'employee_advance_id' => $employeeAdvance->id, 'paid_by_id' => $user->id, 'cash_transaction_id' => $cashTransaction->id, 'amount' => $amountToPay, 'description' => $description, 'paid_at' => now(), ]); }, 'Gagal membayar kasbon', ); } public function indexPageData(array $tableQuery, User $user, string $status = ''): array { return [ 'employeeAdvances' => $this->paginateForIndex($tableQuery, $user, $status), 'summary' => $this->outstandingSummary($user), 'authEmployeeId' => $user->employee?->id, 'canSubmit' => EmployeeAdvance::canSubmit($user), ]; } private function applySorting(Builder $query, string $sort, string $direction): void { if (in_array($sort, ['created_at', 'amount', 'due_date', 'status', 'description'], true)) { $query->orderBy($sort, $direction); return; } $query->latest(); } }