diff --git a/app/Filament/Resources/TicketResource.php b/app/Filament/Resources/TicketResource.php index a7524559..48d80851 100644 --- a/app/Filament/Resources/TicketResource.php +++ b/app/Filament/Resources/TicketResource.php @@ -25,6 +25,7 @@ use Carbon\Carbon; use App\Models\IssueSource; use Filament\Tables\Columns\ViewColumn; +use App\Models\ProblemCategory; class TicketResource extends Resource { @@ -133,12 +134,19 @@ public static function form(Form $form): Form ->columnSpan(2) ->disabled(), + // input kategori masalah tiket (problem category) + Forms\Components\Select::make('problem_category_id') + ->label(__('Problem Category')) + ->searchable() + ->columnSpan(3) + ->options(fn() => ProblemCategory::all()->pluck('name', 'id')->toArray()), + // Input nama tiket Forms\Components\TextInput::make('name') ->label(__('Ticket name')) ->required() ->columnSpan( - fn($livewire) => !($livewire instanceof CreateRecord) ? 10 : 12 + fn($livewire) => !($livewire instanceof CreateRecord) ? 7 : 9 ) ->maxLength(255), ]), diff --git a/app/Filament/Widgets/TicketDuplicateChart.php b/app/Filament/Widgets/TicketDuplicateChart.php index a09445ab..453b44e4 100644 --- a/app/Filament/Widgets/TicketDuplicateChart.php +++ b/app/Filament/Widgets/TicketDuplicateChart.php @@ -1,5 +1,124 @@ month = now()->month; +// $this->year = now()->year; + +// $this->form->fill([ +// 'month' => $this->month, +// 'year' => $this->year, +// ]); +// } + +// protected function getFormSchema(): array +// { +// return [ +// Grid::make(4)->schema([ +// Select::make('month') +// ->label(__('Month')) +// ->options($this->getMonths()) +// ->reactive(), + +// Select::make('year') +// ->label(__('Year')) +// ->options($this->getYears()) +// ->reactive(), +// ]) +// ]; +// } + +// protected function getMonths(): array +// { +// return collect(range(1, 12)) +// ->mapWithKeys(fn ($m) => [$m => Carbon::create()->month($m)->translatedFormat('F')]) +// ->toArray(); +// } + +// protected function getYears(): array +// { +// $now = now()->year; +// return collect(range($now - 2, $now)) +// ->mapWithKeys(fn ($y) => [$y => $y]) +// ->toArray(); +// } + +// public function getChartData(): array +// { + +// $state = $this->form->getState(); + +// $tickets = DB::table('ticket_relations') +// ->select('relation_id', DB::raw('COUNT(*) as total')) +// ->whereYear('created_at', $state['year']) +// ->whereMonth('created_at', $state['month']) +// ->groupBy('relation_id') +// ->orderByDesc('total') +// ->get(); + + +// return [ +// 'labels' => $tickets->pluck('relation_id')->map(fn ($id) => 'ID Tiket : ' . $id)->values(), +// 'data' => $tickets->pluck('total'), +// 'urls' => $tickets->pluck('relation_id')->map(fn ($id) => route('filament.admin.resources.tickets.view', ['record' => $id])), +// ]; +// } + +// public function getColumnSpan(): int|string|array +// { +// return 'full'; // biar full 1 row +// } + + +// public function updated($name, $value): void +// { +// $chartData = $this->getChartData(); + +// $this->dispatch('updateDuplicateChart', [ +// 'labels' => $chartData['labels']->toArray(), +// 'data' => $chartData['data']->toArray(), +// 'urls' => $chartData['urls']->toArray(), +// ]); +// } + +// public function initChart() +// { +// $chartData = $this->getChartData(); + +// $this->dispatch('renderDuplicateChart', [ +// 'labels' => $chartData['labels']->toArray(), +// 'data' => $chartData['data']->toArray(), +// 'urls' => $chartData['urls']->toArray(), +// ]); +// } +// } + + namespace App\Filament\Widgets; use Filament\Widgets\Widget; @@ -9,6 +128,7 @@ use Illuminate\Support\Facades\DB; use Carbon\Carbon; use Filament\Forms\Components\Grid; +use App\Models\ProblemCategory; class TicketDuplicateChart extends Widget implements HasForms { @@ -22,34 +142,61 @@ public function getHeading(): string } // ✅ deklarasi semua properti Livewire - public int $month; - public int $year; + public int $start_month; + public int $start_year; + public int $end_month; + public int $end_year; + public int $problem_category_id; public function mount(): void { - $this->month = now()->month; - $this->year = now()->year; + $this->start_month = now()->subMonths(2)->month; + $this->start_year = now()->subMonths(2)->year; + $this->end_month = now()->month; + $this->end_year = now()->year; + $this->problem_category_id = ProblemCategory::first()->id; // Default to 'Semua' $this->form->fill([ - 'month' => $this->month, - 'year' => $this->year, + 'start_month' => $this->start_month, + 'start_year' => $this->start_year, + 'end_month' => $this->end_month, + 'end_year' => $this->end_year, + 'problem_category_id' => $this->problem_category_id // Default to 'Semua' ]); } protected function getFormSchema(): array { return [ - Grid::make(4)->schema([ - Select::make('month') - ->label(__('Month')) + Grid::make(5)->schema([ + Select::make('start_month') + ->label(__('Start month')) ->options($this->getMonths()) ->reactive(), - Select::make('year') - ->label(__('Year')) + Select::make('start_year') + ->label(__('Start year')) ->options($this->getYears()) ->reactive(), - ]) + + Select::make('end_month') + ->label(__('End month')) + ->options($this->getMonths()) + ->reactive(), + + Select::make('end_year') + ->label(__('End year')) + ->options($this->getYears()) + ->reactive(), + + Select::make('problem_category_id') + ->label(__('Problem Category')) + ->options(function () { + return ProblemCategory::all()->pluck('name', 'id')->toArray(); + }) + ->reactive(), + ]), + ]; } @@ -72,20 +219,28 @@ public function getChartData(): array { $state = $this->form->getState(); + // var_dump($state); + + if (!empty($state['start_month']) && !empty($state['start_year']) && !empty($state['end_month']) && !empty($state['end_year'])) { + $start = Carbon::create($state['start_year'], $state['start_month'], 1)->startOfMonth(); + $end = Carbon::create($state['end_year'], $state['end_month'], 1)->endOfMonth(); + } else { + $start = now()->subMonths(2)->startOfMonth(); + $end = now()->endOfMonth(); + } - $tickets = DB::table('ticket_relations') - ->select('relation_id', DB::raw('COUNT(*) as total')) - ->whereYear('created_at', $state['year']) - ->whereMonth('created_at', $state['month']) - ->groupBy('relation_id') - ->orderByDesc('total') + $tickets = DB::table('tickets') + ->selectRaw("DATE_FORMAT(created_at, '%Y-%m') as bulan, COUNT(*) as total") + ->whereBetween('created_at', [$start, $end]) + ->where('problem_category_id', (int)$state['problem_category_id']) + ->groupBy('bulan') + ->orderBy('bulan') ->get(); return [ - 'labels' => $tickets->pluck('relation_id')->map(fn ($id) => 'ID Tiket : ' . $id)->values(), + 'labels' => $tickets->pluck('bulan'), 'data' => $tickets->pluck('total'), - 'urls' => $tickets->pluck('relation_id')->map(fn ($id) => route('filament.admin.resources.tickets.view', ['record' => $id])), ]; } @@ -102,7 +257,6 @@ public function updated($name, $value): void $this->dispatch('updateDuplicateChart', [ 'labels' => $chartData['labels']->toArray(), 'data' => $chartData['data']->toArray(), - 'urls' => $chartData['urls']->toArray(), ]); } @@ -113,7 +267,6 @@ public function initChart() $this->dispatch('renderDuplicateChart', [ 'labels' => $chartData['labels']->toArray(), 'data' => $chartData['data']->toArray(), - 'urls' => $chartData['urls']->toArray(), ]); } } diff --git a/app/Models/ProblemCategory.php b/app/Models/ProblemCategory.php new file mode 100644 index 00000000..67498072 --- /dev/null +++ b/app/Models/ProblemCategory.php @@ -0,0 +1,23 @@ +hasMany(Ticket::class, 'problem_category_id'); + } +} diff --git a/app/Models/Ticket.php b/app/Models/Ticket.php index 0212c438..3d633a75 100644 --- a/app/Models/Ticket.php +++ b/app/Models/Ticket.php @@ -23,7 +23,7 @@ class Ticket extends Model implements HasMedia 'name', 'content', 'owner_id', 'responsible_id', 'status_id', 'project_id', 'code', 'order', 'type_id', 'priority_id', 'estimation', 'epic_id', 'sprint_id', 'master_application_id', 'milestone_id', - 'issue_source_id', 'github_issue_url', 'github_issue_number', 'github_project_item_id', + 'issue_source_id', 'github_issue_url', 'github_issue_number', 'github_project_item_id', 'problem_category_id' ]; public static function boot() @@ -257,4 +257,9 @@ public function issueSource() { return $this->belongsTo(issueSource::class); } + + public function problemCategory() +{ + return $this->belongsTo(ProblemCategory::class, 'problem_category_id'); +} } diff --git a/database/migrations/2025_11_19_100508_create_problem_categories_table.php b/database/migrations/2025_11_19_100508_create_problem_categories_table.php new file mode 100644 index 00000000..3b316dce --- /dev/null +++ b/database/migrations/2025_11_19_100508_create_problem_categories_table.php @@ -0,0 +1,77 @@ +id(); + $table->string('name')->unique(); // Contoh: "Website tidak bisa diakses" + $table->string('code')->nullable()->unique(); // Contoh: "W001" + $table->text('description')->nullable(); // Penjelasan detail kategori + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + + DB::table('problem_categories')->insert([ + [ + 'name' => 'Website tidak bisa diakses', + 'code' => 'W001', + 'description' => 'Masalah terkait website yang tidak dapat diakses oleh pengguna.', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'Login gagal', + 'code' => 'L001', + 'description' => 'Masalah yang terjadi saat pengguna mencoba untuk login tetapi gagal.', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'Akses lambat', + 'code' => 'N001', + 'description' => 'Masalah terkait kecepatan akses yang lambat atau tidak stabil.', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'Error database', + 'code' => 'D001', + 'description' => 'Masalah yang berhubungan dengan database, seperti koneksi gagal atau query error.', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'Error tampilan', + 'code' => 'UI001', + 'description' => 'Masalah yang terkait dengan tampilan antarmuka pengguna, seperti layout yang rusak atau elemen yang tidak muncul dengan benar.', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'Fitur tidak berfungsi', + 'code' => 'F001', + 'description' => 'Masalah di mana fitur tertentu dalam aplikasi tidak berfungsi sesuai harapan.', + 'created_at' => now(), + 'updated_at' => now(), + ], + ]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('problem_categories'); + } +}; diff --git a/database/migrations/2025_11_19_100723_add_problem_category_id_to_tickets_table.php b/database/migrations/2025_11_19_100723_add_problem_category_id_to_tickets_table.php new file mode 100644 index 00000000..f0d11ab6 --- /dev/null +++ b/database/migrations/2025_11_19_100723_add_problem_category_id_to_tickets_table.php @@ -0,0 +1,33 @@ +unsignedBigInteger('problem_category_id')->nullable(); + $table->foreign('problem_category_id') + ->references('id') + ->on('problem_categories') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('tickets', function (Blueprint $table) { + $table->dropForeign(['problem_category_id']); + $table->dropColumn('problem_category_id'); + }); + } +}; diff --git a/database/seeders/ProblemCategorySeeder.php b/database/seeders/ProblemCategorySeeder.php new file mode 100644 index 00000000..63df0e46 --- /dev/null +++ b/database/seeders/ProblemCategorySeeder.php @@ -0,0 +1,29 @@ + 'Website tidak bisa diakses', 'code' => 'W001', 'description' => 'Masalah terkait website yang tidak dapat diakses oleh pengguna.'], + ['name' => 'Login gagal', 'code' => 'L001', 'description' => 'Masalah yang terjadi saat pengguna mencoba untuk login tetapi gagal.'], + ['name' => 'Akses lambat', 'code' => 'N001', 'description' => 'Masalah terkait kecepatan akses yang lambat atau tidak stabil.'], + ['name' => 'Error database', 'code' => 'D001', 'description' => 'Masalah yang berhubungan dengan database, seperti koneksi gagal atau query error.'], + ['name' => 'Error tampilan', 'code' => 'UI001', 'description' => 'Masalah yang terkait dengan tampilan antarmuka pengguna, seperti layout yang rusak atau elemen yang tidak muncul dengan benar.'], + ['name' => 'Fitur tidak berfungsi', 'code' => 'F001', 'description' => 'Masalah di mana fitur tertentu dalam aplikasi tidak berfungsi sesuai harapan.'], + ]; + + foreach ($data as $item) { + ProblemCategory::updateOrCreate(['name' => $item['name']], $item); + } + } +} diff --git a/lang/id.json b/lang/id.json index 9e7382ca..e1b7c250 100644 --- a/lang/id.json +++ b/lang/id.json @@ -295,5 +295,6 @@ "Completion": "Penyelesaian", "Export to Excel": "Ekspor ke Excel", "Export Report": "Ekspor Laporan", - "This is an action to export data based on the data on the report page, and is equipped with problem solving for the report and then exported to Excel.": "Ini merupakan aksi untuk mengekspor data berdasarkan data yang ada di page laporan, dan dilengkapi penyelesaian masalah untuk laporan kemudian di export ke Excel." + "This is an action to export data based on the data on the report page, and is equipped with problem solving for the report and then exported to Excel.": "Ini merupakan aksi untuk mengekspor data berdasarkan data yang ada di page laporan, dan dilengkapi penyelesaian masalah untuk laporan kemudian di export ke Excel.", + "Problem Category": "Jenis Masalah" } diff --git a/resources/js/ticket-chart.js b/resources/js/ticket-chart.js index 99a21b92..5fc52664 100644 --- a/resources/js/ticket-chart.js +++ b/resources/js/ticket-chart.js @@ -182,55 +182,86 @@ const colors = [ "#84cc16", // lime ]; -function renderDuplicateChart(labels, data, urls) { - console.log(data.length) +// function renderDuplicateChart(labels, data, urls) { +// console.log(data.length) +// const ctx = document.getElementById('ticketDuplicateChart').getContext('2d'); +// const message = document.getElementById('duplicateChartMessage'); + +// if (ticketDuplicateChart) { +// ticketDuplicateChart.destroy(); +// } + +// // Kalau kosong +// if (!data || data.length === 0 || data.every(v => v === 0)) { +// console.log('no data') +// message.style.display = 'block'; +// // return; +// } else { +// console.log('has data') +// message.style.display = 'none'; +// } + +// const backgroundColors = data.map((_, i) => colors[i % colors.length]); + +// ticketDuplicateChart = new Chart(ctx, { +// type: 'pie', +// data: { +// labels: labels, +// datasets: [{ +// label: 'Jumlah Tiket', +// data: data, +// backgroundColor: backgroundColors, +// }] +// }, +// options: { +// responsive: true, +// plugins: { legend: { display: true } }, +// onClick: (evt, activeEls) => { +// if (activeEls.length > 0) { +// const index = activeEls[0].index; +// window.location.href = urls[index]; // 👈 langsung ke detail +// } +// } +// } +// }); +// } + +// window.addEventListener("updateDuplicateChart", event => { +// console.log("Chart updated:", event.detail); +// renderDuplicateChart(event.detail[0].labels, event.detail[0].data, event.detail[0].urls); +// }); + +// window.addEventListener("renderDuplicateChart", event => { +// renderDuplicateChart(event.detail[0].labels, event.detail[0].data, event.detail[0].urls); +// }); + +function renderDuplicateChart(labels, data) { const ctx = document.getElementById('ticketDuplicateChart').getContext('2d'); - const message = document.getElementById('duplicateChartMessage'); if (ticketDuplicateChart) { ticketDuplicateChart.destroy(); } - // Kalau kosong - if (!data || data.length === 0 || data.every(v => v === 0)) { - console.log('no data') - message.style.display = 'block'; - // return; - } else { - console.log('has data') - message.style.display = 'none'; - } - - const backgroundColors = data.map((_, i) => colors[i % colors.length]); - ticketDuplicateChart = new Chart(ctx, { - type: 'pie', + type: 'bar', data: { labels: labels, datasets: [{ label: 'Jumlah Tiket', data: data, - backgroundColor: backgroundColors, + backgroundColor: '#3b82f6', }] }, options: { responsive: true, - plugins: { legend: { display: true } }, - onClick: (evt, activeEls) => { - if (activeEls.length > 0) { - const index = activeEls[0].index; - window.location.href = urls[index]; // 👈 langsung ke detail - } - } + plugins: { legend: { display: true } } } }); } window.addEventListener("updateDuplicateChart", event => { - console.log("Chart updated:", event.detail); - renderDuplicateChart(event.detail[0].labels, event.detail[0].data, event.detail[0].urls); + renderDuplicateChart(event.detail[0].labels, event.detail[0].data); }); - window.addEventListener("renderDuplicateChart", event => { - renderDuplicateChart(event.detail[0].labels, event.detail[0].data, event.detail[0].urls); + renderDuplicateChart(event.detail[0].labels, event.detail[0].data); }); diff --git a/resources/views/filament/widgets/ticket-duplicate-chart.blade.php b/resources/views/filament/widgets/ticket-duplicate-chart.blade.php index c2420c71..2a755277 100644 --- a/resources/views/filament/widgets/ticket-duplicate-chart.blade.php +++ b/resources/views/filament/widgets/ticket-duplicate-chart.blade.php @@ -5,14 +5,20 @@ {{ $this->form }} {{-- Chart --}} -