Initial Ladill Queue release — enterprise QMS standalone app.
Deploy Ladill Queue / deploy (push) Successful in 56s

Phases 1–6: tickets, counters, displays, appointments, workflows, rules, analytics, reports, feedback, admin, device API, and Gitea deploy workflow for queue.ladill.com.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-29 20:19:52 +00:00
co-authored by Cursor
commit cca98eefd2
297 changed files with 27263 additions and 0 deletions
@@ -0,0 +1,13 @@
<x-app-layout title="New branch">
<div class="mx-auto max-w-lg">
<h1 class="text-2xl font-semibold">New branch</h1>
<form method="POST" action="{{ route('qms.branches.store') }}" class="mt-6 space-y-4 rounded-2xl border bg-white p-6 dark:border-slate-700 dark:bg-slate-900">
@csrf
<div><label class="block text-sm font-medium">Name</label><input name="name" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<div><label class="block text-sm font-medium">Code</label><input name="code" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<div><label class="block text-sm font-medium">Address</label><input name="address" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<div><label class="block text-sm font-medium">Phone</label><input name="phone" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<button type="submit" class="btn-primary w-full">Create branch</button>
</form>
</div>
</x-app-layout>
@@ -0,0 +1,14 @@
<x-app-layout title="Edit branch">
<div class="mx-auto max-w-lg">
<h1 class="text-2xl font-semibold">Edit branch</h1>
<form method="POST" action="{{ route('qms.branches.update', $branch) }}" class="mt-6 space-y-4 rounded-2xl border bg-white p-6 dark:border-slate-700 dark:bg-slate-900">
@csrf @method('PUT')
<div><label class="block text-sm font-medium">Name</label><input name="name" value="{{ $branch->name }}" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<div><label class="block text-sm font-medium">Code</label><input name="code" value="{{ $branch->code }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<div><label class="block text-sm font-medium">Address</label><input name="address" value="{{ $branch->address }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<div><label class="block text-sm font-medium">Phone</label><input name="phone" value="{{ $branch->phone }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="is_active" value="1" @checked($branch->is_active)> Active</label>
<button type="submit" class="btn-primary w-full">Save changes</button>
</form>
</div>
</x-app-layout>
@@ -0,0 +1,31 @@
<x-app-layout title="Branches">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-semibold">Branches</h1>
@if(app(\App\Services\Qms\QmsPermissions::class)->can(app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()), 'admin.branches.manage'))
<a href="{{ route('qms.branches.create') }}" class="btn-primary">New branch</a>
@endif
</div>
@include('partials.flash')
<div class="overflow-hidden rounded-2xl border bg-white dark:border-slate-700 dark:bg-slate-900">
<table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50"><tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Name</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Code</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Departments</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Status</th>
<th></th>
</tr></thead>
<tbody class="divide-y divide-slate-200">
@foreach ($branches as $branch)
<tr>
<td class="px-4 py-3 text-sm font-medium">{{ $branch->name }}</td>
<td class="px-4 py-3 text-sm">{{ $branch->code ?? '—' }}</td>
<td class="px-4 py-3 text-sm">{{ $branch->departments_count }}</td>
<td class="px-4 py-3 text-sm">{{ $branch->is_active ? 'Active' : 'Inactive' }}</td>
<td class="px-4 py-3 text-right"><a href="{{ route('qms.branches.edit', $branch) }}" class="text-sm text-indigo-600">Edit</a></td>
</tr>
@endforeach
</tbody>
</table>
</div>
</x-app-layout>
@@ -0,0 +1,16 @@
<x-app-layout title="New department">
<div class="mx-auto max-w-lg">
<h1 class="text-2xl font-semibold">New department</h1>
<form method="POST" action="{{ route('qms.departments.store') }}" class="mt-6 space-y-4 rounded-2xl border bg-white p-6">
@csrf
<div><label class="block text-sm font-medium">Branch</label>
<select name="branch_id" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">@foreach ($branches as $b)<option value="{{ $b->id }}">{{ $b->name }}</option>@endforeach</select>
</div>
<div><label class="block text-sm font-medium">Name</label><input name="name" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<div><label class="block text-sm font-medium">Type</label>
<select name="type" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">@foreach ($types as $v => $l)<option value="{{ $v }}">{{ $l }}</option>@endforeach</select>
</div>
<button type="submit" class="btn-primary w-full">Create</button>
</form>
</div>
</x-app-layout>
@@ -0,0 +1,14 @@
<x-app-layout title="Edit department">
<div class="mx-auto max-w-lg">
<h1 class="text-2xl font-semibold">Edit department</h1>
<form method="POST" action="{{ route('qms.departments.update', $department) }}" class="mt-6 space-y-4 rounded-2xl border bg-white p-6">
@csrf @method('PUT')
<div><label class="block text-sm font-medium">Name</label><input name="name" value="{{ $department->name }}" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<div><label class="block text-sm font-medium">Type</label>
<select name="type" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">@foreach ($types as $v => $l)<option value="{{ $v }}" @selected($department->type === $v)>{{ $l }}</option>@endforeach</select>
</div>
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="is_active" value="1" @checked($department->is_active)> Active</label>
<button type="submit" class="btn-primary w-full">Save</button>
</form>
</div>
</x-app-layout>
@@ -0,0 +1,31 @@
<x-app-layout title="Departments">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-semibold">Departments</h1>
@if(app(\App\Services\Qms\QmsPermissions::class)->can(app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()), 'admin.departments.manage'))
<a href="{{ route('qms.departments.create') }}" class="btn-primary">New department</a>
@endif
</div>
@include('partials.flash')
<div class="overflow-hidden rounded-2xl border bg-white dark:border-slate-700 dark:bg-slate-900">
<table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50"><tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Name</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Branch</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Type</th>
<th></th>
</tr></thead>
<tbody class="divide-y divide-slate-200">
@forelse ($departments as $department)
<tr>
<td class="px-4 py-3 text-sm">{{ $department->name }}</td>
<td class="px-4 py-3 text-sm">{{ $department->branch?->name }}</td>
<td class="px-4 py-3 text-sm">{{ $types[$department->type] ?? $department->type }}</td>
<td class="px-4 py-3 text-right"><a href="{{ route('qms.departments.edit', $department) }}" class="text-sm text-indigo-600">Edit</a></td>
</tr>
@empty
<tr><td colspan="4" class="px-6 py-12 text-center text-sm text-slate-500">No departments.</td></tr>
@endforelse
</tbody>
</table>
</div>
</x-app-layout>
@@ -0,0 +1,16 @@
<x-app-layout title="Add member">
<div class="mx-auto max-w-lg">
<h1 class="text-2xl font-semibold">Add team member</h1>
<form method="POST" action="{{ route('qms.members.store') }}" class="mt-6 space-y-4 rounded-2xl border bg-white p-6">
@csrf
<div><label class="block text-sm font-medium">Platform user ref (public_id)</label><input name="user_ref" required class="mt-1 w-full rounded-lg border-slate-300 text-sm font-mono"></div>
<div><label class="block text-sm font-medium">Role</label>
<select name="role" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">@foreach ($roles as $v => $l)<option value="{{ $v }}">{{ $l }}</option>@endforeach</select>
</div>
<div><label class="block text-sm font-medium">Branch scope (optional)</label>
<select name="branch_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm"><option value="">Organization-wide</option>@foreach ($branches as $b)<option value="{{ $b->id }}">{{ $b->name }}</option>@endforeach</select>
</div>
<button type="submit" class="btn-primary w-full">Save member</button>
</form>
</div>
</x-app-layout>
@@ -0,0 +1,33 @@
<x-app-layout title="Team">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-semibold">Team members</h1>
@if(app(\App\Services\Qms\QmsPermissions::class)->can(app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()), 'admin.members.manage'))
<a href="{{ route('qms.members.create') }}" class="btn-primary">Add member</a>
@endif
</div>
@include('partials.flash')
<div class="overflow-hidden rounded-2xl border bg-white dark:border-slate-700 dark:bg-slate-900">
<table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50"><tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">User ref</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Role</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Branch</th>
<th></th>
</tr></thead>
<tbody class="divide-y divide-slate-200">
@foreach ($members as $member)
<tr>
<td class="px-4 py-3 text-sm font-mono text-xs">{{ $member->user_ref }}</td>
<td class="px-4 py-3 text-sm">{{ $roles[$member->role] ?? $member->role }}</td>
<td class="px-4 py-3 text-sm">{{ $member->branch?->name ?? 'All' }}</td>
<td class="px-4 py-3 text-right">
@if ($member->user_ref !== auth()->user()->public_id)
<form method="POST" action="{{ route('qms.members.destroy', $member) }}">@csrf @method('DELETE')<button class="text-xs text-red-600">Remove</button></form>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</x-app-layout>
@@ -0,0 +1,42 @@
<x-app-layout title="Analytics">
<h1 class="mb-6 text-2xl font-semibold">Analytics</h1>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
@foreach ([
'waiting' => 'Waiting now',
'serving' => 'Being served',
'served_today' => 'Served today',
'no_shows_today' => 'No-shows today',
'avg_wait_seconds' => 'Avg wait (sec)',
'appointments_today' => 'Appointments today',
'feedback_avg_30d' => 'Avg rating (30d)',
] as $key => $label)
<div class="rounded-2xl border bg-white p-4 dark:border-slate-700 dark:bg-slate-900">
<div class="text-xs font-semibold uppercase text-slate-500">{{ $label }}</div>
<div class="mt-2 text-2xl font-semibold">{{ $overview[$key] ?? 0 }}</div>
</div>
@endforeach
</div>
<div class="mt-8 grid gap-6 lg:grid-cols-2">
<div class="rounded-2xl border bg-white p-6 dark:border-slate-700 dark:bg-slate-900">
<h2 class="text-sm font-semibold uppercase text-slate-500">14-day trend</h2>
<table class="mt-4 w-full text-sm">
<thead><tr><th class="text-left py-1">Date</th><th class="text-right">Issued</th><th class="text-right">Completed</th></tr></thead>
<tbody>
@foreach ($trend as $day)
<tr class="border-t"><td class="py-1">{{ $day['date'] }}</td><td class="text-right">{{ $day['issued'] }}</td><td class="text-right">{{ $day['completed'] }}</td></tr>
@endforeach
</tbody>
</table>
</div>
<div class="rounded-2xl border bg-white p-6 dark:border-slate-700 dark:bg-slate-900">
<h2 class="text-sm font-semibold uppercase text-slate-500">Busiest queues</h2>
<ul class="mt-4 space-y-2 text-sm">
@forelse ($overview['busiest_queues'] ?? [] as $q)
<li class="flex justify-between"><span>{{ $q['name'] }}</span><span class="font-medium">{{ $q['waiting'] }} waiting</span></li>
@empty
<li class="text-slate-500">No active queues.</li>
@endforelse
</ul>
</div>
</div>
</x-app-layout>
@@ -0,0 +1,42 @@
<x-app-layout title="New appointment">
<div class="mx-auto max-w-lg">
<h1 class="text-2xl font-semibold">Schedule appointment</h1>
<form method="POST" action="{{ route('qms.appointments.store') }}" class="mt-6 space-y-4 rounded-2xl border bg-white p-6 dark:border-slate-700 dark:bg-slate-900">
@csrf
<div>
<label class="block text-sm font-medium">Branch</label>
<select name="branch_id" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@foreach ($branches as $branch)
<option value="{{ $branch->id }}">{{ $branch->name }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium">Queue (optional)</label>
<select name="service_queue_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<option value="">Auto-select at check-in</option>
@foreach ($queues as $queue)
<option value="{{ $queue->id }}">{{ $queue->name }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium">Customer name</label>
<input name="customer_name" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-sm font-medium">Phone</label>
<input name="customer_phone" type="tel" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-sm font-medium">Email</label>
<input name="customer_email" type="email" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-sm font-medium">Scheduled at</label>
<input name="scheduled_at" type="datetime-local" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<button type="submit" class="btn-primary w-full">Save appointment</button>
</form>
</div>
</x-app-layout>
@@ -0,0 +1,18 @@
<x-app-layout title="Edit appointment">
<div class="mx-auto max-w-lg">
<h1 class="text-2xl font-semibold">Edit appointment</h1>
<form method="POST" action="{{ route('qms.appointments.update', $appointment) }}" class="mt-6 space-y-4 rounded-2xl border bg-white p-6">
@csrf @method('PUT')
<div><label class="block text-sm font-medium">Branch</label>
<select name="branch_id" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">@foreach($branches as $b)<option value="{{ $b->id }}" @selected($appointment->branch_id==$b->id)>{{ $b->name }}</option>@endforeach</select>
</div>
<div><label class="block text-sm font-medium">Queue</label>
<select name="service_queue_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm"><option value="">Auto</option>@foreach($queues as $q)<option value="{{ $q->id }}" @selected($appointment->service_queue_id==$q->id)>{{ $q->name }}</option>@endforeach</select>
</div>
<div><label class="block text-sm font-medium">Customer</label><input name="customer_name" value="{{ $appointment->customer_name }}" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<div><label class="block text-sm font-medium">Phone</label><input name="customer_phone" value="{{ $appointment->customer_phone }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<div><label class="block text-sm font-medium">Scheduled at</label><input type="datetime-local" name="scheduled_at" value="{{ $appointment->scheduled_at->format('Y-m-d\TH:i') }}" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<button type="submit" class="btn-primary w-full">Save</button>
</form>
</div>
</x-app-layout>
@@ -0,0 +1,47 @@
<x-app-layout title="Appointments">
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="text-2xl font-semibold text-slate-900 dark:text-white">Appointments</h1>
<p class="mt-1 text-sm text-slate-600 dark:text-slate-400">Schedule and check in pre-booked customers.</p>
</div>
@if(app(\App\Services\Qms\QmsPermissions::class)->can(app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()), 'appointments.manage'))
<a href="{{ route('qms.appointments.create') }}" class="btn-primary">New appointment</a>
@endif
</div>
@include('partials.flash')
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900">
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-700">
<thead class="bg-slate-50 dark:bg-slate-800">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Customer</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Scheduled</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Queue</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Status</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200 dark:divide-slate-700">
@forelse ($appointments as $appointment)
<tr>
<td class="px-4 py-3 text-sm">{{ $appointment->customer_name }}</td>
<td class="px-4 py-3 text-sm">{{ $appointment->scheduled_at->format('M j, H:i') }}</td>
<td class="px-4 py-3 text-sm">{{ $appointment->serviceQueue?->name ?? '—' }}</td>
<td class="px-4 py-3 text-sm capitalize">{{ str_replace('_', ' ', $appointment->status) }}</td>
<td class="px-4 py-3 text-right text-sm">
@if ($appointment->status === 'scheduled')
<a href="{{ route('qms.appointments.edit', $appointment) }}" class="text-indigo-600">Edit</a>
<form method="POST" action="{{ route('qms.appointments.check-in', $appointment) }}" class="inline ml-2">@csrf<button class="text-indigo-600">Check in</button></form>
<form method="POST" action="{{ route('qms.appointments.cancel', $appointment) }}" class="inline ml-2">@csrf<button class="text-amber-600">Cancel</button></form>
@elseif ($appointment->ticket)
<a href="{{ route('qms.tickets.show', $appointment->ticket) }}" class="text-indigo-600">View ticket</a>
@endif
</td>
</tr>
@empty
<tr><td colspan="5" class="px-6 py-12 text-center text-sm text-slate-500">No appointments yet.</td></tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-4">{{ $appointments->links() }}</div>
</x-app-layout>
+37
View File
@@ -0,0 +1,37 @@
<x-app-layout title="Audit log">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-semibold">Audit log</h1>
@if ($canExport)
<a href="{{ route('qms.audit.export', request()->query()) }}" class="btn-primary">Export CSV</a>
@endif
</div>
<form method="GET" class="mb-4 flex flex-wrap gap-3">
<input type="search" name="q" value="{{ request('q') }}" placeholder="Search…" class="rounded-lg border-slate-300 text-sm">
<select name="action" class="rounded-lg border-slate-300 text-sm">
<option value="">All actions</option>
@foreach ($actions as $key => $label)
<option value="{{ $key }}" @selected(request('action') === $key)>{{ $label }}</option>
@endforeach
</select>
<button type="submit" class="rounded-lg bg-slate-100 px-4 py-2 text-sm">Filter</button>
</form>
<div class="overflow-hidden rounded-2xl border bg-white dark:border-slate-700 dark:bg-slate-900">
<table class="min-w-full divide-y divide-slate-200 text-sm">
<thead class="bg-slate-50"><tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Time</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Action</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Actor</th>
</tr></thead>
<tbody class="divide-y divide-slate-200">
@foreach ($logs as $log)
<tr>
<td class="px-4 py-3 whitespace-nowrap">{{ $log->created_at?->format('M j, H:i') }}</td>
<td class="px-4 py-3">{{ $actions[$log->action] ?? $log->action }}</td>
<td class="px-4 py-3 font-mono text-xs">{{ $log->actor_ref ?? '—' }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="mt-4">{{ $logs->links() }}</div>
</x-app-layout>
+51
View File
@@ -0,0 +1,51 @@
<x-app-layout title="Live board" x-data="{}" x-init="setInterval(() => location.reload(), 15000)">
<div class="mb-6">
<h1 class="text-2xl font-semibold text-slate-900 dark:text-white">Live queue board</h1>
<p class="text-sm text-slate-600 dark:text-slate-400">Auto-refreshes every 15 seconds</p>
</div>
<div class="mb-8 grid gap-4 md:grid-cols-3">
@foreach ($queues as $queue)
<div class="rounded-2xl border border-slate-200 bg-white p-4 dark:border-slate-700 dark:bg-slate-900">
<div class="flex items-center gap-2">
<span class="h-3 w-3 rounded-full" style="background: {{ $queue->color }}"></span>
<h2 class="font-semibold">{{ $queue->name }}</h2>
@if ($queue->is_paused)
<span class="text-xs text-amber-600">Paused</span>
@endif
</div>
<p class="mt-2 text-3xl font-bold">{{ $waiting->where('service_queue_id', $queue->id)->count() }}</p>
<p class="text-sm text-slate-500">waiting</p>
</div>
@endforeach
</div>
<div class="grid gap-6 lg:grid-cols-2">
<div class="rounded-2xl border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900">
<h2 class="border-b border-slate-200 px-4 py-3 font-semibold dark:border-slate-700">Now serving</h2>
<ul class="divide-y divide-slate-200 dark:divide-slate-700">
@forelse ($serving as $ticket)
<li class="flex justify-between px-4 py-3">
<span class="font-mono text-lg font-bold">{{ $ticket->ticket_number }}</span>
<span>{{ $ticket->counter?->name }} · {{ $ticket->serviceQueue?->name }}</span>
</li>
@empty
<li class="px-4 py-6 text-sm text-slate-500">No active tickets</li>
@endforelse
</ul>
</div>
<div class="rounded-2xl border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900">
<h2 class="border-b border-slate-200 px-4 py-3 font-semibold dark:border-slate-700">Waiting</h2>
<ul class="max-h-96 divide-y divide-slate-200 overflow-y-auto dark:divide-slate-700">
@forelse ($waiting as $ticket)
<li class="flex justify-between px-4 py-2 text-sm">
<span class="font-mono font-semibold">{{ $ticket->ticket_number }}</span>
<span class="text-slate-500">{{ $ticket->serviceQueue?->name }}</span>
</li>
@empty
<li class="px-4 py-6 text-sm text-slate-500">Queue is empty</li>
@endforelse
</ul>
</div>
</div>
</x-app-layout>
@@ -0,0 +1,72 @@
<x-app-layout title="Staff console · {{ $counter->name }}">
<div class="mb-6 flex flex-wrap items-center justify-between gap-3">
<div>
<h1 class="text-2xl font-semibold text-slate-900 dark:text-white">{{ $counter->name }}</h1>
<p class="text-sm text-slate-600 dark:text-slate-400">{{ $counter->branch?->name }} · Status: {{ config('qms.counter_statuses.'.$counter->status, $counter->status) }}</p>
</div>
<div class="flex gap-2">
<form method="POST" action="{{ route('qms.console.action', $counter) }}">@csrf<input type="hidden" name="action" value="available"><button class="btn-secondary">Available</button></form>
<form method="POST" action="{{ route('qms.console.action', $counter) }}">@csrf<input type="hidden" name="action" value="offline"><button class="btn-secondary">Go offline</button></form>
</div>
</div>
@if ($currentTicket)
<div class="mb-8 rounded-2xl border-2 border-indigo-200 bg-indigo-50 p-6 dark:border-indigo-800 dark:bg-indigo-950">
<p class="text-sm font-medium text-indigo-700 dark:text-indigo-300">Current customer</p>
<p class="mt-1 text-4xl font-bold text-indigo-900 dark:text-white">{{ $currentTicket->ticket_number }}</p>
<p class="mt-1 text-slate-600 dark:text-slate-400">{{ $currentTicket->serviceQueue?->name }} · {{ $currentTicket->customer_name }}</p>
<div class="mt-4 flex flex-wrap gap-2">
@foreach (['recall' => 'Recall', 'start' => 'Start serving', 'hold' => 'Hold', 'skip' => 'Skip', 'complete' => 'Complete', 'no_show' => 'No show'] as $action => $label)
<form method="POST" action="{{ route('qms.console.action', $counter) }}">
@csrf
<input type="hidden" name="action" value="{{ $action }}">
<input type="hidden" name="ticket_id" value="{{ $currentTicket->id }}">
<button type="submit" class="btn-secondary text-sm">{{ $label }}</button>
</form>
@endforeach
</div>
@if ($allQueues->count() > 1)
<form method="POST" action="{{ route('qms.console.action', $counter) }}" class="mt-4 flex flex-wrap items-end gap-2">
@csrf
<input type="hidden" name="action" value="transfer">
<input type="hidden" name="ticket_id" value="{{ $currentTicket->id }}">
<select name="to_queue_id" class="rounded-lg border-slate-300 text-sm">
@foreach ($allQueues as $q)
@if ($q->id !== $currentTicket->service_queue_id)
<option value="{{ $q->id }}">{{ $q->name }}</option>
@endif
@endforeach
</select>
<input type="text" name="reason" placeholder="Reason" class="rounded-lg border-slate-300 text-sm">
<button type="submit" class="btn-secondary text-sm">Transfer</button>
</form>
@endif
</div>
@endif
<div class="grid gap-6 lg:grid-cols-2">
@foreach ($queues as $queue)
<div class="rounded-2xl border border-slate-200 bg-white p-5 dark:border-slate-700 dark:bg-slate-900">
<div class="mb-4 flex items-center justify-between">
<h2 class="font-semibold text-slate-900 dark:text-white">{{ $queue->name }}</h2>
<form method="POST" action="{{ route('qms.console.action', $counter) }}">
@csrf
<input type="hidden" name="action" value="call_next">
<input type="hidden" name="queue_id" value="{{ $queue->id }}">
<button type="submit" class="btn-primary text-sm">Call next</button>
</form>
</div>
<ul class="space-y-2">
@forelse ($waitingByQueue[$queue->id] ?? [] as $ticket)
<li class="flex justify-between rounded-lg bg-slate-50 px-3 py-2 text-sm dark:bg-slate-800">
<span class="font-mono font-semibold">{{ $ticket->ticket_number }}</span>
<span class="text-slate-500">{{ $ticket->customer_name ?? 'Walk-in' }}</span>
</li>
@empty
<li class="text-sm text-slate-500">No one waiting</li>
@endforelse
</ul>
</div>
@endforeach
</div>
</x-app-layout>
@@ -0,0 +1,20 @@
<x-app-layout title="New counter">
<div class="mx-auto max-w-lg">
<form method="POST" action="{{ route('qms.counters.store') }}" class="space-y-4 rounded-2xl border bg-white p-6 dark:border-slate-700 dark:bg-slate-900">
@csrf
<div><label class="block text-sm font-medium">Name</label><input name="name" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<div><label class="block text-sm font-medium">Code</label><input name="code" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<div><label class="block text-sm font-medium">Branch</label>
<select name="branch_id" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@foreach ($branches as $branch)<option value="{{ $branch->id }}">{{ $branch->name }}</option>@endforeach
</select>
</div>
<div><label class="block text-sm font-medium">Assigned queues</label>
@foreach ($queues as $queue)
<label class="mt-2 flex items-center gap-2 text-sm"><input type="checkbox" name="queue_ids[]" value="{{ $queue->id }}"> {{ $queue->name }}</label>
@endforeach
</div>
<button type="submit" class="btn-primary w-full">Create counter</button>
</form>
</div>
</x-app-layout>
@@ -0,0 +1,18 @@
<x-app-layout title="Edit counter">
<div class="mx-auto max-w-lg">
<h1 class="text-2xl font-semibold">Edit counter</h1>
<form method="POST" action="{{ route('qms.counters.update', $counter) }}" class="mt-6 space-y-4 rounded-2xl border bg-white p-6">
@csrf @method('PUT')
<div><label class="block text-sm font-medium">Name</label><input name="name" value="{{ $counter->name }}" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<div><label class="block text-sm font-medium">Code</label><input name="code" value="{{ $counter->code }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<div><label class="block text-sm font-medium">Branch</label>
<select name="branch_id" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">@foreach($branches as $b)<option value="{{ $b->id }}" @selected($counter->branch_id==$b->id)>{{ $b->name }}</option>@endforeach</select>
</div>
<div><label class="block text-sm font-medium">Queues</label>
<select name="queue_ids[]" multiple class="mt-1 w-full rounded-lg border-slate-300 text-sm">@foreach($queues as $q)<option value="{{ $q->id }}" @selected($counter->serviceQueues->contains('id',$q->id))>{{ $q->name }}</option>@endforeach</select>
</div>
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="is_active" value="1" @checked($counter->is_active)> Active</label>
<button type="submit" class="btn-primary w-full">Save</button>
</form>
</div>
</x-app-layout>
@@ -0,0 +1,20 @@
<x-app-layout title="Counters">
<div class="mb-6 flex justify-between">
<h1 class="text-2xl font-semibold">Counters</h1>
<a href="{{ route('qms.counters.create') }}" class="btn-primary">New counter</a>
</div>
<div class="grid gap-4 md:grid-cols-2">
@foreach ($counters as $counter)
<div class="rounded-2xl border bg-white p-5 dark:border-slate-700 dark:bg-slate-900">
<div class="flex justify-between">
<h2 class="font-semibold">{{ $counter->name }}</h2>
<span class="text-sm capitalize">{{ $counter->status }}</span>
</div>
<p class="mt-1 text-sm text-slate-500">{{ $counter->branch?->name }}</p>
<p class="mt-2 text-sm">Queues: {{ $counter->serviceQueues->pluck('name')->join(', ') ?: '—' }}</p>
<a href="{{ route('qms.console.show', $counter) }}" class="btn-primary mt-4 inline-block text-sm">Open console</a>
</div>
@endforeach
</div>
<div class="mt-4">{{ $counters->links() }}</div>
</x-app-layout>
@@ -0,0 +1,14 @@
<x-app-layout :title="$counter->name">
<div class="mb-6 flex justify-between">
<div>
<a href="{{ route('qms.counters.index') }}" class="text-sm text-indigo-600"> Counters</a>
<h1 class="mt-2 text-2xl font-semibold">{{ $counter->name }}</h1>
</div>
<div class="flex gap-2">
<a href="{{ route('qms.counters.edit', $counter) }}" class="btn-secondary">Edit</a>
<a href="{{ route('qms.console.show', $counter) }}" class="btn-primary">Open console</a>
</div>
</div>
<p class="text-sm text-slate-600">Status: {{ $counter->status }} · {{ $counter->branch?->name }}</p>
<p class="mt-2 text-sm">Queues: {{ $counter->serviceQueues->pluck('name')->join(', ') ?: '—' }}</p>
</x-app-layout>
+130
View File
@@ -0,0 +1,130 @@
<x-app-layout title="Dashboard">
@php
$operationalCards = [
[
'label' => 'Waiting now',
'value' => number_format($operational['waiting']),
'href' => route('qms.queues.index'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />',
],
[
'label' => 'Being served',
'value' => number_format($operational['serving']),
'href' => route('qms.queues.index'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />',
],
[
'label' => 'Served today',
'value' => number_format($operational['served_today']),
'href' => route('qms.queues.index'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />',
'valueClass' => 'text-emerald-700',
],
[
'label' => 'Avg wait',
'value' => $operational['avg_wait_seconds'] > 0
? ceil($operational['avg_wait_seconds'] / 60).' mins'
: '—',
'href' => route('qms.queues.index'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />',
],
[
'label' => 'No-shows today',
'value' => number_format($operational['no_shows_today']),
'href' => route('qms.queues.index'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 0 0 5.636 5.636m12.728 12.728A9 9 0 0 1 5.636 5.636m12.728 12.728L5.636 5.636" />',
],
];
$organizationCards = [
[
'label' => 'Active branches',
'value' => number_format($orgStats['branches']),
'href' => route('qms.queues.index'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5m-1.5 3h1.5m-1.5 3h1.5m-1.5 3h1.5m3-9H15m-1.5 3H15m-1.5 3H15m-1.5 3H15M9 21v-3.375c0-.621.504-1.125 1.125-1.125h3.75c.621 0 1.125.504 1.125 1.125V21" />',
],
[
'label' => 'Service queues',
'value' => number_format($orgStats['queues']),
'href' => route('qms.queues.index'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.25 4.5h13.5a.75.75 0 0 1 .75.75v15.75a.75.75 0 0 1-.75.75H5.25a.75.75 0 0 1-.75-.75V5.25a.75.75 0 0 1 .75-.75Z" />',
],
[
'label' => 'Team members',
'value' => number_format($orgStats['team_members']),
'href' => route('qms.queues.index'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" />',
],
];
@endphp
<div class="mb-6 flex items-center justify-between gap-3">
<div>
<h1 class="text-2xl font-semibold text-slate-900 dark:text-white">{{ $organization->name }}</h1>
<p class="mt-1 text-sm text-slate-600 dark:text-slate-400">Real-time queue operations overview</p>
</div>
@if(auth()->user() && app(\App\Services\Qms\QmsPermissions::class)->can(app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()), 'queues.manage'))
<a href="{{ route('qms.queues.create') }}" class="btn-primary">New queue</a>
@endif
</div>
<section class="mb-8">
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide text-slate-500">Live operations</h2>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
@foreach ($operationalCards as $card)
<a href="{{ $card['href'] }}" class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition hover:border-indigo-200 dark:border-slate-700 dark:bg-slate-900">
<div class="flex items-start justify-between">
<p class="text-sm text-slate-600 dark:text-slate-400">{{ $card['label'] }}</p>
<svg class="h-5 w-5 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">{!! $card['icon'] !!}</svg>
</div>
<p class="mt-2 text-2xl font-semibold {{ $card['valueClass'] ?? 'text-slate-900 dark:text-white' }}">{{ $card['value'] }}</p>
</a>
@endforeach
</div>
</section>
<section class="mb-8">
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide text-slate-500">Organization</h2>
<div class="grid gap-4 sm:grid-cols-3">
@foreach ($organizationCards as $card)
<a href="{{ $card['href'] }}" class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition hover:border-indigo-200 dark:border-slate-700 dark:bg-slate-900">
<div class="flex items-start justify-between">
<p class="text-sm text-slate-600 dark:text-slate-400">{{ $card['label'] }}</p>
<svg class="h-5 w-5 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">{!! $card['icon'] !!}</svg>
</div>
<p class="mt-2 text-2xl font-semibold text-slate-900 dark:text-white">{{ $card['value'] }}</p>
</a>
@endforeach
</div>
</section>
@if ($branches->isNotEmpty())
<section>
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide text-slate-500">Branches</h2>
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900">
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-700">
<thead class="bg-slate-50 dark:bg-slate-800">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Branch</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Queues</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Status</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200 dark:divide-slate-700">
@foreach ($branches as $branch)
<tr>
<td class="px-4 py-3 text-sm font-medium text-slate-900 dark:text-white">{{ $branch->name }}</td>
<td class="px-4 py-3 text-sm text-slate-600 dark:text-slate-400">{{ $branch->service_queues_count }}</td>
<td class="px-4 py-3">
<span class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium {{ $branch->is_active ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-600' }}">
{{ $branch->is_active ? 'Active' : 'Inactive' }}
</span>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</section>
@endif
</x-app-layout>
@@ -0,0 +1,20 @@
<x-app-layout title="Register device">
<div class="mx-auto max-w-lg">
<form method="POST" action="{{ route('qms.devices.store') }}" class="space-y-4 rounded-2xl border bg-white p-6">
@csrf
<div><label class="block text-sm font-medium">Name</label><input name="name" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<div><label class="block text-sm font-medium">Type</label>
<select name="type" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@foreach ($deviceTypes as $k => $v)<option value="{{ $k }}">{{ $v }}</option>@endforeach
</select>
</div>
<div><label class="block text-sm font-medium">Branch</label>
<select name="branch_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<option value=""></option>
@foreach ($branches as $b)<option value="{{ $b->id }}">{{ $b->name }}</option>@endforeach
</select>
</div>
<button type="submit" class="btn-primary w-full">Register</button>
</form>
</div>
</x-app-layout>
@@ -0,0 +1,22 @@
<x-app-layout title="Devices">
<div class="mb-6 flex justify-between">
<h1 class="text-2xl font-semibold">Devices</h1>
<a href="{{ route('qms.devices.create') }}" class="btn-primary">Register device</a>
</div>
@if (session('success'))<p class="mb-4 rounded-lg bg-emerald-50 px-3 py-2 text-sm text-emerald-800">{{ session('success') }}</p>@endif
<div class="space-y-3">
@foreach ($devices as $device)
<div class="flex items-center justify-between rounded-2xl border bg-white p-4 dark:border-slate-700 dark:bg-slate-900">
<div>
<p class="font-semibold">{{ $device->name }}</p>
<p class="text-sm text-slate-500">{{ config('qms.device_types.'.$device->type) }} · {{ $device->status }}</p>
@if ($device->type === 'kiosk' && $device->device_token)
<p class="mt-1 text-xs text-slate-400">Kiosk URL: {{ route('qms.kiosk.device', $device->device_token) }}</p>
@endif
</div>
<form method="POST" action="{{ route('qms.devices.regenerate-token', $device) }}">@csrf<button class="btn-secondary text-sm">Regenerate token</button></form>
</div>
@endforeach
</div>
{{ $devices->links() }}
</x-app-layout>
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en" class="h-full bg-slate-950 text-white">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $screen->name }} · Queue Display</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="min-h-screen p-8" x-data="displayBoard('{{ $dataUrl }}')" x-init="init()">
<div class="mx-auto max-w-6xl">
<div class="mb-8 flex items-center justify-between">
<h1 class="text-4xl font-bold" x-text="payload?.screen?.name ?? '{{ $screen->name }}'"></h1>
<p class="text-slate-400" x-text="payload?.updated_at ? 'Updated ' + new Date(payload.updated_at).toLocaleTimeString() : ''"></p>
</div>
<div class="mb-10">
<h2 class="mb-4 text-sm font-semibold uppercase tracking-widest text-indigo-300">Now serving</h2>
<div class="grid gap-4 md:grid-cols-2">
<template x-for="item in (payload?.now_serving ?? [])" :key="item.ticket_number + item.counter">
<div class="rounded-2xl bg-indigo-600 p-8 text-center shadow-lg">
<p class="text-5xl font-bold" x-text="item.ticket_number"></p>
<p class="mt-2 text-xl" x-text="item.queue_name"></p>
<p class="mt-4 text-2xl font-semibold" x-text="'→ ' + (item.counter ?? '')"></p>
</div>
</template>
</div>
</div>
<div class="grid gap-6 md:grid-cols-3">
<div class="rounded-2xl border border-slate-700 bg-slate-900 p-6 text-center">
<p class="text-sm uppercase text-slate-400">Waiting</p>
<p class="mt-2 text-5xl font-bold" x-text="payload?.waiting_count ?? '—'"></p>
</div>
<div class="rounded-2xl border border-slate-700 bg-slate-900 p-6 text-center">
<p class="text-sm uppercase text-slate-400">Est. wait</p>
<p class="mt-2 text-5xl font-bold" x-text="payload?.estimated_wait_minutes ? payload.estimated_wait_minutes + ' min' : '—'"></p>
</div>
<div class="rounded-2xl border border-slate-700 bg-slate-900 p-6">
<p class="mb-3 text-sm uppercase text-slate-400">Queues</p>
<template x-for="q in (payload?.queues ?? [])" :key="q.name">
<p class="text-lg" x-text="q.name + (q.is_paused ? ' (paused)' : '')"></p>
</template>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,24 @@
<x-app-layout title="New display">
<div class="mx-auto max-w-lg">
<form method="POST" action="{{ route('qms.displays.store') }}" class="space-y-4 rounded-2xl border bg-white p-6">
@csrf
<div><label class="block text-sm font-medium">Name</label><input name="name" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<div><label class="block text-sm font-medium">Branch</label>
<select name="branch_id" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@foreach ($branches as $b)<option value="{{ $b->id }}">{{ $b->name }}</option>@endforeach
</select>
</div>
<div><label class="block text-sm font-medium">Layout</label>
<select name="layout" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@foreach ($layouts as $k => $v)<option value="{{ $k }}">{{ $v }}</option>@endforeach
</select>
</div>
<div><label class="block text-sm font-medium">Queues on screen</label>
@foreach ($queues as $q)
<label class="mt-2 flex gap-2 text-sm"><input type="checkbox" name="queue_ids[]" value="{{ $q->id }}"> {{ $q->name }}</label>
@endforeach
</div>
<button type="submit" class="btn-primary w-full">Create display</button>
</form>
</div>
</x-app-layout>
@@ -0,0 +1,20 @@
<x-app-layout title="Edit display">
<div class="mx-auto max-w-lg">
<h1 class="text-2xl font-semibold">Edit display</h1>
<form method="POST" action="{{ route('qms.displays.update', $display) }}" class="mt-6 space-y-4 rounded-2xl border bg-white p-6">
@csrf @method('PUT')
<div><label class="block text-sm font-medium">Name</label><input name="name" value="{{ $display->name }}" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<div><label class="block text-sm font-medium">Branch</label>
<select name="branch_id" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">@foreach($branches as $b)<option value="{{ $b->id }}" @selected($display->branch_id==$b->id)>{{ $b->name }}</option>@endforeach</select>
</div>
<div><label class="block text-sm font-medium">Layout</label>
<select name="layout" class="mt-1 w-full rounded-lg border-slate-300 text-sm">@foreach($layouts as $k=>$l)<option value="{{ $k }}" @selected($display->layout==$k)>{{ $l }}</option>@endforeach</select>
</div>
<div><label class="block text-sm font-medium">Queues</label>
<select name="queue_ids[]" multiple class="mt-1 w-full rounded-lg border-slate-300 text-sm">@foreach($queues as $q)<option value="{{ $q->id }}" @selected(in_array($q->id, $display->service_queue_ids ?? []))>{{ $q->name }}</option>@endforeach</select>
</div>
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="is_active" value="1" @checked($display->is_active)> Active</label>
<button type="submit" class="btn-primary w-full">Save</button>
</form>
</div>
</x-app-layout>
@@ -0,0 +1,18 @@
<x-app-layout title="Displays">
<div class="mb-6 flex justify-between">
<h1 class="text-2xl font-semibold">Display screens</h1>
<a href="{{ route('qms.displays.create') }}" class="btn-primary">New display</a>
</div>
<div class="space-y-3">
@foreach ($screens as $screen)
<div class="flex items-center justify-between rounded-2xl border bg-white p-4 dark:border-slate-700 dark:bg-slate-900">
<div>
<p class="font-semibold">{{ $screen->name }}</p>
<p class="text-sm text-slate-500">{{ config('qms.display_layouts.'.$screen->layout) }}</p>
</div>
<a href="{{ route('qms.display.public', $screen->access_token) }}" target="_blank" class="btn-secondary text-sm">Open display</a>
</div>
@endforeach
</div>
{{ $screens->links() }}
</x-app-layout>
@@ -0,0 +1,11 @@
<x-app-layout :title="$display->name">
<div class="mb-6 flex justify-between">
<div>
<a href="{{ route('qms.displays.index') }}" class="text-sm text-indigo-600"> Displays</a>
<h1 class="mt-2 text-2xl font-semibold">{{ $display->name }}</h1>
</div>
<a href="{{ route('qms.displays.edit', $display) }}" class="btn-secondary">Edit</a>
</div>
<p class="text-sm text-slate-600">Layout: {{ config('qms.display_layouts.'.$display->layout) }}</p>
<p class="mt-2 text-sm"><a href="{{ route('qms.display.public', $display->access_token) }}" class="text-indigo-600" target="_blank">Open public display</a></p>
</x-app-layout>
@@ -0,0 +1,29 @@
<x-app-layout title="Customer feedback">
<div class="mb-6">
<h1 class="text-2xl font-semibold">Customer feedback</h1>
<p class="mt-1 text-sm text-slate-600">Average rating: {{ $avgRating ?: '—' }} / 5</p>
</div>
<div class="overflow-hidden rounded-2xl border bg-white dark:border-slate-700 dark:bg-slate-900">
<table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50"><tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Ticket</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Rating</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Comments</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">When</th>
</tr></thead>
<tbody class="divide-y divide-slate-200">
@forelse ($feedback as $item)
<tr>
<td class="px-4 py-3 text-sm">{{ $item->ticket?->ticket_number }}</td>
<td class="px-4 py-3 text-sm">{{ $item->rating }}/5</td>
<td class="px-4 py-3 text-sm text-slate-600">{{ Str::limit($item->comments, 80) }}</td>
<td class="px-4 py-3 text-sm">{{ $item->created_at->diffForHumans() }}</td>
</tr>
@empty
<tr><td colspan="4" class="px-6 py-12 text-center text-sm text-slate-500">No feedback yet.</td></tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-4">{{ $feedback->links() }}</div>
</x-app-layout>
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@include('partials.favicon')
<title>Feedback Ladill Queue</title>
@vite(['resources/css/app.css'])
</head>
<body class="min-h-screen bg-slate-50 text-slate-900">
<div class="mx-auto max-w-md px-4 py-12">
<h1 class="text-2xl font-semibold">How was your visit?</h1>
<p class="mt-2 text-sm text-slate-600">Ticket {{ $ticket->ticket_number }}</p>
@if (session('success'))
<div class="mt-4 rounded-lg bg-emerald-50 px-4 py-3 text-sm text-emerald-800">{{ session('success') }}</div>
@elseif ($existing)
<p class="mt-6 text-sm text-slate-600">You already submitted feedback. Thank you!</p>
@else
<form method="POST" action="{{ route('qms.feedback.store', $ticket->qr_token) }}" class="mt-6 space-y-4 rounded-2xl border bg-white p-6">
@csrf
<div>
<label class="block text-sm font-medium">Overall rating</label>
<select name="rating" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@for ($i = 5; $i >= 1; $i--)
<option value="{{ $i }}">{{ $i }} star{{ $i > 1 ? 's' : '' }}</option>
@endfor
</select>
</div>
<div>
<label class="block text-sm font-medium">Comments</label>
<textarea name="comments" rows="4" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></textarea>
</div>
<button type="submit" class="w-full rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white">Submit feedback</button>
</form>
@endif
</div>
</body>
</html>
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en" class="h-full bg-slate-900">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Kiosk · {{ $device->name }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="flex min-h-screen items-center justify-center p-6 text-white" x-data="kioskFlow({ issueUrl: '{{ route('qms.kiosk.device.issue', $device->device_token) }}', csrf: '{{ csrf_token() }}' })">
<div class="w-full max-w-lg">
<h1 class="mb-8 text-center text-3xl font-bold">{{ $device->name }}</h1>
<template x-if="step === 'select'">
<div class="space-y-3">
<p class="mb-4 text-center text-slate-300">Select a service</p>
@foreach ($queues as $queue)
<button type="button" @click="selectQueue({{ $queue->id }})"
class="w-full rounded-2xl border border-slate-600 bg-slate-800 px-6 py-5 text-left text-xl font-semibold hover:border-indigo-400"
style="border-left: 6px solid {{ $queue->color }}">
{{ $queue->name }}
</button>
@endforeach
</div>
</template>
<template x-if="step === 'details'">
<div class="rounded-2xl bg-slate-800 p-6 space-y-4">
<input type="text" x-model="customerName" placeholder="Your name (optional)" class="w-full rounded-lg border-slate-600 bg-slate-900 px-4 py-3 text-white">
<input type="tel" x-model="customerPhone" placeholder="Phone (optional)" class="w-full rounded-lg border-slate-600 bg-slate-900 px-4 py-3 text-white">
<p x-show="error" class="text-red-400 text-sm" x-text="error"></p>
<div class="flex gap-3">
<button type="button" @click="reset()" class="flex-1 rounded-lg border border-slate-600 py-3">Back</button>
<button type="button" @click="issue()" :disabled="loading" class="flex-1 rounded-lg bg-indigo-600 py-3 font-semibold">Get ticket</button>
</div>
</div>
</template>
<template x-if="step === 'ticket' && ticket">
<div class="rounded-2xl bg-indigo-600 p-8 text-center">
<p class="text-sm uppercase tracking-widest opacity-80">Your number</p>
<p class="mt-2 text-6xl font-bold" x-text="ticket.ticket_number"></p>
<p class="mt-4 text-xl" x-text="ticket.queue?.name"></p>
<p class="mt-6 text-lg" x-text="ticket.estimated_wait_seconds ? 'Est. wait: ' + Math.ceil(ticket.estimated_wait_seconds / 60) + ' mins' : ''"></p>
<button type="button" @click="reset()" class="mt-8 rounded-lg bg-white/20 px-6 py-3">Done</button>
</div>
</template>
</div>
</body>
</html>
+34
View File
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Join queue · Ladill Queue</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="min-h-screen bg-slate-100 p-6">
<div class="mx-auto max-w-md rounded-2xl bg-white p-6 shadow">
<h1 class="text-2xl font-semibold text-slate-900">Join a queue</h1>
<p class="mt-2 text-sm text-slate-600">Enter your queue code and phone number to get a ticket remotely.</p>
@if (session('success'))
<p class="mt-4 rounded-lg bg-emerald-50 px-3 py-2 text-sm text-emerald-800">{{ session('success') }}</p>
@endif
<form method="POST" action="{{ route('qms.mobile.join.store') }}" class="mt-6 space-y-4">
@csrf
<div>
<label class="block text-sm font-medium text-slate-700">Queue code (UUID)</label>
<input name="queue_uuid" required class="mt-1 w-full rounded-lg border-slate-300 text-sm" placeholder="Paste queue UUID from your organization">
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Your name</label>
<input name="customer_name" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Phone</label>
<input name="customer_phone" type="tel" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<button type="submit" class="btn-primary w-full">Join queue</button>
</form>
</div>
</body>
</html>
+38
View File
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ticket {{ $ticket->ticket_number }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="min-h-screen bg-slate-100 p-6" x-data="mobileTracker('{{ $pollUrl }}')" x-init="init()">
<div class="mx-auto max-w-md rounded-2xl bg-white p-6 shadow">
@if (session('success'))
<p class="mb-4 rounded-lg bg-emerald-50 px-3 py-2 text-sm text-emerald-800">{{ session('success') }}</p>
@endif
<p class="text-sm uppercase tracking-wide text-slate-500">Your ticket</p>
<p class="text-5xl font-bold text-indigo-600" x-text="data?.ticket_number ?? '{{ $ticket->ticket_number }}'"></p>
<p class="mt-2 text-lg text-slate-700" x-text="data?.queue?.name ?? '{{ $ticket->serviceQueue?->name }}'"></p>
<p class="mt-4 text-sm text-slate-600">Status: <span class="font-semibold capitalize" x-text="data?.status ?? '{{ $ticket->status }}'"></span></p>
<p class="mt-2 text-sm text-slate-600" x-show="(data?.status ?? '{{ $ticket->status }}') === 'waiting'">
<span x-text="ahead + ' customer(s) ahead'"></span>
</p>
<p class="mt-2 text-sm text-slate-600" x-show="data?.counter">
Proceed to <span class="font-semibold" x-text="data?.counter?.name"></span>
</p>
@if (in_array($ticket->status, ['waiting', 'on_hold']))
<form method="POST" action="{{ route('qms.mobile.delay', $ticket->qr_token) }}" class="mt-6 flex gap-2">
@csrf
<input type="number" name="minutes" value="15" min="5" max="120" class="w-20 rounded-lg border-slate-300 text-sm">
<button type="submit" class="btn-secondary flex-1">Delay arrival</button>
</form>
<form method="POST" action="{{ route('qms.mobile.cancel', $ticket->qr_token) }}" class="mt-2" onsubmit="return confirm('Cancel this ticket?')">
@csrf
<button type="submit" class="w-full text-sm text-red-600">Cancel ticket</button>
</form>
@endif
</div>
</body>
</html>
@@ -0,0 +1,71 @@
<x-app-layout title="Set up Queue">
<div class="mx-auto max-w-xl">
<h1 class="text-2xl font-semibold text-slate-900 dark:text-white">Welcome to Ladill Queue</h1>
<p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Configure your organization to start managing customer flow.</p>
<form method="POST" action="{{ route('qms.onboarding.store') }}" enctype="multipart/form-data" class="mt-8 space-y-5 rounded-2xl border border-slate-200 bg-white p-6 dark:border-slate-700 dark:bg-slate-900">
@csrf
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300">Organization name</label>
<input type="text" name="organization_name" value="{{ old('organization_name', $user->name."'s Organization") }}" required
class="mt-1 w-full rounded-lg border-slate-300 text-sm dark:border-slate-600 dark:bg-slate-800">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300">Industry</label>
<select name="industry" class="mt-1 w-full rounded-lg border-slate-300 text-sm dark:border-slate-600 dark:bg-slate-800">
@foreach ($industries as $value => $label)
<option value="{{ $value }}" @selected(old('industry', 'custom') === $value)>{{ $label }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300">Appointment mode</label>
<select name="appointment_mode" class="mt-1 w-full rounded-lg border-slate-300 text-sm dark:border-slate-600 dark:bg-slate-800">
@foreach ($appointmentModes as $value => $label)
<option value="{{ $value }}" @selected(old('appointment_mode', 'hybrid') === $value)>{{ $label }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300">Primary branch</label>
<input type="text" name="branch_name" value="{{ old('branch_name', 'Main Branch') }}" required
class="mt-1 w-full rounded-lg border-slate-300 text-sm dark:border-slate-600 dark:bg-slate-800">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300">Branch address</label>
<input type="text" name="branch_address" value="{{ old('branch_address') }}"
class="mt-1 w-full rounded-lg border-slate-300 text-sm dark:border-slate-600 dark:bg-slate-800">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300">Branch phone</label>
<input type="tel" name="branch_phone" value="{{ old('branch_phone') }}"
class="mt-1 w-full rounded-lg border-slate-300 text-sm dark:border-slate-600 dark:bg-slate-800">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300">Logo (optional)</label>
<input type="file" name="logo" accept="image/png,image/jpeg,image/webp,image/svg+xml"
class="mt-2 block w-full text-sm text-slate-600 file:mr-3 file:rounded-lg file:border-0 file:bg-sky-50 file:px-3 file:py-2 file:text-sm file:font-medium file:text-sky-700">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300">Timezone</label>
<select name="timezone" class="mt-1 w-full rounded-lg border-slate-300 text-sm dark:border-slate-600 dark:bg-slate-800">
@foreach ($timezones as $tz)
<option value="{{ $tz }}" @selected(old('timezone', config('app.timezone')) === $tz)>{{ $tz }}</option>
@endforeach
</select>
</div>
<button type="submit" class="btn-primary btn-primary-lg w-full">
Complete setup
</button>
</form>
</div>
</x-app-layout>
@@ -0,0 +1,44 @@
<div>
<label class="block text-sm font-medium">Queue name</label>
<input type="text" name="name" value="{{ old('name', $queue->name ?? '') }}" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-sm font-medium">Description</label>
<textarea name="description" rows="2" class="mt-1 w-full rounded-lg border-slate-300 text-sm">{{ old('description', $queue->description ?? '') }}</textarea>
</div>
<div>
<label class="block text-sm font-medium">Branch</label>
<select name="branch_id" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@foreach ($branches as $branch)
<option value="{{ $branch->id }}" @selected(old('branch_id', $queue->branch_id ?? '') == $branch->id)>{{ $branch->name }}</option>
@endforeach
</select>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label class="block text-sm font-medium">Ticket prefix</label>
<input type="text" name="prefix" value="{{ old('prefix', $queue->prefix ?? 'A') }}" required maxlength="10" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-sm font-medium">Color</label>
<input type="color" name="color" value="{{ old('color', $queue->color ?? '#4f46e5') }}" class="mt-1 h-10 w-full rounded-lg border-slate-300">
</div>
</div>
<div>
<label class="block text-sm font-medium">Queue strategy</label>
<select name="strategy" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@foreach ($strategies as $value => $label)
<option value="{{ $value }}" @selected(old('strategy', $queue->strategy ?? 'fifo') === $value)>{{ $label }}</option>
@endforeach
</select>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label class="block text-sm font-medium">Avg service time (seconds)</label>
<input type="number" name="avg_service_seconds" value="{{ old('avg_service_seconds', $queue->avg_service_seconds ?? 300) }}" min="60" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-sm font-medium">Max capacity</label>
<input type="number" name="max_capacity" value="{{ old('max_capacity', $queue->max_capacity ?? '') }}" min="1" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
</div>
@@ -0,0 +1,16 @@
<x-app-layout title="New queue">
<div class="mx-auto max-w-xl">
<h1 class="text-2xl font-semibold text-slate-900 dark:text-white">Create service queue</h1>
<p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Define how customers join and flow through this service point.</p>
<form method="POST" action="{{ route('qms.queues.store') }}" class="mt-8 space-y-5 rounded-2xl border border-slate-200 bg-white p-6 dark:border-slate-700 dark:bg-slate-900">
@csrf
@include('qms.queues._form', ['queue' => null])
<div class="flex gap-3">
<a href="{{ route('qms.queues.index') }}" class="btn-secondary flex-1 justify-center">Cancel</a>
<button type="submit" class="btn-primary flex-1">Create queue</button>
</div>
</form>
</div>
</x-app-layout>
+11
View File
@@ -0,0 +1,11 @@
<x-app-layout title="Edit queue">
<div class="mx-auto max-w-xl">
<h1 class="text-2xl font-semibold">Edit queue</h1>
<form method="POST" action="{{ route('qms.queues.update', $queue) }}" class="mt-6 space-y-4 rounded-2xl border bg-white p-6">
@csrf @method('PUT')
@include('qms.queues._form', ['queue' => $queue])
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="is_active" value="1" @checked($queue->is_active)> Active</label>
<button type="submit" class="btn-primary w-full">Save changes</button>
</form>
</div>
</x-app-layout>
@@ -0,0 +1,73 @@
<x-app-layout title="Queues">
<div class="mb-6 flex items-center justify-between gap-3">
<div>
<h1 class="text-2xl font-semibold text-slate-900 dark:text-white">Service queues</h1>
<p class="mt-1 text-sm text-slate-600 dark:text-slate-400">Configure queues for reception, counters, and departments.</p>
</div>
@if(app(\App\Services\Qms\QmsPermissions::class)->can(app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()), 'queues.manage'))
<a href="{{ route('qms.queues.create') }}" class="btn-primary">New queue</a>
@endif
</div>
@if (session('success'))
<div class="mb-4 rounded-lg bg-emerald-50 px-4 py-3 text-sm text-emerald-800">{{ session('success') }}</div>
@endif
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900">
@if ($queues->isEmpty())
<div class="px-6 py-12 text-center text-sm text-slate-500">
No queues yet. Create your first service queue to start issuing tickets.
</div>
@else
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-700">
<thead class="bg-slate-50 dark:bg-slate-800">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Queue</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Branch</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Prefix</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Strategy</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Status</th>
<th class="px-4 py-3"></th>
</thead>
<tbody class="divide-y divide-slate-200 dark:divide-slate-700">
@foreach ($queues as $queue)
<tr>
<td class="px-4 py-3">
<div class="flex items-center gap-2">
<span class="h-3 w-3 rounded-full" style="background-color: {{ $queue->color }}"></span>
<span class="text-sm font-medium text-slate-900 dark:text-white">
<a href="{{ route('qms.queues.show', $queue) }}" class="hover:text-indigo-600">{{ $queue->name }}</a>
</span>
</div>
</td>
<td class="px-4 py-3 text-sm text-slate-600 dark:text-slate-400">{{ $queue->branch?->name }}</td>
<td class="px-4 py-3 text-sm font-mono text-slate-600 dark:text-slate-400">{{ $queue->prefix }}</td>
<td class="px-4 py-3 text-sm text-slate-600 dark:text-slate-400">{{ config('qms.queue_strategies.'.$queue->strategy, $queue->strategy) }}</td>
<td class="px-4 py-3">
<div class="flex items-center gap-2">
@if ($queue->is_paused)
<span class="inline-flex rounded-full bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-700">Paused</span>
<form method="POST" action="{{ route('qms.queues.resume', $queue) }}" class="inline">@csrf<button class="text-xs text-indigo-600">Resume</button></form>
@elseif ($queue->is_active)
<span class="inline-flex rounded-full bg-emerald-50 px-2 py-0.5 text-xs font-medium text-emerald-700">Active</span>
<form method="POST" action="{{ route('qms.queues.pause', $queue) }}" class="inline">@csrf<button class="text-xs text-amber-600">Pause</button></form>
@else
<span class="inline-flex rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-600">Inactive</span>
@endif
</div>
</td>
<td class="px-4 py-3 text-right">
@if(app(\App\Services\Qms\QmsPermissions::class)->can(app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()), 'rules.view'))
<a href="{{ route('qms.rules.index', $queue) }}" class="text-xs text-indigo-600">Rules</a>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="border-t border-slate-200 px-4 py-3 dark:border-slate-700">
{{ $queues->links() }}
</div>
@endif
</div>
</x-app-layout>
+19
View File
@@ -0,0 +1,19 @@
<x-app-layout :title="$queue->name">
<div class="mb-6 flex items-center justify-between">
<div>
<a href="{{ route('qms.queues.index') }}" class="text-sm text-indigo-600"> Queues</a>
<h1 class="mt-2 text-2xl font-semibold">{{ $queue->name }}</h1>
<p class="text-sm text-slate-600">{{ $queue->branch?->name }} · {{ config('qms.queue_strategies.'.$queue->strategy) }}</p>
</div>
<a href="{{ route('qms.queues.edit', $queue) }}" class="btn-secondary">Edit</a>
</div>
@include('partials.flash')
<div class="grid gap-4 sm:grid-cols-3">
<div class="rounded-2xl border bg-white p-4"><div class="text-xs uppercase text-slate-500">Waiting</div><div class="mt-1 text-2xl font-semibold">{{ $waiting }}</div></div>
<div class="rounded-2xl border bg-white p-4"><div class="text-xs uppercase text-slate-500">Prefix</div><div class="mt-1 text-2xl font-mono">{{ $queue->prefix }}</div></div>
<div class="rounded-2xl border bg-white p-4"><div class="text-xs uppercase text-slate-500">Status</div><div class="mt-1 text-lg">{{ $queue->is_paused ? 'Paused' : ($queue->is_active ? 'Active' : 'Inactive') }}</div></div>
</div>
<div class="mt-4 flex gap-2">
<a href="{{ route('qms.rules.index', $queue) }}" class="text-sm text-indigo-600">Queue rules</a>
</div>
</x-app-layout>
@@ -0,0 +1,11 @@
<x-app-layout title="Reports">
<h1 class="mb-6 text-2xl font-semibold">Reports</h1>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
@foreach ($reports as $type => $label)
<a href="{{ route('qms.reports.show', $type) }}" class="rounded-2xl border bg-white p-6 transition hover:border-indigo-300 dark:border-slate-700 dark:bg-slate-900">
<h2 class="font-semibold">{{ $label }}</h2>
<p class="mt-1 text-sm text-slate-600">View metrics and export CSV</p>
</a>
@endforeach
</div>
</x-app-layout>
@@ -0,0 +1,41 @@
<x-app-layout :title="$label">
<div class="mb-6 flex flex-wrap items-end justify-between gap-4">
<div>
<a href="{{ route('qms.reports.index') }}" class="text-sm text-indigo-600"> Reports</a>
<h1 class="mt-2 text-2xl font-semibold">{{ $label }}</h1>
</div>
@if ($canExport)
<a href="{{ route('qms.reports.export', array_merge(['type' => $type], request()->only(['from', 'to', 'branch_id']))) }}" class="btn-primary">Export CSV</a>
@endif
</div>
<form method="GET" class="mb-6 flex flex-wrap gap-3 rounded-2xl border bg-white p-4 dark:border-slate-700 dark:bg-slate-900">
<input type="date" name="from" value="{{ $from }}" class="rounded-lg border-slate-300 text-sm">
<input type="date" name="to" value="{{ $to }}" class="rounded-lg border-slate-300 text-sm">
<select name="branch_id" class="rounded-lg border-slate-300 text-sm">
<option value="">All branches</option>
@foreach ($branches as $branch)
<option value="{{ $branch->id }}" @selected($branchId == $branch->id)>{{ $branch->name }}</option>
@endforeach
</select>
<button type="submit" class="rounded-lg bg-slate-100 px-4 py-2 text-sm">Apply</button>
</form>
<div class="rounded-2xl border bg-white p-6 dark:border-slate-700 dark:bg-slate-900">
@if ($type === 'wait_times' && isset($data['hourly']))
<table class="w-full text-sm"><thead><tr><th class="text-left py-2">Hour</th><th class="text-right">Avg wait (sec)</th></tr></thead>
<tbody>@foreach ($data['hourly'] as $row)<tr class="border-t"><td class="py-2">{{ $row['hour'] }}:00</td><td class="text-right">{{ $row['avg_wait_seconds'] }}</td></tr>@endforeach</tbody>
</table>
@elseif ($type === 'counters' && isset($data['counters']))
<table class="w-full text-sm"><thead><tr><th class="text-left py-2">Counter</th><th class="text-right">Served</th></tr></thead>
<tbody>@foreach ($data['counters'] as $row)<tr class="border-t"><td class="py-2">{{ $row['name'] }}</td><td class="text-right">{{ $row['served'] }}</td></tr>@endforeach</tbody>
</table>
@else
<dl class="space-y-3">
@foreach ($data as $key => $value)
@if (!is_array($value))
<div class="flex justify-between border-b pb-2 text-sm"><dt class="text-slate-600">{{ str_replace('_', ' ', $key) }}</dt><dd class="font-medium">{{ $value }}</dd></div>
@endif
@endforeach
</dl>
@endif
</div>
</x-app-layout>
+68
View File
@@ -0,0 +1,68 @@
<x-app-layout :title="'Rules — '.$queue->name">
<div class="mb-6">
<a href="{{ route('qms.queues.index') }}" class="text-sm text-indigo-600"> Queues</a>
<h1 class="mt-2 text-2xl font-semibold">Queue rules</h1>
<p class="text-sm text-slate-600">{{ $queue->name }}</p>
</div>
@include('partials.flash')
<div class="mb-6 rounded-2xl border bg-white p-6 dark:border-slate-700 dark:bg-slate-900">
<h2 class="text-sm font-semibold">Add rule</h2>
<form method="POST" action="{{ route('qms.rules.store', $queue) }}" class="mt-4 grid gap-4 md:grid-cols-2">
@csrf
<div>
<label class="block text-sm font-medium">Type</label>
<select name="rule_type" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@foreach ($ruleTypes as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium">Overflow queue</label>
<select name="overflow_queue_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<option value=""></option>
@foreach (\App\Models\ServiceQueue::owned($queue->owner_ref)->where('organization_id', $queue->organization_id)->where('id', '!=', $queue->id)->get() as $q)
<option value="{{ $q->id }}">{{ $q->name }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium">Waiting threshold</label>
<input name="waiting_threshold" type="number" min="1" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-sm font-medium">Boost priority to</label>
<select name="priority" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@foreach (config('qms.ticket_priorities') as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
</div>
<div class="md:col-span-2"><button type="submit" class="btn-primary">Add rule</button></div>
</form>
</div>
<div class="overflow-hidden rounded-2xl border bg-white dark:border-slate-700 dark:bg-slate-900">
<table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50"><tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Type</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Conditions</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Actions</th>
<th></th>
</tr></thead>
<tbody class="divide-y divide-slate-200">
@forelse ($rules as $rule)
<tr>
<td class="px-4 py-3 text-sm">{{ $ruleTypes[$rule->rule_type] ?? $rule->rule_type }}</td>
<td class="px-4 py-3 text-xs text-slate-600">{{ json_encode($rule->conditions) }}</td>
<td class="px-4 py-3 text-xs text-slate-600">{{ json_encode($rule->actions) }}</td>
<td class="px-4 py-3 text-right">
<form method="POST" action="{{ route('qms.rules.destroy', [$queue, $rule]) }}">@csrf @method('DELETE')<button class="text-xs text-red-600">Remove</button></form>
</td>
</tr>
@empty
<tr><td colspan="4" class="px-6 py-8 text-center text-sm text-slate-500">No rules yet.</td></tr>
@endforelse
</tbody>
</table>
</div>
</x-app-layout>
@@ -0,0 +1,36 @@
<x-app-layout title="Settings">
<div class="mx-auto max-w-lg">
<h1 class="text-2xl font-semibold">Organization settings</h1>
<form method="POST" action="{{ route('qms.settings.update') }}" class="mt-6 space-y-4 rounded-2xl border bg-white p-6 dark:border-slate-700 dark:bg-slate-900">
@csrf @method('PUT')
<div><label class="block text-sm font-medium">Organization name</label>
<input name="name" value="{{ $organization->name }}" @disabled(! $canManage) required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div><label class="block text-sm font-medium">Timezone</label>
<input name="timezone" value="{{ $organization->timezone }}" @disabled(! $canManage) required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div><label class="block text-sm font-medium">Appointment mode</label>
<select name="appointment_mode" @disabled(! $canManage) class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@foreach ($appointmentModes as $v => $l)
<option value="{{ $v }}" @selected(($organization->settings['appointment_mode'] ?? 'hybrid') === $v)>{{ $l }}</option>
@endforeach
</select>
</div>
<div><label class="block text-sm font-medium">Industry</label>
<select name="industry" @disabled(! $canManage) class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@foreach ($industries as $v => $l)
<option value="{{ $v }}" @selected(($organization->settings['industry'] ?? '') === $v)>{{ $l }}</option>
@endforeach
</select>
</div>
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" name="notifications_enabled" value="1" @checked($organization->settings['notifications_enabled'] ?? true) @disabled(! $canManage)>
Enable SMS/email queue notifications
</label>
@if ($canManage)
<button type="submit" class="btn-primary w-full">Save settings</button>
@endif
</form>
<p class="mt-4 text-sm text-slate-600">{{ $branchCount }} branch{{ $branchCount === 1 ? '' : 'es' }} configured.</p>
</div>
</x-app-layout>
@@ -0,0 +1,33 @@
<x-app-layout title="Issue ticket">
<div class="mx-auto max-w-lg">
<h1 class="text-2xl font-semibold">Issue ticket</h1>
<form method="POST" action="{{ route('qms.tickets.store') }}" class="mt-6 space-y-4 rounded-2xl border bg-white p-6 dark:border-slate-700 dark:bg-slate-900">
@csrf
<div>
<label class="block text-sm font-medium">Queue</label>
<select name="service_queue_id" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@foreach ($queues as $queue)
<option value="{{ $queue->id }}">{{ $queue->name }} ({{ $queue->branch?->name }})</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium">Customer name</label>
<input name="customer_name" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-sm font-medium">Phone</label>
<input name="customer_phone" type="tel" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-sm font-medium">Priority</label>
<select name="priority" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@foreach (config('qms.ticket_priorities') as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
</div>
<button type="submit" class="btn-primary w-full">Issue ticket</button>
</form>
</div>
</x-app-layout>
@@ -0,0 +1,29 @@
<x-app-layout title="Tickets">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-semibold text-slate-900 dark:text-white">Tickets</h1>
@if(app(\App\Services\Qms\QmsPermissions::class)->can(app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()), 'tickets.issue'))
<a href="{{ route('qms.tickets.create') }}" class="btn-primary">Issue ticket</a>
@endif
</div>
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900">
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-700">
<thead class="bg-slate-50 dark:bg-slate-800"><tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Ticket</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Queue</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Status</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Issued</th>
</tr></thead>
<tbody class="divide-y divide-slate-200 dark:divide-slate-700">
@foreach ($tickets as $ticket)
<tr>
<td class="px-4 py-3"><a href="{{ route('qms.tickets.show', $ticket) }}" class="font-mono font-semibold text-indigo-600">{{ $ticket->ticket_number }}</a></td>
<td class="px-4 py-3 text-sm">{{ $ticket->serviceQueue?->name }}</td>
<td class="px-4 py-3 text-sm capitalize">{{ str_replace('_', ' ', $ticket->status) }}</td>
<td class="px-4 py-3 text-sm text-slate-500">{{ $ticket->issued_at?->format('M j, H:i') }}</td>
</tr>
@endforeach
</tbody>
</table>
<div class="border-t px-4 py-3">{{ $tickets->links() }}</div>
</div>
</x-app-layout>
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html><head><title>{{ $ticket->ticket_number }}</title>
<style>body{font-family:system-ui;text-align:center;padding:2rem} .num{font-size:3rem;font-weight:bold}</style></head>
<body onload="window.print()">
<p>{{ $ticket->serviceQueue?->name }}</p>
<p class="num">{{ $ticket->ticket_number }}</p>
<p>{{ $ticket->issued_at?->format('M j, Y H:i') }}</p>
@if ($ticket->estimated_wait_seconds)
<p>Est. wait: {{ ceil($ticket->estimated_wait_seconds / 60) }} minutes</p>
@endif
<p style="font-size:.8rem;margin-top:2rem">{{ $ticket->barcode }}</p>
</body></html>
@@ -0,0 +1,21 @@
<x-app-layout title="Ticket {{ $ticket->ticket_number }}">
<div class="mx-auto max-w-lg">
@if (session('success'))
<p class="mb-4 rounded-lg bg-emerald-50 px-3 py-2 text-sm text-emerald-800">{{ session('success') }}</p>
@endif
<div class="rounded-2xl border bg-white p-8 text-center dark:border-slate-700 dark:bg-slate-900">
<p class="text-sm uppercase text-slate-500">Ticket</p>
<p class="text-5xl font-bold text-indigo-600">{{ $ticket->ticket_number }}</p>
<p class="mt-2 text-lg">{{ $ticket->serviceQueue?->name }}</p>
<p class="mt-1 capitalize text-slate-600">{{ str_replace('_', ' ', $ticket->status) }}</p>
@if ($ticket->estimated_wait_seconds)
<p class="mt-4 text-slate-600">Est. wait: {{ ceil($ticket->estimated_wait_seconds / 60) }} mins</p>
@endif
<p class="mt-4 text-sm text-slate-500">Track: {{ route('qms.mobile.show', $ticket->qr_token) }}</p>
<div class="mt-6 flex justify-center gap-3">
<a href="{{ route('qms.tickets.print', $ticket) }}" target="_blank" class="btn-secondary">Print</a>
<a href="{{ route('qms.tickets.index') }}" class="btn-primary">All tickets</a>
</div>
</div>
</div>
</x-app-layout>
@@ -0,0 +1,50 @@
<x-app-layout title="New workflow">
<div class="mx-auto max-w-2xl" x-data="{
steps: [{ name: '', service_queue_id: '' }],
templates: @js($workflowTemplates),
applyTemplate(key) {
if (!this.templates[key]) return;
this.steps = this.templates[key].map(s => ({ name: s.name, service_queue_id: '' }));
}
}">
<h1 class="text-2xl font-semibold">Create workflow</h1>
<form method="POST" action="{{ route('qms.workflows.store') }}" class="mt-6 space-y-4 rounded-2xl border bg-white p-6 dark:border-slate-700 dark:bg-slate-900">
@csrf
<div>
<label class="block text-sm font-medium">Industry template</label>
<select @change="applyTemplate($event.target.value)" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<option value="">Custom</option>
@foreach ($templates as $key => $label)
<option value="{{ $key }}">{{ $label }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium">Name</label>
<input name="name" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-sm font-medium">Description</label>
<textarea name="description" rows="2" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></textarea>
</div>
<div class="border-t pt-4">
<div class="mb-2 flex items-center justify-between">
<h2 class="text-sm font-semibold">Steps</h2>
<button type="button" @click="steps.push({ name: '', service_queue_id: '' })" class="text-xs text-indigo-600">Add step</button>
</div>
<template x-for="(step, index) in steps" :key="index">
<div class="mb-3 grid grid-cols-2 gap-3 rounded-lg border p-3">
<input :name="'steps['+index+'][name]'" x-model="step.name" placeholder="Step name" required class="rounded-lg border-slate-300 text-sm">
<select :name="'steps['+index+'][service_queue_id]'" x-model="step.service_queue_id" class="rounded-lg border-slate-300 text-sm">
<option value="">Select queue</option>
@foreach ($queues as $queue)
<option value="{{ $queue->id }}">{{ $queue->name }}</option>
@endforeach
</select>
</div>
</template>
</div>
<button type="submit" class="btn-primary w-full">Create workflow</button>
</form>
</div>
</x-app-layout>
@@ -0,0 +1,22 @@
<x-app-layout :title="'Edit '.$workflow->name">
<div class="mx-auto max-w-2xl" x-data="{ steps: @js($workflow->steps->map(fn($s) => ['name' => $s->name, 'service_queue_id' => $s->service_queue_id])->values()) }">
<h1 class="text-2xl font-semibold">Edit workflow</h1>
<form method="POST" action="{{ route('qms.workflows.update', $workflow) }}" class="mt-6 space-y-4 rounded-2xl border bg-white p-6">
@csrf @method('PUT')
<div><label class="block text-sm font-medium">Name</label><input name="name" value="{{ $workflow->name }}" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
<div><label class="block text-sm font-medium">Description</label><textarea name="description" rows="2" class="mt-1 w-full rounded-lg border-slate-300 text-sm">{{ $workflow->description }}</textarea></div>
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="is_active" value="1" @checked($workflow->is_active)> Active</label>
<template x-for="(step, index) in steps" :key="index">
<div class="grid grid-cols-2 gap-3 rounded-lg border p-3">
<input :name="'steps['+index+'][name]'" x-model="step.name" required class="rounded-lg border-slate-300 text-sm">
<select :name="'steps['+index+'][service_queue_id]'" x-model="step.service_queue_id" class="rounded-lg border-slate-300 text-sm">
<option value="">Select queue</option>
@foreach ($queues as $queue)<option value="{{ $queue->id }}">{{ $queue->name }}</option>@endforeach
</select>
</div>
</template>
<button type="button" @click="steps.push({name:'',service_queue_id:''})" class="text-sm text-indigo-600">Add step</button>
<button type="submit" class="btn-primary w-full">Save workflow</button>
</form>
</div>
</x-app-layout>
@@ -0,0 +1,29 @@
<x-app-layout title="Workflows">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-semibold text-slate-900 dark:text-white">Workflows</h1>
@if(app(\App\Services\Qms\QmsPermissions::class)->can(app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()), 'workflows.manage'))
<a href="{{ route('qms.workflows.create') }}" class="btn-primary">New workflow</a>
@endif
</div>
@include('partials.flash')
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900">
<table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50"><tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Name</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Steps</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Status</th>
</tr></thead>
<tbody class="divide-y divide-slate-200">
@forelse ($workflows as $workflow)
<tr>
<td class="px-4 py-3"><a href="{{ route('qms.workflows.show', $workflow) }}" class="text-sm font-medium text-indigo-600">{{ $workflow->name }}</a></td>
<td class="px-4 py-3 text-sm">{{ $workflow->steps_count }}</td>
<td class="px-4 py-3 text-sm">{{ $workflow->is_active ? 'Active' : 'Inactive' }}</td>
</tr>
@empty
<tr><td colspan="3" class="px-6 py-12 text-center text-sm text-slate-500">No workflows configured.</td></tr>
@endforelse
</tbody>
</table>
</div>
</x-app-layout>
@@ -0,0 +1,21 @@
<x-app-layout :title="$workflow->name">
<div class="mb-6">
<a href="{{ route('qms.workflows.index') }}" class="text-sm text-indigo-600"> Workflows</a>
<h1 class="mt-2 text-2xl font-semibold">{{ $workflow->name }}</h1>
@if ($workflow->description)<p class="mt-1 text-sm text-slate-600">{{ $workflow->description }}</p>@endif
</div>
<div class="rounded-2xl border bg-white p-6 dark:border-slate-700 dark:bg-slate-900">
<h2 class="text-sm font-semibold uppercase text-slate-500">Steps</h2>
<ol class="mt-4 space-y-3">
@foreach ($workflow->steps as $step)
<li class="flex items-center gap-3 rounded-lg border px-4 py-3">
<span class="flex h-8 w-8 items-center justify-center rounded-full bg-indigo-50 text-sm font-semibold text-indigo-700">{{ $step->sort_order }}</span>
<div>
<div class="font-medium">{{ $step->name }}</div>
<div class="text-sm text-slate-500">{{ $step->serviceQueue?->name ?? 'No queue linked' }}</div>
</div>
</li>
@endforeach
</ol>
</div>
</x-app-layout>