mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 00:02:15 +01:00
Compare commits
6 Commits
0591b51d52
...
4d9ae6b605
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d9ae6b605 | ||
|
|
f0de89b0e8 | ||
|
|
024283273a | ||
|
|
f01ab5fd9d | ||
|
|
4fb18f343f | ||
|
|
ba374c0371 |
@@ -66,7 +66,7 @@ class UserResource extends Resource
|
||||
->ignore($record?->getKey()),
|
||||
])
|
||||
->rule([
|
||||
'email',
|
||||
'email:rfc,strict',
|
||||
])
|
||||
->maxLength(255),
|
||||
Forms\Components\Toggle::make('is_placeholder')
|
||||
|
||||
@@ -25,7 +25,7 @@ class InvitationStoreRequest extends BaseFormRequest
|
||||
return [
|
||||
'email' => [
|
||||
'required',
|
||||
'email',
|
||||
'email:rfc,strict',
|
||||
],
|
||||
'role' => [
|
||||
'required',
|
||||
|
||||
@@ -41,7 +41,7 @@ class UserUpdateRequest extends BaseFormRequest
|
||||
'max:255',
|
||||
],
|
||||
'email' => [
|
||||
'email',
|
||||
'email:rfc,strict',
|
||||
'max:255',
|
||||
UniqueEloquent::make(User::class, 'email')->ignore($this->user->id)->query(function (Builder $query) {
|
||||
/** @var Builder<User> $query */
|
||||
|
||||
@@ -20,6 +20,11 @@ class RecalculateSpentTimeForProject implements ShouldDispatchAfterCommit, Shoul
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*/
|
||||
public bool $deleteWhenMissingModels = true;
|
||||
|
||||
public Project $project;
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,11 @@ class RecalculateSpentTimeForTask implements ShouldDispatchAfterCommit, ShouldQu
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*/
|
||||
public bool $deleteWhenMissingModels = true;
|
||||
|
||||
public Task $task;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -171,7 +171,7 @@ abstract class DefaultImporter implements ImporterContract
|
||||
}, validate: [
|
||||
'email' => [
|
||||
'required',
|
||||
'email',
|
||||
'email:rfc,strict',
|
||||
'max:255',
|
||||
],
|
||||
]);
|
||||
|
||||
34
package-lock.json
generated
34
package-lock.json
generated
@@ -45,7 +45,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.5",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@inertiajs/vue3": "^3.5.0",
|
||||
"@inertiajs/vue3": "^3.6.0",
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
@@ -763,9 +763,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@inertiajs/core": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-3.5.0.tgz",
|
||||
"integrity": "sha512-k0RpPULYD6d+FsYUIaSkUbvLbMGBRkzMc+qHhwfTVrMwCvXd6FyQ+O99PT5AN4j3BKy96XT45AKDwBhhti8Tgg==",
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-3.6.0.tgz",
|
||||
"integrity": "sha512-56L16uTtv6rtTfSkqqjte39DO4P5nyNf4Df0HCnn8YBteff5rjifO/2bX8he5unXI7udnJj3YWg9KxRyEkYRHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -783,13 +783,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@inertiajs/vue3": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@inertiajs/vue3/-/vue3-3.5.0.tgz",
|
||||
"integrity": "sha512-QhwjmUG32uDtaSoBfWpOZl5krwK+nqua+hPqNOlahOiRPan+TKtMri5fdhZxOfjdPjbjNX1zL1Oi3ileZWvpvw==",
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@inertiajs/vue3/-/vue3-3.6.0.tgz",
|
||||
"integrity": "sha512-o5r7J9uXz/bCQjSKccRyYRRd+v4d/ctNigmciIwdDQF+tOT9a1JM9nlq2uyvJur1o7b6CqZ7hkGR+mEj7l0cfw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inertiajs/core": "3.5.0",
|
||||
"@inertiajs/core": "3.6.0",
|
||||
"es-toolkit": "^1.33.0",
|
||||
"laravel-precognition": "^2.0.0"
|
||||
},
|
||||
@@ -1307,9 +1307,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/pluginutils/node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -6610,9 +6610,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin/node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -6905,9 +6905,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.5",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@inertiajs/vue3": "^3.5.0",
|
||||
"@inertiajs/vue3": "^3.6.0",
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
|
||||
@@ -527,6 +527,11 @@ const selectedProjectColor = computed(() => {
|
||||
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
|
||||
// over an item and some Item in the Dropdown is selected by keyboard navigation to scroll into view
|
||||
const mouseEnterHighlightActivated = ref(true);
|
||||
@@ -572,15 +577,24 @@ const showCreateProject = ref(false);
|
||||
:size="props.size"
|
||||
:class="twMerge('w-full justify-start overflow-hidden', props.class)">
|
||||
<div
|
||||
class="w-3 h-3 rounded-full shrink-0"
|
||||
class="w-2.5 h-2.5 rounded-full shrink-0"
|
||||
:style="{ backgroundColor: selectedProjectColor }"></div>
|
||||
<span class="truncate shrink-[1] text-text-primary pr-1">{{
|
||||
<span class="truncate shrink-[1] text-text-primary">{{
|
||||
selectedProjectName
|
||||
}}</span>
|
||||
<template v-if="currentTask">
|
||||
<ChevronRightIcon class="w-4 h-4 text-text-tertiary shrink-0" />
|
||||
<ChevronRightIcon class="!size-3 text-text-primary shrink-0 -mx-1" />
|
||||
<span class="truncate shrink-[100]">{{ currentTask.name }}</span>
|
||||
</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
|
||||
v-if="allowReset && project !== null"
|
||||
|
||||
2
resources/testfiles/clockify_projects_import_test_4.csv
Normal file
2
resources/testfiles/clockify_projects_import_test_4.csv
Normal file
@@ -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","",""
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -5,7 +5,7 @@
|
||||
{{ __('Your currently running time entry ":description" is now running for more than 8 hours!', ['description' => $timeEntry->description]) }}
|
||||
@endif
|
||||
|
||||
{{ __('If you forgot to stop the Time Tracker you do that in solidtime:') }}
|
||||
{{ __('If you forgot to stop the Time Tracker, you can do so in solidtime:') }}
|
||||
|
||||
@component('mail::button', ['url' => $dashboardUrl])
|
||||
{{ __('Go to solidtime') }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user