6.1
Status, Prioritas, dan Epic
Setiap ticket berdiri di atas tiga entitas konfigurasi: TicketStatus dan Epic yang selalu terikat pada satu project, serta TicketPriority yang berlaku global untuk seluruh project. Halaman ini membahas kolom yang disimpan ketiganya, aturan is_completed pada status, dan cara epic menghitung progres dari status ticket-nya. Pembuatan ticket dan perpindahan antar status dibahas di Manajemen Ticket; tata letak kolom status di board dibahas di Papan Kanban dan Timeline.
Bentuk data ketiga entitas
TicketStatus menyimpan project_id, name, sort_order, color, dan is_completed, dengan is_completed di-cast ke boolean app/Models/TicketStatus.php:10-241. Model ini memiliki project() sebagai BelongsTo dan tickets() sebagai HasMany app/Models/TicketStatus.php:26-342, sehingga satu status hanya melayani satu project.
TicketPriority hanya menyimpan name dan color; relasi tickets() memakai foreign key priority_id app/Models/TicketPriority.php:9-213. Karena tidak ada kolom project_id, satu baris prioritas dipakai bersama oleh semua project.
Epic menyimpan project_id, name, description, start_date, end_date, dan sort_order, dengan kedua kolom tanggal di-cast ke date app/Models/Epic.php:10-364. Relasinya adalah project() dan tickets(), jadi satu epic menaungi banyak ticket.
erDiagram
PROJECTS ||--o{ TICKET_STATUSES : "memiliki status"
PROJECTS ||--o{ EPICS : "memiliki epic"
TICKET_STATUSES ||--o{ TICKETS : "ticket_status_id"
TICKET_PRIORITIES ||--o{ TICKETS : "priority_id"
EPICS ||--o{ TICKETS : "epic_id"
TICKET_STATUSES {
bigint id PK
bigint project_id FK
string name
int sort_order
string color
boolean is_completed
}
TICKET_PRIORITIES {
bigint id PK
string name UK
string color
}
EPICS {
bigint id PK
bigint project_id FK
string name
text description
date start_date
date end_date
int sort_order
}
TICKETS {
bigint id PK
bigint project_id FK
bigint ticket_status_id FK
bigint priority_id FK
bigint epic_id FK
}
%% fig: Relasi status, prioritas, dan epic terhadap ticket
%% desc: Status dan epic dibatasi per project, prioritas dipakai lintas project, dan ketiganya bertemu di tabel tickets lewat foreign key masing-masing.Kolom-kolom itu tidak lahir sekaligus; tabel ticket_statuses tumbuh lewat empat migration.
| Tahap | Berkas migration | Perubahan |
|---|---|---|
| Tabel awal | database/migrations/2025_03_02_200213_create_ticket_statuses_table.php:14-195 | id, project_id (cascade on delete), name, timestamps |
| Urutan | database/migrations/2025_05_06_220233_add_sort_order_to_ticket_statuses_table.php:14-166 | sort_order integer, default 0 |
| Warna | database/migrations/2025_05_06_221002_add_sort_color_to_ticket_statuses_table.php:14-167 | color string, default #3490dc |
| Penanda selesai | database/migrations/2025_08_23_214007_add_is_completed_to_ticket_statuses_table.php:11-138 | is_completed boolean, default false, diletakkan setelah color |
Status berwarna dan penanda is_completed
Status dikelola dari relation manager milik project. Formnya berisi TextInput untuk name, ColorPicker untuk color dengan default #3490dc, TextInput numerik untuk sort_order dengan helper Determines display order in the project board (lower values appear first), dan Toggle berlabel Mark as Completed Status app/Filament/Resources/Projects/RelationManagers/TicketStatusesRelationManager.php:38-729. Helper pada toggle menuliskan batasannya secara eksplisit: Only one status per project can be marked as completed.
Batas satu status completed itu ditegakkan di tiga tempat sekaligus. afterStateUpdated pada toggle memeriksa TicketStatus::where('project_id', $projectId)->where('is_completed', true) dan, bila sudah ada, mengembalikan toggle ke false serta mengirim notifikasi berjudul Cannot mark as completed app/Filament/Resources/Projects/RelationManagers/TicketStatusesRelationManager.php:49-7210. Pemeriksaan yang sama diulang pada CreateAction lewat mutateDataUsing, yang juga menetapkan sort_order baru sebesar max('sort_order') + 1 app/Filament/Resources/Projects/RelationManagers/TicketStatusesRelationManager.php:97-12111. Varian ketiga ada di EditAction, dengan tambahan where('id', '!=', $record->id) agar record yang sedang diedit tidak dianggap bentrok app/Filament/Resources/Projects/RelationManagers/TicketStatusesRelationManager.php:124-14512.
stateDiagram-v2
classDef ok fill:#2d7d46,color:#fff
classDef warn fill:#f59e0b,color:#000
[*] --> BelumCompleted
BelumCompleted --> Completed: Toggle is_completed = true dan belum ada status completed lain
BelumCompleted --> Ditolak: Toggle is_completed = true tetapi project sudah punya status completed
Ditolak --> BelumCompleted: set is_completed = false
Completed --> BelumCompleted: Toggle is_completed = false
class Completed ok
class Ditolak warn
%% fig: Siklus penanda is_completed pada satu status
%% desc: Toggle hanya bisa menjadi completed bila belum ada status lain di project yang sama, dan penolakan mengembalikan nilainya ke false disertai notifikasi.Di tabel relation manager, color tampil lewat ColorColumn dan is_completed lewat IconColumn boolean dengan ikon heroicon-o-check-circle untuk benar dan heroicon-o-x-circle untuk salah app/Filament/Resources/Projects/RelationManagers/TicketStatusesRelationManager.php:80-9313. Tabel yang sama memakai defaultSort('sort_order') dan reorderable('sort_order'), sehingga urutan kolom bisa diubah dengan drag.
Warna dan penanda itu dipakai langsung oleh board: header kolom diberi background-color dari $status->color, dan sebuah badge centang muncul bila $status->is_completed bernilai benar resources/views/filament/pages/project-board.blade.php:539-55414. Query board mengambil is_completed dan mengurutkan kolom dengan orderBy('sort_order') app/Filament/Pages/ProjectBoard.php:151-18715.
is_completed juga menjadi dasar progres project. Project::getProgressPercentageAttribute menghitung ticket selesai lewat whereHas('status', fn ($query) => $query->where('is_completed', true)), lalu membulatkan (completedTickets / totalTickets) * 100 sampai satu desimal app/Models/Project.php:95-11016.
Project baru mendapat lima status bawaan, dan hanya Done yang ditandai selesai app/Filament/Resources/Projects/Pages/CreateProject.php:12-3417:
sort_order | name | color | is_completed |
|---|---|---|---|
| 0 | Backlog | #6B7280 | false |
| 1 | To Do | #F59E0B | false |
| 2 | In Progress | #3B82F6 | false |
| 3 | Review | #8B5CF6 | false |
| 4 | Done | #10B981 | true |
⚠️ Batas "satu status completed per project" hanya dijaga di lapisan Filament. Migration menambahkan index biasa
idx_ticket_statuses_project_completedpada['project_id', 'is_completed'], bukan unique index database/migrations/2025_09_16_051002_add_performance_indexes.php:40-4818. Penulisan langsung ke tabel di luar form, misalnya lewat import atau seeder, tidak akan ditolak oleh database.
Prioritas ticket
Prioritas adalah data referensi global. Migration-nya membuat ticket_priorities dengan name unik dan color sepanjang 7 karakter berdefault #6B7280, lalu langsung menyisipkan tiga baris awal: Low (#10B981), Medium (#F59E0B), dan High (#EF4444) database/migrations/2025_07_04_164429_create_ticket_priorities_table.php:15-4219.
Ticket terhubung ke prioritas lewat kolom priority_id yang nullable, diletakkan setelah ticket_status_id, dan memakai onDelete('set null') database/migrations/2025_07_04_164558_add_priority_id_to_tickets_table.php:14-2020. Konsekuensinya, menghapus satu prioritas tidak menghapus ticket; hanya kolomnya yang dikosongkan sehingga ticket tampil tanpa prioritas.
Pengelolaannya ada di resource tersendiri. Form TicketPriorityResource memakai TextInput name yang wajib dan unik dengan unique(ignoreRecord: true), ditambah ColorPicker color berdefault #6B7280 app/Filament/Resources/TicketPriorities/TicketPriorityResource.php:39-5121. Pada form ticket, prioritas dipilih lewat Select::make('priority_id') yang mengambil opsi dari TicketPriority::pluck('name', 'id') dan bersifat nullable; select epic di sebelahnya justru difilter per project app/Filament/Resources/Tickets/TicketResource.php:110-13322.
Epic
Epic dikelompokkan per project. Migration epics membuat id, project_id dengan cascadeOnDelete, name, description nullable, start_date dan end_date bertipe date nullable, lalu menambahkan epic_id nullable pada tickets setelah ticket_status_id dengan nullOnDelete database/migrations/2025_03_27_065113_create_epics_table.php:14-3023. Kolom sort_order integer berdefault 0 ditambahkan belakangan, diletakkan setelah end_date database/migrations/2025_10_17_215303_add_sort_order_to_epics_table.php:14-1624. Sama seperti priority_id, menghapus epic mengosongkan epic_id pada ticket alih-alih ikut menghapus ticket.
Relation manager epic menyediakan Epic Name wajib dengan maxLength(255), Sort Order numerik berdefault 0 dengan helper Lower numbers appear first, dua DatePicker nullable, dan RichEditor deskripsi yang menyimpan lampiran ke disk public di direktori attachments app/Filament/Resources/Projects/RelationManagers/EpicsRelationManager.php:30-5625. Tabelnya menampilkan sort_order dengan label Order, start_date, end_date, dan kolom tickets_count hasil counts('tickets') berlabel Tickets, lalu mengurutkan baris dengan defaultSort('sort_order', 'asc') app/Filament/Resources/Projects/RelationManagers/EpicsRelationManager.php:58-9926.
Progres epic dihitung di halaman EpicsOverview. Method getEpicStats(Epic $epic) mengembalikan total, completed, in_progress, todo, dan progress_percentage; bila epic belum punya ticket, seluruh angkanya 0 app/Filament/Pages/EpicsOverview.php:141-17527. Pengelompokan dilakukan dengan mencocokkan $ticket->status?->name terhadap tiga daftar nama: ['Done', 'Completed', 'Closed'] untuk completed, ['In Progress', 'Review'] untuk in_progress, dan ['To Do', 'Open', 'New'] untuk todo. Persentasenya adalah round(($completed / $totalTickets) * 100).
graph TD
A["Epic::tickets"] --> B["TicketStatus.name"]
B --> C{"in_array nama status"}
C -->|"Done, Completed, Closed"| D["completed"]
C -->|"In Progress, Review"| P["in_progress"]
C -->|"To Do, Open, New"| T["todo"]
D --> E["progress_percentage = round(completed / total * 100)"]
E --> F["EpicsOverview view"]
style A fill:#336791,color:#fff
style B fill:#336791,color:#fff
style D fill:#2d7d46,color:#fff
style P fill:#8b5cf6,color:#fff
style T fill:#8b5cf6,color:#fff
style E fill:#f59e0b,color:#000
style F fill:#2d7d46,color:#fff
%% fig: Klasifikasi ticket epic menjadi tiga kelompok progres
%% desc: Nama status ticket dicocokkan ke tiga daftar literal, lalu jumlah kelompok completed dibagi total ticket untuk menghasilkan progress_percentage.⚠️ Ada dua definisi "selesai" yang berbeda di aplikasi ini.
Project::getProgressPercentageAttributememakai kolomis_completedpada status app/Models/Project.php:95-11016, sedangkanEpicsOverview::getEpicStatsmemakai nama status literalDone,Completed, atauClosedapp/Filament/Pages/EpicsOverview.php:141-17527. Mengganti nama statusDone, atau menandai status lain sebagaiis_completed, membuat progres epic dan progres project tidak lagi sejalan.