mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-09 16:52:17 +01:00
Compare commits
41 Commits
feature/ba
...
feature/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6b45d3e35 | ||
|
|
b11672732b | ||
|
|
97dcadc795 | ||
|
|
e7fa414c06 | ||
|
|
43073b5be2 | ||
|
|
9589c9106d | ||
|
|
8a0d2235a8 | ||
|
|
38f38790d5 | ||
|
|
e3cfc155b8 | ||
|
|
4b726635b2 | ||
|
|
e1185af281 | ||
|
|
f9c0d64f82 | ||
|
|
3d58f570bd | ||
|
|
400bc434b9 | ||
|
|
2ab28001be | ||
|
|
62d2f4bf4e | ||
|
|
3d4b20f7c8 | ||
|
|
155ed62fcc | ||
|
|
5daa6f2a25 | ||
|
|
47aa65d959 | ||
|
|
b0e638c28b | ||
|
|
24b62d4643 | ||
|
|
dd928508fd | ||
|
|
ead9cf2185 | ||
|
|
7578beb271 | ||
|
|
dc21ac8352 | ||
|
|
4de7868851 | ||
|
|
ffc016a1ec | ||
|
|
be69626970 | ||
|
|
f1dce88dab | ||
|
|
15411ec0c8 | ||
|
|
48f09421d0 | ||
|
|
36caadeb14 | ||
|
|
b4edcaa2dc | ||
|
|
a3dda8b03c | ||
|
|
d64f0c52be | ||
|
|
c80d51c2e1 | ||
|
|
46dea00b34 | ||
|
|
16fed4a2b7 | ||
|
|
9a2af2e743 | ||
|
|
2e3a517502 |
3
.github/workflows/build-private.yml
vendored
3
.github/workflows/build-private.yml
vendored
@@ -10,6 +10,8 @@ on:
|
|||||||
- '.github/workflows/build-private.yml'
|
- '.github/workflows/build-private.yml'
|
||||||
- 'docker/prod/**'
|
- 'docker/prod/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
name: Build - Private
|
name: Build - Private
|
||||||
jobs:
|
jobs:
|
||||||
@@ -17,6 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Check out code"
|
- name: "Check out code"
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|||||||
16
.github/workflows/build-public.yml
vendored
16
.github/workflows/build-public.yml
vendored
@@ -11,6 +11,12 @@ on:
|
|||||||
- 'docker/prod/**'
|
- 'docker/prod/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
attestations: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DOCKERHUB_REPO: solidtime/solidtime
|
DOCKERHUB_REPO: solidtime/solidtime
|
||||||
GHCR_REPO: ghcr.io/solidtime-io/solidtime
|
GHCR_REPO: ghcr.io/solidtime-io/solidtime
|
||||||
@@ -26,11 +32,6 @@ jobs:
|
|||||||
- runs-on: "ubuntu-24.04"
|
- runs-on: "ubuntu-24.04"
|
||||||
platform: "linux/amd64"
|
platform: "linux/amd64"
|
||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
contents: read
|
|
||||||
attestations: write
|
|
||||||
id-token: write
|
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -163,11 +164,6 @@ jobs:
|
|||||||
|
|
||||||
merge:
|
merge:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
contents: read
|
|
||||||
attestations: write
|
|
||||||
id-token: write
|
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
needs:
|
needs:
|
||||||
- build
|
- build
|
||||||
|
|||||||
3
.github/workflows/generate-api-docs.yml
vendored
3
.github/workflows/generate-api-docs.yml
vendored
@@ -3,6 +3,9 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
api_docs:
|
api_docs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
2
.github/workflows/npm-build.yml
vendored
2
.github/workflows/npm-build.yml
vendored
@@ -1,6 +1,8 @@
|
|||||||
name: NPM Build
|
name: NPM Build
|
||||||
|
|
||||||
on: [push]
|
on: [push]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
2
.github/workflows/npm-lint.yml
vendored
2
.github/workflows/npm-lint.yml
vendored
@@ -1,6 +1,8 @@
|
|||||||
name: NPM Lint
|
name: NPM Lint
|
||||||
|
|
||||||
on: [push]
|
on: [push]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
2
.github/workflows/npm-publish-api.yml
vendored
2
.github/workflows/npm-publish-api.yml
vendored
@@ -1,6 +1,8 @@
|
|||||||
name: Publish API package to NPM
|
name: Publish API package to NPM
|
||||||
on:
|
on:
|
||||||
workflow_dispatch
|
workflow_dispatch
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
2
.github/workflows/npm-publish-ui.yml
vendored
2
.github/workflows/npm-publish-ui.yml
vendored
@@ -1,6 +1,8 @@
|
|||||||
name: Publish UI package to NPM
|
name: Publish UI package to NPM
|
||||||
on:
|
on:
|
||||||
workflow_dispatch
|
workflow_dispatch
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
3
.github/workflows/npm-typecheck.yml
vendored
3
.github/workflows/npm-typecheck.yml
vendored
@@ -1,7 +1,8 @@
|
|||||||
name: NPM Typecheck
|
name: NPM Typecheck
|
||||||
|
|
||||||
on: [push]
|
on: [push]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
2
.github/workflows/phpstan.yml
vendored
2
.github/workflows/phpstan.yml
vendored
@@ -1,5 +1,7 @@
|
|||||||
name: Static code analysis (PHPStan)
|
name: Static code analysis (PHPStan)
|
||||||
on: push
|
on: push
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
jobs:
|
jobs:
|
||||||
phpstan:
|
phpstan:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
9
.github/workflows/phpunit.yml
vendored
9
.github/workflows/phpunit.yml
vendored
@@ -1,13 +1,18 @@
|
|||||||
name: PHPUnit Tests
|
name: PHPUnit Tests
|
||||||
on: push
|
on: push
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
jobs:
|
jobs:
|
||||||
phpunit:
|
phpunit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
postgres_version: [ 15, 16, 17 ]
|
||||||
|
|
||||||
services:
|
services:
|
||||||
pgsql_test:
|
pgsql_test:
|
||||||
image: postgres:15
|
image: postgres:${{ matrix.postgres_version }}
|
||||||
env:
|
env:
|
||||||
PGPASSWORD: 'root'
|
PGPASSWORD: 'root'
|
||||||
POSTGRES_DB: 'laravel'
|
POSTGRES_DB: 'laravel'
|
||||||
@@ -63,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.4.2
|
uses: codecov/codecov-action@v5.4.3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
slug: solidtime-io/solidtime
|
slug: solidtime-io/solidtime
|
||||||
|
|||||||
2
.github/workflows/pint.yml
vendored
2
.github/workflows/pint.yml
vendored
@@ -1,5 +1,7 @@
|
|||||||
name: PHP Linting
|
name: PHP Linting
|
||||||
on: push
|
on: push
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
jobs:
|
jobs:
|
||||||
pint:
|
pint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
2
.github/workflows/playwright.yml
vendored
2
.github/workflows/playwright.yml
vendored
@@ -1,5 +1,7 @@
|
|||||||
name: Playwright Tests
|
name: Playwright Tests
|
||||||
on: [push]
|
on: [push]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class CreateNewUser implements CreatesNewUsers
|
|||||||
/**
|
/**
|
||||||
* Create a newly registered user.
|
* Create a newly registered user.
|
||||||
*
|
*
|
||||||
* @param array<string, string> $input
|
* @param array<string, mixed> $input
|
||||||
*
|
*
|
||||||
* @throws ValidationException
|
* @throws ValidationException
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ namespace App\Actions\Fortify;
|
|||||||
|
|
||||||
use App\Enums\Weekday;
|
use App\Enums\Weekday;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
@@ -59,8 +58,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
|||||||
$user->updateProfilePhoto($input['photo']);
|
$user->updateProfilePhoto($input['photo']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($input['email'] !== $user->email &&
|
if ($input['email'] !== $user->email) {
|
||||||
$user instanceof MustVerifyEmail) {
|
|
||||||
$user->forceFill([
|
$user->forceFill([
|
||||||
'name' => $input['name'],
|
'name' => $input['name'],
|
||||||
'email' => $input['email'],
|
'email' => $input['email'],
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class AddOrganizationMember implements AddsTeamMembers
|
|||||||
*/
|
*/
|
||||||
protected function rules(): array
|
protected function rules(): array
|
||||||
{
|
{
|
||||||
return array_filter([
|
return [
|
||||||
'email' => [
|
'email' => [
|
||||||
'required',
|
'required',
|
||||||
'email',
|
'email',
|
||||||
@@ -75,7 +75,7 @@ class AddOrganizationMember implements AddsTeamMembers
|
|||||||
Role::Employee->value,
|
Role::Employee->value,
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
]);
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Console\Commands\Auth;
|
||||||
|
|
||||||
|
use App\Mail\AuthApiTokenExpirationReminderMail;
|
||||||
|
use App\Mail\AuthApiTokenExpiredMail;
|
||||||
|
use App\Models\Passport\Token;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
|
||||||
|
class AuthSendReminderForExpiringApiTokensCommand extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'auth:send-mails-expiring-api-tokens '.
|
||||||
|
' { --dry-run : Do not actually send emails or save anything to the database, just output what would happen }';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Sends emails about expiring API tokens, one week before and when they expired.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$dryRun = (bool) $this->option('dry-run');
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->comment('Running in dry-run mode. No emails will be sent and nothing will be saved to the database.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->comment('Sending reminder emails about expiring API tokens...');
|
||||||
|
$sentMails = 0;
|
||||||
|
Token::query()
|
||||||
|
->where('expires_at', '<=', Carbon::now()->addDays(7))
|
||||||
|
->whereNull('reminder_sent_at')
|
||||||
|
->with([
|
||||||
|
'client',
|
||||||
|
'user',
|
||||||
|
])
|
||||||
|
->whereHas('user', function (Builder $query): void {
|
||||||
|
/** @var Builder<User> $query */
|
||||||
|
$query->where('is_placeholder', '=', false);
|
||||||
|
})
|
||||||
|
->isApiToken(true)
|
||||||
|
->orderBy('created_at', 'asc')
|
||||||
|
->chunk(500, function (Collection $tokens) use ($dryRun, &$sentMails): void {
|
||||||
|
/** @var Collection<int, Token> $tokens */
|
||||||
|
foreach ($tokens as $token) {
|
||||||
|
$user = $token->user;
|
||||||
|
$this->info('Start sending email to user "'.$user->email.'" ('.$user->getKey().') reminding about API token '.$token->getKey());
|
||||||
|
$sentMails++;
|
||||||
|
if (! $dryRun) {
|
||||||
|
Mail::to($user->email)
|
||||||
|
->queue(new AuthApiTokenExpirationReminderMail($token, $user));
|
||||||
|
$token->reminder_sent_at = Carbon::now();
|
||||||
|
$token->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$this->comment('Finished sending '.$sentMails.' expiring API token emails...');
|
||||||
|
|
||||||
|
$this->comment('Sent emails about expired API tokens');
|
||||||
|
$sentMails = 0;
|
||||||
|
Token::query()
|
||||||
|
->where('expires_at', '<=', Carbon::now())
|
||||||
|
->whereNull('expired_info_sent_at')
|
||||||
|
->with([
|
||||||
|
'client',
|
||||||
|
'user',
|
||||||
|
])
|
||||||
|
->whereHas('user', function (Builder $query): void {
|
||||||
|
/** @var Builder<User> $query */
|
||||||
|
$query->where('is_placeholder', '=', false);
|
||||||
|
})
|
||||||
|
->isApiToken(true)
|
||||||
|
->orderBy('created_at', 'asc')
|
||||||
|
->chunk(500, function (Collection $tokens) use ($dryRun, &$sentMails): void {
|
||||||
|
/** @var Collection<int, Token> $tokens */
|
||||||
|
foreach ($tokens as $token) {
|
||||||
|
$user = $token->user;
|
||||||
|
$this->info('Start sending email to user "'.$user->email.'" ('.$user->getKey().') about expired API token '.$token->getKey());
|
||||||
|
$sentMails++;
|
||||||
|
if (! $dryRun) {
|
||||||
|
Mail::to($user->email)
|
||||||
|
->queue(new AuthApiTokenExpiredMail($token, $user));
|
||||||
|
$token->expired_info_sent_at = Carbon::now();
|
||||||
|
$token->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$this->comment('Finished sending '.$sentMails.' expired API token emails...');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
123
app/Console/Commands/SelfHost/SelfHostDatabaseConsistency.php
Normal file
123
app/Console/Commands/SelfHost/SelfHostDatabaseConsistency.php
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Console\Commands\SelfHost;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Database\Query\Builder;
|
||||||
|
use Illuminate\Database\Query\JoinClause;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class SelfHostDatabaseConsistency extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'self-host:database-consistency';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$hadAProblem = false;
|
||||||
|
|
||||||
|
// Task need to be part of project in time entries
|
||||||
|
$problems = DB::table('time_entries')
|
||||||
|
->select(['time_entries.id as id'])
|
||||||
|
->join('tasks', 'time_entries.task_id', '=', 'tasks.id')
|
||||||
|
->where('tasks.project_id', '!=', DB::raw('time_entries.project_id'))
|
||||||
|
->get();
|
||||||
|
$this->logProblems($problems, 'Time entries have a task that does not belong to the project of the time entry', $hadAProblem);
|
||||||
|
|
||||||
|
// Client id is the client id of the project
|
||||||
|
$problems = DB::table('time_entries')
|
||||||
|
->select(['time_entries.id as id'])
|
||||||
|
->join('projects', 'time_entries.project_id', '=', 'projects.id')
|
||||||
|
->where(DB::raw('coalesce(projects.client_id::varchar, \'\')'), '!=', DB::raw('coalesce(time_entries.client_id::varchar, \'\')'))
|
||||||
|
->get();
|
||||||
|
$this->logProblems($problems, 'Time entries have a client that does not match the client of the project', $hadAProblem);
|
||||||
|
|
||||||
|
// Client id can only be not null if the project id is not null
|
||||||
|
$problems = DB::table('time_entries')
|
||||||
|
->select(['time_entries.id as id'])
|
||||||
|
->whereNotNull('client_id')
|
||||||
|
->whereNull('project_id')
|
||||||
|
->get();
|
||||||
|
$this->logProblems($problems, 'Time entries have a client but no project', $hadAProblem);
|
||||||
|
|
||||||
|
// Every user needs to be a member of at least one organization
|
||||||
|
$problems = DB::table('users')
|
||||||
|
->select(['users.id as id'])
|
||||||
|
->leftJoin('members', 'users.id', '=', 'members.user_id')
|
||||||
|
->whereNull('members.id')
|
||||||
|
->get();
|
||||||
|
$this->logProblems($problems, 'Users are not member of any organization', $hadAProblem);
|
||||||
|
|
||||||
|
// Every organization needs at least an owner
|
||||||
|
$problems = DB::table('organizations')
|
||||||
|
->select(['organizations.id as id'])
|
||||||
|
->leftJoin('members', function (JoinClause $join): void {
|
||||||
|
$join->on('organizations.id', '=', 'members.organization_id')
|
||||||
|
->where('members.role', '=', 'owner');
|
||||||
|
})
|
||||||
|
->whereNull('members.id')
|
||||||
|
->get();
|
||||||
|
$this->logProblems($problems, 'Organizations without an owner', $hadAProblem);
|
||||||
|
|
||||||
|
// Every member can only have one running time entry
|
||||||
|
$problems = DB::table('time_entries')
|
||||||
|
->select(['user_id as id'])
|
||||||
|
->whereNull('end')
|
||||||
|
->groupBy('user_id')
|
||||||
|
->havingRaw('count(*) > 1')
|
||||||
|
->get(['user_id', DB::raw('count(*) as count')]);
|
||||||
|
$this->logProblems($problems, 'Users with more than one running time entry', $hadAProblem);
|
||||||
|
|
||||||
|
// Users have a current organization that they are not a member of
|
||||||
|
$problems = DB::table('users')
|
||||||
|
->select(['users.id as id'])
|
||||||
|
->whereNotNull('current_team_id')
|
||||||
|
->whereNotIn('current_team_id', function (Builder $query): void {
|
||||||
|
$query->select('organization_id')
|
||||||
|
->from('members')
|
||||||
|
->whereColumn('members.user_id', 'users.id');
|
||||||
|
})->get();
|
||||||
|
$this->logProblems($problems, 'Users have a current organization that they are not a member of', $hadAProblem);
|
||||||
|
|
||||||
|
return $hadAProblem ? self::FAILURE : self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Collection<int, \stdClass> $problems
|
||||||
|
*/
|
||||||
|
private function logProblems(Collection $problems, string $message, bool &$hadAProblem): void
|
||||||
|
{
|
||||||
|
$message = 'Consistency problem: '.$message;
|
||||||
|
if ($problems->isNotEmpty()) {
|
||||||
|
$ids = $problems->pluck('id');
|
||||||
|
$hadAProblem = true;
|
||||||
|
Log::error($message, [
|
||||||
|
'ids' => $ids,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$error = $message;
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
$error .= "\n - ".$id;
|
||||||
|
}
|
||||||
|
$this->error($error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,10 @@ class Kernel extends ConsoleKernel
|
|||||||
->when(fn (): bool => config('scheduling.tasks.time_entry_send_still_running_mails'))
|
->when(fn (): bool => config('scheduling.tasks.time_entry_send_still_running_mails'))
|
||||||
->everyTenMinutes();
|
->everyTenMinutes();
|
||||||
|
|
||||||
|
$schedule->command('auth:send-mails-expiring-api-tokens')
|
||||||
|
->when(fn (): bool => config('scheduling.tasks.auth_send_mails_expiring_api_tokens'))
|
||||||
|
->everyTenMinutes();
|
||||||
|
|
||||||
$schedule->command('self-host:check-for-update')
|
$schedule->command('self-host:check-for-update')
|
||||||
->when(fn (): bool => config('scheduling.tasks.self_hosting_check_for_update'))
|
->when(fn (): bool => config('scheduling.tasks.self_hosting_check_for_update'))
|
||||||
->twiceDaily();
|
->twiceDaily();
|
||||||
@@ -25,6 +29,10 @@ class Kernel extends ConsoleKernel
|
|||||||
$schedule->command('self-host:telemetry')
|
$schedule->command('self-host:telemetry')
|
||||||
->when(fn (): bool => config('scheduling.tasks.self_hosting_telemetry'))
|
->when(fn (): bool => config('scheduling.tasks.self_hosting_telemetry'))
|
||||||
->twiceDaily();
|
->twiceDaily();
|
||||||
|
|
||||||
|
$schedule->command('self-host:database-consistency')
|
||||||
|
->when(fn (): bool => config('scheduling.tasks.self_hosting_database_consistency'))
|
||||||
|
->everySixHours();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
16
app/Enums/TimeEntryRoundingType.php
Normal file
16
app/Enums/TimeEntryRoundingType.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
||||||
|
|
||||||
|
enum TimeEntryRoundingType: string
|
||||||
|
{
|
||||||
|
use LaravelEnumHelper;
|
||||||
|
|
||||||
|
case Up = 'up';
|
||||||
|
case Down = 'down';
|
||||||
|
case Nearest = 'nearest';
|
||||||
|
}
|
||||||
14
app/Events/DatabaseSeederAfterSeed.php
Normal file
14
app/Events/DatabaseSeederAfterSeed.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
|
||||||
|
class DatabaseSeederAfterSeed
|
||||||
|
{
|
||||||
|
use Dispatchable;
|
||||||
|
|
||||||
|
public function __construct() {}
|
||||||
|
}
|
||||||
14
app/Events/DatabaseSeederBeforeDelete.php
Normal file
14
app/Events/DatabaseSeederBeforeDelete.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
|
||||||
|
class DatabaseSeederBeforeDelete
|
||||||
|
{
|
||||||
|
use Dispatchable;
|
||||||
|
|
||||||
|
public function __construct() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Exceptions\Api;
|
||||||
|
|
||||||
|
class InvitationForTheEmailAlreadyExistsApiException extends ApiException
|
||||||
|
{
|
||||||
|
public const string KEY = 'invitation_for_the_email_already_exists';
|
||||||
|
}
|
||||||
@@ -41,9 +41,7 @@ class PaginatedResourceCollectionTypeToSchema extends TypeToSchemaExtension
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! ($collectingType = $this->openApiTransformer->transform($collectingClassType))) {
|
$collectingType = $this->openApiTransformer->transform($collectingClassType);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$newType = new OpenApiObjectType;
|
$newType = new OpenApiObjectType;
|
||||||
$newType->addProperty('data', (new ArrayType)->setItems($collectingType));
|
$newType->addProperty('data', (new ArrayType)->setItems($collectingType));
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use Filament\Resources\Resource;
|
|||||||
use Filament\Tables\Actions\Action;
|
use Filament\Tables\Actions\Action;
|
||||||
use Filament\Tables\Actions\BulkAction;
|
use Filament\Tables\Actions\BulkAction;
|
||||||
use Filament\Tables\Actions\DeleteAction;
|
use Filament\Tables\Actions\DeleteAction;
|
||||||
|
use Filament\Tables\Actions\DeleteBulkAction;
|
||||||
use Filament\Tables\Actions\ViewAction;
|
use Filament\Tables\Actions\ViewAction;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
@@ -75,7 +76,8 @@ class FailedJobResource extends Resource
|
|||||||
->filters([])
|
->filters([])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
BulkAction::make('retry')
|
BulkAction::make('retry')
|
||||||
->label('Retry')
|
->icon('heroicon-o-arrow-path')
|
||||||
|
->label('Retry selected')
|
||||||
->requiresConfirmation()
|
->requiresConfirmation()
|
||||||
->action(function (Collection $records): void {
|
->action(function (Collection $records): void {
|
||||||
/** @var FailedJob $record */
|
/** @var FailedJob $record */
|
||||||
@@ -87,11 +89,13 @@ class FailedJobResource extends Resource
|
|||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
}),
|
}),
|
||||||
|
DeleteBulkAction::make(),
|
||||||
])
|
])
|
||||||
->actions([
|
->actions([
|
||||||
DeleteAction::make('Delete'),
|
DeleteAction::make(),
|
||||||
ViewAction::make('View'),
|
ViewAction::make(),
|
||||||
Action::make('retry')
|
Action::make('retry')
|
||||||
|
->icon('heroicon-o-arrow-path')
|
||||||
->label('Retry')
|
->label('Retry')
|
||||||
->requiresConfirmation()
|
->requiresConfirmation()
|
||||||
->action(function (FailedJob $record): void {
|
->action(function (FailedJob $record): void {
|
||||||
@@ -109,7 +113,6 @@ class FailedJobResource extends Resource
|
|||||||
return [
|
return [
|
||||||
'index' => ListFailedJobs::route('/'),
|
'index' => ListFailedJobs::route('/'),
|
||||||
'view' => ViewFailedJobs::route('/{record}'),
|
'view' => ViewFailedJobs::route('/{record}'),
|
||||||
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ namespace App\Filament\Resources\FailedJobResource\Pages;
|
|||||||
|
|
||||||
use App\Filament\Resources\FailedJobResource;
|
use App\Filament\Resources\FailedJobResource;
|
||||||
use App\Models\FailedJob;
|
use App\Models\FailedJob;
|
||||||
|
use Filament\Actions\Action;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Pages\Actions\Action;
|
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
|
||||||
@@ -19,7 +19,8 @@ class ListFailedJobs extends ListRecords
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Action::make('retry_all')
|
Action::make('retry_all')
|
||||||
->label('Retry all failed Jobs')
|
->icon('heroicon-o-arrow-path')
|
||||||
|
->label('Retry all')
|
||||||
->requiresConfirmation()
|
->requiresConfirmation()
|
||||||
->action(function (): void {
|
->action(function (): void {
|
||||||
Artisan::call('queue:retry all');
|
Artisan::call('queue:retry all');
|
||||||
@@ -30,7 +31,8 @@ class ListFailedJobs extends ListRecords
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
Action::make('delete_all')
|
Action::make('delete_all')
|
||||||
->label('Delete all failed Jobs')
|
->icon('heroicon-o-trash')
|
||||||
|
->label('Delete all')
|
||||||
->requiresConfirmation()
|
->requiresConfirmation()
|
||||||
->color('danger')
|
->color('danger')
|
||||||
->action(function (): void {
|
->action(function (): void {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
use App\Filament\Resources\TokenResource\Pages;
|
use App\Filament\Resources\TokenResource\Pages;
|
||||||
use App\Models\Passport\Client;
|
|
||||||
use App\Models\Passport\Token;
|
use App\Models\Passport\Token;
|
||||||
use Filament\Forms;
|
use Filament\Forms;
|
||||||
use Filament\Forms\Form;
|
use Filament\Forms\Form;
|
||||||
@@ -40,7 +39,7 @@ class TokenResource extends Resource
|
|||||||
->label('Name')
|
->label('Name')
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
Forms\Components\Select::make('user_id')
|
Forms\Components\Select::make('owner_id')
|
||||||
->label('User')
|
->label('User')
|
||||||
->relationship(name: 'user', titleAttribute: 'name')
|
->relationship(name: 'user', titleAttribute: 'name')
|
||||||
->searchable(['name'])
|
->searchable(['name'])
|
||||||
@@ -79,10 +78,12 @@ class TokenResource extends Resource
|
|||||||
Tables\Columns\TextColumn::make('client.name')
|
Tables\Columns\TextColumn::make('client.name')
|
||||||
->searchable()
|
->searchable()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
Tables\Columns\IconColumn::make('client.personal_access_client')
|
Tables\Columns\IconColumn::make('personal_access_client')
|
||||||
|
->state(function (Token $token): bool {
|
||||||
|
return in_array('personal_access', $token->client->grant_types ?? [], true);
|
||||||
|
})
|
||||||
->boolean()
|
->boolean()
|
||||||
->label('API token?')
|
->label('API token?'),
|
||||||
->sortable(),
|
|
||||||
Tables\Columns\IconColumn::make('revoked')
|
Tables\Columns\IconColumn::make('revoked')
|
||||||
->boolean()
|
->boolean()
|
||||||
->label('Revoked?')
|
->label('Revoked?')
|
||||||
@@ -104,17 +105,11 @@ class TokenResource extends Resource
|
|||||||
->queries(
|
->queries(
|
||||||
true: function (Builder $query) {
|
true: function (Builder $query) {
|
||||||
/** @var Builder<Token> $query */
|
/** @var Builder<Token> $query */
|
||||||
return $query->whereHas('client', function (Builder $query) {
|
return $query->isApiToken();
|
||||||
/** @var Builder<Client> $query */
|
|
||||||
return $query->where('personal_access_client', true);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
false: function (Builder $query) {
|
false: function (Builder $query) {
|
||||||
/** @var Builder<Token> $query */
|
/** @var Builder<Token> $query */
|
||||||
return $query->whereHas('client', function (Builder $query) {
|
return $query->isApiToken(false);
|
||||||
/** @var Builder<Client> $query */
|
|
||||||
return $query->where('personal_access_client', false);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
blank: function (Builder $query) {
|
blank: function (Builder $query) {
|
||||||
/** @var Builder<Token> $query */
|
/** @var Builder<Token> $query */
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use Filament\Tables;
|
|||||||
use Filament\Tables\Filters\TernaryFilter;
|
use Filament\Tables\Filters\TernaryFilter;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||||
@@ -207,6 +208,14 @@ class UserResource extends Resource
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkAction::make('Resend verification email')
|
||||||
|
->icon('heroicon-o-paper-airplane')
|
||||||
|
->action(function (Collection $records): void {
|
||||||
|
foreach ($records as $user) {
|
||||||
|
/** @var User $user */
|
||||||
|
$user->sendEmailVerificationNotification();
|
||||||
|
}
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ use App\Exceptions\Api\PersonalAccessClientIsNotConfiguredException;
|
|||||||
use App\Http\Requests\V1\ApiToken\ApiTokenStoreRequest;
|
use App\Http\Requests\V1\ApiToken\ApiTokenStoreRequest;
|
||||||
use App\Http\Resources\V1\ApiToken\ApiTokenCollection;
|
use App\Http\Resources\V1\ApiToken\ApiTokenCollection;
|
||||||
use App\Http\Resources\V1\ApiToken\ApiTokenWithAccessTokenResource;
|
use App\Http\Resources\V1\ApiToken\ApiTokenWithAccessTokenResource;
|
||||||
|
use App\Models\Passport\Client;
|
||||||
use App\Models\Passport\Token;
|
use App\Models\Passport\Token;
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class ApiTokenController extends Controller
|
class ApiTokenController extends Controller
|
||||||
{
|
{
|
||||||
@@ -28,7 +31,10 @@ class ApiTokenController extends Controller
|
|||||||
$user = $this->user();
|
$user = $this->user();
|
||||||
|
|
||||||
$tokens = $user->tokens()
|
$tokens = $user->tokens()
|
||||||
->where('client_id', '=', config('passport.personal_access_client.id'))
|
->whereHas('client', function (Builder $query): void {
|
||||||
|
/** @var Builder<Client> $query */
|
||||||
|
$query->whereJsonContains('grant_types', 'personal_access');
|
||||||
|
})
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
return new ApiTokenCollection($tokens);
|
return new ApiTokenCollection($tokens);
|
||||||
@@ -48,15 +54,21 @@ class ApiTokenController extends Controller
|
|||||||
{
|
{
|
||||||
$user = $this->user();
|
$user = $this->user();
|
||||||
|
|
||||||
if (config('passport.personal_access_client.id') === null || config('passport.personal_access_client.secret') === null) {
|
try {
|
||||||
|
$token = $user->createToken($request->getName(), ['*']);
|
||||||
|
|
||||||
|
/** @var Token $tokenModel */
|
||||||
|
$tokenModel = $token->getToken();
|
||||||
|
|
||||||
|
return new ApiTokenWithAccessTokenResource($tokenModel, $token->accessToken);
|
||||||
|
} catch (\RuntimeException $exception) {
|
||||||
|
report($exception);
|
||||||
|
if (Str::contains($exception->getMessage(), ['Personal access client not found'])) {
|
||||||
throw new PersonalAccessClientIsNotConfiguredException;
|
throw new PersonalAccessClientIsNotConfiguredException;
|
||||||
}
|
}
|
||||||
|
|
||||||
$token = $user->createToken($request->getName(), ['*']);
|
throw $exception;
|
||||||
/** @var Token $tokenModel */
|
}
|
||||||
$tokenModel = $token->token;
|
|
||||||
|
|
||||||
return new ApiTokenWithAccessTokenResource($tokenModel, $token->accessToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,13 +83,10 @@ class ApiTokenController extends Controller
|
|||||||
{
|
{
|
||||||
$user = $this->user();
|
$user = $this->user();
|
||||||
|
|
||||||
if (config('passport.personal_access_client.id') === null || config('passport.personal_access_client.secret') === null) {
|
|
||||||
throw new PersonalAccessClientIsNotConfiguredException;
|
|
||||||
}
|
|
||||||
if ($apiToken->user_id !== $user->getKey()) {
|
if ($apiToken->user_id !== $user->getKey()) {
|
||||||
throw new AuthorizationException('API token does not belong to user');
|
throw new AuthorizationException('API token does not belong to user');
|
||||||
}
|
}
|
||||||
if ($apiToken->client_id !== config('passport.personal_access_client.id')) {
|
if (! ($apiToken->client?->hasGrantType('personal_access') ?? false)) {
|
||||||
throw new AuthorizationException('API token is not a personal access token');
|
throw new AuthorizationException('API token is not a personal access token');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,13 +106,10 @@ class ApiTokenController extends Controller
|
|||||||
{
|
{
|
||||||
$user = $this->user();
|
$user = $this->user();
|
||||||
|
|
||||||
if (config('passport.personal_access_client.id') === null || config('passport.personal_access_client.secret') === null) {
|
|
||||||
throw new PersonalAccessClientIsNotConfiguredException;
|
|
||||||
}
|
|
||||||
if ($apiToken->user_id !== $user->getKey()) {
|
if ($apiToken->user_id !== $user->getKey()) {
|
||||||
throw new AuthorizationException('API token does not belong to user');
|
throw new AuthorizationException('API token does not belong to user');
|
||||||
}
|
}
|
||||||
if ($apiToken->client_id !== config('passport.personal_access_client.id')) {
|
if (! ($apiToken->client?->hasGrantType('personal_access') ?? false)) {
|
||||||
throw new AuthorizationException('API token is not a personal access token');
|
throw new AuthorizationException('API token is not a personal access token');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Exceptions\Api\InvitationForTheEmailAlreadyExistsApiException;
|
||||||
use App\Exceptions\Api\UserIsAlreadyMemberOfOrganizationApiException;
|
use App\Exceptions\Api\UserIsAlreadyMemberOfOrganizationApiException;
|
||||||
use App\Http\Requests\V1\Invitation\InvitationIndexRequest;
|
use App\Http\Requests\V1\Invitation\InvitationIndexRequest;
|
||||||
use App\Http\Requests\V1\Invitation\InvitationStoreRequest;
|
use App\Http\Requests\V1\Invitation\InvitationStoreRequest;
|
||||||
@@ -50,6 +51,7 @@ class InvitationController extends Controller
|
|||||||
*
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
* @throws UserIsAlreadyMemberOfOrganizationApiException
|
* @throws UserIsAlreadyMemberOfOrganizationApiException
|
||||||
|
* @throws InvitationForTheEmailAlreadyExistsApiException
|
||||||
*
|
*
|
||||||
* @operationId invite
|
* @operationId invite
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ use App\Exceptions\Api\CanNotRemoveOwnerFromOrganization;
|
|||||||
use App\Exceptions\Api\ChangingRoleOfPlaceholderIsNotAllowed;
|
use App\Exceptions\Api\ChangingRoleOfPlaceholderIsNotAllowed;
|
||||||
use App\Exceptions\Api\ChangingRoleToPlaceholderIsNotAllowed;
|
use App\Exceptions\Api\ChangingRoleToPlaceholderIsNotAllowed;
|
||||||
use App\Exceptions\Api\EntityStillInUseApiException;
|
use App\Exceptions\Api\EntityStillInUseApiException;
|
||||||
|
use App\Exceptions\Api\InvitationForTheEmailAlreadyExistsApiException;
|
||||||
use App\Exceptions\Api\OnlyOwnerCanChangeOwnership;
|
use App\Exceptions\Api\OnlyOwnerCanChangeOwnership;
|
||||||
use App\Exceptions\Api\OnlyPlaceholdersCanBeMergedIntoAnotherMember;
|
use App\Exceptions\Api\OnlyPlaceholdersCanBeMergedIntoAnotherMember;
|
||||||
use App\Exceptions\Api\OrganizationNeedsAtLeastOneOwner;
|
use App\Exceptions\Api\OrganizationNeedsAtLeastOneOwner;
|
||||||
use App\Exceptions\Api\ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException;
|
use App\Exceptions\Api\ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException;
|
||||||
use App\Exceptions\Api\UserIsAlreadyMemberOfOrganizationApiException;
|
use App\Exceptions\Api\UserIsAlreadyMemberOfOrganizationApiException;
|
||||||
use App\Exceptions\Api\UserNotPlaceholderApiException;
|
use App\Exceptions\Api\UserNotPlaceholderApiException;
|
||||||
|
use App\Http\Requests\V1\Member\MemberDestroyRequest;
|
||||||
use App\Http\Requests\V1\Member\MemberIndexRequest;
|
use App\Http\Requests\V1\Member\MemberIndexRequest;
|
||||||
use App\Http\Requests\V1\Member\MemberMergeIntoRequest;
|
use App\Http\Requests\V1\Member\MemberMergeIntoRequest;
|
||||||
use App\Http\Requests\V1\Member\MemberUpdateRequest;
|
use App\Http\Requests\V1\Member\MemberUpdateRequest;
|
||||||
@@ -100,11 +102,13 @@ class MemberController extends Controller
|
|||||||
*
|
*
|
||||||
* @operationId removeMember
|
* @operationId removeMember
|
||||||
*/
|
*/
|
||||||
public function destroy(Organization $organization, Member $member, MemberService $memberService): JsonResponse
|
public function destroy(MemberDestroyRequest $request, Organization $organization, Member $member, MemberService $memberService): JsonResponse
|
||||||
{
|
{
|
||||||
$this->checkPermission($organization, 'members:delete', $member);
|
$this->checkPermission($organization, 'members:delete', $member);
|
||||||
|
|
||||||
$memberService->removeMember($member, $organization);
|
$deleteRelated = $request->getDeleteRelated();
|
||||||
|
|
||||||
|
$memberService->removeMember($member, $organization, $deleteRelated);
|
||||||
|
|
||||||
return response()
|
return response()
|
||||||
->json(null, 204);
|
->json(null, 204);
|
||||||
@@ -170,6 +174,7 @@ class MemberController extends Controller
|
|||||||
* @throws UserNotPlaceholderApiException
|
* @throws UserNotPlaceholderApiException
|
||||||
* @throws UserIsAlreadyMemberOfOrganizationApiException
|
* @throws UserIsAlreadyMemberOfOrganizationApiException
|
||||||
* @throws ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException
|
* @throws ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException
|
||||||
|
* @throws InvitationForTheEmailAlreadyExistsApiException
|
||||||
*
|
*
|
||||||
* @operationId invitePlaceholder
|
* @operationId invitePlaceholder
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ class ReportController extends Controller
|
|||||||
false,
|
false,
|
||||||
$report->properties->start,
|
$report->properties->start,
|
||||||
$report->properties->end,
|
$report->properties->end,
|
||||||
true
|
true,
|
||||||
|
$report->properties->roundingType,
|
||||||
|
$report->properties->roundingMinutes,
|
||||||
);
|
);
|
||||||
$historyData = $timeEntryAggregationService->getAggregatedTimeEntriesWithDescriptions(
|
$historyData = $timeEntryAggregationService->getAggregatedTimeEntriesWithDescriptions(
|
||||||
$timeEntriesQuery->clone(),
|
$timeEntriesQuery->clone(),
|
||||||
@@ -84,7 +86,9 @@ class ReportController extends Controller
|
|||||||
true,
|
true,
|
||||||
$report->properties->start,
|
$report->properties->start,
|
||||||
$report->properties->end,
|
$report->properties->end,
|
||||||
true
|
true,
|
||||||
|
$report->properties->roundingType,
|
||||||
|
$report->properties->roundingMinutes,
|
||||||
);
|
);
|
||||||
|
|
||||||
return new DetailedWithDataReportResource($report, $data, $historyData);
|
return new DetailedWithDataReportResource($report, $data, $historyData);
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ class ReportController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$properties->timezone = $timezone;
|
$properties->timezone = $timezone;
|
||||||
|
$properties->roundingType = $request->getPropertyRoundingType();
|
||||||
|
$properties->roundingMinutes = $request->getPropertyRoundingMinutes();
|
||||||
$report->properties = $properties;
|
$report->properties = $properties;
|
||||||
if ($isPublic) {
|
if ($isPublic) {
|
||||||
$report->share_secret = $reportService->generateSecret();
|
$report->share_secret = $reportService->generateSecret();
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ use App\Service\ReportExport\TimeEntriesDetailedExport;
|
|||||||
use App\Service\ReportExport\TimeEntriesReportExport;
|
use App\Service\ReportExport\TimeEntriesReportExport;
|
||||||
use App\Service\TimeEntryAggregationService;
|
use App\Service\TimeEntryAggregationService;
|
||||||
use App\Service\TimeEntryFilter;
|
use App\Service\TimeEntryFilter;
|
||||||
|
use App\Service\TimeEntryService;
|
||||||
use App\Service\TimezoneService;
|
use App\Service\TimezoneService;
|
||||||
use Gotenberg\Exceptions\GotenbergApiErrored;
|
use Gotenberg\Exceptions\GotenbergApiErrored;
|
||||||
use Gotenberg\Exceptions\NoOutputFileInResponse;
|
use Gotenberg\Exceptions\NoOutputFileInResponse;
|
||||||
@@ -47,6 +48,7 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
|||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Blade;
|
use Illuminate\Support\Facades\Blade;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Maatwebsite\Excel\Facades\Excel;
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
@@ -84,7 +86,8 @@ class TimeEntryController extends Controller
|
|||||||
$this->checkPermission($organization, 'time-entries:view:all');
|
$this->checkPermission($organization, 'time-entries:view:all');
|
||||||
}
|
}
|
||||||
|
|
||||||
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member);
|
$canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization);
|
||||||
|
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member, $canAccessPremiumFeatures);
|
||||||
|
|
||||||
$totalCount = $timeEntriesQuery->count();
|
$totalCount = $timeEntriesQuery->count();
|
||||||
|
|
||||||
@@ -138,10 +141,19 @@ class TimeEntryController extends Controller
|
|||||||
/**
|
/**
|
||||||
* @return Builder<TimeEntry>
|
* @return Builder<TimeEntry>
|
||||||
*/
|
*/
|
||||||
private function getTimeEntriesQuery(Organization $organization, TimeEntryIndexRequest|TimeEntryIndexExportRequest $request, ?Member $member): Builder
|
private function getTimeEntriesQuery(Organization $organization, TimeEntryIndexRequest|TimeEntryIndexExportRequest $request, ?Member $member, bool $canAccessPremiumFeatures): Builder
|
||||||
{
|
{
|
||||||
|
$select = TimeEntry::SELECT_COLUMNS;
|
||||||
|
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
|
||||||
|
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
||||||
|
if ($roundingType !== null && $roundingMinutes !== null) {
|
||||||
|
$select = array_diff($select, ['start', 'end']);
|
||||||
|
$select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes).' as start');
|
||||||
|
$select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes).' as end');
|
||||||
|
}
|
||||||
$timeEntriesQuery = TimeEntry::query()
|
$timeEntriesQuery = TimeEntry::query()
|
||||||
->whereBelongsTo($organization, 'organization')
|
->whereBelongsTo($organization, 'organization')
|
||||||
|
->select($select)
|
||||||
->orderBy('start', 'desc');
|
->orderBy('start', 'desc');
|
||||||
|
|
||||||
$filter = new TimeEntryFilter($timeEntriesQuery);
|
$filter = new TimeEntryFilter($timeEntriesQuery);
|
||||||
@@ -175,16 +187,19 @@ class TimeEntryController extends Controller
|
|||||||
} else {
|
} else {
|
||||||
$this->checkPermission($organization, 'time-entries:view:all');
|
$this->checkPermission($organization, 'time-entries:view:all');
|
||||||
}
|
}
|
||||||
|
$canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization);
|
||||||
$debug = $request->getDebug();
|
$debug = $request->getDebug();
|
||||||
$format = $request->getFormatValue();
|
$format = $request->getFormatValue();
|
||||||
if ($format === ExportFormat::PDF && ! $this->canAccessPremiumFeatures($organization)) {
|
if ($format === ExportFormat::PDF && ! $canAccessPremiumFeatures) {
|
||||||
throw new FeatureIsNotAvailableInFreePlanApiException;
|
throw new FeatureIsNotAvailableInFreePlanApiException;
|
||||||
}
|
}
|
||||||
$user = $this->user();
|
$user = $this->user();
|
||||||
$timezone = $user->timezone;
|
$timezone = $user->timezone;
|
||||||
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates;
|
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates;
|
||||||
|
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
|
||||||
|
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
||||||
|
|
||||||
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member);
|
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member, $canAccessPremiumFeatures);
|
||||||
$timeEntriesQuery->with([
|
$timeEntriesQuery->with([
|
||||||
'task',
|
'task',
|
||||||
'client',
|
'client',
|
||||||
@@ -207,8 +222,9 @@ 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, $member);
|
||||||
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries(
|
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries(
|
||||||
$timeEntriesQuery->clone()->reorder()->withOnly([]),
|
$timeEntriesAggregateQuery,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
$user->timezone,
|
$user->timezone,
|
||||||
@@ -216,7 +232,9 @@ class TimeEntryController extends Controller
|
|||||||
false,
|
false,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
$showBillableRate
|
$showBillableRate,
|
||||||
|
$roundingType,
|
||||||
|
$roundingMinutes,
|
||||||
);
|
);
|
||||||
$html = Blade::render($viewFile, [
|
$html = Blade::render($viewFile, [
|
||||||
'timeEntries' => $timeEntriesQuery->get(),
|
'timeEntries' => $timeEntriesQuery->get(),
|
||||||
@@ -226,6 +244,7 @@ class TimeEntryController extends Controller
|
|||||||
'start' => $request->getStart()->timezone($timezone),
|
'start' => $request->getStart()->timezone($timezone),
|
||||||
'end' => $request->getEnd()->timezone($timezone),
|
'end' => $request->getEnd()->timezone($timezone),
|
||||||
'localization' => $localizationService,
|
'localization' => $localizationService,
|
||||||
|
'showBillableRate' => $showBillableRate,
|
||||||
]);
|
]);
|
||||||
$footerViewFile = file_get_contents(resource_path('views/reports/time-entry-index/pdf-footer.blade.php'));
|
$footerViewFile = file_get_contents(resource_path('views/reports/time-entry-index/pdf-footer.blade.php'));
|
||||||
if ($footerViewFile === false) {
|
if ($footerViewFile === false) {
|
||||||
@@ -317,12 +336,15 @@ class TimeEntryController extends Controller
|
|||||||
} else {
|
} else {
|
||||||
$this->checkPermission($organization, 'time-entries:view:all');
|
$this->checkPermission($organization, 'time-entries:view:all');
|
||||||
}
|
}
|
||||||
|
$canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization);
|
||||||
$user = $this->user();
|
$user = $this->user();
|
||||||
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates;
|
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates;
|
||||||
|
|
||||||
$group1Type = $request->getGroup();
|
$group1Type = $request->getGroup();
|
||||||
$group2Type = $request->getSubGroup();
|
$group2Type = $request->getSubGroup();
|
||||||
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
||||||
|
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
|
||||||
|
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
||||||
|
|
||||||
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries(
|
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries(
|
||||||
$timeEntriesAggregateQuery,
|
$timeEntriesAggregateQuery,
|
||||||
@@ -333,7 +355,9 @@ class TimeEntryController extends Controller
|
|||||||
$request->getFillGapsInTimeGroups(),
|
$request->getFillGapsInTimeGroups(),
|
||||||
$request->getStart(),
|
$request->getStart(),
|
||||||
$request->getEnd(),
|
$request->getEnd(),
|
||||||
$showBillableRate
|
$showBillableRate,
|
||||||
|
$roundingType,
|
||||||
|
$roundingMinutes
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -361,6 +385,7 @@ class TimeEntryController extends Controller
|
|||||||
} else {
|
} else {
|
||||||
$this->checkPermission($organization, 'time-entries:view:all');
|
$this->checkPermission($organization, 'time-entries:view:all');
|
||||||
}
|
}
|
||||||
|
$canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization);
|
||||||
$format = $request->getFormatValue();
|
$format = $request->getFormatValue();
|
||||||
if ($format === ExportFormat::PDF && ! $this->canAccessPremiumFeatures($organization)) {
|
if ($format === ExportFormat::PDF && ! $this->canAccessPremiumFeatures($organization)) {
|
||||||
throw new FeatureIsNotAvailableInFreePlanApiException;
|
throw new FeatureIsNotAvailableInFreePlanApiException;
|
||||||
@@ -372,6 +397,8 @@ class TimeEntryController extends Controller
|
|||||||
$group = $request->getGroup();
|
$group = $request->getGroup();
|
||||||
$subGroup = $request->getSubGroup();
|
$subGroup = $request->getSubGroup();
|
||||||
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
||||||
|
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
|
||||||
|
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
||||||
|
|
||||||
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntriesWithDescriptions(
|
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntriesWithDescriptions(
|
||||||
$timeEntriesAggregateQuery->clone(),
|
$timeEntriesAggregateQuery->clone(),
|
||||||
@@ -382,7 +409,9 @@ class TimeEntryController extends Controller
|
|||||||
false,
|
false,
|
||||||
$request->getStart(),
|
$request->getStart(),
|
||||||
$request->getEnd(),
|
$request->getEnd(),
|
||||||
$showBillableRate
|
$showBillableRate,
|
||||||
|
$roundingType,
|
||||||
|
$roundingMinutes
|
||||||
);
|
);
|
||||||
$dataHistoryChart = $timeEntryAggregationService->getAggregatedTimeEntries(
|
$dataHistoryChart = $timeEntryAggregationService->getAggregatedTimeEntries(
|
||||||
$timeEntriesAggregateQuery->clone(),
|
$timeEntriesAggregateQuery->clone(),
|
||||||
@@ -393,7 +422,9 @@ class TimeEntryController extends Controller
|
|||||||
true,
|
true,
|
||||||
$request->getStart(),
|
$request->getStart(),
|
||||||
$request->getEnd(),
|
$request->getEnd(),
|
||||||
$showBillableRate
|
$showBillableRate,
|
||||||
|
$roundingType,
|
||||||
|
$roundingMinutes
|
||||||
);
|
);
|
||||||
$currency = $organization->currency;
|
$currency = $organization->currency;
|
||||||
$timezone = app(TimezoneService::class)->getTimezoneFromUser($this->user());
|
$timezone = app(TimezoneService::class)->getTimezoneFromUser($this->user());
|
||||||
@@ -428,6 +459,7 @@ class TimeEntryController extends Controller
|
|||||||
'end' => $request->getEnd()->timezone($timezone),
|
'end' => $request->getEnd()->timezone($timezone),
|
||||||
'debug' => $debug,
|
'debug' => $debug,
|
||||||
'localization' => $localizationService,
|
'localization' => $localizationService,
|
||||||
|
'showBillableRate' => $showBillableRate,
|
||||||
]);
|
]);
|
||||||
$footerViewFile = file_get_contents(resource_path('views/reports/time-entry-aggregate/pdf-footer.blade.php'));
|
$footerViewFile = file_get_contents(resource_path('views/reports/time-entry-aggregate/pdf-footer.blade.php'));
|
||||||
if ($footerViewFile === false) {
|
if ($footerViewFile === false) {
|
||||||
@@ -456,7 +488,7 @@ class TimeEntryController extends Controller
|
|||||||
->putFileAs($folderPath, new File($tempFolder->path($filenameTemp)), $filename);
|
->putFileAs($folderPath, new File($tempFolder->path($filenameTemp)), $filename);
|
||||||
} else {
|
} else {
|
||||||
Excel::store(
|
Excel::store(
|
||||||
new TimeEntriesReportExport($aggregatedData, $format, $currency, $group, $subGroup),
|
new TimeEntriesReportExport($aggregatedData, $format, $currency, $group, $subGroup, $showBillableRate),
|
||||||
$path,
|
$path,
|
||||||
config('filesystems.private'),
|
config('filesystems.private'),
|
||||||
$format->getExportPackageType(),
|
$format->getExportPackageType(),
|
||||||
@@ -475,7 +507,7 @@ class TimeEntryController extends Controller
|
|||||||
/**
|
/**
|
||||||
* @return Builder<TimeEntry>
|
* @return Builder<TimeEntry>
|
||||||
*/
|
*/
|
||||||
private function getTimeEntriesAggregateQuery(Organization $organization, TimeEntryAggregateRequest|TimeEntryAggregateExportRequest $request, ?Member $member): Builder
|
private function getTimeEntriesAggregateQuery(Organization $organization, TimeEntryAggregateRequest|TimeEntryAggregateExportRequest|TimeEntryIndexExportRequest $request, ?Member $member): Builder
|
||||||
{
|
{
|
||||||
$timeEntriesQuery = TimeEntry::query()
|
$timeEntriesQuery = TimeEntry::query()
|
||||||
->whereBelongsTo($organization, 'organization');
|
->whereBelongsTo($organization, 'organization');
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ class Controller extends BaseController
|
|||||||
/** @var Member|null $member */
|
/** @var Member|null $member */
|
||||||
$member = Member::query()->whereBelongsTo($organization, 'organization')->whereBelongsTo($user, 'user')->first();
|
$member = Member::query()->whereBelongsTo($organization, 'organization')->whereBelongsTo($user, 'user')->first();
|
||||||
if ($member === null) {
|
if ($member === null) {
|
||||||
Log::error('This function should only be called in authenticated context after checking the user is a member of the organization');
|
Log::error('This function should only be called in authenticated context after checking the user is a member of the organization', [
|
||||||
|
'user' => $user->getKey(),
|
||||||
|
'organization' => $organization->getKey(),
|
||||||
|
]);
|
||||||
throw new AuthorizationException;
|
throw new AuthorizationException;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Redirect;
|
use Illuminate\Support\Facades\Redirect;
|
||||||
use Illuminate\Support\Facades\URL;
|
use Illuminate\Support\Facades\URL;
|
||||||
@@ -20,8 +19,7 @@ class EnsureEmailIsVerified
|
|||||||
{
|
{
|
||||||
if (! app()->isLocal()) {
|
if (! app()->isLocal()) {
|
||||||
if ($request->user() === null ||
|
if ($request->user() === null ||
|
||||||
($request->user() instanceof MustVerifyEmail &&
|
(! $request->user()->hasVerifiedEmail())) {
|
||||||
! $request->user()->hasVerifiedEmail())) {
|
|
||||||
return $request->expectsJson()
|
return $request->expectsJson()
|
||||||
? abort(403, 'Your email address is not verified.')
|
? abort(403, 'Your email address is not verified.')
|
||||||
: Redirect::guest(URL::route($redirectToRoute ?: 'verification.notice'));
|
: Redirect::guest(URL::route($redirectToRoute ?: 'verification.notice'));
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class HandleInertiaRequests extends Middleware
|
|||||||
return array_merge(parent::share($request), [
|
return array_merge(parent::share($request), [
|
||||||
'has_billing_extension' => $hasBilling,
|
'has_billing_extension' => $hasBilling,
|
||||||
'has_invoicing_extension' => $hasInvoicing,
|
'has_invoicing_extension' => $hasInvoicing,
|
||||||
'billing' => $billing !== null && $currentOrganization !== null ? [
|
'billing' => $currentOrganization !== null ? [
|
||||||
'has_subscription' => $billing->hasSubscription($currentOrganization),
|
'has_subscription' => $billing->hasSubscription($currentOrganization),
|
||||||
'has_trial' => $billing->hasTrial($currentOrganization),
|
'has_trial' => $billing->hasTrial($currentOrganization),
|
||||||
'trial_until' => $billing->getTrialUntil($currentOrganization)?->toIso8601ZuluString(),
|
'trial_until' => $billing->getTrialUntil($currentOrganization)?->toIso8601ZuluString(),
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class ShareInertiaData
|
|||||||
{
|
{
|
||||||
/** @var PermissionStore $permissions */
|
/** @var PermissionStore $permissions */
|
||||||
$permissions = app(PermissionStore::class);
|
$permissions = app(PermissionStore::class);
|
||||||
Inertia::share(array_filter([
|
Inertia::share([
|
||||||
'jetstream' => function () use ($request) {
|
'jetstream' => function () use ($request) {
|
||||||
/** @var User|null $user */
|
/** @var User|null $user */
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
@@ -101,7 +101,7 @@ class ShareInertiaData
|
|||||||
return [$key => $bag->messages()];
|
return [$key => $bag->messages()];
|
||||||
})->all();
|
})->all();
|
||||||
},
|
},
|
||||||
]));
|
]);
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\ApiToken;
|
namespace App\Http\Requests\V1\ApiToken;
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
|
|
||||||
class ApiTokenStoreRequest extends FormRequest
|
class ApiTokenStoreRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
28
app/Http/Requests/V1/BaseFormRequest.php
Normal file
28
app/Http/Requests/V1/BaseFormRequest.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests\V1;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class BaseFormRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
protected function moneyRules(bool $bigInt = false): array
|
||||||
|
{
|
||||||
|
$rules = [
|
||||||
|
'integer',
|
||||||
|
'min:0',
|
||||||
|
];
|
||||||
|
if ($bigInt) {
|
||||||
|
$rules[] = 'max:9223372036854775807';
|
||||||
|
} else {
|
||||||
|
$rules[] = 'max:2147483647';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rules;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\Client;
|
namespace App\Http\Requests\V1\Client;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class ClientIndexRequest extends FormRequest
|
class ClientIndexRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\Client;
|
namespace App\Http\Requests\V1\Client;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization Organization from model binding
|
* @property Organization $organization Organization from model binding
|
||||||
*/
|
*/
|
||||||
class ClientStoreRequest extends FormRequest
|
class ClientStoreRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\Client;
|
namespace App\Http\Requests\V1\Client;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization Organization from model binding
|
* @property Organization $organization Organization from model binding
|
||||||
* @property Client|null $client Client from model binding
|
* @property Client|null $client Client from model binding
|
||||||
*/
|
*/
|
||||||
class ClientUpdateRequest extends FormRequest
|
class ClientUpdateRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\Import;
|
namespace App\Http\Requests\V1\Import;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class ImportRequest extends FormRequest
|
class ImportRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\Invitation;
|
namespace App\Http\Requests\V1\Invitation;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization
|
* @property Organization $organization
|
||||||
*/
|
*/
|
||||||
class InvitationIndexRequest extends FormRequest
|
class InvitationIndexRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
@@ -5,18 +5,15 @@ declare(strict_types=1);
|
|||||||
namespace App\Http\Requests\V1\Invitation;
|
namespace App\Http\Requests\V1\Invitation;
|
||||||
|
|
||||||
use App\Enums\Role;
|
use App\Enums\Role;
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\OrganizationInvitation;
|
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization
|
* @property Organization $organization
|
||||||
*/
|
*/
|
||||||
class InvitationStoreRequest extends FormRequest
|
class InvitationStoreRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
@@ -29,10 +26,6 @@ class InvitationStoreRequest extends FormRequest
|
|||||||
'email' => [
|
'email' => [
|
||||||
'required',
|
'required',
|
||||||
'email',
|
'email',
|
||||||
UniqueEloquent::make(OrganizationInvitation::class, 'email', function (Builder $builder): Builder {
|
|
||||||
/** @var Builder<OrganizationInvitation> $builder */
|
|
||||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
|
||||||
})->withCustomTranslation('validation.invitation_already_exists'),
|
|
||||||
],
|
],
|
||||||
'role' => [
|
'role' => [
|
||||||
'required',
|
'required',
|
||||||
|
|||||||
35
app/Http/Requests/V1/Member/MemberDestroyRequest.php
Normal file
35
app/Http/Requests/V1/Member/MemberDestroyRequest.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests\V1\Member;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
|
use App\Models\Organization;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property Organization $organization
|
||||||
|
*/
|
||||||
|
class MemberDestroyRequest extends BaseFormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, array<string|ValidationRule>>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'delete_related' => [
|
||||||
|
'string',
|
||||||
|
'in:true,false',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDeleteRelated(): bool
|
||||||
|
{
|
||||||
|
return $this->input('delete_related', 'false') === 'true';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,14 +4,14 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\Member;
|
namespace App\Http\Requests\V1\Member;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization
|
* @property Organization $organization
|
||||||
*/
|
*/
|
||||||
class MemberIndexRequest extends FormRequest
|
class MemberIndexRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\Member;
|
namespace App\Http\Requests\V1\Member;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization
|
* @property Organization $organization
|
||||||
*/
|
*/
|
||||||
class MemberMergeIntoRequest extends FormRequest
|
class MemberMergeIntoRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ declare(strict_types=1);
|
|||||||
namespace App\Http\Requests\V1\Member;
|
namespace App\Http\Requests\V1\Member;
|
||||||
|
|
||||||
use App\Enums\Role;
|
use App\Enums\Role;
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization
|
* @property Organization $organization
|
||||||
*/
|
*/
|
||||||
class MemberUpdateRequest extends FormRequest
|
class MemberUpdateRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
@@ -27,12 +27,12 @@ class MemberUpdateRequest extends FormRequest
|
|||||||
'string',
|
'string',
|
||||||
Rule::enum(Role::class),
|
Rule::enum(Role::class),
|
||||||
],
|
],
|
||||||
'billable_rate' => [
|
'billable_rate' => array_merge(
|
||||||
|
[
|
||||||
'nullable',
|
'nullable',
|
||||||
'integer',
|
|
||||||
'min:0',
|
|
||||||
'max:2147483647',
|
|
||||||
],
|
],
|
||||||
|
$this->moneyRules()
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ use App\Enums\DateFormat;
|
|||||||
use App\Enums\IntervalFormat;
|
use App\Enums\IntervalFormat;
|
||||||
use App\Enums\NumberFormat;
|
use App\Enums\NumberFormat;
|
||||||
use App\Enums\TimeFormat;
|
use App\Enums\TimeFormat;
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization Organization from model binding
|
* @property Organization $organization Organization from model binding
|
||||||
*/
|
*/
|
||||||
class OrganizationUpdateRequest extends FormRequest
|
class OrganizationUpdateRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
@@ -30,12 +30,12 @@ class OrganizationUpdateRequest extends FormRequest
|
|||||||
'string',
|
'string',
|
||||||
'max:255',
|
'max:255',
|
||||||
],
|
],
|
||||||
'billable_rate' => [
|
'billable_rate' => array_merge(
|
||||||
|
[
|
||||||
'nullable',
|
'nullable',
|
||||||
'integer',
|
|
||||||
'min:0',
|
|
||||||
'max:2147483647',
|
|
||||||
],
|
],
|
||||||
|
$this->moneyRules()
|
||||||
|
),
|
||||||
'employees_can_see_billable_rates' => [
|
'employees_can_see_billable_rates' => [
|
||||||
'boolean',
|
'boolean',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\Project;
|
namespace App\Http\Requests\V1\Project;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class ProjectIndexRequest extends FormRequest
|
class ProjectIndexRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\Project;
|
namespace App\Http\Requests\V1\Project;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Rules\ColorRule;
|
use App\Rules\ColorRule;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||||
@@ -18,7 +18,7 @@ use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
|||||||
/**
|
/**
|
||||||
* @property Organization $organization Organization from model binding
|
* @property Organization $organization Organization from model binding
|
||||||
*/
|
*/
|
||||||
class ProjectStoreRequest extends FormRequest
|
class ProjectStoreRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
@@ -55,12 +55,12 @@ class ProjectStoreRequest extends FormRequest
|
|||||||
'required',
|
'required',
|
||||||
'boolean',
|
'boolean',
|
||||||
],
|
],
|
||||||
'billable_rate' => [
|
'billable_rate' => array_merge(
|
||||||
|
[
|
||||||
'nullable',
|
'nullable',
|
||||||
'integer',
|
|
||||||
'min:0',
|
|
||||||
'max:2147483647',
|
|
||||||
],
|
],
|
||||||
|
$this->moneyRules()
|
||||||
|
),
|
||||||
// ID of the client
|
// ID of the client
|
||||||
'client_id' => [
|
'client_id' => [
|
||||||
'present',
|
'present',
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\Project;
|
namespace App\Http\Requests\V1\Project;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Rules\ColorRule;
|
use App\Rules\ColorRule;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||||
@@ -19,7 +19,7 @@ use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
|||||||
* @property Organization $organization Organization from model binding
|
* @property Organization $organization Organization from model binding
|
||||||
* @property Project|null $project Project from model binding
|
* @property Project|null $project Project from model binding
|
||||||
*/
|
*/
|
||||||
class ProjectUpdateRequest extends FormRequest
|
class ProjectUpdateRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
@@ -68,12 +68,11 @@ class ProjectUpdateRequest extends FormRequest
|
|||||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||||
})->uuid(),
|
})->uuid(),
|
||||||
],
|
],
|
||||||
'billable_rate' => [
|
'billable_rate' => array_merge([
|
||||||
'nullable',
|
'nullable',
|
||||||
'integer',
|
|
||||||
'min:0',
|
|
||||||
'max:2147483647',
|
|
||||||
],
|
],
|
||||||
|
$this->moneyRules()
|
||||||
|
),
|
||||||
// Estimated time in seconds
|
// Estimated time in seconds
|
||||||
'estimated_time' => [
|
'estimated_time' => [
|
||||||
'nullable',
|
'nullable',
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\ProjectMember;
|
namespace App\Http\Requests\V1\ProjectMember;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization Organization from model binding
|
* @property Organization $organization Organization from model binding
|
||||||
*/
|
*/
|
||||||
class ProjectMemberStoreRequest extends FormRequest
|
class ProjectMemberStoreRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
@@ -31,12 +31,12 @@ class ProjectMemberStoreRequest extends FormRequest
|
|||||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||||
})->uuid(),
|
})->uuid(),
|
||||||
],
|
],
|
||||||
'billable_rate' => [
|
'billable_rate' => array_merge(
|
||||||
|
[
|
||||||
'nullable',
|
'nullable',
|
||||||
'integer',
|
|
||||||
'min:0',
|
|
||||||
'max:2147483647',
|
|
||||||
],
|
],
|
||||||
|
$this->moneyRules()
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\ProjectMember;
|
namespace App\Http\Requests\V1\ProjectMember;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization Organization from model binding
|
* @property Organization $organization Organization from model binding
|
||||||
*/
|
*/
|
||||||
class ProjectMemberUpdateRequest extends FormRequest
|
class ProjectMemberUpdateRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
@@ -21,12 +21,12 @@ class ProjectMemberUpdateRequest extends FormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'billable_rate' => [
|
'billable_rate' => array_merge(
|
||||||
|
[
|
||||||
'nullable',
|
'nullable',
|
||||||
'integer',
|
|
||||||
'min:0',
|
|
||||||
'max:2147483647',
|
|
||||||
],
|
],
|
||||||
|
$this->moneyRules()
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,18 +6,19 @@ namespace App\Http\Requests\V1\Report;
|
|||||||
|
|
||||||
use App\Enums\TimeEntryAggregationType;
|
use App\Enums\TimeEntryAggregationType;
|
||||||
use App\Enums\TimeEntryAggregationTypeInterval;
|
use App\Enums\TimeEntryAggregationTypeInterval;
|
||||||
|
use App\Enums\TimeEntryRoundingType;
|
||||||
use App\Enums\Weekday;
|
use App\Enums\Weekday;
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use Illuminate\Contracts\Validation\Rule as LegacyValidationRule;
|
use Illuminate\Contracts\Validation\Rule as LegacyValidationRule;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization Organization from model binding
|
* @property Organization $organization Organization from model binding
|
||||||
*/
|
*/
|
||||||
class ReportStoreRequest extends FormRequest
|
class ReportStoreRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
@@ -128,6 +129,18 @@ class ReportStoreRequest extends FormRequest
|
|||||||
'nullable',
|
'nullable',
|
||||||
'timezone:all',
|
'timezone:all',
|
||||||
],
|
],
|
||||||
|
// Rounding type defined where the end of each time entry should be rounded to. For example: nearest rounds the end to the nearest x minutes group. Rounding per time entry is activated if `rounding_type` and `rounding_minutes` is not null.
|
||||||
|
'properties.rounding_type' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
Rule::enum(TimeEntryRoundingType::class),
|
||||||
|
],
|
||||||
|
// Defines the length of the interval that the time entry rounding rounds to.
|
||||||
|
'properties.rounding_minutes' => [
|
||||||
|
'nullable',
|
||||||
|
'numeric',
|
||||||
|
'integer',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,4 +218,22 @@ class ReportStoreRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
return TimeEntryAggregationTypeInterval::from($this->input('properties.history_group'));
|
return TimeEntryAggregationTypeInterval::from($this->input('properties.history_group'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getPropertyRoundingType(): ?TimeEntryRoundingType
|
||||||
|
{
|
||||||
|
if (! $this->has('properties.rounding_type') || $this->input('properties.rounding_type') === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TimeEntryRoundingType::from($this->input('properties.rounding_type'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPropertyRoundingMinutes(): ?int
|
||||||
|
{
|
||||||
|
if (! $this->has('properties.rounding_minutes') || $this->input('properties.rounding_minutes') === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $this->input('properties.rounding_minutes');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\Report;
|
namespace App\Http\Requests\V1\Report;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization Organization from model binding
|
* @property Organization $organization Organization from model binding
|
||||||
*/
|
*/
|
||||||
class ReportUpdateRequest extends FormRequest
|
class ReportUpdateRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\Tag;
|
namespace App\Http\Requests\V1\Tag;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\Tag;
|
use App\Models\Tag;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization Organization from model binding
|
* @property Organization $organization Organization from model binding
|
||||||
*/
|
*/
|
||||||
class TagStoreRequest extends FormRequest
|
class TagStoreRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\Tag;
|
namespace App\Http\Requests\V1\Tag;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\Tag;
|
use App\Models\Tag;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization Organization from model binding
|
* @property Organization $organization Organization from model binding
|
||||||
* @property Tag|null $tag Tag from model binding
|
* @property Tag|null $tag Tag from model binding
|
||||||
*/
|
*/
|
||||||
class TagUpdateRequest extends FormRequest
|
class TagUpdateRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\Task;
|
namespace App\Http\Requests\V1\Task;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Service\PermissionStore;
|
use App\Service\PermissionStore;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization Organization from model binding
|
* @property Organization $organization Organization from model binding
|
||||||
*/
|
*/
|
||||||
class TaskIndexRequest extends FormRequest
|
class TaskIndexRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\Task;
|
namespace App\Http\Requests\V1\Task;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization Organization from model binding
|
* @property Organization $organization Organization from model binding
|
||||||
*/
|
*/
|
||||||
class TaskStoreRequest extends FormRequest
|
class TaskStoreRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\Task;
|
namespace App\Http\Requests\V1\Task;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization Organization from model binding
|
* @property Organization $organization Organization from model binding
|
||||||
* @property Task|null $task Task from model binding
|
* @property Task|null $task Task from model binding
|
||||||
*/
|
*/
|
||||||
class TaskUpdateRequest extends FormRequest
|
class TaskUpdateRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ namespace App\Http\Requests\V1\TimeEntry;
|
|||||||
use App\Enums\ExportFormat;
|
use App\Enums\ExportFormat;
|
||||||
use App\Enums\TimeEntryAggregationType;
|
use App\Enums\TimeEntryAggregationType;
|
||||||
use App\Enums\TimeEntryAggregationTypeInterval;
|
use App\Enums\TimeEntryAggregationTypeInterval;
|
||||||
|
use App\Enums\TimeEntryRoundingType;
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
@@ -16,7 +18,6 @@ use App\Models\Task;
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||||
@@ -24,7 +25,7 @@ use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
|||||||
/**
|
/**
|
||||||
* @property Organization $organization
|
* @property Organization $organization
|
||||||
*/
|
*/
|
||||||
class TimeEntryAggregateExportRequest extends FormRequest
|
class TimeEntryAggregateExportRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
@@ -164,6 +165,18 @@ class TimeEntryAggregateExportRequest extends FormRequest
|
|||||||
'string',
|
'string',
|
||||||
'in:true,false',
|
'in:true,false',
|
||||||
],
|
],
|
||||||
|
// Rounding type defined where the end of each time entry should be rounded to. For example: nearest rounds the end to the nearest x minutes group. Rounding per time entry is activated if `rounding_type` and `rounding_minutes` is not null.
|
||||||
|
'rounding_type' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
Rule::enum(TimeEntryRoundingType::class),
|
||||||
|
],
|
||||||
|
// Defines the length of the interval that the time entry rounding rounds to.
|
||||||
|
'rounding_minutes' => [
|
||||||
|
'nullable',
|
||||||
|
'numeric',
|
||||||
|
'integer',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,4 +224,22 @@ class TimeEntryAggregateExportRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
return ExportFormat::from($this->validated('format'));
|
return ExportFormat::from($this->validated('format'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRoundingType(): ?TimeEntryRoundingType
|
||||||
|
{
|
||||||
|
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TimeEntryRoundingType::from($this->validated('rounding_type'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRoundingMinutes(): ?int
|
||||||
|
{
|
||||||
|
if (! $this->has('rounding_minutes') || $this->validated('rounding_minutes') === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $this->validated('rounding_minutes');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace App\Http\Requests\V1\TimeEntry;
|
namespace App\Http\Requests\V1\TimeEntry;
|
||||||
|
|
||||||
use App\Enums\TimeEntryAggregationType;
|
use App\Enums\TimeEntryAggregationType;
|
||||||
|
use App\Enums\TimeEntryRoundingType;
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
@@ -14,7 +16,6 @@ use App\Models\Task;
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||||
@@ -22,7 +23,7 @@ use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
|||||||
/**
|
/**
|
||||||
* @property Organization $organization
|
* @property Organization $organization
|
||||||
*/
|
*/
|
||||||
class TimeEntryAggregateRequest extends FormRequest
|
class TimeEntryAggregateRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
@@ -146,6 +147,18 @@ class TimeEntryAggregateRequest extends FormRequest
|
|||||||
'string',
|
'string',
|
||||||
'in:true,false',
|
'in:true,false',
|
||||||
],
|
],
|
||||||
|
// Rounding type defined where the end of each time entry should be rounded to. For example: nearest rounds the end to the nearest x minutes group. Rounding per time entry is activated if `rounding_type` and `rounding_minutes` is not null.
|
||||||
|
'rounding_type' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
Rule::enum(TimeEntryRoundingType::class),
|
||||||
|
],
|
||||||
|
// Defines the length of the interval that the time entry rounding rounds to.
|
||||||
|
'rounding_minutes' => [
|
||||||
|
'nullable',
|
||||||
|
'numeric',
|
||||||
|
'integer',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,4 +186,22 @@ class TimeEntryAggregateRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
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 getRoundingType(): ?TimeEntryRoundingType
|
||||||
|
{
|
||||||
|
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TimeEntryRoundingType::from($this->validated('rounding_type'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRoundingMinutes(): ?int
|
||||||
|
{
|
||||||
|
if (! $this->has('rounding_minutes') || $this->validated('rounding_minutes') === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $this->validated('rounding_minutes');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\TimeEntry;
|
namespace App\Http\Requests\V1\TimeEntry;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization Organization from model binding
|
* @property Organization $organization Organization from model binding
|
||||||
*/
|
*/
|
||||||
class TimeEntryDestroyMultipleRequest extends FormRequest
|
class TimeEntryDestroyMultipleRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
@@ -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\TimeEntryRoundingType;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
@@ -133,6 +134,18 @@ class TimeEntryIndexExportRequest extends TimeEntryIndexRequest
|
|||||||
'string',
|
'string',
|
||||||
'in:true,false',
|
'in:true,false',
|
||||||
],
|
],
|
||||||
|
// Rounding type defined where the end of each time entry should be rounded to. For example: nearest rounds the end to the nearest x minutes group. Rounding per time entry is activated if `rounding_type` and `rounding_minutes` is not null.
|
||||||
|
'rounding_type' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
Rule::enum(TimeEntryRoundingType::class),
|
||||||
|
],
|
||||||
|
// Defines the length of the interval that the time entry rounding rounds to.
|
||||||
|
'rounding_minutes' => [
|
||||||
|
'nullable',
|
||||||
|
'numeric',
|
||||||
|
'integer',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,4 +183,22 @@ class TimeEntryIndexExportRequest extends TimeEntryIndexRequest
|
|||||||
{
|
{
|
||||||
return ExportFormat::from($this->validated('format'));
|
return ExportFormat::from($this->validated('format'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRoundingType(): ?TimeEntryRoundingType
|
||||||
|
{
|
||||||
|
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TimeEntryRoundingType::from($this->validated('rounding_type'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRoundingMinutes(): ?int
|
||||||
|
{
|
||||||
|
if (! $this->has('rounding_minutes') || $this->validated('rounding_minutes') === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $this->validated('rounding_minutes');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,26 +4,29 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\TimeEntry;
|
namespace App\Http\Requests\V1\TimeEntry;
|
||||||
|
|
||||||
|
use App\Enums\TimeEntryRoundingType;
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\Tag;
|
use App\Models\Tag;
|
||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
|
use Illuminate\Contracts\Validation\Rule as RuleContract;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Validation\Rule;
|
||||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization
|
* @property Organization $organization
|
||||||
*/
|
*/
|
||||||
class TimeEntryIndexRequest extends FormRequest
|
class TimeEntryIndexRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
*
|
*
|
||||||
* @return array<string, array<string|ValidationRule>>
|
* @return array<string, array<string|ValidationRule|RuleContract>>
|
||||||
*/
|
*/
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
@@ -136,6 +139,18 @@ class TimeEntryIndexRequest extends FormRequest
|
|||||||
'string',
|
'string',
|
||||||
'in:true,false',
|
'in:true,false',
|
||||||
],
|
],
|
||||||
|
// Rounding type defined where the end of each time entry should be rounded to. For example: nearest rounds the end to the nearest x minutes group. Rounding per time entry is activated if `rounding_type` and `rounding_minutes` is not null.
|
||||||
|
'rounding_type' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
Rule::enum(TimeEntryRoundingType::class),
|
||||||
|
],
|
||||||
|
// Defines the length of the interval that the time entry rounding rounds to.
|
||||||
|
'rounding_minutes' => [
|
||||||
|
'nullable',
|
||||||
|
'numeric',
|
||||||
|
'integer',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,4 +168,22 @@ class TimeEntryIndexRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
return $this->has('offset') ? (int) $this->validated('offset', 0) : 0;
|
return $this->has('offset') ? (int) $this->validated('offset', 0) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRoundingType(): ?TimeEntryRoundingType
|
||||||
|
{
|
||||||
|
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TimeEntryRoundingType::from($this->validated('rounding_type'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRoundingMinutes(): ?int
|
||||||
|
{
|
||||||
|
if (! $this->has('rounding_minutes') || $this->validated('rounding_minutes') === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $this->validated('rounding_minutes');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\TimeEntry;
|
namespace App\Http\Requests\V1\TimeEntry;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
@@ -11,13 +12,12 @@ use App\Models\Tag;
|
|||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization Organization from model binding
|
* @property Organization $organization Organization from model binding
|
||||||
*/
|
*/
|
||||||
class TimeEntryStoreRequest extends FormRequest
|
class TimeEntryStoreRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\TimeEntry;
|
namespace App\Http\Requests\V1\TimeEntry;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
@@ -11,13 +12,12 @@ use App\Models\Tag;
|
|||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization Organization from model binding
|
* @property Organization $organization Organization from model binding
|
||||||
*/
|
*/
|
||||||
class TimeEntryUpdateMultipleRequest extends FormRequest
|
class TimeEntryUpdateMultipleRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\TimeEntry;
|
namespace App\Http\Requests\V1\TimeEntry;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
@@ -11,13 +12,12 @@ use App\Models\Tag;
|
|||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Organization $organization Organization from model binding
|
* @property Organization $organization Organization from model binding
|
||||||
*/
|
*/
|
||||||
class TimeEntryUpdateRequest extends FormRequest
|
class TimeEntryUpdateRequest extends BaseFormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|||||||
@@ -8,15 +8,11 @@ use App\Http\Resources\PaginatedResourceCollection;
|
|||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
|
||||||
|
|
||||||
class ProjectCollection extends ResourceCollection implements PaginatedResourceCollection
|
class ProjectCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||||
{
|
{
|
||||||
private bool $showBillableRates;
|
private bool $showBillableRates;
|
||||||
|
|
||||||
/**
|
|
||||||
* @param LengthAwarePaginator<Project> $resource
|
|
||||||
*/
|
|
||||||
public function __construct($resource, bool $showBillableRates)
|
public function __construct($resource, bool $showBillableRates)
|
||||||
{
|
{
|
||||||
parent::__construct($resource);
|
parent::__construct($resource);
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ class DetailedReportResource extends BaseResource
|
|||||||
'tag_ids' => $this->resource->properties->tagIds?->toArray(),
|
'tag_ids' => $this->resource->properties->tagIds?->toArray(),
|
||||||
/** @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 */
|
||||||
|
'rounding_type' => $this->resource->properties->roundingType?->value,
|
||||||
|
/** @var int|null $rounding_minutes Rounding minutes for time entries */
|
||||||
|
'rounding_minutes' => $this->resource->properties->roundingMinutes,
|
||||||
],
|
],
|
||||||
/** @var string $created_at Date when the report was created */
|
/** @var string $created_at Date when the report was created */
|
||||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||||
|
|||||||
44
app/Mail/AuthApiTokenExpirationReminderMail.php
Normal file
44
app/Mail/AuthApiTokenExpirationReminderMail.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use App\Models\Passport\Token;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\URL;
|
||||||
|
|
||||||
|
class AuthApiTokenExpirationReminderMail extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public Token $token;
|
||||||
|
|
||||||
|
public User $user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new message instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Token $token, User $user)
|
||||||
|
{
|
||||||
|
$this->token = $token;
|
||||||
|
$this->user = $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the message.
|
||||||
|
*/
|
||||||
|
public function build(): self
|
||||||
|
{
|
||||||
|
return $this->markdown('emails.auth-api-expiration-reminder', [
|
||||||
|
'profileUrl' => URL::to('user/profile'),
|
||||||
|
'tokenName' => $this->token->name,
|
||||||
|
])
|
||||||
|
->subject(__('Your API token will expire in 7 days!'));
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/Mail/AuthApiTokenExpiredMail.php
Normal file
44
app/Mail/AuthApiTokenExpiredMail.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use App\Models\Passport\Token;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\URL;
|
||||||
|
|
||||||
|
class AuthApiTokenExpiredMail extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public Token $token;
|
||||||
|
|
||||||
|
public User $user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new message instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Token $token, User $user)
|
||||||
|
{
|
||||||
|
$this->token = $token;
|
||||||
|
$this->user = $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the message.
|
||||||
|
*/
|
||||||
|
public function build(): self
|
||||||
|
{
|
||||||
|
return $this->markdown('emails.auth-api-token-expired', [
|
||||||
|
'profileUrl' => URL::to('user/profile'),
|
||||||
|
'tokenName' => $this->token->name,
|
||||||
|
])
|
||||||
|
->subject(__('Your API token has expired!'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,8 +16,8 @@ use OwenIt\Auditing\Models\Audit as PackageAuditModel;
|
|||||||
* @property string $event
|
* @property string $event
|
||||||
* @property string $auditable_type
|
* @property string $auditable_type
|
||||||
* @property string $auditable_id
|
* @property string $auditable_id
|
||||||
* @property array|null $old_values
|
* @property array<string, mixed>|null $old_values
|
||||||
* @property array|null $new_values
|
* @property array<string, mixed>|null $new_values
|
||||||
* @property string|null $url
|
* @property string|null $url
|
||||||
* @property string|null $ip_address
|
* @property string|null $ip_address
|
||||||
* @property string|null $user_agent
|
* @property string|null $user_agent
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class Client extends Model implements AuditableContract
|
|||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Organization, Client>
|
* @return BelongsTo<Organization, $this>
|
||||||
*/
|
*/
|
||||||
public function organization(): BelongsTo
|
public function organization(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -55,7 +55,7 @@ class Client extends Model implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasMany<Project>
|
* @return HasMany<Project, $this>
|
||||||
*/
|
*/
|
||||||
public function projects(): HasMany
|
public function projects(): HasMany
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
|||||||
* @property Carbon|null $updated_at
|
* @property Carbon|null $updated_at
|
||||||
* @property-read Organization $organization
|
* @property-read Organization $organization
|
||||||
* @property-read User $user
|
* @property-read User $user
|
||||||
* @property-read Collection<ProjectMember> $projectMembers
|
* @property-read Collection<int, ProjectMember> $projectMembers
|
||||||
* @property-read Collection<TimeEntry> $timeEntries
|
* @property-read Collection<int, TimeEntry> $timeEntries
|
||||||
*
|
*
|
||||||
* @method static MemberFactory factory()
|
* @method static MemberFactory factory()
|
||||||
*/
|
*/
|
||||||
@@ -47,7 +47,7 @@ class Member extends JetstreamMembership implements AuditableContract
|
|||||||
protected $table = 'members';
|
protected $table = 'members';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<User, Member>
|
* @return BelongsTo<User, $this>
|
||||||
*/
|
*/
|
||||||
public function user(): BelongsTo
|
public function user(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -55,7 +55,7 @@ class Member extends JetstreamMembership implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Organization, Member>
|
* @return BelongsTo<Organization, $this>
|
||||||
*/
|
*/
|
||||||
public function organization(): BelongsTo
|
public function organization(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -63,7 +63,7 @@ class Member extends JetstreamMembership implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasMany<TimeEntry>
|
* @return HasMany<TimeEntry, $this>
|
||||||
*/
|
*/
|
||||||
public function timeEntries(): HasMany
|
public function timeEntries(): HasMany
|
||||||
{
|
{
|
||||||
@@ -71,7 +71,7 @@ class Member extends JetstreamMembership implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasMany<ProjectMember>
|
* @return HasMany<ProjectMember, $this>
|
||||||
*/
|
*/
|
||||||
public function projectMembers(): HasMany
|
public function projectMembers(): HasMany
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Laravel\Jetstream\Events\TeamCreated;
|
use Laravel\Jetstream\Events\TeamCreated;
|
||||||
@@ -47,7 +48,7 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
|||||||
* @property IntervalFormat $interval_format
|
* @property IntervalFormat $interval_format
|
||||||
* @property TimeFormat $time_format
|
* @property TimeFormat $time_format
|
||||||
*
|
*
|
||||||
* @method HasMany<OrganizationInvitation> teamInvitations()
|
* @method HasMany<OrganizationInvitation, $this> teamInvitations()
|
||||||
* @method static OrganizationFactory factory()
|
* @method static OrganizationFactory factory()
|
||||||
*/
|
*/
|
||||||
class Organization extends JetstreamTeam implements AuditableContract
|
class Organization extends JetstreamTeam implements AuditableContract
|
||||||
@@ -79,7 +80,7 @@ class Organization extends JetstreamTeam implements AuditableContract
|
|||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
*
|
*
|
||||||
* @var array<int, string>
|
* @var list<string>
|
||||||
*/
|
*/
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'name',
|
||||||
@@ -125,7 +126,7 @@ class Organization extends JetstreamTeam implements AuditableContract
|
|||||||
/**
|
/**
|
||||||
* Get all the users that belong to the team.
|
* Get all the users that belong to the team.
|
||||||
*
|
*
|
||||||
* @return BelongsToMany<User>
|
* @return BelongsToMany<User, $this, Pivot, 'membership'>
|
||||||
*/
|
*/
|
||||||
public function users(): BelongsToMany
|
public function users(): BelongsToMany
|
||||||
{
|
{
|
||||||
@@ -142,7 +143,7 @@ class Organization extends JetstreamTeam implements AuditableContract
|
|||||||
/**
|
/**
|
||||||
* Get the owner of the team.
|
* Get the owner of the team.
|
||||||
*
|
*
|
||||||
* @return BelongsTo<User, Organization>
|
* @return BelongsTo<User, $this>
|
||||||
*/
|
*/
|
||||||
public function owner(): BelongsTo
|
public function owner(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -150,7 +151,7 @@ class Organization extends JetstreamTeam implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasMany<Member>
|
* @return HasMany<Member, $this>
|
||||||
*/
|
*/
|
||||||
public function members(): HasMany
|
public function members(): HasMany
|
||||||
{
|
{
|
||||||
@@ -158,7 +159,7 @@ class Organization extends JetstreamTeam implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsToMany<User>
|
* @return BelongsToMany<User, $this, Pivot, 'membership'>
|
||||||
*/
|
*/
|
||||||
public function realUsers(): BelongsToMany
|
public function realUsers(): BelongsToMany
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class OrganizationInvitation extends JetstreamTeamInvitation implements Auditabl
|
|||||||
/**
|
/**
|
||||||
* Get the organization that the invitation belongs to.
|
* Get the organization that the invitation belongs to.
|
||||||
*
|
*
|
||||||
* @return BelongsTo<Organization, OrganizationInvitation>
|
* @return BelongsTo<Organization, $this>
|
||||||
*/
|
*/
|
||||||
public function organization(): BelongsTo
|
public function organization(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -63,7 +63,7 @@ class OrganizationInvitation extends JetstreamTeamInvitation implements Auditabl
|
|||||||
/**
|
/**
|
||||||
* Get the organization that the invitation belongs to.
|
* Get the organization that the invitation belongs to.
|
||||||
*
|
*
|
||||||
* @return BelongsTo<Organization, OrganizationInvitation>
|
* @return BelongsTo<Organization, $this>
|
||||||
*/
|
*/
|
||||||
public function team(): BelongsTo
|
public function team(): BelongsTo
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,26 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Models\Passport;
|
namespace App\Models\Passport;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
use Laravel\Passport\AuthCode as PassportAuthCode;
|
use Laravel\Passport\AuthCode as PassportAuthCode;
|
||||||
|
|
||||||
class AuthCode extends PassportAuthCode {}
|
/**
|
||||||
|
* @property string $id
|
||||||
|
* @property string $user_id
|
||||||
|
* @property string $client_id
|
||||||
|
* @property string|null $scopes
|
||||||
|
* @property bool $revoked
|
||||||
|
* @property Carbon $expires_at
|
||||||
|
*/
|
||||||
|
class AuthCode extends PassportAuthCode
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return BelongsTo<User, $this>
|
||||||
|
*/
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'user_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,22 +5,36 @@ declare(strict_types=1);
|
|||||||
namespace App\Models\Passport;
|
namespace App\Models\Passport;
|
||||||
|
|
||||||
use Database\Factories\Passport\ClientFactory;
|
use Database\Factories\Passport\ClientFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
use Laravel\Passport\Client as PassportClient;
|
use Laravel\Passport\Client as PassportClient;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property string $id
|
* @property string $id
|
||||||
* @property string|null $user_id
|
* @property string|null $owner_id
|
||||||
|
* @property string|null $owner_type
|
||||||
* @property string $name
|
* @property string $name
|
||||||
* @property string|null $secret
|
* @property string|null $secret
|
||||||
* @property string|null $provider
|
* @property string|null $provider
|
||||||
* @property string $redirect
|
* @property array<string> $grant_types
|
||||||
* @property bool $personal_access_client
|
* @property array<string> $redirect_uris
|
||||||
* @property bool $password_client
|
* @property Carbon|null $created_at
|
||||||
|
* @property Carbon|null $updated_at
|
||||||
* @property bool $revoked
|
* @property bool $revoked
|
||||||
*/
|
*/
|
||||||
class Client extends PassportClient
|
class Client extends PassportClient
|
||||||
{
|
{
|
||||||
/** @use HasFactory<ClientFactory> */
|
/** @use HasFactory<ClientFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new factory instance for the model.
|
||||||
|
*
|
||||||
|
* @return ClientFactory
|
||||||
|
*/
|
||||||
|
protected static function newFactory(): Factory
|
||||||
|
{
|
||||||
|
return ClientFactory::new();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Models\Passport;
|
|
||||||
|
|
||||||
use Laravel\Passport\PersonalAccessClient as PassportPersonalAccessClient;
|
|
||||||
|
|
||||||
class PersonalAccessClient extends PassportPersonalAccessClient {}
|
|
||||||
@@ -4,7 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Models\Passport;
|
namespace App\Models\Passport;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
use Database\Factories\Passport\TokenFactory;
|
use Database\Factories\Passport\TokenFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
@@ -17,9 +19,15 @@ use Laravel\Passport\Token as PassportToken;
|
|||||||
* @property null|string $name
|
* @property null|string $name
|
||||||
* @property array<string> $scopes
|
* @property array<string> $scopes
|
||||||
* @property bool $revoked
|
* @property bool $revoked
|
||||||
|
* @property Carbon|null $reminder_sent_at
|
||||||
|
* @property Carbon|null $expired_info_sent_at
|
||||||
* @property Carbon|null $created_at
|
* @property Carbon|null $created_at
|
||||||
* @property Carbon|null $updated_at
|
* @property Carbon|null $updated_at
|
||||||
* @property Carbon|null $expires_at
|
* @property Carbon|null $expires_at
|
||||||
|
* @property-read Client|null $client
|
||||||
|
* @property-read User|null $user
|
||||||
|
*
|
||||||
|
* @method Builder<Token> isApiToken(bool $isApiToken = true)
|
||||||
*/
|
*/
|
||||||
class Token extends PassportToken
|
class Token extends PassportToken
|
||||||
{
|
{
|
||||||
@@ -29,10 +37,60 @@ class Token extends PassportToken
|
|||||||
/**
|
/**
|
||||||
* Get the client that the token belongs to.
|
* Get the client that the token belongs to.
|
||||||
*
|
*
|
||||||
* @return BelongsTo<Client, Token>
|
* @return BelongsTo<Client, $this>
|
||||||
*/
|
*/
|
||||||
|
// @phpstan-ignore method.childReturnType
|
||||||
public function client(): BelongsTo
|
public function client(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Client::class, 'client_id', 'id');
|
return $this->belongsTo(Client::class, 'client_id', 'id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user that the token belongs to.
|
||||||
|
*
|
||||||
|
* @deprecated Will be removed in a future Laravel version.
|
||||||
|
*
|
||||||
|
* @return BelongsTo<User, $this>
|
||||||
|
*/
|
||||||
|
// @phpstan-ignore method.childReturnType
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'scopes' => 'array',
|
||||||
|
'revoked' => 'bool',
|
||||||
|
'expires_at' => 'datetime',
|
||||||
|
'reminder_sent_at' => 'datetime',
|
||||||
|
'expired_info_sent_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Builder<static> $query
|
||||||
|
* @return Builder<static>
|
||||||
|
*/
|
||||||
|
public function scopeIsApiToken(Builder $query, bool $isApiToken = true): Builder
|
||||||
|
{
|
||||||
|
if ($isApiToken) {
|
||||||
|
return $query->whereHas('client', function (Builder $query): void {
|
||||||
|
/** @var Builder<Client> $query */
|
||||||
|
$query->whereJsonContains('grant_types', 'personal_access');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return $query->whereHas('client', function (Builder $query): void {
|
||||||
|
/** @var Builder<Client> $query */
|
||||||
|
$query->whereJsonDoesntContain('grant_types', 'personal_access');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ class Project extends Model implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Organization, Project>
|
* @return BelongsTo<Organization, $this>
|
||||||
*/
|
*/
|
||||||
public function organization(): BelongsTo
|
public function organization(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -145,7 +145,7 @@ class Project extends Model implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Client, Project>
|
* @return BelongsTo<Client, $this>
|
||||||
*/
|
*/
|
||||||
public function client(): BelongsTo
|
public function client(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -153,7 +153,7 @@ class Project extends Model implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasMany<ProjectMember>
|
* @return HasMany<ProjectMember, $this>
|
||||||
*/
|
*/
|
||||||
public function members(): HasMany
|
public function members(): HasMany
|
||||||
{
|
{
|
||||||
@@ -161,7 +161,7 @@ class Project extends Model implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasMany<Task>
|
* @return HasMany<Task, $this>
|
||||||
*/
|
*/
|
||||||
public function tasks(): HasMany
|
public function tasks(): HasMany
|
||||||
{
|
{
|
||||||
@@ -169,7 +169,7 @@ class Project extends Model implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasMany<TimeEntry>
|
* @return HasMany<TimeEntry, $this>
|
||||||
*/
|
*/
|
||||||
public function timeEntries(): HasMany
|
public function timeEntries(): HasMany
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class ProjectMember extends Model implements AuditableContract
|
|||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Project, ProjectMember>
|
* @return BelongsTo<Project, $this>
|
||||||
*/
|
*/
|
||||||
public function project(): BelongsTo
|
public function project(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -58,7 +58,7 @@ class ProjectMember extends Model implements AuditableContract
|
|||||||
/**
|
/**
|
||||||
* @deprecated Use member relationship instead
|
* @deprecated Use member relationship instead
|
||||||
*
|
*
|
||||||
* @return BelongsTo<User, ProjectMember>
|
* @return BelongsTo<User, $this>
|
||||||
*/
|
*/
|
||||||
public function user(): BelongsTo
|
public function user(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -66,7 +66,7 @@ class ProjectMember extends Model implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Member, ProjectMember>
|
* @return BelongsTo<Member, $this>
|
||||||
*/
|
*/
|
||||||
public function member(): BelongsTo
|
public function member(): BelongsTo
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class Report extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Organization, Report>
|
* @return BelongsTo<Organization, $this>
|
||||||
*/
|
*/
|
||||||
public function organization(): BelongsTo
|
public function organization(): BelongsTo
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use Staudenmeir\EloquentJsonRelations\Relations\HasManyJson;
|
|||||||
* @property string $organization_id
|
* @property string $organization_id
|
||||||
* @property Carbon|null $created_at
|
* @property Carbon|null $created_at
|
||||||
* @property Carbon|null $updated_at
|
* @property Carbon|null $updated_at
|
||||||
* @property-read Collection<TimeEntry> $timeEntries
|
* @property-read Collection<int, TimeEntry> $timeEntries
|
||||||
* @property-read Organization $organization
|
* @property-read Organization $organization
|
||||||
*
|
*
|
||||||
* @method static TagFactory factory()
|
* @method static TagFactory factory()
|
||||||
@@ -47,7 +47,7 @@ class Tag extends Model implements AuditableContract
|
|||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Organization, Tag>
|
* @return BelongsTo<Organization, $this>
|
||||||
*/
|
*/
|
||||||
public function organization(): BelongsTo
|
public function organization(): BelongsTo
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ class Task extends Model implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Project, Task>
|
* @return BelongsTo<Project, $this>
|
||||||
*/
|
*/
|
||||||
public function project(): BelongsTo
|
public function project(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -128,7 +128,7 @@ class Task extends Model implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Organization, Task>
|
* @return BelongsTo<Organization, $this>
|
||||||
*/
|
*/
|
||||||
public function organization(): BelongsTo
|
public function organization(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -136,7 +136,7 @@ class Task extends Model implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasMany<TimeEntry>
|
* @return HasMany<TimeEntry, $this>
|
||||||
*/
|
*/
|
||||||
public function timeEntries(): HasMany
|
public function timeEntries(): HasMany
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ use Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson;
|
|||||||
* @property Carbon|null $end
|
* @property Carbon|null $end
|
||||||
* @property int|null $billable_rate Billable rate per hour in cents
|
* @property int|null $billable_rate Billable rate per hour in cents
|
||||||
* @property bool $billable
|
* @property bool $billable
|
||||||
* @property array $tags
|
* @property array<string> $tags
|
||||||
* @property string $user_id
|
* @property string $user_id
|
||||||
* @property string $member_id
|
* @property string $member_id
|
||||||
* @property bool $is_imported
|
* @property bool $is_imported
|
||||||
@@ -45,7 +45,7 @@ use Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson;
|
|||||||
* @property-read Client|null $client
|
* @property-read Client|null $client
|
||||||
* @property string|null $task_id
|
* @property string|null $task_id
|
||||||
* @property-read Task|null $task
|
* @property-read Task|null $task
|
||||||
* @property-read Collection<Tag> $tagsRelation
|
* @property-read Collection<int, Tag> $tagsRelation
|
||||||
*
|
*
|
||||||
* @method Builder<TimeEntry> hasTag(Tag $tag)
|
* @method Builder<TimeEntry> hasTag(Tag $tag)
|
||||||
* @method static TimeEntryFactory factory()
|
* @method static TimeEntryFactory factory()
|
||||||
@@ -77,6 +77,26 @@ class TimeEntry extends Model implements AuditableContract
|
|||||||
'still_active_email_sent_at' => 'datetime',
|
'still_active_email_sent_at' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public const array SELECT_COLUMNS = [
|
||||||
|
'id',
|
||||||
|
'description',
|
||||||
|
'start',
|
||||||
|
'end',
|
||||||
|
'billable_rate',
|
||||||
|
'billable',
|
||||||
|
'user_id',
|
||||||
|
'organization_id',
|
||||||
|
'project_id',
|
||||||
|
'task_id',
|
||||||
|
'tags',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
'member_id',
|
||||||
|
'client_id',
|
||||||
|
'is_imported',
|
||||||
|
'still_active_email_sent_at',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are computed. (f.e. for performance reasons)
|
* The attributes that are computed. (f.e. for performance reasons)
|
||||||
* These attributes can be regenerated at any time.
|
* These attributes can be regenerated at any time.
|
||||||
@@ -154,7 +174,7 @@ class TimeEntry extends Model implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<User, TimeEntry>
|
* @return BelongsTo<User, $this>
|
||||||
*/
|
*/
|
||||||
public function user(): BelongsTo
|
public function user(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -162,7 +182,7 @@ class TimeEntry extends Model implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Member, TimeEntry>
|
* @return BelongsTo<Member, $this>
|
||||||
*/
|
*/
|
||||||
public function member(): BelongsTo
|
public function member(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -170,7 +190,7 @@ class TimeEntry extends Model implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Organization, TimeEntry>
|
* @return BelongsTo<Organization, $this>
|
||||||
*/
|
*/
|
||||||
public function organization(): BelongsTo
|
public function organization(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -178,7 +198,7 @@ class TimeEntry extends Model implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Project, TimeEntry>
|
* @return BelongsTo<Project, $this>
|
||||||
*/
|
*/
|
||||||
public function project(): BelongsTo
|
public function project(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -186,7 +206,7 @@ class TimeEntry extends Model implements AuditableContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Task, TimeEntry>
|
* @return BelongsTo<Task, $this>
|
||||||
*/
|
*/
|
||||||
public function task(): BelongsTo
|
public function task(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -196,7 +216,7 @@ class TimeEntry extends Model implements AuditableContract
|
|||||||
/**
|
/**
|
||||||
* This relation can be reconstructed via the task relation. It is only here for performance reasons.
|
* This relation can be reconstructed via the task relation. It is only here for performance reasons.
|
||||||
*
|
*
|
||||||
* @return BelongsTo<Client, TimeEntry>
|
* @return BelongsTo<Client, $this>
|
||||||
*/
|
*/
|
||||||
public function client(): BelongsTo
|
public function client(): BelongsTo
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
@@ -27,6 +28,7 @@ use Laravel\Fortify\TwoFactorAuthenticatable;
|
|||||||
use Laravel\Jetstream\HasProfilePhoto;
|
use Laravel\Jetstream\HasProfilePhoto;
|
||||||
use Laravel\Jetstream\HasTeams;
|
use Laravel\Jetstream\HasTeams;
|
||||||
use Laravel\Passport\AuthCode;
|
use Laravel\Passport\AuthCode;
|
||||||
|
use Laravel\Passport\Contracts\OAuthenticatable;
|
||||||
use Laravel\Passport\HasApiTokens;
|
use Laravel\Passport\HasApiTokens;
|
||||||
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||||
|
|
||||||
@@ -52,13 +54,13 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
|||||||
* @property Collection<int, TimeEntry> $timeEntries
|
* @property Collection<int, TimeEntry> $timeEntries
|
||||||
* @property Member $membership
|
* @property Member $membership
|
||||||
*
|
*
|
||||||
* @method HasMany<Organization> ownedTeams()
|
* @method HasMany<Organization, $this> ownedTeams()
|
||||||
* @method static UserFactory factory()
|
* @method static UserFactory factory()
|
||||||
* @method static Builder<User> query()
|
* @method static Builder<User> query()
|
||||||
* @method Builder<User> belongsToOrganization(Organization $organization)
|
* @method Builder<User> belongsToOrganization(Organization $organization)
|
||||||
* @method Builder<User> active()
|
* @method Builder<User> active()
|
||||||
*/
|
*/
|
||||||
class User extends Authenticatable implements AuditableContract, FilamentUser, MustVerifyEmail
|
class User extends Authenticatable implements AuditableContract, FilamentUser, MustVerifyEmail, OAuthenticatable
|
||||||
{
|
{
|
||||||
use CustomAuditable;
|
use CustomAuditable;
|
||||||
use HasApiTokens;
|
use HasApiTokens;
|
||||||
@@ -75,7 +77,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
|||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
*
|
*
|
||||||
* @var array<int, string>
|
* @var list<string>
|
||||||
*/
|
*/
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'name',
|
||||||
@@ -86,7 +88,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
|||||||
/**
|
/**
|
||||||
* The attributes that should be hidden for serialization.
|
* The attributes that should be hidden for serialization.
|
||||||
*
|
*
|
||||||
* @var array<int, string>
|
* @var list<string>
|
||||||
*/
|
*/
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
'password',
|
'password',
|
||||||
@@ -143,7 +145,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsToMany<Organization>
|
* @return BelongsToMany<Organization, $this, Pivot, 'membership'>
|
||||||
*/
|
*/
|
||||||
public function organizations(): BelongsToMany
|
public function organizations(): BelongsToMany
|
||||||
{
|
{
|
||||||
@@ -158,7 +160,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasMany<TimeEntry>
|
* @return HasMany<TimeEntry, $this>
|
||||||
*/
|
*/
|
||||||
public function timeEntries(): HasMany
|
public function timeEntries(): HasMany
|
||||||
{
|
{
|
||||||
@@ -166,7 +168,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Organization, User>
|
* @return BelongsTo<Organization, $this>
|
||||||
*/
|
*/
|
||||||
public function currentOrganization(): BelongsTo
|
public function currentOrganization(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -174,7 +176,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasMany<ProjectMember>
|
* @return HasMany<ProjectMember, $this>
|
||||||
*/
|
*/
|
||||||
public function projectMembers(): HasMany
|
public function projectMembers(): HasMany
|
||||||
{
|
{
|
||||||
@@ -182,7 +184,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasMany<Token>
|
* @return HasMany<Token, $this>
|
||||||
*/
|
*/
|
||||||
public function accessTokens(): HasMany
|
public function accessTokens(): HasMany
|
||||||
{
|
{
|
||||||
@@ -190,24 +192,13 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasMany<AuthCode>
|
* @return HasMany<AuthCode, $this>
|
||||||
*/
|
*/
|
||||||
public function authCodes(): HasMany
|
public function authCodes(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(AuthCode::class);
|
return $this->hasMany(AuthCode::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the access tokens for the user.
|
|
||||||
*
|
|
||||||
* @return HasMany<Token>
|
|
||||||
*/
|
|
||||||
public function tokens(): HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(Token::class, 'user_id')
|
|
||||||
->orderBy('created_at', 'desc');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Builder<User> $builder
|
* @param Builder<User> $builder
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ namespace App\Providers;
|
|||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\Passport\AuthCode;
|
use App\Models\Passport\AuthCode;
|
||||||
use App\Models\Passport\Client;
|
use App\Models\Passport\Client;
|
||||||
use App\Models\Passport\PersonalAccessClient;
|
|
||||||
use App\Models\Passport\RefreshToken;
|
use App\Models\Passport\RefreshToken;
|
||||||
use App\Models\Passport\Token;
|
use App\Models\Passport\Token;
|
||||||
use App\Policies\OrganizationPolicy;
|
use App\Policies\OrganizationPolicy;
|
||||||
@@ -51,7 +50,8 @@ class AuthServiceProvider extends ServiceProvider
|
|||||||
Passport::useRefreshTokenModel(RefreshToken::class);
|
Passport::useRefreshTokenModel(RefreshToken::class);
|
||||||
Passport::useAuthCodeModel(AuthCode::class);
|
Passport::useAuthCodeModel(AuthCode::class);
|
||||||
Passport::useClientModel(Client::class);
|
Passport::useClientModel(Client::class);
|
||||||
Passport::usePersonalAccessClientModel(PersonalAccessClient::class);
|
|
||||||
|
Passport::authorizationView('auth.oauth.authorize');
|
||||||
|
|
||||||
// Passport::tokensExpireIn(now()->addDays(15));
|
// Passport::tokensExpireIn(now()->addDays(15));
|
||||||
// Passport::refreshTokensExpireIn(now()->addDays(30));
|
// Passport::refreshTokensExpireIn(now()->addDays(30));
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ class JetstreamServiceProvider extends ServiceProvider
|
|||||||
'members:invite-placeholder',
|
'members:invite-placeholder',
|
||||||
'members:make-placeholder',
|
'members:make-placeholder',
|
||||||
'members:merge-into',
|
'members:merge-into',
|
||||||
|
'members:delete',
|
||||||
'members:update',
|
'members:update',
|
||||||
'reports:view',
|
'reports:view',
|
||||||
'reports:create',
|
'reports:create',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Service;
|
namespace App\Service;
|
||||||
|
|
||||||
|
use Brick\Money\ISOCurrencyProvider;
|
||||||
use Brick\Money\Money;
|
use Brick\Money\Money;
|
||||||
|
|
||||||
class CurrencyService
|
class CurrencyService
|
||||||
@@ -374,4 +375,12 @@ class CurrencyService
|
|||||||
|
|
||||||
return $currencyCode;
|
return $currencyCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRandomCurrencyCode(): string
|
||||||
|
{
|
||||||
|
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies();
|
||||||
|
$currencyCodes = array_keys($currencies);
|
||||||
|
|
||||||
|
return $currencyCodes[array_rand($currencyCodes)];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,12 +100,18 @@ class DeletionService
|
|||||||
|
|
||||||
// Make sure all users have at least one organization and delete placeholders
|
// Make sure all users have at least one organization and delete placeholders
|
||||||
foreach ($users as $user) {
|
foreach ($users as $user) {
|
||||||
|
/** @var User $user */
|
||||||
if ($ignoreUser !== null && $user->is($ignoreUser)) {
|
if ($ignoreUser !== null && $user->is($ignoreUser)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ($user->is_placeholder) {
|
if ($user->is_placeholder) {
|
||||||
$user->delete();
|
$user->delete();
|
||||||
} else {
|
} else {
|
||||||
|
if ($user->current_team_id === $organization->getKey()) {
|
||||||
|
$user->currentOrganization()->disassociate();
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
|
||||||
$this->userService->makeSureUserHasAtLeastOneOrganization($user);
|
$this->userService->makeSureUserHasAtLeastOneOrganization($user);
|
||||||
$this->userService->makeSureUserHasCurrentOrganization($user);
|
$this->userService->makeSureUserHasCurrentOrganization($user);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace App\Service\Dto;
|
|||||||
|
|
||||||
use App\Enums\TimeEntryAggregationType;
|
use App\Enums\TimeEntryAggregationType;
|
||||||
use App\Enums\TimeEntryAggregationTypeInterval;
|
use App\Enums\TimeEntryAggregationTypeInterval;
|
||||||
|
use App\Enums\TimeEntryRoundingType;
|
||||||
use App\Enums\Weekday;
|
use App\Enums\Weekday;
|
||||||
use Illuminate\Contracts\Database\Eloquent\Castable;
|
use Illuminate\Contracts\Database\Eloquent\Castable;
|
||||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||||
@@ -59,6 +60,10 @@ class ReportPropertiesDto implements Castable
|
|||||||
*/
|
*/
|
||||||
public ?Collection $taskIds = null;
|
public ?Collection $taskIds = null;
|
||||||
|
|
||||||
|
public ?TimeEntryRoundingType $roundingType = null;
|
||||||
|
|
||||||
|
public ?int $roundingMinutes = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the caster class to use when casting from / to this cast target.
|
* Get the caster class to use when casting from / to this cast target.
|
||||||
*
|
*
|
||||||
@@ -115,13 +120,14 @@ class ReportPropertiesDto implements Castable
|
|||||||
$dto->historyGroup = TimeEntryAggregationTypeInterval::from($data->historyGroup);
|
$dto->historyGroup = TimeEntryAggregationTypeInterval::from($data->historyGroup);
|
||||||
$dto->weekStart = Weekday::from($data->weekStart);
|
$dto->weekStart = Weekday::from($data->weekStart);
|
||||||
$dto->timezone = $data->timezone;
|
$dto->timezone = $data->timezone;
|
||||||
|
// Note: roundingType was added later so it is possible that the value is missing in persisted reports in the DB
|
||||||
|
$dto->roundingType = isset($data->roundingType) ? TimeEntryRoundingType::from($data->roundingType) : null;
|
||||||
|
// Note: roundingMinutes was added later so it is possible that the value is missing in persisted reports in the DB
|
||||||
|
$dto->roundingMinutes = isset($data->roundingMinutes) ? (int) $data->roundingMinutes : null;
|
||||||
|
|
||||||
return $dto;
|
return $dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param ReportPropertiesDto $value
|
|
||||||
*/
|
|
||||||
public function set(Model $model, string $key, mixed $value, array $attributes): string
|
public function set(Model $model, string $key, mixed $value, array $attributes): string
|
||||||
{
|
{
|
||||||
if (! ($value instanceof ReportPropertiesDto)) {
|
if (! ($value instanceof ReportPropertiesDto)) {
|
||||||
@@ -143,6 +149,8 @@ class ReportPropertiesDto implements Castable
|
|||||||
'historyGroup' => $value->historyGroup->value,
|
'historyGroup' => $value->historyGroup->value,
|
||||||
'weekStart' => $value->weekStart->value,
|
'weekStart' => $value->weekStart->value,
|
||||||
'timezone' => $value->timezone,
|
'timezone' => $value->timezone,
|
||||||
|
'roundingType' => $value->roundingType?->value,
|
||||||
|
'roundingMinutes' => $value->roundingMinutes,
|
||||||
];
|
];
|
||||||
|
|
||||||
$jsonString = json_encode($data);
|
$jsonString = json_encode($data);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Service;
|
namespace App\Service;
|
||||||
|
|
||||||
use App\Enums\Role;
|
use App\Enums\Role;
|
||||||
|
use App\Exceptions\Api\InvitationForTheEmailAlreadyExistsApiException;
|
||||||
use App\Exceptions\Api\UserIsAlreadyMemberOfOrganizationApiException;
|
use App\Exceptions\Api\UserIsAlreadyMemberOfOrganizationApiException;
|
||||||
use App\Mail\OrganizationInvitationMail;
|
use App\Mail\OrganizationInvitationMail;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
@@ -16,7 +17,7 @@ use Laravel\Jetstream\Events\InvitingTeamMember;
|
|||||||
class InvitationService
|
class InvitationService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @throws UserIsAlreadyMemberOfOrganizationApiException
|
* @throws UserIsAlreadyMemberOfOrganizationApiException|InvitationForTheEmailAlreadyExistsApiException
|
||||||
*/
|
*/
|
||||||
public function inviteUser(Organization $organization, string $email, Role $role): OrganizationInvitation
|
public function inviteUser(Organization $organization, string $email, Role $role): OrganizationInvitation
|
||||||
{
|
{
|
||||||
@@ -28,6 +29,13 @@ class InvitationService
|
|||||||
throw new UserIsAlreadyMemberOfOrganizationApiException;
|
throw new UserIsAlreadyMemberOfOrganizationApiException;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (OrganizationInvitation::query()
|
||||||
|
->where('email', $email)
|
||||||
|
->whereBelongsTo($organization, 'organization')
|
||||||
|
->exists()) {
|
||||||
|
throw new InvitationForTheEmailAlreadyExistsApiException;
|
||||||
|
}
|
||||||
|
|
||||||
InvitingTeamMember::dispatch($organization, $email, $role->value);
|
InvitingTeamMember::dispatch($organization, $email, $role->value);
|
||||||
|
|
||||||
$invitation = new OrganizationInvitation;
|
$invitation = new OrganizationInvitation;
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ class MemberService
|
|||||||
$member->organization()->associate($organization);
|
$member->organization()->associate($organization);
|
||||||
$member->role = $role->value;
|
$member->role = $role->value;
|
||||||
$member->save();
|
$member->save();
|
||||||
|
|
||||||
|
$user->currentOrganization()->associate($organization);
|
||||||
|
$user->save();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (! $asSuperAdmin) {
|
if (! $asSuperAdmin) {
|
||||||
@@ -58,19 +61,41 @@ class MemberService
|
|||||||
* @throws CanNotRemoveOwnerFromOrganization
|
* @throws CanNotRemoveOwnerFromOrganization
|
||||||
* @throws EntityStillInUseApiException
|
* @throws EntityStillInUseApiException
|
||||||
*/
|
*/
|
||||||
public function removeMember(Member $member, Organization $organization): void
|
public function removeMember(Member $member, Organization $organization, bool $withRelations = false): void
|
||||||
{
|
{
|
||||||
|
if ($member->role === Role::Owner->value) {
|
||||||
|
throw new CanNotRemoveOwnerFromOrganization;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $member->user;
|
||||||
|
$isPlaceholder = $user->is_placeholder;
|
||||||
|
|
||||||
|
if (! $isPlaceholder && $user->current_team_id === $member->organization_id) {
|
||||||
|
$user->currentTeam()->disassociate();
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($withRelations) {
|
||||||
|
TimeEntry::query()->where('user_id', $member->user_id)->whereBelongsTo($organization, 'organization')->delete();
|
||||||
|
ProjectMember::query()->whereBelongsToOrganization($organization)->where('user_id', $member->user_id)->delete();
|
||||||
|
} else {
|
||||||
if (TimeEntry::query()->where('user_id', $member->user_id)->whereBelongsTo($organization, 'organization')->exists()) {
|
if (TimeEntry::query()->where('user_id', $member->user_id)->whereBelongsTo($organization, 'organization')->exists()) {
|
||||||
throw new EntityStillInUseApiException('member', 'time_entry');
|
throw new EntityStillInUseApiException('member', 'time_entry');
|
||||||
}
|
}
|
||||||
if (ProjectMember::query()->whereBelongsToOrganization($organization)->where('user_id', $member->user_id)->exists()) {
|
if (ProjectMember::query()->whereBelongsToOrganization($organization)->where('user_id', $member->user_id)->exists()) {
|
||||||
throw new EntityStillInUseApiException('member', 'project_member');
|
throw new EntityStillInUseApiException('member', 'project_member');
|
||||||
}
|
}
|
||||||
if ($member->role === Role::Owner->value) {
|
|
||||||
throw new CanNotRemoveOwnerFromOrganization;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$member->delete();
|
$member->delete();
|
||||||
|
|
||||||
|
if ($isPlaceholder) {
|
||||||
|
$user->delete();
|
||||||
|
} else {
|
||||||
|
$this->userService->makeSureUserHasAtLeastOneOrganization($user);
|
||||||
|
$this->userService->makeSureUserHasCurrentOrganization($user);
|
||||||
|
}
|
||||||
|
|
||||||
MemberRemoved::dispatch($member, $organization);
|
MemberRemoved::dispatch($member, $organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +189,11 @@ class MemberService
|
|||||||
public function makeMemberToPlaceholder(Member $member, bool $makeSureUserHasAtLeastOneOrganization = true): void
|
public function makeMemberToPlaceholder(Member $member, bool $makeSureUserHasAtLeastOneOrganization = true): void
|
||||||
{
|
{
|
||||||
$user = $member->user;
|
$user = $member->user;
|
||||||
|
if ($user->current_team_id === $member->organization_id) {
|
||||||
|
$user->currentTeam()->disassociate();
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
|
||||||
$placeholderUser = $user->replicate();
|
$placeholderUser = $user->replicate();
|
||||||
$placeholderUser->is_placeholder = true;
|
$placeholderUser->is_placeholder = true;
|
||||||
$placeholderUser->save();
|
$placeholderUser->save();
|
||||||
@@ -175,6 +205,7 @@ class MemberService
|
|||||||
$this->userService->assignOrganizationEntitiesToDifferentUser($member->organization, $user, $placeholderUser);
|
$this->userService->assignOrganizationEntitiesToDifferentUser($member->organization, $user, $placeholderUser);
|
||||||
if ($makeSureUserHasAtLeastOneOrganization) {
|
if ($makeSureUserHasAtLeastOneOrganization) {
|
||||||
$this->userService->makeSureUserHasAtLeastOneOrganization($user);
|
$this->userService->makeSureUserHasAtLeastOneOrganization($user);
|
||||||
|
$this->userService->makeSureUserHasCurrentOrganization($user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class PermissionStore
|
|||||||
/** @var Role|null $roleObj */
|
/** @var Role|null $roleObj */
|
||||||
$roleObj = Jetstream::findRole($role);
|
$roleObj = Jetstream::findRole($role);
|
||||||
|
|
||||||
return $roleObj?->permissions ?? [];
|
return $roleObj->permissions ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ class TimeEntriesReportExport implements FromView, ShouldAutoSize, WithCustomCsv
|
|||||||
|
|
||||||
private TimeEntryAggregationType $subGroup;
|
private TimeEntryAggregationType $subGroup;
|
||||||
|
|
||||||
|
private bool $showBillableRate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{
|
* @param array{
|
||||||
* grouped_type: string|null,
|
* grouped_type: string|null,
|
||||||
@@ -66,13 +68,14 @@ class TimeEntriesReportExport implements FromView, ShouldAutoSize, WithCustomCsv
|
|||||||
* cost: int|null
|
* cost: int|null
|
||||||
* } $data
|
* } $data
|
||||||
*/
|
*/
|
||||||
public function __construct(array $data, ExportFormat $exportFormat, string $currency, TimeEntryAggregationType $group, TimeEntryAggregationType $subGroup)
|
public function __construct(array $data, ExportFormat $exportFormat, string $currency, TimeEntryAggregationType $group, TimeEntryAggregationType $subGroup, bool $showBillableRate)
|
||||||
{
|
{
|
||||||
$this->data = $data;
|
$this->data = $data;
|
||||||
$this->exportFormat = $exportFormat;
|
$this->exportFormat = $exportFormat;
|
||||||
$this->currency = $currency;
|
$this->currency = $currency;
|
||||||
$this->group = $group;
|
$this->group = $group;
|
||||||
$this->subGroup = $subGroup;
|
$this->subGroup = $subGroup;
|
||||||
|
$this->showBillableRate = $showBillableRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function view(): View
|
public function view(): View
|
||||||
@@ -83,6 +86,7 @@ class TimeEntriesReportExport implements FromView, ShouldAutoSize, WithCustomCsv
|
|||||||
'group' => $this->group,
|
'group' => $this->group,
|
||||||
'subGroup' => $this->subGroup,
|
'subGroup' => $this->subGroup,
|
||||||
'exportFormat' => $this->exportFormat,
|
'exportFormat' => $this->exportFormat,
|
||||||
|
'showBillableRate' => $this->showBillableRate,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace App\Service;
|
|||||||
|
|
||||||
use App\Enums\TimeEntryAggregationType;
|
use App\Enums\TimeEntryAggregationType;
|
||||||
use App\Enums\TimeEntryAggregationTypeInterval;
|
use App\Enums\TimeEntryAggregationTypeInterval;
|
||||||
|
use App\Enums\TimeEntryRoundingType;
|
||||||
use App\Enums\Weekday;
|
use App\Enums\Weekday;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
@@ -41,7 +42,7 @@ class TimeEntryAggregationService
|
|||||||
* cost: int|null
|
* cost: int|null
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
public function getAggregatedTimeEntries(Builder $timeEntriesQuery, ?TimeEntryAggregationType $group1Type, ?TimeEntryAggregationType $group2Type, string $timezone, Weekday $startOfWeek, bool $fillGapsInTimeGroups, ?Carbon $start, ?Carbon $end, bool $showBillableRate): array
|
public function getAggregatedTimeEntries(Builder $timeEntriesQuery, ?TimeEntryAggregationType $group1Type, ?TimeEntryAggregationType $group2Type, string $timezone, Weekday $startOfWeek, bool $fillGapsInTimeGroups, ?Carbon $start, ?Carbon $end, bool $showBillableRate, ?TimeEntryRoundingType $roundingType, ?int $roundingMinutes): array
|
||||||
{
|
{
|
||||||
$fillGapsInTimeGroupsIsPossible = $fillGapsInTimeGroups && $start !== null && $end !== null;
|
$fillGapsInTimeGroupsIsPossible = $fillGapsInTimeGroups && $start !== null && $end !== null;
|
||||||
$group1Select = null;
|
$group1Select = null;
|
||||||
@@ -56,15 +57,14 @@ class TimeEntryAggregationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$startRawSelect = app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes);
|
||||||
|
$endRawSelect = app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes);
|
||||||
|
|
||||||
$timeEntriesQuery->selectRaw(
|
$timeEntriesQuery->selectRaw(
|
||||||
($group1Select !== null ? $group1Select.' as group_1,' : '').
|
($group1Select !== null ? $group1Select.' as group_1,' : '').
|
||||||
($group2Select !== null ? $group2Select.' as group_2,' : '').
|
($group2Select !== null ? $group2Select.' as group_2,' : '').
|
||||||
' round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate,'.
|
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'.
|
||||||
' round(
|
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost'
|
||||||
sum(
|
|
||||||
extract(epoch from (coalesce("end", now()) - start)) * (coalesce(billable_rate, 0)::float/60/60)
|
|
||||||
)
|
|
||||||
) as cost'
|
|
||||||
);
|
);
|
||||||
if ($groupBy !== null) {
|
if ($groupBy !== null) {
|
||||||
$timeEntriesQuery->groupBy($groupBy);
|
$timeEntriesQuery->groupBy($groupBy);
|
||||||
@@ -164,9 +164,9 @@ class TimeEntryAggregationService
|
|||||||
* cost: int|null
|
* cost: int|null
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
public function getAggregatedTimeEntriesWithDescriptions(Builder $timeEntriesQuery, ?TimeEntryAggregationType $group1Type, ?TimeEntryAggregationType $group2Type, string $timezone, Weekday $startOfWeek, bool $fillGapsInTimeGroups, ?Carbon $start, ?Carbon $end, bool $showBillableRate): array
|
public function getAggregatedTimeEntriesWithDescriptions(Builder $timeEntriesQuery, ?TimeEntryAggregationType $group1Type, ?TimeEntryAggregationType $group2Type, string $timezone, Weekday $startOfWeek, bool $fillGapsInTimeGroups, ?Carbon $start, ?Carbon $end, bool $showBillableRate, ?TimeEntryRoundingType $roundingType, ?int $roundingMinutes): array
|
||||||
{
|
{
|
||||||
$aggregatedTimeEntries = $this->getAggregatedTimeEntries($timeEntriesQuery, $group1Type, $group2Type, $timezone, $startOfWeek, $fillGapsInTimeGroups, $start, $end, $showBillableRate);
|
$aggregatedTimeEntries = $this->getAggregatedTimeEntries($timeEntriesQuery, $group1Type, $group2Type, $timezone, $startOfWeek, $fillGapsInTimeGroups, $start, $end, $showBillableRate, $roundingType, $roundingMinutes);
|
||||||
|
|
||||||
$keysGroup1 = [];
|
$keysGroup1 = [];
|
||||||
$keysGroup2 = [];
|
$keysGroup2 = [];
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user