- Triggers (Admin > Wyzwalacze): event-driven rules that fire immediately on
  a ticket lifecycle event (created/updated/status/priority/assignee/team/
  category changed, new reply), with AND-conditions and ordered actions
  (set status/priority/team/assignee, send e-mail). Ships its own dedicated,
  freely add/edit/delete-able e-mail templates, kept separate from the fixed
  system templates.
- Ticket watching: operators can star/"Obserwuj" any ticket to follow it
  regardless of assignment/team.
- Real-time notification bell (private per-user broadcast channel, 30s
  fallback poll) with an opt-in in-tab browser push notification.
- Per-user notification preferences (/settings/notifications): scope
  (mine/unassigned/watched/all) and e-mail toggle per event category.
- Admin > Integracje: new tab for LDAP/AD + BookStack config, split out of
  Konfiguracja.
- Operator queue: Podkategoria/Zespół/Utworzono columns (off by default).
- Obserwuj button moved next to the auto-refresh countdown; trigger
  condition builder shows subcategory/zgłaszający as name dropdowns instead
  of raw IDs; /settings/notifications got a back link, full-width push
  card, and a bordered table container; admin panel tab and operator queue
  view now persist across a plain page refresh.
- 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-22 23:43:01 +02:00
parent 0b06687ea1
commit ab90abcaa3
47 changed files with 2480 additions and 139 deletions

View File

