new helpdesk module

This commit is contained in:
Mark Gregory
2026-06-12 10:56:36 +01:00
parent 67faed6165
commit 98c6ee4597
49 changed files with 2554 additions and 255 deletions
@@ -0,0 +1,63 @@
@props([
'label' => 'Details',
'rows' => 10,
'placeholder' => 'Write your message...',
])
@php($model = $attributes->wire('model')->value())
<div
{{ $attributes->except(['wire:model'])->merge(['class' => 'space-y-2']) }}
x-data="{
value: @entangle($model),
command(action, value = null) {
document.execCommand(action, false, value);
this.sync();
},
sync() {
this.value = this.$refs.editor.innerHTML;
},
init() {
this.$refs.editor.innerHTML = this.value || '';
this.$watch('value', (nextValue) => {
if (document.activeElement !== this.$refs.editor && this.$refs.editor.innerHTML !== (nextValue || '')) {
this.$refs.editor.innerHTML = nextValue || '';
}
});
}
}"
>
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-200">{{ $label }}</label>
<input type="hidden" x-model="value" />
<div class="overflow-hidden rounded-xl border border-zinc-300 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-950">
<div class="flex flex-wrap gap-1 border-b border-zinc-200 bg-zinc-50 p-2 dark:border-zinc-800 dark:bg-zinc-900">
<button type="button" x-on:click="command('bold')" class="rounded-lg px-2 py-1 text-sm font-bold hover:bg-zinc-200 dark:hover:bg-zinc-800">B</button>
<button type="button" x-on:click="command('italic')" class="rounded-lg px-2 py-1 text-sm italic hover:bg-zinc-200 dark:hover:bg-zinc-800">I</button>
<button type="button" x-on:click="command('underline')" class="rounded-lg px-2 py-1 text-sm underline hover:bg-zinc-200 dark:hover:bg-zinc-800">U</button>
<button type="button" x-on:click="command('insertUnorderedList')" class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">• List</button>
<button type="button" x-on:click="command('insertOrderedList')" class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">1. List</button>
<button type="button" x-on:click="command('formatBlock', 'blockquote')" class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">Quote</button>
<button type="button" x-on:click="const url = prompt('URL'); if (url) command('createLink', url)" class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">Link</button>
<button type="button" x-on:click="command('removeFormat')" class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">Clear</button>
</div>
<div wire:ignore>
<div
x-ref="editor"
contenteditable="true"
x-on:input.debounce.500ms="sync"
x-on:blur="sync"
x-on:keydown.enter.stop
class="prose prose-sm min-h-[{{ (int) $rows * 28 }}px] max-w-none bg-white p-4 text-sm text-zinc-900 outline-none dark:prose-invert dark:bg-zinc-950 dark:text-zinc-100"
data-placeholder="{{ $placeholder }}"
></div>
</div>
</div>
@error($model)
<div class="text-sm text-red-600">{{ $message }}</div>
@enderror
</div>
@@ -0,0 +1,33 @@
@props(['items'])
<div class="overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900">
<table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
<thead class="bg-zinc-50 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300">
<tr>
<th class="px-4 py-3">Repository</th>
<th class="px-4 py-3">Project</th>
<th class="px-4 py-3">Provider</th>
<th class="px-4 py-3">Branch</th>
<th class="px-4 py-3">Commits</th>
<th class="px-4 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
@forelse ($items as $item)
<tr>
<td class="px-4 py-3 font-medium text-zinc-900 dark:text-zinc-100">{{ $item->name }}</td>
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">{{ $item->project?->title ?? 'Global' }}</td>
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">{{ $item->provider }}</td>
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">{{ $item->default_branch }}</td>
<td class="px-4 py-3"><flux:badge color="zinc">{{ $item->commits_count }}</flux:badge></td>
<td class="px-4 py-3 text-right">
<flux:button size="sm" variant="ghost" wire:click="editRepository({{ $item->id }})">Edit</flux:button>
<flux:button size="sm" variant="danger" wire:click="deleteRepository({{ $item->id }})" wire:confirm="Delete this repository?">Delete</flux:button>
</td>
</tr>
@empty
<tr><td colspan="6" class="px-4 py-8 text-center text-zinc-500">No repositories yet.</td></tr>
@endforelse
</tbody>
</table>
</div>
@@ -1,22 +1,46 @@
<x-layouts.app :title="__('Helpdesk')">
<x-app-layout :title="__('Helpdesk')">
<div class="space-y-6">
<div class="overflow-hidden rounded-3xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="bg-gradient-to-br from-slate-950 via-slate-900 to-blue-950 px-6 py-7 text-white">
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div class="bg-gradient-to-br from-slate-950 via-slate-900 to-blue-950 px-6 py-8 text-white">
<div class="grid gap-6 lg:grid-cols-[1fr_320px] lg:items-center">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-blue-200">Support operations</p>
<h1 class="mt-2 text-3xl font-semibold tracking-tight">Helpdesk</h1>
<p class="mt-2 max-w-3xl text-sm text-blue-100/90">Manage tickets, priorities, statuses, categories, company teams and company projects from one professional workspace.</p>
<h1 class="mt-2 text-3xl font-semibold tracking-tight md:text-4xl">Helpdesk dashboard</h1>
<p class="mt-3 max-w-3xl text-sm leading-6 text-blue-100/90">Track open workload, closed tickets, response performance, project activity, priorities, Git commits and workflow communication from one professional support workspace.</p>
<div class="mt-5 flex flex-wrap gap-2">
<flux:button :href="route('helpdesk.tickets.create')" wire:navigate variant="primary" icon="plus">New ticket</flux:button>
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate variant="ghost" icon="ticket">Active tickets</flux:button>
<flux:button :href="route('helpdesk.kanban')" wire:navigate variant="ghost" icon="squares-2x2">Kanban</flux:button>
<flux:button :href="route('helpdesk.settings')" wire:navigate variant="ghost" icon="cog-6-tooth">Settings</flux:button>
</div>
</div>
<div class="flex flex-wrap gap-2">
<flux:button :href="route('helpdesk.tickets.create')" wire:navigate variant="primary" icon="plus">New ticket</flux:button>
<flux:button :href="route('helpdesk.settings')" wire:navigate variant="ghost" icon="cog-6-tooth">Settings</flux:button>
<div class="rounded-2xl border border-white/10 bg-white/10 p-5 shadow-2xl backdrop-blur">
<div class="grid grid-cols-3 gap-2">
@foreach (range(1, 9) as $cell)
<div class="h-14 rounded-xl bg-white/15"></div>
@endforeach
</div>
<div class="mt-4 rounded-xl bg-white/10 p-4">
<div class="h-2 rounded-full bg-blue-300/80"></div>
<div class="mt-3 h-2 w-3/4 rounded-full bg-emerald-300/80"></div>
<div class="mt-3 h-2 w-1/2 rounded-full bg-rose-300/80"></div>
</div>
</div>
</div>
</div>
</div>
<livewire:helpdesk.ticket-index />
<div class="rounded-2xl border border-zinc-200 bg-white p-3 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="flex flex-wrap gap-2">
<flux:button :href="route('helpdesk.index')" wire:navigate variant="primary">Dashboard</flux:button>
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate variant="ghost">Tickets</flux:button>
<flux:button :href="route('helpdesk.tickets.create')" wire:navigate variant="ghost">Create</flux:button>
<flux:button :href="route('helpdesk.kanban')" wire:navigate variant="ghost">Kanban</flux:button>
<flux:button :href="route('helpdesk.settings')" wire:navigate variant="ghost">Settings</flux:button>
</div>
</div>
<livewire:helpdesk.dashboard-widget />
</div>
</x-layouts.app>
</x-app-layout>
@@ -1,38 +1,86 @@
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900" dusk="helpdesk-dashboard-widget">
<div class="mb-5 flex items-center justify-between">
<div>
<h2 class="text-lg font-semibold text-zinc-900 dark:text-white">Helpdesk</h2>
<p class="text-sm text-zinc-500 dark:text-zinc-400">Your current support workload.</p>
<div class="space-y-6" dusk="helpdesk-dashboard-widget">
<div class="grid gap-4 md:grid-cols-4">
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="text-sm font-medium text-zinc-500">Total tickets</div>
<div class="mt-3 text-4xl font-semibold text-zinc-950 dark:text-white">{{ $totalCount }}</div>
<div class="mt-3 h-2 rounded-full bg-zinc-100 dark:bg-zinc-800"><div class="h-2 rounded-full bg-blue-500" style="width: 100%"></div></div>
</div>
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate size="sm" variant="ghost">View all</flux:button>
</div>
<div class="grid gap-3 sm:grid-cols-3">
<div class="rounded-xl bg-blue-50 p-4 dark:bg-blue-950/40">
<div class="text-2xl font-semibold text-blue-700 dark:text-blue-200">{{ $openCount }}</div>
<div class="text-xs font-medium uppercase tracking-wide text-blue-700/70 dark:text-blue-200/70">Open</div>
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="text-sm font-medium text-zinc-500">Open tickets</div>
<div class="mt-3 text-4xl font-semibold text-blue-700 dark:text-blue-300">{{ $openCount }}</div>
<div class="mt-3 h-2 rounded-full bg-zinc-100 dark:bg-zinc-800"><div class="h-2 rounded-full bg-blue-500" style="width: {{ $openPercent }}%"></div></div>
</div>
<div class="rounded-xl bg-red-50 p-4 dark:bg-red-950/40">
<div class="text-2xl font-semibold text-red-700 dark:text-red-200">{{ $criticalCount }}</div>
<div class="text-xs font-medium uppercase tracking-wide text-red-700/70 dark:text-red-200/70">Critical</div>
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="text-sm font-medium text-zinc-500">Closed tickets</div>
<div class="mt-3 text-4xl font-semibold text-emerald-700 dark:text-emerald-300">{{ $closedCount }}</div>
<div class="mt-3 h-2 rounded-full bg-zinc-100 dark:bg-zinc-800"><div class="h-2 rounded-full bg-emerald-500" style="width: {{ $closedPercent }}%"></div></div>
</div>
<div class="rounded-xl bg-green-50 p-4 dark:bg-green-950/40">
<div class="text-2xl font-semibold text-green-700 dark:text-green-200">{{ $closedCount }}</div>
<div class="text-xs font-medium uppercase tracking-wide text-green-700/70 dark:text-green-200/70">Closed</div>
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="text-sm font-medium text-zinc-500">High priority</div>
<div class="mt-3 text-4xl font-semibold text-red-700 dark:text-red-300">{{ $criticalCount }}</div>
<div class="mt-3 text-xs text-zinc-500">Open urgent/high/critical workload.</div>
</div>
</div>
<div class="mt-5 space-y-2">
@forelse ($recentTickets as $ticket)
<a href="{{ route('helpdesk.tickets.show', $ticket) }}" wire:navigate class="block rounded-xl border border-zinc-100 p-3 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800">
<div class="flex items-center justify-between gap-3">
<span class="truncate text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ $ticket->subject }}</span>
<flux:badge color="{{ $ticket->priority->color }}" size="sm">{{ $ticket->priority->name }}</flux:badge>
<div class="grid gap-6 lg:grid-cols-3">
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900 lg:col-span-2">
<div class="mb-5 flex items-center justify-between">
<div>
<h2 class="text-lg font-semibold text-zinc-900 dark:text-white">Performance</h2>
<p class="text-sm text-zinc-500 dark:text-zinc-400">Closure rate and remaining open workload.</p>
</div>
<div class="mt-1 text-xs text-zinc-500">{{ $ticket->reference }} · {{ $ticket->status->name }}</div>
</a>
@empty
<div class="rounded-xl border border-dashed border-zinc-300 p-4 text-center text-sm text-zinc-500 dark:border-zinc-700">No helpdesk tickets yet.</div>
@endforelse
<flux:badge color="emerald">{{ $closedPercent }}% closed</flux:badge>
</div>
<div class="space-y-5">
<div>
<div class="mb-2 flex justify-between text-sm"><span>Closed</span><span>{{ $closedPercent }}%</span></div>
<div class="h-4 rounded-full bg-zinc-100 dark:bg-zinc-800"><div class="h-4 rounded-full bg-emerald-500" style="width: {{ $closedPercent }}%"></div></div>
</div>
<div>
<div class="mb-2 flex justify-between text-sm"><span>Open</span><span>{{ $openPercent }}%</span></div>
<div class="h-4 rounded-full bg-zinc-100 dark:bg-zinc-800"><div class="h-4 rounded-full bg-blue-500" style="width: {{ $openPercent }}%"></div></div>
</div>
</div>
</div>
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="mb-5 flex items-center justify-between">
<h2 class="text-lg font-semibold text-zinc-900 dark:text-white">Recent commits</h2>
<flux:button :href="route('helpdesk.settings')" wire:navigate size="sm" variant="ghost">Git setup</flux:button>
</div>
<div class="space-y-2">
@forelse ($recentCommits as $commit)
<div class="rounded-xl border border-zinc-100 p-3 dark:border-zinc-800">
<div class="font-mono text-xs text-zinc-500">{{ $commit->short_hash }} · {{ $commit->repository?->name }}</div>
<div class="mt-1 truncate text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ $commit->message }}</div>
</div>
@empty
<div class="rounded-xl border border-dashed border-zinc-300 p-4 text-center text-sm text-zinc-500 dark:border-zinc-700">No commits tracked yet.</div>
@endforelse
</div>
</div>
</div>
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="mb-5 flex items-center justify-between">
<div>
<h2 class="text-lg font-semibold text-zinc-900 dark:text-white">Recent tickets</h2>
<p class="text-sm text-zinc-500 dark:text-zinc-400">Latest support activity.</p>
</div>
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate size="sm" variant="ghost">View all</flux:button>
</div>
<div class="space-y-2">
@forelse ($recentTickets as $ticket)
<a href="{{ route('helpdesk.tickets.show', $ticket) }}" wire:navigate class="block rounded-xl border border-zinc-100 p-3 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800">
<div class="flex items-center justify-between gap-3">
<span class="truncate text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ $ticket->subject }}</span>
<flux:badge color="{{ $ticket->priority->color }}" size="sm">{{ $ticket->priority->name }}</flux:badge>
</div>
<div class="mt-1 text-xs text-zinc-500">{{ $ticket->reference }} · {{ $ticket->status->name }}</div>
</a>
@empty
<div class="rounded-xl border border-dashed border-zinc-300 p-4 text-center text-sm text-zinc-500 dark:border-zinc-700">No helpdesk tickets yet.</div>
@endforelse
</div>
</div>
</div>
@@ -0,0 +1,86 @@
<div class="mx-auto max-w-7xl space-y-6" dusk="helpdesk-kanban-board">
<div class="overflow-hidden rounded-3xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="bg-gradient-to-br from-slate-950 via-slate-900 to-blue-950 px-6 py-7 text-white">
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-blue-200">Helpdesk workflow</p>
<h1 class="mt-2 text-3xl font-semibold tracking-tight">Kanban board</h1>
<p class="mt-2 max-w-3xl text-sm text-blue-100/90">Move tickets through your configured statuses, keep priorities visible and trigger workflow notifications when work changes state.</p>
</div>
<div class="flex flex-wrap gap-2">
<flux:button :href="route('helpdesk.tickets.create')" wire:navigate variant="primary" icon="plus">New ticket</flux:button>
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate variant="ghost" icon="list-bullet">List view</flux:button>
</div>
</div>
</div>
</div>
@if (session('success'))
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800 dark:border-emerald-900 dark:bg-emerald-950 dark:text-emerald-200">
{{ session('success') }}
</div>
@endif
<div class="grid gap-4 overflow-x-auto lg:grid-cols-{{ max(1, min(5, $statuses->count())) }}">
@foreach ($statuses as $status)
@php($tickets = $ticketsByStatus[$status->id] ?? collect())
<section class="min-w-[280px] rounded-2xl border border-zinc-200 bg-zinc-50 shadow-sm dark:border-zinc-700 dark:bg-zinc-950" dusk="kanban-column-{{ $status->slug }}">
<header class="flex items-center justify-between border-b border-zinc-200 bg-white px-4 py-3 dark:border-zinc-800 dark:bg-zinc-900">
<div>
<h2 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">{{ $status->name }}</h2>
<p class="text-xs text-zinc-500">{{ $tickets->count() }} tickets</p>
</div>
<span class="inline-flex h-3 w-3 rounded-full" style="background-color: {{ $status->color }}"></span>
</header>
<div class="space-y-3 p-3">
@forelse ($tickets as $ticket)
<article class="rounded-xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900" dusk="kanban-ticket-{{ $ticket->id }}">
<div class="flex items-start justify-between gap-3">
<div>
<a href="{{ route('helpdesk.tickets.show', $ticket) }}" wire:navigate class="text-sm font-semibold text-blue-700 hover:underline dark:text-blue-300">
{{ $ticket->reference }}
</a>
<h3 class="mt-1 line-clamp-2 text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ $ticket->subject }}</h3>
</div>
@if ($ticket->priority)
<span class="rounded-full px-2 py-1 text-xs font-semibold text-white" style="background-color: {{ $ticket->priority->color }}">
{{ $ticket->priority->name }}
</span>
@endif
</div>
<div class="mt-3 space-y-1 text-xs text-zinc-500 dark:text-zinc-400">
<div>Owner: {{ $ticket->owner?->name ?? 'Unknown' }}</div>
@if ($ticket->project)
<div>Project: {{ $ticket->project->title }}</div>
@endif
@if ($ticket->team)
<div>Team: {{ $ticket->team->name }}</div>
@endif
</div>
<div class="mt-4 grid grid-cols-2 gap-2">
@foreach ($statuses as $targetStatus)
@continue($targetStatus->id === $status->id)
<button
type="button"
wire:click="moveTicket({{ $ticket->id }}, {{ $targetStatus->id }})"
class="rounded-lg border border-zinc-200 px-2 py-1 text-xs font-medium text-zinc-700 hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800"
>
{{ $targetStatus->name }}
</button>
@endforeach
</div>
</article>
@empty
<div class="rounded-xl border border-dashed border-zinc-300 bg-white p-6 text-center text-sm text-zinc-500 dark:border-zinc-700 dark:bg-zinc-900">
No tickets in {{ $status->name }}.
</div>
@endforelse
</div>
</section>
@endforeach
</div>
</div>
@@ -5,7 +5,7 @@
<div>
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-blue-200">Helpdesk administration</p>
<h1 class="mt-2 text-3xl font-semibold tracking-tight">Support configuration</h1>
<p class="mt-2 max-w-3xl text-sm text-blue-100/90">Manage the lookup data used by tickets, including statuses, priorities, categories, company teams and company projects.</p>
<p class="mt-2 max-w-3xl text-sm text-blue-100/90">Manage the lookup data used by tickets, including statuses, priorities, categories, company teams, company projects, Git repositories and workflow messages.</p>
</div>
<div class="flex flex-wrap gap-2">
@@ -20,13 +20,15 @@
<flux:callout variant="success" icon="check-circle">{{ session('success') }}</flux:callout>
@endif
<div class="grid gap-3 md:grid-cols-5">
<div class="grid gap-3 md:grid-cols-7">
@foreach ([
'statuses' => ['label' => 'Statuses', 'count' => $statuses->count()],
'priorities' => ['label' => 'Priorities', 'count' => $priorities->count()],
'categories' => ['label' => 'Categories', 'count' => $categories->count()],
'teams' => ['label' => 'Teams', 'count' => $teams->count()],
'projects' => ['label' => 'Projects', 'count' => $projects->count()],
'git' => ['label' => 'Git', 'count' => $repositories->count()],
'workflow' => ['label' => 'Workflow', 'count' => $workflowTemplates->count()],
] as $key => $item)
<button type="button" wire:click="$set('section', '{{ $key }}')" class="rounded-2xl border px-4 py-3 text-left shadow-sm transition {{ $section === $key ? 'border-blue-500 bg-blue-50 text-blue-900 dark:border-blue-400 dark:bg-blue-950 dark:text-blue-100' : 'border-zinc-200 bg-white text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800' }}">
<div class="text-xs font-semibold uppercase tracking-wide opacity-70">{{ $item['label'] }}</div>
@@ -114,4 +116,107 @@
<x-helpdesk::project-table :items="$projects" />
</x-helpdesk::settings-section>
@endif
@if ($section === 'git')
<x-helpdesk::settings-section title="Git repositories" description="Configure repositories that can be linked to projects and tickets. Commits can then be stored against tickets for traceability.">
<x-slot:form>
<form wire:submit="saveRepository" class="grid gap-4 md:grid-cols-6">
<flux:select wire:model="repositoryForm.company_id" label="Company">
<option value="">Global</option>
@foreach ($companies as $company)
<option value="{{ $company->id }}">{{ $company->name }}</option>
@endforeach
</flux:select>
<flux:select wire:model="repositoryForm.project_id" label="Project">
<option value="">No project</option>
@foreach ($projects as $project)
<option value="{{ $project->id }}">{{ $project->title }}</option>
@endforeach
</flux:select>
<flux:input wire:model="repositoryForm.name" label="Name" />
<flux:input wire:model="repositoryForm.provider" label="Provider" placeholder="git, github, gitlab" />
<flux:input wire:model="repositoryForm.default_branch" label="Default branch" />
<flux:input wire:model="repositoryForm.webhook_secret" label="Webhook secret" />
<div class="md:col-span-6">
<flux:input wire:model="repositoryForm.remote_url" label="Remote URL" placeholder="git@github.com:company/repo.git" />
</div>
<div class="md:col-span-6 flex gap-2">
<flux:button type="submit" variant="primary">Save repository</flux:button>
<flux:button type="button" wire:click="resetRepositoryForm" variant="ghost">Reset</flux:button>
</div>
</form>
</x-slot:form>
<x-helpdesk::repository-table :items="$repositories" />
</x-helpdesk::settings-section>
@endif
@if ($section === 'workflow')
<x-helpdesk::settings-section title="Workflow messages" description="Define the professional email messages sent when tickets are raised, updated, replied to and closed.">
<x-slot:form>
<form wire:submit="saveWorkflowTemplate" class="grid gap-4 md:grid-cols-6">
<flux:input wire:model="workflowForm.name" label="Name" />
<flux:select wire:model="workflowForm.event" label="Event">
<option value="ticket.created">Ticket raised</option>
<option value="ticket.updated">Ticket updated</option>
<option value="ticket.commented">Response added</option>
<option value="ticket.reply_created">Reply added</option>
<option value="ticket.closed">Ticket closed</option>
</flux:select>
<flux:select wire:model="workflowForm.to_role" label="Send to">
<option value="requester">Requester</option>
<option value="agent">Assigned agent</option>
<option value="actor">User making change</option>
</flux:select>
<flux:input wire:model="workflowForm.cc" label="CC" placeholder="Optional comma-separated emails" />
<flux:input wire:model="workflowForm.bcc" label="BCC" placeholder="Optional comma-separated emails" />
<label class="flex items-end gap-2 pb-2 text-sm text-zinc-700 dark:text-zinc-200"><input type="checkbox" wire:model="workflowForm.is_active" class="rounded border-zinc-300"> Active</label>
<div class="md:col-span-6">
<flux:input wire:model="workflowForm.subject" label="Subject" placeholder="Ticket @{{ ticket.reference }} has been raised" />
</div>
<div class="md:col-span-6">
<flux:textarea wire:model="workflowForm.html_body" label="HTML email body" rows="8" placeholder="Use tokens like @{{ ticket.reference }}, @{{ ticket.subject }}, @{{ ticket.status }}, @{{ requester.name }}, @{{ ticket.url }}" />
</div>
<div class="md:col-span-6">
<flux:textarea wire:model="workflowForm.text_body" label="Plain text body" rows="4" placeholder="Optional fallback text" />
</div>
<div class="md:col-span-6 flex gap-2">
<flux:button type="submit" variant="primary">Save workflow message</flux:button>
<flux:button type="button" wire:click="resetWorkflowTemplateForm" variant="ghost">Reset</flux:button>
</div>
</form>
</x-slot:form>
<div class="overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900">
<table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
<thead class="bg-zinc-50 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300">
<tr>
<th class="px-4 py-3">Name</th>
<th class="px-4 py-3">Event</th>
<th class="px-4 py-3">Recipient</th>
<th class="px-4 py-3">Active</th>
<th class="px-4 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
@forelse ($workflowTemplates as $template)
<tr>
<td class="px-4 py-3 font-medium text-zinc-900 dark:text-zinc-100">{{ $template->name }}</td>
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">{{ $template->event }}</td>
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">{{ $template->to_role }}</td>
<td class="px-4 py-3"><flux:badge color="{{ $template->is_active ? 'emerald' : 'zinc' }}">{{ $template->is_active ? 'Active' : 'Inactive' }}</flux:badge></td>
<td class="px-4 py-3 text-right">
<flux:button size="sm" variant="ghost" wire:click="editWorkflowTemplate({{ $template->id }})">Edit</flux:button>
<flux:button size="sm" variant="danger" wire:click="deleteWorkflowTemplate({{ $template->id }})" wire:confirm="Delete this workflow message?">Delete</flux:button>
</td>
</tr>
@empty
<tr><td colspan="5" class="px-4 py-8 text-center text-zinc-500">No workflow messages yet.</td></tr>
@endforelse
</tbody>
</table>
</div>
</x-helpdesk::settings-section>
@endif
</div>
@@ -1,70 +1,176 @@
<div class="mx-auto max-w-5xl space-y-6" dusk="helpdesk-ticket-create">
<div>
<h1 class="text-2xl font-semibold tracking-tight text-zinc-900 dark:text-white">Create ticket</h1>
<p class="text-sm text-zinc-500 dark:text-zinc-400">Raise a support request with optional attachments.</p>
</div>
<div>
@include('helpdesk::partials.page-header', [
'title' => 'Create Ticket',
'subtitle' => 'Raise a new support request with priority, project, category and full details.',
])
<form wire:submit="save" class="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="grid gap-6 md:grid-cols-3">
<div class="space-y-6 md:col-span-2">
<flux:input wire:model="subject" label="Subject" placeholder="Short summary" required />
<flux:textarea wire:model="body" label="Details" rows="10" placeholder="What happened? What did you expect? Include steps to reproduce where useful." required />
<form wire:submit.prevent="save" class="space-y-6">
<div class="rounded-2xl border border-slate-200 bg-white shadow-sm">
<div class="border-b border-slate-200 px-6 py-4">
<h2 class="text-lg font-semibold text-slate-900">Ticket details</h2>
</div>
<div class="grid gap-6 p-6 lg:grid-cols-2">
<div class="lg:col-span-2">
<label class="block text-sm font-semibold text-slate-700" for="title">
Title
</label>
<input
id="title"
type="text"
wire:model.defer="title"
class="mt-2 block w-full rounded-xl border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>
@error('title')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="space-y-6">
<div>
<label class="block text-sm font-semibold text-slate-700" for="priority_id">
Priority
</label>
<flux:select wire:model="company_id" label="Company">
<option value="">Default company</option>
@foreach ($companies as $company)
<option value="{{ $company->id }}">{{ $company->name }}</option>
@endforeach
</flux:select>
<flux:select wire:model="team_id" label="Team">
<option value="">No team</option>
@foreach ($teams as $team)
<option value="{{ $team->id }}">{{ $team->name }}</option>
@endforeach
</flux:select>
<flux:select wire:model="priority_id" label="Priority" required>
<select
id="priority_id"
wire:model.defer="priority_id"
class="mt-2 block w-full rounded-xl border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>
<option value="">Select priority</option>
@foreach ($priorities as $priority)
<option value="{{ $priority->id }}">{{ $priority->name }}</option>
@endforeach
</flux:select>
</select>
<flux:select wire:model="category_id" label="Category">
@error('priority_id')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label class="block text-sm font-semibold text-slate-700" for="category_id">
Category
</label>
<select
id="category_id"
wire:model.defer="category_id"
class="mt-2 block w-full rounded-xl border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>
<option value="">Select category</option>
@foreach ($categories as $category)
<option value="{{ $category->id }}">{{ $category->name }}</option>
@endforeach
</flux:select>
</select>
@error('category_id')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label class="block text-sm font-semibold text-slate-700" for="project_id">
Project
</label>
<select
id="project_id"
wire:model.defer="project_id"
class="mt-2 block w-full rounded-xl border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>
<option value="">Select project</option>
<flux:select wire:model="project_id" label="Project">
<option value="">No project</option>
@foreach ($projects as $project)
<option value="{{ $project->id }}">{{ $project->title }}</option>
<option value="{{ $project->id }}">{{ $project->name }}</option>
@endforeach
</flux:select>
</select>
<flux:input wire:model="due_at" type="date" label="Due date" />
@error('project_id')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label class="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-200">Attachments</label>
<input wire:model="attachments" type="file" multiple class="block w-full rounded-lg border border-zinc-300 bg-white p-2 text-sm dark:border-zinc-700 dark:bg-zinc-950" />
<div class="mt-2 text-xs text-zinc-500">Allowed: {{ implode(', ', config('helpdesk.allowed_mimes')) }}. Max {{ config('helpdesk.max_upload_kb') }}KB each.</div>
@error('attachments.*') <div class="mt-2 text-sm text-red-600">{{ $message }}</div> @enderror
<div>
<label class="block text-sm font-semibold text-slate-700" for="team_id">
Team
</label>
<select
id="team_id"
wire:model.defer="team_id"
class="mt-2 block w-full rounded-xl border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>
<option value="">Select team</option>
@foreach ($teams as $team)
<option value="{{ $team->id }}">{{ $team->name }}</option>
@endforeach
</select>
@error('team_id')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="lg:col-span-2">
<label class="block text-sm font-semibold text-slate-700" for="details_editor">
Details
</label>
<div wire:ignore class="mt-2">
<textarea id="details_editor">{{ $details }}</textarea>
</div>
@error('details')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
</div>
<div class="mt-6 flex items-center justify-end gap-3 border-t border-zinc-200 pt-6 dark:border-zinc-700">
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate variant="ghost">Cancel</flux:button>
<flux:button type="submit" variant="primary" icon="paper-airplane" wire:loading.attr="disabled">
<span wire:loading.remove>Create ticket</span>
<span wire:loading>Creating...</span>
</flux:button>
<div class="flex items-center justify-end gap-3 border-t border-slate-200 px-6 py-4">
<a
href="{{ route('helpdesk.tickets.index') }}"
class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50"
>
Cancel
</a>
<button
type="submit"
class="rounded-xl bg-blue-600 px-5 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700"
>
Create Ticket
</button>
</div>
</form>
</div>
</form>
</div>
@push('scripts')
<script>
document.addEventListener('livewire:init', function () {
const textarea = document.getElementById('details_editor');
if (!textarea || textarea.dataset.initialised === '1') {
return;
}
textarea.dataset.initialised = '1';
const editor = new EasyMDE({
element: textarea,
spellChecker: false,
status: false,
forceSync: true
});
editor.codemirror.on('change', function () {
@this.set('details', editor.value(), false);
});
});
</script>
@endpush
@@ -1,13 +1,18 @@
<div class="space-y-6" dusk="helpdesk-ticket-index">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 class="text-2xl font-semibold tracking-tight text-zinc-900 dark:text-white">Helpdesk</h1>
<p class="text-sm text-zinc-500 dark:text-zinc-400">Search, triage and manage support tickets.</p>
<div class="overflow-hidden rounded-3xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="bg-gradient-to-br from-slate-950 via-slate-900 to-blue-950 px-6 py-7 text-white">
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-blue-200">Ticket management</p>
<h1 class="mt-2 text-3xl font-semibold tracking-tight">Active tickets</h1>
<p class="mt-2 max-w-3xl text-sm text-blue-100/90">Search, triage and manage support requests across companies, projects, teams, priorities and statuses.</p>
</div>
<div class="flex flex-wrap gap-2">
<flux:button :href="route('helpdesk.index')" wire:navigate variant="ghost" icon="chart-bar">Dashboard</flux:button>
<flux:button :href="route('helpdesk.tickets.create')" wire:navigate icon="plus" variant="primary">New ticket</flux:button>
</div>
</div>
</div>
<flux:button :href="route('helpdesk.tickets.create')" wire:navigate icon="plus" variant="primary">
New ticket
</flux:button>
</div>
@if (session('success'))
@@ -1,24 +1,26 @@
<div class="space-y-6" dusk="helpdesk-ticket-show">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<div class="mb-2 flex flex-wrap items-center gap-2">
<span class="font-mono text-xs text-zinc-500">{{ $ticket->reference }}</span>
<flux:badge color="{{ $ticket->status->color }}">{{ $ticket->status->name }}</flux:badge>
<flux:badge color="{{ $ticket->priority->color }}">{{ $ticket->priority->name }}</flux:badge>
<div class="overflow-hidden rounded-3xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="bg-gradient-to-br from-slate-950 via-slate-900 to-blue-950 px-6 py-7 text-white">
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<div class="mb-2 flex flex-wrap items-center gap-2">
<span class="font-mono text-xs text-blue-100">{{ $ticket->reference }}</span>
<flux:badge color="{{ $ticket->status->color }}">{{ $ticket->status->name }}</flux:badge>
<flux:badge color="{{ $ticket->priority->color }}">{{ $ticket->priority->name }}</flux:badge>
</div>
<h1 class="text-3xl font-semibold tracking-tight">{{ $ticket->subject }}</h1>
<p class="mt-2 text-sm text-blue-100/90">Raised by {{ $ticket->owner?->name ?? 'Unknown user' }} · Assigned to {{ $ticket->agent?->name ?? 'Unassigned' }}</p>
</div>
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate icon="arrow-left" variant="ghost">Back</flux:button>
</div>
<h1 class="text-2xl font-semibold tracking-tight text-zinc-900 dark:text-white">{{ $ticket->subject }}</h1>
<p class="text-sm text-zinc-500 dark:text-zinc-400">
Raised by {{ $ticket->owner?->name ?? 'Unknown user' }} · Assigned to {{ $ticket->agent?->name ?? 'Unassigned' }}
</p>
</div>
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate icon="arrow-left" variant="ghost">Back</flux:button>
</div>
<div class="grid gap-6 lg:grid-cols-3">
<div class="space-y-6 lg:col-span-2">
<article class="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="prose max-w-none dark:prose-invert">
{!! nl2br(e($ticket->body)) !!}
{!! $ticket->body !!}
</div>
</article>
@@ -39,37 +41,16 @@
<section class="space-y-4">
<h2 class="text-lg font-semibold text-zinc-900 dark:text-white">Conversation</h2>
@forelse ($ticket->comments as $comment)
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900 {{ $comment->is_internal ? 'ring-2 ring-amber-300' : '' }}">
<div class="mb-3 flex items-center justify-between gap-3">
<div>
<div class="font-medium text-zinc-900 dark:text-zinc-100">{{ $comment->user?->name ?? 'Unknown user' }}</div>
<div class="text-xs text-zinc-500">{{ $comment->created_at->diffForHumans() }}</div>
</div>
@if ($comment->is_internal)
<flux:badge color="amber">Internal note</flux:badge>
@endif
</div>
<div class="prose max-w-none text-sm dark:prose-invert">{!! nl2br(e($comment->body)) !!}</div>
@if ($comment->attachments->isNotEmpty())
<div class="mt-4 flex flex-wrap gap-2">
@foreach ($comment->attachments as $attachment)
<a href="{{ $attachment->url() }}" target="_blank" class="rounded-full bg-zinc-100 px-3 py-1 text-xs text-zinc-700 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-200">
{{ $attachment->original_name }}
</a>
@endforeach
</div>
@endif
</div>
@empty
<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-zinc-500 dark:border-zinc-700">No comments yet.</div>
@endforelse
@if ($ticket->comments->isNotEmpty())
@include('helpdesk::partials.comment-thread', ['comments' => $ticket->comments, 'level' => 0])
@else
<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-zinc-500 dark:border-zinc-700">No responses yet.</div>
@endif
</section>
<form wire:submit="addComment" class="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<h2 class="mb-4 text-base font-semibold text-zinc-900 dark:text-white">Add reply</h2>
<flux:textarea wire:model="commentBody" label="Comment" rows="5" required />
<x-helpdesk::html-editor dusk="ticket-comment-editor" wire:model="commentBody" label="Comment" rows="5" placeholder="Write a professional response..." />
@can('update', $ticket)
<div class="mt-4">
@@ -84,7 +65,7 @@
</div>
<div class="mt-5 flex justify-end">
<flux:button type="submit" variant="primary" icon="chat-bubble-left-right" wire:loading.attr="disabled">
<flux:button dusk="post-comment" type="submit" variant="primary" icon="chat-bubble-left-right" wire:loading.attr="disabled">
<span wire:loading.remove>Post reply</span>
<span wire:loading>Posting...</span>
</flux:button>
@@ -98,11 +79,26 @@
<dl class="space-y-3 text-sm">
<div class="flex justify-between gap-4"><dt class="text-zinc-500">Category</dt><dd>{{ $ticket->category?->name ?? 'None' }}</dd></div>
<div class="flex justify-between gap-4"><dt class="text-zinc-500">Project</dt><dd>{{ $ticket->project?->title ?? 'None' }}</dd></div>
<div class="flex justify-between gap-4"><dt class="text-zinc-500">Team</dt><dd>{{ $ticket->team?->name ?? 'None' }}</dd></div>
<div class="flex justify-between gap-4"><dt class="text-zinc-500">Created</dt><dd>{{ $ticket->created_at->format('d M Y H:i') }}</dd></div>
<div class="flex justify-between gap-4"><dt class="text-zinc-500">Due</dt><dd>{{ $ticket->due_at?->format('d M Y') ?? 'No due date' }}</dd></div>
</dl>
</div>
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<h2 class="mb-4 text-base font-semibold text-zinc-900 dark:text-white">Git commits</h2>
<div class="space-y-2">
@forelse ($ticket->gitCommits as $commit)
<a href="{{ $commit->url ?: '#' }}" target="_blank" class="block rounded-xl border border-zinc-100 p-3 dark:border-zinc-800">
<div class="font-mono text-xs text-zinc-500">{{ $commit->short_hash }} · {{ $commit->repository?->name }}</div>
<div class="mt-1 text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ $commit->message }}</div>
</a>
@empty
<div class="rounded-xl border border-dashed border-zinc-300 p-4 text-center text-sm text-zinc-500 dark:border-zinc-700">No commits linked.</div>
@endforelse
</div>
</div>
@can('update', $ticket)
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<h2 class="mb-4 text-base font-semibold text-zinc-900 dark:text-white">Agent actions</h2>
@@ -111,7 +107,15 @@
<option value="{{ $status->id }}">{{ $status->name }}</option>
@endforeach
</flux:select>
<flux:button wire:click="updateStatus" class="mt-4 w-full" variant="primary" icon="check">Update status</flux:button>
<flux:button dusk="update-status" wire:click="updateStatus" class="mt-4 w-full" variant="primary" icon="check">Update status</flux:button>
<div class="mt-5">
<flux:select wire:model="priority_id" label="Priority">
@foreach ($priorities as $priority)
<option value="{{ $priority->id }}">{{ $priority->name }}</option>
@endforeach
</flux:select>
<flux:button dusk="update-priority" wire:click="updatePriority" class="mt-4 w-full" variant="ghost" icon="flag">Update priority</flux:button>
</div>
</div>
@endcan
</aside>
@@ -0,0 +1,51 @@
@props(['comments', 'level' => 0])
<div class="space-y-3">
@foreach ($comments as $comment)
<div class="rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900" style="margin-left: {{ min((int) $level, 4) * 1.25 }}rem">
<div class="flex items-start justify-between gap-3">
<div>
<div class="font-medium text-zinc-900 dark:text-zinc-100">{{ $comment->user?->name ?? 'Unknown user' }}</div>
<div class="text-xs text-zinc-500">{{ $comment->created_at?->format('d M Y H:i') }}</div>
</div>
@if ($comment->is_internal)
<flux:badge color="amber">Internal</flux:badge>
@endif
</div>
<div class="prose prose-sm mt-3 max-w-none dark:prose-invert">
{!! $comment->body !!}
</div>
@if ($comment->attachments->isNotEmpty())
<div class="mt-3 flex flex-wrap gap-2">
@foreach ($comment->attachments as $attachment)
<a href="{{ $attachment->url() }}" target="_blank" class="rounded-lg border border-zinc-200 px-3 py-1 text-xs text-zinc-600 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800">
{{ $attachment->original_name }}
</a>
@endforeach
</div>
@endif
<div class="mt-3 flex justify-end">
<flux:button size="sm" variant="ghost" wire:click="startReply({{ $comment->id }})">Reply</flux:button>
</div>
@if (($reply_parent_id ?? null) === $comment->id)
<form wire:submit="addReply" class="mt-4 rounded-xl border border-blue-200 bg-blue-50 p-4 dark:border-blue-900 dark:bg-blue-950/40">
<x-helpdesk::html-editor wire:model="replyBody" label="Reply" rows="4" placeholder="Write a reply..." />
<div class="mt-4 flex justify-end gap-2">
<flux:button type="button" wire:click="cancelReply" variant="ghost">Cancel</flux:button>
<flux:button type="submit" variant="primary">Post reply</flux:button>
</div>
</form>
@endif
@if ($comment->replies->isNotEmpty())
<div class="mt-4">
@include('helpdesk::partials.comment-thread', ['comments' => $comment->replies, 'level' => ((int) $level) + 1])
</div>
@endif
</div>
@endforeach
</div>
@@ -0,0 +1,27 @@
<div class="mb-8 overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm">
<div class="bg-gradient-to-r from-slate-950 via-slate-900 to-blue-950 px-6 py-8 text-white">
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<p class="text-sm font-semibold uppercase tracking-[0.3em] text-blue-200">
Helpdesk
</p>
<h1 class="mt-2 text-3xl font-bold tracking-tight">
{{ $title ?? 'Helpdesk' }}
</h1>
@isset($subtitle)
<p class="mt-2 max-w-3xl text-sm text-slate-300">
{{ $subtitle }}
</p>
@endisset
</div>
@isset($actions)
<div class="flex flex-wrap items-center gap-3">
{{ $actions }}
</div>
@endisset
</div>
</div>
</div>