Files
ladill-frontdesk/app/Http/Controllers/Frontdesk/KioskDeviceController.php
T
isaaccladandCursor 890c2c71e3
Deploy Ladill Frontdesk / deploy (push) Failing after 26s
Add employee presence with kiosk sign-in, QR badges, and mobile step-out.
Enables staff roster, PIN/code kiosk flow, attendance reports, evacuation staff roll call, webhooks, branch mismatch warnings, and a linked-user My presence portal.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 21:11:18 +00:00

169 lines
7.3 KiB
PHP

<?php
namespace App\Http\Controllers\Frontdesk;
use App\Http\Controllers\Controller;
use App\Models\Device;
use App\Models\Host;
use App\Models\Organization;
use App\Services\Frontdesk\EmployeePresenceService;
use App\Services\Frontdesk\VisitCheckInService;
use App\Services\Frontdesk\VisitorTypeService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Illuminate\Validation\ValidationException;
class KioskDeviceController extends Controller
{
public function show(Request $request, VisitorTypeService $visitorTypes): View
{
$device = $this->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);
}
}