feat: person tags system - org-level skills with self-reported and organiser-assigned sources
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,29 @@ final class PersonController extends Controller
|
||||
$query->where('status', $request->input('status'));
|
||||
}
|
||||
|
||||
if ($request->filled('tag')) {
|
||||
$organisation = $event->organisation;
|
||||
$query->whereHas('user', function ($q) use ($request, $organisation) {
|
||||
$q->whereHas('organisationTags', function ($q2) use ($request, $organisation) {
|
||||
$q2->where('organisation_id', $organisation->id)
|
||||
->where('person_tag_id', $request->input('tag'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('tags')) {
|
||||
$organisation = $event->organisation;
|
||||
$tagIds = explode(',', $request->input('tags'));
|
||||
foreach ($tagIds as $tagId) {
|
||||
$query->whereHas('user', function ($q) use ($tagId, $organisation) {
|
||||
$q->whereHas('organisationTags', function ($q2) use ($tagId, $organisation) {
|
||||
$q2->where('organisation_id', $organisation->id)
|
||||
->where('person_tag_id', $tagId);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new PersonCollection($query->get());
|
||||
}
|
||||
|
||||
@@ -38,7 +61,7 @@ final class PersonController extends Controller
|
||||
{
|
||||
Gate::authorize('view', [$person, $event]);
|
||||
|
||||
$person->load(['crowdType', 'company']);
|
||||
$person->load(['crowdType', 'company', 'user']);
|
||||
|
||||
return $this->success(new PersonResource($person));
|
||||
}
|
||||
|
||||
67
api/app/Http/Controllers/Api/V1/PersonTagController.php
Normal file
67
api/app/Http/Controllers/Api/V1/PersonTagController.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\V1\StorePersonTagRequest;
|
||||
use App\Http\Requests\Api\V1\UpdatePersonTagRequest;
|
||||
use App\Http\Resources\Api\V1\PersonTagResource;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\PersonTag;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
final class PersonTagController extends Controller
|
||||
{
|
||||
public function index(Organisation $organisation): AnonymousResourceCollection
|
||||
{
|
||||
Gate::authorize('viewAny', [PersonTag::class, $organisation]);
|
||||
|
||||
$tags = $organisation->personTags()->active()->ordered()->get();
|
||||
|
||||
return PersonTagResource::collection($tags);
|
||||
}
|
||||
|
||||
public function store(StorePersonTagRequest $request, Organisation $organisation): JsonResponse
|
||||
{
|
||||
Gate::authorize('create', [PersonTag::class, $organisation]);
|
||||
|
||||
$tag = $organisation->personTags()->create($request->validated());
|
||||
|
||||
return $this->created(new PersonTagResource($tag));
|
||||
}
|
||||
|
||||
public function update(UpdatePersonTagRequest $request, Organisation $organisation, PersonTag $personTag): JsonResponse
|
||||
{
|
||||
Gate::authorize('update', [$personTag, $organisation]);
|
||||
|
||||
$personTag->update($request->validated());
|
||||
|
||||
return $this->success(new PersonTagResource($personTag->fresh()));
|
||||
}
|
||||
|
||||
public function destroy(Organisation $organisation, PersonTag $personTag): JsonResponse
|
||||
{
|
||||
Gate::authorize('delete', [$personTag, $organisation]);
|
||||
|
||||
$personTag->update(['is_active' => false]);
|
||||
|
||||
return response()->json(null, 204);
|
||||
}
|
||||
|
||||
public function categories(Organisation $organisation): JsonResponse
|
||||
{
|
||||
Gate::authorize('viewAny', [PersonTag::class, $organisation]);
|
||||
|
||||
$categories = $organisation->personTags()
|
||||
->whereNotNull('category')
|
||||
->distinct()
|
||||
->orderBy('category')
|
||||
->pluck('category');
|
||||
|
||||
return response()->json(['data' => $categories]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\V1\StoreUserOrganisationTagRequest;
|
||||
use App\Http\Requests\Api\V1\SyncUserOrganisationTagsRequest;
|
||||
use App\Http\Resources\Api\V1\UserOrganisationTagResource;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\User;
|
||||
use App\Models\UserOrganisationTag;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
final class UserOrganisationTagController extends Controller
|
||||
{
|
||||
public function index(Organisation $organisation, User $user): AnonymousResourceCollection
|
||||
{
|
||||
Gate::authorize('viewAny', [UserOrganisationTag::class, $organisation]);
|
||||
|
||||
$tags = $user->organisationTags()
|
||||
->where('organisation_id', $organisation->id)
|
||||
->with(['personTag', 'assignedBy'])
|
||||
->get();
|
||||
|
||||
return UserOrganisationTagResource::collection($tags);
|
||||
}
|
||||
|
||||
public function store(StoreUserOrganisationTagRequest $request, Organisation $organisation, User $user): JsonResponse
|
||||
{
|
||||
Gate::authorize('create', [UserOrganisationTag::class, $organisation]);
|
||||
|
||||
$data = $request->validated();
|
||||
$data['user_id'] = $user->id;
|
||||
$data['organisation_id'] = $organisation->id;
|
||||
$data['assigned_at'] = now();
|
||||
|
||||
if ($data['source'] === 'organiser_assigned') {
|
||||
$data['assigned_by_user_id'] = $request->user()->id;
|
||||
}
|
||||
|
||||
$tag = UserOrganisationTag::create($data);
|
||||
$tag->load(['personTag', 'assignedBy']);
|
||||
|
||||
return $this->created(new UserOrganisationTagResource($tag));
|
||||
}
|
||||
|
||||
public function destroy(Organisation $organisation, User $user, UserOrganisationTag $userOrganisationTag): JsonResponse
|
||||
{
|
||||
Gate::authorize('delete', [$userOrganisationTag, $organisation]);
|
||||
|
||||
$userOrganisationTag->delete();
|
||||
|
||||
return response()->json(null, 204);
|
||||
}
|
||||
|
||||
public function sync(SyncUserOrganisationTagsRequest $request, Organisation $organisation, User $user): AnonymousResourceCollection
|
||||
{
|
||||
Gate::authorize('sync', [UserOrganisationTag::class, $organisation]);
|
||||
|
||||
$source = $request->validated('source');
|
||||
$tagIds = $request->validated('tag_ids');
|
||||
|
||||
// Remove tags of this source that are not in the new list
|
||||
$user->organisationTags()
|
||||
->where('organisation_id', $organisation->id)
|
||||
->where('source', $source)
|
||||
->whereNotIn('person_tag_id', $tagIds)
|
||||
->delete();
|
||||
|
||||
// Add new tags that don't exist yet
|
||||
$existingTagIds = $user->organisationTags()
|
||||
->where('organisation_id', $organisation->id)
|
||||
->where('source', $source)
|
||||
->pluck('person_tag_id')
|
||||
->toArray();
|
||||
|
||||
$newTagIds = array_diff($tagIds, $existingTagIds);
|
||||
|
||||
foreach ($newTagIds as $tagId) {
|
||||
UserOrganisationTag::create([
|
||||
'user_id' => $user->id,
|
||||
'organisation_id' => $organisation->id,
|
||||
'person_tag_id' => $tagId,
|
||||
'source' => $source,
|
||||
'assigned_by_user_id' => $source === 'organiser_assigned' ? $request->user()->id : null,
|
||||
'assigned_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
// Return updated full tag list for this user in this org
|
||||
$tags = $user->organisationTags()
|
||||
->where('organisation_id', $organisation->id)
|
||||
->with(['personTag', 'assignedBy'])
|
||||
->get();
|
||||
|
||||
return UserOrganisationTagResource::collection($tags);
|
||||
}
|
||||
}
|
||||
33
api/app/Http/Requests/Api/V1/StorePersonTagRequest.php
Normal file
33
api/app/Http/Requests/Api/V1/StorePersonTagRequest.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Api\V1;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
final class StorePersonTagRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:50',
|
||||
Rule::unique('person_tags')->where('organisation_id', $this->route('organisation')->id),
|
||||
],
|
||||
'category' => ['nullable', 'string', 'max:50'],
|
||||
'icon' => ['nullable', 'string', 'max:50'],
|
||||
'color' => ['nullable', 'string', 'max:7'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
'sort_order' => ['sometimes', 'integer'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Api\V1;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
final class StoreUserOrganisationTagRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$organisationId = $this->route('organisation')->id;
|
||||
|
||||
return [
|
||||
'person_tag_id' => [
|
||||
'required',
|
||||
'ulid',
|
||||
Rule::exists('person_tags', 'id')->where('organisation_id', $organisationId),
|
||||
],
|
||||
'source' => ['required', 'in:self_reported,organiser_assigned'],
|
||||
'proficiency' => ['nullable', 'in:beginner,experienced,expert'],
|
||||
'notes' => ['nullable', 'string', 'max:500'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Api\V1;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
final class SyncUserOrganisationTagsRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$organisationId = $this->route('organisation')->id;
|
||||
|
||||
return [
|
||||
'tag_ids' => ['required', 'array'],
|
||||
'tag_ids.*' => [
|
||||
'ulid',
|
||||
Rule::exists('person_tags', 'id')->where('organisation_id', $organisationId),
|
||||
],
|
||||
'source' => ['required', 'in:self_reported,organiser_assigned'],
|
||||
];
|
||||
}
|
||||
}
|
||||
35
api/app/Http/Requests/Api/V1/UpdatePersonTagRequest.php
Normal file
35
api/app/Http/Requests/Api/V1/UpdatePersonTagRequest.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Api\V1;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
final class UpdatePersonTagRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => [
|
||||
'sometimes',
|
||||
'string',
|
||||
'max:50',
|
||||
Rule::unique('person_tags')
|
||||
->where('organisation_id', $this->route('organisation')->id)
|
||||
->ignore($this->route('person_tag')),
|
||||
],
|
||||
'category' => ['nullable', 'string', 'max:50'],
|
||||
'icon' => ['nullable', 'string', 'max:50'],
|
||||
'color' => ['nullable', 'string', 'max:7'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
'sort_order' => ['sometimes', 'integer'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,23 @@ final class PersonResource extends JsonResource
|
||||
'created_at' => $this->created_at->toIso8601String(),
|
||||
'crowd_type' => new CrowdTypeResource($this->whenLoaded('crowdType')),
|
||||
'company' => new CompanyResource($this->whenLoaded('company')),
|
||||
'tags' => $this->when(
|
||||
$this->user_id && $this->relationLoaded('user'),
|
||||
function () {
|
||||
$orgId = $this->event?->organisation_id;
|
||||
if (!$orgId || !$this->user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return UserOrganisationTagResource::collection(
|
||||
$this->user->organisationTags()
|
||||
->where('organisation_id', $orgId)
|
||||
->with('personTag')
|
||||
->get()
|
||||
);
|
||||
},
|
||||
[]
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
27
api/app/Http/Resources/Api/V1/PersonTagResource.php
Normal file
27
api/app/Http/Resources/Api/V1/PersonTagResource.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\Api\V1;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
final class PersonTagResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'organisation_id' => $this->organisation_id,
|
||||
'name' => $this->name,
|
||||
'category' => $this->category,
|
||||
'icon' => $this->icon,
|
||||
'color' => $this->color,
|
||||
'is_active' => $this->is_active,
|
||||
'sort_order' => $this->sort_order,
|
||||
'created_at' => $this->created_at->toIso8601String(),
|
||||
'updated_at' => $this->updated_at->toIso8601String(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\Api\V1;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
final class UserOrganisationTagResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'person_tag' => [
|
||||
'id' => $this->personTag->id,
|
||||
'name' => $this->personTag->name,
|
||||
'category' => $this->personTag->category,
|
||||
'icon' => $this->personTag->icon,
|
||||
'color' => $this->personTag->color,
|
||||
],
|
||||
'source' => $this->source,
|
||||
'proficiency' => $this->proficiency,
|
||||
'notes' => $this->notes,
|
||||
'assigned_at' => $this->assigned_at->toIso8601String(),
|
||||
'assigned_by' => $this->when(
|
||||
$this->source === 'organiser_assigned' && $this->relationLoaded('assignedBy') && $this->assignedBy,
|
||||
fn () => ['id' => $this->assignedBy->id, 'name' => $this->assignedBy->name],
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
49
api/app/Models/PersonTag.php
Normal file
49
api/app/Models/PersonTag.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUlids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
final class PersonTag extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use HasUlids;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'category',
|
||||
'icon',
|
||||
'color',
|
||||
'is_active',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function organisation(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Organisation::class);
|
||||
}
|
||||
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeOrdered(Builder $query): Builder
|
||||
{
|
||||
return $query->orderBy('sort_order')->orderBy('name');
|
||||
}
|
||||
}
|
||||
@@ -63,4 +63,14 @@ final class User extends Authenticatable
|
||||
{
|
||||
return $this->hasMany(UserInvitation::class, 'invited_by_user_id');
|
||||
}
|
||||
|
||||
public function organisationTags(): HasMany
|
||||
{
|
||||
return $this->hasMany(UserOrganisationTag::class);
|
||||
}
|
||||
|
||||
public function tagsForOrganisation(string $organisationId): HasMany
|
||||
{
|
||||
return $this->organisationTags()->where('organisation_id', $organisationId);
|
||||
}
|
||||
}
|
||||
|
||||
54
api/app/Models/UserOrganisationTag.php
Normal file
54
api/app/Models/UserOrganisationTag.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
final class UserOrganisationTag extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'organisation_id',
|
||||
'person_tag_id',
|
||||
'source',
|
||||
'assigned_by_user_id',
|
||||
'proficiency',
|
||||
'notes',
|
||||
'assigned_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'assigned_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function organisation(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Organisation::class);
|
||||
}
|
||||
|
||||
public function personTag(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PersonTag::class);
|
||||
}
|
||||
|
||||
public function assignedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'assigned_by_user_id');
|
||||
}
|
||||
}
|
||||
53
api/app/Policies/PersonTagPolicy.php
Normal file
53
api/app/Policies/PersonTagPolicy.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Organisation;
|
||||
use App\Models\PersonTag;
|
||||
use App\Models\User;
|
||||
|
||||
final class PersonTagPolicy
|
||||
{
|
||||
public function viewAny(User $user, Organisation $organisation): bool
|
||||
{
|
||||
return $user->hasRole('super_admin')
|
||||
|| $organisation->users()->where('user_id', $user->id)->exists();
|
||||
}
|
||||
|
||||
public function create(User $user, Organisation $organisation): bool
|
||||
{
|
||||
return $this->canManageOrganisation($user, $organisation);
|
||||
}
|
||||
|
||||
public function update(User $user, PersonTag $personTag, Organisation $organisation): bool
|
||||
{
|
||||
if ($personTag->organisation_id !== $organisation->id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->canManageOrganisation($user, $organisation);
|
||||
}
|
||||
|
||||
public function delete(User $user, PersonTag $personTag, Organisation $organisation): bool
|
||||
{
|
||||
if ($personTag->organisation_id !== $organisation->id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->canManageOrganisation($user, $organisation);
|
||||
}
|
||||
|
||||
private function canManageOrganisation(User $user, Organisation $organisation): bool
|
||||
{
|
||||
if ($user->hasRole('super_admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $organisation->users()
|
||||
->where('user_id', $user->id)
|
||||
->wherePivot('role', 'org_admin')
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
40
api/app/Policies/UserOrganisationTagPolicy.php
Normal file
40
api/app/Policies/UserOrganisationTagPolicy.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Organisation;
|
||||
use App\Models\User;
|
||||
use App\Models\UserOrganisationTag;
|
||||
|
||||
final class UserOrganisationTagPolicy
|
||||
{
|
||||
public function viewAny(User $user, Organisation $organisation): bool
|
||||
{
|
||||
return $user->hasRole('super_admin')
|
||||
|| $organisation->users()->where('user_id', $user->id)->exists();
|
||||
}
|
||||
|
||||
public function create(User $user, Organisation $organisation): bool
|
||||
{
|
||||
return $user->hasRole('super_admin')
|
||||
|| $organisation->users()->where('user_id', $user->id)->exists();
|
||||
}
|
||||
|
||||
public function delete(User $user, UserOrganisationTag $tag, Organisation $organisation): bool
|
||||
{
|
||||
if ($tag->organisation_id !== $organisation->id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->hasRole('super_admin')
|
||||
|| $organisation->users()->where('user_id', $user->id)->exists();
|
||||
}
|
||||
|
||||
public function sync(User $user, Organisation $organisation): bool
|
||||
{
|
||||
return $user->hasRole('super_admin')
|
||||
|| $organisation->users()->where('user_id', $user->id)->exists();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user