mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 08:12:17 +01:00
Compare commits
68 Commits
feature/li
...
feature/gi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c87645bcb2 | ||
|
|
15411ec0c8 | ||
|
|
48f09421d0 | ||
|
|
36caadeb14 | ||
|
|
b4edcaa2dc | ||
|
|
a3dda8b03c | ||
|
|
d64f0c52be | ||
|
|
c80d51c2e1 | ||
|
|
46dea00b34 | ||
|
|
16fed4a2b7 | ||
|
|
9a2af2e743 | ||
|
|
2e3a517502 | ||
|
|
a69fb9c551 | ||
|
|
62b5730fa8 | ||
|
|
098ead8da6 | ||
|
|
d49082d7f3 | ||
|
|
cc88f034c7 | ||
|
|
9620c89545 | ||
|
|
f9c3f42289 | ||
|
|
fca4c26cfc | ||
|
|
d8f4ba1517 | ||
|
|
284d8cd786 | ||
|
|
411fc6ea5e | ||
|
|
02a8367d16 | ||
|
|
68f636c8ff | ||
|
|
9c44abf7aa | ||
|
|
b1ff97a82f | ||
|
|
ed32c6b217 | ||
|
|
8b950d99d6 | ||
|
|
e374d8b3de | ||
|
|
301d09e830 | ||
|
|
49af3d4371 | ||
|
|
b4a6145f40 | ||
|
|
06c6c874eb | ||
|
|
b796d232f5 | ||
|
|
26c50867b3 | ||
|
|
b8110e222a | ||
|
|
7673b365ca | ||
|
|
da5fc3f113 | ||
|
|
8c66068663 | ||
|
|
dd0cc0d60b | ||
|
|
3a482c1e6a | ||
|
|
ef9f353047 | ||
|
|
f1a1d2a266 | ||
|
|
f5efbad703 | ||
|
|
17242188c2 | ||
|
|
0a376b1caa | ||
|
|
10a8310e37 | ||
|
|
89131b9e77 | ||
|
|
c17c5dc6c0 | ||
|
|
3444281703 | ||
|
|
84e2365a6d | ||
|
|
92ac9948a0 | ||
|
|
8da358dbe6 | ||
|
|
b7b9092e64 | ||
|
|
15ac3e9a43 | ||
|
|
d03dd60864 | ||
|
|
827e0fe377 | ||
|
|
e78a551098 | ||
|
|
ae00fdb0e9 | ||
|
|
3c9160a08a | ||
|
|
4fb744db1d | ||
|
|
bc9b104c3f | ||
|
|
880c363ae4 | ||
|
|
8e6d1abbf3 | ||
|
|
d202bd9c47 | ||
|
|
992d8945df | ||
|
|
df2fe1da1e |
24
.github/workflows/build-private.yml
vendored
24
.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
|
||||||
@@ -107,6 +110,24 @@ jobs:
|
|||||||
- name: "Install npm dependencies in services extension"
|
- name: "Install npm dependencies in services extension"
|
||||||
run: cd extensions/Services && npm ci
|
run: cd extensions/Services && npm ci
|
||||||
|
|
||||||
|
- name: "Checkout invoicing extension"
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: solidtime-io/extension-invoicing
|
||||||
|
path: extensions/Invoicing
|
||||||
|
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }}
|
||||||
|
|
||||||
|
- name: "Install composer dependencies in invoicing extension"
|
||||||
|
uses: php-actions/composer@v6
|
||||||
|
with:
|
||||||
|
working_dir: "extensions/Invoicing"
|
||||||
|
command: install
|
||||||
|
only_args: --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
|
||||||
|
php_version: 8.3
|
||||||
|
|
||||||
|
- name: "Install npm dependencies in invoicing extension"
|
||||||
|
run: cd extensions/Invoicing && npm ci
|
||||||
|
|
||||||
- name: "Setup PHP with PECL extension"
|
- name: "Setup PHP with PECL extension"
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
@@ -127,6 +148,9 @@ jobs:
|
|||||||
- name: "Activate services extension"
|
- name: "Activate services extension"
|
||||||
run: php artisan module:enable Services
|
run: php artisan module:enable Services
|
||||||
|
|
||||||
|
- name: "Activate invoicing extension"
|
||||||
|
run: php artisan module:enable Invoicing
|
||||||
|
|
||||||
- name: "Install npm dependencies"
|
- name: "Install npm dependencies"
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
|
|||||||
4
.github/workflows/phpunit.yml
vendored
4
.github/workflows/phpunit.yml
vendored
@@ -1,5 +1,7 @@
|
|||||||
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
|
||||||
@@ -63,7 +65,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.0
|
uses: codecov/codecov-action@v5.4.2
|
||||||
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
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ class CreateNewUser implements CreatesNewUsers
|
|||||||
$ipLookupResponse = app(IpLookupServiceContract::class)->lookup(request()->ip());
|
$ipLookupResponse = app(IpLookupServiceContract::class)->lookup(request()->ip());
|
||||||
|
|
||||||
$startOfWeek = Weekday::Monday;
|
$startOfWeek = Weekday::Monday;
|
||||||
|
$numberFormat = null;
|
||||||
|
$currencyFormat = null;
|
||||||
|
$dateFormat = null;
|
||||||
|
$intervalFormat = null;
|
||||||
|
$timeFormat = null;
|
||||||
$currency = null;
|
$currency = null;
|
||||||
if ($ipLookupResponse !== null) {
|
if ($ipLookupResponse !== null) {
|
||||||
$startOfWeek = $ipLookupResponse->startOfWeek ?? Weekday::Monday;
|
$startOfWeek = $ipLookupResponse->startOfWeek ?? Weekday::Monday;
|
||||||
@@ -85,7 +90,7 @@ class CreateNewUser implements CreatesNewUsers
|
|||||||
$currency = $ipLookupResponse->currency;
|
$currency = $ipLookupResponse->currency;
|
||||||
}
|
}
|
||||||
$user = null;
|
$user = null;
|
||||||
DB::transaction(function () use (&$user, $input, $timezone, $startOfWeek, $currency): void {
|
DB::transaction(function () use (&$user, $input, $timezone, $startOfWeek, $currency, $numberFormat, $currencyFormat, $dateFormat, $intervalFormat, $timeFormat): void {
|
||||||
$userService = app(UserService::class);
|
$userService = app(UserService::class);
|
||||||
$user = $userService->createUser(
|
$user = $userService->createUser(
|
||||||
$input['name'],
|
$input['name'],
|
||||||
@@ -93,7 +98,12 @@ class CreateNewUser implements CreatesNewUsers
|
|||||||
$input['password'],
|
$input['password'],
|
||||||
$timezone ?? 'UTC',
|
$timezone ?? 'UTC',
|
||||||
$startOfWeek,
|
$startOfWeek,
|
||||||
$currency ?? 'EUR',
|
$currency,
|
||||||
|
$numberFormat,
|
||||||
|
$currencyFormat,
|
||||||
|
$dateFormat,
|
||||||
|
$intervalFormat,
|
||||||
|
$timeFormat
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Actions\Jetstream;
|
namespace App\Actions\Jetstream;
|
||||||
|
|
||||||
use App\Enums\Role;
|
|
||||||
use App\Events\AfterCreateOrganization;
|
use App\Events\AfterCreateOrganization;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Service\IpLookup\IpLookupServiceContract;
|
||||||
|
use App\Service\OrganizationService;
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
@@ -33,16 +34,18 @@ class CreateOrganization implements CreatesTeams
|
|||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
])->validateWithBag('createTeam');
|
])->validateWithBag('createTeam');
|
||||||
|
|
||||||
$organization = new Organization;
|
$ipLookupResponse = app(IpLookupServiceContract::class)->lookup(request()->ip());
|
||||||
$organization->name = $input['name'];
|
|
||||||
$organization->personal_team = false;
|
|
||||||
$organization->owner()->associate($user);
|
|
||||||
$organization->save();
|
|
||||||
|
|
||||||
$organization->users()->attach(
|
$currency = null;
|
||||||
$user, [
|
if ($ipLookupResponse !== null) {
|
||||||
'role' => Role::Owner->value,
|
$currency = $ipLookupResponse->currency;
|
||||||
]
|
}
|
||||||
|
|
||||||
|
$organization = app(OrganizationService::class)->createOrganization(
|
||||||
|
$input['name'],
|
||||||
|
$user,
|
||||||
|
false,
|
||||||
|
$currency
|
||||||
);
|
);
|
||||||
|
|
||||||
$user->switchTeam($organization);
|
$user->switchTeam($organization);
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ class UserCreateCommand extends Command
|
|||||||
$password,
|
$password,
|
||||||
'UTC',
|
'UTC',
|
||||||
Weekday::Monday,
|
Weekday::Monday,
|
||||||
'EUR',
|
null,
|
||||||
$verifyEmail
|
verifyEmail: $verifyEmail
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
/** @var Organization|null $organization */
|
/** @var Organization|null $organization */
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,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'))
|
||||||
|
->twiceDaily();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
36
app/Enums/CurrencyFormat.php
Normal file
36
app/Enums/CurrencyFormat.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
||||||
|
|
||||||
|
enum CurrencyFormat: string
|
||||||
|
{
|
||||||
|
use LaravelEnumHelper;
|
||||||
|
|
||||||
|
case ISOCodeBeforeWithSpace = 'iso-code-before-with-space';
|
||||||
|
case ISOCodeAfterWithSpace = 'iso-code-after-with-space';
|
||||||
|
|
||||||
|
case SymbolBefore = 'symbol-before';
|
||||||
|
|
||||||
|
case SymbolAfter = 'symbol-after';
|
||||||
|
|
||||||
|
case SymbolBeforeWithSpace = 'symbol-before-with-space';
|
||||||
|
|
||||||
|
case SymbolAfterWithSpace = 'symbol-after-with-space';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function toSelectArray(): array
|
||||||
|
{
|
||||||
|
$selectArray = [];
|
||||||
|
foreach (self::values() as $value) {
|
||||||
|
$selectArray[(string) $value] = (string) __('enum.currency_format.'.$value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $selectArray;
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/Enums/DateFormat.php
Normal file
48
app/Enums/DateFormat.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
||||||
|
|
||||||
|
enum DateFormat: string
|
||||||
|
{
|
||||||
|
use LaravelEnumHelper;
|
||||||
|
|
||||||
|
case PointSeparatedDMYYYY = 'point-separated-d-m-yyyy';
|
||||||
|
case SlashSeparatedMMDDYYYY = 'slash-separated-mm-dd-yyyy';
|
||||||
|
|
||||||
|
case SlashSeparatedDDMMYYYY = 'slash-separated-dd-mm-yyyy';
|
||||||
|
|
||||||
|
case HyphenSeparatedDDMMYYY = 'hyphen-separated-dd-mm-yyyy';
|
||||||
|
|
||||||
|
case HyphenSeparatedMMDDDYYYY = 'hyphen-separated-mm-dd-yyyy';
|
||||||
|
|
||||||
|
case HyphenSeparatedYYYYMMDD = 'hyphen-separated-yyyy-mm-dd';
|
||||||
|
|
||||||
|
public function toCarbonFormat(): string
|
||||||
|
{
|
||||||
|
return match ($this->value) {
|
||||||
|
self::PointSeparatedDMYYYY->value => 'j.n.Y',
|
||||||
|
self::SlashSeparatedMMDDYYYY->value => 'm/d/Y',
|
||||||
|
self::SlashSeparatedDDMMYYYY->value => 'd/m/Y',
|
||||||
|
self::HyphenSeparatedDDMMYYY->value => 'd-m-Y',
|
||||||
|
self::HyphenSeparatedMMDDDYYYY->value => 'm-d-Y',
|
||||||
|
self::HyphenSeparatedYYYYMMDD->value => 'Y-m-d',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function toSelectArray(): array
|
||||||
|
{
|
||||||
|
$selectArray = [];
|
||||||
|
foreach (self::values() as $value) {
|
||||||
|
$selectArray[(string) $value] = (string) __('enum.date_format.'.$value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $selectArray;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/Enums/IntervalFormat.php
Normal file
32
app/Enums/IntervalFormat.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
||||||
|
|
||||||
|
enum IntervalFormat: string
|
||||||
|
{
|
||||||
|
use LaravelEnumHelper;
|
||||||
|
|
||||||
|
case Decimal = 'decimal';
|
||||||
|
case HoursMinutes = 'hours-minutes';
|
||||||
|
|
||||||
|
case HoursMinutesColonSeparated = 'hours-minutes-colon-separated';
|
||||||
|
|
||||||
|
case HoursMinutesSecondsColonSeparated = 'hours-minutes-seconds-colon-separated';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function toSelectArray(): array
|
||||||
|
{
|
||||||
|
$selectArray = [];
|
||||||
|
foreach (self::values() as $value) {
|
||||||
|
$selectArray[(string) $value] = (string) __('enum.interval_format.'.$value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $selectArray;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/Enums/NumberFormat.php
Normal file
37
app/Enums/NumberFormat.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @info https://en.wikipedia.org/wiki/Decimal_separator
|
||||||
|
*/
|
||||||
|
enum NumberFormat: string
|
||||||
|
{
|
||||||
|
use LaravelEnumHelper;
|
||||||
|
|
||||||
|
case ThousandsPointDecimalComma = 'point-comma';
|
||||||
|
|
||||||
|
case ThousandsCommaDecimalPoint = 'comma-point';
|
||||||
|
case ThousandsSpaceDecimalComma = 'space-comma';
|
||||||
|
|
||||||
|
case ThousandsSpaceDecimalPoint = 'space-point';
|
||||||
|
|
||||||
|
case ThousandsApostropheDecimalPoint = 'apostrophe-point';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function toSelectArray(): array
|
||||||
|
{
|
||||||
|
$selectArray = [];
|
||||||
|
foreach (self::values() as $value) {
|
||||||
|
$selectArray[(string) $value] = (string) __('enum.number_format.'.$value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $selectArray;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/Enums/TimeFormat.php
Normal file
28
app/Enums/TimeFormat.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
||||||
|
|
||||||
|
enum TimeFormat: string
|
||||||
|
{
|
||||||
|
use LaravelEnumHelper;
|
||||||
|
|
||||||
|
case TwelveHours = '12-hours';
|
||||||
|
case TwentyFourHours = '24-hours';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function toSelectArray(): array
|
||||||
|
{
|
||||||
|
$selectArray = [];
|
||||||
|
foreach (self::values() as $value) {
|
||||||
|
$selectArray[(string) $value] = (string) __('enum.time_format.'.$value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $selectArray;
|
||||||
|
}
|
||||||
|
}
|
||||||
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() {}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ use Filament\Tables;
|
|||||||
use Filament\Tables\Columns\IconColumn;
|
use Filament\Tables\Columns\IconColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Novadaemon\FilamentPrettyJson\PrettyJson;
|
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
|
||||||
|
|
||||||
class AuditResource extends Resource
|
class AuditResource extends Resource
|
||||||
{
|
{
|
||||||
@@ -38,8 +38,8 @@ class AuditResource extends Resource
|
|||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
Forms\Components\TextInput::make('auditable_id')
|
Forms\Components\TextInput::make('auditable_id')
|
||||||
->required(),
|
->required(),
|
||||||
PrettyJson::make('old_values'),
|
PrettyJsonField::make('old_values'),
|
||||||
PrettyJson::make('new_values'),
|
PrettyJsonField::make('new_values'),
|
||||||
Forms\Components\Textarea::make('url'),
|
Forms\Components\Textarea::make('url'),
|
||||||
Forms\Components\TextInput::make('ip_address'),
|
Forms\Components\TextInput::make('ip_address'),
|
||||||
Forms\Components\TextInput::make('user_agent')
|
Forms\Components\TextInput::make('user_agent')
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use Filament\Tables\Columns\TextColumn;
|
|||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Novadaemon\FilamentPrettyJson\PrettyJson;
|
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @source https://gitlab.com/amvisor/filament-failed-jobs
|
* @source https://gitlab.com/amvisor/filament-failed-jobs
|
||||||
@@ -50,7 +50,7 @@ class FailedJobResource extends Resource
|
|||||||
|
|
||||||
// make text a little bit smaller because often a complete Stack Trace is shown:
|
// make text a little bit smaller because often a complete Stack Trace is shown:
|
||||||
TextArea::make('exception')->disabled()->columnSpan(4)->extraInputAttributes(['style' => 'font-size: 80%;']),
|
TextArea::make('exception')->disabled()->columnSpan(4)->extraInputAttributes(['style' => 'font-size: 80%;']),
|
||||||
PrettyJson::make('payload')->disabled()->columnSpan(4),
|
PrettyJsonField::make('payload')->disabled()->columnSpan(4),
|
||||||
])->columns(4);
|
])->columns(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Enums\CurrencyFormat;
|
||||||
|
use App\Enums\DateFormat;
|
||||||
|
use App\Enums\IntervalFormat;
|
||||||
|
use App\Enums\NumberFormat;
|
||||||
|
use App\Enums\TimeFormat;
|
||||||
use App\Filament\Resources\OrganizationResource\Pages;
|
use App\Filament\Resources\OrganizationResource\Pages;
|
||||||
use App\Filament\Resources\OrganizationResource\RelationManagers\InvitationsRelationManager;
|
use App\Filament\Resources\OrganizationResource\RelationManagers\InvitationsRelationManager;
|
||||||
use App\Filament\Resources\OrganizationResource\RelationManagers\UsersRelationManager;
|
use App\Filament\Resources\OrganizationResource\RelationManagers\UsersRelationManager;
|
||||||
@@ -56,6 +61,21 @@ class OrganizationResource extends Resource
|
|||||||
->searchable(['name', 'email'])
|
->searchable(['name', 'email'])
|
||||||
->disabledOn(['edit'])
|
->disabledOn(['edit'])
|
||||||
->required(),
|
->required(),
|
||||||
|
Select::make('date_format')
|
||||||
|
->options(DateFormat::toSelectArray())
|
||||||
|
->required(),
|
||||||
|
Select::make('currency_format')
|
||||||
|
->options(CurrencyFormat::toSelectArray())
|
||||||
|
->required(),
|
||||||
|
Select::make('interval_format')
|
||||||
|
->options(IntervalFormat::toSelectArray())
|
||||||
|
->required(),
|
||||||
|
Select::make('number_format')
|
||||||
|
->options(NumberFormat::toSelectArray())
|
||||||
|
->required(),
|
||||||
|
Select::make('time_format')
|
||||||
|
->options(TimeFormat::toSelectArray())
|
||||||
|
->required(),
|
||||||
Forms\Components\Select::make('currency')
|
Forms\Components\Select::make('currency')
|
||||||
->label('Currency')
|
->label('Currency')
|
||||||
->options(function (): array {
|
->options(function (): array {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use Filament\Tables\Columns\TextColumn;
|
|||||||
use Filament\Tables\Columns\ToggleColumn;
|
use Filament\Tables\Columns\ToggleColumn;
|
||||||
use Filament\Tables\Filters\SelectFilter;
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Novadaemon\FilamentPrettyJson\PrettyJson;
|
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
|
||||||
|
|
||||||
class ReportResource extends Resource
|
class ReportResource extends Resource
|
||||||
{
|
{
|
||||||
@@ -58,7 +58,7 @@ class ReportResource extends Resource
|
|||||||
Forms\Components\TextInput::make('share_secret')
|
Forms\Components\TextInput::make('share_secret')
|
||||||
->label('Share Secret')
|
->label('Share Secret')
|
||||||
->nullable(),
|
->nullable(),
|
||||||
PrettyJson::make('properties')
|
PrettyJsonField::make('properties')
|
||||||
->formatStateUsing(function (ReportPropertiesDto $state, Report $record): string {
|
->formatStateUsing(function (ReportPropertiesDto $state, Report $record): string {
|
||||||
return $record->getRawOriginal('properties');
|
return $record->getRawOriginal('properties');
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class CreateUser extends CreateRecord
|
|||||||
$data['timezone'],
|
$data['timezone'],
|
||||||
Weekday::from($data['week_start']),
|
Weekday::from($data['week_start']),
|
||||||
$data['currency'],
|
$data['currency'],
|
||||||
(bool) $data['is_email_verified']
|
verifyEmail: (bool) $data['is_email_verified']
|
||||||
);
|
);
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
|
|||||||
37
app/Http/Controllers/Api/V1/CurrencyController.php
Normal file
37
app/Http/Controllers/Api/V1/CurrencyController.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Service\CurrencyService;
|
||||||
|
use Brick\Money\Currency;
|
||||||
|
use Brick\Money\ISOCurrencyProvider;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class CurrencyController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get all currencies
|
||||||
|
*
|
||||||
|
* @response array{code: string, name: string, symbol: string}[]
|
||||||
|
*
|
||||||
|
* @operationId getCurrencies
|
||||||
|
*/
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
$currencyService = app(CurrencyService::class);
|
||||||
|
|
||||||
|
$currencies = array_values(array_map(
|
||||||
|
fn (Currency $currency): array => [
|
||||||
|
'code' => $currency->getCurrencyCode(),
|
||||||
|
'name' => $currency->getName(),
|
||||||
|
'symbol' => $currencyService->getCurrencySymbol($currency->getCurrencyCode()),
|
||||||
|
],
|
||||||
|
ISOCurrencyProvider::getInstance()->getAvailableCurrencies()
|
||||||
|
));
|
||||||
|
|
||||||
|
return response()->json($currencies);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -136,6 +136,8 @@ class MemberController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Merge one member into another
|
||||||
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
* @throws OnlyPlaceholdersCanBeMergedIntoAnotherMember
|
* @throws OnlyPlaceholdersCanBeMergedIntoAnotherMember
|
||||||
* @throws \Throwable
|
* @throws \Throwable
|
||||||
|
|||||||
@@ -40,15 +40,35 @@ class OrganizationController extends Controller
|
|||||||
{
|
{
|
||||||
$this->checkPermission($organization, 'organizations:update');
|
$this->checkPermission($organization, 'organizations:update');
|
||||||
|
|
||||||
$organization->name = $request->input('name');
|
if ($request->getName() !== null) {
|
||||||
$oldBillableRate = $organization->billable_rate;
|
$organization->name = $request->getName();
|
||||||
if ($request->has('employees_can_see_billable_rates')) {
|
}
|
||||||
$organization->employees_can_see_billable_rates = $request->validated('employees_can_see_billable_rates');
|
if ($request->getEmployeesCanSeeBillableRates() !== null) {
|
||||||
|
$organization->employees_can_see_billable_rates = $request->getEmployeesCanSeeBillableRates();
|
||||||
|
}
|
||||||
|
if ($request->getNumberFormat() !== null) {
|
||||||
|
$organization->number_format = $request->getNumberFormat();
|
||||||
|
}
|
||||||
|
if ($request->getCurrencyFormat() !== null) {
|
||||||
|
$organization->currency_format = $request->getCurrencyFormat();
|
||||||
|
}
|
||||||
|
if ($request->getDateFormat() !== null) {
|
||||||
|
$organization->date_format = $request->getDateFormat();
|
||||||
|
}
|
||||||
|
if ($request->getIntervalFormat() !== null) {
|
||||||
|
$organization->interval_format = $request->getIntervalFormat();
|
||||||
|
}
|
||||||
|
if ($request->getTimeFormat() !== null) {
|
||||||
|
$organization->time_format = $request->getTimeFormat();
|
||||||
|
}
|
||||||
|
$hasBillableRate = $request->has('billable_rate');
|
||||||
|
if ($hasBillableRate) {
|
||||||
|
$oldBillableRate = $organization->billable_rate;
|
||||||
|
$organization->billable_rate = $request->getBillableRate();
|
||||||
}
|
}
|
||||||
$organization->billable_rate = $request->getBillableRate();
|
|
||||||
$organization->save();
|
$organization->save();
|
||||||
|
|
||||||
if ($oldBillableRate !== $request->getBillableRate()) {
|
if ($hasBillableRate && $oldBillableRate !== $request->getBillableRate()) {
|
||||||
$billableRateService->updateTimeEntriesBillableRateForOrganization($organization);
|
$billableRateService->updateTimeEntriesBillableRateForOrganization($organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ use App\Models\Organization;
|
|||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
use App\Models\TimeEntry;
|
use App\Models\TimeEntry;
|
||||||
|
use App\Service\LocalizationService;
|
||||||
use App\Service\ReportExport\TimeEntriesDetailedCsvExport;
|
use App\Service\ReportExport\TimeEntriesDetailedCsvExport;
|
||||||
use App\Service\ReportExport\TimeEntriesDetailedExport;
|
use App\Service\ReportExport\TimeEntriesDetailedExport;
|
||||||
use App\Service\ReportExport\TimeEntriesReportExport;
|
use App\Service\ReportExport\TimeEntriesReportExport;
|
||||||
@@ -194,6 +195,7 @@ class TimeEntryController extends Controller
|
|||||||
$filename = 'time-entries-export-'.now()->format('Y-m-d_H-i-s').'.'.$format->getFileExtension();
|
$filename = 'time-entries-export-'.now()->format('Y-m-d_H-i-s').'.'.$format->getFileExtension();
|
||||||
$folderPath = 'exports';
|
$folderPath = 'exports';
|
||||||
$path = $folderPath.'/'.$filename;
|
$path = $folderPath.'/'.$filename;
|
||||||
|
$localizationService = LocalizationService::forOrganization($organization);
|
||||||
if ($format === ExportFormat::CSV) {
|
if ($format === ExportFormat::CSV) {
|
||||||
$export = new TimeEntriesDetailedCsvExport(config('filesystems.private'), $folderPath, $filename, $timeEntriesQuery, 1000, $timezone);
|
$export = new TimeEntriesDetailedCsvExport(config('filesystems.private'), $folderPath, $filename, $timeEntriesQuery, 1000, $timezone);
|
||||||
$export->export();
|
$export->export();
|
||||||
@@ -223,6 +225,8 @@ class TimeEntryController extends Controller
|
|||||||
'currency' => $organization->currency,
|
'currency' => $organization->currency,
|
||||||
'start' => $request->getStart()->timezone($timezone),
|
'start' => $request->getStart()->timezone($timezone),
|
||||||
'end' => $request->getEnd()->timezone($timezone),
|
'end' => $request->getEnd()->timezone($timezone),
|
||||||
|
'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) {
|
||||||
@@ -257,7 +261,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 TimeEntriesDetailedExport($timeEntriesQuery, $format, $timezone),
|
new TimeEntriesDetailedExport($timeEntriesQuery, $format, $timezone, $localizationService),
|
||||||
$path,
|
$path,
|
||||||
config('filesystems.private'),
|
config('filesystems.private'),
|
||||||
$format->getExportPackageType(),
|
$format->getExportPackageType(),
|
||||||
@@ -394,6 +398,7 @@ class TimeEntryController extends Controller
|
|||||||
);
|
);
|
||||||
$currency = $organization->currency;
|
$currency = $organization->currency;
|
||||||
$timezone = app(TimezoneService::class)->getTimezoneFromUser($this->user());
|
$timezone = app(TimezoneService::class)->getTimezoneFromUser($this->user());
|
||||||
|
$localizationService = LocalizationService::forOrganization($organization);
|
||||||
|
|
||||||
$filename = 'time-entries-report-'.now()->format('Y-m-d_H-i-s').'.'.$format->getFileExtension();
|
$filename = 'time-entries-report-'.now()->format('Y-m-d_H-i-s').'.'.$format->getFileExtension();
|
||||||
$folderPath = 'exports';
|
$folderPath = 'exports';
|
||||||
@@ -419,9 +424,12 @@ class TimeEntryController extends Controller
|
|||||||
'currency' => $currency,
|
'currency' => $currency,
|
||||||
'group' => $group,
|
'group' => $group,
|
||||||
'subGroup' => $subGroup,
|
'subGroup' => $subGroup,
|
||||||
|
'timezone' => $timezone,
|
||||||
'start' => $request->getStart()->timezone($timezone),
|
'start' => $request->getStart()->timezone($timezone),
|
||||||
'end' => $request->getEnd()->timezone($timezone),
|
'end' => $request->getEnd()->timezone($timezone),
|
||||||
'debug' => $debug,
|
'debug' => $debug,
|
||||||
|
'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) {
|
||||||
@@ -450,7 +458,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(),
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ namespace App\Http\Controllers\Api\V1;
|
|||||||
|
|
||||||
use App\Http\Resources\V1\User\UserResource;
|
use App\Http\Resources\V1\User\UserResource;
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
|
||||||
|
|
||||||
class UserController extends Controller
|
class UserController extends Controller
|
||||||
{
|
{
|
||||||
@@ -19,7 +18,7 @@ class UserController extends Controller
|
|||||||
*
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*/
|
*/
|
||||||
public function me(): JsonResource
|
public function me(): UserResource
|
||||||
{
|
{
|
||||||
$user = $this->user();
|
$user = $this->user();
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class HandleInertiaRequests extends Middleware
|
|||||||
public function share(Request $request): array
|
public function share(Request $request): array
|
||||||
{
|
{
|
||||||
$hasBilling = Module::has('Billing') && Module::isEnabled('Billing');
|
$hasBilling = Module::has('Billing') && Module::isEnabled('Billing');
|
||||||
|
$hasInvoicing = Module::has('Invoicing') && Module::isEnabled('Invoicing');
|
||||||
|
|
||||||
/** @var BillingContract $billing */
|
/** @var BillingContract $billing */
|
||||||
$billing = app(BillingContract::class);
|
$billing = app(BillingContract::class);
|
||||||
@@ -48,6 +49,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,
|
||||||
'billing' => $billing !== null && $currentOrganization !== null ? [
|
'billing' => $billing !== null && $currentOrganization !== null ? [
|
||||||
'has_subscription' => $billing->hasSubscription($currentOrganization),
|
'has_subscription' => $billing->hasSubscription($currentOrganization),
|
||||||
'has_trial' => $billing->hasTrial($currentOrganization),
|
'has_trial' => $billing->hasTrial($currentOrganization),
|
||||||
|
|||||||
@@ -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,18 @@ 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 App\Models\OrganizationInvitation;
|
||||||
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 Illuminate\Validation\Rule;
|
||||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
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.
|
||||||
|
|||||||
@@ -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',
|
[
|
||||||
'integer',
|
'nullable',
|
||||||
'min:0',
|
],
|
||||||
'max:2147483647',
|
$this->moneyRules()
|
||||||
],
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,44 +4,98 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Requests\V1\Organization;
|
namespace App\Http\Requests\V1\Organization;
|
||||||
|
|
||||||
|
use App\Enums\CurrencyFormat;
|
||||||
|
use App\Enums\DateFormat;
|
||||||
|
use App\Enums\IntervalFormat;
|
||||||
|
use App\Enums\NumberFormat;
|
||||||
|
use App\Enums\TimeFormat;
|
||||||
|
use App\Http\Requests\V1\BaseFormRequest;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Validation\Rule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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.
|
||||||
*
|
*
|
||||||
* @return array<string, array<string|ValidationRule>>
|
* @return array<string, array<string|\Illuminate\Contracts\Validation\Rule>>
|
||||||
*/
|
*/
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => [
|
'name' => [
|
||||||
'required',
|
|
||||||
'string',
|
'string',
|
||||||
'max:255',
|
'max:255',
|
||||||
],
|
],
|
||||||
'billable_rate' => [
|
'billable_rate' => array_merge(
|
||||||
'nullable',
|
[
|
||||||
'integer',
|
'nullable',
|
||||||
'min:0',
|
],
|
||||||
'max:2147483647',
|
$this->moneyRules()
|
||||||
],
|
),
|
||||||
'employees_can_see_billable_rates' => [
|
'employees_can_see_billable_rates' => [
|
||||||
'boolean',
|
'boolean',
|
||||||
],
|
],
|
||||||
|
'number_format' => [
|
||||||
|
Rule::enum(NumberFormat::class),
|
||||||
|
],
|
||||||
|
'currency_format' => [
|
||||||
|
Rule::enum(CurrencyFormat::class),
|
||||||
|
],
|
||||||
|
'date_format' => [
|
||||||
|
Rule::enum(DateFormat::class),
|
||||||
|
],
|
||||||
|
'interval_format' => [
|
||||||
|
Rule::enum(IntervalFormat::class),
|
||||||
|
],
|
||||||
|
'time_format' => [
|
||||||
|
Rule::enum(TimeFormat::class),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getName(): ?string
|
||||||
|
{
|
||||||
|
return $this->has('name') ? (string) $this->input('name') : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNumberFormat(): ?NumberFormat
|
||||||
|
{
|
||||||
|
return $this->has('number_format') ? NumberFormat::from($this->input('number_format')) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrencyFormat(): ?CurrencyFormat
|
||||||
|
{
|
||||||
|
return $this->has('currency_format') ? CurrencyFormat::from($this->input('currency_format')) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDateFormat(): ?DateFormat
|
||||||
|
{
|
||||||
|
return $this->has('date_format') ? DateFormat::from($this->input('date_format')) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIntervalFormat(): ?IntervalFormat
|
||||||
|
{
|
||||||
|
return $this->has('interval_format') ? IntervalFormat::from($this->input('interval_format')) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTimeFormat(): ?TimeFormat
|
||||||
|
{
|
||||||
|
return $this->has('time_format') ? TimeFormat::from($this->input('time_format')) : null;
|
||||||
|
}
|
||||||
|
|
||||||
public function getBillableRate(): ?int
|
public function getBillableRate(): ?int
|
||||||
{
|
{
|
||||||
$input = $this->input('billable_rate');
|
$input = $this->input('billable_rate');
|
||||||
|
|
||||||
return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null;
|
return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getEmployeesCanSeeBillableRates(): ?bool
|
||||||
|
{
|
||||||
|
return $this->has('employees_can_see_billable_rates') ? $this->boolean('employees_can_see_billable_rates') : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,20 +4,21 @@ 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 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 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.
|
||||||
@@ -27,6 +28,7 @@ class ProjectStoreRequest extends FormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
// Name of the project, the name needs to be unique per client and organization
|
||||||
'name' => [
|
'name' => [
|
||||||
'required',
|
'required',
|
||||||
'string',
|
'string',
|
||||||
@@ -34,7 +36,13 @@ class ProjectStoreRequest extends FormRequest
|
|||||||
'max:255',
|
'max:255',
|
||||||
UniqueEloquent::make(Project::class, 'name', function (Builder $builder): Builder {
|
UniqueEloquent::make(Project::class, 'name', function (Builder $builder): Builder {
|
||||||
/** @var Builder<Project> $builder */
|
/** @var Builder<Project> $builder */
|
||||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
$clientId = $this->input('client_id');
|
||||||
|
if (! is_string($clientId) || ! Str::isUuid($clientId)) {
|
||||||
|
$clientId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $builder->whereBelongsTo($this->organization, 'organization')
|
||||||
|
->where('client_id', $clientId);
|
||||||
})->withCustomTranslation('validation.project_name_already_exists'),
|
})->withCustomTranslation('validation.project_name_already_exists'),
|
||||||
],
|
],
|
||||||
'color' => [
|
'color' => [
|
||||||
@@ -47,14 +55,15 @@ class ProjectStoreRequest extends FormRequest
|
|||||||
'required',
|
'required',
|
||||||
'boolean',
|
'boolean',
|
||||||
],
|
],
|
||||||
'billable_rate' => [
|
'billable_rate' => array_merge(
|
||||||
'nullable',
|
[
|
||||||
'integer',
|
'nullable',
|
||||||
'min:0',
|
],
|
||||||
'max:2147483647',
|
$this->moneyRules()
|
||||||
],
|
),
|
||||||
// ID of the client
|
// ID of the client
|
||||||
'client_id' => [
|
'client_id' => [
|
||||||
|
'present',
|
||||||
'nullable',
|
'nullable',
|
||||||
ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder {
|
ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder {
|
||||||
/** @var Builder<Client> $builder */
|
/** @var Builder<Client> $builder */
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ 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 Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||||
|
|
||||||
@@ -18,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.
|
||||||
@@ -34,7 +35,13 @@ class ProjectUpdateRequest extends FormRequest
|
|||||||
'max:255',
|
'max:255',
|
||||||
UniqueEloquent::make(Project::class, 'name', function (Builder $builder): Builder {
|
UniqueEloquent::make(Project::class, 'name', function (Builder $builder): Builder {
|
||||||
/** @var Builder<Project> $builder */
|
/** @var Builder<Project> $builder */
|
||||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
$clientId = $this->input('client_id');
|
||||||
|
if (! is_string($clientId) || ! Str::isUuid($clientId)) {
|
||||||
|
$clientId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $builder->whereBelongsTo($this->organization, 'organization')
|
||||||
|
->where('client_id', $clientId);
|
||||||
})->ignore($this->project?->getKey())->withCustomTranslation('validation.project_name_already_exists'),
|
})->ignore($this->project?->getKey())->withCustomTranslation('validation.project_name_already_exists'),
|
||||||
],
|
],
|
||||||
'color' => [
|
'color' => [
|
||||||
@@ -54,18 +61,18 @@ class ProjectUpdateRequest extends FormRequest
|
|||||||
'boolean',
|
'boolean',
|
||||||
],
|
],
|
||||||
'client_id' => [
|
'client_id' => [
|
||||||
|
'present',
|
||||||
'nullable',
|
'nullable',
|
||||||
ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder {
|
ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder {
|
||||||
/** @var Builder<Client> $builder */
|
/** @var Builder<Client> $builder */
|
||||||
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',
|
[
|
||||||
'integer',
|
'nullable',
|
||||||
'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',
|
[
|
||||||
'integer',
|
'nullable',
|
||||||
'min:0',
|
],
|
||||||
'max:2147483647',
|
$this->moneyRules()
|
||||||
],
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ 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\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.
|
||||||
@@ -40,7 +40,7 @@ class ReportStoreRequest extends FormRequest
|
|||||||
'required',
|
'required',
|
||||||
'boolean',
|
'boolean',
|
||||||
],
|
],
|
||||||
// After this date the report will be automatically set to private (is_public=false) (ISO 8601 format, UTC timezone)
|
// After this date the report will be automatically set to private (is_public=false) (Format: "Y-m-d\TH:i:s\Z", UTC timezone, Example: "2000-02-22T14:58:59Z")
|
||||||
'public_until' => [
|
'public_until' => [
|
||||||
'nullable',
|
'nullable',
|
||||||
'date_format:Y-m-d\TH:i:s\Z',
|
'date_format:Y-m-d\TH:i:s\Z',
|
||||||
|
|||||||
@@ -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,7 @@ 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\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 +17,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 +24,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.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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\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 +15,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 +22,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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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\Client;
|
use App\Models\Client;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
@@ -12,13 +13,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
|
* @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.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -59,12 +59,12 @@ class TimeEntryStoreRequest extends FormRequest
|
|||||||
->where('project_id', $this->input('project_id'));
|
->where('project_id', $this->input('project_id'));
|
||||||
})->uuid()->withMessage(__('validation.task_belongs_to_project')),
|
})->uuid()->withMessage(__('validation.task_belongs_to_project')),
|
||||||
],
|
],
|
||||||
// Start of time entry (ISO 8601 format, UTC timezone)
|
// Start of time entry (Format: "Y-m-d\TH:i:s\Z", UTC timezone, Example: "2000-02-22T14:58:59Z")
|
||||||
'start' => [
|
'start' => [
|
||||||
'required',
|
'required',
|
||||||
'date_format:Y-m-d\TH:i:s\Z',
|
'date_format:Y-m-d\TH:i:s\Z',
|
||||||
],
|
],
|
||||||
// End of time entry (ISO 8601 format, UTC timezone)
|
// End of time entry (Format: "Y-m-d\TH:i:s\Z", UTC timezone, Example: "2000-02-22T14:58:59Z")
|
||||||
'end' => [
|
'end' => [
|
||||||
'nullable',
|
'nullable',
|
||||||
'date_format:Y-m-d\TH:i:s\Z',
|
'date_format:Y-m-d\TH:i:s\Z',
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -59,11 +59,11 @@ class TimeEntryUpdateRequest extends FormRequest
|
|||||||
->where('project_id', $this->input('project_id'));
|
->where('project_id', $this->input('project_id'));
|
||||||
})->uuid()->withMessage(__('validation.task_belongs_to_project')),
|
})->uuid()->withMessage(__('validation.task_belongs_to_project')),
|
||||||
],
|
],
|
||||||
// Start of time entry (ISO 8601 format, UTC timezone)
|
// Start of time entry (Format: "Y-m-d\TH:i:s\Z", UTC timezone, Example: "2000-02-22T14:58:59Z")
|
||||||
'start' => [
|
'start' => [
|
||||||
'date_format:Y-m-d\TH:i:s\Z',
|
'date_format:Y-m-d\TH:i:s\Z',
|
||||||
],
|
],
|
||||||
// End of time entry (ISO 8601 format, UTC timezone)
|
// End of time entry (Format: "Y-m-d\TH:i:s\Z", UTC timezone, Example: "2000-02-22T14:58:59Z")
|
||||||
'end' => [
|
'end' => [
|
||||||
'nullable',
|
'nullable',
|
||||||
'date_format:Y-m-d\TH:i:s\Z',
|
'date_format:Y-m-d\TH:i:s\Z',
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ abstract class BaseResource extends JsonResource
|
|||||||
protected function formatDateTime(?Carbon $carbon): ?string
|
protected function formatDateTime(?Carbon $carbon): ?string
|
||||||
{
|
{
|
||||||
return $carbon?->toIso8601ZuluString();
|
return $carbon?->toIso8601ZuluString();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formatDate(?Carbon $carbon): ?string
|
||||||
|
{
|
||||||
|
return $carbon?->format('Y-m-d');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,14 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Organization;
|
namespace App\Http\Resources\V1\Organization;
|
||||||
|
|
||||||
|
use App\Enums\CurrencyFormat;
|
||||||
|
use App\Enums\DateFormat;
|
||||||
|
use App\Enums\IntervalFormat;
|
||||||
|
use App\Enums\NumberFormat;
|
||||||
|
use App\Enums\TimeFormat;
|
||||||
use App\Http\Resources\V1\BaseResource;
|
use App\Http\Resources\V1\BaseResource;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
|
use App\Service\CurrencyService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,6 +40,8 @@ class OrganizationResource extends BaseResource
|
|||||||
*/
|
*/
|
||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
|
$currencyService = app(CurrencyService::class);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
/** @var string $id ID */
|
/** @var string $id ID */
|
||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
@@ -47,6 +55,18 @@ class OrganizationResource extends BaseResource
|
|||||||
'employees_can_see_billable_rates' => $this->resource->employees_can_see_billable_rates,
|
'employees_can_see_billable_rates' => $this->resource->employees_can_see_billable_rates,
|
||||||
/** @var string $currency Currency code (ISO 4217) */
|
/** @var string $currency Currency code (ISO 4217) */
|
||||||
'currency' => $this->resource->currency,
|
'currency' => $this->resource->currency,
|
||||||
|
/** @var string $currency_symbol Currency symbol */
|
||||||
|
'currency_symbol' => $currencyService->getCurrencySymbol($this->resource->currency),
|
||||||
|
/** @var NumberFormat $number_format Number format */
|
||||||
|
'number_format' => $this->resource->number_format->value,
|
||||||
|
/** @var CurrencyFormat $currency_format Currency format */
|
||||||
|
'currency_format' => $this->resource->currency_format->value,
|
||||||
|
/** @var DateFormat $date_format Date format */
|
||||||
|
'date_format' => $this->resource->date_format->value,
|
||||||
|
/** @var IntervalFormat $interval_format Interval format */
|
||||||
|
'interval_format' => $this->resource->interval_format->value,
|
||||||
|
/** @var TimeFormat $time_format Time format */
|
||||||
|
'time_format' => $this->resource->time_format->value,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class DetailedReportResource extends BaseResource
|
|||||||
/** @var bool $is_public Whether the report can be accessed via an external link */
|
/** @var bool $is_public Whether the report can be accessed via an external link */
|
||||||
'is_public' => $this->resource->is_public,
|
'is_public' => $this->resource->is_public,
|
||||||
/** @var string|null $public_until Date until the report is public */
|
/** @var string|null $public_until Date until the report is public */
|
||||||
'public_until' => $this->resource->public_until?->toIso8601ZuluString(),
|
'public_until' => $this->formatDateTime($this->resource->public_until),
|
||||||
/** @var string|null $shareable_link Get link to access the report externally, not set if the report is private */
|
/** @var string|null $shareable_link Get link to access the report externally, not set if the report is private */
|
||||||
'shareable_link' => $this->resource->getShareableLink(),
|
'shareable_link' => $this->resource->getShareableLink(),
|
||||||
'properties' => [
|
'properties' => [
|
||||||
@@ -41,9 +41,9 @@ class DetailedReportResource extends BaseResource
|
|||||||
/** @var string $history_group Type of grouping of the historic aggregation (time chart) */
|
/** @var string $history_group Type of grouping of the historic aggregation (time chart) */
|
||||||
'history_group' => $this->resource->properties->historyGroup->value,
|
'history_group' => $this->resource->properties->historyGroup->value,
|
||||||
/** @var string $start Start date of the report */
|
/** @var string $start Start date of the report */
|
||||||
'start' => $this->resource->properties->start->toIso8601ZuluString(),
|
'start' => $this->formatDateTime($this->resource->properties->start),
|
||||||
/** @var string $end End date of the report */
|
/** @var string $end End date of the report */
|
||||||
'end' => $this->resource->properties->end->toIso8601ZuluString(),
|
'end' => $this->formatDateTime($this->resource->properties->end),
|
||||||
/** @var bool|null $active Whether the report is active */
|
/** @var bool|null $active Whether the report is active */
|
||||||
'active' => $this->resource->properties->active,
|
'active' => $this->resource->properties->active,
|
||||||
/** @var array<string>|null $member_ids Filter by multiple member IDs, member IDs are OR combined */
|
/** @var array<string>|null $member_ids Filter by multiple member IDs, member IDs are OR combined */
|
||||||
@@ -60,9 +60,9 @@ class DetailedReportResource extends BaseResource
|
|||||||
'task_ids' => $this->resource->properties->taskIds?->toArray(),
|
'task_ids' => $this->resource->properties->taskIds?->toArray(),
|
||||||
],
|
],
|
||||||
/** @var string $created_at Date when the report was created */
|
/** @var string $created_at Date when the report was created */
|
||||||
'created_at' => $this->resource->created_at?->toIso8601ZuluString(),
|
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||||
/** @var string $updated_at Date when the report was last updated */
|
/** @var string $updated_at Date when the report was last updated */
|
||||||
'updated_at' => $this->resource->updated_at?->toIso8601ZuluString(),
|
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,14 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Report;
|
namespace App\Http\Resources\V1\Report;
|
||||||
|
|
||||||
|
use App\Enums\CurrencyFormat;
|
||||||
|
use App\Enums\DateFormat;
|
||||||
|
use App\Enums\IntervalFormat;
|
||||||
|
use App\Enums\NumberFormat;
|
||||||
|
use App\Enums\TimeFormat;
|
||||||
use App\Http\Resources\V1\BaseResource;
|
use App\Http\Resources\V1\BaseResource;
|
||||||
use App\Models\Report;
|
use App\Models\Report;
|
||||||
|
use App\Service\CurrencyService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,15 +70,29 @@ class DetailedWithDataReportResource extends BaseResource
|
|||||||
*/
|
*/
|
||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
|
$currencyService = app(CurrencyService::class);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
/** @var string $name Name */
|
/** @var string $name Name */
|
||||||
'name' => $this->resource->name,
|
'name' => $this->resource->name,
|
||||||
/** @var string|null $email Description */
|
/** @var string|null $email Description */
|
||||||
'description' => $this->resource->description,
|
'description' => $this->resource->description,
|
||||||
/** @var string|null $public_until Date until the report is public */
|
/** @var string|null $public_until Date until the report is public */
|
||||||
'public_until' => $this->resource->public_until?->toIso8601ZuluString(),
|
'public_until' => $this->formatDateTime($this->resource->public_until),
|
||||||
/** @var string $currency Currency code (ISO 4217) */
|
/** @var string $currency Currency code (ISO 4217) */
|
||||||
'currency' => $this->resource->organization->currency,
|
'currency' => $this->resource->organization->currency,
|
||||||
|
/** @var NumberFormat $number_format Number format */
|
||||||
|
'number_format' => $this->resource->organization->number_format->value,
|
||||||
|
/** @var CurrencyFormat $currency_format Currency format */
|
||||||
|
'currency_format' => $this->resource->organization->currency_format->value,
|
||||||
|
/** @var string $currency_symbol Currency symbol */
|
||||||
|
'currency_symbol' => $currencyService->getCurrencySymbol($this->resource->organization->currency),
|
||||||
|
/** @var DateFormat $date_format Date format */
|
||||||
|
'date_format' => $this->resource->organization->date_format->value,
|
||||||
|
/** @var IntervalFormat $interval_format Interval format */
|
||||||
|
'interval_format' => $this->resource->organization->interval_format->value,
|
||||||
|
/** @var TimeFormat $time_format Time format */
|
||||||
|
'time_format' => $this->resource->organization->time_format->value,
|
||||||
'properties' => [
|
'properties' => [
|
||||||
/** @var string $group Type of first grouping */
|
/** @var string $group Type of first grouping */
|
||||||
'group' => $this->resource->properties->group->value,
|
'group' => $this->resource->properties->group->value,
|
||||||
@@ -81,9 +101,9 @@ class DetailedWithDataReportResource extends BaseResource
|
|||||||
/** @var string $history_group Type of grouping of the historic aggregation (time chart) */
|
/** @var string $history_group Type of grouping of the historic aggregation (time chart) */
|
||||||
'history_group' => $this->resource->properties->historyGroup->value,
|
'history_group' => $this->resource->properties->historyGroup->value,
|
||||||
/** @var string $start Start date of the report */
|
/** @var string $start Start date of the report */
|
||||||
'start' => $this->resource->properties->start->toIso8601ZuluString(),
|
'start' => $this->formatDateTime($this->resource->properties->start),
|
||||||
/** @var string $end End date of the report */
|
/** @var string $end End date of the report */
|
||||||
'end' => $this->resource->properties->end->toIso8601ZuluString(),
|
'end' => $this->formatDateTime($this->resource->properties->end),
|
||||||
],
|
],
|
||||||
/** @var array{
|
/** @var array{
|
||||||
* grouped_type: string|null,
|
* grouped_type: string|null,
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ class ReportResource extends BaseResource
|
|||||||
/** @var bool $is_public Whether the report can be accessed via an external link */
|
/** @var bool $is_public Whether the report can be accessed via an external link */
|
||||||
'is_public' => $this->resource->is_public,
|
'is_public' => $this->resource->is_public,
|
||||||
/** @var string|null $public_until Date until the report is public */
|
/** @var string|null $public_until Date until the report is public */
|
||||||
'public_until' => $this->resource->public_until?->toIso8601ZuluString(),
|
'public_until' => $this->formatDateTime($this->resource->public_until),
|
||||||
/** @var string|null $shareable_link Get link to access the report externally, not set if the report is private */
|
/** @var string|null $shareable_link Get link to access the report externally, not set if the report is private */
|
||||||
'shareable_link' => $this->resource->getShareableLink(),
|
'shareable_link' => $this->resource->getShareableLink(),
|
||||||
/** @var string $created_at Date when the report was created */
|
/** @var string $created_at Date when the report was created */
|
||||||
'created_at' => $this->resource->created_at?->toIso8601ZuluString(),
|
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||||
/** @var string $updated_at Date when the report was last updated */
|
/** @var string $updated_at Date when the report was last updated */
|
||||||
'updated_at' => $this->resource->updated_at?->toIso8601ZuluString(),
|
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\CurrencyFormat;
|
||||||
|
use App\Enums\DateFormat;
|
||||||
|
use App\Enums\IntervalFormat;
|
||||||
|
use App\Enums\NumberFormat;
|
||||||
|
use App\Enums\TimeFormat;
|
||||||
use App\Models\Concerns\CustomAuditable;
|
use App\Models\Concerns\CustomAuditable;
|
||||||
use App\Models\Concerns\HasUuids;
|
use App\Models\Concerns\HasUuids;
|
||||||
use Database\Factories\OrganizationFactory;
|
use Database\Factories\OrganizationFactory;
|
||||||
@@ -18,7 +23,6 @@ use Illuminate\Support\Str;
|
|||||||
use Laravel\Jetstream\Events\TeamCreated;
|
use Laravel\Jetstream\Events\TeamCreated;
|
||||||
use Laravel\Jetstream\Events\TeamDeleted;
|
use Laravel\Jetstream\Events\TeamDeleted;
|
||||||
use Laravel\Jetstream\Events\TeamUpdated;
|
use Laravel\Jetstream\Events\TeamUpdated;
|
||||||
use Laravel\Jetstream\Jetstream;
|
|
||||||
use Laravel\Jetstream\Team as JetstreamTeam;
|
use Laravel\Jetstream\Team as JetstreamTeam;
|
||||||
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||||
|
|
||||||
@@ -37,6 +41,11 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
|||||||
* @property Collection<int, User> $realUsers
|
* @property Collection<int, User> $realUsers
|
||||||
* @property-read Collection<int, OrganizationInvitation> $teamInvitations
|
* @property-read Collection<int, OrganizationInvitation> $teamInvitations
|
||||||
* @property Member $membership
|
* @property Member $membership
|
||||||
|
* @property NumberFormat $number_format
|
||||||
|
* @property CurrencyFormat $currency_format
|
||||||
|
* @property DateFormat $date_format
|
||||||
|
* @property IntervalFormat $interval_format
|
||||||
|
* @property TimeFormat $time_format
|
||||||
*
|
*
|
||||||
* @method HasMany<OrganizationInvitation> teamInvitations()
|
* @method HasMany<OrganizationInvitation> teamInvitations()
|
||||||
* @method static OrganizationFactory factory()
|
* @method static OrganizationFactory factory()
|
||||||
@@ -60,6 +69,11 @@ class Organization extends JetstreamTeam implements AuditableContract
|
|||||||
'personal_team' => 'boolean',
|
'personal_team' => 'boolean',
|
||||||
'currency' => 'string',
|
'currency' => 'string',
|
||||||
'employees_can_see_billable_rates' => 'boolean',
|
'employees_can_see_billable_rates' => 'boolean',
|
||||||
|
'number_format' => NumberFormat::class,
|
||||||
|
'currency_format' => CurrencyFormat::class,
|
||||||
|
'date_format' => DateFormat::class,
|
||||||
|
'interval_format' => IntervalFormat::class,
|
||||||
|
'time_format' => TimeFormat::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,7 +103,6 @@ class Organization extends JetstreamTeam implements AuditableContract
|
|||||||
* @var array<string, mixed>
|
* @var array<string, mixed>
|
||||||
*/
|
*/
|
||||||
protected $attributes = [
|
protected $attributes = [
|
||||||
'currency' => 'EUR',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -133,6 +133,13 @@ class JetstreamServiceProvider extends ServiceProvider
|
|||||||
'reports:create',
|
'reports:create',
|
||||||
'reports:update',
|
'reports:update',
|
||||||
'reports:delete',
|
'reports:delete',
|
||||||
|
'invoices:view',
|
||||||
|
'invoices:create',
|
||||||
|
'invoices:update',
|
||||||
|
'invoices:download',
|
||||||
|
'invoices:delete',
|
||||||
|
'invoice-settings:view',
|
||||||
|
'invoice-settings:update',
|
||||||
])->description('Owner users can perform any action. There is only one owner per organization.');
|
])->description('Owner users can perform any action. There is only one owner per organization.');
|
||||||
|
|
||||||
Jetstream::role(Role::Admin->value, 'Administrator', [
|
Jetstream::role(Role::Admin->value, 'Administrator', [
|
||||||
@@ -185,6 +192,13 @@ class JetstreamServiceProvider extends ServiceProvider
|
|||||||
'reports:create',
|
'reports:create',
|
||||||
'reports:update',
|
'reports:update',
|
||||||
'reports:delete',
|
'reports:delete',
|
||||||
|
'invoices:view',
|
||||||
|
'invoices:create',
|
||||||
|
'invoices:update',
|
||||||
|
'invoices:download',
|
||||||
|
'invoices:delete',
|
||||||
|
'invoice-settings:view',
|
||||||
|
'invoice-settings:update',
|
||||||
])->description('Administrator users can perform any action, except accessing the billing dashboard.');
|
])->description('Administrator users can perform any action, except accessing the billing dashboard.');
|
||||||
|
|
||||||
Jetstream::role(Role::Manager->value, 'Manager', [
|
Jetstream::role(Role::Manager->value, 'Manager', [
|
||||||
@@ -227,6 +241,13 @@ class JetstreamServiceProvider extends ServiceProvider
|
|||||||
'reports:create',
|
'reports:create',
|
||||||
'reports:update',
|
'reports:update',
|
||||||
'reports:delete',
|
'reports:delete',
|
||||||
|
'invoices:view',
|
||||||
|
'invoices:create',
|
||||||
|
'invoices:update',
|
||||||
|
'invoices:download',
|
||||||
|
'invoices:delete',
|
||||||
|
'invoice-settings:view',
|
||||||
|
'invoice-settings:update',
|
||||||
])->description('Managers have full access to all projects, time entries, ect. but cannot manage the organization (add/remove member, edit the organization, ect.).');
|
])->description('Managers have full access to all projects, time entries, ect. but cannot manage the organization (add/remove member, edit the organization, ect.).');
|
||||||
|
|
||||||
Jetstream::role(Role::Employee->value, 'Employee', [
|
Jetstream::role(Role::Employee->value, 'Employee', [
|
||||||
|
|||||||
377
app/Service/CurrencyService.php
Normal file
377
app/Service/CurrencyService.php
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use Brick\Money\Money;
|
||||||
|
|
||||||
|
class CurrencyService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @source https://gist.github.com/stephenfrank/a8245c2486f3e546107c5363706ac93e
|
||||||
|
*
|
||||||
|
* @const array<string, array<{ symbol: string }>>
|
||||||
|
*/
|
||||||
|
private const array CURRENCIES = [
|
||||||
|
'ALL' => [
|
||||||
|
'symbol' => 'L',
|
||||||
|
],
|
||||||
|
'AFN' => [
|
||||||
|
'symbol' => '؋',
|
||||||
|
],
|
||||||
|
'ARS' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'AWG' => [
|
||||||
|
'symbol' => 'ƒ',
|
||||||
|
],
|
||||||
|
'AUD' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'AZN' => [
|
||||||
|
'symbol' => '₼',
|
||||||
|
],
|
||||||
|
'BSD' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'BBD' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'BDT' => [
|
||||||
|
'symbol' => '৳',
|
||||||
|
],
|
||||||
|
'BYR' => [
|
||||||
|
'symbol' => 'Br',
|
||||||
|
],
|
||||||
|
'BZD' => [
|
||||||
|
'symbol' => 'BZ$',
|
||||||
|
],
|
||||||
|
'BMD' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'BOB' => [
|
||||||
|
'symbol' => '$b',
|
||||||
|
],
|
||||||
|
'BAM' => [
|
||||||
|
'symbol' => 'KM',
|
||||||
|
],
|
||||||
|
'BWP' => [
|
||||||
|
'symbol' => 'P',
|
||||||
|
],
|
||||||
|
'BGN' => [
|
||||||
|
'symbol' => 'лв',
|
||||||
|
],
|
||||||
|
'BRL' => [
|
||||||
|
'symbol' => 'R$',
|
||||||
|
],
|
||||||
|
'BND' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'KHR' => [
|
||||||
|
'symbol' => '៛',
|
||||||
|
],
|
||||||
|
'CAD' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'KYD' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'CLP' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'CNY' => [
|
||||||
|
'symbol' => '¥',
|
||||||
|
],
|
||||||
|
'COP' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'CRC' => [
|
||||||
|
'symbol' => '₡',
|
||||||
|
],
|
||||||
|
'HRK' => [
|
||||||
|
'symbol' => 'kn',
|
||||||
|
],
|
||||||
|
'CUP' => [
|
||||||
|
'symbol' => '₱',
|
||||||
|
],
|
||||||
|
'CZK' => [
|
||||||
|
'symbol' => 'Kč',
|
||||||
|
],
|
||||||
|
'DKK' => [
|
||||||
|
'symbol' => 'kr',
|
||||||
|
],
|
||||||
|
'DOP' => [
|
||||||
|
'symbol' => 'RD$',
|
||||||
|
],
|
||||||
|
'XCD' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'EGP' => [
|
||||||
|
'symbol' => '£',
|
||||||
|
],
|
||||||
|
'SVC' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'EEK' => [
|
||||||
|
'symbol' => 'kr',
|
||||||
|
],
|
||||||
|
'EUR' => [
|
||||||
|
'symbol' => '€',
|
||||||
|
],
|
||||||
|
'FKP' => [
|
||||||
|
'symbol' => '£',
|
||||||
|
],
|
||||||
|
'FJD' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'GHC' => [
|
||||||
|
'symbol' => '₵',
|
||||||
|
],
|
||||||
|
'GIP' => [
|
||||||
|
'symbol' => '£',
|
||||||
|
],
|
||||||
|
'GTQ' => [
|
||||||
|
'symbol' => 'Q',
|
||||||
|
],
|
||||||
|
'GGP' => [
|
||||||
|
'symbol' => '£',
|
||||||
|
],
|
||||||
|
'GYD' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'HNL' => [
|
||||||
|
'symbol' => 'L',
|
||||||
|
],
|
||||||
|
'HKD' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'HUF' => [
|
||||||
|
'symbol' => 'Ft',
|
||||||
|
],
|
||||||
|
'ISK' => [
|
||||||
|
'symbol' => 'kr',
|
||||||
|
],
|
||||||
|
'INR' => [
|
||||||
|
'symbol' => '₹',
|
||||||
|
],
|
||||||
|
'IDR' => [
|
||||||
|
'symbol' => 'Rp',
|
||||||
|
],
|
||||||
|
'IRR' => [
|
||||||
|
'symbol' => '﷼',
|
||||||
|
],
|
||||||
|
'IMP' => [
|
||||||
|
'symbol' => '£',
|
||||||
|
],
|
||||||
|
'ILS' => [
|
||||||
|
'symbol' => '₪',
|
||||||
|
],
|
||||||
|
'JMD' => [
|
||||||
|
'symbol' => 'J$',
|
||||||
|
],
|
||||||
|
'JPY' => [
|
||||||
|
'symbol' => '¥',
|
||||||
|
],
|
||||||
|
'JEP' => [
|
||||||
|
'symbol' => '£',
|
||||||
|
],
|
||||||
|
'KZT' => [
|
||||||
|
'symbol' => 'лв',
|
||||||
|
],
|
||||||
|
'KPW' => [
|
||||||
|
'symbol' => '₩',
|
||||||
|
],
|
||||||
|
'KRW' => [
|
||||||
|
'symbol' => '₩',
|
||||||
|
],
|
||||||
|
'KGS' => [
|
||||||
|
'symbol' => 'лв',
|
||||||
|
],
|
||||||
|
'LAK' => [
|
||||||
|
'symbol' => '₭',
|
||||||
|
],
|
||||||
|
'LVL' => [
|
||||||
|
'symbol' => 'Ls',
|
||||||
|
],
|
||||||
|
'LBP' => [
|
||||||
|
'symbol' => '£',
|
||||||
|
],
|
||||||
|
'LRD' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'LTL' => [
|
||||||
|
'symbol' => 'Lt',
|
||||||
|
],
|
||||||
|
'MKD' => [
|
||||||
|
'symbol' => 'ден',
|
||||||
|
],
|
||||||
|
'MYR' => [
|
||||||
|
'symbol' => 'RM',
|
||||||
|
],
|
||||||
|
'MUR' => [
|
||||||
|
'symbol' => '₨',
|
||||||
|
],
|
||||||
|
'MXN' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'MNT' => [
|
||||||
|
'symbol' => '₮',
|
||||||
|
],
|
||||||
|
'MZN' => [
|
||||||
|
'symbol' => 'MT',
|
||||||
|
],
|
||||||
|
'NAD' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'NPR' => [
|
||||||
|
'symbol' => '₨',
|
||||||
|
],
|
||||||
|
'ANG' => [
|
||||||
|
'symbol' => 'ƒ',
|
||||||
|
],
|
||||||
|
'NZD' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'NIO' => [
|
||||||
|
'symbol' => 'C$',
|
||||||
|
],
|
||||||
|
'NGN' => [
|
||||||
|
'symbol' => '₦',
|
||||||
|
],
|
||||||
|
'NOK' => [
|
||||||
|
'symbol' => 'kr',
|
||||||
|
],
|
||||||
|
'OMR' => [
|
||||||
|
'symbol' => '﷼',
|
||||||
|
],
|
||||||
|
'PKR' => [
|
||||||
|
'symbol' => '₨',
|
||||||
|
],
|
||||||
|
'PAB' => [
|
||||||
|
'symbol' => 'B/.',
|
||||||
|
],
|
||||||
|
'PYG' => [
|
||||||
|
'symbol' => 'Gs',
|
||||||
|
],
|
||||||
|
'PEN' => [
|
||||||
|
'symbol' => 'S/.',
|
||||||
|
],
|
||||||
|
'PHP' => [
|
||||||
|
'symbol' => '₱',
|
||||||
|
],
|
||||||
|
'PLN' => [
|
||||||
|
'symbol' => 'zł',
|
||||||
|
],
|
||||||
|
'QAR' => [
|
||||||
|
'symbol' => '﷼',
|
||||||
|
],
|
||||||
|
'RON' => [
|
||||||
|
'symbol' => 'lei',
|
||||||
|
],
|
||||||
|
'RUB' => [
|
||||||
|
'symbol' => '₽',
|
||||||
|
],
|
||||||
|
'SHP' => [
|
||||||
|
'symbol' => '£',
|
||||||
|
],
|
||||||
|
'SAR' => [
|
||||||
|
'symbol' => '﷼',
|
||||||
|
],
|
||||||
|
'RSD' => [
|
||||||
|
'symbol' => 'Дин.',
|
||||||
|
],
|
||||||
|
'SCR' => [
|
||||||
|
'symbol' => '₨',
|
||||||
|
],
|
||||||
|
'SGD' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'SBD' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'SOS' => [
|
||||||
|
'symbol' => 'S',
|
||||||
|
],
|
||||||
|
'ZAR' => [
|
||||||
|
'symbol' => 'R',
|
||||||
|
],
|
||||||
|
'LKR' => [
|
||||||
|
'symbol' => '₨',
|
||||||
|
],
|
||||||
|
'SEK' => [
|
||||||
|
'symbol' => 'kr',
|
||||||
|
],
|
||||||
|
'CHF' => [
|
||||||
|
'symbol' => 'CHF',
|
||||||
|
],
|
||||||
|
'SRD' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'SYP' => [
|
||||||
|
'symbol' => '£',
|
||||||
|
],
|
||||||
|
'TWD' => [
|
||||||
|
'symbol' => 'NT$',
|
||||||
|
],
|
||||||
|
'THB' => [
|
||||||
|
'symbol' => '฿',
|
||||||
|
],
|
||||||
|
'TTD' => [
|
||||||
|
'symbol' => 'TT$',
|
||||||
|
],
|
||||||
|
'TRY' => [
|
||||||
|
'symbol' => '₺',
|
||||||
|
],
|
||||||
|
'TRL' => [
|
||||||
|
'symbol' => '₤',
|
||||||
|
],
|
||||||
|
'TVD' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'UAH' => [
|
||||||
|
'symbol' => '₴',
|
||||||
|
],
|
||||||
|
'GBP' => [
|
||||||
|
'symbol' => '£',
|
||||||
|
],
|
||||||
|
'UGX' => [
|
||||||
|
'symbol' => 'USh',
|
||||||
|
],
|
||||||
|
'USD' => [
|
||||||
|
'symbol' => '$',
|
||||||
|
],
|
||||||
|
'UYU' => [
|
||||||
|
'symbol' => '$U',
|
||||||
|
],
|
||||||
|
'UZS' => [
|
||||||
|
'symbol' => 'лв',
|
||||||
|
],
|
||||||
|
'VEF' => [
|
||||||
|
'symbol' => 'Bs',
|
||||||
|
],
|
||||||
|
'VND' => [
|
||||||
|
'symbol' => '₫',
|
||||||
|
],
|
||||||
|
'YER' => [
|
||||||
|
'symbol' => '﷼',
|
||||||
|
],
|
||||||
|
'ZWD' => [
|
||||||
|
'symbol' => 'Z$',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function getCurrencySymbolForMoney(Money $money): string
|
||||||
|
{
|
||||||
|
return $this->getCurrencySymbol($money->getCurrency()->getCurrencyCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrencySymbol(string $currencyCode): string
|
||||||
|
{
|
||||||
|
if (isset(self::CURRENCIES[$currencyCode]['symbol'])) {
|
||||||
|
return self::CURRENCIES[$currencyCode]['symbol'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $currencyCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ class ClockifyProjectsImporter extends DefaultImporter
|
|||||||
if ($record['Project'] !== '') {
|
if ($record['Project'] !== '') {
|
||||||
$projectId = $this->projectImportHelper->getKey([
|
$projectId = $this->projectImportHelper->getKey([
|
||||||
'name' => $record['Project'],
|
'name' => $record['Project'],
|
||||||
|
'client_id' => $clientId,
|
||||||
'organization_id' => $this->organization->id,
|
'organization_id' => $this->organization->id,
|
||||||
], [
|
], [
|
||||||
'client_id' => $clientId,
|
|
||||||
'color' => $this->colorService->getRandomColor(),
|
'color' => $this->colorService->getRandomColor(),
|
||||||
'is_billable' => $record['Billability'] === 'Yes',
|
'is_billable' => $record['Billability'] === 'Yes',
|
||||||
'billable_rate' => $billableRateKey !== null && $record[$billableRateKey] !== '' ? (int) (((float) $record[$billableRateKey]) * 100) : null,
|
'billable_rate' => $billableRateKey !== null && $record[$billableRateKey] !== '' ? (int) (((float) $record[$billableRateKey]) * 100) : null,
|
||||||
|
|||||||
@@ -83,9 +83,9 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
|||||||
if ($record['Project'] !== '') {
|
if ($record['Project'] !== '') {
|
||||||
$projectId = $this->projectImportHelper->getKey([
|
$projectId = $this->projectImportHelper->getKey([
|
||||||
'name' => $record['Project'],
|
'name' => $record['Project'],
|
||||||
|
'client_id' => $clientId,
|
||||||
'organization_id' => $this->organization->id,
|
'organization_id' => $this->organization->id,
|
||||||
], [
|
], [
|
||||||
'client_id' => $clientId,
|
|
||||||
'color' => $this->colorService->getRandomColor(),
|
'color' => $this->colorService->getRandomColor(),
|
||||||
'is_billable' => false,
|
'is_billable' => false,
|
||||||
]);
|
]);
|
||||||
@@ -124,34 +124,59 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
|||||||
$timeEntry->is_imported = true;
|
$timeEntry->is_imported = true;
|
||||||
|
|
||||||
// Start
|
// Start
|
||||||
|
$start = null;
|
||||||
try {
|
try {
|
||||||
if (preg_match('/^[0-9]{1,2}:[0-9]{1,2} (AM|PM)$/', $record['Start Time']) === 1) {
|
$startDateStr = $record['Start Date'];
|
||||||
$start = Carbon::createFromFormat('m/d/Y h:i A', $record['Start Date'].' '.$record['Start Time'], $timezone);
|
$startTimeStr = $record['Start Time'];
|
||||||
} else {
|
$startStr = $startDateStr.' '.$startTimeStr;
|
||||||
$start = Carbon::createFromFormat('m/d/Y H:i:s A', $record['Start Date'].' '.$record['Start Time'], $timezone);
|
$matches = [];
|
||||||
|
$checkResult = preg_match('/^([0-9]{1,2})\/([0-9]{1,2})\/([0-9]{4}) ([0-9]{1,2}):([0-9]{1,2})(:[0-9]{1,2})? (AM|PM)$/', $startStr, $matches);
|
||||||
|
|
||||||
|
if ($checkResult === 1) {
|
||||||
|
if ((int) $matches[1] > 12) {
|
||||||
|
throw new ImportException('Start date ("'.$startDateStr.'") is invalid, please select the correct date format before exporting from Clockify');
|
||||||
|
}
|
||||||
|
if ($matches[6] === '') {
|
||||||
|
$start = Carbon::createFromFormat('m/d/Y h:i A', $startStr, $timezone);
|
||||||
|
} else {
|
||||||
|
$start = Carbon::createFromFormat('m/d/Y H:i:s A', $startStr, $timezone);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (InvalidFormatException) {
|
} catch (InvalidFormatException) {
|
||||||
throw new ImportException('Start date ("'.$record['Start Date'].'") or time ("'.$record['Start Time'].'") are invalid');
|
throw new ImportException('Start date ("'.$startDateStr.'") or time ("'.$startTimeStr.'") are invalid');
|
||||||
}
|
}
|
||||||
if ($start === null) {
|
if ($start === null) {
|
||||||
throw new ImportException('Start date ("'.$record['Start Date'].'") or time ("'.$record['Start Time'].'") are invalid');
|
throw new ImportException('Start date ("'.$startDateStr.'") or time ("'.$startTimeStr.'") are invalid');
|
||||||
}
|
}
|
||||||
$timeEntry->start = $start->utc();
|
$timeEntry->start = $start->utc();
|
||||||
|
|
||||||
// End
|
// End
|
||||||
|
$end = null;
|
||||||
try {
|
try {
|
||||||
if (preg_match('/^[0-9]{1,2}:[0-9]{1,2} (AM|PM)$/', $record['End Time']) === 1) {
|
$endDateStr = $record['End Date'];
|
||||||
$end = Carbon::createFromFormat('m/d/Y h:i A', $record['End Date'].' '.$record['End Time'], $timezone);
|
$endTimeStr = $record['End Time'];
|
||||||
} else {
|
$endStr = $endDateStr.' '.$endTimeStr;
|
||||||
$end = Carbon::createFromFormat('m/d/Y H:i:s A', $record['End Date'].' '.$record['End Time'], $timezone);
|
$matches = [];
|
||||||
|
$checkResult = preg_match('/^([0-9]{1,2})\/([0-9]{1,2})\/([0-9]{4}) ([0-9]{1,2}):([0-9]{1,2})(:[0-9]{1,2})? (AM|PM)$/', $endStr, $matches);
|
||||||
|
|
||||||
|
if ($checkResult === 1) {
|
||||||
|
if ((int) $matches[1] > 12) {
|
||||||
|
throw new ImportException('Start date ("'.$endDateStr.'") is invalid, please select the correct date format before exporting from Clockify');
|
||||||
|
}
|
||||||
|
if ($matches[6] === '') {
|
||||||
|
$end = Carbon::createFromFormat('m/d/Y h:i A', $endStr, $timezone);
|
||||||
|
} else {
|
||||||
|
$end = Carbon::createFromFormat('m/d/Y H:i:s A', $endStr, $timezone);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (InvalidFormatException) {
|
} catch (InvalidFormatException) {
|
||||||
throw new ImportException('End date ("'.$record['End Date'].'") or time ("'.$record['End Time'].'") are invalid');
|
throw new ImportException('End date ("'.$endDateStr.'") or time ("'.$endTimeStr.'") are invalid');
|
||||||
}
|
}
|
||||||
if ($end === null) {
|
if ($end === null) {
|
||||||
throw new ImportException('End date ("'.$record['End Date'].'") or time ("'.$record['End Time'].'") are invalid');
|
throw new ImportException('End date ("'.$endDateStr.'") or time ("'.$endTimeStr.'") are invalid');
|
||||||
}
|
}
|
||||||
$timeEntry->end = $end->utc();
|
$timeEntry->end = $end->utc();
|
||||||
|
|
||||||
$timeEntry->billable_rate = $this->billableRateService->getBillableRateForTimeEntryWithGivenRelations(
|
$timeEntry->billable_rate = $this->billableRateService->getBillableRateForTimeEntryWithGivenRelations(
|
||||||
$timeEntry,
|
$timeEntry,
|
||||||
$projectMember,
|
$projectMember,
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ abstract class DefaultImporter implements ImporterContract
|
|||||||
'in:placeholder',
|
'in:placeholder',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
$this->projectImportHelper = new ImportDatabaseHelper(Project::class, ['name', 'organization_id'], true, function (Builder $builder) {
|
$this->projectImportHelper = new ImportDatabaseHelper(Project::class, ['name', 'client_id', 'organization_id'], true, function (Builder $builder) {
|
||||||
/** @var Builder<Project> $builder */
|
/** @var Builder<Project> $builder */
|
||||||
return $builder->where('organization_id', $this->organization->id);
|
return $builder->where('organization_id', $this->organization->id);
|
||||||
}, validate: [
|
}, validate: [
|
||||||
@@ -114,6 +114,11 @@ abstract class DefaultImporter implements ImporterContract
|
|||||||
'integer',
|
'integer',
|
||||||
'max:2147483647',
|
'max:2147483647',
|
||||||
],
|
],
|
||||||
|
'client_id' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
'uuid',
|
||||||
|
],
|
||||||
], beforeSave: function (Project $project): void {
|
], beforeSave: function (Project $project): void {
|
||||||
if ($project->billable_rate === 0) {
|
if ($project->billable_rate === 0) {
|
||||||
$project->billable_rate = null;
|
$project->billable_rate = null;
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ class GenericProjectsImporter extends DefaultImporter
|
|||||||
}
|
}
|
||||||
$this->projectImportHelper->getKey([
|
$this->projectImportHelper->getKey([
|
||||||
'name' => $record['name'],
|
'name' => $record['name'],
|
||||||
|
'client_id' => $clientId,
|
||||||
'organization_id' => $this->organization->id,
|
'organization_id' => $this->organization->id,
|
||||||
], [
|
], [
|
||||||
'color' => isset($record['color']) && $record['color'] !== '' ? $record['color'] : app(ColorService::class)->getRandomColor(),
|
'color' => isset($record['color']) && $record['color'] !== '' ? $record['color'] : app(ColorService::class)->getRandomColor(),
|
||||||
'billable_rate' => isset($record['billable_rate']) && $record['billable_rate'] !== '' ? (int) $record['billable_rate'] : null,
|
'billable_rate' => isset($record['billable_rate']) && $record['billable_rate'] !== '' ? (int) $record['billable_rate'] : null,
|
||||||
'is_public' => isset($record['is_public']) && $record['is_public'] === 'true',
|
'is_public' => isset($record['is_public']) && $record['is_public'] === 'true',
|
||||||
'client_id' => $clientId,
|
|
||||||
'is_billable' => isset($record['billable_default']) && $record['billable_default'] === 'true',
|
'is_billable' => isset($record['billable_default']) && $record['billable_default'] === 'true',
|
||||||
'estimated_time' => isset($record['estimated_time']) && $record['estimated_time'] !== '' && is_numeric($record['estimated_time']) && ((int) $record['estimated_time'] !== 0) ? (int) $record['estimated_time'] : null,
|
'estimated_time' => isset($record['estimated_time']) && $record['estimated_time'] !== '' && is_numeric($record['estimated_time']) && ((int) $record['estimated_time'] !== 0) ? (int) $record['estimated_time'] : null,
|
||||||
'archived_at' => $archivedAt,
|
'archived_at' => $archivedAt,
|
||||||
|
|||||||
@@ -99,9 +99,9 @@ class GenericTimeEntriesImporter extends DefaultImporter
|
|||||||
if ($record['project'] !== '') {
|
if ($record['project'] !== '') {
|
||||||
$projectId = $this->projectImportHelper->getKey([
|
$projectId = $this->projectImportHelper->getKey([
|
||||||
'name' => $record['project'],
|
'name' => $record['project'],
|
||||||
|
'client_id' => $clientId,
|
||||||
'organization_id' => $this->organization->id,
|
'organization_id' => $this->organization->id,
|
||||||
], [
|
], [
|
||||||
'client_id' => $clientId,
|
|
||||||
'is_billable' => false,
|
'is_billable' => false,
|
||||||
'color' => $this->colorService->getRandomColor(),
|
'color' => $this->colorService->getRandomColor(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -60,10 +60,10 @@ class HarvestProjectsImporter extends DefaultImporter
|
|||||||
$billableHours = $billableHoursField !== '' && is_numeric($billableHoursField) ? (int) ((float) $billableHoursField) : null;
|
$billableHours = $billableHoursField !== '' && is_numeric($billableHoursField) ? (int) ((float) $billableHoursField) : null;
|
||||||
$this->projectImportHelper->getKey([
|
$this->projectImportHelper->getKey([
|
||||||
'name' => $record['Project'],
|
'name' => $record['Project'],
|
||||||
|
'client_id' => $clientId,
|
||||||
'organization_id' => $this->organization->id,
|
'organization_id' => $this->organization->id,
|
||||||
], [
|
], [
|
||||||
'color' => $this->colorService->getRandomColor(),
|
'color' => $this->colorService->getRandomColor(),
|
||||||
'client_id' => $clientId,
|
|
||||||
'estimated_time' => $estimatedTime,
|
'estimated_time' => $estimatedTime,
|
||||||
'is_billable' => $billableHours > 0,
|
'is_billable' => $billableHours > 0,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -78,9 +78,9 @@ class HarvestTimeEntriesImporter extends DefaultImporter
|
|||||||
if ($record['Project'] !== '') {
|
if ($record['Project'] !== '') {
|
||||||
$projectId = $this->projectImportHelper->getKey([
|
$projectId = $this->projectImportHelper->getKey([
|
||||||
'name' => $record['Project'],
|
'name' => $record['Project'],
|
||||||
|
'client_id' => $clientId,
|
||||||
'organization_id' => $this->organization->id,
|
'organization_id' => $this->organization->id,
|
||||||
], [
|
], [
|
||||||
'client_id' => $clientId,
|
|
||||||
'color' => $this->colorService->getRandomColor(),
|
'color' => $this->colorService->getRandomColor(),
|
||||||
'is_billable' => true,
|
'is_billable' => true,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -176,12 +176,12 @@ class SolidtimeImporter extends DefaultImporter
|
|||||||
|
|
||||||
$this->projectImportHelper->getKey([
|
$this->projectImportHelper->getKey([
|
||||||
'name' => $project['name'],
|
'name' => $project['name'],
|
||||||
|
'client_id' => $clientId,
|
||||||
'organization_id' => $this->organization->getKey(),
|
'organization_id' => $this->organization->getKey(),
|
||||||
], [
|
], [
|
||||||
'color' => $project['color'],
|
'color' => $project['color'],
|
||||||
'billable_rate' => $project['billable_rate'] === '' ? null : (int) $project['billable_rate'],
|
'billable_rate' => $project['billable_rate'] === '' ? null : (int) $project['billable_rate'],
|
||||||
'is_public' => $project['is_public'] === 'true',
|
'is_public' => $project['is_public'] === 'true',
|
||||||
'client_id' => $clientId,
|
|
||||||
'is_billable' => $project['is_billable'] === 'true',
|
'is_billable' => $project['is_billable'] === 'true',
|
||||||
'archived_at' => $project['archived_at'] !== '' ? Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $project['archived_at'], 'UTC') : null,
|
'archived_at' => $project['archived_at'] !== '' ? Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $project['archived_at'], 'UTC') : null,
|
||||||
], $project['id']);
|
], $project['id']);
|
||||||
|
|||||||
@@ -137,9 +137,9 @@ class TogglDataImporter extends DefaultImporter
|
|||||||
|
|
||||||
$projectId = $this->projectImportHelper->getKey([
|
$projectId = $this->projectImportHelper->getKey([
|
||||||
'name' => $project->name,
|
'name' => $project->name,
|
||||||
|
'client_id' => $clientId,
|
||||||
'organization_id' => $this->organization->getKey(),
|
'organization_id' => $this->organization->getKey(),
|
||||||
], [
|
], [
|
||||||
'client_id' => $clientId,
|
|
||||||
'color' => $project->color,
|
'color' => $project->color,
|
||||||
'is_billable' => $project->billable,
|
'is_billable' => $project->billable,
|
||||||
'is_public' => ! $project->is_private,
|
'is_public' => ! $project->is_private,
|
||||||
|
|||||||
@@ -83,9 +83,9 @@ class TogglTimeEntriesImporter extends DefaultImporter
|
|||||||
if ($record['Project'] !== '') {
|
if ($record['Project'] !== '') {
|
||||||
$projectId = $this->projectImportHelper->getKey([
|
$projectId = $this->projectImportHelper->getKey([
|
||||||
'name' => $record['Project'],
|
'name' => $record['Project'],
|
||||||
|
'client_id' => $clientId,
|
||||||
'organization_id' => $this->organization->id,
|
'organization_id' => $this->organization->id,
|
||||||
], [
|
], [
|
||||||
'client_id' => $clientId,
|
|
||||||
'is_billable' => false,
|
'is_billable' => false,
|
||||||
'color' => $this->colorService->getRandomColor(),
|
'color' => $this->colorService->getRandomColor(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
155
app/Service/LocalizationService.php
Normal file
155
app/Service/LocalizationService.php
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Enums\CurrencyFormat;
|
||||||
|
use App\Enums\DateFormat;
|
||||||
|
use App\Enums\IntervalFormat;
|
||||||
|
use App\Enums\NumberFormat;
|
||||||
|
use App\Enums\TimeFormat;
|
||||||
|
use App\Models\Organization;
|
||||||
|
use Brick\Math\BigDecimal;
|
||||||
|
use Brick\Money\Money;
|
||||||
|
use Carbon\CarbonInterface;
|
||||||
|
use Carbon\CarbonInterval;
|
||||||
|
|
||||||
|
class LocalizationService
|
||||||
|
{
|
||||||
|
private CurrencyFormat $currencyFormat;
|
||||||
|
|
||||||
|
private IntervalFormat $intervalFormat;
|
||||||
|
|
||||||
|
private DateFormat $dateFormat;
|
||||||
|
|
||||||
|
private TimeFormat $timeFormat;
|
||||||
|
|
||||||
|
private NumberFormat $numberFormat;
|
||||||
|
|
||||||
|
public function __construct(CurrencyFormat $currencyFormat, DateFormat $dateFormat, TimeFormat $timeFormat, NumberFormat $numberFormat, IntervalFormat $intervalFormat)
|
||||||
|
{
|
||||||
|
$this->currencyFormat = $currencyFormat;
|
||||||
|
$this->dateFormat = $dateFormat;
|
||||||
|
$this->timeFormat = $timeFormat;
|
||||||
|
$this->numberFormat = $numberFormat;
|
||||||
|
$this->intervalFormat = $intervalFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function forOrganization(Organization $organization): self
|
||||||
|
{
|
||||||
|
return new LocalizationService(
|
||||||
|
$organization->currency_format,
|
||||||
|
$organization->date_format,
|
||||||
|
$organization->time_format,
|
||||||
|
$organization->number_format,
|
||||||
|
$organization->interval_format
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function formatNumber(BigDecimal|float $number): string
|
||||||
|
{
|
||||||
|
$numberFloat = $number instanceof BigDecimal ? $number->toFloat() : $number;
|
||||||
|
|
||||||
|
if ($this->numberFormat === NumberFormat::ThousandsPointDecimalComma) {
|
||||||
|
return number_format($numberFloat, 2, ',', '.');
|
||||||
|
} elseif ($this->numberFormat === NumberFormat::ThousandsSpaceDecimalPoint) {
|
||||||
|
return number_format($numberFloat, 2, '.', ' ');
|
||||||
|
} elseif ($this->numberFormat === NumberFormat::ThousandsCommaDecimalPoint) {
|
||||||
|
return number_format($numberFloat, 2, '.', ',');
|
||||||
|
} elseif ($this->numberFormat === NumberFormat::ThousandsSpaceDecimalComma) {
|
||||||
|
return number_format($numberFloat, 2, ',', ' ');
|
||||||
|
} elseif ($this->numberFormat === NumberFormat::ThousandsApostropheDecimalPoint) {
|
||||||
|
return number_format($numberFloat, 2, '.', '\'');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function formatNumberWithoutTrailingZeros(BigDecimal|float $number): string
|
||||||
|
{
|
||||||
|
$number = $this->formatNumber($number);
|
||||||
|
|
||||||
|
$number = rtrim($number, '0');
|
||||||
|
$number = rtrim($number, '.');
|
||||||
|
$number = rtrim($number, ',');
|
||||||
|
|
||||||
|
return $number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function formatInterval(CarbonInterval $interval): string
|
||||||
|
{
|
||||||
|
if ($this->intervalFormat === IntervalFormat::Decimal) {
|
||||||
|
$interval->cascade();
|
||||||
|
|
||||||
|
return $this->formatNumber($interval->totalHours).' h';
|
||||||
|
} elseif ($this->intervalFormat === IntervalFormat::HoursMinutes) {
|
||||||
|
$interval->cascade();
|
||||||
|
|
||||||
|
return ((int) floor($interval->totalHours)).'h '.$interval->format('%I').'m';
|
||||||
|
} elseif ($this->intervalFormat === IntervalFormat::HoursMinutesColonSeparated) {
|
||||||
|
$interval->cascade();
|
||||||
|
|
||||||
|
return ((int) floor($interval->totalHours)).':'.$interval->format('%I');
|
||||||
|
} elseif ($this->intervalFormat === IntervalFormat::HoursMinutesSecondsColonSeparated) {
|
||||||
|
$interval->cascade();
|
||||||
|
|
||||||
|
return ((int) floor($interval->totalHours)).':'.$interval->format('%I:%S');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function formatCurrency(Money $money): string
|
||||||
|
{
|
||||||
|
$currencyService = app(CurrencyService::class);
|
||||||
|
if ($this->currencyFormat === CurrencyFormat::ISOCodeAfterWithSpace) {
|
||||||
|
return $this->formatNumber($money->getAmount()).' '.$money->getCurrency()->getCurrencyCode();
|
||||||
|
} elseif ($this->currencyFormat === CurrencyFormat::ISOCodeBeforeWithSpace) {
|
||||||
|
return $money->getCurrency()->getCurrencyCode().' '.$this->formatNumber($money->getAmount());
|
||||||
|
} elseif ($this->currencyFormat === CurrencyFormat::SymbolAfter) {
|
||||||
|
return $this->formatNumber($money->getAmount()).$currencyService->getCurrencySymbolForMoney($money);
|
||||||
|
} elseif ($this->currencyFormat === CurrencyFormat::SymbolBefore) {
|
||||||
|
return $currencyService->getCurrencySymbolForMoney($money).$this->formatNumber($money->getAmount());
|
||||||
|
} elseif ($this->currencyFormat === CurrencyFormat::SymbolBeforeWithSpace) {
|
||||||
|
return $currencyService->getCurrencySymbolForMoney($money).' '.$this->formatNumber($money->getAmount());
|
||||||
|
} elseif ($this->currencyFormat === CurrencyFormat::SymbolAfterWithSpace) {
|
||||||
|
return $this->formatNumber($money->getAmount()).' '.$currencyService->getCurrencySymbolForMoney($money);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function formatTime(CarbonInterface $time): string
|
||||||
|
{
|
||||||
|
if ($this->timeFormat === TimeFormat::TwelveHours) {
|
||||||
|
return $time->format('h:i a'); // Examples: "11:01 am", "1:02 am"
|
||||||
|
} elseif ($this->timeFormat === TimeFormat::TwentyFourHours) {
|
||||||
|
return $time->format('H:i'); // Examples: "23:01", "01:02"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function formatDate(CarbonInterface $date): string
|
||||||
|
{
|
||||||
|
return $date->format($this->dateFormat->toCarbonFormat());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDateFormat(DateFormat $dateFormat): void
|
||||||
|
{
|
||||||
|
$this->dateFormat = $dateFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCurrencyFormat(CurrencyFormat $currencyFormat): void
|
||||||
|
{
|
||||||
|
$this->currencyFormat = $currencyFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIntervalFormat(IntervalFormat $intervalFormat): void
|
||||||
|
{
|
||||||
|
$this->intervalFormat = $intervalFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTimeFormat(TimeFormat $timeFormat): void
|
||||||
|
{
|
||||||
|
$this->timeFormat = $timeFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNumberFormat(NumberFormat $numberFormat): void
|
||||||
|
{
|
||||||
|
$this->numberFormat = $numberFormat;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -164,6 +164,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 +180,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
68
app/Service/OrganizationService.php
Normal file
68
app/Service/OrganizationService.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Enums\CurrencyFormat;
|
||||||
|
use App\Enums\DateFormat;
|
||||||
|
use App\Enums\IntervalFormat;
|
||||||
|
use App\Enums\NumberFormat;
|
||||||
|
use App\Enums\Role;
|
||||||
|
use App\Enums\TimeFormat;
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class OrganizationService
|
||||||
|
{
|
||||||
|
public function createOrganization(
|
||||||
|
string $name,
|
||||||
|
User $owner,
|
||||||
|
bool $personalOrganization,
|
||||||
|
?string $currency = null,
|
||||||
|
?NumberFormat $numberFormat = null,
|
||||||
|
?CurrencyFormat $currencyFormat = null,
|
||||||
|
?DateFormat $dateFormat = null,
|
||||||
|
?IntervalFormat $intervalFormat = null,
|
||||||
|
?TimeFormat $timeFormat = null,
|
||||||
|
): Organization {
|
||||||
|
|
||||||
|
$organization = new Organization;
|
||||||
|
$organization->name = $name;
|
||||||
|
$organization->personal_team = $personalOrganization;
|
||||||
|
if ($currency === null) {
|
||||||
|
$currency = config('app.localization.default_currency');
|
||||||
|
}
|
||||||
|
$organization->currency = $currency;
|
||||||
|
if ($numberFormat === null) {
|
||||||
|
$numberFormat = NumberFormat::from(config('app.localization.default_number_format'));
|
||||||
|
}
|
||||||
|
$organization->number_format = $numberFormat;
|
||||||
|
if ($currencyFormat === null) {
|
||||||
|
$currencyFormat = CurrencyFormat::from(config('app.localization.default_currency_format'));
|
||||||
|
}
|
||||||
|
$organization->currency_format = $currencyFormat;
|
||||||
|
if ($dateFormat === null) {
|
||||||
|
$dateFormat = DateFormat::from(config('app.localization.default_date_format'));
|
||||||
|
}
|
||||||
|
$organization->date_format = $dateFormat;
|
||||||
|
if ($intervalFormat === null) {
|
||||||
|
$intervalFormat = IntervalFormat::from(config('app.localization.default_interval_format'));
|
||||||
|
}
|
||||||
|
$organization->interval_format = $intervalFormat;
|
||||||
|
if ($timeFormat === null) {
|
||||||
|
$timeFormat = TimeFormat::from(config('app.localization.default_time_format'));
|
||||||
|
}
|
||||||
|
$organization->time_format = $timeFormat;
|
||||||
|
$organization->owner()->associate($owner);
|
||||||
|
$organization->save();
|
||||||
|
|
||||||
|
$organization->users()->attach(
|
||||||
|
$owner, [
|
||||||
|
'role' => Role::Owner->value,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return $organization;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ namespace App\Service\ReportExport;
|
|||||||
|
|
||||||
use App\Enums\ExportFormat;
|
use App\Enums\ExportFormat;
|
||||||
use App\Models\TimeEntry;
|
use App\Models\TimeEntry;
|
||||||
use App\Service\IntervalService;
|
use App\Service\LocalizationService;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use LogicException;
|
use LogicException;
|
||||||
use Maatwebsite\Excel\Concerns\Exportable;
|
use Maatwebsite\Excel\Concerns\Exportable;
|
||||||
@@ -37,14 +37,17 @@ class TimeEntriesDetailedExport implements FromQuery, ShouldAutoSize, WithColumn
|
|||||||
|
|
||||||
private string $timezone;
|
private string $timezone;
|
||||||
|
|
||||||
|
private LocalizationService $localizationService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Builder<TimeEntry> $builder
|
* @param Builder<TimeEntry> $builder
|
||||||
*/
|
*/
|
||||||
public function __construct(Builder $builder, ExportFormat $exportFormat, string $timezone)
|
public function __construct(Builder $builder, ExportFormat $exportFormat, string $timezone, LocalizationService $localizationService)
|
||||||
{
|
{
|
||||||
$this->builder = $builder;
|
$this->builder = $builder;
|
||||||
$this->exportFormat = $exportFormat;
|
$this->exportFormat = $exportFormat;
|
||||||
$this->timezone = $timezone;
|
$this->timezone = $timezone;
|
||||||
|
$this->localizationService = $localizationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,7 +116,6 @@ class TimeEntriesDetailedExport implements FromQuery, ShouldAutoSize, WithColumn
|
|||||||
*/
|
*/
|
||||||
public function map($model): array
|
public function map($model): array
|
||||||
{
|
{
|
||||||
$interval = app(IntervalService::class);
|
|
||||||
$duration = $model->getDuration();
|
$duration = $model->getDuration();
|
||||||
|
|
||||||
if ($this->exportFormat === ExportFormat::XLSX) {
|
if ($this->exportFormat === ExportFormat::XLSX) {
|
||||||
@@ -125,7 +127,7 @@ class TimeEntriesDetailedExport implements FromQuery, ShouldAutoSize, WithColumn
|
|||||||
$model->user->name,
|
$model->user->name,
|
||||||
Date::dateTimeToExcel($model->start->timezone($this->timezone)),
|
Date::dateTimeToExcel($model->start->timezone($this->timezone)),
|
||||||
$model->end !== null ? Date::dateTimeToExcel($model->end->timezone($this->timezone)) : null,
|
$model->end !== null ? Date::dateTimeToExcel($model->end->timezone($this->timezone)) : null,
|
||||||
$duration !== null ? $interval->format($duration) : null,
|
$duration !== null ? $this->localizationService->formatInterval($duration) : null,
|
||||||
$duration?->totalHours,
|
$duration?->totalHours,
|
||||||
$model->billable ? 'Yes' : 'No',
|
$model->billable ? 'Yes' : 'No',
|
||||||
$model->tagsRelation->pluck('name')->implode(', '),
|
$model->tagsRelation->pluck('name')->implode(', '),
|
||||||
@@ -139,7 +141,7 @@ class TimeEntriesDetailedExport implements FromQuery, ShouldAutoSize, WithColumn
|
|||||||
$model->user->name,
|
$model->user->name,
|
||||||
$model->start->timezone($this->timezone)->format('Y-m-d H:i:s'),
|
$model->start->timezone($this->timezone)->format('Y-m-d H:i:s'),
|
||||||
$model->end?->timezone($this->timezone)?->format('Y-m-d H:i:s'),
|
$model->end?->timezone($this->timezone)?->format('Y-m-d H:i:s'),
|
||||||
$duration !== null ? (int) floor($duration->totalHours).':'.$duration->format('%I:%S') : null,
|
$duration !== null ? $this->localizationService->formatInterval($duration) : null,
|
||||||
$duration?->totalHours,
|
$duration?->totalHours,
|
||||||
$model->billable ? 'Yes' : 'No',
|
$model->billable ? 'Yes' : 'No',
|
||||||
$model->tagsRelation->pluck('name')->implode(', '),
|
$model->tagsRelation->pluck('name')->implode(', '),
|
||||||
|
|||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -280,6 +280,20 @@ class TimeEntryAggregationService
|
|||||||
'color' => null,
|
'color' => null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
} elseif ($type === TimeEntryAggregationType::Description) {
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
$descriptorMap[$key] = [
|
||||||
|
'description' => $key,
|
||||||
|
'color' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} elseif ($type === TimeEntryAggregationType::Billable) {
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
$descriptorMap[$key] = [
|
||||||
|
'description' => $key === '0' ? 'Non-billable' : 'Billable',
|
||||||
|
'color' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $descriptorMap;
|
return $descriptorMap;
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Service;
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Enums\CurrencyFormat;
|
||||||
|
use App\Enums\DateFormat;
|
||||||
|
use App\Enums\IntervalFormat;
|
||||||
|
use App\Enums\NumberFormat;
|
||||||
use App\Enums\Role;
|
use App\Enums\Role;
|
||||||
|
use App\Enums\TimeFormat;
|
||||||
use App\Enums\Weekday;
|
use App\Enums\Weekday;
|
||||||
use App\Events\AfterCreateOrganization;
|
use App\Events\AfterCreateOrganization;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
@@ -17,8 +22,20 @@ use Illuminate\Support\Facades\Hash;
|
|||||||
|
|
||||||
class UserService
|
class UserService
|
||||||
{
|
{
|
||||||
public function createUser(string $name, string $email, string $password, string $timezone, Weekday $weekStart, string $currency, bool $verifyEmail = false): User
|
public function createUser(
|
||||||
{
|
string $name,
|
||||||
|
string $email,
|
||||||
|
string $password,
|
||||||
|
string $timezone,
|
||||||
|
Weekday $weekStart,
|
||||||
|
?string $currency,
|
||||||
|
?NumberFormat $numberFormat = null,
|
||||||
|
?CurrencyFormat $currencyFormat = null,
|
||||||
|
?DateFormat $dateFormat = null,
|
||||||
|
?IntervalFormat $intervalFormat = null,
|
||||||
|
?TimeFormat $timeFormat = null,
|
||||||
|
bool $verifyEmail = false
|
||||||
|
): User {
|
||||||
$user = new User;
|
$user = new User;
|
||||||
$user->name = $name;
|
$user->name = $name;
|
||||||
$user->email = $email;
|
$user->email = $email;
|
||||||
@@ -30,17 +47,16 @@ class UserService
|
|||||||
}
|
}
|
||||||
$user->save();
|
$user->save();
|
||||||
|
|
||||||
$organization = new Organization;
|
$organization = app(OrganizationService::class)->createOrganization(
|
||||||
$organization->name = explode(' ', $user->name, 2)[0]."'s Organization";
|
$this->getOrganizationNameForUserName($user->name),
|
||||||
$organization->personal_team = true;
|
$user,
|
||||||
$organization->currency = $currency;
|
true,
|
||||||
$organization->owner()->associate($user);
|
$currency,
|
||||||
$organization->save();
|
$numberFormat,
|
||||||
|
$currencyFormat,
|
||||||
$organization->users()->attach(
|
$dateFormat,
|
||||||
$user, [
|
$intervalFormat,
|
||||||
'role' => Role::Owner->value,
|
$timeFormat,
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$user->ownedTeams()->save($organization);
|
$user->ownedTeams()->save($organization);
|
||||||
@@ -78,14 +94,11 @@ class UserService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a new organization
|
// Create a new organization
|
||||||
$organization = new Organization;
|
$organization = app(OrganizationService::class)->createOrganization(
|
||||||
$organization->name = $user->name."'s Organization";
|
$this->getOrganizationNameForUserName($user->name),
|
||||||
$organization->personal_team = true;
|
$user,
|
||||||
$organization->user_id = $user->id;
|
true
|
||||||
$organization->save();
|
);
|
||||||
|
|
||||||
// Attach the user to the organization
|
|
||||||
$organization->users()->attach($user, ['role' => Role::Owner->value]);
|
|
||||||
|
|
||||||
// Set the organization as the user's current organization
|
// Set the organization as the user's current organization
|
||||||
$user->currentOrganization()->associate($organization);
|
$user->currentOrganization()->associate($organization);
|
||||||
@@ -94,15 +107,22 @@ class UserService
|
|||||||
AfterCreateOrganization::dispatch($organization);
|
AfterCreateOrganization::dispatch($organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getOrganizationNameForUserName(string $username): string
|
||||||
|
{
|
||||||
|
return explode(' ', $username, 2)[0]."'s Organization";
|
||||||
|
}
|
||||||
|
|
||||||
public function makeSureUserHasCurrentOrganization(User $user): void
|
public function makeSureUserHasCurrentOrganization(User $user): void
|
||||||
{
|
{
|
||||||
if ($user->currentOrganization !== null) {
|
if ($user->current_team_id !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$organization = $user->organizations()->first();
|
$organization = $user->organizations()->first();
|
||||||
$user->currentOrganization()->associate($organization);
|
if ($organization !== null) {
|
||||||
$user->save();
|
$user->currentOrganization()->associate($organization);
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
20
components.json
Normal file
20
components.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-vue.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"typescript": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "resources/css/app.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"composables": "@/composables",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"guzzlehttp/guzzle": "^7.2",
|
"guzzlehttp/guzzle": "^7.2",
|
||||||
"inertiajs/inertia-laravel": "^1.0",
|
"inertiajs/inertia-laravel": "^1.0",
|
||||||
"korridor/laravel-computed-attributes": "^3.1",
|
"korridor/laravel-computed-attributes": "^3.1",
|
||||||
|
"korridor/laravel-has-many-sync": "^3.1",
|
||||||
"korridor/laravel-model-validation-rules": "^3.0",
|
"korridor/laravel-model-validation-rules": "^3.0",
|
||||||
"laravel/framework": "^11.16.0",
|
"laravel/framework": "^11.16.0",
|
||||||
"laravel/jetstream": "^5.0",
|
"laravel/jetstream": "^5.0",
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
"laravel/tinker": "^2.8",
|
"laravel/tinker": "^2.8",
|
||||||
"league/csv": "^9.16.0",
|
"league/csv": "^9.16.0",
|
||||||
"league/flysystem-aws-s3-v3": "^3.0",
|
"league/flysystem-aws-s3-v3": "^3.0",
|
||||||
|
"league/iso3166": "^4.3",
|
||||||
"maatwebsite/excel": "^3.1",
|
"maatwebsite/excel": "^3.1",
|
||||||
"novadaemon/filament-pretty-json": "^2.2",
|
"novadaemon/filament-pretty-json": "^2.2",
|
||||||
"nwidart/laravel-modules": "^11.0.11",
|
"nwidart/laravel-modules": "^11.0.11",
|
||||||
|
|||||||
1811
composer.lock
generated
1811
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Enums\CurrencyFormat;
|
||||||
|
use App\Enums\DateFormat;
|
||||||
|
use App\Enums\IntervalFormat;
|
||||||
|
use App\Enums\NumberFormat;
|
||||||
|
use App\Enums\TimeFormat;
|
||||||
use Illuminate\Support\Facades\Facade;
|
use Illuminate\Support\Facades\Facade;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
@@ -138,6 +143,15 @@ return [
|
|||||||
|
|
||||||
'cipher' => 'AES-256-CBC',
|
'cipher' => 'AES-256-CBC',
|
||||||
|
|
||||||
|
'localization' => [
|
||||||
|
'default_currency' => env('LOCALIZATION_DEFAULT_CURRENCY', 'EUR'),
|
||||||
|
'default_number_format' => env('LOCALIZATION_DEFAULT_NUMBER_FORMAT', NumberFormat::ThousandsPointDecimalComma->value),
|
||||||
|
'default_currency_format' => env('LOCALIZATION_DEFAULT_CURRENCY_FORMAT', CurrencyFormat::ISOCodeAfterWithSpace->value),
|
||||||
|
'default_date_format' => env('LOCALIZATION_DEFAULT_DATE_FORMAT', DateFormat::HyphenSeparatedYYYYMMDD->value),
|
||||||
|
'default_time_format' => env('LOCALIZATION_DEFAULT_TIME_FORMAT', TimeFormat::TwentyFourHours->value),
|
||||||
|
'default_interval_format' => env('LOCALIZATION_DEFAULT_INTERVAL_FORMAT', IntervalFormat::HoursMinutes->value),
|
||||||
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Maintenance Mode Driver
|
| Maintenance Mode Driver
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ return [
|
|||||||
'time_entry_send_still_running_mails' => (bool) env('SCHEDULING_TASK_TIME_ENTRY_SEND_STILL_RUNNING_MAILS', true),
|
'time_entry_send_still_running_mails' => (bool) env('SCHEDULING_TASK_TIME_ENTRY_SEND_STILL_RUNNING_MAILS', true),
|
||||||
'self_hosting_check_for_update' => (bool) env('SCHEDULING_TASK_SELF_HOSTING_CHECK_FOR_UPDATE', true),
|
'self_hosting_check_for_update' => (bool) env('SCHEDULING_TASK_SELF_HOSTING_CHECK_FOR_UPDATE', true),
|
||||||
'self_hosting_telemetry' => (bool) env('SCHEDULING_TASK_SELF_HOSTING_TELEMETRY', true),
|
'self_hosting_telemetry' => (bool) env('SCHEDULING_TASK_SELF_HOSTING_TELEMETRY', true),
|
||||||
|
'self_hosting_database_consistency' => (bool) env('SCHEDULING_TASK_SELF_HOSTING_DATABASE_CONSISTENCY', false),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Enums\CurrencyFormat;
|
||||||
|
use App\Enums\DateFormat;
|
||||||
|
use App\Enums\IntervalFormat;
|
||||||
|
use App\Enums\NumberFormat;
|
||||||
|
use App\Enums\TimeFormat;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
@@ -27,6 +32,11 @@ class OrganizationFactory extends Factory
|
|||||||
'user_id' => User::factory(),
|
'user_id' => User::factory(),
|
||||||
'personal_team' => true,
|
'personal_team' => true,
|
||||||
'employees_can_see_billable_rates' => false,
|
'employees_can_see_billable_rates' => false,
|
||||||
|
'number_format' => $this->faker->randomElement(NumberFormat::values()),
|
||||||
|
'currency_format' => $this->faker->randomElement(CurrencyFormat::values()),
|
||||||
|
'date_format' => $this->faker->randomElement(DateFormat::values()),
|
||||||
|
'interval_format' => $this->faker->randomElement(IntervalFormat::values()),
|
||||||
|
'time_format' => $this->faker->randomElement(TimeFormat::values()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('organizations', function (Blueprint $table): void {
|
||||||
|
$table->string('number_format')->default(config('app.localization.default_number_format'))->nullable(false);
|
||||||
|
$table->string('currency_format')->default(config('app.localization.default_currency_format'))->nullable(false);
|
||||||
|
$table->string('date_format')->default(config('app.localization.default_date_format'))->nullable(false);
|
||||||
|
$table->string('interval_format')->default(config('app.localization.default_interval_format'))->nullable(false);
|
||||||
|
$table->string('time_format')->default(config('app.localization.default_time_format'))->nullable(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('organizations', function (Blueprint $table): void {
|
||||||
|
$table->string('number_format')->default(null)->nullable(false)->change();
|
||||||
|
$table->string('currency_format')->default(null)->nullable(false)->change();
|
||||||
|
$table->string('date_format')->default(null)->nullable(false)->change();
|
||||||
|
$table->string('interval_format')->default(null)->nullable(false)->change();
|
||||||
|
$table->string('time_format')->default(null)->nullable(false)->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('organizations', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('number_format');
|
||||||
|
$table->dropColumn('currency_format');
|
||||||
|
$table->dropColumn('date_format');
|
||||||
|
$table->dropColumn('interval_format');
|
||||||
|
$table->dropColumn('time_format');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user