From d1d2aedbae0621b561808104d2c5bbe299837cc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Gro=C3=9F?= Date: Fri, 17 Oct 2025 00:55:41 +0200 Subject: [PATCH] Show clients that are assigned to the employee, closes #893 --- .../Controllers/Api/V1/ClientController.php | 13 +++++++ app/Providers/JetstreamServiceProvider.php | 6 +++- .../Endpoint/Api/V1/ClientEndpointTest.php | 36 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/V1/ClientController.php b/app/Http/Controllers/Api/V1/ClientController.php index b992ca6e..2fbe0363 100644 --- a/app/Http/Controllers/Api/V1/ClientController.php +++ b/app/Http/Controllers/Api/V1/ClientController.php @@ -12,6 +12,7 @@ use App\Http\Resources\V1\Client\ClientCollection; use App\Http\Resources\V1\Client\ClientResource; use App\Models\Client; use App\Models\Organization; +use App\Models\Project; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\JsonResponse; use Illuminate\Support\Carbon; @@ -38,11 +39,23 @@ class ClientController extends Controller public function index(Organization $organization, ClientIndexRequest $request): ClientCollection { $this->checkPermission($organization, 'clients:view'); + $canViewAllClients = $this->hasPermission($organization, 'clients:view:all'); + $user = $this->user(); $clientsQuery = Client::query() ->whereBelongsTo($organization, 'organization') ->orderBy('created_at', 'desc'); + if (! $canViewAllClients) { + $projectsQuery = Project::query() + ->whereBelongsTo($organization, 'organization') + ->visibleByEmployee($user) + ->distinct() + ->select('client_id'); + + $clientsQuery->whereIn('id', $projectsQuery); + } + $filterArchived = $request->getFilterArchived(); if ($filterArchived === 'true') { $clientsQuery->whereNotNull('archived_at'); diff --git a/app/Providers/JetstreamServiceProvider.php b/app/Providers/JetstreamServiceProvider.php index bca7bbbd..610c2fd3 100644 --- a/app/Providers/JetstreamServiceProvider.php +++ b/app/Providers/JetstreamServiceProvider.php @@ -109,6 +109,7 @@ class JetstreamServiceProvider extends ServiceProvider 'tags:update', 'tags:delete', 'clients:view', + 'clients:view:all', 'clients:create', 'clients:update', 'clients:delete', @@ -172,6 +173,7 @@ class JetstreamServiceProvider extends ServiceProvider 'tags:update', 'tags:delete', 'clients:view', + 'clients:view:all', 'clients:create', 'clients:update', 'clients:delete', @@ -232,6 +234,7 @@ class JetstreamServiceProvider extends ServiceProvider 'tags:update', 'tags:delete', 'clients:view', + 'clients:view:all', 'clients:create', 'clients:update', 'clients:delete', @@ -256,12 +259,13 @@ class JetstreamServiceProvider extends ServiceProvider 'projects:view', 'tags:view', 'tasks:view', + 'clients:view', 'time-entries:view:own', 'time-entries:create:own', 'time-entries:update:own', 'time-entries:delete:own', 'organizations:view', - ])->description('Employees have the ability to read, create, and update their own time entries and they can see the projects that they are members of.'); + ])->description('Employees have the ability to read, create, and update their own time entries, they can see the projects that they are members of and the clients they are assigned to.'); Jetstream::role(Role::Placeholder->value, 'Placeholder', [ ])->description('Placeholders are used for importing data. They cannot log in and have no permissions.'); diff --git a/tests/Unit/Endpoint/Api/V1/ClientEndpointTest.php b/tests/Unit/Endpoint/Api/V1/ClientEndpointTest.php index faf2af53..38100969 100644 --- a/tests/Unit/Endpoint/Api/V1/ClientEndpointTest.php +++ b/tests/Unit/Endpoint/Api/V1/ClientEndpointTest.php @@ -34,6 +34,7 @@ class ClientEndpointTest extends ApiEndpointTestAbstract // Arrange $data = $this->createUserWithPermission([ 'clients:view', + 'clients:view:all', ]); $clients = Client::factory()->forOrganization($data->organization)->randomCreatedAt()->createMany(4); Passport::actingAs($data->user); @@ -57,11 +58,43 @@ class ClientEndpointTest extends ApiEndpointTestAbstract ); } + public function test_index_endpoint_returns_list_of_clients_assigned_to_employee_user(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'clients:view' + ]); + + $clients = Client::factory()->forOrganization($data->organization)->createMany(2); + $projectWithMembership1 = Project::factory()->forOrganization($data->organization)->forClient($clients->get(0))->addMember($data->member)->isPrivate()->create(); + $projectWithMembership2 = Project::factory()->forOrganization($data->organization)->forClient($clients->get(1))->addMember($data->member)->isPrivate()->create(); + + $otherClients = Client::factory()->forOrganization($data->organization)->createMany(2); + $projectWithoutMembership = Project::factory()->forOrganization($data->organization)->forClient($otherClients->get(0))->isPrivate()->create(); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.clients.index', [$data->organization->getKey()])); + + // Assert + $response->assertStatus(200); + $response->assertJsonCount(2, 'data'); + $response->assertJson(fn (AssertableJson $json) => $json + ->has('data') + ->has('links') + ->has('meta') + ->count('data', 2) + ->where('data.0.id', $clients->get(0)->getKey()) + ->where('data.1.id', $clients->get(1)->getKey()) + ); + } + public function test_index_endpoint_without_filter_archived_returns_only_non_archived_clients(): void { // Arrange $data = $this->createUserWithPermission([ 'clients:view', + 'clients:view:all', ]); $archivedClients = Client::factory()->forOrganization($data->organization)->archived()->createMany(2); $nonArchivedClients = Client::factory()->forOrganization($data->organization)->createMany(2); @@ -81,6 +114,7 @@ class ClientEndpointTest extends ApiEndpointTestAbstract // Arrange $data = $this->createUserWithPermission([ 'clients:view', + 'clients:view:all', ]); $archivedClients = Client::factory()->forOrganization($data->organization)->archived()->createMany(2); $nonArchivedClients = Client::factory()->forOrganization($data->organization)->createMany(2); @@ -103,6 +137,7 @@ class ClientEndpointTest extends ApiEndpointTestAbstract // Arrange $data = $this->createUserWithPermission([ 'clients:view', + 'clients:view:all', ]); $archivedClients = Client::factory()->forOrganization($data->organization)->archived()->createMany(2); $nonArchivedClients = Client::factory()->forOrganization($data->organization)->createMany(2); @@ -125,6 +160,7 @@ class ClientEndpointTest extends ApiEndpointTestAbstract // Arrange $data = $this->createUserWithPermission([ 'clients:view', + 'clients:view:all', ]); $archivedClients = Client::factory()->forOrganization($data->organization)->archived()->createMany(2); $nonArchivedClients = Client::factory()->forOrganization($data->organization)->createMany(2);