Lompat ke konten
dokumentasi.id
Commit a2522f5
v1
Star645

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-34, ProjectTimeline memakai slug project-timeline app/Filament/Pages/ProjectTimeline.php:10-17, sedangkan TicketTimeline memakai slug ticket-timeline/{project_id?} app/Filament/Pages/TicketTimeline.php:17-27.

Kolom papan dibangun dari ticketStatuses

Relasi Project::ticketStatuses() adalah hasMany ke model TicketStatus app/Models/Project.php:47-50. 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-187. 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-538.

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-97. selectProject mengulang pemeriksaan yang sama, mengosongkan selectedTicket, lalu berpindah ke URL yang memuat project_id app/Filament/Pages/ProjectBoard.php:126-149.

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-365. 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-203. 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.
Fig. 1: Siklus satu perpindahan ticket di papanDrop 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-290 app/Filament/Pages/ProjectBoard.php:464-476.

Setelah update berhasil, papan memuat ulang status lalu memancarkan ticket-updated app/Filament/Pages/ProjectBoard.php:261-290. 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-502. Tombol "Refresh Board" memanggil refreshBoard, yang menjalankan loadTicketStatuses() dan memancarkan ticket-updated lagi app/Filament/Pages/ProjectBoard.php:292-297. loadTicketStatuses() sendiri hanya menjalankan unset($this->ticketStatuses) agar computed property dihitung ulang app/Filament/Pages/ProjectBoard.php:189-193.

Mode hanya-baca ditentukan canMoveTickets(), yang mengembalikan auth()->user()->can('update_ticket') app/Filament/Pages/ProjectBoard.php:555-558. 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-369.

⚠️ moveTicket bersifat publik dan hanya dijaga oleh canManageTicket. Panggilan $wire.call('moveTicket', ...) dari browser lain tetap harus lolos pemeriksaan itu; jangan menambah jalur pemindahan yang melewatinya app/Filament/Pages/ProjectBoard.php:261-290.

Urutan dan filter kartu

setSortOrder menyimpan pilihan per statusId di $sortOrders lalu memuat ulang papan app/Filament/Pages/ProjectBoard.php:229-233. applySorting menerjemahkan lima nilai yang mungkin:

Nilai sortOrderKunci pengurutan
date_created_newestmengandalkan urutan query created_at dan id menurun, hanya values()
date_created_oldesttimestamp created_at digabung id berpadding 10 digit
card_name_alphabeticalsortBy('name')
due_datedue_date, atau '9999-12-31' bila kosong
prioritypriority->id, atau 999 bila tidak ada prioritas

Nama nilai dan kuncinya diambil apa adanya dari applySorting app/Filament/Pages/ProjectBoard.php:235-259.

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-172. 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-216.

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.
Fig. 2: Tiga tampilan timeline dan sumber datanyaProject memasok baris ke halaman ProjectTimeline dan widget, ticket memasok baris ke TicketTimeline lalu ke export Gantt.
TampilanUnit barisSyarat tanggalSumber progress
ProjectTimeline pageprojectstart_date dan end_date wajib terisipastDays / totalDays dari jarak hari
TicketTimeline pageticketdue_date wajib, start_date opsionalposisi status pada sort_order
Widgets\ProjectTimelineprojectstart_date dan end_date wajib terisiCarbon::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-52. getGanttData menghitung totalDays = diffInDays(end) + 1, pastDays = min(totalDays, max(0, ...)), dan progress = min(1.0, pastDays / totalDays) app/Filament/Pages/ProjectTimeline.php:54-108.

Status dan warna bar dipilih berurutan, dan urutan itu menentukan hasil ketika dua kondisi terpenuhi sekaligus:

statuscolorKondisi
Overdue#ef4444now > end_date
Nearly Complete#10b981progress >= 0.8
Approaching Deadline#f59e0bdiffInDays(end_date) <= 7
In Progress#3b82f6tidak ada kondisi di atas yang terpenuhi

Pemeriksaan isOverdue, isNearDeadline, dan isNearlyComplete beserta penetapan warna ada di app/Filament/Pages/ProjectTimeline.php:71-89.

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.
Fig. 3: Klasifikasi status bar projectgetGanttData 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-63. 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-26. mount memanggil loadData(), yang mengisi $counts dari getViewData() dan $ganttData dari getGanttData() app/Filament/Pages/ProjectTimeline.php:19-31; getViewData mengembalikan kunci all dari query yang disaring per peran dengan aturan yang sama seperti getProjects app/Filament/Pages/ProjectTimeline.php:110-131.

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-135. 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-201. Nama ticket dipotong pada 50 karakter sebelum dikirim ke gantt app/Filament/Pages/TicketTimeline.php:203-206.

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-245. 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-101, dan view menangkapnya lewat Livewire.on('refreshGanttChart', ...) untuk menginisialisasi ulang gantt resources/views/filament/pages/ticket-timeline.blade.php:362-367.

⚠️ Ticket tanpa due_date tidak pernah muncul di TicketTimeline. View menampilkan pesan kosong "No tickets with due dates" ketika daftar gantt kosong, jadi ticket berstatus apa pun tetap tidak terlihat sampai due_date diisi app/Filament/Pages/TicketTimeline.php:123-135 app/Filament/Pages/TicketTimeline.php:137-201.

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-24. 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-55. 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-134. getTotalProjects mengembalikan dua hitungan, all dan pinned, dari query yang sama-sama disaring per peran app/Filament/Widgets/ProjectTimeline.php:67-88.

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-35. 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-76. 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-100.

⚠️ array() membaca ticket_details.name, sedangkan data yang dihasilkan TicketTimeline::getGanttDataProperty hanya memuat kunci text app/Exports/TicketsTimelineGantt.php:78-100 app/Filament/Pages/TicketTimeline.php:137-201. Selain itu kelas ini belum punya pemanggil di repository; pencarian TicketsTimelineGantt hanya menemukan deklarasinya app/Exports/TicketsTimelineGantt.php:21-35. 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.