Commit workspace changes

This commit is contained in:
2026-04-05 19:42:33 +02:00
parent 148a3bbe43
commit 08ad757bcb
312 changed files with 35149 additions and 399 deletions

View File

@@ -0,0 +1,96 @@
<?php
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('groups', function (Blueprint $table): void {
$table->id();
$table->foreignId('owner_user_id')
->constrained('users')
->cascadeOnDelete();
$table->string('name', 80);
$table->string('slug', 90)->unique();
$table->string('headline', 160)->nullable();
$table->text('bio')->nullable();
$table->enum('visibility', ['public', 'private'])->default('public');
$table->string('website_url', 2048)->nullable();
$table->json('links_json')->nullable();
$table->string('avatar_path')->nullable();
$table->string('banner_path')->nullable();
$table->unsignedInteger('artworks_count')->default(0);
$table->unsignedInteger('collections_count')->default(0);
$table->unsignedInteger('followers_count')->default(0);
$table->timestamp('last_activity_at')->nullable();
$table->timestamps();
$table->index(['visibility', 'followers_count']);
$table->index(['owner_user_id', 'created_at']);
});
Schema::create('group_members', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_id')
->constrained('groups')
->cascadeOnDelete();
$table->foreignId('user_id')
->constrained('users')
->cascadeOnDelete();
$table->foreignId('invited_by_user_id')
->nullable()
->constrained('users')
->nullOnDelete();
$table->enum('role', ['owner', 'admin', 'editor', 'member'])->default('member');
$table->enum('status', ['pending', 'active', 'revoked'])->default('pending');
$table->text('note')->nullable();
$table->timestamp('invited_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamp('accepted_at')->nullable();
$table->timestamp('revoked_at')->nullable();
$table->timestamps();
$table->unique(['group_id', 'user_id'], 'group_members_group_user_unique');
$table->index(['group_id', 'status', 'role'], 'group_members_status_role_idx');
});
Schema::create('group_follows', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_id')
->constrained('groups')
->cascadeOnDelete();
$table->foreignId('user_id')
->constrained('users')
->cascadeOnDelete();
$table->timestamps();
$table->unique(['group_id', 'user_id'], 'group_follows_group_user_unique');
});
Schema::create('artwork_contributors', function (Blueprint $table): void {
$table->id();
$table->foreignId('artwork_id')
->constrained('artworks')
->cascadeOnDelete();
$table->foreignId('user_id')
->constrained('users')
->cascadeOnDelete();
$table->unsignedInteger('sort_order')->default(0);
$table->timestamps();
$table->unique(['artwork_id', 'user_id'], 'artwork_contributors_artwork_user_unique');
$table->index(['artwork_id', 'sort_order'], 'artwork_contributors_sort_idx');
});
}
public function down(): void
{
Schema::dropIfExists('artwork_contributors');
Schema::dropIfExists('group_follows');
Schema::dropIfExists('group_members');
Schema::dropIfExists('groups');
}
};

View File

@@ -0,0 +1,56 @@
<?php
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::table('artworks', function (Blueprint $table): void {
$table->foreignId('group_id')
->nullable()
->after('user_id')
->constrained('groups')
->nullOnDelete();
$table->foreignId('uploaded_by_user_id')
->nullable()
->after('group_id')
->constrained('users')
->nullOnDelete();
$table->foreignId('primary_author_user_id')
->nullable()
->after('uploaded_by_user_id')
->constrained('users')
->nullOnDelete();
$table->index(['group_id', 'published_at'], 'artworks_group_published_idx');
});
Schema::table('collections', function (Blueprint $table): void {
$table->foreignId('group_id')
->nullable()
->after('user_id')
->constrained('groups')
->nullOnDelete();
$table->index(['group_id', 'visibility'], 'collections_group_visibility_idx');
});
}
public function down(): void
{
Schema::table('collections', function (Blueprint $table): void {
$table->dropIndex('collections_group_visibility_idx');
$table->dropConstrainedForeignId('group_id');
});
Schema::table('artworks', function (Blueprint $table): void {
$table->dropIndex('artworks_group_published_idx');
$table->dropConstrainedForeignId('primary_author_user_id');
$table->dropConstrainedForeignId('uploaded_by_user_id');
$table->dropConstrainedForeignId('group_id');
});
}
};

