feat: implement ensureEditable method in LeaveRequest model for validation; update LeaveRequestController and LeaveRequestService to handle editability checks and transaction management for update and delete operations; enhance LeaveRequestService with new methods for pending requests and index page data

This commit is contained in:
Yoga Pangestu 2026-07-04 00:29:57 +07:00
parent a8ab96b10b
commit e153497114
5 changed files with 87 additions and 95 deletions

View File

@ -12,6 +12,7 @@
use App\Services\Hr\LeaveRequestService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
@ -45,6 +46,12 @@ public function store(SubmitLeaveRequest $request): RedirectResponse
public function update(SubmitLeaveRequest $request, LeaveRequest $leaveRequest): RedirectResponse
{
try {
$leaveRequest->ensureEditable();
} catch (ValidationException) {
abort(403);
}
$this->leaveRequestService->update($leaveRequest, $request->validated(), $request->user());
$this->flashUpdated('Pengajuan cuti');
@ -54,6 +61,12 @@ public function update(SubmitLeaveRequest $request, LeaveRequest $leaveRequest):
public function destroy(LeaveRequest $leaveRequest): RedirectResponse
{
try {
$leaveRequest->ensureEditable();
} catch (ValidationException) {
abort(403);
}
$this->leaveRequestService->delete($leaveRequest);
$this->flashDeleted('Pengajuan cuti');

View File

@ -3,6 +3,7 @@
namespace App\Models;
use App\Enums\LeaveRequestStatus;
use App\Enums\Permission;
use App\Models\Concerns\HasRejection;
use App\Models\Concerns\InteractsWithActivityLog;
use Illuminate\Database\Eloquent\Attributes\Appends;
@ -13,6 +14,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Validation\ValidationException;
#[Guarded(['id'])]
#[Appends([
@ -136,7 +138,24 @@ public function statusLabel(): Attribute
);
}
// 5. Relation
// 5. Other Methods
public function ensureEditable(): void
{
if ($this->status !== LeaveRequestStatus::PENDING) {
throw ValidationException::withMessages([
'status' => 'Pengajuan cuti tidak dapat diubah.',
]);
}
}
public static function canSubmit(User $user): bool
{
return $user->can(Permission::LEAVE_REQUESTS_CREATE->value)
&& ! $user->can(Permission::LEAVE_REQUESTS_VERIFY->value)
&& $user->employee !== null;
}
// 6. Relation
public function employee(): BelongsTo
{
return $this->belongsTo(Employee::class);

View File

@ -3,21 +3,19 @@
namespace App\Services\Hr;
use App\Enums\LeaveRequestStatus;
use App\Enums\Permission;
use App\Models\LeaveRequest;
use App\Models\User;
use App\Services\Concerns\ResolvesAuthenticatedEmployee;
use App\Services\Concerns\RunsInTransaction;
use App\Services\System\PushNotificationService;
use Carbon\Carbon;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
class LeaveRequestService
{
use ResolvesAuthenticatedEmployee;
use ResolvesAuthenticatedEmployee, RunsInTransaction;
public function __construct(
private readonly PushNotificationService $pushNotificationService,
@ -109,7 +107,12 @@ public function delete(LeaveRequest $leaveRequest): void
$totalDays = $leaveRequest->total_days;
$employeeName = $leaveRequest->employee?->user?->profile?->full_name;
$leaveRequest->delete();
$this->runInTransaction(
function () use ($leaveRequest): void {
$leaveRequest->delete();
},
'Gagal menghapus pengajuan cuti',
);
$this->pushNotificationService->sendToRoles(
'🗑️ Pengajuan Cuti Dihapus',
@ -121,25 +124,16 @@ public function delete(LeaveRequest $leaveRequest): void
public function approve(LeaveRequest $leaveRequest, User $user): void
{
try {
DB::transaction(function () use ($leaveRequest, $user): void {
$this->runInTransaction(
function () use ($leaveRequest, $user): void {
$leaveRequest->update([
'status' => LeaveRequestStatus::APPROVED,
'verified_at' => Carbon::now(),
'verified_by_id' => $user->id,
]);
});
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal menyetujui pengajuan cuti: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
},
'Gagal menyetujui pengajuan cuti',
);
$leaveRequest->loadMissing('employee.user');
if ($leaveRequest->employee?->user_id) {
@ -154,8 +148,8 @@ public function approve(LeaveRequest $leaveRequest, User $user): void
public function reject(LeaveRequest $leaveRequest, string $reason, User $user): void
{
try {
DB::transaction(function () use ($leaveRequest, $user, $reason): void {
$this->runInTransaction(
function () use ($leaveRequest, $user, $reason): void {
$leaveRequest->update([
'status' => LeaveRequestStatus::REJECTED,
'verified_at' => Carbon::now(),
@ -166,18 +160,9 @@ public function reject(LeaveRequest $leaveRequest, string $reason, User $user):
'reason' => $reason,
'rejected_by_id' => $user->id,
]);
});
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal menolak pengajuan cuti: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
},
'Gagal menolak pengajuan cuti',
);
$leaveRequest->loadMissing('employee.user');
if ($leaveRequest->employee?->user_id) {
@ -190,6 +175,28 @@ public function reject(LeaveRequest $leaveRequest, string $reason, User $user):
}
}
public function hasPendingForEmployee(User $user): bool
{
if ($user->employee === null) {
return false;
}
return LeaveRequest::query()
->pending()
->where('employee_id', $user->employee->id)
->exists();
}
public function indexPageData(array $tableQuery, User $user): array
{
return [
'leaveRequests' => $this->paginateForIndex($tableQuery, $user),
'authEmployeeId' => $user->employee?->id,
'canSubmit' => LeaveRequest::canSubmit($user),
'hasPending' => $this->hasPendingForEmployee($user),
];
}
private function calculateTotalDays(Carbon $startDate, Carbon $endDate): int
{
return (int) $startDate->diffInDays($endDate) + 1;
@ -216,33 +223,4 @@ private function applySorting(Builder $query, string $sort, string $direction):
$query->latest();
}
public function hasPendingForEmployee(User $user): bool
{
if ($user->employee === null) {
return false;
}
return LeaveRequest::query()
->pending()
->where('employee_id', $user->employee->id)
->exists();
}
public function canSubmit(User $user): bool
{
return $user->can(Permission::LEAVE_REQUESTS_CREATE->value)
&& ! $user->can(Permission::LEAVE_REQUESTS_VERIFY->value)
&& $user->employee !== null;
}
public function indexPageData(array $tableQuery, User $user): array
{
return [
'leaveRequests' => $this->paginateForIndex($tableQuery, $user),
'authEmployeeId' => $user->employee?->id,
'canSubmit' => $this->canSubmit($user),
'hasPending' => $this->hasPendingForEmployee($user),
];
}
}

View File

@ -1,4 +1,6 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { computed, ref, watch } from 'vue';
import CreateButton from '@/components/button/CreateButton.vue';
import { DataTable } from '@/components/data-table';
import { Card, CardContent } from '@/components/ui/card';
@ -8,17 +10,16 @@ import {
useDataTableQuerySync,
} from '@/composables/useDataTableQuery';
import AdminLayout from '@/layouts/AdminLayout.vue';
import { index } from '@/routes/admin/hr/leave_requests';
import type { DataTableSort } from '@/types/data-table';
import type {
LeaveRequestListItem,
LeaveRequestPageProps,
} from '@/types/leave-request';
import { Head } from '@inertiajs/vue3';
import { computed, ref, watch } from 'vue';
import LeaveRequestFormModal from './form/LeaveRequestFormModal.vue';
import RejectLeaveRequestModal from './form/RejectLeaveRequestModal.vue';
import { createColumns } from './table/columns';
import { index } from '@/routes/admin/hr/leave_requests';
const props = defineProps<LeaveRequestPageProps>();
@ -87,48 +88,29 @@ watch(
</script>
<template>
<Head title="Cuti" />
<AdminLayout>
<div
class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"
>
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="space-y-1">
<h2 class="text-2xl font-bold tracking-tight">Cuti</h2>
</div>
<CreateButton
v-if="canSubmit && !hasPending"
@click="openCreateModal"
label="Ajukan"
/>
<CreateButton v-if="canSubmit && !hasPending" @click="openCreateModal" label="Ajukan" />
</div>
<Card class="min-w-0">
<CardContent class="min-w-0 pt-6">
<DataTable
v-model:search="search"
:columns="columns"
:data="leaveRequests.data"
:pagination="pagination"
:pagination-links="leaveRequests.links"
:sort="currentSort"
@sort-change="setSort"
@filters-reset="resetFilters"
/>
<DataTable v-model:search="search" :columns="columns" :data="leaveRequests.data"
:pagination="pagination" :pagination-links="leaveRequests.links" :sort="currentSort"
@sort-change="setSort" @filters-reset="resetFilters" />
</CardContent>
</Card>
<LeaveRequestFormModal
v-if="canSubmit"
v-model:open="formModalOpen"
:leave-request="editingLeaveRequest"
/>
<LeaveRequestFormModal v-if="canSubmit" v-model:open="formModalOpen" :leave-request="editingLeaveRequest" />
<RejectLeaveRequestModal
v-if="can('leave_requests.verify')"
v-model:open="rejectModalOpen"
:leave-request="rejectingLeaveRequest"
/>
<RejectLeaveRequestModal v-if="can('leave_requests.verify')" v-model:open="rejectModalOpen"
:leave-request="rejectingLeaveRequest" />
</AdminLayout>
</template>

View File

@ -44,7 +44,7 @@ function createLeaveEmployeeUser(): User
'full_name' => fake()->name(),
]);
Employee::factory()->create(['user_id' => $user->id]);
$user->assignRole('marketing');
$user->assignRole('marketing-offline');
$user->forgetCachedPermissions();
return $user;