Compare commits

..

1 Commits

Author SHA1 Message Date
Gregor Vostrak
6b84ba67cd add tanstack table, add clients table 2024-09-27 15:05:44 +02:00
215 changed files with 1473 additions and 5779 deletions

View File

@@ -1,6 +1,4 @@
APP_NAME=solidtime APP_NAME=solidtime
APP_VERSION=0.0.0
APP_BUILD=0
VITE_APP_NAME=solidtime VITE_APP_NAME=solidtime
APP_ENV=production APP_ENV=production
APP_DEBUG=false APP_DEBUG=false

View File

@@ -20,55 +20,15 @@ jobs:
steps: steps:
- name: "Check out code" - name: "Check out code"
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
- name: "Get build"
id: build
run: echo "build=$(git rev-parse --short=8 HEAD)" >> "$GITHUB_OUTPUT"
- name: "Get Previous tag (normal push)"
id: previoustag
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
prefix: "v"
- name: "Get version"
id: version
run: |
if ${{ !startsWith(github.ref, 'refs/tags/v') }}; then
if ${{ startsWith(steps.previoustag.outputs.tag, 'v') }}; then
version=$(echo "${{ steps.previoustag.outputs.tag }}" | cut -c 2-)
echo "app_version=${version}" >> "$GITHUB_OUTPUT"
else
echo "ERROR: No previous tag found";
exit 1;
fi
else
version=$(echo "${{ github.ref }}" | cut -c 12-)
echo "app_version=${version}" >> "$GITHUB_OUTPUT"
fi
- name: "Copy .env template for production"
run: |
cp .env.production .env
rm .env.production .env.ci .env.example
- name: "Add version to .env"
run: sed -i 's/APP_VERSION=0.0.0/APP_VERSION=${{ steps.version.outputs.app_version }}/g' .env
- name: "Add build to .env"
run: sed -i 's/APP_BUILD=0/APP_BUILD=${{ steps.build.outputs.build }}/g' .env
- name: "Output .env"
run: cat .env
- name: "Use Node.js" - name: "Use Node.js"
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20.x' node-version: '20.x'
- name: "Copy .env template for production"
run: cp .env.production .env && cat .env
- name: "Checkout billing extension" - name: "Checkout billing extension"
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:

View File

@@ -0,0 +1,90 @@
on:
push:
tags:
- '*'
pull_request:
paths:
- '.github/workflows/build-public.yml'
- 'docker/prod/**'
workflow_dispatch:
name: Build - Public (Release)
jobs:
build:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
attestations: write
id-token: write
timeout-minutes: 90
steps:
- name: "Check out code"
uses: actions/checkout@v4
- name: "Copy .env template for production"
run: cp .env.production .env
- name: "Install dependencies"
uses: php-actions/composer@v6
if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit
with:
command: install
only_args: --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
php_version: 8.3
- name: "Use Node.js"
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: "Install npm dependencies"
run: npm ci
- name: "Build"
run: npm run build
- name: "Login to GitHub Container Registry"
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: "Login to GitHub Container Registry"
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: "Docker meta"
id: "meta"
uses: docker/metadata-action@v5
with:
images: |
solidtime/solidtime
ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: "Set up QEMU"
uses: docker/setup-qemu-action@v3
- name: "Set up Docker Buildx"
uses: docker/setup-buildx-action@v3
- name: "Build and push"
uses: docker/build-push-action@v6
with:
context: .
file: docker/prod/Dockerfile
build-args: |
DOCKER_FILES_BASE_PATH=docker/prod/
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -3,8 +3,6 @@ on:
branches: branches:
- main - main
- develop - develop
tags:
- '*'
pull_request: pull_request:
paths: paths:
- '.github/workflows/build-public.yml' - '.github/workflows/build-public.yml'
@@ -25,49 +23,9 @@ jobs:
steps: steps:
- name: "Check out code" - name: "Check out code"
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
- name: "Get build"
id: build
run: echo "build=$(git rev-parse --short=8 HEAD)" >> "$GITHUB_OUTPUT"
- name: "Get Previous tag (normal push)"
id: previoustag
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
prefix: "v"
- name: "Get version"
id: version
run: |
if ${{ !startsWith(github.ref, 'refs/tags/v') }}; then
if ${{ startsWith(steps.previoustag.outputs.tag, 'v') }}; then
version=$(echo "${{ steps.previoustag.outputs.tag }}" | cut -c 2-)
echo "app_version=${version}" >> "$GITHUB_OUTPUT"
else
echo "ERROR: No previous tag found";
exit 1;
fi
else
version=$(echo "${{ github.ref }}" | cut -c 12-)
echo "app_version=${version}" >> "$GITHUB_OUTPUT"
fi
- name: "Copy .env template for production" - name: "Copy .env template for production"
run: | run: cp .env.production .env
cp .env.production .env
rm .env.production .env.ci .env.example
- name: "Add version to .env"
run: sed -i 's/APP_VERSION=0.0.0/APP_VERSION=${{ steps.version.outputs.app_version }}/g' .env
- name: "Add build to .env"
run: sed -i 's/APP_BUILD=0/APP_BUILD=${{ steps.build.outputs.build }}/g' .env
- name: "Output .env"
run: cat .env
- name: "Install dependencies" - name: "Install dependencies"
uses: php-actions/composer@v6 uses: php-actions/composer@v6

View File

