Add queue kiosk and display devices for Care Queue management.
Deploy Ladill Care / deploy (push) Successful in 39s
Deploy Ladill Care / deploy (push) Successful in 39s
Register walk-up kiosks and waiting-room display devices under Devices, with public kiosk ticket issue and linked TV boards surfaced in sidebar and Queue shortcuts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,14 +3,14 @@
|
||||
<div>
|
||||
<a href="{{ route('care.devices.index') }}" class="text-sm text-slate-500 hover:text-slate-800">← Devices</a>
|
||||
<h1 class="mt-2 text-xl font-semibold text-slate-900">Register device</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Inventory is branch-scoped. Agent devices get a token shown once after create.</p>
|
||||
<p class="mt-1 text-sm text-slate-500">Queue kiosks/displays and clinical scanners/agents. Queue devices need Care Pro.</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('care.devices.store') }}" class="space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
@csrf
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Name</label>
|
||||
<input type="text" name="name" value="{{ old('name') }}" required placeholder="Nurse station scanner"
|
||||
<input type="text" name="name" value="{{ old('name') }}" required placeholder="Lobby kiosk"
|
||||
class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
@@ -18,39 +18,85 @@
|
||||
<select name="type" id="device-type" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($deviceTypes as $value => $label)
|
||||
@php
|
||||
$needsPro = in_array($value, $agentTypes, true);
|
||||
$needsPro = in_array($value, $agentTypes, true) || in_array($value, $queueDeviceTypes, true);
|
||||
$allowed = ! $needsPro || ($value === 'kiosk' || $value === 'display' ? $hasQueueDevices : $hasAgentDevices);
|
||||
@endphp
|
||||
<option value="{{ $value }}"
|
||||
data-mode="{{ $defaultModes[$value] ?? 'manual' }}"
|
||||
data-pro="{{ $needsPro ? '1' : '0' }}"
|
||||
@selected(old('type', 'barcode_scanner') === $value)
|
||||
@disabled($needsPro && ! $hasAgentDevices)>
|
||||
{{ $label }}@if ($needsPro && ! $hasAgentDevices) (Pro)@endif
|
||||
data-queue="{{ in_array($value, $queueDeviceTypes, true) ? '1' : '0' }}"
|
||||
data-display="{{ $value === 'display' ? '1' : '0' }}"
|
||||
@selected(old('type', $prefillType ?? 'barcode_scanner') === $value)
|
||||
@disabled(! $allowed)>
|
||||
{{ $label }}@if ($needsPro && ! $allowed) (Pro)@endif
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if (! $hasAgentDevices)
|
||||
<p class="mt-1 text-xs text-amber-700">Clinical agent devices need Care Pro. USB barcode/QR scanners work on Free.</p>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<div id="connection-wrap">
|
||||
<label class="block text-sm font-medium text-slate-700">Connection mode</label>
|
||||
<select name="connection_mode" id="connection-mode" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($connectionModes as $value => $label)
|
||||
<option value="{{ $value }}" @selected(old('connection_mode', 'browser_wedge') === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-slate-500" id="connection-hint">USB scanners act as a keyboard in the browser. Serial/BLE needs the local agent.</p>
|
||||
<p class="mt-1 text-xs text-slate-500" id="connection-hint">USB scanners act as a keyboard in the browser.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Branch</label>
|
||||
<select name="branch_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
<select name="branch_id" id="branch-id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
<option value="">All branches</option>
|
||||
@foreach ($branches as $branch)
|
||||
<option value="{{ $branch->id }}" @selected(old('branch_id') == $branch->id)>{{ $branch->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-slate-500" id="branch-hint" hidden>Queue displays require a branch.</p>
|
||||
</div>
|
||||
|
||||
<div id="queue-config" class="space-y-3 border-t border-slate-100 pt-4" hidden>
|
||||
<p class="text-sm font-medium text-slate-800">Service queues</p>
|
||||
<p class="text-xs text-slate-500">Leave unchecked to allow all active queues for the branch.</p>
|
||||
<div class="max-h-40 space-y-2 overflow-y-auto rounded-lg border border-slate-100 p-3">
|
||||
@forelse ($queues as $queue)
|
||||
<label class="flex items-center gap-2 text-sm text-slate-700">
|
||||
<input type="checkbox" name="queue_ids[]" value="{{ $queue->id }}"
|
||||
@checked(collect(old('queue_ids', []))->contains($queue->id))
|
||||
class="rounded border-slate-300">
|
||||
{{ $queue->name }} <span class="text-xs text-slate-400">({{ $queue->prefix }})</span>
|
||||
</label>
|
||||
@empty
|
||||
<p class="text-xs text-amber-700">No service queues yet — enable service queues in Settings and check patients in.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div id="kiosk-options" class="space-y-3">
|
||||
<label class="flex items-center gap-2 text-sm text-slate-700">
|
||||
<input type="checkbox" name="collect_name" value="1" @checked(old('collect_name')) class="rounded border-slate-300">
|
||||
Collect visitor name
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-slate-700">
|
||||
<input type="checkbox" name="collect_phone" value="1" @checked(old('collect_phone')) class="rounded border-slate-300">
|
||||
Collect phone
|
||||
</label>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Reset after (seconds)</label>
|
||||
<input type="number" name="reset_seconds" min="5" max="120" value="{{ old('reset_seconds', 15) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Welcome message</label>
|
||||
<input type="text" name="welcome_message" value="{{ old('welcome_message') }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm" placeholder="Optional">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="display-options" hidden>
|
||||
<label class="block text-sm font-medium text-slate-700">Display layout</label>
|
||||
<select name="layout" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($layouts as $value => $label)
|
||||
<option value="{{ $value }}" @selected(old('layout', 'standard') === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary w-full">Register device</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -59,18 +105,30 @@
|
||||
const typeEl = document.getElementById('device-type');
|
||||
const modeEl = document.getElementById('connection-mode');
|
||||
const hintEl = document.getElementById('connection-hint');
|
||||
const connWrap = document.getElementById('connection-wrap');
|
||||
const queueConfig = document.getElementById('queue-config');
|
||||
const kioskOpts = document.getElementById('kiosk-options');
|
||||
const displayOpts = document.getElementById('display-options');
|
||||
const branchHint = document.getElementById('branch-hint');
|
||||
const hints = {
|
||||
browser_wedge: 'Works in the browser today as a USB keyboard wedge. No local agent required.',
|
||||
agent: 'Needs the Care Device Agent on a local machine (serial / BLE / vendor SDK).',
|
||||
web_bluetooth: 'Experimental Web Bluetooth — prefer the local agent for clinical devices.',
|
||||
manual: 'Manual / inventory only — no automated capture path.',
|
||||
manual: 'Manual / inventory — or open the public URL for kiosk / display devices.',
|
||||
};
|
||||
function sync() {
|
||||
const opt = typeEl.selectedOptions[0];
|
||||
if (!opt) return;
|
||||
const mode = opt.dataset.mode || 'manual';
|
||||
const isQueue = opt.dataset.queue === '1';
|
||||
const isDisplay = opt.dataset.display === '1';
|
||||
modeEl.value = mode;
|
||||
hintEl.textContent = hints[mode] || hints.manual;
|
||||
connWrap.hidden = isQueue;
|
||||
queueConfig.hidden = !isQueue;
|
||||
kioskOpts.hidden = isDisplay;
|
||||
displayOpts.hidden = !isDisplay;
|
||||
branchHint.hidden = !isDisplay;
|
||||
}
|
||||
typeEl.addEventListener('change', sync);
|
||||
modeEl.addEventListener('change', () => {
|
||||
|
||||
@@ -8,9 +8,30 @@
|
||||
|
||||
@if ($plainToken)
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-950">
|
||||
<p class="font-medium">Device token (copy now — shown once)</p>
|
||||
<p class="font-medium">Device token (copy now)</p>
|
||||
<code class="mt-2 block break-all rounded-lg bg-white px-3 py-2 font-mono text-xs text-slate-800">{{ $plainToken }}</code>
|
||||
<p class="mt-2 text-xs text-amber-800">Send as <code class="font-mono">X-Care-Device-Token</code> or <code class="font-mono">Authorization: Bearer …</code> from the Care Device Agent.</p>
|
||||
@if ($device->type === 'kiosk')
|
||||
<p class="mt-2 text-xs text-amber-800">Kiosk URL:
|
||||
<a class="font-medium underline" href="{{ route('care.kiosk.device', $plainToken) }}" target="_blank" rel="noopener">
|
||||
{{ route('care.kiosk.device', $plainToken) }}
|
||||
</a>
|
||||
</p>
|
||||
@elseif ($device->type !== 'display')
|
||||
<p class="mt-2 text-xs text-amber-800">Send as <code class="font-mono">X-Care-Device-Token</code> from the Care Device Agent.</p>
|
||||
@endif
|
||||
</div>
|
||||
@elseif ($kioskUrl)
|
||||
<div class="rounded-2xl border border-teal-200 bg-teal-50 px-4 py-3 text-sm text-teal-950">
|
||||
<p class="font-medium">Kiosk URL</p>
|
||||
<a class="mt-2 block break-all font-mono text-xs underline" href="{{ $kioskUrl }}" target="_blank" rel="noopener">{{ $kioskUrl }}</a>
|
||||
</div>
|
||||
@elseif ($displayPublicUrl)
|
||||
<div class="rounded-2xl border border-sky-200 bg-sky-50 px-4 py-3 text-sm text-sky-950">
|
||||
<p class="font-medium">Display board URL</p>
|
||||
<a class="mt-2 block break-all font-mono text-xs underline" href="{{ $displayPublicUrl }}" target="_blank" rel="noopener">{{ $displayPublicUrl }}</a>
|
||||
@if ($displayScreen)
|
||||
<a href="{{ route('care.displays.show', $displayScreen) }}" class="mt-2 inline-block text-xs font-medium text-sky-800 underline">Manage waiting display</a>
|
||||
@endif
|
||||
</div>
|
||||
@elseif ($device->hasToken())
|
||||
<div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-700">
|
||||
@@ -28,22 +49,33 @@
|
||||
<label class="block text-sm font-medium text-slate-700">Type</label>
|
||||
<select name="type" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($deviceTypes as $value => $label)
|
||||
@php $needsPro = in_array($value, $agentTypes, true); @endphp
|
||||
@php
|
||||
$needsPro = in_array($value, $agentTypes, true) || in_array($value, $queueDeviceTypes, true);
|
||||
$allowed = ! $needsPro
|
||||
|| $device->type === $value
|
||||
|| ($value === 'kiosk' || $value === 'display' ? $hasQueueDevices : $hasAgentDevices);
|
||||
@endphp
|
||||
<option value="{{ $value }}" @selected(old('type', $device->type) === $value)
|
||||
@disabled($needsPro && ! $hasAgentDevices && $device->type !== $value)>
|
||||
@disabled(! $allowed)>
|
||||
{{ $label }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Connection mode</label>
|
||||
<select name="connection_mode" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($connectionModes as $value => $label)
|
||||
<option value="{{ $value }}" @selected(old('connection_mode', $device->connection_mode) === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (! in_array($device->type, $queueDeviceTypes, true))
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Connection mode</label>
|
||||
<select name="connection_mode" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($connectionModes as $value => $label)
|
||||
<option value="{{ $value }}" @selected(old('connection_mode', $device->connection_mode) === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@else
|
||||
<input type="hidden" name="connection_mode" value="{{ $device->connection_mode }}">
|
||||
@endif
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Status</label>
|
||||
<select name="status" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@@ -61,6 +93,52 @@
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (in_array($device->type, $queueDeviceTypes, true))
|
||||
@php $selectedQueues = collect(old('queue_ids', $device->metadata['service_queue_ids'] ?? [])); @endphp
|
||||
<div class="space-y-2 border-t border-slate-100 pt-4">
|
||||
<p class="text-sm font-medium text-slate-800">Service queues</p>
|
||||
<div class="max-h-40 space-y-2 overflow-y-auto rounded-lg border border-slate-100 p-3">
|
||||
@foreach ($queues as $queue)
|
||||
<label class="flex items-center gap-2 text-sm text-slate-700">
|
||||
<input type="checkbox" name="queue_ids[]" value="{{ $queue->id }}"
|
||||
@checked($selectedQueues->contains($queue->id))
|
||||
class="rounded border-slate-300">
|
||||
{{ $queue->name }}
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@if ($device->type === 'kiosk')
|
||||
<label class="flex items-center gap-2 text-sm text-slate-700">
|
||||
<input type="checkbox" name="collect_name" value="1" @checked(old('collect_name', $kioskSettings['collect_name'] ?? false)) class="rounded border-slate-300">
|
||||
Collect visitor name
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-slate-700">
|
||||
<input type="checkbox" name="collect_phone" value="1" @checked(old('collect_phone', $kioskSettings['collect_phone'] ?? false)) class="rounded border-slate-300">
|
||||
Collect phone
|
||||
</label>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Reset after (seconds)</label>
|
||||
<input type="number" name="reset_seconds" min="5" max="120" value="{{ old('reset_seconds', $kioskSettings['reset_seconds'] ?? 15) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Welcome message</label>
|
||||
<input type="text" name="welcome_message" value="{{ old('welcome_message', $kioskSettings['welcome_message'] ?? '') }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
@endif
|
||||
@if ($device->type === 'display')
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Display layout</label>
|
||||
<select name="layout" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($layouts as $value => $label)
|
||||
<option value="{{ $value }}" @selected(old('layout', $displayScreen?->layout ?? 'standard') === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button type="submit" class="btn-primary">Save</button>
|
||||
<a href="{{ route('care.devices.index') }}" class="rounded-lg border border-slate-200 px-4 py-2 text-sm text-slate-600">Cancel</a>
|
||||
@@ -71,7 +149,7 @@
|
||||
<form method="POST" action="{{ route('care.devices.token.regenerate', $device) }}">
|
||||
@csrf
|
||||
<button type="submit" class="rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm text-slate-700 hover:bg-slate-50">
|
||||
{{ $device->hasToken() ? 'Regenerate token' : 'Issue agent token' }}
|
||||
{{ $device->hasToken() ? 'Regenerate token' : 'Issue token' }}
|
||||
</button>
|
||||
</form>
|
||||
@if ($device->hasToken())
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
<x-app-layout title="Devices">
|
||||
<div class="space-y-6">
|
||||
<x-care.page-hero
|
||||
badge="Scanners · Vitals · Lab agents"
|
||||
badge="Queue · Scanners · Agents"
|
||||
title="Devices"
|
||||
description="Branch inventory for barcode scanners (browser wedge) and agent-connected clinical devices. USB scanners work in the browser; serial/BLE hardware needs the Care Device Agent."
|
||||
description="Register queue kiosks and waiting displays, plus barcode scanners and agent-connected clinical hardware."
|
||||
:stats="[
|
||||
['value' => number_format($heroStats['total']), 'label' => 'Devices'],
|
||||
['value' => number_format($heroStats['kiosks'] ?? 0), 'label' => 'Kiosks'],
|
||||
['value' => number_format($heroStats['displays'] ?? 0), 'label' => 'Displays'],
|
||||
['value' => number_format($heroStats['online']), 'label' => 'Online'],
|
||||
['value' => number_format($heroStats['agent']), 'label' => 'Agent-linked'],
|
||||
]">
|
||||
@if ($canManage)
|
||||
<x-slot name="actions">
|
||||
@if ($hasQueueDevices ?? false)
|
||||
<a href="{{ route('care.devices.create', ['type' => 'kiosk']) }}" class="btn-secondary">Add kiosk</a>
|
||||
<a href="{{ route('care.devices.create', ['type' => 'display']) }}" class="btn-secondary">Add display</a>
|
||||
@endif
|
||||
<a href="{{ route('care.devices.create') }}" class="btn-primary">Add device</a>
|
||||
</x-slot>
|
||||
@endif
|
||||
</x-care.page-hero>
|
||||
|
||||
<div class="rounded-2xl border border-sky-100 bg-sky-50 px-4 py-3 text-sm text-sky-900">
|
||||
<p class="font-medium">How devices connect</p>
|
||||
<p class="font-medium">Queue devices vs clinical devices</p>
|
||||
<ul class="mt-1 list-disc space-y-1 pl-5 text-sky-800/90">
|
||||
<li><strong>Barcode / QR scanners</strong> — USB keyboard wedge works in Patients and Lab today. Free on all plans.</li>
|
||||
<li><strong>Thermometers, BP, SpO₂, scales, analyzers</strong> — need a local Care Device Agent (Pro / Enterprise). See docs/devices.md.</li>
|
||||
<li><strong>Queue kiosk</strong> — walk-up ticket issue on a tablet (Pro).</li>
|
||||
<li><strong>Queue display</strong> — registers a waiting-room TV board URL (Pro).</li>
|
||||
<li><strong>Barcode / QR scanners</strong> — USB keyboard wedge in Patients and Lab (all plans).</li>
|
||||
<li><strong>Vitals / analyzers</strong> — Care Device Agent (Pro).</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<meta name="theme-color" content="#f1f5f9">
|
||||
<title>{{ $device->name }} · Care Kiosk</title>
|
||||
<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">
|
||||
@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(13 148 136 / 0.45); }
|
||||
50% { transform: scale(1.02); box-shadow: 0 24px 48px -10px rgb(13 148 136 / 0.55); }
|
||||
}
|
||||
.kiosk-tap-btn { animation: kiosk-tap-pulse 2.4s ease-in-out infinite; }
|
||||
[x-cloak] { display: none !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body
|
||||
class="min-h-screen bg-slate-100 font-sans text-slate-900 antialiased"
|
||||
x-data="careKioskFlow(@js([
|
||||
'issueUrl' => route('care.kiosk.device.issue', $kioskToken),
|
||||
'csrf' => csrf_token(),
|
||||
'queues' => $queues->map(fn ($q) => [
|
||||
'id' => $q->id,
|
||||
'name' => $q->name,
|
||||
])->values()->all(),
|
||||
'collectName' => $settings['collect_name'],
|
||||
'collectPhone' => $settings['collect_phone'],
|
||||
'resetSeconds' => $settings['reset_seconds'],
|
||||
'welcomeMessage' => $settings['welcome_message'] ?? null,
|
||||
]))"
|
||||
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-teal-200/40 blur-3xl"></div>
|
||||
<div class="absolute -right-16 bottom-0 h-80 w-80 rounded-full bg-sky-200/40 blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
<div class="fixed inset-x-0 top-0 z-10 h-1.5 bg-gradient-to-r from-teal-600 to-sky-600"></div>
|
||||
|
||||
<div class="fixed left-0 top-0 z-20 p-6">
|
||||
<img src="{{ $logoUrl }}" alt="{{ $logoAlt }}" class="h-8 w-auto max-w-[220px] object-contain object-left">
|
||||
</div>
|
||||
|
||||
<div class="relative mx-auto flex min-h-screen max-w-3xl flex-col pt-20">
|
||||
<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-teal-700">Ladill Care</p>
|
||||
<h1 class="mt-3 text-4xl font-bold tracking-tight text-slate-900 sm:text-5xl">
|
||||
Welcome to {{ $organization?->name ?? 'Care' }}
|
||||
</h1>
|
||||
<p class="mt-4 text-lg text-slate-500" x-text="welcomeMessage || 'Tap below to get your ticket'"></p>
|
||||
</div>
|
||||
<button type="button" @click="beginKiosk()" class="kiosk-tap-btn btn-primary mt-16 w-full max-w-lg py-8 text-xl font-bold" :disabled="loading">
|
||||
<span x-show="!loading">Tap to begin</span>
|
||||
<span x-show="loading">Please wait…</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="step === 'select'">
|
||||
<div class="relative flex min-h-[calc(100vh-5rem)] flex-col items-center justify-center px-6 py-12">
|
||||
<button type="button" @click="goBack()" class="absolute left-6 top-8 z-10 text-sm font-medium text-slate-500 hover:text-slate-800">← Back</button>
|
||||
<div class="max-w-xl text-center">
|
||||
<h2 class="text-2xl font-bold text-slate-900 sm:text-3xl">Choose a service</h2>
|
||||
<p class="mt-2 text-slate-500">Select an option to get your ticket</p>
|
||||
</div>
|
||||
|
||||
<template x-if="queues.length >= 1">
|
||||
<div class="mt-12 grid w-full max-w-2xl gap-4" :class="queues.length > 1 ? 'sm:grid-cols-2' : ''">
|
||||
<template x-for="queue in queues" :key="queue.id">
|
||||
<button
|
||||
type="button"
|
||||
class="group flex flex-col items-center rounded-3xl border-2 border-slate-200 bg-white px-6 py-10 text-center shadow-sm transition hover:border-teal-300 hover:shadow-md active:scale-[0.98]"
|
||||
:disabled="loading"
|
||||
@click="selectQueue(queue.id)"
|
||||
>
|
||||
<span class="text-xl font-bold text-slate-900" x-text="queue.name"></span>
|
||||
<span class="mt-1 text-sm text-slate-500">Tap to get ticket</span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="queues.length === 0">
|
||||
<div class="mt-12 w-full max-w-lg rounded-3xl border border-slate-200 bg-white p-8 text-center shadow-sm">
|
||||
<p class="text-lg font-semibold text-slate-900">No services available</p>
|
||||
<p class="mt-2 text-sm text-slate-500">Ask a staff member for assistance.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<p x-show="error" x-cloak class="mt-6 max-w-lg rounded-xl bg-red-50 px-4 py-3 text-sm text-red-700" x-text="error"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="step === 'details'">
|
||||
<div class="relative flex min-h-[calc(100vh-5rem)] flex-col justify-center px-6 py-8">
|
||||
<button type="button" @click="goBack()" class="absolute left-6 top-8 z-10 text-sm font-medium text-slate-500 hover:text-slate-800">← Back</button>
|
||||
<div class="mx-auto w-full max-w-lg rounded-3xl border border-slate-200 bg-white p-6 shadow-sm">
|
||||
<p class="text-sm font-semibold text-slate-900">Your details</p>
|
||||
<p x-show="error" x-cloak class="mt-4 rounded-xl bg-red-50 px-4 py-3 text-sm text-red-700" x-text="error"></p>
|
||||
<div class="mt-4 space-y-4">
|
||||
<div x-show="collectName">
|
||||
<label class="block text-sm font-medium text-slate-600">Name</label>
|
||||
<input type="text" x-model="customerName" placeholder="Your name" autocomplete="name" class="mt-1 w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
|
||||
</div>
|
||||
<div x-show="collectPhone">
|
||||
<label class="block text-sm font-medium text-slate-600">Phone</label>
|
||||
<input type="tel" x-model="customerPhone" placeholder="Phone number" autocomplete="tel" class="mt-1 w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn-primary mt-6 w-full py-3" @click="issue()" :disabled="loading">
|
||||
<span x-show="!loading">Get ticket</span>
|
||||
<span x-show="loading">Please wait…</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="step === 'ticket' && ticket">
|
||||
<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-teal-100 text-4xl text-teal-700">✓</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900">You're in the queue!</h2>
|
||||
<p class="mt-4 text-sm font-semibold uppercase tracking-wider text-slate-500">Your ticket number</p>
|
||||
<p class="mt-2 text-5xl font-bold tracking-tight text-slate-900" x-text="ticket?.ticket_number"></p>
|
||||
<p class="mt-2 text-sm text-slate-500" x-show="ticket?.queue?.name" x-text="ticket?.queue?.name"></p>
|
||||
<p class="mt-6 text-sm text-slate-500">Please take a seat. Your number will be called on the display.</p>
|
||||
<button type="button" class="btn-primary mt-8 w-full py-3 text-lg" @click="reset()">
|
||||
Done
|
||||
<span x-show="resetCountdown" x-text="'(' + resetCountdown + 's)'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -20,6 +20,19 @@
|
||||
@if ($canManageQueue)
|
||||
<x-slot name="actions">
|
||||
<a href="{{ route('care.appointments.walk-in.create') }}" class="btn-primary">Walk-in</a>
|
||||
@if (app(\App\Services\Care\CarePermissions::class)->can(
|
||||
auth()->user() ? app(\App\Services\Care\OrganizationResolver::class)->memberFor(auth()->user()) : null,
|
||||
'devices.view'
|
||||
))
|
||||
<a href="{{ route('care.devices.create', ['type' => 'kiosk']) }}" class="btn-secondary">Add kiosk</a>
|
||||
<a href="{{ route('care.devices.create', ['type' => 'display']) }}" class="btn-secondary">Add display</a>
|
||||
@endif
|
||||
@if (app(\App\Services\Care\CarePermissions::class)->can(
|
||||
auth()->user() ? app(\App\Services\Care\OrganizationResolver::class)->memberFor(auth()->user()) : null,
|
||||
'displays.view'
|
||||
))
|
||||
<a href="{{ route('care.displays.index') }}" class="btn-secondary">Displays</a>
|
||||
@endif
|
||||
</x-slot>
|
||||
@endif
|
||||
</x-care.page-hero>
|
||||
|
||||
@@ -94,14 +94,14 @@
|
||||
<span class="ml-2 rounded-full bg-amber-50 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-700">Agent · Pro</span>
|
||||
@endif
|
||||
<span class="mt-0.5 block text-xs text-slate-500">
|
||||
Barcode scanners (browser) · agent-connected vitals / lab hardware
|
||||
Queue kiosks & displays · barcode scanners · agent vitals / lab hardware
|
||||
</span>
|
||||
</span>
|
||||
<span class="text-slate-400">→</span>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if ($canViewDisplays && ($queueIntegrationEnabled || $canUseQueueIntegration))
|
||||
@if ($canViewDisplays && ! empty($hasPaidPlan))
|
||||
<li>
|
||||
<a href="{{ route('care.displays.index') }}"
|
||||
class="flex items-center justify-between rounded-xl border border-slate-100 bg-slate-50 px-4 py-3 text-sm text-slate-700 hover:text-indigo-700">
|
||||
@@ -116,6 +116,21 @@
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if ($canViewDevices && ! empty($hasPaidPlan))
|
||||
<li>
|
||||
<a href="{{ route('care.devices.create', ['type' => 'kiosk']) }}"
|
||||
class="flex items-center justify-between rounded-xl border border-slate-100 bg-slate-50 px-4 py-3 text-sm text-slate-700 hover:text-indigo-700">
|
||||
<span>
|
||||
Queue kiosk
|
||||
<span class="ml-2 rounded-full bg-amber-50 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-700">Pro</span>
|
||||
<span class="mt-0.5 block text-xs text-slate-500">
|
||||
Walk-up ticket issue on a tablet
|
||||
</span>
|
||||
</span>
|
||||
<span class="text-slate-400">→</span>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
<li>
|
||||
<a href="{{ route('care.settings.gp-pricing') }}"
|
||||
class="flex items-center justify-between rounded-xl border border-slate-100 bg-slate-50 px-4 py-3 text-sm text-slate-700 hover:text-indigo-700">
|
||||
|
||||
@@ -32,6 +32,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($hasPaidPlan) && $permissions->can($member, 'displays.view')) {
|
||||
$nav[] = ['name' => 'Displays', 'route' => route('care.displays.index'), 'active' => request()->routeIs('care.displays.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M6 20.25h12m-7.5-4v4m3-4v4M6.75 4.5h10.5a2.25 2.25 0 0 1 2.25 2.25v6.75a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 13.5V6.75A2.25 2.25 0 0 1 6.75 4.5Z" />'];
|
||||
}
|
||||
if (! empty($hasPaidPlan) && $permissions->can($member, 'devices.view')) {
|
||||
$nav[] = ['name' => 'Devices', 'route' => route('care.devices.index'), 'active' => request()->routeIs('care.devices.*') || request()->routeIs('care.kiosk.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 0 0 6 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h7.5A2.25 2.25 0 0 0 18 20.25V3.75a2.25 2.25 0 0 0-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />'];
|
||||
}
|
||||
|
||||
// Clinical assessments (layered intake / pathways) — on by default; toggle in Settings.
|
||||
$assessmentsEngineOn = $organization
|
||||
&& app(\App\Services\Care\CareFeatures::class)->enabled(
|
||||
|
||||
Reference in New Issue
Block a user