Add hero sections to Queue index pages and soften analytics cards.
Deploy Ladill Queue / deploy (push) Successful in 36s
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:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user