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