feat: Add many-to-many relationship for Issue Management with Departments, including migration, Filament UI updates, and a new department issue chart.

This commit is contained in:
Yoga Pangestu 2026-03-26 10:24:03 +07:00
parent 89b4793667
commit 01db1c4794
10 changed files with 187 additions and 1 deletions

View File

@ -35,13 +35,13 @@ public function handle()
'migrate:classification' => '🏷️ Migrating classifications & sub-classifications...',
'migrate:theme' => '🎨 Migrating themes...',
'migrate:media-monitoring' => '🖥️ Migrating media monitoring...',
'migrate:issue-management' => '⚠️ Migrating issue management...',
'migrate:company' => '🏭 Migrating companies & partner media...',
'migrate:journalist' => '📰 Migrating journalists...',
'migrate:news' => '🗞️ Migrating news...',
'migrate:announcement' => '📢 Migrating announcements...',
'migrate:cooperation' => '🤝 Migrating cooperations, proposals & reports...',
'migrate:content-recap' => '📊 Migrating content recaps...',
'migrate:issue-management' => '⚠️ Migrating issue management...',
];
foreach ($commands as $command => $description) {

View File

@ -2,9 +2,12 @@
namespace App\Console\Commands;
use App\Enums\IsActive;
use App\Enums\IssueSentiment;
use App\Models\Department;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class MigrateIssueManagementCommand extends Command
{
@ -57,6 +60,38 @@ public function handle()
'deleted_at' => $legacy->deleted_at,
]
);
// Handle Departments (Legacy 'agency' field is string, can be multiple separated by comma)
$agencyString = trim($legacy->agency);
if (! empty($agencyString)) {
$agencyNames = explode(',', $agencyString);
foreach ($agencyNames as $agencyName) {
$agencyName = trim($agencyName);
if (empty($agencyName)) {
continue;
}
$department = Department::firstOrCreate(
['name' => $agencyName],
[
'alias' => Str::limit($agencyName, 20, ''),
'is_active' => IsActive::ACTIVE,
]
);
DB::table('department_issue_management')->updateOrInsert(
[
'department_id' => $department->id,
'issue_management_id' => $legacy->id,
],
[
'created_at' => now(),
'updated_at' => now(),
]
);
}
}
});
$this->newLine();

View File

