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

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Database\Factories;
use App\Models\Organisation;
use App\Models\PersonTag;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<PersonTag>
*/
final class PersonTagFactory extends Factory
{
protected $model = PersonTag::class;
public function definition(): array
{
return [
'organisation_id' => Organisation::factory(),
'name' => fake()->unique()->randomElement([
'Tapper', 'EHBO', 'Kassa ervaring', 'Barhoofd', 'Beveiliging',
'Duits', 'Engels', 'Frans', 'Spaans', 'Italiaans',
'Heftruck', 'VCA', 'BHV', 'Geluidstechniek', 'Lichttechniek',
'Podiummanager', 'Runner', 'Barista', 'Chauffeur', 'Fotograaf',
'Gastheer', 'Logistiek', 'Catering', 'Decoratie', 'Schoonmaak',
'Ticketing', 'Marketing', 'Social media', 'Communicatie', 'Grafisch ontwerp',
'Video', 'Drone piloot', 'Horeca', 'Kassamedewerker', 'Backstage',
'Artiestencoördinator', 'Parkeerplaats', 'Camping', 'Infopunt', 'Kinderanimatie',
]),
'category' => fake()->randomElement(['Vaardigheid', 'Taal', 'Certificaat', null]),
'icon' => fake()->randomElement(['tabler-beer', 'tabler-first-aid-kit', 'tabler-language', null]),
'color' => fake()->optional()->hexColor(),
'is_active' => true,
'sort_order' => fake()->numberBetween(0, 10),
];
}
public function inactive(): static
{
return $this->state(fn () => ['is_active' => false]);
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Database\Factories;
use App\Models\Organisation;
use App\Models\PersonTag;
use App\Models\User;
use App\Models\UserOrganisationTag;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<UserOrganisationTag>
*/
final class UserOrganisationTagFactory extends Factory
{
protected $model = UserOrganisationTag::class;
public function definition(): array
{
return [
'user_id' => User::factory(),
'organisation_id' => Organisation::factory(),
'person_tag_id' => PersonTag::factory(),
'source' => fake()->randomElement(['self_reported', 'organiser_assigned']),
'assigned_by_user_id' => null,
'proficiency' => fake()->optional()->randomElement(['beginner', 'experienced', 'expert']),
'notes' => null,
'assigned_at' => now(),
];
}
public function selfReported(): static
{
return $this->state(fn () => ['source' => 'self_reported']);
}
public function organiserAssigned(): static
{
return $this->state(fn () => ['source' => 'organiser_assigned']);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('person_tags', function (Blueprint $table) {
$table->ulid('id')->primary();
$table->foreignUlid('organisation_id')->constrained()->cascadeOnDelete();
$table->string('name', 50);
$table->string('category', 50)->nullable();
$table->string('icon', 50)->nullable();
$table->string('color', 7)->nullable();
$table->boolean('is_active')->default(true);
$table->integer('sort_order')->default(0);
$table->timestamps();
$table->index(['organisation_id', 'is_active', 'sort_order']);
$table->unique(['organisation_id', 'name']);
});
}
public function down(): void
{
Schema::dropIfExists('person_tags');
}
};

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('user_organisation_tags', function (Blueprint $table) {
$table->id();
$table->foreignUlid('user_id')->constrained()->cascadeOnDelete();
$table->foreignUlid('organisation_id')->constrained()->cascadeOnDelete();
$table->foreignUlid('person_tag_id')->constrained()->cascadeOnDelete();
$table->enum('source', ['self_reported', 'organiser_assigned']);
$table->foreignUlid('assigned_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->enum('proficiency', ['beginner', 'experienced', 'expert'])->nullable();
$table->text('notes')->nullable();
$table->timestamp('assigned_at');
$table->unique(['user_id', 'organisation_id', 'person_tag_id', 'source'], 'uot_user_org_tag_source_unique');
$table->index(['user_id', 'organisation_id']);
$table->index('person_tag_id');
$table->index(['organisation_id', 'person_tag_id', 'proficiency'], 'uot_org_tag_proficiency_index');
});
}
public function down(): void
{
Schema::dropIfExists('user_organisation_tags');
}
};