device($request); $organization = Organization::findOrFail($device->organization_id); $settings = $organization->settings ?? []; $hosts = Host::owned($device->owner_ref) ->where('organization_id', $organization->id) ->where('is_available', true); if ($device->branch_id) { $hosts->where(function ($q) use ($device) { $q->whereNull('branch_id')->orWhere('branch_id', $device->branch_id); }); } $hosts = $hosts->orderBy('name')->get(); $employeeKioskEnabled = (bool) ($settings['employee_kiosk_enabled'] ?? true); 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, 'checkInUrl' => route('frontdesk.kiosk.device.check-in', $device->device_token), 'employeeKioskEnabled' => $employeeKioskEnabled, 'stepOutReasons' => collect(config('frontdesk.employee_step_out_reasons')) ->map(fn ($label, $key) => ['key' => $key, 'label' => $label]) ->values(), 'staffActionUrl' => route('frontdesk.kiosk.device.staff.action', $device->device_token), ]); } public function staffAction(Request $request, EmployeePresenceService $presence): JsonResponse { $device = $this->device($request); $organization = Organization::findOrFail($device->organization_id); $validated = $request->validate([ 'employee_code' => ['nullable', 'string', 'max:32'], 'qr_token' => ['nullable', 'string', 'max:500'], 'pin' => ['required', 'string', 'regex:/^\d{4,6}$/'], 'action' => ['required', 'string', 'in:identify,sign_in,sign_out,step_out,step_in'], 'step_out_reason' => ['nullable', 'string', 'in:'.implode(',', array_keys(config('frontdesk.employee_step_out_reasons')))], 'destination' => ['nullable', 'string', 'max:255'], 'notes' => ['nullable', 'string', 'max:500'], 'expected_return_at' => ['nullable', 'date'], 'confirm_branch_mismatch' => ['boolean'], ]); if (empty($validated['employee_code']) && empty($validated['qr_token'])) { throw ValidationException::withMessages([ 'credentials' => 'Enter your employee code or scan your badge.', ]); } $employee = $presence->resolveEmployee($organization, $validated); if ($validated['action'] === 'identify') { return response()->json([ 'employee' => $presence->employeePayload($employee), ]); } match ($validated['action']) { 'sign_in' => $result = $presence->signIn($employee, $device, (bool) ($validated['confirm_branch_mismatch'] ?? false)), 'sign_out' => $presence->signOut($employee, $device), 'step_out' => $presence->stepOut($employee, [ 'step_out_reason' => $validated['step_out_reason'] ?? 'other', 'destination' => $validated['destination'] ?? null, 'notes' => $validated['notes'] ?? null, 'expected_return_at' => $validated['expected_return_at'] ?? null, ], $device), 'step_in' => $presence->stepIn($employee, $device), default => null, }; $response = [ 'employee' => $presence->employeePayload($employee->fresh()), 'message' => match ($validated['action']) { 'sign_in' => 'Signed in successfully.', 'sign_out' => 'Signed out for the day.', 'step_out' => 'Stepped out recorded.', 'step_in' => 'Welcome back!', default => 'Done.', }, ]; if ($validated['action'] === 'sign_in' && isset($result['warning']) && $result['warning']) { $response['warning'] = $result['warning']; } return response()->json($response); } public function checkIn(Request $request, VisitCheckInService $checkIn, VisitorTypeService $visitorTypes): JsonResponse { $device = $this->device($request); $organization = Organization::findOrFail($device->organization_id); $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'], 'vehicle_info' => ['nullable', 'array'], 'signature_path' => ['nullable', 'string'], ], $visitorTypes->validationRules($visitorType))); $payload = $visitorTypes->enrichCheckInData($visitorType, $validated, $organization); if (! empty($validated['photo_data'])) { $payload['photo_data'] = $validated['photo_data']; } $payload['branch_id'] = $device->branch_id; $payload['reception_desk_id'] = $device->reception_desk_id; $visit = $checkIn->checkIn($device->owner_ref, $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), ], ]); } protected function device(Request $request): Device { return $request->attributes->get('frontdesk.device') ?? abort(404); } }