@ -32,6 +32,14 @@ public static function configure(Schema $schema): Schema
->autofocus()
->helperText('Jika judul berita dari monitoring media tidak ada di opsi, silakan lakukan pencarian.'),
Select::make('departments')
->label('Perangkat Daerah')
->relationship('departments', 'name', fn (Builder $query): Builder => $query->active())
->searchable()
->preload()
->multiple()
->required(),
Grid::make(2)
->schema([
Select::make('location_id')

View File

@ -50,6 +50,12 @@ public static function configure(Table $table): Table
->searchable()
->sortable(),
TextColumn::make('departments.name')
->label('Perangkat Daerah')
->badge()
->searchable()
->sortable(),
TextColumn::make('location.name')
->label('Lokus')
->searchable()
@ -101,6 +107,14 @@ public static function configure(Table $table): Table
->preload()
->native(false),
SelectFilter::make('departments')
->label('Perangkat Daerah')
->relationship('departments', 'name')
->searchable()
->preload()
->multiple()
->native(false),
SelectFilter::make('classification')
->label('Klasifikasi')
->relationship('classification', 'name')

View File

@ -0,0 +1,71 @@
<?php
namespace App\Filament\Widgets\Charts;
use App\Models\Department;
use App\Models\IssueManagement;
use BezhanSalleh\FilamentShield\Traits\HasWidgetShield;
use Filament\Widgets\ChartWidget;
use Filament\Widgets\Concerns\InteractsWithPageFilters;
use Illuminate\Support\Facades\DB;
class IssueDepartmentChart extends ChartWidget
{
use HasWidgetShield, InteractsWithPageFilters;
protected ?string $heading = 'Sebaran Isu Berdasarkan Perangkat Daerah';
protected static ?int $sort = 16;
protected int|string|array $columnSpan = 1;
protected ?string $pollingInterval = null;
protected function getData(): array
{
$startDate = $this->filters['startDate'] ?? null;
$endDate = $this->filters['endDate'] ?? null;
$issueTable = (new IssueManagement)->getTable();
$departmentTable = (new Department)->getTable();
$pivotTable = 'department_issue_management';
$data = IssueManagement::query()
->join($pivotTable, "{$issueTable}.id", '=', "{$pivotTable}.issue_management_id")
->join($departmentTable, "{$pivotTable}.department_id", '=', "{$departmentTable}.id")
->when($startDate, fn ($q) => $q->whereDate("{$issueTable}.created_at", '>=', $startDate))
->when($endDate, fn ($q) => $q->whereDate("{$issueTable}.created_at", '<=', $endDate))
->select("{$departmentTable}.name", DB::raw('count(*) as total'))
->groupBy("{$departmentTable}.id", "{$departmentTable}.name")
->orderByDesc('total')
->limit(10)
->get();
return [
'datasets' => [
[
'label' => 'Jumlah Isu',
'data' => $data->pluck('total')->toArray(),
'backgroundColor' => [
'#10B981',
'#3B82F6',
'#F59E0B',
'#EC4899',
'#6366F1',
'#8B5CF6',
'#F43F5E',
'#06B6D4',
'#14B8A6',
'#F97316',
],
],
],
'labels' => $data->pluck('name')->toArray(),
];
}
protected function getType(): string
{
return 'bar';
}
}

View File

@ -3,8 +3,11 @@
namespace App\Models;
use App\Enums\IsActive;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Swindon\FilamentHashids\Traits\HasHashid;
@ -20,4 +23,15 @@ protected function casts(): array
'is_active' => IsActive::class,
];
}
#[Scope]
protected function active(Builder $query): void
{
$query->where('is_active', IsActive::ACTIVE);
}
public function issueManagements(): BelongsToMany
{
return $this->belongsToMany(IssueManagement::class, 'department_issue_management');
}
}

View File

@ -6,6 +6,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Swindon\FilamentHashids\Traits\HasHashid;
@ -52,4 +53,9 @@ public function subLocation(): BelongsTo
{
return $this->belongsTo(SubLocation::class);
}
public function departments(): BelongsToMany
{
return $this->belongsToMany(Department::class, 'department_issue_management');
}
}

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('department_issue_management', function (Blueprint $table) {
$table->id();
$table->foreignId('department_id')->constrained()->cascadeOnDelete();
$table->foreignId('issue_management_id')->constrained('issue_management')->cascadeOnDelete();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('department_issue_management');
}
};

View File

@ -124,6 +124,8 @@ public function run(): void
"Replicate:Department",
"Reorder:Department",
"View:IssueDepartmentChart",
"View:IssueLocationChart",
"ViewAny:IssueManagement",
@ -387,6 +389,8 @@ public function run(): void
"Replicate:Department",
"Reorder:Department",
"View:IssueDepartmentChart",
"View:IssueLocationChart",
"ViewAny:IssueManagement",

View File

@ -2,6 +2,7 @@
use App\Enums\IssueSentiment;
use App\Models\Classification;
use App\Models\Department;
use App\Models\IssueManagement;
use App\Models\Location;
use App\Models\MediaMonitoring;
@ -13,6 +14,7 @@
$location = Location::factory()->create();
$classification = Classification::factory()->create();
$monitoring = MediaMonitoring::factory()->create();
$department = Department::factory()->create();
$issue = IssueManagement::factory()->create([
'location_id' => $location->id,
@ -21,7 +23,10 @@
'issue' => IssueSentiment::POSITIVE,
]);
$issue->departments()->attach($department->id);
expect($issue->issue)->toBe(IssueSentiment::POSITIVE);
expect($issue->location->id)->toBe($location->id);
expect($issue->mediaMonitoring->id)->toBe($monitoring->id);
expect($issue->departments->first()->id)->toBe($department->id);
});