@@ -45,7 +45,7 @@ class CreateNewUser implements CreatesNewUsers
'string', 'string',
'email', 'email',
'max:255', 'max:255',
UniqueEloquent::make(User::class, 'email', function (Builder $builder): Builder { new UniqueEloquent(User::class, 'email', function (Builder $builder): Builder {
/** @var Builder<User> $builder */ /** @var Builder<User> $builder */
return $builder->where('is_placeholder', '=', false); return $builder->where('is_placeholder', '=', false);
}), }),
@@ -82,7 +82,7 @@ class CreateNewUser implements CreatesNewUsers
} }
$user = null; $user = null;
$organization = null; $organization = null;
DB::transaction(function () use (&$user, &$organization, $input, $timezone, $startOfWeek, $currency): void { DB::transaction(function () use (&$user, &$organization, $input, $timezone, $startOfWeek, $currency) {
$user = User::create([ $user = User::create([
'name' => $input['name'], 'name' => $input['name'],
'email' => $input['email'], 'email' => $input['email'],

View File

@@ -35,7 +35,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
'required', 'required',
'email', 'email',
'max:255', 'max:255',
UniqueEloquent::make(User::class, 'email')->ignore($user->id)->query(function (Builder $query) { (new UniqueEloquent(User::class, 'email'))->ignore($user->id)->query(function (Builder $query) {
/** @var Builder<User> $query */ /** @var Builder<User> $query */
return $query->where('is_placeholder', '=', false); return $query->where('is_placeholder', '=', false);
}), }),

View File

@@ -38,7 +38,7 @@ class AddOrganizationMember implements AddsTeamMembers
AddingTeamMember::dispatch($organization, $newOrganizationMember); AddingTeamMember::dispatch($organization, $newOrganizationMember);
DB::transaction(function () use ($organization, $newOrganizationMember, $role): void { DB::transaction(function () use ($organization, $newOrganizationMember, $role) {
$organization->users()->attach( $organization->users()->attach(
$newOrganizationMember, ['role' => $role] $newOrganizationMember, ['role' => $role]
); );
@@ -71,10 +71,10 @@ class AddOrganizationMember implements AddsTeamMembers
'email' => [ 'email' => [
'required', 'required',
'email', 'email',
ExistsEloquent::make(User::class, 'email', function (Builder $builder) { (new ExistsEloquent(User::class, 'email', function (Builder $builder) {
/** @var Builder<User> $builder */ /** @var Builder<User> $builder */
return $builder->where('is_placeholder', '=', false); return $builder->where('is_placeholder', '=', false);
})->withMessage(__('We were unable to find a registered user with this email address.')), }))->withMessage(__('We were unable to find a registered user with this email address.')),
], ],
'role' => [ 'role' => [
'required', 'required',
@@ -93,7 +93,7 @@ class AddOrganizationMember implements AddsTeamMembers
*/ */
protected function ensureUserIsNotAlreadyOnTeam(Organization $team, string $email): Closure protected function ensureUserIsNotAlreadyOnTeam(Organization $team, string $email): Closure
{ {
return function ($validator) use ($team, $email): void { return function ($validator) use ($team, $email) {
$validator->errors()->addIf( $validator->errors()->addIf(
$team->hasRealUserWithEmail($email), $team->hasRealUserWithEmail($email),
'email', 'email',

View File

@@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\SelfHost;
use App\Service\ApiService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
class SelfHostCheckForUpdateCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'self-host:check-for-update';
/**
* The console command description.
*
* @var string
*/
protected $description = '';
/**
* Execute the console command.
*/
public function handle(): int
{
$apiService = app(ApiService::class);
$latestVersion = $apiService->checkForUpdate();
if ($latestVersion === null) {
$this->error('Failed to check for update, check the logs for more information.');
return self::FAILURE;
}
// Note: Cache for 13 hours, because the command runs twice daily (every 12 hours).
Cache::put('latest_version', $latestVersion, 60 * 60 * 12);
return self::SUCCESS;
}
}

View File

@@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\SelfHost;
use App\Service\ApiService;
use Illuminate\Console\Command;
class SelfHostTelemetryCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'self-host:telemetry';
/**
* The console command description.
*
* @var string
*/
protected $description = '';
/**
* Execute the console command.
*/
public function handle(): int
{
$apiService = app(ApiService::class);
$success = $apiService->telemetry();
if (! $success) {
$this->error('Failed to send telemetry data, check the logs for more information.');
return self::FAILURE;
}
return self::SUCCESS;
}
}

View File

@@ -54,7 +54,7 @@ class TimeEntrySendStillRunningMailsCommand extends Command
$query->where('is_placeholder', '=', false); $query->where('is_placeholder', '=', false);
}) })
->orderBy('created_at', 'asc') ->orderBy('created_at', 'asc')
->chunk(500, function (Collection $timeEntries) use ($dryRun, &$sentMails): void { ->chunk(500, function (Collection $timeEntries) use ($dryRun, &$sentMails) {
/** @var Collection<int, TimeEntry> $timeEntries */ /** @var Collection<int, TimeEntry> $timeEntries */
foreach ($timeEntries as $timeEntry) { foreach ($timeEntries as $timeEntry) {
$user = $timeEntry->user; $user = $timeEntry->user;

View File

@@ -17,14 +17,6 @@ class Kernel extends ConsoleKernel
$schedule->command('time-entry:send-still-running-mails') $schedule->command('time-entry:send-still-running-mails')
->when(fn (): bool => config('scheduling.tasks.time_entry_send_still_running_mails')) ->when(fn (): bool => config('scheduling.tasks.time_entry_send_still_running_mails'))
->everyTenMinutes(); ->everyTenMinutes();
$schedule->command('self-host:check-for-update')
->when(fn (): bool => config('scheduling.tasks.self_hosting_check_for_update'))
->twiceDaily();
$schedule->command('self-host:telemetry')
->when(fn (): bool => config('scheduling.tasks.self_hosting_telemetry'))
->twiceDaily();
} }
/** /**

View File

@@ -27,7 +27,7 @@ class Handler extends ExceptionHandler
*/ */
public function register(): void public function register(): void
{ {
$this->reportable(function (Throwable $e): void { $this->reportable(function (Throwable $e) {
// //
}); });
} }

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace App\Extensions\Scramble; namespace App\Extensions\Scramble;
use App\Http\Resources\PaginatedResourceCollection; use App\Http\Resources\PaginatedResourceCollection;
use App\Http\Resources\V1\TimeEntry\TimeEntryCollection;
use Dedoc\Scramble\Extensions\TypeToSchemaExtension; use Dedoc\Scramble\Extensions\TypeToSchemaExtension;
use Dedoc\Scramble\Support\Generator\Response; use Dedoc\Scramble\Support\Generator\Response;
use Dedoc\Scramble\Support\Generator\Schema; use Dedoc\Scramble\Support\Generator\Schema;
@@ -45,49 +44,39 @@ class PaginatedResourceCollectionTypeToSchema extends TypeToSchemaExtension
return null; return null;
} }
$newType = new OpenApiObjectType; $type = new OpenApiObjectType;
$newType->addProperty('data', (new ArrayType)->setItems($collectingType)); $type->addProperty('data', (new ArrayType)->setItems($collectingType));
if ($type instanceof ObjectType && $type->isInstanceOf(TimeEntryCollection::class)) { $type->addProperty(
$newType->addProperty( 'links',
'meta', (new OpenApiObjectType)
(new OpenApiObjectType) ->addProperty('first', (new StringType)->nullable(true))
->addProperty('total', (new IntegerType)->setDescription('Total number of items being paginated.')) ->addProperty('last', (new StringType)->nullable(true))
->setRequired(['total']) ->addProperty('prev', (new StringType)->nullable(true))
); ->addProperty('next', (new StringType)->nullable(true))
$newType->setRequired(['data', 'meta']); ->setRequired(['first', 'last', 'prev', 'next'])
} else { );
$newType->addProperty( $type->addProperty(
'links', 'meta',
(new OpenApiObjectType) (new OpenApiObjectType)
->addProperty('first', (new StringType)->nullable(true)) ->addProperty('current_page', new IntegerType)
->addProperty('last', (new StringType)->nullable(true)) ->addProperty('from', (new IntegerType)->nullable(true))
->addProperty('prev', (new StringType)->nullable(true)) ->addProperty('last_page', new IntegerType)
->addProperty('next', (new StringType)->nullable(true)) ->addProperty('links', (new ArrayType)->setItems(
->setRequired(['first', 'last', 'prev', 'next']) (new OpenApiObjectType)
); ->addProperty('url', (new StringType)->nullable(true))
$newType->addProperty( ->addProperty('label', new StringType)
'meta', ->addProperty('active', new BooleanType)
(new OpenApiObjectType) ->setRequired(['url', 'label', 'active'])
->addProperty('current_page', new IntegerType) )->setDescription('Generated paginator links.'))
->addProperty('from', (new IntegerType)->nullable(true)) ->addProperty('path', (new StringType)->nullable(true)->setDescription('Base path for paginator generated URLs.'))
->addProperty('last_page', new IntegerType) ->addProperty('per_page', (new IntegerType)->setDescription('Number of items shown per page.'))
->addProperty('links', (new ArrayType)->setItems( ->addProperty('to', (new IntegerType)->nullable(true)->setDescription('Number of the last item in the slice.'))
(new OpenApiObjectType) ->addProperty('total', (new IntegerType)->setDescription('Total number of items being paginated.'))
->addProperty('url', (new StringType)->nullable(true)) ->setRequired(['current_page', 'from', 'last_page', 'links', 'path', 'per_page', 'to', 'total'])
->addProperty('label', new StringType) );
->addProperty('active', new BooleanType) $type->setRequired(['data', 'links', 'meta']);
->setRequired(['url', 'label', 'active'])
)->setDescription('Generated paginator links.'))
->addProperty('path', (new StringType)->nullable(true)->setDescription('Base path for paginator generated URLs.'))
->addProperty('per_page', (new IntegerType)->setDescription('Number of items shown per page.'))
->addProperty('to', (new IntegerType)->nullable(true)->setDescription('Number of the last item in the slice.'))
->addProperty('total', (new IntegerType)->setDescription('Total number of items being paginated.'))
->setRequired(['current_page', 'from', 'last_page', 'links', 'path', 'per_page', 'to', 'total'])
);
$newType->setRequired(['data', 'links', 'meta']);
}
return $newType; return $type;
} }
/** /**

View File

@@ -70,7 +70,6 @@ class OrganizationResource extends Resource
'nullable', 'nullable',
'integer', 'integer',
'gt:0', 'gt:0',
'max:2147483647',
]) ])
->numeric(), ->numeric(),
Forms\Components\DateTimePicker::make('created_at') Forms\Components\DateTimePicker::make('created_at')
@@ -123,7 +122,7 @@ class OrganizationResource extends Resource
->persistent() ->persistent()
->send(); ->send();
return response()->streamDownload(function () use ($file): void { return response()->streamDownload(function () use ($file) {
echo Storage::disk(config('filesystems.private'))->get($file); echo Storage::disk(config('filesystems.private'))->get($file);
}, 'export.zip'); }, 'export.zip');
} catch (\Exception $exception) { } catch (\Exception $exception) {
@@ -138,7 +137,7 @@ class OrganizationResource extends Resource
}), }),
Action::make('Import') Action::make('Import')
->icon('heroicon-o-inbox-arrow-down') ->icon('heroicon-o-inbox-arrow-down')
->action(function (Organization $record, array $data): void { ->action(function (Organization $record, array $data) {
try { try {
$file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']); $file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']);
if ($file === null) { if ($file === null) {

View File

@@ -29,7 +29,6 @@ class ProjectMemberResource extends Resource
'nullable', 'nullable',
'integer', 'integer',
'gt:0', 'gt:0',
'max:2147483647',
]) ])
->numeric(), ->numeric(),
Forms\Components\Select::make('user_id') Forms\Components\Select::make('user_id')

View File

@@ -45,7 +45,6 @@ class ProjectResource extends Resource
'nullable', 'nullable',
'integer', 'integer',
'gt:0', 'gt:0',
'max:2147483647',
]) ])
->numeric(), ->numeric(),
Forms\Components\Select::make('organization_id') Forms\Components\Select::make('organization_id')

View File

@@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use Filament\Widgets\Widget;
use Illuminate\Support\Facades\Cache;
class ServerOverview extends Widget
{
protected static string $view = 'filament.widgets.server-overview';
/**
* @return array<string, mixed>
*/
protected function getViewData(): array
{
/** @var string|null $currentVersion */
$currentVersion = config('app.version');
/** @var string|null $build */
$build = config('app.build');
$latestVersion = Cache::get('latest_version', null);
$needsUpdate = false;
if ($latestVersion !== null && $currentVersion !== null && version_compare($latestVersion, $currentVersion) > 0) {
$needsUpdate = true;
}
return [
'version' => $currentVersion,
'build' => $build,
'environment' => config('app.env'),
'currentVersion' => $latestVersion,
'needsUpdate' => $needsUpdate,
];
}
}

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace App\Http\Controllers\Api\V1; namespace App\Http\Controllers\Api\V1;
use App\Models\Organization; use App\Models\Organization;
use App\Service\BillingContract;
use App\Service\PermissionStore; use App\Service\PermissionStore;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
@@ -44,9 +43,4 @@ class Controller extends \App\Http\Controllers\Controller
{ {
return $this->permissionStore->has($organization, $permission); return $this->permissionStore->has($organization, $permission);
} }
protected function canAccessPremiumFeatures(Organization $organization): bool
{
return app(BillingContract::class)->hasSubscription($organization) || app(BillingContract::class)->hasTrial($organization);
}
} }

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Http\Controllers\Api\V1; namespace App\Http\Controllers\Api\V1;
use App\Enums\Role;
use App\Http\Requests\V1\Organization\OrganizationUpdateRequest; use App\Http\Requests\V1\Organization\OrganizationUpdateRequest;
use App\Http\Resources\V1\Organization\OrganizationResource; use App\Http\Resources\V1\Organization\OrganizationResource;
use App\Models\Organization; use App\Models\Organization;
@@ -24,9 +23,7 @@ class OrganizationController extends Controller
{ {
$this->checkPermission($organization, 'organizations:view'); $this->checkPermission($organization, 'organizations:view');
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates; return new OrganizationResource($organization);
return new OrganizationResource($organization, $showBillableRate);
} }
/** /**
@@ -42,9 +39,6 @@ class OrganizationController extends Controller
$organization->name = $request->input('name'); $organization->name = $request->input('name');
$oldBillableRate = $organization->billable_rate; $oldBillableRate = $organization->billable_rate;
if ($request->has('employees_can_see_billable_rates')) {
$organization->employees_can_see_billable_rates = $request->validated('employees_can_see_billable_rates');
}
$organization->billable_rate = $request->getBillableRate(); $organization->billable_rate = $request->getBillableRate();
$organization->save(); $organization->save();
@@ -52,6 +46,6 @@ class OrganizationController extends Controller
$billableRateService->updateTimeEntriesBillableRateForOrganization($organization); $billableRateService->updateTimeEntriesBillableRateForOrganization($organization);
} }
return new OrganizationResource($organization, true); return new OrganizationResource($organization);
} }
} }

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Http\Controllers\Api\V1; namespace App\Http\Controllers\Api\V1;
use App\Enums\Role;
use App\Exceptions\Api\EntityStillInUseApiException; use App\Exceptions\Api\EntityStillInUseApiException;
use App\Http\Requests\V1\Project\ProjectIndexRequest; use App\Http\Requests\V1\Project\ProjectIndexRequest;
use App\Http\Requests\V1\Project\ProjectStoreRequest; use App\Http\Requests\V1\Project\ProjectStoreRequest;
@@ -14,7 +13,6 @@ use App\Http\Resources\V1\Project\ProjectResource;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectMember; use App\Models\ProjectMember;
use App\Models\TimeEntry;
use App\Service\BillableRateService; use App\Service\BillableRateService;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@@ -62,9 +60,7 @@ class ProjectController extends Controller
$projects = $projectsQuery->paginate(config('app.pagination_per_page_default')); $projects = $projectsQuery->paginate(config('app.pagination_per_page_default'));
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates; return new ProjectCollection($projects);
return new ProjectCollection($projects, $showBillableRate);
} }
/** /**
@@ -78,12 +74,9 @@ class ProjectController extends Controller
{ {
$this->checkPermission($organization, 'projects:view', $project); $this->checkPermission($organization, 'projects:view', $project);
// Note: There is currently no need to check if a user is a member of the project,
// since this is only relevant for users with the role "employee" and they can not access this endpoint.
$project->load('organization'); $project->load('organization');
return new ProjectResource($project, true); return new ProjectResource($project);
} }
/** /**
@@ -102,13 +95,10 @@ class ProjectController extends Controller
$project->is_billable = (bool) $request->input('is_billable'); $project->is_billable = (bool) $request->input('is_billable');
$project->billable_rate = $request->getBillableRate(); $project->billable_rate = $request->getBillableRate();
$project->client_id = $request->input('client_id'); $project->client_id = $request->input('client_id');
if ($this->canAccessPremiumFeatures($organization) && $request->has('estimated_time')) {
$project->estimated_time = $request->getEstimatedTime();
}
$project->organization()->associate($organization); $project->organization()->associate($organization);
$project->save(); $project->save();
return new ProjectResource($project, true); return new ProjectResource($project);
} }
/** /**
@@ -127,29 +117,16 @@ class ProjectController extends Controller
if ($request->has('is_archived')) { if ($request->has('is_archived')) {
$project->archived_at = $request->getIsArchived() ? Carbon::now() : null; $project->archived_at = $request->getIsArchived() ? Carbon::now() : null;
} }
if ($this->canAccessPremiumFeatures($organization) && $request->has('estimated_time')) {
$project->estimated_time = $request->getEstimatedTime();
}
$oldBillableRate = $project->billable_rate; $oldBillableRate = $project->billable_rate;
$clientIdChanged = false;
$project->billable_rate = $request->getBillableRate(); $project->billable_rate = $request->getBillableRate();
if ($project->client_id !== $request->input('client_id')) { $project->client_id = $request->input('client_id');
$project->client_id = $request->input('client_id');
$clientIdChanged = true;
}
$project->save(); $project->save();
if ($oldBillableRate !== $request->getBillableRate()) { if ($oldBillableRate !== $request->getBillableRate()) {
$billableRateService->updateTimeEntriesBillableRateForProject($project); $billableRateService->updateTimeEntriesBillableRateForProject($project);
} }
if ($clientIdChanged) {
TimeEntry::query()
->whereBelongsTo($organization, 'organization')
->whereBelongsTo($project, 'project')
->update(['client_id' => $project->client_id]);
}
return new ProjectResource($project, true); return new ProjectResource($project);
} }
/** /**
@@ -170,8 +147,8 @@ class ProjectController extends Controller
throw new EntityStillInUseApiException('project', 'time_entry'); throw new EntityStillInUseApiException('project', 'time_entry');
} }
DB::transaction(function () use (&$project): void { DB::transaction(function () use (&$project) {
$project->members->each(function (ProjectMember $member): void { $project->members->each(function (ProjectMember $member) {
$member->delete(); $member->delete();
}); });

View File

@@ -79,9 +79,6 @@ class TaskController extends Controller
$task = new Task; $task = new Task;
$task->name = $request->input('name'); $task->name = $request->input('name');
$task->project_id = $request->input('project_id'); $task->project_id = $request->input('project_id');
if ($this->canAccessPremiumFeatures($organization) && $request->has('estimated_time')) {
$task->estimated_time = $request->getEstimatedTime();
}
$task->organization()->associate($organization); $task->organization()->associate($organization);
$task->save(); $task->save();
@@ -99,9 +96,6 @@ class TaskController extends Controller
{ {
$this->checkPermission($organization, 'tasks:update', $task); $this->checkPermission($organization, 'tasks:update', $task);
$task->name = $request->input('name'); $task->name = $request->input('name');
if ($this->canAccessPremiumFeatures($organization) && $request->has('estimated_time')) {
$task->estimated_time = $request->getEstimatedTime();
}
if ($request->has('is_done')) { if ($request->has('is_done')) {
$task->done_at = $request->getIsDone() ? Carbon::now() : null; $task->done_at = $request->getIsDone() ? Carbon::now() : null;
} }

View File

@@ -7,19 +7,15 @@ namespace App\Http\Controllers\Api\V1;
use App\Exceptions\Api\TimeEntryCanNotBeRestartedApiException; use App\Exceptions\Api\TimeEntryCanNotBeRestartedApiException;
use App\Exceptions\Api\TimeEntryStillRunningApiException; use App\Exceptions\Api\TimeEntryStillRunningApiException;
use App\Http\Requests\V1\TimeEntry\TimeEntryAggregateRequest; use App\Http\Requests\V1\TimeEntry\TimeEntryAggregateRequest;
use App\Http\Requests\V1\TimeEntry\TimeEntryDestroyMultipleRequest;
use App\Http\Requests\V1\TimeEntry\TimeEntryIndexRequest; use App\Http\Requests\V1\TimeEntry\TimeEntryIndexRequest;
use App\Http\Requests\V1\TimeEntry\TimeEntryStoreRequest; use App\Http\Requests\V1\TimeEntry\TimeEntryStoreRequest;
use App\Http\Requests\V1\TimeEntry\TimeEntryUpdateMultipleRequest; use App\Http\Requests\V1\TimeEntry\TimeEntryUpdateMultipleRequest;
use App\Http\Requests\V1\TimeEntry\TimeEntryUpdateRequest; use App\Http\Requests\V1\TimeEntry\TimeEntryUpdateRequest;
use App\Http\Resources\V1\TimeEntry\TimeEntryCollection; use App\Http\Resources\V1\TimeEntry\TimeEntryCollection;
use App\Http\Resources\V1\TimeEntry\TimeEntryResource; use App\Http\Resources\V1\TimeEntry\TimeEntryResource;
use App\Jobs\RecalculateSpentTimeForProject;
use App\Jobs\RecalculateSpentTimeForTask;
use App\Models\Member; use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\Task;
use App\Models\TimeEntry; use App\Models\TimeEntry;
use App\Service\TimeEntryAggregationService; use App\Service\TimeEntryAggregationService;
use App\Service\TimeEntryFilter; use App\Service\TimeEntryFilter;
@@ -47,8 +43,6 @@ class TimeEntryController extends Controller
* If you only need time entries for a specific user, you can filter by `user_id`. * If you only need time entries for a specific user, you can filter by `user_id`.
* Users with the permission `time-entries:view:own` can only use this endpoint with their own user ID in the user_id filter. * Users with the permission `time-entries:view:own` can only use this endpoint with their own user ID in the user_id filter.
* *
* @return TimeEntryCollection<TimeEntryResource>
*
* @throws AuthorizationException * @throws AuthorizationException
* *
* @operationId getTimeEntries * @operationId getTimeEntries
@@ -79,14 +73,11 @@ class TimeEntryController extends Controller
$filter->addClientIdsFilter($request->input('client_ids')); $filter->addClientIdsFilter($request->input('client_ids'));
$filter->addBillableFilter($request->input('billable')); $filter->addBillableFilter($request->input('billable'));
$totalCount = $timeEntriesQuery->count(); $limit = $request->has('limit') ? (int) $request->input('limit', 100) : 100;
$limit = $request->getLimit();
if ($limit > 1000) { if ($limit > 1000) {
$limit = 1000; $limit = 1000;
} }
$timeEntriesQuery->limit($limit); $timeEntriesQuery->limit($limit);
$timeEntriesQuery->skip($request->getOffset());
$timeEntries = $timeEntriesQuery->get(); $timeEntries = $timeEntriesQuery->get();
@@ -120,12 +111,7 @@ class TimeEntryController extends Controller
} }
} }
return (new TimeEntryCollection($timeEntries)) return new TimeEntryCollection($timeEntries);
->additional([
'meta' => [
'total' => $totalCount,
],
]);
} }
/** /**
@@ -229,16 +215,7 @@ class TimeEntryController extends Controller
throw new TimeEntryStillRunningApiException; throw new TimeEntryStillRunningApiException;
} }
$project = $request->input('project_id') !== null ? Project::findOrFail((string) $request->input('project_id')) : null; $client = $request->input('project_id') !== null ? Project::findOrFail((string) $request->input('project_id'))->client : null;
$client = $project?->client;
$task = $request->input('task_id') !== null ? $project->tasks()->findOrFail((string) $request->input('task_id')) : null;
if ($project !== null) {
RecalculateSpentTimeForProject::dispatch($project);
}
if ($task !== null) {
RecalculateSpentTimeForTask::dispatch($task);
}
$timeEntry = new TimeEntry; $timeEntry = new TimeEntry;
$timeEntry->fill($request->validated()); $timeEntry->fill($request->validated());
@@ -273,38 +250,16 @@ class TimeEntryController extends Controller
throw new TimeEntryCanNotBeRestartedApiException; throw new TimeEntryCanNotBeRestartedApiException;
} }
$oldProject = $timeEntry->project;
$oldTask = $timeEntry->task;
$project = null;
if ($request->has('project_id')) { if ($request->has('project_id')) {
$project = $request->input('project_id') !== null ? Project::findOrFail((string) $request->input('project_id')) : null; $client = $request->input('project_id') !== null ? Project::findOrFail((string) $request->input('project_id'))->client : null;
$client = $project?->client;
$timeEntry->client()->associate($client); $timeEntry->client()->associate($client);
} }
$task = null;
if ($request->has('task_id')) {
$task = $request->input('task_id') !== null ? Task::findOrFail((string) $request->input('task_id')) : null;
}
$timeEntry->fill($request->validated()); $timeEntry->fill($request->validated());
$timeEntry->description = $request->input('description', $timeEntry->description) ?? ''; $timeEntry->description = $request->input('description', $timeEntry->description) ?? '';
$timeEntry->setComputedAttributeValue('billable_rate'); $timeEntry->setComputedAttributeValue('billable_rate');
$timeEntry->save(); $timeEntry->save();
if ($oldProject !== null) {
RecalculateSpentTimeForProject::dispatch($oldProject);
}
if ($oldTask !== null) {
RecalculateSpentTimeForTask::dispatch($oldTask);
}
if ($project !== null && ($oldProject === null || $project->isNot($oldProject))) {
RecalculateSpentTimeForProject::dispatch($project);
}
if ($task !== null && ($oldTask === null || $task->isNot($oldTask))) {
RecalculateSpentTimeForTask::dispatch($task);
}
return new TimeEntryResource($timeEntry); return new TimeEntryResource($timeEntry);
} }
@@ -324,10 +279,6 @@ class TimeEntryController extends Controller
$timeEntries = TimeEntry::query() $timeEntries = TimeEntry::query()
->whereBelongsTo($organization, 'organization') ->whereBelongsTo($organization, 'organization')
->with([
'project',
'task',
])
->whereIn('id', $ids) ->whereIn('id', $ids)
->get(); ->get();
@@ -337,20 +288,13 @@ class TimeEntryController extends Controller
throw new AuthorizationException; throw new AuthorizationException;
} }
$project = null;
$client = null; $client = null;
$overwriteClient = false; $overwriteClient = false;
if ($request->has('changes.project_id')) { if ($request->has('changes.project_id')) {
$project = $request->input('changes.project_id') !== null ? Project::findOrFail((string) $request->input('changes.project_id')) : null; $client = $request->input('changes.project_id') !== null ? Project::findOrFail((string) $request->input('changes.project_id'))->client : null;
$client = $project?->client;
$overwriteClient = true; $overwriteClient = true;
} }
$task = null;
if ($request->has('changes.task_id')) {
$task = $request->input('changes.task_id') !== null ? Task::findOrFail((string) $request->input('changes.task_id')) : null;
}
$success = new Collection; $success = new Collection;
$error = new Collection; $error = new Collection;
@@ -369,32 +313,12 @@ class TimeEntryController extends Controller
continue; continue;
} }
$oldProject = $timeEntry->project;
$oldTask = $timeEntry->task;
$timeEntry->fill($changes); $timeEntry->fill($changes);
// If project is changed, but task is not, we remove the old task from the time entry
if ($oldProject !== null && $project !== null && $oldProject->isNot($project) && $task === null) {
$timeEntry->task()->disassociate();
}
if ($overwriteClient) { if ($overwriteClient) {
$timeEntry->client()->associate($client); $timeEntry->client()->associate($client);
} }
$timeEntry->setComputedAttributeValue('billable_rate'); $timeEntry->setComputedAttributeValue('billable_rate');
$timeEntry->save(); $timeEntry->save();
if ($oldTask !== null) {
RecalculateSpentTimeForTask::dispatch($oldTask);
}
if ($oldProject !== null) {
RecalculateSpentTimeForProject::dispatch($oldProject);
}
if ($project !== null && ($oldProject === null || $project->isNot($oldProject))) {
RecalculateSpentTimeForProject::dispatch($project);
}
if ($task !== null && ($oldTask === null || $task->isNot($oldTask))) {
RecalculateSpentTimeForTask::dispatch($task);
}
$success->push($id); $success->push($id);
} }
@@ -419,81 +343,9 @@ class TimeEntryController extends Controller
$this->checkPermission($organization, 'time-entries:delete:all', $timeEntry); $this->checkPermission($organization, 'time-entries:delete:all', $timeEntry);
} }
$project = $timeEntry->project;
$task = $timeEntry->task;
$timeEntry->delete(); $timeEntry->delete();
if ($project !== null) {
RecalculateSpentTimeForProject::dispatch($project);
}
if ($task !== null) {
RecalculateSpentTimeForTask::dispatch($task);
}
return response() return response()
->json(null, 204); ->json(null, 204);
} }
/**
* Delete multiple time entries
*
* @throws AuthorizationException
*
* @operationId deleteTimeEntries
*/
public function destroyMultiple(Organization $organization, TimeEntryDestroyMultipleRequest $request): JsonResponse
{
$this->checkAnyPermission($organization, ['time-entries:delete:all', 'time-entries:delete:own']);
$canDeleteAll = $this->hasPermission($organization, 'time-entries:delete:all');
$ids = $request->validated('ids');
$timeEntries = TimeEntry::query()
->whereBelongsTo($organization, 'organization')
->with([
'project',
'task',
])
->whereIn('id', $ids)
->get();
$success = new Collection;
$error = new Collection;
foreach ($ids as $id) {
/** @var TimeEntry|null $timeEntry */
$timeEntry = $timeEntries->firstWhere('id', $id);
if ($timeEntry === null) {
// Note: ID wrong or time entry in different organization
$error->push($id);
continue;
}
if (! $canDeleteAll && $timeEntry->user_id !== Auth::id()) {
$error->push($id);
continue;
}
$project = $timeEntry->project;
$task = $timeEntry->task;
$timeEntry->delete();
if ($project !== null) {
RecalculateSpentTimeForProject::dispatch($project);
}
if ($task !== null) {
RecalculateSpentTimeForTask::dispatch($task);
}
$success->push($id);
}
return response()->json([
'success' => $success->toArray(),
'error' => $error->toArray(),
]);
}
} }

View File

@@ -20,7 +20,6 @@ class ClientIndexRequest extends FormRequest
'page' => [ 'page' => [
'integer', 'integer',
'min:1', 'min:1',
'max:2147483647',
], ],
'archived' => [ 'archived' => [
'string', 'string',

View File

@@ -29,10 +29,10 @@ class ClientStoreRequest extends FormRequest
'string', 'string',
'min:1', 'min:1',
'max:255', 'max:255',
UniqueEloquent::make(Client::class, 'name', function (Builder $builder): Builder { (new UniqueEloquent(Client::class, 'name', function (Builder $builder): Builder {
/** @var Builder<Client> $builder */ /** @var Builder<Client> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->withCustomTranslation('validation.client_name_already_exists'), }))->withCustomTranslation('validation.client_name_already_exists'),
], ],
]; ];
} }

View File

@@ -31,10 +31,10 @@ class ClientUpdateRequest extends FormRequest
'string', 'string',
'min:1', 'min:1',
'max:255', 'max:255',
UniqueEloquent::make(Client::class, 'name', function (Builder $builder): Builder { (new UniqueEloquent(Client::class, 'name', function (Builder $builder): Builder {
/** @var Builder<Client> $builder */ /** @var Builder<Client> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->ignore($this->client?->getKey())->withCustomTranslation('validation.client_name_already_exists'), }))->ignore($this->client?->getKey())->withCustomTranslation('validation.client_name_already_exists'),
], ],
'is_archived' => [ 'is_archived' => [
'boolean', 'boolean',

View File

@@ -29,10 +29,10 @@ class InvitationStoreRequest extends FormRequest
'email' => [ 'email' => [
'required', 'required',
'email', 'email',
UniqueEloquent::make(OrganizationInvitation::class, 'email', function (Builder $builder): Builder { (new UniqueEloquent(OrganizationInvitation::class, 'email', function (Builder $builder): Builder {
/** @var Builder<OrganizationInvitation> $builder */ /** @var Builder<OrganizationInvitation> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->withCustomTranslation('validation.invitation_already_exists'), }))->withCustomTranslation('validation.invitation_already_exists'),
], ],
'role' => [ 'role' => [
'required', 'required',

View File

@@ -31,7 +31,6 @@ class MemberUpdateRequest extends FormRequest
'nullable', 'nullable',
'integer', 'integer',
'min:0', 'min:0',
'max:2147483647',
], ],
]; ];
} }

View File

@@ -30,10 +30,6 @@ class OrganizationUpdateRequest extends FormRequest
'nullable', 'nullable',
'integer', 'integer',
'min:0', 'min:0',
'max:2147483647',
],
'employees_can_see_billable_rates' => [
'boolean',
], ],
]; ];
} }

View File

@@ -20,7 +20,6 @@ class ProjectIndexRequest extends FormRequest
'page' => [ 'page' => [
'integer', 'integer',
'min:1', 'min:1',
'max:2147483647',
], ],
'archived' => [ 'archived' => [
'string', 'string',

View File

@@ -32,10 +32,10 @@ class ProjectStoreRequest extends FormRequest
'string', 'string',
'min:1', 'min:1',
'max:255', 'max:255',
UniqueEloquent::make(Project::class, 'name', function (Builder $builder): Builder { (new UniqueEloquent(Project::class, 'name', function (Builder $builder): Builder {
/** @var Builder<Project> $builder */ /** @var Builder<Project> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->withCustomTranslation('validation.project_name_already_exists'), }))->withCustomTranslation('validation.project_name_already_exists'),
], ],
'color' => [ 'color' => [
'required', 'required',
@@ -51,22 +51,13 @@ class ProjectStoreRequest extends FormRequest
'nullable', 'nullable',
'integer', 'integer',
'min:0', 'min:0',
'max:2147483647',
], ],
// ID of the client
'client_id' => [ 'client_id' => [
'nullable', 'nullable',
ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder { new ExistsEloquent(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(), }),
],
// Estimated time in seconds
'estimated_time' => [
'nullable',
'integer',
'min:0',
'max:2147483647',
], ],
]; ];
} }
@@ -77,11 +68,4 @@ class ProjectStoreRequest extends FormRequest
return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null; return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null;
} }
public function getEstimatedTime(): ?int
{
$input = $this->input('estimated_time');
return $input !== null && $input !== 0 ? (int) $this->input('estimated_time') : null;
}
} }

View File

@@ -32,10 +32,10 @@ class ProjectUpdateRequest extends FormRequest
'required', 'required',
'string', 'string',
'max:255', 'max:255',
UniqueEloquent::make(Project::class, 'name', function (Builder $builder): Builder { (new UniqueEloquent(Project::class, 'name', function (Builder $builder): Builder {
/** @var Builder<Project> $builder */ /** @var Builder<Project> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->ignore($this->project?->getKey())->withCustomTranslation('validation.project_name_already_exists'), }))->ignore($this->project?->getKey())->withCustomTranslation('validation.project_name_already_exists'),
], ],
'color' => [ 'color' => [
'required', 'required',
@@ -52,23 +52,15 @@ class ProjectUpdateRequest extends FormRequest
], ],
'client_id' => [ 'client_id' => [
'nullable', 'nullable',
ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder { new ExistsEloquent(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(), }),
], ],
'billable_rate' => [ 'billable_rate' => [
'nullable', 'nullable',
'integer', 'integer',
'min:0', 'min:0',
'max:2147483647',
],
// Estimated time in seconds
'estimated_time' => [
'nullable',
'integer',
'min:0',
'max:2147483647',
], ],
]; ];
} }
@@ -86,11 +78,4 @@ class ProjectUpdateRequest extends FormRequest
return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null; return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null;
} }
public function getEstimatedTime(): ?int
{
$input = $this->input('estimated_time');
return $input !== null && $input !== 0 ? (int) $this->input('estimated_time') : null;
}
} }

View File

@@ -26,16 +26,16 @@ class ProjectMemberStoreRequest extends FormRequest
return [ return [
'member_id' => [ 'member_id' => [
'required', 'required',
ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
/** @var Builder<Member> $builder */ /** @var Builder<Member> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
'billable_rate' => [ 'billable_rate' => [
'nullable', 'nullable',
'integer', 'integer',
'min:0', 'min:0',
'max:2147483647',
], ],
]; ];
} }

View File

@@ -25,7 +25,6 @@ class ProjectMemberUpdateRequest extends FormRequest
'nullable', 'nullable',
'integer', 'integer',
'min:0', 'min:0',
'max:2147483647',
], ],
]; ];
} }

View File

@@ -29,10 +29,10 @@ class TagStoreRequest extends FormRequest
'string', 'string',
'min:1', 'min:1',
'max:255', 'max:255',
UniqueEloquent::make(Tag::class, 'name', function (Builder $builder): Builder { (new UniqueEloquent(Tag::class, 'name', function (Builder $builder): Builder {
/** @var Builder<Tag> $builder */ /** @var Builder<Tag> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->withCustomTranslation('validation.tag_name_already_exists'), }))->withCustomTranslation('validation.tag_name_already_exists'),
], ],
]; ];
} }

View File

@@ -30,10 +30,10 @@ class TagUpdateRequest extends FormRequest
'string', 'string',
'min:1', 'min:1',
'max:255', 'max:255',
UniqueEloquent::make(Tag::class, 'name', function (Builder $builder): Builder { (new UniqueEloquent(Tag::class, 'name', function (Builder $builder): Builder {
/** @var Builder<Tag> $builder */ /** @var Builder<Tag> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->ignore($this->tag?->getKey())->withCustomTranslation('validation.tag_name_already_exists'), }))->ignore($this->tag?->getKey())->withCustomTranslation('validation.tag_name_already_exists'),
], ],
]; ];
} }

View File

@@ -27,7 +27,8 @@ class TaskIndexRequest extends FormRequest
{ {
return [ return [
'project_id' => [ 'project_id' => [
ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Project::class, null, function (Builder $builder): Builder {
/** @var Builder<Project> $builder */ /** @var Builder<Project> $builder */
$builder = $builder->whereBelongsTo($this->organization, 'organization'); $builder = $builder->whereBelongsTo($this->organization, 'organization');
@@ -36,7 +37,7 @@ class TaskIndexRequest extends FormRequest
} }
return $builder; return $builder;
})->uuid(), }),
], ],
'done' => [ 'done' => [
'string', 'string',

View File

@@ -31,32 +31,18 @@ class TaskStoreRequest extends FormRequest
'string', 'string',
'min:1', 'min:1',
'max:255', 'max:255',
UniqueEloquent::make(Task::class, 'name', function (Builder $builder): Builder { (new UniqueEloquent(Task::class, 'name', function (Builder $builder): Builder {
/** @var Builder<Task> $builder */ /** @var Builder<Task> $builder */
return $builder->where('project_id', '=', $this->input('project_id')); return $builder->where('project_id', '=', $this->input('project_id'));
})->withCustomTranslation('validation.task_name_already_exists'), }))->withCustomTranslation('validation.task_name_already_exists'),
], ],
'project_id' => [ 'project_id' => [
'required', 'required',
ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { new ExistsEloquent(Project::class, null, function (Builder $builder): Builder {
/** @var Builder<Project> $builder */ /** @var Builder<Project> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
],
// Estimated time in seconds
'estimated_time' => [
'nullable',
'integer',
'min:0',
'max:2147483647',
], ],
]; ];
} }
public function getEstimatedTime(): ?int
{
$input = $this->input('estimated_time');
return $input !== null && $input !== 0 ? (int) $this->input('estimated_time') : null;
}
} }

View File

@@ -30,21 +30,14 @@ class TaskUpdateRequest extends FormRequest
'string', 'string',
'min:1', 'min:1',
'max:255', 'max:255',
UniqueEloquent::make(Task::class, 'name', function (Builder $builder): Builder { (new UniqueEloquent(Task::class, 'name', function (Builder $builder): Builder {
/** @var Builder<Task> $builder */ /** @var Builder<Task> $builder */
return $builder->where('project_id', '=', $this->task->project_id); return $builder->where('project_id', '=', $this->task->project_id);
})->ignore($this->task?->getKey())->withCustomTranslation('validation.task_name_already_exists'), }))->ignore($this->task?->getKey())->withCustomTranslation('validation.task_name_already_exists'),
], ],
'is_done' => [ 'is_done' => [
'boolean', 'boolean',
], ],
// Estimated time in seconds
'estimated_time' => [
'nullable',
'integer',
'min:0',
'max:2147483647',
],
]; ];
} }
@@ -54,11 +47,4 @@ class TaskUpdateRequest extends FormRequest
return $this->boolean('is_done'); return $this->boolean('is_done');
} }
public function getEstimatedTime(): ?int
{
$input = $this->input('estimated_time');
return $input !== null && $input !== 0 ? (int) $this->input('estimated_time') : null;
}
} }

