mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-17 12:42:15 +01:00
Compare commits
59 Commits
feature/us
...
d0f2ee7d7e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0f2ee7d7e | ||
|
|
767eb00f7b | ||
|
|
7e993a9249 | ||
|
|
3b43ccf661 | ||
|
|
37a8f43b23 | ||
|
|
c94aa8038d | ||
|
|
c9e84260b1 | ||
|
|
9a53f903d8 | ||
|
|
d50ce52b5b | ||
|
|
aea310e639 | ||
|
|
a94627321b | ||
|
|
806d138fc9 | ||
|
|
240dd809f4 | ||
|
|
dedb50ea73 | ||
|
|
6fe856fd31 | ||
|
|
20735c2fed | ||
|
|
b14e40c64b | ||
|
|
f2277ff708 | ||
|
|
6a197f7f34 | ||
|
|
24c94af952 | ||
|
|
111ce94150 | ||
|
|
49e69b85a6 | ||
|
|
90a5c2b87c | ||
|
|
6b01034bdd | ||
|
|
a4c400ef4e | ||
|
|
5d01b20dbf | ||
|
|
24875f9424 | ||
|
|
4975cf1dab | ||
|
|
b3fb04c4be | ||
|
|
0cc8d762a5 | ||
|
|
e3e45161a1 | ||
|
|
0c825a691e | ||
|
|
24775d2c47 | ||
|
|
c5c129b239 | ||
|
|
89a9341d91 | ||
|
|
bffd0773be | ||
|
|
56ea0ed90e | ||
|
|
24c19e7e7c | ||
|
|
734aab6353 | ||
|
|
5eb1a6a006 | ||
|
|
d0334bd730 | ||
|
|
fe4e903203 | ||
|
|
43e42ace54 | ||
|
|
84728f0914 | ||
|
|
8f02f98afc | ||
|
|
c1ecd0eff5 | ||
|
|
486b5d2152 | ||
|
|
5476b4e5a9 | ||
|
|
ceec4cbf62 | ||
|
|
372c7b02a0 | ||
|
|
cb8047028c | ||
|
|
e29be581fa | ||
|
|
02da133fb3 | ||
|
|
d00fe2baff | ||
|
|
a73fe59892 | ||
|
|
5d744d70c4 | ||
|
|
12d58ee42c | ||
|
|
f9be92894e | ||
|
|
a30d192ea2 |
24
.github/workflows/build-onpremise.yml
vendored
24
.github/workflows/build-onpremise.yml
vendored
@@ -46,9 +46,9 @@ jobs:
|
|||||||
- name: "Get Previous tag (normal push)"
|
- name: "Get Previous tag (normal push)"
|
||||||
id: previoustag
|
id: previoustag
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
uses: "WyriHaximus/github-action-get-previous-tag@v2"
|
||||||
with:
|
with:
|
||||||
prefix: "v"
|
pattern: "v*[0-9].*[0-9].*[0-9]"
|
||||||
|
|
||||||
- name: "Get version"
|
- name: "Get version"
|
||||||
id: release-version
|
id: release-version
|
||||||
@@ -124,27 +124,27 @@ jobs:
|
|||||||
|
|
||||||
- name: "Docker meta"
|
- name: "Docker meta"
|
||||||
id: "meta"
|
id: "meta"
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.DOCKER_REPO }}
|
${{ env.DOCKER_REPO }}
|
||||||
|
|
||||||
- name: "Login to solidtime OnPremise Registry"
|
- name: "Login to solidtime OnPremise Registry"
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: registry.on-premise.solidtime.io
|
registry: registry.on-premise.solidtime.io
|
||||||
username: ${{ secrets.ONPREMISE_USERNAME }}
|
username: ${{ secrets.ONPREMISE_USERNAME }}
|
||||||
password: ${{ secrets.ONPREMISE_TOKEN }}
|
password: ${{ secrets.ONPREMISE_TOKEN }}
|
||||||
|
|
||||||
- name: "Set up QEMU"
|
- name: "Set up QEMU"
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
- name: "Set up Docker Buildx"
|
- name: "Set up Docker Buildx"
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: "Build and push by digest"
|
- name: "Build and push by digest"
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/prod/Dockerfile
|
file: docker/prod/Dockerfile
|
||||||
@@ -163,7 +163,7 @@ jobs:
|
|||||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
- name: "Upload digest"
|
- name: "Upload digest"
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: digests-${{ env.PLATFORM_PAIR }}
|
name: digests-${{ env.PLATFORM_PAIR }}
|
||||||
path: ${{ runner.temp }}/digests/*
|
path: ${{ runner.temp }}/digests/*
|
||||||
@@ -177,25 +177,25 @@ jobs:
|
|||||||
- build
|
- build
|
||||||
steps:
|
steps:
|
||||||
- name: "Download digests"
|
- name: "Download digests"
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.temp }}/digests
|
path: ${{ runner.temp }}/digests
|
||||||
pattern: digests-*
|
pattern: digests-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: "Login to solidtime OnPremise Registry"
|
- name: "Login to solidtime OnPremise Registry"
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: registry.on-premise.solidtime.io
|
registry: registry.on-premise.solidtime.io
|
||||||
username: ${{ secrets.ONPREMISE_USERNAME }}
|
username: ${{ secrets.ONPREMISE_USERNAME }}
|
||||||
password: ${{ secrets.ONPREMISE_TOKEN }}
|
password: ${{ secrets.ONPREMISE_TOKEN }}
|
||||||
|
|
||||||
- name: "Set up Docker Buildx"
|
- name: "Set up Docker Buildx"
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: "Docker meta"
|
- name: "Docker meta"
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.DOCKER_REPO }}
|
${{ env.DOCKER_REPO }}
|
||||||
|
|||||||
14
.github/workflows/build-private.yml
vendored
14
.github/workflows/build-private.yml
vendored
@@ -33,9 +33,9 @@ jobs:
|
|||||||
- name: "Get Previous tag (normal push)"
|
- name: "Get Previous tag (normal push)"
|
||||||
id: previoustag
|
id: previoustag
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
uses: "WyriHaximus/github-action-get-previous-tag@v2"
|
||||||
with:
|
with:
|
||||||
prefix: "v"
|
pattern: "v*[0-9].*[0-9].*[0-9]"
|
||||||
|
|
||||||
- name: "Get version"
|
- name: "Get version"
|
||||||
id: version
|
id: version
|
||||||
@@ -160,7 +160,7 @@ jobs:
|
|||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
|
||||||
- name: "Login to GitHub Container Registry"
|
- name: "Login to GitHub Container Registry"
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: rg.fr-par.scw.cloud/solidtime
|
registry: rg.fr-par.scw.cloud/solidtime
|
||||||
username: nologin
|
username: nologin
|
||||||
@@ -168,7 +168,7 @@ jobs:
|
|||||||
|
|
||||||
- name: "Docker meta"
|
- name: "Docker meta"
|
||||||
id: "meta"
|
id: "meta"
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: rg.fr-par.scw.cloud/solidtime/solidtime
|
images: rg.fr-par.scw.cloud/solidtime/solidtime
|
||||||
tags: |
|
tags: |
|
||||||
@@ -179,13 +179,13 @@ jobs:
|
|||||||
type=sha,format=long
|
type=sha,format=long
|
||||||
|
|
||||||
- name: "Set up QEMU"
|
- name: "Set up QEMU"
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
- name: "Set up Docker Buildx"
|
- name: "Set up Docker Buildx"
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: "Build and push"
|
- name: "Build and push"
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
build-args: |
|
build-args: |
|
||||||
|
|||||||
28
.github/workflows/build-public.yml
vendored
28
.github/workflows/build-public.yml
vendored
@@ -47,9 +47,9 @@ jobs:
|
|||||||
- name: "Get Previous tag (normal push)"
|
- name: "Get Previous tag (normal push)"
|
||||||
id: previoustag
|
id: previoustag
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
uses: "WyriHaximus/github-action-get-previous-tag@v2"
|
||||||
with:
|
with:
|
||||||
prefix: "v"
|
pattern: "v*[0-9].*[0-9].*[0-9]"
|
||||||
|
|
||||||
- name: "Get version"
|
- name: "Get version"
|
||||||
id: release-version
|
id: release-version
|
||||||
@@ -109,34 +109,34 @@ jobs:
|
|||||||
|
|
||||||
- name: "Docker meta"
|
- name: "Docker meta"
|
||||||
id: "meta"
|
id: "meta"
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.DOCKERHUB_REPO }}
|
${{ env.DOCKERHUB_REPO }}
|
||||||
${{ env.GHCR_REPO }}
|
${{ env.GHCR_REPO }}
|
||||||
|
|
||||||
- name: "Login to Docker Hub Container Registry"
|
- name: "Login to Docker Hub Container Registry"
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: "Login to GitHub Container Registry"
|
- name: "Login to GitHub Container Registry"
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: "Set up QEMU"
|
- name: "Set up QEMU"
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
- name: "Set up Docker Buildx"
|
- name: "Set up Docker Buildx"
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: "Build and push by digest"
|
- name: "Build and push by digest"
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/prod/Dockerfile
|
file: docker/prod/Dockerfile
|
||||||
@@ -155,7 +155,7 @@ jobs:
|
|||||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
- name: "Upload digest"
|
- name: "Upload digest"
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: digests-${{ env.PLATFORM_PAIR }}
|
name: digests-${{ env.PLATFORM_PAIR }}
|
||||||
path: ${{ runner.temp }}/digests/*
|
path: ${{ runner.temp }}/digests/*
|
||||||
@@ -169,31 +169,31 @@ jobs:
|
|||||||
- build
|
- build
|
||||||
steps:
|
steps:
|
||||||
- name: "Download digests"
|
- name: "Download digests"
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.temp }}/digests
|
path: ${{ runner.temp }}/digests
|
||||||
pattern: digests-*
|
pattern: digests-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: "Login to Docker Hub"
|
- name: "Login to Docker Hub"
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: "Login to GHCR"
|
- name: "Login to GHCR"
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: "Set up Docker Buildx"
|
- name: "Set up Docker Buildx"
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: "Docker meta"
|
- name: "Docker meta"
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.DOCKERHUB_REPO }}
|
${{ env.DOCKERHUB_REPO }}
|
||||||
|
|||||||
2
.github/workflows/generate-api-docs.yml
vendored
2
.github/workflows/generate-api-docs.yml
vendored
@@ -52,7 +52,7 @@ jobs:
|
|||||||
run: php artisan scramble:export --path=build/api-docs.json
|
run: php artisan scramble:export --path=build/api-docs.json
|
||||||
|
|
||||||
- name: "Upload API docs to GitHub"
|
- name: "Upload API docs to GitHub"
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: api-docs.json
|
name: api-docs.json
|
||||||
path: build/api-docs.json
|
path: build/api-docs.json
|
||||||
|
|||||||
4
.github/workflows/npm-test-unit.yml
vendored
4
.github/workflows/npm-test-unit.yml
vendored
@@ -13,10 +13,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: "Use Node.js"
|
- name: "Use Node.js"
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/phpunit.yml
vendored
2
.github/workflows/phpunit.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
|||||||
run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml
|
run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml
|
||||||
|
|
||||||
- name: "Upload coverage reports to Codecov"
|
- name: "Upload coverage reports to Codecov"
|
||||||
uses: codecov/codecov-action@v5.5.1
|
uses: codecov/codecov-action@v7.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
slug: solidtime-io/solidtime
|
slug: solidtime-io/solidtime
|
||||||
|
|||||||
8
.github/workflows/playwright.yml
vendored
8
.github/workflows/playwright.yml
vendored
@@ -86,7 +86,7 @@ jobs:
|
|||||||
MAILPIT_BASE_URL: 'http://localhost:8025'
|
MAILPIT_BASE_URL: 'http://localhost:8025'
|
||||||
|
|
||||||
- name: "Upload blob report"
|
- name: "Upload blob report"
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: blob-report-${{ matrix.shardIndex }}
|
name: blob-report-${{ matrix.shardIndex }}
|
||||||
@@ -102,7 +102,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: "Setup node"
|
- name: "Setup node"
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ jobs:
|
|||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: "Download blob reports"
|
- name: "Download blob reports"
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
path: all-blob-reports
|
path: all-blob-reports
|
||||||
pattern: blob-report-*
|
pattern: blob-report-*
|
||||||
@@ -120,7 +120,7 @@ jobs:
|
|||||||
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
||||||
|
|
||||||
- name: "Upload merged HTML report"
|
- name: "Upload merged HTML report"
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: playwright-report
|
name: playwright-report
|
||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
|
|||||||
16
app/Enums/TagMatchType.php
Normal file
16
app/Enums/TagMatchType.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
||||||
|
|
||||||
|
enum TagMatchType: string
|
||||||
|
{
|
||||||
|
use LaravelEnumHelper;
|
||||||
|
|
||||||
|
case Contains = 'contains';
|
||||||
|
|
||||||
|
case NotContains = 'not_contains';
|
||||||
|
}
|
||||||
@@ -59,7 +59,7 @@ class ReportController extends Controller
|
|||||||
$filter->addBillable($properties->billable);
|
$filter->addBillable($properties->billable);
|
||||||
$filter->addMemberIdsFilter($properties->memberIds?->toArray());
|
$filter->addMemberIdsFilter($properties->memberIds?->toArray());
|
||||||
$filter->addProjectIdsFilter($properties->projectIds?->toArray());
|
$filter->addProjectIdsFilter($properties->projectIds?->toArray());
|
||||||
$filter->addTagIdsFilter($properties->tagIds?->toArray());
|
$filter->addTagIdsFilter($properties->tagIds?->toArray(), $properties->tagMatchType);
|
||||||
$filter->addTaskIdsFilter($properties->taskIds?->toArray());
|
$filter->addTaskIdsFilter($properties->taskIds?->toArray());
|
||||||
$filter->addClientIdsFilter($properties->clientIds?->toArray());
|
$filter->addClientIdsFilter($properties->clientIds?->toArray());
|
||||||
$timeEntriesQuery = $filter->get();
|
$timeEntriesQuery = $filter->get();
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ class ReportController extends Controller
|
|||||||
$properties->setClientIds($request->input('properties.client_ids', null));
|
$properties->setClientIds($request->input('properties.client_ids', null));
|
||||||
$properties->setProjectIds($request->input('properties.project_ids', null));
|
$properties->setProjectIds($request->input('properties.project_ids', null));
|
||||||
$properties->setTagIds($request->input('properties.tag_ids', null));
|
$properties->setTagIds($request->input('properties.tag_ids', null));
|
||||||
|
$properties->setTagMatchType($request->getPropertyTagMatchType());
|
||||||
$properties->setTaskIds($request->input('properties.task_ids', null));
|
$properties->setTaskIds($request->input('properties.task_ids', null));
|
||||||
$properties->weekStart = $request->has('properties.week_start') ? Weekday::from($request->input('properties.week_start')) : $user->week_start;
|
$properties->weekStart = $request->has('properties.week_start') ? Weekday::from($request->input('properties.week_start')) : $user->week_start;
|
||||||
$timezone = $user->timezone;
|
$timezone = $user->timezone;
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ class TimeEntryController extends Controller
|
|||||||
$filter->addMemberIdFilter($member);
|
$filter->addMemberIdFilter($member);
|
||||||
$filter->addMemberIdsFilter($request->input('member_ids'));
|
$filter->addMemberIdsFilter($request->input('member_ids'));
|
||||||
$filter->addProjectIdsFilter($request->input('project_ids'));
|
$filter->addProjectIdsFilter($request->input('project_ids'));
|
||||||
$filter->addTagIdsFilter($request->input('tag_ids'));
|
$filter->addTagIdsFilter($request->input('tag_ids'), $request->getTagMatchType());
|
||||||
$filter->addTaskIdsFilter($request->input('task_ids'));
|
$filter->addTaskIdsFilter($request->input('task_ids'));
|
||||||
$filter->addClientIdsFilter($request->input('client_ids'));
|
$filter->addClientIdsFilter($request->input('client_ids'));
|
||||||
$filter->addBillableFilter($request->input('billable'));
|
$filter->addBillableFilter($request->input('billable'));
|
||||||
@@ -559,7 +559,7 @@ class TimeEntryController extends Controller
|
|||||||
$filter->addMemberIdFilter($member);
|
$filter->addMemberIdFilter($member);
|
||||||
$filter->addMemberIdsFilter($request->input('member_ids'));
|
$filter->addMemberIdsFilter($request->input('member_ids'));
|
||||||
$filter->addProjectIdsFilter($request->input('project_ids'));
|
$filter->addProjectIdsFilter($request->input('project_ids'));
|
||||||
$filter->addTagIdsFilter($request->input('tag_ids'));
|
$filter->addTagIdsFilter($request->input('tag_ids'), $request->getTagMatchType());
|
||||||
$filter->addTaskIdsFilter($request->input('task_ids'));
|
$filter->addTaskIdsFilter($request->input('task_ids'));
|
||||||
$filter->addClientIdsFilter($request->input('client_ids'));
|
$filter->addClientIdsFilter($request->input('client_ids'));
|
||||||
$filter->addBillableFilter($request->input('billable'));
|
$filter->addBillableFilter($request->input('billable'));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\Report;
|
namespace App\Http\Requests\V1\Report;
|
||||||
|
|
||||||
|
use App\Enums\TagMatchType;
|
||||||
use App\Enums\TimeEntryAggregationType;
|
use App\Enums\TimeEntryAggregationType;
|
||||||
use App\Enums\TimeEntryAggregationTypeInterval;
|
use App\Enums\TimeEntryAggregationTypeInterval;
|
||||||
use App\Enums\TimeEntryRoundingType;
|
use App\Enums\TimeEntryRoundingType;
|
||||||
@@ -124,6 +125,11 @@ class ReportStoreRequest extends BaseFormRequest
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
'properties.tag_match_type' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
Rule::enum(TagMatchType::class),
|
||||||
|
],
|
||||||
'properties.task_ids' => [
|
'properties.task_ids' => [
|
||||||
'nullable',
|
'nullable',
|
||||||
'array',
|
'array',
|
||||||
@@ -249,6 +255,15 @@ class ReportStoreRequest extends BaseFormRequest
|
|||||||
return TimeEntryAggregationTypeInterval::from($this->input('properties.history_group'));
|
return TimeEntryAggregationTypeInterval::from($this->input('properties.history_group'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getPropertyTagMatchType(): ?TagMatchType
|
||||||
|
{
|
||||||
|
if (! $this->has('properties.tag_match_type') || $this->input('properties.tag_match_type') === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TagMatchType::from($this->input('properties.tag_match_type'));
|
||||||
|
}
|
||||||
|
|
||||||
public function getPropertyRoundingType(): ?TimeEntryRoundingType
|
public function getPropertyRoundingType(): ?TimeEntryRoundingType
|
||||||
{
|
{
|
||||||
if (! $this->has('properties.rounding_type') || $this->input('properties.rounding_type') === null) {
|
if (! $this->has('properties.rounding_type') || $this->input('properties.rounding_type') === null) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Http\Requests\V1\TimeEntry;
|
namespace App\Http\Requests\V1\TimeEntry;
|
||||||
|
|
||||||
use App\Enums\ExportFormat;
|
use App\Enums\ExportFormat;
|
||||||
|
use App\Enums\TagMatchType;
|
||||||
use App\Enums\TimeEntryAggregationType;
|
use App\Enums\TimeEntryAggregationType;
|
||||||
use App\Enums\TimeEntryAggregationTypeInterval;
|
use App\Enums\TimeEntryAggregationTypeInterval;
|
||||||
use App\Enums\TimeEntryRoundingType;
|
use App\Enums\TimeEntryRoundingType;
|
||||||
@@ -139,6 +140,10 @@ class TimeEntryAggregateExportRequest extends BaseFormRequest
|
|||||||
})->uuid()->validate($attribute, $value, $fail);
|
})->uuid()->validate($attribute, $value, $fail);
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
'tag_match_type' => [
|
||||||
|
'string',
|
||||||
|
Rule::enum(TagMatchType::class),
|
||||||
|
],
|
||||||
// Filter by task IDs, task IDs are OR combined
|
// Filter by task IDs, task IDs are OR combined
|
||||||
'task_ids' => [
|
'task_ids' => [
|
||||||
'array',
|
'array',
|
||||||
@@ -246,6 +251,15 @@ class TimeEntryAggregateExportRequest extends BaseFormRequest
|
|||||||
return ExportFormat::from($this->validated('format'));
|
return ExportFormat::from($this->validated('format'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTagMatchType(): ?TagMatchType
|
||||||
|
{
|
||||||
|
if (! $this->has('tag_match_type') || $this->validated('tag_match_type') === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TagMatchType::from($this->validated('tag_match_type'));
|
||||||
|
}
|
||||||
|
|
||||||
public function getRoundingType(): ?TimeEntryRoundingType
|
public function getRoundingType(): ?TimeEntryRoundingType
|
||||||
{
|
{
|
||||||
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
|
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\TimeEntry;
|
namespace App\Http\Requests\V1\TimeEntry;
|
||||||
|
|
||||||
|
use App\Enums\TagMatchType;
|
||||||
use App\Enums\TimeEntryAggregationType;
|
use App\Enums\TimeEntryAggregationType;
|
||||||
use App\Enums\TimeEntryRoundingType;
|
use App\Enums\TimeEntryRoundingType;
|
||||||
use App\Http\Requests\V1\BaseFormRequest;
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
@@ -125,6 +126,10 @@ class TimeEntryAggregateRequest extends BaseFormRequest
|
|||||||
})->uuid()->validate($attribute, $value, $fail);
|
})->uuid()->validate($attribute, $value, $fail);
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
'tag_match_type' => [
|
||||||
|
'string',
|
||||||
|
Rule::enum(TagMatchType::class),
|
||||||
|
],
|
||||||
// Filter by task IDs, task IDs are OR combined
|
// Filter by task IDs, task IDs are OR combined
|
||||||
'task_ids' => [
|
'task_ids' => [
|
||||||
'array',
|
'array',
|
||||||
@@ -208,6 +213,15 @@ class TimeEntryAggregateRequest extends BaseFormRequest
|
|||||||
return $this->input('end') !== null ? Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('end'), 'UTC') : null;
|
return $this->input('end') !== null ? Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('end'), 'UTC') : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTagMatchType(): ?TagMatchType
|
||||||
|
{
|
||||||
|
if (! $this->has('tag_match_type') || $this->validated('tag_match_type') === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TagMatchType::from($this->validated('tag_match_type'));
|
||||||
|
}
|
||||||
|
|
||||||
public function getRoundingType(): ?TimeEntryRoundingType
|
public function getRoundingType(): ?TimeEntryRoundingType
|
||||||
{
|
{
|
||||||
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
|
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Http\Requests\V1\TimeEntry;
|
namespace App\Http\Requests\V1\TimeEntry;
|
||||||
|
|
||||||
use App\Enums\ExportFormat;
|
use App\Enums\ExportFormat;
|
||||||
|
use App\Enums\TagMatchType;
|
||||||
use App\Enums\TimeEntryRoundingType;
|
use App\Enums\TimeEntryRoundingType;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
@@ -110,6 +111,10 @@ class TimeEntryIndexExportRequest extends TimeEntryIndexRequest
|
|||||||
})->uuid()->validate($attribute, $value, $fail);
|
})->uuid()->validate($attribute, $value, $fail);
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
'tag_match_type' => [
|
||||||
|
'string',
|
||||||
|
Rule::enum(TagMatchType::class),
|
||||||
|
],
|
||||||
// Filter by task IDs, task IDs are OR combined
|
// Filter by task IDs, task IDs are OR combined
|
||||||
'task_ids' => [
|
'task_ids' => [
|
||||||
'array',
|
'array',
|
||||||
@@ -215,6 +220,15 @@ class TimeEntryIndexExportRequest extends TimeEntryIndexRequest
|
|||||||
return ExportFormat::from($this->validated('format'));
|
return ExportFormat::from($this->validated('format'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTagMatchType(): ?TagMatchType
|
||||||
|
{
|
||||||
|
if (! $this->has('tag_match_type') || $this->validated('tag_match_type') === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TagMatchType::from($this->validated('tag_match_type'));
|
||||||
|
}
|
||||||
|
|
||||||
public function getRoundingType(): ?TimeEntryRoundingType
|
public function getRoundingType(): ?TimeEntryRoundingType
|
||||||
{
|
{
|
||||||
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
|
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\TimeEntry;
|
namespace App\Http\Requests\V1\TimeEntry;
|
||||||
|
|
||||||
|
use App\Enums\TagMatchType;
|
||||||
use App\Enums\TimeEntryRoundingType;
|
use App\Enums\TimeEntryRoundingType;
|
||||||
use App\Http\Requests\V1\BaseFormRequest;
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
@@ -103,6 +104,10 @@ class TimeEntryIndexRequest extends BaseFormRequest
|
|||||||
})->uuid()->validate($attribute, $value, $fail);
|
})->uuid()->validate($attribute, $value, $fail);
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
'tag_match_type' => [
|
||||||
|
'string',
|
||||||
|
Rule::enum(TagMatchType::class),
|
||||||
|
],
|
||||||
// Filter by task IDs, task IDs are OR combined
|
// Filter by task IDs, task IDs are OR combined
|
||||||
'task_ids' => [
|
'task_ids' => [
|
||||||
'array',
|
'array',
|
||||||
@@ -190,6 +195,15 @@ class TimeEntryIndexRequest extends BaseFormRequest
|
|||||||
return $this->has('offset') ? (int) $this->validated('offset', 0) : 0;
|
return $this->has('offset') ? (int) $this->validated('offset', 0) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTagMatchType(): ?TagMatchType
|
||||||
|
{
|
||||||
|
if (! $this->has('tag_match_type') || $this->validated('tag_match_type') === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TagMatchType::from($this->validated('tag_match_type'));
|
||||||
|
}
|
||||||
|
|
||||||
public function getRoundingType(): ?TimeEntryRoundingType
|
public function getRoundingType(): ?TimeEntryRoundingType
|
||||||
{
|
{
|
||||||
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
|
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ class DetailedReportResource extends BaseResource
|
|||||||
'project_ids' => $this->resource->properties->projectIds?->toArray(),
|
'project_ids' => $this->resource->properties->projectIds?->toArray(),
|
||||||
/** @var array<string>|null $tags_ids Filter by tag IDs, tag IDs are OR combined */
|
/** @var array<string>|null $tags_ids Filter by tag IDs, tag IDs are OR combined */
|
||||||
'tag_ids' => $this->resource->properties->tagIds?->toArray(),
|
'tag_ids' => $this->resource->properties->tagIds?->toArray(),
|
||||||
|
/** @var string|null $tag_match_type Tag match type */
|
||||||
|
'tag_match_type' => $this->resource->properties->tagMatchType?->value,
|
||||||
/** @var array<string>|null $task_ids Filter by task IDs, task IDs are OR combined */
|
/** @var array<string>|null $task_ids Filter by task IDs, task IDs are OR combined */
|
||||||
'task_ids' => $this->resource->properties->taskIds?->toArray(),
|
'task_ids' => $this->resource->properties->taskIds?->toArray(),
|
||||||
/** @var string|null $rounding_type Rounding type for time entries */
|
/** @var string|null $rounding_type Rounding type for time entries */
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Service\Dto;
|
namespace App\Service\Dto;
|
||||||
|
|
||||||
|
use App\Enums\TagMatchType;
|
||||||
use App\Enums\TimeEntryAggregationType;
|
use App\Enums\TimeEntryAggregationType;
|
||||||
use App\Enums\TimeEntryAggregationTypeInterval;
|
use App\Enums\TimeEntryAggregationTypeInterval;
|
||||||
use App\Enums\TimeEntryRoundingType;
|
use App\Enums\TimeEntryRoundingType;
|
||||||
@@ -56,6 +57,8 @@ class ReportPropertiesDto implements Castable
|
|||||||
*/
|
*/
|
||||||
public ?Collection $tagIds = null;
|
public ?Collection $tagIds = null;
|
||||||
|
|
||||||
|
public ?TagMatchType $tagMatchType = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, string>|null
|
* @var Collection<int, string>|null
|
||||||
*/
|
*/
|
||||||
@@ -115,6 +118,7 @@ class ReportPropertiesDto implements Castable
|
|||||||
$dto->clientIds = $data->clientIds !== null ? ReportPropertiesDto::idArrayToCollection($data->clientIds) : null;
|
$dto->clientIds = $data->clientIds !== null ? ReportPropertiesDto::idArrayToCollection($data->clientIds) : null;
|
||||||
$dto->projectIds = $data->projectIds !== null ? ReportPropertiesDto::idArrayToCollection($data->projectIds) : null;
|
$dto->projectIds = $data->projectIds !== null ? ReportPropertiesDto::idArrayToCollection($data->projectIds) : null;
|
||||||
$dto->tagIds = $data->tagIds !== null ? ReportPropertiesDto::idArrayToCollection($data->tagIds) : null;
|
$dto->tagIds = $data->tagIds !== null ? ReportPropertiesDto::idArrayToCollection($data->tagIds) : null;
|
||||||
|
$dto->tagMatchType = isset($data->tagMatchType) ? TagMatchType::from($data->tagMatchType) : null;
|
||||||
$dto->taskIds = $data->taskIds ? ReportPropertiesDto::idArrayToCollection($data->taskIds) : null;
|
$dto->taskIds = $data->taskIds ? ReportPropertiesDto::idArrayToCollection($data->taskIds) : null;
|
||||||
$dto->group = TimeEntryAggregationType::from($data->group);
|
$dto->group = TimeEntryAggregationType::from($data->group);
|
||||||
$dto->subGroup = TimeEntryAggregationType::from($data->subGroup);
|
$dto->subGroup = TimeEntryAggregationType::from($data->subGroup);
|
||||||
@@ -144,6 +148,7 @@ class ReportPropertiesDto implements Castable
|
|||||||
'clientIds' => $value->clientIds?->toArray(),
|
'clientIds' => $value->clientIds?->toArray(),
|
||||||
'projectIds' => $value->projectIds?->toArray(),
|
'projectIds' => $value->projectIds?->toArray(),
|
||||||
'tagIds' => $value->tagIds?->toArray(),
|
'tagIds' => $value->tagIds?->toArray(),
|
||||||
|
'tagMatchType' => $value->tagMatchType?->value,
|
||||||
'taskIds' => $value->taskIds?->toArray(),
|
'taskIds' => $value->taskIds?->toArray(),
|
||||||
'group' => $value->group->value,
|
'group' => $value->group->value,
|
||||||
'subGroup' => $value->subGroup->value,
|
'subGroup' => $value->subGroup->value,
|
||||||
@@ -216,6 +221,11 @@ class ReportPropertiesDto implements Castable
|
|||||||
$this->tagIds = $tagIds !== null ? ReportPropertiesDto::idArrayToCollection($tagIds) : null;
|
$this->tagIds = $tagIds !== null ? ReportPropertiesDto::idArrayToCollection($tagIds) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setTagMatchType(?TagMatchType $tagMatchType): void
|
||||||
|
{
|
||||||
|
$this->tagMatchType = $tagMatchType;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<mixed>|null $taskIds
|
* @param array<mixed>|null $taskIds
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -116,10 +116,12 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
|||||||
throw new ImportException('Time entry description is too long');
|
throw new ImportException('Time entry description is too long');
|
||||||
}
|
}
|
||||||
$timeEntry->description = $record['Description'];
|
$timeEntry->description = $record['Description'];
|
||||||
if (! in_array($record['Billable'], ['Yes', 'No'], true)) {
|
if (isset($record['Billable'])) {
|
||||||
throw new ImportException('Invalid billable value');
|
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->tags = $this->getTags($record['Tags']);
|
||||||
$timeEntry->is_imported = true;
|
$timeEntry->is_imported = true;
|
||||||
|
|
||||||
@@ -219,7 +221,6 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
|||||||
'Group',
|
'Group',
|
||||||
'Email',
|
'Email',
|
||||||
'Tags',
|
'Tags',
|
||||||
'Billable',
|
|
||||||
'Start Date',
|
'Start Date',
|
||||||
'Start Time',
|
'Start Time',
|
||||||
'End Date',
|
'End Date',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Service;
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Enums\TagMatchType;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use App\Models\TimeEntry;
|
use App\Models\TimeEntry;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
@@ -192,15 +193,21 @@ class TimeEntryFilter
|
|||||||
/**
|
/**
|
||||||
* @param array<string>|null $tagIds
|
* @param array<string>|null $tagIds
|
||||||
*/
|
*/
|
||||||
public function addTagIdsFilter(?array $tagIds): self
|
public function addTagIdsFilter(?array $tagIds, ?TagMatchType $tagMatchType = TagMatchType::Contains): self
|
||||||
{
|
{
|
||||||
if ($tagIds === null) {
|
if ($tagIds === null) {
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
$tagMatchType ??= TagMatchType::Contains;
|
||||||
$includeNone = in_array(self::NONE_VALUE, $tagIds, true);
|
$includeNone = in_array(self::NONE_VALUE, $tagIds, true);
|
||||||
$tagIds = array_values(array_filter($tagIds, fn (string $id): bool => $id !== self::NONE_VALUE));
|
$tagIds = array_values(array_filter($tagIds, fn (string $id): bool => $id !== self::NONE_VALUE));
|
||||||
|
// An empty selection (no tag IDs and not filtering for "none") is no constraint, so apply nothing.
|
||||||
|
// This also prevents the not-contains branch from collapsing into "only entries with null tags".
|
||||||
|
if (count($tagIds) === 0 && ! $includeNone) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
$this->builder->where(function (Builder $builder) use ($tagIds, $includeNone): void {
|
$tagCondition = function (Builder $builder) use ($tagIds, $includeNone): void {
|
||||||
foreach ($tagIds as $tagId) {
|
foreach ($tagIds as $tagId) {
|
||||||
$builder->orWhereJsonContains('tags', $tagId);
|
$builder->orWhereJsonContains('tags', $tagId);
|
||||||
}
|
}
|
||||||
@@ -209,7 +216,18 @@ class TimeEntryFilter
|
|||||||
$query->whereJsonLength('tags', 0)->orWhereNull('tags');
|
$query->whereJsonLength('tags', 0)->orWhereNull('tags');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if ($tagMatchType === TagMatchType::NotContains) {
|
||||||
|
$this->builder->where(function (Builder $builder) use ($tagCondition, $includeNone): void {
|
||||||
|
$builder->whereNot($tagCondition);
|
||||||
|
if (! $includeNone) {
|
||||||
|
$builder->orWhereNull('tags');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$this->builder->where($tagCondition);
|
||||||
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import DateRangePicker from '@/packages/ui/src/Input/DateRangePicker.vue';
|
|||||||
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
|
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
|
||||||
import { useTagsQuery } from '@/utils/useTagsQuery';
|
import { useTagsQuery } from '@/utils/useTagsQuery';
|
||||||
import { useTagsStore } from '@/utils/useTags';
|
import { useTagsStore } from '@/utils/useTags';
|
||||||
|
import type { TagMatchType } from '@/types/reporting';
|
||||||
|
|
||||||
type TimeEntryRoundingType = 'up' | 'down' | 'nearest';
|
type TimeEntryRoundingType = 'up' | 'down' | 'nearest';
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ const selectedProjects = defineModel<string[]>('selectedProjects', { required: t
|
|||||||
const selectedTasks = defineModel<string[]>('selectedTasks', { required: true });
|
const selectedTasks = defineModel<string[]>('selectedTasks', { required: true });
|
||||||
const selectedClients = defineModel<string[]>('selectedClients', { required: true });
|
const selectedClients = defineModel<string[]>('selectedClients', { required: true });
|
||||||
const selectedTags = defineModel<string[]>('selectedTags', { required: true });
|
const selectedTags = defineModel<string[]>('selectedTags', { required: true });
|
||||||
|
const tagMatchType = defineModel<TagMatchType>('tagMatchType', { required: true });
|
||||||
const billable = defineModel<'true' | 'false' | null>('billable', { required: true });
|
const billable = defineModel<'true' | 'false' | null>('billable', { required: true });
|
||||||
const roundingEnabled = defineModel<boolean>('roundingEnabled', { required: true });
|
const roundingEnabled = defineModel<boolean>('roundingEnabled', { required: true });
|
||||||
const roundingType = defineModel<TimeEntryRoundingType>('roundingType', { required: true });
|
const roundingType = defineModel<TimeEntryRoundingType>('roundingType', { required: true });
|
||||||
@@ -93,6 +95,44 @@ async function createTag(name: string) {
|
|||||||
title="Tags"
|
title="Tags"
|
||||||
:icon="TagIcon" />
|
:icon="TagIcon" />
|
||||||
</template>
|
</template>
|
||||||
|
<template #content-before-list>
|
||||||
|
<div class="mt-2 border-b border-card-background-separator pb-2">
|
||||||
|
<div
|
||||||
|
class="mb-1.5 px-2 text-xs font-medium text-text-tertiary uppercase">
|
||||||
|
Match
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full rounded-md px-2 py-1.5 text-left text-sm font-medium"
|
||||||
|
:class="
|
||||||
|
tagMatchType === 'contains'
|
||||||
|
? 'bg-card-background-active text-text-primary'
|
||||||
|
: 'text-text-secondary hover:bg-card-background-active'
|
||||||
|
"
|
||||||
|
@click="
|
||||||
|
tagMatchType = 'contains';
|
||||||
|
emit('submit');
|
||||||
|
">
|
||||||
|
Contains
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full rounded-md px-2 py-1.5 text-left text-sm font-medium"
|
||||||
|
:class="
|
||||||
|
tagMatchType === 'not_contains'
|
||||||
|
? 'bg-card-background-active text-text-primary'
|
||||||
|
: 'text-text-secondary hover:bg-card-background-active'
|
||||||
|
"
|
||||||
|
@click="
|
||||||
|
tagMatchType = 'not_contains';
|
||||||
|
emit('submit');
|
||||||
|
">
|
||||||
|
Does Not Contain
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</TagDropdown>
|
</TagDropdown>
|
||||||
|
|
||||||
<Select v-model="billable" @update:model-value="emit('submit')">
|
<Select v-model="billable" @update:model-value="emit('submit')">
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import type { ExportFormat } from '@/types/reporting';
|
|||||||
import { getRandomColorWithSeed } from '@/packages/ui/src/utils/color';
|
import { getRandomColorWithSeed } from '@/packages/ui/src/utils/color';
|
||||||
import { useProjectsQuery } from '@/utils/useProjectsQuery';
|
import { useProjectsQuery } from '@/utils/useProjectsQuery';
|
||||||
import { useAggregatedTimeEntriesQuery } from '@/utils/useAggregatedTimeEntriesQuery';
|
import { useAggregatedTimeEntriesQuery } from '@/utils/useAggregatedTimeEntriesQuery';
|
||||||
|
import type { TagMatchType } from '@/types/reporting';
|
||||||
|
|
||||||
type TimeEntryRoundingType = 'up' | 'down' | 'nearest';
|
type TimeEntryRoundingType = 'up' | 'down' | 'nearest';
|
||||||
|
|
||||||
@@ -67,6 +68,7 @@ const selectedProjects = ref<string[]>([]);
|
|||||||
const selectedMembers = ref<string[]>([]);
|
const selectedMembers = ref<string[]>([]);
|
||||||
const selectedTasks = ref<string[]>([]);
|
const selectedTasks = ref<string[]>([]);
|
||||||
const selectedClients = ref<string[]>([]);
|
const selectedClients = ref<string[]>([]);
|
||||||
|
const tagMatchType = ref<TagMatchType>('contains');
|
||||||
|
|
||||||
const billable = ref<'true' | 'false' | null>(null);
|
const billable = ref<'true' | 'false' | null>(null);
|
||||||
const roundingEnabled = ref<boolean>(false);
|
const roundingEnabled = ref<boolean>(false);
|
||||||
@@ -122,6 +124,7 @@ const filterParams = computed<AggregatedTimeEntriesQueryParams>(() => {
|
|||||||
task_ids: selectedTasks.value.length > 0 ? selectedTasks.value : undefined,
|
task_ids: selectedTasks.value.length > 0 ? selectedTasks.value : undefined,
|
||||||
client_ids: selectedClients.value.length > 0 ? selectedClients.value : undefined,
|
client_ids: selectedClients.value.length > 0 ? selectedClients.value : undefined,
|
||||||
tag_ids: selectedTags.value.length > 0 ? selectedTags.value : undefined,
|
tag_ids: selectedTags.value.length > 0 ? selectedTags.value : undefined,
|
||||||
|
tag_match_type: selectedTags.value.length > 0 ? tagMatchType.value : undefined,
|
||||||
billable: billable.value !== null ? billable.value : undefined,
|
billable: billable.value !== null ? billable.value : undefined,
|
||||||
member_id: getCurrentRole() === 'employee' ? getCurrentMembershipId() : undefined,
|
member_id: getCurrentRole() === 'employee' ? getCurrentMembershipId() : undefined,
|
||||||
rounding_type: roundingEnabled.value ? roundingType.value : undefined,
|
rounding_type: roundingEnabled.value ? roundingType.value : undefined,
|
||||||
@@ -366,6 +369,7 @@ const tableData = computed(() => {
|
|||||||
v-model:selected-tasks="selectedTasks"
|
v-model:selected-tasks="selectedTasks"
|
||||||
v-model:selected-clients="selectedClients"
|
v-model:selected-clients="selectedClients"
|
||||||
v-model:selected-tags="selectedTags"
|
v-model:selected-tags="selectedTags"
|
||||||
|
v-model:tag-match-type="tagMatchType"
|
||||||
v-model:billable="billable"
|
v-model:billable="billable"
|
||||||
v-model:rounding-enabled="roundingEnabled"
|
v-model:rounding-enabled="roundingEnabled"
|
||||||
v-model:rounding-type="roundingType"
|
v-model:rounding-type="roundingType"
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import ReportingFilterBar from '@/Components/Common/Reporting/ReportingFilterBar
|
|||||||
import { useTimeEntriesReportQuery } from '@/utils/useTimeEntriesReportQuery';
|
import { useTimeEntriesReportQuery } from '@/utils/useTimeEntriesReportQuery';
|
||||||
import { useTimeEntriesMutations } from '@/utils/useTimeEntriesMutations';
|
import { useTimeEntriesMutations } from '@/utils/useTimeEntriesMutations';
|
||||||
import { useOrganizationQuery } from '@/utils/useOrganizationQuery';
|
import { useOrganizationQuery } from '@/utils/useOrganizationQuery';
|
||||||
|
import type { TagMatchType } from '@/types/reporting';
|
||||||
|
|
||||||
// TimeEntryRoundingType is now defined in ReportingRoundingControls component
|
// TimeEntryRoundingType is now defined in ReportingRoundingControls component
|
||||||
type TimeEntryRoundingType = 'up' | 'down' | 'nearest';
|
type TimeEntryRoundingType = 'up' | 'down' | 'nearest';
|
||||||
@@ -84,6 +85,7 @@ const selectedProjects = ref<string[]>([]);
|
|||||||
const selectedMembers = ref<string[]>([]);
|
const selectedMembers = ref<string[]>([]);
|
||||||
const selectedTasks = ref<string[]>([]);
|
const selectedTasks = ref<string[]>([]);
|
||||||
const selectedClients = ref<string[]>([]);
|
const selectedClients = ref<string[]>([]);
|
||||||
|
const tagMatchType = ref<TagMatchType>('contains');
|
||||||
const billable = ref<'true' | 'false' | null>(null);
|
const billable = ref<'true' | 'false' | null>(null);
|
||||||
const roundingEnabled = ref<boolean>(false);
|
const roundingEnabled = ref<boolean>(false);
|
||||||
const roundingType = ref<TimeEntryRoundingType>('nearest');
|
const roundingType = ref<TimeEntryRoundingType>('nearest');
|
||||||
@@ -115,6 +117,7 @@ function getFilterAttributes() {
|
|||||||
task_ids: selectedTasks.value.length > 0 ? selectedTasks.value : undefined,
|
task_ids: selectedTasks.value.length > 0 ? selectedTasks.value : undefined,
|
||||||
client_ids: selectedClients.value.length > 0 ? selectedClients.value : undefined,
|
client_ids: selectedClients.value.length > 0 ? selectedClients.value : undefined,
|
||||||
tag_ids: selectedTags.value.length > 0 ? selectedTags.value : undefined,
|
tag_ids: selectedTags.value.length > 0 ? selectedTags.value : undefined,
|
||||||
|
tag_match_type: selectedTags.value.length > 0 ? tagMatchType.value : undefined,
|
||||||
billable: billable.value !== null ? billable.value : undefined,
|
billable: billable.value !== null ? billable.value : undefined,
|
||||||
rounding_type: roundingEnabled.value ? roundingType.value : undefined,
|
rounding_type: roundingEnabled.value ? roundingType.value : undefined,
|
||||||
rounding_minutes: roundingEnabled.value ? roundingMinutes.value : undefined,
|
rounding_minutes: roundingEnabled.value ? roundingMinutes.value : undefined,
|
||||||
@@ -337,6 +340,7 @@ async function downloadExport(format: ExportFormat) {
|
|||||||
v-model:selected-tasks="selectedTasks"
|
v-model:selected-tasks="selectedTasks"
|
||||||
v-model:selected-clients="selectedClients"
|
v-model:selected-clients="selectedClients"
|
||||||
v-model:selected-tags="selectedTags"
|
v-model:selected-tags="selectedTags"
|
||||||
|
v-model:tag-match-type="tagMatchType"
|
||||||
v-model:billable="billable"
|
v-model:billable="billable"
|
||||||
v-model:rounding-enabled="roundingEnabled"
|
v-model:rounding-enabled="roundingEnabled"
|
||||||
v-model:rounding-type="roundingType"
|
v-model:rounding-type="roundingType"
|
||||||
|
|||||||
@@ -43,17 +43,17 @@ onMounted(async () => {
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<UpdateTeamNameForm :team="team" :permissions="permissions" />
|
<UpdateTeamNameForm :team="team" :permissions="permissions" />
|
||||||
|
|
||||||
<SectionBorder />
|
<SectionBorder v-if="permissions.canUpdateTeam" />
|
||||||
<OrganizationBillableRate v-if="permissions.canUpdateTeam" :team="team" />
|
<OrganizationBillableRate v-if="permissions.canUpdateTeam" :team="team" />
|
||||||
<SectionBorder />
|
|
||||||
|
|
||||||
|
<SectionBorder v-if="permissions.canUpdateTeam" />
|
||||||
<OrganizationFormatSettings v-if="permissions.canUpdateTeam" :team="team" />
|
<OrganizationFormatSettings v-if="permissions.canUpdateTeam" :team="team" />
|
||||||
<SectionBorder />
|
|
||||||
|
|
||||||
|
<SectionBorder v-if="permissions.canUpdateTeam" />
|
||||||
<OrganizationTimeEntrySettings v-if="permissions.canUpdateTeam" />
|
<OrganizationTimeEntrySettings v-if="permissions.canUpdateTeam" />
|
||||||
<SectionBorder />
|
|
||||||
|
|
||||||
<template v-if="permissions.canDeleteTeam">
|
<template v-if="permissions.canDeleteTeam">
|
||||||
|
<SectionBorder />
|
||||||
<DeleteTeamForm class="mt-10 sm:mt-0" :team="team" />
|
<DeleteTeamForm class="mt-10 sm:mt-0" :team="team" />
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -448,6 +448,7 @@ const ReportStoreRequest = z
|
|||||||
client_ids: z.union([z.array(z.string()), z.null()]).optional(),
|
client_ids: z.union([z.array(z.string()), z.null()]).optional(),
|
||||||
project_ids: z.union([z.array(z.string()), z.null()]).optional(),
|
project_ids: z.union([z.array(z.string()), z.null()]).optional(),
|
||||||
tag_ids: z.union([z.array(z.string()), z.null()]).optional(),
|
tag_ids: z.union([z.array(z.string()), z.null()]).optional(),
|
||||||
|
tag_match_type: z.enum(['contains', 'not_contains']).optional(),
|
||||||
task_ids: z.union([z.array(z.string()), z.null()]).optional(),
|
task_ids: z.union([z.array(z.string()), z.null()]).optional(),
|
||||||
group: TimeEntryAggregationType,
|
group: TimeEntryAggregationType,
|
||||||
sub_group: TimeEntryAggregationType,
|
sub_group: TimeEntryAggregationType,
|
||||||
@@ -481,6 +482,7 @@ const DetailedReportResource = z
|
|||||||
client_ids: z.union([z.array(z.string()), z.null()]),
|
client_ids: z.union([z.array(z.string()), z.null()]),
|
||||||
project_ids: z.union([z.array(z.string()), z.null()]),
|
project_ids: z.union([z.array(z.string()), z.null()]),
|
||||||
tag_ids: z.union([z.array(z.string()), z.null()]),
|
tag_ids: z.union([z.array(z.string()), z.null()]),
|
||||||
|
tag_match_type: z.union([z.enum(['contains', 'not_contains']), z.null()]),
|
||||||
task_ids: z.union([z.array(z.string()), z.null()]),
|
task_ids: z.union([z.array(z.string()), z.null()]),
|
||||||
rounding_type: z.union([z.string(), z.null()]),
|
rounding_type: z.union([z.string(), z.null()]),
|
||||||
rounding_minutes: z.union([z.number(), z.null()]),
|
rounding_minutes: z.union([z.number(), z.null()]),
|
||||||
@@ -3784,6 +3786,11 @@ Users with the permission `time-entries:view:own` can only use this en
|
|||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.array(z.string()).min(1).optional(),
|
schema: z.array(z.string()).min(1).optional(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'tag_match_type',
|
||||||
|
type: 'Query',
|
||||||
|
schema: z.enum(['contains', 'not_contains']).optional(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'task_ids',
|
name: 'task_ids',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
@@ -4165,6 +4172,11 @@ If the group parameters are all set to `null` or are all missing, the
|
|||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.array(z.string()).min(1).optional(),
|
schema: z.array(z.string()).min(1).optional(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'tag_match_type',
|
||||||
|
type: 'Query',
|
||||||
|
schema: z.enum(['contains', 'not_contains']).optional(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'task_ids',
|
name: 'task_ids',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
@@ -4359,6 +4371,11 @@ If the group parameters are all set to `null` or are all missing, the
|
|||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.array(z.string()).min(1).optional(),
|
schema: z.array(z.string()).min(1).optional(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'tag_match_type',
|
||||||
|
type: 'Query',
|
||||||
|
schema: z.enum(['contains', 'not_contains']).optional(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'task_ids',
|
name: 'task_ids',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
@@ -4487,6 +4504,11 @@ If the group parameters are all set to `null` or are all missing, the
|
|||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.array(z.string()).min(1).optional(),
|
schema: z.array(z.string()).min(1).optional(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'tag_match_type',
|
||||||
|
type: 'Query',
|
||||||
|
schema: z.enum(['contains', 'not_contains']).optional(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'task_ids',
|
name: 'task_ids',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ const showCreateTagModal = ref(false);
|
|||||||
class="w-full rounded-md border border-input-border bg-input-background px-3 py-1.5 text-sm text-text-primary placeholder:text-text-tertiary focus:outline-none"
|
class="w-full rounded-md border border-input-border bg-input-background px-3 py-1.5 text-sm text-text-primary placeholder:text-text-tertiary focus:outline-none"
|
||||||
placeholder="Search for a Tag..." />
|
placeholder="Search for a Tag..." />
|
||||||
</ComboboxAnchor>
|
</ComboboxAnchor>
|
||||||
|
<slot name="content-before-list"></slot>
|
||||||
<ComboboxContent
|
<ComboboxContent
|
||||||
:dismiss-able="false"
|
:dismiss-able="false"
|
||||||
position="inline"
|
position="inline"
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export type ExportFormat = 'xlsx' | 'csv' | 'ods' | 'pdf';
|
export type ExportFormat = 'xlsx' | 'csv' | 'ods' | 'pdf';
|
||||||
|
export type TagMatchType = 'contains' | 'not_contains';
|
||||||
|
|||||||
@@ -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"
|
||||||
|
@@ -41,6 +41,30 @@ class ClockifyTimeEntriesImporterTest extends ImporterTestAbstract
|
|||||||
$this->assertSame(1, $report->clientsCreated);
|
$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
|
public function test_import_of_test_with_special_characters_description_succeeds(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class ImporterTestAbstract extends TestCase
|
|||||||
/**
|
/**
|
||||||
* @return object{user1: User, project1: Project, project2: Project, tag1: Tag, tag2: Tag}
|
* @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();
|
$users = User::all();
|
||||||
$this->assertCount(2, $users);
|
$this->assertCount(2, $users);
|
||||||
@@ -80,12 +80,12 @@ class ImporterTestAbstract extends TestCase
|
|||||||
$this->assertSame('#ef5350', $project1->color);
|
$this->assertSame('#ef5350', $project1->color);
|
||||||
$this->assertSame(null, $project1->billable_rate);
|
$this->assertSame(null, $project1->billable_rate);
|
||||||
// Project for Big Company
|
// Project for Big Company
|
||||||
$this->assertSame(true, $project2->is_billable);
|
$this->assertSame(! $billableDefault, $project2->is_billable);
|
||||||
$this->assertSame(false, $project2->is_public);
|
$this->assertSame(false, $project2->is_public);
|
||||||
$this->assertSame('#ec407a', $project2->color);
|
$this->assertSame('#ec407a', $project2->color);
|
||||||
$this->assertSame(10001, $project2->billable_rate);
|
$this->assertSame(10001, $project2->billable_rate);
|
||||||
// Project (Archived)
|
// Project (Archived)
|
||||||
$this->assertSame(true, $project3->is_billable);
|
$this->assertSame(! $billableDefault, $project3->is_billable);
|
||||||
$this->assertSame(true, $project3->is_public);
|
$this->assertSame(true, $project3->is_public);
|
||||||
$this->assertSame('#6a407f', $project3->color);
|
$this->assertSame('#6a407f', $project3->color);
|
||||||
$this->assertSame(null, $project3->billable_rate);
|
$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
|
* @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();
|
$timeEntries = TimeEntry::all();
|
||||||
if ($secondRun) {
|
if ($secondRun) {
|
||||||
@@ -197,7 +197,7 @@ class ImporterTestAbstract extends TestCase
|
|||||||
$this->assertSame('Working hard', $timeEntry2->description);
|
$this->assertSame('Working hard', $timeEntry2->description);
|
||||||
$this->assertSame('2024-03-04 09:23:00', $timeEntry2->start->toDateTimeString());
|
$this->assertSame('2024-03-04 09:23:00', $timeEntry2->start->toDateTimeString());
|
||||||
$this->assertSame('2024-03-04 10:23:01', $timeEntry2->end->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->assertTrue($timeEntry2->is_imported);
|
||||||
$this->assertSame([], $timeEntry2->tags);
|
$this->assertSame([], $timeEntry2->tags);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ async function getConfig() {
|
|||||||
const additionalPlugins = await collectModulePlugins('extensions');
|
const additionalPlugins = await collectModulePlugins('extensions');
|
||||||
|
|
||||||
return defineConfig({
|
return defineConfig({
|
||||||
|
base: './',
|
||||||
build: {
|
build: {
|
||||||
sourcemap: true, // Source map generation must be turned on
|
sourcemap: true, // Source map generation must be turned on
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user