@@ -8,6 +8,7 @@ $tabGroups = [
['key' => 'reply-quick-actions', 'label' => 'Szybkie akcje odpowiedzi', 'icon' => 'bolt'],
['key' => 'response-templates', 'label' => 'Szablony odpowiedzi', 'icon' => 'chat'],
['key' => 'automation-rules', 'label' => 'Automatyzacja SLA', 'icon' => 'bolt'],
['key' => 'triggers', 'label' => 'Wyzwalacze', 'icon' => 'rule'],
],
'Zespół' => [
['key' => 'users', 'label' => 'Użytkownicy', 'icon' => 'group'],
@@ -16,8 +17,10 @@ $tabGroups = [
],
'Ustawienia' => [
['key' => 'templates', 'label' => 'Szablony e-mail', 'icon' => 'mail'],
['key' => 'email', 'label' => 'E-MAIL', 'icon' => 'forward_to_inbox'],
['key' => 'branding', 'label' => 'Wygląd i branding', 'icon' => 'palette'],
['key' => 'config', 'label' => 'Konfiguracja', 'icon' => 'settings'],
['key' => 'integrations', 'label' => 'Integracje', 'icon' => 'hub'],
['key' => 'api-keys', 'label' => 'Klucze API', 'icon' => 'vpn_key'],
['key' => 'about', 'label' => 'O aplikacji', 'icon' => 'info'],
],
@@ -356,6 +359,11 @@ $tabGroups = [
@endif
@endif
{{-- ================= TRIGGERS ================= --}}
@if ($tab === 'triggers')
<livewire:admin.triggers />
@endif
{{-- ================= STATUSES ================= --}}
@if ($tab === 'statuses')
<h3 style="margin:0 0 6px">Statusy</h3>
@@ -435,6 +443,27 @@ $tabGroups = [
{{-- ================= EMAIL TEMPLATES ================= --}}
@if ($tab === 'templates')
<h3 style="margin:0 0 6px">Powiadomienia e-mail</h3>
<p class="text-muted" style="font-size:12.5px;margin:0 0 14px">Każde zdarzenie ma stały, przypisany na stałe szablon możesz go dowolnie edytować, ale nie zmienić na inny. Wyłącz przełącznik, żeby dana wiadomość nigdy nie była wysyłana.</p>
<div class="table-wrap">
<table class="table">
<thead><tr><th>Zdarzenie</th><th>Odbiorca</th><th>Wysyłane</th><th></th></tr></thead>
<tbody>
@foreach ($this->notificationSettings as $ns)
<tr>
<td style="white-space:nowrap">{{ $ns->trigger_label }}</td>
<td><span class="tag tag-outline">{{ $ns->recipient === 'operator' ? 'Operator' : 'Zgłaszający' }}</span></td>
<td><input type="checkbox" @checked($ns->enabled) wire:click="toggleNotificationEnabled({{ $ns->id }})"></td>
<td><button type="button" class="btn btn-ghost" wire:click="editEmailTemplate({{ $ns->email_template_id }})">Edytuj szablon</button></td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
{{-- ================= E-MAIL ================= --}}
@if ($tab === 'email')
<h3 style="margin:0 0 6px">Wygląd wiadomości e-mail</h3>
<p class="text-muted" style="font-size:12.5px;margin:0 0 14px">Każde powiadomienie wysyłane jest w stałym „pudełku” (nazwa firmy, ramka, treść zdarzenia) tu edytujesz tylko jego stopkę. Po prawej podgląd na żywo na przykładowym zgłoszeniu.</p>
@@ -454,23 +483,45 @@ $tabGroups = [
</div>
</div>
<h3 style="margin:0 0 6px">Powiadomienia e-mail</h3>
<p class="text-muted" style="font-size:12.5px;margin:0 0 14px">Każde zdarzenie ma stały, przypisany na stałe szablon możesz go dowolnie edytować, ale nie zmienić na inny. Wyłącz przełącznik, żeby dana wiadomość nigdy nie była wysyłana.</p>
<div class="table-wrap">
<table class="table">
<thead><tr><th>Zdarzenie</th><th>Odbiorca</th><th>Wysyłane</th><th></th></tr></thead>
<tbody>
@foreach ($this->notificationSettings as $ns)
<tr>
<td style="white-space:nowrap">{{ $ns->trigger_label }}</td>
<td><span class="tag tag-outline">{{ $ns->recipient === 'operator' ? 'Operator' : 'Zgłaszający' }}</span></td>
<td><input type="checkbox" @checked($ns->enabled) wire:click="toggleNotificationEnabled({{ $ns->id }})"></td>
<td><button type="button" class="btn btn-ghost" wire:click="editEmailTemplate({{ $ns->email_template_id }})">Edytuj szablon</button></td>
</tr>
@endforeach
</tbody>
</table>
</div>
<h3 style="margin:0 0 14px">E-mail (SMTP)</h3>
<form wire:submit="saveMailConfig" class="card" style="padding:20px;gap:14px;max-width:480px">
<div class="field"><label>Adres nadawcy</label><input class="input" type="email" placeholder="wsparcie@firma.pl" wire:model="mailConfig.fromAddress"></div>
<div class="field"><label>Nazwa nadawcy</label><input class="input" placeholder="Zespół Wsparcia" wire:model="mailConfig.fromName"></div>
<div class="hr"></div>
<label class="radio"><input type="checkbox" wire:model="mailConfig.smtpEnabled" style="position:static;opacity:1;width:auto;height:auto"><strong>Włącz wysyłkę przez własny serwer SMTP</strong></label>
<span class="text-muted" style="font-size:11.5px;margin-top:-8px">Bez włączenia aplikacja wysyła pocztę zgodnie z konfiguracją środowiska (.env).</span>
@if ($mailConfig['smtpEnabled'])
<div class="field"><label>Host SMTP</label><input class="input" placeholder="smtp.example.com" wire:model="mailConfig.smtpHost"></div>
<div style="display:flex;gap:10px">
<div class="field" style="flex:1"><label>Port</label><input class="input" type="number" placeholder="587" wire:model="mailConfig.smtpPort"></div>
<div class="field" style="flex:1">
<label>Szyfrowanie</label>
<select class="input" wire:model="mailConfig.smtpEncryption">
<option value="none">Brak</option>
<option value="tls">STARTTLS</option>
<option value="ssl">SSL/TLS</option>
</select>
</div>
</div>
<div class="field"><label>Użytkownik</label><input class="input" wire:model="mailConfig.smtpUsername"></div>
<div class="field"><label>Hasło</label><input class="input" type="password" placeholder="(bez zmian jeśli puste)" wire:model="mailConfig.smtpPassword"></div>
<div style="display:flex;gap:10px;margin-top:8px;align-items:center;flex-wrap:wrap">
<button type="button" class="btn btn-secondary" wire:click="testMailConnection">Wyślij testową wiadomość</button>
<button type="submit" class="btn btn-primary">Zapisz</button>
@if ($mailTestResult === 'ok')
<div style="display:flex;align-items:center;gap:6px;color:var(--color-success)"><span class="material-symbols-outlined" style="font-size:18px">check_circle</span>Wysłano na Twój adres</div>
@elseif ($mailTestResult === 'error')
<div style="display:flex;align-items:center;gap:6px;color:var(--color-danger)"><span class="material-symbols-outlined" style="font-size:18px">error</span>Błąd wysyłki</div>
@endif
</div>
@else
<button type="submit" class="btn btn-primary" style="align-self:flex-start">Zapisz</button>
@endif
</form>
@endif
{{-- ================= BRANDING ================= --}}
@@ -615,46 +666,14 @@ $tabGroups = [
</div>
</form>
<form wire:submit="saveMailConfig" class="card" style="padding:20px;gap:14px">
<h4 style="margin:0">E-mail (SMTP)</h4>
<div class="field"><label>Adres nadawcy</label><input class="input" type="email" placeholder="wsparcie@firma.pl" wire:model="mailConfig.fromAddress"></div>
<div class="field"><label>Nazwa nadawcy</label><input class="input" placeholder="Zespół Wsparcia" wire:model="mailConfig.fromName"></div>
<p class="text-muted" style="font-size:11.5px;margin:-4px 0 0">Stopka wiadomości e-mail edytowana jest w zakładce „Szablony e-mail”.</p>
</div>
@endif
<div class="hr"></div>
{{-- ================= INTEGRATIONS ================= --}}
@if ($tab === 'integrations')
<h3 style="margin:0 0 16px">Integracje</h3>
<label class="radio"><input type="checkbox" wire:model="mailConfig.smtpEnabled" style="position:static;opacity:1;width:auto;height:auto"><strong>Włącz wysyłkę przez własny serwer SMTP</strong></label>
<span class="text-muted" style="font-size:11.5px;margin-top:-8px">Bez włączenia aplikacja wysyła pocztę zgodnie z konfiguracją środowiska (.env).</span>
@if ($mailConfig['smtpEnabled'])
<div class="field"><label>Host SMTP</label><input class="input" placeholder="smtp.example.com" wire:model="mailConfig.smtpHost"></div>
<div style="display:flex;gap:10px">
<div class="field" style="flex:1"><label>Port</label><input class="input" type="number" placeholder="587" wire:model="mailConfig.smtpPort"></div>
<div class="field" style="flex:1">
<label>Szyfrowanie</label>
<select class="input" wire:model="mailConfig.smtpEncryption">
<option value="none">Brak</option>
<option value="tls">STARTTLS</option>
<option value="ssl">SSL/TLS</option>
</select>
</div>
</div>
<div class="field"><label>Użytkownik</label><input class="input" wire:model="mailConfig.smtpUsername"></div>
<div class="field"><label>Hasło</label><input class="input" type="password" placeholder="(bez zmian jeśli puste)" wire:model="mailConfig.smtpPassword"></div>
<div style="display:flex;gap:10px;margin-top:8px;align-items:center;flex-wrap:wrap">
<button type="button" class="btn btn-secondary" wire:click="testMailConnection">Wyślij testową wiadomość</button>
<button type="submit" class="btn btn-primary">Zapisz</button>
@if ($mailTestResult === 'ok')
<div style="display:flex;align-items:center;gap:6px;color:var(--color-success)"><span class="material-symbols-outlined" style="font-size:18px">check_circle</span>Wysłano na Twój adres</div>
@elseif ($mailTestResult === 'error')
<div style="display:flex;align-items:center;gap:6px;color:var(--color-danger)"><span class="material-symbols-outlined" style="font-size:18px">error</span>Błąd wysyłki</div>
@endif
</div>
@else
<button type="submit" class="btn btn-primary" style="align-self:flex-start">Zapisz</button>
@endif
</form>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:20px;align-items:start">
<form wire:submit="saveLdapConfig" class="card" style="padding:20px;gap:14px">
<h4 style="margin:0">LDAP / Active Directory</h4>

View File

@@ -0,0 +1,303 @@
@php
$eventLabels = \App\Livewire\Admin\Triggers::eventLabels();
$fieldLabels = \App\Livewire\Admin\Triggers::fieldLabels();
$operatorLabels = \App\Livewire\Admin\Triggers::operatorLabels();
$actionTypeLabels = \App\Livewire\Admin\Triggers::actionTypeLabels();
@endphp
<div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
<h3 style="margin:0">Wyzwalacze</h3>
<button class="btn btn-primary" type="button" wire:click="openForm">+ Nowy wyzwalacz</button>
</div>
<p class="text-muted" style="font-size:12.5px;margin:0 0 14px">
Wyzwalacze reagują natychmiast na zdarzenie w zgłoszeniu (utworzenie, zmiana pola, nowy komentarz) w odróżnieniu od Automatyzacji SLA (zakładka obok), która działa na podstawie czasu milczenia klienta. Warunki wyzwalacza muszą być spełnione wszystkie naraz (ORAZ); akcje wykonują się w podanej kolejności.
</p>
@if ($this->triggers->isNotEmpty())
<div class="table-wrap">
<table class="table">
<thead>
<tr>
<th></th>
<th>Nazwa</th>
<th>Zdarzenie</th>
<th>Warunki</th>
<th>Akcje</th>
<th>Aktywny</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach ($this->triggers as $trigger)
<tr wire:key="trigger-{{ $trigger->id }}">
<td style="white-space:nowrap">
<button type="button" class="btn btn-ghost" style="padding:0 4px" wire:click="moveUp({{ $trigger->id }})" title="Przenieś wyżej">
<span class="material-symbols-outlined" style="font-size:16px">arrow_upward</span>
</button>
<button type="button" class="btn btn-ghost" style="padding:0 4px" wire:click="moveDown({{ $trigger->id }})" title="Przenieś niżej">
<span class="material-symbols-outlined" style="font-size:16px">arrow_downward</span>
</button>
</td>
<td style="white-space:nowrap">{{ $trigger->name }}</td>
<td><span class="tag tag-outline">{{ $eventLabels[$trigger->event] ?? $trigger->event }}</span></td>
<td class="text-muted" style="font-size:12px">
{{ count($trigger->conditions) }} {{ count($trigger->conditions) === 1 ? 'warunek' : 'warunków' }}
</td>
<td class="text-muted" style="font-size:12px">
{{ count($trigger->actions) }} {{ count($trigger->actions) === 1 ? 'akcja' : 'akcji' }}
</td>
<td><input type="checkbox" @checked($trigger->enabled) wire:click="toggleEnabled({{ $trigger->id }})"></td>
<td>
<div style="display:flex;gap:6px;justify-content:flex-end">
<button class="btn btn-ghost" type="button" wire:click="editTrigger({{ $trigger->id }})">Edytuj</button>
<button class="btn btn-ghost" type="button" wire:click="removeTrigger({{ $trigger->id }})" wire:confirm="Usunąć wyzwalacz „{{ $trigger->name }}”?">Usuń</button>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<p class="text-muted" style="font-size:13px">Brak wyzwalaczy. Utwórz pierwszy używając przycisku wyżej.</p>
@endif
<div class="hr" style="margin:22px 0"></div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
<div>
<h3 style="margin:0">Szablony e-mail wyzwalaczy</h3>
<p class="text-muted" style="font-size:12.5px;margin:4px 0 0">
Osobne od szablonów w zakładce „Szablony e-mail” (te przypisane na stałe do zdarzeń systemowych) te
tutaj możesz dowolnie dodawać, edytować i usuwać, do wykorzystania w akcji „Wyślij powiadomienie e-mail” wyzwalacza.
</p>
</div>
<button class="btn btn-primary" type="button" wire:click="openTemplateForm" style="flex:none">+ Nowy szablon</button>
</div>
@if ($this->emailTemplates->isNotEmpty())
<div class="table-wrap">
<table class="table">
<thead><tr><th>Nazwa</th><th>Temat</th><th></th></tr></thead>
<tbody>
@foreach ($this->emailTemplates as $template)
<tr wire:key="trigger-template-{{ $template->id }}">
<td style="white-space:nowrap">{{ $template->name }}</td>
<td>{{ $template->subject }}</td>
<td>
<div style="display:flex;gap:6px;justify-content:flex-end">
<button class="btn btn-ghost" type="button" wire:click="editTemplate({{ $template->id }})">Edytuj</button>
<button class="btn btn-ghost" type="button" wire:click="removeTemplate({{ $template->id }})" wire:confirm="Usunąć szablon „{{ $template->name }}”?">Usuń</button>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<p class="text-muted" style="font-size:13px">Brak szablonów. Dodaj pierwszy używając przycisku wyżej.</p>
@endif
@if ($templateFormOpen)
<div class="dialog-backdrop">
<form wire:submit="submitTemplate" class="dialog" style="max-width:560px;max-height:88vh;overflow:auto">
<div class="dialog-title">{{ $editingTemplateId ? 'Edytuj szablon' : 'Nowy szablon' }}</div>
<div class="field">
<label>Nazwa szablonu</label>
<input class="input" wire:model="templateForm.name" placeholder="np. Przypomnienie o braku odpowiedzi">
@error('templateForm.name') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
</div>
<div class="field">
<label>Temat wiadomości</label>
<input class="input" wire:model="templateForm.subject">
@error('templateForm.subject') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
</div>
<div class="field">
<label>Treść</label>
<x-quill-editor wire:key="trigger-template-body-{{ $editingTemplateId ?? 'new' }}" :value="$templateForm['body']" on-change="setTemplateBodyDraft" min-height="160px" />
@error('templateForm.body') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
</div>
<p class="text-muted" style="font-size:12px;margin:0">Dostępne zmienne: {numer}, {imie}, {temat}, {status}, {kategoria}, {priorytet}, {zespol}, {operator}, {link}. Ta treść trafia do wspólnego szablonu-pudełka (zakładka „E-MAIL”) w miejscu {tresc}.</p>
<div class="dialog-actions">
<button type="button" class="btn btn-secondary" wire:click="closeTemplateForm">Anuluj</button>
<button type="submit" class="btn btn-primary">{{ $editingTemplateId ? 'Zapisz' : 'Utwórz' }}</button>
</div>
</form>
</div>
@endif
@if ($formOpen)
<div class="dialog-backdrop">
<form wire:submit="submit" class="dialog" style="max-width:640px;max-height:88vh;overflow:auto">
<div class="dialog-title">{{ $editingId ? 'Edytuj wyzwalacz' : 'Nowy wyzwalacz' }}</div>
<div class="field">
<label>Nazwa</label>
<input class="input" wire:model="form.name" placeholder="np. Priorytet krytyczny → zespół VIP">
@error('form.name') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
</div>
<div class="field">
<label>Zdarzenie</label>
<select class="input" wire:model="form.event">
@foreach ($eventLabels as $key => $label)
<option value="{{ $key }}">{{ $label }}</option>
@endforeach
</select>
</div>
<label class="radio"><input type="checkbox" wire:model="form.enabled" style="position:static;opacity:1;width:auto;height:auto">Aktywny</label>
<div class="hr"></div>
<div style="display:flex;justify-content:space-between;align-items:center">
<label style="font-weight:500">Warunki (wszystkie muszą być spełnione)</label>
<button type="button" class="btn btn-ghost" wire:click="addCondition">+ Dodaj warunek</button>
</div>
@foreach ($form['conditions'] as $i => $condition)
<div wire:key="condition-{{ $i }}" style="display:flex;gap:6px;align-items:flex-start">
<select class="input" style="flex:1" wire:model.live="form.conditions.{{ $i }}.field">
@foreach ($fieldLabels as $key => $label)
<option value="{{ $key }}">{{ $label }}</option>
@endforeach
</select>
<select class="input" style="flex:1" wire:model.live="form.conditions.{{ $i }}.operator">
@foreach ($operatorLabels as $key => $label)
<option value="{{ $key }}">{{ $label }}</option>
@endforeach
</select>
@if (! in_array($condition['operator'], ['is_empty', 'is_not_empty']))
@if (($condition['field'] ?? null) === 'status_key')
<select class="input" style="flex:1" wire:model="form.conditions.{{ $i }}.value">
@foreach ($this->statuses as $status)
<option value="{{ $status->key }}">{{ $status->label }}</option>
@endforeach
</select>
@elseif (($condition['field'] ?? null) === 'priority_key')
<select class="input" style="flex:1" wire:model="form.conditions.{{ $i }}.value">
@foreach ($this->priorities as $priority)
<option value="{{ $priority->key }}">{{ $priority->label }}</option>
@endforeach
</select>
@elseif (($condition['field'] ?? null) === 'team_id')
<select class="input" style="flex:1" wire:model="form.conditions.{{ $i }}.value">
@foreach ($this->teams as $team)
<option value="{{ $team->id }}">{{ $team->name }}</option>
@endforeach
</select>
@elseif (($condition['field'] ?? null) === 'assignee_id')
<select class="input" style="flex:1" wire:model="form.conditions.{{ $i }}.value">
@foreach ($this->operators as $operator)
<option value="{{ $operator->id }}">{{ $operator->name }}</option>
@endforeach
</select>
@elseif (($condition['field'] ?? null) === 'subcategory_id')
<select class="input" style="flex:1" wire:model="form.conditions.{{ $i }}.value">
@foreach ($this->subcategories as $sub)
<option value="{{ $sub->id }}">{{ $sub->category->name }} / {{ $sub->name }}</option>
@endforeach
</select>
@elseif (($condition['field'] ?? null) === 'customer_id')
<select class="input" style="flex:1" wire:model="form.conditions.{{ $i }}.value">
@foreach ($this->customers as $customer)
<option value="{{ $customer->id }}">{{ $customer->name }}</option>
@endforeach
</select>
@else
<input class="input" style="flex:1" wire:model="form.conditions.{{ $i }}.value" placeholder="wartość">
@endif
@else
<div style="flex:1"></div>
@endif
<button type="button" class="btn btn-ghost" style="padding:0 6px" wire:click="removeCondition({{ $i }})" title="Usuń warunek">
<span class="material-symbols-outlined" style="font-size:16px">close</span>
</button>
</div>
@endforeach
@if (empty($form['conditions']))
<p class="text-muted" style="font-size:12px;margin:0">Brak warunków wyzwalacz zadziała za każdym razem, gdy wybrane zdarzenie wystąpi.</p>
@endif
<div class="hr"></div>
<div style="display:flex;justify-content:space-between;align-items:center">
<label style="font-weight:500">Akcje (wykonywane po kolei)</label>
<button type="button" class="btn btn-ghost" wire:click="addAction">+ Dodaj akcję</button>
</div>
@error('form.actions') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
@foreach ($form['actions'] as $i => $action)
<div wire:key="action-{{ $i }}" class="card" style="padding:10px;gap:6px">
<div style="display:flex;gap:6px;align-items:center">
<select class="input" style="flex:1" wire:model.live="form.actions.{{ $i }}.type">
@foreach ($actionTypeLabels as $key => $label)
<option value="{{ $key }}">{{ $label }}</option>
@endforeach
</select>
<button type="button" class="btn btn-ghost" style="padding:0 4px" wire:click="moveActionUp({{ $i }})" title="Przenieś wyżej">
<span class="material-symbols-outlined" style="font-size:16px">arrow_upward</span>
</button>
<button type="button" class="btn btn-ghost" style="padding:0 4px" wire:click="moveActionDown({{ $i }})" title="Przenieś niżej">
<span class="material-symbols-outlined" style="font-size:16px">arrow_downward</span>
</button>
<button type="button" class="btn btn-ghost" style="padding:0 6px" wire:click="removeAction({{ $i }})" title="Usuń akcję">
<span class="material-symbols-outlined" style="font-size:16px">close</span>
</button>
</div>
@if (($action['type'] ?? null) === 'set_status')
<select class="input" wire:model="form.actions.{{ $i }}.value">
@foreach ($this->statuses as $status)
<option value="{{ $status->key }}">{{ $status->label }}</option>
@endforeach
</select>
@elseif (($action['type'] ?? null) === 'set_priority')
<select class="input" wire:model="form.actions.{{ $i }}.value">
@foreach ($this->priorities as $priority)
<option value="{{ $priority->key }}">{{ $priority->label }}</option>
@endforeach
</select>
@elseif (($action['type'] ?? null) === 'set_team')
<select class="input" wire:model="form.actions.{{ $i }}.value">
@foreach ($this->teams as $team)
<option value="{{ $team->id }}">{{ $team->name }}</option>
@endforeach
</select>
@elseif (($action['type'] ?? null) === 'set_assignee')
<select class="input" wire:model="form.actions.{{ $i }}.value">
@foreach ($this->operators as $operator)
<option value="{{ $operator->id }}">{{ $operator->name }}</option>
@endforeach
</select>
@elseif (($action['type'] ?? null) === 'send_notification')
<div style="display:flex;gap:6px">
<select class="input" style="flex:1" wire:model="form.actions.{{ $i }}.recipient">
<option value="client">Zgłaszający</option>
<option value="operator">Przypisany operator</option>
</select>
<select class="input" style="flex:1" wire:model="form.actions.{{ $i }}.email_template_id">
<option value=""> wybierz szablon </option>
@foreach ($this->emailTemplates as $template)
<option value="{{ $template->id }}">{{ $template->name }}</option>
@endforeach
</select>
</div>
@endif
</div>
@endforeach
<div class="dialog-actions">
<button type="button" class="btn btn-secondary" wire:click="closeForm">Anuluj</button>
<button type="submit" class="btn btn-primary">{{ $editingId ? 'Zapisz' : 'Utwórz' }}</button>
</div>
</form>
</div>
@endif
</div>

View File

@@ -172,6 +172,9 @@
@if (in_array('category', $visibleColumns))
<td data-label="Kategoria" style="white-space:nowrap">{{ $t->categoryLabel() }}</td>
@endif
@if (in_array('subcategory', $visibleColumns))
<td data-label="Podkategoria" style="white-space:nowrap">{{ $t->subcategory?->name ?? '—' }}</td>
@endif
@if (in_array('priority', $visibleColumns))
<td data-label="Priorytet"><span style="{{ $t->priorityStyle() }}">{{ $t->priorityLabel() }}</span></td>
@endif
@@ -184,6 +187,12 @@
@if (in_array('assignee', $visibleColumns))
<td data-label="Przypisany" style="white-space:nowrap">{{ $t->assignee?->name ?? 'Nieprzypisane' }}</td>
@endif
@if (in_array('team', $visibleColumns))
<td data-label="Zespół" style="white-space:nowrap">{{ $t->team?->name ?? '—' }}</td>
@endif
@if (in_array('created', $visibleColumns))
<td data-label="Utworzono" style="white-space:nowrap">{{ \App\Support\Rel::format($t->created_at) }}</td>
@endif
</tr>
@endforeach
</tbody>

View File

@@ -4,7 +4,18 @@
<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">&larr; Wróć do listy</a>
<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

View File

@@ -0,0 +1,97 @@
@php
$categoryLabels = [
'new_ticket' => 'Nowe zgłoszenie',
'ticket_update' => 'Aktualizacja zgłoszenia',
'escalation' => 'Zgłoszenie eskalowane',
];
$scopeColumns = [
'scope_mine' => 'Moje zgłoszenia',
'scope_unassigned' => 'Nie przypisany',
'scope_watched' => 'Obserwowane zgłoszenia',
'scope_all' => 'Wszystkie zgłoszenia',
];
@endphp
<div style="flex:1;display:flex;flex-direction:column">
<x-topbar />
<div class="page-pad" style="flex:1;padding:28px;display:flex;flex-direction:column;gap:24px;max-width:920px;width:100%;margin:0 auto;box-sizing:border-box">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap">
<div>
<h3 style="margin:0 0 6px">Powiadomienia</h3>
<p class="text-muted" style="font-size:12.5px;margin:0">Wybierz, o których zgłoszeniach chcesz być informowany dzwoneczkiem w aplikacji, i przy których zdarzeniach dodatkowo wysłać Ci e-mail.</p>
</div>
<a href="{{ auth()->user()->isAdmin() ? route('admin.panel') : route('operator.queue') }}" wire:navigate class="btn btn-ghost" style="padding:0;flex:none">&larr; Wróć</a>
</div>
<div class="card" style="padding:0;overflow:hidden">
<div class="table-wrap">
<table class="table" style="margin:0">
<thead>
<tr>
<th></th>
@foreach ($scopeColumns as $label)
<th style="text-align:center">{{ $label }}</th>
@endforeach
<th style="text-align:center">Informuj również przez e-mail</th>
</tr>
</thead>
<tbody>
@foreach ($categoryLabels as $category => $label)
<tr wire:key="pref-row-{{ $category }}">
<td style="white-space:nowrap">{{ $label }}</td>
@foreach ($scopeColumns as $field => $ignored)
<td style="text-align:center">
<input type="checkbox" @checked($rows[$category][$field]) wire:click="toggle('{{ $category }}', '{{ $field }}')">
</td>
@endforeach
<td style="text-align:center">
<input type="checkbox" @checked($rows[$category]['email']) wire:click="toggle('{{ $category }}', 'email')">
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<div class="card" style="padding:18px;gap:10px" x-data="{
supported: typeof Notification !== 'undefined',
permission: typeof Notification !== 'undefined' ? Notification.permission : 'unsupported',
enabled: localStorage.getItem('browserNotificationsEnabled') === '1',
async enable() {
if (!this.supported) return;
this.permission = await Notification.requestPermission();
this.enabled = this.permission === 'granted';
localStorage.setItem('browserNotificationsEnabled', this.enabled ? '1' : '0');
},
disable() {
this.enabled = false;
localStorage.setItem('browserNotificationsEnabled', '0');
},
}">
<h4 style="margin:0">Powiadomienia push w przeglądarce</h4>
<p class="text-muted" style="font-size:12px;margin:0">Gdy ta karta jest otwarta, nowe zdarzenia z dzwoneczka mogą dodatkowo pojawić się jako natywne powiadomienie przeglądarki.</p>
<template x-if="!supported">
<span class="text-muted" style="font-size:12px">Ta przeglądarka nie obsługuje powiadomień push.</span>
</template>
<template x-if="supported && permission === 'denied'">
<span style="font-size:12px;color:var(--color-danger)">Powiadomienia zostały zablokowane w ustawieniach przeglądarki.</span>
</template>
<template x-if="supported && permission !== 'denied' && !enabled">
<button type="button" class="btn btn-secondary" style="align-self:flex-start" x-on:click="enable">Włącz powiadomienia push</button>
</template>
<template x-if="supported && enabled">
<div style="display:flex;align-items:center;gap:10px">
<div style="display:flex;align-items:center;gap:6px;color:var(--color-success)">
<span class="material-symbols-outlined" style="font-size:18px">check_circle</span>Włączone
</div>
<button type="button" class="btn btn-ghost" x-on:click="disable">Wyłącz</button>
</div>
</template>
</div>
</div>
</div>