View File

@@ -45,10 +45,11 @@ class TimeEntryAggregateRequest extends FormRequest
// Filter by member ID // Filter by member ID
'member_id' => [ 'member_id' => [
'string', 'string',
ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
/** @var Builder<Member> $builder */ /** @var Builder<Member> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
// Filter by multiple member IDs, member IDs are OR combined, but AND combined with the member_id parameter // Filter by multiple member IDs, member IDs are OR combined, but AND combined with the member_id parameter
'member_ids' => [ 'member_ids' => [
@@ -57,19 +58,21 @@ class TimeEntryAggregateRequest extends FormRequest
], ],
'member_ids.*' => [ 'member_ids.*' => [
'string', 'string',
ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
/** @var Builder<Member> $builder */ /** @var Builder<Member> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
// Filter by user ID // Filter by user ID
'user_id' => [ 'user_id' => [
'string', 'string',
ExistsEloquent::make(User::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(User::class, null, function (Builder $builder): Builder {
/** @var Builder<User> $builder */ /** @var Builder<User> $builder */
return $builder->belongsToOrganization($this->organization); return $builder->belongsToOrganization($this->organization);
})->uuid(), }),
], ],
// Filter by project IDs, project IDs are OR combined // Filter by project IDs, project IDs are OR combined
'project_ids' => [ 'project_ids' => [
@@ -78,10 +81,11 @@ class TimeEntryAggregateRequest extends FormRequest
], ],
'project_ids.*' => [ 'project_ids.*' => [
'string', 'string',
ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Project::class, null, function (Builder $builder): Builder {
/** @var Builder<Project> $builder */ /** @var Builder<Project> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
// Filter by client IDs, client IDs are OR combined // Filter by client IDs, client IDs are OR combined
'client_ids' => [ 'client_ids' => [
@@ -90,10 +94,11 @@ class TimeEntryAggregateRequest extends FormRequest
], ],
'client_ids.*' => [ 'client_ids.*' => [
'string', 'string',
ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(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(), }),
], ],
// Filter by tag IDs, tag IDs are AND combined // Filter by tag IDs, tag IDs are AND combined
'tag_ids' => [ 'tag_ids' => [
@@ -102,10 +107,11 @@ class TimeEntryAggregateRequest extends FormRequest
], ],
'tag_ids.*' => [ 'tag_ids.*' => [
'string', 'string',
ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Tag::class, null, function (Builder $builder): Builder {
/** @var Builder<Tag> $builder */ /** @var Builder<Tag> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
// Filter by task IDs, task IDs are OR combined // Filter by task IDs, task IDs are OR combined
'task_ids' => [ 'task_ids' => [
@@ -114,9 +120,10 @@ class TimeEntryAggregateRequest extends FormRequest
], ],
'task_ids.*' => [ 'task_ids.*' => [
'string', 'string',
ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Task::class, null, function (Builder $builder): Builder {
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
// Filter only time entries that have a start date after the given timestamp in UTC (example: 2021-01-01T00:00:00Z) // Filter only time entries that have a start date after the given timestamp in UTC (example: 2021-01-01T00:00:00Z)
'start' => [ 'start' => [

View File

@@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\V1\TimeEntry;
use App\Models\Organization;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
/**
* @property Organization $organization Organization from model binding
*/
class TimeEntryDestroyMultipleRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, array<string|ValidationRule>>
*/
public function rules(): array
{
return [
'ids' => [
'required',
'array',
],
'ids.*' => [
'string',
'uuid',
],
];
}
}

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Http\Requests\V1\TimeEntry; namespace App\Http\Requests\V1\TimeEntry;
use App\Models\Client;
use App\Models\Member; use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
@@ -31,10 +30,11 @@ class TimeEntryIndexRequest extends FormRequest
// Filter by member ID // Filter by member ID
'member_id' => [ 'member_id' => [
'string', 'string',
ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
/** @var Builder<Member> $builder */ /** @var Builder<Member> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
// Filter by multiple member IDs, member IDs are OR combined, but AND combined with the member_id parameter // Filter by multiple member IDs, member IDs are OR combined, but AND combined with the member_id parameter
'member_ids' => [ 'member_ids' => [
@@ -43,22 +43,11 @@ class TimeEntryIndexRequest extends FormRequest
], ],
'member_ids.*' => [ 'member_ids.*' => [
'string', 'string',
ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
/** @var Builder<Member> $builder */ /** @var Builder<Member> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
],
// Filter by client IDs, client IDs are OR combined
'client_ids' => [
'array',
'min:1',
],
'client_ids.*' => [
'string',
ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder {
/** @var Builder<Client> $builder */
return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(),
], ],
// Filter by project IDs, project IDs are OR combined // Filter by project IDs, project IDs are OR combined
'project_ids' => [ 'project_ids' => [
@@ -67,10 +56,11 @@ class TimeEntryIndexRequest extends FormRequest
], ],
'project_ids.*' => [ 'project_ids.*' => [
'string', 'string',
ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Project::class, null, function (Builder $builder): Builder {
/** @var Builder<Project> $builder */ /** @var Builder<Project> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
// Filter by tag IDs, tag IDs are AND combined // Filter by tag IDs, tag IDs are AND combined
'tag_ids' => [ 'tag_ids' => [
@@ -79,10 +69,11 @@ class TimeEntryIndexRequest extends FormRequest
], ],
'tag_ids.*' => [ 'tag_ids.*' => [
'string', 'string',
ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Tag::class, null, function (Builder $builder): Builder {
/** @var Builder<Tag> $builder */ /** @var Builder<Tag> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
// Filter by task IDs, task IDs are OR combined // Filter by task IDs, task IDs are OR combined
'task_ids' => [ 'task_ids' => [
@@ -91,10 +82,11 @@ class TimeEntryIndexRequest extends FormRequest
], ],
'task_ids.*' => [ 'task_ids.*' => [
'string', 'string',
ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Task::class, null, function (Builder $builder): Builder {
/** @var Builder<Task> $builder */ /** @var Builder<Task> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
// Filter only time entries that have a start date after the given timestamp in UTC (example: 2021-01-01T00:00:00Z) // Filter only time entries that have a start date after the given timestamp in UTC (example: 2021-01-01T00:00:00Z)
'start' => [ 'start' => [
@@ -125,12 +117,6 @@ class TimeEntryIndexRequest extends FormRequest
'min:1', 'min:1',
'max:500', 'max:500',
], ],
// Skip the first n time entries (default: 0)
'offset' => [
'integer',
'min:0',
'max:2147483647',
],
// Filter makes sure that only time entries of a whole date are returned // Filter makes sure that only time entries of a whole date are returned
'only_full_dates' => [ 'only_full_dates' => [
'string', 'string',
@@ -143,14 +129,4 @@ class TimeEntryIndexRequest extends FormRequest
{ {
return $this->input('only_full_dates', 'false') === 'true'; return $this->input('only_full_dates', 'false') === 'true';
} }
public function getLimit(): int
{
return $this->has('limit') ? (int) $this->validated('limit', 100) : 100;
}
public function getOffset(): int
{
return $this->has('offset') ? (int) $this->validated('offset', 0) : 0;
}
} }

View File

@@ -31,33 +31,36 @@ class TimeEntryStoreRequest extends FormRequest
'member_id' => [ 'member_id' => [
'required', 'required',
'string', 'string',
ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
/** @var Builder<Member> $builder */ /** @var Builder<Member> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
'project_id' => [ 'project_id' => [
'nullable', 'nullable',
'string', 'string',
'uuid',
'required_with:task_id', 'required_with:task_id',
ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { new ExistsEloquent(Project::class, null, function (Builder $builder): Builder {
/** @var Builder<Project> $builder */ /** @var Builder<Project> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
// ID of the task that the time entry should belong to // ID of the task that the time entry should belong to
'task_id' => [ 'task_id' => [
'nullable', 'nullable',
'string', 'string',
ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Task::class, null, function (Builder $builder): Builder {
/** @var Builder<Task> $builder */ /** @var Builder<Task> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { (new ExistsEloquent(Task::class, null, function (Builder $builder): Builder {
/** @var Builder<Task> $builder */ /** @var Builder<Task> $builder */
return $builder->whereBelongsTo($this->organization, 'organization') return $builder->whereBelongsTo($this->organization, 'organization')
->where('project_id', $this->input('project_id')); ->where('project_id', $this->input('project_id'));
})->uuid()->withMessage(__('validation.task_belongs_to_project')), }))->withMessage(__('validation.task_belongs_to_project')),
], ],
// Start of time entry (ISO 8601 format, UTC timezone) // Start of time entry (ISO 8601 format, UTC timezone)
'start' => [ 'start' => [
@@ -87,10 +90,12 @@ class TimeEntryStoreRequest extends FormRequest
'array', 'array',
], ],
'tags.*' => [ 'tags.*' => [
ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder { 'string',
'uuid',
new ExistsEloquent(Tag::class, null, function (Builder $builder): Builder {
/** @var Builder<Tag> $builder */ /** @var Builder<Tag> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
]; ];
} }

View File

@@ -42,34 +42,37 @@ class TimeEntryUpdateMultipleRequest extends FormRequest
// ID of the organization member that the time entry should belong to // ID of the organization member that the time entry should belong to
'changes.member_id' => [ 'changes.member_id' => [
'string', 'string',
ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
/** @var Builder<Member> $builder */ /** @var Builder<Member> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
// ID of the project that the time entry should belong to // ID of the project that the time entry should belong to
'changes.project_id' => [ 'changes.project_id' => [
'nullable', 'nullable',
'string', 'string',
'uuid',
'required_with:task_id', 'required_with:task_id',
ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { new ExistsEloquent(Project::class, null, function (Builder $builder): Builder {
/** @var Builder<Project> $builder */ /** @var Builder<Project> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
// ID of the task that the time entry should belong to // ID of the task that the time entry should belong to
'changes.task_id' => [ 'changes.task_id' => [
'nullable', 'nullable',
'string', 'string',
ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Task::class, null, function (Builder $builder): Builder {
/** @var Builder<Task> $builder */ /** @var Builder<Task> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { (new ExistsEloquent(Task::class, null, function (Builder $builder): Builder {
/** @var Builder<Task> $builder */ /** @var Builder<Task> $builder */
return $builder->whereBelongsTo($this->organization, 'organization') return $builder->whereBelongsTo($this->organization, 'organization')
->where('project_id', $this->input('changes.project_id')); ->where('project_id', $this->input('changes.project_id'));
})->uuid()->withMessage(__('validation.task_belongs_to_project')), }))->withMessage(__('validation.task_belongs_to_project')),
], ],
// Whether time entry is billable // Whether time entry is billable
'changes.billable' => [ 'changes.billable' => [
@@ -88,10 +91,11 @@ class TimeEntryUpdateMultipleRequest extends FormRequest
], ],
'changes.tags.*' => [ 'changes.tags.*' => [
'string', 'string',
ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Tag::class, null, function (Builder $builder): Builder {
/** @var Builder<Tag> $builder */ /** @var Builder<Tag> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
]; ];
} }

View File

@@ -30,34 +30,37 @@ class TimeEntryUpdateRequest extends FormRequest
// ID of the organization member that the time entry should belong to // ID of the organization member that the time entry should belong to
'member_id' => [ 'member_id' => [
'string', 'string',
ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
/** @var Builder<Member> $builder */ /** @var Builder<Member> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
// ID of the project that the time entry should belong to // ID of the project that the time entry should belong to
'project_id' => [ 'project_id' => [
'nullable', 'nullable',
'string', 'string',
'uuid',
'required_with:task_id', 'required_with:task_id',
ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { new ExistsEloquent(Project::class, null, function (Builder $builder): Builder {
/** @var Builder<Project> $builder */ /** @var Builder<Project> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
// ID of the task that the time entry should belong to // ID of the task that the time entry should belong to
'task_id' => [ 'task_id' => [
'nullable', 'nullable',
'string', 'string',
ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Task::class, null, function (Builder $builder): Builder {
/** @var Builder<Task> $builder */ /** @var Builder<Task> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { (new ExistsEloquent(Task::class, null, function (Builder $builder): Builder {
/** @var Builder<Task> $builder */ /** @var Builder<Task> $builder */
return $builder->whereBelongsTo($this->organization, 'organization') return $builder->whereBelongsTo($this->organization, 'organization')
->where('project_id', $this->input('project_id')); ->where('project_id', $this->input('project_id'));
})->uuid()->withMessage(__('validation.task_belongs_to_project')), }))->withMessage(__('validation.task_belongs_to_project')),
], ],
// Start of time entry (ISO 8601 format, UTC timezone) // Start of time entry (ISO 8601 format, UTC timezone)
'start' => [ 'start' => [
@@ -86,10 +89,11 @@ class TimeEntryUpdateRequest extends FormRequest
], ],
'tags.*' => [ 'tags.*' => [
'string', 'string',
ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder { 'uuid',
new ExistsEloquent(Tag::class, null, function (Builder $builder): Builder {
/** @var Builder<Tag> $builder */ /** @var Builder<Tag> $builder */
return $builder->whereBelongsTo($this->organization, 'organization'); return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(), }),
], ],
]; ];
} }

