Files
servicedesk/src/app/Livewire/Client/NewTicket.php
Kacper 0b06687ea1 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>
2026-07-22 20:43:05 +02:00

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');
}
}