diff --git a/app/Service/Import/Importers/ClockifyProjectsImporter.php b/app/Service/Import/Importers/ClockifyProjectsImporter.php index 6ca0575a..f60a385c 100644 --- a/app/Service/Import/Importers/ClockifyProjectsImporter.php +++ b/app/Service/Import/Importers/ClockifyProjectsImporter.php @@ -29,7 +29,8 @@ class ClockifyProjectsImporter extends DefaultImporter $records = $reader->getRecords(); foreach ($records as $record) { $clientId = null; - if ($record['Client'] !== '') { + // Newer Clockify exports no longer contain a "Client" column. + if (($record['Client'] ?? '') !== '') { $clientId = $this->clientImportHelper->getKey([ 'name' => $record['Client'], 'organization_id' => $this->organization->id, @@ -45,7 +46,7 @@ class ClockifyProjectsImporter extends DefaultImporter 'color' => $this->colorService->getRandomColor(), 'is_billable' => $record['Billability'] === 'Yes', 'billable_rate' => $billableRateKey !== null && $record[$billableRateKey] !== '' ? (int) (((float) $record[$billableRateKey]) * 100) : null, - 'estimated_time' => $record['Estimated (h)'] !== '' && is_numeric($record['Estimated (h)']) ? (int) ($record['Estimated (h)'] * 3600) : null, + 'estimated_time' => isset($record['Estimated (h)']) && is_numeric($record['Estimated (h)']) ? (int) ($record['Estimated (h)'] * 3600) : null, 'archived_at' => $record['Status'] === 'Archived' ? Carbon::now() : null, ]); } @@ -80,7 +81,6 @@ class ClockifyProjectsImporter extends DefaultImporter { $requiredFields = [ 'Project', - 'Client', 'Status', 'Visibility', 'Billability', diff --git a/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php b/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php index 9f0c57fc..0492b87c 100644 --- a/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php +++ b/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php @@ -72,7 +72,7 @@ class ClockifyTimeEntriesImporter extends DefaultImporter ]); $member = $this->memberImportHelper->getModelById($memberId); $clientId = null; - if ($record['Client'] !== '') { + if (($record['Client'] ?? '') !== '') { $clientId = $this->clientImportHelper->getKey([ 'name' => $record['Client'], 'organization_id' => $this->organization->id, @@ -215,7 +215,6 @@ class ClockifyTimeEntriesImporter extends DefaultImporter { $requiredFields = [ 'Project', - 'Client', 'Description', 'User', 'Group', diff --git a/resources/testfiles/clockify_projects_import_test_4.csv b/resources/testfiles/clockify_projects_import_test_4.csv new file mode 100644 index 00000000..a2bbd516 --- /dev/null +++ b/resources/testfiles/clockify_projects_import_test_4.csv @@ -0,0 +1,2 @@ +"Project","Status","Visibility","Billability","Tasks","Tracked (h)","Estimated (h)","Remaining (h)","Overage (h)","Progress(%)","Billable (h)","Non-billable (h)","Billable Rate (USD)","Amount (USD)","Project members","Project manager","Note" +"Project Without Client Column","Active","Public","Yes","Task 1, Task 2","0.00","100.00","","","","0.00","0.00","100.01","0.00","Constantin Graf","","" diff --git a/resources/testfiles/clockify_time_entries_import_test_6.csv b/resources/testfiles/clockify_time_entries_import_test_6.csv new file mode 100644 index 00000000..77fa4efe --- /dev/null +++ b/resources/testfiles/clockify_time_entries_import_test_6.csv @@ -0,0 +1,3 @@ +"Project","Description","Task","User","Group","Email","Tags","Billable","Start Date","Start Time","End Date","End Time","Duration (h)","Duration (decimal)","Billable Rate (USD)","Billable Amount (USD)" +"Project A","","","Peter Tester","","peter.test@email.test","Development, Backend","No","03/04/2024","10:23:52 AM","03/04/2024","10:23:52 AM","00:00:00","0.00","0.00","0.00" +"Project B","Working hard","Task 1","Peter Tester","","peter.test@email.test","","Yes","03/04/2024","10:23 AM","03/04/2024","11:23:01 AM","01:00:01","0.00","0.00","0.00" diff --git a/resources/testfiles/clockify_time_entries_import_test_7.csv b/resources/testfiles/clockify_time_entries_import_test_7.csv new file mode 100644 index 00000000..d0a1e253 --- /dev/null +++ b/resources/testfiles/clockify_time_entries_import_test_7.csv @@ -0,0 +1,2 @@ +"Project","Description","Task","User","Group","Email","Tags","Billable","Start Date","Start Time","End Date","End Time","Client" +"Project A","Working hard","Task 1","Peter Tester","","peter.test@email.test","","Yes","03/04/2024","10:23 AM","03/04/2024","11:23:01 AM" diff --git a/tests/Unit/Service/Import/Importers/ClockifyProjectsImporterTest.php b/tests/Unit/Service/Import/Importers/ClockifyProjectsImporterTest.php index ff033fc3..2daef3a1 100644 --- a/tests/Unit/Service/Import/Importers/ClockifyProjectsImporterTest.php +++ b/tests/Unit/Service/Import/Importers/ClockifyProjectsImporterTest.php @@ -96,6 +96,29 @@ class ClockifyProjectsImporterTest extends ImporterTestAbstract ); } + public function test_import_of_test_file_without_client_column_succeeds(): void + { + // Arrange + $organization = Organization::factory()->create(); + $timezone = 'Europe/Vienna'; + $importer = new ClockifyProjectsImporter; + $importer->init($organization); + // Newer Clockify exports no longer contain a "Client" column. + $data = Storage::disk('testfiles')->get('clockify_projects_import_test_4.csv'); + + // Act + $importer->importData($data, $timezone); + + // Assert + $project = Project::query()->where('organization_id', $organization->id)->where('name', 'Project Without Client Column')->firstOrFail(); + $this->assertNull($project->client_id); + $this->assertSame(100 * 3600, $project->estimated_time); + $this->assertEqualsCanonicalizing( + ['Task 1', 'Task 2'], + Task::query()->where('project_id', $project->id)->pluck('name')->all(), + ); + } + public function test_import_supports_activities_column_alias_for_tasks(): void { // Arrange diff --git a/tests/Unit/Service/Import/Importers/ClockifyTimeEntriesImporterTest.php b/tests/Unit/Service/Import/Importers/ClockifyTimeEntriesImporterTest.php index cb293fff..ff3d9d65 100644 --- a/tests/Unit/Service/Import/Importers/ClockifyTimeEntriesImporterTest.php +++ b/tests/Unit/Service/Import/Importers/ClockifyTimeEntriesImporterTest.php @@ -136,6 +136,46 @@ class ClockifyTimeEntriesImporterTest extends ImporterTestAbstract $this->assertSame(1, $report->tasksCreated); } + public function test_import_of_test_file_without_client_column_succeeds(): void + { + // Arrange + $organization = Organization::factory()->create(); + $timezone = 'Europe/Vienna'; + $importer = new ClockifyTimeEntriesImporter; + $importer->init($organization); + // Newer Clockify exports no longer contain a "Client" column. + $data = Storage::disk('testfiles')->get('clockify_time_entries_import_test_6.csv'); + + // Act + $importer->importData($data, $timezone); + $report = $importer->getReport(); + + // Assert + $this->assertSame(2, $report->timeEntriesCreated); + $this->assertSame(2, $report->projectsCreated); + $this->assertSame(0, $report->clientsCreated); + } + + public function test_import_of_test_file_with_client_column_but_missing_values_succeeds(): void + { + // Arrange + $organization = Organization::factory()->create(); + $timezone = 'Europe/Vienna'; + $importer = new ClockifyTimeEntriesImporter; + $importer->init($organization); + // Rows shorter than the header are padded with null by the CSV reader. + $data = Storage::disk('testfiles')->get('clockify_time_entries_import_test_7.csv'); + + // Act + $importer->importData($data, $timezone); + $report = $importer->getReport(); + + // Assert + $this->assertSame(1, $report->timeEntriesCreated); + $this->assertSame(1, $report->projectsCreated); + $this->assertSame(0, $report->clientsCreated); + } + public function test_import_fails_if_month_in_date_is_bigger_than_12(): void { // Arrange