Add visitor self-checkout on the kiosk.
Deploy Ladill Frontdesk / deploy (push) Successful in 46s

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>
This commit is contained in:
isaacclad
2026-06-29 09:07:39 +00:00
co-authored by Cursor
parent fedc1b2763
commit 0c092b7711
8 changed files with 505 additions and 3 deletions
@@ -9,9 +9,11 @@ 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
{
@@ -36,6 +38,7 @@ class KioskController extends Controller
'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'))
@@ -86,4 +89,46 @@ class KioskController extends Controller
],
]);
}
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(),
],
]);
}
}
@@ -8,7 +8,9 @@ use App\Models\Host;
use App\Models\Organization;
use App\Services\Frontdesk\EmployeePresenceService;
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;
@@ -44,6 +46,7 @@ class KioskDeviceController extends Controller
'resetSeconds' => (int) ($settings['kiosk_reset_seconds'] ?? config('frontdesk.kiosk.inactivity_reset_seconds', 120)),
'visitorPolicy' => $settings['visitor_policy'] ?? null,
'checkInUrl' => route('frontdesk.kiosk.device.check-in', $device->device_token),
'checkOutUrl' => route('frontdesk.kiosk.device.check-out', $device->device_token),
'employeeKioskEnabled' => $employeeKioskEnabled,
'stepOutReasons' => collect(config('frontdesk.employee_step_out_reasons'))
->map(fn ($label, $key) => ['key' => $key, 'label' => $label])
@@ -160,6 +163,45 @@ class KioskDeviceController extends Controller
]);
}
public function checkOut(Request $request, VisitCheckOutService $checkOut): JsonResponse
{
$device = $this->device($request);
$organization = Organization::findOrFail($device->organization_id);
$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.',
]);
}
try {
$visit = $checkOut->checkOutFromKiosk(
$device->owner_ref,
$organization,
$validated['badge_code'] ?? null,
$validated['qr_token'] ?? null,
$device->branch_id,
);
} 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(),
],
]);
}
protected function device(Request $request): Device
{
return $request->attributes->get('frontdesk.device')