Improve kiosk type picker and split check-in into steps.
Deploy Ladill Frontdesk / deploy (push) Successful in 27s
Deploy Ladill Frontdesk / deploy (push) Successful in 27s
Center visitor type carousel with swipe and dots; break check-in into About you, Your visit, optional type fields, and Photo & finish. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,3 +1,8 @@
|
|||||||
|
@php
|
||||||
|
$visitorTypeCards = collect($visitorTypes)
|
||||||
|
->map(fn ($label, $key) => ['key' => $key, 'label' => $label])
|
||||||
|
->values();
|
||||||
|
@endphp
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@@ -25,6 +30,7 @@
|
|||||||
@include('frontdesk.partials.kiosk-brand')
|
@include('frontdesk.partials.kiosk-brand')
|
||||||
|
|
||||||
<div class="relative mx-auto flex min-h-screen max-w-3xl flex-col pt-20">
|
<div class="relative mx-auto flex min-h-screen max-w-3xl flex-col pt-20">
|
||||||
|
{{-- Welcome --}}
|
||||||
<template x-if="step === '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="flex min-h-[calc(100vh-5rem)] flex-col items-center justify-center px-6 py-12 text-center">
|
||||||
<div class="max-w-xl">
|
<div class="max-w-xl">
|
||||||
@@ -42,31 +48,86 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="step !== 'welcome'">
|
{{-- Visitor type carousel --}}
|
||||||
<div class="flex min-h-[calc(100vh-5rem)] flex-col px-6 py-6">
|
<template x-if="step === 'type'">
|
||||||
<main class="flex-1">
|
<div class="flex min-h-[calc(100vh-5rem)] flex-col items-center justify-center px-6 py-8"
|
||||||
<template x-if="step === 'type'">
|
@touchstart="onTypeTouchStart($event)"
|
||||||
<div>
|
@touchend="onTypeTouchEnd($event)">
|
||||||
<h2 class="mb-6 text-center text-2xl font-semibold text-slate-900">What brings you here?</h2>
|
<h2 class="mb-10 text-center text-2xl font-semibold text-slate-900 sm:text-3xl">What brings you here?</h2>
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
|
||||||
@foreach ($visitorTypes as $key => $label)
|
<div class="flex w-full max-w-lg items-center gap-3">
|
||||||
<button type="button" @click="selectType('{{ $key }}')"
|
<button type="button"
|
||||||
class="rounded-2xl border border-slate-200 bg-white px-6 py-8 text-lg font-semibold text-slate-800 shadow-sm transition hover:border-indigo-200 hover:shadow-md active:scale-[0.98]">
|
@click="prevType()"
|
||||||
{{ $label }}
|
: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>
|
</button>
|
||||||
@endforeach
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template x-if="step === 'details'">
|
{{-- Multi-step check-in form --}}
|
||||||
<form @submit.prevent="submitCheckIn" class="space-y-4 rounded-3xl border border-slate-200 bg-white p-6 shadow-sm">
|
<template x-if="step === 'details'">
|
||||||
<p class="text-sm font-medium text-indigo-700" x-text="typeConfigs[form.visitor_type]?.label"></p>
|
<div class="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-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>
|
||||||
|
|
||||||
|
<form @submit.prevent="isLastFormStep ? submitCheckIn() : nextFormStep()">
|
||||||
|
{{-- Step 1: Identity --}}
|
||||||
|
<div x-show="formStep === 1" class="space-y-4">
|
||||||
<input type="text" x-model="form.full_name" required placeholder="Full name" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
|
<input type="text" x-model="form.full_name" required 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.company" placeholder="Company" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
|
||||||
<input type="tel" x-model="form.phone" placeholder="Phone" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
|
<input type="tel" x-model="form.phone" placeholder="Phone" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
|
||||||
<input type="email" x-model="form.email" placeholder="Email" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
|
<input type="email" x-model="form.email" placeholder="Email" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Step 2: Visit --}}
|
||||||
|
<div x-show="formStep === 2" class="space-y-4">
|
||||||
<select x-model="form.host_id" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
|
<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>
|
<option value="">Who are you visiting?</option>
|
||||||
@foreach ($hosts as $host)
|
@foreach ($hosts as $host)
|
||||||
@@ -74,18 +135,10 @@
|
|||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
<input type="text" x-model="form.purpose" placeholder="Purpose of visit" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
|
<input type="text" x-model="form.purpose" placeholder="Purpose of visit" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border border-slate-200 p-4">
|
{{-- Step 3: Type-specific fields (when present) --}}
|
||||||
<p class="mb-2 text-sm font-medium text-slate-600">Visitor photo (optional)</p>
|
<div x-show="hasTypeFields && formStep === 3" class="space-y-4">
|
||||||
<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-for="field in typeConfigs[form.visitor_type]?.fields || []" :key="field.name">
|
<template x-for="field in typeConfigs[form.visitor_type]?.fields || []" :key="field.name">
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1 block text-sm font-medium text-slate-600" x-text="field.label"></label>
|
<label class="mb-1 block text-sm font-medium text-slate-600" x-text="field.label"></label>
|
||||||
@@ -109,6 +162,20 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Final step: Photo & finish --}}
|
||||||
|
<div x-show="formStep === finishStep()" 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">
|
<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>
|
<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>
|
||||||
@@ -118,25 +185,38 @@
|
|||||||
<input type="checkbox" x-model="form.policies_accepted" required class="h-5 w-5 rounded">
|
<input type="checkbox" x-model="form.policies_accepted" required class="h-5 w-5 rounded">
|
||||||
I have read and accept the visitor policies
|
I have read and accept the visitor policies
|
||||||
</label>
|
</label>
|
||||||
<button type="submit" :disabled="loading" class="btn-primary btn-primary-lg w-full disabled:opacity-50">
|
|
||||||
<span x-text="loading ? 'Submitting…' : 'Complete check-in'"></span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="step === 'done'">
|
|
||||||
<div class="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.' : 'Your host has been notified.'"></p>
|
|
||||||
<button type="button" @click="reset()" class="btn-primary btn-primary-lg mt-8">Done</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 flex gap-3">
|
||||||
|
<button type="button"
|
||||||
|
@click="prevFormStep()"
|
||||||
|
class="flex-1 rounded-full border border-slate-200 px-4 py-3 text-sm font-semibold text-slate-700 hover:bg-slate-50">
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<button type="submit"
|
||||||
|
:disabled="loading"
|
||||||
|
class="btn-primary flex-1 py-3 disabled:opacity-50">
|
||||||
|
<span x-text="loading ? 'Submitting…' : (isLastFormStep ? 'Complete check-in' : 'Continue')"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</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>
|
</template>
|
||||||
</main>
|
<p class="mt-4 text-sm text-slate-500" x-text="result?.awaiting_approval ? 'Please wait at reception.' : 'Your host has been notified.'"></p>
|
||||||
|
<button type="button" @click="reset()" class="btn-primary btn-primary-lg mt-8 w-full">Done</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,9 +225,13 @@
|
|||||||
function kioskFlow() {
|
function kioskFlow() {
|
||||||
return {
|
return {
|
||||||
step: 'welcome',
|
step: 'welcome',
|
||||||
|
formStep: 1,
|
||||||
|
typeIndex: 0,
|
||||||
|
typeTouchStart: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
result: null,
|
result: null,
|
||||||
timer: null,
|
timer: null,
|
||||||
|
visitorTypes: @json($visitorTypeCards),
|
||||||
typeConfigs: @json($typeConfigs),
|
typeConfigs: @json($typeConfigs),
|
||||||
signing: { active: false, field: null },
|
signing: { active: false, field: null },
|
||||||
canvases: {},
|
canvases: {},
|
||||||
@@ -165,24 +249,136 @@
|
|||||||
policies_accepted: false,
|
policies_accepted: false,
|
||||||
type_fields: {},
|
type_fields: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get hasTypeFields() {
|
||||||
|
return (this.typeConfigs[this.form.visitor_type]?.fields || []).length > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
get totalFormSteps() {
|
||||||
|
return this.hasTypeFields ? 4 : 3;
|
||||||
|
},
|
||||||
|
|
||||||
|
get isLastFormStep() {
|
||||||
|
return this.formStep === this.finishStep();
|
||||||
|
},
|
||||||
|
|
||||||
|
get currentFormStepLabel() {
|
||||||
|
if (this.formStep === 1) return 'About you';
|
||||||
|
if (this.formStep === 2) return 'Your visit';
|
||||||
|
if (this.hasTypeFields && this.formStep === 3) return 'Additional details';
|
||||||
|
return 'Photo & finish';
|
||||||
|
},
|
||||||
|
|
||||||
|
finishStep() {
|
||||||
|
return this.hasTypeFields ? 4 : 3;
|
||||||
|
},
|
||||||
|
|
||||||
selectType(type) {
|
selectType(type) {
|
||||||
this.form.visitor_type = type;
|
this.form.visitor_type = type;
|
||||||
this.form.type_fields = {};
|
this.form.type_fields = {};
|
||||||
|
this.formStep = 1;
|
||||||
this.step = 'details';
|
this.step = 'details';
|
||||||
this.resetTimer();
|
this.resetTimer();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
nextType() {
|
||||||
|
if (this.typeIndex < this.visitorTypes.length - 1) {
|
||||||
|
this.typeIndex++;
|
||||||
|
this.resetTimer();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
prevType() {
|
||||||
|
if (this.typeIndex > 0) {
|
||||||
|
this.typeIndex--;
|
||||||
|
this.resetTimer();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onTypeTouchStart(e) {
|
||||||
|
this.typeTouchStart = e.touches[0].clientX;
|
||||||
|
},
|
||||||
|
|
||||||
|
onTypeTouchEnd(e) {
|
||||||
|
if (this.typeTouchStart === null) return;
|
||||||
|
const delta = e.changedTouches[0].clientX - this.typeTouchStart;
|
||||||
|
if (delta > 60) this.prevType();
|
||||||
|
else if (delta < -60) this.nextType();
|
||||||
|
this.typeTouchStart = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
validateFormStep() {
|
||||||
|
if (this.formStep === 1) {
|
||||||
|
if (!this.form.full_name.trim()) {
|
||||||
|
alert('Please enter your full name.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasTypeFields && this.formStep === 3) {
|
||||||
|
const fields = this.typeConfigs[this.form.visitor_type]?.fields || [];
|
||||||
|
for (const field of fields) {
|
||||||
|
if (!field.required) continue;
|
||||||
|
const val = this.form.type_fields[field.name];
|
||||||
|
if (field.type === 'checkbox' && !val) {
|
||||||
|
alert(`Please confirm: ${field.label}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (field.type !== 'checkbox' && (!val || String(val).trim() === '')) {
|
||||||
|
alert(`Please complete: ${field.label}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isLastFormStep && !this.form.policies_accepted) {
|
||||||
|
alert('Please accept the visitor policies.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
nextFormStep() {
|
||||||
|
if (!this.validateFormStep()) return;
|
||||||
|
if (this.formStep < this.totalFormSteps) {
|
||||||
|
this.formStep++;
|
||||||
|
this.resetTimer();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
prevFormStep() {
|
||||||
|
if (this.formStep > 1) {
|
||||||
|
this.formStep--;
|
||||||
|
this.resetTimer();
|
||||||
|
} else {
|
||||||
|
this.step = 'type';
|
||||||
|
this.resetTimer();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
resetTimer() {
|
resetTimer() {
|
||||||
clearTimeout(this.timer);
|
clearTimeout(this.timer);
|
||||||
this.timer = setTimeout(() => this.reset(), {{ $resetSeconds }} * 1000);
|
this.timer = setTimeout(() => this.reset(), {{ $resetSeconds }} * 1000);
|
||||||
},
|
},
|
||||||
|
|
||||||
startTimer() { this.resetTimer(); },
|
startTimer() { this.resetTimer(); },
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.stopCamera();
|
this.stopCamera();
|
||||||
this.step = 'welcome';
|
this.step = 'welcome';
|
||||||
|
this.formStep = 1;
|
||||||
|
this.typeIndex = 0;
|
||||||
this.result = null;
|
this.result = null;
|
||||||
this.form = { full_name: '', company: '', phone: '', email: '', host_id: '', visitor_type: 'visitor', purpose: '', photo_data: '', policies_accepted: false, type_fields: {} };
|
this.form = {
|
||||||
|
full_name: '', company: '', phone: '', email: '', host_id: '',
|
||||||
|
visitor_type: 'visitor', purpose: '', photo_data: '',
|
||||||
|
policies_accepted: false, type_fields: {},
|
||||||
|
};
|
||||||
this.resetTimer();
|
this.resetTimer();
|
||||||
},
|
},
|
||||||
|
|
||||||
async startCamera() {
|
async startCamera() {
|
||||||
try {
|
try {
|
||||||
this.cameraStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' } });
|
this.cameraStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' } });
|
||||||
@@ -192,6 +388,7 @@
|
|||||||
alert('Camera unavailable on this device.');
|
alert('Camera unavailable on this device.');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
capturePhoto() {
|
capturePhoto() {
|
||||||
const video = this.$refs.camera;
|
const video = this.$refs.camera;
|
||||||
if (!video?.videoWidth) return;
|
if (!video?.videoWidth) return;
|
||||||
@@ -202,14 +399,17 @@
|
|||||||
this.form.photo_data = canvas.toDataURL('image/jpeg', 0.85);
|
this.form.photo_data = canvas.toDataURL('image/jpeg', 0.85);
|
||||||
this.stopCamera();
|
this.stopCamera();
|
||||||
},
|
},
|
||||||
|
|
||||||
clearPhoto() {
|
clearPhoto() {
|
||||||
this.form.photo_data = '';
|
this.form.photo_data = '';
|
||||||
},
|
},
|
||||||
|
|
||||||
stopCamera() {
|
stopCamera() {
|
||||||
this.cameraStream?.getTracks().forEach(track => track.stop());
|
this.cameraStream?.getTracks().forEach(track => track.stop());
|
||||||
this.cameraStream = null;
|
this.cameraStream = null;
|
||||||
this.cameraActive = false;
|
this.cameraActive = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
startSign(e, field) {
|
startSign(e, field) {
|
||||||
this.signing = { active: true, field };
|
this.signing = { active: true, field };
|
||||||
this.canvases[field] = e.target;
|
this.canvases[field] = e.target;
|
||||||
@@ -220,6 +420,7 @@
|
|||||||
const p = this.pointer(e);
|
const p = this.pointer(e);
|
||||||
ctx.moveTo(p.x, p.y);
|
ctx.moveTo(p.x, p.y);
|
||||||
},
|
},
|
||||||
|
|
||||||
drawSign(e, field) {
|
drawSign(e, field) {
|
||||||
if (!this.signing.active || this.signing.field !== field) return;
|
if (!this.signing.active || this.signing.field !== field) return;
|
||||||
const ctx = this.canvases[field].getContext('2d');
|
const ctx = this.canvases[field].getContext('2d');
|
||||||
@@ -227,18 +428,21 @@
|
|||||||
ctx.lineTo(p.x, p.y);
|
ctx.lineTo(p.x, p.y);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
},
|
},
|
||||||
|
|
||||||
endSign(field) {
|
endSign(field) {
|
||||||
if (this.signing.field === field) {
|
if (this.signing.field === field) {
|
||||||
this.form.type_fields[field] = this.canvases[field].toDataURL('image/png');
|
this.form.type_fields[field] = this.canvases[field].toDataURL('image/png');
|
||||||
}
|
}
|
||||||
this.signing = { active: false, field: null };
|
this.signing = { active: false, field: null };
|
||||||
},
|
},
|
||||||
|
|
||||||
clearSign(field) {
|
clearSign(field) {
|
||||||
const canvas = this.canvases[field];
|
const canvas = this.canvases[field];
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
|
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
|
||||||
this.form.type_fields[field] = '';
|
this.form.type_fields[field] = '';
|
||||||
},
|
},
|
||||||
|
|
||||||
pointer(e) {
|
pointer(e) {
|
||||||
const canvas = e.target;
|
const canvas = e.target;
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
@@ -247,7 +451,10 @@
|
|||||||
const clientY = touch ? touch.clientY : e.clientY;
|
const clientY = touch ? touch.clientY : e.clientY;
|
||||||
return { x: clientX - rect.left, y: clientY - rect.top };
|
return { x: clientX - rect.left, y: clientY - rect.top };
|
||||||
},
|
},
|
||||||
|
|
||||||
async submitCheckIn() {
|
async submitCheckIn() {
|
||||||
|
if (!this.validateFormStep()) return;
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|||||||
Reference in New Issue
Block a user