View File

@@ -13,20 +13,6 @@ use Illuminate\Http\Request;
*/ */
class OrganizationResource extends BaseResource class OrganizationResource extends BaseResource
{ {
private bool $showBillableRate;
/**
* Create a new resource instance.
*
* @return void
*/
public function __construct(Organization $resource, bool $showBillableRate)
{
parent::__construct($resource);
$this->showBillableRate = $showBillableRate;
}
/** /**
* Transform the resource into an array. * Transform the resource into an array.
* *
@@ -42,9 +28,7 @@ class OrganizationResource extends BaseResource
/** @var bool $color Personal organizations automatically created after registration */ /** @var bool $color Personal organizations automatically created after registration */
'is_personal' => $this->resource->personal_team, 'is_personal' => $this->resource->personal_team,
/** @var int|null $billable_rate Billable rate in cents per hour */ /** @var int|null $billable_rate Billable rate in cents per hour */
'billable_rate' => $this->showBillableRate ? $this->resource->billable_rate : null, 'billable_rate' => $this->resource->billable_rate,
/** @var bool $employees_can_see_billable_rates Can members of the organization with role "employee" see the billable rates */
'employees_can_see_billable_rates' => $this->resource->employees_can_see_billable_rates,
]; ];
} }
} }

View File

@@ -5,39 +5,14 @@ declare(strict_types=1);
namespace App\Http\Resources\V1\Project; namespace App\Http\Resources\V1\Project;
use App\Http\Resources\PaginatedResourceCollection; use App\Http\Resources\PaginatedResourceCollection;
use App\Models\Project;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection; use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Pagination\LengthAwarePaginator;
class ProjectCollection extends ResourceCollection implements PaginatedResourceCollection class ProjectCollection extends ResourceCollection implements PaginatedResourceCollection
{ {
private bool $showBillableRates;
/** /**
* @param LengthAwarePaginator<Project> $resource * The resource that this resource collects.
*/
public function __construct($resource, bool $showBillableRates)
{
parent::__construct($resource);
$this->showBillableRates = $showBillableRates;
}
protected function collects(): ?string
{
return null;
}
/**
* Transform the resource collection into an array.
* *
* @return array<array<string, string|bool|int|null>> * @var string
*/ */
public function toArray(Request $request): array public $collects = ProjectResource::class;
{
return $this->collection->map(function (Project $project) use ($request): array {
return (new ProjectResource($project, $this->showBillableRates))
->toArray($request);
})->all();
}
} }

