feat: implement Story 2.3 — Worker-Scoped Dashboard

Scope stat cards and urgent declarations table to the authenticated
worker's own assignments. Add empty state when no declarations are
assigned, hide the "Assigné à" column for worker role, and expose
isWorker flag through DashboardController and dashboard types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 17:31:23 +01:00
parent 4807376c49
commit 3baf456640
7 changed files with 818 additions and 161 deletions

View File

@@ -0,0 +1,316 @@
<?php
use App\Enums\DeclarationStatus;
use App\Models\Client;
use App\Models\Declaration;
use App\Models\User;
use App\Models\Workspace;
use Illuminate\Support\Facades\Cache;
function setupWorkerWorkspace(): array
{
$worker = User::factory()->create();
$workspace = Workspace::factory()->create();
$workspace->users()->attach($worker->id, ['role' => 'worker']);
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
session(['current_workspace_id' => $workspace->id]);
return [$worker, $workspace, $client];
}
test('worker sees only their assigned declarations in kpi counts', function () {
[$worker, $workspace, $client] = setupWorkerWorkspace();
$otherUser = User::factory()->create();
$workspace->users()->attach($otherUser->id, ['role' => 'worker']);
// Assigned to this worker — overdue
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $worker->id,
'status' => DeclarationStatus::EnCours,
'due_date' => now()->subDays(3),
]);
// Assigned to this worker — due this week
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $worker->id,
'status' => DeclarationStatus::EnCours,
'due_date' => now()->addDays(2),
]);
// Assigned to this worker — en attente client
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $worker->id,
'status' => DeclarationStatus::EnAttenteClient,
'due_date' => now()->addDays(10),
]);
// Assigned to OTHER worker (should NOT count)
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $otherUser->id,
'status' => DeclarationStatus::EnCours,
'due_date' => now()->subDays(1),
]);
$this->actingAs($worker);
$response = $this->get(route('dashboard'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Dashboard')
->where('stats.overdue', 1)
->where('stats.dueThisWeek', 1)
->where('stats.enAttenteClient', 1)
->where('stats.enCours', 2)
);
});
test('worker does not see declarations assigned to other team members in kpi counts', function () {
[$worker, $workspace, $client] = setupWorkerWorkspace();
$otherWorker = User::factory()->create();
$workspace->users()->attach($otherWorker->id, ['role' => 'worker']);
// Only assigned to other worker
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $otherWorker->id,
'status' => DeclarationStatus::EnCours,
'due_date' => now()->subDays(5),
]);
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $otherWorker->id,
'status' => DeclarationStatus::EnAttenteClient,
'due_date' => now()->addDays(2),
]);
$this->actingAs($worker);
$response = $this->get(route('dashboard'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Dashboard')
->where('stats.overdue', 0)
->where('stats.dueThisWeek', 0)
->where('stats.enAttenteClient', 0)
->where('stats.enCours', 0)
);
});
test('worker sees only their assigned declarations in the urgent declarations table', function () {
[$worker, $workspace, $client] = setupWorkerWorkspace();
$otherWorker = User::factory()->create();
$workspace->users()->attach($otherWorker->id, ['role' => 'worker']);
// Assigned to this worker
$myDeclaration = Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $worker->id,
'status' => DeclarationStatus::EnCours,
'due_date' => now()->addDays(2),
]);
// Assigned to other worker
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $otherWorker->id,
'status' => DeclarationStatus::EnCours,
'due_date' => now()->addDays(1),
]);
$this->actingAs($worker);
$response = $this->get(route('dashboard'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Dashboard')
->has('declarations', 1)
->where('declarations.0.id', $myDeclaration->id)
);
});
test('worker sees only their assigned declarations in priority alerts', function () {
[$worker, $workspace, $client] = setupWorkerWorkspace();
$otherWorker = User::factory()->create();
$workspace->users()->attach($otherWorker->id, ['role' => 'worker']);
// Overdue declaration assigned to this worker (generates critical alert)
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $worker->id,
'status' => DeclarationStatus::EnCours,
'due_date' => now()->subDays(5),
]);
// Overdue declaration assigned to other worker (should NOT appear)
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $otherWorker->id,
'status' => DeclarationStatus::EnCours,
'due_date' => now()->subDays(3),
]);
$this->actingAs($worker);
$response = $this->get(route('dashboard'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Dashboard')
->has('alerts', 1)
->where('alerts.0.severity', 'critical')
);
});
test('worker dashboard returns isWorker true in inertia props', function () {
[$worker, $workspace, $client] = setupWorkerWorkspace();
$this->actingAs($worker);
$response = $this->get(route('dashboard'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Dashboard')
->where('isWorker', true)
);
});
test('worker with no assigned declarations gets zero counts and empty arrays', function () {
[$worker, $workspace, $client] = setupWorkerWorkspace();
// Declarations exist but assigned to someone else
$otherUser = User::factory()->create();
$workspace->users()->attach($otherUser->id, ['role' => 'worker']);
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $otherUser->id,
'status' => DeclarationStatus::EnCours,
'due_date' => now()->subDays(1),
]);
$this->actingAs($worker);
$response = $this->get(route('dashboard'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Dashboard')
->where('stats.overdue', 0)
->where('stats.dueThisWeek', 0)
->where('stats.enAttenteClient', 0)
->where('stats.enCours', 0)
->where('declarations', [])
->where('alerts', [])
->where('isWorker', true)
);
});
test('worker stat card hrefs include assignee scoping param', function () {
[$worker, $workspace, $client] = setupWorkerWorkspace();
$this->actingAs($worker);
$response = $this->get(route('dashboard'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Dashboard')
->has('statCards', 4)
->where('statCards.0.href', fn ($href) => str_contains($href, 'assignee='.$worker->id))
->where('statCards.1.href', fn ($href) => str_contains($href, 'assignee='.$worker->id))
->where('statCards.2.href', fn ($href) => str_contains($href, 'assignee='.$worker->id))
->where('statCards.3.href', fn ($href) => str_contains($href, 'assignee='.$worker->id))
);
});
test('owner and manager dashboard returns isWorker false', function () {
$ownerUser = User::factory()->create();
$managerUser = User::factory()->create();
$workspace = Workspace::factory()->create();
$workspace->users()->attach($ownerUser->id, ['role' => 'owner']);
$workspace->users()->attach($managerUser->id, ['role' => 'manager']);
session(['current_workspace_id' => $workspace->id]);
// Owner
$this->actingAs($ownerUser);
$response = $this->get(route('dashboard'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Dashboard')
->where('isWorker', false)
);
// Manager
$this->actingAs($managerUser);
$response = $this->get(route('dashboard'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Dashboard')
->where('isWorker', false)
);
});
test('cached data is scoped per user with worker cache key including user id', function () {
[$worker, $workspace, $client] = setupWorkerWorkspace();
$otherWorker = User::factory()->create();
$workspace->users()->attach($otherWorker->id, ['role' => 'worker']);
// Declaration assigned to first worker
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $worker->id,
'status' => DeclarationStatus::EnCours,
'due_date' => now()->subDays(1),
]);
// Declaration assigned to second worker
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $otherWorker->id,
'status' => DeclarationStatus::EnCours,
'due_date' => now()->subDays(2),
]);
// First worker request
$this->actingAs($worker);
$this->get(route('dashboard'))->assertOk();
$workerCacheKey = "dashboard:{$workspace->id}:{$worker->id}";
$otherCacheKey = "dashboard:{$workspace->id}:{$otherWorker->id}";
expect(Cache::has($workerCacheKey))->toBeTrue();
expect(Cache::has($otherCacheKey))->toBeFalse();
$workerCached = Cache::get($workerCacheKey);
expect($workerCached['overdue'])->toBe(1)
->and($workerCached['enCours'])->toBe(1);
// Second worker request
$this->actingAs($otherWorker);
$this->get(route('dashboard'))->assertOk();
expect(Cache::has($otherCacheKey))->toBeTrue();
$otherCached = Cache::get($otherCacheKey);
expect($otherCached['overdue'])->toBe(1)
->and($otherCached['enCours'])->toBe(1);
});