View File

@@ -0,0 +1,83 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('groups', function (Blueprint $table): void {
if (! Schema::hasColumn('groups', 'status')) {
$table->string('status', 24)->default('active')->after('visibility');
}
if (! Schema::hasColumn('groups', 'membership_policy')) {
$table->string('membership_policy', 32)->default('invite_only')->after('status');
}
if (! Schema::hasColumn('groups', 'type')) {
$table->string('type', 80)->nullable()->after('bio');
}
if (! Schema::hasColumn('groups', 'founded_at')) {
$table->timestamp('founded_at')->nullable()->after('owner_user_id');
}
if (! Schema::hasColumn('groups', 'featured_artwork_id')) {
$table->foreignId('featured_artwork_id')->nullable()->after('owner_user_id')->constrained('artworks')->nullOnDelete();
}
if (! Schema::hasColumn('groups', 'is_verified')) {
$table->boolean('is_verified')->default(false)->after('featured_artwork_id');
}
if (! Schema::hasColumn('groups', 'deleted_at')) {
$table->softDeletes();
}
});
if (DB::connection()->getDriverName() === 'mysql') {
DB::statement("ALTER TABLE `groups` MODIFY `visibility` ENUM('public','private','unlisted') NOT NULL DEFAULT 'public'");
}
}
public function down(): void
{
if (DB::connection()->getDriverName() === 'mysql') {
DB::statement("ALTER TABLE `groups` MODIFY `visibility` ENUM('public','private') NOT NULL DEFAULT 'public'");
}
Schema::table('groups', function (Blueprint $table): void {
if (Schema::hasColumn('groups', 'deleted_at')) {
$table->dropSoftDeletes();
}
if (Schema::hasColumn('groups', 'is_verified')) {
$table->dropColumn('is_verified');
}
if (Schema::hasColumn('groups', 'featured_artwork_id')) {
$table->dropConstrainedForeignId('featured_artwork_id');
}
if (Schema::hasColumn('groups', 'founded_at')) {
$table->dropColumn('founded_at');
}
if (Schema::hasColumn('groups', 'type')) {
$table->dropColumn('type');
}
if (Schema::hasColumn('groups', 'membership_policy')) {
$table->dropColumn('membership_policy');
}
if (Schema::hasColumn('groups', 'status')) {
$table->dropColumn('status');
}
});
}
};

View File

