phases one of time logged now with log display

This commit is contained in:
Mark Gregory
2026-06-22 14:22:58 +01:00
parent bc213e0c6e
commit 0159a638f6
6 changed files with 195 additions and 175 deletions
+38 -67
View File
@@ -5,8 +5,8 @@ namespace Modules\Helpdesk\Livewire;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Livewire\Component; use Livewire\Component;
use Modules\Helpdesk\Models\Ticket; use Modules\Helpdesk\Models\Ticket;
@@ -25,7 +25,7 @@ class TicketTimer extends Component
public int $elapsedSeconds = 0; public int $elapsedSeconds = 0;
/** /**
* Mount the ticket timer component. * Mount the ticket timer and load any active database-backed timer.
*/ */
public function mount(Ticket $ticket): void public function mount(Ticket $ticket): void
{ {
@@ -34,7 +34,7 @@ class TicketTimer extends Component
} }
/** /**
* Start a user-specific timer for this ticket. * Start a database-backed timer for the current authenticated user.
* *
* @throws \Illuminate\Validation\ValidationException * @throws \Illuminate\Validation\ValidationException
*/ */
@@ -42,51 +42,47 @@ class TicketTimer extends Component
{ {
$user = $this->authenticatedUser(); $user = $this->authenticatedUser();
Gate::authorize('update', $this->ticket);
if ($this->activeUserTimerQuery($user)->exists()) { if ($this->activeUserTimerQuery($user)->exists()) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'timer' => 'You already have a running timer. End it before starting another one.', 'timer' => 'You already have a running timer.',
]);
}
if ($this->activeAnyTicketTimerQuery($user)->exists()) {
throw ValidationException::withMessages([
'timer' => 'You already have a running timer on another ticket. End it before starting another one.',
]); ]);
} }
$startedAt = Carbon::now(); $startedAt = Carbon::now();
TicketTimeLog::query()->create([ try {
'ticket_id' => $this->ticket->getKey(), $timer = TicketTimeLog::query()->create([
'user_id' => $user->getAuthIdentifier(), 'ticket_id' => $this->ticket->getKey(),
'minutes' => 0, 'user_id' => $user->getAuthIdentifier(),
'seconds' => 0, 'minutes' => 0,
'description' => $this->normalisedDescription(), 'seconds' => 0,
'started_at' => $startedAt, 'description' => $this->normalisedDescription(),
'ended_at' => null, 'started_at' => $startedAt,
'logged_at' => $startedAt, 'ended_at' => null,
]); 'logged_at' => $startedAt,
]);
} catch (QueryException $exception) {
throw ValidationException::withMessages([
'timer' => 'You already have a running timer.',
]);
}
$this->activeTimer = $timer->fresh();
$this->elapsedSeconds = 0;
$this->description = ''; $this->description = '';
$this->message = 'Timer started.'; $this->message = 'Timer started.';
$this->refreshActiveTimer();
$this->dispatch('timer-started'); $this->dispatch('timer-started');
} }
/** /**
* End the active user timer and add the elapsed time to the ticket. * End the current user's active timer and persist seconds/minutes.
* *
* @throws \Illuminate\Validation\ValidationException * @throws \Illuminate\Validation\ValidationException
*/ */
public function endTimer(): void public function endTimer(): void
{ {
$user = $this->authenticatedUser(); $user = $this->authenticatedUser();
Gate::authorize('update', $this->ticket);
$timer = $this->activeUserTimerQuery($user)->first(); $timer = $this->activeUserTimerQuery($user)->first();
if (! $timer instanceof TicketTimeLog) { if (! $timer instanceof TicketTimeLog) {
@@ -108,7 +104,14 @@ class TicketTimer extends Component
'logged_at' => $endedAt, 'logged_at' => $endedAt,
])->save(); ])->save();
$this->updateTicketTimeSpentFromSeconds(); $totalSeconds = (int) TicketTimeLog::query()
->where('ticket_id', $this->ticket->getKey())
->whereNotNull('ended_at')
->sum('seconds');
$this->ticket->forceFill([
'time_spent_minutes' => (int) ceil($totalSeconds / 60),
])->save();
$this->activeTimer = null; $this->activeTimer = null;
$this->elapsedSeconds = 0; $this->elapsedSeconds = 0;
@@ -119,15 +122,15 @@ class TicketTimer extends Component
} }
/** /**
* Render the ticket timer view. * Render the timer view.
*/ */
public function render(): mixed public function render()
{ {
return view('helpdesk::livewire.ticket-timer'); return view('helpdesk::livewire.ticket-timer');
} }
/** /**
* Refresh the active timer from the database. * Refresh the active timer state from the database.
*/ */
private function refreshActiveTimer(): void private function refreshActiveTimer(): void
{ {
@@ -142,7 +145,7 @@ class TicketTimer extends Component
$this->activeTimer = $this->activeUserTimerQuery($user)->first(); $this->activeTimer = $this->activeUserTimerQuery($user)->first();
$this->elapsedSeconds = $this->activeTimer instanceof TicketTimeLog $this->elapsedSeconds = $this->activeTimer instanceof TicketTimeLog
? (int) $this->activeTimer->started_at->diffInSeconds(Carbon::now()) ? $this->activeTimer->started_at->diffInSeconds(Carbon::now())
: 0; : 0;
} }
@@ -157,38 +160,6 @@ class TicketTimer extends Component
->whereNull('ended_at'); ->whereNull('ended_at');
} }
/**
* Build the active timer query for the authenticated user across all tickets.
*/
private function activeAnyTicketTimerQuery(Authenticatable $user): Builder
{
return TicketTimeLog::query()
->where('user_id', $user->getAuthIdentifier())
->whereNull('ended_at');
}
/**
* Update the ticket's rounded minute total using stored seconds.
*/
private function updateTicketTimeSpentFromSeconds(): void
{
$totalSeconds = (int) TicketTimeLog::query()
->where('ticket_id', $this->ticket->getKey())
->whereNotNull('ended_at')
->sum('seconds');
if ($totalSeconds === 0) {
$totalSeconds = (int) TicketTimeLog::query()
->where('ticket_id', $this->ticket->getKey())
->whereNotNull('ended_at')
->sum('minutes') * 60;
}
$this->ticket->forceFill([
'time_spent_minutes' => (int) ceil($totalSeconds / 60),
])->save();
}
/** /**
* Return the authenticated user or fail validation cleanly. * Return the authenticated user or fail validation cleanly.
* *
@@ -200,7 +171,7 @@ class TicketTimer extends Component
if (! $user instanceof Authenticatable) { if (! $user instanceof Authenticatable) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'timer' => 'You must be signed in to use the ticket timer.', 'timer' => 'You must be logged in to use the timer.',
]); ]);
} }
@@ -208,12 +179,12 @@ class TicketTimer extends Component
} }
/** /**
* Return a safe note for the timer. * Return the safe timer description.
*/ */
private function normalisedDescription(): string private function normalisedDescription(): string
{ {
$description = trim($this->description); $description = trim($this->description);
return $description !== '' ? $description : 'Work timer'; return $description !== '' ? $description : 'Timer entry';
} }
} }
@@ -1,5 +1,5 @@
@php @php
use Illuminate\Support\Str; use Illuminate\Support\Str;use Modules\Helpdesk\Models\TicketTimeLog;
$currentUser = auth()->user(); $currentUser = auth()->user();
$currentCompany = $currentUser?->company; $currentCompany = $currentUser?->company;
@@ -8,7 +8,11 @@
Str::lower((string) ($currentCompany?->slug ?? '')) === 'mpba-limited' || Str::lower((string) ($currentCompany?->slug ?? '')) === 'mpba-limited' ||
Str::contains(Str::lower((string) ($currentCompany?->name ?? '')), 'mpba'); Str::contains(Str::lower((string) ($currentCompany?->name ?? '')), 'mpba');
$visibleCommentCount = $ticket->comments->count(); $visibleCommentCount = $ticket->comments->count();
$ticketTimeLogs = collect($ticket->timeLogs ?? []); $ticketTimeLogs = TicketTimeLog::query()
->with('user')
->where('ticket_id', $ticket->getKey())
->orderByDesc('started_at')
->get();
@endphp @endphp
<div data-dusk="helpdesk-ticket-show" dusk="helpdesk-ticket-show" class="hd-helpdesk-surface space-y-4"> <div data-dusk="helpdesk-ticket-show" dusk="helpdesk-ticket-show" class="hd-helpdesk-surface space-y-4">
@@ -33,14 +37,14 @@
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<a href="{{ route('helpdesk.tickets.index') }}" wire:navigate <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"> 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> <i class="fa-solid fa-arrow-left mr-2" aria-hidden="true"></i>
Back Back
</a> </a>
@can('update', $ticket) @can('update', $ticket)
<a href="{{ route('helpdesk.tickets.edit', $ticket) }}" wire:navigate <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"> 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> <i class="fa-solid fa-pen-to-square mr-2" aria-hidden="true"></i>
Edit Edit
</a> </a>
@@ -119,29 +123,31 @@
<div class="space-y-3 p-4"> <div class="space-y-3 p-4">
@can('update', $ticket) @can('update', $ticket)
<form method="POST" action="{{ route('helpdesk.tickets.attachments.store', $ticket) }}" <form method="POST" action="{{ route('helpdesk.tickets.attachments.store', $ticket) }}"
enctype="multipart/form-data" enctype="multipart/form-data"
class="rounded-xl border border-blue-500/25 bg-slate-950/70 p-3 shadow-sm" class="rounded-xl border border-blue-500/25 bg-slate-950/70 p-3 shadow-sm"
data-helpdesk-ticket-attachment-form> data-helpdesk-ticket-attachment-form>
@csrf @csrf
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between"> <div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div class="min-w-0"> <div class="min-w-0">
<p class="text-xs font-black uppercase tracking-[0.18em] text-blue-200">Add files</p> <p class="text-xs font-black uppercase tracking-[0.18em] text-blue-200">Add
<p class="mt-1 truncate text-xs font-semibold text-slate-200" data-helpdesk-file-label> files</p>
<p class="mt-1 truncate text-xs font-semibold text-slate-200"
data-helpdesk-file-label>
No files selected</p> No files selected</p>
</div> </div>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center"> <div class="flex flex-col gap-2 sm:flex-row sm:items-center">
<label data-helpdesk-file-button <label data-helpdesk-file-button
class="hd-attachment-action !rounded-lg !px-3 !py-2 !text-xs"> class="hd-attachment-action !rounded-lg !px-3 !py-2 !text-xs">
<i class="fa-solid fa-paperclip" aria-hidden="true"></i> <i class="fa-solid fa-paperclip" aria-hidden="true"></i>
<span>Choose files</span> <span>Choose files</span>
<input name="attachments[]" type="file" multiple class="sr-only" <input name="attachments[]" type="file" multiple class="sr-only"
data-helpdesk-file-input> data-helpdesk-file-input>
</label> </label>
<button type="submit" <button type="submit"
class="hd-attachment-action hd-attachment-action-upload !rounded-lg !px-3 !py-2 !text-xs"> 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> <i class="fa-solid fa-upload" aria-hidden="true"></i>
<span>Upload</span> <span>Upload</span>
</button> </button>
@@ -149,11 +155,11 @@
</div> </div>
@error('attachments') @error('attachments')
<p class="mt-2 text-xs font-semibold text-red-300">{{ $message }}</p> <p class="mt-2 text-xs font-semibold text-red-300">{{ $message }}</p>
@enderror @enderror
@error('attachments.*') @error('attachments.*')
<p class="mt-2 text-xs font-semibold text-red-300">{{ $message }}</p> <p class="mt-2 text-xs font-semibold text-red-300">{{ $message }}</p>
@enderror @enderror
</form> </form>
@endcan @endcan
@@ -167,7 +173,7 @@
<div class="grid gap-2 sm:grid-cols-2"> <div class="grid gap-2 sm:grid-cols-2">
@foreach ($ticket->attachments as $attachment) @foreach ($ticket->attachments as $attachment)
<a href="{{ route('helpdesk.attachments.download', $attachment->id) }}" target="_blank" <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"> 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="min-w-0">
<span class="block truncate font-bold text-zinc-900 dark:text-zinc-100"> <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 ?? ''))))) }} {{ $attachment->original_name ?? ($attachment->original_filename ?? ($attachment->filename ?? ($attachment->file_name ?? basename($attachment->path ?? ($attachment->file_path ?? ''))))) }}
@@ -211,7 +217,7 @@
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"> 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' }} {{ $visibleCommentCount }} item{{ $visibleCommentCount === 1 ? '' : 's' }}
<i class="fa-solid fa-chevron-right transition group-open:rotate-90" <i class="fa-solid fa-chevron-right transition group-open:rotate-90"
aria-hidden="true"></i> aria-hidden="true"></i>
</span> </span>
</summary> </summary>
@@ -219,23 +225,23 @@
<div class="grid gap-2 md:grid-cols-[1fr_auto]"> <div class="grid gap-2 md:grid-cols-[1fr_auto]">
<label class="sr-only" for="helpdesk-comment-search">Search comments</label> <label class="sr-only" for="helpdesk-comment-search">Search comments</label>
<input id="helpdesk-comment-search" type="search" placeholder="Search comments..." <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" 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> data-helpdesk-comment-search>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<button type="button" <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" 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> data-helpdesk-expand-comments>
<i class="fa-solid fa-up-right-and-down-left-from-center mr-2" <i class="fa-solid fa-up-right-and-down-left-from-center mr-2"
aria-hidden="true"></i> aria-hidden="true"></i>
Expand all Expand all
</button> </button>
<button type="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" 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> data-helpdesk-collapse-comments>
<i class="fa-solid fa-down-left-and-up-right-to-center mr-2" <i class="fa-solid fa-down-left-and-up-right-to-center mr-2"
aria-hidden="true"></i> aria-hidden="true"></i>
Collapse all Collapse all
</button> </button>
</div> </div>
@@ -259,9 +265,9 @@
</section> </section>
<form method="POST" action="{{ route('helpdesk.tickets.comments.store', $ticket) }}" <form method="POST" action="{{ route('helpdesk.tickets.comments.store', $ticket) }}"
enctype="multipart/form-data" 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" 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> data-helpdesk-comment-form>
@csrf @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="bg-gradient-to-r from-slate-950 via-blue-950 to-slate-900 px-5 py-5 text-white sm:px-6">
@@ -356,18 +362,20 @@
); );
@endphp @endphp
<script type="application/json" id="helpdesk-canned-response-bodies">@json($cannedResponseBodies)</script> <script type="application/json"
<script type="application/json" id="helpdesk-canned-response-names">@json($cannedResponseNames)</script> id="helpdesk-canned-response-bodies">@json($cannedResponseBodies)</script>
<script type="application/json"
id="helpdesk-canned-response-names">@json($cannedResponseNames)</script>
@if ($cannedResponses->isNotEmpty()) @if ($cannedResponses->isNotEmpty())
<div class="grid gap-3 md:grid-cols-[1fr_auto]"> <div class="grid gap-3 md:grid-cols-[1fr_auto]">
<div> <div>
<label for="ticket-canned-response" <label for="ticket-canned-response"
class="mb-2 block text-xs font-bold text-zinc-700 dark:text-zinc-200">Canned class="mb-2 block text-xs font-bold text-zinc-700 dark:text-zinc-200">Canned
response</label> response</label>
<select id="ticket-canned-response" name="canned_response_id" <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" 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> data-canned-response-select>
<option value="">Choose a saved response...</option> <option value="">Choose a saved response...</option>
@foreach ($cannedResponses as $response) @foreach ($cannedResponses as $response)
<option value="{{ $response->id }}">{{ $response->name }}</option> <option value="{{ $response->id }}">{{ $response->name }}</option>
@@ -377,8 +385,8 @@
<div class="flex items-end"> <div class="flex items-end">
<button type="button" dusk="insert-canned-response" <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" 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> data-insert-canned-response>
<i class="fa-solid fa-copy mr-2" aria-hidden="true"></i> <i class="fa-solid fa-copy mr-2" aria-hidden="true"></i>
Insert response Insert response
</button> </button>
@@ -388,41 +396,48 @@
<div> <div>
<label for="ticket-comment-subject" <label for="ticket-comment-subject"
class="mb-2 block text-xs font-bold text-zinc-700 dark:text-zinc-200">Subject</label> 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') }}" <input id="ticket-comment-subject" name="subject" value="{{ old('subject') }}"
placeholder="Response 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" 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> data-helpdesk-comment-subject>
</div> </div>
<div> <div>
<label for="ticket-comment-body-editor" <label for="ticket-comment-body-editor"
class="mb-2 block text-xs font-bold text-zinc-700 dark:text-zinc-200">Comment</label> 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" <div class="mb-2 flex flex-wrap gap-2"
data-helpdesk-html-toolbar="ticket-comment-body-editor"> data-helpdesk-html-toolbar="ticket-comment-body-editor">
<button type="button" data-helpdesk-command="bold" <button type="button" data-helpdesk-command="bold"
class="rounded-lg border px-2 py-1 text-xs font-black">B</button> class="rounded-lg border px-2 py-1 text-xs font-black">B
</button>
<button type="button" data-helpdesk-command="italic" <button type="button" data-helpdesk-command="italic"
class="rounded-lg border px-2 py-1 text-xs italic">I</button> class="rounded-lg border px-2 py-1 text-xs italic">I
</button>
<button type="button" data-helpdesk-command="underline" <button type="button" data-helpdesk-command="underline"
class="rounded-lg border px-2 py-1 text-xs underline">U</button> class="rounded-lg border px-2 py-1 text-xs underline">U
</button>
<button type="button" data-helpdesk-command="insertUnorderedList" <button type="button" data-helpdesk-command="insertUnorderedList"
class="rounded-lg border px-2 py-1 text-xs">List</button> class="rounded-lg border px-2 py-1 text-xs">List
</button>
<button type="button" data-helpdesk-block="blockquote" <button type="button" data-helpdesk-block="blockquote"
class="rounded-lg border px-2 py-1 text-xs">Quote</button> class="rounded-lg border px-2 py-1 text-xs">Quote
</button>
<button type="button" data-helpdesk-block="code" <button type="button" data-helpdesk-block="code"
class="rounded-lg border px-2 py-1 text-xs">Code</button> class="rounded-lg border px-2 py-1 text-xs">Code
</button>
</div> </div>
<div id="ticket-comment-body-editor" dusk="ticket-comment-editor" contenteditable="true" <div id="ticket-comment-body-editor" dusk="ticket-comment-editor" contenteditable="true"
role="textbox" aria-multiline="true" data-helpdesk-rich-editor role="textbox" aria-multiline="true" data-helpdesk-rich-editor
data-helpdesk-sync-target="ticket-comment-body" 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"> 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> {!! $oldCommentBody !!}</div>
<textarea dusk="comment-body" data-dusk="comment-body" id="ticket-comment-body" name="commentBody" required <textarea dusk="comment-body" data-dusk="comment-body" id="ticket-comment-body"
class="sr-only" tabindex="-1" aria-hidden="true">{{ $oldCommentBody }}</textarea> name="commentBody" required
class="sr-only" tabindex="-1" aria-hidden="true">{{ $oldCommentBody }}</textarea>
</div> </div>
@can('update', $ticket) @can('update', $ticket)
@@ -430,27 +445,28 @@
class="rounded-xl border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-950"> 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="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="min-w-0"> <div class="min-w-0">
<label class="block text-xs font-bold text-zinc-700 dark:text-zinc-200">Attach files to <label class="block text-xs font-bold text-zinc-700 dark:text-zinc-200">Attach files
to
this reply</label> this reply</label>
<p class="mt-1 truncate text-xs font-semibold text-zinc-500 dark:text-zinc-400" <p class="mt-1 truncate text-xs font-semibold text-zinc-500 dark:text-zinc-400"
data-helpdesk-file-label>No files selected</p> data-helpdesk-file-label>No files selected</p>
</div> </div>
<label data-helpdesk-file-button <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"> 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> <i class="fa-solid fa-paperclip mr-2 text-xs" aria-hidden="true"></i>
<span>Choose files</span> <span>Choose files</span>
<input name="comment_attachments[]" type="file" multiple class="sr-only" <input name="comment_attachments[]" type="file" multiple class="sr-only"
data-helpdesk-file-input> data-helpdesk-file-input>
</label> </label>
</div> </div>
@error('comment_attachments') @error('comment_attachments')
<p class="mt-2 text-xs font-semibold text-red-600 dark:text-red-400">{{ $message }}</p> <p class="mt-2 text-xs font-semibold text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
@error('comment_attachments.*') @error('comment_attachments.*')
<p class="mt-2 text-xs font-semibold text-red-600 dark:text-red-400">{{ $message }}</p> <p class="mt-2 text-xs font-semibold text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
@@ -458,7 +474,7 @@
<label <label
class="mt-3 flex items-center gap-2 text-xs font-semibold text-zinc-700 dark:text-zinc-200"> class="mt-3 flex items-center gap-2 text-xs font-semibold text-zinc-700 dark:text-zinc-200">
<input type="checkbox" name="internal" value="1" @checked(old('internal')) <input type="checkbox" name="internal" value="1" @checked(old('internal'))
class="rounded border-zinc-300"> class="rounded border-zinc-300">
MPBA internal note only MPBA internal note only
</label> </label>
@endif @endif
@@ -466,7 +482,7 @@
<div class="flex justify-end"> <div class="flex justify-end">
<button dusk="post-comment" data-dusk="post-comment" type="submit" <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"> 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> <i class="fa-solid fa-comments mr-2" aria-hidden="true"></i>
Post reply Post reply
</button> </button>
@@ -475,7 +491,7 @@
</form> </form>
<script> <script>
document.addEventListener('click', function(event) { document.addEventListener('click', function (event) {
const insertButton = event.target.closest('[data-insert-canned-response]'); const insertButton = event.target.closest('[data-insert-canned-response]');
if (insertButton) { if (insertButton) {
@@ -566,7 +582,7 @@
} }
}); });
document.addEventListener('submit', function(event) { document.addEventListener('submit', function (event) {
const form = event.target.closest('[data-helpdesk-comment-form]'); const form = event.target.closest('[data-helpdesk-comment-form]');
if (!form) { if (!form) {
@@ -578,7 +594,7 @@
}); });
}); });
document.addEventListener('input', function(event) { document.addEventListener('input', function (event) {
const richEditor = event.target.closest('[data-helpdesk-rich-editor]'); const richEditor = event.target.closest('[data-helpdesk-rich-editor]');
if (richEditor) { if (richEditor) {
@@ -766,7 +782,7 @@
return span.innerHTML; return span.innerHTML;
} }
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('[data-helpdesk-rich-editor]').forEach((editor) => syncRichEditor(editor)); document.querySelectorAll('[data-helpdesk-rich-editor]').forEach((editor) => syncRichEditor(editor));
updateCommentToggleButtons(); updateCommentToggleButtons();
}); });
@@ -883,8 +899,8 @@
</div> </div>
<button type="button" <button type="button"
onclick="document.getElementById('ticket-time-log-modal').showModal()" 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"> 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> <i class="fa-solid fa-clock-rotate-left mr-2" aria-hidden="true"></i>
View log View log
</button> </button>
@@ -927,7 +943,7 @@
<div class="mt-3 flex flex-wrap gap-2"> <div class="mt-3 flex flex-wrap gap-2">
@foreach ($ticket->tags as $tag) @foreach ($ticket->tags as $tag)
<span class="rounded-full px-2 py-1 text-xs font-semibold text-white" <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> style="background-color: {{ \Modules\Helpdesk\Support\Color::css($tag->color) }}">{{ $tag->name }}</span>
@endforeach @endforeach
</div> </div>
@endif @endif
@@ -936,7 +952,9 @@
@if ($ticket->checklistItems->isNotEmpty()) @if ($ticket->checklistItems->isNotEmpty())
<div <div
class="rounded-xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900"> 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()) @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="-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 justify-between gap-3">
@@ -960,7 +978,7 @@
<label <label
class="flex items-start gap-2 rounded-lg border border-zinc-200 p-2 text-xs dark:border-zinc-700"> 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 }})" <input type="checkbox" wire:click="toggleChecklistItem({{ $item->id }})"
@checked($item->is_completed) class="mt-1 rounded border-zinc-300"> @checked($item->is_completed) class="mt-1 rounded border-zinc-300">
<span <span
class="{{ $item->is_completed ? 'text-zinc-500 line-through' : 'text-zinc-800 dark:text-zinc-100' }}">{{ $item->title }}</span> class="{{ $item->is_completed ? 'text-zinc-500 line-through' : 'text-zinc-800 dark:text-zinc-100' }}">{{ $item->title }}</span>
</label> </label>
@@ -987,7 +1005,7 @@
<div class="space-y-2"> <div class="space-y-2">
@forelse ($ticket->gitCommits as $commit) @forelse ($ticket->gitCommits as $commit)
<a href="{{ $commit->url ?: '#' }}" target="_blank" <a href="{{ $commit->url ?: '#' }}" target="_blank"
class="block rounded-lg border border-zinc-100 p-2 dark:border-zinc-800"> 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 }} · <div class="font-mono text-[0.68rem] text-zinc-500">{{ $commit->short_hash }} ·
{{ $commit->repository?->name }}</div> {{ $commit->repository?->name }}</div>
<div class="mt-1 text-xs font-medium text-zinc-900 dark:text-zinc-100"> <div class="mt-1 text-xs font-medium text-zinc-900 dark:text-zinc-100">
@@ -1013,7 +1031,8 @@
</span> </span>
<div class="min-w-0"> <div class="min-w-0">
<h2 class="text-lg font-black uppercase tracking-wide">Ticket controls</h2> <h2 class="text-lg font-black uppercase tracking-wide">Ticket controls</h2>
<p class="text-xs font-semibold text-indigo-100">Log time, assignment, status and priority <p class="text-xs font-semibold text-indigo-100">Log time, assignment, status and
priority
</p> </p>
</div> </div>
</div> </div>
@@ -1029,50 +1048,52 @@
'ticket' => $ticket, 'ticket' => $ticket,
]) ])
<form method="POST" action="{{ route('helpdesk.tickets.time-logs.store', $ticket) }}" <form method="POST" action="{{ route('helpdesk.tickets.time-logs.store', $ticket) }}"
class="mb-4 space-y-2"> class="mb-4 space-y-2">
@csrf @csrf
<label class="block text-xs font-bold text-zinc-700 dark:text-zinc-200" <label class="block text-xs font-bold text-zinc-700 dark:text-zinc-200"
for="ticket_time_minutes">Log time</label> for="ticket_time_minutes">Log time</label>
<div class="grid grid-cols-[1fr_auto] gap-2"> <div class="grid grid-cols-[1fr_auto] gap-2">
<input id="ticket_time_minutes" name="minutes" type="number" min="1" max="1440" <input id="ticket_time_minutes" name="minutes" type="number" min="1" max="1440"
placeholder="Minutes" placeholder="Minutes"
class="block w-full rounded-lg border border-zinc-300 bg-white p-2 text-xs font-semibold text-zinc-900 shadow-sm focus:border-emerald-500 focus:ring-emerald-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100" class="block w-full rounded-lg border border-zinc-300 bg-white p-2 text-xs font-semibold text-zinc-900 shadow-sm focus:border-emerald-500 focus:ring-emerald-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
data-dusk="ticket-time-minutes"> data-dusk="ticket-time-minutes">
<button type="submit" dusk="log-ticket-time" data-dusk="log-ticket-time" <button type="submit" dusk="log-ticket-time" data-dusk="log-ticket-time"
class="inline-flex items-center justify-center rounded-lg bg-emerald-600 px-3 py-2 text-xs font-black text-white shadow-sm transition hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400/70"> class="inline-flex items-center justify-center rounded-lg bg-emerald-600 px-3 py-2 text-xs font-black text-white shadow-sm transition hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400/70">
<i class="fa-solid fa-clock mr-2" aria-hidden="true"></i> <i class="fa-solid fa-clock mr-2" aria-hidden="true"></i>
<span>Log</span> <span>Log</span>
</button> </button>
</div> </div>
<textarea name="description" rows="2" placeholder="Optional work note" <textarea name="description" rows="2" placeholder="Optional work note"
class="block w-full rounded-lg border border-zinc-300 bg-white p-2 text-xs font-semibold text-zinc-900 shadow-sm focus:border-emerald-500 focus:ring-emerald-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100" class="block w-full rounded-lg border border-zinc-300 bg-white p-2 text-xs font-semibold text-zinc-900 shadow-sm focus:border-emerald-500 focus:ring-emerald-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
data-dusk="ticket-time-description"></textarea> data-dusk="ticket-time-description"></textarea>
@error('minutes') @error('minutes')
<p class="text-xs font-bold text-red-600 dark:text-red-400">{{ $message }}</p> <p class="text-xs font-bold text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
@error('description') @error('description')
<p class="text-xs font-bold text-red-600 dark:text-red-400">{{ $message }}</p> <p class="text-xs font-bold text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</form> </form>
<form method="POST" action="{{ route('helpdesk.tickets.agent.update', $ticket) }}" <form method="POST" action="{{ route('helpdesk.tickets.agent.update', $ticket) }}"
class="mb-4 space-y-2"> class="mb-4 space-y-2">
@csrf @csrf
<label class="block text-xs font-bold text-zinc-700 dark:text-zinc-200" <label class="block text-xs font-bold text-zinc-700 dark:text-zinc-200"
for="show_agent_id">Assigned agent</label> for="show_agent_id">Assigned agent</label>
<select id="show_agent_id" name="agent_id" <select id="show_agent_id" name="agent_id"
class="hd-select block w-full rounded-lg border border-zinc-300 bg-white p-2 text-xs font-semibold text-zinc-900 shadow-sm focus:border-emerald-500 focus:ring-emerald-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100" class="hd-select block w-full rounded-lg border border-zinc-300 bg-white p-2 text-xs font-semibold text-zinc-900 shadow-sm focus:border-emerald-500 focus:ring-emerald-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
data-dusk="ticket-show-agent"> data-dusk="ticket-show-agent">
<option value="" @selected($agent_id === null)>Unassigned</option> <option value="" @selected($agent_id === null)>Unassigned</option>
@forelse ($agents as $agent) @forelse ($agents as $agent)
<option value="{{ $agent->id }}" @selected((int) $agent_id === (int) $agent->id)>{{ $agent->name }} · <option
value="{{ $agent->id }}" @selected((int) $agent_id === (int) $agent->id)>{{ $agent->name }}
·
{{ $agent->email }}</option> {{ $agent->email }}</option>
@empty @empty
<option value="" disabled>No users available for assignment</option> <option value="" disabled>No users available for assignment</option>
@@ -1080,63 +1101,65 @@
</select> </select>
@error('agent_id') @error('agent_id')
<p class="text-xs font-bold text-red-600 dark:text-red-400">{{ $message }}</p> <p class="text-xs font-bold text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
<button type="submit" dusk="update-agent" data-dusk="update-agent" <button type="submit" dusk="update-agent" data-dusk="update-agent"
class="inline-flex items-center justify-center rounded-lg bg-emerald-600 px-3 py-2 text-xs font-black text-white shadow-sm transition hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400/70"> class="inline-flex items-center justify-center rounded-lg bg-emerald-600 px-3 py-2 text-xs font-black text-white shadow-sm transition hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400/70">
<i class="fa-solid fa-user-check mr-2" aria-hidden="true"></i> <i class="fa-solid fa-user-check mr-2" aria-hidden="true"></i>
<span>Assign agent</span> <span>Assign agent</span>
</button> </button>
</form> </form>
<form method="POST" action="{{ route('helpdesk.tickets.status.update', $ticket) }}" <form method="POST" action="{{ route('helpdesk.tickets.status.update', $ticket) }}"
class="mb-4 space-y-2"> class="mb-4 space-y-2">
@csrf @csrf
<label class="block text-xs font-bold text-zinc-700 dark:text-zinc-200" <label class="block text-xs font-bold text-zinc-700 dark:text-zinc-200"
for="show_status_id">Status</label> for="show_status_id">Status</label>
<select id="show_status_id" name="status_id" <select id="show_status_id" name="status_id"
class="hd-select block min-h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-xs 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" class="hd-select block min-h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-xs 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-dusk="ticket-show-status"> data-dusk="ticket-show-status">
@foreach ($statuses as $status) @foreach ($statuses as $status)
<option value="{{ $status->id }}" @selected((int) $status_id === (int) $status->id)>{{ $status->name }} <option
value="{{ $status->id }}" @selected((int) $status_id === (int) $status->id)>{{ $status->name }}
</option> </option>
@endforeach @endforeach
</select> </select>
@error('status_id') @error('status_id')
<p class="text-xs font-bold text-red-600 dark:text-red-400">{{ $message }}</p> <p class="text-xs font-bold text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
<button type="submit" dusk="update-status" <button type="submit" dusk="update-status"
class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-3 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"> class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-3 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">
<i class="fa-solid fa-circle-check mr-2" aria-hidden="true"></i> <i class="fa-solid fa-circle-check mr-2" aria-hidden="true"></i>
<span>Update status</span> <span>Update status</span>
</button> </button>
</form> </form>
<form method="POST" action="{{ route('helpdesk.tickets.priority.update', $ticket) }}" <form method="POST" action="{{ route('helpdesk.tickets.priority.update', $ticket) }}"
class="space-y-2"> class="space-y-2">
@csrf @csrf
<label class="block text-xs font-bold text-zinc-700 dark:text-zinc-200" <label class="block text-xs font-bold text-zinc-700 dark:text-zinc-200"
for="show_priority_id">Priority</label> for="show_priority_id">Priority</label>
<select id="show_priority_id" name="priority_id" <select id="show_priority_id" name="priority_id"
class="hd-select block w-full rounded-lg border border-zinc-300 bg-white p-2 text-xs font-semibold text-zinc-900 shadow-sm focus:border-amber-500 focus:ring-amber-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100" class="hd-select block w-full rounded-lg border border-zinc-300 bg-white p-2 text-xs font-semibold text-zinc-900 shadow-sm focus:border-amber-500 focus:ring-amber-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
data-dusk="ticket-show-priority"> data-dusk="ticket-show-priority">
@foreach ($priorities as $priority) @foreach ($priorities as $priority)
<option value="{{ $priority->id }}" @selected((int) $priority_id === (int) $priority->id)>{{ $priority->name }} <option
value="{{ $priority->id }}" @selected((int) $priority_id === (int) $priority->id)>{{ $priority->name }}
</option> </option>
@endforeach @endforeach
</select> </select>
@error('priority_id') @error('priority_id')
<p class="text-xs font-bold text-red-600 dark:text-red-400">{{ $message }}</p> <p class="text-xs font-bold text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
<button type="submit" dusk="update-priority" <button type="submit" dusk="update-priority"
class="inline-flex items-center justify-center rounded-lg bg-amber-500 px-3 py-2 text-xs font-black text-white shadow-sm transition hover:bg-amber-600 focus:outline-none focus:ring-2 focus:ring-amber-300/70"> class="inline-flex items-center justify-center rounded-lg bg-amber-500 px-3 py-2 text-xs font-black text-white shadow-sm transition hover:bg-amber-600 focus:outline-none focus:ring-2 focus:ring-amber-300/70">
<i class="fa-solid fa-flag mr-2" aria-hidden="true"></i> <i class="fa-solid fa-flag mr-2" aria-hidden="true"></i>
<span>Update priority</span> <span>Update priority</span>
</button> </button>
@@ -1147,7 +1170,7 @@
</div> </div>
<dialog id="ticket-time-log-modal" <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"> 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="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="flex items-center justify-between gap-3">
<div class="min-w-0"> <div class="min-w-0">
@@ -1158,7 +1181,7 @@
</div> </div>
<button type="button" onclick="document.getElementById('ticket-time-log-modal').close()" <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"> class="shrink-0 rounded-lg bg-white/15 px-3 py-2 text-xs font-black text-white hover:bg-white/25">
Close Close
</button> </button>
</div> </div>
@@ -1181,9 +1204,11 @@
{{ $timeLog->logged_at?->format('d M Y H:i') ?? $timeLog->created_at?->format('d M Y H:i') }} {{ $timeLog->logged_at?->format('d M Y H:i') ?? $timeLog->created_at?->format('d M Y H:i') }}
</p> </p>
</div> </div>
@php
$timeLogSeconds = (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"> <span class="shrink-0 rounded-lg bg-emerald-600 px-3 py-1.5 text-xs font-black text-white">
{{ $timeLog->minutes }}m {{ gmdate('H:i:s', $timeLogSeconds) }}
</span> </span>
</div> </div>
</div> </div>
@@ -55,6 +55,7 @@
} }
}" }"
x-init="start()" x-init="start()"
x-on:timer-started.window="start()"
x-on:timer-ended.window="stop(); elapsedSeconds = 0" x-on:timer-ended.window="stop(); elapsedSeconds = 0"
x-on:livewire:navigating.window="stop()" x-on:livewire:navigating.window="stop()"
class="rounded-xl bg-emerald-700 px-3 py-2 text-right text-white shadow-sm" class="rounded-xl bg-emerald-700 px-3 py-2 text-right text-white shadow-sm"
@@ -1,4 +1,3 @@
@php use Modules\Helpdesk\Livewire\TicketTimer; @endphp
@can('update', $ticket) @can('update', $ticket)
@livewire(TicketTimer::class, ['ticket' => $ticket], key('helpdesk-ticket-timer-'.$ticket->getKey())) @livewire('Modules\\Helpdesk\\Livewire\\TicketTimer', ['ticket' => $ticket], key('helpdesk-ticket-timer-'.$ticket->getKey()))
@endcan @endcan
@@ -1 +0,0 @@
@livewire(\Modules\Helpdesk\Livewire\TicketTimer::class, ['ticket' => $ticket], key('helpdesk-ticket-timer-'.$ticket->getKey()))
@@ -0,0 +1,25 @@
<?php
namespace Modules\Helpdesk\Tests\Feature;
use Tests\TestCase;
class TicketTimerStartDisplayFilesTest extends TestCase
{
/**
* The component must hydrate the active timer immediately after start so the running card displays.
*/
public function test_start_timer_refreshes_running_display_state(): void
{
$component = (string) file_get_contents(base_path('Modules/Helpdesk/Livewire/TicketTimer.php'));
$view = (string) file_get_contents(base_path('Modules/Helpdesk/resources/views/livewire/ticket-timer.blade.php'));
$this->assertStringContainsString('$this->activeTimer = $timer->fresh();', $component);
$this->assertStringContainsString('$this->elapsedSeconds = 0;', $component);
$this->assertStringContainsString("dispatch('timer-started')", $component);
$this->assertStringContainsString('$hasRunningTimer = $activeTimer && $activeTimer->ended_at === null;', $view);
$this->assertStringContainsString('data-helpdesk-running-timer', $view);
$this->assertStringContainsString('x-on:timer-started.window', $view);
$this->assertStringNotContainsString('wire:ignore', $view);
}
}