- Configurable ticket numbering (Admin > Konfiguracja > Ogólne): admin-set
  prefix and minimum zero-padded length for the ticket number.
- "Ukryj kolejność zgłoszeń": an opt-in mode that displays a stable,
  HMAC-derived checksum instead of the sequential ticket number, so it gives
  no indication of ticket volume or creation order. Ticket URLs switch to
  the same checksum when this is on, so a link and the number on the page it
  points to always match. The REST API is unaffected — pinned to `id`
  regardless of this setting. Search now also matches by checksum.
- Fixed: attachments no longer show an inline image thumbnail in the
  message thread — every attachment (images included) shows as just its
  filename, opening in a new tab on click.
- Docs: README/ARCHITECTURE/wiki updated for all of the above.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 09:57:39 +02:00
parent ab90abcaa3
commit 63178b366e
21 changed files with 368 additions and 42 deletions

View File

@@ -2,24 +2,12 @@
@php
$url = \Illuminate\Support\Facades\Storage::disk('public')->url($attachment->path);
$isImage = \Illuminate\Support\Str::startsWith($attachment->mime ?? '', 'image/');
@endphp
@if ($isImage)
<a href="{{ $url }}" target="_blank" style="display:block;margin-top:8px">
<img
src="{{ $url }}"
alt="{{ $attachment->original_name }}"
loading="lazy"
style="max-width:220px;max-height:160px;border-radius:8px;border:1px solid var(--color-divider);object-fit:cover;cursor:zoom-in;display:block"
>
</a>
@else
<a
href="{{ $url }}"
target="_blank"
style="display:inline-flex;align-items:center;gap:6px;margin-top:8px;padding:5px 10px;border:1px solid var(--color-divider);border-radius:6px;font-size:12.5px;color:inherit;text-decoration:none;background:color-mix(in srgb, var(--color-text) 5%, transparent)"
>
<span class="material-symbols-outlined" style="font-size:15px">attach_file</span>{{ $attachment->original_name }}
</a>
@endif
<a
href="{{ $url }}"
target="_blank"
style="display:inline-flex;align-items:center;gap:6px;margin-top:8px;padding:5px 10px;border:1px solid var(--color-divider);border-radius:6px;font-size:12.5px;color:inherit;text-decoration:none;background:color-mix(in srgb, var(--color-text) 5%, transparent)"
>
<span class="material-symbols-outlined" style="font-size:15px">attach_file</span>{{ $attachment->original_name }}
</a>

View File

