feat: ticket views, statuses, participants, merge; mobile layout fixes (#5)
- New migrations: ticket views, statuses, participants, merge support - New models: TicketView, TicketStatus, TicketParticipant - New seeder: EmailTemplatesSeeder - Console commands for ticketing - Mobile: sidebar min-w-0/overflow-hidden, tab nav overflow-x-auto Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
<?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
|
||||
{
|
||||
// Deduplicate before removing group_id:
|
||||
// If the same priority name exists multiple times (once per group), keep the first
|
||||
// and delete duplicates, then reassign any tickets that referenced the deleted ones.
|
||||
$names = DB::table('ticketing_priority_levels')
|
||||
->select('name')
|
||||
->groupBy('name')
|
||||
->havingRaw('COUNT(*) > 1')
|
||||
->pluck('name');
|
||||
|
||||
foreach ($names as $name) {
|
||||
$rows = DB::table('ticketing_priority_levels')
|
||||
->where('name', $name)
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
$keepId = $rows->first()->id;
|
||||
|
||||
foreach ($rows->skip(1) as $row) {
|
||||
// Reassign tickets pointing at the duplicate
|
||||
DB::table('tickets')
|
||||
->where('priority_id', $row->id)
|
||||
->update(['priority_id' => $keepId]);
|
||||
|
||||
DB::table('ticketing_priority_levels')->where('id', $row->id)->delete();
|
||||
}
|
||||
}
|
||||
|
||||
Schema::table('ticketing_priority_levels', function (Blueprint $table) {
|
||||
// Drop the foreign key and column
|
||||
$table->dropForeign(['group_id']);
|
||||
$table->dropColumn('group_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('ticketing_priority_levels', function (Blueprint $table) {
|
||||
$table->foreignId('group_id')->nullable()->constrained('ticketing_groups')->nullOnDelete();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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('ticket_views', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('ticket_id')->constrained('tickets')->cascadeOnDelete();
|
||||
$table->unsignedBigInteger('user_id');
|
||||
$table->timestamp('viewed_at');
|
||||
$table->index(['ticket_id', 'user_id']);
|
||||
$table->index('viewed_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ticket_views');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
<?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
|
||||
{
|
||||
// 1. Create ticket_participants table
|
||||
Schema::create('ticket_participants', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('ticket_id')->constrained()->cascadeOnDelete();
|
||||
$table->unsignedBigInteger('user_id');
|
||||
$table->unsignedBigInteger('added_by')->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->unique(['ticket_id', 'user_id']);
|
||||
});
|
||||
|
||||
// 2. Add merged_into_id to tickets
|
||||
Schema::table('tickets', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('merged_into_id')->nullable()->after('due_date');
|
||||
$table->foreign('merged_into_id')->references('id')->on('tickets')->nullOnDelete();
|
||||
});
|
||||
|
||||
// 3. Expand status ENUM to include 'merged'
|
||||
DB::statement("ALTER TABLE tickets MODIFY COLUMN status ENUM('open','in_progress','pending','resolved','closed','merged') DEFAULT 'open'");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Reverse ENUM change (remove merged)
|
||||
DB::statement("UPDATE tickets SET status = 'closed' WHERE status = 'merged'");
|
||||
DB::statement("ALTER TABLE tickets MODIFY COLUMN status ENUM('open','in_progress','pending','resolved','closed') DEFAULT 'open'");
|
||||
|
||||
Schema::table('tickets', function (Blueprint $table) {
|
||||
$table->dropForeign(['merged_into_id']);
|
||||
$table->dropColumn('merged_into_id');
|
||||
});
|
||||
|
||||
Schema::dropIfExists('ticket_participants');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
<?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
|
||||
{
|
||||
// 1. Create ticketing_statuses table
|
||||
Schema::create('ticketing_statuses', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('slug', 50)->unique(); // machine key — immutable after creation
|
||||
$table->string('name', 100); // user-facing display name
|
||||
$table->string('color', 7)->default('#6b7280'); // hex color
|
||||
$table->unsignedSmallInteger('sort_order')->default(0);
|
||||
$table->boolean('is_closed')->default(false); // counts as "resolved/closed" in filters
|
||||
$table->boolean('is_system')->default(false); // cannot be deleted
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 2. Seed default statuses
|
||||
$now = now();
|
||||
DB::table('ticketing_statuses')->insert([
|
||||
['slug' => 'open', 'name' => 'Open', 'color' => '#3b82f6', 'sort_order' => 1, 'is_closed' => false, 'is_system' => true, 'created_at' => $now, 'updated_at' => $now],
|
||||
['slug' => 'in_progress', 'name' => 'In Progress', 'color' => '#7c3aed', 'sort_order' => 2, 'is_closed' => false, 'is_system' => true, 'created_at' => $now, 'updated_at' => $now],
|
||||
['slug' => 'pending', 'name' => 'Pending', 'color' => '#d97706', 'sort_order' => 3, 'is_closed' => false, 'is_system' => true, 'created_at' => $now, 'updated_at' => $now],
|
||||
['slug' => 'resolved', 'name' => 'Resolved', 'color' => '#16a34a', 'sort_order' => 4, 'is_closed' => true, 'is_system' => true, 'created_at' => $now, 'updated_at' => $now],
|
||||
['slug' => 'closed', 'name' => 'Closed', 'color' => '#6b7280', 'sort_order' => 5, 'is_closed' => true, 'is_system' => true, 'created_at' => $now, 'updated_at' => $now],
|
||||
['slug' => 'merged', 'name' => 'Merged', 'color' => '#9ca3af', 'sort_order' => 99, 'is_closed' => true, 'is_system' => true, 'created_at' => $now, 'updated_at' => $now],
|
||||
]);
|
||||
|
||||
// 3. Change tickets.status from ENUM to VARCHAR(50) — existing slug values are kept as-is
|
||||
DB::statement("ALTER TABLE tickets MODIFY COLUMN status VARCHAR(50) NOT NULL DEFAULT 'open'");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Restore ENUM (data already uses these slugs so no data loss)
|
||||
DB::statement("ALTER TABLE tickets MODIFY COLUMN status ENUM('open','in_progress','pending','resolved','closed','merged') DEFAULT 'open'");
|
||||
|
||||
Schema::dropIfExists('ticketing_statuses');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user