- 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:
2026-07-22 20:43:05 +02:00
parent def7c70887
commit 0b06687ea1
64 changed files with 3613 additions and 168 deletions

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Admin;
use App\Models\AutomationRule;
use App\Models\Category;
use App\Models\CustomField;
use App\Models\EmailTemplate;
@@ -100,6 +101,15 @@ class Panel extends Component
public array $responseTemplateForm = ['id' => null, 'label' => '', 'body' => ''];
// ---- automation rules ----
public bool $automationRuleFormOpen = false;
public array $automationRuleForm = [
'id' => null, 'label' => '', 'enabled' => true, 'condition_minutes' => 60,
'scope_priority_key' => '', 'scope_subcategory_id' => '', 'scope_team_id' => '',
'action_type' => 'change_priority', 'action_value' => '',
];
// ---- templates ----
public ?int $editingTemplateId = null;
@@ -854,6 +864,96 @@ class Panel extends Component
$this->requestDelete('response-template', $id, 'Szablon odpowiedzi zostanie usunięty z listy dostępnej operatorom.');
}
// ===================== AUTOMATION RULES =====================
#[Computed]
public function automationRules()
{
return AutomationRule::query()->orderBy('id')->get();
}
public function openAutomationRuleForm(): void
{
$this->automationRuleForm = [
'id' => null, 'label' => '', 'enabled' => true, 'condition_minutes' => 60,
'scope_priority_key' => '', 'scope_subcategory_id' => '', 'scope_team_id' => '',
'action_type' => 'change_priority', 'action_value' => '',
];
$this->automationRuleFormOpen = true;
}
public function editAutomationRule(int $id): void
{
$rule = AutomationRule::query()->findOrFail($id);
$this->automationRuleForm = [
'id' => $rule->id,
'label' => $rule->label,
'enabled' => $rule->enabled,
'condition_minutes' => $rule->condition_minutes,
'scope_priority_key' => $rule->scope_priority_key ?? '',
'scope_subcategory_id' => $rule->scope_subcategory_id ?? '',
'scope_team_id' => $rule->scope_team_id ?? '',
'action_type' => $rule->action_type,
'action_value' => $rule->action_value,
];
$this->automationRuleFormOpen = true;
}
public function closeAutomationRuleForm(): void
{
$this->automationRuleFormOpen = false;
}
// Switching action_type invalidates whichever action_value was picked for
// the previous type (e.g. a priority key isn't a valid team id).
public function updatedAutomationRuleFormActionType(): void
{
$this->automationRuleForm['action_value'] = '';
}
public function submitAutomationRule(): void
{
$this->validate([
'automationRuleForm.label' => 'required|string|max:255',
'automationRuleForm.condition_minutes' => 'required|integer|min:1',
'automationRuleForm.action_type' => 'required|in:'.implode(',', AutomationRule::ACTION_TYPES),
'automationRuleForm.action_value' => 'required|string',
]);
$data = [
'label' => $this->automationRuleForm['label'],
'enabled' => (bool) $this->automationRuleForm['enabled'],
'condition_minutes' => (int) $this->automationRuleForm['condition_minutes'],
'scope_priority_key' => $this->automationRuleForm['scope_priority_key'] ?: null,
'scope_subcategory_id' => $this->automationRuleForm['scope_subcategory_id'] ?: null,
'scope_team_id' => $this->automationRuleForm['scope_team_id'] ?: null,
'action_type' => $this->automationRuleForm['action_type'],
'action_value' => $this->automationRuleForm['action_value'],
];
if ($this->automationRuleForm['id']) {
AutomationRule::query()->find($this->automationRuleForm['id'])?->update($data);
} else {
AutomationRule::query()->create($data);
}
$this->automationRuleFormOpen = false;
unset($this->automationRules);
}
public function toggleAutomationRuleEnabled(int $id): void
{
$rule = AutomationRule::query()->find($id);
$rule?->update(['enabled' => ! $rule->enabled]);
unset($this->automationRules);
}
public function removeAutomationRule(int $id): void
{
$this->requestDelete('automation-rule', $id, 'Reguła automatyzacji zostanie usunięta.');
}
// ===================== STATUSES / PRIORITIES =====================
#[Computed]
@@ -1491,10 +1591,11 @@ class Panel extends Component
'user-field' => UserField::query()->find($this->pendingDeleteId)?->delete(),
'reply-quick-action' => ReplyQuickAction::query()->find($this->pendingDeleteId)?->delete(),
'response-template' => ResponseTemplate::query()->find($this->pendingDeleteId)?->delete(),
'automation-rule' => AutomationRule::query()->find($this->pendingDeleteId)?->delete(),
default => null,
};
unset($this->categories, $this->customFields, $this->statuses, $this->priorities, $this->teams, $this->users, $this->userFields, $this->replyQuickActions, $this->responseTemplates, $this->notificationSettings);
unset($this->categories, $this->customFields, $this->statuses, $this->priorities, $this->teams, $this->users, $this->userFields, $this->replyQuickActions, $this->responseTemplates, $this->notificationSettings, $this->automationRules);
$this->cancelPendingDelete();
}