Files
isaaccladandCursor 0c092b7711
Deploy Ladill Frontdesk / deploy (push) Successful in 46s
Add visitor self-checkout on the kiosk.
Let guests check out by badge code or QR scan before leaving, with device and staff kiosk API routes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 09:07:39 +00:00

135 lines
5.6 KiB
PHP

<?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\VisitCheckInService;
use App\Services\Frontdesk\VisitCheckOutService;
use App\Services\Frontdesk\VisitorTypeService;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Illuminate\Validation\ValidationException;
class KioskController extends Controller
{
use ScopesToAccount;
public function show(Request $request, VisitorTypeService $visitorTypes): View
{
$this->authorizeAbility($request, 'kiosk.use');
$organization = $this->organization($request);
$settings = $organization->settings ?? [];
$hosts = Host::owned($this->ownerRef($request))
->where('organization_id', $organization->id)
->where('is_available', true);
$this->scopeToBranch($request, $hosts);
$hosts = $hosts->orderBy('name')->get();
return view('frontdesk.kiosk.index', [
'organization' => $organization,
'hosts' => $hosts,
'visitorTypes' => config('frontdesk.visitor_types'),
'typeConfigs' => $visitorTypes->configsForFrontend(),
'resetSeconds' => (int) ($settings['kiosk_reset_seconds'] ?? config('frontdesk.kiosk.inactivity_reset_seconds', 120)),
'visitorPolicy' => $settings['visitor_policy'] ?? null,
'checkOutUrl' => route('frontdesk.kiosk.check-out'),
// Staff sign-in requires a registered kiosk device (/kiosk/d/{token}).
'employeeKioskEnabled' => false,
'stepOutReasons' => collect(config('frontdesk.employee_step_out_reasons'))
->map(fn ($label, $key) => ['key' => $key, 'label' => $label])
->values(),
]);
}
public function checkIn(Request $request, VisitCheckInService $checkIn, VisitorTypeService $visitorTypes): JsonResponse
{
$this->authorizeAbility($request, 'kiosk.use');
$organization = $this->organization($request);
$visitorType = $request->input('visitor_type', 'visitor');
$validated = $request->validate(array_merge([
'full_name' => ['required', 'string', 'max:255'],
'company' => ['nullable', 'string', 'max:255'],
'phone' => ['nullable', 'string', 'max:50'],
'email' => ['nullable', 'email', 'max:255'],
'host_id' => ['nullable', 'integer'],
'visitor_type' => ['required', 'string', 'in:'.implode(',', array_keys(config('frontdesk.visitor_types')))],
'purpose' => ['nullable', 'string', 'max:500'],
'expected_duration_minutes' => ['nullable', 'integer'],
'policies_accepted' => ['accepted'],
'photo_data' => ['nullable', 'string'],
'signature_path' => ['nullable', 'string'],
], $visitorTypes->validationRules($visitorType)));
$payload = $visitorTypes->enrichCheckInData($visitorType, $validated, $organization);
if (! empty($validated['photo_data'])) {
$payload['photo_data'] = $validated['photo_data'];
}
$visit = $checkIn->checkIn($this->ownerRef($request), $organization, $payload);
return response()->json([
'visit' => [
'id' => $visit->id,
'public_id' => $visit->public_id,
'badge_code' => $visit->badge_code,
'visitor_name' => $visit->visitor->full_name,
'host_name' => $visit->host?->name,
'status' => $visit->status,
'awaiting_approval' => $visit->awaitingApproval(),
'checked_in_at' => $visit->checked_in_at?->toIso8601String(),
'badge_url' => $visit->isInside() ? route('frontdesk.visits.badge', $visit) : null,
'host_notified' => (bool) ($visit->host_notified ?? false),
],
]);
}
public function checkOut(Request $request, VisitCheckOutService $checkOut): JsonResponse
{
$this->authorizeAbility($request, 'kiosk.use');
$organization = $this->organization($request);
$validated = $request->validate([
'badge_code' => ['nullable', 'string', 'max:32'],
'qr_token' => ['nullable', 'string', 'max:500'],
]);
if (trim((string) ($validated['badge_code'] ?? '')) === '' && trim((string) ($validated['qr_token'] ?? '')) === '') {
throw ValidationException::withMessages([
'credentials' => 'Enter your badge code or scan your badge QR code.',
]);
}
$branchScope = app(\App\Services\Frontdesk\OrganizationResolver::class)
->branchScope($this->member($request));
try {
$visit = $checkOut->checkOutFromKiosk(
$this->ownerRef($request),
$organization,
$validated['badge_code'] ?? null,
$validated['qr_token'] ?? null,
$branchScope,
);
} catch (ModelNotFoundException) {
throw ValidationException::withMessages([
'credentials' => 'No active visit found for that badge. Ask reception if you need help.',
]);
}
return response()->json([
'visit' => [
'visitor_name' => $visit->visitor->full_name,
'badge_code' => $visit->badge_code,
'checked_out_at' => $visit->checked_out_at?->toIso8601String(),
],
]);
}
}