mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-18 13:12:16 +01:00
Compare commits
1 Commits
v0.16.0
...
6d7d6d46e0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d7d6d46e0 |
@@ -29,8 +29,7 @@ class ClockifyProjectsImporter extends DefaultImporter
|
|||||||
$records = $reader->getRecords();
|
$records = $reader->getRecords();
|
||||||
foreach ($records as $record) {
|
foreach ($records as $record) {
|
||||||
$clientId = null;
|
$clientId = null;
|
||||||
// Newer Clockify exports no longer contain a "Client" column.
|
if ($record['Client'] !== '') {
|
||||||
if (($record['Client'] ?? '') !== '') {
|
|
||||||
$clientId = $this->clientImportHelper->getKey([
|
$clientId = $this->clientImportHelper->getKey([
|
||||||
'name' => $record['Client'],
|
'name' => $record['Client'],
|
||||||
'organization_id' => $this->organization->id,
|
'organization_id' => $this->organization->id,
|
||||||
@@ -46,7 +45,7 @@ class ClockifyProjectsImporter extends DefaultImporter
|
|||||||
'color' => $this->colorService->getRandomColor(),
|
'color' => $this->colorService->getRandomColor(),
|
||||||
'is_billable' => $record['Billability'] === 'Yes',
|
'is_billable' => $record['Billability'] === 'Yes',
|
||||||
'billable_rate' => $billableRateKey !== null && $record[$billableRateKey] !== '' ? (int) (((float) $record[$billableRateKey]) * 100) : null,
|
'billable_rate' => $billableRateKey !== null && $record[$billableRateKey] !== '' ? (int) (((float) $record[$billableRateKey]) * 100) : null,
|
||||||
'estimated_time' => isset($record['Estimated (h)']) && is_numeric($record['Estimated (h)']) ? (int) ($record['Estimated (h)'] * 3600) : null,
|
'estimated_time' => $record['Estimated (h)'] !== '' && is_numeric($record['Estimated (h)']) ? (int) ($record['Estimated (h)'] * 3600) : null,
|
||||||
'archived_at' => $record['Status'] === 'Archived' ? Carbon::now() : null,
|
'archived_at' => $record['Status'] === 'Archived' ? Carbon::now() : null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -81,6 +80,7 @@ class ClockifyProjectsImporter extends DefaultImporter
|
|||||||
{
|
{
|
||||||
$requiredFields = [
|
$requiredFields = [
|
||||||
'Project',
|
'Project',
|
||||||
|
'Client',
|
||||||
'Status',
|
'Status',
|
||||||
'Visibility',
|
'Visibility',
|
||||||
'Billability',
|
'Billability',
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
|||||||
]);
|
]);
|
||||||
$member = $this->memberImportHelper->getModelById($memberId);
|
$member = $this->memberImportHelper->getModelById($memberId);
|
||||||
$clientId = null;
|
$clientId = null;
|
||||||
if (($record['Client'] ?? '') !== '') {
|
if ($record['Client'] !== '') {
|
||||||
$clientId = $this->clientImportHelper->getKey([
|
$clientId = $this->clientImportHelper->getKey([
|
||||||
'name' => $record['Client'],
|
'name' => $record['Client'],
|
||||||
'organization_id' => $this->organization->id,
|
'organization_id' => $this->organization->id,
|
||||||
@@ -215,6 +215,7 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
|||||||
{
|
{
|
||||||
$requiredFields = [
|
$requiredFields = [
|
||||||
'Project',
|
'Project',
|
||||||
|
'Client',
|
||||||
'Description',
|
'Description',
|
||||||
'User',
|
'User',
|
||||||
'Group',
|
'Group',
|
||||||
|
|||||||
1053
package-lock.json
generated
1053
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@@ -28,58 +28,58 @@
|
|||||||
"@eslint/eslintrc": "^3.3.5",
|
"@eslint/eslintrc": "^3.3.5",
|
||||||
"@eslint/js": "^9.39.4",
|
"@eslint/js": "^9.39.4",
|
||||||
"@inertiajs/vue3": "^2.3.23",
|
"@inertiajs/vue3": "^2.3.23",
|
||||||
"@playwright/test": "^1.60.0",
|
"@playwright/test": "^1.61.1",
|
||||||
"@tailwindcss/forms": "^0.5.11",
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.20",
|
||||||
"@types/chroma-js": "^3.1.2",
|
"@types/chroma-js": "^3.1.2",
|
||||||
"@types/node": "^22.19.19",
|
"@types/node": "^22.19.19",
|
||||||
"@vitejs/plugin-vue": "^6.0.6",
|
"@vitejs/plugin-vue": "^6.0.7",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.11",
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.9.1",
|
||||||
"autoprefixer": "^10.5.0",
|
"autoprefixer": "^10.5.2",
|
||||||
"axios": "^1.16.0",
|
"axios": "^1.18.1",
|
||||||
"eslint-plugin-unused-imports": "^4.4.1",
|
"eslint-plugin-unused-imports": "^4.4.1",
|
||||||
"happy-dom": "^20.8.9",
|
"happy-dom": "^20.10.6",
|
||||||
"laravel-vite-plugin": "^2.1.0",
|
"laravel-vite-plugin": "^2.1.0",
|
||||||
"openapi-zod-client": "^1.18.3",
|
"openapi-zod-client": "^1.18.3",
|
||||||
"postcss": "^8.5.14",
|
"postcss": "^8.5.16",
|
||||||
"postcss-import": "^15.1.0",
|
"postcss-import": "^15.1.0",
|
||||||
"postcss-nesting": "^12.1.5",
|
"postcss-nesting": "^12.1.5",
|
||||||
"tailwindcss": "^3.4.19",
|
"tailwindcss": "^3.4.19",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.3",
|
"vite": "^7.3.3",
|
||||||
"vite-plugin-checker": "^0.12.0",
|
"vite-plugin-checker": "^0.14.4",
|
||||||
"vitest": "^4.1.4",
|
"vitest": "^4.1.9",
|
||||||
"vue": "^3.5.34",
|
"vue": "^3.5.39",
|
||||||
"vue-tsc": "^3.2.8"
|
"vue-tsc": "^3.3.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/core": "^1.7.5",
|
"@floating-ui/core": "^1.7.5",
|
||||||
"@floating-ui/vue": "^1.1.11",
|
"@floating-ui/vue": "^1.1.11",
|
||||||
"@heroicons/vue": "^2.2.0",
|
"@heroicons/vue": "^2.2.0",
|
||||||
"@lucide/vue": "^1.14.0",
|
"@lucide/vue": "^1.23.0",
|
||||||
"@rushstack/eslint-patch": "^1.16.1",
|
"@rushstack/eslint-patch": "^1.16.1",
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
"@tanstack/vue-form": "^1.32.0",
|
"@tanstack/vue-form": "^1.33.0",
|
||||||
"@tanstack/vue-query": "^5.100.10",
|
"@tanstack/vue-query": "^5.101.2",
|
||||||
"@tanstack/vue-query-devtools": "^5.91.0",
|
"@tanstack/vue-query-devtools": "^5.91.0",
|
||||||
"@tanstack/vue-table": "^8.21.3",
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
"@tanstack/vue-virtual": "^3.13.24",
|
"@tanstack/vue-virtual": "^3.13.31",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vue/eslint-config-typescript": "^14.7.0",
|
"@vue/eslint-config-typescript": "^14.9.0",
|
||||||
"@vueuse/core": "^14.3.0",
|
"@vueuse/core": "^14.3.0",
|
||||||
"@vueuse/integrations": "^14.3.0",
|
"@vueuse/integrations": "^14.3.0",
|
||||||
"@zodios/core": "^10.9.6",
|
"@zodios/core": "^10.9.6",
|
||||||
"chroma-js": "^3.2.0",
|
"chroma-js": "^3.2.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.21",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.1.0",
|
||||||
"focus-trap": "^8.2.0",
|
"focus-trap": "^8.2.2",
|
||||||
"parse-duration": "^2.1.6",
|
"parse-duration": "^2.1.6",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"radix-vue": "^1.9.17",
|
"radix-vue": "^1.9.17",
|
||||||
"reka-ui": "^2.9.7",
|
"reka-ui": "^2.10.1",
|
||||||
"tailwind-merge": "^2.6.1",
|
"tailwind-merge": "^2.6.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vue-draggable-plus": "^0.6.1",
|
"vue-draggable-plus": "^0.6.1",
|
||||||
|
|||||||
@@ -527,11 +527,6 @@ const selectedProjectColor = computed(() => {
|
|||||||
return currentProject.value?.color || 'var(--theme-color-icon-default)';
|
return currentProject.value?.color || 'var(--theme-color-icon-default)';
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedClientName = computed(() => {
|
|
||||||
const clientId = currentProject.value?.client_id;
|
|
||||||
return clientId ? clientsById.value.get(clientId)?.name : undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
// This state prevents the selection to jump to random items when the mouse cursor is
|
// This state prevents the selection to jump to random items when the mouse cursor is
|
||||||
// over an item and some Item in the Dropdown is selected by keyboard navigation to scroll into view
|
// over an item and some Item in the Dropdown is selected by keyboard navigation to scroll into view
|
||||||
const mouseEnterHighlightActivated = ref(true);
|
const mouseEnterHighlightActivated = ref(true);
|
||||||
@@ -577,24 +572,15 @@ const showCreateProject = ref(false);
|
|||||||
:size="props.size"
|
:size="props.size"
|
||||||
:class="twMerge('w-full justify-start overflow-hidden', props.class)">
|
:class="twMerge('w-full justify-start overflow-hidden', props.class)">
|
||||||
<div
|
<div
|
||||||
class="w-2.5 h-2.5 rounded-full shrink-0"
|
class="w-3 h-3 rounded-full shrink-0"
|
||||||
:style="{ backgroundColor: selectedProjectColor }"></div>
|
:style="{ backgroundColor: selectedProjectColor }"></div>
|
||||||
<span class="truncate shrink-[1] text-text-primary">{{
|
<span class="truncate shrink-[1] text-text-primary pr-1">{{
|
||||||
selectedProjectName
|
selectedProjectName
|
||||||
}}</span>
|
}}</span>
|
||||||
<template v-if="currentTask">
|
<template v-if="currentTask">
|
||||||
<ChevronRightIcon class="!size-3 text-text-primary shrink-0 -mx-1" />
|
<ChevronRightIcon class="w-4 h-4 text-text-tertiary shrink-0" />
|
||||||
<span class="truncate shrink-[100]">{{ currentTask.name }}</span>
|
<span class="truncate shrink-[100]">{{ currentTask.name }}</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="selectedClientName">
|
|
||||||
<span class="dark:text-text-tertiary text-text-quaternary shrink-0"
|
|
||||||
>•</span
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="truncate shrink-[200] dark:text-text-tertiary text-text-quaternary"
|
|
||||||
>{{ selectedClientName }}</span
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</Button>
|
</Button>
|
||||||
<button
|
<button
|
||||||
v-if="allowReset && project !== null"
|
v-if="allowReset && project !== null"
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
"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","",""
|
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
"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"
|
|
||||||
|
@@ -1,2 +0,0 @@
|
|||||||
"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"
|
|
||||||
|
@@ -96,29 +96,6 @@ 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
|
public function test_import_supports_activities_column_alias_for_tasks(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|||||||
@@ -136,46 +136,6 @@ class ClockifyTimeEntriesImporterTest extends ImporterTestAbstract
|
|||||||
$this->assertSame(1, $report->tasksCreated);
|
$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
|
public function test_import_fails_if_month_in_date_is_bigger_than_12(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|||||||
Reference in New Issue
Block a user