View File

@@ -13,15 +13,6 @@ use Illuminate\Http\Request;
*/ */
class ProjectResource extends BaseResource class ProjectResource extends BaseResource
{ {
private bool $showBillableRate;
public function __construct(Project $resource, bool $showBillableRate)
{
parent::__construct($resource);
$this->showBillableRate = $showBillableRate;
}
/** /**
* Transform the resource into an array. * Transform the resource into an array.
* *
@@ -41,13 +32,9 @@ class ProjectResource extends BaseResource
/** @var bool $is_archived Whether the client is archived */ /** @var bool $is_archived Whether the client is archived */
'is_archived' => $this->resource->is_archived, 'is_archived' => $this->resource->is_archived,
/** @var int|null $billable_rate Billable rate in cents per hour */ /** @var int|null $billable_rate Billable rate in cents per hour */
'billable_rate' => $this->showBillableRate ? $this->resource->billable_rate : null, 'billable_rate' => $this->resource->billable_rate,
/** @var bool $is_billable Project time entries billable default */ /** @var bool $is_billable Project time entries billable default */
'is_billable' => $this->resource->is_billable, 'is_billable' => $this->resource->is_billable,
/** @var int|null $estimated_time Estimated time in seconds */
'estimated_time' => $this->resource->estimated_time,
/** @var int $spent_time Spent time on this project in seconds (sum of the duration of all associated time entries, excl. still running time entries) */
'spent_time' => $this->resource->spent_time,
]; ];
} }
} }

View File

@@ -30,10 +30,6 @@ class TaskResource extends BaseResource
'is_done' => $this->resource->is_done, 'is_done' => $this->resource->is_done,
/** @var string $project_id ID of the project */ /** @var string $project_id ID of the project */
'project_id' => $this->resource->project_id, 'project_id' => $this->resource->project_id,
/** @var int|null $estimated_time Estimated time in seconds */
'estimated_time' => $this->resource->estimated_time,
/** @var int $spent_time Spent time on this task in seconds (sum of the duration of all associated time entries, excl. still running time entries) */
'spent_time' => $this->resource->spent_time,
/** @var string $created_at When the tag was created */ /** @var string $created_at When the tag was created */
'created_at' => $this->formatDateTime($this->resource->created_at), 'created_at' => $this->formatDateTime($this->resource->created_at),
/** @var string $updated_at When the tag was last updated */ /** @var string $updated_at When the tag was last updated */

View File

@@ -4,10 +4,9 @@ declare(strict_types=1);
namespace App\Http\Resources\V1\TimeEntry; namespace App\Http\Resources\V1\TimeEntry;
use App\Http\Resources\PaginatedResourceCollection;
use Illuminate\Http\Resources\Json\ResourceCollection; use Illuminate\Http\Resources\Json\ResourceCollection;
class TimeEntryCollection extends ResourceCollection implements PaginatedResourceCollection class TimeEntryCollection extends ResourceCollection
{ {
/** /**
* The resource that this resource collects. * The resource that this resource collects.

View File

@@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\Project;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class RecalculateSpentTimeForProject implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public Project $project;
/**
* Create a new job instance.
*/
public function __construct(Project $project)
{
$this->project = $project;
}
/**
* Execute the job.
*
* @throws Exception
*/
public function handle(): void
{
$this->project->setComputedAttributeValue('spent_time');
if ($this->project->isDirty()) {
$this->project->save();
}
}
}

View File

@@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\Task;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class RecalculateSpentTimeForTask implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public Task $task;
/**
* Create a new job instance.
*/
public function __construct(Task $task)
{
$this->task = $task;
}
/**
* Execute the job.
*
* @throws Exception
*/
public function handle(): void
{
$this->task->setComputedAttributeValue('spent_time');
if ($this->task->isDirty()) {
$this->task->save();
}
}
}

View File

