7
Papan Kanban dan Timeline
Papan Kanban dan timeline membaca data yang sama dari dua sudut berbeda: ticket_status_id pada tabel tickets, serta kolom tanggal start_date dan due_date. Tiga kelas Filament menyajikan sudut pandang itu. ProjectBoard memakai slug project-board/{project_id?} dan bergrup navigasi "Project Management" app/Filament/Pages/ProjectBoard.php:22-341, ProjectTimeline memakai slug project-timeline app/Filament/Pages/ProjectTimeline.php:10-172, sedangkan TicketTimeline memakai slug ticket-timeline/{project_id?} app/Filament/Pages/TicketTimeline.php:17-273.
Kolom papan dibangun dari ticketStatuses
Relasi Project::ticketStatuses() adalah hasMany ke model TicketStatus app/Models/Project.php:47-504. Computed property ProjectBoard::ticketStatuses() memuat status berurut sort_order, lalu eager load tickets beserta assignees, status, priority, dan creator; kartu di dalam satu status sudah terurut created_at dan id menurun sebelum applySorting dijalankan app/Filament/Pages/ProjectBoard.php:151-1875. View merender satu div.status-column per status dengan atribut data-status-id yang dibaca listener drop resources/views/filament/pages/project-board.blade.php:532-5386.
mount membatasi daftar project: super_admin melihat semua project, pengguna lain hanya auth()->user()->projects(). Bila project_id tidak ada dalam daftar itu, halaman mengirim notifikasi berjudul "Project Not Found" lalu berhenti app/Filament/Pages/ProjectBoard.php:61-977. selectProject mengulang pemeriksaan yang sama, mengosongkan selectedTicket, lalu berpindah ke URL yang memuat project_id app/Filament/Pages/ProjectBoard.php:126-1498.
Action new_ticket mengisi ticket_status_id dengan status pertama pada papan dan menugaskan pengguna yang sedang login bila ia anggota project app/Filament/Pages/ProjectBoard.php:350-3659. Ini berarti ticket baru selalu mendarat di kolom paling kiri sesuai sort_order, bukan di kolom yang sedang dilihat.
Memindahkan ticket antar status
Alpine pada view menyediakan moveTicketToStatus, yang memanggil $wire.call('moveTicket', parseInt(ticketId), parseInt(statusId)) resources/views/filament/pages/project-board.blade.php:193-20310. Server menerima panggilan itu lewat listener #[On('ticket-moved')].
sequenceDiagram
participant Card as ticket-card
participant Col as status-column
participant Board as board Alpine state
participant Page as ProjectBoard
participant DB as tickets
Card->>Board: dragstart atau long press 500 ms
Board->>Board: draggingTicket = data-ticket-id
Col->>Board: drop pada kolom tujuan
Board->>Page: $wire.call("moveTicket", ticketId, statusId)
Page->>Page: cek ticket->project_id === selectedProject->id
Page->>Page: canManageTicket(ticket)
alt izin ditolak
Page-->>Board: Notification "Permission Denied"
else izin diberikan
Page->>DB: update ticket_status_id
Page->>Page: loadTicketStatuses()
Page-->>Board: dispatch "ticket-updated"
Page-->>Board: Notification "Ticket Updated"
end
%% fig: Siklus satu perpindahan ticket di papan
%% desc: Drop kartu memicu moveTicket di server, yang memeriksa izin, memperbarui ticket_status_id, lalu memuat ulang papan.moveTicket hanya bekerja bila ticket->project_id sama dengan selectedProject->id dan canManageTicket mengembalikan true. canManageTicket mensyaratkan permission update_ticket dan salah satu dari: super_admin, user_id ticket, atau assignee ticket app/Filament/Pages/ProjectBoard.php:261-29011 app/Filament/Pages/ProjectBoard.php:464-47612.
Setelah update berhasil, papan memuat ulang status lalu memancarkan ticket-updated app/Filament/Pages/ProjectBoard.php:261-29011. View mendengarkan @ticket-moved.window, @ticket-updated.window, dan @refresh-board.window untuk memasang ulang listener drag resources/views/filament/pages/project-board.blade.php:500-50213. Tombol "Refresh Board" memanggil refreshBoard, yang menjalankan loadTicketStatuses() dan memancarkan ticket-updated lagi app/Filament/Pages/ProjectBoard.php:292-29714. loadTicketStatuses() sendiri hanya menjalankan unset($this->ticketStatuses) agar computed property dihitung ulang app/Filament/Pages/ProjectBoard.php:189-19315.
Mode hanya-baca ditentukan canMoveTickets(), yang mengembalikan auth()->user()->can('update_ticket') app/Filament/Pages/ProjectBoard.php:555-55816. Ketika izin itu tidak ada, attachAllEventListeners berhenti lebih awal sehingga atribut draggable tidak pernah dipasang pada .ticket-card resources/views/filament/pages/project-board.blade.php:366-36917.
⚠️
moveTicketbersifat publik dan hanya dijaga olehcanManageTicket. Panggilan$wire.call('moveTicket', ...)dari browser lain tetap harus lolos pemeriksaan itu; jangan menambah jalur pemindahan yang melewatinya app/Filament/Pages/ProjectBoard.php:261-29011.
Urutan dan filter kartu
setSortOrder menyimpan pilihan per statusId di $sortOrders lalu memuat ulang papan app/Filament/Pages/ProjectBoard.php:229-23318. applySorting menerjemahkan lima nilai yang mungkin:
Nilai sortOrder | Kunci pengurutan |
|---|---|
date_created_newest | mengandalkan urutan query created_at dan id menurun, hanya values() |
date_created_oldest | timestamp created_at digabung id berpadding 10 digit |
card_name_alphabetical | sortBy('name') |
due_date | due_date, atau '9999-12-31' bila kosong |
priority | priority->id, atau 999 bila tidak ada prioritas |
Nama nilai dan kuncinya diambil apa adanya dari applySorting app/Filament/Pages/ProjectBoard.php:235-25919.
Filter user bekerja di level query: blok when(! empty($this->selectedUserIds)) menambahkan whereHas('assignees', ...) sehingga hanya kartu dengan assignee terpilih yang dimuat app/Filament/Pages/ProjectBoard.php:168-17220. Daftar pilihan filter dibatasi oleh loadProjectUsers, yang mengumpulkan id assignee dari ticket project itu dan menyaring User sesuai hasilnya app/Filament/Pages/ProjectBoard.php:195-21621.
Tiga permukaan timeline
Datanya sama, tetapi satuannya berbeda: dua tampilan menarik baris dari project, satu tampilan menarik baris dari ticket, dan satu export menerjemahkan baris ticket ke sel spreadsheet.
graph LR
PROJ["projects: start_date, end_date"] --> PT["ProjectTimeline page"]
PROJ --> WI["Widgets ProjectTimeline"]
TKT["tickets: start_date, due_date"] --> TT["TicketTimeline page"]
TT --> GX["TicketsTimelineGantt export"]
WI --> RNG["getTimelineRange"]
style PROJ fill:#336791,color:#fff
style TKT fill:#336791,color:#fff
style RNG fill:#8b5cf6,color:#fff
style GX fill:#8b5cf6,color:#fff
style PT fill:#2d7d46,color:#fff
style TT fill:#2d7d46,color:#fff
style WI fill:#f59e0b,color:#000
%% fig: Tiga tampilan timeline dan sumber datanya
%% desc: Project memasok baris ke halaman ProjectTimeline dan widget, ticket memasok baris ke TicketTimeline lalu ke export Gantt.| Tampilan | Unit baris | Syarat tanggal | Sumber progress |
|---|---|---|---|
ProjectTimeline page | project | start_date dan end_date wajib terisi | pastDays / totalDays dari jarak hari |
TicketTimeline page | ticket | due_date wajib, start_date opsional | posisi status pada sort_order |
Widgets\ProjectTimeline | project | start_date dan end_date wajib terisi | Carbon::today() dibanding rentang project |
ProjectTimeline: satu bar per project
getProjects menyaring start_date dan end_date yang tidak null, mengurutkan start_date, dan untuk non-super_admin hanya menyertakan project tempat pengguna terdaftar di members app/Filament/Pages/ProjectTimeline.php:33-5222. getGanttData menghitung totalDays = diffInDays(end) + 1, pastDays = min(totalDays, max(0, ...)), dan progress = min(1.0, pastDays / totalDays) app/Filament/Pages/ProjectTimeline.php:54-10823.
Status dan warna bar dipilih berurutan, dan urutan itu menentukan hasil ketika dua kondisi terpenuhi sekaligus:
status | color | Kondisi |
|---|---|---|
Overdue | #ef4444 | now > end_date |
Nearly Complete | #10b981 | progress >= 0.8 |
Approaching Deadline | #f59e0b | diffInDays(end_date) <= 7 |
In Progress | #3b82f6 | tidak ada kondisi di atas yang terpenuhi |
Pemeriksaan isOverdue, isNearDeadline, dan isNearlyComplete beserta penetapan warna ada di app/Filament/Pages/ProjectTimeline.php:71-8924.
graph TD
NOW["Carbon::now() dibanding end_date"] --> OV{"now > end_date"}
OV -->|"ya"| OD["Overdue #ef4444"]
OV -->|"tidak"| NC{"progress >= 0.8"}
NC -->|"ya"| NE["Nearly Complete #10b981"]
NC -->|"tidak"| DL{"diffInDays end_date <= 7"}
DL -->|"ya"| AP["Approaching Deadline #f59e0b"]
DL -->|"tidak"| IP["In Progress #3b82f6"]
style NOW fill:#8b5cf6,color:#fff
style OV fill:#f59e0b,color:#000
style NC fill:#f59e0b,color:#000
style DL fill:#f59e0b,color:#000
style OD fill:#dc2626,color:#fff
style NE fill:#2d7d46,color:#fff
style AP fill:#f59e0b,color:#000
style IP fill:#336791,color:#fff
%% fig: Klasifikasi status bar project
%% desc: getGanttData menentukan status dan warna bar dari urutan pemeriksaan overdue, progress, lalu jarak ke tenggat.View menampilkan legenda empat warna yang sama resources/views/filament/pages/project-timeline.blade.php:46-6325. Tinggi container #gantt_here dihitung max(400, $projectCount * 40 + 80) sehingga papan bertambah tinggi mengikuti jumlah project resources/views/filament/pages/project-timeline.blade.php:18-2626. mount memanggil loadData(), yang mengisi $counts dari getViewData() dan $ganttData dari getGanttData() app/Filament/Pages/ProjectTimeline.php:19-3127; getViewData mengembalikan kunci all dari query yang disaring per peran dengan aturan yang sama seperti getProjects app/Filament/Pages/ProjectTimeline.php:110-13128.
TicketTimeline: satu bar per ticket
getTicketsProperty hanya memilih ticket yang due_date-nya tidak null, mengambil id, name, due_date, start_date, ticket_status_id, lalu mengurutkan berdasarkan due_date app/Filament/Pages/TicketTimeline.php:123-13529. getGanttDataProperty memakai start_date bila ada, atau due_date - 7 hari bila tidak; bila endDate <= startDate maka endDate digeser ke startDate + 1 hari. Setiap task ditandai 'type' => 'task' dan 'readonly' => true, dengan warna #ef4444 saat overdue atau warna status ticket bila tidak app/Filament/Pages/TicketTimeline.php:137-20130. Nama ticket dipotong pada 50 karakter sebelum dikirim ke gantt app/Filament/Pages/TicketTimeline.php:203-20631.
Progress ticket tidak dibaca dari kolom tersimpan. getSimpleProgress mengambil ticketStatuses() berurut sort_order, mencari posisi status ticket saat ini, lalu menghitung (($currentPosition + 1) / $totalStatuses) * 100 app/Filament/Pages/TicketTimeline.php:208-24532. Artinya status pertama sudah bernilai lebih dari nol, dan status terakhir selalu 100 persen; status is_completed tidak ikut diperiksa.
Memilih project memancarkan refreshGanttChart app/Filament/Pages/TicketTimeline.php:87-10133, dan view menangkapnya lewat Livewire.on('refreshGanttChart', ...) untuk menginisialisasi ulang gantt resources/views/filament/pages/ticket-timeline.blade.php:362-36734.
⚠️ Ticket tanpa
due_datetidak pernah muncul diTicketTimeline. View menampilkan pesan kosong "No tickets with due dates" ketika daftar gantt kosong, jadi ticket berstatus apa pun tetap tidak terlihat sampaidue_datediisi app/Filament/Pages/TicketTimeline.php:123-13529 app/Filament/Pages/TicketTimeline.php:137-20130.
Widget ProjectTimeline
Widget ini memakai trait HasWidgetShield, columnSpan bernilai full, serta dua properti keadaan: $filter dengan nilai awal pinned dan $viewMode dengan nilai awal projects app/Filament/Widgets/ProjectTimeline.php:12-2435. getProjects meng-eager-load tickets yang punya due_date beserta status, assignees, dan priority, menyaring start_date dan end_date, lalu mengurutkan pinned_date menurun bila $filter bernilai pinned, atau start_date bila tidak app/Filament/Widgets/ProjectTimeline.php:26-5536. getTimelineRange mengambil min(start_date) dan max(end_date), memotongnya ke awal dan akhir bulan, menambah satu bulan di kiri dan kanan, lalu menyusun daftar bulan beserta total_days app/Filament/Widgets/ProjectTimeline.php:90-13437. getTotalProjects mengembalikan dua hitungan, all dan pinned, dari query yang sama-sama disaring per peran app/Filament/Widgets/ProjectTimeline.php:67-8838.
Export Gantt ke Excel
TicketsTimelineGantt mengimplementasikan FromArray, WithHeadings, WithStyles, WithEvents, WithTitle, ShouldAutoSize, dan WithColumnFormatting; konstruktornya menerima Project dan array $ganttData lalu memanggil calculateDateRange() app/Exports/TicketsTimelineGantt.php:21-3539. calculateDateRange membaca setiap start_date dan end_date berformat d-m-Y H:i, mengambil nilai min dan max, menambah 2 hari di kedua sisi, dan membangun dateRange harian sepanjang totalDays app/Exports/TicketsTimelineGantt.php:37-7640. array() menulis tiga baris pembuka, yaitu nama project dengan akhiran "Gantt Chart Timeline", waktu generate, dan rentang tanggal, lalu menerjemahkan setiap task menjadi satu baris yang diawali $task['ticket_details']['name'] app/Exports/TicketsTimelineGantt.php:78-10041.
⚠️
array()membacaticket_details.name, sedangkan data yang dihasilkanTicketTimeline::getGanttDataPropertyhanya memuat kuncitextapp/Exports/TicketsTimelineGantt.php:78-10041 app/Filament/Pages/TicketTimeline.php:137-20130. Selain itu kelas ini belum punya pemanggil di repository; pencarianTicketsTimelineGantthanya menemukan deklarasinya app/Exports/TicketsTimelineGantt.php:21-3539. Untuk export ticket yang benar-benar dipakai papan, lihat Import dan Export Ticket.
Untuk aturan siapa yang boleh memindahkan kartu dan siapa yang melihat semua project, lihat Kontrol Akses dan Peran. Untuk isi status, prioritas, dan epic yang mengisi kolom serta warna bar, lihat Status, Prioritas, dan Epic.