feat: implement Story 2.4 — Dashboard Activity Feed with review fixes
Add role-scoped activity feed to the dashboard showing the 20 most recent workspace events. Owners/Managers see all activity (declarations, clients, team changes); Workers see only their assigned declarations. Includes French descriptions, relative timestamps, responsive layout (desktop sidebar, tablet inline, mobile collapsible), and 7 passing Pest tests. Review fixes applied: batch-load declarations/clients/users to eliminate N+1 queries, consistent soft-delete handling in URL resolution, French grammar singular/plural fix, missing icon map entry, and corrected tablet breakpoint per spec. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
211
tests/Feature/Dashboard/ActivityFeedTest.php
Normal file
211
tests/Feature/Dashboard/ActivityFeedTest.php
Normal file
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\DeclarationStatus;
|
||||
use App\Models\Client;
|
||||
use App\Models\Declaration;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
|
||||
function setupActivityWorkspace(string $role = 'owner'): array
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($user->id, ['role' => $role]);
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
return [$user, $workspace, $client];
|
||||
}
|
||||
|
||||
test('owner sees workspace-wide activity in activities prop', function () {
|
||||
[$owner, $workspace, $client] = setupActivityWorkspace('owner');
|
||||
|
||||
$otherUser = User::factory()->create();
|
||||
$workspace->users()->attach($otherUser->id, ['role' => 'worker']);
|
||||
|
||||
// Act as owner BEFORE creating declaration so Spatie logs the causer
|
||||
$this->actingAs($owner);
|
||||
|
||||
Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_to' => $otherUser->id,
|
||||
'status' => DeclarationStatus::EnCours,
|
||||
'due_date' => now()->addDays(10),
|
||||
]);
|
||||
|
||||
$response = $this->get(route('dashboard'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('Dashboard')
|
||||
->has('activities')
|
||||
->where('activities.0.actorName', $owner->name)
|
||||
);
|
||||
});
|
||||
|
||||
test('worker sees only activity related to their assigned declarations', function () {
|
||||
[$worker, $workspace, $client] = setupActivityWorkspace('worker');
|
||||
|
||||
$otherWorker = User::factory()->create();
|
||||
$workspace->users()->attach($otherWorker->id, ['role' => 'worker']);
|
||||
|
||||
// Declaration assigned to THIS worker — should appear in feed
|
||||
$assignedDeclaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_to' => $worker->id,
|
||||
'status' => DeclarationStatus::EnCours,
|
||||
'due_date' => now()->addDays(10),
|
||||
]);
|
||||
|
||||
// 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()->addDays(5),
|
||||
]);
|
||||
|
||||
$this->actingAs($worker);
|
||||
$response = $this->get(route('dashboard'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('Dashboard')
|
||||
->has('activities')
|
||||
);
|
||||
|
||||
// Verify the worker only sees activities for their own declarations
|
||||
$activitiesData = $response->original->getData()['page']['props']['activities'];
|
||||
expect(count($activitiesData))->toBeGreaterThan(0);
|
||||
|
||||
// All activities should relate to the worker's assigned client
|
||||
collect($activitiesData)->each(function ($a) use ($client) {
|
||||
expect($a['description'])->toContain($client->company_name);
|
||||
});
|
||||
});
|
||||
|
||||
test('activity entries contain expected fields', function () {
|
||||
[$owner, $workspace, $client] = setupActivityWorkspace('owner');
|
||||
|
||||
Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_to' => $owner->id,
|
||||
'status' => DeclarationStatus::EnCours,
|
||||
'due_date' => now()->addDays(10),
|
||||
]);
|
||||
|
||||
$this->actingAs($owner);
|
||||
$response = $this->get(route('dashboard'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('Dashboard')
|
||||
->has('activities.0', fn ($activity) => $activity
|
||||
->has('id')
|
||||
->has('actorName')
|
||||
->has('actorInitials')
|
||||
->has('description')
|
||||
->has('targetUrl')
|
||||
->has('targetLabel')
|
||||
->has('timestamp')
|
||||
->has('eventType')
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('feed is limited to 20 most recent events', function () {
|
||||
[$owner, $workspace, $client] = setupActivityWorkspace('owner');
|
||||
|
||||
// Create 25 declarations to generate 25 activity log entries
|
||||
for ($i = 0; $i < 25; $i++) {
|
||||
Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_to' => $owner->id,
|
||||
'status' => DeclarationStatus::EnCours,
|
||||
'due_date' => now()->addDays($i + 1),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->actingAs($owner);
|
||||
$response = $this->get(route('dashboard'));
|
||||
|
||||
$response->assertOk();
|
||||
$activitiesData = $response->original->getData()['page']['props']['activities'];
|
||||
expect(count($activitiesData))->toBeLessThanOrEqual(20);
|
||||
});
|
||||
|
||||
test('empty activity returns empty array', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($user->id, ['role' => 'owner']);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
// No declarations or activity created
|
||||
|
||||
$this->actingAs($user);
|
||||
$response = $this->get(route('dashboard'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('Dashboard')
|
||||
->where('activities', [])
|
||||
);
|
||||
});
|
||||
|
||||
test('activity with deleted subject does not cause errors', function () {
|
||||
[$owner, $workspace, $client] = setupActivityWorkspace('owner');
|
||||
|
||||
// Create a declaration (logs activity), then soft-delete it
|
||||
$declaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_to' => $owner->id,
|
||||
'status' => DeclarationStatus::EnCours,
|
||||
'due_date' => now()->addDays(10),
|
||||
]);
|
||||
|
||||
$declaration->delete();
|
||||
|
||||
$this->actingAs($owner);
|
||||
$response = $this->get(route('dashboard'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('Dashboard')
|
||||
->has('activities')
|
||||
);
|
||||
});
|
||||
|
||||
test('manager sees workspace-wide activity same as owner', function () {
|
||||
[$manager, $workspace, $client] = setupActivityWorkspace('manager');
|
||||
|
||||
$worker = User::factory()->create();
|
||||
$workspace->users()->attach($worker->id, ['role' => 'worker']);
|
||||
|
||||
// Create a declaration by the worker
|
||||
$this->actingAs($worker);
|
||||
Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_to' => $worker->id,
|
||||
'status' => DeclarationStatus::EnCours,
|
||||
'due_date' => now()->addDays(10),
|
||||
]);
|
||||
|
||||
$this->actingAs($manager);
|
||||
$response = $this->get(route('dashboard'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('Dashboard')
|
||||
->has('activities')
|
||||
);
|
||||
|
||||
$activitiesData = $response->original->getData()['page']['props']['activities'];
|
||||
expect(count($activitiesData))->toBeGreaterThan(0);
|
||||
});
|
||||
@@ -267,6 +267,23 @@ test('owner and manager dashboard returns isWorker false', function () {
|
||||
);
|
||||
});
|
||||
|
||||
test('worker without workspace session gets isWorker false in fallback', function () {
|
||||
$worker = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($worker->id, ['role' => 'worker']);
|
||||
|
||||
// No session(['current_workspace_id' => ...]) — simulate no active workspace
|
||||
$this->actingAs($worker);
|
||||
$response = $this->get(route('dashboard'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('Dashboard')
|
||||
->where('isWorker', false)
|
||||
->where('workspaceName', null)
|
||||
);
|
||||
});
|
||||
|
||||
test('cached data is scoped per user with worker cache key including user id', function () {
|
||||
[$worker, $workspace, $client] = setupWorkerWorkspace();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user