From 39a74964fc5e2ce8e79a87f94f235453ca3496f8 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Thu, 11 Jun 2026 00:40:53 +0700 Subject: [PATCH] Add employee advance management features including EmployeeAdvance model, EmployeeAdvanceController for CRUD operations, and corresponding permissions in the Permission and Role enums. Implement request validation for employee advances and enhance UI components for employee advance listing and management, including data table and form modal. Update sidebar for employee advances navigation. --- app/Enums/EmployeeAdvanceStatus.php | 25 ++ app/Enums/Permission.php | 17 ++ app/Enums/Role.php | 10 + .../Finance/EmployeeAdvanceController.php | 99 ++++++++ .../Admin/Finance/EmployeeAdvanceRequest.php | 47 ++++ .../Finance/RejectEmployeeAdvanceRequest.php | 24 ++ app/Models/CashTransaction.php | 28 ++- app/Models/Concerns/HasRejection.php | 14 ++ app/Models/Employee.php | 6 + app/Models/EmployeeAdvance.php | 136 +++++++++++ app/Models/Rejection.php | 22 ++ app/Services/Finance/CashService.php | 3 +- .../Finance/EmployeeAdvanceService.php | 215 ++++++++++++++++++ ..._160001_create_employee_advances_table.php | 37 +++ ...6_06_10_170001_create_rejections_table.php | 26 +++ resources/js/components/AppSidebar.vue | 15 +- .../EmployeeAdvanceFormModal.vue | 138 +++++++++++ .../RejectEmployeeAdvanceModal.vue | 93 ++++++++ .../finance/employee-advances/columns.ts | 80 +++++++ .../employee-advances/data-table-actions.vue | 169 ++++++++++++++ .../admin/finance/employee-advances/Index.vue | 133 +++++++++++ resources/js/types/employee-advance.ts | 54 +++++ routes/web.php | 33 ++- 23 files changed, 1418 insertions(+), 6 deletions(-) create mode 100644 app/Enums/EmployeeAdvanceStatus.php create mode 100644 app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php create mode 100644 app/Http/Requests/Admin/Finance/EmployeeAdvanceRequest.php create mode 100644 app/Http/Requests/Admin/Finance/RejectEmployeeAdvanceRequest.php create mode 100644 app/Models/Concerns/HasRejection.php create mode 100644 app/Models/EmployeeAdvance.php create mode 100644 app/Models/Rejection.php create mode 100644 app/Services/Finance/EmployeeAdvanceService.php create mode 100644 database/migrations/2026_06_10_160001_create_employee_advances_table.php create mode 100644 database/migrations/2026_06_10_170001_create_rejections_table.php create mode 100644 resources/js/components/admin/finance/employee-advances/EmployeeAdvanceFormModal.vue create mode 100644 resources/js/components/admin/finance/employee-advances/RejectEmployeeAdvanceModal.vue create mode 100644 resources/js/components/admin/finance/employee-advances/columns.ts create mode 100644 resources/js/components/admin/finance/employee-advances/data-table-actions.vue create mode 100644 resources/js/pages/admin/finance/employee-advances/Index.vue create mode 100644 resources/js/types/employee-advance.ts diff --git a/app/Enums/EmployeeAdvanceStatus.php b/app/Enums/EmployeeAdvanceStatus.php new file mode 100644 index 0000000..2233737 --- /dev/null +++ b/app/Enums/EmployeeAdvanceStatus.php @@ -0,0 +1,25 @@ + 'Menunggu', + self::APPROVED => 'Disetujui', + self::REJECTED => 'Ditolak', + self::PAID => 'Lunas', + }; + } +} diff --git a/app/Enums/Permission.php b/app/Enums/Permission.php index d12c1ba..9048ff0 100644 --- a/app/Enums/Permission.php +++ b/app/Enums/Permission.php @@ -54,6 +54,13 @@ enum Permission: string case EXPENSES_UPDATE = 'expenses.update'; case EXPENSES_DELETE = 'expenses.delete'; + case EMPLOYEE_ADVANCES_VIEW = 'employee-advances.view'; + case EMPLOYEE_ADVANCES_CREATE = 'employee-advances.create'; + case EMPLOYEE_ADVANCES_UPDATE = 'employee-advances.update'; + case EMPLOYEE_ADVANCES_DELETE = 'employee-advances.delete'; + case EMPLOYEE_ADVANCES_VERIFY = 'employee-advances.verify'; + case EMPLOYEE_ADVANCES_PAY = 'employee-advances.pay'; + public function label(): string { return match ($this) { @@ -102,6 +109,13 @@ public function label(): string self::EXPENSES_CREATE => 'Tambah Pengeluaran', self::EXPENSES_UPDATE => 'Ubah Pengeluaran', self::EXPENSES_DELETE => 'Hapus Pengeluaran', + + self::EMPLOYEE_ADVANCES_VIEW => 'Lihat Kasbon', + self::EMPLOYEE_ADVANCES_CREATE => 'Ajukan Kasbon', + self::EMPLOYEE_ADVANCES_UPDATE => 'Ubah Kasbon', + self::EMPLOYEE_ADVANCES_DELETE => 'Hapus Kasbon', + self::EMPLOYEE_ADVANCES_VERIFY => 'Setujui/Tolak Kasbon', + self::EMPLOYEE_ADVANCES_PAY => 'Pelunasi Kasbon', }; } @@ -124,6 +138,9 @@ public function group(): string self::CASH_VIEW, self::CASH_DEPOSIT, self::CASH_UPDATE, self::CASH_DELETE => 'Kas', self::EXPENSES_VIEW, self::EXPENSES_CREATE, self::EXPENSES_UPDATE, self::EXPENSES_DELETE => 'Pengeluaran', + 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', }; } diff --git a/app/Enums/Role.php b/app/Enums/Role.php index 74e79d6..4c3e46d 100644 --- a/app/Enums/Role.php +++ b/app/Enums/Role.php @@ -74,6 +74,11 @@ public function permissions(): array Permission::EXPENSES_CREATE, Permission::EXPENSES_UPDATE, Permission::EXPENSES_DELETE, + Permission::EMPLOYEE_ADVANCES_VIEW, + Permission::EMPLOYEE_ADVANCES_CREATE, + Permission::EMPLOYEE_ADVANCES_UPDATE, + Permission::EMPLOYEE_ADVANCES_DELETE, + Permission::EMPLOYEE_ADVANCES_PAY, ], self::ADMIN_TOKO => [ Permission::DASHBOARD_VIEW, @@ -107,6 +112,11 @@ public function permissions(): array Permission::EXPENSES_CREATE, Permission::EXPENSES_UPDATE, Permission::EXPENSES_DELETE, + Permission::EMPLOYEE_ADVANCES_VIEW, + Permission::EMPLOYEE_ADVANCES_CREATE, + Permission::EMPLOYEE_ADVANCES_UPDATE, + Permission::EMPLOYEE_ADVANCES_DELETE, + Permission::EMPLOYEE_ADVANCES_PAY, ], self::ADMIN_BAHAN_BAKU => [ Permission::DASHBOARD_VIEW, diff --git a/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php b/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php new file mode 100644 index 0000000..88cffe6 --- /dev/null +++ b/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php @@ -0,0 +1,99 @@ +parseDataTableQuery($request); + $user = auth()->user(); + + return Inertia::render('admin/finance/employee-advances/Index', [ + 'employeeAdvances' => $this->employeeAdvanceService->paginateForIndex($tableQuery), + 'summary' => $this->employeeAdvanceService->outstandingSummary(), + 'authEmployeeId' => $user?->employee?->id, + 'canSubmit' => $user?->can(Permission::EMPLOYEE_ADVANCES_CREATE->value) + && ! $user->can(Permission::EMPLOYEE_ADVANCES_VERIFY->value) + && $user->employee !== null, + 'filters' => $this->dataTableFilters($tableQuery), + ]); + } + + public function store(EmployeeAdvanceRequest $request): RedirectResponse + { + $this->employeeAdvanceService->create($request->validated()); + + Inertia::flash('success', 'Kasbon berhasil diajukan.'); + + return redirect()->route('admin.finance.employee-advances.index'); + } + + public function update(EmployeeAdvanceRequest $request, EmployeeAdvance $employeeAdvance): RedirectResponse + { + $this->employeeAdvanceService->update($employeeAdvance, $request->validated()); + + Inertia::flash('success', 'Kasbon berhasil diperbarui.'); + + return redirect()->route('admin.finance.employee-advances.index'); + } + + public function destroy(EmployeeAdvance $employeeAdvance): RedirectResponse + { + abort_unless(! auth()->user()?->can(Permission::EMPLOYEE_ADVANCES_VERIFY->value), 403); + + $this->employeeAdvanceService->delete($employeeAdvance); + + Inertia::flash('success', 'Kasbon berhasil dihapus.'); + + return redirect()->route('admin.finance.employee-advances.index'); + } + + public function approve(EmployeeAdvance $employeeAdvance): RedirectResponse + { + $this->employeeAdvanceService->approve($employeeAdvance); + + Inertia::flash('success', 'Kasbon disetujui dan dicairkan dari kas.'); + + return redirect()->route('admin.finance.employee-advances.index'); + } + + public function reject(RejectEmployeeAdvanceRequest $request, EmployeeAdvance $employeeAdvance): RedirectResponse + { + $this->employeeAdvanceService->reject( + $employeeAdvance, + $request->validated('reason'), + ); + + Inertia::flash('success', 'Kasbon berhasil ditolak.'); + + return redirect()->route('admin.finance.employee-advances.index'); + } + + public function pay(EmployeeAdvance $employeeAdvance): RedirectResponse + { + $this->employeeAdvanceService->pay($employeeAdvance); + + Inertia::flash('success', 'Kasbon berhasil dilunasi.'); + + return redirect()->route('admin.finance.employee-advances.index'); + } +} diff --git a/app/Http/Requests/Admin/Finance/EmployeeAdvanceRequest.php b/app/Http/Requests/Admin/Finance/EmployeeAdvanceRequest.php new file mode 100644 index 0000000..000166e --- /dev/null +++ b/app/Http/Requests/Admin/Finance/EmployeeAdvanceRequest.php @@ -0,0 +1,47 @@ +user(); + + if ($user === null || $user->can(Permission::EMPLOYEE_ADVANCES_VERIFY->value)) { + return false; + } + + if ($this->isMethod('POST')) { + return $user->can(Permission::EMPLOYEE_ADVANCES_CREATE->value) + && $user->employee !== null; + } + + if (! $user->can(Permission::EMPLOYEE_ADVANCES_UPDATE->value)) { + return false; + } + + /** @var EmployeeAdvance|null $employeeAdvance */ + $employeeAdvance = $this->route('employeeAdvance'); + + return $employeeAdvance?->status === EmployeeAdvanceStatus::PENDING + && $user->employee?->id === $employeeAdvance->employee_id; + } + + /** + * @return array + */ + public function rules(): array + { + return [ + 'amount' => ['required', 'integer', 'min:1'], + 'description' => ['required', 'string', 'max:500'], + 'due_date' => ['required', 'date'], + ]; + } +} diff --git a/app/Http/Requests/Admin/Finance/RejectEmployeeAdvanceRequest.php b/app/Http/Requests/Admin/Finance/RejectEmployeeAdvanceRequest.php new file mode 100644 index 0000000..b67e1b1 --- /dev/null +++ b/app/Http/Requests/Admin/Finance/RejectEmployeeAdvanceRequest.php @@ -0,0 +1,24 @@ +user()?->can(Permission::EMPLOYEE_ADVANCES_VERIFY->value) ?? false; + } + + /** + * @return array + */ + public function rules(): array + { + return [ + 'reason' => ['required', 'string', 'max:500'], + ]; + } +} diff --git a/app/Models/CashTransaction.php b/app/Models/CashTransaction.php index 73184b0..44c719a 100644 --- a/app/Models/CashTransaction.php +++ b/app/Models/CashTransaction.php @@ -48,14 +48,28 @@ public function balanceAfterFormatted(): Attribute public function referenceLabel(): Attribute { return Attribute::make( - get: fn () => self::labelForReferenceType($this->reference_type), + get: function () { + if ($this->reference_type === EmployeeAdvance::class && $this->reference instanceof EmployeeAdvance) { + return $this->reference->repayment_cash_transaction_id === $this->id + ? 'Pelunasan Kasbon' + : 'Pencairan Kasbon'; + } + + return self::labelForReferenceType($this->reference_type); + }, ); } public function isIncoming(): Attribute { return Attribute::make( - get: fn () => self::isIncomingReference($this->reference_type), + get: function () { + if ($this->reference_type === EmployeeAdvance::class && $this->reference instanceof EmployeeAdvance) { + return $this->reference->repayment_cash_transaction_id === $this->id; + } + + return self::isIncomingTransaction($this); + }, ); } @@ -81,6 +95,7 @@ public static function labelForReferenceType(?string $referenceType): string return match ($referenceType) { Expense::class => 'Pengeluaran', + EmployeeAdvance::class => 'Kasbon Pegawai', default => class_basename($referenceType), }; } @@ -96,6 +111,15 @@ public static function isIncomingReference(?string $referenceType): bool }; } + public static function isIncomingTransaction(self $transaction): bool + { + if ($transaction->reference_type === EmployeeAdvance::class && $transaction->reference instanceof EmployeeAdvance) { + return $transaction->reference->repayment_cash_transaction_id === $transaction->id; + } + + return self::isIncomingReference($transaction->reference_type); + } + public function cashAccount(): BelongsTo { return $this->belongsTo(CashAccount::class); diff --git a/app/Models/Concerns/HasRejection.php b/app/Models/Concerns/HasRejection.php new file mode 100644 index 0000000..a5b57ba --- /dev/null +++ b/app/Models/Concerns/HasRejection.php @@ -0,0 +1,14 @@ +morphOne(Rejection::class, 'rejectable'); + } +} diff --git a/app/Models/Employee.php b/app/Models/Employee.php index 6ecc018..78108ec 100644 --- a/app/Models/Employee.php +++ b/app/Models/Employee.php @@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; #[Guarded(['id'])] @@ -91,4 +92,9 @@ public function user(): BelongsTo { return $this->belongsTo(User::class); } + + public function advances(): HasMany + { + return $this->hasMany(EmployeeAdvance::class); + } } diff --git a/app/Models/EmployeeAdvance.php b/app/Models/EmployeeAdvance.php new file mode 100644 index 0000000..1de739e --- /dev/null +++ b/app/Models/EmployeeAdvance.php @@ -0,0 +1,136 @@ + 'integer', + 'due_date' => 'date', + 'status' => EmployeeAdvanceStatus::class, + 'verified_at' => 'datetime', + 'paid_at' => 'datetime', + ]; + } + + public function amountFormatted(): Attribute + { + return Attribute::make( + get: fn () => 'Rp '.number_format($this->amount, 0, ',', '.'), + ); + } + + public function dueDateFormatted(): Attribute + { + return Attribute::make( + get: fn () => $this->due_date?->translatedFormat('l, d F Y'), + ); + } + + public function dueDateInput(): Attribute + { + return Attribute::make( + get: fn () => $this->due_date?->format('Y-m-d'), + ); + } + + public function statusLabel(): Attribute + { + return Attribute::make( + get: fn () => $this->status?->label(), + ); + } + + public function employeeName(): Attribute + { + return Attribute::make( + get: fn () => $this->employee?->user?->profile?->full_name + ?? $this->employee?->user?->username, + ); + } + + public function rejectionReason(): Attribute + { + return Attribute::make( + get: fn () => $this->rejection?->reason, + ); + } + + public function createdAtFormatted(): Attribute + { + return Attribute::make( + get: fn () => $this->created_at?->translatedFormat('l, d F Y H:i'), + ); + } + + public function isEditable(): Attribute + { + return Attribute::make( + get: fn () => $this->status === EmployeeAdvanceStatus::PENDING, + ); + } + + public function canVerify(): Attribute + { + return Attribute::make( + get: fn () => $this->status === EmployeeAdvanceStatus::PENDING, + ); + } + + public function canPay(): Attribute + { + return Attribute::make( + get: fn () => $this->status === EmployeeAdvanceStatus::APPROVED, + ); + } + + public function employee(): BelongsTo + { + return $this->belongsTo(Employee::class); + } + + public function cashTransaction(): BelongsTo + { + return $this->belongsTo(CashTransaction::class); + } + + public function repaymentCashTransaction(): BelongsTo + { + return $this->belongsTo(CashTransaction::class, 'repayment_cash_transaction_id'); + } + + public function verifiedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'verified_by_id'); + } + + public function paidBy(): BelongsTo + { + return $this->belongsTo(User::class, 'paid_by_id'); + } +} diff --git a/app/Models/Rejection.php b/app/Models/Rejection.php new file mode 100644 index 0000000..618c367 --- /dev/null +++ b/app/Models/Rejection.php @@ -0,0 +1,22 @@ +morphTo(); + } + + public function rejectedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'rejected_by_id'); + } +} diff --git a/app/Services/Finance/CashService.php b/app/Services/Finance/CashService.php index 2289068..28f3dfe 100644 --- a/app/Services/Finance/CashService.php +++ b/app/Services/Finance/CashService.php @@ -215,13 +215,14 @@ private function recalculateBalances(CashAccount $cashAccount): void $runningBalance = 0; $transactions = CashTransaction::query() + ->with('reference') ->where('cash_account_id', $account->id) ->orderBy('created_at') ->orderBy('id') ->get(); foreach ($transactions as $transaction) { - if (CashTransaction::isIncomingReference($transaction->reference_type)) { + if (CashTransaction::isIncomingTransaction($transaction)) { $runningBalance += $transaction->amount; } else { $runningBalance -= $transaction->amount; diff --git a/app/Services/Finance/EmployeeAdvanceService.php b/app/Services/Finance/EmployeeAdvanceService.php new file mode 100644 index 0000000..d6b11e8 --- /dev/null +++ b/app/Services/Finance/EmployeeAdvanceService.php @@ -0,0 +1,215 @@ +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']) + ->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(); + } + + /** + * @param array{amount: int, description: string, due_date: string} $validated + */ + public function create(array $validated): void + { + $employee = auth()->user()?->employee; + + if ($employee === null) { + throw ValidationException::withMessages([ + 'employee' => 'Akun Anda tidak terhubung ke data pegawai.', + ]); + } + + EmployeeAdvance::create([ + 'employee_id' => $employee->id, + 'amount' => (int) $validated['amount'], + 'description' => $validated['description'], + 'due_date' => $validated['due_date'], + 'status' => EmployeeAdvanceStatus::PENDING, + ]); + } + + /** + * @param array{amount: int, description: string, due_date: string} $validated + */ + public function update(EmployeeAdvance $employeeAdvance, array $validated): void + { + $this->ensureOwnedBySubmitter($employeeAdvance); + $this->ensurePending($employeeAdvance, 'Kasbon hanya dapat diubah saat status menunggu.'); + + $employeeAdvance->amount = (int) $validated['amount']; + $employeeAdvance->description = $validated['description']; + $employeeAdvance->due_date = $validated['due_date']; + $employeeAdvance->save(); + } + + public function delete(EmployeeAdvance $employeeAdvance): void + { + $this->ensureOwnedBySubmitter($employeeAdvance); + $this->ensurePending($employeeAdvance, 'Kasbon hanya dapat dihapus saat status menunggu.'); + + $employeeAdvance->delete(); + } + + public function approve(EmployeeAdvance $employeeAdvance): void + { + $this->ensurePending($employeeAdvance, 'Kasbon ini sudah diverifikasi.'); + + $user = auth()->user(); + + 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(); + }); + } + + public function reject(EmployeeAdvance $employeeAdvance, string $reason): void + { + $this->ensurePending($employeeAdvance, 'Kasbon ini sudah diverifikasi.'); + + $user = auth()->user(); + + 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, + ]); + }); + } + + public function pay(EmployeeAdvance $employeeAdvance): void + { + if ($employeeAdvance->status !== EmployeeAdvanceStatus::APPROVED) { + throw ValidationException::withMessages([ + 'employee_advance' => 'Kasbon hanya dapat dilunasi setelah disetujui.', + ]); + } + + $user = auth()->user(); + + 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(); + }); + } + + private function ensureOwnedBySubmitter(EmployeeAdvance $employeeAdvance): void + { + if (auth()->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(); + } +} diff --git a/database/migrations/2026_06_10_160001_create_employee_advances_table.php b/database/migrations/2026_06_10_160001_create_employee_advances_table.php new file mode 100644 index 0000000..80a43b5 --- /dev/null +++ b/database/migrations/2026_06_10_160001_create_employee_advances_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('employee_id')->constrained()->cascadeOnDelete(); + $table->unsignedBigInteger('amount'); + $table->string('description', 500); + $table->date('due_date'); + $table->enum('status', array_column(EmployeeAdvanceStatus::cases(), 'value')) + ->default(EmployeeAdvanceStatus::PENDING->value); + $table->foreignId('cash_transaction_id')->nullable()->unique()->constrained()->restrictOnDelete(); + $table->foreignId('repayment_cash_transaction_id')->nullable()->unique()->constrained('cash_transactions')->restrictOnDelete(); + $table->timestamp('verified_at')->nullable(); + $table->foreignId('verified_by_id')->nullable()->constrained('users')->restrictOnDelete(); + $table->timestamp('paid_at')->nullable(); + $table->foreignId('paid_by_id')->nullable()->constrained('users')->restrictOnDelete(); + $table->timestamps(); + + $table->index(['employee_id', 'status']); + $table->index(['status', 'created_at']); + }); + } + + public function down(): void + { + Schema::dropIfExists('employee_advances'); + } +}; diff --git a/database/migrations/2026_06_10_170001_create_rejections_table.php b/database/migrations/2026_06_10_170001_create_rejections_table.php new file mode 100644 index 0000000..e3588e4 --- /dev/null +++ b/database/migrations/2026_06_10_170001_create_rejections_table.php @@ -0,0 +1,26 @@ +id(); + $table->morphs('rejectable'); + $table->string('reason', 500); + $table->foreignId('rejected_by_id')->constrained('users')->restrictOnDelete(); + $table->timestamps(); + + $table->unique(['rejectable_type', 'rejectable_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('rejections'); + } +}; diff --git a/resources/js/components/AppSidebar.vue b/resources/js/components/AppSidebar.vue index 8e3475e..b604f4f 100644 --- a/resources/js/components/AppSidebar.vue +++ b/resources/js/components/AppSidebar.vue @@ -1,6 +1,6 @@ + +