@@ -0,0 +1,89 @@
<?php
use App\Models\GroupInvitation;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
return new class extends Migration
{
public function up(): void
{
Schema::create('group_invitations', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_id')
->constrained('groups')
->cascadeOnDelete();
$table->foreignId('invited_user_id')
->constrained('users')
->cascadeOnDelete();
$table->foreignId('invited_by_user_id')
->nullable()
->constrained('users')
->nullOnDelete();
$table->foreignId('source_group_member_id')
->nullable()
->constrained('group_members')
->nullOnDelete();
$table->enum('role', ['admin', 'editor', 'member'])->default('member');
$table->enum('status', [
GroupInvitation::STATUS_PENDING,
GroupInvitation::STATUS_ACCEPTED,
GroupInvitation::STATUS_DECLINED,
GroupInvitation::STATUS_REVOKED,
GroupInvitation::STATUS_EXPIRED,
])->default(GroupInvitation::STATUS_PENDING);
$table->string('token', 80)->unique();
$table->text('note')->nullable();
$table->timestamp('invited_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamp('responded_at')->nullable();
$table->timestamp('accepted_at')->nullable();
$table->timestamp('revoked_at')->nullable();
$table->timestamps();
$table->index(['group_id', 'status', 'role'], 'group_invitations_status_role_idx');
$table->index(['invited_user_id', 'status'], 'group_invitations_user_status_idx');
});
if (! Schema::hasTable('group_members')) {
return;
}
DB::table('group_members')
->whereIn('status', ['pending', 'revoked'])
->orderBy('id')
->get()
->each(function (object $member): void {
$status = match ((string) $member->status) {
'pending' => GroupInvitation::STATUS_PENDING,
default => GroupInvitation::STATUS_REVOKED,
};
DB::table('group_invitations')->insert([
'group_id' => (int) $member->group_id,
'invited_user_id' => (int) $member->user_id,
'invited_by_user_id' => $member->invited_by_user_id ? (int) $member->invited_by_user_id : null,
'source_group_member_id' => (int) $member->id,
'role' => (string) $member->role,
'status' => $status,
'token' => Str::random(64),
'note' => $member->note,
'invited_at' => $member->invited_at,
'expires_at' => $member->expires_at,
'responded_at' => $member->accepted_at ?? $member->revoked_at,
'accepted_at' => null,
'revoked_at' => $member->revoked_at,
'created_at' => $member->created_at ?? now(),
'updated_at' => $member->updated_at ?? now(),
]);
});
}
public function down(): void
{
Schema::dropIfExists('group_invitations');
}
};

View File

@@ -0,0 +1,45 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('artworks', function (Blueprint $table): void {
$table->string('published_as_type', 16)
->nullable()
->after('primary_author_user_id');
$table->unsignedBigInteger('published_as_id')
->nullable()
->after('published_as_type');
$table->index(['published_as_type', 'published_as_id'], 'artworks_published_as_idx');
});
DB::table('artworks')
->whereNotNull('group_id')
->update([
'published_as_type' => 'group',
'published_as_id' => DB::raw('group_id'),
]);
DB::table('artworks')
->whereNull('published_as_id')
->update([
'published_as_type' => 'user',
'published_as_id' => DB::raw('user_id'),
]);
}
public function down(): void
{
Schema::table('artworks', function (Blueprint $table): void {
$table->dropIndex('artworks_published_as_idx');
$table->dropColumn(['published_as_type', 'published_as_id']);
});
}
};

View File

@@ -0,0 +1,23 @@
<?php
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::table('artwork_contributors', function (Blueprint $table): void {
$table->string('credit_role', 80)->nullable()->after('user_id');
$table->boolean('is_primary')->default(false)->after('credit_role');
});
}
public function down(): void
{
Schema::table('artwork_contributors', function (Blueprint $table): void {
$table->dropColumn(['credit_role', 'is_primary']);
});
}
};

View File

