phase 6 added helpdesk
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
<div class="space-y-5" dusk="helpdesk-dashboard-widget">
|
||||
<span class="sr-only">Total tickets Open tickets Closed tickets</span>
|
||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div class="rounded-2xl border border-blue-400/20 bg-gradient-to-br from-slate-950 to-blue-950/40 p-4 shadow-sm ring-1 ring-white/5">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-xs font-black uppercase tracking-[0.18em] text-blue-200/80">Total tickets</div>
|
||||
<div class="mt-2 text-3xl font-black text-white">{{ $totalCount }}</div>
|
||||
</div>
|
||||
<div class="flex h-11 w-11 items-center justify-center rounded-xl bg-blue-500/20 text-blue-200 ring-1 ring-blue-300/20">
|
||||
<i class="fa-solid fa-ticket" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 h-1.5 rounded-full bg-slate-800">
|
||||
<div class="h-1.5 rounded-full bg-blue-400" style="width: 100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-blue-400/20 bg-gradient-to-br from-slate-950 to-blue-950/40 p-4 shadow-sm ring-1 ring-white/5">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-xs font-black uppercase tracking-[0.18em] text-blue-200/80">Open tickets</div>
|
||||
<div class="mt-2 text-3xl font-black text-blue-200">{{ $openCount }}</div>
|
||||
</div>
|
||||
<div class="flex h-11 w-11 items-center justify-center rounded-xl bg-blue-500/20 text-blue-200 ring-1 ring-blue-300/20">
|
||||
<i class="fa-solid fa-inbox" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 h-1.5 rounded-full bg-slate-800">
|
||||
<div class="h-1.5 rounded-full bg-blue-400" style="width: {{ $openPercent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-emerald-400/20 bg-gradient-to-br from-slate-950 to-emerald-950/30 p-4 shadow-sm ring-1 ring-white/5">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-xs font-black uppercase tracking-[0.18em] text-emerald-200/80">Closed tickets</div>
|
||||
<div class="mt-2 text-3xl font-black text-emerald-200">{{ $closedCount }}</div>
|
||||
</div>
|
||||
<div class="flex h-11 w-11 items-center justify-center rounded-xl bg-emerald-500/20 text-emerald-200 ring-1 ring-emerald-300/20">
|
||||
<i class="fa-solid fa-circle-check" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 h-1.5 rounded-full bg-slate-800">
|
||||
<div class="h-1.5 rounded-full bg-emerald-400" style="width: {{ $closedPercent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-rose-400/20 bg-gradient-to-br from-slate-950 to-rose-950/25 p-4 shadow-sm ring-1 ring-white/5">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-xs font-black uppercase tracking-[0.18em] text-rose-200/80">High priority</div>
|
||||
<div class="mt-2 text-3xl font-black text-rose-200">{{ $criticalCount }}</div>
|
||||
</div>
|
||||
<div class="flex h-11 w-11 items-center justify-center rounded-xl bg-rose-500/20 text-rose-200 ring-1 ring-rose-300/20">
|
||||
<i class="fa-solid fa-triangle-exclamation" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 text-xs font-semibold text-slate-400">Open urgent/high/critical workload.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-5 xl:grid-cols-[minmax(0,2fr)_minmax(22rem,1fr)]">
|
||||
<div class="rounded-2xl border border-zinc-700 bg-gradient-to-br from-slate-950 via-slate-950 to-blue-950/20 shadow-sm ring-1 ring-white/5">
|
||||
<div class="flex items-center justify-between border-b border-white/10 px-5 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="flex h-10 w-10 items-center justify-center rounded-xl bg-blue-500/15 text-blue-200 ring-1 ring-blue-300/20">
|
||||
<i class="fa-solid fa-list-check" aria-hidden="true"></i>
|
||||
</span>
|
||||
<div>
|
||||
<h2 class="text-lg font-black text-white">Recent tickets</h2>
|
||||
<p class="text-sm text-slate-400">Latest support activity.</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('helpdesk.tickets.index') }}" wire:navigate class="hd-btn hd-btn-sm hd-btn-blue">View all tickets</a>
|
||||
</div>
|
||||
{{-- Recent Tickets --}}
|
||||
<div class="divide-y divide-white/10">
|
||||
@forelse ($recentTickets as $ticket)
|
||||
<a href="{{ route('helpdesk.tickets.show', $ticket) }}" wire:navigate
|
||||
class="flex flex-nowrap items-center gap-4 px-5 py-4 transition hover:bg-blue-500/10">
|
||||
<div class="flex w-36 shrink-0 items-center gap-3 text-sm font-semibold text-slate-300">
|
||||
<span class="h-2 w-2 shrink-0 rounded-full bg-blue-400"></span>
|
||||
<span class="whitespace-nowrap">{{ $ticket->reference }}</span>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate font-black text-white">{{ $ticket->subject }}</div>
|
||||
<div class="mt-1 truncate text-xs text-slate-400">
|
||||
{{ optional($ticket->last_activity_at)->format('d M Y H:i') ?? 'No activity yet' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-28 shrink-0">
|
||||
<x-helpdesk::colour-badge :color="$ticket->status?->color ?? 'zinc'">
|
||||
{{ $ticket->status?->name ?? 'No status' }}
|
||||
</x-helpdesk::colour-badge>
|
||||
</div>
|
||||
|
||||
<div class="w-28 shrink-0">
|
||||
<x-helpdesk::colour-badge :color="$ticket->priority?->color ?? 'zinc'">
|
||||
{{ $ticket->priority?->name ?? 'No priority' }}
|
||||
</x-helpdesk::colour-badge>
|
||||
</div>
|
||||
|
||||
<div class="w-28 shrink-0 whitespace-nowrap text-right text-sm font-semibold text-slate-400">
|
||||
{{ optional($ticket->last_activity_at)->diffForHumans() ?? 'No activity' }}
|
||||
</div>
|
||||
</a>
|
||||
@empty
|
||||
<div class="px-5 py-8 text-center text-sm text-slate-400">No helpdesk tickets yet.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
{{-- Recent Tickets End--}}
|
||||
</div>
|
||||
|
||||
<div class="space-y-5">
|
||||
<div class="rounded-2xl border border-zinc-700 bg-gradient-to-br from-slate-950 to-slate-900 p-5 shadow-sm ring-1 ring-white/5">
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-black text-white">SLA overview</h2>
|
||||
<p class="text-sm text-slate-400">Closure rate and open workload.</p>
|
||||
</div>
|
||||
<span class="rounded-xl bg-emerald-500/15 px-3 py-1.5 text-sm font-black text-emerald-200 ring-1 ring-emerald-300/20">{{ $closedPercent }}% closed</span>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-5 sm:grid-cols-[9rem_minmax(0,1fr)] sm:items-center">
|
||||
<div class="relative mx-auto flex h-32 w-32 items-center justify-center rounded-full bg-slate-900 ring-8 ring-emerald-400/20">
|
||||
<div class="absolute inset-2 rounded-full border-8 border-emerald-400" style="clip-path: polygon(0 0, {{ max($closedPercent, 8) }}% 0, {{ max($closedPercent, 8) }}% 100%, 0 100%);"></div>
|
||||
<div class="relative text-center">
|
||||
<div class="text-3xl font-black text-white">{{ $closedPercent }}%</div>
|
||||
<div class="text-xs font-bold text-slate-400">closed</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="mb-2 flex justify-between text-sm font-semibold text-slate-300"><span>Closed</span><span>{{ $closedPercent }}%</span></div>
|
||||
<div class="h-2 rounded-full bg-slate-800"><div class="h-2 rounded-full bg-emerald-400" style="width: {{ $closedPercent }}%"></div></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-2 flex justify-between text-sm font-semibold text-slate-300"><span>Open</span><span>{{ $openPercent }}%</span></div>
|
||||
<div class="h-2 rounded-full bg-slate-800"><div class="h-2 rounded-full bg-blue-400" style="width: {{ $openPercent }}%"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details
|
||||
class="group rounded-xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900"
|
||||
dusk="recent-commits-card">
|
||||
<summary
|
||||
class="-mx-4 -mt-4 mb-4 flex cursor-pointer list-none items-center justify-between gap-3 rounded-t-xl bg-violet-700 px-4 py-4 text-white marker:hidden">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<span
|
||||
class="inline-flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-violet-950 text-white ring-1 ring-violet-300/60 shadow-lg">
|
||||
<i class="fa-solid fa-code-branch text-lg" aria-hidden="true"></i>
|
||||
</span>
|
||||
|
||||
<div class="min-w-0">
|
||||
<h2 class="text-lg font-black uppercase tracking-wide">Recent commits</h2>
|
||||
<p class="text-xs font-semibold text-violet-100">Latest linked Git activity</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-white/15 text-white ring-1 ring-white/20">
|
||||
<i class="fa-solid fa-chevron-right text-xs transition group-open:rotate-90" aria-hidden="true"></i>
|
||||
</span>
|
||||
</summary>
|
||||
|
||||
<div class="mb-4 flex justify-end">
|
||||
<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-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-950">
|
||||
<div class="font-mono text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{{ $commit->short_hash }} · {{ $commit->repository?->name }}
|
||||
</div>
|
||||
<div class="mt-1 truncate text-sm font-semibold 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 dark:text-zinc-400">
|
||||
No commits tracked yet.
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,349 @@
|
||||
@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">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="{{ route('helpdesk.index') }}" wire:navigate class="hd-btn hd-btn-blue">
|
||||
<i class="fa-solid fa-gauge-high" aria-hidden="true"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
|
||||
<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.settings') }}" wire:navigate class="hd-btn hd-btn-amber">
|
||||
<i class="fa-solid fa-gear" aria-hidden="true"></i>
|
||||
<span>Configure boards</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('helpdesk.tickets.index') }}" wire:navigate class="hd-btn hd-btn-blue">
|
||||
<i class="fa-solid fa-list" aria-hidden="true"></i>
|
||||
<span>List view</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
No Kanban boards exist yet. Open Helpdesk Settings and create a board with at least one status column.
|
||||
</div>
|
||||
@else
|
||||
<div
|
||||
class="flex flex-col gap-4 rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900 md:flex-row md:items-center md:justify-between">
|
||||
<div class="min-w-0">
|
||||
<h2 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">Active board: {{ $board?->name ?? 'None' }}</h2>
|
||||
<p class="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{{ $board?->description ?: 'Use settings to configure which statuses appear as draggable columns.' }}
|
||||
</p>
|
||||
@if ($board?->owner)
|
||||
<p class="mt-1 text-xs font-semibold text-emerald-600 dark:text-emerald-300">Owner:
|
||||
{{ $board->owner->name }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="hd-select-wrap shrink-0 md:w-72">
|
||||
<select
|
||||
class="hd-select"
|
||||
aria-label="Select Kanban board"
|
||||
onchange="window.location.href = '{{ route('helpdesk.kanban') }}?board_id=' + encodeURIComponent(this.value)"
|
||||
>
|
||||
@foreach ($boards as $availableBoard)
|
||||
<option value="{{ $availableBoard->id }}" @selected((int) $board_id === (int) $availableBoard->id)>
|
||||
{{ $availableBoard->name }}{{ $availableBoard->owner ? ' · ' . $availableBoard->owner->name : '' }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($statuses->isEmpty())
|
||||
<div
|
||||
class="rounded-2xl border border-amber-500/40 bg-amber-950/70 px-5 py-4 text-sm font-semibold text-amber-100">
|
||||
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())
|
||||
|
||||
<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>
|
||||
</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>
|
||||
|
||||
@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>
|
||||
|
||||
|
||||
<form method="POST" action="{{ route('helpdesk.tickets.board.update', $ticket) }}"
|
||||
class="mt-3 rounded-xl border border-emerald-500/20 bg-emerald-950/20 p-2"
|
||||
onclick="event.stopPropagation()"
|
||||
onmousedown="event.stopPropagation()"
|
||||
ondragstart="event.preventDefault(); event.stopPropagation();">
|
||||
@csrf
|
||||
<input type="hidden" name="return_to_board_id" value="{{ $board?->id }}">
|
||||
<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 name="board_id"
|
||||
class="hd-ticket-board-select"
|
||||
draggable="false"
|
||||
onchange="this.form.requestSubmit ? this.form.requestSubmit() : this.form.submit()"
|
||||
onclick="event.stopPropagation()"
|
||||
onmousedown="event.stopPropagation()"
|
||||
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>
|
||||
</form>
|
||||
@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>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@script
|
||||
<script>
|
||||
const initialiseHelpdeskKanban = function() {
|
||||
let draggedTicketId = null;
|
||||
let draggedCard = null;
|
||||
|
||||
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 + '"]');
|
||||
|
||||
if (!column) {
|
||||
return;
|
||||
}
|
||||
|
||||
counter.textContent = column.querySelectorAll('[data-helpdesk-kanban-ticket]').length
|
||||
.toString();
|
||||
});
|
||||
};
|
||||
|
||||
const forceLivewireRefresh = function() {
|
||||
if (typeof $wire !== 'undefined' && typeof $wire.$refresh === 'function') {
|
||||
return Promise.resolve($wire.$refresh());
|
||||
}
|
||||
|
||||
if (typeof Livewire !== 'undefined' && typeof Livewire.find === 'function') {
|
||||
const root = document.querySelector('[wire\\:id]');
|
||||
const component = root ? Livewire.find(root.getAttribute('wire:id')) : null;
|
||||
|
||||
if (component && typeof component.$refresh === 'function') {
|
||||
return Promise.resolve(component.$refresh());
|
||||
}
|
||||
}
|
||||
|
||||
window.location.reload();
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
document.querySelectorAll('[data-helpdesk-kanban-ticket]').forEach(function(card) {
|
||||
if (card.dataset.nativeDragReady === '1') return;
|
||||
card.dataset.nativeDragReady = '1';
|
||||
|
||||
card.addEventListener('dragstart', function(event) {
|
||||
draggedTicketId = card.dataset.ticketId;
|
||||
draggedCard = card;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', draggedTicketId);
|
||||
event.dataTransfer.setData('application/x-helpdesk-ticket-id', draggedTicketId);
|
||||
card.classList.add('opacity-60');
|
||||
});
|
||||
|
||||
card.addEventListener('dragend', function() {
|
||||
card.classList.remove('opacity-60');
|
||||
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) {
|
||||
if (column.dataset.nativeDropReady === '1') return;
|
||||
column.dataset.nativeDropReady = '1';
|
||||
|
||||
column.addEventListener('dragover', function(event) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
column.dataset.dragOver = 'true';
|
||||
});
|
||||
|
||||
column.addEventListener('dragleave', function(event) {
|
||||
if (!column.contains(event.relatedTarget)) {
|
||||
column.dataset.dragOver = 'false';
|
||||
}
|
||||
});
|
||||
|
||||
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 statusId = column.dataset.statusId;
|
||||
const card = draggedCard || document.querySelector(
|
||||
'[data-helpdesk-kanban-ticket][data-ticket-id="' + ticketId + '"]');
|
||||
|
||||
if (!ticketId || !statusId || !card) return;
|
||||
|
||||
column.dataset.refreshing = 'true';
|
||||
column.appendChild(card);
|
||||
updateColumnCounts();
|
||||
|
||||
Promise.resolve($wire.moveTicket(ticketId, statusId))
|
||||
.then(forceLivewireRefresh)
|
||||
.finally(function() {
|
||||
column.dataset.refreshing = 'false';
|
||||
draggedTicketId = null;
|
||||
draggedCard = null;
|
||||
updateColumnCounts();
|
||||
setTimeout(initialiseHelpdeskKanban, 50);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
updateColumnCounts();
|
||||
};
|
||||
|
||||
initialiseHelpdeskKanban();
|
||||
document.addEventListener('livewire:navigated', initialiseHelpdeskKanban);
|
||||
document.addEventListener('livewire:updated', initialiseHelpdeskKanban);
|
||||
document.addEventListener('helpdesk-kanban-moved', function() {
|
||||
setTimeout(function() {
|
||||
initialiseHelpdeskKanban();
|
||||
|
||||
if (typeof $wire !== 'undefined' && typeof $wire.$refresh === 'function') {
|
||||
$wire.$refresh();
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
</script>
|
||||
@endscript
|
||||
</div>
|
||||
@@ -0,0 +1,988 @@
|
||||
@php
|
||||
/**
|
||||
* Keep the settings view safe during rolling module upgrades or Livewire navigate
|
||||
* hydration where the compiled Blade view may be newer than the loaded component.
|
||||
*/
|
||||
$companies = collect($companies ?? []);
|
||||
$statuses = collect($statuses ?? []);
|
||||
$priorities = collect($priorities ?? []);
|
||||
$categories = collect($categories ?? []);
|
||||
$teams = collect($teams ?? []);
|
||||
$projects = collect($projects ?? []);
|
||||
$boards = collect($boards ?? []);
|
||||
$repositories = collect($repositories ?? []);
|
||||
$workflowTemplates = collect($workflowTemplates ?? []);
|
||||
$cannedResponses = collect($cannedResponses ?? []);
|
||||
$slaPolicies = collect($slaPolicies ?? []);
|
||||
$automationRules = collect($automationRules ?? []);
|
||||
$checklistTemplates = collect($checklistTemplates ?? []);
|
||||
$tags = collect($tags ?? []);
|
||||
$products = collect($products ?? []);
|
||||
@endphp
|
||||
|
||||
<div class="hd-helpdesk-surface space-y-4" dusk="helpdesk-settings-manager">
|
||||
<div
|
||||
class="overflow-hidden rounded-2xl 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-5 py-5 text-white">
|
||||
<div class="flex flex-col gap-3 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>
|
||||
<h1 class="mt-2 text-2xl font-semibold tracking-tight">Support configuration</h1>
|
||||
<p class="mt-1 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">
|
||||
<a href="{{ route('helpdesk.index') }}" wire:navigate class="hd-btn hd-btn-blue">
|
||||
<i class="fa-solid fa-gauge-high" aria-hidden="true"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2 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)
|
||||
@php
|
||||
$sectionPalette = match ($key) {
|
||||
'statuses' => ['border' => '#38bdf8', 'bg' => 'rgba(14, 165, 233, .22)', 'active' => '#0284c7'],
|
||||
'priorities' => ['border' => '#f87171', 'bg' => 'rgba(239, 68, 68, .22)', 'active' => '#dc2626'],
|
||||
'categories' => ['border' => '#818cf8', 'bg' => 'rgba(99, 102, 241, .22)', 'active' => '#4f46e5'],
|
||||
'teams' => ['border' => '#60a5fa', 'bg' => 'rgba(37, 99, 235, .22)', 'active' => '#2563eb'],
|
||||
'projects' => ['border' => '#22d3ee', 'bg' => 'rgba(8, 145, 178, .22)', 'active' => '#0891b2'],
|
||||
'boards' => ['border' => '#fbbf24', 'bg' => 'rgba(245, 158, 11, .24)', 'active' => '#f59e0b'],
|
||||
'git' => ['border' => '#94a3b8', 'bg' => 'rgba(71, 85, 105, .28)', 'active' => '#475569'],
|
||||
'workflow' => ['border' => '#34d399', 'bg' => 'rgba(5, 150, 105, .22)', 'active' => '#059669'],
|
||||
'canned' => ['border' => '#2dd4bf', 'bg' => 'rgba(13, 148, 136, .22)', 'active' => '#0d9488'],
|
||||
'sla' => ['border' => '#fb923c', 'bg' => 'rgba(234, 88, 12, .22)', 'active' => '#ea580c'],
|
||||
'automation' => ['border' => '#c084fc', 'bg' => 'rgba(147, 51, 234, .22)', 'active' => '#9333ea'],
|
||||
'checklists' => ['border' => '#a3e635', 'bg' => 'rgba(101, 163, 13, .22)', 'active' => '#65a30d'],
|
||||
'tags' => ['border' => '#f472b6', 'bg' => 'rgba(219, 39, 119, .22)', 'active' => '#db2777'],
|
||||
'products' => ['border' => '#a78bfa', 'bg' => 'rgba(124, 58, 237, .22)', 'active' => '#7c3aed'],
|
||||
'defaults' => ['border' => '#4ade80', 'bg' => 'rgba(22, 163, 74, .22)', 'active' => '#16a34a'],
|
||||
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);';
|
||||
@endphp
|
||||
<a href="{{ route('helpdesk.settings', ['section' => $key]) }}"
|
||||
dusk="helpdesk-settings-section-{{ $key }}"
|
||||
class="rounded-xl border px-3 py-2 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-0.5 text-xl font-semibold">{{ $item['count'] }}</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
|
||||
@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-slot:form>
|
||||
<form class="grid gap-3 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' : '' }}
|
||||
</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
||||
<div class="md:col-span-3 flex gap-2">
|
||||
<flux:button size="sm" type="button" wire:click="saveDefaultHelpdeskCompany" variant="primary"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">
|
||||
Save default company
|
||||
</flux:button>
|
||||
</div>
|
||||
</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">
|
||||
<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>
|
||||
</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-slot:form>
|
||||
<form wire:submit.prevent="saveStatus" class="grid gap-3 xl:grid-cols-12">
|
||||
<div class="xl:col-span-3">
|
||||
<flux:input wire:model="statusForm.name" label="Name" />
|
||||
</div>
|
||||
<div class="xl:col-span-3">
|
||||
<flux:input wire:model="statusForm.slug" label="Slug" placeholder="auto-generated" />
|
||||
</div>
|
||||
<x-helpdesk::colour-select class="xl:col-span-12" model="statusForm.color" label="Badge colour" :options="$colourOptions" />
|
||||
<div class="xl:col-span-2">
|
||||
<flux:input wire:model="statusForm.sort_order" label="Sort" type="number" min="0" />
|
||||
</div>
|
||||
<label class="flex min-h-9 items-center gap-2 rounded-lg border border-zinc-200 bg-white px-3 py-2 text-xs font-semibold text-zinc-700 shadow-sm dark:border-zinc-700 dark:bg-zinc-950/60 dark:text-zinc-200 xl:col-span-3">
|
||||
<input type="checkbox" wire:model="statusForm.is_closed" class="rounded border-zinc-600 bg-zinc-950 text-blue-600 focus:ring-blue-500">
|
||||
Closed status
|
||||
</label>
|
||||
<label class="flex min-h-9 items-center gap-2 rounded-lg border border-zinc-200 bg-white px-3 py-2 text-xs font-semibold text-zinc-700 shadow-sm dark:border-zinc-700 dark:bg-zinc-950/60 dark:text-zinc-200 xl:col-span-4">
|
||||
<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="flex flex-wrap items-center gap-2 xl:col-span-12">
|
||||
<flux:button size="sm" type="submit" variant="primary"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save status
|
||||
</flux:button>
|
||||
<flux:button size="sm" type="button" wire:click="resetStatusForm" variant="ghost"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !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" />
|
||||
</x-helpdesk::settings-section>
|
||||
@endif
|
||||
|
||||
@if ($section === 'priorities')
|
||||
<x-helpdesk::settings-section title="Priorities" description="Define ticket urgency levels and display order.">
|
||||
<x-slot:form>
|
||||
<form wire:submit.prevent="savePriority" class="grid gap-3 xl:grid-cols-12">
|
||||
<div class="xl:col-span-3">
|
||||
<flux:input wire:model="priorityForm.name" label="Name" />
|
||||
</div>
|
||||
<div class="xl:col-span-3">
|
||||
<flux:input wire:model="priorityForm.slug" label="Slug" placeholder="auto-generated" />
|
||||
</div>
|
||||
<x-helpdesk::colour-select class="xl:col-span-12" model="priorityForm.color" label="Badge colour" :options="$colourOptions" />
|
||||
<div class="xl:col-span-2">
|
||||
<flux:input wire:model="priorityForm.sort_order" label="Sort" type="number" min="0" />
|
||||
</div>
|
||||
<label class="flex min-h-9 items-center gap-2 rounded-lg border border-zinc-200 bg-white px-3 py-2 text-xs font-semibold text-zinc-700 shadow-sm dark:border-zinc-700 dark:bg-zinc-950/60 dark:text-zinc-200 xl:col-span-4">
|
||||
<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="flex flex-wrap items-center gap-2 xl:col-span-12">
|
||||
<flux:button size="sm" type="submit" variant="primary"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save priority
|
||||
</flux:button>
|
||||
<flux:button size="sm" type="button" wire:click="resetPriorityForm" variant="ghost"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !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" />
|
||||
</x-helpdesk::settings-section>
|
||||
@endif
|
||||
|
||||
@if ($section === 'categories')
|
||||
<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-3 xl:grid-cols-12">
|
||||
<div class="xl:col-span-3">
|
||||
<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>
|
||||
</div>
|
||||
<div class="xl:col-span-3">
|
||||
<flux:input wire:model="categoryForm.name" label="Name" />
|
||||
</div>
|
||||
<div class="xl:col-span-3">
|
||||
<flux:input wire:model="categoryForm.slug" label="Slug" placeholder="auto-generated" />
|
||||
</div>
|
||||
<x-helpdesk::colour-select class="xl:col-span-12" model="categoryForm.color" label="Badge colour" :options="$colourOptions" />
|
||||
<div class="xl:col-span-12">
|
||||
<flux:textarea wire:model="categoryForm.description" label="Description" rows="3" />
|
||||
</div>
|
||||
<label class="flex min-h-9 items-center gap-2 rounded-lg border border-zinc-200 bg-white px-3 py-2 text-xs font-semibold text-zinc-700 shadow-sm dark:border-zinc-700 dark:bg-zinc-950/60 dark:text-zinc-200 xl:col-span-4">
|
||||
<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="flex flex-wrap items-center gap-2 xl:col-span-12">
|
||||
<flux:button size="sm" type="submit" variant="primary"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save
|
||||
category</flux:button>
|
||||
<flux:button size="sm" type="button" wire:click="resetCategoryForm" variant="ghost"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !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" />
|
||||
</x-helpdesk::settings-section>
|
||||
@endif
|
||||
|
||||
@if ($section === 'teams')
|
||||
<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-3 xl:grid-cols-12">
|
||||
<div class="xl:col-span-3">
|
||||
<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>
|
||||
</div>
|
||||
<div class="xl:col-span-3">
|
||||
<flux:input wire:model="teamForm.name" label="Name" />
|
||||
</div>
|
||||
<div class="xl:col-span-3">
|
||||
<flux:input wire:model="teamForm.slug" label="Slug" placeholder="auto-generated" />
|
||||
</div>
|
||||
<x-helpdesk::colour-select class="xl:col-span-12" model="teamForm.color" label="Badge colour" :options="$colourOptions" />
|
||||
<div class="xl:col-span-12">
|
||||
<flux:textarea wire:model="teamForm.description" label="Description" rows="3" />
|
||||
</div>
|
||||
<label class="flex min-h-9 items-center gap-2 rounded-lg border border-zinc-200 bg-white px-3 py-2 text-xs font-semibold text-zinc-700 shadow-sm dark:border-zinc-700 dark:bg-zinc-950/60 dark:text-zinc-200 xl:col-span-4">
|
||||
<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="flex flex-wrap items-center gap-2 xl:col-span-12">
|
||||
<flux:button size="sm" type="submit" variant="primary"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save team
|
||||
</flux:button>
|
||||
<flux:button size="sm" type="button" wire:click="resetTeamForm" variant="ghost"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !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" />
|
||||
</x-helpdesk::settings-section>
|
||||
@endif
|
||||
|
||||
@if ($section === 'projects')
|
||||
<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-3 xl:grid-cols-12">
|
||||
<div class="xl:col-span-2">
|
||||
<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>
|
||||
</div>
|
||||
<div class="xl:col-span-2">
|
||||
<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>
|
||||
</div>
|
||||
<div class="xl:col-span-2">
|
||||
<flux:input wire:model="projectForm.title" label="Title" />
|
||||
</div>
|
||||
<div class="xl:col-span-2">
|
||||
<flux:input wire:model="projectForm.slug" label="Slug" placeholder="auto-generated" />
|
||||
</div>
|
||||
<x-helpdesk::colour-select class="xl:col-span-12" model="projectForm.color" label="Badge colour" :options="$colourOptions" />
|
||||
<div class="xl:col-span-12">
|
||||
<flux:textarea wire:model="projectForm.description" label="Description" rows="3" />
|
||||
</div>
|
||||
<label class="flex min-h-9 items-center gap-2 rounded-lg border border-zinc-200 bg-white px-3 py-2 text-xs font-semibold text-zinc-700 shadow-sm dark:border-zinc-700 dark:bg-zinc-950/60 dark:text-zinc-200 xl:col-span-4">
|
||||
<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="flex flex-wrap items-center gap-2 xl:col-span-12">
|
||||
<flux:button size="sm" type="submit" variant="primary"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save project
|
||||
</flux:button>
|
||||
<flux:button size="sm" type="button" wire:click="resetProjectForm" variant="ghost"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !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" />
|
||||
</x-helpdesk::settings-section>
|
||||
@endif
|
||||
|
||||
|
||||
@if ($section === 'boards')
|
||||
<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-3 md:grid-cols-7">
|
||||
<flux:input wire:model="boardForm.name" label="Name" />
|
||||
<flux:input wire:model="boardForm.slug" label="Slug" placeholder="auto-generated" />
|
||||
<flux:input wire:model="boardForm.sort_order" label="Sort" type="number" min="0" />
|
||||
<flux:select wire:model="boardForm.owner_id" label="Owner">
|
||||
<option value="">No owner</option>
|
||||
@foreach ($boardOwners as $owner)
|
||||
<option value="{{ $owner->id }}">{{ $owner->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<label class="flex min-h-9 items-center gap-2 rounded-lg border border-zinc-200 bg-white px-3 py-2 text-xs font-semibold text-zinc-700 shadow-sm dark:border-zinc-700 dark:bg-zinc-950/60 dark:text-zinc-200">
|
||||
<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-full">
|
||||
<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>
|
||||
<span>{{ $status->name }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-span-full flex gap-2">
|
||||
<flux:button size="sm" type="submit" variant="primary"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save board
|
||||
</flux:button>
|
||||
<flux:button size="sm" type="button" wire:click="resetBoardForm" variant="ghost"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !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 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>
|
||||
<th class="px-4 py-3">Columns</th>
|
||||
<th class="px-4 py-3">Tickets</th>
|
||||
<th class="px-4 py-3">Default</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 ($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="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">
|
||||
<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>
|
||||
@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 text-right">
|
||||
<flux:button size="sm" variant="primary"
|
||||
wire:click="editBoard({{ $boardItem->id }})"
|
||||
class="!h-8 !rounded-lg !px-2.5 !text-xs !font-semibold !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="!h-8 !rounded-lg !px-2.5 !text-xs !font-semibold !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>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</x-helpdesk::settings-section>
|
||||
@endif
|
||||
|
||||
@if ($section === 'git')
|
||||
<x-helpdesk::settings-section title="Git repositories"
|
||||
description="Configure repositories that can be linked to projects and tickets. Commits can then be stored against tickets for traceability.">
|
||||
<x-slot:form>
|
||||
<form wire:submit.prevent="saveRepository" class="grid gap-3 md:grid-cols-6">
|
||||
<flux:select wire:model="repositoryForm.company_id" label="Company">
|
||||
<option value="">Global</option>
|
||||
@foreach ($companies as $company)
|
||||
<option value="{{ $company->id }}">{{ $company->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:select wire:model="repositoryForm.project_id" label="Project">
|
||||
<option value="">No project</option>
|
||||
@foreach ($projects as $project)
|
||||
<option value="{{ $project->id }}">{{ $project->title }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:input wire:model="repositoryForm.name" label="Name" />
|
||||
<flux:input wire:model="repositoryForm.provider" label="Provider"
|
||||
placeholder="git, github, gitlab" />
|
||||
<flux:input wire:model="repositoryForm.default_branch" label="Default branch" />
|
||||
<flux:input wire:model="repositoryForm.webhook_secret" label="Webhook secret" />
|
||||
<div class="md:col-span-full">
|
||||
<flux:input wire:model="repositoryForm.remote_url" label="Remote URL"
|
||||
placeholder="git@github.com:company/repo.git" />
|
||||
</div>
|
||||
<div class="md:col-span-full flex gap-2">
|
||||
<flux:button size="sm" type="submit" variant="primary"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save
|
||||
repository</flux:button>
|
||||
<flux:button size="sm" type="button" wire:click="resetRepositoryForm" variant="ghost"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</x-slot:form>
|
||||
<x-helpdesk::repository-table :items="$repositories" />
|
||||
</x-helpdesk::settings-section>
|
||||
@endif
|
||||
|
||||
|
||||
@if ($section === 'workflow')
|
||||
<x-helpdesk::settings-section title="Workflow messages"
|
||||
description="Define the professional email messages sent when tickets are raised, updated, replied to and closed.">
|
||||
<x-slot:form>
|
||||
<form wire:submit.prevent="saveWorkflowTemplate" class="grid gap-3 md:grid-cols-6">
|
||||
<flux:input wire:model="workflowForm.name" label="Name" />
|
||||
<flux:select wire:model="workflowForm.event" label="Event">
|
||||
<option value="ticket.created">Ticket raised</option>
|
||||
<option value="ticket.updated">Ticket updated</option>
|
||||
<option value="ticket.commented">Response added</option>
|
||||
<option value="ticket.reply_created">Reply added</option>
|
||||
<option value="ticket.closed">Ticket closed</option>
|
||||
</flux:select>
|
||||
<flux:select wire:model="workflowForm.to_role" label="Send to">
|
||||
<option value="requester">Requester</option>
|
||||
<option value="agent">Assigned agent</option>
|
||||
<option value="actor">User making change</option>
|
||||
</flux:select>
|
||||
<flux:input wire:model="workflowForm.cc" label="CC"
|
||||
placeholder="Optional comma-separated emails" />
|
||||
<flux:input wire:model="workflowForm.bcc" label="BCC"
|
||||
placeholder="Optional comma-separated emails" />
|
||||
<label class="flex min-h-9 items-center gap-2 rounded-lg border border-zinc-200 bg-white px-3 py-2 text-xs font-semibold text-zinc-700 shadow-sm dark:border-zinc-700 dark:bg-zinc-950/60 dark:text-zinc-200"><input
|
||||
type="checkbox" wire:model="workflowForm.is_active" class="rounded border-zinc-300">
|
||||
Active</label>
|
||||
<div class="md:col-span-full">
|
||||
<flux:input wire:model="workflowForm.subject" label="Subject"
|
||||
placeholder="Ticket @{{ ticket.reference }} has been raised" />
|
||||
</div>
|
||||
<div class="md:col-span-full">
|
||||
<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-full">
|
||||
<flux:textarea wire:model="workflowForm.text_body" label="Plain text body" rows="4"
|
||||
placeholder="Optional fallback text" />
|
||||
</div>
|
||||
<div class="md:col-span-full flex gap-2">
|
||||
<flux:button size="sm" type="submit" variant="primary"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save
|
||||
workflow message</flux:button>
|
||||
<flux:button size="sm" type="button" wire:click="resetWorkflowTemplateForm" variant="ghost"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !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 tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Name</th>
|
||||
<th class="px-4 py-3">Event</th>
|
||||
<th class="px-4 py-3">Recipient</th>
|
||||
<th class="px-4 py-3">Active</th>
|
||||
<th class="px-4 py-3 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
@forelse ($workflowTemplates as $template)
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-medium text-zinc-900 dark:text-zinc-100">
|
||||
{{ $template->name }}</td>
|
||||
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">{{ $template->event }}</td>
|
||||
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">{{ $template->to_role }}</td>
|
||||
<td class="px-4 py-3">
|
||||
<flux:badge color="{{ $template->is_active ? 'emerald' : 'zinc' }}">
|
||||
{{ $template->is_active ? 'Active' : 'Inactive' }}</flux:badge>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<flux:button size="sm" variant="ghost"
|
||||
wire:click="editWorkflowTemplate({{ $template->id }})"
|
||||
class="!h-8 !rounded-lg !px-2.5 !text-xs !font-semibold !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="!h-8 !rounded-lg !px-2.5 !text-xs !font-semibold !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>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</x-helpdesk::settings-section>
|
||||
@endif
|
||||
|
||||
|
||||
@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-slot:form>
|
||||
<form wire:submit.prevent="saveCannedResponse" class="grid gap-3 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 min-h-9 items-center gap-2 rounded-lg border border-zinc-200 bg-white px-3 py-2 text-xs font-semibold text-zinc-700 shadow-sm dark:border-zinc-700 dark:bg-zinc-950/60 dark:text-zinc-200"><input
|
||||
type="checkbox" wire:model="cannedForm.is_active" class="rounded border-zinc-300">
|
||||
Active</label>
|
||||
<div class="md:col-span-full">
|
||||
<flux:input wire:model="cannedForm.subject" label="Optional subject" />
|
||||
</div>
|
||||
<div class="md:col-span-full">
|
||||
<flux:textarea wire:model="cannedForm.body" label="Reply body" rows="6"
|
||||
placeholder="Hello @{{ requester.name }}, Your ticket @{{ ticket.reference }} has been reviewed." />
|
||||
</div>
|
||||
<div class="md:col-span-full flex gap-2">
|
||||
<flux:button size="sm" type="submit" variant="primary"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save canned
|
||||
response</flux:button>
|
||||
<flux:button size="sm" type="button" wire:click="resetCannedResponseForm" variant="ghost"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !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="!h-8 !rounded-lg !px-2.5 !text-xs !font-semibold !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="!h-8 !rounded-lg !px-2.5 !text-xs !font-semibold !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>
|
||||
@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-slot:form>
|
||||
<form wire:submit.prevent="saveSlaPolicy" class="grid gap-3 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-full flex gap-2">
|
||||
<flux:button size="sm" type="submit" variant="primary"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save SLA
|
||||
policy</flux:button>
|
||||
<flux:button size="sm" type="button" wire:click="resetSlaPolicyForm" variant="ghost"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !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="!h-8 !rounded-lg !px-2.5 !text-xs !font-semibold !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="!h-8 !rounded-lg !px-2.5 !text-xs !font-semibold !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-slot:form>
|
||||
<form wire:submit.prevent="saveAutomationRule" class="grid gap-3 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.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 min-h-9 items-center gap-2 rounded-lg border border-zinc-200 bg-white px-3 py-2 text-xs font-semibold text-zinc-700 shadow-sm dark:border-zinc-700 dark:bg-zinc-950/60 dark:text-zinc-200"><input
|
||||
type="checkbox" wire:model="automationForm.is_active" class="rounded border-zinc-300">
|
||||
Active</label>
|
||||
<div class="md:col-span-full flex gap-2">
|
||||
<flux:button size="sm" type="submit" variant="primary"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save
|
||||
automation rule</flux:button>
|
||||
<flux:button size="sm" type="button" wire:click="resetAutomationRuleForm" variant="ghost"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !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="!h-8 !rounded-lg !px-2.5 !text-xs !font-semibold !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="!h-8 !rounded-lg !px-2.5 !text-xs !font-semibold !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-3 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-full">
|
||||
<flux:textarea wire:model="checklistTemplateForm.items_text"
|
||||
label="Checklist items, one per line" rows="6" />
|
||||
</div>
|
||||
<div class="md:col-span-full flex gap-2">
|
||||
<flux:button size="sm" type="submit" variant="primary"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save
|
||||
checklist</flux:button>
|
||||
<flux:button size="sm" type="button" wire:click="resetChecklistTemplateForm" variant="ghost"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !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="!h-8 !rounded-lg !px-2.5 !text-xs !font-semibold !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="!h-8 !rounded-lg !px-2.5 !text-xs !font-semibold !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-3 xl:grid-cols-12">
|
||||
<div class="xl:col-span-4">
|
||||
<flux:input wire:model="tagForm.name" label="Name" />
|
||||
</div>
|
||||
<div class="xl:col-span-4">
|
||||
<flux:input wire:model="tagForm.slug" label="Slug" />
|
||||
</div>
|
||||
<x-helpdesk::colour-select class="xl:col-span-12" model="tagForm.color" label="Colour" :options="$colourOptions" />
|
||||
<div class="flex flex-wrap items-center gap-2 xl:col-span-12">
|
||||
<flux:button size="sm" type="submit" variant="primary"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save tag
|
||||
</flux:button>
|
||||
<flux:button size="sm" type="button" wire:click="resetTagForm" variant="ghost"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !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="!h-8 !rounded-lg !px-2.5 !text-xs !font-semibold !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="!h-8 !rounded-lg !px-2.5 !text-xs !font-semibold !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-3 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-full">
|
||||
<flux:textarea wire:model="productForm.description" label="Description" rows="3" />
|
||||
</div>
|
||||
<div class="md:col-span-full flex gap-2">
|
||||
<flux:button size="sm" type="submit" variant="primary"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save
|
||||
product/service</flux:button>
|
||||
<flux:button size="sm" type="button" wire:click="resetProductForm" variant="ghost"
|
||||
class="!h-9 !rounded-lg !px-3 !text-xs !font-semibold !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="!h-8 !rounded-lg !px-2.5 !text-xs !font-semibold !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="!h-8 !rounded-lg !px-2.5 !text-xs !font-semibold !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
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,405 @@
|
||||
@php
|
||||
$helpdeskAttachmentAccept = collect(config('helpdesk.allowed_extensions', config('helpdesk.allowed_mimes', [])))
|
||||
->filter(fn ($extension) => is_string($extension) && trim($extension) !== '')
|
||||
->map(fn ($extension) => '.' . strtolower(ltrim(trim($extension), '.')))
|
||||
->unique()
|
||||
->implode(',');
|
||||
@endphp
|
||||
<div class="hd-helpdesk-surface space-y-6" dusk="helpdesk-ticket-create" x-data="{ helpdeskAttachmentsUploading: false }">
|
||||
@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.',
|
||||
])
|
||||
|
||||
<form
|
||||
wire:submit.prevent="save"
|
||||
x-on:submit.capture="
|
||||
if (helpdeskAttachmentsUploading) {
|
||||
$event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = document.querySelector('[data-helpdesk-sync-target="body"], [data-helpdesk-editor-model="body"]');
|
||||
const target = Array.from(document.querySelectorAll('textarea')).find(function (textarea) {
|
||||
|
||||
return textarea.getAttribute('wire:model.defer') === 'body'
|
||||
|
||||
|| textarea.getAttribute('wire:model') === 'body'
|
||||
|
||||
|| textarea.getAttribute('name') === 'body'
|
||||
|
||||
|| textarea.id === 'body';
|
||||
|
||||
});
|
||||
const body = (editor?.innerText || editor?.textContent || editor?.value || '').trim();
|
||||
|
||||
if (target) {
|
||||
target.value = body;
|
||||
target.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
target.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
if ($wire) {
|
||||
$wire.set('body', body, false);
|
||||
}
|
||||
|
||||
$dispatch('helpdesk-editor-sync', { model: 'body', live: false });
|
||||
"
|
||||
class="helpdesk-ticket-form space-y-6"
|
||||
>
|
||||
<div class="overflow-hidden rounded-2xl border border-zinc-700 bg-zinc-900 shadow-sm">
|
||||
<div class="border-b border-zinc-700 bg-zinc-800/80 px-6 py-4">
|
||||
<h2 class="text-lg font-semibold text-zinc-100">Ticket details</h2>
|
||||
<p class="mt-1 text-sm text-zinc-400">
|
||||
Mandatory fields are marked with <span class="helpdesk-required-star">*</span>.
|
||||
Use the searchable lists to keep the ticket correctly routed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-5 p-6 lg:grid-cols-3 xl:grid-cols-6">
|
||||
<div class="lg:col-span-3 xl:col-span-6">
|
||||
<label class="block text-sm font-semibold text-zinc-200" for="subject">
|
||||
Subject <span class="helpdesk-required-star">*</span>
|
||||
</label>
|
||||
|
||||
<input id="subject" type="text" spellcheck="true" lang="en-GB" autocorrect="on" autocomplete="on" autocapitalize="sentences" inputmode="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" />
|
||||
|
||||
@error('subject')
|
||||
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
@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 wire:model.defer="company_id" name="company_id"
|
||||
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>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@error('company_id')
|
||||
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="xl:col-span-2">
|
||||
<label class="block text-sm font-semibold text-zinc-200" for="board_id">
|
||||
Board <span class="helpdesk-required-star">*</span>
|
||||
</label>
|
||||
|
||||
<div class="mt-2" wire:ignore>
|
||||
<select id="board_id" data-helpdesk-select2 wire:model.defer="board_id" name="board_id" 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">
|
||||
<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>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@error('board_id')
|
||||
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="xl:col-span-2">
|
||||
<label class="block text-sm font-semibold text-zinc-200" for="priority_id">
|
||||
Priority <span class="helpdesk-required-star">*</span>
|
||||
</label>
|
||||
|
||||
<div class="mt-2" wire:ignore>
|
||||
<select id="priority_id" data-helpdesk-select2 wire:model.defer="priority_id" name="priority_id" 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">
|
||||
<option value="">Select priority</option>
|
||||
@foreach ($priorities as $priority)
|
||||
<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>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="xl:col-span-2">
|
||||
<label class="block text-sm font-semibold text-zinc-200" for="category_id">
|
||||
Category <span class="helpdesk-required-star">*</span>
|
||||
</label>
|
||||
|
||||
<div class="mt-2" wire:ignore>
|
||||
<select id="category_id" data-helpdesk-select2 wire:model.defer="category_id" name="category_id" 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">
|
||||
<option value="">Select category</option>
|
||||
@foreach ($categories as $category)
|
||||
<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>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="xl:col-span-2">
|
||||
<label class="block text-sm font-semibold text-zinc-200" for="project_id">
|
||||
Project <span class="helpdesk-required-star">*</span>
|
||||
</label>
|
||||
|
||||
<div class="mt-2" wire:ignore>
|
||||
<select id="project_id" data-helpdesk-select2 wire:model.defer="project_id" name="project_id" 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">
|
||||
<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>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@error('project_id')
|
||||
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="xl:col-span-2">
|
||||
<label class="block text-sm font-semibold text-zinc-200" for="team_id">
|
||||
Team <span class="helpdesk-required-star">*</span>
|
||||
</label>
|
||||
|
||||
<div class="mt-2" wire:ignore>
|
||||
<select id="team_id" data-helpdesk-select2 wire:model.defer="team_id" name="team_id" 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">
|
||||
<option value="">Select team</option>
|
||||
@foreach ($teams as $team)
|
||||
<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>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-3 xl:col-span-6">
|
||||
<fieldset class="rounded-2xl border border-zinc-700 bg-zinc-950/70 p-4">
|
||||
<div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
||||
<legend class="text-sm font-semibold text-zinc-200">
|
||||
Tags
|
||||
</legend>
|
||||
|
||||
<span class="text-xs text-zinc-500">
|
||||
Optional · select all that apply
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
value="{{ $tag->id }}"
|
||||
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">
|
||||
No tags are available.
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
@error('tag_ids')
|
||||
<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>
|
||||
@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 }}" />
|
||||
|
||||
@error('due_at')
|
||||
<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
|
||||
model="body"
|
||||
label="Details"
|
||||
rows="12"
|
||||
placeholder="Describe the issue, expected outcome, steps to reproduce and any useful context..."
|
||||
editor-dusk="ticket-body-editor"
|
||||
/>
|
||||
|
||||
<p class="mt-2 flex items-center gap-2 text-xs font-medium text-zinc-400">
|
||||
<i class="fa-solid fa-circle-info text-blue-400" aria-hidden="true"></i>
|
||||
Rich formatting is available. Browser spelling suggestions appear by right-clicking or Ctrl-clicking red-underlined words.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-3 xl:col-span-6">
|
||||
<div
|
||||
wire:key="ticket-create-attachments-uploader"
|
||||
class="rounded-2xl border border-zinc-700 bg-zinc-950/70 p-4"
|
||||
x-data="{ progress: 0 }"
|
||||
x-on:livewire-upload-start="helpdeskAttachmentsUploading = true"
|
||||
x-on:livewire-upload-progress="progress = $event.detail.progress"
|
||||
x-on:livewire-upload-finish="helpdeskAttachmentsUploading = false"
|
||||
x-on:livewire-upload-error="helpdeskAttachmentsUploading = false"
|
||||
x-on:livewire-upload-cancel="helpdeskAttachmentsUploading = false"
|
||||
>
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-zinc-200" for="attachments">
|
||||
Attachments
|
||||
</label>
|
||||
|
||||
<p class="mt-1 text-sm text-zinc-500">
|
||||
Files are uploaded to Livewire temporary storage first. They are attached permanently only after the ticket is created.
|
||||
</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">
|
||||
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
|
||||
<span>Add attachments</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label for="attachments"
|
||||
class="mt-4 flex cursor-pointer flex-col items-center justify-center rounded-2xl border border-dashed border-zinc-700 bg-zinc-900/70 px-4 py-8 text-center transition hover:border-blue-500 hover:bg-zinc-900">
|
||||
<i class="fa-solid fa-cloud-arrow-up text-2xl text-blue-300" aria-hidden="true"></i>
|
||||
<span class="mt-3 text-sm font-bold text-zinc-100">Click to choose files</span>
|
||||
<span class="mt-1 text-xs text-zinc-500">Maximum {{ (int) config('helpdesk.max_upload_kb', 10240) }} KB per file.</span>
|
||||
</label>
|
||||
<div x-data class="rounded-2xl border border-zinc-700 bg-zinc-950/70 p-4">
|
||||
<input
|
||||
x-ref="attachmentsInput"
|
||||
id="attachments"
|
||||
type="file"
|
||||
wire:model="attachments"
|
||||
multiple
|
||||
accept="{{ $helpdeskAttachmentAccept }}"
|
||||
class="hidden"
|
||||
wire:key="ticket-create-attachments-input"
|
||||
dusk="ticket-create-attachments-input"
|
||||
data-dusk="ticket-create-attachments-input"/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
x-on:click="$refs.attachmentsInput.click()"
|
||||
class="inline-flex items-center justify-center gap-2 rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white">
|
||||
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
|
||||
Add attachments
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
x-on:click="$refs.attachmentsInput.click()"
|
||||
class="mt-4 flex w-full flex-col items-center justify-center rounded-2xl border border-dashed border-zinc-700 bg-zinc-900/70 px-4 py-8 text-center">
|
||||
<i class="fa-solid fa-cloud-arrow-up text-2xl text-blue-300" aria-hidden="true"></i>
|
||||
<span class="mt-3 text-sm font-bold text-zinc-100">Click to choose files</span>
|
||||
<span class="mt-1 text-xs text-zinc-500">
|
||||
Maximum {{ (int) config('helpdesk.max_upload_kb', 10240) }} KB per file.
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@if (count($attachments) > 0)
|
||||
<div class="mt-4 grid gap-2 sm:grid-cols-2" dusk="ticket-create-selected-attachments">
|
||||
@foreach ($attachments as $attachment)
|
||||
<div class="flex items-center gap-3 rounded-xl border border-zinc-700 bg-zinc-900 px-3 py-2 text-xs text-zinc-300" wire:key="ticket-create-selected-attachment-{{ $loop->index }}">
|
||||
<i class="fa-regular fa-file-lines text-blue-300" aria-hidden="true"></i>
|
||||
<span class="min-w-0 flex-1 truncate">{{ $attachment->getClientOriginalName() }}</span>
|
||||
<span class="shrink-0 text-zinc-500">{{ number_format($attachment->getSize() / 1024, 1) }} KB</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div x-show="helpdeskAttachmentsUploading" x-cloak
|
||||
class="mt-4 rounded-xl border border-blue-500/30 bg-blue-500/10 px-4 py-3 text-sm font-medium text-blue-200">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="inline-flex items-center gap-2"><i class="fa-solid fa-circle-notch fa-spin" aria-hidden="true"></i>Uploading attachments...</span>
|
||||
<span x-text="progress + '%'"></span>
|
||||
</div>
|
||||
<div class="mt-3 h-2 overflow-hidden rounded-full bg-zinc-800">
|
||||
<div class="h-full rounded-full bg-blue-500 transition-all" x-bind:style="`width: ${progress}%`"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@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 class="flex flex-col-reverse gap-2 border-t border-zinc-700 bg-zinc-900 px-6 py-4 sm:flex-row sm:items-center sm:justify-end">
|
||||
<button type="button" wire:click="clearForm" wire:loading.attr="disabled" wire:target="clearForm"
|
||||
class="inline-flex min-h-10 items-center justify-center rounded-lg bg-red-600 px-4 py-2 text-xs font-black text-white shadow-sm transition hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-400/70 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
data-dusk="ticket-create-clear" dusk="ticket-create-clear">
|
||||
<i class="fa-solid fa-eraser mr-2" aria-hidden="true"></i>
|
||||
<span wire:loading.remove wire:target="clearForm">Clear</span>
|
||||
<span wire:loading wire:target="clearForm">Clearing...</span>
|
||||
</button>
|
||||
|
||||
<a href="{{ route('helpdesk.tickets.index') }}" wire:navigate
|
||||
class="inline-flex min-h-10 items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-xs font-black text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400/70"
|
||||
data-dusk="ticket-create-cancel" dusk="ticket-create-cancel">
|
||||
<i class="fa-solid fa-arrow-left mr-2" aria-hidden="true"></i>
|
||||
Cancel
|
||||
</a>
|
||||
|
||||
<button type="submit"
|
||||
class="inline-flex min-h-10 items-center justify-center rounded-lg bg-emerald-600 px-4 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 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
data-dusk="ticket-create-submit" dusk="ticket-create-submit" wire:loading.attr="disabled" wire:target="save,attachments" x-bind:disabled="helpdeskAttachmentsUploading" wire:dirty.class="ring-2 ring-amber-400/40">
|
||||
<i class="fa-regular fa-floppy-disk mr-2" aria-hidden="true"></i>
|
||||
<span wire:loading.remove wire:target="save,attachments">Create ticket</span>
|
||||
<span wire:loading wire:target="attachments">Uploading...</span>
|
||||
<span wire:loading wire:target="save">Creating...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,429 @@
|
||||
@php
|
||||
$helpdeskAttachmentAccept = collect(config('helpdesk.allowed_extensions', config('helpdesk.allowed_mimes', [])))
|
||||
->filter(fn ($extension) => is_string($extension) && trim($extension) !== '')
|
||||
->map(fn ($extension) => '.' . strtolower(ltrim(trim($extension), '.')))
|
||||
->unique()
|
||||
->implode(',');
|
||||
@endphp
|
||||
<div class="space-y-6">
|
||||
@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.',
|
||||
])
|
||||
|
||||
<form wire:submit.prevent="save" class="space-y-6">
|
||||
<div class="overflow-hidden rounded-2xl border border-zinc-700 bg-zinc-900 shadow-sm">
|
||||
<div class="border-b border-zinc-700 bg-zinc-800/80 px-6 py-4">
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<h2 class="text-lg font-semibold text-zinc-100">
|
||||
{{ $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">
|
||||
<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>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-sm text-zinc-400">
|
||||
Mandatory fields are marked with <span class="helpdesk-required-star">*</span>.
|
||||
Assignment and status updates trigger configured workflow messages.
|
||||
</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">
|
||||
Back to ticket
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-5 p-6 lg:grid-cols-3 xl:grid-cols-6">
|
||||
<div class="lg:col-span-3 xl:col-span-6">
|
||||
<label class="block text-sm font-semibold text-zinc-200" for="subject">
|
||||
Subject <span class="helpdesk-required-star">*</span>
|
||||
</label>
|
||||
|
||||
<input id="subject" type="text" spellcheck="true" lang="en-GB" autocorrect="on" autocomplete="on" autocapitalize="sentences" inputmode="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" dusk="ticket-edit-subject" />
|
||||
|
||||
@error('subject')
|
||||
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
@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"
|
||||
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>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@error('company_id')
|
||||
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="xl:col-span-2">
|
||||
<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"
|
||||
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">
|
||||
<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>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@error('board_id')
|
||||
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="xl:col-span-2">
|
||||
<label class="block text-sm font-semibold text-zinc-200" for="status_id">
|
||||
Status <span class="helpdesk-required-star">*</span>
|
||||
</label>
|
||||
|
||||
<div class="mt-2" wire:ignore>
|
||||
<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">
|
||||
<option value="">No status</option>
|
||||
@foreach ($statuses as $status)
|
||||
<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>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="xl:col-span-2">
|
||||
<label class="block text-sm font-semibold text-zinc-200" for="priority_id">
|
||||
Priority <span class="helpdesk-required-star">*</span>
|
||||
</label>
|
||||
|
||||
<div class="mt-2" wire:ignore>
|
||||
<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">
|
||||
<option value="">Select priority</option>
|
||||
@foreach ($priorities as $priority)
|
||||
<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>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="xl:col-span-2">
|
||||
<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"
|
||||
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">
|
||||
<option value="">Unassigned</option>
|
||||
@forelse ($agents as $agent)
|
||||
<option value="{{ $agent->id }}">{{ $agent->name }} · {{ $agent->email }}
|
||||
</option>
|
||||
@empty
|
||||
<option value="" disabled>No users available for assignment</option>
|
||||
@endforelse
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@error('agent_id')
|
||||
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="xl:col-span-2">
|
||||
<label class="block text-sm font-semibold text-zinc-200" for="category_id">
|
||||
Category <span class="helpdesk-required-star">*</span>
|
||||
</label>
|
||||
|
||||
<div class="mt-2" wire:ignore>
|
||||
<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">
|
||||
<option value="">No category</option>
|
||||
@foreach ($categories as $category)
|
||||
<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>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="xl:col-span-2">
|
||||
<label class="block text-sm font-semibold text-zinc-200" for="project_id">
|
||||
Project <span class="helpdesk-required-star">*</span>
|
||||
</label>
|
||||
|
||||
<div class="mt-2" wire:ignore>
|
||||
<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">
|
||||
<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>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@error('project_id')
|
||||
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="xl:col-span-2">
|
||||
<label class="block text-sm font-semibold text-zinc-200" for="team_id">
|
||||
Team <span class="helpdesk-required-star">*</span>
|
||||
</label>
|
||||
|
||||
<div class="mt-2" wire:ignore>
|
||||
<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">
|
||||
<option value="">No team</option>
|
||||
@foreach ($teams as $team)
|
||||
<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>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-3 xl:col-span-6">
|
||||
<fieldset class="rounded-2xl border border-zinc-700 bg-zinc-950/70 p-4">
|
||||
<div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
||||
<legend class="text-sm font-semibold text-zinc-200">
|
||||
Tags
|
||||
</legend>
|
||||
|
||||
<span class="text-xs text-zinc-500">
|
||||
Optional · select all that apply
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
value="{{ $tag->id }}"
|
||||
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">
|
||||
No tags are available.
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
@error('tag_ids')
|
||||
<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>
|
||||
@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 }}" />
|
||||
|
||||
@error('due_at')
|
||||
<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
|
||||
model="body"
|
||||
label="Details *"
|
||||
rows="10"
|
||||
placeholder="Update the ticket details..."
|
||||
editor-dusk="ticket-edit-body-editor"
|
||||
/>
|
||||
|
||||
@error('body')
|
||||
<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="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">
|
||||
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
|
||||
</span>
|
||||
Attachments
|
||||
</label>
|
||||
|
||||
<p class="mt-2 max-w-2xl text-sm font-medium text-blue-50/90">
|
||||
Add screenshots, documents, logs or supporting files to help resolve the ticket.
|
||||
</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">
|
||||
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
||||
<span>Add attachments</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
|
||||
<input id="attachments" type="file" wire:model="attachments" multiple accept="{{ $helpdeskAttachmentAccept }}"
|
||||
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>
|
||||
|
||||
@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>
|
||||
@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
|
||||
</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">
|
||||
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">
|
||||
Cancel
|
||||
</a>
|
||||
|
||||
<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">
|
||||
<i class="fa-regular fa-floppy-disk" aria-hidden="true"></i>
|
||||
<span wire:loading.remove>Save Ticket</span>
|
||||
<span wire:loading>Saving...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,219 @@
|
||||
<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="bg-gradient-to-br from-slate-950 via-slate-900 to-blue-950 px-6 py-7 text-white">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-blue-200">Ticket management</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold tracking-tight">Active tickets</h1>
|
||||
<p class="mt-2 max-w-3xl text-sm text-blue-100/90">Search, triage and manage support requests across
|
||||
companies, projects, teams, priorities and statuses.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="{{ route('helpdesk.index') }}" wire:navigate class="hd-btn hd-btn-blue">
|
||||
<i class="fa-solid fa-chart-bar" aria-hidden="true"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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:select wire:model.live="scope">
|
||||
<option value="open">Open tickets</option>
|
||||
<option value="closed">Closed tickets</option>
|
||||
<option value="all">All tickets</option>
|
||||
</flux:select>
|
||||
|
||||
<flux:select wire:model.live="status">
|
||||
<option value="">All statuses</option>
|
||||
@foreach ($statuses as $status)
|
||||
<option value="{{ $status->id }}">{{ $status->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
||||
<flux:select wire:model.live="priority">
|
||||
<option value="">All priorities</option>
|
||||
@foreach ($priorities as $priority)
|
||||
<option value="{{ $priority->id }}">{{ $priority->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</div>
|
||||
</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">
|
||||
<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">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Reference</th>
|
||||
<th class="px-4 py-3">Subject</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
<th class="px-4 py-3">Priority</th>
|
||||
<th class="px-4 py-3">Agent</th>
|
||||
<th class="px-4 py-3 text-center">Files</th>
|
||||
<th class="px-4 py-3">Last activity</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 ($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">
|
||||
{{ $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>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<x-helpdesk::colour-badge :color="$ticket->status?->color ?? 'zinc'">
|
||||
{{ $ticket->status?->name ?? 'No status' }}
|
||||
</x-helpdesk::colour-badge>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<x-helpdesk::colour-badge :color="$ticket->priority?->color ?? 'zinc'">
|
||||
{{ $ticket->priority?->name ?? 'No priority' }}
|
||||
</x-helpdesk::colour-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' }}">
|
||||
<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>
|
||||
@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">
|
||||
<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-right">
|
||||
@can('delete', $ticket)
|
||||
<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 }}">
|
||||
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
@endcan
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="px-4 py-12 text-center text-zinc-500">No tickets matched your
|
||||
filters.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-zinc-200 p-4 dark:border-zinc-700">
|
||||
{{ $tickets->links() }}
|
||||
</div>
|
||||
</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"
|
||||
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">
|
||||
<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">
|
||||
<i class="fa-solid fa-triangle-exclamation" aria-hidden="true"></i>
|
||||
</span>
|
||||
|
||||
<div class="min-w-0">
|
||||
<h2 id="delete-ticket-modal-title" class="text-xl font-black tracking-tight text-white">
|
||||
Delete ticket?
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 px-6 py-5">
|
||||
@if ($pendingDeleteTicketSubject)
|
||||
<div class="rounded-2xl border border-zinc-800 bg-zinc-900 px-4 py-3">
|
||||
<p class="text-xs font-bold uppercase tracking-[0.22em] text-zinc-500">Ticket subject</p>
|
||||
<p class="mt-1 text-sm font-semibold text-zinc-100">{{ $pendingDeleteTicketSubject }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<p class="text-sm leading-6 text-zinc-300">
|
||||
Confirm only if this ticket was created in error or is no longer required.
|
||||
</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">
|
||||
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,151 @@
|
||||
<div
|
||||
class="mb-4 rounded-xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-950"
|
||||
data-helpdesk-ticket-timer
|
||||
>
|
||||
@php
|
||||
$hasRunningTimer = $activeTimer && $activeTimer->ended_at === null;
|
||||
$startedAtIso = $hasRunningTimer ? $activeTimer->started_at?->toIso8601String() : null;
|
||||
$startedAtDisplay = $hasRunningTimer ? $activeTimer->started_at?->format('d M Y H:i') : null;
|
||||
@endphp
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="flex min-w-0 items-start gap-3">
|
||||
<span class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-blue-600 text-white shadow-sm">
|
||||
<i class="fa-solid fa-stopwatch text-sm" aria-hidden="true"></i>
|
||||
</span>
|
||||
|
||||
<div class="min-w-0">
|
||||
<h3 class="text-sm font-black text-zinc-950 dark:text-zinc-100">
|
||||
Ticket timer
|
||||
</h3>
|
||||
|
||||
<p class="mt-1 text-xs font-semibold text-zinc-600 dark:text-zinc-300">
|
||||
Record focused work against this ticket.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($hasRunningTimer)
|
||||
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-right shadow-sm dark:border-emerald-900 dark:bg-emerald-950/40">
|
||||
<div class="flex items-center justify-end gap-2 text-[0.65rem] font-black uppercase tracking-[0.16em] text-emerald-700 dark:text-emerald-300">
|
||||
<span class="h-2 w-2 rounded-full bg-emerald-500"></span>
|
||||
Running
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-1 font-mono text-2xl font-black leading-none text-emerald-950 dark:text-emerald-100"
|
||||
data-helpdesk-live-elapsed
|
||||
data-helpdesk-started-at="{{ $startedAtIso }}"
|
||||
data-dusk="ticket-running-time"
|
||||
>
|
||||
{{ gmdate('H:i:s', max(0, (int) $elapsedSeconds)) }}
|
||||
</div>
|
||||
|
||||
<p class="mt-1 text-[0.68rem] font-bold text-emerald-700 dark:text-emerald-300">
|
||||
Started {{ $startedAtDisplay }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($message)
|
||||
<div class="rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs font-bold text-emerald-800 dark:border-emerald-900 dark:bg-emerald-950/40 dark:text-emerald-100">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@error('timer')
|
||||
<div class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs font-bold text-red-800 dark:border-red-900 dark:bg-red-950/40 dark:text-red-100">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-bold text-zinc-800 dark:text-zinc-100" for="ticket_timer_description">
|
||||
Timer note
|
||||
</label>
|
||||
|
||||
<textarea
|
||||
id="ticket_timer_description"
|
||||
wire:model.defer="description"
|
||||
rows="2"
|
||||
placeholder="Optional work note"
|
||||
class="block w-full rounded-lg 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-900 dark:text-zinc-100"
|
||||
data-dusk="ticket-timer-description"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p class="text-xs font-semibold text-zinc-500 dark:text-zinc-400">
|
||||
{{ $hasRunningTimer ? 'End the timer to add the elapsed time to the ticket.' : 'Start a timer when work begins. Only one active timer is allowed.' }}
|
||||
</p>
|
||||
|
||||
@if ($hasRunningTimer)
|
||||
<button
|
||||
type="button"
|
||||
wire:click="endTimer"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="endTimer"
|
||||
class="inline-flex min-w-[9.5rem] shrink-0 items-center justify-center whitespace-nowrap rounded-md bg-red-600 px-4 py-2 text-sm font-black text-white shadow-sm transition hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 dark:focus:ring-offset-zinc-950"
|
||||
data-dusk="end-ticket-timer"
|
||||
>
|
||||
<i class="fa-solid fa-stop mr-2" aria-hidden="true"></i>
|
||||
<span wire:loading.remove wire:target="endTimer">End timer</span>
|
||||
<span wire:loading wire:target="endTimer">Ending...</span>
|
||||
</button>
|
||||
@else
|
||||
<button
|
||||
type="button"
|
||||
wire:click="startTimer"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="startTimer"
|
||||
class="inline-flex min-w-[9.5rem] shrink-0 items-center justify-center whitespace-nowrap rounded-md bg-blue-600 px-4 py-2 text-sm font-black text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 dark:focus:ring-offset-zinc-950"
|
||||
data-dusk="start-ticket-timer"
|
||||
>
|
||||
<i class="fa-solid fa-play mr-2 text-xs" aria-hidden="true"></i>
|
||||
<span wire:loading.remove wire:target="startTimer">Start timer</span>
|
||||
<span wire:loading wire:target="startTimer">Starting...</span>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
if (window.helpdeskTicketTimerDisplayBooted === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.helpdeskTicketTimerDisplayBooted = true;
|
||||
|
||||
const formatElapsed = (totalSeconds) => {
|
||||
const seconds = Math.max(0, Number.parseInt(totalSeconds, 10) || 0);
|
||||
const hours = String(Math.floor(seconds / 3600)).padStart(2, '0');
|
||||
const minutes = String(Math.floor((seconds % 3600) / 60)).padStart(2, '0');
|
||||
const remainingSeconds = String(seconds % 60).padStart(2, '0');
|
||||
|
||||
return `${hours}:${minutes}:${remainingSeconds}`;
|
||||
};
|
||||
|
||||
const updateTimers = () => {
|
||||
document.querySelectorAll('[data-helpdesk-live-elapsed][data-helpdesk-started-at]').forEach((timer) => {
|
||||
const startedAt = Date.parse(timer.getAttribute('data-helpdesk-started-at') || '');
|
||||
|
||||
if (!Number.isFinite(startedAt)) {
|
||||
timer.textContent = '00:00:00';
|
||||
return;
|
||||
}
|
||||
|
||||
timer.textContent = formatElapsed(Math.floor((Date.now() - startedAt) / 1000));
|
||||
});
|
||||
};
|
||||
|
||||
updateTimers();
|
||||
window.setInterval(updateTimers, 1000);
|
||||
document.addEventListener('livewire:navigated', updateTimers);
|
||||
document.addEventListener('livewire:update', updateTimers);
|
||||
document.addEventListener('livewire:updated', updateTimers);
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
Reference in New Issue
Block a user