From 48f09421d0435a2aafbbd89843cd1e9f5d0665bf Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Fri, 16 May 2025 15:14:22 +0200 Subject: [PATCH] Fixed time entries exports for employees #2 --- .../Api/V1/TimeEntryController.php | 1 + .../reports/time-entry-index/pdf.blade.php | 8 +- .../Endpoint/Api/V1/TimeEntryEndpointTest.php | 190 ++++++++++++++++++ 3 files changed, 196 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Api/V1/TimeEntryController.php b/app/Http/Controllers/Api/V1/TimeEntryController.php index 3cfb25dc..1e69d928 100644 --- a/app/Http/Controllers/Api/V1/TimeEntryController.php +++ b/app/Http/Controllers/Api/V1/TimeEntryController.php @@ -226,6 +226,7 @@ class TimeEntryController extends Controller 'start' => $request->getStart()->timezone($timezone), 'end' => $request->getEnd()->timezone($timezone), 'localization' => $localizationService, + 'showBillableRate' => $showBillableRate, ]); $footerViewFile = file_get_contents(resource_path('views/reports/time-entry-index/pdf-footer.blade.php')); if ($footerViewFile === false) { diff --git a/resources/views/reports/time-entry-index/pdf.blade.php b/resources/views/reports/time-entry-index/pdf.blade.php index c08f0167..d0491054 100644 --- a/resources/views/reports/time-entry-index/pdf.blade.php +++ b/resources/views/reports/time-entry-index/pdf.blade.php @@ -140,12 +140,14 @@
{{ $localization->formatInterval(CarbonInterval::seconds($aggregatedData['seconds'])) }}
+ @if($showBillableRate)
Total cost
-
{{ $localization->formatCurrency(Money::of(BigDecimal::ofUnscaledValue($aggregatedData['cost'], 2)->__toString(), $currency)) }}
+
+ {{ $localization->formatCurrency(Money::of(BigDecimal::ofUnscaledValue($aggregatedData['cost'], 2)->__toString(), $currency)) }} +
- + @endif
diff --git a/tests/Unit/Endpoint/Api/V1/TimeEntryEndpointTest.php b/tests/Unit/Endpoint/Api/V1/TimeEntryEndpointTest.php index d1b1acd6..69742587 100644 --- a/tests/Unit/Endpoint/Api/V1/TimeEntryEndpointTest.php +++ b/tests/Unit/Endpoint/Api/V1/TimeEntryEndpointTest.php @@ -686,6 +686,10 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract 'time-entries:view:all', ]); Passport::actingAs($data->user); + $client = Client::factory()->forOrganization($data->organization)->create(); + $project = Project::factory()->forOrganization($data->organization)->forClient($client)->create(); + $timeEntry1 = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create(); + $timeEntry2 = TimeEntry::factory()->forOrganization($data->organization)->forProject($project)->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create(); $this->actAsOrganizationWithSubscription(); // Act @@ -700,6 +704,192 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract $this->assertResponseCode($response, 200); } + public function test_index_export_endpoint_can_create_a_detailed_time_entry_report_in_format_csv_as_employee_role_with_show_billable_rate(): void + { + // Arrange + $data = $this->createUserWithRole(Role::Employee, true); + $client = Client::factory()->forOrganization($data->organization)->create(); + $project = Project::factory()->forOrganization($data->organization)->forClient($client)->create(); + $timeEntry1 = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create(); + $timeEntry2 = TimeEntry::factory()->forOrganization($data->organization)->forProject($project)->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create(); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.time-entries.index-export', [ + $data->organization->getKey(), + 'format' => ExportFormat::CSV, + 'start' => Carbon::now()->startOfYear()->toIso8601ZuluString(), + 'end' => Carbon::now()->endOfYear()->toIso8601ZuluString(), + 'member_id' => $data->member->id, + ])); + + // Assert + $this->assertResponseCode($response, 200); + } + + public function test_index_export_endpoint_can_create_a_detailed_time_entry_report_in_format_ods_as_employee_role_with_show_billable_rate(): void + { + // Arrange + $data = $this->createUserWithRole(Role::Employee, true); + $client = Client::factory()->forOrganization($data->organization)->create(); + $project = Project::factory()->forOrganization($data->organization)->forClient($client)->create(); + $timeEntry1 = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create(); + $timeEntry2 = TimeEntry::factory()->forOrganization($data->organization)->forProject($project)->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create(); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.time-entries.index-export', [ + $data->organization->getKey(), + 'format' => ExportFormat::ODS, + 'start' => Carbon::now()->startOfYear()->toIso8601ZuluString(), + 'end' => Carbon::now()->endOfYear()->toIso8601ZuluString(), + 'member_id' => $data->member->id, + ])); + + // Assert + $this->assertResponseCode($response, 200); + } + + public function test_index_export_endpoint_can_create_a_detailed_time_entry_report_in_format_xlxs_as_employee_role_with_show_billable_rate(): void + { + // Arrange + $data = $this->createUserWithRole(Role::Employee, true); + $client = Client::factory()->forOrganization($data->organization)->create(); + $project = Project::factory()->forOrganization($data->organization)->forClient($client)->create(); + $timeEntry1 = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create(); + $timeEntry2 = TimeEntry::factory()->forOrganization($data->organization)->forProject($project)->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create(); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.time-entries.index-export', [ + $data->organization->getKey(), + 'format' => ExportFormat::XLSX, + 'start' => Carbon::now()->startOfYear()->toIso8601ZuluString(), + 'end' => Carbon::now()->endOfYear()->toIso8601ZuluString(), + 'member_id' => $data->member->id, + ])); + + // Assert + $this->assertResponseCode($response, 200); + } + + public function test_index_export_endpoint_can_create_a_detailed_time_entry_report_in_format_pdf_as_employee_role_with_show_billable_rate(): void + { + // Arrange + $data = $this->createUserWithRole(Role::Employee, true); + Passport::actingAs($data->user); + $client = Client::factory()->forOrganization($data->organization)->create(); + $project = Project::factory()->forOrganization($data->organization)->forClient($client)->create(); + $timeEntry1 = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create(); + $timeEntry2 = TimeEntry::factory()->forOrganization($data->organization)->forProject($project)->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create(); + $this->actAsOrganizationWithSubscription(); + + // Act + $response = $this->getJson(route('api.v1.time-entries.index-export', [ + $data->organization->getKey(), + 'format' => ExportFormat::PDF, + 'start' => Carbon::now()->startOfYear()->toIso8601ZuluString(), + 'end' => Carbon::now()->endOfYear()->toIso8601ZuluString(), + 'member_id' => $data->member->id, + ])); + + // Assert + $this->assertResponseCode($response, 200); + } + + public function test_index_export_endpoint_can_create_a_detailed_time_entry_report_in_format_csv_as_employee_role_without_show_billable_rate(): void + { + // Arrange + $data = $this->createUserWithRole(Role::Employee, false); + $client = Client::factory()->forOrganization($data->organization)->create(); + $project = Project::factory()->forOrganization($data->organization)->forClient($client)->create(); + $timeEntry1 = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create(); + $timeEntry2 = TimeEntry::factory()->forOrganization($data->organization)->forProject($project)->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create(); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.time-entries.index-export', [ + $data->organization->getKey(), + 'format' => ExportFormat::CSV, + 'start' => Carbon::now()->startOfYear()->toIso8601ZuluString(), + 'end' => Carbon::now()->endOfYear()->toIso8601ZuluString(), + 'member_id' => $data->member->id, + ])); + + // Assert + $this->assertResponseCode($response, 200); + } + + public function test_index_export_endpoint_can_create_a_detailed_time_entry_report_in_format_ods_as_employee_role_without_show_billable_rate(): void + { + // Arrange + $data = $this->createUserWithRole(Role::Employee, false); + $client = Client::factory()->forOrganization($data->organization)->create(); + $project = Project::factory()->forOrganization($data->organization)->forClient($client)->create(); + $timeEntry1 = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create(); + $timeEntry2 = TimeEntry::factory()->forOrganization($data->organization)->forProject($project)->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create(); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.time-entries.index-export', [ + $data->organization->getKey(), + 'format' => ExportFormat::ODS, + 'start' => Carbon::now()->startOfYear()->toIso8601ZuluString(), + 'end' => Carbon::now()->endOfYear()->toIso8601ZuluString(), + 'member_id' => $data->member->id, + ])); + + // Assert + $this->assertResponseCode($response, 200); + } + + public function test_index_export_endpoint_can_create_a_detailed_time_entry_report_in_format_xlxs_as_employee_role_without_show_billable_rate(): void + { + // Arrange + $data = $this->createUserWithRole(Role::Employee, false); + $client = Client::factory()->forOrganization($data->organization)->create(); + $project = Project::factory()->forOrganization($data->organization)->forClient($client)->create(); + $timeEntry1 = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create(); + $timeEntry2 = TimeEntry::factory()->forOrganization($data->organization)->forProject($project)->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create(); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.time-entries.index-export', [ + $data->organization->getKey(), + 'format' => ExportFormat::XLSX, + 'start' => Carbon::now()->startOfYear()->toIso8601ZuluString(), + 'end' => Carbon::now()->endOfYear()->toIso8601ZuluString(), + 'member_id' => $data->member->id, + ])); + + // Assert + $this->assertResponseCode($response, 200); + } + + public function test_index_export_endpoint_can_create_a_detailed_time_entry_report_in_format_pdf_as_employee_role_without_show_billable_rate(): void + { + // Arrange + $data = $this->createUserWithRole(Role::Employee, false); + Passport::actingAs($data->user); + $client = Client::factory()->forOrganization($data->organization)->create(); + $project = Project::factory()->forOrganization($data->organization)->forClient($client)->create(); + $timeEntry1 = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create(); + $timeEntry2 = TimeEntry::factory()->forOrganization($data->organization)->forProject($project)->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create(); + $this->actAsOrganizationWithSubscription(); + + // Act + $response = $this->getJson(route('api.v1.time-entries.index-export', [ + $data->organization->getKey(), + 'format' => ExportFormat::PDF, + 'start' => Carbon::now()->startOfYear()->toIso8601ZuluString(), + 'end' => Carbon::now()->endOfYear()->toIso8601ZuluString(), + 'member_id' => $data->member->id, + ])); + + // Assert + $this->assertResponseCode($response, 200); + } + public function test_aggregate_export_endpoints_fails_if_user_no_permission_to_view_time_entries(): void { // Arrange