@@ -0,0 +1,163 @@
<?php
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('group_join_requests', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_id')
->constrained('groups')
->cascadeOnDelete();
$table->foreignId('user_id')
->constrained('users')
->cascadeOnDelete();
$table->text('message')->nullable();
$table->string('portfolio_url', 2048)->nullable();
$table->string('desired_role', 32)->nullable();
$table->json('skills_json')->nullable();
$table->string('status', 24)->default('pending');
$table->foreignId('reviewed_by_user_id')
->nullable()
->constrained('users')
->nullOnDelete();
$table->text('review_notes')->nullable();
$table->timestamp('reviewed_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
$table->index(['group_id', 'status', 'created_at'], 'group_join_requests_group_status_idx');
$table->index(['group_id', 'user_id', 'status'], 'group_join_requests_group_user_status_idx');
});
Schema::create('group_posts', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_id')
->constrained('groups')
->cascadeOnDelete();
$table->foreignId('author_user_id')
->constrained('users')
->cascadeOnDelete();
$table->string('type', 32)->default('announcement');
$table->string('title', 180);
$table->string('slug', 190)->unique();
$table->string('excerpt', 320)->nullable();
$table->longText('content')->nullable();
$table->string('cover_path', 2048)->nullable();
$table->string('status', 24)->default('draft');
$table->boolean('is_pinned')->default(false);
$table->timestamp('published_at')->nullable();
$table->softDeletes();
$table->timestamps();
$table->index(['group_id', 'status', 'published_at'], 'group_posts_group_status_published_idx');
$table->index(['group_id', 'is_pinned', 'published_at'], 'group_posts_group_pinned_idx');
});
Schema::create('group_recruitment_profiles', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_id')
->constrained('groups')
->cascadeOnDelete();
$table->boolean('is_recruiting')->default(false);
$table->string('headline', 180)->nullable();
$table->text('description')->nullable();
$table->json('roles_json')->nullable();
$table->json('skills_json')->nullable();
$table->string('contact_mode', 32)->nullable();
$table->string('visibility', 24)->default('public');
$table->timestamps();
$table->unique('group_id', 'group_recruitment_profiles_group_unique');
});
Schema::create('group_histories', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_id')
->constrained('groups')
->cascadeOnDelete();
$table->foreignId('actor_user_id')
->nullable()
->constrained('users')
->nullOnDelete();
$table->string('action_type', 64);
$table->string('target_type', 64)->nullable();
$table->unsignedBigInteger('target_id')->nullable();
$table->string('summary', 255)->nullable();
$table->json('before_json')->nullable();
$table->json('after_json')->nullable();
$table->timestamp('created_at')->nullable();
$table->index(['group_id', 'action_type', 'created_at'], 'group_histories_group_action_idx');
$table->index(['group_id', 'target_type', 'target_id'], 'group_histories_target_idx');
});
Schema::table('group_members', function (Blueprint $table): void {
if (! Schema::hasColumn('group_members', 'permission_overrides_json')) {
$table->json('permission_overrides_json')->nullable()->after('note');
}
});
Schema::table('artworks', function (Blueprint $table): void {
if (! Schema::hasColumn('artworks', 'group_review_status')) {
$table->string('group_review_status', 24)->default('none')->after('artwork_status');
}
if (! Schema::hasColumn('artworks', 'group_review_submitted_at')) {
$table->timestamp('group_review_submitted_at')->nullable()->after('group_review_status');
}
if (! Schema::hasColumn('artworks', 'group_reviewed_by_user_id')) {
$table->foreignId('group_reviewed_by_user_id')
->nullable()
->after('group_review_submitted_at')
->constrained('users')
->nullOnDelete();
}
if (! Schema::hasColumn('artworks', 'group_reviewed_at')) {
$table->timestamp('group_reviewed_at')->nullable()->after('group_reviewed_by_user_id');
}
if (! Schema::hasColumn('artworks', 'group_review_notes')) {
$table->text('group_review_notes')->nullable()->after('group_reviewed_at');
}
$table->index(['group_id', 'group_review_status', 'group_review_submitted_at'], 'artworks_group_review_queue_idx');
});
}
public function down(): void
{
Schema::table('artworks', function (Blueprint $table): void {
if (Schema::hasColumn('artworks', 'group_reviewed_by_user_id')) {
$table->dropConstrainedForeignId('group_reviewed_by_user_id');
}
if (Schema::hasColumn('artworks', 'group_review_status')) {
$table->dropIndex('artworks_group_review_queue_idx');
$table->dropColumn([
'group_review_status',
'group_review_submitted_at',
'group_reviewed_at',
'group_review_notes',
]);
}
});
Schema::table('group_members', function (Blueprint $table): void {
if (Schema::hasColumn('group_members', 'permission_overrides_json')) {
$table->dropColumn('permission_overrides_json');
}
});
Schema::dropIfExists('group_histories');
Schema::dropIfExists('group_recruitment_profiles');
Schema::dropIfExists('group_posts');
Schema::dropIfExists('group_join_requests');
}
};

