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:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user