structure updates

This commit is contained in:
Mark Gregory
2026-08-26 12:38:12 +01:00
parent 66fd8b0e8a
commit 138a028537
14 changed files with 819 additions and 270 deletions
@@ -11,6 +11,7 @@ use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Livewire\WithPagination;
use Modules\Helpdesk\Models\AutomationRule;
@@ -1034,9 +1035,12 @@ final class SettingsManager extends Component
$this->checklistTemplateForm = ['id' => null, 'category_id' => null, 'project_id' => null, 'name' => '', 'slug' => '', 'items_text' => '', 'is_active' => true];
}
#[Layout('layouts.app')]
public function render(): View
{
return view('modules.helpdesk.livewire.settings-manager', [
return view()->file(
resource_path('themes/backend/views/modules/helpdesk/livewire/settings-manager.blade.php'),
[
'statuses' => $this->orderedStatuses(),
'priorities' => $this->orderedPriorities(),
'categories' => $this->orderedCategories(),
@@ -1088,7 +1092,8 @@ final class SettingsManager extends Component
->orderBy('name')
->paginate(10, ['*'], 'checksPage'),
'colourOptions' => Color::options(),
])->layout('layouts.app');
]
);
}
private function orderedStatuses(): mixed
@@ -1017,3 +1017,378 @@ span.flatpickr-weekday {
padding-inline: 0.25rem;
}
}
/* Professional coloured headers for Helpdesk settings modals */
.hd-settings-modal-header {
position: relative;
overflow: hidden;
margin-bottom: 1.35rem;
padding: 1.05rem 1.2rem;
border: 1px solid rgba(255, 255, 255, 0.10);
border-radius: 0.9rem;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
0 12px 30px rgba(0, 0, 0, 0.14);
}
.hd-settings-modal-header::after {
content: "";
position: absolute;
top: -3.5rem;
right: -2rem;
width: 11rem;
height: 11rem;
border-radius: 9999px;
background: rgba(255, 255, 255, 0.08);
filter: blur(1px);
pointer-events: none;
}
.hd-settings-modal-header [data-flux-heading],
.hd-settings-modal-header h1,
.hd-settings-modal-header h2,
.hd-settings-modal-header h3 {
position: relative;
z-index: 1;
color: #fff !important;
font-weight: 700;
letter-spacing: -0.015em;
}
.hd-settings-modal-header [data-flux-subheading],
.hd-settings-modal-header p {
position: relative;
z-index: 1;
color: rgba(255, 255, 255, 0.74) !important;
}
.hd-settings-modal-header--status {
background: linear-gradient(135deg, #075985 0%, #0369a1 48%, #0e7490 100%);
}
.hd-settings-modal-header--priority {
background: linear-gradient(135deg, #7f1d1d 0%, #b91c1c 48%, #c2410c 100%);
}
.hd-settings-modal-header--category {
background: linear-gradient(135deg, #3730a3 0%, #4f46e5 48%, #7c3aed 100%);
}
.hd-settings-modal-header--team {
background: linear-gradient(135deg, #1e3a8a 0%, #1d4ed8 48%, #0369a1 100%);
}
.hd-settings-modal-header--project {
background: linear-gradient(135deg, #155e75 0%, #0e7490 48%, #0f766e 100%);
}
.hd-settings-modal-header--board {
background: linear-gradient(135deg, #78350f 0%, #b45309 48%, #d97706 100%);
}
.hd-settings-modal-header--git {
background: linear-gradient(135deg, #1f2937 0%, #374151 52%, #475569 100%);
}
.hd-settings-modal-header--workflow {
background: linear-gradient(135deg, #115e59 0%, #0f766e 48%, #047857 100%);
}
.hd-settings-modal-header--canned {
background: linear-gradient(135deg, #164e63 0%, #0891b2 48%, #0284c7 100%);
}
.hd-settings-modal-header--sla {
background: linear-gradient(135deg, #7c2d12 0%, #c2410c 48%, #ea580c 100%);
}
.hd-settings-modal-header--automation {
background: linear-gradient(135deg, #581c87 0%, #7e22ce 48%, #9333ea 100%);
}
.hd-settings-modal-header--checklist {
background: linear-gradient(135deg, #365314 0%, #4d7c0f 48%, #65a30d 100%);
}
.hd-settings-modal-header--tag {
background: linear-gradient(135deg, #831843 0%, #be185d 48%, #db2777 100%);
}
.hd-settings-modal-header--product {
background: linear-gradient(135deg, #312e81 0%, #4338ca 48%, #6366f1 100%);
}
@media (max-width: 640px) {
.hd-settings-modal-header {
padding: 0.95rem 1rem;
border-radius: 0.8rem;
}
}
/* Settings modal shell alignment + typography */
.hd-settings-modal,
.hd-settings-modal * {
font-family: "Instrument Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
}
.hd-settings-modal {
text-align: left;
}
/*
* Flux supplies the modal body padding. Pull the coloured banner back over
* that padding so it visually becomes the modal's own top header rather than
* looking like a card floating inside the dialog.
*/
.hd-settings-modal .hd-settings-modal-header {
margin-top: -1.5rem;
margin-right: -1.5rem;
margin-bottom: 1.5rem;
margin-left: -1.5rem;
padding: 1.35rem 1.5rem 1.3rem;
border-top: 0;
border-right: 0;
border-left: 0;
border-radius: 1rem 1rem 0 0;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
0 10px 26px rgba(0, 0, 0, 0.16);
}
.hd-settings-modal .hd-settings-modal-header [data-flux-heading],
.hd-settings-modal .hd-settings-modal-header h1,
.hd-settings-modal .hd-settings-modal-header h2,
.hd-settings-modal .hd-settings-modal-header h3 {
margin: 0;
font-family: inherit !important;
font-size: 1.25rem !important;
font-weight: 700 !important;
line-height: 1.35 !important;
letter-spacing: -0.02em !important;
}
.hd-settings-modal .hd-settings-modal-header [data-flux-subheading],
.hd-settings-modal .hd-settings-modal-header p {
margin-top: 0.45rem !important;
margin-bottom: 0 !important;
font-family: inherit !important;
font-size: 0.95rem !important;
font-weight: 400 !important;
line-height: 1.5 !important;
letter-spacing: 0 !important;
}
.hd-settings-modal input,
.hd-settings-modal select,
.hd-settings-modal textarea,
.hd-settings-modal button {
font-family: inherit !important;
}
@media (max-width: 640px) {
.hd-settings-modal .hd-settings-modal-header {
margin-top: -1rem;
margin-right: -1rem;
margin-left: -1rem;
padding: 1.1rem 1rem;
border-radius: 0.85rem 0.85rem 0 0;
}
}
/* Kanban-style settings form rhythm */
.hd-settings-modal {
color: rgb(244 244 245);
}
.hd-settings-modal :is(label, [data-flux-label]) {
font-family: "Instrument Sans", ui-sans-serif, system-ui, sans-serif !important;
font-size: 0.95rem !important;
font-weight: 600 !important;
line-height: 1.35 !important;
letter-spacing: -0.01em !important;
text-transform: none !important;
}
.hd-settings-modal :is(input:not([type="checkbox"]):not([type="radio"]):not([type="color"]), select, textarea) {
min-height: 3rem;
font-family: "Instrument Sans", ui-sans-serif, system-ui, sans-serif !important;
font-size: 1rem !important;
font-weight: 500 !important;
line-height: 1.4 !important;
letter-spacing: -0.01em !important;
color: rgb(244 244 245) !important;
background: rgb(39 39 42) !important;
border-color: rgb(82 82 91) !important;
border-radius: 0.75rem !important;
}
.hd-settings-modal :is(input, select, textarea):focus {
border-color: rgb(59 130 246) !important;
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.16) !important;
}
.hd-settings-modal textarea {
min-height: 7.5rem;
padding-top: 0.85rem !important;
padding-bottom: 0.85rem !important;
}
.hd-settings-modal button,
.hd-settings-modal [role="button"] {
font-family: "Instrument Sans", ui-sans-serif, system-ui, sans-serif !important;
font-weight: 650 !important;
letter-spacing: -0.01em !important;
}
/* Shared horizontal colour control for Status, Priority, Category, Team, Project and Tag editors. */
.hd-colour-control {
width: 100%;
padding: 1rem 1.1rem;
border: 1px solid rgb(63 63 70);
border-radius: 0.9rem;
background: rgb(24 24 27 / 0.78);
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.025);
}
.hd-colour-control__label {
margin-bottom: 0.65rem;
font-family: "Instrument Sans", ui-sans-serif, system-ui, sans-serif;
font-size: 0.95rem;
font-weight: 650;
line-height: 1.35;
letter-spacing: -0.01em;
color: rgb(228 228 231);
text-transform: none;
}
.hd-colour-control__row {
display: grid;
grid-template-columns: minmax(18rem, 1fr) minmax(7.5rem, auto) auto 3.25rem minmax(8rem, 10rem);
align-items: center;
gap: 0.8rem;
}
.hd-colour-control__select,
.hd-colour-control__value {
height: 3rem;
min-width: 0;
border: 1px solid rgb(82 82 91);
border-radius: 0.75rem;
background: rgb(39 39 42);
color: rgb(244 244 245);
font-family: "Instrument Sans", ui-sans-serif, system-ui, sans-serif;
font-size: 1rem;
font-weight: 600;
outline: none;
transition: border-color 150ms ease, box-shadow 150ms ease;
}
.hd-colour-control__select {
width: 100%;
padding: 0 0.95rem;
}
.hd-colour-control__value {
width: 100%;
padding: 0 0.9rem;
}
.hd-colour-control__select:focus,
.hd-colour-control__value:focus {
border-color: rgb(59 130 246);
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.16);
}
.hd-colour-control__badge {
display: inline-flex;
height: 3rem;
min-width: 7.5rem;
align-items: center;
justify-content: center;
padding: 0 1rem;
border-radius: 0.75rem;
font-family: "Instrument Sans", ui-sans-serif, system-ui, sans-serif;
font-size: 0.95rem;
font-weight: 700;
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.12);
}
.hd-colour-control__swatches {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 0.45rem;
}
.hd-colour-control__swatch {
width: 1.75rem;
height: 1.75rem;
flex: 0 0 auto;
border: 2px solid rgb(228 228 231 / 0.86);
border-radius: 9999px;
box-shadow: 0 0 0 1px rgb(0 0 0 / 0.28);
transition: transform 120ms ease, box-shadow 120ms ease;
}
.hd-colour-control__swatch:hover {
transform: translateY(-1px) scale(1.06);
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.14);
}
.hd-colour-control__picker {
width: 3.25rem;
height: 3rem;
padding: 0.35rem;
cursor: pointer;
border: 1px solid rgb(82 82 91);
border-radius: 0.75rem;
background: rgb(39 39 42);
}
@media (max-width: 1180px) {
.hd-colour-control__row {
grid-template-columns: minmax(16rem, 1fr) minmax(7rem, auto) auto;
}
.hd-colour-control__picker,
.hd-colour-control__value {
grid-row: 2;
}
.hd-colour-control__value {
grid-column: 2 / 4;
max-width: 13rem;
}
}
@media (max-width: 720px) {
.hd-colour-control {
padding: 0.9rem;
}
.hd-colour-control__row {
display: flex;
flex-wrap: wrap;
}
.hd-colour-control__select {
flex: 1 1 100%;
}
.hd-colour-control__badge {
flex: 1 1 8rem;
}
.hd-colour-control__swatches {
order: 5;
width: 100%;
overflow-x: auto;
padding-top: 0.15rem;
}
.hd-colour-control__value {
flex: 1 1 8rem;
}
}
+11
View File
@@ -51,6 +51,17 @@ class User extends Authenticatable implements PasskeyUser
}
public function gravatarUrl(int $size = 160): string
{
$email = strtolower(trim((string) $this->email));
$hash = hash('sha256', $email);
return 'https://www.gravatar.com/avatar/'.$hash
.'?s='.max(1, min($size, 2048))
.'&d=404&r=g';
}
public function roles(): BelongsToMany
{
return $this->belongsToMany(
@@ -1,17 +0,0 @@
@import 'tailwindcss';
@import '@fortawesome/fontawesome-free/css/all.min.css';
@import '../../../../../vendor/livewire/flux/dist/flux.css';
@import './helpdesk.css';
@source '../../views/**/*.blade.php';
@source '../../../../../Modules/Helpdesk/**/*.php';
@source '../../../../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../../../../vendor/livewire/flux/stubs/**/*.blade.php';
@source '../../../../../vendor/livewire/flux-pro/stubs/**/*.blade.php';
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
@@ -1337,3 +1337,213 @@ span.flatpickr-weekday {
@media(max-width:1100px){.hd-metric-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.hd-dashboard-grid{grid-template-columns:1fr}.hd-priority-body{min-height:240px}.hd-top-search{display:none}}
@media(max-width:1023px){:root{--hd-sidebar-width:300px}.hd-topbar{display:flex}.hd-topbar-brand{width:auto;border-right:0}.hd-topbar-main{flex:1;padding-left:8px}.hd-mobile-menu-button{display:inline-flex}.hd-shell-sidebar{transform:translateX(-102%);width:min(84vw,320px);box-shadow:15px 0 40px rgba(0,0,0,.35)}.hd-sidebar-open .hd-shell-sidebar{transform:translateX(0)}.hd-sidebar-resizer{display:none}.hd-shell-main{margin-left:0;padding:calc(var(--hd-topbar-height) + 20px) 18px 25px}.hd-sidebar-backdrop{position:fixed;z-index:55;inset:var(--hd-topbar-height) 0 0 0;background:rgba(0,0,0,.52);border:0}.hd-sidebar-open .hd-sidebar-backdrop{display:block}}
@media(max-width:700px){.hd-account-copy{display:none}.hd-topbar-main{padding-right:10px}.hd-topbar-brand{padding-left:10px;padding-right:8px}.hd-brand-name{font-size:.95rem}.hd-shell-main{padding-left:10px;padding-right:10px}.hd-dashboard-heading{align-items:flex-start;flex-direction:column}.hd-range-select{width:100%}.hd-metric-grid{grid-template-columns:1fr}.hd-dashboard-card{border-radius:10px}.hd-priority-body{grid-template-columns:1fr}.hd-latest-list>a{grid-template-columns:65px minmax(0,1fr);gap:10px}.hd-latest-list time,.hd-latest-list .hd-status-mini{display:none}.hd-page-hero{align-items:flex-start;flex-direction:column;padding:18px}.hd-page-hero .hd-action{width:100%}.hd-toolbar{flex-direction:column}.hd-form-grid,.hd-role-options{grid-template-columns:1fr}.hd-action{min-height:44px}}
/* Dark account menu + Gravatar */
.hd-account-menu {
position: relative;
display: inline-block;
color: #f8fafc;
}
.hd-account-menu > summary {
list-style: none;
}
.hd-account-menu > summary::-webkit-details-marker {
display: none;
}
.hd-account-menu[open] .hd-account-button {
background: #202732;
border-color: #3a4554;
}
.hd-account-menu[open] .hd-account-chevron {
transform: rotate(180deg);
}
.hd-account-chevron {
font-size: .75rem;
transition: transform .16s ease;
}
.hd-user-avatar--gravatar {
position: relative;
overflow: hidden;
}
.hd-user-avatar--gravatar img {
position: absolute;
inset: 0;
z-index: 2;
width: 100%;
height: 100%;
display: block;
border-radius: inherit;
object-fit: cover;
background: #252c36;
}
.hd-user-avatar-fallback {
position: relative;
z-index: 1;
display: inline-flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
}
.hd-user-avatar--large {
width: 46px;
height: 46px;
flex-basis: 46px;
border-radius: 12px;
}
.hd-account-dropdown {
position: absolute;
top: calc(100% + 10px);
right: 0;
z-index: 120;
width: min(310px, calc(100vw - 24px));
overflow: hidden;
border: 1px solid #333d4a;
border-radius: 14px;
background:
linear-gradient(180deg, rgba(28, 34, 44, .98), rgba(18, 23, 30, .99));
color: #e5e7eb;
box-shadow:
0 24px 60px rgba(0, 0, 0, .48),
0 8px 22px rgba(0, 0, 0, .28),
inset 0 1px 0 rgba(255, 255, 255, .035);
}
.hd-account-dropdown-user {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: rgba(255, 255, 255, .018);
}
.hd-account-dropdown-copy {
min-width: 0;
display: grid;
gap: 2px;
}
.hd-account-dropdown-copy strong {
overflow: hidden;
color: #f8fafc;
font-size: .95rem;
font-weight: 700;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.hd-account-dropdown-copy span {
overflow: hidden;
color: #9ca3af;
font-size: .78rem;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.hd-account-dropdown-copy small {
margin-top: 2px;
color: #60a5fa;
font-size: .7rem;
font-weight: 700;
letter-spacing: .055em;
text-transform: uppercase;
}
.hd-account-dropdown-divider {
height: 1px;
background: #303846;
}
.hd-account-dropdown-nav {
display: grid;
gap: 4px;
padding: 8px;
}
.hd-account-dropdown-nav a,
.hd-account-logout {
width: 100%;
min-height: 42px;
display: flex;
align-items: center;
gap: 11px;
padding: 9px 11px;
border: 1px solid transparent;
border-radius: 9px;
background: transparent;
color: #d7dce4;
font: inherit;
font-size: .86rem;
font-weight: 600;
line-height: 1.25;
text-align: left;
text-decoration: none;
cursor: pointer;
transition:
background .14s ease,
border-color .14s ease,
color .14s ease;
}
.hd-account-dropdown-nav a i,
.hd-account-logout i {
width: 17px;
color: #8fa0b5;
text-align: center;
}
.hd-account-dropdown-nav a:hover,
.hd-account-dropdown-nav a:focus-visible {
border-color: #35465c;
background: #263244;
color: #fff;
outline: none;
}
.hd-account-dropdown-nav a:hover i {
color: #60a5fa;
}
.hd-account-dropdown form {
padding: 8px;
}
.hd-account-logout:hover,
.hd-account-logout:focus-visible {
border-color: rgba(239, 68, 68, .32);
background: rgba(127, 29, 29, .24);
color: #fecaca;
outline: none;
}
.hd-account-logout:hover i,
.hd-account-logout:focus-visible i {
color: #f87171;
}
.hd-account-menu--legacy .hd-account-dropdown {
left: 0;
right: auto;
}
@media (max-width: 700px) {
.hd-account-dropdown {
position: fixed;
top: calc(var(--hd-topbar-height) - 2px);
right: 10px;
width: min(310px, calc(100vw - 20px));
}
}
@@ -1,39 +1,66 @@
<flux:dropdown position="bottom" align="start">
<flux:sidebar.profile
:name="auth()->user()->name"
:initials="auth()->user()->initials()"
icon:trailing="chevrons-up-down"
data-test="sidebar-menu-button"
/>
@php
$user = auth()->user();
@endphp
<flux:menu>
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
<flux:avatar
:name="auth()->user()->name"
:initials="auth()->user()->initials()"
/>
<div class="grid flex-1 text-start text-sm leading-tight">
<flux:heading class="truncate">{{ auth()->user()->name }}</flux:heading>
<flux:text class="truncate">{{ auth()->user()->email }}</flux:text>
<details class="hd-account-menu hd-account-menu--legacy">
<summary class="hd-account-button" data-test="sidebar-menu-button">
<span class="hd-user-avatar hd-user-avatar--gravatar">
<span class="hd-user-avatar-fallback">{{ $user->initials() }}</span>
<img
src="{{ $user->gravatarUrl(160) }}"
alt="{{ $user->name }}"
loading="lazy"
referrerpolicy="no-referrer"
onerror="this.style.display='none'"
>
</span>
<span class="hd-account-copy">
<strong>{{ $user->name }}</strong>
<small>{{ $user->email }}</small>
</span>
<i class="fa-solid fa-chevron-down hd-account-chevron"></i>
</summary>
<div class="hd-account-dropdown" role="menu">
<div class="hd-account-dropdown-user">
<span class="hd-user-avatar hd-user-avatar--gravatar hd-user-avatar--large">
<span class="hd-user-avatar-fallback">{{ $user->initials() }}</span>
<img
src="{{ $user->gravatarUrl(160) }}"
alt="{{ $user->name }}"
loading="lazy"
referrerpolicy="no-referrer"
onerror="this.style.display='none'"
>
</span>
<div class="hd-account-dropdown-copy">
<strong>{{ $user->name }}</strong>
<span>{{ $user->email }}</span>
</div>
</div>
<flux:menu.separator />
<flux:menu.radio.group>
<flux:menu.item :href="route('profile.edit')" icon="cog" wire:navigate>
{{ __('Settings') }}
</flux:menu.item>
<form method="POST" action="{{ route('logout') }}" class="w-full">
@csrf
<flux:menu.item
as="button"
type="submit"
icon="arrow-right-start-on-rectangle"
class="w-full cursor-pointer"
data-test="logout-button"
>
{{ __('Log out') }}
</flux:menu.item>
</form>
</flux:menu.radio.group>
</flux:menu>
</flux:dropdown>
<div class="hd-account-dropdown-divider"></div>
<nav class="hd-account-dropdown-nav" aria-label="Account">
@if(Route::has('profile.edit'))
<a href="{{ route('profile.edit') }}" wire:navigate role="menuitem">
<i class="fa-solid fa-gear"></i>
<span>Settings</span>
</a>
@endif
</nav>
<div class="hd-account-dropdown-divider"></div>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button class="hd-account-logout" type="submit" data-test="logout-button" role="menuitem">
<i class="fa-solid fa-arrow-right-from-bracket"></i>
<span>Log out</span>
</button>
</form>
</div>
</details>
@@ -11,64 +11,57 @@
$currentLabel = \Modules\Helpdesk\Support\Color::label($currentValue);
@endphp
<div {{ $attributes->class('rounded-xl border border-zinc-200 bg-white p-3 shadow-sm dark:border-zinc-700 dark:bg-zinc-950/50') }}>
<div class="grid gap-3 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-end">
<div class="min-w-0 space-y-2">
<label class="block text-xs font-bold uppercase tracking-wide text-zinc-600 dark:text-zinc-300">
{{ $label }}
</label>
<div {{ $attributes->class('hd-colour-control') }}>
<div class="hd-colour-control__label">{{ $label }}</div>
<div class="grid gap-2 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center">
<select
wire:model.live="{{ $model }}"
class="h-9 w-full rounded-lg border border-zinc-300 bg-white px-3 text-sm font-semibold text-zinc-900 shadow-sm outline-none transition-colors duration-150 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100"
<div class="hd-colour-control__row">
<select
wire:model.live="{{ $model }}"
class="hd-colour-control__select"
aria-label="{{ $label }}"
>
@foreach ($options as $key => $option)
<option value="{{ $key }}" style="color: {{ $option['hex'] }};">
● {{ $option['name'] }} — {{ $option['hex'] }}
</option>
@endforeach
</select>
<span
class="hd-colour-control__badge"
style="background-color: {{ $currentHex }}; color: {{ $currentText }};"
>
{{ $currentLabel }}
</span>
<div class="hd-colour-control__swatches" aria-label="Available Helpdesk colours">
@foreach ($options as $key => $option)
<button
type="button"
wire:click="$set('{{ $model }}', '{{ $key }}')"
title="{{ $option['name'] }} {{ $option['hex'] }}"
class="hd-colour-control__swatch"
style="background-color: {{ $option['hex'] }}"
>
@foreach ($options as $key => $option)
<option value="{{ $key }}" style="color: {{ $option['hex'] }};">
● {{ $option['name'] }} — {{ $option['hex'] }}
</option>
@endforeach
</select>
<span
class="inline-flex h-9 min-w-28 items-center justify-center rounded-lg px-3 text-xs font-bold shadow-sm ring-1 ring-zinc-900/10 dark:ring-white/10"
style="background-color: {{ $currentHex }}; color: {{ $currentText }};"
>
{{ $currentLabel }}
</span>
</div>
<div class="flex flex-wrap items-center gap-1.5" aria-label="Available Helpdesk colours">
@foreach ($options as $key => $option)
<button
type="button"
wire:click="$set('{{ $model }}', '{{ $key }}')"
title="{{ $option['name'] }} {{ $option['hex'] }}"
class="inline-flex h-6 w-6 items-center justify-center rounded-full border border-white/80 shadow-sm ring-1 ring-zinc-900/10 transition hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-500/40 dark:ring-white/15"
style="background-color: {{ $option['hex'] }}"
>
<span class="sr-only">Use {{ $option['name'] }}</span>
</button>
@endforeach
</div>
<span class="sr-only">Use {{ $option['name'] }}</span>
</button>
@endforeach
</div>
<div class="grid grid-cols-[2.75rem_minmax(0,11rem)] items-center gap-2 lg:justify-end">
<input
type="color"
value="{{ $currentHex }}"
wire:change="$set('{{ $model }}', $event.target.value)"
class="h-9 w-11 cursor-pointer rounded-lg border border-zinc-300 bg-white p-1 shadow-sm dark:border-zinc-700 dark:bg-zinc-900"
aria-label="Choose a custom hex colour"
/>
<input
type="color"
value="{{ $currentHex }}"
wire:change="$set('{{ $model }}', $event.target.value)"
class="hd-colour-control__picker"
aria-label="Choose a custom hex colour"
/>
<input
type="text"
wire:model.live.debounce.500ms="{{ $model }}"
class="h-9 w-full rounded-lg border border-zinc-300 bg-white px-3 text-xs font-semibold text-zinc-900 placeholder:text-zinc-400 shadow-sm outline-none transition-colors duration-150 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100 dark:placeholder:text-zinc-500"
placeholder="#ffffff"
aria-label="Custom hex colour"
/>
</div>
<input
type="text"
wire:model.live.debounce.500ms="{{ $model }}"
class="hd-colour-control__value"
placeholder="#ffffff"
aria-label="Custom hex colour"
/>
</div>
</div>
@@ -1,4 +1,4 @@
<div class="flex items-start max-md:flex-col">
<div class="hd-account-settings flex items-start max-md:flex-col">
<div class="me-10 w-full pb-4 md:w-[220px]">
<flux:navlist aria-label="{{ __('Settings') }}">
<flux:navlist.item :href="route('profile.edit')" wire:navigate>{{ __('Profile') }}</flux:navlist.item>
@@ -1,3 +1,4 @@
<x-layouts::app.sidebar :title="$title ?? null">
{{ $slot }}
</x-layouts::app.sidebar>
@include('layouts.app.sidebar', [
'slot' => $slot,
'title' => $title ?? null,
])
@@ -1,86 +0,0 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>@include('partials.head')</head>
<body class="hd-app-body">
@php
$user = auth()->user();
$isAdmin = $user !== null && (
(bool) ($user->helpdesk_admin ?? false)
|| (method_exists($user, 'isHelpdeskAdmin') && $user->isHelpdeskAdmin())
);
$roleLabel = $isAdmin ? 'Administrator' : ((method_exists($user, 'isHelpdeskAgent') && $user->isHelpdeskAgent()) || (bool) ($user->helpdesk_agent ?? false) ? 'Agent' : 'User');
@endphp
<div class="hd-app-shell" data-hd-shell>
<header class="hd-topbar">
<div class="hd-topbar-brand">
<button class="hd-icon-button hd-mobile-menu-button" type="button" data-hd-sidebar-toggle aria-label="Toggle navigation"><i class="fa-solid fa-bars"></i></button>
<a href="{{ route('dashboard') }}" wire:navigate class="hd-brand-name">{{ config('app.name', 'Laravel') }}</a>
</div>
<div class="hd-topbar-main">
@if(Route::has('helpdesk.tickets.index'))
<form method="GET" action="{{ route('helpdesk.tickets.index') }}" class="hd-top-search">
<i class="fa-solid fa-magnifying-glass"></i><input name="search" placeholder="Search tickets" aria-label="Search tickets">
</form>
@endif
<div class="hd-topbar-spacer"></div>
<flux:dropdown position="bottom" align="end">
<button class="hd-account-button" type="button">
<span class="hd-user-avatar">{{ $user->initials() }}</span>
<span class="hd-account-copy"><strong>{{ $user->name }}</strong><small>{{ $roleLabel }}</small></span>
<i class="fa-solid fa-chevron-down text-xs"></i>
</button>
<flux:menu>
@if(Route::has('profile.edit'))<flux:menu.item :href="route('profile.edit')" icon="user-circle" wire:navigate>Profile</flux:menu.item>@endif
@if(Route::has('helpdesk.settings'))<flux:menu.item :href="route('helpdesk.settings')" icon="cog" wire:navigate>Helpdesk Settings</flux:menu.item>@endif
@if($isAdmin)<flux:menu.item :href="url('/admin/users')" icon="user-circle" wire:navigate>User Management</flux:menu.item>@endif
<flux:menu.separator />
<form method="POST" action="{{ route('logout') }}">@csrf<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle">Log out</flux:menu.item></form>
</flux:menu>
</flux:dropdown>
</div>
</header>
<aside class="hd-shell-sidebar" data-hd-sidebar>
<nav class="hd-shell-nav" aria-label="Primary navigation">
<div class="hd-nav-section">
<div class="hd-nav-heading">Platform</div>
<a
class="hd-nav-link {{ request()->routeIs('dashboard') ? 'is-active' : '' }}"
href="{{ route('dashboard') }}"
wire:navigate
>
<i class="fa-solid fa-house"></i>
<span>Dashboard</span>
</a>
</div>
<div class="hd-nav-section"><div class="hd-nav-heading">Helpdesk</div>
@if(Route::has('helpdesk.tickets.index'))<a class="hd-nav-link {{ request()->routeIs('helpdesk.tickets.*') ? 'is-active' : '' }}" href="{{ route('helpdesk.tickets.index') }}" wire:navigate><i class="fa-solid fa-ticket"></i><span>Tickets</span></a>@endif
@if(Route::has('helpdesk.kanban'))<a class="hd-nav-link {{ request()->routeIs('helpdesk.kanban') ? 'is-active' : '' }}" href="{{ route('helpdesk.kanban') }}" wire:navigate><i class="fa-solid fa-table-columns"></i><span>Kanban</span></a>@endif
@if(Route::has('helpdesk.settings'))<a class="hd-nav-link {{ request()->routeIs('helpdesk.settings*') ? 'is-active' : '' }}" href="{{ route('helpdesk.settings') }}" wire:navigate><i class="fa-solid fa-gear"></i><span>Settings</span></a>@endif
</div>
@if(Route::has('documentation.index') || Route::has('documentation.documents.index'))
<div class="hd-nav-section"><div class="hd-nav-heading">Documentation</div>
<a class="hd-nav-link {{ request()->routeIs('documentation.documents.*') || request()->routeIs('documentation.index') ? 'is-active' : '' }}" href="{{ Route::has('documentation.documents.index') ? route('documentation.documents.index') : route('documentation.index') }}" wire:navigate><i class="fa-regular fa-file-lines"></i><span>Documents</span></a>
@if(Route::has('documentation.categories.index'))<a class="hd-nav-link {{ request()->routeIs('documentation.categories.*') ? 'is-active' : '' }}" href="{{ route('documentation.categories.index') }}" wire:navigate><i class="fa-regular fa-folder"></i><span>Categories</span></a>@endif
</div>
@endif
@if($isAdmin && Route::has('user-management.index'))
<div class="hd-nav-section"><div class="hd-nav-heading">Users</div>
<a class="hd-nav-link {{ request()->routeIs('user-management.index') || request()->routeIs('user-management.create') || request()->routeIs('user-management.edit') ? 'is-active' : '' }}" href="{{ route('user-management.index') }}" wire:navigate><i class="fa-solid fa-users"></i><span>Users</span></a>
<a class="hd-nav-link {{ request()->routeIs('user-management.roles') ? 'is-active' : '' }}" href="{{ route('user-management.roles') }}" wire:navigate><i class="fa-solid fa-shield-halved"></i><span>Roles & Permissions</span></a>
</div>
@endif
</nav>
<div class="hd-sidebar-resizer" data-hd-sidebar-resizer title="Drag to resize sidebar"><span></span></div>
</aside>
<button type="button" class="hd-sidebar-backdrop" data-hd-sidebar-toggle aria-label="Close navigation"></button>
<main class="hd-shell-main">{{ $slot }}</main>
</div>
@persist('toast')<flux:toast.group><flux:toast /></flux:toast.group>@endpersist
@fluxScripts
</body>
</html>
@@ -3,7 +3,7 @@
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
<body class="hd-auth-surface min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
<div class="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex w-full max-w-sm flex-col gap-2">
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
@@ -11,7 +11,8 @@
columns are configured in Helpdesk Settings.</p>
</div>
<div class="flex flex-wrap gap-2">
<div class="flex w-full flex-col gap-3 lg:w-auto lg:min-w-[420px]">
<div class="flex flex-wrap justify-start gap-2 lg:justify-end">
<a href="{{ route('helpdesk.index') }}" wire:navigate class="hd-btn hd-btn-blue">
<i class="fa-solid fa-gauge-high" aria-hidden="true"></i>
<span>Dashboard</span>
@@ -31,6 +32,39 @@
<i class="fa-solid fa-list" aria-hidden="true"></i>
<span>List view</span>
</a>
</div>
@if ($boards->isNotEmpty())
<div class="hd-kanban-active-board-card">
<div class="min-w-0">
<p class="hd-kanban-active-board-label">Active board</p>
<h2 class="hd-kanban-active-board-title">{{ $board?->name ?? 'None' }}</h2>
<p class="hd-kanban-active-board-description">
{{ $board?->description ?: 'Select the workflow board to display.' }}
</p>
@if ($board?->owner)
<p class="hd-kanban-active-board-owner">
<i class="fa-solid fa-user-shield" aria-hidden="true"></i>
{{ $board->owner->name }}
</p>
@endif
</div>
<div class="hd-select-wrap hd-kanban-board-selector">
<select
class="hd-select"
aria-label="Select Kanban board"
onchange="window.location.href = '{{ route('helpdesk.kanban') }}?board_id=' + encodeURIComponent(this.value)"
>
@foreach ($boards as $availableBoard)
<option value="{{ $availableBoard->id }}" @selected((int) $board_id === (int) $availableBoard->id)>
{{ $availableBoard->name }}{{ $availableBoard->owner ? ' · ' . $availableBoard->owner->name : '' }}
</option>
@endforeach
</select>
</div>
</div>
@endif
</div>
</div>
</div>
@@ -41,34 +75,6 @@
No Kanban boards exist yet. Open Helpdesk Settings and create a board with at least one status column.
</div>
@else
<div
class="flex flex-col gap-4 rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900 md:flex-row md:items-center md:justify-between">
<div class="min-w-0">
<h2 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">Active board: {{ $board?->name ?? 'None' }}</h2>
<p class="text-xs text-zinc-500 dark:text-zinc-400">
{{ $board?->description ?: 'Use settings to configure which statuses appear as draggable columns.' }}
</p>
@if ($board?->owner)
<p class="mt-1 text-xs font-semibold text-emerald-600 dark:text-emerald-300">Owner:
{{ $board->owner->name }}</p>
@endif
</div>
<div class="hd-select-wrap shrink-0 md:w-72">
<select
class="hd-select"
aria-label="Select Kanban board"
onchange="window.location.href = '{{ route('helpdesk.kanban') }}?board_id=' + encodeURIComponent(this.value)"
>
@foreach ($boards as $availableBoard)
<option value="{{ $availableBoard->id }}" @selected((int) $board_id === (int) $availableBoard->id)>
{{ $availableBoard->name }}{{ $availableBoard->owner ? ' · ' . $availableBoard->owner->name : '' }}
</option>
@endforeach
</select>
</div>
</div>
@if ($statuses->isEmpty())
<div
class="rounded-2xl border border-amber-500/40 bg-amber-950/70 px-5 py-4 text-sm font-semibold text-amber-100">
@@ -111,12 +117,7 @@
draggable="true" data-helpdesk-kanban-ticket data-ticket-id="{{ $ticket->id }}"
wire:key="kanban-ticket-{{ $ticket->id }}-{{ $ticket->status_id }}-{{ $ticket->board_id }}-{{ optional($ticket->updated_at)->timestamp }}"
dusk="kanban-ticket-{{ $ticket->id }}">
<div class="mb-2 flex justify-end">
<span class="hd-kanban-drag-handle" draggable="true" data-kanban-drag-handle title="Drag ticket" aria-label="Drag {{ $ticket->reference }}">
<i class="fa-solid fa-grip-vertical"></i>
</span>
</div>
<div class="flex items-start justify-between gap-3">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<div
class="font-mono text-xs font-semibold text-blue-700 dark:text-blue-300">
@@ -122,7 +122,7 @@
class="hd-settings-modal w-full max-w-4xl"
@close="closeStatusModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<div class="hd-settings-modal-header hd-settings-modal-header--status">
<flux:heading size="lg">{{ filled($statusForm['id'] ?? null) ? 'Edit status' : 'Create status' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this status without leaving the settings list.</flux:subheading>
</div>
@@ -183,7 +183,7 @@
class="hd-settings-modal w-full max-w-4xl"
@close="closePriorityModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<div class="hd-settings-modal-header hd-settings-modal-header--priority">
<flux:heading size="lg">{{ filled($priorityForm['id'] ?? null) ? 'Edit priority' : 'Create priority' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this priority without leaving the settings list.</flux:subheading>
</div>
@@ -240,7 +240,7 @@
class="hd-settings-modal w-full max-w-4xl"
@close="closeCategoryModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<div class="hd-settings-modal-header hd-settings-modal-header--category">
<flux:heading size="lg">{{ filled($categoryForm['id'] ?? null) ? 'Edit category' : 'Create category' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this category without leaving the settings list.</flux:subheading>
</div>
@@ -300,7 +300,7 @@
@close="closeTeamModal"
>
<form wire:submit="saveTeam" class="space-y-5">
<div class="flex items-start justify-between gap-4 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<div class="hd-settings-modal-header hd-settings-modal-header--team flex items-start justify-between gap-4">
<div>
<flux:heading size="lg">
{{ filled($teamForm['id'] ?? null) ? 'Edit team' : 'Create team' }}
@@ -399,7 +399,7 @@
class="hd-settings-modal w-full max-w-5xl"
@close="closeProjectModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<div class="hd-settings-modal-header hd-settings-modal-header--project">
<flux:heading size="lg">{{ filled($projectForm['id'] ?? null) ? 'Edit project' : 'Create project' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this project without leaving the settings list.</flux:subheading>
</div>
@@ -467,7 +467,7 @@
class="hd-settings-modal w-full max-w-6xl"
@close="closeBoardModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<div class="hd-settings-modal-header hd-settings-modal-header--board">
<flux:heading size="lg">{{ filled($boardForm['id'] ?? null) ? 'Edit board' : 'Create board' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this board without leaving the settings list.</flux:subheading>
</div>
@@ -599,7 +599,7 @@
class="hd-settings-modal w-full max-w-5xl"
@close="closeRepositoryModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<div class="hd-settings-modal-header hd-settings-modal-header--git">
<flux:heading size="lg">{{ filled($repositoryForm['id'] ?? null) ? 'Edit repository' : 'Create repository' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this repository without leaving the settings list.</flux:subheading>
</div>
@@ -657,7 +657,7 @@
class="hd-settings-modal w-full max-w-6xl"
@close="closeWorkflowTemplateModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<div class="hd-settings-modal-header hd-settings-modal-header--workflow">
<flux:heading size="lg">{{ filled($workflowForm['id'] ?? null) ? 'Edit workflow message' : 'Create workflow message' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this workflow message without leaving the settings list.</flux:subheading>
</div>
@@ -777,7 +777,7 @@
class="hd-settings-modal w-full max-w-6xl"
@close="closeCannedResponseModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<div class="hd-settings-modal-header hd-settings-modal-header--canned">
<flux:heading size="lg">{{ filled($cannedForm['id'] ?? null) ? 'Edit canned response' : 'Create canned response' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this canned response without leaving the settings list.</flux:subheading>
</div>
@@ -875,7 +875,7 @@
class="hd-settings-modal w-full max-w-5xl"
@close="closeSlaPolicyModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<div class="hd-settings-modal-header hd-settings-modal-header--sla">
<flux:heading size="lg">{{ filled($slaForm['id'] ?? null) ? 'Edit SLA policy' : 'Create SLA policy' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this SLA policy without leaving the settings list.</flux:subheading>
</div>
@@ -984,7 +984,7 @@
class="hd-settings-modal w-full max-w-6xl"
@close="closeAutomationRuleModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<div class="hd-settings-modal-header hd-settings-modal-header--automation">
<flux:heading size="lg">{{ filled($automationForm['id'] ?? null) ? 'Edit automation rule' : 'Create automation rule' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this automation rule without leaving the settings list.</flux:subheading>
</div>
@@ -1087,7 +1087,7 @@
class="hd-settings-modal w-full max-w-5xl"
@close="closeChecklistTemplateModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<div class="hd-settings-modal-header hd-settings-modal-header--checklist">
<flux:heading size="lg">{{ filled($checklistTemplateForm['id'] ?? null) ? 'Edit checklist template' : 'Create checklist' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this checklist template without leaving the settings list.</flux:subheading>
</div>
@@ -1170,7 +1170,7 @@
class="hd-settings-modal w-full max-w-4xl"
@close="closeTagModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<div class="hd-settings-modal-header hd-settings-modal-header--tag">
<flux:heading size="lg">{{ filled($tagForm['id'] ?? null) ? 'Edit tag' : 'Create tag' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this tag without leaving the settings list.</flux:subheading>
</div>
@@ -1238,7 +1238,7 @@
class="hd-settings-modal w-full max-w-5xl"
@close="closeProductModal"
>
<div class="mb-5 border-b border-zinc-200 pb-4 dark:border-zinc-700">
<div class="hd-settings-modal-header hd-settings-modal-header--product">
<flux:heading size="lg">{{ filled($productForm['id'] ?? null) ? 'Edit product/service' : 'Create product' }}</flux:heading>
<flux:subheading class="mt-1">Create or edit this product/service without leaving the settings list.</flux:subheading>
</div>
@@ -25,28 +25,42 @@
<div class="rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="grid gap-4 md:grid-cols-4">
<flux:input wire:model.live.debounce.350ms="search" icon="magnifying-glass"
placeholder="Search reference, subject or body" />
<div class="hd-ticket-search-wrap">
<i class="fa-solid fa-magnifying-glass hd-ticket-search-icon" aria-hidden="true"></i>
<input
type="search"
class="hd-ticket-search"
wire:model.live.debounce.350ms="search"
placeholder="Search reference, subject or body"
aria-label="Search tickets"
>
</div>
<flux:select wire:model.live="scope">
<option value="open">Open tickets</option>
<option value="closed">Closed tickets</option>
<option value="all">All tickets</option>
</flux:select>
<div class="hd-select-wrap">
<select class="hd-select" wire:model.live="scope" aria-label="Ticket scope">
<option value="open">Open tickets</option>
<option value="closed">Closed tickets</option>
<option value="all">All tickets</option>
</select>
</div>
<flux:select wire:model.live="status">
<option value="">All statuses</option>
@foreach ($statuses as $status)
<option value="{{ $status->id }}">{{ $status->name }}</option>
@endforeach
</flux:select>
<div class="hd-select-wrap">
<select class="hd-select" wire:model.live="status" aria-label="Ticket status">
<option value="">All statuses</option>
@foreach ($statuses as $status)
<option value="{{ $status->id }}">{{ $status->name }}</option>
@endforeach
</select>
</div>
<flux:select wire:model.live="priority">
<option value="">All priorities</option>
@foreach ($priorities as $priority)
<option value="{{ $priority->id }}">{{ $priority->name }}</option>
@endforeach
</flux:select>
<div class="hd-select-wrap">
<select class="hd-select" wire:model.live="priority" aria-label="Ticket priority">
<option value="">All priorities</option>
@foreach ($priorities as $priority)
<option value="{{ $priority->id }}">{{ $priority->name }}</option>
@endforeach
</select>
</div>
</div>
</div>
@@ -150,8 +164,23 @@
</table>
</div>
<div class="border-t border-zinc-200 p-4 dark:border-zinc-700">
{{ $tickets->links() }}
<div class="flex flex-col gap-3 border-t border-zinc-200 bg-zinc-50/70 px-4 py-4 dark:border-zinc-700 dark:bg-zinc-950/35 sm:flex-row sm:items-center sm:justify-between">
<div class="text-sm font-medium text-zinc-500 dark:text-zinc-400">
Showing
<span class="font-semibold text-zinc-800 dark:text-zinc-200">{{ $tickets->firstItem() ?? 0 }}</span>
–
<span class="font-semibold text-zinc-800 dark:text-zinc-200">{{ $tickets->lastItem() ?? 0 }}</span>
of
<span class="font-semibold text-zinc-800 dark:text-zinc-200">{{ $tickets->total() }}</span> tickets
</div>
<div class="min-w-0">
@if ($tickets->hasPages())
{{ $tickets->onEachSide(1)->links() }}
@else
<span class="inline-flex rounded-lg border border-zinc-200 bg-white px-3 py-1.5 text-xs font-semibold text-zinc-500 dark:border-zinc-700 dark:bg-zinc-900">Page 1 of 1</span>
@endif
</div>
</div>
</div>