@@ -617,6 +617,21 @@ $tabGroups = [
</select>
</div>
<label class="radio"><input type="checkbox" wire:model="systemConfig.autoAssignByCategory" style="position:static;opacity:1;width:auto;height:auto">Automatyczne przypisywanie do zespołu wg kategorii</label>
<div class="hr"></div>
<div class="field">
<label>Prefiks numeru zgłoszenia</label>
<input class="input" maxlength="20" placeholder="#" wire:model.live="systemConfig.ticketNumberPrefix">
</div>
<div class="field">
<label>Minimalna długość numeru (uzupełniana zerami z przodu)</label>
<input class="input" type="number" min="1" max="10" wire:model.live="systemConfig.ticketNumberMinLength">
</div>
<label class="radio"><input type="checkbox" wire:model.live="systemConfig.ticketNumberObfuscate" style="position:static;opacity:1;width:auto;height:auto">Ukryj kolejność zgłoszeń (numer wyświetlany jako suma kontrolna zamiast kolejnego numeru)</label>
<div class="text-muted" style="font-size:12px">
ID z bazy: {{ $this->ticketNumberPreview['id'] }} &rarr; podgląd numeru: {{ $this->ticketNumberPreview['formatted'] }}
</div>
</div>
<div class="card" style="padding:20px;gap:14px">

View File

@@ -19,7 +19,7 @@
@foreach (($tab === 'current' ? $this->currentTickets : $this->archiveTickets) as $ticket)
<a href="{{ route('client.ticket', $ticket) }}" wire:navigate class="card elev-sm" style="padding:16px;cursor:pointer;flex-direction:row;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;text-decoration:none;color:inherit">
<div>
<div style="font-weight:500">#{{ $ticket->number }} — {{ $ticket->subject }}</div>
<div style="font-weight:500">{{ $ticket->displayNumber() }} {{ $ticket->subject }}</div>
<div class="card-meta">{{ $ticket->categoryLabel() }} &middot; {{ \App\Support\Rel::format($ticket->updated_at) }}</div>
</div>
<div style="display:flex;gap:6px">

View File

@@ -28,7 +28,7 @@
@endphp
<div class="card" style="padding:22px;gap:10px">
<div class="card-kicker">Zgłoszenie #{{ $ticket->number }}</div>
<div class="card-kicker">Zgłoszenie {{ $ticket->displayNumber() }}</div>
<h2 style="margin:2px 0 0">{{ $ticket->subject }}</h2>
<div class="card-meta">{{ $ticket->categoryLabel() }} &middot; utworzono {{ \App\Support\Rel::format($ticket->created_at) }}</div>
<div style="white-space:pre-wrap;font-size:14px;margin-top:4px">{{ $ticket->body }}</div>
@@ -168,7 +168,7 @@
<div class="card-kicker">Inne Twoje zgłoszenia</div>
@forelse ($this->otherTickets as $ot)
<a href="{{ route('client.ticket', $ot) }}" wire:navigate style="display:flex;justify-content:space-between;align-items:center;gap:8px;cursor:pointer;text-decoration:none;color:inherit">
<span style="font-size:13px">#{{ $ot->number }} — {{ $ot->subject }}</span>
<span style="font-size:13px">{{ $ot->displayNumber() }} {{ $ot->subject }}</span>
<span style="{{ $ot->statusStyle() }};flex:none">{{ $ot->statusLabel() }}</span>
</a>
@empty

View File

@@ -9,11 +9,11 @@
@if ($this->submittedTicket)
<div class="card elev-md" style="padding:32px;gap:14px;text-align:left">
<span class="tag tag-accent" style="align-self:flex-start">Zgłoszenie przyjęte</span>
<h2 style="margin:0">Zgłoszenie #{{ $this->submittedTicket->number }} zostało utworzone</h2>
<h2 style="margin:0">Zgłoszenie {{ $this->submittedTicket->displayNumber() }} zostało utworzone</h2>
<p class="text-muted" style="margin:0">Zapisz numer zgłoszenia i adres e-mail będziesz mógł/mogła sprawdzić status, kontaktując się z zespołem wsparcia. Aktualizacje będziemy wysyłać na Twój adres e-mail.</p>
<div class="hr"></div>
<div style="display:flex;flex-direction:column;gap:4px;font-size:14px">
<div><strong>Numer zgłoszenia:</strong> #{{ $this->submittedTicket->number }}</div>
<div><strong>Numer zgłoszenia:</strong> {{ $this->submittedTicket->displayNumber() }}</div>
<div><strong>Temat:</strong> {{ $this->submittedTicket->subject }}</div>
<div><strong>Kategoria:</strong> {{ $this->submittedTicket->categoryLabel() }}</div>
<div><strong>Zgłaszający:</strong> {{ $this->submittedTicket->email }}</div>

View File

@@ -161,7 +161,7 @@
<tr wire:key="ticket-{{ $t->id }}">
<td class="td-select"><input type="checkbox" @checked(in_array($t->id, $selectedIds)) wire:click="toggleSelect({{ $t->id }})"></td>
@if (in_array('number', $visibleColumns))
<td data-label="Numer" class="td-title"><a href="{{ route('operator.ticket', $t) }}" wire:navigate style="color:inherit;text-decoration:none;cursor:pointer">{{ $t->number }}</a></td>
<td data-label="Numer" class="td-title"><a href="{{ route('operator.ticket', $t) }}" wire:navigate style="color:inherit;text-decoration:none;cursor:pointer">{{ $t->displayNumber() }}</a></td>
@endif
@if (in_array('subject', $visibleColumns))
<td data-label="Temat" class="td-title"><a href="{{ route('operator.ticket', $t) }}" wire:navigate style="color:inherit;text-decoration:none;cursor:pointer;white-space:nowrap">{{ $t->subject }}</a></td>

View File

@@ -36,7 +36,7 @@
<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 class="card-kicker">Zgłoszenie #{{ $ticket->number }}</div>
<div class="card-kicker">Zgłoszenie {{ $ticket->displayNumber() }}</div>
@unless ($editingDetails)
<button type="button" class="btn btn-ghost" style="padding:0" wire:click="toggleEditDetails">Edytuj</button>
@endunless
@@ -456,7 +456,7 @@
<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->number }}?</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>