Files
servicedesk/src/resources/views/livewire/operator/ticket-show.blade.php
Kacper 313e01ad24 v1.2.1
- Generic AI integration (Admin > Integracje > "Integracja AI"), optional and
  off by default: an OpenAI-compatible /chat/completions client (Groq, OpenAI,
  or a self-hosted Ollama instance) configured by base URL, optional API key,
  model, and an SSL-verification toggle. Foundation for the two AI features
  below and anything else that wants an LLM call in the future.
- BookStack automatic content tagging (AI): "Otaguj nową treść"/"Otaguj
  wszystko ponownie" buttons plus `php artisan bookstack:tag-content`
  (--dry-run/--force/--limit=N) tag every book/chapter/page with matching
  helpdesk subcategory names, idempotent by default.
- BookStack search refinement: "Przeszukuj" is now three independent
  checkboxes (Książki/Strony/Rozdziały) instead of a single dropdown, plus a
  new "Szukaj po" setting (nazwa/tagi/oba) — tag matching uses the bare
  subcategory name, matching what auto-tagging writes.
- AI-driven ticket triage + summary (Admin > Integracje > "Automatyzacja AI
  dla zgłoszeń", via new scheduled ai:run-ticket-automation): five toggles
  auto-assign/correct category+subcategory, rewrite an unclear subject, and
  set priority from content, once per ticket in the background; every change
  is logged in the ticket's history. Separately, an AI summary + suggested
  action for every ticket, shown to operators only, with an admin-editable
  prompt.
- Operators can now reassign a ticket to any team, not just one they belong
  to.
- The auto-refresh countdown badges (ticket view, operator queue) are now
  clickable — fetch immediately and reset the countdown.
- All 7 "cyclical" intervals (3 browser refresh countdowns, the notification
  bell poll, and the 4 background scheduled commands) are now configurable
  from Admin > Konfiguracja instead of fixed in code.
- Fixed: an operator viewing a ticket that's deleted or moved outside their
  team scope mid-session is now redirected to the operator queue instead of
  hitting an error.
- Docs: README/ARCHITECTURE/CLAUDE/install/wiki updated for all of the above.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 13:38:39 +02:00

519 lines
37 KiB
PHP

<div style="flex:1;display:flex;flex-direction:column">
<x-topbar />
<div class="page-pad" style="flex:1;padding:20px 24px;overflow:auto">
<div style="display:flex;flex-direction:column;gap:16px;max-width:1180px;margin:0 auto">
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
<a href="{{ route('operator.queue') }}" wire:navigate class="btn btn-ghost" style="padding:0;margin-right:auto">&larr; Wróć do listy</a>
<button
type="button"
class="btn btn-secondary"
style="gap:6px"
wire:click="toggleWatch"
title="{{ $this->isWatching ? 'Przestań obserwować to zgłoszenie' : 'Obserwuj to zgłoszenie' }}"
>
<span class="material-symbols-outlined" style="font-size:18px">{{ $this->isWatching ? 'star' : 'star_outline' }}</span>
{{ $this->isWatching ? 'Obserwowane' : 'Obserwuj' }}
</button>
{{-- Live updates arrive via broadcasting, but websocket connections can
drop silently this is a periodic fallback refresh, with a visible
countdown so it's clear the thread is still refreshing on its own.
Also clickable, to fetch immediately and reset the countdown. --}}
@php
$refreshTicketSeconds = max(1, (int) \App\Support\Settings::get('refresh_ticket_view_seconds'));
@endphp
<div
class="btn btn-secondary"
style="cursor:pointer;gap:6px"
x-data="{ remaining: {{ $refreshTicketSeconds }}, total: {{ $refreshTicketSeconds }} }"
x-init="setInterval(() => { remaining = remaining <= 1 ? total : remaining - 1; if (remaining === total) $wire.refreshTicketData(); }, 1000)"
x-on:click="remaining = total; $wire.refreshTicketData()"
title="Zgłoszenie odświeża się automatycznie — kliknij, aby odświeżyć teraz"
>
<span class="material-symbols-outlined" style="font-size:18px">schedule</span>
<span x-text="remaining + 's'"></span>
</div>
</div>
<div style="display:flex;gap:20px;align-items:flex-start;flex-wrap:wrap">
<div class="main-col" style="display:flex;flex-direction:column;gap:16px">
<div class="card" style="padding:20px;gap:8px">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px">
<div style="display:flex;align-items:center;gap:8px">
<div class="card-kicker">Zgłoszenie {{ $ticket->displayNumber() }}</div>
@if ($ticket->source === 'email')
<span class="tag tag-outline" style="display:inline-flex;align-items:center;gap:3px;font-size:10.5px" title="Utworzone przez e-mail">
<span class="material-symbols-outlined" style="font-size:13px">mail</span>E-mail
</span>
@endif
</div>
@unless ($editingDetails)
<button type="button" class="btn btn-ghost" style="padding:0" wire:click="toggleEditDetails">Edytuj</button>
@endunless
</div>
@unless ($editingDetails)
<h3 style="margin:2px 0 0">{{ $ticket->subject }}</h3>
<div class="card-meta">{{ $ticket->categoryLabel() }}</div>
<div style="white-space:pre-wrap;font-size:14px;margin-top:4px">{{ $ticket->body }}</div>
@foreach ($this->publicMessages->first()?->attachments ?? [] as $a)
<x-message-attachment :attachment="$a" />
@endforeach
@if (count($ticket->customFieldEntries()))
<div style="display:flex;flex-direction:column;gap:2px;margin-top:2px">
@foreach ($ticket->customFieldEntries() as $cf)
<div style="font-size:14px"><strong>{{ $cf['label'] }}:</strong> {{ $cf['value'] }}</div>
@endforeach
</div>
@endif
@else
<div class="field"><label>Temat</label><input class="input" wire:model="editDetailsForm.subject"></div>
<div class="field"><label>Treść</label><textarea class="input" wire:model="editDetailsForm.body"></textarea></div>
<div style="display:flex;gap:10px">
<div class="field" style="flex:1">
<label>Kategoria</label>
<select class="input" wire:model.live="editDetailsForm.categoryId">
<option value="">Wybierz</option>
@foreach ($this->categories as $c)
<option value="{{ $c->id }}">{{ $c->name }}</option>
@endforeach
</select>
</div>
<div class="field" style="flex:1">
<label>Podkategoria</label>
<select class="input" wire:model.live="editDetailsForm.subcategoryId" @disabled(! $editDetailsForm['categoryId'])>
<option value="">Wybierz</option>
@foreach ($this->editDetailsSubcategoryOptions as $s)
<option value="{{ $s->id }}">{{ $s->name }}</option>
@endforeach
</select>
</div>
</div>
@foreach ($this->editDetailsSelectedSubcategory?->customFields ?? [] as $field)
<x-custom-field-input :field="$field" wire-model="editDetailsForm.customValues" />
@endforeach
<div style="display:flex;gap:8px;margin-top:6px">
<button type="button" class="btn btn-secondary" wire:click="cancelEditDetails">Anuluj</button>
<button type="button" class="btn btn-primary" wire:click="saveDetails">Zapisz</button>
</div>
@endunless
</div>
<div class="card" style="padding:16px;gap:10px">
<div class="card-kicker">Notatki wewnętrzne</div>
@forelse ($this->internalMessages as $m)
<div wire:key="note-{{ $m->id }}" style="padding:10px 12px;border-left:3px solid var(--color-accent);background:var(--color-surface);border-radius:4px">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px">
<div style="font-size:11px;opacity:0.65;margin-bottom:4px">{{ $m->author_name }} &middot; {{ \App\Support\Rel::format($m->created_at) }}</div>
@if ($m->role === 'operator')
<div style="display:flex;gap:6px;flex:none">
<span class="material-symbols-outlined" style="font-size:16px;cursor:pointer;opacity:0.7" wire:click="startEditNote({{ $m->id }}, @js($m->body))">edit</span>
<span class="material-symbols-outlined" style="font-size:16px;cursor:pointer;opacity:0.7" wire:click="requestDeleteMessage({{ $m->id }})">delete</span>
</div>
@endif
</div>
@if ($editingNoteId === $m->id)
<textarea class="input" wire:model="editingNoteDraft"></textarea>
<div style="display:flex;gap:8px;margin-top:6px">
<button type="button" class="btn btn-secondary" wire:click="cancelEditNote">Anuluj</button>
<button type="button" class="btn btn-primary" wire:click="saveEditNote">Zapisz</button>
</div>
@else
<div style="white-space:pre-wrap">{{ $m->body }}</div>
@endif
@foreach ($m->attachments as $a)
<x-message-attachment :attachment="$a" />
@endforeach
</div>
@empty
<p class="text-muted" style="font-size:13px;margin:0">Brak notatek wewnętrznych.</p>
@endforelse
@if ($addingNote)
<textarea class="input" placeholder="Dodaj notatkę widoczną tylko dla zespołu…" wire:model="noteDraft"></textarea>
<div style="display:flex;align-items:center;gap:10px">
<div
x-data="{ dragging: false }"
@dragover.prevent="dragging = true"
@dragleave.prevent="dragging = false"
@drop.prevent="dragging = false; const input = $el.querySelector('input[type=file]'); input.files = $event.dataTransfer.files; input.dispatchEvent(new Event('change'))"
:style="{ borderColor: dragging ? 'var(--color-accent)' : undefined, background: dragging ? 'color-mix(in srgb, var(--color-accent) 8%, transparent)' : undefined }"
style="flex:1;min-width:0;border:1px dashed var(--color-divider);border-radius:8px;padding:10px 14px;display:flex;flex-wrap:wrap;align-items:center;gap:10px"
>
<label class="btn btn-secondary" style="cursor:pointer;flex:none">Załącz pliki<input type="file" multiple style="display:none" wire:model="noteAttachments"></label>
@forelse ($noteAttachments as $i => $file)
<span class="text-muted" style="font-size:13px;display:flex;align-items:center;gap:6px;min-width:0">
<span class="material-symbols-outlined" style="font-size:16px;flex:none">attach_file</span>
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ $file->getClientOriginalName() }}</span>
<span class="material-symbols-outlined" style="font-size:16px;cursor:pointer;opacity:0.7;flex:none" wire:click="removeNoteAttachment({{ $i }})">close</span>
</span>
@empty
<span class="text-muted" style="font-size:13px">Brak załączników</span>
@endforelse
</div>
</div>
@error('noteAttachments') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
<div style="display:flex;gap:8px">
<button type="button" class="btn btn-secondary" wire:click="cancelAddNote">Anuluj</button>
<button type="button" class="btn btn-primary" wire:click="addNote">Dodaj notatkę</button>
</div>
@else
<button type="button" class="btn btn-secondary" wire:click="startAddNote">Dodaj notatkę</button>
@endif
</div>
@php $threadMessages = $this->publicMessages->skip(1); @endphp
<div style="display:flex;flex-direction:column;gap:10px">
@foreach ($threadMessages as $m)
@php $mine = $m->role === 'operator'; @endphp
<div wire:key="msg-{{ $m->id }}" style="display:flex;justify-content:{{ $mine ? 'flex-end' : 'flex-start' }}">
<div style="max-width:75%;padding:10px 14px;border-radius:12px;font-size:14px;background:{{ $mine ? 'var(--color-accent-800)' : 'var(--color-surface)' }};color:{{ $mine ? 'var(--color-accent-100)' : 'var(--color-text)' }};border:1px solid var(--color-divider)">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px">
<div style="font-size:11px;opacity:0.65;margin-bottom:4px;display:flex;align-items:center;gap:4px">
@if ($m->source === 'email')
<span class="material-symbols-outlined" style="font-size:13px" title="Odebrane e-mailem">mail</span>
@endif
{{ $m->author_name }} &middot; {{ \App\Support\Rel::format($m->created_at) }}{{ $m->edited ? ' · edytowano' : '' }}
</div>
@if ($m->role === 'operator')
<div style="display:flex;gap:6px;flex:none">
<span class="material-symbols-outlined" style="font-size:15px;cursor:pointer;opacity:0.7" wire:click="startEditMessage({{ $m->id }}, @js($m->body))">edit</span>
<span class="material-symbols-outlined" style="font-size:15px;cursor:pointer;opacity:0.7" wire:click="requestDeleteMessage({{ $m->id }})">delete</span>
</div>
@endif
</div>
@if ($editingMessageId === $m->id)
<textarea class="input" wire:model="editingMessageDraft"></textarea>
<div style="display:flex;gap:8px;margin-top:6px">
<button type="button" class="btn btn-secondary" wire:click="cancelEditMessage">Anuluj</button>
<button type="button" class="btn btn-primary" wire:click="saveEditMessage">Zapisz</button>
</div>
@else
<div style="white-space:pre-wrap">{{ $m->body }}</div>
@endif
@foreach ($m->attachments as $a)
<x-message-attachment :attachment="$a" />
@endforeach
</div>
</div>
@endforeach
</div>
<form wire:submit="sendReply" class="card" style="padding:16px;gap:10px">
<select class="input" wire:change="setTemplate($event.target.value)">
<option value="">Wstaw szablon odpowiedzi…</option>
@foreach ($this->responseTemplates as $rt)
<option value="{{ $rt->id }}" @selected($templateId === (string) $rt->id)>{{ $rt->label }}</option>
@endforeach
</select>
<textarea class="input" placeholder="Napisz odpowiedź do klienta…" wire:model="reply"></textarea>
<div style="display:flex;align-items:center;gap:10px">
<div
x-data="{ dragging: false }"
@dragover.prevent="dragging = true"
@dragleave.prevent="dragging = false"
@drop.prevent="dragging = false; const input = $el.querySelector('input[type=file]'); input.files = $event.dataTransfer.files; input.dispatchEvent(new Event('change'))"
:style="{ borderColor: dragging ? 'var(--color-accent)' : undefined, background: dragging ? 'color-mix(in srgb, var(--color-accent) 8%, transparent)' : undefined }"
style="flex:1;min-width:0;border:1px dashed var(--color-divider);border-radius:8px;padding:10px 14px;display:flex;flex-wrap:wrap;align-items:center;gap:10px"
>
<label class="btn btn-secondary" style="cursor:pointer;flex:none">Załącz pliki<input type="file" multiple style="display:none" wire:model="replyAttachments"></label>
@forelse ($replyAttachments as $i => $file)
<span class="text-muted" style="font-size:13px;display:flex;align-items:center;gap:6px;min-width:0">
<span class="material-symbols-outlined" style="font-size:16px;flex:none">attach_file</span>
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ $file->getClientOriginalName() }}</span>
<span class="material-symbols-outlined" style="font-size:16px;cursor:pointer;opacity:0.7;flex:none" wire:click="removeReplyAttachment({{ $i }})">close</span>
</span>
@empty
<span class="text-muted" style="font-size:13px">Brak załączników</span>
@endforelse
</div>
<div style="display:flex;position:relative;flex:none" x-data="{ open: false }" @click.outside="open = false">
<button class="btn btn-primary" style="border-top-right-radius:0;border-bottom-right-radius:0" type="submit">Wyślij odpowiedź</button>
<button class="btn btn-primary" type="button" style="border-top-left-radius:0;border-bottom-left-radius:0;border-left:1px solid var(--color-bg);padding:0 6px" @click="open = !open">
<span class="material-symbols-outlined" style="font-size:16px">expand_more</span>
</button>
<div x-show="open" x-cloak style="position:absolute;bottom:100%;right:0;margin-bottom:6px;background:var(--color-surface);border:1px solid var(--color-divider);border-radius:8px;box-shadow:var(--shadow-md);min-width:220px;overflow:hidden;z-index:10">
@forelse ($this->replyQuickActions as $qa)
<button type="button" class="theme-toggle-option" @click="open = false" wire:click="sendAndTransition({{ $qa->id }})">{{ $qa->label }}</button>
@empty
<div class="text-muted" style="font-size:12.5px;padding:8px 12px">Brak skonfigurowanych szybkich akcji.</div>
@endforelse
</div>
</div>
</div>
@error('replyAttachments') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
</form>
</div>
<div class="aside-col aside-col-wide" style="display:flex;flex-direction:column;gap:14px">
<div class="card" style="padding:16px;gap:6px">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px">
<div class="card-kicker">Zgłaszający</div>
<span class="material-symbols-outlined" style="font-size:16px;cursor:pointer;opacity:0.7" wire:click="startEditReporter">edit</span>
</div>
<div style="font-weight:500">{{ $ticket->name }}</div>
<div class="text-muted" style="font-size:13px">{{ $ticket->email }}</div>
@if ($ticket->customer_id)
<a href="{{ route('operator.queue', ['filterCustomerId' => $ticket->customer_id]) }}" wire:navigate class="text-muted" style="font-size:12px;color:var(--color-accent);text-decoration:none">Zobacz zgłoszenia tej osoby &rarr;</a>
@php $reporterFields = $ticket->customer?->customFieldEntries() ?? []; @endphp
@if (count($reporterFields))
<div style="display:flex;flex-direction:column;gap:2px;margin-top:6px;padding-top:8px;border-top:1px solid var(--color-divider)">
@foreach ($reporterFields as $cf)
<div class="card-meta"><strong>{{ $cf['label'] }}:</strong> {{ $cf['value'] }}</div>
@endforeach
</div>
@endif
@endif
</div>
@if ($editingReporter)
<div class="card" style="padding:16px;gap:10px">
<div class="field">
<label>Zmień zgłaszającego</label>
<select class="input" wire:model="editReporterCustomerId">
<option value="">Wybierz klienta</option>
@foreach ($this->clients as $cl)
<option value="{{ $cl->id }}">{{ $cl->name }} — {{ $cl->email }}</option>
@endforeach
</select>
</div>
<div style="display:flex;gap:8px">
<button type="button" class="btn btn-secondary" wire:click="cancelEditReporter">Anuluj</button>
<button type="button" class="btn btn-primary" wire:click="saveReporter">Zapisz</button>
</div>
</div>
@endif
<div class="card" style="padding:16px;gap:10px">
<div class="card-kicker">Status i przypisanie</div>
<div class="field">
<label>Status</label>
<select class="input" wire:change="setStatus($event.target.value)">
@foreach ($this->statuses as $s)
<option value="{{ $s->key }}" @selected($ticket->status_key === $s->key)>{{ $s->label }}</option>
@endforeach
</select>
</div>
<div class="field">
<label>Priorytet</label>
<select class="input" wire:change="setPriority($event.target.value)">
@foreach ($this->priorities as $p)
<option value="{{ $p->key }}" @selected($ticket->priority_key === $p->key)>{{ $p->label }}</option>
@endforeach
</select>
</div>
<div class="field">
<label>Zespół</label>
<select class="input" wire:change="setTeam($event.target.value)">
<option value="">Brak</option>
@foreach ($this->teams as $tm)
<option value="{{ $tm->id }}" @selected($ticket->team_id === $tm->id)>{{ $tm->name }}</option>
@endforeach
</select>
</div>
<div class="field">
<label>Przypisany do</label>
<div style="display:flex;gap:6px">
<select class="input" wire:change="setAssignee($event.target.value)" style="flex:1">
<option value="">Nieprzypisane</option>
@foreach ($this->operators as $o)
<option value="{{ $o->id }}" @selected($ticket->assignee_id === $o->id)>{{ $o->name }}</option>
@endforeach
</select>
@if ($ticket->assignee_id !== auth()->id())
<button type="button" class="btn btn-secondary" style="flex:none" wire:click="assignToMe">Przypisz do mnie</button>
@endif
</div>
</div>
</div>
<div wire:init="loadSuggestedArticles">
<x-bookstack-suggestions :articles="$this->suggestedArticles" variant="sidebar" title="Baza wiedzy" :show-copy="true" />
</div>
@if (\App\Support\Settings::bool('ai_summary_enabled'))
<div wire:init="loadAiSummary" class="card" style="padding:16px;gap:8px">
<div class="card-kicker">Podsumowanie AI</div>
@if ($aiSummaryLoaded)
@if ($ticket->ai_summary)
<div style="font-size:13px;line-height:1.5">{{ $ticket->ai_summary }}</div>
@if ($ticket->ai_suggested_action)
<div style="margin-top:6px;padding-top:8px;border-top:1px solid var(--color-divider);font-size:13px">
<strong>Sugerowana akcja:</strong> {{ $ticket->ai_suggested_action }}
</div>
@endif
<div class="text-muted" style="font-size:11px;margin-top:4px">Zaktualizowano: {{ $ticket->ai_summary_generated_at?->diffForHumans() }}</div>
@else
<p class="text-muted" style="font-size:12.5px;margin:0">Podsumowanie pojawi się po najbliższym cyklu automatyzacji AI.</p>
@endif
@endif
</div>
@endif
<div class="card" style="padding:16px;gap:8px">
<div class="card-kicker">SLA</div>
<div style="font-size:12.5px">{{ $ticket->slaInfo()['text'] }}</div>
</div>
@if ($ticket->hasCsatRating())
<div class="card" style="padding:16px;gap:8px">
<div class="card-kicker">Ocena obsługi</div>
<div style="display:flex;gap:2px">
@for ($i = 1; $i <= 5; $i++)
<span class="material-symbols-outlined" style="font-size:18px;color:{{ $i <= $ticket->csat_rating ? 'var(--color-accent)' : 'var(--color-divider)' }}">star</span>
@endfor
</div>
@if ($ticket->csat_comment)
<p style="font-size:12.5px;margin:0;white-space:pre-wrap">{{ $ticket->csat_comment }}</p>
@endif
</div>
@endif
<div class="card" style="padding:16px;gap:8px">
<div class="card-kicker">MONITOR CZASU PRACY</div>
<div
style="display:flex;flex-direction:column;gap:6px"
x-data="{
seconds: {{ $ticket->timerElapsedSeconds() }},
running: @js((bool) $ticket->timer_started_at),
tick: null,
navigatingHandler: null,
pagehideHandler: null,
format() {
const h = Math.floor(this.seconds / 3600);
const m = Math.floor((this.seconds % 3600) / 60);
const s = this.seconds % 60;
return [h, m, s].map(n => String(n).padStart(2, '0')).join(':');
},
start() {
clearInterval(this.tick);
if (this.running) {
this.tick = setInterval(() => { this.seconds++ }, 1000);
}
},
init() {
this.start();
// The timer only tracks time this ticket is actually open in
// the browser: stop it server-side the moment the operator
// navigates elsewhere in the app (wire:navigate — a normal
// Livewire call, since the document itself isn't unloading)
this.navigatingHandler = () => {
if (this.running) $wire.call('stopTimer');
};
document.addEventListener('livewire:navigating', this.navigatingHandler);
// …or closes/reloads the tab. Regular fetch/Livewire calls
// can get cut off mid-flight during an actual page unload, so
// this uses sendBeacon against a plain endpoint instead, which
// browsers guarantee to deliver.
this.pagehideHandler = () => {
if (this.running) {
navigator.sendBeacon(@js(route('operator.ticket.stop-timer', $ticket)));
}
};
window.addEventListener('pagehide', this.pagehideHandler);
},
destroy() {
clearInterval(this.tick);
document.removeEventListener('livewire:navigating', this.navigatingHandler);
window.removeEventListener('pagehide', this.pagehideHandler);
},
}"
>
@if ($editingTimer)
<div style="display:flex;align-items:center;gap:4px;font-size:12px">
<input class="input" type="number" min="0" style="width:60px" wire:model="editTimerHours"> godz.
<input class="input" type="number" min="0" max="59" style="width:52px" wire:model="editTimerMinutes"> min.
<input class="input" type="number" min="0" max="59" style="width:52px" wire:model="editTimerSeconds"> sek.
</div>
<div style="display:flex;gap:6px">
<button type="button" class="btn btn-secondary" wire:click="cancelEditTimer">Anuluj</button>
<button type="button" class="btn btn-primary" wire:click="saveEditTimer">Zapisz</button>
</div>
@else
<div style="font-size:12px;display:flex;align-items:center;gap:6px">
<span>Czas w zgłoszeniu: <strong x-text="format()"></strong></span>
<span class="material-symbols-outlined" style="font-size:16px;cursor:pointer;opacity:0.7" wire:click="startEditTimer">edit</span>
</div>
@if ($ticket->isClosed())
<div style="display:flex;flex-direction:column;gap:6px;align-items:flex-start">
<span style="font-size:12px;opacity:0.7">Zgłoszenie zamknięte — zliczanie wstrzymane</span>
<button type="button" class="btn btn-secondary" wire:click="resetTimer" @click="seconds = 0; running = false; clearInterval(tick)">Resetuj</button>
</div>
@else
<div style="display:flex;gap:6px">
@if ($ticket->timer_started_at)
<button type="button" class="btn btn-secondary" wire:click="stopTimer" @click="running = false; clearInterval(tick)">Zatrzymaj</button>
@else
<button type="button" class="btn btn-secondary" wire:click="resumeTimer" @click="running = true; start()">Wznów</button>
@endif
<button type="button" class="btn btn-secondary" wire:click="resetTimer" @click="seconds = 0; running = false; clearInterval(tick)">Resetuj</button>
</div>
@endif
@endif
</div>
</div>
<div class="card" style="padding:16px;gap:8px">
<div class="card-kicker">Historia zmian</div>
@forelse ($ticket->histories as $h)
<div style="font-size:12.5px"><span>{{ $h->text }}</span><div class="text-muted" style="font-size:11px">{{ \App\Support\Rel::format($h->created_at) }}</div></div>
@empty
<p class="text-muted" style="font-size:12px;margin:0">Brak historii zmian.</p>
@endforelse
</div>
<div class="card" style="padding:16px;gap:8px">
<button type="button" class="btn btn-secondary btn-block" wire:click="requestDeleteTicket">Usuń zgłoszenie</button>
</div>
</div>
</div>
</div>
</div>
@if ($pendingDeleteMessageId)
<div class="dialog-backdrop">
<div class="dialog" style="max-width:400px">
<div class="dialog-title">Potwierdź usunięcie</div>
<div class="dialog-body">Czy na pewno usunąć tę wiadomość?</div>
<div class="dialog-actions">
<button type="button" class="btn btn-secondary" wire:click="cancelDeleteMessage">Anuluj</button>
<button type="button" class="btn btn-primary" wire:click="confirmDeleteMessage">Usuń</button>
</div>
</div>
</div>
@endif
@if ($pendingDeleteTicket)
<div class="dialog-backdrop">
<div class="dialog" style="max-width:400px">
<div class="dialog-title">Potwierdź usunięcie</div>
<div class="dialog-body">Czy na pewno usunąć zgłoszenie {{ $ticket->displayNumber() }}?</div>
<div class="dialog-actions">
<button type="button" class="btn btn-secondary" wire:click="cancelDeleteTicket">Anuluj</button>
<button type="button" class="btn btn-primary" wire:click="confirmDeleteTicket">Usuń</button>
</div>
</div>
</div>
@endif
@script
<script>
// Re-runs on every mount of this component, including after a
// wire:navigate to a different ticket, so the socket subscription
// always matches whichever ticket is currently on screen.
if (window.subscribeToTicketChannel) {
window.subscribeToTicketChannel({{ $ticket->id }});
} else {
// echo.js (a deferred module) hasn't run yet — queue the id so it
// subscribes as soon as it does, instead of silently doing nothing.
(window.__pendingTicketChannelIds = window.__pendingTicketChannelIds || []).push({{ $ticket->id }});
}
</script>
@endscript
</div>