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:
342
src/app/Livewire/Admin/Triggers.php
Normal file
342
src/app/Livewire/Admin/Triggers.php
Normal file
@@ -0,0 +1,342 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Admin;
|
||||
|
||||
use App\Models\Priority;
|
||||
use App\Models\Status;
|
||||
use App\Models\Subcategory;
|
||||
use App\Models\Team;
|
||||
use App\Models\Trigger;
|
||||
use App\Models\TriggerEmailTemplate;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Component;
|
||||
|
||||
class Triggers extends Component
|
||||
{
|
||||
public bool $formOpen = false;
|
||||
|
||||
public ?int $editingId = null;
|
||||
|
||||
public array $form = [
|
||||
'name' => '',
|
||||
'enabled' => true,
|
||||
'event' => 'ticket_created',
|
||||
'conditions' => [],
|
||||
'actions' => [],
|
||||
];
|
||||
|
||||
public bool $templateFormOpen = false;
|
||||
|
||||
public ?int $editingTemplateId = null;
|
||||
|
||||
public array $templateForm = ['name' => '', 'subject' => '', 'body' => ''];
|
||||
|
||||
public static function eventLabels(): array
|
||||
{
|
||||
return [
|
||||
'ticket_created' => 'Zgłoszenie utworzone',
|
||||
'ticket_updated' => 'Zgłoszenie zaktualizowane (dowolne pole)',
|
||||
'status_changed' => 'Zmiana statusu',
|
||||
'priority_changed' => 'Zmiana priorytetu',
|
||||
'assignee_changed' => 'Zmiana przypisanego operatora',
|
||||
'team_changed' => 'Zmiana zespołu',
|
||||
'category_changed' => 'Zmiana kategorii',
|
||||
'comment_added' => 'Nowa wiadomość (publiczna)',
|
||||
];
|
||||
}
|
||||
|
||||
public static function fieldLabels(): array
|
||||
{
|
||||
return [
|
||||
'status_key' => 'Status',
|
||||
'priority_key' => 'Priorytet',
|
||||
'team_id' => 'Zespół',
|
||||
'subcategory_id' => 'Podkategoria',
|
||||
'assignee_id' => 'Operator przypisany',
|
||||
'customer_id' => 'Zgłaszający',
|
||||
'subject' => 'Temat',
|
||||
'body' => 'Treść',
|
||||
];
|
||||
}
|
||||
|
||||
public static function operatorLabels(): array
|
||||
{
|
||||
return [
|
||||
'equals' => 'jest równe',
|
||||
'not_equals' => 'jest różne od',
|
||||
'is_empty' => 'jest puste',
|
||||
'is_not_empty' => 'nie jest puste',
|
||||
'contains' => 'zawiera',
|
||||
];
|
||||
}
|
||||
|
||||
public static function actionTypeLabels(): array
|
||||
{
|
||||
return [
|
||||
'set_status' => 'Ustaw status',
|
||||
'set_priority' => 'Ustaw priorytet',
|
||||
'set_team' => 'Ustaw zespół',
|
||||
'set_assignee' => 'Ustaw operatora',
|
||||
'send_notification' => 'Wyślij powiadomienie e-mail',
|
||||
];
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function triggers(): Collection
|
||||
{
|
||||
return Trigger::query()->orderBy('sort_order')->get();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function statuses(): Collection
|
||||
{
|
||||
return Status::query()->orderBy('sort_order')->get();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function priorities(): Collection
|
||||
{
|
||||
return Priority::query()->orderBy('sort_order')->get();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function teams(): Collection
|
||||
{
|
||||
return Team::query()->orderBy('name')->get();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function operators(): Collection
|
||||
{
|
||||
return User::query()->whereHas('roleAssignments', fn ($q) => $q->whereIn('key', ['operator', 'admin']))->orderBy('name')->get();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function subcategories(): Collection
|
||||
{
|
||||
return Subcategory::query()->with('category')->get()
|
||||
->sortBy(fn (Subcategory $s) => $s->category->name.' / '.$s->name, SORT_NATURAL | SORT_FLAG_CASE)
|
||||
->values();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function customers(): Collection
|
||||
{
|
||||
return User::query()->whereHas('roleAssignments', fn ($q) => $q->where('key', 'client'))->orderBy('name')->get();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function emailTemplates(): Collection
|
||||
{
|
||||
return TriggerEmailTemplate::query()->orderBy('name')->get();
|
||||
}
|
||||
|
||||
public function openForm(): void
|
||||
{
|
||||
$this->editingId = null;
|
||||
$this->form = ['name' => '', 'enabled' => true, 'event' => 'ticket_created', 'conditions' => [], 'actions' => []];
|
||||
$this->resetErrorBag();
|
||||
$this->formOpen = true;
|
||||
}
|
||||
|
||||
public function editTrigger(int $id): void
|
||||
{
|
||||
$trigger = Trigger::query()->findOrFail($id);
|
||||
|
||||
$this->editingId = $trigger->id;
|
||||
$this->form = [
|
||||
'name' => $trigger->name,
|
||||
'enabled' => $trigger->enabled,
|
||||
'event' => $trigger->event,
|
||||
'conditions' => $trigger->conditions,
|
||||
'actions' => $trigger->actions,
|
||||
];
|
||||
$this->resetErrorBag();
|
||||
$this->formOpen = true;
|
||||
}
|
||||
|
||||
public function closeForm(): void
|
||||
{
|
||||
$this->formOpen = false;
|
||||
}
|
||||
|
||||
public function addCondition(): void
|
||||
{
|
||||
$this->form['conditions'][] = ['field' => Trigger::CONDITION_FIELDS[0], 'operator' => 'equals', 'value' => ''];
|
||||
}
|
||||
|
||||
public function removeCondition(int $index): void
|
||||
{
|
||||
unset($this->form['conditions'][$index]);
|
||||
$this->form['conditions'] = array_values($this->form['conditions']);
|
||||
}
|
||||
|
||||
public function addAction(): void
|
||||
{
|
||||
$this->form['actions'][] = ['type' => Trigger::ACTION_TYPES[0], 'value' => '', 'recipient' => 'client', 'email_template_id' => ''];
|
||||
}
|
||||
|
||||
public function removeAction(int $index): void
|
||||
{
|
||||
unset($this->form['actions'][$index]);
|
||||
$this->form['actions'] = array_values($this->form['actions']);
|
||||
}
|
||||
|
||||
public function moveActionUp(int $index): void
|
||||
{
|
||||
$this->swapFormActions($index, $index - 1);
|
||||
}
|
||||
|
||||
public function moveActionDown(int $index): void
|
||||
{
|
||||
$this->swapFormActions($index, $index + 1);
|
||||
}
|
||||
|
||||
protected function swapFormActions(int $a, int $b): void
|
||||
{
|
||||
if ($b < 0 || $b >= count($this->form['actions'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
[$this->form['actions'][$a], $this->form['actions'][$b]] = [$this->form['actions'][$b], $this->form['actions'][$a]];
|
||||
}
|
||||
|
||||
public function submit(): void
|
||||
{
|
||||
$this->validate([
|
||||
'form.name' => ['required', 'string', 'max:255'],
|
||||
'form.event' => ['required', 'string', 'in:'.implode(',', Trigger::EVENTS)],
|
||||
'form.conditions' => ['array'],
|
||||
'form.conditions.*.field' => ['required', 'string', 'in:'.implode(',', Trigger::CONDITION_FIELDS)],
|
||||
'form.conditions.*.operator' => ['required', 'string', 'in:'.implode(',', Trigger::CONDITION_OPERATORS)],
|
||||
'form.actions' => ['required', 'array', 'min:1'],
|
||||
'form.actions.*.type' => ['required', 'string', 'in:'.implode(',', Trigger::ACTION_TYPES)],
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'name' => $this->form['name'],
|
||||
'enabled' => (bool) $this->form['enabled'],
|
||||
'event' => $this->form['event'],
|
||||
'conditions' => array_values($this->form['conditions']),
|
||||
'actions' => array_values($this->form['actions']),
|
||||
];
|
||||
|
||||
if ($this->editingId) {
|
||||
Trigger::query()->findOrFail($this->editingId)->update($data);
|
||||
} else {
|
||||
$data['sort_order'] = (Trigger::query()->max('sort_order') ?? 0) + 1;
|
||||
Trigger::query()->create($data);
|
||||
}
|
||||
|
||||
$this->formOpen = false;
|
||||
unset($this->triggers);
|
||||
}
|
||||
|
||||
public function toggleEnabled(int $id): void
|
||||
{
|
||||
$trigger = Trigger::query()->findOrFail($id);
|
||||
$trigger->update(['enabled' => ! $trigger->enabled]);
|
||||
unset($this->triggers);
|
||||
}
|
||||
|
||||
public function removeTrigger(int $id): void
|
||||
{
|
||||
Trigger::query()->findOrFail($id)->delete();
|
||||
unset($this->triggers);
|
||||
}
|
||||
|
||||
public function moveUp(int $id): void
|
||||
{
|
||||
$this->swapAdjacentSortOrder($id, -1);
|
||||
}
|
||||
|
||||
public function moveDown(int $id): void
|
||||
{
|
||||
$this->swapAdjacentSortOrder($id, 1);
|
||||
}
|
||||
|
||||
protected function swapAdjacentSortOrder(int $id, int $direction): void
|
||||
{
|
||||
$ordered = $this->triggers;
|
||||
$index = $ordered->search(fn ($row) => $row->id === $id);
|
||||
$swapIndex = $index + $direction;
|
||||
|
||||
if ($index === false || $swapIndex < 0 || $swapIndex >= $ordered->count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$row = $ordered[$index];
|
||||
$neighbor = $ordered[$swapIndex];
|
||||
|
||||
[$rowOrder, $neighborOrder] = [$row->sort_order, $neighbor->sort_order];
|
||||
$row->update(['sort_order' => $neighborOrder]);
|
||||
$neighbor->update(['sort_order' => $rowOrder]);
|
||||
|
||||
unset($this->triggers);
|
||||
}
|
||||
|
||||
// ===================== TEMPLATES (wyzwalaczy) =====================
|
||||
|
||||
public function openTemplateForm(): void
|
||||
{
|
||||
$this->editingTemplateId = null;
|
||||
$this->templateForm = ['name' => '', 'subject' => '', 'body' => ''];
|
||||
$this->resetErrorBag();
|
||||
$this->templateFormOpen = true;
|
||||
}
|
||||
|
||||
public function editTemplate(int $id): void
|
||||
{
|
||||
$template = TriggerEmailTemplate::query()->findOrFail($id);
|
||||
|
||||
$this->editingTemplateId = $template->id;
|
||||
$this->templateForm = [
|
||||
'name' => $template->name,
|
||||
'subject' => $template->subject,
|
||||
'body' => $template->body,
|
||||
];
|
||||
$this->resetErrorBag();
|
||||
$this->templateFormOpen = true;
|
||||
}
|
||||
|
||||
public function closeTemplateForm(): void
|
||||
{
|
||||
$this->templateFormOpen = false;
|
||||
}
|
||||
|
||||
public function setTemplateBodyDraft(string $value): void
|
||||
{
|
||||
$this->templateForm['body'] = $value;
|
||||
}
|
||||
|
||||
public function submitTemplate(): void
|
||||
{
|
||||
$this->validate([
|
||||
'templateForm.name' => ['required', 'string', 'max:255'],
|
||||
'templateForm.subject' => ['required', 'string', 'max:255'],
|
||||
'templateForm.body' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
if ($this->editingTemplateId) {
|
||||
TriggerEmailTemplate::query()->findOrFail($this->editingTemplateId)->update($this->templateForm);
|
||||
} else {
|
||||
TriggerEmailTemplate::query()->create($this->templateForm);
|
||||
}
|
||||
|
||||
$this->templateFormOpen = false;
|
||||
unset($this->emailTemplates);
|
||||
}
|
||||
|
||||
public function removeTemplate(int $id): void
|
||||
{
|
||||
TriggerEmailTemplate::query()->findOrFail($id)->delete();
|
||||
unset($this->emailTemplates);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.admin.triggers');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user