Compare commits

...

4 Commits

Author SHA1 Message Date
dependabot[bot]
ccf2b1246d Bump the major-updates group across 1 directory with 14 updates
Bumps the major-updates group with 14 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@floating-ui/vue](https://github.com/floating-ui/floating-ui/tree/HEAD/packages/vue) | `1.1.11` | `2.0.1` |
| [@tanstack/vue-query-devtools](https://github.com/TanStack/query/tree/HEAD/packages/vue-query-devtools) | `5.91.0` | `6.1.36` |
| [pinia](https://github.com/vuejs/pinia) | `3.0.4` | `4.0.2` |
| [tailwind-merge](https://github.com/dcastil/tailwind-merge) | `2.6.1` | `3.6.0` |
| [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.39.4` | `10.0.1` |
| [@inertiajs/vue3](https://github.com/inertiajs/inertia/tree/HEAD/packages/vue3) | `2.3.23` | `3.6.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `22.19.19` | `26.1.1` |
| [laravel-vite-plugin](https://github.com/laravel/vite-plugin) | `2.1.0` | `3.1.3` |
| [postcss-import](https://github.com/postcss/postcss-import) | `15.1.0` | `16.1.1` |
| [postcss-nesting](https://github.com/csstools/postcss-plugins/tree/HEAD/plugins/postcss-nesting) | `12.1.5` | `14.0.0` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `3.4.19` | `4.3.3` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.9.3` | `7.0.2` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.3.3` | `8.1.5` |
| [vite-plugin-dts](https://github.com/qmhc/unplugin-dts/tree/HEAD/packages/vite-plugin-dts) | `4.5.4` | `5.0.3` |



Updates `@floating-ui/vue` from 1.1.11 to 2.0.1
- [Release notes](https://github.com/floating-ui/floating-ui/releases)
- [Changelog](https://github.com/floating-ui/floating-ui/blob/master/packages/vue/CHANGELOG.md)
- [Commits](https://github.com/floating-ui/floating-ui/commits/@floating-ui/vue@2.0.1/packages/vue)

Updates `@tanstack/vue-query-devtools` from 5.91.0 to 6.1.36
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/vue-query-devtools/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/vue-query-devtools@6.1.36/packages/vue-query-devtools)

Updates `pinia` from 3.0.4 to 4.0.2
- [Release notes](https://github.com/vuejs/pinia/releases)
- [Commits](https://github.com/vuejs/pinia/compare/v3.0.4...v4.0.2)

Updates `tailwind-merge` from 2.6.1 to 3.6.0
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/compare/v2.6.1...v3.6.0)

Updates `@eslint/js` from 9.39.4 to 10.0.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v10.0.1/packages/js)

Updates `@inertiajs/vue3` from 2.3.23 to 3.6.1
- [Release notes](https://github.com/inertiajs/inertia/releases)
- [Changelog](https://github.com/inertiajs/inertia/blob/3.x/CHANGELOG.md)
- [Commits](https://github.com/inertiajs/inertia/commits/v3.6.1/packages/vue3)

Updates `@types/node` from 22.19.19 to 26.1.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `laravel-vite-plugin` from 2.1.0 to 3.1.3
- [Release notes](https://github.com/laravel/vite-plugin/releases)
- [Changelog](https://github.com/laravel/vite-plugin/blob/3.x/CHANGELOG.md)
- [Upgrade guide](https://github.com/laravel/vite-plugin/blob/3.x/UPGRADE.md)
- [Commits](https://github.com/laravel/vite-plugin/compare/v2.1.0...v3.1.3)

Updates `postcss-import` from 15.1.0 to 16.1.1
- [Release notes](https://github.com/postcss/postcss-import/releases)
- [Changelog](https://github.com/postcss/postcss-import/blob/master/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss-import/compare/15.1.0...16.1.1)

Updates `postcss-nesting` from 12.1.5 to 14.0.0
- [Changelog](https://github.com/csstools/postcss-plugins/blob/main/plugins/postcss-nesting/CHANGELOG.md)
- [Commits](https://github.com/csstools/postcss-plugins/commits/HEAD/plugins/postcss-nesting)

Updates `tailwindcss` from 3.4.19 to 4.3.3
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/tailwindcss)

Updates `typescript` from 5.9.3 to 7.0.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/commits)

Updates `vite` from 7.3.3 to 8.1.5
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.1.5/packages/vite)

Updates `vite-plugin-dts` from 4.5.4 to 5.0.3
- [Release notes](https://github.com/qmhc/unplugin-dts/releases)
- [Changelog](https://github.com/qmhc/unplugin-dts/blob/main/packages/vite-plugin-dts/CHANGELOG.md)
- [Commits](https://github.com/qmhc/unplugin-dts/commits/vite-plugin-dts@5.0.3/packages/vite-plugin-dts)

---
updated-dependencies:
- dependency-name: "@floating-ui/vue"
  dependency-version: 2.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: "@tanstack/vue-query-devtools"
  dependency-version: 6.1.36
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: pinia
  dependency-version: 4.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: tailwind-merge
  dependency-version: 3.6.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: "@eslint/js"
  dependency-version: 10.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: "@inertiajs/vue3"
  dependency-version: 3.6.1
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: "@types/node"
  dependency-version: 26.1.1
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: laravel-vite-plugin
  dependency-version: 3.1.3
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: postcss-import
  dependency-version: 16.1.1
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: postcss-nesting
  dependency-version: 14.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: tailwindcss
  dependency-version: 4.3.3
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: typescript
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: vite
  dependency-version: 8.1.5
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: vite-plugin-dts
  dependency-version: 5.0.3
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 08:24:05 +00:00
Constantin Graf
114a32536d Fixed update of member_id in time_entries.update and time_entries.updateMultiple
Removed usage of legacy user_id in TimeEntryController
2026-07-23 12:02:46 +02:00
Constantin Graf
ff8a0f065b Updated extension billing 2026-07-23 11:42:32 +02:00
Constantin Graf
44fd0ffb91 Fix .dockerignore 2026-07-23 11:40:19 +02:00
12 changed files with 2158 additions and 2565 deletions

View File

@@ -1,5 +1,7 @@
.git
**/.git
.gitmodules
**/.gitmodules
.github
.DS_Store
.fleet
@@ -8,6 +10,13 @@
*.log
npm-debug.log
yarn-error.log
k8s
docs
e2e
tests
docker-compose.yml
docker/local
.phpunit.cache
.phpunit.result.cache
@@ -16,6 +25,18 @@ test-results
playwright-report
blob-report
playwright/.cache
openapi.json
playwright
playwright.config.ts
vitest.config.ts
phpunit.xml
phpstan.neon
pint.json
eslint.config.mjs
tsconfig.json
jsconfig.json
postcss.config.js
tailwind.config.js
node_modules
extensions/*/node_modules
@@ -30,3 +51,4 @@ _ide_helper.php
.phpstorm.meta.php
storage/logs/*
storage/*.key

View File

@@ -8,6 +8,7 @@ on:
pull_request:
paths:
- '.github/workflows/build-onpremise.yml'
- '.dockerignore'
- 'extensions/manifest.json'
- 'docker/prod/**'
workflow_dispatch:

View File

@@ -8,6 +8,7 @@ on:
pull_request:
paths:
- '.github/workflows/build-private.yml'
- '.dockerignore'
- 'extensions/manifest.json'
- 'docker/prod/**'
workflow_dispatch:

View File

@@ -8,6 +8,7 @@ on:
pull_request:
paths:
- '.github/workflows/build-public.yml'
- '.dockerignore'
- 'docker/prod/**'
workflow_dispatch:

View File

@@ -67,7 +67,7 @@ class TimeEntryController extends Controller
$query = TimeEntry::query()
->where('organization_id', $organization->getKey())
->where('user_id', $member->user_id)
->where('member_id', $member->getKey())
->when($exclude !== null, function (Builder $q) use ($exclude): void {
$q->where('id', '!=', $exclude->getKey());
})
@@ -107,8 +107,8 @@ class TimeEntryController extends Controller
/**
* Get time entries in organization
*
* If you only need time entries for a specific user, you can filter by `user_id`.
* Users with the permission `time-entries:view:own` can only use this endpoint with their own user ID in the user_id filter.
* If you only need time entries for a specific user, you can filter by `member_id`.
* Users with the permission `time-entries:view:own` can only use this endpoint with their own member ID in the member_id filter.
*
* @return TimeEntryCollection<TimeEntryResource>
*
@@ -118,16 +118,17 @@ class TimeEntryController extends Controller
*/
public function index(Organization $organization, TimeEntryIndexRequest $request): JsonResource
{
/** @var Member|null $member */
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
if ($member !== null && $member->user_id === Auth::id()) {
$member = $this->member($organization);
/** @var Member|null $memberFilter */
$memberFilter = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
if ($memberFilter !== null && $memberFilter->getKey() === $member->getKey()) {
$this->checkPermission($organization, 'time-entries:view:own');
} else {
$this->checkPermission($organization, 'time-entries:view:all');
}
$canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization);
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member, $canAccessPremiumFeatures);
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $memberFilter, $canAccessPremiumFeatures);
$totalCount = $timeEntriesQuery->count();
@@ -158,7 +159,7 @@ class TimeEntryController extends Controller
if ($timeEntries->count() === 0) {
Log::warning('User has has more than '.$limit.' time entries on one date', [
'date' => $lastDate->toDateString(),
'user_id' => $request->input('user_id'),
'member_id' => $request->input('member_id'),
'auth_user_id' => Auth::id(),
'limit' => $limit,
]);
@@ -221,9 +222,10 @@ class TimeEntryController extends Controller
*/
public function indexExport(Organization $organization, TimeEntryIndexExportRequest $request, TimeEntryAggregationService $timeEntryAggregationService): JsonResponse
{
/** @var Member|null $member */
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
if ($member !== null && $member->user_id === Auth::id()) {
$member = $this->member($organization);
/** @var Member|null $memberFilter */
$memberFilter = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
if ($memberFilter !== null && $memberFilter->getKey() === $member->getKey()) {
$this->checkPermission($organization, 'time-entries:view:own');
} else {
$this->checkPermission($organization, 'time-entries:view:all');
@@ -240,7 +242,7 @@ class TimeEntryController extends Controller
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member, $canAccessPremiumFeatures);
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $memberFilter, $canAccessPremiumFeatures);
$timeEntriesQuery->with([
'task',
'client',
@@ -263,7 +265,7 @@ class TimeEntryController extends Controller
if ($viewFile === false) {
throw new \LogicException('View file not found');
}
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $memberFilter);
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries(
$timeEntriesAggregateQuery,
null,
@@ -370,9 +372,10 @@ class TimeEntryController extends Controller
*/
public function aggregate(Organization $organization, TimeEntryAggregateRequest $request, TimeEntryAggregationService $timeEntryAggregationService): array
{
/** @var Member|null $member */
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
if ($member !== null && $member->user_id === Auth::id()) {
$member = $this->member($organization);
/** @var Member|null $memberFilter */
$memberFilter = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
if ($memberFilter !== null && $memberFilter->getKey() === $member->getKey()) {
$this->checkPermission($organization, 'time-entries:view:own');
} else {
$this->checkPermission($organization, 'time-entries:view:all');
@@ -383,7 +386,7 @@ class TimeEntryController extends Controller
$group1Type = $request->getGroup();
$group2Type = $request->getSubGroup();
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $memberFilter);
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
@@ -419,9 +422,10 @@ class TimeEntryController extends Controller
*/
public function aggregateExport(Organization $organization, TimeEntryAggregateExportRequest $request, TimeEntryAggregationService $timeEntryAggregationService): JsonResponse
{
/** @var Member|null $member */
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
if ($member !== null && $member->user_id === Auth::id()) {
$member = $this->member($organization);
/** @var Member|null $memberFilter */
$memberFilter = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
if ($memberFilter !== null && $memberFilter->getKey() === $member->getKey()) {
$this->checkPermission($organization, 'time-entries:view:own');
} else {
$this->checkPermission($organization, 'time-entries:view:all');
@@ -437,7 +441,7 @@ class TimeEntryController extends Controller
$group = $request->getGroup();
$subGroup = $request->getSubGroup();
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $memberFilter);
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
@@ -580,7 +584,7 @@ class TimeEntryController extends Controller
{
/** @var Member $member */
$member = Member::query()->findOrFail($request->input('member_id'));
if ($member->user_id === Auth::id()) {
if ($member->getKey() === $this->member($organization)->getKey()) {
$this->checkPermission($organization, 'time-entries:create:own');
} else {
$this->checkPermission($organization, 'time-entries:create:all');
@@ -627,9 +631,10 @@ class TimeEntryController extends Controller
*/
public function update(Organization $organization, TimeEntry $timeEntry, TimeEntryUpdateRequest $request): JsonResource
{
/** @var Member|null $member */
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
if ($timeEntry->member->user_id === Auth::id() && ($member === null || $member->user_id === Auth::id())) {
$member = $this->member($organization);
/** @var Member|null $newMember */
$newMember = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
if ($timeEntry->member_id === $member->getKey() && ($newMember === null || $newMember->getKey() === $member->getKey())) {
$this->checkPermission($organization, 'time-entries:update:own', $timeEntry);
} else {
$this->checkPermission($organization, 'time-entries:update:all', $timeEntry);
@@ -661,6 +666,10 @@ class TimeEntryController extends Controller
}
$timeEntry->fill($request->validated());
if ($newMember !== null) {
$timeEntry->member()->associate($newMember);
$timeEntry->user()->associate($newMember->user);
}
$timeEntry->description = $request->input('description', $timeEntry->description) ?? '';
$timeEntry->setComputedAttributeValue('billable_rate');
$timeEntry->save();
@@ -690,6 +699,7 @@ class TimeEntryController extends Controller
*/
public function updateMultiple(Organization $organization, TimeEntryUpdateMultipleRequest $request): JsonResponse
{
$member = $this->member($organization);
$this->checkAnyPermission($organization, ['time-entries:update:all', 'time-entries:update:own']);
$canAccessAll = $this->hasPermission($organization, 'time-entries:update:all');
@@ -714,6 +724,9 @@ class TimeEntryController extends Controller
throw new AuthorizationException;
}
/** @var Member|null $newMember */
$newMember = isset($changes['member_id']) ? Member::query()->findOrFail($changes['member_id']) : null;
$project = null;
$client = null;
$overwriteClient = false;
@@ -740,7 +753,7 @@ class TimeEntryController extends Controller
continue;
}
if (! $canAccessAll && $timeEntry->user_id !== Auth::id()) {
if (! $canAccessAll && $timeEntry->member_id !== $member->getKey()) {
$error->push($id);
continue;
@@ -750,6 +763,10 @@ class TimeEntryController extends Controller
$oldTask = $timeEntry->task;
$timeEntry->fill($changes);
if ($newMember !== null) {
$timeEntry->member()->associate($newMember);
$timeEntry->user_id = $newMember->user_id;
}
// If project is changed, but task is not, we remove the old task from the time entry
if ($oldProject !== null && $project !== null && $oldProject->isNot($project) && $task === null) {
$timeEntry->task()->disassociate();
@@ -790,7 +807,8 @@ class TimeEntryController extends Controller
*/
public function destroy(Organization $organization, TimeEntry $timeEntry): JsonResponse
{
if ($timeEntry->member->user_id === Auth::id()) {
$member = $this->member($organization);
if ($timeEntry->member_id === $member->getKey()) {
$this->checkPermission($organization, 'time-entries:delete:own', $timeEntry);
} else {
$this->checkPermission($organization, 'time-entries:delete:all', $timeEntry);
@@ -847,7 +865,7 @@ class TimeEntryController extends Controller
continue;
}
if (! $canDeleteAll && $timeEntry->user_id !== Auth::id()) {
if (! $canDeleteAll && $timeEntry->member_id !== $this->member($organization)->getKey()) {
$error->push($id);
continue;

View File

@@ -189,7 +189,9 @@ ENV WITH_HORIZON=false \
WITH_SCHEDULER=false \
WITH_REVERB=false
COPY --link --chown=${WWWUSER}:${WWWUSER} . .
COPY --link --chown=${WWWUSER}:${WWWUSER} . ./
RUN test -z "$(find . -name .git -print -quit)"
#COPY --link --chown=${WWWUSER}:${WWWUSER} --from=build ${ROOT}/public public
RUN mkdir -p \

View File

@@ -1,7 +1,7 @@
{
"Billing": {
"repository": "solidtime-io/extension-billing",
"ref": "v0.0.1"
"ref": "v0.0.3"
},
"Services": {
"repository": "solidtime-io/extension-services",

4391
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,13 +26,13 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.5",
"@eslint/js": "^9.39.4",
"@inertiajs/vue3": "^2.3.23",
"@eslint/js": "^10.0.1",
"@inertiajs/vue3": "^3.6.1",
"@playwright/test": "^1.60.0",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19",
"@types/chroma-js": "^3.1.2",
"@types/node": "^22.19.19",
"@types/node": "^26.1.1",
"@vitejs/plugin-vue": "^6.0.6",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.8.1",
@@ -40,14 +40,14 @@
"axios": "^1.16.0",
"eslint-plugin-unused-imports": "^4.4.1",
"happy-dom": "^20.8.9",
"laravel-vite-plugin": "^2.1.0",
"laravel-vite-plugin": "^3.1.3",
"openapi-zod-client": "^1.18.3",
"postcss": "^8.5.14",
"postcss-import": "^15.1.0",
"postcss-nesting": "^12.1.5",
"tailwindcss": "^3.4.19",
"typescript": "^5.9.3",
"vite": "^7.3.3",
"postcss-import": "^16.1.1",
"postcss-nesting": "^14.0.0",
"tailwindcss": "^4.3.3",
"typescript": "^7.0.2",
"vite": "^8.1.5",
"vite-plugin-checker": "^0.12.0",
"vitest": "^4.1.4",
"vue": "^3.5.34",
@@ -55,14 +55,14 @@
},
"dependencies": {
"@floating-ui/core": "^1.7.5",
"@floating-ui/vue": "^1.1.11",
"@floating-ui/vue": "^2.0.1",
"@heroicons/vue": "^2.2.0",
"@lucide/vue": "^1.14.0",
"@rushstack/eslint-patch": "^1.16.1",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/vue-form": "^1.32.0",
"@tanstack/vue-query": "^5.100.10",
"@tanstack/vue-query-devtools": "^5.91.0",
"@tanstack/vue-query-devtools": "^6.1.36",
"@tanstack/vue-table": "^8.21.3",
"@tanstack/vue-virtual": "^3.13.24",
"@vue/eslint-config-prettier": "^10.2.0",
@@ -77,10 +77,10 @@
"echarts": "^6.0.0",
"focus-trap": "^8.2.0",
"parse-duration": "^2.1.6",
"pinia": "^3.0.4",
"pinia": "^4.0.2",
"radix-vue": "^1.9.17",
"reka-ui": "^2.9.7",
"tailwind-merge": "^2.6.1",
"tailwind-merge": "^3.6.0",
"tailwindcss-animate": "^1.0.7",
"vue-draggable-plus": "^0.6.1",
"vue-echarts": "^8.0.1",

View File

@@ -29,7 +29,7 @@
"author": "solidtime",
"license": "AGPL-3.0",
"devDependencies": {
"vite-plugin-dts": "^4.5.4"
"vite-plugin-dts": "^5.0.3"
},
"peerDependencies": {
"@zodios/core": "^10.9.6",

View File

@@ -50,7 +50,7 @@
"devDependencies": {
"@types/chroma-js": "^3.1.2",
"@zodios/core": "^10.9.6",
"vite-plugin-dts": "^4.5.4",
"vite-plugin-dts": "^5.0.3",
"zod": "^3.25.76"
},
"peerDependencies": {

View File

@@ -92,6 +92,30 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$response->assertJsonPath('data.0.id', $timeEntry->getKey());
}
public function test_index_endpoint_filters_by_member_id_instead_of_legacy_user_id(): void
{
// Arrange
$data = $this->createUserWithPermission([
'time-entries:view:own',
]);
$legacyUser = User::factory()->create();
$timeEntry = TimeEntry::factory()->forMember($data->member)->create([
'user_id' => $legacyUser->getKey(),
]);
Passport::actingAs($data->user);
// Act
$response = $this->getJson(route('api.v1.time-entries.index', [
$data->organization->getKey(),
'member_id' => $data->member->getKey(),
]));
// Assert
$this->assertResponseCode($response, 200);
$response->assertJsonCount(1, 'data');
$response->assertJsonPath('data.0.id', $timeEntry->getKey());
}
public function test_index_endpoint_fails_if_user_filter_is_from_different_organization(): void
{
// Arrange
@@ -126,7 +150,10 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
Passport::actingAs($data->user);
// Act
$response = $this->getJson(route('api.v1.time-entries.index', [$data->organization->getKey(), 'user_id' => $user->getKey()]));
$response = $this->getJson(route('api.v1.time-entries.index', [
$data->organization->getKey(),
'member_id' => $member->getKey(),
]));
// Assert
$this->assertResponseCode($response, 200);
@@ -1772,6 +1799,29 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
]);
}
public function test_aggregate_endpoint_filters_by_member_id_instead_of_legacy_user_id(): void
{
// Arrange
$data = $this->createUserWithPermission([
'time-entries:view:own',
]);
$legacyUser = User::factory()->create();
TimeEntry::factory()->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create([
'user_id' => $legacyUser->getKey(),
]);
Passport::actingAs($data->user);
// Act
$response = $this->getJson(route('api.v1.time-entries.aggregate', [
$data->organization->getKey(),
'member_id' => $data->member->getKey(),
]));
// Assert
$response->assertSuccessful();
$response->assertJsonPath('data.seconds', 100);
}
public function test_aggregate_endpoint_groups_by_two_groups(): void
{
// Arrange
@@ -2819,6 +2869,32 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
]);
}
public function test_update_endpoint_updates_user_id_when_member_id_changes(): void
{
// Arrange
$data = $this->createUserWithPermission([
'time-entries:update:all',
]);
$otherUser = User::factory()->create();
$otherMember = Member::factory()->forOrganization($data->organization)->forUser($otherUser)->role(Role::Employee)->create();
$timeEntry = TimeEntry::factory()->forMember($data->member)->create();
Passport::actingAs($data->user);
// Act
$response = $this->putJson(route('api.v1.time-entries.update', [$data->organization->getKey(), $timeEntry->getKey()]), [
'member_id' => $otherMember->getKey(),
]);
// Assert
$response->assertValid();
$this->assertResponseCode($response, 200);
$this->assertDatabaseHas(TimeEntry::class, [
'id' => $timeEntry->getKey(),
'member_id' => $otherMember->getKey(),
'user_id' => $otherUser->getKey(),
]);
}
public function test_update_endpoint_can_update_project_and_automatically_set_client(): void
{
// Arrange
@@ -3155,6 +3231,40 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
]);
}
public function test_destroy_multiple_uses_member_id_for_own_permission_checks(): void
{
// Arrange
$data = $this->createUserWithPermission([
'time-entries:delete:own',
]);
$otherUser = User::factory()->create();
$otherMember = Member::factory()->forOrganization($data->organization)->forUser($otherUser)->role(Role::Employee)->create();
$timeEntry = TimeEntry::factory()->forMember($otherMember)->create([
'user_id' => $data->user->getKey(),
]);
Passport::actingAs($data->user);
// Act
$response = $this->deleteJson(route('api.v1.time-entries.destroy-multiple', [$data->organization->getKey()]), [
'ids' => [
$timeEntry->getKey(),
],
]);
// Assert
$response->assertValid();
$this->assertResponseCode($response, 200);
$response->assertExactJson([
'success' => [],
'error' => [
$timeEntry->getKey(),
],
]);
$this->assertDatabaseHas(TimeEntry::class, [
'id' => $timeEntry->getKey(),
]);
}
public function test_destroy_multiple_deletes_all_time_entries_and_fails_for_time_entries_of_other_users_and_and_other_organizations_with_all_time_entries_permission(): void
{
// Arrange
@@ -3566,6 +3676,46 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
]);
}
public function test_update_multiple_uses_member_id_for_own_permission_checks(): void
{
// Arrange
$data = $this->createUserWithPermission([
'time-entries:update:own',
'projects:view:all',
]);
$otherUser = User::factory()->create();
$otherMember = Member::factory()->forOrganization($data->organization)->forUser($otherUser)->role(Role::Employee)->create();
$timeEntry = TimeEntry::factory()->forMember($otherMember)->create([
'user_id' => $data->user->getKey(),
]);
$timeEntriesFake = TimeEntry::factory()->forOrganization($data->organization)->make();
Passport::actingAs($data->user);
// Act
$response = $this->patchJson(route('api.v1.time-entries.update-multiple', [$data->organization->getKey()]), [
'ids' => [
$timeEntry->getKey(),
],
'changes' => [
'description' => $timeEntriesFake->description,
],
]);
// Assert
$response->assertValid();
$this->assertResponseCode($response, 200);
$response->assertExactJson([
'success' => [],
'error' => [
$timeEntry->getKey(),
],
]);
$this->assertDatabaseHas(TimeEntry::class, [
'id' => $timeEntry->getKey(),
'description' => $timeEntry->description,
]);
}
public function test_update_multiple_updates_sets_description_to_empty_if_the_client_sends_null(): void
{
// Arrange
@@ -3612,6 +3762,51 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
]);
}
public function test_update_multiple_updates_user_id_when_member_id_changes(): void
{
// Arrange
$data = $this->createUserWithPermission([
'time-entries:update:all',
]);
$otherUser = User::factory()->create();
$otherMember = Member::factory()->forOrganization($data->organization)->forUser($otherUser)->role(Role::Employee)->create();
$timeEntry1 = TimeEntry::factory()->forMember($data->member)->create();
$timeEntry2 = TimeEntry::factory()->forMember($data->member)->create();
Passport::actingAs($data->user);
// Act
$response = $this->patchJson(route('api.v1.time-entries.update-multiple', [$data->organization->getKey()]), [
'ids' => [
$timeEntry1->getKey(),
$timeEntry2->getKey(),
],
'changes' => [
'member_id' => $otherMember->getKey(),
],
]);
// Assert
$response->assertValid();
$response->assertStatus(200);
$response->assertExactJson([
'success' => [
$timeEntry1->getKey(),
$timeEntry2->getKey(),
],
'error' => [],
]);
$this->assertDatabaseHas(TimeEntry::class, [
'id' => $timeEntry1->getKey(),
'member_id' => $otherMember->getKey(),
'user_id' => $otherUser->getKey(),
]);
$this->assertDatabaseHas(TimeEntry::class, [
'id' => $timeEntry2->getKey(),
'member_id' => $otherMember->getKey(),
'user_id' => $otherUser->getKey(),
]);
}
public function test_update_multiple_updates_all_time_entries_and_fails_for_time_entries_of_other_users_and_and_other_organizations_with_all_time_entries_permission(): void
{
// Arrange