- 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:
2026-07-22 20:43:05 +02:00
parent def7c70887
commit 0b06687ea1
64 changed files with 3613 additions and 168 deletions

View File

@@ -0,0 +1,30 @@
<?php
use App\Livewire\Operator\Stats;
use App\Models\Category;
use App\Models\User;
use Livewire\Livewire;
test('bySubcategory groups tickets per subcategory, labeled "Category / Subcategory"', function () {
seedStatusesAndPriorities();
$admin = User::query()->create(['name' => 'Admin', 'email' => 'stats-admin-sub@example.com', 'roles' => ['admin']]);
$it = Category::query()->create(['name' => 'IT-Pomoc']);
$vpn = $it->subcategories()->create(['name' => 'VPN']);
$printers = $it->subcategories()->create(['name' => 'Drukarki']);
$orders = Category::query()->create(['name' => 'Zamówienia']);
$hardware = $orders->subcategories()->create(['name' => 'Sprzęt']);
makeTicket(['number' => '4001', 'subcategory_id' => $vpn->id]);
makeTicket(['number' => '4002', 'subcategory_id' => $vpn->id]);
makeTicket(['number' => '4003', 'subcategory_id' => $printers->id]);
makeTicket(['number' => '4004', 'subcategory_id' => $hardware->id]);
makeTicket(['number' => '4005', 'subcategory_id' => null]);
$rows = Livewire::actingAs($admin)->test(Stats::class)->instance()->bySubcategory;
expect($rows->firstWhere('label', 'IT-Pomoc / VPN'))->toBe(['label' => 'IT-Pomoc / VPN', 'count' => 2])
->and($rows->firstWhere('label', 'IT-Pomoc / Drukarki'))->toBe(['label' => 'IT-Pomoc / Drukarki', 'count' => 1])
->and($rows->firstWhere('label', 'Zamówienia / Sprzęt'))->toBe(['label' => 'Zamówienia / Sprzęt', 'count' => 1])
->and($rows->count())->toBe(3);
});