Initial Ladill Queue release — enterprise QMS standalone app.
Deploy Ladill Queue / deploy (push) Successful in 56s
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:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user