From 50f57f699738f6a4ce9d42f8d992f09dfc0cf841 Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Thu, 25 Jun 2026 12:10:44 +0200 Subject: [PATCH] add support for activity columns in clockify exports --- .../Importers/ClockifyProjectsImporter.php | 19 +++++++++----- .../Importers/ClockifyTimeEntriesImporter.php | 26 ++++++++++++++++--- .../clockify_projects_import_test_3.csv | 2 ++ .../clockify_time_entries_import_test_5.csv | 3 +++ .../ClockifyProjectsImporterTest.php | 21 +++++++++++++++ .../ClockifyTimeEntriesImporterTest.php | 19 ++++++++++++++ 6 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 resources/testfiles/clockify_projects_import_test_3.csv create mode 100644 resources/testfiles/clockify_time_entries_import_test_5.csv diff --git a/app/Service/Import/Importers/ClockifyProjectsImporter.php b/app/Service/Import/Importers/ClockifyProjectsImporter.php index d38023fa..6ca0575a 100644 --- a/app/Service/Import/Importers/ClockifyProjectsImporter.php +++ b/app/Service/Import/Importers/ClockifyProjectsImporter.php @@ -50,7 +50,7 @@ class ClockifyProjectsImporter extends DefaultImporter ]); } - if ($record[$tasksKey] !== '') { + if ($tasksKey !== null && $record[$tasksKey] !== '') { $tasks = explode(', ', $record[$tasksKey]); foreach ($tasks as $task) { $this->taskImportHelper->getKey([ @@ -90,20 +90,27 @@ class ClockifyProjectsImporter extends DefaultImporter 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)) { + // Clockify names the tasks column "Task", "Tasks" or "Activities" depending on the export; accept any. + if ($this->getTasksKey($header) === null) { throw new ImportException('Invalid CSV header, missing field: Tasks'); } } /** - * Clockify renamed the "Task" column to "Tasks" in newer exports. + * Clockify names the tasks column differently depending on the export + * version: "Task" (older), "Tasks" (newer) or "Activities". * * @param array $header */ - private function getTasksKey(array $header): string + private function getTasksKey(array $header): ?string { - return in_array('Tasks', $header, true) ? 'Tasks' : 'Task'; + foreach (['Tasks', 'Task', 'Activities'] as $field) { + if (in_array($field, $header, true)) { + return $field; + } + } + + return null; } /** diff --git a/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php b/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php index ba051c71..9f0c57fc 100644 --- a/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php +++ b/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php @@ -54,6 +54,7 @@ class ClockifyTimeEntriesImporter extends DefaultImporter $reader->setEscape(''); $header = $reader->getHeader(); $this->validateHeader($header); + $taskKey = $this->getTaskKey($header); $records = $reader->getRecords(); foreach ($records as $record) { $userId = $this->userImportHelper->getKey([ @@ -96,9 +97,9 @@ class ClockifyTimeEntriesImporter extends DefaultImporter ]); } $taskId = null; - if ($record['Task'] !== '') { + if ($taskKey !== null && $record[$taskKey] !== '') { $taskId = $this->taskImportHelper->getKey([ - 'name' => $record['Task'], + 'name' => $record[$taskKey], 'project_id' => $projectId, 'organization_id' => $this->organization->id, ]); @@ -216,7 +217,6 @@ class ClockifyTimeEntriesImporter extends DefaultImporter 'Project', 'Client', 'Description', - 'Task', 'User', 'Group', 'Email', @@ -231,6 +231,26 @@ class ClockifyTimeEntriesImporter extends DefaultImporter throw new ImportException('Invalid CSV header, missing field: '.$requiredField); } } + // Clockify names the task column "Task" or "Activity" depending on the export; accept either. + if ($this->getTaskKey($header) === null) { + throw new ImportException('Invalid CSV header, missing field: Task'); + } + } + + /** + * Clockify names the task column "Task" or "Activity" depending on the export version. + * + * @param array $header + */ + private function getTaskKey(array $header): ?string + { + foreach (['Task', 'Activity'] as $field) { + if (in_array($field, $header, true)) { + return $field; + } + } + + return null; } #[\Override] diff --git a/resources/testfiles/clockify_projects_import_test_3.csv b/resources/testfiles/clockify_projects_import_test_3.csv new file mode 100644 index 00000000..1982f7d3 --- /dev/null +++ b/resources/testfiles/clockify_projects_import_test_3.csv @@ -0,0 +1,2 @@ +"Project","Client","Status","Visibility","Billability","Activities","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" +"Project With Activities","","Active","Public","Yes","Activity A, Activity B","0.00","","","","","","","","","","0.00","0.00","","0.00","","0.00","0.00","0.00","","","","" diff --git a/resources/testfiles/clockify_time_entries_import_test_5.csv b/resources/testfiles/clockify_time_entries_import_test_5.csv new file mode 100644 index 00000000..78dc266a --- /dev/null +++ b/resources/testfiles/clockify_time_entries_import_test_5.csv @@ -0,0 +1,3 @@ +"Project","Client","Description","Activity","User","Group","Email","Tags","Billable","Start Date","Start Time","End Date","End Time","Duration (h)","Duration (decimal)","Billable Rate (USD)","Billable Amount (USD)" +"Project without Client","","","","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 for Big Company","Big Company","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/tests/Unit/Service/Import/Importers/ClockifyProjectsImporterTest.php b/tests/Unit/Service/Import/Importers/ClockifyProjectsImporterTest.php index a792661d..ff033fc3 100644 --- a/tests/Unit/Service/Import/Importers/ClockifyProjectsImporterTest.php +++ b/tests/Unit/Service/Import/Importers/ClockifyProjectsImporterTest.php @@ -95,4 +95,25 @@ class ClockifyProjectsImporterTest extends ImporterTestAbstract Task::query()->where('project_id', $activeProject->id)->pluck('name')->all(), ); } + + public function test_import_supports_activities_column_alias_for_tasks(): void + { + // Arrange + $organization = Organization::factory()->create(); + $timezone = 'Europe/Vienna'; + $importer = new ClockifyProjectsImporter; + $importer->init($organization); + // Some Clockify exports name the tasks column "Activities". + $data = Storage::disk('testfiles')->get('clockify_projects_import_test_3.csv'); + + // Act + $importer->importData($data, $timezone); + + // Assert + $project = Project::query()->where('organization_id', $organization->id)->where('name', 'Project With Activities')->firstOrFail(); + $this->assertEqualsCanonicalizing( + ['Activity A', 'Activity B'], + Task::query()->where('project_id', $project->id)->pluck('name')->all(), + ); + } } diff --git a/tests/Unit/Service/Import/Importers/ClockifyTimeEntriesImporterTest.php b/tests/Unit/Service/Import/Importers/ClockifyTimeEntriesImporterTest.php index 9712b909..cb293fff 100644 --- a/tests/Unit/Service/Import/Importers/ClockifyTimeEntriesImporterTest.php +++ b/tests/Unit/Service/Import/Importers/ClockifyTimeEntriesImporterTest.php @@ -117,6 +117,25 @@ class ClockifyTimeEntriesImporterTest extends ImporterTestAbstract $this->assertSame(0, $report->clientsCreated); } + public function test_import_supports_activity_column_alias_for_task(): void + { + // Arrange + $organization = Organization::factory()->create(); + $timezone = 'Europe/Vienna'; + $importer = new ClockifyTimeEntriesImporter; + $importer->init($organization); + // Some Clockify exports name the task column "Activity". + $data = Storage::disk('testfiles')->get('clockify_time_entries_import_test_5.csv'); + + // Act + $importer->importData($data, $timezone); + $report = $importer->getReport(); + + // Assert + $this->assertSame(2, $report->timeEntriesCreated); + $this->assertSame(1, $report->tasksCreated); + } + public function test_import_fails_if_month_in_date_is_bigger_than_12(): void { // Arrange