phases one of time logged now with log display

This commit is contained in:
Mark Gregory
2026-06-22 12:53:56 +01:00
parent c1a9d0de27
commit 052bae718e
231 changed files with 15113 additions and 12891 deletions
@@ -1,10 +1,4 @@
@props([
'id',
'model',
'label' => null,
'required' => false,
'placeholder' => 'Select date',
])
@props(['id', 'model', 'label' => null, 'required' => false, 'placeholder' => 'Select date'])
<div>
@if ($label)
@@ -17,17 +11,11 @@
@endif
<div class="mt-2" wire:ignore>
<input
id="{{ $id }}"
type="text"
value="{{ $attributes->get('value') }}"
data-helpdesk-flatpickr
data-wire-model="{{ $model }}"
placeholder="{{ $placeholder }}"
autocomplete="off"
<input id="{{ $id }}" type="text" value="{{ $attributes->get('value') }}" data-helpdesk-flatpickr
data-wire-model="{{ $model }}" placeholder="{{ $placeholder }}" autocomplete="off"
{{ $attributes->except('value')->merge([
'class' => 'block w-full rounded-xl border border-zinc-700 bg-zinc-950 px-4 py-3 text-sm text-zinc-100 shadow-sm outline-none transition-colors duration-150 placeholder:text-zinc-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/40',
]) }}
/>
'class' =>
'block w-full rounded-xl border border-zinc-700 bg-zinc-950 px-4 py-3 text-sm text-zinc-100 shadow-sm outline-none transition-colors duration-150 placeholder:text-zinc-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/40',
]) }} />
</div>
</div>
@@ -10,12 +10,10 @@
$initialValue = $model ? data_get($this, $model, '') : '';
@endphp
<div
{{ $attributes->whereDoesntStartWith('wire:model')->merge(['class' => 'space-y-2']) }}
@if($model) data-helpdesk-editor-model="{{ $model }}" @endif
<div {{ $attributes->whereDoesntStartWith('wire:model')->merge(['class' => 'space-y-2']) }}
@if ($model) data-helpdesk-editor-model="{{ $model }}" @endif
x-on:helpdesk-editor-reset.window="if ($event.detail.model === model) clear()"
x-on:helpdesk-editor-sync.window="if ($event.detail.model === model) sync()"
x-data="{
x-on:helpdesk-editor-sync.window="if ($event.detail.model === model) sync()" x-data="{
model: @js($model),
value: @js($initialValue),
sync() {
@@ -39,39 +37,41 @@
init() {
this.$refs.editor.innerHTML = this.value || '';
}
}"
>
}">
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-200">{{ $label }}</label>
@if($model)
@if ($model)
<input type="hidden" name="{{ $model }}" x-bind:value="value">
@endif
<div class="overflow-hidden rounded-xl border border-zinc-300 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-950" wire:ignore>
<div class="overflow-hidden rounded-xl border border-zinc-300 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-950"
wire:ignore>
<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>
<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
x-ref="editor"
contenteditable="true"
x-on:input.debounce.150ms="sync"
x-on:keyup="sync"
x-on:paste="$nextTick(() => sync())"
x-on:blur="sync"
<div x-ref="editor" contenteditable="true" x-on:input.debounce.150ms="sync" x-on:keyup="sync"
x-on:paste="$nextTick(() => sync())" x-on:blur="sync"
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>
data-placeholder="{{ $placeholder }}"></div>
</div>
@if($model)
@if ($model)
@error($model)
<div class="text-sm text-red-600">{{ $message }}</div>
@enderror
@@ -1,6 +1,7 @@
<div class="overflow-x-auto">
<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">
<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">Slug</th>
@@ -15,22 +16,28 @@
<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 font-mono text-xs text-zinc-500">{{ $item->slug }}</td>
<td class="px-4 py-3"><flux:badge color="{{ $item->color }}">{{ $item->color }}</flux:badge></td>
<td class="px-4 py-3">
<flux:badge color="{{ ($item->is_default ?? false) ? 'emerald' : 'zinc' }}">
{{ ($item->is_default ?? false) ? 'Default' : 'No' }}
<flux:badge color="{{ $item->color }}">{{ $item->color }}</flux:badge>
</td>
<td class="px-4 py-3">
<flux:badge color="{{ $item->is_default ?? false ? 'emerald' : 'zinc' }}">
{{ $item->is_default ?? false ? 'Default' : 'No' }}
</flux:badge>
</td>
<td class="px-4 py-3 text-zinc-500">{{ $item->tickets_count ?? 0 }}</td>
<td class="px-4 py-3 text-right">
<div class="flex justify-end gap-2">
<button type="button" wire:click="{{ $edit }}({{ $item->id }})" class="hd-btn hd-btn-sm hd-btn-blue">Edit</button>
<button type="button" wire:click="{{ $delete }}({{ $item->id }})" class="hd-btn hd-btn-sm hd-btn-red">Delete</button>
<button type="button" wire:click="{{ $edit }}({{ $item->id }})"
class="hd-btn hd-btn-sm hd-btn-blue">Edit</button>
<button type="button" wire:click="{{ $delete }}({{ $item->id }})"
class="hd-btn hd-btn-sm hd-btn-red">Delete</button>
</div>
</td>
</tr>
@empty
<tr><td colspan="6" class="px-4 py-10 text-center text-zinc-500">No records yet.</td></tr>
<tr>
<td colspan="6" class="px-4 py-10 text-center text-zinc-500">No records yet.</td>
</tr>
@endforelse
</tbody>
</table>
@@ -1,7 +1,15 @@
<div class="overflow-x-auto">
<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">Project</th><th class="px-4 py-3">Company</th><th class="px-4 py-3">Team</th><th class="px-4 py-3">Default</th><th class="px-4 py-3">Tickets</th><th class="px-4 py-3 text-right">Actions</th></tr>
<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">Project</th>
<th class="px-4 py-3">Company</th>
<th class="px-4 py-3">Team</th>
<th class="px-4 py-3">Default</th>
<th class="px-4 py-3">Tickets</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)
@@ -9,12 +17,23 @@
<td class="px-4 py-3 font-medium text-zinc-900 dark:text-zinc-100">{{ $item->title }}</td>
<td class="px-4 py-3 text-zinc-500">{{ $item->company?->name ?? 'Global' }}</td>
<td class="px-4 py-3 text-zinc-500">{{ $item->team?->name ?? 'No team' }}</td>
<td class="px-4 py-3"><flux:badge color="{{ ($item->is_default ?? false) ? 'emerald' : 'zinc' }}">{{ ($item->is_default ?? false) ? 'Default' : 'No' }}</flux:badge></td>
<td class="px-4 py-3">
<flux:badge color="{{ $item->is_default ?? false ? 'emerald' : 'zinc' }}">
{{ $item->is_default ?? false ? 'Default' : 'No' }}</flux:badge>
</td>
<td class="px-4 py-3 text-zinc-500">{{ $item->tickets_count ?? 0 }}</td>
<td class="px-4 py-3 text-right"><div class="flex justify-end gap-2"><button type="button" wire:click="editProject({{ $item->id }})" class="hd-btn hd-btn-sm hd-btn-blue">Edit</button><button type="button" wire:click="deleteProject({{ $item->id }})" class="hd-btn hd-btn-sm hd-btn-red">Delete</button></div></td>
<td class="px-4 py-3 text-right">
<div class="flex justify-end gap-2"><button type="button"
wire:click="editProject({{ $item->id }})"
class="hd-btn hd-btn-sm hd-btn-blue">Edit</button><button type="button"
wire:click="deleteProject({{ $item->id }})"
class="hd-btn hd-btn-sm hd-btn-red">Delete</button></div>
</td>
</tr>
@empty
<tr><td colspan="6" class="px-4 py-10 text-center text-zinc-500">No projects yet.</td></tr>
<tr>
<td colspan="6" class="px-4 py-10 text-center text-zinc-500">No projects yet.</td>
</tr>
@endforelse
</tbody>
</table>
@@ -1,7 +1,14 @@
<div class="overflow-x-auto">
<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">Provider</th><th class="px-4 py-3">Project</th><th class="px-4 py-3">Commits</th><th class="px-4 py-3 text-right">Actions</th></tr>
<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">Provider</th>
<th class="px-4 py-3">Project</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)
@@ -10,10 +17,19 @@
<td class="px-4 py-3 text-zinc-500">{{ strtoupper($item->provider) }}</td>
<td class="px-4 py-3 text-zinc-500">{{ $item->project?->title ?? 'No project' }}</td>
<td class="px-4 py-3 text-zinc-500">{{ $item->commits_count ?? 0 }}</td>
<td class="px-4 py-3 text-right"><div class="flex justify-end gap-2"><button type="button" wire:click="editRepository({{ $item->id }})" class="hd-btn hd-btn-sm hd-btn-blue">Edit</button><button type="button" wire:click="deleteRepository({{ $item->id }})" wire:confirm="Delete this repository?" class="hd-btn hd-btn-sm hd-btn-red">Delete</button></div></td>
<td class="px-4 py-3 text-right">
<div class="flex justify-end gap-2"><button type="button"
wire:click="editRepository({{ $item->id }})"
class="hd-btn hd-btn-sm hd-btn-blue">Edit</button><button type="button"
wire:click="deleteRepository({{ $item->id }})"
wire:confirm="Delete this repository?"
class="hd-btn hd-btn-sm hd-btn-red">Delete</button></div>
</td>
</tr>
@empty
<tr><td colspan="5" class="px-4 py-10 text-center text-zinc-500">No repositories yet.</td></tr>
<tr>
<td colspan="5" class="px-4 py-10 text-center text-zinc-500">No repositories yet.</td>
</tr>
@endforelse
</tbody>
</table>
@@ -1,4 +1,5 @@
<section class="overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<section
class="overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="border-b border-zinc-200 bg-zinc-50 px-5 py-4 dark:border-zinc-700 dark:bg-zinc-800/70">
<h2 class="text-lg font-semibold text-zinc-900 dark:text-white">{{ $title }}</h2>
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">{{ $description }}</p>
@@ -1,19 +1,37 @@
<div class="overflow-x-auto">
<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">Team</th><th class="px-4 py-3">Company</th><th class="px-4 py-3">Default</th><th class="px-4 py-3">Tickets</th><th class="px-4 py-3 text-right">Actions</th></tr>
<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">Team</th>
<th class="px-4 py-3">Company</th>
<th class="px-4 py-3">Default</th>
<th class="px-4 py-3">Tickets</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-500">{{ $item->company?->name ?? 'Global' }}</td>
<td class="px-4 py-3"><flux:badge color="{{ ($item->is_default ?? false) ? 'emerald' : 'zinc' }}">{{ ($item->is_default ?? false) ? 'Default' : 'No' }}</flux:badge></td>
<td class="px-4 py-3">
<flux:badge color="{{ $item->is_default ?? false ? 'emerald' : 'zinc' }}">
{{ $item->is_default ?? false ? 'Default' : 'No' }}</flux:badge>
</td>
<td class="px-4 py-3 text-zinc-500">{{ $item->tickets_count ?? 0 }}</td>
<td class="px-4 py-3 text-right"><div class="flex justify-end gap-2"><button type="button" wire:click="editTeam({{ $item->id }})" class="hd-btn hd-btn-sm hd-btn-blue">Edit</button><button type="button" wire:click="deleteTeam({{ $item->id }})" class="hd-btn hd-btn-sm hd-btn-red">Delete</button></div></td>
<td class="px-4 py-3 text-right">
<div class="flex justify-end gap-2"><button type="button"
wire:click="editTeam({{ $item->id }})"
class="hd-btn hd-btn-sm hd-btn-blue">Edit</button><button type="button"
wire:click="deleteTeam({{ $item->id }})"
class="hd-btn hd-btn-sm hd-btn-red">Delete</button></div>
</td>
</tr>
@empty
<tr><td colspan="5" class="px-4 py-10 text-center text-zinc-500">No teams yet.</td></tr>
<tr>
<td colspan="5" class="px-4 py-10 text-center text-zinc-500">No teams yet.</td>
</tr>
@endforelse
</tbody>
</table>
+213 -172
View File
@@ -1,184 +1,225 @@
<style>
.hd-helpdesk-surface .hd-btn,
.hd-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: .5rem;
border-radius: .85rem;
padding: .62rem 1.05rem;
font-size: .875rem;
line-height: 1.25rem;
font-weight: 800;
color: #ffffff !important;
text-decoration: none !important;
border: 1px solid rgba(255, 255, 255, .14);
box-shadow: 0 12px 28px rgba(0, 0, 0, .22);
transition: background-color .15s ease, transform .15s ease, box-shadow .15s ease;
}
<style>
.hd-helpdesk-surface .hd-btn,
.hd-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: .5rem;
border-radius: .85rem;
padding: .62rem 1.05rem;
font-size: .875rem;
line-height: 1.25rem;
font-weight: 800;
color: #ffffff !important;
text-decoration: none !important;
border: 1px solid rgba(255,255,255,.14);
box-shadow: 0 12px 28px rgba(0,0,0,.22);
transition: background-color .15s ease, transform .15s ease, box-shadow .15s ease;
}
.hd-helpdesk-surface .hd-btn:hover,
.hd-btn:hover {
transform: translateY(-1px);
box-shadow: 0 16px 34px rgba(0,0,0,.30);
}
.hd-helpdesk-surface .hd-btn-sm,
.hd-btn-sm {
border-radius: .65rem;
padding: .42rem .78rem;
font-size: .8125rem;
}
.hd-helpdesk-surface .hd-btn-blue,
.hd-btn-blue { background: #2563eb !important; }
.hd-helpdesk-surface .hd-btn-blue:hover,
.hd-btn-blue:hover { background: #1d4ed8 !important; }
.hd-helpdesk-surface .hd-btn-green,
.hd-btn-green { background: #16a34a !important; }
.hd-helpdesk-surface .hd-btn-green:hover,
.hd-btn-green:hover { background: #15803d !important; }
.hd-helpdesk-surface .hd-btn-amber,
.hd-btn-amber { background: #f59e0b !important; }
.hd-helpdesk-surface .hd-btn-amber:hover,
.hd-btn-amber:hover { background: #d97706 !important; }
.hd-helpdesk-surface .hd-btn-red,
.hd-btn-red { background: #dc2626 !important; }
.hd-helpdesk-surface .hd-btn-red:hover,
.hd-btn-red:hover { background: #b91c1c !important; }
.hd-helpdesk-surface .hd-btn-violet,
.hd-btn-violet { background: #7c3aed !important; }
.hd-helpdesk-surface .hd-btn-violet:hover,
.hd-btn-violet:hover { background: #6d28d9 !important; }
.hd-helpdesk-surface .hd-btn:hover,
.hd-btn:hover {
transform: translateY(-1px);
box-shadow: 0 16px 34px rgba(0, 0, 0, .30);
}
.hd-helpdesk-surface button[type="submit"],
.hd-helpdesk-surface button[data-flux-button][type="submit"] {
background: #16a34a !important;
color: #ffffff !important;
border-color: rgba(255,255,255,.14) !important;
}
.hd-helpdesk-surface button[wire\:click^="reset"],
.hd-helpdesk-surface button[wire\:target^="reset"] {
background: #f59e0b !important;
color: #ffffff !important;
border-color: rgba(255,255,255,.14) !important;
}
.hd-helpdesk-surface button[wire\:click^="edit"],
.hd-helpdesk-surface button[wire\:target^="edit"] {
background: #2563eb !important;
color: #ffffff !important;
border-color: rgba(255,255,255,.14) !important;
}
.hd-helpdesk-surface button[wire\:click^="delete"],
.hd-helpdesk-surface button[wire\:target^="delete"] {
background: #dc2626 !important;
color: #ffffff !important;
border-color: rgba(255,255,255,.14) !important;
}
.hd-helpdesk-surface .hd-btn-sm,
.hd-btn-sm {
border-radius: .65rem;
padding: .42rem .78rem;
font-size: .8125rem;
}
.hd-select {
width: 100%;
min-height: 3.35rem;
border-radius: 1rem;
border: 1px solid #3f3f46 !important;
background: #09090b !important;
color: #f4f4f5 !important;
padding: .78rem 3rem .78rem 1rem;
font-weight: 800;
outline: none;
box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 12px 26px rgba(0,0,0,.24);
}
.hd-select:focus {
border-color: #60a5fa !important;
box-shadow: 0 0 0 3px rgba(96,165,250,.38), inset 0 1px 0 rgba(255,255,255,.05);
}
.hd-helpdesk-surface .hd-btn-blue,
.hd-btn-blue {
background: #2563eb !important;
}
.helpdesk-ticket-form,
.helpdesk-ticket-form * {
-webkit-tap-highlight-color: transparent;
}
.helpdesk-ticket-form input,
.helpdesk-ticket-form textarea,
.helpdesk-ticket-form select,
.helpdesk-ticket-form .select2-container--default .select2-selection--single,
.helpdesk-ticket-form .select2-container--default .select2-selection--multiple,
.helpdesk-ticket-form .select2-container--default .select2-selection__rendered,
.helpdesk-ticket-form .select2-container--default .select2-selection__choice {
background: #09090b !important;
background-color: #09090b !important;
color: #f4f4f5 !important;
border-color: #3f3f46 !important;
caret-color: #f4f4f5 !important;
opacity: 1 !important;
}
.helpdesk-ticket-form input:disabled,
.helpdesk-ticket-form textarea:disabled,
.helpdesk-ticket-form select:disabled,
.helpdesk-ticket-form .select2-container--disabled .select2-selection--single,
.helpdesk-ticket-form .select2-container--disabled .select2-selection--multiple,
.helpdesk-ticket-form [disabled],
.helpdesk-ticket-form [aria-disabled="true"] {
background: #09090b !important;
background-color: #09090b !important;
color: #a1a1aa !important;
opacity: 1 !important;
}
.helpdesk-ticket-form input:-webkit-autofill,
.helpdesk-ticket-form input:-webkit-autofill:hover,
.helpdesk-ticket-form input:-webkit-autofill:focus,
.helpdesk-ticket-form textarea:-webkit-autofill,
.helpdesk-ticket-form textarea:-webkit-autofill:hover,
.helpdesk-ticket-form textarea:-webkit-autofill:focus,
.helpdesk-ticket-form select:-webkit-autofill,
.helpdesk-ticket-form select:-webkit-autofill:hover,
.helpdesk-ticket-form select:-webkit-autofill:focus {
-webkit-text-fill-color: #f4f4f5 !important;
box-shadow: 0 0 0 1000px #09090b inset !important;
transition: background-color 9999s ease-in-out 0s;
}
</style>
.hd-helpdesk-surface .hd-btn-blue:hover,
.hd-btn-blue:hover {
background: #1d4ed8 !important;
}
<div class="hd-helpdesk-surface 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-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 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">
<a href="{{ route('helpdesk.tickets.create') }}" wire:navigate class="hd-btn hd-btn-green">
<i class="fa-solid fa-plus" aria-hidden="true"></i>
<span>New Ticket</span>
</a>
<a href="{{ route('helpdesk.tickets.index') }}" wire:navigate class="hd-btn hd-btn-blue">
<i class="fa-solid fa-ticket" aria-hidden="true"></i>
<span>Active tickets</span>
</a>
<a href="{{ route('helpdesk.kanban') }}" wire:navigate class="hd-btn hd-btn-amber">
<i class="fa-solid fa-table-columns" aria-hidden="true"></i>
<span>Kanban</span>
</a>
<a href="{{ route('helpdesk.settings') }}" wire:navigate class="hd-btn hd-btn-violet">
<i class="fa-solid fa-gear" aria-hidden="true"></i>
<span>Settings</span>
</a>
</div>
.hd-helpdesk-surface .hd-btn-green,
.hd-btn-green {
background: #16a34a !important;
}
.hd-helpdesk-surface .hd-btn-green:hover,
.hd-btn-green:hover {
background: #15803d !important;
}
.hd-helpdesk-surface .hd-btn-amber,
.hd-btn-amber {
background: #f59e0b !important;
}
.hd-helpdesk-surface .hd-btn-amber:hover,
.hd-btn-amber:hover {
background: #d97706 !important;
}
.hd-helpdesk-surface .hd-btn-red,
.hd-btn-red {
background: #dc2626 !important;
}
.hd-helpdesk-surface .hd-btn-red:hover,
.hd-btn-red:hover {
background: #b91c1c !important;
}
.hd-helpdesk-surface .hd-btn-violet,
.hd-btn-violet {
background: #7c3aed !important;
}
.hd-helpdesk-surface .hd-btn-violet:hover,
.hd-btn-violet:hover {
background: #6d28d9 !important;
}
.hd-helpdesk-surface button[type="submit"],
.hd-helpdesk-surface button[data-flux-button][type="submit"] {
background: #16a34a !important;
color: #ffffff !important;
border-color: rgba(255, 255, 255, .14) !important;
}
.hd-helpdesk-surface button[wire\:click^="reset"],
.hd-helpdesk-surface button[wire\:target^="reset"] {
background: #f59e0b !important;
color: #ffffff !important;
border-color: rgba(255, 255, 255, .14) !important;
}
.hd-helpdesk-surface button[wire\:click^="edit"],
.hd-helpdesk-surface button[wire\:target^="edit"] {
background: #2563eb !important;
color: #ffffff !important;
border-color: rgba(255, 255, 255, .14) !important;
}
.hd-helpdesk-surface button[wire\:click^="delete"],
.hd-helpdesk-surface button[wire\:target^="delete"] {
background: #dc2626 !important;
color: #ffffff !important;
border-color: rgba(255, 255, 255, .14) !important;
}
.hd-select {
width: 100%;
min-height: 3.35rem;
border-radius: 1rem;
border: 1px solid #3f3f46 !important;
background: #09090b !important;
color: #f4f4f5 !important;
padding: .78rem 3rem .78rem 1rem;
font-weight: 800;
outline: none;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .05), 0 12px 26px rgba(0, 0, 0, .24);
}
.hd-select:focus {
border-color: #60a5fa !important;
box-shadow: 0 0 0 3px rgba(96, 165, 250, .38), inset 0 1px 0 rgba(255, 255, 255, .05);
}
.helpdesk-ticket-form,
.helpdesk-ticket-form * {
-webkit-tap-highlight-color: transparent;
}
.helpdesk-ticket-form input,
.helpdesk-ticket-form textarea,
.helpdesk-ticket-form select,
.helpdesk-ticket-form .select2-container--default .select2-selection--single,
.helpdesk-ticket-form .select2-container--default .select2-selection--multiple,
.helpdesk-ticket-form .select2-container--default .select2-selection__rendered,
.helpdesk-ticket-form .select2-container--default .select2-selection__choice {
background: #09090b !important;
background-color: #09090b !important;
color: #f4f4f5 !important;
border-color: #3f3f46 !important;
caret-color: #f4f4f5 !important;
opacity: 1 !important;
}
.helpdesk-ticket-form input:disabled,
.helpdesk-ticket-form textarea:disabled,
.helpdesk-ticket-form select:disabled,
.helpdesk-ticket-form .select2-container--disabled .select2-selection--single,
.helpdesk-ticket-form .select2-container--disabled .select2-selection--multiple,
.helpdesk-ticket-form [disabled],
.helpdesk-ticket-form [aria-disabled="true"] {
background: #09090b !important;
background-color: #09090b !important;
color: #a1a1aa !important;
opacity: 1 !important;
}
.helpdesk-ticket-form input:-webkit-autofill,
.helpdesk-ticket-form input:-webkit-autofill:hover,
.helpdesk-ticket-form input:-webkit-autofill:focus,
.helpdesk-ticket-form textarea:-webkit-autofill,
.helpdesk-ticket-form textarea:-webkit-autofill:hover,
.helpdesk-ticket-form textarea:-webkit-autofill:focus,
.helpdesk-ticket-form select:-webkit-autofill,
.helpdesk-ticket-form select:-webkit-autofill:hover,
.helpdesk-ticket-form select:-webkit-autofill:focus {
-webkit-text-fill-color: #f4f4f5 !important;
box-shadow: 0 0 0 1000px #09090b inset !important;
transition: background-color 9999s ease-in-out 0s;
}
</style>
<div class="hd-helpdesk-surface 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-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 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">
<a href="{{ route('helpdesk.tickets.create') }}" wire:navigate class="hd-btn hd-btn-green">
<i class="fa-solid fa-plus" aria-hidden="true"></i>
<span>New Ticket</span>
</a>
<a href="{{ route('helpdesk.tickets.index') }}" wire:navigate class="hd-btn hd-btn-blue">
<i class="fa-solid fa-ticket" aria-hidden="true"></i>
<span>Active tickets</span>
</a>
<a href="{{ route('helpdesk.kanban') }}" wire:navigate class="hd-btn hd-btn-amber">
<i class="fa-solid fa-table-columns" aria-hidden="true"></i>
<span>Kanban</span>
</a>
<a href="{{ route('helpdesk.settings') }}" wire:navigate class="hd-btn hd-btn-violet">
<i class="fa-solid fa-gear" aria-hidden="true"></i>
<span>Settings</span>
</a>
</div>
</div>
<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 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>
<livewire:helpdesk.dashboard-widget />
</div>
<livewire:helpdesk.dashboard-widget />
</div>
File diff suppressed because it is too large Load Diff
@@ -1,31 +1,27 @@
@php
$user = auth()->user();
$dashboardRoute = Route::has('platform.dashboard') && method_exists($user, 'isMpbaPlatformAdmin') && $user?->isMpbaPlatformAdmin()
? 'platform.dashboard'
: (Route::has('dashboard') ? 'dashboard' : null);
$dashboardRoute =
Route::has('platform.dashboard') && method_exists($user, 'isMpbaPlatformAdmin') && $user?->isMpbaPlatformAdmin()
? 'platform.dashboard'
: (Route::has('dashboard')
? 'dashboard'
: null);
@endphp
<header class="sticky top-0 z-40 border-b border-white/10 bg-[#111118]/95 backdrop-blur" role="banner">
<div class="flex h-16 items-center justify-between px-4 lg:px-6">
<div class="flex min-w-0 items-center gap-3">
<button
type="button"
<button type="button"
class="inline-flex h-10 w-10 items-center justify-center rounded-xl border border-white/10 bg-white/5 text-white lg:hidden"
data-wms-mobile-open
aria-label="{{ __('Open navigation menu') }}"
aria-controls="wms-main-sidebar"
aria-expanded="false"
>
data-wms-mobile-open aria-label="{{ __('Open navigation menu') }}" aria-controls="wms-main-sidebar"
aria-expanded="false">
<i class="fa-solid fa-bars" aria-hidden="true"></i>
</button>
<a
href="{{ $dashboardRoute ? route($dashboardRoute) : '#' }}"
wire:navigate
class="flex min-w-0 items-center gap-3"
aria-label="{{ __('Warehouse Management System') }}"
>
<span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-cyan-600 text-sm font-bold text-white shadow-lg shadow-cyan-950/40">
<a href="{{ $dashboardRoute ? route($dashboardRoute) : '#' }}" wire:navigate
class="flex min-w-0 items-center gap-3" aria-label="{{ __('Warehouse Management System') }}">
<span
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-cyan-600 text-sm font-bold text-white shadow-lg shadow-cyan-950/40">
W
</span>
@@ -40,62 +36,63 @@
</a>
</div>
<div class="relative flex shrink-0 items-center gap-3" x-data="{ open: false }" x-on:keydown.escape.window="open = false">
<button
type="button"
<div class="relative flex shrink-0 items-center gap-3" x-data="{ open: false }"
x-on:keydown.escape.window="open = false">
<button type="button"
class="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/5 px-3 py-2 text-left transition hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-[#111118]"
x-on:click="open = ! open"
x-bind:aria-expanded="open.toString()"
aria-haspopup="menu"
data-test="top-user-menu-button"
>
x-on:click="open = ! open" x-bind:aria-expanded="open.toString()" aria-haspopup="menu"
data-test="top-user-menu-button">
<span class="hidden text-right sm:block">
<span class="block text-sm font-semibold text-white">{{ $user?->name ?? __('User') }}</span>
<span class="block text-xs text-white/50">{{ $user?->email ?? '' }}</span>
</span>
<span class="flex h-10 w-10 items-center justify-center rounded-full bg-blue-700 text-xs font-bold text-white shadow-lg shadow-blue-950/40">
<span
class="flex h-10 w-10 items-center justify-center rounded-full bg-blue-700 text-xs font-bold text-white shadow-lg shadow-blue-950/40">
{{ $user?->initials() ?? 'U' }}
</span>
<i class="fa-solid fa-chevron-down hidden text-white/50 sm:block" aria-hidden="true"></i>
</button>
<div
x-cloak
x-show="open"
x-transition.origin.top.right
x-on:click.outside="open = false"
<div x-cloak x-show="open" x-transition.origin.top.right x-on:click.outside="open = false"
class="absolute right-0 top-full mt-2 w-72 overflow-hidden rounded-2xl border border-white/10 bg-[#181820] shadow-2xl shadow-black/40"
role="menu"
>
role="menu">
<div class="border-b border-white/10 px-4 py-3">
<div class="text-sm font-semibold text-white">{{ $user?->name ?? __('User') }}</div>
<div class="truncate text-xs text-white/50">{{ $user?->email ?? '' }}</div>
</div>
<div class="p-2">
@if($dashboardRoute)
<a href="{{ route($dashboardRoute) }}" wire:navigate class="block rounded-xl px-3 py-2 text-sm text-white/80 hover:bg-white/10 hover:text-white" role="menuitem">
@if ($dashboardRoute)
<a href="{{ route($dashboardRoute) }}" wire:navigate
class="block rounded-xl px-3 py-2 text-sm text-white/80 hover:bg-white/10 hover:text-white"
role="menuitem">
{{ __('Dashboard') }}
</a>
@endif
@if(Route::has('profile.edit'))
<a href="{{ route('profile.edit') }}" wire:navigate class="block rounded-xl px-3 py-2 text-sm text-white/80 hover:bg-white/10 hover:text-white" role="menuitem">
@if (Route::has('profile.edit'))
<a href="{{ route('profile.edit') }}" wire:navigate
class="block rounded-xl px-3 py-2 text-sm text-white/80 hover:bg-white/10 hover:text-white"
role="menuitem">
{{ __('Profile') }}
</a>
@endif
@if(Route::has('settings.index'))
<a href="{{ route('settings.index') }}" wire:navigate class="block rounded-xl px-3 py-2 text-sm text-white/80 hover:bg-white/10 hover:text-white" role="menuitem">
@if (Route::has('settings.index'))
<a href="{{ route('settings.index') }}" wire:navigate
class="block rounded-xl px-3 py-2 text-sm text-white/80 hover:bg-white/10 hover:text-white"
role="menuitem">
{{ __('Settings') }}
</a>
@endif
<form method="POST" action="{{ route('logout') }}" class="mt-2 border-t border-white/10 pt-2">
@csrf
<button type="submit" class="w-full rounded-xl px-3 py-2 text-left text-sm text-red-300 hover:bg-red-500/10 hover:text-red-200" role="menuitem" data-test="logout-button">
<button type="submit"
class="w-full rounded-xl px-3 py-2 text-left text-sm text-red-300 hover:bg-red-500/10 hover:text-red-200"
role="menuitem" data-test="logout-button">
{{ __('Sign out') }}
</button>
</form>
@@ -3,17 +3,23 @@
<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 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>
<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 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-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 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-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>
@@ -23,7 +29,8 @@
</div>
<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="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>
@@ -33,12 +40,18 @@
</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 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 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>
@@ -46,16 +59,21 @@
<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>
<a href="{{ route('helpdesk.settings') }}" wire:navigate class="hd-btn hd-btn-sm hd-btn-violet">Git setup</a>
<a href="{{ route('helpdesk.settings') }}" wire:navigate class="hd-btn hd-btn-sm hd-btn-violet">Git
setup</a>
</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 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>
<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>
@@ -67,19 +85,26 @@
<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>
<a href="{{ route('helpdesk.tickets.index') }}" wire:navigate class="hd-btn hd-btn-sm hd-btn-blue">View all</a>
<a href="{{ route('helpdesk.tickets.index') }}" wire:navigate class="hd-btn hd-btn-sm hd-btn-blue">View
all</a>
</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">
<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>
<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>
<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>
<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>
@@ -1,14 +1,16 @@
@vite(['resources/css/app.css'])
<div class="hd-helpdesk-surface mx-auto max-w-[1800px] space-y-6" dusk="helpdesk-kanban-board" wire:poll.visible.5s="refreshBoard" wire:key="helpdesk-kanban-root-{{ $board_id ?? 0 }}-{{ $refreshNonce }}">
<div class="overflow-hidden rounded-3xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="hd-helpdesk-surface mx-auto max-w-[1800px] space-y-6" dusk="helpdesk-kanban-board"
wire:poll.visible.5s="refreshBoard" wire:key="helpdesk-kanban-root-{{ $board_id ?? 0 }}-{{ $refreshNonce }}">
<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">Drag tickets between board columns. Boards and columns are configured in Helpdesk Settings.</p>
<p class="mt-2 max-w-3xl text-sm text-blue-100/90">Drag tickets between board columns. Boards and
columns are configured in Helpdesk Settings.</p>
</div>
<div class="flex flex-wrap gap-2">
@@ -36,162 +38,198 @@
</div>
</div>
@if ($boards->isEmpty())
<div class="rounded-2xl border border-amber-300 bg-amber-50 px-5 py-4 text-sm text-amber-900 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-100">
<div
class="rounded-2xl border border-amber-300 bg-amber-50 px-5 py-4 text-sm text-amber-900 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-100">
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="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</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>
<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>
<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 wire:model.live="board_id" class="hd-select" aria-label="Select Kanban board">
@foreach ($boards as $availableBoard)
<option value="{{ $availableBoard->id }}">{{ $availableBoard->name }}{{ $availableBoard->owner ? ' · '.$availableBoard->owner->name : '' }}</option>
<option value="{{ $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">
This board is being initialised with the default workflow statuses. Refresh the page if the columns do not appear immediately.
<div
class="rounded-2xl border border-amber-500/40 bg-amber-950/70 px-5 py-4 text-sm font-semibold text-amber-100">
This board is being initialised with the default workflow statuses. Refresh the page if the columns do
not appear immediately.
</div>
@else
<div class="flex gap-4 overflow-x-auto pb-4" data-helpdesk-kanban-board wire:key="helpdesk-kanban-columns-{{ $board?->id ?? 0 }}-{{ $refreshNonce }}">
@foreach ($statuses as $status)
@php($tickets = $ticketsByStatus[$status->id] ?? collect())
<div class="flex gap-4 overflow-x-auto pb-4" data-helpdesk-kanban-board
wire:key="helpdesk-kanban-columns-{{ $board?->id ?? 0 }}-{{ $refreshNonce }}">
@foreach ($statuses as $status)
@php($tickets = $ticketsByStatus[$status->id] ?? collect())
<section class="flex min-h-[640px] w-[330px] shrink-0 flex-col overflow-hidden rounded-2xl border border-zinc-200 bg-zinc-100 shadow-sm dark:border-zinc-700 dark:bg-zinc-950" dusk="kanban-column-{{ $status->slug }}" wire:key="kanban-column-{{ $board?->id ?? 0 }}-{{ $status->id }}-{{ $refreshNonce }}">
<header class="border-b border-zinc-200 bg-white px-4 py-4 dark:border-zinc-800 dark:bg-zinc-900">
<div class="flex items-start justify-between gap-3">
<div>
<h2 class="text-sm font-bold uppercase tracking-wide text-zinc-900 dark:text-zinc-100">{{ $status->name }}</h2>
<p class="mt-1 text-xs text-zinc-500"><span data-helpdesk-kanban-count data-status-id="{{ $status->id }}">{{ $tickets->count() }}</span> tickets</p>
<section
class="flex min-h-[640px] w-[330px] shrink-0 flex-col overflow-hidden rounded-2xl border border-zinc-200 bg-zinc-100 shadow-sm dark:border-zinc-700 dark:bg-zinc-950"
dusk="kanban-column-{{ $status->slug }}"
wire:key="kanban-column-{{ $board?->id ?? 0 }}-{{ $status->id }}-{{ $refreshNonce }}">
<header
class="border-b border-zinc-200 bg-white px-4 py-4 dark:border-zinc-800 dark:bg-zinc-900">
<div class="flex items-start justify-between gap-3">
<div>
<h2
class="text-sm font-bold uppercase tracking-wide text-zinc-900 dark:text-zinc-100">
{{ $status->name }}</h2>
<p class="mt-1 text-xs text-zinc-500"><span data-helpdesk-kanban-count
data-status-id="{{ $status->id }}">{{ $tickets->count() }}</span>
tickets</p>
</div>
<span class="inline-flex h-4 w-4 rounded-full ring-4 ring-zinc-100 dark:ring-zinc-800"
style="background-color: {{ \Modules\Helpdesk\Support\Color::css($status->color) }}"></span>
</div>
<span class="inline-flex h-4 w-4 rounded-full ring-4 ring-zinc-100 dark:ring-zinc-800" style="background-color: {{ \Modules\Helpdesk\Support\Color::css($status->color) }}"></span>
</div>
</header>
</header>
<div
id="helpdesk-kanban-column-{{ $status->id }}"
class="helpdesk-kanban-column min-h-[560px] flex-1 space-y-3 p-3 transition data-[drag-over=true]:bg-blue-50 dark:data-[drag-over=true]:bg-blue-950/30"
data-helpdesk-kanban-column
data-status-id="{{ $status->id }}"
wire:key="kanban-dropzone-{{ $board?->id ?? 0 }}-{{ $status->id }}-{{ $refreshNonce }}"
>
@forelse ($tickets as $ticket)
<article
class="group cursor-grab rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md active:cursor-grabbing dark:border-zinc-800 dark:bg-zinc-900"
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="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">{{ $ticket->reference }}</div>
<h3 class="mt-1 line-clamp-3 text-sm font-semibold leading-5 text-zinc-900 dark:text-zinc-100">{{ $ticket->subject }}</h3>
<div id="helpdesk-kanban-column-{{ $status->id }}"
class="helpdesk-kanban-column min-h-[560px] flex-1 space-y-3 p-3 transition data-[drag-over=true]:bg-blue-50 dark:data-[drag-over=true]:bg-blue-950/30"
data-helpdesk-kanban-column data-status-id="{{ $status->id }}"
wire:key="kanban-dropzone-{{ $board?->id ?? 0 }}-{{ $status->id }}-{{ $refreshNonce }}">
@forelse ($tickets as $ticket)
<article
class="group cursor-grab rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md active:cursor-grabbing dark:border-zinc-800 dark:bg-zinc-900"
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="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">
{{ $ticket->reference }}</div>
<h3
class="mt-1 line-clamp-3 text-sm font-semibold leading-5 text-zinc-900 dark:text-zinc-100">
{{ $ticket->subject }}</h3>
</div>
@if ($ticket->priority)
<span class="shrink-0 rounded-full px-2 py-1 text-xs font-bold text-white"
style="background-color: {{ \Modules\Helpdesk\Support\Color::css($ticket->priority->color) }}">
{{ $ticket->priority->name }}
</span>
@endif
</div>
@if ($ticket->priority)
<span class="shrink-0 rounded-full px-2 py-1 text-xs font-bold text-white" style="background-color: {{ \Modules\Helpdesk\Support\Color::css($ticket->priority->color) }}">
{{ $ticket->priority->name }}
</span>
@endif
</div>
<div class="mt-3 space-y-1.5 text-xs text-zinc-500 dark:text-zinc-400">
<div class="flex items-center gap-2"><i class="fa-solid fa-user w-3" aria-hidden="true"></i> {{ $ticket->owner?->name ?? 'Unknown' }}</div>
<div class="flex items-center gap-2"><i class="fa-solid fa-user-gear w-3" aria-hidden="true"></i> {{ $ticket->agent?->name ?? 'Unassigned' }}</div>
@if ($ticket->project)
<div class="flex items-center gap-2"><i class="fa-solid fa-diagram-project w-3" aria-hidden="true"></i> {{ $ticket->project->title }}</div>
@endif
@if ($ticket->team)
<div class="flex items-center gap-2"><i class="fa-solid fa-users w-3" aria-hidden="true"></i> {{ $ticket->team->name }}</div>
@endif
@if ($ticket->product)
<div class="flex items-center gap-2"><i class="fa-solid fa-box w-3" aria-hidden="true"></i> {{ $ticket->product->name }}</div>
@endif
</div>
<div class="mt-3 space-y-1.5 text-xs text-zinc-500 dark:text-zinc-400">
<div class="flex items-center gap-2"><i class="fa-solid fa-user w-3"
aria-hidden="true"></i> {{ $ticket->owner?->name ?? 'Unknown' }}</div>
<div class="flex items-center gap-2"><i class="fa-solid fa-user-gear w-3"
aria-hidden="true"></i> {{ $ticket->agent?->name ?? 'Unassigned' }}
</div>
@if ($ticket->project)
<div class="flex items-center gap-2"><i
class="fa-solid fa-diagram-project w-3" aria-hidden="true"></i>
{{ $ticket->project->title }}</div>
@endif
@if ($ticket->team)
<div class="flex items-center gap-2"><i class="fa-solid fa-users w-3"
aria-hidden="true"></i> {{ $ticket->team->name }}</div>
@endif
@if ($ticket->product)
<div class="flex items-center gap-2"><i class="fa-solid fa-box w-3"
aria-hidden="true"></i> {{ $ticket->product->name }}</div>
@endif
</div>
<div class="mt-3 rounded-xl border border-emerald-500/20 bg-emerald-950/20 p-2">
<label class="mb-1 block text-[11px] font-bold uppercase tracking-wide text-emerald-200/80">Board</label>
<div class="hd-ticket-board-select-wrap">
<select
class="hd-ticket-board-select"
wire:change="moveTicketToBoard({{ $ticket->id }}, $event.target.value)"
aria-label="Move {{ $ticket->reference }} to another board"
>
@foreach ($boards as $ticketBoard)
<option value="{{ $ticketBoard->id }}" @selected((int) $ticket->board_id === (int) $ticketBoard->id)>{{ $ticketBoard->name }}</option>
<div class="mt-3 rounded-xl border border-emerald-500/20 bg-emerald-950/20 p-2">
<label
class="mb-1 block text-[11px] font-bold uppercase tracking-wide text-emerald-200/80">Board</label>
<div class="hd-ticket-board-select-wrap">
<select class="hd-ticket-board-select"
wire:change="moveTicketToBoard({{ $ticket->id }}, $event.target.value)"
aria-label="Move {{ $ticket->reference }} to another board">
@foreach ($boards as $ticketBoard)
<option value="{{ $ticketBoard->id }}"
@selected((int) $ticket->board_id === (int) $ticketBoard->id)>{{ $ticketBoard->name }}</option>
@endforeach
</select>
</div>
</div>
@if ($ticket->tags->isNotEmpty())
<div class="mt-3 flex flex-wrap gap-1">
@foreach ($ticket->tags as $tag)
<span
class="rounded-full px-2 py-0.5 text-[11px] font-semibold text-white"
style="background-color: {{ \Modules\Helpdesk\Support\Color::css($tag->color) }}">{{ $tag->name }}</span>
@endforeach
</select>
</div>
@endif
@if ($ticket->checklistItems->isNotEmpty())
@php($done = $ticket->checklistItems->where('is_completed', true)->count())
<div class="mt-3 text-xs text-zinc-500">Checklist
{{ $done }}/{{ $ticket->checklistItems->count() }}</div>
@endif
@if ($ticket->sla_first_response_breached || $ticket->sla_resolution_breached)
<div
class="mt-3 rounded-lg border border-red-300 bg-red-50 px-3 py-2 text-xs font-semibold text-red-800 dark:border-red-900 dark:bg-red-950 dark:text-red-100">
SLA breach</div>
@endif
<div
class="mt-4 flex items-center justify-between gap-2 border-t border-zinc-100 pt-3 dark:border-zinc-800">
<a href="{{ route('helpdesk.tickets.show', $ticket) }}" wire:navigate
class="rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-blue-700">Open</a>
@can('update', $ticket)
<a href="{{ route('helpdesk.tickets.edit', $ticket) }}" wire:navigate
class="rounded-lg bg-emerald-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-emerald-700">Edit</a>
@endcan
</div>
</article>
@empty
<div
class="rounded-2xl 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">
Drop tickets here for {{ $status->name }}.
</div>
@if ($ticket->tags->isNotEmpty())
<div class="mt-3 flex flex-wrap gap-1">
@foreach ($ticket->tags as $tag)
<span class="rounded-full px-2 py-0.5 text-[11px] font-semibold text-white" style="background-color: {{ \Modules\Helpdesk\Support\Color::css($tag->color) }}">{{ $tag->name }}</span>
@endforeach
</div>
@endif
@if ($ticket->checklistItems->isNotEmpty())
@php($done = $ticket->checklistItems->where('is_completed', true)->count())
<div class="mt-3 text-xs text-zinc-500">Checklist {{ $done }}/{{ $ticket->checklistItems->count() }}</div>
@endif
@if ($ticket->sla_first_response_breached || $ticket->sla_resolution_breached)
<div class="mt-3 rounded-lg border border-red-300 bg-red-50 px-3 py-2 text-xs font-semibold text-red-800 dark:border-red-900 dark:bg-red-950 dark:text-red-100">SLA breach</div>
@endif
<div class="mt-4 flex items-center justify-between gap-2 border-t border-zinc-100 pt-3 dark:border-zinc-800">
<a href="{{ route('helpdesk.tickets.show', $ticket) }}" wire:navigate class="rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-blue-700">Open</a>
@can('update', $ticket)
<a href="{{ route('helpdesk.tickets.edit', $ticket) }}" wire:navigate class="rounded-lg bg-emerald-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-emerald-700">Edit</a>
@endcan
</div>
</article>
@empty
<div class="rounded-2xl 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">
Drop tickets here for {{ $status->name }}.
</div>
@endforelse
</div>
</section>
@endforeach
</div>
@endforelse
</div>
</section>
@endforeach
</div>
@endif
@endif
@script
<script>
const initialiseHelpdeskKanban = function () {
const initialiseHelpdeskKanban = function() {
let draggedTicketId = null;
let draggedCard = null;
const updateColumnCounts = function () {
document.querySelectorAll('[data-helpdesk-kanban-count]').forEach(function (counter) {
const updateColumnCounts = function() {
document.querySelectorAll('[data-helpdesk-kanban-count]').forEach(function(counter) {
const statusId = counter.dataset.statusId;
const column = document.querySelector('[data-helpdesk-kanban-column][data-status-id="' + statusId + '"]');
const column = document.querySelector('[data-helpdesk-kanban-column][data-status-id="' +
statusId + '"]');
if (!column) {
return;
}
counter.textContent = column.querySelectorAll('[data-helpdesk-kanban-ticket]').length.toString();
counter.textContent = column.querySelectorAll('[data-helpdesk-kanban-ticket]').length
.toString();
});
};
const forceLivewireRefresh = function () {
const forceLivewireRefresh = function() {
if (typeof $wire !== 'undefined' && typeof $wire.$refresh === 'function') {
return Promise.resolve($wire.$refresh());
}
@@ -210,11 +248,11 @@
return Promise.resolve();
};
document.querySelectorAll('[data-helpdesk-kanban-ticket]').forEach(function (card) {
document.querySelectorAll('[data-helpdesk-kanban-ticket]').forEach(function(card) {
if (card.dataset.nativeDragReady === '1') return;
card.dataset.nativeDragReady = '1';
card.addEventListener('dragstart', function (event) {
card.addEventListener('dragstart', function(event) {
draggedTicketId = card.dataset.ticketId;
draggedCard = card;
event.dataTransfer.effectAllowed = 'move';
@@ -223,38 +261,41 @@
card.classList.add('opacity-60');
});
card.addEventListener('dragend', function () {
card.addEventListener('dragend', function() {
card.classList.remove('opacity-60');
document.querySelectorAll('[data-helpdesk-kanban-column]').forEach(function (column) {
document.querySelectorAll('[data-helpdesk-kanban-column]').forEach(function(
column) {
column.dataset.dragOver = 'false';
column.dataset.refreshing = 'false';
});
});
});
document.querySelectorAll('[data-helpdesk-kanban-column]').forEach(function (column) {
document.querySelectorAll('[data-helpdesk-kanban-column]').forEach(function(column) {
if (column.dataset.nativeDropReady === '1') return;
column.dataset.nativeDropReady = '1';
column.addEventListener('dragover', function (event) {
column.addEventListener('dragover', function(event) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
column.dataset.dragOver = 'true';
});
column.addEventListener('dragleave', function (event) {
column.addEventListener('dragleave', function(event) {
if (!column.contains(event.relatedTarget)) {
column.dataset.dragOver = 'false';
}
});
column.addEventListener('drop', function (event) {
column.addEventListener('drop', function(event) {
event.preventDefault();
column.dataset.dragOver = 'false';
const ticketId = event.dataTransfer.getData('application/x-helpdesk-ticket-id') || event.dataTransfer.getData('text/plain') || draggedTicketId;
const ticketId = event.dataTransfer.getData('application/x-helpdesk-ticket-id') ||
event.dataTransfer.getData('text/plain') || draggedTicketId;
const statusId = column.dataset.statusId;
const card = draggedCard || document.querySelector('[data-helpdesk-kanban-ticket][data-ticket-id="' + ticketId + '"]');
const card = draggedCard || document.querySelector(
'[data-helpdesk-kanban-ticket][data-ticket-id="' + ticketId + '"]');
if (!ticketId || !statusId || !card) return;
@@ -264,7 +305,7 @@
Promise.resolve($wire.moveTicket(ticketId, statusId))
.then(forceLivewireRefresh)
.finally(function () {
.finally(function() {
column.dataset.refreshing = 'false';
draggedTicketId = null;
draggedCard = null;
@@ -280,8 +321,8 @@
initialiseHelpdeskKanban();
document.addEventListener('livewire:navigated', initialiseHelpdeskKanban);
document.addEventListener('livewire:updated', initialiseHelpdeskKanban);
document.addEventListener('helpdesk-kanban-moved', function () {
setTimeout(function () {
document.addEventListener('helpdesk-kanban-moved', function() {
setTimeout(function() {
initialiseHelpdeskKanban();
if (typeof $wire !== 'undefined' && typeof $wire.$refresh === 'function') {
@@ -21,13 +21,16 @@
@endphp
<div class="hd-helpdesk-surface space-y-6" dusk="helpdesk-settings-manager">
<div class="overflow-hidden rounded-3xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<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 administration</p>
<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 lookup data, workflow messages, canned responses, SLA policies, automation rules, checklists, tags and product/service linkage.</p>
<p class="mt-2 max-w-3xl text-sm text-blue-100/90">Manage lookup data, workflow messages, canned
responses, SLA policies, automation rules, checklists, tags and product/service linkage.</p>
</div>
<div class="flex flex-wrap gap-2">
@@ -41,22 +44,22 @@
</div>
<div class="grid gap-3 md:grid-cols-4 xl: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()],
'boards' => ['label' => 'Boards', 'count' => $boards->count()],
'git' => ['label' => 'Git', 'count' => $repositories->count()],
'workflow' => ['label' => 'Workflow', 'count' => $workflowTemplates->count()],
'canned' => ['label' => 'Canned', 'count' => $cannedResponses->count()],
'sla' => ['label' => 'SLA', 'count' => $slaPolicies->count()],
'automation' => ['label' => 'Rules', 'count' => $automationRules->count()],
'checklists' => ['label' => 'Checks', 'count' => $checklistTemplates->count()],
'tags' => ['label' => 'Tags', 'count' => $tags->count()],
'products' => ['label' => 'Products', 'count' => $products->count()],
'defaults' => ['label' => 'Defaults', 'count' => 1],
] as $key => $item)
'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()],
'boards' => ['label' => 'Boards', 'count' => $boards->count()],
'git' => ['label' => 'Git', 'count' => $repositories->count()],
'workflow' => ['label' => 'Workflow', 'count' => $workflowTemplates->count()],
'canned' => ['label' => 'Canned', 'count' => $cannedResponses->count()],
'sla' => ['label' => 'SLA', 'count' => $slaPolicies->count()],
'automation' => ['label' => 'Rules', 'count' => $automationRules->count()],
'checklists' => ['label' => 'Checks', 'count' => $checklistTemplates->count()],
'tags' => ['label' => 'Tags', 'count' => $tags->count()],
'products' => ['label' => 'Products', 'count' => $products->count()],
'defaults' => ['label' => 'Defaults', 'count' => 1],
] as $key => $item)
@php
$sectionPalette = match ($key) {
'statuses' => ['border' => '#38bdf8', 'bg' => 'rgba(14, 165, 233, .22)', 'active' => '#0284c7'],
@@ -77,11 +80,23 @@
default => ['border' => '#71717a', 'bg' => 'rgba(63, 63, 70, .22)', 'active' => '#3f3f46'],
};
$sectionStyle = $section === $key
? 'border-color: '.$sectionPalette['border'].'; background-color: '.$sectionPalette['active'].'; color: #ffffff; box-shadow: 0 16px 36px rgba(0,0,0,.32);'
: 'border-color: '.$sectionPalette['border'].'; background-color: '.$sectionPalette['bg'].'; color: #ffffff; box-shadow: 0 10px 24px rgba(0,0,0,.20);';
$sectionStyle =
$section === $key
? 'border-color: ' .
$sectionPalette['border'] .
'; background-color: ' .
$sectionPalette['active'] .
'; color: #ffffff; box-shadow: 0 16px 36px rgba(0,0,0,.32);'
: 'border-color: ' .
$sectionPalette['border'] .
'; background-color: ' .
$sectionPalette['bg'] .
'; color: #ffffff; box-shadow: 0 10px 24px rgba(0,0,0,.20);';
@endphp
<a href="{{ route('helpdesk.settings', ['section' => $key]) }}" dusk="helpdesk-settings-section-{{ $key }}" class="rounded-2xl border px-4 py-3 text-left shadow-sm transition hover:-translate-y-0.5" style="{{ $sectionStyle }}">
<a href="{{ route('helpdesk.settings', ['section' => $key]) }}"
dusk="helpdesk-settings-section-{{ $key }}"
class="rounded-2xl border px-4 py-3 text-left shadow-sm transition hover:-translate-y-0.5"
style="{{ $sectionStyle }}">
<div class="text-xs font-semibold uppercase tracking-wide opacity-70">{{ $item['label'] }}</div>
<div class="mt-1 text-2xl font-semibold">{{ $item['count'] }}</div>
</a>
@@ -90,23 +105,21 @@
@if ($section === 'defaults')
<x-helpdesk::settings-section title="Ticket defaults" description="Choose the company default used when a new Helpdesk ticket is created. Lookup defaults are configured on the Statuses, Priorities, Categories, Teams and Projects sections.">
<x-helpdesk::settings-section title="Ticket defaults"
description="Choose the company default used when a new Helpdesk ticket is created. Lookup defaults are configured on the Statuses, Priorities, Categories, Teams and Projects sections.">
<x-slot:form>
<form class="grid gap-4 md:grid-cols-3">
<flux:select wire:model="default_helpdesk_company_id" label="Default company">
<option value="">No default company</option>
@foreach ($companies as $company)
<option value="{{ $company->id }}">
{{ $company->name }}{{ ($company->is_default_helpdesk_company ?? false) ? ' — Default' : '' }}
{{ $company->name }}{{ $company->is_default_helpdesk_company ?? false ? ' — Default' : '' }}
</option>
@endforeach
</flux:select>
<div class="md:col-span-3 flex gap-2">
<flux:button
type="button"
wire:click="saveDefaultHelpdeskCompany"
variant="primary"
<flux:button type="button" wire:click="saveDefaultHelpdeskCompany" variant="primary"
class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">
Save default company
</flux:button>
@@ -114,27 +127,41 @@
</form>
</x-slot:form>
<div class="rounded-2xl border border-zinc-200 bg-zinc-50 p-4 text-sm text-zinc-700 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200">
<div
class="rounded-2xl border border-zinc-200 bg-zinc-50 p-4 text-sm text-zinc-700 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200">
<p class="font-semibold">Lookup defaults</p>
<p class="mt-1">Set the default Status, Priority, Category, Team and Project from their own settings sections. Only one default per company/global scope is kept for each lookup type.</p>
<p class="mt-1">Set the default Status, Priority, Category, Team and Project from their own settings
sections. Only one default per company/global scope is kept for each lookup type.</p>
</div>
</x-helpdesk::settings-section>
@endif
@if ($section === 'statuses')
<x-helpdesk::settings-section title="Statuses" description="Control workflow stages and mark which statuses close tickets.">
<x-helpdesk::settings-section title="Statuses"
description="Control workflow stages and mark which statuses close tickets.">
<x-slot:form>
<form wire:submit.prevent="saveStatus" class="grid gap-4 md:grid-cols-5">
<flux:input wire:model="statusForm.name" label="Name" />
<flux:input wire:model="statusForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="statusForm.color" label="Badge colour" />
<flux:input wire:model="statusForm.sort_order" label="Sort" type="number" min="0" />
<label class="flex items-end gap-2 pb-2 text-sm text-zinc-700 dark:text-zinc-200"><input type="checkbox" wire:model="statusForm.is_closed" class="rounded border-zinc-300"> Closed status</label>
<label class="md:col-span-5 flex items-center gap-2 text-sm font-semibold text-zinc-700 dark:text-zinc-200">
<input type="checkbox" wire:model="statusForm.is_default" class="rounded border-zinc-300 bg-zinc-950 text-blue-600 focus:ring-blue-500">
<label class="flex items-end gap-2 pb-2 text-sm text-zinc-700 dark:text-zinc-200"><input
type="checkbox" wire:model="statusForm.is_closed" class="rounded border-zinc-300"> Closed
status</label>
<label
class="md:col-span-5 flex items-center gap-2 text-sm font-semibold text-zinc-700 dark:text-zinc-200">
<input type="checkbox" wire:model="statusForm.is_default"
class="rounded border-zinc-300 bg-zinc-950 text-blue-600 focus:ring-blue-500">
Default status for new tickets
</label>
<div class="md:col-span-5 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save status</flux:button><flux:button type="button" wire:click="resetStatusForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div>
<div class="md:col-span-5 flex gap-2">
<flux:button type="submit" variant="primary"
class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save status
</flux:button>
<flux:button type="button" wire:click="resetStatusForm" variant="ghost"
class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
</flux:button>
</div>
</form>
</x-slot:form>
<x-helpdesk::lookup-table :items="$statuses" edit="editStatus" delete="deleteStatus" />
@@ -150,10 +177,18 @@
<flux:input wire:model="priorityForm.color" label="Badge colour" />
<flux:input wire:model="priorityForm.sort_order" label="Sort" type="number" min="0" />
<label class="flex items-end gap-2 pb-2 text-sm font-semibold text-zinc-700 dark:text-zinc-200">
<input type="checkbox" wire:model="priorityForm.is_default" class="rounded border-zinc-300 bg-zinc-950 text-blue-600 focus:ring-blue-500">
<input type="checkbox" wire:model="priorityForm.is_default"
class="rounded border-zinc-300 bg-zinc-950 text-blue-600 focus:ring-blue-500">
Default for new tickets
</label>
<div class="md:col-span-4 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save priority</flux:button><flux:button type="button" wire:click="resetPriorityForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div>
<div class="md:col-span-4 flex gap-2">
<flux:button type="submit" variant="primary"
class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save priority
</flux:button>
<flux:button type="button" wire:click="resetPriorityForm" variant="ghost"
class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
</flux:button>
</div>
</form>
</x-slot:form>
<x-helpdesk::lookup-table :items="$priorities" edit="editPriority" delete="deletePriority" />
@@ -161,19 +196,36 @@
@endif
@if ($section === 'categories')
<x-helpdesk::settings-section title="Categories" description="Group tickets by support area, optionally per company.">
<x-helpdesk::settings-section title="Categories"
description="Group tickets by support area, optionally per company.">
<x-slot:form>
<form wire:submit.prevent="saveCategory" class="grid gap-4 md:grid-cols-4">
<flux:select wire:model="categoryForm.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="categoryForm.company_id" label="Company">
<option value="">Global</option>
@foreach ($companies as $company)
<option value="{{ $company->id }}">{{ $company->name }}</option>
@endforeach
</flux:select>
<flux:input wire:model="categoryForm.name" label="Name" />
<flux:input wire:model="categoryForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="categoryForm.color" label="Badge colour" />
<div class="md:col-span-4"><flux:textarea wire:model="categoryForm.description" label="Description" rows="3" /></div>
<label class="md:col-span-4 flex items-center gap-2 text-sm font-semibold text-zinc-700 dark:text-zinc-200">
<input type="checkbox" wire:model="categoryForm.is_default" class="rounded border-zinc-300 bg-zinc-950 text-blue-600 focus:ring-blue-500">
<div class="md:col-span-4">
<flux:textarea wire:model="categoryForm.description" label="Description" rows="3" />
</div>
<label
class="md:col-span-4 flex items-center gap-2 text-sm font-semibold text-zinc-700 dark:text-zinc-200">
<input type="checkbox" wire:model="categoryForm.is_default"
class="rounded border-zinc-300 bg-zinc-950 text-blue-600 focus:ring-blue-500">
Default category for new tickets
</label>
<div class="md:col-span-4 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save category</flux:button><flux:button type="button" wire:click="resetCategoryForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div>
<div class="md:col-span-4 flex gap-2">
<flux:button type="submit" variant="primary"
class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save
category</flux:button>
<flux:button type="button" wire:click="resetCategoryForm" variant="ghost"
class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
</flux:button>
</div>
</form>
</x-slot:form>
<x-helpdesk::lookup-table :items="$categories" edit="editCategory" delete="deleteCategory" />
@@ -181,19 +233,36 @@
@endif
@if ($section === 'teams')
<x-helpdesk::settings-section title="Teams" description="Create support teams under each company and assign projects to them.">
<x-helpdesk::settings-section title="Teams"
description="Create support teams under each company and assign projects to them.">
<x-slot:form>
<form wire:submit.prevent="saveTeam" class="grid gap-4 md:grid-cols-4">
<flux:select wire:model="teamForm.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="teamForm.company_id" label="Company">
<option value="">Global</option>
@foreach ($companies as $company)
<option value="{{ $company->id }}">{{ $company->name }}</option>
@endforeach
</flux:select>
<flux:input wire:model="teamForm.name" label="Name" />
<flux:input wire:model="teamForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="teamForm.color" label="Badge colour" />
<div class="md:col-span-4"><flux:textarea wire:model="teamForm.description" label="Description" rows="3" /></div>
<label class="md:col-span-4 flex items-center gap-2 text-sm font-semibold text-zinc-700 dark:text-zinc-200">
<input type="checkbox" wire:model="teamForm.is_default" class="rounded border-zinc-300 bg-zinc-950 text-blue-600 focus:ring-blue-500">
<div class="md:col-span-4">
<flux:textarea wire:model="teamForm.description" label="Description" rows="3" />
</div>
<label
class="md:col-span-4 flex items-center gap-2 text-sm font-semibold text-zinc-700 dark:text-zinc-200">
<input type="checkbox" wire:model="teamForm.is_default"
class="rounded border-zinc-300 bg-zinc-950 text-blue-600 focus:ring-blue-500">
Default team for new tickets
</label>
<div class="md:col-span-4 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save team</flux:button><flux:button type="button" wire:click="resetTeamForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div>
<div class="md:col-span-4 flex gap-2">
<flux:button type="submit" variant="primary"
class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save team
</flux:button>
<flux:button type="button" wire:click="resetTeamForm" variant="ghost"
class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
</flux:button>
</div>
</form>
</x-slot:form>
<x-helpdesk::team-table :items="$teams" />
@@ -201,20 +270,42 @@
@endif
@if ($section === 'projects')
<x-helpdesk::settings-section title="Projects" description="Create company projects and optionally assign them to support teams.">
<x-helpdesk::settings-section title="Projects"
description="Create company projects and optionally assign them to support teams.">
<x-slot:form>
<form wire:submit.prevent="saveProject" class="grid gap-4 md:grid-cols-5">
<flux:select wire:model="projectForm.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="projectForm.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="projectForm.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="projectForm.team_id" label="Team">
<option value="">No team</option>
@foreach ($teams as $team)
<option value="{{ $team->id }}">{{ $team->name }}</option>
@endforeach
</flux:select>
<flux:input wire:model="projectForm.title" label="Title" />
<flux:input wire:model="projectForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="projectForm.color" label="Badge colour" />
<div class="md:col-span-5"><flux:textarea wire:model="projectForm.description" label="Description" rows="3" /></div>
<label class="md:col-span-5 flex items-center gap-2 text-sm font-semibold text-zinc-700 dark:text-zinc-200">
<input type="checkbox" wire:model="projectForm.is_default" class="rounded border-zinc-300 bg-zinc-950 text-blue-600 focus:ring-blue-500">
<div class="md:col-span-5">
<flux:textarea wire:model="projectForm.description" label="Description" rows="3" />
</div>
<label
class="md:col-span-5 flex items-center gap-2 text-sm font-semibold text-zinc-700 dark:text-zinc-200">
<input type="checkbox" wire:model="projectForm.is_default"
class="rounded border-zinc-300 bg-zinc-950 text-blue-600 focus:ring-blue-500">
Default project for new tickets
</label>
<div class="md:col-span-5 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save project</flux:button><flux:button type="button" wire:click="resetProjectForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div>
<div class="md:col-span-5 flex gap-2">
<flux:button type="submit" variant="primary"
class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save project
</flux:button>
<flux:button type="button" wire:click="resetProjectForm" variant="ghost"
class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
</flux:button>
</div>
</form>
</x-slot:form>
<x-helpdesk::project-table :items="$projects" />
@@ -223,7 +314,8 @@
@if ($section === 'boards')
<x-helpdesk::settings-section title="Kanban Boards" description="Create configurable workflow boards and choose which statuses appear as draggable columns.">
<x-helpdesk::settings-section title="Kanban Boards"
description="Create configurable workflow boards and choose which statuses appear as draggable columns.">
<x-slot:form>
<form wire:submit.prevent="saveBoard" class="grid gap-4 md:grid-cols-7">
<flux:input wire:model="boardForm.name" label="Name" />
@@ -236,33 +328,44 @@
@endforeach
</flux:select>
<label class="flex items-end gap-2 pb-2 text-sm text-zinc-700 dark:text-zinc-200">
<input type="checkbox" wire:model="boardForm.is_default" class="rounded border-zinc-300"> Default board
<input type="checkbox" wire:model="boardForm.is_default" class="rounded border-zinc-300">
Default board
</label>
<div class="md:col-span-2">
<flux:textarea wire:model="boardForm.description" label="Description" rows="2" />
</div>
<div class="md:col-span-7">
<label class="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-200">Board columns</label>
<label class="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-200">Board
columns</label>
<div class="grid gap-2 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
@foreach ($statuses as $status)
<label class="flex items-center gap-2 rounded-xl border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-700 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200">
<input type="checkbox" wire:model="boardForm.status_ids" value="{{ $status->id }}" class="rounded border-zinc-300">
<span class="inline-flex h-3 w-3 rounded-full" style="background-color: {{ \Modules\Helpdesk\Support\Color::css($status->color) }}"></span>
<label
class="flex items-center gap-2 rounded-xl border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-700 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200">
<input type="checkbox" wire:model="boardForm.status_ids"
value="{{ $status->id }}" class="rounded border-zinc-300">
<span class="inline-flex h-3 w-3 rounded-full"
style="background-color: {{ \Modules\Helpdesk\Support\Color::css($status->color) }}"></span>
<span>{{ $status->name }}</span>
</label>
@endforeach
</div>
</div>
<div class="md:col-span-7 flex gap-2">
<flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save board</flux:button>
<flux:button type="button" wire:click="resetBoardForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button>
<flux:button type="submit" variant="primary"
class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save board
</flux:button>
<flux:button type="button" wire:click="resetBoardForm" variant="ghost"
class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">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">
<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">
<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">Board</th>
<th class="px-4 py-3">Owner</th>
@@ -276,26 +379,42 @@
@forelse ($boards as $boardItem)
<tr>
<td class="px-4 py-3">
<div class="font-medium text-zinc-900 dark:text-zinc-100">{{ $boardItem->name }}</div>
<div class="font-medium text-zinc-900 dark:text-zinc-100">{{ $boardItem->name }}
</div>
<div class="text-xs text-zinc-500">{{ $boardItem->slug }}</div>
</td>
<td class="px-4 py-3 text-sm text-zinc-600 dark:text-zinc-300">{{ $boardItem->owner?->name ?? 'Unassigned' }}</td>
<td class="px-4 py-3 text-sm text-zinc-600 dark:text-zinc-300">
{{ $boardItem->owner?->name ?? 'Unassigned' }}</td>
<td class="px-4 py-3">
<div class="flex flex-wrap gap-1">
@foreach ($boardItem->statuses as $status)
<span class="rounded-full px-2 py-1 text-xs font-semibold text-white" style="background-color: {{ \Modules\Helpdesk\Support\Color::css($status->color) }}">{{ $status->name }}</span>
<span class="rounded-full px-2 py-1 text-xs font-semibold text-white"
style="background-color: {{ \Modules\Helpdesk\Support\Color::css($status->color) }}">{{ $status->name }}</span>
@endforeach
</div>
</td>
<td class="px-4 py-3 text-zinc-500">{{ $boardItem->tickets_count ?? 0 }}</td>
<td class="px-4 py-3"><flux:badge color="{{ $boardItem->is_default ? 'emerald' : 'zinc' }}">{{ $boardItem->is_default ? 'Default' : 'No' }}</flux:badge></td>
<td class="px-4 py-3">
<flux:badge color="{{ $boardItem->is_default ? 'emerald' : 'zinc' }}">
{{ $boardItem->is_default ? 'Default' : 'No' }}</flux:badge>
</td>
<td class="px-4 py-3 text-right">
<flux:button size="sm" variant="primary" wire:click="editBoard({{ $boardItem->id }})" class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">Edit</flux:button>
<flux:button size="sm" variant="danger" wire:click="deleteBoard({{ $boardItem->id }})" wire:confirm="Delete this board?" class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">Delete</flux:button>
<flux:button size="sm" variant="primary"
wire:click="editBoard({{ $boardItem->id }})"
class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">
Edit</flux:button>
<flux:button size="sm" variant="danger"
wire:click="deleteBoard({{ $boardItem->id }})"
wire:confirm="Delete this board?"
class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">Delete
</flux:button>
</td>
</tr>
@empty
<tr><td colspan="6" class="px-4 py-8 text-center text-zinc-500">No Kanban boards yet.</td></tr>
<tr>
<td colspan="6" class="px-4 py-8 text-center text-zinc-500">No Kanban boards yet.
</td>
</tr>
@endforelse
</tbody>
</table>
@@ -304,7 +423,8 @@
@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-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.prevent="saveRepository" class="grid gap-4 md:grid-cols-6">
<flux:select wire:model="repositoryForm.company_id" label="Company">
@@ -320,15 +440,21 @@
@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.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-7">
<flux:input wire:model="repositoryForm.remote_url" label="Remote URL" placeholder="git@github.com:company/repo.git" />
<flux:input wire:model="repositoryForm.remote_url" label="Remote URL"
placeholder="git@github.com:company/repo.git" />
</div>
<div class="md:col-span-7 flex gap-2">
<flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save repository</flux:button>
<flux:button type="button" wire:click="resetRepositoryForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button>
<flux:button type="submit" variant="primary"
class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save
repository</flux:button>
<flux:button type="button" wire:click="resetRepositoryForm" variant="ghost"
class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
</flux:button>
</div>
</form>
</x-slot:form>
@@ -338,7 +464,8 @@
@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-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.prevent="saveWorkflowTemplate" class="grid gap-4 md:grid-cols-6">
<flux:input wire:model="workflowForm.name" label="Name" />
@@ -354,28 +481,41 @@
<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>
<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-7">
<flux:input wire:model="workflowForm.subject" label="Subject" placeholder="Ticket @{{ ticket.reference }} has been raised" />
<flux:input wire:model="workflowForm.subject" label="Subject"
placeholder="Ticket @{{ ticket.reference }} has been raised" />
</div>
<div class="md:col-span-7">
<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 }}" />
<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-7">
<flux:textarea wire:model="workflowForm.text_body" label="Plain text body" rows="4" placeholder="Optional fallback text" />
<flux:textarea wire:model="workflowForm.text_body" label="Plain text body" rows="4"
placeholder="Optional fallback text" />
</div>
<div class="md:col-span-7 flex gap-2">
<flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save workflow message</flux:button>
<flux:button type="button" wire:click="resetWorkflowTemplateForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button>
<flux:button type="submit" variant="primary"
class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save
workflow message</flux:button>
<flux:button type="button" wire:click="resetWorkflowTemplateForm" variant="ghost"
class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">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">
<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">
<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>
@@ -387,17 +527,31 @@
<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 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">
<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 }})" class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">Edit</flux:button>
<flux:button size="sm" variant="danger" wire:click="deleteWorkflowTemplate({{ $template->id }})" wire:confirm="Delete this workflow message?" class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">Delete</flux:button>
<flux:button size="sm" variant="ghost"
wire:click="editWorkflowTemplate({{ $template->id }})"
class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">
Edit</flux:button>
<flux:button size="sm" variant="danger"
wire:click="deleteWorkflowTemplate({{ $template->id }})"
wire:confirm="Delete this workflow message?"
class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">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>
<tr>
<td colspan="5" class="px-4 py-8 text-center text-zinc-500">No workflow messages
yet.</td>
</tr>
@endforelse
</tbody>
</table>
@@ -407,95 +561,394 @@
@if ($section === 'canned')
<x-helpdesk::settings-section title="Canned responses" description="Save professional reusable replies with macro tokens for ticket, requester, agent, status and URL values.">
<x-helpdesk::settings-section title="Canned responses"
description="Save professional reusable replies with macro tokens for ticket, requester, agent, status and URL values.">
<x-slot:form>
<form wire:submit.prevent="saveCannedResponse" class="grid gap-4 md:grid-cols-6">
<flux:input wire:model="cannedForm.name" label="Name" />
<flux:input wire:model="cannedForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="cannedForm.sort_order" label="Sort" type="number" min="0" />
<flux:select wire:model="cannedForm.category_id" label="Category"><option value="">Any category</option>@foreach ($categories as $category)<option value="{{ $category->id }}">{{ $category->name }}</option>@endforeach</flux:select>
<flux:select wire:model="cannedForm.team_id" label="Team"><option value="">Any team</option>@foreach ($teams as $team)<option value="{{ $team->id }}">{{ $team->name }}</option>@endforeach</flux:select>
<label class="flex items-end gap-2 pb-2 text-sm text-zinc-700 dark:text-zinc-200"><input type="checkbox" wire:model="cannedForm.is_active" class="rounded border-zinc-300"> Active</label>
<div class="md:col-span-7"><flux:input wire:model="cannedForm.subject" label="Optional subject" /></div>
<div class="md:col-span-7"><flux:textarea wire:model="cannedForm.body" label="Reply body" rows="6" placeholder="Hello @{{ requester.name }},&#10;&#10;Your ticket @{{ ticket.reference }} has been reviewed." /></div>
<div class="md:col-span-7 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save canned response</flux:button><flux:button type="button" wire:click="resetCannedResponseForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div>
<flux:select wire:model="cannedForm.category_id" label="Category">
<option value="">Any category</option>
@foreach ($categories as $category)
<option value="{{ $category->id }}">{{ $category->name }}</option>
@endforeach
</flux:select>
<flux:select wire:model="cannedForm.team_id" label="Team">
<option value="">Any team</option>
@foreach ($teams as $team)
<option value="{{ $team->id }}">{{ $team->name }}</option>
@endforeach
</flux:select>
<label class="flex items-end gap-2 pb-2 text-sm text-zinc-700 dark:text-zinc-200"><input
type="checkbox" wire:model="cannedForm.is_active" class="rounded border-zinc-300">
Active</label>
<div class="md:col-span-7">
<flux:input wire:model="cannedForm.subject" label="Optional subject" />
</div>
<div class="md:col-span-7">
<flux:textarea wire:model="cannedForm.body" label="Reply body" rows="6"
placeholder="Hello @{{ requester.name }},&#10;&#10;Your ticket @{{ ticket.reference }} has been reviewed." />
</div>
<div class="md:col-span-7 flex gap-2">
<flux:button type="submit" variant="primary"
class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save canned
response</flux:button>
<flux:button type="button" wire:click="resetCannedResponseForm" variant="ghost"
class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
</flux:button>
</div>
</form>
</x-slot:form>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
@forelse ($cannedResponses as $response)
<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
<div class="flex items-start justify-between gap-3"><div><h3 class="font-semibold text-zinc-900 dark:text-zinc-100">{{ $response->name }}</h3><p class="text-xs text-zinc-500">{{ $response->category?->name ?? 'Any category' }} · {{ $response->team?->name ?? 'Any team' }}</p></div><flux:badge color="{{ $response->is_active ? 'emerald' : 'zinc' }}">{{ $response->is_active ? 'Active' : 'Inactive' }}</flux:badge></div>
<p class="mt-3 line-clamp-3 text-sm text-zinc-600 dark:text-zinc-300">{{ strip_tags($response->body) }}</p>
<div class="mt-4 flex justify-end gap-2"><flux:button size="sm" variant="primary" wire:click="editCannedResponse({{ $response->id }})" class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">Edit</flux:button><flux:button size="sm" variant="danger" wire:click="deleteCannedResponse({{ $response->id }})" wire:confirm="Delete this canned response?" class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">Delete</flux:button></div>
<div class="flex items-start justify-between gap-3">
<div>
<h3 class="font-semibold text-zinc-900 dark:text-zinc-100">{{ $response->name }}</h3>
<p class="text-xs text-zinc-500">{{ $response->category?->name ?? 'Any category' }} ·
{{ $response->team?->name ?? 'Any team' }}</p>
</div>
<flux:badge color="{{ $response->is_active ? 'emerald' : 'zinc' }}">
{{ $response->is_active ? 'Active' : 'Inactive' }}</flux:badge>
</div>
<p class="mt-3 line-clamp-3 text-sm text-zinc-600 dark:text-zinc-300">
{{ strip_tags($response->body) }}</p>
<div class="mt-4 flex justify-end gap-2">
<flux:button size="sm" variant="primary"
wire:click="editCannedResponse({{ $response->id }})"
class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">Edit
</flux:button>
<flux:button size="sm" variant="danger"
wire:click="deleteCannedResponse({{ $response->id }})"
wire:confirm="Delete this canned response?"
class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">Delete
</flux:button>
</div>
</div>
@empty
<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700 md:col-span-2 xl:col-span-3">No canned responses yet.</div>
<div
class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700 md:col-span-2 xl:col-span-3">
No canned responses yet.</div>
@endforelse
</div>
</x-helpdesk::settings-section>
@endif
@if ($section === 'sla')
<x-helpdesk::settings-section title="SLA policies" description="Define first response and resolution targets used for breach indicators, reporting and ticket cards.">
<x-helpdesk::settings-section title="SLA policies"
description="Define first response and resolution targets used for breach indicators, reporting and ticket cards.">
<x-slot:form>
<form wire:submit.prevent="saveSlaPolicy" class="grid gap-4 md:grid-cols-6">
<flux:input wire:model="slaForm.name" label="Name" />
<flux:input wire:model="slaForm.slug" label="Slug" placeholder="auto-generated" />
<flux:select wire:model="slaForm.priority_id" label="Priority"><option value="">Any priority</option>@foreach ($priorities as $priority)<option value="{{ $priority->id }}">{{ $priority->name }}</option>@endforeach</flux:select>
<flux:select wire:model="slaForm.category_id" label="Category"><option value="">Any category</option>@foreach ($categories as $category)<option value="{{ $category->id }}">{{ $category->name }}</option>@endforeach</flux:select>
<flux:input wire:model="slaForm.first_response_minutes" label="First response minutes" type="number" min="1" />
<flux:input wire:model="slaForm.resolution_minutes" label="Resolution minutes" type="number" min="1" />
<label class="flex items-center gap-2 text-sm text-zinc-700 dark:text-zinc-200"><input type="checkbox" wire:model="slaForm.is_default" class="rounded border-zinc-300"> Default</label>
<label class="flex items-center gap-2 text-sm text-zinc-700 dark:text-zinc-200"><input type="checkbox" wire:model="slaForm.business_hours_only" class="rounded border-zinc-300"> Business hours</label>
<label class="flex items-center gap-2 text-sm text-zinc-700 dark:text-zinc-200"><input type="checkbox" wire:model="slaForm.is_active" class="rounded border-zinc-300"> Active</label>
<div class="md:col-span-7 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save SLA policy</flux:button><flux:button type="button" wire:click="resetSlaPolicyForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div>
<flux:select wire:model="slaForm.priority_id" label="Priority">
<option value="">Any priority</option>
@foreach ($priorities as $priority)
<option value="{{ $priority->id }}">{{ $priority->name }}</option>
@endforeach
</flux:select>
<flux:select wire:model="slaForm.category_id" label="Category">
<option value="">Any category</option>
@foreach ($categories as $category)
<option value="{{ $category->id }}">{{ $category->name }}</option>
@endforeach
</flux:select>
<flux:input wire:model="slaForm.first_response_minutes" label="First response minutes"
type="number" min="1" />
<flux:input wire:model="slaForm.resolution_minutes" label="Resolution minutes" type="number"
min="1" />
<label class="flex items-center gap-2 text-sm text-zinc-700 dark:text-zinc-200"><input
type="checkbox" wire:model="slaForm.is_default" class="rounded border-zinc-300">
Default</label>
<label class="flex items-center gap-2 text-sm text-zinc-700 dark:text-zinc-200"><input
type="checkbox" wire:model="slaForm.business_hours_only" class="rounded border-zinc-300">
Business hours</label>
<label class="flex items-center gap-2 text-sm text-zinc-700 dark:text-zinc-200"><input
type="checkbox" wire:model="slaForm.is_active" class="rounded border-zinc-300">
Active</label>
<div class="md:col-span-7 flex gap-2">
<flux:button type="submit" variant="primary"
class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save SLA
policy</flux:button>
<flux:button type="button" wire:click="resetSlaPolicyForm" variant="ghost"
class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">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 text-zinc-500 dark:bg-zinc-800"><tr><th class="px-4 py-3">Policy</th><th class="px-4 py-3">Targets</th><th class="px-4 py-3">Applies to</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 ($slaPolicies as $policy)<tr><td class="px-4 py-3 font-medium">{{ $policy->name }} @if($policy->is_default)<flux:badge color="blue">Default</flux:badge>@endif</td><td class="px-4 py-3">{{ $policy->first_response_minutes }}m response · {{ $policy->resolution_minutes }}m close</td><td class="px-4 py-3">{{ $policy->priority?->name ?? 'Any priority' }} · {{ $policy->category?->name ?? 'Any category' }}</td><td class="px-4 py-3 text-right"><flux:button size="sm" variant="primary" wire:click="editSlaPolicy({{ $policy->id }})" class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">Edit</flux:button> <flux:button size="sm" variant="danger" wire:click="deleteSlaPolicy({{ $policy->id }})" wire:confirm="Delete this SLA policy?" class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">Delete</flux:button></td></tr>@empty<tr><td colspan="6" class="px-4 py-8 text-center text-zinc-500">No SLA policies yet.</td></tr>@endforelse</tbody></table></div>
<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 text-zinc-500 dark:bg-zinc-800">
<tr>
<th class="px-4 py-3">Policy</th>
<th class="px-4 py-3">Targets</th>
<th class="px-4 py-3">Applies to</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 ($slaPolicies as $policy)
<tr>
<td class="px-4 py-3 font-medium">{{ $policy->name }} @if ($policy->is_default)
<flux:badge color="blue">Default</flux:badge>
@endif
</td>
<td class="px-4 py-3">{{ $policy->first_response_minutes }}m response ·
{{ $policy->resolution_minutes }}m close</td>
<td class="px-4 py-3">{{ $policy->priority?->name ?? 'Any priority' }} ·
{{ $policy->category?->name ?? 'Any category' }}</td>
<td class="px-4 py-3 text-right">
<flux:button size="sm" variant="primary"
wire:click="editSlaPolicy({{ $policy->id }})"
class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">
Edit</flux:button>
<flux:button size="sm" variant="danger"
wire:click="deleteSlaPolicy({{ $policy->id }})"
wire:confirm="Delete this SLA policy?"
class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">Delete
</flux:button>
</td>
</tr>@empty<tr>
<td colspan="6" class="px-4 py-8 text-center text-zinc-500">No SLA policies yet.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</x-helpdesk::settings-section>
@endif
@if ($section === 'automation')
<x-helpdesk::settings-section title="Email automation rules" description="Match incoming support messages and automatically assign priority, category, team, project, agent and status.">
<x-helpdesk::settings-section title="Email automation rules"
description="Match incoming support messages and automatically assign priority, category, team, project, agent and status.">
<x-slot:form>
<form wire:submit.prevent="saveAutomationRule" class="grid gap-4 md:grid-cols-6">
<flux:input wire:model="automationForm.name" label="Name" />
<flux:input wire:model="automationForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="automationForm.sort_order" label="Sort" type="number" min="0" />
<flux:input wire:model="automationForm.sort_order" label="Sort" type="number"
min="0" />
<flux:input wire:model="automationForm.match_sender" label="Sender contains" />
<flux:input wire:model="automationForm.match_subject" label="Subject contains" />
<flux:input wire:model="automationForm.match_mailbox" label="Mailbox contains" />
<flux:select wire:model="automationForm.set_priority_id" label="Set priority"><option value="">No change</option>@foreach ($priorities as $priority)<option value="{{ $priority->id }}">{{ $priority->name }}</option>@endforeach</flux:select>
<flux:select wire:model="automationForm.set_category_id" label="Set category"><option value="">No change</option>@foreach ($categories as $category)<option value="{{ $category->id }}">{{ $category->name }}</option>@endforeach</flux:select>
<flux:select wire:model="automationForm.set_team_id" label="Set team"><option value="">No change</option>@foreach ($teams as $team)<option value="{{ $team->id }}">{{ $team->name }}</option>@endforeach</flux:select>
<flux:select wire:model="automationForm.set_project_id" label="Set project"><option value="">No change</option>@foreach ($projects as $project)<option value="{{ $project->id }}">{{ $project->title }}</option>@endforeach</flux:select>
<flux:select wire:model="automationForm.set_status_id" label="Set status"><option value="">No change</option>@foreach ($statuses as $status)<option value="{{ $status->id }}">{{ $status->name }}</option>@endforeach</flux:select>
<label class="flex items-end gap-2 pb-2 text-sm text-zinc-700 dark:text-zinc-200"><input type="checkbox" wire:model="automationForm.is_active" class="rounded border-zinc-300"> Active</label>
<div class="md:col-span-7 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save automation rule</flux:button><flux:button type="button" wire:click="resetAutomationRuleForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div>
<flux:select wire:model="automationForm.set_priority_id" label="Set priority">
<option value="">No change</option>
@foreach ($priorities as $priority)
<option value="{{ $priority->id }}">{{ $priority->name }}</option>
@endforeach
</flux:select>
<flux:select wire:model="automationForm.set_category_id" label="Set category">
<option value="">No change</option>
@foreach ($categories as $category)
<option value="{{ $category->id }}">{{ $category->name }}</option>
@endforeach
</flux:select>
<flux:select wire:model="automationForm.set_team_id" label="Set team">
<option value="">No change</option>
@foreach ($teams as $team)
<option value="{{ $team->id }}">{{ $team->name }}</option>
@endforeach
</flux:select>
<flux:select wire:model="automationForm.set_project_id" label="Set project">
<option value="">No change</option>
@foreach ($projects as $project)
<option value="{{ $project->id }}">{{ $project->title }}</option>
@endforeach
</flux:select>
<flux:select wire:model="automationForm.set_status_id" label="Set status">
<option value="">No change</option>
@foreach ($statuses as $status)
<option value="{{ $status->id }}">{{ $status->name }}</option>
@endforeach
</flux:select>
<label class="flex items-end gap-2 pb-2 text-sm text-zinc-700 dark:text-zinc-200"><input
type="checkbox" wire:model="automationForm.is_active" class="rounded border-zinc-300">
Active</label>
<div class="md:col-span-7 flex gap-2">
<flux:button type="submit" variant="primary"
class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save
automation rule</flux:button>
<flux:button type="button" wire:click="resetAutomationRuleForm" variant="ghost"
class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
</flux:button>
</div>
</form>
</x-slot:form>
<div class="grid gap-3 md:grid-cols-2">@forelse ($automationRules as $rule)<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900"><h3 class="font-semibold">{{ $rule->name }}</h3><p class="mt-1 text-sm text-zinc-500">Sender: {{ $rule->match_sender ?: 'Any' }} · Subject: {{ $rule->match_subject ?: 'Any' }}</p><div class="mt-4 flex justify-end gap-2"><flux:button size="sm" variant="primary" wire:click="editAutomationRule({{ $rule->id }})" class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">Edit</flux:button><flux:button size="sm" variant="danger" wire:click="deleteAutomationRule({{ $rule->id }})" wire:confirm="Delete this rule?" class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">Delete</flux:button></div></div>@empty<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700 md:col-span-2">No automation rules yet.</div>@endforelse</div>
<div class="grid gap-3 md:grid-cols-2">
@forelse ($automationRules as $rule)
<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
<h3 class="font-semibold">{{ $rule->name }}</h3>
<p class="mt-1 text-sm text-zinc-500">Sender: {{ $rule->match_sender ?: 'Any' }} · Subject:
{{ $rule->match_subject ?: 'Any' }}</p>
<div class="mt-4 flex justify-end gap-2">
<flux:button size="sm" variant="primary"
wire:click="editAutomationRule({{ $rule->id }})"
class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">Edit
</flux:button>
<flux:button size="sm" variant="danger"
wire:click="deleteAutomationRule({{ $rule->id }})"
wire:confirm="Delete this rule?"
class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">Delete
</flux:button>
</div>
</div>@empty<div
class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700 md:col-span-2">
No automation rules yet.</div>
@endforelse
</div>
</x-helpdesk::settings-section>
@endif
@if ($section === 'checklists')
<x-helpdesk::settings-section title="Checklist templates" description="Apply repeatable checklist items by category or project and show completion progress on tickets and Kanban cards.">
<x-slot:form><form wire:submit.prevent="saveChecklistTemplate" class="grid gap-4 md:grid-cols-6"><flux:input wire:model="checklistTemplateForm.name" label="Name" /><flux:input wire:model="checklistTemplateForm.slug" label="Slug" /><flux:select wire:model="checklistTemplateForm.category_id" label="Category"><option value="">Any category</option>@foreach ($categories as $category)<option value="{{ $category->id }}">{{ $category->name }}</option>@endforeach</flux:select><flux:select wire:model="checklistTemplateForm.project_id" label="Project"><option value="">Any project</option>@foreach ($projects as $project)<option value="{{ $project->id }}">{{ $project->title }}</option>@endforeach</flux:select><label class="flex items-end gap-2 pb-2 text-sm"><input type="checkbox" wire:model="checklistTemplateForm.is_active" class="rounded border-zinc-300"> Active</label><div class="md:col-span-7"><flux:textarea wire:model="checklistTemplateForm.items_text" label="Checklist items, one per line" rows="6" /></div><div class="md:col-span-7 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save checklist</flux:button><flux:button type="button" wire:click="resetChecklistTemplateForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div></form></x-slot:form>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">@forelse ($checklistTemplates as $template)<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900"><h3 class="font-semibold">{{ $template->name }}</h3><p class="mt-1 text-sm text-zinc-500">{{ count($template->items ?? []) }} items</p><div class="mt-4 flex justify-end gap-2"><flux:button size="sm" variant="primary" wire:click="editChecklistTemplate({{ $template->id }})" class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">Edit</flux:button><flux:button size="sm" variant="danger" wire:click="deleteChecklistTemplate({{ $template->id }})" wire:confirm="Delete this checklist?" class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">Delete</flux:button></div></div>@empty<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700 md:col-span-2 xl:col-span-3">No checklist templates yet.</div>@endforelse</div>
<x-helpdesk::settings-section title="Checklist templates"
description="Apply repeatable checklist items by category or project and show completion progress on tickets and Kanban cards.">
<x-slot:form>
<form wire:submit.prevent="saveChecklistTemplate" class="grid gap-4 md:grid-cols-6">
<flux:input wire:model="checklistTemplateForm.name" label="Name" />
<flux:input wire:model="checklistTemplateForm.slug" label="Slug" />
<flux:select wire:model="checklistTemplateForm.category_id" label="Category">
<option value="">Any category</option>
@foreach ($categories as $category)
<option value="{{ $category->id }}">{{ $category->name }}</option>
@endforeach
</flux:select>
<flux:select wire:model="checklistTemplateForm.project_id" label="Project">
<option value="">Any project</option>
@foreach ($projects as $project)
<option value="{{ $project->id }}">{{ $project->title }}</option>
@endforeach
</flux:select>
<label class="flex items-end gap-2 pb-2 text-sm"><input type="checkbox"
wire:model="checklistTemplateForm.is_active" class="rounded border-zinc-300">
Active</label>
<div class="md:col-span-7">
<flux:textarea wire:model="checklistTemplateForm.items_text"
label="Checklist items, one per line" rows="6" />
</div>
<div class="md:col-span-7 flex gap-2">
<flux:button type="submit" variant="primary"
class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save
checklist</flux:button>
<flux:button type="button" wire:click="resetChecklistTemplateForm" variant="ghost"
class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
</flux:button>
</div>
</form>
</x-slot:form>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
@forelse ($checklistTemplates as $template)
<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
<h3 class="font-semibold">{{ $template->name }}</h3>
<p class="mt-1 text-sm text-zinc-500">{{ count($template->items ?? []) }} items</p>
<div class="mt-4 flex justify-end gap-2">
<flux:button size="sm" variant="primary"
wire:click="editChecklistTemplate({{ $template->id }})"
class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">Edit
</flux:button>
<flux:button size="sm" variant="danger"
wire:click="deleteChecklistTemplate({{ $template->id }})"
wire:confirm="Delete this checklist?"
class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">Delete
</flux:button>
</div>
</div>@empty<div
class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700 md:col-span-2 xl:col-span-3">
No checklist templates yet.</div>
@endforelse
</div>
</x-helpdesk::settings-section>
@endif
@if ($section === 'tags')
<x-helpdesk::settings-section title="Ticket tags" description="Maintain visible ticket labels for search, triage and Kanban card context.">
<x-slot:form><form wire:submit.prevent="saveTag" class="grid gap-4 md:grid-cols-5"><flux:input wire:model="tagForm.name" label="Name" /><flux:input wire:model="tagForm.slug" label="Slug" /><flux:input wire:model="tagForm.color" label="Colour" /><div class="md:col-span-5 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save tag</flux:button><flux:button type="button" wire:click="resetTagForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div></form></x-slot:form>
<div class="flex flex-wrap gap-3">@forelse ($tags as $tag)<div class="rounded-2xl border border-zinc-200 bg-white px-4 py-3 dark:border-zinc-700 dark:bg-zinc-900"><span class="rounded-full px-2 py-1 text-xs font-semibold text-white" style="background-color: {{ \Modules\Helpdesk\Support\Color::css($tag->color) }}">{{ $tag->name }}</span><div class="mt-3 flex gap-2"><flux:button size="sm" variant="primary" wire:click="editTag({{ $tag->id }})" class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">Edit</flux:button><flux:button size="sm" variant="danger" wire:click="deleteTag({{ $tag->id }})" wire:confirm="Delete this tag?" class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">Delete</flux:button></div></div>@empty<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700">No tags yet.</div>@endforelse</div>
<x-helpdesk::settings-section title="Ticket tags"
description="Maintain visible ticket labels for search, triage and Kanban card context.">
<x-slot:form>
<form wire:submit.prevent="saveTag" class="grid gap-4 md:grid-cols-5">
<flux:input wire:model="tagForm.name" label="Name" />
<flux:input wire:model="tagForm.slug" label="Slug" />
<flux:input wire:model="tagForm.color" label="Colour" />
<div class="md:col-span-5 flex gap-2">
<flux:button type="submit" variant="primary"
class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save tag
</flux:button>
<flux:button type="button" wire:click="resetTagForm" variant="ghost"
class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
</flux:button>
</div>
</form>
</x-slot:form>
<div class="flex flex-wrap gap-3">
@forelse ($tags as $tag)
<div
class="rounded-2xl border border-zinc-200 bg-white px-4 py-3 dark:border-zinc-700 dark:bg-zinc-900">
<span class="rounded-full px-2 py-1 text-xs font-semibold text-white"
style="background-color: {{ \Modules\Helpdesk\Support\Color::css($tag->color) }}">{{ $tag->name }}</span>
<div class="mt-3 flex gap-2">
<flux:button size="sm" variant="primary" wire:click="editTag({{ $tag->id }})"
class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">Edit
</flux:button>
<flux:button size="sm" variant="danger" wire:click="deleteTag({{ $tag->id }})"
wire:confirm="Delete this tag?"
class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">Delete
</flux:button>
</div>
</div>@empty<div
class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700">
No tags yet.</div>
@endforelse
</div>
</x-helpdesk::settings-section>
@endif
@if ($section === 'products')
<x-helpdesk::settings-section title="Products and services" description="Link tickets to products, services, releases or builds and report support cost/time by product.">
<x-slot:form><form wire:submit.prevent="saveProduct" class="grid gap-4 md:grid-cols-6"><flux:input wire:model="productForm.name" label="Name" /><flux:input wire:model="productForm.slug" label="Slug" /><flux:input wire:model="productForm.type" label="Type" /><flux:input wire:model="productForm.version" label="Version" /><flux:input wire:model="productForm.release_channel" label="Release/build" /><label class="flex items-end gap-2 pb-2 text-sm"><input type="checkbox" wire:model="productForm.is_active" class="rounded border-zinc-300"> Active</label><div class="md:col-span-7"><flux:textarea wire:model="productForm.description" label="Description" rows="3" /></div><div class="md:col-span-7 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save product/service</flux:button><flux:button type="button" wire:click="resetProductForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div></form></x-slot:form>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">@forelse ($products as $product)<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900"><h3 class="font-semibold">{{ $product->name }}</h3><p class="mt-1 text-sm text-zinc-500">{{ ucfirst($product->type) }} · {{ $product->version ?: 'No version' }} · {{ $product->tickets_count }} tickets</p><div class="mt-4 flex justify-end gap-2"><flux:button size="sm" variant="primary" wire:click="editProduct({{ $product->id }})" class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">Edit</flux:button><flux:button size="sm" variant="danger" wire:click="deleteProduct({{ $product->id }})" wire:confirm="Delete this product/service?" class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">Delete</flux:button></div></div>@empty<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700 md:col-span-2 xl:col-span-3">No products or services yet.</div>@endforelse</div>
<x-helpdesk::settings-section title="Products and services"
description="Link tickets to products, services, releases or builds and report support cost/time by product.">
<x-slot:form>
<form wire:submit.prevent="saveProduct" class="grid gap-4 md:grid-cols-6">
<flux:input wire:model="productForm.name" label="Name" />
<flux:input wire:model="productForm.slug" label="Slug" />
<flux:input wire:model="productForm.type" label="Type" />
<flux:input wire:model="productForm.version" label="Version" />
<flux:input wire:model="productForm.release_channel" label="Release/build" /><label
class="flex items-end gap-2 pb-2 text-sm"><input type="checkbox"
wire:model="productForm.is_active" class="rounded border-zinc-300"> Active</label>
<div class="md:col-span-7">
<flux:textarea wire:model="productForm.description" label="Description" rows="3" />
</div>
<div class="md:col-span-7 flex gap-2">
<flux:button type="submit" variant="primary"
class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save
product/service</flux:button>
<flux:button type="button" wire:click="resetProductForm" variant="ghost"
class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
</flux:button>
</div>
</form>
</x-slot:form>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
@forelse ($products as $product)
<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
<h3 class="font-semibold">{{ $product->name }}</h3>
<p class="mt-1 text-sm text-zinc-500">{{ ucfirst($product->type) }} ·
{{ $product->version ?: 'No version' }} · {{ $product->tickets_count }} tickets</p>
<div class="mt-4 flex justify-end gap-2">
<flux:button size="sm" variant="primary"
wire:click="editProduct({{ $product->id }})"
class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">Edit
</flux:button>
<flux:button size="sm" variant="danger"
wire:click="deleteProduct({{ $product->id }})"
wire:confirm="Delete this product/service?"
class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">Delete
</flux:button>
</div>
</div>@empty<div
class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700 md:col-span-2 xl:col-span-3">
No products or services yet.</div>
@endforelse
</div>
</x-helpdesk::settings-section>
@endif
@@ -1,12 +1,10 @@
<div class="hd-helpdesk-surface space-y-6" dusk="helpdesk-ticket-create">
@vite([
'resources/css/app.css',
'resources/js/app.js',
])
@vite(['resources/css/app.css', 'resources/js/app.js'])
@include('helpdesk::partials.page-header', [
'eyebrow' => 'Helpdesk',
'title' => 'Create ticket',
'subtitle' => 'Raise a new support request with priority, product/service, project, category, team and full details.',
'subtitle' =>
'Raise a new support request with priority, product/service, project, category, team and full details.',
])
<form wire:submit.prevent="save" enctype="multipart/form-data" class="helpdesk-ticket-form space-y-6">
@@ -25,46 +23,35 @@
Subject <span class="helpdesk-required-star">*</span>
</label>
<input
id="subject"
type="text"
maxlength="180"
wire:model.defer="subject"
<input id="subject" type="text" maxlength="180" wire:model.defer="subject"
class="mt-2 block h-14 w-full max-w-4xl rounded-xl border border-zinc-700 bg-zinc-950 px-4 text-base font-medium text-zinc-100 shadow-sm outline-none transition-colors duration-150 placeholder:text-zinc-500 focus:border-blue-500 focus:bg-zinc-950 focus:ring-2 focus:ring-blue-500/40"
data-dusk="ticket-subject"
dusk="ticket-subject"
autocomplete="off"
/>
data-dusk="ticket-subject" dusk="ticket-subject" autocomplete="off" />
@error('subject')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
@if($companies->isNotEmpty())
@if ($companies->isNotEmpty())
<div class="xl:col-span-2">
<label class="block text-sm font-semibold text-zinc-200" for="company_id">
Company <span class="helpdesk-required-star">*</span>
</label>
<div class="mt-2" wire:ignore>
<select
id="company_id"
dusk="ticket-company"
data-helpdesk-select2
data-placeholder="Select option"
data-wire-model="company_id"
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>
<select id="company_id" dusk="ticket-company" data-helpdesk-select2
data-placeholder="Select option" data-wire-model="company_id"
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">Select company</option>
@foreach ($companies as $company)
<option value="{{ $company->id }}" @selected((string) $company_id === (string) $company->id)>{{ $company->name }}</option>
<option value="{{ $company->id }}" @selected((string) $company_id === (string) $company->id)>
{{ $company->name }}</option>
@endforeach
</select>
</div>
@error('company_id')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
@endif
@@ -75,24 +62,20 @@
</label>
<div class="mt-2" wire:ignore>
<select
id="board_id"
data-helpdesk-select2
data-placeholder="Select option"
<select id="board_id" data-helpdesk-select2 data-placeholder="Select option"
data-wire-model="board_id"
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500"
data-dusk="ticket-board"
dusk="ticket-board"
>
data-dusk="ticket-board" dusk="ticket-board">
<option value="">No board</option>
@foreach ($boards as $board)
<option value="{{ $board->id }}" @selected((int) $board_id === (int) $board->id)>{{ $board->name }}{{ $board->owner ? ' · '.$board->owner->name : '' }}</option>
<option value="{{ $board->id }}" @selected((int) $board_id === (int) $board->id)>
{{ $board->name }}{{ $board->owner ? ' · ' . $board->owner->name : '' }}</option>
@endforeach
</select>
</div>
@error('board_id')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
@@ -102,24 +85,20 @@
</label>
<div class="mt-2" wire:ignore>
<select
id="priority_id"
data-helpdesk-select2
data-placeholder="Select option"
<select id="priority_id" data-helpdesk-select2 data-placeholder="Select option"
data-wire-model="priority_id"
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500"
data-dusk="ticket-priority"
dusk="ticket-priority"
>
data-dusk="ticket-priority" dusk="ticket-priority">
<option value="">Select priority</option>
@foreach ($priorities as $priority)
<option value="{{ $priority->id }}" @selected((string) $priority_id === (string) $priority->id)>{{ $priority->name }}</option>
<option value="{{ $priority->id }}" @selected((string) $priority_id === (string) $priority->id)>{{ $priority->name }}
</option>
@endforeach
</select>
</div>
@error('priority_id')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
@@ -129,22 +108,19 @@
</label>
<div class="mt-2" wire:ignore>
<select
id="category_id"
data-helpdesk-select2
data-placeholder="Select option"
<select id="category_id" data-helpdesk-select2 data-placeholder="Select option"
data-wire-model="category_id"
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">Select category</option>
@foreach ($categories as $category)
<option value="{{ $category->id }}" @selected((string) $category_id === (string) $category->id)>{{ $category->name }}</option>
<option value="{{ $category->id }}" @selected((string) $category_id === (string) $category->id)>{{ $category->name }}
</option>
@endforeach
</select>
</div>
@error('category_id')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
@@ -154,22 +130,19 @@
</label>
<div class="mt-2" wire:ignore>
<select
id="project_id"
data-helpdesk-select2
data-placeholder="Select option"
<select id="project_id" data-helpdesk-select2 data-placeholder="Select option"
data-wire-model="project_id"
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">Select project</option>
@foreach ($projects as $project)
<option value="{{ $project->id }}" @selected((string) $project_id === (string) $project->id)>{{ $project->title ?? $project->name }}</option>
<option value="{{ $project->id }}" @selected((string) $project_id === (string) $project->id)>
{{ $project->title ?? $project->name }}</option>
@endforeach
</select>
</div>
@error('project_id')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
@@ -179,22 +152,19 @@
</label>
<div class="mt-2" wire:ignore>
<select
id="team_id"
data-helpdesk-select2
data-placeholder="Select option"
<select id="team_id" data-helpdesk-select2 data-placeholder="Select option"
data-wire-model="team_id"
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">Select team</option>
@foreach ($teams as $team)
<option value="{{ $team->id }}" @selected((string) $team_id === (string) $team->id)>{{ $team->name }}</option>
<option value="{{ $team->id }}" @selected((string) $team_id === (string) $team->id)>{{ $team->name }}
</option>
@endforeach
</select>
</div>
@error('team_id')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
@@ -212,50 +182,40 @@
<div class="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
@forelse ($tags as $tag)
<label
for="tag_{{ $tag->id }}"
class="flex cursor-pointer items-center gap-3 rounded-xl border border-zinc-700 bg-zinc-900 px-4 py-3 text-sm font-medium text-zinc-200 transition hover:border-blue-500 hover:bg-zinc-800"
>
<input
id="tag_{{ $tag->id }}"
type="checkbox"
wire:model.defer="tag_ids"
<label for="tag_{{ $tag->id }}"
class="flex cursor-pointer items-center gap-3 rounded-xl border border-zinc-700 bg-zinc-900 px-4 py-3 text-sm font-medium text-zinc-200 transition hover:border-blue-500 hover:bg-zinc-800">
<input id="tag_{{ $tag->id }}" type="checkbox" wire:model.defer="tag_ids"
value="{{ $tag->id }}"
class="h-4 w-4 rounded border-zinc-600 bg-zinc-950 text-blue-600 focus:ring-blue-500"
>
class="h-4 w-4 rounded border-zinc-600 bg-zinc-950 text-blue-600 focus:ring-blue-500">
<span class="truncate">
{{ $tag->name }}
</span>
</label>
@empty
<div class="rounded-xl border border-dashed border-zinc-700 bg-zinc-900 px-4 py-3 text-sm text-zinc-400">
<div
class="rounded-xl border border-dashed border-zinc-700 bg-zinc-900 px-4 py-3 text-sm text-zinc-400">
No tags are available.
</div>
@endforelse
</div>
@error('tag_ids')
<p class="mt-3 text-sm text-red-500">{{ $message }}</p>
<p class="mt-3 text-sm text-red-500">{{ $message }}</p>
@enderror
@error('tag_ids.*')
<p class="mt-3 text-sm text-red-500">{{ $message }}</p>
<p class="mt-3 text-sm text-red-500">{{ $message }}</p>
@enderror
</fieldset>
</div>
<div class="xl:col-span-2">
<x-helpdesk::date-picker
id="due_at"
model="due_at"
label="Due date"
placeholder="Select due date"
value="{{ $due_at }}"
/>
<x-helpdesk::date-picker id="due_at" model="due_at" label="Due date"
placeholder="Select due date" value="{{ $due_at }}" />
@error('due_at')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
@@ -264,56 +224,60 @@
Details <span class="helpdesk-required-star">*</span>
</label>
<div class="mt-2 overflow-hidden rounded-xl border border-zinc-700 bg-zinc-950 shadow-sm focus-within:border-blue-500 focus-within:ring-2 focus-within:ring-blue-500/40" wire:ignore>
<div class="mt-2 overflow-hidden rounded-xl border border-zinc-700 bg-zinc-950 shadow-sm focus-within:border-blue-500 focus-within:ring-2 focus-within:ring-blue-500/40"
wire:ignore>
<div class="flex flex-wrap items-center gap-1 border-b border-zinc-700 bg-zinc-900 px-3 py-2">
<button type="button" class="helpdesk-editor-button" data-helpdesk-editor-command="bold" aria-label="Bold">
<button type="button" class="helpdesk-editor-button" data-helpdesk-editor-command="bold"
aria-label="Bold">
<i class="fa-solid fa-bold" aria-hidden="true"></i>
</button>
<button type="button" class="helpdesk-editor-button" data-helpdesk-editor-command="italic" aria-label="Italic">
<button type="button" class="helpdesk-editor-button"
data-helpdesk-editor-command="italic" aria-label="Italic">
<i class="fa-solid fa-italic" aria-hidden="true"></i>
</button>
<button type="button" class="helpdesk-editor-button" data-helpdesk-editor-command="underline" aria-label="Underline">
<button type="button" class="helpdesk-editor-button"
data-helpdesk-editor-command="underline" aria-label="Underline">
<i class="fa-solid fa-underline" aria-hidden="true"></i>
</button>
<span class="mx-1 h-6 w-px bg-zinc-700" aria-hidden="true"></span>
<button type="button" class="helpdesk-editor-button" data-helpdesk-editor-command="insertUnorderedList" aria-label="Bullet list">
<button type="button" class="helpdesk-editor-button"
data-helpdesk-editor-command="insertUnorderedList" aria-label="Bullet list">
<i class="fa-solid fa-list-ul" aria-hidden="true"></i>
</button>
<button type="button" class="helpdesk-editor-button" data-helpdesk-editor-command="insertOrderedList" aria-label="Numbered list">
<button type="button" class="helpdesk-editor-button"
data-helpdesk-editor-command="insertOrderedList" aria-label="Numbered list">
<i class="fa-solid fa-list-ol" aria-hidden="true"></i>
</button>
<span class="mx-1 h-6 w-px bg-zinc-700" aria-hidden="true"></span>
<button type="button" class="helpdesk-editor-button" data-helpdesk-editor-format="pre" aria-label="Code block">
<button type="button" class="helpdesk-editor-button" data-helpdesk-editor-format="pre"
aria-label="Code block">
<i class="fa-solid fa-code" aria-hidden="true"></i>
</button>
<button type="button" class="helpdesk-editor-button" data-helpdesk-editor-link aria-label="Insert link">
<button type="button" class="helpdesk-editor-button" data-helpdesk-editor-link
aria-label="Insert link">
<i class="fa-solid fa-link" aria-hidden="true"></i>
</button>
</div>
<div
id="body-editor"
<div id="body-editor"
class="min-h-[18rem] w-full px-4 py-3 text-sm leading-6 text-zinc-100 outline-none empty:before:text-zinc-500 empty:before:content-[attr(data-placeholder)]"
contenteditable="true"
data-helpdesk-details-editor
contenteditable="true" data-helpdesk-details-editor
data-placeholder="Describe the issue, expected outcome, steps to reproduce and any useful context..."
data-dusk="ticket-body-editor"
dusk="ticket-body-editor"
>{!! $body ?? '' !!}</div>
data-dusk="ticket-body-editor" dusk="ticket-body-editor">{!! $body ?? '' !!}</div>
</div>
<textarea id="body" wire:model.defer="body" class="sr-only" tabindex="-1" aria-hidden="true"></textarea>
@error('body')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
@@ -330,63 +294,46 @@
</p>
</div>
<label
for="attachments"
class="inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-zinc-950"
>
<label for="attachments"
class="inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-zinc-950">
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
<span>Add attachments</span>
</label>
</div>
<input
id="attachments"
type="file"
wire:model="attachments"
multiple
class="sr-only"
>
<input id="attachments" type="file" wire:model="attachments" multiple class="sr-only">
<div
wire:loading.flex
wire:target="attachments"
class="mt-4 items-center gap-2 rounded-xl border border-blue-500/30 bg-blue-500/10 px-4 py-3 text-sm font-medium text-blue-200"
>
<div wire:loading.flex wire:target="attachments"
class="mt-4 items-center gap-2 rounded-xl border border-blue-500/30 bg-blue-500/10 px-4 py-3 text-sm font-medium text-blue-200">
<i class="fa-solid fa-circle-notch fa-spin" aria-hidden="true"></i>
Uploading attachments...
</div>
@error('attachments')
<p class="mt-3 text-sm text-red-500">{{ $message }}</p>
<p class="mt-3 text-sm text-red-500">{{ $message }}</p>
@enderror
@error('attachments.*')
<p class="mt-3 text-sm text-red-500">{{ $message }}</p>
<p class="mt-3 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
</div>
</div>
<div class="flex items-center justify-end gap-3 border-t border-zinc-700 bg-zinc-900 px-6 py-4">
<button
type="button"
wire:click="clearForm"
class="rounded-xl bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-700"
>
<button type="button" wire:click="clearForm"
class="rounded-xl bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-700">
Clear
</button>
<a href="{{ route('helpdesk.tickets.index') }}" wire:navigate class="rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">
<a href="{{ route('helpdesk.tickets.index') }}" wire:navigate
class="rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">
Cancel
</a>
<button
type="submit"
<button type="submit"
class="inline-flex items-center gap-2 rounded-xl bg-green-600 px-5 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-50"
data-dusk="ticket-create-submit"
dusk="ticket-create-submit"
wire:loading.attr="disabled"
>
data-dusk="ticket-create-submit" dusk="ticket-create-submit" wire:loading.attr="disabled">
<i class="fa-regular fa-floppy-disk" aria-hidden="true"></i>
<span wire:loading.remove>Create Ticket</span>
<span wire:loading>Creating...</span>
@@ -414,7 +361,7 @@
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('.helpdesk-ticket-form');
const editor = document.querySelector('[data-helpdesk-details-editor]');
const bodyField = document.getElementById('body');
@@ -423,30 +370,34 @@
return;
}
const syncBody = function () {
const syncBody = function() {
bodyField.value = editor.innerHTML.trim();
bodyField.dispatchEvent(new Event('input', { bubbles: true }));
bodyField.dispatchEvent(new Event('change', { bubbles: true }));
bodyField.dispatchEvent(new Event('input', {
bubbles: true
}));
bodyField.dispatchEvent(new Event('change', {
bubbles: true
}));
};
document.querySelectorAll('[data-helpdesk-editor-command]').forEach(function (button) {
button.addEventListener('click', function () {
document.querySelectorAll('[data-helpdesk-editor-command]').forEach(function(button) {
button.addEventListener('click', function() {
editor.focus();
document.execCommand(button.dataset.helpdeskEditorCommand, false, null);
syncBody();
});
});
document.querySelectorAll('[data-helpdesk-editor-format]').forEach(function (button) {
button.addEventListener('click', function () {
document.querySelectorAll('[data-helpdesk-editor-format]').forEach(function(button) {
button.addEventListener('click', function() {
editor.focus();
document.execCommand('formatBlock', false, button.dataset.helpdeskEditorFormat);
syncBody();
});
});
document.querySelectorAll('[data-helpdesk-editor-link]').forEach(function (button) {
button.addEventListener('click', function () {
document.querySelectorAll('[data-helpdesk-editor-link]').forEach(function(button) {
button.addEventListener('click', function() {
const url = window.prompt('Enter link URL');
if (!url) {
@@ -1,12 +1,10 @@
<div class="space-y-6">
@vite([
'resources/css/app.css',
'resources/js/app.js',
])
@vite(['resources/css/app.css', 'resources/js/app.js'])
@include('helpdesk::partials.page-header', [
'eyebrow' => 'Helpdesk',
'title' => 'Edit Ticket',
'subtitle' => 'Update ownership, assignment, workflow status, product/service, project, team, category, priority and ticket details.',
'subtitle' =>
'Update ownership, assignment, workflow status, product/service, project, team, category, priority and ticket details.',
])
<form wire:submit.prevent="save" class="space-y-6">
@@ -19,7 +17,8 @@
{{ $ticket->reference }} · Ticket controls
</h2>
<span class="inline-flex items-center gap-2 rounded-xl border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm font-medium text-zinc-200">
<span
class="inline-flex items-center gap-2 rounded-xl border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm font-medium text-zinc-200">
<i class="fa-regular fa-user text-zinc-400" aria-hidden="true"></i>
Owner:
<span class="font-semibold text-white">{{ $ticket->owner?->name ?? 'Unknown' }}</span>
@@ -32,7 +31,8 @@
</p>
</div>
<a href="{{ route('helpdesk.tickets.show', $ticket) }}" wire:navigate class="inline-flex items-center justify-center rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">
<a href="{{ route('helpdesk.tickets.show', $ticket) }}" wire:navigate
class="inline-flex items-center justify-center rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">
Back to ticket
</a>
</div>
@@ -44,44 +44,35 @@
Subject <span class="helpdesk-required-star">*</span>
</label>
<input
id="subject"
type="text"
maxlength="180"
wire:model.defer="subject"
<input id="subject" type="text" maxlength="180" wire:model.defer="subject"
class="mt-2 block h-14 w-full max-w-4xl rounded-xl border border-zinc-700 bg-zinc-950 px-4 text-base font-medium text-zinc-100 shadow-sm outline-none transition-colors duration-150 placeholder:text-zinc-500 focus:border-blue-500 focus:bg-zinc-950 focus:ring-2 focus:ring-blue-500/40"
data-dusk="ticket-edit-subject"
autocomplete="off"
/>
data-dusk="ticket-edit-subject" autocomplete="off" />
@error('subject')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
@if($companies->isNotEmpty())
@if ($companies->isNotEmpty())
<div class="xl:col-span-2">
<label class="block text-sm font-semibold text-zinc-200" for="company_id">
Company <span class="helpdesk-required-star">*</span>
</label>
<div class="mt-2" wire:ignore>
<select
id="company_id"
data-helpdesk-select2
data-placeholder="Select option"
<select id="company_id" data-helpdesk-select2 data-placeholder="Select option"
data-wire-model="company_id"
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">Select company</option>
@foreach ($companies as $company)
<option value="{{ $company->id }}" @selected((string) $company_id === (string) $company->id)>{{ $company->name }}</option>
<option value="{{ $company->id }}" @selected((string) $company_id === (string) $company->id)>
{{ $company->name }}</option>
@endforeach
</select>
</div>
@error('company_id')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
@endif
@@ -90,23 +81,20 @@
<label class="block text-sm font-semibold text-zinc-200" for="board_id">Board</label>
<div class="mt-2" wire:ignore>
<select
id="board_id"
data-helpdesk-select2
data-placeholder="Select option"
<select id="board_id" data-helpdesk-select2 data-placeholder="Select option"
data-wire-model="board_id"
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500"
data-dusk="ticket-edit-board"
>
data-dusk="ticket-edit-board">
<option value="">No board</option>
@foreach ($boards as $board)
<option value="{{ $board->id }}" @selected((string) $board_id === (string) $board->id)>{{ $board->name }}{{ $board->owner ? ' · '.$board->owner->name : '' }}</option>
<option value="{{ $board->id }}" @selected((string) $board_id === (string) $board->id)>
{{ $board->name }}{{ $board->owner ? ' · ' . $board->owner->name : '' }}</option>
@endforeach
</select>
</div>
@error('board_id')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
@@ -116,23 +104,20 @@
</label>
<div class="mt-2" wire:ignore>
<select
id="status_id"
data-helpdesk-select2
data-placeholder="Select option"
<select id="status_id" data-helpdesk-select2 data-placeholder="Select option"
data-wire-model="status_id"
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500"
data-dusk="ticket-edit-status"
>
data-dusk="ticket-edit-status">
<option value="">No status</option>
@foreach ($statuses as $status)
<option value="{{ $status->id }}" @selected((string) $status_id === (string) $status->id)>{{ $status->name }}</option>
<option value="{{ $status->id }}" @selected((string) $status_id === (string) $status->id)>{{ $status->name }}
</option>
@endforeach
</select>
</div>
@error('status_id')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
@@ -142,23 +127,20 @@
</label>
<div class="mt-2" wire:ignore>
<select
id="priority_id"
data-helpdesk-select2
data-placeholder="Select option"
<select id="priority_id" data-helpdesk-select2 data-placeholder="Select option"
data-wire-model="priority_id"
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500"
data-dusk="ticket-edit-priority"
>
data-dusk="ticket-edit-priority">
<option value="">Select priority</option>
@foreach ($priorities as $priority)
<option value="{{ $priority->id }}" @selected((string) $priority_id === (string) $priority->id)>{{ $priority->name }}</option>
<option value="{{ $priority->id }}" @selected((string) $priority_id === (string) $priority->id)>{{ $priority->name }}
</option>
@endforeach
</select>
</div>
@error('priority_id')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
@@ -166,15 +148,13 @@
<label class="block text-sm font-semibold text-zinc-200" for="agent_id">Assigned agent</label>
<div class="mt-2">
<select
id="agent_id"
wire:model.live="agent_id"
<select id="agent_id" wire:model.live="agent_id"
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500"
data-dusk="ticket-edit-agent"
>
data-dusk="ticket-edit-agent">
<option value="">Unassigned</option>
@forelse ($agents as $agent)
<option value="{{ $agent->id }}">{{ $agent->name }} · {{ $agent->email }}</option>
<option value="{{ $agent->id }}">{{ $agent->name }} · {{ $agent->email }}
</option>
@empty
<option value="" disabled>No users available for assignment</option>
@endforelse
@@ -182,7 +162,7 @@
</div>
@error('agent_id')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
@@ -192,22 +172,19 @@
</label>
<div class="mt-2" wire:ignore>
<select
id="category_id"
data-helpdesk-select2
data-placeholder="Select option"
<select id="category_id" data-helpdesk-select2 data-placeholder="Select option"
data-wire-model="category_id"
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">No category</option>
@foreach ($categories as $category)
<option value="{{ $category->id }}" @selected((string) $category_id === (string) $category->id)>{{ $category->name }}</option>
<option value="{{ $category->id }}" @selected((string) $category_id === (string) $category->id)>
{{ $category->name }}</option>
@endforeach
</select>
</div>
@error('category_id')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
@@ -217,22 +194,19 @@
</label>
<div class="mt-2" wire:ignore>
<select
id="project_id"
data-helpdesk-select2
data-placeholder="Select option"
<select id="project_id" data-helpdesk-select2 data-placeholder="Select option"
data-wire-model="project_id"
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">No project</option>
@foreach ($projects as $project)
<option value="{{ $project->id }}" @selected((string) $project_id === (string) $project->id)>{{ $project->title ?? $project->name }}</option>
<option value="{{ $project->id }}" @selected((string) $project_id === (string) $project->id)>
{{ $project->title ?? $project->name }}</option>
@endforeach
</select>
</div>
@error('project_id')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
@@ -242,22 +216,19 @@
</label>
<div class="mt-2" wire:ignore>
<select
id="team_id"
data-helpdesk-select2
data-placeholder="Select option"
<select id="team_id" data-helpdesk-select2 data-placeholder="Select option"
data-wire-model="team_id"
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">No team</option>
@foreach ($teams as $team)
<option value="{{ $team->id }}" @selected((string) $team_id === (string) $team->id)>{{ $team->name }}</option>
<option value="{{ $team->id }}" @selected((string) $team_id === (string) $team->id)>{{ $team->name }}
</option>
@endforeach
</select>
</div>
@error('team_id')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
@@ -275,68 +246,63 @@
<div class="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
@forelse ($tags as $tag)
<label
for="tag_{{ $tag->id }}"
class="flex cursor-pointer items-center gap-3 rounded-xl border border-zinc-700 bg-zinc-900 px-4 py-3 text-sm font-medium text-zinc-200 transition hover:border-blue-500 hover:bg-zinc-800"
>
<input
id="tag_{{ $tag->id }}"
type="checkbox"
wire:model.defer="tag_ids"
<label for="tag_{{ $tag->id }}"
class="flex cursor-pointer items-center gap-3 rounded-xl border border-zinc-700 bg-zinc-900 px-4 py-3 text-sm font-medium text-zinc-200 transition hover:border-blue-500 hover:bg-zinc-800">
<input id="tag_{{ $tag->id }}" type="checkbox" wire:model.defer="tag_ids"
value="{{ $tag->id }}"
class="h-4 w-4 rounded border-zinc-600 bg-zinc-950 text-blue-600 focus:ring-blue-500"
>
class="h-4 w-4 rounded border-zinc-600 bg-zinc-950 text-blue-600 focus:ring-blue-500">
<span class="truncate">
{{ $tag->name }}
</span>
</label>
@empty
<div class="rounded-xl border border-dashed border-zinc-700 bg-zinc-900 px-4 py-3 text-sm text-zinc-400">
<div
class="rounded-xl border border-dashed border-zinc-700 bg-zinc-900 px-4 py-3 text-sm text-zinc-400">
No tags are available.
</div>
@endforelse
</div>
@error('tag_ids')
<p class="mt-3 text-sm text-red-500">{{ $message }}</p>
<p class="mt-3 text-sm text-red-500">{{ $message }}</p>
@enderror
@error('tag_ids.*')
<p class="mt-3 text-sm text-red-500">{{ $message }}</p>
<p class="mt-3 text-sm text-red-500">{{ $message }}</p>
@enderror
</fieldset>
</div>
<div class="xl:col-span-2">
<x-helpdesk::date-picker
id="due_at"
model="due_at"
label="Due date"
placeholder="Select due date"
value="{{ $due_at }}"
/>
<x-helpdesk::date-picker id="due_at" model="due_at" label="Due date"
placeholder="Select due date" value="{{ $due_at }}" />
@error('due_at')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
<div class="lg:col-span-3 xl:col-span-6">
<x-helpdesk::html-editor dusk="ticket-edit-body-editor" wire:model="body" label="Details *" rows="10" placeholder="Update the ticket details..." />
<x-helpdesk::html-editor dusk="ticket-edit-body-editor" wire:model="body" label="Details *"
rows="10" placeholder="Update the ticket details..." />
@error('body')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
<div class="lg:col-span-3 xl:col-span-6">
<div class="overflow-hidden rounded-2xl border border-blue-500/30 bg-zinc-950/70 shadow-sm shadow-blue-950/30">
<div
class="overflow-hidden rounded-2xl border border-blue-500/30 bg-zinc-950/70 shadow-sm shadow-blue-950/30">
<div class="bg-gradient-to-r from-blue-700 via-sky-600 to-emerald-600 px-5 py-4 text-white">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<label class="flex items-center gap-2 text-base font-extrabold text-white drop-shadow-sm" for="attachments">
<span class="inline-flex h-9 w-9 items-center justify-center rounded-xl bg-white/15 ring-1 ring-white/25">
<label
class="flex items-center gap-2 text-base font-extrabold text-white drop-shadow-sm"
for="attachments">
<span
class="inline-flex h-9 w-9 items-center justify-center rounded-xl bg-white/15 ring-1 ring-white/25">
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
</span>
Attachments
@@ -347,10 +313,8 @@
</p>
</div>
<label
for="attachments"
class="inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl bg-white px-4 py-2.5 text-sm font-bold text-blue-700 shadow-sm transition hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-white/80 focus:ring-offset-2 focus:ring-offset-blue-700"
>
<label for="attachments"
class="inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl bg-white px-4 py-2.5 text-sm font-bold text-blue-700 shadow-sm transition hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-white/80 focus:ring-offset-2 focus:ring-offset-blue-700">
<i class="fa-solid fa-plus" aria-hidden="true"></i>
<span>Add attachments</span>
</label>
@@ -359,100 +323,90 @@
<div class="p-4">
<input
id="attachments"
type="file"
wire:model="attachments"
multiple
class="sr-only"
>
<input id="attachments" type="file" wire:model="attachments" multiple
class="sr-only">
<div
wire:loading.flex
wire:target="attachments"
class="mt-4 items-center gap-2 rounded-xl border border-blue-500/30 bg-blue-500/10 px-4 py-3 text-sm font-medium text-blue-200"
>
<i class="fa-solid fa-circle-notch fa-spin" aria-hidden="true"></i>
Uploading attachments...
</div>
@if ($ticket->attachments->isNotEmpty())
<div class="mt-4 space-y-3">
<div class="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-emerald-500/30 bg-emerald-500/10 px-4 py-3">
<h3 class="inline-flex items-center gap-2 text-sm font-extrabold text-emerald-200">
<span class="inline-flex h-7 w-7 items-center justify-center rounded-lg bg-emerald-500 text-white shadow-sm">
<i class="fa-solid fa-folder-open" aria-hidden="true"></i>
</span>
Existing attachments
</h3>
<span class="inline-flex items-center gap-2 rounded-full bg-emerald-100 px-3 py-1 text-xs font-extrabold text-emerald-800 shadow-sm ring-1 ring-emerald-300/70">
<span class="inline-flex h-2 w-2 rounded-full bg-emerald-500"></span>
{{ $ticket->attachments->count() }} file(s)
</span>
<div wire:loading.flex wire:target="attachments"
class="mt-4 items-center gap-2 rounded-xl border border-blue-500/30 bg-blue-500/10 px-4 py-3 text-sm font-medium text-blue-200">
<i class="fa-solid fa-circle-notch fa-spin" aria-hidden="true"></i>
Uploading attachments...
</div>
@foreach ($ticket->attachments as $attachment)
<div class="flex flex-col gap-3 rounded-xl border border-zinc-700 bg-zinc-900 p-3 text-sm sm:flex-row sm:items-center sm:justify-between">
<a
href="{{ route('helpdesk.attachments.download', $attachment->id) }}"
target="_blank"
class="min-w-0 font-medium text-blue-300 hover:text-blue-200"
>
<i class="fa-regular fa-file mr-2" aria-hidden="true"></i>
{{ $attachment->original_name ?? $attachment->original_filename ?? $attachment->filename ?? $attachment->file_name ?? basename($attachment->path ?? $attachment->file_path ?? '') }}
</a>
@if ($ticket->attachments->isNotEmpty())
<div class="mt-4 space-y-3">
<div
class="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-emerald-500/30 bg-emerald-500/10 px-4 py-3">
<h3
class="inline-flex items-center gap-2 text-sm font-extrabold text-emerald-200">
<span
class="inline-flex h-7 w-7 items-center justify-center rounded-lg bg-emerald-500 text-white shadow-sm">
<i class="fa-solid fa-folder-open" aria-hidden="true"></i>
</span>
Existing attachments
</h3>
<button
type="button"
wire:click="removeAttachment({{ $attachment->id }})"
wire:confirm="Remove this attachment?"
class="inline-flex items-center justify-center rounded-lg bg-red-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-red-700"
>
Remove
</button>
<span
class="inline-flex items-center gap-2 rounded-full bg-emerald-100 px-3 py-1 text-xs font-extrabold text-emerald-800 shadow-sm ring-1 ring-emerald-300/70">
<span class="inline-flex h-2 w-2 rounded-full bg-emerald-500"></span>
{{ $ticket->attachments->count() }} file(s)
</span>
</div>
@foreach ($ticket->attachments as $attachment)
<div
class="flex flex-col gap-3 rounded-xl border border-zinc-700 bg-zinc-900 p-3 text-sm sm:flex-row sm:items-center sm:justify-between">
<a href="{{ route('helpdesk.attachments.download', $attachment->id) }}"
target="_blank"
class="min-w-0 font-medium text-blue-300 hover:text-blue-200">
<i class="fa-regular fa-file mr-2" aria-hidden="true"></i>
{{ $attachment->original_name ?? ($attachment->original_filename ?? ($attachment->filename ?? ($attachment->file_name ?? basename($attachment->path ?? ($attachment->file_path ?? ''))))) }}
</a>
<button type="button"
wire:click="removeAttachment({{ $attachment->id }})"
wire:confirm="Remove this attachment?"
class="inline-flex items-center justify-center rounded-lg bg-red-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-red-700">
Remove
</button>
</div>
@endforeach
</div>
@endforeach
</div>
@endif
@endif
@if ($attachments !== [])
<div class="mt-4 rounded-xl border border-blue-500/30 bg-blue-500/10 px-4 py-3 text-sm text-blue-100">
{{ count($attachments) }} new attachment(s) selected. Save the ticket to attach them.
</div>
@endif
@if ($attachments !== [])
<div
class="mt-4 rounded-xl border border-blue-500/30 bg-blue-500/10 px-4 py-3 text-sm text-blue-100">
{{ count($attachments) }} new attachment(s) selected. Save the ticket to attach
them.
</div>
@endif
@error('attachments')
<p class="mt-3 text-sm text-red-500">{{ $message }}</p>
@enderror
@error('attachments')
<p class="mt-3 text-sm text-red-500">{{ $message }}</p>
@enderror
@error('attachments.*')
<p class="mt-3 text-sm text-red-500">{{ $message }}</p>
@enderror
@error('attachments.*')
<p class="mt-3 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
</div>
</div>
</div>
<div class="flex items-center justify-end gap-3 border-t border-zinc-700 bg-zinc-900 px-6 py-4">
<button
type="button"
wire:click="clearForm"
class="rounded-xl bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-700"
>
<button type="button" wire:click="clearForm"
class="rounded-xl bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-700">
Clear
</button>
<a href="{{ route('helpdesk.tickets.show', $ticket) }}" wire:navigate class="rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">
<a href="{{ route('helpdesk.tickets.show', $ticket) }}" wire:navigate
class="rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">
Cancel
</a>
<button
type="submit"
<button type="submit"
class="inline-flex items-center gap-2 rounded-xl bg-green-600 px-5 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-50"
data-dusk="ticket-edit-submit"
wire:loading.attr="disabled"
>
data-dusk="ticket-edit-submit" wire:loading.attr="disabled">
<i class="fa-regular fa-floppy-disk" aria-hidden="true"></i>
<span wire:loading.remove>Save Ticket</span>
<span wire:loading>Saving...</span>
@@ -1,12 +1,13 @@
<div class="hd-helpdesk-surface space-y-6" dusk="helpdesk-ticket-index">
<div class="overflow-hidden rounded-3xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<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>
<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">
<a href="{{ route('helpdesk.index') }}" wire:navigate class="hd-btn hd-btn-blue">
@@ -24,7 +25,8 @@
<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" />
<flux:input wire:model.live.debounce.350ms="search" icon="magnifying-glass"
placeholder="Search reference, subject or body" />
<flux:select wire:model.live="scope">
<option value="open">Open tickets</option>
@@ -48,14 +50,17 @@
</div>
</div>
<div class="overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div wire:loading.flex class="items-center gap-2 border-b border-zinc-200 bg-blue-50 px-4 py-3 text-sm text-blue-800 dark:border-zinc-700 dark:bg-blue-950 dark:text-blue-200">
<div
class="overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div wire:loading.flex
class="items-center gap-2 border-b border-zinc-200 bg-blue-50 px-4 py-3 text-sm text-blue-800 dark:border-zinc-700 dark:bg-blue-950 dark:text-blue-200">
<flux:icon.arrow-path class="size-4 animate-spin" /> Refreshing tickets...
</div>
<div class="overflow-x-auto">
<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">
<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">Reference</th>
<th class="px-4 py-3">Subject</th>
@@ -71,43 +76,59 @@
@forelse ($tickets as $ticket)
<tr class="hover:bg-zinc-50 dark:hover:bg-zinc-800/70">
<td class="whitespace-nowrap px-4 py-3 font-mono text-xs">
<a href="{{ route('helpdesk.tickets.show', $ticket) }}" wire:navigate class="font-semibold text-blue-700 hover:underline dark:text-blue-300">
<a href="{{ route('helpdesk.tickets.show', $ticket) }}" wire:navigate
class="font-semibold text-blue-700 hover:underline dark:text-blue-300">
{{ $ticket->reference }}
</a>
</td>
<td class="px-4 py-3">
<div class="font-medium text-zinc-900 dark:text-zinc-100">{{ $ticket->subject }}</div>
<div class="text-xs text-zinc-500">{{ $ticket->category?->name ?? 'No category' }} @if($ticket->project) · {{ $ticket->project->title }} @endif @if($ticket->team) · {{ $ticket->team->name }} @endif</div>
<div class="text-xs text-zinc-500">{{ $ticket->category?->name ?? 'No category' }}
@if ($ticket->project)
· {{ $ticket->project->title }}
@endif @if ($ticket->team)
· {{ $ticket->team->name }}
@endif
</div>
</td>
<td class="px-4 py-3"><flux:badge color="{{ $ticket->status->color }}">{{ $ticket->status->name }}</flux:badge></td>
<td class="px-4 py-3"><flux:badge color="{{ $ticket->priority->color }}">{{ $ticket->priority->name }}</flux:badge></td>
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">{{ $ticket->agent?->name ?? 'Unassigned' }}</td>
<td class="px-4 py-3">
<flux:badge color="{{ $ticket->status->color }}">{{ $ticket->status->name }}
</flux:badge>
</td>
<td class="px-4 py-3">
<flux:badge color="{{ $ticket->priority->color }}">{{ $ticket->priority->name }}
</flux:badge>
</td>
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">
{{ $ticket->agent?->name ?? 'Unassigned' }}</td>
<td class="whitespace-nowrap px-4 py-3 text-center">
@if (($ticket->attachments_count ?? 0) > 0)
<span class="inline-flex items-center justify-center gap-1 rounded-full border border-blue-200 bg-blue-50 px-2.5 py-1 text-xs font-black text-blue-700 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-200" title="{{ $ticket->attachments_count }} attachment{{ $ticket->attachments_count === 1 ? '' : 's' }}">
<span
class="inline-flex items-center justify-center gap-1 rounded-full border border-blue-200 bg-blue-50 px-2.5 py-1 text-xs font-black text-blue-700 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-200"
title="{{ $ticket->attachments_count }} attachment{{ $ticket->attachments_count === 1 ? '' : 's' }}">
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
<span>{{ $ticket->attachments_count }}</span>
<span class="sr-only">attachment{{ $ticket->attachments_count === 1 ? '' : 's' }}</span>
<span
class="sr-only">attachment{{ $ticket->attachments_count === 1 ? '' : 's' }}</span>
</span>
@else
<span class="inline-flex items-center justify-center rounded-full border border-zinc-200 bg-zinc-50 px-2.5 py-1 text-xs font-bold text-zinc-400 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-500" title="No attachments">
<span
class="inline-flex items-center justify-center rounded-full border border-zinc-200 bg-zinc-50 px-2.5 py-1 text-xs font-bold text-zinc-400 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-500"
title="No attachments">
<i class="fa-regular fa-circle" aria-hidden="true"></i>
<span class="sr-only">No attachments</span>
</span>
@endif
</td>
<td class="whitespace-nowrap px-4 py-3 text-zinc-500">{{ $ticket->last_activity_at?->diffForHumans() ?? $ticket->created_at->diffForHumans() }}</td>
<td class="whitespace-nowrap px-4 py-3 text-zinc-500">
{{ $ticket->last_activity_at?->diffForHumans() ?? $ticket->created_at->diffForHumans() }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-right">
@can('delete', $ticket)
<button
type="button"
wire:click="askToDeleteTicket({{ $ticket->id }})"
wire:loading.attr="disabled"
wire:target="askToDeleteTicket({{ $ticket->id }})"
<button type="button" wire:click="askToDeleteTicket({{ $ticket->id }})"
wire:loading.attr="disabled" wire:target="askToDeleteTicket({{ $ticket->id }})"
class="hd-btn hd-btn-sm hd-btn-red disabled:cursor-not-allowed disabled:opacity-60"
title="Delete {{ $ticket->reference }}"
dusk="delete-ticket-{{ $ticket->id }}"
>
title="Delete {{ $ticket->reference }}" dusk="delete-ticket-{{ $ticket->id }}">
<i class="fa-solid fa-trash" aria-hidden="true"></i>
<span>Delete</span>
</button>
@@ -116,7 +137,8 @@
</tr>
@empty
<tr>
<td colspan="8" class="px-4 py-12 text-center text-zinc-500">No tickets matched your filters.</td>
<td colspan="8" class="px-4 py-12 text-center text-zinc-500">No tickets matched your
filters.</td>
</tr>
@endforelse
</tbody>
@@ -129,18 +151,16 @@
</div>
@if ($deleteModalOpen)
<div
class="fixed inset-0 z-[9998] flex items-center justify-center bg-black/75 px-4 py-6 backdrop-blur-sm"
role="dialog"
aria-modal="true"
aria-labelledby="delete-ticket-modal-title"
<div class="fixed inset-0 z-[9998] flex items-center justify-center bg-black/75 px-4 py-6 backdrop-blur-sm"
role="dialog" aria-modal="true" aria-labelledby="delete-ticket-modal-title"
wire:key="ticket-delete-modal-{{ $pendingDeleteTicketId ?? 'none' }}"
dusk="delete-ticket-confirmation-modal"
>
<div class="w-full max-w-lg overflow-hidden rounded-3xl border border-red-500/30 bg-zinc-950 text-zinc-100 shadow-2xl">
dusk="delete-ticket-confirmation-modal">
<div
class="w-full max-w-lg overflow-hidden rounded-3xl border border-red-500/30 bg-zinc-950 text-zinc-100 shadow-2xl">
<div class="border-b border-red-500/20 bg-red-950/40 px-6 py-5">
<div class="flex items-start gap-4">
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-red-600 text-white shadow-lg shadow-red-950/40">
<span
class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-red-600 text-white shadow-lg shadow-red-950/40">
<i class="fa-solid fa-triangle-exclamation" aria-hidden="true"></i>
</span>
@@ -150,7 +170,9 @@
</h2>
<p class="mt-1 text-sm leading-6 text-red-100/90">
This will delete <span class="font-mono font-bold">{{ $pendingDeleteTicketReference }}</span>. This action cannot be undone.
This will delete <span
class="font-mono font-bold">{{ $pendingDeleteTicketReference }}</span>. This
action cannot be undone.
</p>
</div>
</div>
@@ -169,25 +191,18 @@
</p>
</div>
<div class="flex flex-col-reverse gap-3 border-t border-zinc-800 bg-zinc-900/80 px-6 py-4 sm:flex-row sm:justify-end">
<button
type="button"
wire:click="cancelTicketDelete"
class="hd-btn hd-btn-sm hd-btn-blue"
dusk="cancel-delete-ticket"
>
<div
class="flex flex-col-reverse gap-3 border-t border-zinc-800 bg-zinc-900/80 px-6 py-4 sm:flex-row sm:justify-end">
<button type="button" wire:click="cancelTicketDelete" class="hd-btn hd-btn-sm hd-btn-blue"
dusk="cancel-delete-ticket">
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
<span>Cancel</span>
</button>
<button
type="button"
wire:click="confirmTicketDelete"
wire:loading.attr="disabled"
<button type="button" wire:click="confirmTicketDelete" wire:loading.attr="disabled"
wire:target="confirmTicketDelete"
class="hd-btn hd-btn-sm hd-btn-red disabled:cursor-not-allowed disabled:opacity-60"
dusk="confirm-delete-ticket"
>
dusk="confirm-delete-ticket">
<i class="fa-solid fa-trash" aria-hidden="true"></i>
<span wire:loading.remove wire:target="confirmTicketDelete">Delete ticket</span>
<span wire:loading wire:target="confirmTicketDelete">Deleting...</span>
File diff suppressed because it is too large Load Diff
@@ -4,14 +4,24 @@
@foreach ($comments as $comment)
@php
$commentUser = $comment->relationLoaded('user') ? $comment->getRelation('user') : null;
$commentAttachments = $comment->relationLoaded('attachments') ? $comment->getRelation('attachments') : collect();
$commentAttachments = $comment->relationLoaded('attachments')
? $comment->getRelation('attachments')
: collect();
$commentReplies = $comment->relationLoaded('replies') ? $comment->getRelation('replies') : collect();
$replyFormId = 'helpdesk-reply-form-'.$comment->id;
$currentTicket = $ticket ?? $comment->ticket ?? null;
$replyFormId = 'helpdesk-reply-form-' . $comment->id;
$currentTicket = $ticket ?? ($comment->ticket ?? null);
$replyCount = $commentReplies->count();
$isInternal = (bool) $comment->is_internal;
$borderClass = $isInternal ? 'border-purple-400/70 dark:border-purple-600/70' : 'border-zinc-200 dark:border-zinc-700';
$railClass = $isInternal ? 'bg-purple-500' : ((int) $level % 3 === 0 ? 'bg-red-500' : (((int) $level % 3 === 1) ? 'bg-blue-500' : 'bg-emerald-500'));
$borderClass = $isInternal
? 'border-purple-400/70 dark:border-purple-600/70'
: 'border-zinc-200 dark:border-zinc-700';
$railClass = $isInternal
? 'bg-purple-500'
: ((int) $level % 3 === 0
? 'bg-red-500'
: ((int) $level % 3 === 1
? 'bg-blue-500'
: 'bg-emerald-500'));
$commentSubject = trim((string) ($comment->subject ?? ''));
if ($commentSubject === '') {
@@ -19,12 +29,16 @@
$commentSubject = Str::limit($plainSubject !== '' ? $plainSubject : 'Response', 72);
}
if ($isInternal && ! Str::startsWith(Str::lower($commentSubject), 'internal')) {
if ($isInternal && !Str::startsWith(Str::lower($commentSubject), 'internal')) {
$commentSubject = 'Internal note';
}
$renderedBody = html_entity_decode((string) $comment->body, ENT_QUOTES | ENT_HTML5, 'UTF-8');
while (str_contains($renderedBody, '&lt;') || str_contains($renderedBody, '&gt;') || str_contains($renderedBody, '&amp;lt;')) {
while (
str_contains($renderedBody, '&lt;') ||
str_contains($renderedBody, '&gt;') ||
str_contains($renderedBody, '&amp;lt;')
) {
$decodedBody = html_entity_decode($renderedBody, ENT_QUOTES | ENT_HTML5, 'UTF-8');
if ($decodedBody === $renderedBody) {
@@ -36,17 +50,21 @@
if ($currentTicket !== null) {
$renderedBody = strtr($renderedBody, [
'{{ ticket.reference }}' => (string) e((string) ($currentTicket->reference ?? '')),
'{{ ticket.subject }}' => (string) e((string) ($currentTicket->subject ?? '')),
'{{ ticket.status }}' => (string) e((string) ($currentTicket->status?->name ?? '')),
'{{ ticket.priority }}' => (string) e((string) ($currentTicket->priority?->name ?? '')),
'{{ ticket.category }}' => (string) e((string) ($currentTicket->category?->name ?? '')),
'{{ ticket.project }}' => (string) e((string) ($currentTicket->project?->title ?? '')),
'{{ ticket.team }}' => (string) e((string) ($currentTicket->team?->name ?? '')),
'{{ requester.name }}' => (string) e((string) ($currentTicket->owner?->name ?? $currentTicket->user?->name ?? '')),
'{{ agent.name }}' => (string) e((string) ($currentTicket->agent?->name ?? '')),
'{{ actor.name }}' => (string) e((string) ($commentUser?->name ?? '')),
'{{ ticket.url }}' => (string) e(route('helpdesk.tickets.show', ['ticket' => $currentTicket->getKey()])),
'{{ ticket . reference }}' => (string) e((string) ($currentTicket->reference ?? '')),
'{{ ticket . subject }}' => (string) e((string) ($currentTicket->subject ?? '')),
'{{ ticket . status }}' => (string) e((string) ($currentTicket->status?->name ?? '')),
'{{ ticket . priority }}' => (string) e((string) ($currentTicket->priority?->name ?? '')),
'{{ ticket . category }}' => (string) e((string) ($currentTicket->category?->name ?? '')),
'{{ ticket . project }}' => (string) e((string) ($currentTicket->project?->title ?? '')),
'{{ ticket . team }}' => (string) e((string) ($currentTicket->team?->name ?? '')),
'{{ requester . name }}' => (string) e(
(string) ($currentTicket->owner?->name ?? ($currentTicket->user?->name ?? '')),
),
'{{ agent . name }}' => (string) e((string) ($currentTicket->agent?->name ?? '')),
'{{ actor . name }}' => (string) e((string) ($commentUser?->name ?? '')),
'{{ ticket . url }}' => (string) e(
route('helpdesk.tickets.show', ['ticket' => $currentTicket->getKey()]),
),
]);
}
@@ -54,34 +72,50 @@
$renderedBody = preg_replace('/<\s*(script|style)[^>]*>.*?<\s*\/\s*\1\s*>/is', '', $renderedBody) ?: '';
$renderedBody = preg_replace('/\s+on[a-z]+\s*=\s*("[^"]*"|\'[^\']*\'|[^\s>]+)/i', '', $renderedBody) ?: '';
$renderedBody = preg_replace('/javascript\s*:/i', '', $renderedBody) ?: '';
$renderedBody = strip_tags($renderedBody, '<p><br><strong><b><em><i><u><ul><ol><li><blockquote><pre><code><a>');
$renderedBody = strip_tags(
$renderedBody,
'<p><br><strong><b><em><i><u><ul><ol><li><blockquote><pre><code><a>',
);
$renderedBody = $containsHtml ? $renderedBody : nl2br(e($renderedBody));
@endphp
@if (! $isInternal || $canSeeInternalNotes)
<details class="group rounded-2xl border {{ $borderClass }} bg-white shadow-sm transition dark:bg-zinc-900" data-helpdesk-comment-card data-helpdesk-comment-text="{{ e(Str::lower($commentSubject.' '.strip_tags((string) $comment->body).' '.($comment->body_text ?? '').' '.($commentUser?->name ?? '').' '.($comment->unid ?? '').' '.($comment->guid ?? ''))) }}" style="margin-left: {{ min((int) $level, 4) * 1.25 }}rem">
@if (!$isInternal || $canSeeInternalNotes)
<details class="group rounded-2xl border {{ $borderClass }} bg-white shadow-sm transition dark:bg-zinc-900"
data-helpdesk-comment-card
data-helpdesk-comment-text="{{ e(Str::lower($commentSubject . ' ' . strip_tags((string) $comment->body) . ' ' . ($comment->body_text ?? '') . ' ' . ($commentUser?->name ?? '') . ' ' . ($comment->unid ?? '') . ' ' . ($comment->guid ?? ''))) }}"
style="margin-left: {{ min((int) $level, 4) * 1.25 }}rem">
<summary class="flex cursor-pointer list-none items-start gap-3 rounded-2xl p-4 marker:hidden">
<span class="mt-1 inline-flex h-12 w-2 shrink-0 rounded-full {{ $railClass }}" aria-hidden="true"></span>
<span class="mt-1 inline-flex h-12 w-2 shrink-0 rounded-full {{ $railClass }}"
aria-hidden="true"></span>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-zinc-100 text-zinc-700 ring-1 ring-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:ring-zinc-700">
<i class="fa-solid {{ $isInternal ? 'fa-note-sticky' : 'fa-envelope' }}" aria-hidden="true"></i>
<span
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-zinc-100 text-zinc-700 ring-1 ring-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:ring-zinc-700">
<i class="fa-solid {{ $isInternal ? 'fa-note-sticky' : 'fa-envelope' }}"
aria-hidden="true"></i>
</span>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="truncate text-base font-extrabold text-zinc-900 dark:text-zinc-100">{{ $commentSubject }}</span>
<span class="rounded-full bg-zinc-100 px-2.5 py-1 text-[11px] font-extrabold text-zinc-700 dark:bg-zinc-800 dark:text-zinc-100">{{ $replyCount }} {{ $replyCount === 1 ? 'reply' : 'replies' }}</span>
<span
class="truncate text-base font-extrabold text-zinc-900 dark:text-zinc-100">{{ $commentSubject }}</span>
<span
class="rounded-full bg-zinc-100 px-2.5 py-1 text-[11px] font-extrabold text-zinc-700 dark:bg-zinc-800 dark:text-zinc-100">{{ $replyCount }}
{{ $replyCount === 1 ? 'reply' : 'replies' }}</span>
@if ($commentAttachments->isNotEmpty())
<span class="rounded-full bg-emerald-100 px-2.5 py-1 text-[11px] font-extrabold text-emerald-800 dark:bg-emerald-950 dark:text-emerald-100">
<i class="fa-solid fa-paperclip mr-1" aria-hidden="true"></i>{{ $commentAttachments->count() }}
<span
class="rounded-full bg-emerald-100 px-2.5 py-1 text-[11px] font-extrabold text-emerald-800 dark:bg-emerald-950 dark:text-emerald-100">
<i class="fa-solid fa-paperclip mr-1"
aria-hidden="true"></i>{{ $commentAttachments->count() }}
</span>
@endif
@if ($isInternal)
<span class="rounded-full bg-red-600 px-2.5 py-1 text-[11px] font-extrabold uppercase tracking-wide text-white">Internal</span>
<span
class="rounded-full bg-red-600 px-2.5 py-1 text-[11px] font-extrabold uppercase tracking-wide text-white">Internal</span>
@endif
</div>
<div class="mt-1 flex flex-wrap items-center gap-2 text-xs font-semibold text-zinc-500 dark:text-zinc-400">
<div
class="mt-1 flex flex-wrap items-center gap-2 text-xs font-semibold text-zinc-500 dark:text-zinc-400">
<span>{{ $commentUser?->name ?? 'Unknown user' }}</span>
<span aria-hidden="true">•</span>
<span>{{ $comment->created_at?->format('d M Y H:i') }}</span>
@@ -90,7 +124,8 @@
</div>
</div>
<span class="mt-1 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-zinc-100 text-zinc-600 transition group-open:rotate-90 dark:bg-zinc-800 dark:text-zinc-200">
<span
class="mt-1 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-zinc-100 text-zinc-600 transition group-open:rotate-90 dark:bg-zinc-800 dark:text-zinc-200">
<i class="fa-solid fa-chevron-right text-xs" aria-hidden="true"></i>
</span>
</summary>
@@ -101,16 +136,20 @@
</div>
@if ($commentAttachments->isNotEmpty())
<div class="mt-4 rounded-xl border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-950/60">
<div class="mb-2 flex items-center gap-2 text-xs font-extrabold uppercase tracking-wide text-zinc-500">
<div
class="mt-4 rounded-xl border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-950/60">
<div
class="mb-2 flex items-center gap-2 text-xs font-extrabold uppercase tracking-wide text-zinc-500">
<i class="fa-solid fa-paperclip text-blue-500" aria-hidden="true"></i>
Attachments
</div>
<div class="flex flex-wrap gap-2">
@foreach ($commentAttachments as $attachment)
<a href="{{ route('helpdesk.attachments.download', $attachment->id) }}" target="_blank" class="inline-flex items-center gap-2 rounded-lg border border-blue-200 bg-white px-3 py-2 text-xs font-bold text-blue-700 shadow-sm hover:bg-blue-50 dark:border-blue-900 dark:bg-zinc-900 dark:text-blue-200 dark:hover:bg-blue-950/50">
<a href="{{ route('helpdesk.attachments.download', $attachment->id) }}"
target="_blank"
class="inline-flex items-center gap-2 rounded-lg border border-blue-200 bg-white px-3 py-2 text-xs font-bold text-blue-700 shadow-sm hover:bg-blue-50 dark:border-blue-900 dark:bg-zinc-900 dark:text-blue-200 dark:hover:bg-blue-950/50">
<i class="fa-solid fa-thumbtack" aria-hidden="true"></i>
{{ $attachment->original_name ?? $attachment->original_filename ?? $attachment->filename ?? $attachment->file_name ?? basename($attachment->path ?? $attachment->file_path ?? '') }}
{{ $attachment->original_name ?? ($attachment->original_filename ?? ($attachment->filename ?? ($attachment->file_name ?? basename($attachment->path ?? ($attachment->file_path ?? ''))))) }}
</a>
@endforeach
</div>
@@ -119,16 +158,21 @@
@if ($currentTicket !== null)
<div class="mt-4 flex flex-wrap justify-end gap-2">
<button type="button" dusk="reply-to-comment-{{ $comment->id }}" data-helpdesk-reply-toggle="{{ $replyFormId }}" class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-3 py-2 text-xs font-bold text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400/70">
<button type="button" dusk="reply-to-comment-{{ $comment->id }}"
data-helpdesk-reply-toggle="{{ $replyFormId }}"
class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-3 py-2 text-xs font-bold text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400/70">
<i class="fa-solid fa-reply mr-2" aria-hidden="true"></i>
Respond
</button>
@if (auth()->user()?->isMpbaPlatformAdmin())
<form method="POST" action="{{ route('helpdesk.tickets.comments.destroy', [$currentTicket, $comment]) }}" onsubmit="return confirm('Delete this response? This will remove it from the visible conversation but keep the replication identity/audit trail.');">
<form method="POST"
action="{{ route('helpdesk.tickets.comments.destroy', [$currentTicket, $comment]) }}"
onsubmit="return confirm('Delete this response? This will remove it from the visible conversation but keep the replication identity/audit trail.');">
@csrf
@method('DELETE')
<button type="submit" dusk="delete-comment-{{ $comment->id }}" class="inline-flex items-center justify-center rounded-lg bg-red-600 px-3 py-2 text-xs font-bold text-white shadow-sm transition hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-400/70">
<button type="submit" dusk="delete-comment-{{ $comment->id }}"
class="inline-flex items-center justify-center rounded-lg bg-red-600 px-3 py-2 text-xs font-bold text-white shadow-sm transition hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-400/70">
<i class="fa-solid fa-trash-can mr-2" aria-hidden="true"></i>
Delete
</button>
@@ -136,63 +180,98 @@
@endif
</div>
<form id="{{ $replyFormId }}" method="POST" action="{{ route('helpdesk.tickets.comments.store', $currentTicket) }}" enctype="multipart/form-data" class="mt-4 hidden rounded-xl border border-red-200 bg-red-50/60 p-4 dark:border-red-900 dark:bg-red-950/20" data-helpdesk-comment-form>
<form id="{{ $replyFormId }}" method="POST"
action="{{ route('helpdesk.tickets.comments.store', $currentTicket) }}"
enctype="multipart/form-data"
class="mt-4 hidden rounded-xl border border-red-200 bg-red-50/60 p-4 dark:border-red-900 dark:bg-red-950/20"
data-helpdesk-comment-form>
@csrf
<input type="hidden" name="parent_id" value="{{ $comment->id }}">
<div class="mb-3">
<label for="reply-subject-{{ $comment->id }}" class="mb-2 block text-sm font-bold text-zinc-700 dark:text-zinc-200">Subject</label>
<input id="reply-subject-{{ $comment->id }}" name="subject" value="{{ old('subject', 'RE: '.$commentSubject) }}" class="block w-full rounded-xl border border-zinc-300 bg-white p-3 text-sm font-semibold text-zinc-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100">
<label for="reply-subject-{{ $comment->id }}"
class="mb-2 block text-sm font-bold text-zinc-700 dark:text-zinc-200">Subject</label>
<input id="reply-subject-{{ $comment->id }}" name="subject"
value="{{ old('subject', 'RE: ' . $commentSubject) }}"
class="block w-full rounded-xl border border-zinc-300 bg-white p-3 text-sm font-semibold text-zinc-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100">
</div>
<label for="reply-body-{{ $comment->id }}" class="mb-2 block text-sm font-bold text-zinc-700 dark:text-zinc-200">Response to response</label>
<div class="mb-2 flex flex-wrap gap-2" data-helpdesk-html-toolbar="reply-body-{{ $comment->id }}">
<button type="button" data-helpdesk-tag="strong" class="rounded-lg border px-2 py-1 text-xs font-black">B</button>
<button type="button" data-helpdesk-tag="em" class="rounded-lg border px-2 py-1 text-xs italic">I</button>
<button type="button" data-helpdesk-tag="ul" class="rounded-lg border px-2 py-1 text-xs">List</button>
<button type="button" data-helpdesk-wrap="<blockquote>" data-helpdesk-wrap-end="</blockquote>" class="rounded-lg border px-2 py-1 text-xs">Quote</button>
<button type="button" data-helpdesk-wrap="<code>" data-helpdesk-wrap-end="</code>" class="rounded-lg border px-2 py-1 text-xs">Code</button>
<label for="reply-body-{{ $comment->id }}"
class="mb-2 block text-sm font-bold text-zinc-700 dark:text-zinc-200">Response to
response</label>
<div class="mb-2 flex flex-wrap gap-2"
data-helpdesk-html-toolbar="reply-body-{{ $comment->id }}">
<button type="button" data-helpdesk-tag="strong"
class="rounded-lg border px-2 py-1 text-xs font-black">B</button>
<button type="button" data-helpdesk-tag="em"
class="rounded-lg border px-2 py-1 text-xs italic">I</button>
<button type="button" data-helpdesk-tag="ul"
class="rounded-lg border px-2 py-1 text-xs">List</button>
<button type="button" data-helpdesk-wrap="<blockquote>"
data-helpdesk-wrap-end="</blockquote>"
class="rounded-lg border px-2 py-1 text-xs">Quote</button>
<button type="button" data-helpdesk-wrap="<code>" data-helpdesk-wrap-end="</code>"
class="rounded-lg border px-2 py-1 text-xs">Code</button>
</div>
<textarea id="reply-body-{{ $comment->id }}" dusk="reply-body-{{ $comment->id }}" name="commentBody" rows="4" required class="block w-full rounded-xl border border-zinc-300 bg-white p-3 text-sm text-zinc-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100" placeholder="Write a threaded Lotus Notes-style response..."></textarea>
<textarea id="reply-body-{{ $comment->id }}" dusk="reply-body-{{ $comment->id }}" name="commentBody" rows="4"
required
class="block w-full rounded-xl border border-zinc-300 bg-white p-3 text-sm text-zinc-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
placeholder="Write a threaded Lotus Notes-style response..."></textarea>
@can('update', $currentTicket)
@if ($canSeeInternalNotes)
<label class="mt-3 flex items-center gap-2 text-sm font-semibold text-zinc-700 dark:text-zinc-200">
<input type="checkbox" name="internal" value="1" class="rounded border-zinc-300">
<label
class="mt-3 flex items-center gap-2 text-sm font-semibold text-zinc-700 dark:text-zinc-200">
<input type="checkbox" name="internal" value="1"
class="rounded border-zinc-300">
MPBA internal note only
</label>
@endif
@endcan
<details class="mt-4 overflow-hidden rounded-xl border border-zinc-200 bg-white/80 shadow-sm dark:border-zinc-800 dark:bg-zinc-950/50">
<summary class="group flex cursor-pointer list-none items-center justify-between gap-3 bg-gradient-to-r from-slate-950 via-blue-950 to-slate-900 px-4 py-3 text-white [&::-webkit-details-marker]:hidden">
<details
class="mt-4 overflow-hidden rounded-xl border border-zinc-200 bg-white/80 shadow-sm dark:border-zinc-800 dark:bg-zinc-950/50">
<summary
class="group flex cursor-pointer list-none items-center justify-between gap-3 bg-gradient-to-r from-slate-950 via-blue-950 to-slate-900 px-4 py-3 text-white [&::-webkit-details-marker]:hidden">
<span class="flex min-w-0 items-center gap-3">
<span class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border border-blue-400/30 bg-blue-500/15 text-blue-200">
<span
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border border-blue-400/30 bg-blue-500/15 text-blue-200">
<i class="fa-solid fa-paperclip text-xs" aria-hidden="true"></i>
</span>
<span class="min-w-0">
<span class="block text-[10px] font-black uppercase tracking-[0.22em] text-blue-200">Attachments</span>
<span class="block truncate text-xs font-semibold text-zinc-300">Pin response files.</span>
<span
class="block text-[10px] font-black uppercase tracking-[0.22em] text-blue-200">Attachments</span>
<span class="block truncate text-xs font-semibold text-zinc-300">Pin
response files.</span>
</span>
</span>
<span class="flex shrink-0 items-center gap-2">
<span class="rounded-full border border-white/10 bg-white/10 px-2.5 py-1 text-[10px] font-black text-white" data-helpdesk-file-count>0 files</span>
<i class="fa-solid fa-chevron-down text-[10px] text-blue-200 transition group-open:rotate-180" aria-hidden="true"></i>
<span
class="rounded-full border border-white/10 bg-white/10 px-2.5 py-1 text-[10px] font-black text-white"
data-helpdesk-file-count>0 files</span>
<i class="fa-solid fa-chevron-down text-[10px] text-blue-200 transition group-open:rotate-180"
aria-hidden="true"></i>
</span>
</summary>
<div class="p-4">
<div class="flex min-h-[4.5rem] flex-col gap-3 rounded-xl border border-zinc-200 bg-zinc-50/80 p-4 dark:border-zinc-800 dark:bg-zinc-950/70 sm:flex-row sm:items-center sm:justify-between">
<span class="text-xs font-semibold text-zinc-500 dark:text-zinc-400" data-helpdesk-file-label>No files attached</span>
<div
class="flex min-h-[4.5rem] flex-col gap-3 rounded-xl border border-zinc-200 bg-zinc-50/80 p-4 dark:border-zinc-800 dark:bg-zinc-950/70 sm:flex-row sm:items-center sm:justify-between">
<span class="text-xs font-semibold text-zinc-500 dark:text-zinc-400"
data-helpdesk-file-label>No files attached</span>
<label data-helpdesk-file-button>
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
<span>Attach files</span>
<input name="attachments[]" type="file" multiple class="sr-only" data-helpdesk-file-input>
<input name="attachments[]" type="file" multiple class="sr-only"
data-helpdesk-file-input>
</label>
</div>
</div>
</details>
<div class="mt-4 flex justify-end gap-2">
<button type="button" data-helpdesk-reply-toggle="{{ $replyFormId }}" class="inline-flex items-center justify-center rounded-lg bg-slate-600 px-3 py-2 text-xs font-bold text-white shadow-sm transition hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-400/70">Cancel</button>
<button type="submit" dusk="post-reply-{{ $comment->id }}" class="inline-flex items-center justify-center rounded-lg bg-emerald-600 px-3 py-2 text-xs font-bold text-white shadow-sm transition hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400/70">Post response</button>
<button type="button" data-helpdesk-reply-toggle="{{ $replyFormId }}"
class="inline-flex items-center justify-center rounded-lg bg-slate-600 px-3 py-2 text-xs font-bold text-white shadow-sm transition hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-400/70">Cancel</button>
<button type="submit" dusk="post-reply-{{ $comment->id }}"
class="inline-flex items-center justify-center rounded-lg bg-emerald-600 px-3 py-2 text-xs font-bold text-white shadow-sm transition hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400/70">Post
response</button>
</div>
</form>
@endif
@@ -10,7 +10,7 @@
{{ $title ?? 'Helpdesk' }}
</h1>
@if(! empty($subtitle))
@if (!empty($subtitle))
<p class="mt-2 max-w-3xl text-sm text-slate-300">
{{ $subtitle }}
</p>
@@ -7,8 +7,9 @@
}
@endphp
@if($helpdeskEnabled)
<a href="{{ route('helpdesk.index') }}" wire:navigate class="wms-sidebar-link @if(request()->routeIs('helpdesk.*')) is-active @endif" title="{{ __('Support') }}">
@if ($helpdeskEnabled)
<a href="{{ route('helpdesk.index') }}" wire:navigate
class="wms-sidebar-link @if (request()->routeIs('helpdesk.*')) is-active @endif" title="{{ __('Support') }}">
<i class="fa-solid fa-headset wms-sidebar-icon" aria-hidden="true"></i>
<span data-wms-sidebar-label>{{ __('Support') }}</span>
</a>
@@ -6,31 +6,21 @@
</label>
<div class="grid grid-cols-[1fr_auto] gap-2">
<input
id="time_minutes"
name="minutes"
type="number"
min="1"
max="1440"
value="{{ old('minutes') }}"
<input id="time_minutes" name="minutes" type="number" min="1" max="1440" value="{{ old('minutes') }}"
placeholder="Minutes"
class="block w-full rounded-lg border border-zinc-300 bg-white p-2 text-xs font-semibold text-zinc-900 shadow-sm focus:border-emerald-500 focus:ring-emerald-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
data-dusk="ticket-time-minutes"
>
data-dusk="ticket-time-minutes">
<button type="submit" dusk="log-ticket-time" data-dusk="log-ticket-time" class="inline-flex items-center justify-center rounded-lg bg-emerald-600 px-3 py-2 text-xs font-black text-white shadow-sm transition hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400/70">
<button type="submit" dusk="log-ticket-time" data-dusk="log-ticket-time"
class="inline-flex items-center justify-center rounded-lg bg-emerald-600 px-3 py-2 text-xs font-black text-white shadow-sm transition hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400/70">
<i class="fa-solid fa-clock mr-2" aria-hidden="true"></i>
<span>Log</span>
</button>
</div>
<textarea
name="description"
rows="2"
placeholder="Optional work note"
<textarea name="description" rows="2" placeholder="Optional work note"
class="block w-full rounded-lg border border-zinc-300 bg-white p-2 text-xs font-semibold text-zinc-900 shadow-sm focus:border-emerald-500 focus:ring-emerald-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
data-dusk="ticket-time-description"
>{{ old('description') }}</textarea>
data-dusk="ticket-time-description">{{ old('description') }}</textarea>
@error('minutes')
<p class="text-xs font-bold text-red-600 dark:text-red-400">{{ $message }}</p>