- 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>
150 lines
3.7 KiB
PHP
150 lines
3.7 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Client;
|
|
|
|
use App\Models\Category;
|
|
use App\Models\Subcategory;
|
|
use App\Services\BookStackClient;
|
|
use App\Services\TicketService;
|
|
use App\Support\Settings;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Livewire\Attributes\Computed;
|
|
use Livewire\Component;
|
|
use Livewire\WithFileUploads;
|
|
|
|
class NewTicket extends Component
|
|
{
|
|
use WithFileUploads;
|
|
|
|
public int $step = 1;
|
|
|
|
public ?int $categoryId = null;
|
|
|
|
public ?int $subcategoryId = null;
|
|
|
|
public string $subject = '';
|
|
|
|
public string $body = '';
|
|
|
|
public array $customValues = [];
|
|
|
|
public array $attachments = [];
|
|
|
|
// Set via wire:init (see the blade view) rather than on the initial
|
|
// render, so the BookStack HTTP call in suggestedArticles() never
|
|
// delays the page's first paint — it loads in a beat later instead.
|
|
public bool $suggestedArticlesLoaded = false;
|
|
|
|
public function loadSuggestedArticles(): void
|
|
{
|
|
$this->suggestedArticlesLoaded = true;
|
|
}
|
|
|
|
#[Computed]
|
|
public function categories()
|
|
{
|
|
return Category::query()->with('subcategories')->get();
|
|
}
|
|
|
|
#[Computed]
|
|
public function selectedCategory(): ?Category
|
|
{
|
|
return $this->categoryId ? Category::query()->with('subcategories')->find($this->categoryId) : null;
|
|
}
|
|
|
|
#[Computed]
|
|
public function selectedSubcategory(): ?Subcategory
|
|
{
|
|
return $this->subcategoryId ? Subcategory::query()->with('customFields')->find($this->subcategoryId) : null;
|
|
}
|
|
|
|
public function selectCategory(int $id): void
|
|
{
|
|
$this->categoryId = $id;
|
|
$this->subcategoryId = null;
|
|
$this->step = 2;
|
|
}
|
|
|
|
public function selectSubcategory(int $id): void
|
|
{
|
|
$this->subcategoryId = $id;
|
|
$this->step = 3;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array{name: string, url: ?string}>
|
|
*/
|
|
#[Computed]
|
|
public function suggestedArticles(): array
|
|
{
|
|
if (! $this->suggestedArticlesLoaded) {
|
|
return [];
|
|
}
|
|
|
|
$query = trim(($this->selectedCategory?->name ?? '').' '.($this->selectedSubcategory?->name ?? ''));
|
|
|
|
return app(BookStackClient::class)->search($query);
|
|
}
|
|
|
|
public function backToCategory(): void
|
|
{
|
|
$this->step = 1;
|
|
}
|
|
|
|
public function backToSubcategory(): void
|
|
{
|
|
$this->step = 2;
|
|
}
|
|
|
|
public function updatedAttachments(): void
|
|
{
|
|
if (! $this->attachments) {
|
|
return;
|
|
}
|
|
|
|
if ($error = Settings::validateAttachments($this->attachments)) {
|
|
$this->attachments = [];
|
|
$this->addError('attachments', $error);
|
|
}
|
|
}
|
|
|
|
public function removeAttachment(int $index): void
|
|
{
|
|
unset($this->attachments[$index]);
|
|
$this->attachments = array_values($this->attachments);
|
|
}
|
|
|
|
public function submit(): void
|
|
{
|
|
$rules = [
|
|
'subject' => 'required|string|max:255',
|
|
'body' => 'required|string',
|
|
];
|
|
|
|
foreach ($this->selectedSubcategory?->customFields ?? [] as $field) {
|
|
$rules["customValues.{$field->id}"] = $field->required ? 'required' : 'nullable';
|
|
}
|
|
|
|
$this->validate($rules);
|
|
|
|
$user = Auth::user();
|
|
|
|
$ticket = app(TicketService::class)->create([
|
|
'email' => $user->email,
|
|
'subcategory_id' => $this->subcategoryId,
|
|
'subject' => $this->subject,
|
|
'body' => $this->body,
|
|
'custom_values' => $this->customValues,
|
|
], $user);
|
|
|
|
app(TicketService::class)->attachFiles($ticket, $ticket->messages()->first(), $this->attachments);
|
|
|
|
$this->redirect(route('client.ticket', $ticket), navigate: true);
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
return view('livewire.client.new-ticket');
|
|
}
|
|
}
|