- 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>
82 lines
4.2 KiB
PHP
82 lines
4.2 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Operator\Stats;
|
|
use App\Models\Category;
|
|
use App\Models\User;
|
|
use Livewire\Livewire;
|
|
|
|
test('customerSubcategoryMatrix cross-tabs clients against their top subcategories', function () {
|
|
seedStatusesAndPriorities();
|
|
$admin = User::query()->create(['name' => 'Admin', 'email' => 'stats-admin-cust-matrix@example.com', 'roles' => ['admin']]);
|
|
$clientA = User::query()->create(['name' => 'Klient A', 'email' => 'stats-matrix-client-a@example.com', 'roles' => ['client']]);
|
|
$clientB = User::query()->create(['name' => 'Klient B', 'email' => 'stats-matrix-client-b@example.com', 'roles' => ['client']]);
|
|
|
|
$it = Category::query()->create(['name' => 'IT-Pomoc']);
|
|
$vpn = $it->subcategories()->create(['name' => 'VPN']);
|
|
$printers = $it->subcategories()->create(['name' => 'Drukarki']);
|
|
|
|
makeTicket(['number' => '9001', 'customer_id' => $clientA->id, 'subcategory_id' => $vpn->id]);
|
|
makeTicket(['number' => '9002', 'customer_id' => $clientA->id, 'subcategory_id' => $vpn->id]);
|
|
makeTicket(['number' => '9003', 'customer_id' => $clientA->id, 'subcategory_id' => $printers->id]);
|
|
makeTicket(['number' => '9004', 'customer_id' => $clientB->id, 'subcategory_id' => $printers->id]);
|
|
makeTicket(['number' => '9005', 'customer_id' => null, 'subcategory_id' => $vpn->id]);
|
|
|
|
$matrix = Livewire::actingAs($admin)->test(Stats::class)->instance()->customerSubcategoryMatrix;
|
|
|
|
expect($matrix['columns'])->toBe(['IT-Pomoc / VPN', 'IT-Pomoc / Drukarki'])
|
|
->and($matrix['hasOther'])->toBeFalse();
|
|
|
|
$rowsByLabel = collect($matrix['rows'])->keyBy('label');
|
|
|
|
expect($rowsByLabel['Klient A'])->toBe(['label' => 'Klient A', 'cells' => [2, 1], 'other' => null, 'total' => 3])
|
|
->and($rowsByLabel['Klient B'])->toBe(['label' => 'Klient B', 'cells' => [0, 1], 'other' => null, 'total' => 1]);
|
|
});
|
|
|
|
test('customerSubcategoryMatrix caps rows at top 10 clients and columns at top 5 subcategories, folding the rest into "Inne"', function () {
|
|
seedStatusesAndPriorities();
|
|
$admin = User::query()->create(['name' => 'Admin', 'email' => 'stats-admin-cust-matrix-2@example.com', 'roles' => ['admin']]);
|
|
$client = User::query()->create(['name' => 'Klient', 'email' => 'stats-matrix-client-c@example.com', 'roles' => ['client']]);
|
|
|
|
$category = Category::query()->create(['name' => 'Kategoria']);
|
|
$ticketNumber = 9100;
|
|
|
|
foreach (range(1, 7) as $i) {
|
|
$sub = $category->subcategories()->create(['name' => "Sub {$i}"]);
|
|
// Distinct volumes (7 down to 1) so which 5 make the cut is deterministic.
|
|
foreach (range(1, 8 - $i) as $n) {
|
|
makeTicket(['number' => (string) $ticketNumber++, 'customer_id' => $client->id, 'subcategory_id' => $sub->id]);
|
|
}
|
|
}
|
|
|
|
$matrix = Livewire::actingAs($admin)->test(Stats::class)->instance()->customerSubcategoryMatrix;
|
|
|
|
expect($matrix['columns'])->toBe(['Kategoria / Sub 1', 'Kategoria / Sub 2', 'Kategoria / Sub 3', 'Kategoria / Sub 4', 'Kategoria / Sub 5'])
|
|
->and($matrix['hasOther'])->toBeTrue();
|
|
|
|
$row = $matrix['rows'][0];
|
|
expect($row['cells'])->toBe([7, 6, 5, 4, 3])
|
|
->and($row['other'])->toBe(2 + 1)
|
|
->and($row['total'])->toBe(7 + 6 + 5 + 4 + 3 + 2 + 1);
|
|
});
|
|
|
|
test('customerSubcategoryMatrix caps rows at the top 10 clients by volume', function () {
|
|
seedStatusesAndPriorities();
|
|
$admin = User::query()->create(['name' => 'Admin', 'email' => 'stats-admin-cust-matrix-3@example.com', 'roles' => ['admin']]);
|
|
$category = Category::query()->create(['name' => 'Kategoria']);
|
|
$sub = $category->subcategories()->create(['name' => 'Sub']);
|
|
$ticketNumber = 9200;
|
|
|
|
foreach (range(1, 12) as $i) {
|
|
$client = User::query()->create(['name' => "Klient {$i}", 'email' => "stats-matrix-client-{$i}@example.com", 'roles' => ['client']]);
|
|
foreach (range(1, 13 - $i) as $n) {
|
|
makeTicket(['number' => (string) $ticketNumber++, 'customer_id' => $client->id, 'subcategory_id' => $sub->id]);
|
|
}
|
|
}
|
|
|
|
$matrix = Livewire::actingAs($admin)->test(Stats::class)->instance()->customerSubcategoryMatrix;
|
|
|
|
expect($matrix['rows'])->toHaveCount(10)
|
|
->and($matrix['rows'][0]['label'])->toBe('Klient 1')
|
|
->and($matrix['rows'][0]['total'])->toBe(12);
|
|
});
|