View File

@@ -0,0 +1,180 @@
<?php
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('group_projects', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
$table->string('title', 180);
$table->string('slug', 190)->unique();
$table->string('summary', 320)->nullable();
$table->longText('description')->nullable();
$table->string('cover_path', 2048)->nullable();
$table->string('status', 24)->default('planned');
$table->string('visibility', 24)->default('public');
$table->date('start_date')->nullable();
$table->date('target_date')->nullable();
$table->timestamp('released_at')->nullable();
$table->foreignId('created_by_user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('lead_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('linked_collection_id')->nullable()->constrained('collections')->nullOnDelete();
$table->foreignId('linked_featured_artwork_id')->nullable()->constrained('artworks')->nullOnDelete();
$table->foreignId('pinned_post_id')->nullable()->constrained('group_posts')->nullOnDelete();
$table->softDeletes();
$table->timestamps();
$table->index(['group_id', 'status', 'visibility'], 'group_projects_group_status_visibility_idx');
$table->index(['group_id', 'released_at'], 'group_projects_group_released_idx');
});
Schema::create('group_project_artworks', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_project_id')->constrained('group_projects')->cascadeOnDelete();
$table->foreignId('artwork_id')->constrained('artworks')->cascadeOnDelete();
$table->unsignedInteger('sort_order')->default(0);
$table->timestamps();
$table->unique(['group_project_id', 'artwork_id'], 'group_project_artworks_unique');
$table->index(['group_project_id', 'sort_order'], 'group_project_artworks_sort_idx');
});
Schema::create('group_project_members', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_project_id')->constrained('group_projects')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->string('role_label', 80)->nullable();
$table->boolean('is_lead')->default(false);
$table->timestamps();
$table->unique(['group_project_id', 'user_id'], 'group_project_members_unique');
$table->index(['group_project_id', 'is_lead'], 'group_project_members_lead_idx');
});
Schema::create('group_challenges', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
$table->string('title', 180);
$table->string('slug', 190)->unique();
$table->string('summary', 320)->nullable();
$table->longText('description')->nullable();
$table->string('cover_path', 2048)->nullable();
$table->string('visibility', 24)->default('public');
$table->string('participation_scope', 24)->default('group_only');
$table->string('status', 24)->default('draft');
$table->timestamp('start_at')->nullable();
$table->timestamp('end_at')->nullable();
$table->text('rules_text')->nullable();
$table->text('submission_instructions')->nullable();
$table->string('judging_mode', 32)->nullable();
$table->foreignId('linked_collection_id')->nullable()->constrained('collections')->nullOnDelete();
$table->foreignId('linked_project_id')->nullable()->constrained('group_projects')->nullOnDelete();
$table->foreignId('created_by_user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('featured_artwork_id')->nullable()->constrained('artworks')->nullOnDelete();
$table->softDeletes();
$table->timestamps();
$table->index(['group_id', 'status', 'visibility'], 'group_challenges_group_status_visibility_idx');
$table->index(['group_id', 'start_at', 'end_at'], 'group_challenges_group_window_idx');
});
Schema::create('group_challenge_artworks', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_challenge_id')->constrained('group_challenges')->cascadeOnDelete();
$table->foreignId('artwork_id')->constrained('artworks')->cascadeOnDelete();
$table->foreignId('submitted_by_user_id')->constrained('users')->cascadeOnDelete();
$table->unsignedInteger('sort_order')->default(0);
$table->timestamps();
$table->unique(['group_challenge_id', 'artwork_id'], 'group_challenge_artworks_unique');
$table->index(['group_challenge_id', 'sort_order'], 'group_challenge_artworks_sort_idx');
});
Schema::create('group_events', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
$table->string('title', 180);
$table->string('slug', 190)->unique();
$table->string('summary', 320)->nullable();
$table->longText('description')->nullable();
$table->string('event_type', 32)->default('launch');
$table->string('visibility', 24)->default('public');
$table->timestamp('start_at')->nullable();
$table->timestamp('end_at')->nullable();
$table->string('timezone', 80)->default('UTC');
$table->string('cover_path', 2048)->nullable();
$table->string('location', 180)->nullable();
$table->string('external_url', 2048)->nullable();
$table->foreignId('linked_project_id')->nullable()->constrained('group_projects')->nullOnDelete();
$table->foreignId('linked_collection_id')->nullable()->constrained('collections')->nullOnDelete();
$table->foreignId('linked_challenge_id')->nullable()->constrained('group_challenges')->nullOnDelete();
$table->string('status', 24)->default('draft');
$table->boolean('is_featured')->default(false);
$table->foreignId('created_by_user_id')->constrained('users')->cascadeOnDelete();
$table->timestamp('published_at')->nullable();
$table->softDeletes();
$table->timestamps();
$table->index(['group_id', 'status', 'visibility'], 'group_events_group_status_visibility_idx');
$table->index(['group_id', 'start_at'], 'group_events_group_start_idx');
});
Schema::create('group_assets', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
$table->string('title', 180);
$table->text('description')->nullable();
$table->string('category', 32)->default('misc');
$table->string('file_path', 2048);
$table->string('preview_path', 2048)->nullable();
$table->string('visibility', 24)->default('members_only');
$table->string('status', 24)->default('active');
$table->foreignId('linked_project_id')->nullable()->constrained('group_projects')->nullOnDelete();
$table->foreignId('uploaded_by_user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('approved_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->boolean('is_featured')->default(false);
$table->json('file_meta_json')->nullable();
$table->softDeletes();
$table->timestamps();
$table->index(['group_id', 'visibility', 'status'], 'group_assets_group_visibility_status_idx');
$table->index(['group_id', 'category'], 'group_assets_group_category_idx');
});
Schema::create('group_activity_items', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
$table->string('type', 48);
$table->string('visibility', 24)->default('public');
$table->foreignId('actor_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('subject_type', 64);
$table->unsignedBigInteger('subject_id')->nullable();
$table->string('headline', 255);
$table->string('summary', 500)->nullable();
$table->boolean('is_pinned')->default(false);
$table->timestamp('occurred_at');
$table->timestamps();
$table->index(['group_id', 'visibility', 'occurred_at'], 'group_activity_items_group_visibility_occurred_idx');
$table->index(['group_id', 'type', 'occurred_at'], 'group_activity_items_group_type_occurred_idx');
$table->index(['group_id', 'subject_type', 'subject_id'], 'group_activity_items_subject_idx');
});
}
public function down(): void
{
Schema::dropIfExists('group_activity_items');
Schema::dropIfExists('group_assets');
Schema::dropIfExists('group_events');
Schema::dropIfExists('group_challenge_artworks');
Schema::dropIfExists('group_challenges');
Schema::dropIfExists('group_project_members');
Schema::dropIfExists('group_project_artworks');
Schema::dropIfExists('group_projects');
}
};