@@ -29,7 +29,6 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
* @property string $currency * @property string $currency
* @property int|null $billable_rate * @property int|null $billable_rate
* @property string $user_id * @property string $user_id
* @property bool $employees_can_see_billable_rates
* @property User $owner * @property User $owner
* @property Carbon|null $created_at * @property Carbon|null $created_at
* @property Carbon|null $updated_at * @property Carbon|null $updated_at
@@ -59,7 +58,6 @@ class Organization extends JetstreamTeam implements AuditableContract
'name' => 'string', 'name' => 'string',
'personal_team' => 'boolean', 'personal_team' => 'boolean',
'currency' => 'string', 'currency' => 'string',
'employees_can_see_billable_rates' => 'boolean',
]; ];
/** /**

View File

@@ -15,8 +15,6 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Korridor\LaravelComputedAttributes\ComputedAttributes;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract; use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
/** /**
@@ -29,8 +27,6 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
* @property bool $is_public * @property bool $is_public
* @property bool $is_billable * @property bool $is_billable
* @property-read bool $is_archived * @property-read bool $is_archived
* @property int|null $estimated_time
* @property int $spent_time
* @property Carbon|null $archived_at * @property Carbon|null $archived_at
* @property Carbon|null $created_at * @property Carbon|null $created_at
* @property Carbon|null $updated_at * @property Carbon|null $updated_at
@@ -44,7 +40,6 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
*/ */
class Project extends Model implements AuditableContract class Project extends Model implements AuditableContract
{ {
use ComputedAttributes;
use CustomAuditable; use CustomAuditable;
/** @use HasFactory<ProjectFactory> */ /** @use HasFactory<ProjectFactory> */
@@ -61,8 +56,6 @@ class Project extends Model implements AuditableContract
'name' => 'string', 'name' => 'string',
'color' => 'string', 'color' => 'string',
'archived_at' => 'datetime', 'archived_at' => 'datetime',
'estimated_time' => 'integer',
'spent_time' => 'integer',
]; ];
/** /**
@@ -74,68 +67,6 @@ class Project extends Model implements AuditableContract
'is_billable' => false, 'is_billable' => false,
]; ];
/**
* The attributes that are computed. (f.e. for performance reasons)
* These attributes can be regenerated at any time.
*
* @var string[]
*/
protected array $computed = [
'spent_time',
];
/**
* Attributes to exclude from the Audit.
*
* @var array<string>
*/
protected array $auditExclude = [
'spent_time',
];
public function getSpentTimeComputed(): ?int
{
if ($this->hasAttribute('spent_time_computed')) {
return $this->attributes['spent_time_computed'] === null ? 0 : (int) $this->attributes['spent_time_computed'];
} else {
/** @var object{ spent_time: string } $result */
$result = $this->timeEntries()
->whereNotNull('end')
->selectRaw('sum(extract(epoch from ("end" - start))) as spent_time')
->first();
return (int) $result->spent_time;
}
}
/**
* This scope will be applied during the computed property generation with artisan computed-attributes:generate.
*
* @param Builder<Project> $builder
* @param array<string> $attributes Attributes that will be generated.
* @return Builder<Project>
*/
public function scopeComputedAttributesGenerate(Builder $builder, array $attributes): Builder
{
if (in_array('spent_time', $attributes, true)) {
$builder->withAggregate('timeEntries as spent_time_computed', DB::raw('extract(epoch from ("end" - start))'), 'sum');
}
return $builder;
}
/**
* This scope will be applied during the computed property validation with artisan computed-attributes:validate.
*
* @param Builder<Project> $builder
* @param array<string> $attributes Attributes that will be validated.
* @return Builder<Project>
*/
public function scopeComputedAttributesValidate(Builder $builder, array $attributes): Builder
{
return $this->scopeComputedAttributesGenerate($builder, $attributes);
}
/** /**
* @return BelongsTo<Organization, Project> * @return BelongsTo<Organization, Project>
*/ */

View File

