Initial Ladill Frontdesk release with deploy pipeline.
Visitor management app with SSO, kiosk, badges, reports, and Gitea CI deploy to frontdesk.ladill.com. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Frontdesk;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount;
|
||||
use App\Models\Host;
|
||||
use App\Models\Visit;
|
||||
use App\Services\Frontdesk\FrontdeskPermissions;
|
||||
use App\Services\Frontdesk\OrganizationResolver;
|
||||
use App\Services\Frontdesk\VisitLifecycleService;
|
||||
use App\Services\Frontdesk\VisitScheduleService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class HostPortalController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$host = $this->resolveLinkedHost($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$pending = Visit::owned($host->owner_ref)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('host_id', $host->id)
|
||||
->where('status', Visit::STATUS_WAITING)
|
||||
->with('visitor')
|
||||
->orderByDesc('updated_at')
|
||||
->get()
|
||||
->filter(fn (Visit $v) => $v->awaitingApproval());
|
||||
|
||||
$upcoming = Visit::owned($host->owner_ref)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('host_id', $host->id)
|
||||
->whereIn('status', [Visit::STATUS_SCHEDULED, Visit::STATUS_EXPECTED, Visit::STATUS_OVERDUE])
|
||||
->with('visitor')
|
||||
->orderBy('scheduled_at')
|
||||
->limit(10)
|
||||
->get();
|
||||
|
||||
$recent = Visit::owned($host->owner_ref)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('host_id', $host->id)
|
||||
->whereIn('status', [Visit::STATUS_CHECKED_IN, Visit::STATUS_CHECKED_OUT, Visit::STATUS_CANCELLED])
|
||||
->with('visitor')
|
||||
->orderByDesc('updated_at')
|
||||
->limit(15)
|
||||
->get();
|
||||
|
||||
return view('frontdesk.host-portal.index', compact('host', 'organization', 'pending', 'upcoming', 'recent'));
|
||||
}
|
||||
|
||||
public function scheduleForm(Request $request): View
|
||||
{
|
||||
$host = $this->resolveLinkedHost($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
return view('frontdesk.host-portal.schedule', compact('host', 'organization'));
|
||||
}
|
||||
|
||||
public function scheduleStore(Request $request, VisitScheduleService $scheduler): RedirectResponse
|
||||
{
|
||||
$host = $this->resolveLinkedHost($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$validated = $request->validate([
|
||||
'full_name' => ['required', 'string', 'max:255'],
|
||||
'company' => ['nullable', 'string', 'max:255'],
|
||||
'phone' => ['nullable', 'string', 'max:50'],
|
||||
'email' => ['nullable', 'email', 'max:255'],
|
||||
'visitor_type' => ['required', 'string', 'in:'.implode(',', array_keys(config('frontdesk.visitor_types')))],
|
||||
'purpose' => ['nullable', 'string', 'max:500'],
|
||||
'expected_duration_minutes' => ['nullable', 'integer', 'min:5', 'max:480'],
|
||||
'scheduled_at' => ['required', 'date'],
|
||||
'notes' => ['nullable', 'string', 'max:2000'],
|
||||
]);
|
||||
|
||||
$scheduler->schedule(
|
||||
$host->owner_ref,
|
||||
$organization,
|
||||
[
|
||||
...$validated,
|
||||
'host_id' => $host->id,
|
||||
'branch_id' => $host->branch_id,
|
||||
],
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
return redirect()->route('frontdesk.host.index')->with('success', 'Visit scheduled.');
|
||||
}
|
||||
|
||||
public function approve(Request $request, Visit $visit, VisitLifecycleService $lifecycle): RedirectResponse
|
||||
{
|
||||
$host = $this->resolveLinkedHost($request);
|
||||
$this->authorizeOwner($request, $visit);
|
||||
abort_unless($visit->host_id === $host->id, 403);
|
||||
abort_unless($visit->awaitingApproval(), 422);
|
||||
|
||||
$lifecycle->approve($visit, $this->ownerRef($request));
|
||||
|
||||
return redirect()->route('frontdesk.host.index')->with('success', 'Visit approved.');
|
||||
}
|
||||
|
||||
public function toggleAvailability(Request $request): RedirectResponse
|
||||
{
|
||||
$host = $this->resolveLinkedHost($request);
|
||||
$host->update(['is_available' => ! $host->is_available]);
|
||||
|
||||
return back()->with('success', $host->is_available ? 'You are now available.' : 'You are marked unavailable.');
|
||||
}
|
||||
|
||||
protected function resolveLinkedHost(Request $request): Host
|
||||
{
|
||||
$permissions = app(FrontdeskPermissions::class);
|
||||
$member = $this->member($request);
|
||||
|
||||
abort_unless(
|
||||
$permissions->can($member, 'host.portal') || app(OrganizationResolver::class)->hostFor($request->user()) !== null,
|
||||
403,
|
||||
);
|
||||
|
||||
$host = app(OrganizationResolver::class)->hostFor($request->user());
|
||||
|
||||
abort_unless($host, 403, 'No host profile linked to your account.');
|
||||
|
||||
$this->authorizeOwner($request, $host);
|
||||
|
||||
return $host;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user