Files
travel/resources/views/livewire/travel-request/approve.blade.php
Tim Basten 564f78dcda
All checks were successful
linter / quality (push) Successful in 1m37s
tests / ci (8.4) (push) Successful in 2m13s
tests / ci (8.5) (push) Successful in 1m25s
initial
2026-03-05 11:41:39 +08:00

124 lines
5.2 KiB
PHP

<?php
use App\Models\TravelRequest;
use App\Models\TravelRequestApproval;
use App\Services\ApprovalService;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Layout;
use Livewire\Component;
new #[Layout('components.layouts.app')] class extends Component {
public TravelRequest $travelRequest;
public ?TravelRequestApproval $pendingApproval = null;
public string $comments = '';
public function mount(int $id): void
{
$user = Auth::user();
abort_unless($user->hasAnyRole(['travel_approver', 'administrator']), 403);
$this->travelRequest = TravelRequest::with([
'user', 'journeys', 'costCodes',
'approvals.step', 'approvals.approver',
])->findOrFail($id);
$this->pendingApproval = $this->travelRequest->approvals()
->where('status', \App\Enums\ApprovalStatus::Pending->value)
->with('step')
->first();
}
public function approve(): void
{
abort_unless($this->pendingApproval, 403);
app(ApprovalService::class)->approve($this->pendingApproval, Auth::user(), $this->comments ?: null);
session()->flash('success', 'Request approved successfully.');
$this->redirect(route('dashboard'), navigate: true);
}
public function reject(): void
{
$this->validate(['comments' => ['required', 'string', 'min:5']], [
'comments.required' => 'Please provide a reason for rejection.',
'comments.min' => 'Rejection reason must be at least 5 characters.',
]);
abort_unless($this->pendingApproval, 403);
app(ApprovalService::class)->reject($this->pendingApproval, Auth::user(), $this->comments);
session()->flash('success', 'Request rejected.');
$this->redirect(route('dashboard'), navigate: true);
}
public function render(): mixed
{
return view('livewire.travel-request.approve');
}
}
?>
<div>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="h4 mb-0">Review Travel Request #{{ $travelRequest->id }}</h2>
<a href="{{ route('dashboard') }}" class="btn btn-outline-secondary btn-sm">Back</a>
</div>
@if (! $pendingApproval)
<div class="alert alert-info">This request has no pending approval steps.</div>
@else
<div class="alert alert-info">
<strong>Step {{ $pendingApproval->step->order }}: {{ $pendingApproval->step->name }}</strong>
— Awaiting your review.
</div>
@endif
{{-- Request Summary --}}
<div class="card mb-4">
<div class="card-header fw-semibold">Request Summary</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-3">Applicant</dt>
<dd class="col-sm-9">{{ $travelRequest->user->name }} ({{ $travelRequest->user->email }})</dd>
<dt class="col-sm-3">Reason</dt>
<dd class="col-sm-9">{{ $travelRequest->reason_summary }}</dd>
<dt class="col-sm-3">Journeys</dt>
<dd class="col-sm-9">
@foreach ($travelRequest->journeys as $journey)
<div>{{ $journey->origin }} → {{ $journey->destination }} on {{ $journey->date->format('d M Y') }} ({{ $journey->method->label() }})</div>
@endforeach
</dd>
<dt class="col-sm-3">Accommodation</dt>
<dd class="col-sm-9">{{ $travelRequest->needs_accommodation ? 'Required' : 'Not required' }}</dd>
<dt class="col-sm-3">Car Hire</dt>
<dd class="col-sm-9">{{ $travelRequest->needs_car_hire ? 'Required' : 'Not required' }}</dd>
<dt class="col-sm-3">Business Days</dt>
<dd class="col-sm-9">{{ $travelRequest->business_days }}</dd>
</dl>
</div>
</div>
@if ($pendingApproval)
{{-- Approve/Reject Form --}}
<div class="card mb-4">
<div class="card-header fw-semibold">Decision</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Comments (optional for approval, required for rejection)</label>
<textarea wire:model="comments" rows="3" class="form-control @error('comments') is-invalid @enderror" placeholder="Add your comments..."></textarea>
@error('comments') <div class="invalid-feedback">{{ $message }}</div> @enderror
</div>
<div class="d-flex gap-2">
<button type="button" wire:click="approve" class="btn btn-success" wire:loading.attr="disabled" wire:target="approve">
<span wire:loading wire:target="approve" class="spinner-border spinner-border-sm me-1"></span>
Approve
</button>
<button type="button" wire:click="reject" class="btn btn-danger" wire:loading.attr="disabled" wire:target="reject">
<span wire:loading wire:target="reject" class="spinner-border spinner-border-sm me-1"></span>
Reject
</button>
</div>
</div>
</div>
@endif
</div>