@@ -15,8 +15,6 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Korridor\LaravelComputedAttributes\ComputedAttributes;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract; use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
/** /**
@@ -25,8 +23,6 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
* @property string $project_id * @property string $project_id
* @property string $organization_id * @property string $organization_id
* @property Carbon|null $done_at * @property Carbon|null $done_at
* @property int|null $estimated_time
* @property int $spent_time
* @property Carbon|null $created_at * @property Carbon|null $created_at
* @property Carbon|null $updated_at * @property Carbon|null $updated_at
* @property-read Project $project * @property-read Project $project
@@ -38,7 +34,6 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
*/ */
class Task extends Model implements AuditableContract class Task extends Model implements AuditableContract
{ {
use ComputedAttributes;
use CustomAuditable; use CustomAuditable;
/** @use HasFactory<TaskFactory> */ /** @use HasFactory<TaskFactory> */
@@ -53,72 +48,9 @@ class Task extends Model implements AuditableContract
*/ */
protected $casts = [ protected $casts = [
'name' => 'string', 'name' => 'string',
'estimated_time' => 'integer',
'done_at' => 'datetime', 'done_at' => 'datetime',
]; ];
/**
* The attributes that are computed. (f.e. for performance reasons)
* These attributes can be regenerated at any time.
*
* @var string[]
*/
protected array $computed = [
'spent_time',
];
/**
* Attributes to exclude from the Audit.
*
* @var array<string>
*/
protected array $auditExclude = [
'spent_time',
];
public function getSpentTimeComputed(): ?int
{
if ($this->hasAttribute('spent_time_computed')) {
return $this->attributes['spent_time_computed'] === null ? 0 : (int) $this->attributes['spent_time_computed'];
} else {
/** @var object{ spent_time: string } $result */
$result = $this->timeEntries()
->whereNotNull('end')
->selectRaw('sum(extract(epoch from ("end" - start))) as spent_time')
->first();
return (int) $result->spent_time;
}
}
/**
* This scope will be applied during the computed property generation with artisan computed-attributes:generate.
*
* @param Builder<Task> $builder
* @param array<string> $attributes Attributes that will be generated.
* @return Builder<Task>
*/
public function scopeComputedAttributesGenerate(Builder $builder, array $attributes): Builder
{
if (in_array('spent_time', $attributes, true)) {
$builder->withAggregate('timeEntries as spent_time_computed', DB::raw('extract(epoch from ("end" - start))'), 'sum');
}
return $builder;
}
/**
* This scope will be applied during the computed property validation with artisan computed-attributes:validate.
*
* @param Builder<Task> $builder
* @param array<string> $attributes Attributes that will be validated.
* @return Builder<Task>
*/
public function scopeComputedAttributesValidate(Builder $builder, array $attributes): Builder
{
return $this->scopeComputedAttributesGenerate($builder, $attributes);
}
/** /**
* @return BelongsTo<Project, Task> * @return BelongsTo<Project, Task>
*/ */

View File

@@ -13,7 +13,6 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Korridor\LaravelComputedAttributes\ComputedAttributes; use Korridor\LaravelComputedAttributes\ComputedAttributes;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract; use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
@@ -80,16 +79,6 @@ class TimeEntry extends Model implements AuditableContract
*/ */
protected array $computed = [ protected array $computed = [
'billable_rate', 'billable_rate',
'client_id',
];
/**
* Attributes to exclude from the Audit.
*
* @var array<string>
*/
protected array $auditExclude = [
'billable_rate',
]; ];
public function getBillableRateComputed(): ?int public function getBillableRateComputed(): ?int
@@ -97,44 +86,6 @@ class TimeEntry extends Model implements AuditableContract
return app(BillableRateService::class)->getBillableRateForTimeEntry($this); return app(BillableRateService::class)->getBillableRateForTimeEntry($this);
} }
public function getClientIdComputed(): ?string
{
return $this->project_id === null ? null : $this->project->client_id;
}
/**
* This scope will be applied during the computed property generation with artisan computed-attributes:generate.
*
* @param Builder<TimeEntry> $builder
* @param array<string> $attributes Attributes that will be generated.
* @return Builder<TimeEntry>
*/
public function scopeComputedAttributesGenerate(Builder $builder, array $attributes): Builder
{
if (in_array('client_id', $attributes, true)) {
$builder->with([
'project' => function (Relation $builder): void {
/** @var Builder<Project> $builder */
$builder->select('id', 'client_id');
},
]);
}
return $builder;
}
/**
* This scope will be applied during the computed property validation with artisan computed-attributes:validate.
*
* @param Builder<TimeEntry> $builder
* @param array<string> $attributes Attributes that will be validated.
* @return Builder<TimeEntry>
*/
public function scopeComputedAttributesValidate(Builder $builder, array $attributes): Builder
{
return $this->scopeComputedAttributesGenerate($builder, $attributes);
}
public function getDuration(): ?CarbonInterval public function getDuration(): ?CarbonInterval
{ {
return $this->end === null ? null : $this->start->diffAsCarbonInterval($this->end); return $this->end === null ? null : $this->start->diffAsCarbonInterval($this->end);

View File

@@ -81,10 +81,10 @@ class AppServiceProvider extends ServiceProvider
}); });
// Scramble // Scramble
Scramble::extendOpenApi(function (OpenApi $openApi): void { Scramble::extendOpenApi(function (OpenApi $openApi) {
$openApi->secure( $openApi->secure(
SecurityScheme::oauth2() SecurityScheme::oauth2()
->flow('authorizationCode', function (OAuthFlow $flow): void { ->flow('authorizationCode', function (OAuthFlow $flow) {
$flow $flow
->authorizationUrl('https://solidtime.test/oauth/authorize'); ->authorizationUrl('https://solidtime.test/oauth/authorize');
}) })

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace App\Providers\Filament; namespace App\Providers\Filament;
use App\Filament\Widgets\ActiveUserOverview; use App\Filament\Widgets\ActiveUserOverview;
use App\Filament\Widgets\ServerOverview;
use App\Filament\Widgets\TimeEntriesCreated; use App\Filament\Widgets\TimeEntriesCreated;
use App\Filament\Widgets\TimeEntriesImported; use App\Filament\Widgets\TimeEntriesImported;
use App\Filament\Widgets\UserRegistrations; use App\Filament\Widgets\UserRegistrations;
@@ -45,13 +44,11 @@ class AdminPanelProvider extends PanelProvider
]) ])
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([ ->widgets([
ServerOverview::class,
ActiveUserOverview::class, ActiveUserOverview::class,
UserRegistrations::class, UserRegistrations::class,
TimeEntriesCreated::class, TimeEntriesCreated::class,
TimeEntriesImported::class, TimeEntriesImported::class,
]) ])
->viteTheme('resources/css/filament/admin/theme.css')
->plugins([ ->plugins([
EnvironmentIndicatorPlugin::make() EnvironmentIndicatorPlugin::make()
->color(fn () => match (App::environment()) { ->color(fn () => match (App::environment()) {

View File

@@ -37,9 +37,9 @@ class RouteServiceProvider extends ServiceProvider
: Limit::perMinute(60)->by($request->ip()); : Limit::perMinute(60)->by($request->ip());
}); });
$this->routes(function (): void { $this->routes(function () {
Route::middleware('health-check') Route::middleware('health-check')
->group(function (): void { ->group(function () {
Route::get('health-check/up', [HealthCheckController::class, 'up']); Route::get('health-check/up', [HealthCheckController::class, 'up']);
Route::get('health-check/debug', [HealthCheckController::class, 'debug']); Route::get('health-check/debug', [HealthCheckController::class, 'debug']);
}); });

View File

@@ -1,93 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Models\Audit;
use App\Models\Client;
use App\Models\Organization;
use App\Models\Project;
use App\Models\ProjectMember;
use App\Models\Task;
use App\Models\TimeEntry;
use App\Models\User;
use Exception;
use Illuminate\Support\Facades\Http;
use Log;
class ApiService
{
private const string API_URL = 'https://app.solidtime.io/api/v1';
public function checkForUpdate(): ?string
{
try {
$response = Http::asJson()
->timeout(3)
->connectTimeout(2)
->post(self::API_URL.'/ping/version', [
'version' => config('app.version'),
'build' => config('app.build'),
'url' => config('app.url'),
]);
if ($response->status() === 200 && isset($response->json()['version']) && is_string($response->json()['version'])) {
return $response->json()['version'];
} else {
Log::warning('Failed to check for update', [
'status' => $response->status(),
'body' => $response->body(),
]);
return null;
}
} catch (\Throwable $e) {
Log::warning('Failed to check for update', [
'message' => $e->getMessage(),
]);
return null;
}
}
public function telemetry(): bool
{
try {
$response = Http::asJson()
->timeout(3)
->connectTimeout(2)
->post(self::API_URL.'/ping/telemetry', [
'version' => config('app.version'),
'build' => config('app.build'),
'url' => config('app.url'),
// telemetry data
'user_count' => User::count(),
'organization_count' => Organization::count(),
'audit_count' => Audit::count(),
'project_count' => Project::count(),
'project_member_count' => ProjectMember::count(),
'client_count' => Client::count(),
'task_count' => Task::count(),
'time_entry_count' => TimeEntry::count(),
]);
if ($response->status() === 200) {
return true;
} else {
Log::warning('Failed send telemetry data', [
'status' => $response->status(),
'body' => $response->body(),
]);
return false;
}
} catch (Exception $e) {
Log::warning('Failed send telemetry data', [
'message' => $e->getMessage(),
]);
return false;
}
}
}

View File

@@ -28,9 +28,9 @@ class BillableRateService
->where('billable', '=', true) ->where('billable', '=', true)
->where('organization_id', '=', $project->organization_id) ->where('organization_id', '=', $project->organization_id)
->whereBelongsTo($project, 'project') ->whereBelongsTo($project, 'project')
->whereDoesntHave('member', function (Builder $query) use ($project): void { ->whereDoesntHave('member', function (Builder $query) use ($project) {
/** @var Builder<Member> $query */ /** @var Builder<Member> $query */
$query->whereHas('projectMembers', function (Builder $query) use ($project): void { $query->whereHas('projectMembers', function (Builder $query) use ($project) {
/** @var Builder<ProjectMember> $query */ /** @var Builder<ProjectMember> $query */
$query->whereBelongsTo($project, 'project') $query->whereBelongsTo($project, 'project')
->whereNotNull('billable_rate'); ->whereNotNull('billable_rate');
@@ -62,7 +62,7 @@ class BillableRateService
TimeEntry::query() TimeEntry::query()
->where('billable', '=', true) ->where('billable', '=', true)
->where('organization_id', '=', $organization->getKey()) ->where('organization_id', '=', $organization->getKey())
->whereDoesntHave('member', function (Builder $builder): void { ->whereDoesntHave('member', function (Builder $builder) {
/** @var Builder<Member> $builder */ /** @var Builder<Member> $builder */
$builder->whereNotNull('billable_rate'); $builder->whereNotNull('billable_rate');
}) })

View File

@@ -35,7 +35,7 @@ class DeletionService
public function deleteOrganization(Organization $organization, bool $inTransaction = true, ?User $ignoreUser = null): void public function deleteOrganization(Organization $organization, bool $inTransaction = true, ?User $ignoreUser = null): void
{ {
if ($inTransaction) { if ($inTransaction) {
DB::transaction(function () use ($organization): void { DB::transaction(function () use ($organization) {
$this->deleteOrganization($organization, false); $this->deleteOrganization($organization, false);
}); });
@@ -123,7 +123,7 @@ class DeletionService
public function deleteUser(User $user, bool $inTransaction = true): void public function deleteUser(User $user, bool $inTransaction = true): void
{ {
if ($inTransaction) { if ($inTransaction) {
DB::transaction(function () use ($user): void { DB::transaction(function () use ($user) {
$this->deleteUser($user, false); $this->deleteUser($user, false);
}); });

View File

@@ -47,9 +47,6 @@ class ExportService
// Organizations // Organizations
try { try {
$writer = Writer::createFromPath($temporaryDirectory->path('organizations.csv'), 'w+'); $writer = Writer::createFromPath($temporaryDirectory->path('organizations.csv'), 'w+');
$writer->setDelimiter(',');
$writer->setEnclosure('"');
$writer->setEscape('');
$writer->insertOne([ $writer->insertOne([
'id', 'id',
'name', 'name',
@@ -69,9 +66,6 @@ class ExportService
// Organization invitations // Organization invitations
$writer = Writer::createFromPath($temporaryDirectory->path('organization_invitations.csv'), 'w+'); $writer = Writer::createFromPath($temporaryDirectory->path('organization_invitations.csv'), 'w+');
$writer->setDelimiter(',');
$writer->setEnclosure('"');
$writer->setEscape('');
$writer->insertOne([ $writer->insertOne([
'id', 'id',
'email', 'email',
@@ -97,9 +91,6 @@ class ExportService
// Time entries // Time entries
$writer = Writer::createFromPath($temporaryDirectory->path('time_entries.csv'), 'w+'); $writer = Writer::createFromPath($temporaryDirectory->path('time_entries.csv'), 'w+');
$writer->setDelimiter(',');
$writer->setEnclosure('"');
$writer->setEscape('');
$writer->insertOne([ $writer->insertOne([
'id', 'id',
'description', 'description',
@@ -148,9 +139,6 @@ class ExportService
// Clients // Clients
$writer = Writer::createFromPath($temporaryDirectory->path('clients.csv'), 'w+'); $writer = Writer::createFromPath($temporaryDirectory->path('clients.csv'), 'w+');
$writer->setDelimiter(',');
$writer->setEnclosure('"');
$writer->setEscape('');
$writer->insertOne([ $writer->insertOne([
'id', 'id',
'name', 'name',
@@ -176,9 +164,6 @@ class ExportService
// Projects // Projects
$writer = Writer::createFromPath($temporaryDirectory->path('projects.csv'), 'w+'); $writer = Writer::createFromPath($temporaryDirectory->path('projects.csv'), 'w+');
$writer->setDelimiter(',');
$writer->setEnclosure('"');
$writer->setEscape('');
$writer->insertOne([ $writer->insertOne([
'id', 'id',
'name', 'name',
@@ -214,9 +199,6 @@ class ExportService
// Project members // Project members
$writer = Writer::createFromPath($temporaryDirectory->path('project_members.csv'), 'w+'); $writer = Writer::createFromPath($temporaryDirectory->path('project_members.csv'), 'w+');
$writer->setDelimiter(',');
$writer->setEnclosure('"');
$writer->setEscape('');
$writer->insertOne([ $writer->insertOne([
'id', 'id',
'billable_rate', 'billable_rate',
@@ -244,9 +226,6 @@ class ExportService
// Members // Members
$writer = Writer::createFromPath($temporaryDirectory->path('members.csv'), 'w+'); $writer = Writer::createFromPath($temporaryDirectory->path('members.csv'), 'w+');
$writer->setDelimiter(',');
$writer->setEnclosure('"');
$writer->setEscape('');
$writer->insertOne([ $writer->insertOne([
'id', 'id',
'user_id', 'user_id',
@@ -281,9 +260,6 @@ class ExportService
// Tasks // Tasks
$writer = Writer::createFromPath($temporaryDirectory->path('tasks.csv'), 'w+'); $writer = Writer::createFromPath($temporaryDirectory->path('tasks.csv'), 'w+');
$writer->setDelimiter(',');
$writer->setEnclosure('"');
$writer->setEscape('');
$writer->insertOne([ $writer->insertOne([
'id', 'id',
'name', 'name',
@@ -311,9 +287,6 @@ class ExportService
// Tags // Tags
$writer = Writer::createFromPath($temporaryDirectory->path('tags.csv'), 'w+'); $writer = Writer::createFromPath($temporaryDirectory->path('tags.csv'), 'w+');
$writer->setDelimiter(',');
$writer->setEnclosure('"');
$writer->setEscape('');
$writer->insertOne([ $writer->insertOne([
'id', 'id',
'name', 'name',

View File

@@ -31,7 +31,7 @@ class ImportService
$lock = Cache::lock('import:'.$organization->getKey(), config('octane.max_execution_time', 60) + 1); $lock = Cache::lock('import:'.$organization->getKey(), config('octane.max_execution_time', 60) + 1);
if ($lock->get()) { if ($lock->get()) {
DB::transaction(function () use (&$importer, &$data, &$timezone): void { DB::transaction(function () use (&$importer, &$data, &$timezone) {
$importer->importData($data, $timezone); $importer->importData($data, $timezone);
}); });
$lock->release(); $lock->release();

View File

@@ -47,8 +47,6 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
$reader = Reader::createFromString($data); $reader = Reader::createFromString($data);
$reader->setHeaderOffset(0); $reader->setHeaderOffset(0);
$reader->setDelimiter(','); $reader->setDelimiter(',');
$reader->setEnclosure('"');
$reader->setEscape('');
$header = $reader->getHeader(); $header = $reader->getHeader();
$this->validateHeader($header); $this->validateHeader($header);
$records = $reader->getRecords(); $records = $reader->getRecords();

View File

@@ -112,9 +112,8 @@ abstract class DefaultImporter implements ImporterContract
'billable_rate' => [ 'billable_rate' => [
'nullable', 'nullable',
'integer', 'integer',
'max:2147483647',
], ],
], beforeSave: function (Project $project): void { ], beforeSave: function (Project $project) {
if ($project->billable_rate === 0) { if ($project->billable_rate === 0) {
$project->billable_rate = null; $project->billable_rate = null;
} }
@@ -126,9 +125,8 @@ abstract class DefaultImporter implements ImporterContract
'billable_rate' => [ 'billable_rate' => [
'nullable', 'nullable',
'integer', 'integer',
'max:2147483647',
], ],
], beforeSave: function (ProjectMember $projectMember): void { ], beforeSave: function (ProjectMember $projectMember) {
if ($projectMember->billable_rate === 0) { if ($projectMember->billable_rate === 0) {
$projectMember->billable_rate = null; $projectMember->billable_rate = null;
} }

View File

@@ -60,8 +60,6 @@ class SolidtimeImporter extends DefaultImporter
$clientsReader = Reader::createFromPath($temporaryDirectory->path('clients.csv')); $clientsReader = Reader::createFromPath($temporaryDirectory->path('clients.csv'));
$clientsReader->setHeaderOffset(0); $clientsReader->setHeaderOffset(0);
$clientsReader->setDelimiter(','); $clientsReader->setDelimiter(',');
$clientsReader->setEnclosure('"');
$clientsReader->setEscape('');
if (! file_exists($temporaryDirectory->path('members.csv'))) { if (! file_exists($temporaryDirectory->path('members.csv'))) {
throw new ImportException('File "members.csv" missing in ZIP'); throw new ImportException('File "members.csv" missing in ZIP');
@@ -69,8 +67,6 @@ class SolidtimeImporter extends DefaultImporter
$membersReader = Reader::createFromPath($temporaryDirectory->path('members.csv')); $membersReader = Reader::createFromPath($temporaryDirectory->path('members.csv'));
$membersReader->setHeaderOffset(0); $membersReader->setHeaderOffset(0);
$membersReader->setDelimiter(','); $membersReader->setDelimiter(',');
$membersReader->setEnclosure('"');
$membersReader->setEscape('');
if (! file_exists($temporaryDirectory->path('organization_invitations.csv'))) { if (! file_exists($temporaryDirectory->path('organization_invitations.csv'))) {
throw new ImportException('File "organization_invitations.csv" missing in ZIP'); throw new ImportException('File "organization_invitations.csv" missing in ZIP');
@@ -78,8 +74,6 @@ class SolidtimeImporter extends DefaultImporter
$organizationInvitationsReader = Reader::createFromPath($temporaryDirectory->path('organization_invitations.csv')); $organizationInvitationsReader = Reader::createFromPath($temporaryDirectory->path('organization_invitations.csv'));
$organizationInvitationsReader->setHeaderOffset(0); $organizationInvitationsReader->setHeaderOffset(0);
$organizationInvitationsReader->setDelimiter(','); $organizationInvitationsReader->setDelimiter(',');
$organizationInvitationsReader->setEnclosure('"');
$organizationInvitationsReader->setEscape('');
if (! file_exists($temporaryDirectory->path('project_members.csv'))) { if (! file_exists($temporaryDirectory->path('project_members.csv'))) {
throw new ImportException('File "project_members.csv" missing in ZIP'); throw new ImportException('File "project_members.csv" missing in ZIP');
@@ -87,8 +81,6 @@ class SolidtimeImporter extends DefaultImporter
$projectMembersReader = Reader::createFromPath($temporaryDirectory->path('project_members.csv')); $projectMembersReader = Reader::createFromPath($temporaryDirectory->path('project_members.csv'));
$projectMembersReader->setHeaderOffset(0); $projectMembersReader->setHeaderOffset(0);
$projectMembersReader->setDelimiter(','); $projectMembersReader->setDelimiter(',');
$projectMembersReader->setEnclosure('"');
$projectMembersReader->setEscape('');
if (! file_exists($temporaryDirectory->path('projects.csv'))) { if (! file_exists($temporaryDirectory->path('projects.csv'))) {
throw new ImportException('File "projects.csv" missing in ZIP'); throw new ImportException('File "projects.csv" missing in ZIP');
@@ -96,8 +88,6 @@ class SolidtimeImporter extends DefaultImporter
$projectsReader = Reader::createFromPath($temporaryDirectory->path('projects.csv')); $projectsReader = Reader::createFromPath($temporaryDirectory->path('projects.csv'));
$projectsReader->setHeaderOffset(0); $projectsReader->setHeaderOffset(0);
$projectsReader->setDelimiter(','); $projectsReader->setDelimiter(',');
$projectsReader->setEnclosure('"');
$projectsReader->setEscape('');
if (! file_exists($temporaryDirectory->path('tags.csv'))) { if (! file_exists($temporaryDirectory->path('tags.csv'))) {
throw new ImportException('File "tags.csv" missing in ZIP'); throw new ImportException('File "tags.csv" missing in ZIP');
@@ -105,8 +95,6 @@ class SolidtimeImporter extends DefaultImporter
$tagsReader = Reader::createFromPath($temporaryDirectory->path('tags.csv')); $tagsReader = Reader::createFromPath($temporaryDirectory->path('tags.csv'));
$tagsReader->setHeaderOffset(0); $tagsReader->setHeaderOffset(0);
$tagsReader->setDelimiter(','); $tagsReader->setDelimiter(',');
$tagsReader->setEnclosure('"');
$tagsReader->setEscape('');
if (! file_exists($temporaryDirectory->path('tasks.csv'))) { if (! file_exists($temporaryDirectory->path('tasks.csv'))) {
throw new ImportException('File "tasks.csv" missing in ZIP'); throw new ImportException('File "tasks.csv" missing in ZIP');
@@ -114,8 +102,6 @@ class SolidtimeImporter extends DefaultImporter
$tasksReader = Reader::createFromPath($temporaryDirectory->path('tasks.csv')); $tasksReader = Reader::createFromPath($temporaryDirectory->path('tasks.csv'));
$tasksReader->setHeaderOffset(0); $tasksReader->setHeaderOffset(0);
$tasksReader->setDelimiter(','); $tasksReader->setDelimiter(',');
$tasksReader->setEnclosure('"');
$tasksReader->setEscape('');
if (! file_exists($temporaryDirectory->path('time_entries.csv'))) { if (! file_exists($temporaryDirectory->path('time_entries.csv'))) {
throw new ImportException('File "time_entries.csv" missing in ZIP'); throw new ImportException('File "time_entries.csv" missing in ZIP');
@@ -123,8 +109,6 @@ class SolidtimeImporter extends DefaultImporter
$timeEntriesReader = Reader::createFromPath($temporaryDirectory->path('time_entries.csv')); $timeEntriesReader = Reader::createFromPath($temporaryDirectory->path('time_entries.csv'));
$timeEntriesReader->setHeaderOffset(0); $timeEntriesReader->setHeaderOffset(0);
$timeEntriesReader->setDelimiter(','); $timeEntriesReader->setDelimiter(',');
$timeEntriesReader->setEnclosure('"');
$timeEntriesReader->setEscape('');
foreach ($clientsReader as $client) { foreach ($clientsReader as $client) {
$this->clientImportHelper->getKey([ $this->clientImportHelper->getKey([

View File

@@ -47,8 +47,6 @@ class TogglTimeEntriesImporter extends DefaultImporter
$reader = Reader::createFromString($data); $reader = Reader::createFromString($data);
$reader->setHeaderOffset(0); $reader->setHeaderOffset(0);
$reader->setDelimiter(','); $reader->setDelimiter(',');
$reader->setEnclosure('"');
$reader->setEscape('');
$header = $reader->getHeader(); $header = $reader->getHeader();
$this->validateHeader($header); $this->validateHeader($header);
$records = $reader->getRecords(); $records = $reader->getRecords();

View File

@@ -2,6 +2,7 @@
"name": "solidtime-io/solidtime", "name": "solidtime-io/solidtime",
"type": "project", "type": "project",
"description": "An open-source time-tracking app", "description": "An open-source time-tracking app",
"version": "0.0.1",
"keywords": [], "keywords": [],
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"require": { "require": {
@@ -28,7 +29,7 @@
"spatie/temporary-directory": "^2.2", "spatie/temporary-directory": "^2.2",
"stechstudio/filament-impersonate": "^3.8", "stechstudio/filament-impersonate": "^3.8",
"tightenco/ziggy": "^2.1.0", "tightenco/ziggy": "^2.1.0",
"tpetry/laravel-postgresql-enhanced": "^2.0.0", "tpetry/laravel-postgresql-enhanced": "^1.0.0",
"wikimedia/composer-merge-plugin": "^2.1.0" "wikimedia/composer-merge-plugin": "^2.1.0"
}, },
"require-dev": { "require-dev": {

1188
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,11 +18,7 @@ return [
| |
*/ */
'name' => 'solidtime', 'name' => env('APP_NAME', 'solidtime'),
'version' => env('APP_VERSION'),
'build' => env('APP_BUILD'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@@ -6,7 +6,5 @@ return [
'tasks' => [ 'tasks' => [
'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_telemetry' => (bool) env('SCHEDULING_TASK_SELF_HOSTING_TELEMETRY', true),
], ],
]; ];

View File

@@ -85,7 +85,7 @@ class MemberFactory extends Factory
public function attachToOrganization(Organization $organization, array $pivot = []): static public function attachToOrganization(Organization $organization, array $pivot = []): static
{ {
return $this->afterCreating(function (User $user) use ($organization, $pivot): void { return $this->afterCreating(function (User $user) use ($organization, $pivot) {
$user->organizations()->attach($organization, $pivot); $user->organizations()->attach($organization, $pivot);
}); });
} }

View File

@@ -26,7 +26,6 @@ class OrganizationFactory extends Factory
'billable_rate' => null, 'billable_rate' => null,
'user_id' => User::factory(), 'user_id' => User::factory(),
'personal_team' => true, 'personal_team' => true,
'employees_can_see_billable_rates' => false,
]; ];
} }

View File

@@ -11,7 +11,6 @@ use App\Models\Project;
use App\Models\ProjectMember; use App\Models\ProjectMember;
use App\Service\ColorService; use App\Service\ColorService;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
/** /**
* @extends Factory<Project> * @extends Factory<Project>
@@ -34,34 +33,15 @@ class ProjectFactory extends Factory
'archived_at' => null, 'archived_at' => null,
'client_id' => null, 'client_id' => null,
'organization_id' => Organization::factory(), 'organization_id' => Organization::factory(),
'estimated_time' => null,
]; ];
} }
public function withEstimatedTime(): self public function billable(): self
{ {
return $this->state(function (array $attributes): array { return $this->state(function (array $attributes): array {
return [
'estimated_time' => $this->faker->randomNumber(3),
];
});
}
public function billable(?int $billableRate = null): self
{
return $this->state(function (array $attributes) use ($billableRate): array {
return [ return [
'is_billable' => true, 'is_billable' => true,
'billable_rate' => $billableRate === null ? $this->faker->numberBetween(50, 1000) * 100 : $billableRate, 'billable_rate' => $this->faker->numberBetween(50, 1000) * 100,
];
});
}
public function createdAt(Carbon $createdAt): self
{
return $this->state(function (array $attributes) use ($createdAt): array {
return [
'created_at' => $createdAt,
]; ];
}); });
} }

View File

@@ -26,7 +26,6 @@ class TaskFactory extends Factory
'project_id' => Project::factory(), 'project_id' => Project::factory(),
'organization_id' => Organization::factory(), 'organization_id' => Organization::factory(),
'done_at' => null, 'done_at' => null,
'estimated_time' => null,
]; ];
} }

View File

@@ -147,8 +147,8 @@ class TimeEntryFactory extends Factory
{ {
return $this->state(function (array $attributes) use ($start, $durationInSeconds): array { return $this->state(function (array $attributes) use ($start, $durationInSeconds): array {
return [ return [
'start' => $start->copy()->utc(), 'start' => $start->utc(),
'end' => $start->copy()->utc()->addSeconds($durationInSeconds), 'end' => $start->copy()->addSeconds($durationInSeconds),
]; ];
}); });
} }
@@ -157,7 +157,7 @@ class TimeEntryFactory extends Factory
{ {
return $this->state(function (array $attributes) use ($start): array { return $this->state(function (array $attributes) use ($start): array {
return [ return [
'start' => $start->copy()->utc(), 'start' => $start->utc(),
]; ];
}); });
} }

View File

@@ -82,7 +82,7 @@ class UserFactory extends Factory
public function attachToOrganization(Organization $organization, array $pivot = []): static public function attachToOrganization(Organization $organization, array $pivot = []): static
{ {
return $this->afterCreating(function (User $user) use ($organization, $pivot): void { return $this->afterCreating(function (User $user) use ($organization, $pivot) {
$user->organizations()->attach($organization, $pivot); $user->organizations()->attach($organization, $pivot);
}); });
} }

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('users', function (Blueprint $table): void { Schema::create('users', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->string('name'); $table->string('name');
$table->string('email'); $table->string('email');

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('password_reset_tokens', function (Blueprint $table): void { Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary(); $table->string('email')->primary();
$table->string('token'); $table->string('token');
$table->timestamp('created_at')->nullable(); $table->timestamp('created_at')->nullable();

View File

@@ -14,7 +14,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::table('users', function (Blueprint $table): void { Schema::table('users', function (Blueprint $table) {
$table->text('two_factor_secret') $table->text('two_factor_secret')
->after('password') ->after('password')
->nullable(); ->nullable();
@@ -36,7 +36,7 @@ return new class extends Migration
*/ */
public function down(): void public function down(): void
{ {
Schema::table('users', function (Blueprint $table): void { Schema::table('users', function (Blueprint $table) {
$table->dropColumn(array_merge([ $table->dropColumn(array_merge([
'two_factor_secret', 'two_factor_secret',
'two_factor_recovery_codes', 'two_factor_recovery_codes',

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('oauth_auth_codes', function (Blueprint $table): void { Schema::create('oauth_auth_codes', function (Blueprint $table) {
$table->string('id', 100)->primary(); $table->string('id', 100)->primary();
$table->foreignUuid('user_id')->index(); $table->foreignUuid('user_id')->index();
$table->uuid('client_id'); $table->uuid('client_id');

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('oauth_access_tokens', function (Blueprint $table): void { Schema::create('oauth_access_tokens', function (Blueprint $table) {
$table->string('id', 100)->primary(); $table->string('id', 100)->primary();
$table->foreignUuid('user_id')->nullable()->index(); $table->foreignUuid('user_id')->nullable()->index();
$table->uuid('client_id'); $table->uuid('client_id');

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('oauth_refresh_tokens', function (Blueprint $table): void { Schema::create('oauth_refresh_tokens', function (Blueprint $table) {
$table->string('id', 100)->primary(); $table->string('id', 100)->primary();
$table->string('access_token_id', 100)->index(); $table->string('access_token_id', 100)->index();
$table->boolean('revoked'); $table->boolean('revoked');

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('oauth_clients', function (Blueprint $table): void { Schema::create('oauth_clients', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->foreignUuid('user_id')->nullable()->index(); $table->foreignUuid('user_id')->nullable()->index();
$table->string('name'); $table->string('name');

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('oauth_personal_access_clients', function (Blueprint $table): void { Schema::create('oauth_personal_access_clients', function (Blueprint $table) {
$table->bigIncrements('id'); $table->bigIncrements('id');
$table->uuid('client_id'); $table->uuid('client_id');
$table->timestamps(); $table->timestamps();

View File

@@ -26,7 +26,7 @@ return new class extends Migration
} }
$schema = Schema::connection($this->getConnection()); $schema = Schema::connection($this->getConnection());
$schema->create('telescope_entries', function (Blueprint $table): void { $schema->create('telescope_entries', function (Blueprint $table) {
$table->bigIncrements('sequence'); $table->bigIncrements('sequence');
$table->uuid('uuid'); $table->uuid('uuid');
$table->uuid('batch_id'); $table->uuid('batch_id');
@@ -43,7 +43,7 @@ return new class extends Migration
$table->index(['type', 'should_display_on_index']); $table->index(['type', 'should_display_on_index']);
}); });
$schema->create('telescope_entries_tags', function (Blueprint $table): void { $schema->create('telescope_entries_tags', function (Blueprint $table) {
$table->uuid('entry_uuid'); $table->uuid('entry_uuid');
$table->string('tag'); $table->string('tag');
@@ -56,7 +56,7 @@ return new class extends Migration
->onDelete('cascade'); ->onDelete('cascade');
}); });
$schema->create('telescope_monitoring', function (Blueprint $table): void { $schema->create('telescope_monitoring', function (Blueprint $table) {
$table->string('tag')->primary(); $table->string('tag')->primary();
}); });
} }

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('failed_jobs', function (Blueprint $table): void { Schema::create('failed_jobs', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->uuid('uuid')->unique(); $table->uuid('uuid')->unique();
$table->text('connection'); $table->text('connection');

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('personal_access_tokens', function (Blueprint $table): void { Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->morphs('tokenable'); $table->morphs('tokenable');
$table->string('name'); $table->string('name');

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('organizations', function (Blueprint $table): void { Schema::create('organizations', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->foreignUuid('user_id')->index(); $table->foreignUuid('user_id')->index();
$table->string('name'); $table->string('name');

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('organization_user', function (Blueprint $table): void { Schema::create('organization_user', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->foreignUuid('organization_id'); $table->foreignUuid('organization_id');
$table->foreignUuid('user_id'); $table->foreignUuid('user_id');

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('organization_invitations', function (Blueprint $table): void { Schema::create('organization_invitations', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->foreignUuid('organization_id') $table->foreignUuid('organization_id')
->constrained() ->constrained()

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('sessions', function (Blueprint $table): void { Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary(); $table->string('id')->primary();
$table->foreignUuid('user_id')->nullable()->index(); $table->foreignUuid('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable(); $table->string('ip_address', 45)->nullable();

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('clients', function (Blueprint $table): void { Schema::create('clients', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->string('name', 255); $table->string('name', 255);
$table->uuid('organization_id'); $table->uuid('organization_id');

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('projects', function (Blueprint $table): void { Schema::create('projects', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->string('name', 255); $table->string('name', 255);
$table->string('color', 16); $table->string('color', 16);

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('tasks', function (Blueprint $table): void { Schema::create('tasks', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->string('name', 500); $table->string('name', 500);
$table->uuid('project_id'); $table->uuid('project_id');

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('tags', function (Blueprint $table): void { Schema::create('tags', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->string('name', 255); $table->string('name', 255);
$table->uuid('organization_id'); $table->uuid('organization_id');

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('time_entries', function (Blueprint $table): void { Schema::create('time_entries', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->string('description', 500); $table->string('description', 500);
$table->dateTime('start'); $table->dateTime('start');

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('project_members', function (Blueprint $table): void { Schema::create('project_members', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->integer('billable_rate')->unsigned()->nullable(); $table->integer('billable_rate')->unsigned()->nullable();
$table->uuid('project_id'); $table->uuid('project_id');

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('jobs', function (Blueprint $table): void { Schema::create('jobs', function (Blueprint $table) {
$table->bigIncrements('id'); $table->bigIncrements('id');
$table->string('queue')->index(); $table->string('queue')->index();
$table->longText('payload'); $table->longText('payload');

View File

@@ -13,13 +13,13 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('cache', function (Blueprint $table): void { Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary(); $table->string('key')->primary();
$table->mediumText('value'); $table->mediumText('value');
$table->integer('expiration'); $table->integer('expiration');
}); });
Schema::create('cache_locks', function (Blueprint $table): void { Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary(); $table->string('key')->primary();
$table->string('owner'); $table->string('owner');
$table->integer('expiration'); $table->integer('expiration');

Some files were not shown because too many files have changed in this diff Show More