v1.1.2
- Real-time updates (Laravel Reverb): live operator queue, live ticket chat/detail updates for operator and client, periodic fallback refresh with a visible countdown as a backstop for dropped websocket connections. - SLA automation rules (Admin > Automatyzacja SLA): act on a ticket after N minutes of customer silence (change priority/status/team/assignee), evaluated every 15 minutes, reusing TicketService's own setters so automated changes get the same history/notification/broadcast a manual change would. - New notification: every operator on a matching team gets notified when a new ticket lands in one of their subcategories. - BookStack knowledge-base sidebar now also shown on the client's own ticket view (previously operator-only); suggestions everywhere now load in after first paint instead of blocking it. - Client ticket view: shows assigned operator + team; page widened to match the operator's. - Notification bell shows unread only; read notifications disappear instead of just dimming. - Stats dashboard: sectioned layout, new breakdowns (by subcategory, CSAT by team/operator, top clients, client x subcategory cross-tab). - Mobile: nav dropdowns (theme/notifications/profile) now expand full width instead of overflowing off-screen below 640px. - Fixed two bugs that silently disabled all real-time updates (missing CSRF header on Echo's private-channel auth; a script-load-order race that could miss the livewire:init event) and the mariadb healthcheck (world-writable credentials file on this stack's NFS mount). - Assorted test-suite fixes (roles virtual attribute needs the roles table seeded; a few missing seeds/wrong assertions found along the way). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ $tabGroups = [
|
||||
['key' => 'priorities', 'label' => 'Priorytety i SLA', 'icon' => 'priority_high'],
|
||||
['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'],
|
||||
],
|
||||
'Zespół' => [
|
||||
['key' => 'users', 'label' => 'Użytkownicy', 'icon' => 'group'],
|
||||
@@ -322,6 +323,39 @@ $tabGroups = [
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- ================= AUTOMATION RULES ================= --}}
|
||||
@if ($tab === 'automation-rules')
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
||||
<h3 style="margin:0">Automatyzacja SLA</h3>
|
||||
<button class="btn btn-primary" wire:click="openAutomationRuleForm">+ Dodaj regułę</button>
|
||||
</div>
|
||||
<p class="text-muted" style="font-size:12.5px;margin:0 0 14px">Co 15 minut sprawdzane jest, czy zgłoszenie milczy (brak odpowiedzi klienta) dłużej niż próg reguły — jeśli tak (i pasuje do opcjonalnego zawężenia), wykonywana jest wybrana akcja. Reguła nie powtarza się dla tego samego zgłoszenia, dopóki klient znów nie napisze albo zgłoszenie nie zostanie zamknięte i otwarte ponownie.</p>
|
||||
@if ($this->automationRules->isNotEmpty())
|
||||
<div style="display:flex;flex-direction:column;gap:10px">
|
||||
@foreach ($this->automationRules as $rule)
|
||||
<div class="card" style="padding:14px 16px;gap:6px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px">
|
||||
<div>
|
||||
<span class="card-title">{{ $rule->label }}</span>
|
||||
<span class="text-muted" style="font-size:12px;display:block">Brak odpowiedzi klienta ≥ {{ $rule->condition_minutes }} min. → {{ match($rule->action_type) { 'change_priority' => 'zmień priorytet', 'change_status' => 'zmień status', 'change_team' => 'zmień zespół', 'change_assignee' => 'zmień przypisanie', default => $rule->action_type } }}</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;flex:none;align-items:center">
|
||||
<label style="display:flex;align-items:center;gap:4px;font-size:12px">
|
||||
<input type="checkbox" wire:click="toggleAutomationRuleEnabled({{ $rule->id }})" @checked($rule->enabled)>
|
||||
Aktywna
|
||||
</label>
|
||||
<button class="btn btn-ghost" type="button" wire:click="editAutomationRule({{ $rule->id }})">Edytuj</button>
|
||||
<button class="btn btn-ghost" type="button" wire:click="removeAutomationRule({{ $rule->id }})">Usuń</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<p class="text-muted" style="font-size:13px">Brak reguł automatyzacji. Dodaj pierwszą używając przycisku wyżej.</p>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- ================= STATUSES ================= --}}
|
||||
@if ($tab === 'statuses')
|
||||
<h3 style="margin:0 0 6px">Statusy</h3>
|
||||
@@ -803,6 +837,96 @@ $tabGroups = [
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($automationRuleFormOpen)
|
||||
<div class="dialog-backdrop">
|
||||
<form wire:submit="submitAutomationRule" class="dialog" style="max-width:520px">
|
||||
<div class="dialog-title">{{ $automationRuleForm['id'] ? 'Edytuj regułę automatyzacji' : 'Nowa reguła automatyzacji' }}</div>
|
||||
<div class="field"><label>Nazwa reguły</label><input class="input" wire:model="automationRuleForm.label" placeholder="np. Eskalacja przy braku odpowiedzi"></div>
|
||||
@error('automationRuleForm.label') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
|
||||
<div class="field">
|
||||
<label>Brak odpowiedzi klienta przez (minuty)</label>
|
||||
<input class="input" type="number" min="1" wire:model="automationRuleForm.condition_minutes">
|
||||
</div>
|
||||
@error('automationRuleForm.condition_minutes') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
|
||||
|
||||
<div class="field">
|
||||
<label>Zawężenie (opcjonalne — puste = dowolne)</label>
|
||||
<select class="input" wire:model="automationRuleForm.scope_priority_key" style="margin-bottom:6px">
|
||||
<option value="">Dowolny priorytet</option>
|
||||
@foreach ($this->priorities as $p)
|
||||
<option value="{{ $p->key }}">{{ $p->label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<select class="input" wire:model="automationRuleForm.scope_subcategory_id" style="margin-bottom:6px">
|
||||
<option value="">Dowolna kategoria</option>
|
||||
@foreach ($this->subcategoriesForTeamForm as $s)
|
||||
<option value="{{ $s['id'] }}">{{ $s['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<select class="input" wire:model="automationRuleForm.scope_team_id">
|
||||
<option value="">Dowolny zespół</option>
|
||||
@foreach ($this->teams as $t)
|
||||
<option value="{{ $t->id }}">{{ $t->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Akcja</label>
|
||||
<select class="input" wire:model.live="automationRuleForm.action_type">
|
||||
<option value="change_priority">Zmień priorytet</option>
|
||||
<option value="change_status">Zmień status</option>
|
||||
<option value="change_team">Zmień zespół</option>
|
||||
<option value="change_assignee">Zmień przypisanie</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Nowa wartość</label>
|
||||
@switch ($automationRuleForm['action_type'])
|
||||
@case ('change_priority')
|
||||
<select class="input" wire:model="automationRuleForm.action_value">
|
||||
<option value="">— wybierz —</option>
|
||||
@foreach ($this->priorities as $p)
|
||||
<option value="{{ $p->key }}">{{ $p->label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@break
|
||||
@case ('change_status')
|
||||
<select class="input" wire:model="automationRuleForm.action_value">
|
||||
<option value="">— wybierz —</option>
|
||||
@foreach ($this->statuses as $s)
|
||||
<option value="{{ $s->key }}">{{ $s->label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@break
|
||||
@case ('change_team')
|
||||
<select class="input" wire:model="automationRuleForm.action_value">
|
||||
<option value="">— wybierz —</option>
|
||||
@foreach ($this->teams as $t)
|
||||
<option value="{{ $t->id }}">{{ $t->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@break
|
||||
@case ('change_assignee')
|
||||
<select class="input" wire:model="automationRuleForm.action_value">
|
||||
<option value="">— wybierz —</option>
|
||||
@foreach ($this->operatorsForTeamForm as $o)
|
||||
<option value="{{ $o['id'] }}">{{ $o['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@break
|
||||
@endswitch
|
||||
</div>
|
||||
@error('automationRuleForm.action_value') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button class="btn btn-secondary" type="button" wire:click="closeAutomationRuleForm">Anuluj</button>
|
||||
<button class="btn btn-primary" type="submit">{{ $automationRuleForm['id'] ? 'Zapisz' : 'Dodaj regułę' }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($this->editingTemplate)
|
||||
<div class="dialog-backdrop">
|
||||
<div class="dialog" style="max-width:560px">
|
||||
|
||||
Reference in New Issue
Block a user