Files
ladill-frontdesk/resources/views/frontdesk/kiosk/index.blade.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

375 lines
25 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@php
$visitorTypeCards = collect($visitorTypes)
->map(fn ($label, $key) => ['key' => $key, 'label' => $label])
->values();
@endphp
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Reception · {{ $organization->name }}</title>
@include('partials.favicon')
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600,700&display=swap" rel="stylesheet" />
<script>
window.__KIOSK__ = {
visitorTypes: @json($visitorTypeCards),
typeConfigs: @json($typeConfigs),
resetSeconds: {{ $resetSeconds }},
checkInUrl: @json($checkInUrl ?? route('frontdesk.kiosk.check-in')),
employeeKioskEnabled: @json($employeeKioskEnabled ?? false),
staffActionUrl: @json($staffActionUrl ?? ''),
stepOutReasons: @json($stepOutReasons ?? []),
};
</script>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<style>
@keyframes kiosk-tap-pulse {
0%, 100% { transform: scale(1); box-shadow: 0 20px 40px -12px rgb(79 70 229 / 0.45); }
50% { transform: scale(1.02); box-shadow: 0 24px 48px -10px rgb(79 70 229 / 0.55); }
}
.kiosk-tap-btn { animation: kiosk-tap-pulse 2.4s ease-in-out infinite; }
</style>
</head>
<body class="min-h-screen bg-slate-100 font-sans text-slate-900 antialiased" x-data="kioskFlow" x-init="startTimer()">
<div class="pointer-events-none fixed inset-0 overflow-hidden" aria-hidden="true">
<div class="absolute -left-24 top-0 h-72 w-72 rounded-full bg-indigo-200/40 blur-3xl"></div>
<div class="absolute -right-16 bottom-0 h-80 w-80 rounded-full bg-violet-200/40 blur-3xl"></div>
</div>
<div class="fixed inset-x-0 top-0 z-10 h-1.5 bg-gradient-to-r from-indigo-600 to-violet-600"></div>
@include('frontdesk.partials.kiosk-brand')
<div class="relative mx-auto flex min-h-screen max-w-3xl flex-col pt-20">
{{-- Welcome --}}
<template x-if="step === 'welcome'">
<div class="flex min-h-[calc(100vh-5rem)] flex-col items-center justify-center px-6 py-12 text-center">
<div class="max-w-xl">
<p class="text-sm font-semibold uppercase tracking-wider text-indigo-600">Reception</p>
<h1 class="mt-3 text-4xl font-bold tracking-tight text-slate-900 sm:text-5xl">
Welcome to {{ $organization->name }}
</h1>
</div>
<div class="mt-16 grid w-full max-w-lg gap-4" :class="employeeKioskEnabled ? 'sm:grid-cols-2' : ''">
<button type="button"
@click="startVisitorFlow()"
class="kiosk-tap-btn btn-primary btn-primary-lg w-full py-8 text-xl font-bold">
I'm a visitor
</button>
<template x-if="employeeKioskEnabled">
<button type="button"
@click="startStaffFlow()"
class="w-full rounded-3xl border-2 border-slate-200 bg-white py-8 text-xl font-bold text-slate-900 shadow-sm transition hover:border-indigo-300 hover:shadow-md active:scale-[0.98]">
I'm an employee
</button>
</template>
</div>
</div>
</template>
{{-- Visitor type carousel --}}
<template x-if="step === 'type'">
<div class="relative flex min-h-[calc(100vh-5rem)] flex-col items-center justify-center px-6 py-8"
@touchstart="onTypeTouchStart($event)"
@touchend="onTypeTouchEnd($event)">
<div class="absolute left-6 top-8 z-10">
@include('frontdesk.partials.kiosk-back')
</div>
<h2 class="mb-10 text-center text-2xl font-semibold text-slate-900 sm:text-3xl">What brings you here?</h2>
<div class="flex w-full max-w-lg items-center gap-3">
<button type="button"
@click="prevType()"
:disabled="typeIndex === 0"
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-full border border-slate-200 bg-white text-slate-600 shadow-sm transition hover:border-indigo-200 disabled:opacity-30"
aria-label="Previous option">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" /></svg>
</button>
<div class="relative min-w-0 flex-1 overflow-hidden">
<div class="flex transition-transform duration-300 ease-out"
:style="`transform: translateX(-${typeIndex * 100}%)`">
<template x-for="type in visitorTypes" :key="type.key">
<div class="w-full shrink-0 px-1">
<button type="button"
@click="selectType(type.key)"
class="w-full rounded-3xl border-2 border-slate-200 bg-white px-6 py-14 text-center shadow-sm transition hover:border-indigo-300 hover:shadow-md active:scale-[0.98]">
<span class="block text-2xl font-bold text-slate-900" x-text="type.label"></span>
<span class="mt-3 block text-sm font-medium text-indigo-600">Tap to select</span>
</button>
</div>
</template>
</div>
</div>
<button type="button"
@click="nextType()"
:disabled="typeIndex === visitorTypes.length - 1"
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-full border border-slate-200 bg-white text-slate-600 shadow-sm transition hover:border-indigo-200 disabled:opacity-30"
aria-label="Next option">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" /></svg>
</button>
</div>
<div class="mt-8 flex justify-center gap-2">
<template x-for="(type, i) in visitorTypes" :key="'dot-' + type.key">
<button type="button"
@click="typeIndex = i; resetTimer()"
class="h-2.5 rounded-full transition-all duration-200"
:class="typeIndex === i ? 'w-8 bg-indigo-600' : 'w-2.5 bg-slate-300'"
:aria-label="'Go to ' + type.label"></button>
</template>
</div>
</div>
</template>
{{-- Multi-step check-in form --}}
<template x-if="step === 'details'">
<div class="relative flex min-h-[calc(100vh-5rem)] flex-col justify-center px-6 py-8">
<div class="absolute left-6 top-8 z-10">
@include('frontdesk.partials.kiosk-back')
</div>
<div class="mx-auto w-full max-w-lg rounded-3xl border border-slate-200 bg-white p-6 shadow-sm">
<div class="mb-6">
<div class="flex items-center justify-between text-xs font-medium text-slate-500">
<span x-text="typeConfigs[form.visitor_type]?.label"></span>
<span x-text="`Step ${formStep} of ${totalFormSteps()}`"></span>
</div>
<div class="mt-2 h-1.5 overflow-hidden rounded-full bg-slate-100">
<div class="h-full rounded-full bg-gradient-to-r from-indigo-600 to-violet-600 transition-all duration-300"
:style="`width: ${(formStep / totalFormSteps()) * 100}%`"></div>
</div>
<p class="mt-2 text-sm font-semibold text-slate-900" x-text="currentFormStepLabel()"></p>
</div>
<p x-show="errorMessage" x-cloak class="mb-4 rounded-xl bg-red-50 px-4 py-3 text-sm text-red-700" x-text="errorMessage"></p>
<div>
{{-- Step 1: Identity --}}
<template x-if="currentStepKind() === 'identity'">
<div class="space-y-4">
<input type="text" x-model="form.full_name" placeholder="Full name *" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<input type="text" x-model="form.company" placeholder="Company" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<input type="text" x-model="form.phone" placeholder="Phone" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<input type="text" x-model="form.email" placeholder="Email" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
</div>
</template>
{{-- Step 2: Visit --}}
<template x-if="currentStepKind() === 'visit'">
<div class="space-y-4">
<select x-model="form.host_id" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<option value="">Who are you visiting?</option>
@foreach ($hosts as $host)
<option value="{{ $host->id }}">{{ $host->name }}</option>
@endforeach
</select>
<select x-model="form.purpose" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<option value="">Purpose of visit</option>
@foreach (config('frontdesk.visit_purposes') as $label)
<option value="{{ $label }}">{{ $label }}</option>
@endforeach
</select>
</div>
</template>
{{-- Step 3: Type-specific fields (when present) --}}
<template x-if="currentStepKind() === 'typeFields'">
<div class="space-y-4">
<template x-for="field in typeConfigs[form.visitor_type]?.fields || []" :key="field.name">
<div>
<label class="mb-1 block text-sm font-medium text-slate-600" x-text="field.label"></label>
<template x-if="field.type === 'textarea'">
<textarea x-model="form.type_fields[field.name]" rows="2" class="w-full rounded-xl border-slate-200 px-4 py-3 text-lg"></textarea>
</template>
<template x-if="field.type === 'checkbox'">
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" :checked="form.type_fields[field.name]" @change="form.type_fields[field.name] = $event.target.checked" class="h-5 w-5 rounded">
Yes
</label>
</template>
<template x-if="field.type === 'date' || field.type === 'datetime-local' || field.type === 'text'">
<input :type="field.type" x-model="form.type_fields[field.name]" class="w-full rounded-xl border-slate-200 px-4 py-3 text-lg">
</template>
<template x-if="field.type === 'signature'">
<canvas :x-ref="'sig_' + field.name" width="400" height="120" class="w-full rounded-xl border border-slate-200 bg-slate-50"
@mousedown="startSign($event, field.name)" @mousemove="drawSign($event, field.name)" @mouseup="endSign(field.name)"
@touchstart.prevent="startSign($event, field.name)" @touchmove.prevent="drawSign($event, field.name)" @touchend.prevent="endSign(field.name)"></canvas>
<button type="button" @click="clearSign(field.name)" class="mt-1 text-xs text-slate-500">Clear signature</button>
</template>
</div>
</template>
</div>
</template>
{{-- Final step: Photo & finish --}}
<template x-if="currentStepKind() === 'finish'">
<div class="space-y-4">
<div class="rounded-xl border border-slate-200 p-4">
<p class="mb-2 text-sm font-medium text-slate-600">Visitor photo (optional)</p>
<video x-ref="camera" x-show="cameraActive" autoplay playsinline class="mb-2 w-full max-h-48 rounded-lg bg-slate-900 object-cover"></video>
<img x-show="form.photo_data" :src="form.photo_data" alt="Captured photo preview" class="mb-2 max-h-48 rounded-lg">
<div class="flex flex-wrap gap-2">
<button type="button" @click="startCamera()" class="rounded-lg bg-slate-100 px-3 py-2 text-sm">Open camera</button>
<button type="button" @click="capturePhoto()" class="rounded-lg bg-slate-100 px-3 py-2 text-sm">Capture</button>
<button type="button" @click="clearPhoto()" class="rounded-lg bg-slate-100 px-3 py-2 text-sm">Clear</button>
</div>
</div>
<template x-if="typeConfigs[form.visitor_type]?.requires_approval">
<p class="rounded-xl bg-amber-50 px-4 py-3 text-sm text-amber-800">Reception will review your visit before issuing a badge.</p>
</template>
<label class="flex items-center gap-3 text-sm">
<input type="checkbox" x-model="form.policies_accepted" class="h-5 w-5 rounded">
I have read and accept the visitor policies
</label>
</div>
</template>
<div class="mt-6">
<button type="button"
@click="handleContinue()"
:disabled="loading"
class="btn-primary w-full py-3 disabled:opacity-50">
<span x-text="loading ? 'Submitting…' : (isLastFormStep() ? 'Complete check-in' : 'Continue')"></span>
</button>
</div>
</div>
</div>
</div>
</template>
{{-- Employee: identify --}}
<template x-if="step === 'staff_identify'">
<div class="relative flex min-h-[calc(100vh-5rem)] flex-col justify-center px-6 py-8">
<div class="absolute left-6 top-8 z-10">
@include('frontdesk.partials.kiosk-back')
</div>
<div class="mx-auto w-full max-w-lg rounded-3xl border border-slate-200 bg-white p-6 shadow-sm">
<h2 class="text-2xl font-bold text-slate-900">Employee sign-in</h2>
<p class="mt-1 text-sm text-slate-500">Enter your employee code and PIN, or scan your badge.</p>
<p x-show="errorMessage" x-cloak class="mt-4 rounded-xl bg-red-50 px-4 py-3 text-sm text-red-700" x-text="errorMessage"></p>
<div class="mt-4 flex gap-2 text-sm">
<button type="button" @click="staffIdentifyMode = 'code'" :class="staffIdentifyMode === 'code' ? 'bg-indigo-100 text-indigo-800' : 'bg-slate-100 text-slate-600'" class="rounded-lg px-3 py-1.5 font-medium">Code</button>
<button type="button" @click="staffIdentifyMode = 'badge'" :class="staffIdentifyMode === 'badge' ? 'bg-indigo-100 text-indigo-800' : 'bg-slate-100 text-slate-600'" class="rounded-lg px-3 py-1.5 font-medium">Badge scan</button>
</div>
<div class="mt-6 space-y-4">
<template x-if="staffIdentifyMode === 'code'">
<input type="text" x-model="staffForm.employee_code" placeholder="Employee code" autocomplete="off" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg uppercase">
</template>
<template x-if="staffIdentifyMode === 'badge'">
<input type="text" x-model="staffForm.qr_token" placeholder="Scan badge QR code" autocomplete="off" x-ref="staffQrInput" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
</template>
<input type="password" inputmode="numeric" x-model="staffForm.pin" placeholder="PIN (46 digits)" maxlength="6" autocomplete="off" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg tracking-widest">
</div>
<button type="button" @click="staffIdentify()" :disabled="loading" class="btn-primary mt-6 w-full py-3 disabled:opacity-50">
<span x-text="loading ? 'Checking…' : 'Continue'"></span>
</button>
</div>
</div>
</template>
{{-- Employee: actions --}}
<template x-if="step === 'staff_actions'">
<div class="relative flex min-h-[calc(100vh-5rem)] flex-col justify-center px-6 py-8">
<div class="absolute left-6 top-8 z-10">
@include('frontdesk.partials.kiosk-back')
</div>
<div class="mx-auto w-full max-w-lg rounded-3xl border border-slate-200 bg-white p-6 text-center shadow-sm">
<p class="text-sm font-medium text-indigo-600" x-text="staffEmployee?.status_label"></p>
<h2 class="mt-1 text-2xl font-bold text-slate-900" x-text="staffEmployee?.full_name"></h2>
<p class="mt-1 text-sm text-slate-500" x-show="staffEmployee?.destination" x-text="'Currently: ' + staffEmployee?.destination"></p>
<p x-show="errorMessage" x-cloak class="mt-4 rounded-xl bg-red-50 px-4 py-3 text-sm text-red-700" x-text="errorMessage"></p>
<div class="mt-6 space-y-3">
<template x-for="action in staffEmployee?.available_actions || []" :key="action">
<button type="button"
@click="action === 'step_out' ? openStaffStepOut() : staffPerform(action)"
:disabled="loading"
class="w-full rounded-xl border border-slate-200 py-4 text-lg font-semibold text-slate-800 transition hover:border-indigo-300 hover:bg-indigo-50 disabled:opacity-50"
x-text="staffActionLabel(action)"></button>
</template>
</div>
</div>
</div>
</template>
{{-- Employee: step out --}}
<template x-if="step === 'staff_step_out'">
<div class="relative flex min-h-[calc(100vh-5rem)] flex-col justify-center px-6 py-8">
<div class="absolute left-6 top-8 z-10">
@include('frontdesk.partials.kiosk-back')
</div>
<div class="mx-auto w-full max-w-lg rounded-3xl border border-slate-200 bg-white p-6 shadow-sm">
<h2 class="text-2xl font-bold text-slate-900">Where are you going?</h2>
<p class="mt-1 text-sm text-slate-500">Let reception know you're stepping out.</p>
<p x-show="errorMessage" x-cloak class="mt-4 rounded-xl bg-red-50 px-4 py-3 text-sm text-red-700" x-text="errorMessage"></p>
<div class="mt-6 space-y-4">
<select x-model="staffStepOut.step_out_reason" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<template x-for="reason in stepOutReasons" :key="reason.key">
<option :value="reason.key" x-text="reason.label"></option>
</template>
</select>
<input type="text" x-model="staffStepOut.destination" placeholder="Destination (required for Other)" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<input type="datetime-local" x-model="staffStepOut.expected_return_at" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<input type="text" x-model="staffStepOut.notes" placeholder="Notes (optional)" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
</div>
<button type="button" @click="submitStaffStepOut()" :disabled="loading" class="btn-primary mt-6 w-full py-3 disabled:opacity-50">
<span x-text="loading ? 'Saving…' : 'Confirm step out'"></span>
</button>
</div>
</div>
</template>
{{-- Employee: branch mismatch confirm --}}
<template x-if="step === 'staff_branch_confirm'">
<div class="relative flex min-h-[calc(100vh-5rem)] flex-col justify-center px-6 py-8">
<div class="mx-auto w-full max-w-lg rounded-3xl border border-amber-200 bg-white p-6 shadow-sm">
<h2 class="text-xl font-bold text-slate-900">Different branch</h2>
<p class="mt-3 text-sm text-slate-600" x-text="staffBranchWarning"></p>
<div class="mt-6 flex gap-3">
<button type="button" @click="step = 'staff_actions'" class="flex-1 rounded-xl border border-slate-200 py-3 font-medium">Cancel</button>
<button type="button" @click="staffConfirmSignIn()" :disabled="loading" class="btn-primary flex-1 py-3 disabled:opacity-50">Sign in here</button>
</div>
</div>
</div>
</template>
{{-- Employee: done --}}
<template x-if="step === 'staff_done'">
<div class="flex min-h-[calc(100vh-5rem)] flex-col items-center justify-center px-6 py-8">
<div class="w-full max-w-lg rounded-3xl border border-slate-200 bg-white p-8 text-center shadow-sm">
<div class="mx-auto mb-4 flex h-20 w-20 items-center justify-center rounded-full bg-indigo-100 text-4xl">✓</div>
<h2 class="text-2xl font-bold" x-text="staffMessage"></h2>
<p class="mt-2 text-slate-600" x-text="staffEmployee?.full_name"></p>
<p class="mt-1 text-sm text-slate-500" x-text="staffEmployee?.status_label"></p>
<button type="button" @click="reset()" class="btn-primary btn-primary-lg mt-8 w-full">Done</button>
</div>
</div>
</template>
{{-- Done --}}
<template x-if="step === 'done'">
<div class="flex min-h-[calc(100vh-5rem)] flex-col items-center justify-center px-6 py-8">
<div class="w-full max-w-lg rounded-3xl border border-slate-200 bg-white p-8 text-center shadow-sm">
<div class="mx-auto mb-4 flex h-20 w-20 items-center justify-center rounded-full bg-indigo-100 text-4xl">✓</div>
<h2 class="text-2xl font-bold" x-text="result?.awaiting_approval ? 'Submitted for approval' : 'You\'re checked in!'"></h2>
<p class="mt-2 text-slate-600" x-text="result?.visitor_name"></p>
<template x-if="!result?.awaiting_approval">
<p class="mt-1 text-sm text-slate-500">Badge code: <span class="font-mono font-bold" x-text="result?.badge_code"></span></p>
</template>
<p class="mt-4 text-sm text-slate-500"
x-text="result?.awaiting_approval ? 'Please wait at reception.' : (result?.host_notified ? 'Your host has been notified.' : (result?.host_name ? 'Check in complete.' : 'Thank you for checking in.'))"></p>
<button type="button" @click="reset()" class="btn-primary btn-primary-lg mt-8 w-full">Done</button>
</div>
</div>
</template>
</div>
</body>
</html>