View File

@@ -0,0 +1,164 @@
<?php
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('group_releases', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
$table->string('title', 180);
$table->string('slug', 190)->unique();
$table->string('summary', 320)->nullable();
$table->longText('description')->nullable();
$table->string('cover_path', 2048)->nullable();
$table->string('status', 32)->default('planned');
$table->string('current_stage', 32)->default('concept');
$table->string('visibility', 24)->default('public');
$table->timestamp('planned_release_at')->nullable();
$table->timestamp('released_at')->nullable();
$table->foreignId('lead_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('linked_project_id')->nullable()->constrained('group_projects')->nullOnDelete();
$table->foreignId('linked_collection_id')->nullable()->constrained('collections')->nullOnDelete();
$table->foreignId('featured_artwork_id')->nullable()->constrained('artworks')->nullOnDelete();
$table->longText('release_notes')->nullable();
$table->foreignId('created_by_user_id')->constrained('users')->cascadeOnDelete();
$table->timestamp('published_at')->nullable();
$table->boolean('is_featured')->default(false);
$table->softDeletes();
$table->timestamps();
$table->index(['group_id', 'status', 'visibility'], 'group_releases_group_status_visibility_idx');
$table->index(['group_id', 'current_stage'], 'group_releases_group_stage_idx');
$table->index(['group_id', 'planned_release_at'], 'group_releases_group_planned_idx');
$table->index(['group_id', 'released_at'], 'group_releases_group_released_idx');
});
Schema::create('group_release_artworks', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_release_id')->constrained('group_releases')->cascadeOnDelete();
$table->foreignId('artwork_id')->constrained('artworks')->cascadeOnDelete();
$table->unsignedInteger('sort_order')->default(0);
$table->timestamps();
$table->unique(['group_release_id', 'artwork_id'], 'group_release_artworks_unique');
$table->index(['group_release_id', 'sort_order'], 'group_release_artworks_sort_idx');
});
Schema::create('group_release_contributors', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_release_id')->constrained('group_releases')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->string('role_label', 80)->nullable();
$table->unsignedInteger('sort_order')->default(0);
$table->timestamps();
$table->unique(['group_release_id', 'user_id'], 'group_release_contributors_unique');
$table->index(['group_release_id', 'sort_order'], 'group_release_contributors_sort_idx');
});
Schema::create('group_project_milestones', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_project_id')->constrained('group_projects')->cascadeOnDelete();
$table->string('title', 180);
$table->string('summary', 320)->nullable();
$table->string('status', 24)->default('pending');
$table->date('due_date')->nullable();
$table->foreignId('owner_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->unsignedInteger('sort_order')->default(0);
$table->text('notes')->nullable();
$table->timestamps();
$table->index(['group_project_id', 'status'], 'group_project_milestones_project_status_idx');
$table->index(['group_project_id', 'due_date'], 'group_project_milestones_project_due_idx');
});
Schema::create('group_release_milestones', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_release_id')->constrained('group_releases')->cascadeOnDelete();
$table->string('title', 180);
$table->string('summary', 320)->nullable();
$table->string('status', 24)->default('pending');
$table->date('due_date')->nullable();
$table->foreignId('owner_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->unsignedInteger('sort_order')->default(0);
$table->text('notes')->nullable();
$table->timestamps();
$table->index(['group_release_id', 'status'], 'group_release_milestones_release_status_idx');
$table->index(['group_release_id', 'due_date'], 'group_release_milestones_release_due_idx');
});
Schema::create('group_contributor_stats', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->unsignedInteger('credited_artworks_count')->default(0);
$table->unsignedInteger('release_count')->default(0);
$table->unsignedInteger('project_count')->default(0);
$table->unsignedInteger('review_actions_count')->default(0);
$table->unsignedInteger('approved_submissions_count')->default(0);
$table->json('reputation_meta_json')->nullable();
$table->timestamps();
$table->unique(['group_id', 'user_id'], 'group_contributor_stats_group_user_unique');
$table->index(['group_id', 'release_count'], 'group_contributor_stats_group_release_idx');
$table->index(['group_id', 'credited_artworks_count'], 'group_contributor_stats_group_artworks_idx');
});
Schema::create('group_badges', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
$table->string('badge_key', 80);
$table->timestamp('awarded_at');
$table->json('meta_json')->nullable();
$table->timestamps();
$table->unique(['group_id', 'badge_key'], 'group_badges_group_badge_unique');
});
Schema::create('group_member_badges', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->string('badge_key', 80);
$table->timestamp('awarded_at');
$table->json('meta_json')->nullable();
$table->timestamps();
$table->unique(['group_id', 'user_id', 'badge_key'], 'group_member_badges_group_user_badge_unique');
$table->index(['group_id', 'user_id'], 'group_member_badges_group_user_idx');
});
Schema::create('group_discovery_metrics', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
$table->decimal('freshness_score', 8, 2)->default(0);
$table->decimal('activity_score', 8, 2)->default(0);
$table->decimal('release_score', 8, 2)->default(0);
$table->decimal('trust_score', 8, 2)->default(0);
$table->decimal('collaboration_score', 8, 2)->default(0);
$table->timestamp('last_calculated_at')->nullable();
$table->timestamps();
$table->unique('group_id', 'group_discovery_metrics_group_unique');
});
}
public function down(): void
{
Schema::dropIfExists('group_discovery_metrics');
Schema::dropIfExists('group_member_badges');
Schema::dropIfExists('group_badges');
Schema::dropIfExists('group_contributor_stats');
Schema::dropIfExists('group_release_milestones');
Schema::dropIfExists('group_project_milestones');
Schema::dropIfExists('group_release_contributors');
Schema::dropIfExists('group_release_artworks');
Schema::dropIfExists('group_releases');
}
};

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (Schema::hasTable('news_articles')) {
$this->addNewsArticleColumns();
$this->backfillNewsArticleColumns();
}
if (! Schema::hasTable('news_article_relations')) {
Schema::create('news_article_relations', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('article_id')->index();
$table->string('entity_type', 40)->index();
$table->unsignedBigInteger('entity_id')->index();
$table->string('context_label', 120)->nullable();
$table->unsignedSmallInteger('sort_order')->default(0);
$table->timestamps();
$table->foreign('article_id')->references('id')->on('news_articles')->onDelete('cascade');
$table->unique(['article_id', 'entity_type', 'entity_id'], 'news_article_relations_unique');
});
}
}
public function down(): void
{
Schema::dropIfExists('news_article_relations');
if (! Schema::hasTable('news_articles')) {
return;
}
$columns = [
'editorial_status',
'type',
'is_pinned',
'canonical_url',
];
$existing = array_values(array_filter($columns, static fn (string $column): bool => Schema::hasColumn('news_articles', $column)));
if ($existing !== []) {
Schema::table('news_articles', function (Blueprint $table) use ($existing): void {
$table->dropColumn($existing);
});
}
}
private function addNewsArticleColumns(): void
{
$needsTableChange = false;
foreach (['editorial_status', 'type', 'is_pinned', 'canonical_url'] as $column) {
if (! Schema::hasColumn('news_articles', $column)) {
$needsTableChange = true;
break;
}
}
if (! $needsTableChange) {
return;
}
Schema::table('news_articles', function (Blueprint $table): void {
if (! Schema::hasColumn('news_articles', 'editorial_status')) {
$table->string('editorial_status', 30)->default('draft')->after('status')->index();
}
if (! Schema::hasColumn('news_articles', 'type')) {
$table->string('type', 40)->default('announcement')->after('cover_image')->index();
}
if (! Schema::hasColumn('news_articles', 'is_pinned')) {
$table->boolean('is_pinned')->default(false)->after('is_featured')->index();
}
if (! Schema::hasColumn('news_articles', 'canonical_url')) {
$table->string('canonical_url')->nullable()->after('meta_description');
}
});
}
private function backfillNewsArticleColumns(): void
{
DB::table('news_articles')
->select(['id', 'status', 'editorial_status', 'type'])
->orderBy('id')
->chunkById(200, function ($rows): void {
foreach ($rows as $row) {
$payload = [];
if (property_exists($row, 'editorial_status') && ($row->editorial_status === null || $row->editorial_status === '')) {
$payload['editorial_status'] = match ((string) $row->status) {
'published' => 'published',
'scheduled' => 'scheduled',
default => 'draft',
};
}
if (property_exists($row, 'type') && ($row->type === null || $row->type === '')) {
$payload['type'] = 'announcement';
}
if ($payload !== []) {
DB::table('news_articles')->where('id', $row->id)->update($payload);
}
}
});
}
};