where('status', EmployeeAdvanceStatus::APPROVED) ->sum('amount'); $outstandingCount = EmployeeAdvance::query() ->where('status', EmployeeAdvanceStatus::APPROVED) ->count(); return [ 'outstanding_amount' => $outstandingAmount, 'outstanding_amount_formatted' => 'Rp '.number_format($outstandingAmount, 0, ',', '.'), 'outstanding_count' => $outstandingCount, ]; } /** * @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery */ public function paginateForIndex(array $tableQuery): LengthAwarePaginator { $query = EmployeeAdvance::query() ->with(['employee.user.profile', 'rejection', 'media']) ->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(10) ->withQueryString() ->through(function (EmployeeAdvance $employeeAdvance) { $employeeAdvance->setAttribute( 'photos', MediaPresenter::collection($employeeAdvance, 'photos'), ); return $employeeAdvance; }); } /** * @param array{amount: int, description: string, due_date: string} $validated */ public function create(array $validated, User $user): void { $employee = $this->resolveAuthEmployee($user); $employeeAdvance = DB::transaction(function () use ($validated, $employee) { $employeeAdvance = EmployeeAdvance::create([ 'employee_id' => $employee->id, 'amount' => (int) $validated['amount'], 'description' => $validated['description'], 'due_date' => $validated['due_date'], 'status' => EmployeeAdvanceStatus::PENDING, ]); $this->syncPhotos($employeeAdvance, $validated); return $employeeAdvance; }); $this->pushNotificationService->sendToRoles( '💰 Pengajuan Kasbon Baru', "Karyawan {$user->profil?->full_name} mengajukan kasbon sebesar {$employeeAdvance->amount_formatted} dengan keterangan: {$employeeAdvance->description}.", ['owner', 'developer'], '/admin/finance/employee-advances', ); } /** * @param array{amount: int, description: string, due_date: string} $validated */ public function update(EmployeeAdvance $employeeAdvance, array $validated, User $user): void { $this->ensureOwnedBySubmitter($employeeAdvance, $user); $this->ensurePending($employeeAdvance, 'Kasbon hanya dapat diubah saat status menunggu.'); DB::transaction(function () use ($employeeAdvance, $validated): void { $employeeAdvance->amount = (int) $validated['amount']; $employeeAdvance->description = $validated['description']; $employeeAdvance->due_date = $validated['due_date']; $employeeAdvance->save(); $this->syncPhotos($employeeAdvance, $validated); }); } public function delete(EmployeeAdvance $employeeAdvance, User $user): void { $this->ensureOwnedBySubmitter($employeeAdvance, $user); $this->ensurePending($employeeAdvance, 'Kasbon hanya dapat dihapus saat status menunggu.'); $employeeAdvance->clearMediaCollection('photos'); $employeeAdvance->delete(); } public function approve(EmployeeAdvance $employeeAdvance, User $user): void { $this->ensurePending($employeeAdvance, 'Kasbon ini sudah diverifikasi.'); DB::transaction(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->cash_transaction_id = $cashTransaction->id; $employeeAdvance->status = EmployeeAdvanceStatus::APPROVED; $employeeAdvance->verified_at = now(); $employeeAdvance->verified_by_id = $user->id; $employeeAdvance->save(); }); $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, '/admin/finance/employee-advances', ); } } public function reject(EmployeeAdvance $employeeAdvance, string $reason, User $user): void { $this->ensurePending($employeeAdvance, 'Kasbon ini sudah diverifikasi.'); DB::transaction(function () use ($employeeAdvance, $user, $reason): void { $employeeAdvance->status = EmployeeAdvanceStatus::REJECTED; $employeeAdvance->verified_at = now(); $employeeAdvance->verified_by_id = $user->id; $employeeAdvance->save(); $employeeAdvance->rejection()->create([ 'reason' => $reason, 'rejected_by_id' => $user->id, ]); }); $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, '/admin/finance/employee-advances', ); } } public function pay(EmployeeAdvance $employeeAdvance, User $user): void { if ($employeeAdvance->status !== EmployeeAdvanceStatus::APPROVED) { throw ValidationException::withMessages([ 'employee_advance' => 'Kasbon hanya dapat dilunasi setelah disetujui.', ]); } DB::transaction(function () use ($employeeAdvance, $user): void { $employeeAdvance->loadMissing('employee.user.profile'); $description = sprintf( 'Pelunasan kasbon: %s', $employeeAdvance->employeeName, ); $cashTransaction = $this->cashService->recordIncoming( $employeeAdvance, $employeeAdvance->amount, $description, $user, ); $employeeAdvance->repayment_cash_transaction_id = $cashTransaction->id; $employeeAdvance->paid_at = now(); $employeeAdvance->paid_by_id = $user->id; $employeeAdvance->status = EmployeeAdvanceStatus::PAID; $employeeAdvance->save(); }); } /** * @param array $validated */ private function syncPhotos(EmployeeAdvance $employeeAdvance, array $validated): void { $this->mediaService->syncCollection( $employeeAdvance, 'photos', $validated['photos'] ?? null, $validated['remove_media_ids'] ?? null, self::MAX_PHOTOS, required: true, errorKey: 'photos', ); } private function ensureOwnedBySubmitter(EmployeeAdvance $employeeAdvance, User $user): void { if ($user->employee?->id !== $employeeAdvance->employee_id) { throw ValidationException::withMessages([ 'employee_advance' => 'Anda tidak memiliki akses untuk mengubah kasbon ini.', ]); } } private function ensurePending(EmployeeAdvance $employeeAdvance, string $message): void { if ($employeeAdvance->status !== EmployeeAdvanceStatus::PENDING) { throw ValidationException::withMessages([ 'employee_advance' => $message, ]); } } 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(); } }