From ab9f6e6afbd21a25f97bd7a317b79f7b6646632f Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Thu, 25 Jun 2026 00:20:29 +0200 Subject: [PATCH] adapt clockify importer to support task -> tasks column rename --- .../Importers/ClockifyProjectsImporter.php | 20 ++++++++++++++--- .../clockify_projects_import_test_2.csv | 6 ++--- .../ClockifyProjectsImporterTest.php | 22 +++++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/app/Service/Import/Importers/ClockifyProjectsImporter.php b/app/Service/Import/Importers/ClockifyProjectsImporter.php index 5c41c0a3..d38023fa 100644 --- a/app/Service/Import/Importers/ClockifyProjectsImporter.php +++ b/app/Service/Import/Importers/ClockifyProjectsImporter.php @@ -25,6 +25,7 @@ class ClockifyProjectsImporter extends DefaultImporter $header = $reader->getHeader(); $this->validateHeader($header); $billableRateKey = $this->getBillableRateKey($header); + $tasksKey = $this->getTasksKey($header); $records = $reader->getRecords(); foreach ($records as $record) { $clientId = null; @@ -49,8 +50,8 @@ class ClockifyProjectsImporter extends DefaultImporter ]); } - if ($record['Task'] !== '') { - $tasks = explode(', ', $record['Task']); + if ($record[$tasksKey] !== '') { + $tasks = explode(', ', $record[$tasksKey]); foreach ($tasks as $task) { $this->taskImportHelper->getKey([ 'name' => $task, @@ -83,13 +84,26 @@ class ClockifyProjectsImporter extends DefaultImporter 'Status', 'Visibility', 'Billability', - 'Task', ]; foreach ($requiredFields as $requiredField) { if (! in_array($requiredField, $header, true)) { throw new ImportException('Invalid CSV header, missing field: '.$requiredField); } } + // Clockify renamed the "Task" column to "Tasks" in newer exports; accept either. + if (! in_array('Task', $header, true) && ! in_array('Tasks', $header, true)) { + throw new ImportException('Invalid CSV header, missing field: Tasks'); + } + } + + /** + * Clockify renamed the "Task" column to "Tasks" in newer exports. + * + * @param array $header + */ + private function getTasksKey(array $header): string + { + return in_array('Tasks', $header, true) ? 'Tasks' : 'Task'; } /** diff --git a/resources/testfiles/clockify_projects_import_test_2.csv b/resources/testfiles/clockify_projects_import_test_2.csv index 40fabe0f..8d1b415f 100644 --- a/resources/testfiles/clockify_projects_import_test_2.csv +++ b/resources/testfiles/clockify_projects_import_test_2.csv @@ -1,3 +1,3 @@ -"Project","Client","Status","Visibility","Billability","Task","Tracked (h)","Estimated (h)","Remaining (h)","Overage (h)","Progress(%)","Billable (h)","Non-billable (h)","Billable Rate (USD)","Amount (USD)","Project members","Project manager","Note" -"Active Project","Big Company","Active","Public","Yes","Task 1, Task 2","0.00","100.00","","","","0.00","0.00","100.01","0.00","Constantin Graf","","" -"Archived Project","","Archived","Public","Yes","","0.00","","","","","0.00","0.00","","0.00","Constantin Graf","","" +"Project","Client","Status","Visibility","Billability","Tasks","Tracked (h)","Estimated (h)","Remaining (h)","Overage (h)","Tracked (USD)","Estimated (USD)","Remaining (USD)","Overage (USD)","Progress(%)","Recurring estimate","Billable (h)","Non-billable (h)","Billable Rate (USD)","Amount (USD)","Cost Rate (USD)","Expenses (USD)","Billable expenses (USD)","Non-billable expenses (USD)","Additional fields","Project members","Project manager","Note" +"Active Project","Big Company","Active","Public","Yes","Task 1, Task 2","0.00","100.00","","","","","","","","","0.00","0.00","100.01","0.00","","0.00","0.00","0.00","","Constantin Graf","","" +"Archived Project","","Archived","Public","Yes","","0.00","","","","","","","","","","0.00","0.00","","0.00","","0.00","0.00","0.00","","Constantin Graf","","" diff --git a/tests/Unit/Service/Import/Importers/ClockifyProjectsImporterTest.php b/tests/Unit/Service/Import/Importers/ClockifyProjectsImporterTest.php index 1b313717..a792661d 100644 --- a/tests/Unit/Service/Import/Importers/ClockifyProjectsImporterTest.php +++ b/tests/Unit/Service/Import/Importers/ClockifyProjectsImporterTest.php @@ -6,6 +6,7 @@ namespace Tests\Unit\Service\Import\Importers; use App\Models\Organization; use App\Models\Project; +use App\Models\Task; use App\Service\Import\Importers\ClockifyProjectsImporter; use App\Service\Import\Importers\DefaultImporter; use App\Service\Import\Importers\ImportException; @@ -73,4 +74,25 @@ class ClockifyProjectsImporterTest extends ImporterTestAbstract $this->assertNotNull($archivedProject->archived_at); $this->assertTrue($archivedProject->is_archived); } + + public function test_import_supports_renamed_tasks_column(): void + { + // Arrange + $organization = Organization::factory()->create(); + $timezone = 'Europe/Vienna'; + $importer = new ClockifyProjectsImporter; + $importer->init($organization); + // Newer Clockify exports rename the "Task" column to "Tasks". + $data = Storage::disk('testfiles')->get('clockify_projects_import_test_2.csv'); + + // Act + $importer->importData($data, $timezone); + + // Assert + $activeProject = Project::query()->where('organization_id', $organization->id)->where('name', 'Active Project')->firstOrFail(); + $this->assertEqualsCanonicalizing( + ['Task 1', 'Task 2'], + Task::query()->where('project_id', $activeProject->id)->pluck('name')->all(), + ); + } }