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:
2026-04-10 11:15:43 +02:00
parent 5dbe7a254e
commit d37a45b028
21 changed files with 1375 additions and 1 deletions

View File

@@ -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));
}

View 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]);
}
}

View File

@@ -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);
}
}