v1.1.3
- 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:
@@ -55,6 +55,34 @@ if (window.currentUserId) {
|
||||
.error((error) => console.error('operator.queue subscription error', error));
|
||||
}
|
||||
|
||||
/**
|
||||
* Every logged-in user's own private notification stream — refreshes the
|
||||
* bell instantly (see NotificationBell::onBellNotification()) and, when the
|
||||
* viewer has opted in via the toggle on the notification-preferences page,
|
||||
* also raises an in-tab browser Notification. Deliberately lightweight: no
|
||||
* service worker, no push subscription — this only fires while the tab
|
||||
* calling it is open, same limitation as the operator.queue block above.
|
||||
*/
|
||||
if (window.currentUserId) {
|
||||
window.Echo.private('App.Models.User.' + window.currentUserId)
|
||||
.listen('.NotificationCreated', (e) => {
|
||||
Livewire.dispatch('bell-notification-received', { notificationId: e.notificationId });
|
||||
|
||||
if (
|
||||
localStorage.getItem('browserNotificationsEnabled') === '1'
|
||||
&& typeof Notification !== 'undefined'
|
||||
&& Notification.permission === 'granted'
|
||||
) {
|
||||
const popup = new Notification(e.message, { tag: e.notificationId });
|
||||
popup.onclick = () => {
|
||||
window.focus();
|
||||
window.location.href = e.url;
|
||||
};
|
||||
}
|
||||
})
|
||||
.error((error) => console.error('user notification channel subscription error', error));
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to a single ticket's channel — called by the Blade view of
|
||||
* whichever TicketShow component (operator or client) is currently mounted,
|
||||
|
||||
@@ -42,6 +42,18 @@
|
||||
<div style="border-top:1px solid var(--color-divider)"></div>
|
||||
@endif
|
||||
|
||||
@if ($user && ($user->isOperator() || $user->isAdmin()))
|
||||
<a
|
||||
href="{{ route('settings.notifications') }}"
|
||||
wire:navigate
|
||||
@click="open = false"
|
||||
class="theme-toggle-option"
|
||||
style="text-decoration:none;color:{{ request()->routeIs('settings.*') ? 'var(--color-accent)' : 'var(--color-text)' }};font-size:12.5px"
|
||||
>Powiadomienia</a>
|
||||
|
||||
<div style="border-top:1px solid var(--color-divider)"></div>
|
||||
@endif
|
||||
|
||||
<a
|
||||
href="{{ route('logout') }}"
|
||||
onclick="event.preventDefault(); document.getElementById('profile-menu-logout-form').submit();"
|
||||
|
||||
@@ -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>
|
||||
|
||||
303
src/resources/views/livewire/admin/triggers.blade.php
Normal file
303
src/resources/views/livewire/admin/triggers.blade.php
Normal 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 są 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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">← Wróć do listy</a>
|
||||
<a href="{{ route('operator.queue') }}" wire:navigate class="btn btn-ghost" style="padding:0;margin-right:auto">← 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
|
||||
|
||||
@@ -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">← 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>
|
||||
Reference in New Issue
Block a user