From 01f6f0f5ea558f8603f93f95675577a9627fcbf8 Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Fri, 14 Mar 2025 12:34:31 +0100 Subject: [PATCH] Add chart endpoints --- .../Controllers/Api/V1/ChartController.php | 136 ++++++++ app/Providers/JetstreamServiceProvider.php | 7 + routes/api.php | 14 + tests/TestCaseWithDatabase.php | 3 + .../Endpoint/Api/V1/ChartEndpointTest.php | 303 ++++++++++++++++++ 5 files changed, 463 insertions(+) create mode 100644 app/Http/Controllers/Api/V1/ChartController.php create mode 100644 tests/Unit/Endpoint/Api/V1/ChartEndpointTest.php diff --git a/app/Http/Controllers/Api/V1/ChartController.php b/app/Http/Controllers/Api/V1/ChartController.php new file mode 100644 index 00000000..caafdd2c --- /dev/null +++ b/app/Http/Controllers/Api/V1/ChartController.php @@ -0,0 +1,136 @@ +checkPermission($organization, 'charts:view:own'); + $user = $this->user(); + + $weeklyProjectOverview = $dashboardService->weeklyProjectOverview($user, $organization); + + return response()->json($weeklyProjectOverview); + } + + /** + * @throws AuthorizationException + */ + public function latestTasks(Organization $organization, DashboardService $dashboardService): JsonResponse + { + $this->checkPermission($organization, 'charts:view:own'); + $user = $this->user(); + + $latestTasks = $dashboardService->latestTasks($user, $organization); + + return response()->json($latestTasks); + } + + /** + * @throws AuthorizationException + */ + public function lastSevenDays(Organization $organization, DashboardService $dashboardService): JsonResponse + { + $this->checkPermission($organization, 'charts:view:own'); + $user = $this->user(); + + $lastSevenDays = $dashboardService->lastSevenDays($user, $organization); + + return response()->json($lastSevenDays); + } + + /** + * @throws AuthorizationException + */ + public function latestTeamActivity(Organization $organization, DashboardService $dashboardService, PermissionStore $permissionStore): JsonResponse + { + $this->checkPermission($organization, 'charts:view:all'); + + $latestTeamActivity = $dashboardService->latestTeamActivity($organization); + + return response()->json($latestTeamActivity); + } + + /** + * @throws AuthorizationException + */ + public function dailyTrackedHours(Organization $organization, DashboardService $dashboardService): JsonResponse + { + $this->checkPermission($organization, 'charts:view:own'); + $user = $this->user(); + + $dailyTrackedHours = $dashboardService->getDailyTrackedHours($user, $organization, 60); + + return response()->json($dailyTrackedHours); + } + + /** + * @throws AuthorizationException + */ + public function totalWeeklyTime(Organization $organization, DashboardService $dashboardService): JsonResponse + { + $this->checkPermission($organization, 'charts:view:own'); + $user = $this->user(); + + $totalWeeklyTime = $dashboardService->totalWeeklyTime($user, $organization); + + return response()->json($totalWeeklyTime); + } + + /** + * @throws AuthorizationException + */ + public function totalWeeklyBillableTime(Organization $organization, DashboardService $dashboardService): JsonResponse + { + $this->checkPermission($organization, 'charts:view:own'); + $user = $this->user(); + + $totalWeeklyBillableTime = $dashboardService->totalWeeklyBillableTime($user, $organization); + + return response()->json($totalWeeklyBillableTime); + } + + /** + * @throws AuthorizationException + */ + public function totalWeeklyBillableAmount(Organization $organization, DashboardService $dashboardService): JsonResponse + { + $this->checkPermission($organization, 'charts:view:own'); + $user = $this->user(); + + $showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates; + if (! $showBillableRate) { + throw new AuthorizationException('You do not have permission to view billable rates.'); + } + + $totalWeeklyBillableAmount = $dashboardService->totalWeeklyBillableAmount($user, $organization); + + return response()->json($totalWeeklyBillableAmount); + } + + /** + * @throws AuthorizationException + */ + public function weeklyHistory(Organization $organization, DashboardService $dashboardService): JsonResponse + { + $this->checkPermission($organization, 'charts:view:own'); + $user = $this->user(); + + $weeklyHistory = $dashboardService->getWeeklyHistory($user, $organization); + + return response()->json($weeklyHistory); + } +} diff --git a/app/Providers/JetstreamServiceProvider.php b/app/Providers/JetstreamServiceProvider.php index 2fe1f8f0..2460b224 100644 --- a/app/Providers/JetstreamServiceProvider.php +++ b/app/Providers/JetstreamServiceProvider.php @@ -80,6 +80,8 @@ class JetstreamServiceProvider extends ServiceProvider Jetstream::defaultApiTokenPermissions([]); Jetstream::role(Role::Owner->value, 'Owner', [ + 'charts:view:own', + 'charts:view:all', 'projects:view', 'projects:view:all', 'projects:create', @@ -134,6 +136,8 @@ class JetstreamServiceProvider extends ServiceProvider ])->description('Owner users can perform any action. There is only one owner per organization.'); Jetstream::role(Role::Admin->value, 'Administrator', [ + 'charts:view:own', + 'charts:view:all', 'projects:view', 'projects:view:all', 'projects:create', @@ -184,6 +188,8 @@ class JetstreamServiceProvider extends ServiceProvider ])->description('Administrator users can perform any action, except accessing the billing dashboard.'); Jetstream::role(Role::Manager->value, 'Manager', [ + 'charts:view:own', + 'charts:view:all', 'projects:view', 'projects:view:all', 'projects:create', @@ -224,6 +230,7 @@ class JetstreamServiceProvider extends ServiceProvider ])->description('Managers have full access to all projects, time entries, ect. but cannot manage the organization (add/remove member, edit the organization, ect.).'); Jetstream::role(Role::Employee->value, 'Employee', [ + 'charts:view:own', 'projects:view', 'tags:view', 'tasks:view', diff --git a/routes/api.php b/routes/api.php index 1c38f348..47bbe3f7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,6 +3,7 @@ declare(strict_types=1); use App\Http\Controllers\Api\V1\ApiTokenController; +use App\Http\Controllers\Api\V1\ChartController; use App\Http\Controllers\Api\V1\ClientController; use App\Http\Controllers\Api\V1\ExportController; use App\Http\Controllers\Api\V1\ImportController; @@ -123,6 +124,19 @@ Route::prefix('v1')->name('v1.')->group(static function (): void { Route::delete('/reports/{report}', [ReportController::class, 'destroy'])->name('destroy'); }); + // Chart routes + Route::name('charts.')->prefix('/organizations/{organization}/charts')->group(static function (): void { + Route::get('/weekly-project-overview', [ChartController::class, 'weeklyProjectOverview'])->name('weekly-project-overview'); + Route::get('/latest-tasks', [ChartController::class, 'latestTasks'])->name('latest-tasks'); + Route::get('/last-seven-days', [ChartController::class, 'lastSevenDays'])->name('last-seven-days'); + Route::get('/latest-team-activity', [ChartController::class, 'latestTeamActivity'])->name('latest-team-activity'); + Route::get('/daily-tracked-hours', [ChartController::class, 'dailyTrackedHours'])->name('daily-tracked-hours'); + Route::get('/total-weekly-time', [ChartController::class, 'totalWeeklyTime'])->name('total-weekly-time'); + Route::get('/total-weekly-billable-time', [ChartController::class, 'totalWeeklyBillableTime'])->name('total-weekly-billable-time'); + Route::get('/total-weekly-billable-amount', [ChartController::class, 'totalWeeklyBillableAmount'])->name('total-weekly-billable-amount'); + Route::get('/weekly-history', [ChartController::class, 'weeklyHistory'])->name('weekly-history'); + }); + // Tag routes Route::name('tags.')->prefix('/organizations/{organization}')->group(static function (): void { Route::get('/tags', [TagController::class, 'index'])->name('index'); diff --git a/tests/TestCaseWithDatabase.php b/tests/TestCaseWithDatabase.php index 2db4dd5b..e252ad2f 100644 --- a/tests/TestCaseWithDatabase.php +++ b/tests/TestCaseWithDatabase.php @@ -53,6 +53,9 @@ abstract class TestCaseWithDatabase extends TestCase ]; } + /** + * @return object{user: User, organization: Organization, member: Member, owner: User, ownerMember: Member} + */ public function createUserWithRole(Role $role): object { $owner = User::factory()->create(); diff --git a/tests/Unit/Endpoint/Api/V1/ChartEndpointTest.php b/tests/Unit/Endpoint/Api/V1/ChartEndpointTest.php new file mode 100644 index 00000000..4face21f --- /dev/null +++ b/tests/Unit/Endpoint/Api/V1/ChartEndpointTest.php @@ -0,0 +1,303 @@ +createUserWithPermission(); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.weekly-project-overview', [ + 'organization' => $user->organization, + ])); + + // Assert + $response->assertStatus(403); + } + + public function test_weekly_project_overview_endpoint_returns_chart_data(): void + { + // Arrange + $user = $this->createUserWithPermission(['charts:view:own']); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.weekly-project-overview', [ + 'organization' => $user->organization, + ])); + + // Assert + $response->assertOk(); + } + + public function test_latest_tasks_endpoint_fails_if_user_has_no_permission_to_view_chart(): void + { + // Arrange + $user = $this->createUserWithPermission(); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.latest-tasks', [ + 'organization' => $user->organization, + ])); + + // Assert + $response->assertStatus(403); + } + + public function test_latest_tasks_endpoint_returns_chart_data(): void + { + // Arrange + $user = $this->createUserWithPermission(['charts:view:own']); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.latest-tasks', [ + 'organization' => $user->organization, + ])); + + // Assert + $response->assertOk(); + } + + public function test_last_seven_days_endpoint_fails_if_user_has_no_permission_to_view_chart(): void + { + // Arrange + $user = $this->createUserWithPermission(); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.last-seven-days', [ + 'organization' => $user->organization, + ])); + + // Assert + $response->assertStatus(403); + } + + public function test_last_seven_days_endpoint_returns_chart_data(): void + { + // Arrange + $user = $this->createUserWithPermission(['charts:view:own']); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.last-seven-days', [ + 'organization' => $user->organization, + ])); + + // Assert + $response->assertOk(); + } + + public function test_latest_team_activity_endpoint_fails_if_user_has_no_permission_to_view_chart_for_the_whole_orgnaization(): void + { + // Arrange + $user = $this->createUserWithPermission(); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.latest-team-activity', [ + 'organization' => $user->organization, + ])); + + // Assert + $response->assertStatus(403); + } + + public function test_latest_team_activity_endpoint_returns_chart_data(): void + { + // Arrange + $user = $this->createUserWithPermission(['charts:view:all']); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.latest-team-activity', [ + 'organization' => $user->organization, + ])); + + // Assert + $response->assertOk(); + } + + public function test_daily_tracked_hours_endpoint_fails_if_user_has_no_permission_to_view_chart(): void + { + // Arrange + $user = $this->createUserWithPermission(); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.daily-tracked-hours', [ + 'organization' => $user->organization, + ])); + + // Assert + $response->assertStatus(403); + } + + public function test_daily_tracked_hours_endpoint_returns_chart_data(): void + { + // Arrange + $user = $this->createUserWithPermission(['charts:view:own']); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.daily-tracked-hours', [ + 'organization' => $user->organization, + ])); + + // Assert + $response->assertOk(); + } + + public function test_total_weekly_time_endpoint_fails_if_user_has_no_permission_to_view_chart(): void + { + // Arrange + $user = $this->createUserWithPermission(); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.total-weekly-time', [ + 'organization' => $user->organization, + ])); + + // Assert + $response->assertStatus(403); + } + + public function test_total_weekly_time_endpoint_returns_chart_data(): void + { + // Arrange + $user = $this->createUserWithPermission(['charts:view:own']); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.total-weekly-time', [ + 'organization' => $user->organization, + ])); + + // Assert + $response->assertOk(); + } + + public function test_total_weekly_billable_time_endpoint_fails_if_user_has_no_permission_to_view_chart(): void + { + // Arrange + $user = $this->createUserWithPermission(); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.total-weekly-billable-time', [ + 'organization' => $user->organization, + ])); + + // Assert + $response->assertStatus(403); + } + + public function test_total_weekly_billable_time_endpoint_returns_chart_data(): void + { + // Arrange + $user = $this->createUserWithPermission(['charts:view:own']); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.total-weekly-billable-time', [ + 'organization' => $user->organization, + ])); + + // Assert + $response->assertOk(); + } + + public function test_total_weekly_billable_amount_endpoint_fails_if_user_has_no_permission_to_view_chart(): void + { + // Arrange + $user = $this->createUserWithPermission(); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.total-weekly-billable-amount', [ + 'organization' => $user->organization, + ])); + + // Assert + $response->assertStatus(403); + } + + public function test_total_weekly_billable_amount_endpoint_fails_if_the_user_is_an_employee_but_the_organization_does_not_allow_employees_to_view_billable_rates(): void + { + // Arrange + $user = $this->createUserWithRole(Role::Employee); + $organization = $user->organization; + $organization->employees_can_see_billable_rates = false; + $organization->save(); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.total-weekly-billable-amount', [ + 'organization' => $organization, + ])); + + // Assert + $response->assertStatus(403); + } + + public function test_total_weekly_billable_amount_endpoint_returns_chart_data(): void + { + // Arrange + $user = $this->createUserWithRole(Role::Employee); + $organization = $user->organization; + $organization->employees_can_see_billable_rates = true; + $organization->save(); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.total-weekly-billable-amount', [ + 'organization' => $user->organization, + ])); + + // Assert + $response->assertOk(); + } + + public function test_weekly_history_endpoint_fails_if_user_has_no_permission_to_view_chart(): void + { + // Arrange + $user = $this->createUserWithPermission(); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.weekly-history', [ + 'organization' => $user->organization, + ])); + + // Assert + $response->assertStatus(403); + } + + public function test_weekly_history_endpoint_returns_chart_data(): void + { + // Arrange + $user = $this->createUserWithPermission(['charts:view:own']); + Passport::actingAs($user->user); + + // Act + $response = $this->getJson(route('api.v1.charts.weekly-history', [ + 'organization' => $user->organization, + ])); + + // Assert + $response->assertOk(); + } +}