diff --git a/app/Http/Controllers/Admin/HR/LeaveRequestController.php b/app/Http/Controllers/Admin/HR/LeaveRequestController.php index 568aa38..ebab12a 100644 --- a/app/Http/Controllers/Admin/HR/LeaveRequestController.php +++ b/app/Http/Controllers/Admin/HR/LeaveRequestController.php @@ -7,6 +7,7 @@ use App\Models\LeaveRequest; use App\Services\Admin\HR\LeaveRequestService; use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; use Inertia\Inertia; use Inertia\Response; @@ -16,10 +17,13 @@ public function __construct( private LeaveRequestService $service ) {} - public function index(): Response + public function index(Request $request): Response { return Inertia::render('admin/hr/leave-request/index', [ - 'leaveRequests' => $this->service->getAll(), + 'leaveRequests' => $this->service->getAll( + $request->only(['status']) + ), + 'filters' => $request->only(['status']), ]); } diff --git a/app/Services/Admin/HR/LeaveRequestService.php b/app/Services/Admin/HR/LeaveRequestService.php index f42e29a..b731f58 100644 --- a/app/Services/Admin/HR/LeaveRequestService.php +++ b/app/Services/Admin/HR/LeaveRequestService.php @@ -10,10 +10,13 @@ class LeaveRequestService { - public function getAll(): Collection + public function getAll(array $filters = []): Collection { return LeaveRequest::select('id', 'employee_id', 'start_date', 'end_date', 'total_days', 'status', 'created_at') ->with(['employee.user.userProfile']) + ->when($filters['status'] ?? null, function ($query, $status) { + $query->where('status', $status); + }) ->latest() ->get(); } diff --git a/resources/js/pages/admin/hr/leave-request/index.tsx b/resources/js/pages/admin/hr/leave-request/index.tsx index 246d717..26ced49 100644 --- a/resources/js/pages/admin/hr/leave-request/index.tsx +++ b/resources/js/pages/admin/hr/leave-request/index.tsx @@ -11,18 +11,23 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { approve, destroy, index as leaveRequestIndex, reject, store, update } from '@/routes/admin/hr/leave-requests'; import { Form, Head, router } from '@inertiajs/react'; -import { Plus } from 'lucide-react'; +import { Filter, Plus, X } from 'lucide-react'; import { useEffect, useState } from 'react'; import type { LeaveRequest } from './columns'; import { createLeaveRequestColumns } from './columns'; type Props = { leaveRequests: LeaveRequest[]; + filters: { + status?: string; + }; }; -export default function LeaveRequestIndex({ leaveRequests }: Props) { +export default function LeaveRequestIndex({ leaveRequests, filters }: Props) { const [createOpen, setCreateOpen] = useState(false); const [editing, setEditing] = useState(null); const [deleting, setDeleting] = useState(null); @@ -32,6 +37,9 @@ export default function LeaveRequestIndex({ leaveRequests }: Props) { const [endDate, setEndDate] = useState(undefined); const [editingStartDate, setEditingStartDate] = useState(undefined); const [editingEndDate, setEditingEndDate] = useState(undefined); + const [filterOpen, setFilterOpen] = useState(false); + + const hasActiveFilters = filters.status; useEffect(() => { if (editing) { @@ -43,6 +51,29 @@ export default function LeaveRequestIndex({ leaveRequests }: Props) { } }, [editing]); + function applyFilter(key: string, value: string) { + const newFilters = { ...filters }; + + if (value === '' || value === 'all') { + delete newFilters[key as keyof typeof newFilters]; + } else { + newFilters[key as keyof typeof newFilters] = value; + } + + router.get(leaveRequestIndex(), newFilters, { + preserveState: true, + replace: true, + }); + } + + function clearFilters() { + router.get(leaveRequestIndex(), {}, { + preserveState: true, + replace: true, + }); + setFilterOpen(false); + } + function handleDelete() { if (!deleting) { return; @@ -80,6 +111,61 @@ export default function LeaveRequestIndex({ leaveRequests }: Props) { handleReject: (leaveRequest) => setRejecting(leaveRequest), }); + const filterToolbar = ( + + + + + +
+
+ Filter + {hasActiveFilters && ( + + )} +
+ +
+ + +
+
+
+
+ ); + return ( <> @@ -175,6 +261,7 @@ export default function LeaveRequestIndex({ leaveRequests }: Props) { searchKey="employee.user.user_profile.full_name" searchPlaceholder="Cari nama karyawan..." emptyText="Belum ada data permohonan cuti." + toolbar={filterToolbar} /> get(route('admin.hr.leave-requests.index')); + $response->assertRedirect(route('login')); +}); + +test('authenticated users can visit the leave request index page', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->get(route('admin.hr.leave-requests.index')); + $response->assertOk(); +}); + +/* +|-------------------------------------------------------------------------- +| INDEX PAGE +|-------------------------------------------------------------------------- +*/ + +test('leave request index page displays leave requests', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + LeaveRequest::factory()->count(3)->create(); + + $response = $this->get(route('admin.hr.leave-requests.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/hr/leave-request/index') + ->has('leaveRequests', 3) + ); +}); + +test('index page works when no leave requests exist', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->get(route('admin.hr.leave-requests.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/hr/leave-request/index') + ->has('leaveRequests', 0) + ); +}); + +test('index page displays correct count after delete', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + LeaveRequest::factory()->count(2)->create(); + + $leaveRequest = LeaveRequest::first(); + $this->delete(route('admin.hr.leave-requests.destroy', $leaveRequest)); + + $response = $this->get(route('admin.hr.leave-requests.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/hr/leave-request/index') + ->has('leaveRequests', 1) + ); +}); + +/* +|-------------------------------------------------------------------------- +| FILTER - STATUS +|-------------------------------------------------------------------------- +*/ + +test('index can filter by status pending', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + LeaveRequest::factory()->count(2)->create(['status' => LeaveRequestStatus::PENDING]); + LeaveRequest::factory()->create(['status' => LeaveRequestStatus::APPROVED]); + LeaveRequest::factory()->create(['status' => LeaveRequestStatus::REJECTED]); + + $response = $this->get(route('admin.hr.leave-requests.index', ['status' => 'pending'])); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/hr/leave-request/index') + ->has('leaveRequests', 2) + ); +}); + +test('index can filter by status approved', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + LeaveRequest::factory()->create(['status' => LeaveRequestStatus::PENDING]); + LeaveRequest::factory()->count(2)->create(['status' => LeaveRequestStatus::APPROVED]); + LeaveRequest::factory()->create(['status' => LeaveRequestStatus::REJECTED]); + + $response = $this->get(route('admin.hr.leave-requests.index', ['status' => 'approved'])); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/hr/leave-request/index') + ->has('leaveRequests', 2) + ); +}); + +test('index can filter by status rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + LeaveRequest::factory()->create(['status' => LeaveRequestStatus::PENDING]); + LeaveRequest::factory()->create(['status' => LeaveRequestStatus::APPROVED]); + LeaveRequest::factory()->count(2)->create(['status' => LeaveRequestStatus::REJECTED]); + + $response = $this->get(route('admin.hr.leave-requests.index', ['status' => 'rejected'])); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/hr/leave-request/index') + ->has('leaveRequests', 2) + ); +}); + +test('index can filter by status cancelled', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + LeaveRequest::factory()->create(['status' => LeaveRequestStatus::PENDING]); + LeaveRequest::factory()->create(['status' => LeaveRequestStatus::APPROVED]); + LeaveRequest::factory()->count(2)->create(['status' => LeaveRequestStatus::CANCELLED]); + + $response = $this->get(route('admin.hr.leave-requests.index', ['status' => 'cancelled'])); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/hr/leave-request/index') + ->has('leaveRequests', 2) + ); +}); + +test('index without status filter shows all leave requests', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + LeaveRequest::factory()->create(['status' => LeaveRequestStatus::PENDING]); + LeaveRequest::factory()->create(['status' => LeaveRequestStatus::APPROVED]); + LeaveRequest::factory()->create(['status' => LeaveRequestStatus::REJECTED]); + LeaveRequest::factory()->create(['status' => LeaveRequestStatus::CANCELLED]); + + $response = $this->get(route('admin.hr.leave-requests.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/hr/leave-request/index') + ->has('leaveRequests', 4) + ); +}); + +test('index passes status filter to view', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->get(route('admin.hr.leave-requests.index', ['status' => 'approved'])); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->where('filters.status', 'approved') + ); +}); + +test('index passes empty filters when no status specified', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->get(route('admin.hr.leave-requests.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->where('filters', []) + ); +}); + +test('index with invalid status filter shows no leave requests', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + LeaveRequest::factory()->create(['status' => LeaveRequestStatus::PENDING]); + + $response = $this->get(route('admin.hr.leave-requests.index', ['status' => 'invalid_status'])); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/hr/leave-request/index') + ->has('leaveRequests', 0) + ); +}); + +/* +|-------------------------------------------------------------------------- +| STORE +|-------------------------------------------------------------------------- +*/ + +test('leave request can be created', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $employee = Employee::factory()->create(['user_id' => $user->id]); + + $response = $this->post(route('admin.hr.leave-requests.store'), [ + 'start_date' => now()->addDays(1)->format('Y-m-d'), + 'end_date' => now()->addDays(3)->format('Y-m-d'), + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('admin.hr.leave-requests.index')); + + $this->assertDatabaseCount('leave_requests', 1); + $this->assertDatabaseHas('leave_requests', [ + 'employee_id' => $employee->id, + 'status' => LeaveRequestStatus::PENDING->value, + 'total_days' => 3, + ]); +}); + +test('leave request creates with correct total_days for single day', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Employee::factory()->create(['user_id' => $user->id]); + + $this->post(route('admin.hr.leave-requests.store'), [ + 'start_date' => now()->addDays(1)->format('Y-m-d'), + 'end_date' => now()->addDays(1)->format('Y-m-d'), + ]); + + $this->assertDatabaseHas('leave_requests', [ + 'total_days' => 1, + ]); +}); + +test('leave request creates with correct total_days for multi day', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Employee::factory()->create(['user_id' => $user->id]); + + $this->post(route('admin.hr.leave-requests.store'), [ + 'start_date' => now()->addDays(1)->format('Y-m-d'), + 'end_date' => now()->addDays(5)->format('Y-m-d'), + ]); + + $this->assertDatabaseHas('leave_requests', [ + 'total_days' => 5, + ]); +}); + +test('leave request always creates with pending status', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Employee::factory()->create(['user_id' => $user->id]); + + $this->post(route('admin.hr.leave-requests.store'), [ + 'start_date' => now()->addDays(1)->format('Y-m-d'), + 'end_date' => now()->addDays(3)->format('Y-m-d'), + ]); + + $this->assertDatabaseHas('leave_requests', [ + 'status' => LeaveRequestStatus::PENDING->value, + ]); +}); + +test('store flashes success toast via Inertia', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Employee::factory()->create(['user_id' => $user->id]); + + $response = $this->post(route('admin.hr.leave-requests.store'), [ + 'start_date' => now()->addDays(1)->format('Y-m-d'), + 'end_date' => now()->addDays(3)->format('Y-m-d'), + ]); + + $response->assertRedirect(); +}); + +/* +|-------------------------------------------------------------------------- +| STORE VALIDATION +|-------------------------------------------------------------------------- +*/ + +test('leave request start_date is required', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Employee::factory()->create(['user_id' => $user->id]); + + $response = $this->post(route('admin.hr.leave-requests.store'), [ + 'start_date' => '', + 'end_date' => now()->addDays(3)->format('Y-m-d'), + ]); + + $response->assertSessionHasErrors('start_date'); +}); + +test('leave request start_date must be valid date', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Employee::factory()->create(['user_id' => $user->id]); + + $response = $this->post(route('admin.hr.leave-requests.store'), [ + 'start_date' => 'not-a-date', + 'end_date' => now()->addDays(3)->format('Y-m-d'), + ]); + + $response->assertSessionHasErrors('start_date'); +}); + +test('leave request end_date is required', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Employee::factory()->create(['user_id' => $user->id]); + + $response = $this->post(route('admin.hr.leave-requests.store'), [ + 'start_date' => now()->addDays(1)->format('Y-m-d'), + 'end_date' => '', + ]); + + $response->assertSessionHasErrors('end_date'); +}); + +test('leave request end_date must be valid date', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Employee::factory()->create(['user_id' => $user->id]); + + $response = $this->post(route('admin.hr.leave-requests.store'), [ + 'start_date' => now()->addDays(1)->format('Y-m-d'), + 'end_date' => 'not-a-date', + ]); + + $response->assertSessionHasErrors('end_date'); +}); + +test('leave request end_date must be after or equal start_date', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Employee::factory()->create(['user_id' => $user->id]); + + $response = $this->post(route('admin.hr.leave-requests.store'), [ + 'start_date' => now()->addDays(5)->format('Y-m-d'), + 'end_date' => now()->addDays(3)->format('Y-m-d'), + ]); + + $response->assertSessionHasErrors('end_date'); +}); + +test('leave request end_date equal to start_date is accepted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Employee::factory()->create(['user_id' => $user->id]); + + $date = now()->addDays(1)->format('Y-m-d'); + $response = $this->post(route('admin.hr.leave-requests.store'), [ + 'start_date' => $date, + 'end_date' => $date, + ]); + + $response->assertSessionHasNoErrors(); +}); + +test('leave request with past start_date is rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Employee::factory()->create(['user_id' => $user->id]); + + $response = $this->post(route('admin.hr.leave-requests.store'), [ + 'start_date' => now()->subDay()->format('Y-m-d'), + 'end_date' => now()->addDays(3)->format('Y-m-d'), + ]); + + $response->assertSessionHasErrors('start_date'); +}); + +test('guest cannot create leave request', function () { + $response = $this->post(route('admin.hr.leave-requests.store'), [ + 'start_date' => now()->addDays(1)->format('Y-m-d'), + 'end_date' => now()->addDays(3)->format('Y-m-d'), + ]); + + $response->assertRedirect(route('login')); + $this->assertDatabaseCount('leave_requests', 0); +}); + +test('user tries to create leave request without submitting any data', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Employee::factory()->create(['user_id' => $user->id]); + + $response = $this->post(route('admin.hr.leave-requests.store'), []); + + $response->assertSessionHasErrors(['start_date', 'end_date']); +}); + +/* +|-------------------------------------------------------------------------- +| UPDATE +|-------------------------------------------------------------------------- +*/ + +test('leave request can be updated', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $employee = Employee::factory()->create(['user_id' => $user->id]); + + $leaveRequest = LeaveRequest::factory()->create([ + 'employee_id' => $employee->id, + 'start_date' => now()->addDays(1)->format('Y-m-d'), + 'end_date' => now()->addDays(3)->format('Y-m-d'), + 'total_days' => 3, + ]); + + $response = $this->put(route('admin.hr.leave-requests.update', $leaveRequest), [ + 'start_date' => now()->addDays(5)->format('Y-m-d'), + 'end_date' => now()->addDays(7)->format('Y-m-d'), + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('admin.hr.leave-requests.index')); + + $leaveRequest->refresh(); + expect($leaveRequest->total_days)->toBe(3); +}); + +test('update recalculates total_days correctly', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $employee = Employee::factory()->create(['user_id' => $user->id]); + + $leaveRequest = LeaveRequest::factory()->create([ + 'employee_id' => $employee->id, + 'start_date' => now()->addDays(1)->format('Y-m-d'), + 'end_date' => now()->addDays(3)->format('Y-m-d'), + 'total_days' => 3, + ]); + + $this->put(route('admin.hr.leave-requests.update', $leaveRequest), [ + 'start_date' => now()->addDays(10)->format('Y-m-d'), + 'end_date' => now()->addDays(12)->format('Y-m-d'), + ]); + + $leaveRequest->refresh(); + expect($leaveRequest->total_days)->toBe(3); +}); + +test('update flashes success toast via Inertia', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $leaveRequest = LeaveRequest::factory()->create(); + + $response = $this->put(route('admin.hr.leave-requests.update', $leaveRequest), [ + 'start_date' => now()->addDays(5)->format('Y-m-d'), + 'end_date' => now()->addDays(7)->format('Y-m-d'), + ]); + + $response->assertRedirect(); +}); + +test('updating non-existent leave request returns 404', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->put(route('admin.hr.leave-requests.update', 999999), [ + 'start_date' => now()->addDays(5)->format('Y-m-d'), + 'end_date' => now()->addDays(7)->format('Y-m-d'), + ]); + + $response->assertStatus(404); +}); + +test('guest cannot update leave request', function () { + $leaveRequest = LeaveRequest::factory()->create(); + + $response = $this->put(route('admin.hr.leave-requests.update', $leaveRequest), [ + 'start_date' => now()->addDays(5)->format('Y-m-d'), + 'end_date' => now()->addDays(7)->format('Y-m-d'), + ]); + + $response->assertRedirect(route('login')); +}); + +/* +|-------------------------------------------------------------------------- +| DELETE +|-------------------------------------------------------------------------- +*/ + +test('leave request can be deleted', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $leaveRequest = LeaveRequest::factory()->create(); + + $response = $this->delete(route('admin.hr.leave-requests.destroy', $leaveRequest)); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('admin.hr.leave-requests.index')); + + $this->assertSoftDeleted('leave_requests', ['id' => $leaveRequest->id]); +}); + +test('delete flashes success toast via Inertia', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $leaveRequest = LeaveRequest::factory()->create(); + + $response = $this->delete(route('admin.hr.leave-requests.destroy', $leaveRequest)); + + $response->assertRedirect(); +}); + +test('deleting non-existent leave request returns 404', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->delete(route('admin.hr.leave-requests.destroy', 999999)); + + $response->assertStatus(404); +}); + +test('guest cannot delete leave request', function () { + $leaveRequest = LeaveRequest::factory()->create(); + + $response = $this->delete(route('admin.hr.leave-requests.destroy', $leaveRequest)); + + $response->assertRedirect(route('login')); + $this->assertDatabaseCount('leave_requests', 1); +}); + +/* +|-------------------------------------------------------------------------- +| APPROVE +|-------------------------------------------------------------------------- +*/ + +test('pending leave request can be approved', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $leaveRequest = LeaveRequest::factory()->create([ + 'status' => LeaveRequestStatus::PENDING, + ]); + + $response = $this->post(route('admin.hr.leave-requests.approve', $leaveRequest)); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('admin.hr.leave-requests.index')); + + $leaveRequest->refresh(); + expect($leaveRequest->status)->toBe(LeaveRequestStatus::APPROVED); + expect($leaveRequest->verified_by_id)->toBe($user->id); + expect($leaveRequest->verified_at)->not->toBeNull(); +}); + +test('approve flashes success toast via Inertia', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $leaveRequest = LeaveRequest::factory()->create([ + 'status' => LeaveRequestStatus::PENDING, + ]); + + $response = $this->post(route('admin.hr.leave-requests.approve', $leaveRequest)); + + $response->assertRedirect(); +}); + +test('approving non-existent leave request returns 404', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.hr.leave-requests.approve', 999999)); + + $response->assertStatus(404); +}); + +test('guest cannot approve leave request', function () { + $leaveRequest = LeaveRequest::factory()->create([ + 'status' => LeaveRequestStatus::PENDING, + ]); + + $response = $this->post(route('admin.hr.leave-requests.approve', $leaveRequest)); + + $response->assertRedirect(route('login')); + + $leaveRequest->refresh(); + expect($leaveRequest->status)->toBe(LeaveRequestStatus::PENDING); +}); + +/* +|-------------------------------------------------------------------------- +| REJECT +|-------------------------------------------------------------------------- +*/ + +test('pending leave request can be rejected', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $leaveRequest = LeaveRequest::factory()->create([ + 'status' => LeaveRequestStatus::PENDING, + ]); + + $response = $this->post(route('admin.hr.leave-requests.reject', $leaveRequest)); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('admin.hr.leave-requests.index')); + + $leaveRequest->refresh(); + expect($leaveRequest->status)->toBe(LeaveRequestStatus::REJECTED); + expect($leaveRequest->verified_by_id)->toBe($user->id); + expect($leaveRequest->verified_at)->not->toBeNull(); +}); + +test('reject flashes success toast via Inertia', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $leaveRequest = LeaveRequest::factory()->create([ + 'status' => LeaveRequestStatus::PENDING, + ]); + + $response = $this->post(route('admin.hr.leave-requests.reject', $leaveRequest)); + + $response->assertRedirect(); +}); + +test('rejecting non-existent leave request returns 404', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->post(route('admin.hr.leave-requests.reject', 999999)); + + $response->assertStatus(404); +}); + +test('guest cannot reject leave request', function () { + $leaveRequest = LeaveRequest::factory()->create([ + 'status' => LeaveRequestStatus::PENDING, + ]); + + $response = $this->post(route('admin.hr.leave-requests.reject', $leaveRequest)); + + $response->assertRedirect(route('login')); + + $leaveRequest->refresh(); + expect($leaveRequest->status)->toBe(LeaveRequestStatus::PENDING); +}); + +/* +|-------------------------------------------------------------------------- +| DATA INTEGRITY +|-------------------------------------------------------------------------- +*/ + +test('created leave request has correct timestamps', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Employee::factory()->create(['user_id' => $user->id]); + + $this->post(route('admin.hr.leave-requests.store'), [ + 'start_date' => now()->addDays(1)->format('Y-m-d'), + 'end_date' => now()->addDays(3)->format('Y-m-d'), + ]); + + $leaveRequest = LeaveRequest::first(); + expect($leaveRequest->created_at)->not->toBeNull(); + expect($leaveRequest->updated_at)->not->toBeNull(); +}); + +test('leave request factory creates valid leave request', function () { + $leaveRequest = LeaveRequest::factory()->create(); + + expect($leaveRequest->employee)->not->toBeNull(); + expect($leaveRequest->start_date)->not->toBeNull(); + expect($leaveRequest->end_date)->not->toBeNull(); + expect($leaveRequest->total_days)->toBeGreaterThanOrEqual(1); + expect($leaveRequest->status)->toBeInstanceOf(LeaveRequestStatus::class); +}); + +test('approved leave request has verified_by_id and verified_at', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $leaveRequest = LeaveRequest::factory()->create([ + 'status' => LeaveRequestStatus::PENDING, + ]); + + $this->post(route('admin.hr.leave-requests.approve', $leaveRequest)); + + $leaveRequest->refresh(); + expect($leaveRequest->verified_by_id)->toBe($user->id); + expect($leaveRequest->verified_at)->not->toBeNull(); +}); + +test('rejected leave request has verified_by_id and verified_at', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $leaveRequest = LeaveRequest::factory()->create([ + 'status' => LeaveRequestStatus::PENDING, + ]); + + $this->post(route('admin.hr.leave-requests.reject', $leaveRequest)); + + $leaveRequest->refresh(); + expect($leaveRequest->verified_by_id)->toBe($user->id); + expect($leaveRequest->verified_at)->not->toBeNull(); +}); + +/* +|-------------------------------------------------------------------------- +| REALISTIC USER SCENARIOS +|-------------------------------------------------------------------------- +*/ + +test('user creates leave request then immediately edits it', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Employee::factory()->create(['user_id' => $user->id]); + + $this->post(route('admin.hr.leave-requests.store'), [ + 'start_date' => now()->addDays(1)->format('Y-m-d'), + 'end_date' => now()->addDays(3)->format('Y-m-d'), + ]); + + $leaveRequest = LeaveRequest::first(); + + $this->put(route('admin.hr.leave-requests.update', $leaveRequest), [ + 'start_date' => now()->addDays(5)->format('Y-m-d'), + 'end_date' => now()->addDays(7)->format('Y-m-d'), + ]); + + $leaveRequest->refresh(); + expect($leaveRequest->start_date->format('Y-m-d'))->toBe(now()->addDays(5)->format('Y-m-d')); + expect($leaveRequest->end_date->format('Y-m-d'))->toBe(now()->addDays(7)->format('Y-m-d')); + expect($leaveRequest->total_days)->toBe(3); +}); + +test('user creates multiple leave requests and deletes one', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Employee::factory()->create(['user_id' => $user->id]); + + $this->post(route('admin.hr.leave-requests.store'), [ + 'start_date' => now()->addDays(1)->format('Y-m-d'), + 'end_date' => now()->addDays(3)->format('Y-m-d'), + ]); + + $this->post(route('admin.hr.leave-requests.store'), [ + 'start_date' => now()->addDays(10)->format('Y-m-d'), + 'end_date' => now()->addDays(12)->format('Y-m-d'), + ]); + + $toDelete = LeaveRequest::first(); + $this->delete(route('admin.hr.leave-requests.destroy', $toDelete)); + + $response = $this->get(route('admin.hr.leave-requests.index')); + $response->assertOk(); + $response->assertInertia(fn (Assert $page) => $page + ->component('admin/hr/leave-request/index') + ->has('leaveRequests', 1) + ); +}); + +test('user approves a leave request and it no longer shows as pending', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + LeaveRequest::factory()->create(['status' => LeaveRequestStatus::PENDING]); + + $pendingCount = LeaveRequest::where('status', LeaveRequestStatus::PENDING)->count(); + expect($pendingCount)->toBe(1); + + $leaveRequest = LeaveRequest::first(); + $this->post(route('admin.hr.leave-requests.approve', $leaveRequest)); + + $pendingCount = LeaveRequest::where('status', LeaveRequestStatus::PENDING)->count(); + expect($pendingCount)->toBe(0); + + $approvedCount = LeaveRequest::where('status', LeaveRequestStatus::APPROVED)->count(); + expect($approvedCount)->toBe(1); +}); + +test('user filters by pending then approves a request', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + LeaveRequest::factory()->count(2)->create(['status' => LeaveRequestStatus::PENDING]); + LeaveRequest::factory()->create(['status' => LeaveRequestStatus::APPROVED]); + + $response = $this->get(route('admin.hr.leave-requests.index', ['status' => 'pending'])); + $response->assertInertia(fn (Assert $page) => $page + ->has('leaveRequests', 2) + ); + + $leaveRequest = LeaveRequest::where('status', LeaveRequestStatus::PENDING)->first(); + $this->post(route('admin.hr.leave-requests.approve', $leaveRequest)); + + $response = $this->get(route('admin.hr.leave-requests.index', ['status' => 'pending'])); + $response->assertInertia(fn (Assert $page) => $page + ->has('leaveRequests', 1) + ); + + $response = $this->get(route('admin.hr.leave-requests.index', ['status' => 'approved'])); + $response->assertInertia(fn (Assert $page) => $page + ->has('leaveRequests', 2) + ); +});