- 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>
343 lines
10 KiB
PHP
343 lines
10 KiB
PHP
<?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');
|
|
}
|
|
}
|