Add hero sections to Queue index pages and soften analytics cards.
Deploy Ladill Queue / deploy (push) Successful in 36s

Introduce shared x-qms.page-hero with summary stats across queues,
tickets, counters, admin pages, and insights; remove bordered cards on
Analytics, Reports, Feedback, and Branches list views.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-06 07:32:10 +00:00
co-authored by Cursor
parent 29bf896f21
commit bf879d6abe
26 changed files with 850 additions and 526 deletions
@@ -0,0 +1,39 @@
@props([
'badge',
'title',
'description' => null,
'stats' => [],
])
<div {{ $attributes->merge(['class' => 'relative overflow-hidden rounded-2xl border border-slate-200 bg-white']) }}>
<div class="absolute inset-0 bg-gradient-to-br from-indigo-50/80 via-white to-violet-50/60"></div>
<div class="relative px-6 py-8 sm:px-10">
<div class="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
<div class="max-w-xl">
<div class="inline-flex items-center gap-1.5 rounded-full bg-indigo-100 px-3 py-1 text-xs font-semibold text-indigo-700">
{{ $badge }}
</div>
<h1 class="mt-3 text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">{{ $title }}</h1>
@if ($description)
<p class="mt-2 text-sm leading-6 text-slate-600">{{ $description }}</p>
@endif
@isset($actions)
<div class="mt-6 flex flex-wrap items-center gap-2">
{{ $actions }}
</div>
@endisset
</div>
@if (count($stats) > 0)
<div class="scrollbar-hide flex snap-x snap-mandatory gap-3 overflow-x-auto pb-2 sm:grid sm:grid-cols-3 sm:overflow-visible sm:pb-0 lg:gap-4" style="-ms-overflow-style:none;scrollbar-width:none;">
@foreach ($stats as $stat)
<div class="mobile-stats-card shrink-0 snap-start rounded-xl border border-slate-200 bg-white/90 px-4 py-3 text-center backdrop-blur-sm">
<p class="whitespace-nowrap text-xl font-bold text-slate-900 sm:text-2xl">{{ $stat['value'] }}</p>
<p class="mt-0.5 whitespace-nowrap text-[11px] font-medium text-slate-500">{{ $stat['label'] }}</p>
</div>
@endforeach
</div>
@endif
</div>
</div>
</div>
@@ -1,31 +1,51 @@
@php
$canManageBranches = app(\App\Services\Qms\QmsPermissions::class)->can(
app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()),
'admin.branches.manage'
);
@endphp
<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 class="space-y-6">
<x-qms.page-hero
badge="Locations · Sites · Coverage"
title="Branches"
description="Organize queue operations by location — branches scope queues, counters, displays, and team access."
:stats="[
['value' => number_format($heroStats['total']), 'label' => 'Branches'],
['value' => number_format($heroStats['active']), 'label' => 'Active'],
['value' => number_format($heroStats['departments']), 'label' => 'Departments'],
]">
@if ($canManageBranches)
<x-slot name="actions">
<a href="{{ route('qms.branches.create') }}" class="btn-primary">New branch</a>
</x-slot>
@endif
</x-qms.page-hero>
@include('partials.flash')
<div class="overflow-hidden rounded-2xl bg-white">
<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>
</div>
</x-app-layout>
@@ -1,31 +1,51 @@
@php
$canManageDepartments = app(\App\Services\Qms\QmsPermissions::class)->can(
app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()),
'admin.departments.manage'
);
@endphp
<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 class="space-y-6">
<x-qms.page-hero
badge="Reception · Service · Routing"
title="Departments"
description="Group queues and counters by department within each branch — reception, customer service, payments, and more."
:stats="[
['value' => number_format($heroStats['total']), 'label' => 'Departments'],
['value' => number_format($heroStats['active']), 'label' => 'Active'],
['value' => number_format($heroStats['branches']), 'label' => 'Branches'],
]">
@if ($canManageDepartments)
<x-slot name="actions">
<a href="{{ route('qms.departments.create') }}" class="btn-primary">New department</a>
</x-slot>
@endif
</x-qms.page-hero>
@include('partials.flash')
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
<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>
</div>
</x-app-layout>
@@ -1,38 +1,58 @@
@php
$canManageMembers = app(\App\Services\Qms\QmsPermissions::class)->can(
app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()),
'admin.members.manage'
);
@endphp
<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">Member</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)
@php
$display = str_contains($member->user_ref, '@')
? $member->user_ref
: (\App\Models\User::where('public_id', $member->user_ref)->value('email') ?? $member->user_ref);
@endphp
<tr>
<td class="px-4 py-3 text-sm">{{ $display }}</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 class="space-y-6">
<x-qms.page-hero
badge="Roles · Access · Branch assignment"
title="Team"
description="Manage who can access Queue, their roles, and which branch each operator or admin belongs to."
:stats="[
['value' => number_format($heroStats['total']), 'label' => 'Members'],
['value' => number_format($heroStats['operators']), 'label' => 'Operators'],
['value' => number_format($heroStats['branches']), 'label' => 'Branches staffed'],
]">
@if ($canManageMembers)
<x-slot name="actions">
<a href="{{ route('qms.members.create') }}" class="btn-primary">Add member</a>
</x-slot>
@endif
</x-qms.page-hero>
@include('partials.flash')
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
<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">Member</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)
@php
$display = str_contains($member->user_ref, '@')
? $member->user_ref
: (\App\Models\User::where('public_id', $member->user_ref)->value('email') ?? $member->user_ref);
@endphp
<tr>
<td class="px-4 py-3 text-sm">{{ $display }}</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>
</div>
</x-app-layout>
+45 -36
View File
@@ -1,42 +1,51 @@
<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 class="space-y-6">
<x-qms.page-hero
badge="Live ops · Trends · Performance"
title="Analytics"
description="Real-time queue performance, wait times, and customer satisfaction across your organization."
:stats="[
['value' => number_format($overview['waiting'] ?? 0), 'label' => 'Waiting now'],
['value' => number_format($overview['served_today'] ?? 0), 'label' => 'Served today'],
['value' => $overview['feedback_avg_30d'] ?? '—', 'label' => 'Avg rating (30d)'],
]" />
<div class="grid gap-6 lg:grid-cols-2">
<div class="rounded-2xl bg-white p-6 shadow-sm">
<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 border-slate-100"><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 bg-white p-6 shadow-sm">
<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>
@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 class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
@foreach ([
'serving' => 'Being served',
'no_shows_today' => 'No-shows today',
'avg_wait_seconds' => 'Avg wait (sec)',
'appointments_today' => 'Appointments today',
] as $key => $label)
<div class="rounded-2xl bg-white p-4 shadow-sm">
<div class="text-xs font-semibold uppercase text-slate-500">{{ $label }}</div>
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ $overview[$key] ?? 0 }}</div>
</div>
@endforeach
</div>
</div>
</x-app-layout>
@@ -1,47 +1,64 @@
@php
$canManageAppointments = app(\App\Services\Qms\QmsPermissions::class)->can(
app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()),
'appointments.manage'
);
@endphp
<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)
<div class="space-y-6">
<x-qms.page-hero
badge="Pre-booked · Check-in · Tickets"
title="Appointments"
description="Schedule and check in pre-booked customers — checked-in appointments can issue queue tickets automatically."
:stats="[
['value' => number_format($heroStats['today']), 'label' => 'Today'],
['value' => number_format($heroStats['scheduled']), 'label' => 'Scheduled'],
['value' => number_format($heroStats['checked_in']), 'label' => 'Checked in'],
]">
@if ($canManageAppointments)
<x-slot name="actions">
<a href="{{ route('qms.appointments.create') }}" class="btn-primary">New appointment</a>
</x-slot>
@endif
</x-qms.page-hero>
@include('partials.flash')
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
<table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50">
<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>
<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>
@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>
</thead>
<tbody class="divide-y divide-slate-200">
@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>{{ $appointments->links() }}</div>
</div>
<div class="mt-4">{{ $appointments->links() }}</div>
</x-app-layout>
+46 -38
View File
@@ -1,42 +1,50 @@
<x-app-layout title="Counters">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<h1 class="text-xl font-semibold text-slate-900">Counters</h1>
<p class="mt-1 text-sm text-slate-500">Service desks and windows where staff call and serve tickets</p>
</div>
<a href="{{ route('qms.counters.create') }}" class="btn-primary">New counter</a>
</div>
<div class="space-y-6">
<x-qms.page-hero
badge="Desks · Windows · Consoles"
title="Counters"
description="Service desks and windows where staff call and serve tickets — each counter links to one or more queues."
:stats="[
['value' => number_format($heroStats['total']), 'label' => 'Counters'],
['value' => number_format($heroStats['available']), 'label' => 'Available'],
['value' => number_format($heroStats['busy']), 'label' => 'Busy'],
]">
<x-slot name="actions">
<a href="{{ route('qms.counters.create') }}" class="btn-primary">New counter</a>
</x-slot>
</x-qms.page-hero>
<div class="mt-5 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
@forelse ($counters as $counter)
<article class="flex flex-col rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
<div class="flex items-start justify-between gap-3">
<div>
<a href="{{ route('qms.counters.show', $counter) }}" class="text-lg font-semibold text-slate-900 hover:text-indigo-700">{{ $counter->name }}</a>
<p class="mt-1 text-sm text-slate-500">{{ $counter->branch?->name }}</p>
</div>
<x-counter-status :status="$counter->status" />
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
@forelse ($counters as $counter)
<article class="flex flex-col rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
<div class="flex items-start justify-between gap-3">
<div>
<a href="{{ route('qms.counters.show', $counter) }}" class="text-lg font-semibold text-slate-900 hover:text-indigo-700">{{ $counter->name }}</a>
<p class="mt-1 text-sm text-slate-500">{{ $counter->branch?->name }}</p>
</div>
<x-counter-status :status="$counter->status" />
</div>
<p class="mt-4 text-sm text-slate-600">
<span class="font-medium text-slate-700">Queues:</span>
{{ $counter->serviceQueues->pluck('name')->join(', ') ?: 'None assigned' }}
</p>
<div class="mt-5 btn-group">
<a href="{{ route('qms.console.show', $counter) }}" class="btn-primary flex-1 text-center text-sm">Open console</a>
<a href="{{ route('qms.counters.show', $counter) }}" class="btn-secondary text-sm">Details</a>
</div>
</article>
@empty
<div class="col-span-full">
<x-empty-state title="No counters yet" description="Create a counter for each service desk or teller window.">
<x-slot:action>
<a href="{{ route('qms.counters.create') }}" class="btn-primary">Create your first counter</a>
</x-slot:action>
</x-empty-state>
</div>
@endforelse
</div>
<p class="mt-4 text-sm text-slate-600">
<span class="font-medium text-slate-700">Queues:</span>
{{ $counter->serviceQueues->pluck('name')->join(', ') ?: 'None assigned' }}
</p>
<div class="mt-5 btn-group">
<a href="{{ route('qms.console.show', $counter) }}" class="btn-primary flex-1 text-center text-sm">Open console</a>
<a href="{{ route('qms.counters.show', $counter) }}" class="btn-secondary text-sm">Details</a>
</div>
</article>
@empty
<div class="col-span-full">
<x-empty-state title="No counters yet" description="Create a counter for each service desk or teller window.">
<x-slot:action>
<a href="{{ route('qms.counters.create') }}" class="btn-primary">Create your first counter</a>
</x-slot:action>
</x-empty-state>
</div>
@endforelse
</div>
@if ($counters->hasPages())
<div class="mt-4">{{ $counters->links() }}</div>
@endif
@if ($counters->hasPages())
<div>{{ $counters->links() }}</div>
@endif
</div>
</x-app-layout>
+76 -68
View File
@@ -1,75 +1,83 @@
<x-app-layout title="Devices">
<div class="flex items-center justify-between gap-3">
<div>
<h1 class="text-xl font-semibold text-slate-900">Devices</h1>
<p class="text-sm text-slate-500">Kiosks and ticket printers for self-service and reception</p>
</div>
@if ($canManage)
<a href="{{ route('qms.devices.create') }}" class="btn-primary">Register device</a>
<div class="space-y-6">
<x-qms.page-hero
badge="Kiosks · Printers · Self-service"
title="Devices"
description="Kiosks and ticket printers for self-service check-in and reception — register devices and open kiosk mode from here."
:stats="[
['value' => number_format($heroStats['total']), 'label' => 'Devices'],
['value' => number_format($heroStats['online']), 'label' => 'Online'],
['value' => number_format($heroStats['kiosks']), 'label' => 'Kiosks'],
]">
@if ($canManage)
<x-slot name="actions">
<a href="{{ route('qms.devices.create') }}" class="btn-primary">Register device</a>
</x-slot>
@endif
</x-qms.page-hero>
@if (session('success'))
<div class="rounded-lg bg-emerald-50 px-4 py-3 text-sm text-emerald-800">{{ session('success') }}</div>
@endif
</div>
@if (session('success'))
<div class="mt-4 rounded-lg bg-emerald-50 px-4 py-3 text-sm text-emerald-800">{{ session('success') }}</div>
@endif
<div class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white">
@if ($devices->isEmpty())
<x-empty-state
title="No devices registered yet"
description="Register a kiosk tablet or ticket printer so customers can join queues without staff assistance."
>
<x-slot:icon>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-15 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21" />
</svg>
</x-slot:icon>
@if ($canManage)
<x-slot:action>
<a href="{{ route('qms.devices.create') }}" class="btn-primary">Register your first device</a>
</x-slot:action>
@endif
</x-empty-state>
@else
<table class="min-w-full divide-y divide-slate-100 text-sm">
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
<tr>
<th class="px-4 py-3">Name</th>
<th class="px-4 py-3">Type</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3">Branch</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50">
@foreach ($devices as $device)
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
@if ($devices->isEmpty())
<x-empty-state
title="No devices registered yet"
description="Register a kiosk tablet or ticket printer so customers can join queues without staff assistance."
>
<x-slot:icon>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-15 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21" />
</svg>
</x-slot:icon>
@if ($canManage)
<x-slot:action>
<a href="{{ route('qms.devices.create') }}" class="btn-primary">Register your first device</a>
</x-slot:action>
@endif
</x-empty-state>
@else
<table class="min-w-full divide-y divide-slate-100 text-sm">
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
<tr>
<td class="px-4 py-3 font-medium text-slate-900">{{ $device->name }}</td>
<td class="px-4 py-3 text-slate-600">{{ $deviceTypes[$device->type] ?? $device->type }}</td>
<td class="px-4 py-3">
<span class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium {{ $device->status === 'online' ? 'bg-emerald-100 text-emerald-800' : 'bg-slate-100 text-slate-600' }}">
{{ ucfirst($device->status) }}
</span>
</td>
<td class="px-4 py-3 text-slate-600">{{ $device->branch?->name ?? '—' }}</td>
<td class="px-4 py-3 text-right">
@if ($device->type === 'kiosk' && $device->device_token)
<a href="{{ route('qms.kiosk.device', $device->device_token) }}" target="_blank" class="text-indigo-700 hover:underline">Open kiosk</a>
@endif
@if ($canManage)
<form method="POST" action="{{ route('qms.devices.regenerate-token', $device) }}" class="ml-3 inline">
@csrf
<button type="submit" class="text-indigo-700 hover:underline">Regenerate token</button>
</form>
@endif
</td>
<th class="px-4 py-3">Name</th>
<th class="px-4 py-3">Type</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3">Branch</th>
<th class="px-4 py-3"></th>
</tr>
@endforeach
</tbody>
</table>
<div class="border-t border-slate-200 px-4 py-3">
{{ $devices->links() }}
</div>
@endif
</thead>
<tbody class="divide-y divide-slate-50">
@foreach ($devices as $device)
<tr>
<td class="px-4 py-3 font-medium text-slate-900">{{ $device->name }}</td>
<td class="px-4 py-3 text-slate-600">{{ $deviceTypes[$device->type] ?? $device->type }}</td>
<td class="px-4 py-3">
<span class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium {{ $device->status === 'online' ? 'bg-emerald-100 text-emerald-800' : 'bg-slate-100 text-slate-600' }}">
{{ ucfirst($device->status) }}
</span>
</td>
<td class="px-4 py-3 text-slate-600">{{ $device->branch?->name ?? '—' }}</td>
<td class="px-4 py-3 text-right">
@if ($device->type === 'kiosk' && $device->device_token)
<a href="{{ route('qms.kiosk.device', $device->device_token) }}" target="_blank" class="text-indigo-700 hover:underline">Open kiosk</a>
@endif
@if ($canManage)
<form method="POST" action="{{ route('qms.devices.regenerate-token', $device) }}" class="ml-3 inline">
@csrf
<button type="submit" class="text-indigo-700 hover:underline">Regenerate token</button>
</form>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="border-t border-slate-200 px-4 py-3">
{{ $devices->links() }}
</div>
@endif
</div>
</div>
</x-app-layout>
+61 -53
View File
@@ -1,59 +1,67 @@
<x-app-layout title="Displays">
<div class="flex items-center justify-between gap-3">
<div>
<h1 class="text-xl font-semibold text-slate-900">Display screens</h1>
<p class="text-sm text-slate-500">Waiting-area screens and voice announcements for called tickets</p>
</div>
@if ($canManage)
<a href="{{ route('qms.displays.create') }}" class="btn-primary">New display</a>
@endif
</div>
<div class="space-y-6">
<x-qms.page-hero
badge="Waiting areas · Voice · Ticket calls"
title="Display screens"
description="Waiting-area screens and voice announcements so customers see called tickets and hear when their number is up."
:stats="[
['value' => number_format($heroStats['total']), 'label' => 'Displays'],
['value' => number_format($heroStats['active']), 'label' => 'Active'],
['value' => number_format($heroStats['branches']), 'label' => 'Branches'],
]">
@if ($canManage)
<x-slot name="actions">
<a href="{{ route('qms.displays.create') }}" class="btn-primary">New display</a>
</x-slot>
@endif
</x-qms.page-hero>
<div class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white">
@if ($screens->isEmpty())
<x-empty-state
title="No display screens yet"
description="Create a display for your waiting area so customers can see the queue and hear ticket calls."
>
<x-slot:icon>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 20.25h12m-7.5-3v3m3-3v3m-10.125-3H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25ZM6.75 12h.008v.008H6.75V12Zm0 3h.008v.008H6.75V15Zm0 3h.008v.008H6.75V18Z" />
</svg>
</x-slot:icon>
@if ($canManage)
<x-slot:action>
<a href="{{ route('qms.displays.create') }}" class="btn-primary">Create your first display</a>
</x-slot:action>
@endif
</x-empty-state>
@else
<table class="min-w-full divide-y divide-slate-100 text-sm">
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
<tr>
<th class="px-4 py-3">Name</th>
<th class="px-4 py-3">Layout</th>
<th class="px-4 py-3">Branch</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50">
@foreach ($screens as $screen)
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
@if ($screens->isEmpty())
<x-empty-state
title="No display screens yet"
description="Create a display for your waiting area so customers can see the queue and hear ticket calls."
>
<x-slot:icon>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 20.25h12m-7.5-3v3m3-3v3m-10.125-3H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25ZM6.75 12h.008v.008H6.75V12Zm0 3h.008v.008H6.75V15Zm0 3h.008v.008H6.75V18Z" />
</svg>
</x-slot:icon>
@if ($canManage)
<x-slot:action>
<a href="{{ route('qms.displays.create') }}" class="btn-primary">Create your first display</a>
</x-slot:action>
@endif
</x-empty-state>
@else
<table class="min-w-full divide-y divide-slate-100 text-sm">
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
<tr>
<td class="px-4 py-3 font-medium text-slate-900">
<a href="{{ route('qms.displays.show', $screen) }}" class="hover:text-indigo-600">{{ $screen->name }}</a>
</td>
<td class="px-4 py-3 text-slate-600">{{ config('qms.display_layouts.'.$screen->layout, $screen->layout) }}</td>
<td class="px-4 py-3 text-slate-600">{{ $screen->branch?->name ?? '—' }}</td>
<td class="px-4 py-3 text-right">
<a href="{{ route('qms.display.public', $screen->access_token) }}" target="_blank" class="text-indigo-700 hover:underline">Open display</a>
</td>
<th class="px-4 py-3">Name</th>
<th class="px-4 py-3">Layout</th>
<th class="px-4 py-3">Branch</th>
<th class="px-4 py-3"></th>
</tr>
@endforeach
</tbody>
</table>
<div class="border-t border-slate-200 px-4 py-3">
{{ $screens->links() }}
</div>
@endif
</thead>
<tbody class="divide-y divide-slate-50">
@foreach ($screens as $screen)
<tr>
<td class="px-4 py-3 font-medium text-slate-900">
<a href="{{ route('qms.displays.show', $screen) }}" class="hover:text-indigo-600">{{ $screen->name }}</a>
</td>
<td class="px-4 py-3 text-slate-600">{{ config('qms.display_layouts.'.$screen->layout, $screen->layout) }}</td>
<td class="px-4 py-3 text-slate-600">{{ $screen->branch?->name ?? '—' }}</td>
<td class="px-4 py-3 text-right">
<a href="{{ route('qms.display.public', $screen->access_token) }}" target="_blank" class="text-indigo-700 hover:underline">Open display</a>
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="border-t border-slate-200 px-4 py-3">
{{ $screens->links() }}
</div>
@endif
</div>
</div>
</x-app-layout>
+34 -26
View File
@@ -1,29 +1,37 @@
<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 class="space-y-6">
<x-qms.page-hero
badge="Ratings · Comments · Service quality"
title="Customer feedback"
description="Reviews collected after service — track average ratings and read customer comments by ticket."
:stats="[
['value' => number_format($heroStats['total']), 'label' => 'Responses'],
['value' => $heroStats['avg_rating'] ?: '—', 'label' => 'Avg rating'],
['value' => number_format($heroStats['this_month']), 'label' => 'This month'],
]" />
<div class="overflow-hidden rounded-2xl bg-white">
<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>{{ $feedback->links() }}</div>
</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>
+82 -66
View File
@@ -1,73 +1,89 @@
@php
$canManageQueues = app(\App\Services\Qms\QmsPermissions::class)->can(
app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()),
'queues.manage'
);
@endphp
<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>
<div class="space-y-6">
<x-qms.page-hero
badge="Service lines · Prefixes · Routing"
title="Service queues"
description="Configure queues for reception, counters, and departments — each with its own ticket prefix and routing strategy."
:stats="[
['value' => number_format($heroStats['total']), 'label' => 'Queues'],
['value' => number_format($heroStats['active']), 'label' => 'Active'],
['value' => number_format($heroStats['paused']), 'label' => 'Paused'],
]">
@if ($canManageQueues)
<x-slot name="actions">
<a href="{{ route('qms.queues.create') }}" class="btn-primary">New queue</a>
</x-slot>
@endif
</x-qms.page-hero>
@if (session('success'))
<div class="rounded-lg bg-emerald-50 px-4 py-3 text-sm text-emerald-800">{{ session('success') }}</div>
@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)
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
@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">
<thead class="bg-slate-50">
<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>
<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>
</tr>
@endforeach
</tbody>
</table>
<div class="border-t border-slate-200 px-4 py-3 dark:border-slate-700">
{{ $queues->links() }}
</div>
@endif
</thead>
<tbody class="divide-y divide-slate-200">
@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">
<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">{{ $queue->branch?->name }}</td>
<td class="px-4 py-3 text-sm font-mono text-slate-600">{{ $queue->prefix }}</td>
<td class="px-4 py-3 text-sm text-slate-600">{{ 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">
{{ $queues->links() }}
</div>
@endif
</div>
</div>
</x-app-layout>
+19 -8
View File
@@ -1,11 +1,22 @@
<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 class="space-y-6">
<x-qms.page-hero
badge="Export · Metrics · CSV"
title="Reports"
description="Drill into tickets, wait times, counters, appointments, and feedback — view metrics and export CSV for any date range."
:stats="[
['value' => number_format($heroStats['reports']), 'label' => 'Report types'],
['value' => number_format($heroStats['branches']), 'label' => 'Branches'],
['value' => 'CSV', 'label' => 'Export format'],
]" />
<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 bg-white p-6 shadow-sm transition hover:shadow-md">
<h2 class="font-semibold text-slate-900">{{ $label }}</h2>
<p class="mt-1 text-sm text-slate-600">View metrics and export CSV</p>
</a>
@endforeach
</div>
</div>
</x-app-layout>
+81 -66
View File
@@ -1,70 +1,85 @@
@php
$canIssueTickets = app(\App\Services\Qms\QmsPermissions::class)->can(
app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()),
'tickets.issue'
);
@endphp
<x-app-layout title="Tickets">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<h1 class="text-xl font-semibold text-slate-900">Tickets</h1>
<p class="mt-1 text-sm text-slate-500">Live and historical queue tickets across all service lines</p>
</div>
@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="space-y-6">
<x-qms.page-hero
badge="Live · Historical · All service lines"
title="Tickets"
description="Live and historical queue tickets across all service lines — filter by queue, status, or issue new walk-in tickets."
:stats="[
['value' => number_format($heroStats['today']), 'label' => 'Issued today'],
['value' => number_format($heroStats['waiting']), 'label' => 'Waiting'],
['value' => number_format($heroStats['serving']), 'label' => 'Being served'],
]">
@if ($canIssueTickets)
<x-slot name="actions">
<a href="{{ route('qms.tickets.create') }}" class="btn-primary">Issue ticket</a>
</x-slot>
@endif
</x-qms.page-hero>
<form method="GET" class="mt-5 flex flex-wrap items-end gap-3">
<div>
<label class="block text-xs font-medium uppercase tracking-wide text-slate-500">Queue</label>
<select name="queue" class="mt-1 rounded-lg border-slate-300 text-sm">
<option value="">All queues</option>
@foreach ($queues as $queue)
<option value="{{ $queue->uuid }}" @selected(request('queue') === $queue->uuid)>{{ $queue->name }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-medium uppercase tracking-wide text-slate-500">Status</label>
<select name="status" class="mt-1 rounded-lg border-slate-300 text-sm">
<option value="">All statuses</option>
@foreach (config('qms.ticket_statuses') as $value => $label)
<option value="{{ $value }}" @selected(request('status') === $value)>{{ $label }}</option>
@endforeach
</select>
</div>
<button type="submit" class="btn-secondary text-sm">Filter</button>
@if (request()->hasAny(['queue', 'status']))
<a href="{{ route('qms.tickets.index') }}" class="text-sm text-slate-500 hover:text-slate-700">Clear</a>
@endif
</form>
<form method="GET" class="flex flex-wrap items-end gap-3 rounded-2xl border border-slate-200 bg-white p-4">
<div>
<label class="block text-xs font-medium uppercase tracking-wide text-slate-500">Queue</label>
<select name="queue" class="mt-1 rounded-lg border-slate-300 text-sm">
<option value="">All queues</option>
@foreach ($queues as $queue)
<option value="{{ $queue->uuid }}" @selected(request('queue') === $queue->uuid)>{{ $queue->name }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-medium uppercase tracking-wide text-slate-500">Status</label>
<select name="status" class="mt-1 rounded-lg border-slate-300 text-sm">
<option value="">All statuses</option>
@foreach (config('qms.ticket_statuses') as $value => $label)
<option value="{{ $value }}" @selected(request('status') === $value)>{{ $label }}</option>
@endforeach
</select>
</div>
<button type="submit" class="btn-secondary text-sm">Filter</button>
@if (request()->hasAny(['queue', 'status']))
<a href="{{ route('qms.tickets.index') }}" class="text-sm text-slate-500 hover:text-slate-700">Clear</a>
@endif
</form>
<div class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white">
@if ($tickets->isEmpty())
<x-empty-state title="No tickets match your filters" description="Issue a walk-in ticket or adjust filters to see results." />
@else
<table class="min-w-full divide-y divide-slate-100 text-sm">
<thead class="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500">
<tr>
<th class="px-4 py-3">Ticket</th>
<th class="px-4 py-3">Customer</th>
<th class="px-4 py-3">Queue</th>
<th class="px-4 py-3">Counter</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3">Issued</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50">
@foreach ($tickets as $ticket)
<tr class="hover:bg-slate-50/80">
<td class="px-4 py-3">
<a href="{{ route('qms.tickets.show', $ticket) }}" class="font-mono text-base font-semibold text-indigo-700 hover:underline">{{ $ticket->ticket_number }}</a>
</td>
<td class="px-4 py-3 text-slate-700">{{ $ticket->customer_name ?: 'Walk-in' }}</td>
<td class="px-4 py-3 text-slate-600">{{ $ticket->serviceQueue?->name }}</td>
<td class="px-4 py-3 text-slate-600">{{ $ticket->counter?->name ?? '—' }}</td>
<td class="px-4 py-3"><x-ticket-status :status="$ticket->status" /></td>
<td class="px-4 py-3 text-slate-500">{{ $ticket->issued_at?->format('M j, H:i') }}</td>
</tr>
@endforeach
</tbody>
</table>
<div class="border-t border-slate-200 px-4 py-3">{{ $tickets->links() }}</div>
@endif
</div>
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
@if ($tickets->isEmpty())
<x-empty-state title="No tickets match your filters" description="Issue a walk-in ticket or adjust filters to see results." />
@else
<table class="min-w-full divide-y divide-slate-100 text-sm">
<thead class="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500">
<tr>
<th class="px-4 py-3">Ticket</th>
<th class="px-4 py-3">Customer</th>
<th class="px-4 py-3">Queue</th>
<th class="px-4 py-3">Counter</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3">Issued</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50">
@foreach ($tickets as $ticket)
<tr class="hover:bg-slate-50/80">
<td class="px-4 py-3">
<a href="{{ route('qms.tickets.show', $ticket) }}" class="font-mono text-base font-semibold text-indigo-700 hover:underline">{{ $ticket->ticket_number }}</a>
</td>
<td class="px-4 py-3 text-slate-700">{{ $ticket->customer_name ?: 'Walk-in' }}</td>
<td class="px-4 py-3 text-slate-600">{{ $ticket->serviceQueue?->name }}</td>
<td class="px-4 py-3 text-slate-600">{{ $ticket->counter?->name ?? '—' }}</td>
<td class="px-4 py-3"><x-ticket-status :status="$ticket->status" /></td>
<td class="px-4 py-3 text-slate-500">{{ $ticket->issued_at?->format('M j, H:i') }}</td>
</tr>
@endforeach
</tbody>
</table>
<div class="border-t border-slate-200 px-4 py-3">{{ $tickets->links() }}</div>
@endif
</div>
</div>
</x-app-layout>
+46 -26
View File
@@ -1,29 +1,49 @@
@php
$canManageWorkflows = app(\App\Services\Qms\QmsPermissions::class)->can(
app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()),
'workflows.manage'
);
@endphp
<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 class="space-y-6">
<x-qms.page-hero
badge="Multi-step · Routing · Automation"
title="Workflows"
description="Define multi-step customer journeys that route tickets through queues, counters, and departments in sequence."
:stats="[
['value' => number_format($heroStats['total']), 'label' => 'Workflows'],
['value' => number_format($heroStats['active']), 'label' => 'Active'],
['value' => number_format($heroStats['steps']), 'label' => 'Total steps'],
]">
@if ($canManageWorkflows)
<x-slot name="actions">
<a href="{{ route('qms.workflows.create') }}" class="btn-primary">New workflow</a>
</x-slot>
@endif
</x-qms.page-hero>
@include('partials.flash')
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
<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>
</div>
</x-app-layout>