phases one of time logged now with log display
This commit is contained in:
@@ -5,8 +5,8 @@ namespace Modules\Helpdesk\Livewire;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Component;
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
@@ -25,7 +25,7 @@ class TicketTimer extends Component
|
||||
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
|
||||
{
|
||||
@@ -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
|
||||
*/
|
||||
@@ -42,51 +42,47 @@ class TicketTimer extends Component
|
||||
{
|
||||
$user = $this->authenticatedUser();
|
||||
|
||||
Gate::authorize('update', $this->ticket);
|
||||
|
||||
if ($this->activeUserTimerQuery($user)->exists()) {
|
||||
throw ValidationException::withMessages([
|
||||
'timer' => 'You already have a running timer. End it before starting another one.',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->activeAnyTicketTimerQuery($user)->exists()) {
|
||||
throw ValidationException::withMessages([
|
||||
'timer' => 'You already have a running timer on another ticket. End it before starting another one.',
|
||||
'timer' => 'You already have a running timer.',
|
||||
]);
|
||||
}
|
||||
|
||||
$startedAt = Carbon::now();
|
||||
|
||||
TicketTimeLog::query()->create([
|
||||
'ticket_id' => $this->ticket->getKey(),
|
||||
'user_id' => $user->getAuthIdentifier(),
|
||||
'minutes' => 0,
|
||||
'seconds' => 0,
|
||||
'description' => $this->normalisedDescription(),
|
||||
'started_at' => $startedAt,
|
||||
'ended_at' => null,
|
||||
'logged_at' => $startedAt,
|
||||
]);
|
||||
try {
|
||||
$timer = TicketTimeLog::query()->create([
|
||||
'ticket_id' => $this->ticket->getKey(),
|
||||
'user_id' => $user->getAuthIdentifier(),
|
||||
'minutes' => 0,
|
||||
'seconds' => 0,
|
||||
'description' => $this->normalisedDescription(),
|
||||
'started_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->message = 'Timer started.';
|
||||
$this->refreshActiveTimer();
|
||||
|
||||
$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
|
||||
*/
|
||||
public function endTimer(): void
|
||||
{
|
||||
$user = $this->authenticatedUser();
|
||||
|
||||
Gate::authorize('update', $this->ticket);
|
||||
|
||||
$timer = $this->activeUserTimerQuery($user)->first();
|
||||
|
||||
if (! $timer instanceof TicketTimeLog) {
|
||||
@@ -108,7 +104,14 @@ class TicketTimer extends Component
|
||||
'logged_at' => $endedAt,
|
||||
])->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->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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the active timer from the database.
|
||||
* Refresh the active timer state from the database.
|
||||
*/
|
||||
private function refreshActiveTimer(): void
|
||||
{
|
||||
@@ -142,7 +145,7 @@ class TicketTimer extends Component
|
||||
|
||||
$this->activeTimer = $this->activeUserTimerQuery($user)->first();
|
||||
$this->elapsedSeconds = $this->activeTimer instanceof TicketTimeLog
|
||||
? (int) $this->activeTimer->started_at->diffInSeconds(Carbon::now())
|
||||
? $this->activeTimer->started_at->diffInSeconds(Carbon::now())
|
||||
: 0;
|
||||
}
|
||||
|
||||
@@ -157,38 +160,6 @@ class TicketTimer extends Component
|
||||
->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.
|
||||
*
|
||||
@@ -200,7 +171,7 @@ class TicketTimer extends Component
|
||||
|
||||
if (! $user instanceof Authenticatable) {
|
||||
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
|
||||
{
|
||||
$description = trim($this->description);
|
||||
|
||||
return $description !== '' ? $description : 'Work timer';
|
||||
return $description !== '' ? $description : 'Timer entry';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@php
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Str;use Modules\Helpdesk\Models\TicketTimeLog;
|
||||
|
||||
$currentUser = auth()->user();
|
||||
$currentCompany = $currentUser?->company;
|
||||
@@ -8,7 +8,11 @@
|
||||
Str::lower((string) ($currentCompany?->slug ?? '')) === 'mpba-limited' ||
|
||||
Str::contains(Str::lower((string) ($currentCompany?->name ?? '')), 'mpba');
|
||||
$visibleCommentCount = $ticket->comments->count();
|
||||
$ticketTimeLogs = collect($ticket->timeLogs ?? []);
|
||||
$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">
|
||||
@@ -33,14 +37,14 @@
|
||||
|
||||
<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">
|
||||
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">
|
||||
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>
|
||||
@@ -119,29 +123,31 @@
|
||||
<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>
|
||||
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>
|
||||
<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">
|
||||
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>
|
||||
data-helpdesk-file-input>
|
||||
</label>
|
||||
|
||||
<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>
|
||||
<span>Upload</span>
|
||||
</button>
|
||||
@@ -149,11 +155,11 @@
|
||||
</div>
|
||||
|
||||
@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
|
||||
|
||||
@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
|
||||
</form>
|
||||
@endcan
|
||||
@@ -167,7 +173,7 @@
|
||||
<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">
|
||||
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 ?? ''))))) }}
|
||||
@@ -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">
|
||||
{{ $visibleCommentCount }} item{{ $visibleCommentCount === 1 ? '' : 's' }}
|
||||
<i class="fa-solid fa-chevron-right transition group-open:rotate-90"
|
||||
aria-hidden="true"></i>
|
||||
aria-hidden="true"></i>
|
||||
</span>
|
||||
</summary>
|
||||
|
||||
@@ -219,23 +225,23 @@
|
||||
<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>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
aria-hidden="true"></i>
|
||||
Collapse all
|
||||
</button>
|
||||
</div>
|
||||
@@ -259,9 +265,9 @@
|
||||
</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>
|
||||
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">
|
||||
@@ -356,18 +362,20 @@
|
||||
);
|
||||
@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>
|
||||
<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
|
||||
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>
|
||||
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>
|
||||
@@ -377,8 +385,8 @@
|
||||
|
||||
<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>
|
||||
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>
|
||||
@@ -388,41 +396,48 @@
|
||||
|
||||
<div>
|
||||
<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') }}"
|
||||
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>
|
||||
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>
|
||||
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">
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
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">
|
||||
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>
|
||||
<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)
|
||||
@@ -430,27 +445,28 @@
|
||||
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
|
||||
<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>
|
||||
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">
|
||||
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>
|
||||
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>
|
||||
<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>
|
||||
<p class="mt-2 text-xs font-semibold text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
@@ -458,7 +474,7 @@
|
||||
<label
|
||||
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'))
|
||||
class="rounded border-zinc-300">
|
||||
class="rounded border-zinc-300">
|
||||
MPBA internal note only
|
||||
</label>
|
||||
@endif
|
||||
@@ -466,7 +482,7 @@
|
||||
|
||||
<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">
|
||||
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>
|
||||
@@ -475,7 +491,7 @@
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.addEventListener('click', function(event) {
|
||||
document.addEventListener('click', function (event) {
|
||||
const insertButton = event.target.closest('[data-insert-canned-response]');
|
||||
|
||||
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]');
|
||||
|
||||
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]');
|
||||
|
||||
if (richEditor) {
|
||||
@@ -766,7 +782,7 @@
|
||||
return span.innerHTML;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.querySelectorAll('[data-helpdesk-rich-editor]').forEach((editor) => syncRichEditor(editor));
|
||||
updateCommentToggleButtons();
|
||||
});
|
||||
@@ -883,8 +899,8 @@
|
||||
</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">
|
||||
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>
|
||||
@@ -927,7 +943,7 @@
|
||||
<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>
|
||||
style="background-color: {{ \Modules\Helpdesk\Support\Color::css($tag->color) }}">{{ $tag->name }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@@ -936,7 +952,9 @@
|
||||
@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())
|
||||
@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">
|
||||
@@ -960,7 +978,7 @@
|
||||
<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">
|
||||
@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>
|
||||
@@ -987,7 +1005,7 @@
|
||||
<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">
|
||||
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">
|
||||
@@ -1013,7 +1031,8 @@
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1029,50 +1048,52 @@
|
||||
'ticket' => $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
|
||||
|
||||
<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">
|
||||
<input id="ticket_time_minutes" name="minutes" type="number" min="1" max="1440"
|
||||
placeholder="Minutes"
|
||||
class="block w-full rounded-lg border border-zinc-300 bg-white p-2 text-xs font-semibold text-zinc-900 shadow-sm focus:border-emerald-500 focus:ring-emerald-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
||||
data-dusk="ticket-time-minutes">
|
||||
placeholder="Minutes"
|
||||
class="block w-full rounded-lg border border-zinc-300 bg-white p-2 text-xs font-semibold text-zinc-900 shadow-sm focus:border-emerald-500 focus:ring-emerald-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
||||
data-dusk="ticket-time-minutes">
|
||||
|
||||
<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>
|
||||
<span>Log</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<textarea name="description" rows="2" placeholder="Optional work note"
|
||||
class="block w-full rounded-lg border border-zinc-300 bg-white p-2 text-xs font-semibold text-zinc-900 shadow-sm focus:border-emerald-500 focus:ring-emerald-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
||||
data-dusk="ticket-time-description"></textarea>
|
||||
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>
|
||||
|
||||
@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
|
||||
|
||||
@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
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ route('helpdesk.tickets.agent.update', $ticket) }}"
|
||||
class="mb-4 space-y-2">
|
||||
class="mb-4 space-y-2">
|
||||
@csrf
|
||||
|
||||
<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"
|
||||
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">
|
||||
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">
|
||||
<option value="" @selected($agent_id === null)>Unassigned</option>
|
||||
@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>
|
||||
@empty
|
||||
<option value="" disabled>No users available for assignment</option>
|
||||
@@ -1080,63 +1101,65 @@
|
||||
</select>
|
||||
|
||||
@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
|
||||
|
||||
<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>
|
||||
<span>Assign agent</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ route('helpdesk.tickets.status.update', $ticket) }}"
|
||||
class="mb-4 space-y-2">
|
||||
class="mb-4 space-y-2">
|
||||
@csrf
|
||||
|
||||
<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"
|
||||
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">
|
||||
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">
|
||||
@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>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
@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
|
||||
|
||||
<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>
|
||||
<span>Update status</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ route('helpdesk.tickets.priority.update', $ticket) }}"
|
||||
class="space-y-2">
|
||||
class="space-y-2">
|
||||
@csrf
|
||||
|
||||
<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"
|
||||
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">
|
||||
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">
|
||||
@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>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
@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
|
||||
|
||||
<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>
|
||||
<span>Update priority</span>
|
||||
</button>
|
||||
@@ -1147,7 +1170,7 @@
|
||||
</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">
|
||||
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">
|
||||
@@ -1158,7 +1181,7 @@
|
||||
</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">
|
||||
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>
|
||||
@@ -1181,9 +1204,11 @@
|
||||
{{ $timeLog->logged_at?->format('d M Y H:i') ?? $timeLog->created_at?->format('d M Y H:i') }}
|
||||
</p>
|
||||
</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">
|
||||
{{ $timeLog->minutes }}m
|
||||
{{ gmdate('H:i:s', $timeLogSeconds) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
}
|
||||
}"
|
||||
x-init="start()"
|
||||
x-on:timer-started.window="start()"
|
||||
x-on:timer-ended.window="stop(); elapsedSeconds = 0"
|
||||
x-on:livewire:navigating.window="stop()"
|
||||
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)
|
||||
@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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user