Files
WMS/Modules/Helpdesk/Resources/views/livewire/ticket-show.blade.php
T
Mark Gregory 373f78ea15 added correct SLA support
Awaiting Response

No qualifying response yet

Due Soon

SLA is approaching its deadline

MET

SLA target achieved on time

BREACHED

SLA target missed
2026-06-23 10:46:32 +01:00

1179 lines
68 KiB
PHP

@php
use Illuminate\Support\Str;
use Modules\Helpdesk\Models\TicketTimeLog;
$currentUser = auth()->user();
$currentCompany = $currentUser?->company;
$canSeeInternalNotes =
(bool) ($currentCompany?->is_system_company ?? false) ||
Str::lower((string) ($currentCompany?->slug ?? '')) === 'mpba-limited' ||
Str::contains(Str::lower((string) ($currentCompany?->name ?? '')), 'mpba');
$visibleCommentCount = $ticket->comments->count();
$ticketTimeLogs = TicketTimeLog::query()
->with('user')
->where('ticket_id', $ticket->getKey())
->orderByDesc('started_at')
->get();
@endphp
<div data-dusk="helpdesk-ticket-show" dusk="helpdesk-ticket-show" class="hd-helpdesk-surface space-y-4">
<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-4 text-white">
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div class="min-w-0">
<div class="mb-2 flex flex-wrap items-center gap-2">
<span class="font-mono text-xs text-blue-100">{{ $ticket->reference }}</span>
<flux:badge color="{{ $ticket->status->color }}">{{ $ticket->status->name }}</flux:badge>
<flux:badge color="{{ $ticket->priority->color }}">{{ $ticket->priority->name }}</flux:badge>
</div>
<h1 class="truncate text-2xl font-semibold tracking-tight">{{ $ticket->subject }}</h1>
<p class="mt-1 text-xs text-blue-100/90">
Raised by {{ $ticket->owner?->name ?? 'Unknown user' }} · Assigned to
{{ $ticket->agent?->name ?? 'Unassigned' }}
</p>
</div>
<div class="flex flex-wrap gap-2">
<a href="{{ route('helpdesk.tickets.index') }}" wire:navigate
class="inline-flex items-center rounded-lg bg-blue-600 px-3 py-2 text-xs font-bold text-white shadow-sm hover:bg-blue-700">
<i class="fa-solid fa-arrow-left mr-2" aria-hidden="true"></i>
Back
</a>
@can('update', $ticket)
<a href="{{ route('helpdesk.tickets.edit', $ticket) }}" wire:navigate
class="inline-flex items-center rounded-lg bg-emerald-600 px-3 py-2 text-xs font-bold text-white shadow-sm hover:bg-emerald-700">
<i class="fa-solid fa-pen-to-square mr-2" aria-hidden="true"></i>
Edit
</a>
@endcan
</div>
</div>
</div>
</div>
<div class="grid gap-4 lg:grid-cols-3">
<div class="space-y-4 lg:col-span-2">
<article
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-r from-slate-950 via-blue-950 to-slate-900 px-5 py-5 text-white sm:px-6">
<div class="flex flex-col gap-4 xl:flex-row xl:items-center xl: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-500/20 text-white ring-1 ring-white/25 shadow-sm">
<i class="fa-solid fa-file-lines text-base" aria-hidden="true"></i>
</span>
<div class="min-w-0 space-y-1">
<p class="text-[0.68rem] font-black uppercase tracking-[0.18em] text-blue-200">Ticket
document</p>
<h2 class="text-xl font-extrabold tracking-tight text-white sm:text-2xl">Request details
</h2>
<p class="max-w-4xl text-sm font-medium leading-5 text-blue-50/90">Original request
content recorded against this helpdesk ticket.</p>
</div>
</div>
<span
class="inline-flex w-fit shrink-0 items-center gap-2 rounded-xl bg-white/15 px-3 py-2 text-xs font-extrabold text-blue-50 ring-1 ring-white/20 shadow-sm">
<i class="fa-solid fa-hashtag" aria-hidden="true"></i>
{{ $ticket->reference }}
</span>
</div>
</div>
<div class="p-5 sm:p-6">
<div
class="hd-ticket-body-card min-h-[10rem] rounded-2xl border border-zinc-200 bg-zinc-50/80 px-5 py-5 text-zinc-900 shadow-inner dark:border-zinc-700 dark:bg-zinc-950/80 dark:text-zinc-100 sm:min-h-[12rem] sm:px-6 sm:py-6">
<div class="prose max-w-none text-sm leading-6 dark:prose-invert sm:text-base sm:leading-7">
{!! $ticket->body !!}
</div>
</div>
</div>
</article>
<details
class="group overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900"
data-helpdesk-supporting-files-card>
<summary
class="flex cursor-pointer list-none items-center justify-between gap-3 bg-gradient-to-r from-slate-950 via-blue-950 to-slate-900 px-4 py-3 text-white marker:hidden sm:px-5">
<div class="flex min-w-0 items-center gap-3">
<span
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-blue-500/25 text-white ring-1 ring-white/20">
<i class="fa-solid fa-paperclip text-sm" aria-hidden="true"></i>
</span>
<div class="min-w-0">
<p class="text-[0.65rem] font-black uppercase tracking-[0.18em] text-blue-200">Attachments
</p>
<h2 class="truncate text-base font-extrabold tracking-tight">Supporting files</h2>
</div>
</div>
<span
class="inline-flex shrink-0 items-center gap-2 rounded-xl bg-white/15 px-3 py-1.5 text-xs font-extrabold ring-1 ring-white/20">
<i class="fa-solid fa-paperclip text-xs" aria-hidden="true"></i>
{{ $ticket->attachments->count() }} file{{ $ticket->attachments->count() === 1 ? '' : 's' }}
<i class="fa-solid fa-chevron-right transition group-open:rotate-90" aria-hidden="true"></i>
</span>
</summary>
<div class="space-y-3 p-4">
@can('update', $ticket)
<form method="POST" action="{{ route('helpdesk.tickets.attachments.store', $ticket) }}"
enctype="multipart/form-data"
class="rounded-xl border border-blue-500/25 bg-slate-950/70 p-3 shadow-sm"
data-helpdesk-ticket-attachment-form>
@csrf
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div class="min-w-0">
<p class="text-xs font-black uppercase tracking-[0.18em] text-blue-200">Add
files</p>
<p class="mt-1 truncate text-xs font-semibold text-slate-200"
data-helpdesk-file-label>
No files selected</p>
</div>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
<label data-helpdesk-file-button
class="hd-attachment-action !rounded-lg !px-3 !py-2 !text-xs">
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
<span>Choose files</span>
<input name="attachments[]" type="file" multiple class="sr-only"
data-helpdesk-file-input>
</label>
<button type="submit"
class="hd-attachment-action hd-attachment-action-upload !rounded-lg !px-3 !py-2 !text-xs">
<i class="fa-solid fa-upload" aria-hidden="true"></i>
<span>Upload</span>
</button>
</div>
</div>
@error('attachments')
<p class="mt-2 text-xs font-semibold text-red-300">{{ $message }}</p>
@enderror
@error('attachments.*')
<p class="mt-2 text-xs font-semibold text-red-300">{{ $message }}</p>
@enderror
</form>
@endcan
@if ($ticket->attachments->isEmpty())
<div
class="rounded-xl border border-dashed border-zinc-300 p-4 text-center text-xs font-semibold text-zinc-500 dark:border-zinc-700">
No supporting files have been attached yet.
</div>
@else
<div class="grid gap-2 sm:grid-cols-2">
@foreach ($ticket->attachments as $attachment)
<a href="{{ route('helpdesk.attachments.download', $attachment->id) }}" target="_blank"
class="group/file flex items-center justify-between gap-3 rounded-lg border border-zinc-200 px-3 py-2 text-xs transition hover:bg-zinc-50 dark:border-zinc-700 dark:hover:bg-zinc-800">
<span class="min-w-0">
<span class="block truncate font-bold text-zinc-900 dark:text-zinc-100">
{{ $attachment->original_name ?? ($attachment->original_filename ?? ($attachment->filename ?? ($attachment->file_name ?? basename($attachment->path ?? ($attachment->file_path ?? ''))))) }}
</span>
<span class="block text-[0.7rem] text-zinc-500">
{{ Number::fileSize($attachment->size ?? ($attachment->size_bytes ?? 0)) }}
</span>
</span>
<span
class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-zinc-100 text-zinc-700 transition group-hover/file:bg-blue-600 group-hover/file:text-white dark:bg-zinc-800 dark:text-zinc-200">
<i class="fa-solid fa-download text-xs" aria-hidden="true"></i>
</span>
</a>
@endforeach
</div>
@endif
</div>
</details>
<section class="space-y-3">
<details
class="overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900"
data-helpdesk-conversation-section>
<summary
class="flex cursor-pointer list-none items-center justify-between gap-3 bg-gradient-to-r from-slate-950 via-blue-950 to-slate-900 px-4 py-3 text-white marker:hidden sm:px-5">
<div class="flex min-w-0 items-center gap-3">
<span
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-blue-500/25 text-white ring-1 ring-white/20">
<i class="fa-solid fa-comments text-sm" aria-hidden="true"></i>
</span>
<div class="min-w-0">
<p class="text-[0.65rem] font-black uppercase tracking-[0.18em] text-blue-200">
Conversation</p>
<h2 class="truncate text-base font-extrabold tracking-tight">Document responses</h2>
</div>
</div>
<span
class="inline-flex shrink-0 items-center gap-2 rounded-xl bg-white/15 px-3 py-1.5 text-xs font-extrabold ring-1 ring-white/20">
{{ $visibleCommentCount }} item{{ $visibleCommentCount === 1 ? '' : 's' }}
<i class="fa-solid fa-chevron-right transition group-open:rotate-90"
aria-hidden="true"></i>
</span>
</summary>
<div class="space-y-3 p-4">
<div class="grid gap-2 md:grid-cols-[1fr_auto]">
<label class="sr-only" for="helpdesk-comment-search">Search comments</label>
<input id="helpdesk-comment-search" type="search" placeholder="Search comments..."
class="block w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-xs text-zinc-900 shadow-sm focus:border-red-500 focus:ring-red-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
data-helpdesk-comment-search>
<div class="flex flex-wrap items-center gap-2">
<button type="button"
class="inline-flex items-center justify-center rounded-lg bg-red-600 px-3 py-2 text-xs font-bold text-white shadow-sm hover:bg-red-700"
data-helpdesk-expand-comments>
<i class="fa-solid fa-up-right-and-down-left-from-center mr-2"
aria-hidden="true"></i>
Expand all
</button>
<button type="button"
class="hidden items-center justify-center rounded-lg bg-blue-600 px-3 py-2 text-xs font-bold text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-blue-500 dark:hover:bg-blue-400 dark:focus:ring-blue-400 dark:focus:ring-offset-zinc-900"
data-helpdesk-collapse-comments>
<i class="fa-solid fa-down-left-and-up-right-to-center mr-2"
aria-hidden="true"></i>
Collapse all
</button>
</div>
</div>
@if ($ticket->comments->isNotEmpty())
@include('helpdesk::partials.comment-thread', [
'comments' => $ticket->comments,
'level' => 0,
'ticket' => $ticket,
'canSeeInternalNotes' => $canSeeInternalNotes,
])
@else
<div
class="rounded-xl border border-dashed border-zinc-300 p-4 text-center text-xs text-zinc-500 dark:border-zinc-700">
No responses yet.
</div>
@endif
</div>
</details>
</section>
<form method="POST" action="{{ route('helpdesk.tickets.comments.store', $ticket) }}"
enctype="multipart/form-data"
class="overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900"
data-helpdesk-comment-form>
@csrf
<div class="bg-gradient-to-r from-slate-950 via-blue-950 to-slate-900 px-5 py-5 text-white sm:px-6">
<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-500/20 text-white ring-1 ring-white/25 shadow-sm">
<i class="fa-solid fa-square-plus text-base" aria-hidden="true"></i>
</span>
<div class="min-w-0 space-y-1">
<p class="text-[0.68rem] font-black uppercase tracking-[0.18em] text-blue-200">Response
document</p>
<h2 class="text-xl font-extrabold tracking-tight text-white sm:text-2xl">Add reply</h2>
<p class="max-w-4xl text-sm font-medium leading-5 text-blue-50/90">Create a response,
internal MPBA note or threaded document reply.</p>
</div>
</div>
</div>
<div class="space-y-5 p-5 sm:p-6">
@if (session('ticket-comment-added'))
<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-800 dark:bg-emerald-950 dark:text-emerald-100">
{{ session('ticket-comment-added') }}
</div>
@endif
@if ($errors->any())
<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-800 dark:bg-red-950 dark:text-red-100">
{{ $errors->first() }}
</div>
@endif
@php
$cannedResponseBodies = $cannedResponses
->mapWithKeys(function ($response) use ($ticket, $currentUser) {
$body = (string) $response->body;
$body = strtr($body, [
'{{ ticket . reference }}' => e((string) ($ticket->reference ?? '')),
'{{ ticket . subject }}' => e((string) ($ticket->subject ?? '')),
'{{ ticket . status }}' => e((string) ($ticket->status?->name ?? '')),
'{{ ticket . priority }}' => e((string) ($ticket->priority?->name ?? '')),
'{{ ticket . category }}' => e((string) ($ticket->category?->name ?? '')),
'{{ ticket . project }}' => e((string) ($ticket->project?->title ?? '')),
'{{ ticket . team }}' => e((string) ($ticket->team?->name ?? '')),
'{{ requester . name }}' => e(
(string) ($ticket->owner?->name ?? ($ticket->user?->name ?? '')),
),
'{{ agent . name }}' => e(
(string) ($ticket->agent?->name ?? ($currentUser?->name ?? '')),
),
'{{ actor . name }}' => e((string) ($currentUser?->name ?? '')),
'{{ ticket . url }}' => e(
route('helpdesk.tickets.show', ['ticket' => $ticket->getKey()]),
),
]);
$body = html_entity_decode($body, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$body = preg_replace('/<\s*(script|style)[^>]*>.*?<\s*\/\s*\1\s*>/is', '', $body) ?: '';
$body =
preg_replace('/\s+on[a-z]+\s*=\s*("[^"]*"|\'[^\']*\'|[^\s>]+)/i', '', $body) ?: '';
$body = preg_replace('/javascript\s*:/i', '', $body) ?: '';
$body = strip_tags(
$body,
'<p><br><strong><b><em><i><u><ul><ol><li><blockquote><pre><code><a>',
);
return [(string) $response->id => trim($body)];
})
->all();
$cannedResponseNames = $cannedResponses
->mapWithKeys(fn($response) => [(string) $response->id => (string) $response->name])
->all();
$oldCommentBody = html_entity_decode(
(string) old('commentBody'),
ENT_QUOTES | ENT_HTML5,
'UTF-8',
);
$oldCommentBody =
preg_replace('/<\s*(script|style)[^>]*>.*?<\s*\/\s*\1\s*>/is', '', $oldCommentBody) ?: '';
$oldCommentBody =
preg_replace('/\s+on[a-z]+\s*=\s*("[^"]*"|\'[^\']*\'|[^\s>]+)/i', '', $oldCommentBody) ?:
'';
$oldCommentBody = preg_replace('/javascript\s*:/i', '', $oldCommentBody) ?: '';
$oldCommentBody = strip_tags(
$oldCommentBody,
'<p><br><strong><b><em><i><u><ul><ol><li><blockquote><pre><code><a>',
);
@endphp
<script type="application/json"
id="helpdesk-canned-response-bodies">@json($cannedResponseBodies)</script>
<script type="application/json"
id="helpdesk-canned-response-names">@json($cannedResponseNames)</script>
@if ($cannedResponses->isNotEmpty())
<div class="grid gap-3 md:grid-cols-[1fr_auto]">
<div>
<label for="ticket-canned-response"
class="mb-2 block text-xs font-bold text-zinc-700 dark:text-zinc-200">Canned
response</label>
<select id="ticket-canned-response" name="canned_response_id"
class="hd-select block min-h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm font-semibold text-zinc-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
data-canned-response-select>
<option value="">Choose a saved response...</option>
@foreach ($cannedResponses as $response)
<option value="{{ $response->id }}">{{ $response->name }}</option>
@endforeach
</select>
</div>
<div class="flex items-end">
<button type="button" dusk="insert-canned-response"
class="inline-flex min-h-10 items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-xs font-bold text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400/70"
data-insert-canned-response>
<i class="fa-solid fa-copy mr-2" aria-hidden="true"></i>
Insert response
</button>
</div>
</div>
@endif
<div>
<label for="ticket-comment-subject"
class="mb-2 block text-xs font-bold text-zinc-700 dark:text-zinc-200">Subject</label>
<input id="ticket-comment-subject" name="subject" value="{{ old('subject') }}"
placeholder="Response subject"
class="block min-h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm font-semibold text-zinc-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
data-helpdesk-comment-subject>
</div>
<div>
<label for="ticket-comment-body-editor"
class="mb-2 block text-xs font-bold text-zinc-700 dark:text-zinc-200">Comment</label>
<div class="mb-2 flex flex-wrap gap-2"
data-helpdesk-html-toolbar="ticket-comment-body-editor">
<button type="button" data-helpdesk-command="bold"
class="rounded-lg border px-2 py-1 text-xs font-black">B
</button>
<button type="button" data-helpdesk-command="italic"
class="rounded-lg border px-2 py-1 text-xs italic">I
</button>
<button type="button" data-helpdesk-command="underline"
class="rounded-lg border px-2 py-1 text-xs underline">U
</button>
<button type="button" data-helpdesk-command="insertUnorderedList"
class="rounded-lg border px-2 py-1 text-xs">List
</button>
<button type="button" data-helpdesk-block="blockquote"
class="rounded-lg border px-2 py-1 text-xs">Quote
</button>
<button type="button" data-helpdesk-block="code"
class="rounded-lg border px-2 py-1 text-xs">Code
</button>
</div>
<div id="ticket-comment-body-editor" dusk="ticket-comment-editor" contenteditable="true"
role="textbox" aria-multiline="true" data-helpdesk-rich-editor
data-helpdesk-sync-target="ticket-comment-body"
class="prose block min-h-[12rem] w-full max-w-none rounded-xl border border-zinc-300 bg-white p-4 text-sm leading-6 text-zinc-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100 dark:prose-invert">
{!! $oldCommentBody !!}</div>
<textarea dusk="comment-body" data-dusk="comment-body" id="ticket-comment-body"
name="commentBody" required
class="sr-only" tabindex="-1" aria-hidden="true">{{ $oldCommentBody }}</textarea>
</div>
@can('update', $ticket)
<div
class="rounded-xl border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-950">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="min-w-0">
<label class="block text-xs font-bold text-zinc-700 dark:text-zinc-200">Attach files
to
this reply</label>
<p class="mt-1 truncate text-xs font-semibold text-zinc-500 dark:text-zinc-400"
data-helpdesk-file-label>No files selected</p>
</div>
<label data-helpdesk-file-button
class="inline-flex cursor-pointer items-center justify-center rounded-lg bg-blue-600 px-3 py-2 text-xs font-bold text-white shadow-sm transition hover:bg-blue-700 focus-within:outline-none focus-within:ring-2 focus-within:ring-blue-400/70">
<i class="fa-solid fa-paperclip mr-2 text-xs" aria-hidden="true"></i>
<span>Choose files</span>
<input name="comment_attachments[]" type="file" multiple class="sr-only"
data-helpdesk-file-input>
</label>
</div>
@error('comment_attachments')
<p class="mt-2 text-xs font-semibold text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
@error('comment_attachments.*')
<p class="mt-2 text-xs font-semibold text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<div class="rounded-xl border border-zinc-200 bg-white p-3 shadow-sm dark:border-zinc-700 dark:bg-zinc-950">
<div class="mb-3 flex items-start justify-between gap-3">
<div>
<p class="text-xs font-extrabold uppercase tracking-[0.16em] text-zinc-500 dark:text-zinc-400">Visibility</p>
<p class="mt-1 text-sm font-semibold text-zinc-700 dark:text-zinc-200">Choose whether this response is customer visible.</p>
</div>
</div>
@if ($canSeeInternalNotes)
<div class="grid gap-3 sm:grid-cols-2">
<label class="flex cursor-pointer items-start gap-3 rounded-xl border border-emerald-300 bg-emerald-50 p-3 text-emerald-950 shadow-sm transition hover:bg-emerald-100 dark:border-emerald-900 dark:bg-emerald-950/60 dark:text-emerald-100">
<input type="radio" name="internal" value="0" @checked(! old('internal')) class="mt-1 border-emerald-400 text-emerald-600 focus:ring-emerald-500">
<span>
<span class="block text-sm font-extrabold">Public reply</span>
<span class="mt-1 block text-xs font-semibold opacity-80">Visible to the requester and counts as the first SLA response.</span>
</span>
</label>
<label class="flex cursor-pointer items-start gap-3 rounded-xl border border-purple-300 bg-purple-50 p-3 text-purple-950 shadow-sm transition hover:bg-purple-100 dark:border-purple-900 dark:bg-purple-950/60 dark:text-purple-100">
<input type="radio" name="internal" value="1" @checked((bool) old('internal')) class="mt-1 border-purple-400 text-purple-600 focus:ring-purple-500">
<span>
<span class="block text-sm font-extrabold">Internal note</span>
<span class="mt-1 block text-xs font-semibold opacity-80">MPBA/support-only note. Does not count as a public SLA response.</span>
</span>
</label>
</div>
@else
<input type="hidden" name="internal" value="0">
<div class="rounded-xl border border-emerald-300 bg-emerald-50 p-3 text-sm font-semibold text-emerald-950 dark:border-emerald-900 dark:bg-emerald-950/60 dark:text-emerald-100">
<i class="fa-solid fa-envelope-circle-check mr-2" aria-hidden="true"></i>
This will be posted as a public reply and will count towards the first-response SLA.
</div>
@endif
</div>
@endcan
<div class="flex justify-end">
<button dusk="post-comment" data-dusk="post-comment" type="submit"
class="inline-flex items-center justify-center rounded-lg bg-emerald-600 px-4 py-2 text-xs font-bold text-white shadow-sm transition hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400/70">
<i class="fa-solid fa-comments mr-2" aria-hidden="true"></i>
Post reply
</button>
</div>
</div>
</form>
<script>
document.addEventListener('click', function (event) {
const insertButton = event.target.closest('[data-insert-canned-response]');
if (insertButton) {
const form = insertButton.closest('[data-helpdesk-comment-form]');
const select = form?.querySelector('[data-canned-response-select]');
const editor = form?.querySelector('[data-helpdesk-rich-editor]');
const textarea = form?.querySelector('textarea[name="commentBody"]');
const selected = select?.selectedOptions?.[0];
const subjectInput = form?.querySelector('[data-helpdesk-comment-subject]');
const selectedId = String(selected?.value || '');
const cannedResponses = getCannedResponseBodies();
const cannedResponseNames = getCannedResponseNames();
const body = normaliseCannedHtml(cannedResponses[selectedId] || selected?.dataset?.body || '');
if (subjectInput && selectedId !== '' && subjectInput.value.trim() === '') {
subjectInput.value = cannedResponseNames[selectedId] || selected?.textContent?.trim() ||
'Response';
subjectInput.dispatchEvent(new Event('input', {
bubbles: true
}));
}
if (editor && textarea && body.trim() !== '') {
insertHtmlAtEnd(editor, body);
syncRichEditor(editor);
editor.focus();
}
return;
}
const toolbarButton = event.target.closest(
'[data-helpdesk-command], [data-helpdesk-block], [data-helpdesk-tag], [data-helpdesk-wrap]');
if (toolbarButton) {
const toolbar = toolbarButton.closest('[data-helpdesk-html-toolbar]');
const editor = document.getElementById(toolbar?.dataset?.helpdeskHtmlToolbar || '');
if (editor?.matches('[contenteditable="true"]')) {
applyRichEditorCommand(editor, toolbarButton);
} else if (editor) {
const tag = toolbarButton.dataset.helpdeskTag;
const before = toolbarButton.dataset.helpdeskWrap || (tag === 'ul' ? '<ul><li>' : `<${tag}>`);
const after = toolbarButton.dataset.helpdeskWrapEnd || (tag === 'ul' ? '</li></ul>' :
`</${tag}>`);
wrapSelection(editor, before, after);
}
return;
}
const expandButton = event.target.closest('[data-helpdesk-expand-comments]');
if (expandButton) {
document.querySelectorAll('[data-helpdesk-comment-card]').forEach((card) => {
card.open = true;
});
updateCommentToggleButtons();
return;
}
const collapseButton = event.target.closest('[data-helpdesk-collapse-comments]');
if (collapseButton) {
document.querySelectorAll('[data-helpdesk-comment-card]').forEach((card) => {
card.open = false;
});
updateCommentToggleButtons();
return;
}
const replyButton = event.target.closest('[data-helpdesk-reply-toggle]');
if (!replyButton) {
return;
}
const target = document.getElementById(replyButton.dataset.helpdeskReplyToggle || '');
if (!target) {
return;
}
target.classList.toggle('hidden');
if (!target.classList.contains('hidden')) {
target.querySelector('textarea')?.focus();
}
});
document.addEventListener('submit', function (event) {
const form = event.target.closest('[data-helpdesk-comment-form]');
if (!form) {
return;
}
form.querySelectorAll('[data-helpdesk-rich-editor]').forEach((editor) => {
syncRichEditor(editor);
});
});
document.addEventListener('input', function (event) {
const richEditor = event.target.closest('[data-helpdesk-rich-editor]');
if (richEditor) {
syncRichEditor(richEditor);
}
const search = event.target.closest('[data-helpdesk-comment-search]');
if (search) {
const term = search.value.toLowerCase().trim();
document.querySelectorAll('[data-helpdesk-comment-card]').forEach((card) => {
const match = term === '' || (card.dataset.helpdeskCommentText || '').includes(term);
card.classList.toggle('hidden', !match);
if (match) {
card.open = true;
}
});
}
const fileInput = event.target.closest('[data-helpdesk-file-input]');
if (fileInput) {
const panel = fileInput.closest(
'[data-helpdesk-ticket-attachment-form], [data-helpdesk-comment-form], details, form');
const label = panel?.querySelector('[data-helpdesk-file-label]');
const count = fileInput.files?.length || 0;
const names = Array.from(fileInput.files || []).map((file) => file.name);
if (label) {
label.textContent = count === 0 ?
'No files selected' :
`${count} file${count === 1 ? '' : 's'} selected${names.length ? ': ' + names.join(', ') : ''}`;
}
}
});
function updateCommentToggleButtons() {
const commentCards = Array.from(document.querySelectorAll('[data-helpdesk-comment-card]'));
const expandButton = document.querySelector('[data-helpdesk-expand-comments]');
const collapseButton = document.querySelector('[data-helpdesk-collapse-comments]');
if (!expandButton || !collapseButton) {
return;
}
const hasExpandableComments = commentCards.some((card) => !card.open);
const hasExpandedComments = commentCards.some((card) => card.open);
expandButton.classList.toggle('hidden', !hasExpandableComments);
expandButton.classList.toggle('inline-flex', hasExpandableComments);
collapseButton.classList.toggle('hidden', !hasExpandedComments || hasExpandableComments);
collapseButton.classList.toggle('inline-flex', hasExpandedComments && !hasExpandableComments);
}
function getCannedResponseBodies() {
const source = document.getElementById('helpdesk-canned-response-bodies');
if (!source) {
return {};
}
try {
return JSON.parse(source.textContent || '{}') || {};
} catch (error) {
console.error('Unable to read canned response bodies.', error);
return {};
}
}
function getCannedResponseNames() {
const source = document.getElementById('helpdesk-canned-response-names');
if (!source) {
return {};
}
try {
return JSON.parse(source.textContent || '{}') || {};
} catch (error) {
console.error('Unable to read canned response names.', error);
return {};
}
}
function insertHtmlAtEnd(editor, html) {
const current = editor.innerHTML.trim();
const cleaned = html.trim();
if (current === '' || current === '<br>') {
editor.innerHTML = cleaned;
return;
}
editor.innerHTML = current + '<p><br></p>' + cleaned;
}
function decodeHtmlEntities(value) {
const textarea = document.createElement('textarea');
textarea.innerHTML = value;
return textarea.value;
}
function normaliseCannedHtml(value) {
let decoded = decodeHtmlEntities(value || '');
for (let i = 0; i < 5; i += 1) {
if (!decoded.includes('&lt;') && !decoded.includes('&gt;') && !decoded.includes('&amp;lt;')) {
break;
}
const next = decodeHtmlEntities(decoded);
if (next === decoded) {
break;
}
decoded = next;
}
decoded = decoded.trim();
if (decoded === '') {
return '';
}
if (/<[a-z][\s\S]*>/i.test(decoded)) {
return decoded;
}
return decoded
.split(/\n{2,}/)
.map((paragraph) => paragraph.trim())
.filter((paragraph) => paragraph !== '')
.map((paragraph) => '<p>' + escapeHtml(paragraph).replace(/\n/g, '<br>') + '</p>')
.join('');
}
function syncRichEditor(editor) {
const target = document.getElementById(editor.dataset.helpdeskSyncTarget || '');
if (!target) {
return;
}
target.value = editor.innerHTML.trim();
target.dispatchEvent(new Event('input', {
bubbles: true
}));
target.dispatchEvent(new Event('change', {
bubbles: true
}));
}
function applyRichEditorCommand(editor, button) {
editor.focus();
const command = button.dataset.helpdeskCommand;
if (command) {
document.execCommand(command, false, null);
syncRichEditor(editor);
return;
}
const block = button.dataset.helpdeskBlock;
if (block === 'blockquote') {
document.execCommand('formatBlock', false, 'blockquote');
syncRichEditor(editor);
return;
}
if (block === 'code') {
const selection = window.getSelection();
const selected = selection && selection.toString() !== '' ? selection.toString() : 'code';
document.execCommand('insertHTML', false, '<code>' + escapeHtml(selected) + '</code>');
syncRichEditor(editor);
}
}
function escapeHtml(value) {
const span = document.createElement('span');
span.textContent = value;
return span.innerHTML;
}
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('[data-helpdesk-rich-editor]').forEach((editor) => syncRichEditor(editor));
updateCommentToggleButtons();
});
function wrapSelection(textarea, before, after) {
const start = textarea.selectionStart || 0;
const end = textarea.selectionEnd || 0;
const selected = textarea.value.substring(start, end) || 'text';
textarea.value = textarea.value.substring(0, start) + before + selected + after + textarea.value.substring(end);
textarea.focus();
textarea.setSelectionRange(start + before.length, start + before.length + selected.length);
textarea.dispatchEvent(new Event('input', {
bubbles: true
}));
}
</script>
</div>
<aside class="space-y-4">
<div
class="rounded-xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="-mx-4 -mt-4 mb-4 rounded-t-xl bg-blue-700 px-4 py-4 text-white">
<div class="flex items-center gap-3">
<span
class="inline-flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-blue-950 text-white ring-1 ring-blue-300/60 shadow-lg">
<i class="fa-solid fa-file-lines text-lg" aria-hidden="true"></i>
</span>
<div class="min-w-0">
<h2 class="text-lg font-black uppercase tracking-wide">Ticket details</h2>
<p class="text-xs font-semibold text-blue-100">Key information about this ticket</p>
</div>
</div>
</div>
<dl class="space-y-2 text-xs">
<div class="flex justify-between gap-3">
<dt class="text-zinc-500">Category</dt>
<dd>{{ $ticket->category?->name ?? 'None' }}</dd>
</div>
<div class="flex justify-between gap-3">
<dt class="text-zinc-500">Project</dt>
<dd>{{ $ticket->project?->title ?? 'None' }}</dd>
</div>
<div class="flex justify-between gap-3">
<dt class="text-zinc-500">Product/service</dt>
<dd>{{ $ticket->product?->name ?? 'None' }}</dd>
</div>
<div class="flex justify-between gap-3">
<dt class="text-zinc-500">Team</dt>
<dd>{{ $ticket->team?->name ?? 'None' }}</dd>
</div>
<div class="flex justify-between gap-3">
<dt class="text-zinc-500">Created</dt>
<dd>{{ $ticket->created_at->format('d M Y H:i') }}</dd>
</div>
<div class="flex justify-between gap-3">
<dt class="text-zinc-500">Due</dt>
<dd>{{ $ticket->due_at?->format('d M Y') ?? 'No due date' }}</dd>
</div>
</dl>
</div>
<div
class="rounded-xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="-mx-4 -mt-4 mb-4 rounded-t-xl bg-red-700 px-4 py-4 text-white">
<div class="flex items-center gap-3">
<span
class="inline-flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-red-950 text-white ring-1 ring-red-300/60 shadow-lg">
<i class="fa-solid fa-clock text-lg" aria-hidden="true"></i>
</span>
<div class="min-w-0">
<h2 class="text-lg font-black uppercase tracking-wide">SLA metrics</h2>
<p class="text-xs font-semibold text-red-100">Service level agreement overview</p>
</div>
</div>
</div>
<div class="space-y-3 text-xs">
<div class="rounded-lg border {{ $this->firstResponseSlaClasses() }} p-3">
<div class="flex items-start justify-between gap-3">
<div>
<div class="text-[0.68rem] font-semibold uppercase tracking-wide">First response</div>
<div class="mt-1 text-sm font-black">{{ $this->firstResponseSlaHeadline() }}</div>
<div class="mt-1 text-[0.68rem] font-semibold opacity-80">{{ $this->firstResponseSlaDetail() }}</div>
</div>
@if ($ticket->first_response_at)
<span class="rounded-full bg-white/60 px-2 py-1 text-[0.6rem] font-black uppercase tracking-wide dark:bg-black/20">
Met
</span>
@endif
</div>
@if ($ticket->first_response_at)
<dl class="mt-3 grid gap-1 border-t border-current/15 pt-2 text-[0.65rem] font-semibold opacity-85">
<div class="flex justify-between gap-3">
<dt>Responder</dt>
<dd class="text-right">{{ $this->firstResponderName() }}</dd>
</div>
<div class="flex justify-between gap-3">
<dt>Responded at</dt>
<dd class="text-right">{{ $ticket->first_response_at->format('d M Y H:i') }}</dd>
</div>
</dl>
@endif
</div>
<div class="rounded-lg border {{ $this->resolutionSlaClasses() }} p-3">
<div class="flex items-start justify-between gap-3">
<div>
<div class="text-[0.68rem] font-semibold uppercase tracking-wide">Resolution</div>
<div class="mt-1 text-sm font-black">{{ $this->resolutionSlaHeadline() }}</div>
<div class="mt-1 text-[0.68rem] font-semibold opacity-80">{{ $this->resolutionSlaDetail() }}</div>
</div>
@if ($ticket->completed_at)
<span class="rounded-full bg-white/60 px-2 py-1 text-[0.6rem] font-black uppercase tracking-wide dark:bg-black/20">
Closed
</span>
@endif
</div>
</div>
<div class="grid grid-cols-2 gap-2 text-center">
<div
class="rounded-lg border border-blue-300 bg-blue-50 p-2 text-blue-900 shadow-sm dark:border-blue-900 dark:bg-blue-950 dark:text-blue-100">
<div class="text-lg font-extrabold">{{ $ticket->public_replies_count }}</div>
<div class="text-[0.68rem] font-semibold text-blue-700 dark:text-blue-200">Public replies
</div>
</div>
<div
class="rounded-lg border border-emerald-300 bg-emerald-50 p-2 text-emerald-900 shadow-sm dark:border-emerald-900 dark:bg-emerald-950 dark:text-emerald-100">
<div class="text-sm font-extrabold leading-tight">
{{ $this->formattedTimeSpent() }}
</div>
<div class="mt-0.5 text-[0.6rem] font-semibold text-emerald-600 dark:text-emerald-300">
{{ number_format((int) $ticket->time_spent_minutes) }} total minutes
</div>
<div class="text-[0.65rem] font-semibold text-emerald-700 dark:text-emerald-200">
Time logged
</div>
<button type="button"
onclick="document.getElementById('ticket-time-log-modal').showModal()"
class="mt-2 inline-flex w-full items-center justify-center rounded-lg bg-emerald-600 px-2 py-1.5 text-[0.65rem] font-black text-white shadow-sm hover:bg-emerald-700">
<i class="fa-solid fa-clock-rotate-left mr-2" aria-hidden="true"></i>
View log
</button>
</div>
</div>
<div class="rounded-lg border border-zinc-200 bg-zinc-50 p-3 text-zinc-800 shadow-sm dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100">
<div class="flex items-center justify-between gap-3">
<div class="text-[0.68rem] font-black uppercase tracking-wide">SLA audit trail</div>
<div class="text-[0.62rem] font-semibold text-zinc-500 dark:text-zinc-400">
{{ $ticket->slaEvents->count() }} events
</div>
</div>
<div class="mt-3 space-y-2">
@forelse ($ticket->slaEvents->take(5) as $event)
<div class="rounded-md border border-zinc-200 bg-white p-2 dark:border-zinc-700 dark:bg-zinc-900">
<div class="flex items-start justify-between gap-3">
<div>
<div class="font-bold">{{ $this->formatSlaEventName($event->event) }}</div>
<div class="mt-0.5 text-[0.62rem] text-zinc-500 dark:text-zinc-400">
{{ $event->occurred_at?->format('d M Y H:i') ?? 'Not timestamped' }}
</div>
</div>
<span class="rounded-full px-2 py-0.5 text-[0.58rem] font-black uppercase {{ $event->breached ? 'bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-100' : 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-100' }}">
{{ $event->breached ? 'Breached' : 'Met' }}
</span>
</div>
@if ($event->due_at || filled(data_get($event->meta, 'actor_name')))
<dl class="mt-2 grid gap-1 text-[0.6rem] text-zinc-600 dark:text-zinc-300">
@if ($event->due_at)
<div class="flex justify-between gap-2">
<dt>Due</dt>
<dd>{{ $event->due_at->format('d M Y H:i') }}</dd>
</div>
@endif
@if (filled(data_get($event->meta, 'actor_name')))
<div class="flex justify-between gap-2">
<dt>Actor</dt>
<dd>{{ data_get($event->meta, 'actor_name') }}</dd>
</div>
@endif
</dl>
@endif
</div>
@empty
<div class="rounded-md border border-dashed border-zinc-300 p-3 text-center text-[0.65rem] font-semibold text-zinc-500 dark:border-zinc-700 dark:text-zinc-400">
No SLA events have been recorded yet.
</div>
@endforelse
</div>
</div>
</div>
</div>
<div
class="rounded-xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="-mx-4 -mt-4 mb-4 rounded-t-xl bg-emerald-700 px-4 py-4 text-white">
<div class="flex items-center gap-3">
<span
class="inline-flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-emerald-950 text-white ring-1 ring-emerald-300/60 shadow-lg">
<i class="fa-solid fa-user text-lg" aria-hidden="true"></i>
</span>
<div class="min-w-0">
<h2 class="text-lg font-black uppercase tracking-wide">Requester profile</h2>
<p class="text-xs font-semibold text-emerald-100">Information about the requester</p>
</div>
</div>
</div>
<dl class="space-y-2 text-xs">
<div class="flex justify-between gap-3">
<dt class="text-zinc-500">Name</dt>
<dd>{{ $ticket->owner?->name ?? 'Unknown' }}</dd>
</div>
<div class="flex justify-between gap-3">
<dt class="text-zinc-500">Email</dt>
<dd class="truncate">{{ $ticket->owner?->email ?? 'None' }}</dd>
</div>
<div class="flex justify-between gap-3">
<dt class="text-zinc-500">Company</dt>
<dd>{{ $ticket->owner?->company?->name ?? 'Current company' }}</dd>
</div>
</dl>
@if ($ticket->tags->isNotEmpty())
<div class="mt-3 flex flex-wrap gap-2">
@foreach ($ticket->tags as $tag)
<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>
@endforeach
</div>
@endif
</div>
@if ($ticket->checklistItems->isNotEmpty())
<div
class="rounded-xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
@php
$completedChecks = $ticket->checklistItems->where('is_completed', true)->count();
@endphp
<div class="-mx-4 -mt-4 mb-4 rounded-t-xl bg-amber-600 px-4 py-4 text-white">
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3">
<span
class="inline-flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-amber-950 text-white ring-1 ring-amber-300/60 shadow-lg">
<i class="fa-solid fa-list-check text-lg" aria-hidden="true"></i>
</span>
<div class="min-w-0">
<h2 class="text-lg font-black uppercase tracking-wide">Checklist</h2>
<p class="text-xs font-semibold text-amber-100">Completion progress</p>
</div>
</div>
<flux:badge color="blue">{{ $completedChecks }}/{{ $ticket->checklistItems->count() }}
</flux:badge>
</div>
</div>
<div class="space-y-2">
@foreach ($ticket->checklistItems as $item)
<label
class="flex items-start gap-2 rounded-lg border border-zinc-200 p-2 text-xs dark:border-zinc-700">
<input type="checkbox" wire:click="toggleChecklistItem({{ $item->id }})"
@checked($item->is_completed) class="mt-1 rounded border-zinc-300">
<span
class="{{ $item->is_completed ? 'text-zinc-500 line-through' : 'text-zinc-800 dark:text-zinc-100' }}">{{ $item->title }}</span>
</label>
@endforeach
</div>
</div>
@endif
<div
class="rounded-xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="-mx-4 -mt-4 mb-4 rounded-t-xl bg-violet-700 px-4 py-4 text-white">
<div class="flex 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">Git commits</h2>
<p class="text-xs font-semibold text-violet-100">Code commits linked to this ticket</p>
</div>
</div>
</div>
<div class="space-y-2">
@forelse ($ticket->gitCommits as $commit)
<a href="{{ $commit->url ?: '#' }}" target="_blank"
class="block rounded-lg border border-zinc-100 p-2 dark:border-zinc-800">
<div class="font-mono text-[0.68rem] text-zinc-500">{{ $commit->short_hash }} ·
{{ $commit->repository?->name }}</div>
<div class="mt-1 text-xs font-medium text-zinc-900 dark:text-zinc-100">
{{ $commit->message }}</div>
</a>
@empty
<div
class="rounded-lg border border-dashed border-zinc-300 p-3 text-center text-xs text-zinc-500 dark:border-zinc-700">
No commits linked.
</div>
@endforelse
</div>
</div>
@can('update', $ticket)
@include('helpdesk::partials.ticket-controls')
@endcan
</aside>
</div>
<dialog id="ticket-time-log-modal"
class="w-full max-w-2xl rounded-2xl border border-zinc-200 bg-white p-0 shadow-2xl backdrop:bg-slate-950/70 dark:border-zinc-700 dark:bg-zinc-900">
<div class="bg-gradient-to-r from-emerald-800 via-emerald-700 to-teal-700 px-5 py-4 text-white">
<div class="flex items-center justify-between gap-3">
<div class="min-w-0">
<h2 class="text-lg font-black tracking-tight">Time log history</h2>
<p class="text-xs font-semibold text-emerald-100">
{{ $ticketTimeLogs->count() }} entries · {{ $this->formattedTimeSpent() }}
</p>
</div>
<button type="button" onclick="document.getElementById('ticket-time-log-modal').close()"
class="shrink-0 rounded-lg bg-white/15 px-3 py-2 text-xs font-black text-white hover:bg-white/25">
Close
</button>
</div>
</div>
<div class="max-h-[70vh] overflow-y-auto p-5">
<div class="space-y-3">
@forelse ($ticketTimeLogs as $timeLog)
<div
class="rounded-xl border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-950">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<p class="text-sm font-bold text-zinc-900 dark:text-zinc-100">
{{ $timeLog->description ?: 'Time logged' }}
</p>
<p class="mt-1 text-xs font-semibold text-zinc-500 dark:text-zinc-400">
{{ $timeLog->user?->name ?? 'Unknown user' }}
·
{{ $timeLog->logged_at?->format('d M Y H:i') ?? $timeLog->created_at?->format('d M Y H:i') }}
</p>
</div>
@php
$timeLogSeconds = max(0, (int) ($timeLog->seconds ?? ((int) $timeLog->minutes * 60)));
@endphp
<span class="shrink-0 rounded-lg bg-emerald-600 px-3 py-1.5 text-xs font-black text-white">
{{ gmdate('H:i:s', $timeLogSeconds) }}
</span>
</div>
</div>
@empty
<div
class="rounded-xl border border-dashed border-zinc-300 p-5 text-center text-xs font-semibold text-zinc-500 dark:border-zinc-700">
No time log entries yet.
</div>
@endforelse
</div>
</div>
</dialog>
</div>