Compare commits

..

59 Commits

Author SHA1 Message Date
Gregor Vostrak
ab9f6e6afb adapt clockify importer to support task -> tasks column rename 2026-06-25 00:20:29 +02:00
Gregor Vostrak
2a40bb7edb add support for archived projects in the clockify importer 2026-06-25 00:08:42 +02:00
Gregor Vostrak
365f672cfc clarify import order for clockify and harvest 2026-06-25 00:02:41 +02:00
Gregor Vostrak
25f30fa820 fix destructive color contrast 2026-06-24 17:44:56 +02:00
Constantin Graf
37a8f43b23 Allow Clockify import without Billable column 2026-06-24 13:44:48 +02:00
Constantin Graf
c94aa8038d Add base path to the vite config to be able to tunnel all vite assets through a CDN 2026-06-20 21:55:29 +02:00
Constantin Graf
c9e84260b1 Updated GitHub actions 2026-06-19 16:53:40 +02:00
dependabot[bot]
9a53f903d8 Bump WyriHaximus/github-action-get-previous-tag from 1 to 2
Bumps [WyriHaximus/github-action-get-previous-tag](https://github.com/wyrihaximus/github-action-get-previous-tag) from 1 to 2.
- [Release notes](https://github.com/wyrihaximus/github-action-get-previous-tag/releases)
- [Commits](https://github.com/wyrihaximus/github-action-get-previous-tag/compare/v1...v2)

---
updated-dependencies:
- dependency-name: WyriHaximus/github-action-get-previous-tag
  dependency-version: '2'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-19 13:13:24 +02:00
dependabot[bot]
d50ce52b5b Bump actions/upload-artifact from 4 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-19 13:10:17 +02:00
dependabot[bot]
aea310e639 Bump actions/setup-node from 4 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-19 13:10:01 +02:00
dependabot[bot]
a94627321b Bump docker/setup-buildx-action from 3 to 4
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-19 13:09:51 +02:00
dependabot[bot]
806d138fc9 Bump docker/build-push-action from 6 to 7
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-19 13:09:37 +02:00
Constantin Graf
240dd809f4 Fixed organiztion page for employees 2026-06-18 17:30:54 +02:00
dependabot[bot]
dedb50ea73 Bump docker/setup-qemu-action from 3 to 4
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-18 13:32:01 +02:00
dependabot[bot]
6fe856fd31 Bump docker/metadata-action from 5 to 6
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-18 13:31:39 +02:00
dependabot[bot]
20735c2fed Bump codecov/codecov-action from 5.5.1 to 7.0.0
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.5.1 to 7.0.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.5.1...v7.0.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-18 13:31:20 +02:00
dependabot[bot]
b14e40c64b Bump actions/download-artifact from 4 to 8
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-18 13:30:41 +02:00
dependabot[bot]
f2277ff708 Bump docker/login-action from 3 to 4
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-18 13:30:11 +02:00
Constantin Graf
6a197f7f34 Add password check to users.destroy and organizations.destroy 2026-06-18 13:27:43 +02:00
Constantin Graf
24c94af952 Add option to enable email verification locally for testing 2026-06-18 13:27:43 +02:00
Constantin Graf
111ce94150 Add comments to events about legacy events 2026-06-18 13:27:43 +02:00
Constantin Graf
49e69b85a6 Fixed middlwares after jetstream removal 2026-06-18 13:27:43 +02:00
Gregor Vostrak
90a5c2b87c add comprehensive 2 factor authentication e2e tests 2026-06-18 13:27:43 +02:00
Gregor Vostrak
6b01034bdd add back destroy other browser sessions endpoint (jetstream migration) 2026-06-18 13:27:43 +02:00
Gregor Vostrak
a4c400ef4e update e2e test setup to use user settings api endpoint 2026-06-18 13:27:43 +02:00
Gregor Vostrak
5d01b20dbf change redirects and references to new organization routes 2026-06-18 13:27:43 +02:00
Gregor Vostrak
24875f9424 add currency to organization update endpoint 2026-06-18 13:27:43 +02:00
Gregor Vostrak
4975cf1dab replace hardcoded inertia props with organization delete/update perms 2026-06-18 13:27:43 +02:00
Gregor Vostrak
b3fb04c4be remove unused inertia organization page props 2026-06-18 13:27:43 +02:00
Gregor Vostrak
0cc8d762a5 fix inertia backend role data structure after jetstream migration 2026-06-18 13:27:43 +02:00
Gregor Vostrak
e3e45161a1 call api for organization create/update/delete and switch 2026-06-18 13:27:43 +02:00
Constantin Graf
0c825a691e Fixed current organization after normal registration 2026-06-18 13:27:43 +02:00
Constantin Graf
24775d2c47 Added tests 2026-06-18 13:27:43 +02:00
Gregor Vostrak
c5c129b239 add switch current organization endpoint 2026-06-18 13:27:43 +02:00
Constantin Graf
89a9341d91 Removed Laravel Jetstream 2026-06-18 13:27:43 +02:00
Gregor Vostrak
bffd0773be remove jetstream inertia properties; remove unused ApiTokenManager; 2026-06-18 13:27:43 +02:00
Gregor Vostrak
56ea0ed90e move banners on login and register cards into the cards 2026-06-18 13:27:43 +02:00
Gregor Vostrak
24c19e7e7c add pending email cancel button 2026-06-18 13:27:43 +02:00
Constantin Graf
734aab6353 Replaces all Jetstream model trait functions and relations 2026-06-18 13:27:43 +02:00
Constantin Graf
5eb1a6a006 Add reset pending email endpoint to user controller 2026-06-18 13:27:43 +02:00
Gregor Vostrak
d0334bd730 move user delete to api endpoint 2026-06-18 13:27:43 +02:00
Gregor Vostrak
fe4e903203 use api routes for profile information updates 2026-06-18 13:27:43 +02:00
Gregor Vostrak
43e42ace54 show null billable rate as empty not as 0 to avoid confusion 2026-06-18 13:27:43 +02:00
Gregor Vostrak
84728f0914 fix e2e selectors to adapt to reka-ui change; 2026-06-18 13:27:43 +02:00
Gregor Vostrak
8f02f98afc add pending email to UserResource and update openapi client 2026-06-18 13:27:43 +02:00
Gregor Vostrak
c1ecd0eff5 update ui package dependencies; update lucide imports 2026-06-18 13:27:43 +02:00
Gregor Vostrak
486b5d2152 add user endpoint tests for idempotence email update, unauthenticated
update and invalid email
2026-06-18 13:27:43 +02:00
Gregor Vostrak
5476b4e5a9 update npm dependencies 2026-06-18 13:27:43 +02:00
Gregor Vostrak
ceec4cbf62 add profile page e2e tests 2026-06-18 13:27:43 +02:00
Gregor Vostrak
372c7b02a0 update email address change info to use session based banners 2026-06-18 13:27:43 +02:00
Gregor Vostrak
cb8047028c add 1MB photo upload limit 2026-06-18 13:27:43 +02:00
Gregor Vostrak
e29be581fa add photo delete logic to user update endpoint 2026-06-18 13:27:43 +02:00
Constantin Graf
02da133fb3 Updated composer dependencies 2026-06-18 13:27:43 +02:00
Constantin Graf
d00fe2baff Add more tests 2026-06-18 13:27:43 +02:00
Constantin Graf
a73fe59892 Add migration to lower case the user emails 2026-06-18 13:27:43 +02:00
Constantin Graf
5d744d70c4 Migrate permission away from Jetstream; Moved update user to REST API 2026-06-18 13:27:43 +02:00
Gregor Vostrak
12d58ee42c rephrase logged out user invite accept message to clarify that the
invite was accepted
2026-06-18 13:27:43 +02:00
Gregor Vostrak
f9be92894e add banners for invitation accept 2026-06-18 13:27:43 +02:00
Constantin Graf
a30d192ea2 Updated invitation flow, Moved jetstream function to REST endpoints; Lower case email 2026-06-18 13:27:43 +02:00
18 changed files with 155 additions and 60 deletions

View File

@@ -46,9 +46,9 @@ jobs:
- name: "Get Previous tag (normal push)"
id: previoustag
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
uses: "WyriHaximus/github-action-get-previous-tag@v1"
uses: "WyriHaximus/github-action-get-previous-tag@v2"
with:
prefix: "v"
pattern: "v*[0-9].*[0-9].*[0-9]"
- name: "Get version"
id: release-version
@@ -124,27 +124,27 @@ jobs:
- name: "Docker meta"
id: "meta"
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: |
${{ env.DOCKER_REPO }}
- name: "Login to solidtime OnPremise Registry"
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: registry.on-premise.solidtime.io
username: ${{ secrets.ONPREMISE_USERNAME }}
password: ${{ secrets.ONPREMISE_TOKEN }}
- name: "Set up QEMU"
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: "Set up Docker Buildx"
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: "Build and push by digest"
id: build
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
file: docker/prod/Dockerfile
@@ -163,7 +163,7 @@ jobs:
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: "Upload digest"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
@@ -177,25 +177,25 @@ jobs:
- build
steps:
- name: "Download digests"
uses: actions/download-artifact@v6
uses: actions/download-artifact@v8
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true
- name: "Login to solidtime OnPremise Registry"
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: registry.on-premise.solidtime.io
username: ${{ secrets.ONPREMISE_USERNAME }}
password: ${{ secrets.ONPREMISE_TOKEN }}
- name: "Set up Docker Buildx"
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: "Docker meta"
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: |
${{ env.DOCKER_REPO }}

View File

@@ -33,9 +33,9 @@ jobs:
- name: "Get Previous tag (normal push)"
id: previoustag
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
uses: "WyriHaximus/github-action-get-previous-tag@v1"
uses: "WyriHaximus/github-action-get-previous-tag@v2"
with:
prefix: "v"
pattern: "v*[0-9].*[0-9].*[0-9]"
- name: "Get version"
id: version
@@ -160,7 +160,7 @@ jobs:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: "Login to GitHub Container Registry"
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: rg.fr-par.scw.cloud/solidtime
username: nologin
@@ -168,7 +168,7 @@ jobs:
- name: "Docker meta"
id: "meta"
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: rg.fr-par.scw.cloud/solidtime/solidtime
tags: |
@@ -179,13 +179,13 @@ jobs:
type=sha,format=long
- name: "Set up QEMU"
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: "Set up Docker Buildx"
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: "Build and push"
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
build-args: |

View File

@@ -47,9 +47,9 @@ jobs:
- name: "Get Previous tag (normal push)"
id: previoustag
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
uses: "WyriHaximus/github-action-get-previous-tag@v1"
uses: "WyriHaximus/github-action-get-previous-tag@v2"
with:
prefix: "v"
pattern: "v*[0-9].*[0-9].*[0-9]"
- name: "Get version"
id: release-version
@@ -109,34 +109,34 @@ jobs:
- name: "Docker meta"
id: "meta"
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: |
${{ env.DOCKERHUB_REPO }}
${{ env.GHCR_REPO }}
- name: "Login to Docker Hub Container Registry"
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: "Login to GitHub Container Registry"
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: "Set up QEMU"
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: "Set up Docker Buildx"
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: "Build and push by digest"
id: build
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
file: docker/prod/Dockerfile
@@ -155,7 +155,7 @@ jobs:
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: "Upload digest"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
@@ -169,31 +169,31 @@ jobs:
- build
steps:
- name: "Download digests"
uses: actions/download-artifact@v6
uses: actions/download-artifact@v8
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true
- name: "Login to Docker Hub"
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: "Login to GHCR"
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: "Set up Docker Buildx"
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: "Docker meta"
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: |
${{ env.DOCKERHUB_REPO }}

View File

@@ -52,7 +52,7 @@ jobs:
run: php artisan scramble:export --path=build/api-docs.json
- name: "Upload API docs to GitHub"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: api-docs.json
path: build/api-docs.json

View File

@@ -13,10 +13,10 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: "Use Node.js"
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20.x'

View File

@@ -68,7 +68,7 @@ jobs:
run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml
- name: "Upload coverage reports to Codecov"
uses: codecov/codecov-action@v5.5.1
uses: codecov/codecov-action@v7.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: solidtime-io/solidtime

View File

@@ -86,7 +86,7 @@ jobs:
MAILPIT_BASE_URL: 'http://localhost:8025'
- name: "Upload blob report"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
if: always()
with:
name: blob-report-${{ matrix.shardIndex }}
@@ -102,7 +102,7 @@ jobs:
uses: actions/checkout@v6
- name: "Setup node"
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20.x'
@@ -110,7 +110,7 @@ jobs:
run: npm ci
- name: "Download blob reports"
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
path: all-blob-reports
pattern: blob-report-*
@@ -120,7 +120,7 @@ jobs:
run: npx playwright merge-reports --reporter html ./all-blob-reports
- name: "Upload merged HTML report"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: playwright-report
path: playwright-report/

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Service\Import\Importers;
use Exception;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use League\Csv\Exception as CsvException;
use League\Csv\Reader;
@@ -24,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;
@@ -44,11 +46,12 @@ class ClockifyProjectsImporter extends DefaultImporter
'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,
'archived_at' => $record['Status'] === 'Archived' ? Carbon::now() : null,
]);
}
if ($record['Task'] !== '') {
$tasks = explode(', ', $record['Task']);
if ($record[$tasksKey] !== '') {
$tasks = explode(', ', $record[$tasksKey]);
foreach ($tasks as $task) {
$this->taskImportHelper->getKey([
'name' => $task,
@@ -81,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<string> $header
*/
private function getTasksKey(array $header): string
{
return in_array('Tasks', $header, true) ? 'Tasks' : 'Task';
}
/**

View File

@@ -116,10 +116,12 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
throw new ImportException('Time entry description is too long');
}
$timeEntry->description = $record['Description'];
if (! in_array($record['Billable'], ['Yes', 'No'], true)) {
throw new ImportException('Invalid billable value');
if (isset($record['Billable'])) {
if (! in_array($record['Billable'], ['Yes', 'No'], true)) {
throw new ImportException('Invalid billable value');
}
$timeEntry->billable = $record['Billable'] === 'Yes';
}
$timeEntry->billable = $record['Billable'] === 'Yes';
$timeEntry->tags = $this->getTags($record['Tags']);
$timeEntry->is_imported = true;
@@ -219,7 +221,6 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
'Group',
'Email',
'Tags',
'Billable',
'Start Date',
'Start Time',
'End Date',

View File

@@ -5,7 +5,8 @@ declare(strict_types=1);
return [
'clockify_time_entries' => [
'name' => 'Clockify Time Entries',
'description' => '1. First make sure that you set the Date format to "MM/DD/YYYY" and the Time format to "12-hour" in the user settings.<br>'.
'description' => '<strong>Important:</strong> If you also want to import your projects use the "Clockify Projects" importer before this one, since that export contains more details such as billable status, billable rates and estimated time.<br><br>'.
'1. First make sure that you set the Date format to "MM/DD/YYYY" and the Time format to "12-hour" in the user settings.<br>'.
'2. In the same preferences page change the language of Clockfiy to English.<br>'.
'3. Go to REPORTS -> TIME -> Detailed in the navigation on the left. <br>'.
'4. Now select the date range that you want to export in the right top. '.
@@ -61,7 +62,8 @@ return [
],
'harvest_time_entries' => [
'name' => 'Harvest Time Entries',
'description' => '1. Go to Settings (right top corner)<br>2. Click on "Import/Export" in the left navigation'.
'description' => '<strong>Important:</strong> If you also want to import your projects use the "Harvest Projects" importer before this one, since that export contains more details such as billable status and estimated time.<br><br>'.
'1. Go to Settings (right top corner)<br>2. Click on "Import/Export" in the left navigation'.
'<br>3. Now click on "Export all time" '.
'<br><br>Before you import make sure that the Timezone settings in Harvest are the same as in solidtime.',
],

View File

@@ -43,17 +43,17 @@ onMounted(async () => {
<template v-else>
<UpdateTeamNameForm :team="team" :permissions="permissions" />
<SectionBorder />
<SectionBorder v-if="permissions.canUpdateTeam" />
<OrganizationBillableRate v-if="permissions.canUpdateTeam" :team="team" />
<SectionBorder />
<SectionBorder v-if="permissions.canUpdateTeam" />
<OrganizationFormatSettings v-if="permissions.canUpdateTeam" :team="team" />
<SectionBorder />
<SectionBorder v-if="permissions.canUpdateTeam" />
<OrganizationTimeEntrySettings v-if="permissions.canUpdateTeam" />
<SectionBorder />
<template v-if="permissions.canDeleteTeam">
<SectionBorder />
<DeleteTeamForm class="mt-10 sm:mt-0" :team="team" />
</template>
</template>

View File

@@ -199,7 +199,7 @@ body {
--muted-foreground: var(--color-text-tertiary);
--accent: var(--color-bg-tertiary);
--accent-foreground: var(--color-text-primary);
--destructive: 0 62.8% 30.6%;
--destructive: 0 72% 60%;
--destructive-foreground: var(--color-text-primary);
--border: var(--color-border-primary);
--input: var(--color-border-tertiary);

View File

@@ -0,0 +1,3 @@
"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","",""
1 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
2 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
3 Archived Project Archived Public Yes 0.00 0.00 0.00 0.00 0.00 0.00 0.00 Constantin Graf

View File

@@ -0,0 +1,3 @@
"Project","Client","Description","Task","User","Group","Email","Tags","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","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","","03/04/2024","10:23 AM","03/04/2024","11:23:01 AM","01:00:01","0.00","0.00","0.00"
1 Project Client Description Task User Group Email Tags Start Date Start Time End Date End Time Duration (h) Duration (decimal) Billable Rate (USD) Billable Amount (USD)
2 Project without Client Peter Tester peter.test@email.test Development, Backend 03/04/2024 10:23:52 AM 03/04/2024 10:23:52 AM 00:00:00 0.00 0.00 0.00
3 Project for Big Company Big Company Working hard Task 1 Peter Tester peter.test@email.test 03/04/2024 10:23 AM 03/04/2024 11:23:01 AM 01:00:01 0.00 0.00 0.00

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
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;
@@ -50,4 +52,47 @@ class ClockifyProjectsImporterTest extends ImporterTestAbstract
// Assert
$this->checkTestScenarioProjectsOnlyAfterImport();
}
public function test_import_sets_archived_at_based_on_status_column(): void
{
// Arrange
$organization = Organization::factory()->create();
$timezone = 'Europe/Vienna';
$importer = new ClockifyProjectsImporter;
$importer->init($organization);
$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->assertNull($activeProject->archived_at);
$this->assertFalse($activeProject->is_archived);
$archivedProject = Project::query()->where('organization_id', $organization->id)->where('name', 'Archived Project')->firstOrFail();
$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(),
);
}
}

View File

@@ -41,6 +41,30 @@ class ClockifyTimeEntriesImporterTest extends ImporterTestAbstract
$this->assertSame(1, $report->clientsCreated);
}
public function test_import_of_test_file_without_billable_works_and_defaults_to_non_billable(): void
{
// Arrange
$organization = Organization::factory()->create();
$timezone = 'Europe/Vienna';
$importer = new ClockifyTimeEntriesImporter;
$importer->init($organization);
$data = Storage::disk('testfiles')->get('clockify_time_entries_import_test_4.csv');
// Act
$importer->importData($data, $timezone);
$report = $importer->getReport();
// Assert
$testScenario = $this->checkTestScenarioAfterImportExcludingTimeEntries(false, true);
$this->checkTimeEntries($testScenario, false, true);
$this->assertSame(2, $report->timeEntriesCreated);
$this->assertSame(2, $report->tagsCreated);
$this->assertSame(1, $report->tasksCreated);
$this->assertSame(1, $report->usersCreated);
$this->assertSame(2, $report->projectsCreated);
$this->assertSame(1, $report->clientsCreated);
}
public function test_import_of_test_with_special_characters_description_succeeds(): void
{
// Arrange

View File

@@ -26,7 +26,7 @@ class ImporterTestAbstract extends TestCase
/**
* @return object{user1: User, project1: Project, project2: Project, tag1: Tag, tag2: Tag}
*/
protected function checkTestScenarioAfterImportExcludingTimeEntries(bool $detailed = false): object
protected function checkTestScenarioAfterImportExcludingTimeEntries(bool $detailed = false, bool $billableDefault = false): object
{
$users = User::all();
$this->assertCount(2, $users);
@@ -80,12 +80,12 @@ class ImporterTestAbstract extends TestCase
$this->assertSame('#ef5350', $project1->color);
$this->assertSame(null, $project1->billable_rate);
// Project for Big Company
$this->assertSame(true, $project2->is_billable);
$this->assertSame(! $billableDefault, $project2->is_billable);
$this->assertSame(false, $project2->is_public);
$this->assertSame('#ec407a', $project2->color);
$this->assertSame(10001, $project2->billable_rate);
// Project (Archived)
$this->assertSame(true, $project3->is_billable);
$this->assertSame(! $billableDefault, $project3->is_billable);
$this->assertSame(true, $project3->is_public);
$this->assertSame('#6a407f', $project3->color);
$this->assertSame(null, $project3->billable_rate);
@@ -176,7 +176,7 @@ class ImporterTestAbstract extends TestCase
/**
* @param object{user1: User, project1: Project, project2: Project, tag1: Tag, tag2: Tag} $testScenario
*/
protected function checkTimeEntries(object $testScenario, bool $secondRun = false): void
protected function checkTimeEntries(object $testScenario, bool $secondRun = false, bool $billableDefault = false): void
{
$timeEntries = TimeEntry::all();
if ($secondRun) {
@@ -197,7 +197,7 @@ class ImporterTestAbstract extends TestCase
$this->assertSame('Working hard', $timeEntry2->description);
$this->assertSame('2024-03-04 09:23:00', $timeEntry2->start->toDateTimeString());
$this->assertSame('2024-03-04 10:23:01', $timeEntry2->end->toDateTimeString());
$this->assertTrue($timeEntry2->billable);
$this->assertSame(! $billableDefault, $timeEntry2->billable);
$this->assertTrue($timeEntry2->is_imported);
$this->assertSame([], $timeEntry2->tags);
}

View File

@@ -14,6 +14,7 @@ async function getConfig() {
const additionalPlugins = await collectModulePlugins('extensions');
return defineConfig({
base: './',
build: {
sourcemap: true, // Source map generation must be turned on
},