mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-15 11:42:15 +01:00
Added placeholder users; Better exception handling; Enhanced local setup
This commit is contained in:
@@ -6,9 +6,12 @@ namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||
use Laravel\Jetstream\Jetstream;
|
||||
|
||||
@@ -20,12 +23,27 @@ class CreateNewUser implements CreatesNewUsers
|
||||
* Create a newly registered user.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function create(array $input): User
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
new UniqueEloquent(User::class, 'email', function (Builder $builder): Builder {
|
||||
/** @var Builder<User> $builder */
|
||||
return $builder->where('is_placeholder', '=', false);
|
||||
}),
|
||||
],
|
||||
'password' => $this->passwordRules(),
|
||||
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',
|
||||
])->validate();
|
||||
|
||||
@@ -8,8 +8,10 @@ use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||
use Laravel\Jetstream\Contracts\AddsTeamMembers;
|
||||
use Laravel\Jetstream\Events\AddingTeamMember;
|
||||
use Laravel\Jetstream\Events\TeamMemberAdded;
|
||||
@@ -21,21 +23,24 @@ class AddOrganizationMember implements AddsTeamMembers
|
||||
/**
|
||||
* Add a new team member to the given team.
|
||||
*/
|
||||
public function add(User $user, Organization $organization, string $email, ?string $role = null): void
|
||||
public function add(User $owner, Organization $organization, string $email, ?string $role = null): void
|
||||
{
|
||||
Gate::forUser($user)->authorize('addTeamMember', $organization);
|
||||
Gate::forUser($owner)->authorize('addTeamMember', $organization);
|
||||
|
||||
$this->validate($organization, $email, $role);
|
||||
|
||||
$newTeamMember = Jetstream::findUserByEmailOrFail($email);
|
||||
$newOrganizationMember = User::query()
|
||||
->where('email', $email)
|
||||
->where('is_placeholder', '=', false)
|
||||
->firstOrFail();
|
||||
|
||||
AddingTeamMember::dispatch($organization, $newTeamMember);
|
||||
AddingTeamMember::dispatch($organization, $newOrganizationMember);
|
||||
|
||||
$organization->users()->attach(
|
||||
$newTeamMember, ['role' => $role]
|
||||
$newOrganizationMember, ['role' => $role]
|
||||
);
|
||||
|
||||
TeamMemberAdded::dispatch($organization, $newTeamMember);
|
||||
TeamMemberAdded::dispatch($organization, $newOrganizationMember);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,9 +51,7 @@ class AddOrganizationMember implements AddsTeamMembers
|
||||
Validator::make([
|
||||
'email' => $email,
|
||||
'role' => $role,
|
||||
], $this->rules(), [
|
||||
'email.exists' => __('We were unable to find a registered user with this email address.'),
|
||||
])->after(
|
||||
], $this->rules())->after(
|
||||
$this->ensureUserIsNotAlreadyOnTeam($organization, $email)
|
||||
)->validateWithBag('addTeamMember');
|
||||
}
|
||||
@@ -61,7 +64,13 @@ class AddOrganizationMember implements AddsTeamMembers
|
||||
protected function rules(): array
|
||||
{
|
||||
return array_filter([
|
||||
'email' => ['required', 'email', 'exists:users'],
|
||||
'email' => [
|
||||
'required',
|
||||
'email',
|
||||
(new ExistsEloquent(User::class, 'email', function (Builder $builder) {
|
||||
return $builder->where('is_placeholder', '=', false);
|
||||
}))->withMessage(__('We were unable to find a registered user with this email address.')),
|
||||
],
|
||||
'role' => Jetstream::hasRoles()
|
||||
? ['required', 'string', new Role]
|
||||
: null,
|
||||
@@ -75,7 +84,7 @@ class AddOrganizationMember implements AddsTeamMembers
|
||||
{
|
||||
return function ($validator) use ($team, $email) {
|
||||
$validator->errors()->addIf(
|
||||
$team->hasUserWithEmail($email),
|
||||
$team->hasRealUserWithEmail($email),
|
||||
'email',
|
||||
__('This user already belongs to the team.')
|
||||
);
|
||||
|
||||
@@ -34,6 +34,7 @@ class InviteOrganizationMember implements InvitesTeamMembers
|
||||
|
||||
InvitingTeamMember::dispatch($organization, $email, $role);
|
||||
|
||||
/** @var TeamInvitation $invitation */
|
||||
$invitation = $organization->teamInvitations()->create([
|
||||
'email' => $email,
|
||||
'role' => $role,
|
||||
@@ -50,9 +51,7 @@ class InviteOrganizationMember implements InvitesTeamMembers
|
||||
Validator::make([
|
||||
'email' => $email,
|
||||
'role' => $role,
|
||||
], $this->rules($organization), [
|
||||
'email.unique' => __('This user has already been invited to the team.'),
|
||||
])->after(
|
||||
], $this->rules($organization))->after(
|
||||
$this->ensureUserIsNotAlreadyOnTeam($organization, $email)
|
||||
)->validateWithBag('addTeamMember');
|
||||
}
|
||||
@@ -68,10 +67,10 @@ class InviteOrganizationMember implements InvitesTeamMembers
|
||||
'email' => [
|
||||
'required',
|
||||
'email',
|
||||
new UniqueEloquent(OrganizationInvitation::class, 'email', function (Builder $builder) use ($organization) {
|
||||
(new UniqueEloquent(OrganizationInvitation::class, 'email', function (Builder $builder) use ($organization) {
|
||||
/** @var Builder<OrganizationInvitation> $builder */
|
||||
return $builder->whereBelongsTo($organization, 'organization');
|
||||
}),
|
||||
}))->withMessage(__('This user has already been invited to the team.')),
|
||||
],
|
||||
'role' => Jetstream::hasRoles()
|
||||
? ['required', 'string', new Role]
|
||||
@@ -86,7 +85,7 @@ class InviteOrganizationMember implements InvitesTeamMembers
|
||||
{
|
||||
return function ($validator) use ($organization, $email) {
|
||||
$validator->errors()->addIf(
|
||||
$organization->hasUserWithEmail($email),
|
||||
$organization->hasRealUserWithEmail($email),
|
||||
'email',
|
||||
__('This user already belongs to the team.')
|
||||
);
|
||||
|
||||
46
app/Exceptions/Api/ApiException.php
Normal file
46
app/Exceptions/Api/ApiException.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions\Api;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use LogicException;
|
||||
|
||||
abstract class ApiException extends Exception
|
||||
{
|
||||
/**
|
||||
* Render the exception into an HTTP response.
|
||||
*/
|
||||
public function render(Request $request): JsonResponse
|
||||
{
|
||||
return response()
|
||||
->json([
|
||||
'error' => true,
|
||||
'key' => $this->getKey(),
|
||||
'message' => $this->getTranslatedMessage(),
|
||||
], 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key for the exception.
|
||||
*/
|
||||
public function getKey(): string
|
||||
{
|
||||
if (defined(static::class.'::KEY')) {
|
||||
return static::KEY;
|
||||
}
|
||||
|
||||
throw new LogicException('API exceptions need the KEY constant defined.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the translated message for the exception.
|
||||
*/
|
||||
public function getTranslatedMessage(): string
|
||||
{
|
||||
return __('exceptions.api.'.$this->getKey());
|
||||
}
|
||||
}
|
||||
10
app/Exceptions/Api/TimeEntryStillRunningApiException.php
Normal file
10
app/Exceptions/Api/TimeEntryStillRunningApiException.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions\Api;
|
||||
|
||||
class TimeEntryStillRunningApiException extends ApiException
|
||||
{
|
||||
const string KEY = 'time_entry_still_running';
|
||||
}
|
||||
10
app/Exceptions/Api/UserNotPlaceholderApiException.php
Normal file
10
app/Exceptions/Api/UserNotPlaceholderApiException.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions\Api;
|
||||
|
||||
class UserNotPlaceholderApiException extends ApiException
|
||||
{
|
||||
const string KEY = 'user_not_placeholder';
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ApiException extends Exception
|
||||
{
|
||||
/**
|
||||
* Render the exception into an HTTP response.
|
||||
*/
|
||||
public function render(Request $request): JsonResponse
|
||||
{
|
||||
return response()
|
||||
->json([
|
||||
'error' => true,
|
||||
'message' => $this->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class TimeEntryStillRunning extends ApiException
|
||||
{
|
||||
}
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Exceptions\TimeEntryStillRunning;
|
||||
use App\Exceptions\Api\TimeEntryStillRunningApiException;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryIndexRequest;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryStoreRequest;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryUpdateRequest;
|
||||
@@ -102,7 +102,7 @@ class TimeEntryController extends Controller
|
||||
/**
|
||||
* Create time entry
|
||||
*
|
||||
* @throws AuthorizationException|TimeEntryStillRunning
|
||||
* @throws AuthorizationException|TimeEntryStillRunningApiException
|
||||
*/
|
||||
public function store(Organization $organization, TimeEntryStoreRequest $request): JsonResource
|
||||
{
|
||||
@@ -114,8 +114,7 @@ class TimeEntryController extends Controller
|
||||
|
||||
if ($request->get('end') === null && TimeEntry::query()->where('user_id', $request->get('user_id'))->where('end', null)->exists()) {
|
||||
// TODO: API documentation
|
||||
// TODO: Create concept for api exceptions
|
||||
throw new TimeEntryStillRunning('User already has an active time entry');
|
||||
throw new TimeEntryStillRunningApiException();
|
||||
}
|
||||
|
||||
$timeEntry = new TimeEntry();
|
||||
|
||||
56
app/Http/Controllers/Api/V1/UserController.php
Normal file
56
app/Http/Controllers/Api/V1/UserController.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Exceptions\Api\UserNotPlaceholderApiException;
|
||||
use App\Http\Requests\V1\User\UserIndexRequest;
|
||||
use App\Http\Resources\V1\User\UserCollection;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Jetstream\Contracts\InvitesTeamMembers;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
/**
|
||||
* List all users in an organization
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function index(Organization $organization, UserIndexRequest $request): UserCollection
|
||||
{
|
||||
$this->checkPermission($organization, 'users:view');
|
||||
|
||||
$users = $organization->users()
|
||||
->paginate();
|
||||
|
||||
return UserCollection::make($users);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite a placeholder user to become a real user in the organization
|
||||
*
|
||||
* @throws AuthorizationException|UserNotPlaceholderApiException
|
||||
*/
|
||||
public function invitePlaceholder(Organization $organization, User $user, Request $request): JsonResponse
|
||||
{
|
||||
$this->checkPermission($organization, 'users:invite-placeholder');
|
||||
|
||||
if (! $user->is_placeholder) {
|
||||
throw new UserNotPlaceholderApiException();
|
||||
}
|
||||
|
||||
app(InvitesTeamMembers::class)->invite(
|
||||
$request->user(),
|
||||
$organization,
|
||||
$user->email,
|
||||
'employee'
|
||||
);
|
||||
|
||||
return response()->json($user);
|
||||
}
|
||||
}
|
||||
26
app/Http/Requests/V1/User/UserIndexRequest.php
Normal file
26
app/Http/Requests/V1/User/UserIndexRequest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\V1\User;
|
||||
|
||||
use App\Models\Organization;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
* @property Organization $organization
|
||||
*/
|
||||
class UserIndexRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, array<string|ValidationRule>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
];
|
||||
}
|
||||
}
|
||||
17
app/Http/Resources/V1/User/UserCollection.php
Normal file
17
app/Http/Resources/V1/User/UserCollection.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\V1\User;
|
||||
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class UserCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* The resource that this resource collects.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $collects = UserResource::class;
|
||||
}
|
||||
40
app/Http/Resources/V1/User/UserResource.php
Normal file
40
app/Http/Resources/V1/User/UserResource.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\V1\User;
|
||||
|
||||
use App\Http\Resources\V1\BaseResource;
|
||||
use App\Models\Membership;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* @property User $resource
|
||||
*/
|
||||
class UserResource extends BaseResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, string|bool|int|null|array<string>>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
/** @var Membership $membership */
|
||||
$membership = $this->resource->getRelationValue('membership');
|
||||
|
||||
return [
|
||||
/** @var string $id ID */
|
||||
'id' => $this->resource->id,
|
||||
/** @var string $name Name */
|
||||
'name' => $this->resource->name,
|
||||
/** @var string $email Email */
|
||||
'email' => $this->resource->email,
|
||||
/** @var string $role Role */
|
||||
'role' => $membership->role,
|
||||
/** @var bool $is_placeholder Placeholder user for imports, user might not really exist and does not know about this placeholder membership */
|
||||
'is_placeholder' => $this->resource->is_placeholder,
|
||||
];
|
||||
}
|
||||
}
|
||||
30
app/Listeners/RemovePlaceholder.php
Normal file
30
app/Listeners/RemovePlaceholder.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Service\UserService;
|
||||
use Laravel\Jetstream\Events\TeamMemberAdded;
|
||||
|
||||
class RemovePlaceholder
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*/
|
||||
public function handle(TeamMemberAdded $event): void
|
||||
{
|
||||
/** @var UserService $userService */
|
||||
$userService = app(UserService::class);
|
||||
$placeholders = User::query()
|
||||
->where('is_placeholder', '=', true)
|
||||
->where('email', '=', $event->user->email)
|
||||
->belongsToOrganization($event->team)
|
||||
->get();
|
||||
|
||||
foreach ($placeholders as $placeholder) {
|
||||
$userService->assignOrganizationEntitiesToDifferentUser($event->team, $placeholder, $event->user);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use Database\Factories\OrganizationFactory;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Laravel\Jetstream\Events\TeamCreated;
|
||||
use Laravel\Jetstream\Events\TeamDeleted;
|
||||
@@ -59,4 +60,30 @@ class Organization extends JetstreamTeam
|
||||
'updated' => TeamUpdated::class,
|
||||
'deleted' => TeamDeleted::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Get all the non-placeholder users of the organization including its owner.
|
||||
*
|
||||
* @return Collection<User>
|
||||
*/
|
||||
public function allRealUsers(): Collection
|
||||
{
|
||||
return $this->realUsers->merge([$this->owner]);
|
||||
}
|
||||
|
||||
public function hasRealUserWithEmail(string $email): bool
|
||||
{
|
||||
return $this->allRealUsers()->contains(function (User $user) use ($email): bool {
|
||||
return $user->email === $email;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsToMany<User>
|
||||
*/
|
||||
public function realUsers(): BelongsToMany
|
||||
{
|
||||
return $this->users()
|
||||
->where('is_placeholder', false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace App\Models;
|
||||
|
||||
use Database\Factories\UserFactory;
|
||||
use Filament\Panel;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
@@ -21,9 +23,16 @@ use Laravel\Passport\HasApiTokens;
|
||||
* @property string $id
|
||||
* @property string $name
|
||||
* @property string $email
|
||||
* @property string|null $email_verified_at
|
||||
* @property string|null $password
|
||||
* @property bool $is_placeholder
|
||||
* @property Collection<Organization> $organizations
|
||||
* @property Collection<TimeEntry> $timeEntries
|
||||
*
|
||||
* @method HasMany<Organization> ownedTeams()
|
||||
* @method static UserFactory factory()
|
||||
* @method static Builder<User> query()
|
||||
* @method Builder<User> belongsToOrganization(Organization $organization)
|
||||
*/
|
||||
class User extends Authenticatable
|
||||
{
|
||||
@@ -97,4 +106,27 @@ class User extends Authenticatable
|
||||
->withTimestamps()
|
||||
->as('membership');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<TimeEntry>
|
||||
*/
|
||||
public function timeEntries(): HasMany
|
||||
{
|
||||
return $this->hasMany(TimeEntry::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<User> $builder
|
||||
* @return Builder<User>
|
||||
*/
|
||||
public function scopeBelongsToOrganization(Builder $builder, Organization $organization): Builder
|
||||
{
|
||||
return $builder->where(function (Builder $builder) use ($organization): Builder {
|
||||
return $builder->whereHas('organizations', function (Builder $query) use ($organization): void {
|
||||
$query->whereKey($organization->getKey());
|
||||
})->orWhereHas('ownedTeams', function (Builder $query) use ($organization): void {
|
||||
$query->whereKey($organization->getKey());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Listeners\RemovePlaceholder;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Laravel\Jetstream\Events\TeamMemberAdded;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -20,6 +21,9 @@ class EventServiceProvider extends ServiceProvider
|
||||
Registered::class => [
|
||||
SendEmailVerificationNotification::class,
|
||||
],
|
||||
TeamMemberAdded::class => [
|
||||
RemovePlaceholder::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -75,6 +75,8 @@ class JetstreamServiceProvider extends ServiceProvider
|
||||
'organizations:view',
|
||||
'organizations:update',
|
||||
'import',
|
||||
'users:invite-placeholder',
|
||||
'users:view',
|
||||
])->description('Administrator users can perform any action.');
|
||||
|
||||
Jetstream::role('manager', 'Manager', [
|
||||
@@ -95,6 +97,7 @@ class JetstreamServiceProvider extends ServiceProvider
|
||||
'tags:update',
|
||||
'tags:delete',
|
||||
'organizations:view',
|
||||
'users:view',
|
||||
])->description('Editor users have the ability to read, create, and update.');
|
||||
|
||||
Jetstream::role('employee', 'Employee', [
|
||||
|
||||
@@ -6,6 +6,9 @@ namespace App\Service\Import\Importers;
|
||||
|
||||
class ImporterProvider
|
||||
{
|
||||
/**
|
||||
* @var array<string, class-string<ImporterContract>>
|
||||
*/
|
||||
private array $importers = [
|
||||
'toggl_time_entries' => TogglTimeEntriesImporter::class,
|
||||
];
|
||||
|
||||
23
app/Service/UserService.php
Normal file
23
app/Service/UserService.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Models\Organization;
|
||||
use App\Models\TimeEntry;
|
||||
use App\Models\User;
|
||||
|
||||
class UserService
|
||||
{
|
||||
public function assignOrganizationEntitiesToDifferentUser(Organization $organization, User $fromUser, User $toUser): void
|
||||
{
|
||||
// Time entries
|
||||
dump(TimeEntry::query()
|
||||
->whereBelongsTo($organization, 'organization')
|
||||
->whereBelongsTo($fromUser, 'user')
|
||||
->update([
|
||||
'user_id' => $toUser->getKey(),
|
||||
]));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user