major updates

This commit is contained in:
Mark Gregory
2026-06-17 11:26:08 +01:00
parent 3b1fc48a3d
commit c4646d2f5c
10 changed files with 256 additions and 51 deletions
@@ -61,7 +61,7 @@
</div>
<div class="p-6">
<div class="prose max-w-none rounded-2xl border border-zinc-200 bg-zinc-50 p-5 text-zinc-900 dark:prose-invert dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100">
<div class="prose max-w-none rounded-2xl border border-zinc-200 bg-zinc-50 p-6 text-zinc-900 shadow-inner dark:prose-invert dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100 sm:min-h-40">
{!! $ticket->body !!}
</div>
</div>
@@ -101,16 +101,19 @@
@endif
<section class="space-y-4">
<details open class="overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<summary class="flex cursor-pointer list-none items-center justify-between gap-4 bg-gradient-to-r from-slate-950 via-slate-900 to-red-950 px-5 py-4 text-white marker:hidden">
<div>
<h2 class="flex items-center gap-2 text-lg font-extrabold">
<i class="fa-solid fa-comments" aria-hidden="true"></i>
Conversation
</h2>
<p class="mt-1 text-sm font-medium text-red-50/85">Lotus Notes-style document responses and responses to responses.</p>
<details class="overflow-hidden rounded-2xl 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-4 bg-gradient-to-r from-slate-950 via-blue-950 to-slate-900 px-5 py-5 text-white marker:hidden">
<div class="flex min-w-0 items-center gap-4">
<span class="inline-flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-blue-500/25 text-white ring-1 ring-white/20">
<i class="fa-solid fa-comments text-lg" aria-hidden="true"></i>
</span>
<div class="min-w-0">
<p class="text-xs font-black uppercase tracking-[0.22em] text-blue-200">Conversation</p>
<h2 class="mt-1 text-xl font-extrabold tracking-tight">Document responses</h2>
<p class="mt-1 text-sm font-medium text-blue-50/85">Lotus Notes-style document responses and replies.</p>
</div>
</div>
<span class="inline-flex items-center gap-2 rounded-full bg-white/15 px-3 py-1 text-xs font-extrabold ring-1 ring-white/20">
<span class="inline-flex items-center gap-3 rounded-full bg-white/15 px-4 py-2 text-sm 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>
@@ -119,7 +122,7 @@
<div class="space-y-4 p-5">
<div class="grid gap-3 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, GUID, UNID, author or text..." class="block w-full rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm 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>
<input id="helpdesk-comment-search" type="search" placeholder="Search comments by subject, author, text or technical reference..." class="block w-full rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm 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-xl bg-red-600 px-4 py-2 text-sm 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>
@@ -183,7 +186,11 @@
})->all();
@endphp
@php
$cannedResponseNames = $cannedResponses->mapWithKeys(fn ($response) => [(string) $response->id => (string) $response->name])->all();
@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="mb-4 grid gap-3 md:grid-cols-[1fr_auto]">
@@ -213,6 +220,11 @@
$oldCommentBody = strip_tags($oldCommentBody, '<p><br><strong><b><em><i><u><ul><ol><li><blockquote><pre><code><a>');
@endphp
<div class="mb-4">
<label for="ticket-comment-subject" class="mb-2 block text-sm 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 w-full rounded-xl border border-zinc-300 bg-white p-3 text-sm font-semibold text-zinc-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100" data-helpdesk-comment-subject>
</div>
<div>
<label for="ticket-comment-body-editor" class="mb-2 block text-sm font-medium 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">
@@ -264,8 +276,16 @@
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 body = normaliseCannedHtml(cannedResponses[String(selected?.value || '')] || selected?.dataset?.body || '');
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);
@@ -409,6 +429,21 @@
}
}
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();
@@ -526,7 +561,12 @@
<aside class="space-y-6">
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<h2 class="mb-4 text-base font-semibold text-zinc-900 dark:text-white">Ticket details</h2>
<div class="mb-5 border-b border-blue-200 pb-3 dark:border-blue-900/70">
<h2 class="flex items-center gap-3 text-base font-extrabold text-zinc-900 dark:text-white">
<span class="inline-flex h-9 w-9 items-center justify-center rounded-xl bg-blue-600 text-white shadow-sm"><i class="fa-solid fa-file-lines" aria-hidden="true"></i></span>
Ticket details
</h2>
</div>
<dl class="space-y-3 text-sm">
<div class="flex justify-between gap-4"><dt class="text-zinc-500">Category</dt><dd>{{ $ticket->category?->name ?? 'None' }}</dd></div>
<div class="flex justify-between gap-4"><dt class="text-zinc-500">Project</dt><dd>{{ $ticket->project?->title ?? 'None' }}</dd></div>
@@ -538,7 +578,12 @@
</div>
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<h2 class="mb-4 text-base font-semibold text-zinc-900 dark:text-white">SLA metrics</h2>
<div class="mb-5 border-b border-red-200 pb-3 dark:border-red-900/70">
<h2 class="flex items-center gap-3 text-base font-extrabold text-zinc-900 dark:text-white">
<span class="inline-flex h-9 w-9 items-center justify-center rounded-xl bg-red-600 text-white shadow-sm"><i class="fa-solid fa-clock" aria-hidden="true"></i></span>
SLA metrics
</h2>
</div>
<div class="grid gap-3 text-sm">
<div class="rounded-xl border {{ $ticket->sla_first_response_breached ? 'border-red-300 bg-red-50 text-red-900 dark:border-red-900 dark:bg-red-950 dark:text-red-100' : 'border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-900 dark:bg-emerald-950 dark:text-emerald-100' }} p-3">
<div class="text-xs font-semibold uppercase tracking-wide">First response</div>
@@ -558,7 +603,12 @@
</div>
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<h2 class="mb-4 text-base font-semibold text-zinc-900 dark:text-white">Requester profile</h2>
<div class="mb-5 border-b border-emerald-200 pb-3 dark:border-emerald-900/70">
<h2 class="flex items-center gap-3 text-base font-extrabold text-zinc-900 dark:text-white">
<span class="inline-flex h-9 w-9 items-center justify-center rounded-xl bg-emerald-600 text-white shadow-sm"><i class="fa-solid fa-user" aria-hidden="true"></i></span>
Requester profile
</h2>
</div>
<dl class="space-y-3 text-sm">
<div class="flex justify-between gap-4"><dt class="text-zinc-500">Name</dt><dd>{{ $ticket->owner?->name ?? 'Unknown' }}</dd></div>
<div class="flex justify-between gap-4"><dt class="text-zinc-500">Email</dt><dd class="truncate">{{ $ticket->owner?->email ?? 'None' }}</dd></div>
@@ -10,8 +10,18 @@
$currentTicket = $ticket ?? $comment->ticket ?? null;
$replyCount = $commentReplies->count();
$isInternal = (bool) $comment->is_internal;
$borderClass = $isInternal ? 'border-amber-300 dark:border-amber-700' : 'border-red-200 dark:border-red-900/70';
$railClass = $isInternal ? 'bg-amber-500' : 'bg-red-500';
$borderClass = $isInternal ? 'border-purple-400/70 dark:border-purple-600/70' : 'border-zinc-200 dark:border-zinc-700';
$railClass = $isInternal ? 'bg-purple-500' : ((int) $level % 3 === 0 ? 'bg-red-500' : (((int) $level % 3 === 1) ? 'bg-blue-500' : 'bg-emerald-500'));
$commentSubject = trim((string) ($comment->subject ?? ''));
if ($commentSubject === '') {
$plainSubject = trim((string) ($comment->body_text ?? strip_tags((string) $comment->body)));
$commentSubject = Str::limit($plainSubject !== '' ? $plainSubject : 'Response', 72);
}
if ($isInternal && ! Str::startsWith(Str::lower($commentSubject), 'internal')) {
$commentSubject = 'Internal note';
}
$renderedBody = html_entity_decode((string) $comment->body, ENT_QUOTES | ENT_HTML5, 'UTF-8');
while (str_contains($renderedBody, '&lt;') || str_contains($renderedBody, '&gt;') || str_contains($renderedBody, '&amp;lt;')) {
@@ -49,32 +59,34 @@
@endphp
@if (! $isInternal || $canSeeInternalNotes)
<details class="group rounded-2xl border {{ $borderClass }} bg-white shadow-sm transition dark:bg-zinc-900" data-helpdesk-comment-card data-helpdesk-comment-text="{{ e(Str::lower(strip_tags((string) $comment->body).' '.($comment->body_text ?? '').' '.($commentUser?->name ?? '').' '.($comment->unid ?? '').' '.($comment->guid ?? ''))) }}" style="margin-left: {{ min((int) $level, 4) * 1.25 }}rem">
<details class="group rounded-2xl border {{ $borderClass }} bg-white shadow-sm transition dark:bg-zinc-900" data-helpdesk-comment-card data-helpdesk-comment-text="{{ e(Str::lower($commentSubject.' '.strip_tags((string) $comment->body).' '.($comment->body_text ?? '').' '.($commentUser?->name ?? '').' '.($comment->unid ?? '').' '.($comment->guid ?? ''))) }}" style="margin-left: {{ min((int) $level, 4) * 1.25 }}rem">
<summary class="flex cursor-pointer list-none items-start gap-3 rounded-2xl p-4 marker:hidden">
<span class="mt-1 inline-flex h-12 w-2 shrink-0 rounded-full {{ $railClass }}" aria-hidden="true"></span>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="font-bold text-zinc-900 dark:text-zinc-100">{{ $commentUser?->name ?? 'Unknown user' }}</span>
<span class="text-xs font-semibold text-zinc-500">{{ $comment->created_at?->format('d M Y H:i') }}</span>
<span class="rounded-full bg-blue-100 px-2.5 py-1 text-[11px] font-extrabold text-blue-800 dark:bg-blue-950 dark:text-blue-100">{{ $replyCount }} response{{ $replyCount === 1 ? '' : 's' }}</span>
@if ($commentAttachments->isNotEmpty())
<span class="rounded-full bg-emerald-100 px-2.5 py-1 text-[11px] font-extrabold text-emerald-800 dark:bg-emerald-950 dark:text-emerald-100">
<i class="fa-solid fa-paperclip mr-1" aria-hidden="true"></i>{{ $commentAttachments->count() }}
</span>
@endif
@if ($isInternal)
<span class="rounded-full bg-amber-100 px-2.5 py-1 text-[11px] font-extrabold text-amber-800 dark:bg-amber-950 dark:text-amber-100">MPBA internal</span>
@endif
</div>
<div class="mt-1 flex flex-wrap items-center gap-2 text-[11px] font-mono text-zinc-400">
@if (! blank($comment->unid))
<span>UNID {{ Str::limit((string) $comment->unid, 16, '') }}</span>
@endif
@if (! blank($comment->guid))
<span>GUID {{ Str::limit((string) $comment->guid, 18, '') }}</span>
@endif
<span class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-zinc-100 text-zinc-700 ring-1 ring-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:ring-zinc-700">
<i class="fa-solid {{ $isInternal ? 'fa-note-sticky' : 'fa-envelope' }}" aria-hidden="true"></i>
</span>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="truncate text-base font-extrabold text-zinc-900 dark:text-zinc-100">{{ $commentSubject }}</span>
<span class="rounded-full bg-zinc-100 px-2.5 py-1 text-[11px] font-extrabold text-zinc-700 dark:bg-zinc-800 dark:text-zinc-100">{{ $replyCount }} {{ $replyCount === 1 ? 'reply' : 'replies' }}</span>
@if ($commentAttachments->isNotEmpty())
<span class="rounded-full bg-emerald-100 px-2.5 py-1 text-[11px] font-extrabold text-emerald-800 dark:bg-emerald-950 dark:text-emerald-100">
<i class="fa-solid fa-paperclip mr-1" aria-hidden="true"></i>{{ $commentAttachments->count() }}
</span>
@endif
@if ($isInternal)
<span class="rounded-full bg-red-600 px-2.5 py-1 text-[11px] font-extrabold uppercase tracking-wide text-white">Internal</span>
@endif
</div>
<div class="mt-1 flex flex-wrap items-center gap-2 text-xs font-semibold text-zinc-500 dark:text-zinc-400">
<span>{{ $commentUser?->name ?? 'Unknown user' }}</span>
<span aria-hidden="true">•</span>
<span>{{ $comment->created_at?->format('d M Y H:i') }}</span>
</div>
</div>
</div>
</div>
@@ -116,6 +128,10 @@
<form id="{{ $replyFormId }}" method="POST" action="{{ route('helpdesk.tickets.comments.store', $currentTicket) }}" enctype="multipart/form-data" class="mt-4 hidden rounded-xl border border-red-200 bg-red-50/60 p-4 dark:border-red-900 dark:bg-red-950/20" data-helpdesk-comment-form>
@csrf
<input type="hidden" name="parent_id" value="{{ $comment->id }}">
<div class="mb-3">
<label for="reply-subject-{{ $comment->id }}" class="mb-2 block text-sm font-bold text-zinc-700 dark:text-zinc-200">Subject</label>
<input id="reply-subject-{{ $comment->id }}" name="subject" value="{{ old('subject', 'RE: '.$commentSubject) }}" class="block w-full rounded-xl border border-zinc-300 bg-white p-3 text-sm font-semibold text-zinc-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100">
</div>
<label for="reply-body-{{ $comment->id }}" class="mb-2 block text-sm font-bold text-zinc-700 dark:text-zinc-200">Response to response</label>
<div class="mb-2 flex flex-wrap gap-2" data-helpdesk-html-toolbar="reply-body-{{ $comment->id }}">
<button type="button" data-helpdesk-tag="strong" class="rounded-lg border px-2 py-1 text-xs font-black">B</button>