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
@@ -38,7 +38,17 @@ class AppointmentController extends Controller
->orderBy('scheduled_at') ->orderBy('scheduled_at')
->paginate(30); ->paginate(30);
return view('qms.appointments.index', compact('appointments', 'organization')); $appointmentQuery = QueueAppointment::owned($owner)
->where('organization_id', $organization->id)
->when($branchScope, fn ($q) => $q->where('branch_id', $branchScope));
$heroStats = [
'today' => (clone $appointmentQuery)->whereDate('scheduled_at', today())->count(),
'scheduled' => (clone $appointmentQuery)->where('status', 'scheduled')->count(),
'checked_in' => (clone $appointmentQuery)->where('status', 'checked_in')->count(),
];
return view('qms.appointments.index', compact('appointments', 'organization', 'heroStats'));
} }
public function create(Request $request): View public function create(Request $request): View
@@ -26,7 +26,13 @@ class BranchController extends Controller
->orderBy('name') ->orderBy('name')
->get(); ->get();
return view('qms.admin.branches.index', compact('branches', 'organization')); $heroStats = [
'total' => $branches->count(),
'active' => $branches->where('is_active', true)->count(),
'departments' => $branches->sum('departments_count'),
];
return view('qms.admin.branches.index', compact('branches', 'organization', 'heroStats'));
} }
public function create(Request $request): View public function create(Request $request): View
+12 -1
View File
@@ -29,7 +29,18 @@ class CounterController extends Controller
->orderBy('name') ->orderBy('name')
->paginate(20); ->paginate(20);
return view('qms.counters.index', compact('counters', 'organization')); $counterStats = Counter::owned($owner)
->where('organization_id', $organization->id)
->when(true, fn ($q) => $this->scopeToBranch($request, $q))
->get();
$heroStats = [
'total' => $counterStats->count(),
'available' => $counterStats->where('status', 'available')->count(),
'busy' => $counterStats->where('status', 'busy')->count(),
];
return view('qms.counters.index', compact('counters', 'organization', 'heroStats'));
} }
public function show(Request $request, Counter $counter): View public function show(Request $request, Counter $counter): View
@@ -27,10 +27,17 @@ class DepartmentController extends Controller
->orderBy('name') ->orderBy('name')
->get(); ->get();
$heroStats = [
'total' => $departments->count(),
'active' => $departments->where('is_active', true)->count(),
'branches' => $departments->pluck('branch_id')->unique()->count(),
];
return view('qms.admin.departments.index', [ return view('qms.admin.departments.index', [
'departments' => $departments, 'departments' => $departments,
'organization' => $organization, 'organization' => $organization,
'types' => config('qms.department_types'), 'types' => config('qms.department_types'),
'heroStats' => $heroStats,
]); ]);
} }
@@ -38,11 +38,22 @@ class DeviceController extends Controller
->orderBy('name') ->orderBy('name')
->paginate(20); ->paginate(20);
$deviceStats = Device::owned($owner)
->where('organization_id', $organization->id)
->get();
$heroStats = [
'total' => $deviceStats->count(),
'online' => $deviceStats->where('status', 'online')->count(),
'kiosks' => $deviceStats->where('type', 'kiosk')->count(),
];
return view('qms.devices.index', [ return view('qms.devices.index', [
'devices' => $devices, 'devices' => $devices,
'organization' => $organization, 'organization' => $organization,
'deviceTypes' => config('qms.device_types'), 'deviceTypes' => config('qms.device_types'),
'canManage' => $permissions->can($member, 'displays.manage'), 'canManage' => $permissions->can($member, 'displays.manage'),
'heroStats' => $heroStats,
]); ]);
} }
@@ -33,10 +33,21 @@ class DisplayScreenController extends Controller
->orderBy('name') ->orderBy('name')
->paginate(20); ->paginate(20);
$screenStats = DisplayScreen::owned($owner)
->where('organization_id', $organization->id)
->get();
$heroStats = [
'total' => $screenStats->count(),
'active' => $screenStats->where('is_active', true)->count(),
'branches' => $screenStats->pluck('branch_id')->filter()->unique()->count(),
];
return view('qms.displays.index', [ return view('qms.displays.index', [
'screens' => $screens, 'screens' => $screens,
'organization' => $organization, 'organization' => $organization,
'canManage' => $permissions->can($member, 'displays.manage'), 'canManage' => $permissions->can($member, 'displays.manage'),
'heroStats' => $heroStats,
]); ]);
} }
@@ -32,10 +32,21 @@ class FeedbackAdminController extends Controller
->when($branchScope, fn ($q) => $q->whereHas('ticket', fn ($t) => $t->where('branch_id', $branchScope))) ->when($branchScope, fn ($q) => $q->whereHas('ticket', fn ($t) => $t->where('branch_id', $branchScope)))
->avg('rating'); ->avg('rating');
$feedbackQuery = CustomerFeedback::owned($owner)
->where('organization_id', $organization->id)
->when($branchScope, fn ($q) => $q->whereHas('ticket', fn ($t) => $t->where('branch_id', $branchScope)));
$heroStats = [
'total' => (clone $feedbackQuery)->count(),
'avg_rating' => round((float) $avgRating, 1),
'this_month' => (clone $feedbackQuery)->where('created_at', '>=', now()->startOfMonth())->count(),
];
return view('qms.feedback.index', [ return view('qms.feedback.index', [
'feedback' => $feedback, 'feedback' => $feedback,
'organization' => $organization, 'organization' => $organization,
'avgRating' => round((float) $avgRating, 1), 'avgRating' => round((float) $avgRating, 1),
'heroStats' => $heroStats,
]); ]);
} }
} }
@@ -27,10 +27,19 @@ class MemberController extends Controller
->orderBy('created_at') ->orderBy('created_at')
->get(); ->get();
$operatorRoles = ['queue_operator', 'counter_staff', 'receptionist'];
$heroStats = [
'total' => $members->count(),
'operators' => $members->whereIn('role', $operatorRoles)->count(),
'branches' => $members->whereNotNull('branch_id')->pluck('branch_id')->unique()->count(),
];
return view('qms.admin.members.index', [ return view('qms.admin.members.index', [
'members' => $members, 'members' => $members,
'organization' => $organization, 'organization' => $organization,
'roles' => config('qms.roles'), 'roles' => config('qms.roles'),
'heroStats' => $heroStats,
]); ]);
} }
@@ -36,6 +36,10 @@ class ReportController extends Controller
'organization' => $organization, 'organization' => $organization,
'branches' => $branches, 'branches' => $branches,
'reports' => config('qms.report_types'), 'reports' => config('qms.report_types'),
'heroStats' => [
'reports' => count(config('qms.report_types')),
'branches' => $branches->count(),
],
]); ]);
} }
@@ -30,7 +30,18 @@ class ServiceQueueController extends Controller
->orderBy('name') ->orderBy('name')
->paginate(20); ->paginate(20);
return view('qms.queues.index', compact('queues', 'organization')); $queueStats = ServiceQueue::owned($owner)
->where('organization_id', $organization->id)
->when(true, fn ($q) => $this->scopeToBranch($request, $q))
->get();
$heroStats = [
'total' => $queueStats->count(),
'active' => $queueStats->where('is_active', true)->where('is_paused', false)->count(),
'paused' => $queueStats->where('is_paused', true)->count(),
];
return view('qms.queues.index', compact('queues', 'organization', 'heroStats'));
} }
public function show(Request $request, ServiceQueue $serviceQueue): View public function show(Request $request, ServiceQueue $serviceQueue): View
@@ -37,7 +37,15 @@ class TicketController extends Controller
$queues = ServiceQueue::owned($owner)->where('organization_id', $organization->id)->orderBy('name')->get(); $queues = ServiceQueue::owned($owner)->where('organization_id', $organization->id)->orderBy('name')->get();
return view('qms.tickets.index', compact('tickets', 'queues', 'organization')); $ticketQuery = Ticket::owned($owner)->where('organization_id', $organization->id);
$heroStats = [
'today' => (clone $ticketQuery)->whereDate('issued_at', today())->count(),
'waiting' => (clone $ticketQuery)->where('status', 'waiting')->count(),
'serving' => (clone $ticketQuery)->whereIn('status', ['called', 'serving', 'on_hold'])->count(),
];
return view('qms.tickets.index', compact('tickets', 'queues', 'organization', 'heroStats'));
} }
public function create(Request $request): View public function create(Request $request): View
@@ -34,7 +34,13 @@ class WorkflowController extends Controller
->orderBy('name') ->orderBy('name')
->get(); ->get();
return view('qms.workflows.index', compact('workflows', 'organization')); $heroStats = [
'total' => $workflows->count(),
'active' => $workflows->where('is_active', true)->count(),
'steps' => $workflows->sum('steps_count'),
];
return view('qms.workflows.index', compact('workflows', 'organization', 'heroStats'));
} }
public function create(Request $request): View public function create(Request $request): View
@@ -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,12 +1,31 @@
@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"> <x-app-layout title="Branches">
<div class="mb-6 flex items-center justify-between"> <div class="space-y-6">
<h1 class="text-2xl font-semibold">Branches</h1> <x-qms.page-hero
@if(app(\App\Services\Qms\QmsPermissions::class)->can(app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()), 'admin.branches.manage')) 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> <a href="{{ route('qms.branches.create') }}" class="btn-primary">New branch</a>
</x-slot>
@endif @endif
</div> </x-qms.page-hero>
@include('partials.flash') @include('partials.flash')
<div class="overflow-hidden rounded-2xl border bg-white dark:border-slate-700 dark:bg-slate-900">
<div class="overflow-hidden rounded-2xl bg-white">
<table class="min-w-full divide-y divide-slate-200"> <table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50"><tr> <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">Name</th>
@@ -28,4 +47,5 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
</x-app-layout> </x-app-layout>
@@ -1,12 +1,31 @@
@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"> <x-app-layout title="Departments">
<div class="mb-6 flex items-center justify-between"> <div class="space-y-6">
<h1 class="text-2xl font-semibold">Departments</h1> <x-qms.page-hero
@if(app(\App\Services\Qms\QmsPermissions::class)->can(app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()), 'admin.departments.manage')) 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> <a href="{{ route('qms.departments.create') }}" class="btn-primary">New department</a>
</x-slot>
@endif @endif
</div> </x-qms.page-hero>
@include('partials.flash') @include('partials.flash')
<div class="overflow-hidden rounded-2xl border bg-white dark:border-slate-700 dark:bg-slate-900">
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
<table class="min-w-full divide-y divide-slate-200"> <table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50"><tr> <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">Name</th>
@@ -28,4 +47,5 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
</x-app-layout> </x-app-layout>
@@ -1,12 +1,31 @@
@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"> <x-app-layout title="Team">
<div class="mb-6 flex items-center justify-between"> <div class="space-y-6">
<h1 class="text-2xl font-semibold">Team members</h1> <x-qms.page-hero
@if(app(\App\Services\Qms\QmsPermissions::class)->can(app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()), 'admin.members.manage')) 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> <a href="{{ route('qms.members.create') }}" class="btn-primary">Add member</a>
</x-slot>
@endif @endif
</div> </x-qms.page-hero>
@include('partials.flash') @include('partials.flash')
<div class="overflow-hidden rounded-2xl border bg-white dark:border-slate-700 dark:bg-slate-900">
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
<table class="min-w-full divide-y divide-slate-200"> <table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50"><tr> <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">Member</th>
@@ -35,4 +54,5 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
</x-app-layout> </x-app-layout>
+30 -21
View File
@@ -1,34 +1,28 @@
<x-app-layout title="Analytics"> <x-app-layout title="Analytics">
<h1 class="mb-6 text-2xl font-semibold">Analytics</h1> <div class="space-y-6">
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4"> <x-qms.page-hero
@foreach ([ badge="Live ops · Trends · Performance"
'waiting' => 'Waiting now', title="Analytics"
'serving' => 'Being served', description="Real-time queue performance, wait times, and customer satisfaction across your organization."
'served_today' => 'Served today', :stats="[
'no_shows_today' => 'No-shows today', ['value' => number_format($overview['waiting'] ?? 0), 'label' => 'Waiting now'],
'avg_wait_seconds' => 'Avg wait (sec)', ['value' => number_format($overview['served_today'] ?? 0), 'label' => 'Served today'],
'appointments_today' => 'Appointments today', ['value' => $overview['feedback_avg_30d'] ?? '—', 'label' => 'Avg rating (30d)'],
'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="grid gap-6 lg:grid-cols-2">
<div class="text-xs font-semibold uppercase text-slate-500">{{ $label }}</div> <div class="rounded-2xl bg-white p-6 shadow-sm">
<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> <h2 class="text-sm font-semibold uppercase text-slate-500">14-day trend</h2>
<table class="mt-4 w-full text-sm"> <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> <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> <tbody>
@foreach ($trend as $day) @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> <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 @endforeach
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="rounded-2xl border bg-white p-6 dark:border-slate-700 dark:bg-slate-900"> <div class="rounded-2xl bg-white p-6 shadow-sm">
<h2 class="text-sm font-semibold uppercase text-slate-500">Busiest queues</h2> <h2 class="text-sm font-semibold uppercase text-slate-500">Busiest queues</h2>
<ul class="mt-4 space-y-2 text-sm"> <ul class="mt-4 space-y-2 text-sm">
@forelse ($overview['busiest_queues'] ?? [] as $q) @forelse ($overview['busiest_queues'] ?? [] as $q)
@@ -39,4 +33,19 @@
</ul> </ul>
</div> </div>
</div> </div>
<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> </x-app-layout>
@@ -1,17 +1,33 @@
@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"> <x-app-layout title="Appointments">
<div class="mb-6 flex items-center justify-between"> <div class="space-y-6">
<div> <x-qms.page-hero
<h1 class="text-2xl font-semibold text-slate-900 dark:text-white">Appointments</h1> badge="Pre-booked · Check-in · Tickets"
<p class="mt-1 text-sm text-slate-600 dark:text-slate-400">Schedule and check in pre-booked customers.</p> title="Appointments"
</div> description="Schedule and check in pre-booked customers — checked-in appointments can issue queue tickets automatically."
@if(app(\App\Services\Qms\QmsPermissions::class)->can(app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()), 'appointments.manage')) :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> <a href="{{ route('qms.appointments.create') }}" class="btn-primary">New appointment</a>
</x-slot>
@endif @endif
</div> </x-qms.page-hero>
@include('partials.flash') @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"> <div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
<thead class="bg-slate-50 dark:bg-slate-800"> <table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50">
<tr> <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">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">Scheduled</th>
@@ -20,7 +36,7 @@
<th class="px-4 py-3"></th> <th class="px-4 py-3"></th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-slate-200 dark:divide-slate-700"> <tbody class="divide-y divide-slate-200">
@forelse ($appointments as $appointment) @forelse ($appointments as $appointment)
<tr> <tr>
<td class="px-4 py-3 text-sm">{{ $appointment->customer_name }}</td> <td class="px-4 py-3 text-sm">{{ $appointment->customer_name }}</td>
@@ -43,5 +59,6 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="mt-4">{{ $appointments->links() }}</div> <div>{{ $appointments->links() }}</div>
</div>
</x-app-layout> </x-app-layout>
+16 -8
View File
@@ -1,13 +1,20 @@
<x-app-layout title="Counters"> <x-app-layout title="Counters">
<div class="flex flex-wrap items-start justify-between gap-4"> <div class="space-y-6">
<div> <x-qms.page-hero
<h1 class="text-xl font-semibold text-slate-900">Counters</h1> badge="Desks · Windows · Consoles"
<p class="mt-1 text-sm text-slate-500">Service desks and windows where staff call and serve tickets</p> title="Counters"
</div> 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> <a href="{{ route('qms.counters.create') }}" class="btn-primary">New counter</a>
</div> </x-slot>
</x-qms.page-hero>
<div class="mt-5 grid gap-4 md:grid-cols-2 xl:grid-cols-3"> <div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
@forelse ($counters as $counter) @forelse ($counters as $counter)
<article class="flex flex-col rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"> <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 class="flex items-start justify-between gap-3">
@@ -37,6 +44,7 @@
@endforelse @endforelse
</div> </div>
@if ($counters->hasPages()) @if ($counters->hasPages())
<div class="mt-4">{{ $counters->links() }}</div> <div>{{ $counters->links() }}</div>
@endif @endif
</div>
</x-app-layout> </x-app-layout>
+16 -8
View File
@@ -1,19 +1,26 @@
<x-app-layout title="Devices"> <x-app-layout title="Devices">
<div class="flex items-center justify-between gap-3"> <div class="space-y-6">
<div> <x-qms.page-hero
<h1 class="text-xl font-semibold text-slate-900">Devices</h1> badge="Kiosks · Printers · Self-service"
<p class="text-sm text-slate-500">Kiosks and ticket printers for self-service and reception</p> title="Devices"
</div> 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) @if ($canManage)
<x-slot name="actions">
<a href="{{ route('qms.devices.create') }}" class="btn-primary">Register device</a> <a href="{{ route('qms.devices.create') }}" class="btn-primary">Register device</a>
</x-slot>
@endif @endif
</div> </x-qms.page-hero>
@if (session('success')) @if (session('success'))
<div class="mt-4 rounded-lg bg-emerald-50 px-4 py-3 text-sm text-emerald-800">{{ session('success') }}</div> <div class="rounded-lg bg-emerald-50 px-4 py-3 text-sm text-emerald-800">{{ session('success') }}</div>
@endif @endif
<div class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white"> <div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
@if ($devices->isEmpty()) @if ($devices->isEmpty())
<x-empty-state <x-empty-state
title="No devices registered yet" title="No devices registered yet"
@@ -72,4 +79,5 @@
</div> </div>
@endif @endif
</div> </div>
</div>
</x-app-layout> </x-app-layout>
+15 -7
View File
@@ -1,15 +1,22 @@
<x-app-layout title="Displays"> <x-app-layout title="Displays">
<div class="flex items-center justify-between gap-3"> <div class="space-y-6">
<div> <x-qms.page-hero
<h1 class="text-xl font-semibold text-slate-900">Display screens</h1> badge="Waiting areas · Voice · Ticket calls"
<p class="text-sm text-slate-500">Waiting-area screens and voice announcements for called tickets</p> title="Display screens"
</div> 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) @if ($canManage)
<x-slot name="actions">
<a href="{{ route('qms.displays.create') }}" class="btn-primary">New display</a> <a href="{{ route('qms.displays.create') }}" class="btn-primary">New display</a>
</x-slot>
@endif @endif
</div> </x-qms.page-hero>
<div class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white"> <div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
@if ($screens->isEmpty()) @if ($screens->isEmpty())
<x-empty-state <x-empty-state
title="No display screens yet" title="No display screens yet"
@@ -56,4 +63,5 @@
</div> </div>
@endif @endif
</div> </div>
</div>
</x-app-layout> </x-app-layout>
+14 -6
View File
@@ -1,9 +1,16 @@
<x-app-layout title="Customer feedback"> <x-app-layout title="Customer feedback">
<div class="mb-6"> <div class="space-y-6">
<h1 class="text-2xl font-semibold">Customer feedback</h1> <x-qms.page-hero
<p class="mt-1 text-sm text-slate-600">Average rating: {{ $avgRating ?: '—' }} / 5</p> badge="Ratings · Comments · Service quality"
</div> title="Customer feedback"
<div class="overflow-hidden rounded-2xl border bg-white dark:border-slate-700 dark:bg-slate-900"> 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"> <table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50"><tr> <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">Ticket</th>
@@ -25,5 +32,6 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="mt-4">{{ $feedback->links() }}</div> <div>{{ $feedback->links() }}</div>
</div>
</x-app-layout> </x-app-layout>
+33 -17
View File
@@ -1,26 +1,40 @@
@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"> <x-app-layout title="Queues">
<div class="mb-6 flex items-center justify-between gap-3"> <div class="space-y-6">
<div> <x-qms.page-hero
<h1 class="text-2xl font-semibold text-slate-900 dark:text-white">Service queues</h1> badge="Service lines · Prefixes · Routing"
<p class="mt-1 text-sm text-slate-600 dark:text-slate-400">Configure queues for reception, counters, and departments.</p> title="Service queues"
</div> description="Configure queues for reception, counters, and departments — each with its own ticket prefix and routing strategy."
@if(app(\App\Services\Qms\QmsPermissions::class)->can(app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()), 'queues.manage')) :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> <a href="{{ route('qms.queues.create') }}" class="btn-primary">New queue</a>
</x-slot>
@endif @endif
</div> </x-qms.page-hero>
@if (session('success')) @if (session('success'))
<div class="mb-4 rounded-lg bg-emerald-50 px-4 py-3 text-sm text-emerald-800">{{ session('success') }}</div> <div class="rounded-lg bg-emerald-50 px-4 py-3 text-sm text-emerald-800">{{ session('success') }}</div>
@endif @endif
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900"> <div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
@if ($queues->isEmpty()) @if ($queues->isEmpty())
<div class="px-6 py-12 text-center text-sm text-slate-500"> <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. No queues yet. Create your first service queue to start issuing tickets.
</div> </div>
@else @else
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-700"> <table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50 dark:bg-slate-800"> <thead class="bg-slate-50">
<tr> <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">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">Branch</th>
@@ -28,21 +42,22 @@
<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">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 text-left text-xs font-semibold uppercase text-slate-500">Status</th>
<th class="px-4 py-3"></th> <th class="px-4 py-3"></th>
</tr>
</thead> </thead>
<tbody class="divide-y divide-slate-200 dark:divide-slate-700"> <tbody class="divide-y divide-slate-200">
@foreach ($queues as $queue) @foreach ($queues as $queue)
<tr> <tr>
<td class="px-4 py-3"> <td class="px-4 py-3">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="h-3 w-3 rounded-full" style="background-color: {{ $queue->color }}"></span> <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"> <span class="text-sm font-medium text-slate-900">
<a href="{{ route('qms.queues.show', $queue) }}" class="hover:text-indigo-600">{{ $queue->name }}</a> <a href="{{ route('qms.queues.show', $queue) }}" class="hover:text-indigo-600">{{ $queue->name }}</a>
</span> </span>
</div> </div>
</td> </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 text-slate-600">{{ $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 font-mono text-slate-600">{{ $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 text-sm text-slate-600">{{ config('qms.queue_strategies.'.$queue->strategy, $queue->strategy) }}</td>
<td class="px-4 py-3"> <td class="px-4 py-3">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@if ($queue->is_paused) @if ($queue->is_paused)
@@ -65,9 +80,10 @@
@endforeach @endforeach
</tbody> </tbody>
</table> </table>
<div class="border-t border-slate-200 px-4 py-3 dark:border-slate-700"> <div class="border-t border-slate-200 px-4 py-3">
{{ $queues->links() }} {{ $queues->links() }}
</div> </div>
@endif @endif
</div> </div>
</div>
</x-app-layout> </x-app-layout>
+14 -3
View File
@@ -1,11 +1,22 @@
<x-app-layout title="Reports"> <x-app-layout title="Reports">
<h1 class="mb-6 text-2xl font-semibold">Reports</h1> <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"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
@foreach ($reports as $type => $label) @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"> <a href="{{ route('qms.reports.show', $type) }}" class="rounded-2xl bg-white p-6 shadow-sm transition hover:shadow-md">
<h2 class="font-semibold">{{ $label }}</h2> <h2 class="font-semibold text-slate-900">{{ $label }}</h2>
<p class="mt-1 text-sm text-slate-600">View metrics and export CSV</p> <p class="mt-1 text-sm text-slate-600">View metrics and export CSV</p>
</a> </a>
@endforeach @endforeach
</div> </div>
</div>
</x-app-layout> </x-app-layout>
+27 -12
View File
@@ -1,15 +1,29 @@
<x-app-layout title="Tickets"> @php
<div class="flex flex-wrap items-start justify-between gap-4"> $canIssueTickets = app(\App\Services\Qms\QmsPermissions::class)->can(
<div> app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()),
<h1 class="text-xl font-semibold text-slate-900">Tickets</h1> 'tickets.issue'
<p class="mt-1 text-sm text-slate-500">Live and historical queue tickets across all service lines</p> );
</div> @endphp
@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>
<form method="GET" class="mt-5 flex flex-wrap items-end gap-3"> <x-app-layout title="Tickets">
<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="flex flex-wrap items-end gap-3 rounded-2xl border border-slate-200 bg-white p-4">
<div> <div>
<label class="block text-xs font-medium uppercase tracking-wide text-slate-500">Queue</label> <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"> <select name="queue" class="mt-1 rounded-lg border-slate-300 text-sm">
@@ -34,7 +48,7 @@
@endif @endif
</form> </form>
<div class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white"> <div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
@if ($tickets->isEmpty()) @if ($tickets->isEmpty())
<x-empty-state title="No tickets match your filters" description="Issue a walk-in ticket or adjust filters to see results." /> <x-empty-state title="No tickets match your filters" description="Issue a walk-in ticket or adjust filters to see results." />
@else @else
@@ -67,4 +81,5 @@
<div class="border-t border-slate-200 px-4 py-3">{{ $tickets->links() }}</div> <div class="border-t border-slate-200 px-4 py-3">{{ $tickets->links() }}</div>
@endif @endif
</div> </div>
</div>
</x-app-layout> </x-app-layout>
+25 -5
View File
@@ -1,12 +1,31 @@
@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"> <x-app-layout title="Workflows">
<div class="mb-6 flex items-center justify-between"> <div class="space-y-6">
<h1 class="text-2xl font-semibold text-slate-900 dark:text-white">Workflows</h1> <x-qms.page-hero
@if(app(\App\Services\Qms\QmsPermissions::class)->can(app(\App\Services\Qms\OrganizationResolver::class)->memberFor(auth()->user()), 'workflows.manage')) 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> <a href="{{ route('qms.workflows.create') }}" class="btn-primary">New workflow</a>
</x-slot>
@endif @endif
</div> </x-qms.page-hero>
@include('partials.flash') @include('partials.flash')
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900">
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
<table class="min-w-full divide-y divide-slate-200"> <table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50"><tr> <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">Name</th>
@@ -26,4 +45,5 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
</x-app-layout> </x-app-layout>