core timer fixes
This commit is contained in:
@@ -1,133 +1,151 @@
|
||||
<div
|
||||
class="mb-4 rounded-xl border border-emerald-200 bg-emerald-50 p-3 shadow-sm dark:border-emerald-900 dark:bg-emerald-950/40"
|
||||
class="mb-4 rounded-xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-950"
|
||||
data-helpdesk-ticket-timer
|
||||
>
|
||||
@php
|
||||
$hasRunningTimer = $activeTimer && $activeTimer->ended_at === null;
|
||||
$startedAtIso = $hasRunningTimer ? $activeTimer->started_at?->toIso8601String() : null;
|
||||
$startedAtDisplay = $hasRunningTimer ? $activeTimer->started_at?->format('d M Y H:i') : null;
|
||||
@endphp
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-emerald-600 text-white shadow-sm">
|
||||
<i class="fa-solid fa-stopwatch text-sm" aria-hidden="true"></i>
|
||||
</span>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="flex min-w-0 items-start gap-3">
|
||||
<span class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-blue-600 text-white shadow-sm">
|
||||
<i class="fa-solid fa-stopwatch text-sm" aria-hidden="true"></i>
|
||||
</span>
|
||||
|
||||
<div class="min-w-0 flex-1 space-y-3">
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="min-w-0">
|
||||
<h3 class="text-sm font-black text-emerald-950 dark:text-emerald-100">Work timer</h3>
|
||||
<p class="mt-1 text-xs font-semibold text-emerald-800 dark:text-emerald-200">
|
||||
Start a user-specific timer. It remains active if the browser is closed.
|
||||
<h3 class="text-sm font-black text-zinc-950 dark:text-zinc-100">
|
||||
Ticket timer
|
||||
</h3>
|
||||
|
||||
<p class="mt-1 text-xs font-semibold text-zinc-600 dark:text-zinc-300">
|
||||
Record focused work against this ticket.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if ($hasRunningTimer)
|
||||
<div
|
||||
x-data="{
|
||||
startedAt: Date.parse('{{ $activeTimer->started_at?->toIso8601String() }}'),
|
||||
elapsedSeconds: {{ max(0, (int) $elapsedSeconds) }},
|
||||
intervalId: null,
|
||||
formatted() {
|
||||
const total = Math.max(0, Number(this.elapsedSeconds || 0));
|
||||
const hours = String(Math.floor(total / 3600)).padStart(2, '0');
|
||||
const minutes = String(Math.floor((total % 3600) / 60)).padStart(2, '0');
|
||||
const seconds = String(total % 60).padStart(2, '0');
|
||||
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
},
|
||||
tick() {
|
||||
if (!Number.isFinite(this.startedAt)) {
|
||||
this.elapsedSeconds = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
this.elapsedSeconds = Math.floor((Date.now() - this.startedAt) / 1000);
|
||||
},
|
||||
start() {
|
||||
this.stop();
|
||||
this.tick();
|
||||
this.intervalId = window.setInterval(() => this.tick(), 1000);
|
||||
},
|
||||
stop() {
|
||||
if (this.intervalId) {
|
||||
window.clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
}
|
||||
}"
|
||||
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"
|
||||
data-helpdesk-running-timer
|
||||
>
|
||||
<div class="text-[0.65rem] font-black uppercase tracking-[0.16em] text-emerald-100">Running</div>
|
||||
<div class="font-mono text-lg font-black leading-tight" x-text="formatted()" data-dusk="ticket-running-time">00:00:00</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="rounded-xl bg-white px-3 py-2 text-right text-emerald-900 shadow-sm ring-1 ring-emerald-200 dark:bg-zinc-950 dark:text-emerald-100 dark:ring-emerald-900">
|
||||
<div class="text-[0.65rem] font-black uppercase tracking-[0.16em] text-emerald-700 dark:text-emerald-300">Stopped</div>
|
||||
<div class="font-mono text-lg font-black leading-tight" data-dusk="ticket-running-time">00:00:00</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($message)
|
||||
<div class="rounded-lg border border-emerald-300 bg-white px-3 py-2 text-xs font-bold text-emerald-900 dark:border-emerald-800 dark:bg-zinc-950 dark:text-emerald-100">
|
||||
{{ $message }}
|
||||
@if ($hasRunningTimer)
|
||||
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-right shadow-sm dark:border-emerald-900 dark:bg-emerald-950/40">
|
||||
<div class="flex items-center justify-end gap-2 text-[0.65rem] font-black uppercase tracking-[0.16em] text-emerald-700 dark:text-emerald-300">
|
||||
<span class="h-2 w-2 rounded-full bg-emerald-500"></span>
|
||||
Running
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-1 font-mono text-2xl font-black leading-none text-emerald-950 dark:text-emerald-100"
|
||||
data-helpdesk-live-elapsed
|
||||
data-helpdesk-started-at="{{ $startedAtIso }}"
|
||||
data-dusk="ticket-running-time"
|
||||
>
|
||||
{{ gmdate('H:i:s', max(0, (int) $elapsedSeconds)) }}
|
||||
</div>
|
||||
|
||||
<p class="mt-1 text-[0.68rem] font-bold text-emerald-700 dark:text-emerald-300">
|
||||
Started {{ $startedAtDisplay }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@error('timer')
|
||||
<div class="rounded-lg border border-red-300 bg-red-50 px-3 py-2 text-xs font-bold text-red-800 dark:border-red-900 dark:bg-red-950 dark:text-red-100">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-bold text-emerald-950 dark:text-emerald-100" for="ticket_timer_description">
|
||||
Timer note
|
||||
</label>
|
||||
|
||||
<textarea
|
||||
id="ticket_timer_description"
|
||||
wire:model.defer="description"
|
||||
rows="2"
|
||||
placeholder="Optional work note"
|
||||
class="block w-full rounded-lg border border-emerald-300 bg-white p-2 text-xs font-semibold text-zinc-900 shadow-sm focus:border-emerald-500 focus:ring-emerald-500 dark:border-emerald-900 dark:bg-zinc-950 dark:text-zinc-100"
|
||||
data-dusk="ticket-timer-description"
|
||||
></textarea>
|
||||
@if ($message)
|
||||
<div class="rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs font-bold text-emerald-800 dark:border-emerald-900 dark:bg-emerald-950/40 dark:text-emerald-100">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@if ($hasRunningTimer)
|
||||
<button
|
||||
type="button"
|
||||
wire:click="endTimer"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="endTimer"
|
||||
class="inline-flex items-center justify-center rounded-lg bg-red-600 px-3 py-2 text-xs font-black text-white shadow-sm transition hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
data-dusk="end-ticket-timer"
|
||||
>
|
||||
<i class="fa-solid fa-stop mr-2" aria-hidden="true"></i>
|
||||
<span wire:loading.remove wire:target="endTimer">End timer</span>
|
||||
<span wire:loading wire:target="endTimer">Ending...</span>
|
||||
</button>
|
||||
@else
|
||||
<button
|
||||
type="button"
|
||||
wire:click="startTimer"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="startTimer"
|
||||
class="inline-flex 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 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
data-dusk="start-ticket-timer"
|
||||
>
|
||||
<i class="fa-solid fa-play mr-2" aria-hidden="true"></i>
|
||||
<span wire:loading.remove wire:target="startTimer">Start timer</span>
|
||||
<span wire:loading wire:target="startTimer">Starting...</span>
|
||||
</button>
|
||||
@endif
|
||||
@error('timer')
|
||||
<div class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs font-bold text-red-800 dark:border-red-900 dark:bg-red-950/40 dark:text-red-100">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-bold text-zinc-800 dark:text-zinc-100" for="ticket_timer_description">
|
||||
Timer note
|
||||
</label>
|
||||
|
||||
<textarea
|
||||
id="ticket_timer_description"
|
||||
wire:model.defer="description"
|
||||
rows="2"
|
||||
placeholder="Optional work note"
|
||||
class="block w-full rounded-lg border border-zinc-300 bg-white p-3 text-sm font-semibold text-zinc-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100"
|
||||
data-dusk="ticket-timer-description"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p class="text-xs font-semibold text-zinc-500 dark:text-zinc-400">
|
||||
{{ $hasRunningTimer ? 'End the timer to add the elapsed time to the ticket.' : 'Start a timer when work begins. Only one active timer is allowed.' }}
|
||||
</p>
|
||||
|
||||
@if ($hasRunningTimer)
|
||||
<button
|
||||
type="button"
|
||||
wire:click="endTimer"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="endTimer"
|
||||
class="inline-flex items-center justify-center rounded-lg bg-red-600 px-4 py-2.5 text-sm font-black text-white shadow-sm transition hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 dark:focus:ring-offset-zinc-950"
|
||||
data-dusk="end-ticket-timer"
|
||||
>
|
||||
<i class="fa-solid fa-stop mr-2" aria-hidden="true"></i>
|
||||
<span wire:loading.remove wire:target="endTimer">End timer</span>
|
||||
<span wire:loading wire:target="endTimer">Ending...</span>
|
||||
</button>
|
||||
@else
|
||||
<button
|
||||
type="button"
|
||||
wire:click="startTimer"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="startTimer"
|
||||
class="inline-flex min-w-[9.5rem] shrink-0 items-center justify-center whitespace-nowrap rounded-md bg-blue-600 px-4 py-2 text-sm font-black text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 dark:focus:ring-offset-zinc-950"
|
||||
data-dusk="start-ticket-timer"
|
||||
>
|
||||
<i class="fa-solid fa-play mr-2 text-xs" aria-hidden="true"></i>
|
||||
<span wire:loading.remove wire:target="startTimer">Start timer</span>
|
||||
<span wire:loading wire:target="startTimer">Starting...</span>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
if (window.helpdeskTicketTimerDisplayBooted === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.helpdeskTicketTimerDisplayBooted = true;
|
||||
|
||||
const formatElapsed = (totalSeconds) => {
|
||||
const seconds = Math.max(0, Number.parseInt(totalSeconds, 10) || 0);
|
||||
const hours = String(Math.floor(seconds / 3600)).padStart(2, '0');
|
||||
const minutes = String(Math.floor((seconds % 3600) / 60)).padStart(2, '0');
|
||||
const remainingSeconds = String(seconds % 60).padStart(2, '0');
|
||||
|
||||
return `${hours}:${minutes}:${remainingSeconds}`;
|
||||
};
|
||||
|
||||
const updateTimers = () => {
|
||||
document.querySelectorAll('[data-helpdesk-live-elapsed][data-helpdesk-started-at]').forEach((timer) => {
|
||||
const startedAt = Date.parse(timer.getAttribute('data-helpdesk-started-at') || '');
|
||||
|
||||
if (!Number.isFinite(startedAt)) {
|
||||
timer.textContent = '00:00:00';
|
||||
return;
|
||||
}
|
||||
|
||||
timer.textContent = formatElapsed(Math.floor((Date.now() - startedAt) / 1000));
|
||||
});
|
||||
};
|
||||
|
||||
updateTimers();
|
||||
window.setInterval(updateTimers, 1000);
|
||||
document.addEventListener('livewire:navigated', updateTimers);
|
||||
document.addEventListener('livewire:update', updateTimers);
|
||||
document.addEventListener('livewire:updated', updateTimers);
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user