mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-15 11:42:15 +01:00
Compare commits
2 Commits
ccf2b1246d
...
feature/pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c61303d88 | ||
|
|
a70c4fe3fb |
@@ -1,7 +1,5 @@
|
|||||||
.git
|
.git
|
||||||
**/.git
|
|
||||||
.gitmodules
|
.gitmodules
|
||||||
**/.gitmodules
|
|
||||||
.github
|
.github
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.fleet
|
.fleet
|
||||||
@@ -10,13 +8,6 @@
|
|||||||
*.log
|
*.log
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
k8s
|
|
||||||
docs
|
|
||||||
e2e
|
|
||||||
tests
|
|
||||||
|
|
||||||
docker-compose.yml
|
|
||||||
docker/local
|
|
||||||
|
|
||||||
.phpunit.cache
|
.phpunit.cache
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
@@ -25,18 +16,6 @@ test-results
|
|||||||
playwright-report
|
playwright-report
|
||||||
blob-report
|
blob-report
|
||||||
playwright/.cache
|
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
|
node_modules
|
||||||
extensions/*/node_modules
|
extensions/*/node_modules
|
||||||
@@ -51,4 +30,3 @@ _ide_helper.php
|
|||||||
.phpstorm.meta.php
|
.phpstorm.meta.php
|
||||||
|
|
||||||
storage/logs/*
|
storage/logs/*
|
||||||
storage/*.key
|
|
||||||
|
|||||||
1
.github/workflows/build-onpremise.yml
vendored
1
.github/workflows/build-onpremise.yml
vendored
@@ -8,7 +8,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/build-onpremise.yml'
|
- '.github/workflows/build-onpremise.yml'
|
||||||
- '.dockerignore'
|
|
||||||
- 'extensions/manifest.json'
|
- 'extensions/manifest.json'
|
||||||
- 'docker/prod/**'
|
- 'docker/prod/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
1
.github/workflows/build-private.yml
vendored
1
.github/workflows/build-private.yml
vendored
@@ -8,7 +8,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/build-private.yml'
|
- '.github/workflows/build-private.yml'
|
||||||
- '.dockerignore'
|
|
||||||
- 'extensions/manifest.json'
|
- 'extensions/manifest.json'
|
||||||
- 'docker/prod/**'
|
- 'docker/prod/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
1
.github/workflows/build-public.yml
vendored
1
.github/workflows/build-public.yml
vendored
@@ -8,7 +8,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/build-public.yml'
|
- '.github/workflows/build-public.yml'
|
||||||
- '.dockerignore'
|
|
||||||
- 'docker/prod/**'
|
- 'docker/prod/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/phpunit.yml
vendored
2
.github/workflows/phpunit.yml
vendored
@@ -65,7 +65,7 @@ jobs:
|
|||||||
php artisan passport:keys
|
php artisan passport:keys
|
||||||
|
|
||||||
- name: "Run PHPUnit"
|
- name: "Run PHPUnit"
|
||||||
run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml
|
run: php artisan test --parallel --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@v7.0.0
|
uses: codecov/codecov-action@v7.0.0
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class TimeEntryController extends Controller
|
|||||||
|
|
||||||
$query = TimeEntry::query()
|
$query = TimeEntry::query()
|
||||||
->where('organization_id', $organization->getKey())
|
->where('organization_id', $organization->getKey())
|
||||||
->where('member_id', $member->getKey())
|
->where('user_id', $member->user_id)
|
||||||
->when($exclude !== null, function (Builder $q) use ($exclude): void {
|
->when($exclude !== null, function (Builder $q) use ($exclude): void {
|
||||||
$q->where('id', '!=', $exclude->getKey());
|
$q->where('id', '!=', $exclude->getKey());
|
||||||
})
|
})
|
||||||
@@ -107,8 +107,8 @@ class TimeEntryController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Get time entries in organization
|
* Get time entries in organization
|
||||||
*
|
*
|
||||||
* If you only need time entries for a specific user, you can filter by `member_id`.
|
* 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 member ID in the member_id filter.
|
* Users with the permission `time-entries:view:own` can only use this endpoint with their own user ID in the user_id filter.
|
||||||
*
|
*
|
||||||
* @return TimeEntryCollection<TimeEntryResource>
|
* @return TimeEntryCollection<TimeEntryResource>
|
||||||
*
|
*
|
||||||
@@ -118,17 +118,16 @@ class TimeEntryController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(Organization $organization, TimeEntryIndexRequest $request): JsonResource
|
public function index(Organization $organization, TimeEntryIndexRequest $request): JsonResource
|
||||||
{
|
{
|
||||||
$member = $this->member($organization);
|
/** @var Member|null $member */
|
||||||
/** @var Member|null $memberFilter */
|
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
||||||
$memberFilter = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
if ($member !== null && $member->user_id === Auth::id()) {
|
||||||
if ($memberFilter !== null && $memberFilter->getKey() === $member->getKey()) {
|
|
||||||
$this->checkPermission($organization, 'time-entries:view:own');
|
$this->checkPermission($organization, 'time-entries:view:own');
|
||||||
} else {
|
} else {
|
||||||
$this->checkPermission($organization, 'time-entries:view:all');
|
$this->checkPermission($organization, 'time-entries:view:all');
|
||||||
}
|
}
|
||||||
|
|
||||||
$canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization);
|
$canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization);
|
||||||
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $memberFilter, $canAccessPremiumFeatures);
|
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member, $canAccessPremiumFeatures);
|
||||||
|
|
||||||
$totalCount = $timeEntriesQuery->count();
|
$totalCount = $timeEntriesQuery->count();
|
||||||
|
|
||||||
@@ -159,7 +158,7 @@ class TimeEntryController extends Controller
|
|||||||
if ($timeEntries->count() === 0) {
|
if ($timeEntries->count() === 0) {
|
||||||
Log::warning('User has has more than '.$limit.' time entries on one date', [
|
Log::warning('User has has more than '.$limit.' time entries on one date', [
|
||||||
'date' => $lastDate->toDateString(),
|
'date' => $lastDate->toDateString(),
|
||||||
'member_id' => $request->input('member_id'),
|
'user_id' => $request->input('user_id'),
|
||||||
'auth_user_id' => Auth::id(),
|
'auth_user_id' => Auth::id(),
|
||||||
'limit' => $limit,
|
'limit' => $limit,
|
||||||
]);
|
]);
|
||||||
@@ -222,10 +221,9 @@ class TimeEntryController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function indexExport(Organization $organization, TimeEntryIndexExportRequest $request, TimeEntryAggregationService $timeEntryAggregationService): JsonResponse
|
public function indexExport(Organization $organization, TimeEntryIndexExportRequest $request, TimeEntryAggregationService $timeEntryAggregationService): JsonResponse
|
||||||
{
|
{
|
||||||
$member = $this->member($organization);
|
/** @var Member|null $member */
|
||||||
/** @var Member|null $memberFilter */
|
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
||||||
$memberFilter = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
if ($member !== null && $member->user_id === Auth::id()) {
|
||||||
if ($memberFilter !== null && $memberFilter->getKey() === $member->getKey()) {
|
|
||||||
$this->checkPermission($organization, 'time-entries:view:own');
|
$this->checkPermission($organization, 'time-entries:view:own');
|
||||||
} else {
|
} else {
|
||||||
$this->checkPermission($organization, 'time-entries:view:all');
|
$this->checkPermission($organization, 'time-entries:view:all');
|
||||||
@@ -242,7 +240,7 @@ class TimeEntryController extends Controller
|
|||||||
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
|
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
|
||||||
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
||||||
|
|
||||||
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $memberFilter, $canAccessPremiumFeatures);
|
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member, $canAccessPremiumFeatures);
|
||||||
$timeEntriesQuery->with([
|
$timeEntriesQuery->with([
|
||||||
'task',
|
'task',
|
||||||
'client',
|
'client',
|
||||||
@@ -265,7 +263,7 @@ class TimeEntryController extends Controller
|
|||||||
if ($viewFile === false) {
|
if ($viewFile === false) {
|
||||||
throw new \LogicException('View file not found');
|
throw new \LogicException('View file not found');
|
||||||
}
|
}
|
||||||
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $memberFilter);
|
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
||||||
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries(
|
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries(
|
||||||
$timeEntriesAggregateQuery,
|
$timeEntriesAggregateQuery,
|
||||||
null,
|
null,
|
||||||
@@ -372,10 +370,9 @@ class TimeEntryController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function aggregate(Organization $organization, TimeEntryAggregateRequest $request, TimeEntryAggregationService $timeEntryAggregationService): array
|
public function aggregate(Organization $organization, TimeEntryAggregateRequest $request, TimeEntryAggregationService $timeEntryAggregationService): array
|
||||||
{
|
{
|
||||||
$member = $this->member($organization);
|
/** @var Member|null $member */
|
||||||
/** @var Member|null $memberFilter */
|
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
||||||
$memberFilter = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
if ($member !== null && $member->user_id === Auth::id()) {
|
||||||
if ($memberFilter !== null && $memberFilter->getKey() === $member->getKey()) {
|
|
||||||
$this->checkPermission($organization, 'time-entries:view:own');
|
$this->checkPermission($organization, 'time-entries:view:own');
|
||||||
} else {
|
} else {
|
||||||
$this->checkPermission($organization, 'time-entries:view:all');
|
$this->checkPermission($organization, 'time-entries:view:all');
|
||||||
@@ -386,7 +383,7 @@ class TimeEntryController extends Controller
|
|||||||
|
|
||||||
$group1Type = $request->getGroup();
|
$group1Type = $request->getGroup();
|
||||||
$group2Type = $request->getSubGroup();
|
$group2Type = $request->getSubGroup();
|
||||||
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $memberFilter);
|
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
||||||
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
|
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
|
||||||
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
||||||
|
|
||||||
@@ -422,10 +419,9 @@ class TimeEntryController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function aggregateExport(Organization $organization, TimeEntryAggregateExportRequest $request, TimeEntryAggregationService $timeEntryAggregationService): JsonResponse
|
public function aggregateExport(Organization $organization, TimeEntryAggregateExportRequest $request, TimeEntryAggregationService $timeEntryAggregationService): JsonResponse
|
||||||
{
|
{
|
||||||
$member = $this->member($organization);
|
/** @var Member|null $member */
|
||||||
/** @var Member|null $memberFilter */
|
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
||||||
$memberFilter = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
if ($member !== null && $member->user_id === Auth::id()) {
|
||||||
if ($memberFilter !== null && $memberFilter->getKey() === $member->getKey()) {
|
|
||||||
$this->checkPermission($organization, 'time-entries:view:own');
|
$this->checkPermission($organization, 'time-entries:view:own');
|
||||||
} else {
|
} else {
|
||||||
$this->checkPermission($organization, 'time-entries:view:all');
|
$this->checkPermission($organization, 'time-entries:view:all');
|
||||||
@@ -441,7 +437,7 @@ class TimeEntryController extends Controller
|
|||||||
|
|
||||||
$group = $request->getGroup();
|
$group = $request->getGroup();
|
||||||
$subGroup = $request->getSubGroup();
|
$subGroup = $request->getSubGroup();
|
||||||
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $memberFilter);
|
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
||||||
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
|
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
|
||||||
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
||||||
|
|
||||||
@@ -584,7 +580,7 @@ class TimeEntryController extends Controller
|
|||||||
{
|
{
|
||||||
/** @var Member $member */
|
/** @var Member $member */
|
||||||
$member = Member::query()->findOrFail($request->input('member_id'));
|
$member = Member::query()->findOrFail($request->input('member_id'));
|
||||||
if ($member->getKey() === $this->member($organization)->getKey()) {
|
if ($member->user_id === Auth::id()) {
|
||||||
$this->checkPermission($organization, 'time-entries:create:own');
|
$this->checkPermission($organization, 'time-entries:create:own');
|
||||||
} else {
|
} else {
|
||||||
$this->checkPermission($organization, 'time-entries:create:all');
|
$this->checkPermission($organization, 'time-entries:create:all');
|
||||||
@@ -631,10 +627,9 @@ class TimeEntryController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function update(Organization $organization, TimeEntry $timeEntry, TimeEntryUpdateRequest $request): JsonResource
|
public function update(Organization $organization, TimeEntry $timeEntry, TimeEntryUpdateRequest $request): JsonResource
|
||||||
{
|
{
|
||||||
$member = $this->member($organization);
|
/** @var Member|null $member */
|
||||||
/** @var Member|null $newMember */
|
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
||||||
$newMember = $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())) {
|
||||||
if ($timeEntry->member_id === $member->getKey() && ($newMember === null || $newMember->getKey() === $member->getKey())) {
|
|
||||||
$this->checkPermission($organization, 'time-entries:update:own', $timeEntry);
|
$this->checkPermission($organization, 'time-entries:update:own', $timeEntry);
|
||||||
} else {
|
} else {
|
||||||
$this->checkPermission($organization, 'time-entries:update:all', $timeEntry);
|
$this->checkPermission($organization, 'time-entries:update:all', $timeEntry);
|
||||||
@@ -666,10 +661,6 @@ class TimeEntryController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$timeEntry->fill($request->validated());
|
$timeEntry->fill($request->validated());
|
||||||
if ($newMember !== null) {
|
|
||||||
$timeEntry->member()->associate($newMember);
|
|
||||||
$timeEntry->user()->associate($newMember->user);
|
|
||||||
}
|
|
||||||
$timeEntry->description = $request->input('description', $timeEntry->description) ?? '';
|
$timeEntry->description = $request->input('description', $timeEntry->description) ?? '';
|
||||||
$timeEntry->setComputedAttributeValue('billable_rate');
|
$timeEntry->setComputedAttributeValue('billable_rate');
|
||||||
$timeEntry->save();
|
$timeEntry->save();
|
||||||
@@ -699,7 +690,6 @@ class TimeEntryController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function updateMultiple(Organization $organization, TimeEntryUpdateMultipleRequest $request): JsonResponse
|
public function updateMultiple(Organization $organization, TimeEntryUpdateMultipleRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
$member = $this->member($organization);
|
|
||||||
$this->checkAnyPermission($organization, ['time-entries:update:all', 'time-entries:update:own']);
|
$this->checkAnyPermission($organization, ['time-entries:update:all', 'time-entries:update:own']);
|
||||||
$canAccessAll = $this->hasPermission($organization, 'time-entries:update:all');
|
$canAccessAll = $this->hasPermission($organization, 'time-entries:update:all');
|
||||||
|
|
||||||
@@ -724,9 +714,6 @@ class TimeEntryController extends Controller
|
|||||||
throw new AuthorizationException;
|
throw new AuthorizationException;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var Member|null $newMember */
|
|
||||||
$newMember = isset($changes['member_id']) ? Member::query()->findOrFail($changes['member_id']) : null;
|
|
||||||
|
|
||||||
$project = null;
|
$project = null;
|
||||||
$client = null;
|
$client = null;
|
||||||
$overwriteClient = false;
|
$overwriteClient = false;
|
||||||
@@ -753,7 +740,7 @@ class TimeEntryController extends Controller
|
|||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (! $canAccessAll && $timeEntry->member_id !== $member->getKey()) {
|
if (! $canAccessAll && $timeEntry->user_id !== Auth::id()) {
|
||||||
$error->push($id);
|
$error->push($id);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -763,10 +750,6 @@ class TimeEntryController extends Controller
|
|||||||
$oldTask = $timeEntry->task;
|
$oldTask = $timeEntry->task;
|
||||||
|
|
||||||
$timeEntry->fill($changes);
|
$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 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) {
|
if ($oldProject !== null && $project !== null && $oldProject->isNot($project) && $task === null) {
|
||||||
$timeEntry->task()->disassociate();
|
$timeEntry->task()->disassociate();
|
||||||
@@ -807,8 +790,7 @@ class TimeEntryController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function destroy(Organization $organization, TimeEntry $timeEntry): JsonResponse
|
public function destroy(Organization $organization, TimeEntry $timeEntry): JsonResponse
|
||||||
{
|
{
|
||||||
$member = $this->member($organization);
|
if ($timeEntry->member->user_id === Auth::id()) {
|
||||||
if ($timeEntry->member_id === $member->getKey()) {
|
|
||||||
$this->checkPermission($organization, 'time-entries:delete:own', $timeEntry);
|
$this->checkPermission($organization, 'time-entries:delete:own', $timeEntry);
|
||||||
} else {
|
} else {
|
||||||
$this->checkPermission($organization, 'time-entries:delete:all', $timeEntry);
|
$this->checkPermission($organization, 'time-entries:delete:all', $timeEntry);
|
||||||
@@ -865,7 +847,7 @@ class TimeEntryController extends Controller
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $canDeleteAll && $timeEntry->member_id !== $this->member($organization)->getKey()) {
|
if (! $canDeleteAll && $timeEntry->user_id !== Auth::id()) {
|
||||||
$error->push($id);
|
$error->push($id);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -189,9 +189,7 @@ ENV WITH_HORIZON=false \
|
|||||||
WITH_SCHEDULER=false \
|
WITH_SCHEDULER=false \
|
||||||
WITH_REVERB=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
|
#COPY --link --chown=${WWWUSER}:${WWWUSER} --from=build ${ROOT}/public public
|
||||||
|
|
||||||
RUN mkdir -p \
|
RUN mkdir -p \
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Billing": {
|
"Billing": {
|
||||||
"repository": "solidtime-io/extension-billing",
|
"repository": "solidtime-io/extension-billing",
|
||||||
"ref": "v0.0.3"
|
"ref": "v0.0.1"
|
||||||
},
|
},
|
||||||
"Services": {
|
"Services": {
|
||||||
"repository": "solidtime-io/extension-services",
|
"repository": "solidtime-io/extension-services",
|
||||||
|
|||||||
4395
package-lock.json
generated
4395
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@@ -26,13 +26,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.5",
|
"@eslint/eslintrc": "^3.3.5",
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^9.39.4",
|
||||||
"@inertiajs/vue3": "^3.6.1",
|
"@inertiajs/vue3": "^2.3.23",
|
||||||
"@playwright/test": "^1.60.0",
|
"@playwright/test": "^1.60.0",
|
||||||
"@tailwindcss/forms": "^0.5.11",
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/chroma-js": "^3.1.2",
|
"@types/chroma-js": "^3.1.2",
|
||||||
"@types/node": "^26.1.1",
|
"@types/node": "^22.19.19",
|
||||||
"@vitejs/plugin-vue": "^6.0.6",
|
"@vitejs/plugin-vue": "^6.0.6",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
@@ -40,14 +40,14 @@
|
|||||||
"axios": "^1.16.0",
|
"axios": "^1.16.0",
|
||||||
"eslint-plugin-unused-imports": "^4.4.1",
|
"eslint-plugin-unused-imports": "^4.4.1",
|
||||||
"happy-dom": "^20.8.9",
|
"happy-dom": "^20.8.9",
|
||||||
"laravel-vite-plugin": "^3.1.3",
|
"laravel-vite-plugin": "^2.1.0",
|
||||||
"openapi-zod-client": "^1.18.3",
|
"openapi-zod-client": "^1.18.3",
|
||||||
"postcss": "^8.5.14",
|
"postcss": "^8.5.14",
|
||||||
"postcss-import": "^16.1.1",
|
"postcss-import": "^15.1.0",
|
||||||
"postcss-nesting": "^14.0.0",
|
"postcss-nesting": "^12.1.5",
|
||||||
"tailwindcss": "^4.3.3",
|
"tailwindcss": "^3.4.19",
|
||||||
"typescript": "^7.0.2",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^8.1.5",
|
"vite": "^7.3.3",
|
||||||
"vite-plugin-checker": "^0.12.0",
|
"vite-plugin-checker": "^0.12.0",
|
||||||
"vitest": "^4.1.4",
|
"vitest": "^4.1.4",
|
||||||
"vue": "^3.5.34",
|
"vue": "^3.5.34",
|
||||||
@@ -55,14 +55,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/core": "^1.7.5",
|
"@floating-ui/core": "^1.7.5",
|
||||||
"@floating-ui/vue": "^2.0.1",
|
"@floating-ui/vue": "^1.1.11",
|
||||||
"@heroicons/vue": "^2.2.0",
|
"@heroicons/vue": "^2.2.0",
|
||||||
"@lucide/vue": "^1.14.0",
|
"@lucide/vue": "^1.14.0",
|
||||||
"@rushstack/eslint-patch": "^1.16.1",
|
"@rushstack/eslint-patch": "^1.16.1",
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
"@tanstack/vue-form": "^1.32.0",
|
"@tanstack/vue-form": "^1.32.0",
|
||||||
"@tanstack/vue-query": "^5.100.10",
|
"@tanstack/vue-query": "^5.100.10",
|
||||||
"@tanstack/vue-query-devtools": "^6.1.36",
|
"@tanstack/vue-query-devtools": "^5.91.0",
|
||||||
"@tanstack/vue-table": "^8.21.3",
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
"@tanstack/vue-virtual": "^3.13.24",
|
"@tanstack/vue-virtual": "^3.13.24",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
@@ -77,10 +77,10 @@
|
|||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"focus-trap": "^8.2.0",
|
"focus-trap": "^8.2.0",
|
||||||
"parse-duration": "^2.1.6",
|
"parse-duration": "^2.1.6",
|
||||||
"pinia": "^4.0.2",
|
"pinia": "^3.0.4",
|
||||||
"radix-vue": "^1.9.17",
|
"radix-vue": "^1.9.17",
|
||||||
"reka-ui": "^2.9.7",
|
"reka-ui": "^2.9.7",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^2.6.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vue-draggable-plus": "^0.6.1",
|
"vue-draggable-plus": "^0.6.1",
|
||||||
"vue-echarts": "^8.0.1",
|
"vue-echarts": "^8.0.1",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"author": "solidtime",
|
"author": "solidtime",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite-plugin-dts": "^5.0.3"
|
"vite-plugin-dts": "^4.5.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@zodios/core": "^10.9.6",
|
"@zodios/core": "^10.9.6",
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chroma-js": "^3.1.2",
|
"@types/chroma-js": "^3.1.2",
|
||||||
"@zodios/core": "^10.9.6",
|
"@zodios/core": "^10.9.6",
|
||||||
"vite-plugin-dts": "^5.0.3",
|
"vite-plugin-dts": "^4.5.4",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ use Illuminate\Http\Client\ConnectionException;
|
|||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use Tests\TestCase;
|
use Tests\TestCaseWithDatabase;
|
||||||
|
|
||||||
#[CoversClass(SelfHostTelemetryCommand::class)]
|
#[CoversClass(SelfHostTelemetryCommand::class)]
|
||||||
#[CoversClass(ApiService::class)]
|
#[CoversClass(ApiService::class)]
|
||||||
class SelfHostTelemetryCommandTest extends TestCase
|
class SelfHostTelemetryCommandTest extends TestCaseWithDatabase
|
||||||
{
|
{
|
||||||
public function test_telemetry_sends_data_to_telemetry_endpoint_of_solidtime_cloud(): void
|
public function test_telemetry_sends_data_to_telemetry_endpoint_of_solidtime_cloud(): void
|
||||||
{
|
{
|
||||||
@@ -28,9 +28,12 @@ class SelfHostTelemetryCommandTest extends TestCase
|
|||||||
$exitCode = $this->withoutMockingConsoleOutput()->artisan('self-host:telemetry');
|
$exitCode = $this->withoutMockingConsoleOutput()->artisan('self-host:telemetry');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
$this->assertSame(Command::SUCCESS, $exitCode);
|
|
||||||
$output = Artisan::output();
|
$output = Artisan::output();
|
||||||
$this->assertSame('', $output);
|
$this->assertSame('', $output);
|
||||||
|
if ($exitCode !== Command::SUCCESS) {
|
||||||
|
dump($output);
|
||||||
|
}
|
||||||
|
$this->assertSame(Command::SUCCESS, $exitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_telemetry_sends_fails_gracefully_if_response_has_error_status_code(): void
|
public function test_telemetry_sends_fails_gracefully_if_response_has_error_status_code(): void
|
||||||
|
|||||||
@@ -92,30 +92,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
$response->assertJsonPath('data.0.id', $timeEntry->getKey());
|
$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
|
public function test_index_endpoint_fails_if_user_filter_is_from_different_organization(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -150,10 +126,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
Passport::actingAs($data->user);
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
$response = $this->getJson(route('api.v1.time-entries.index', [
|
$response = $this->getJson(route('api.v1.time-entries.index', [$data->organization->getKey(), 'user_id' => $user->getKey()]));
|
||||||
$data->organization->getKey(),
|
|
||||||
'member_id' => $member->getKey(),
|
|
||||||
]));
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
$this->assertResponseCode($response, 200);
|
$this->assertResponseCode($response, 200);
|
||||||
@@ -1799,29 +1772,6 @@ 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
|
public function test_aggregate_endpoint_groups_by_two_groups(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -2869,32 +2819,6 @@ 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
|
public function test_update_endpoint_can_update_project_and_automatically_set_client(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -3231,40 +3155,6 @@ 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
|
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
|
// Arrange
|
||||||
@@ -3676,46 +3566,6 @@ 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
|
public function test_update_multiple_updates_sets_description_to_empty_if_the_client_sends_null(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -3762,51 +3612,6 @@ 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
|
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
|
// Arrange
|
||||||
|
|||||||
Reference in New Issue
Block a user