6
Manajemen Ticket
Ticket adalah unit kerja utama aplikasi ini. Semua pengelolaannya melewati satu resource Filament, TicketResource, yang menunjuk model App\Models\Ticket app/Filament/Resources/Tickets/TicketResource.php:33-431. Resource itu mendaftarkan empat route halaman: index, create, view, dan edit app/Filament/Resources/Tickets/TicketResource.php:391-3992.
Halaman ini membahas pembuatan Ticket, pembentukan uuid berprefiks proyek, penugasan multi user, dan tenggat waktu. Status, prioritas, dan epic dibahas di Status, Prioritas, dan Epic. Komentar serta riwayat perubahan status dibahas di Komentar dan Riwayat Ticket.
Siklus hidup satu pembuatan Ticket
Diagram berikut menelusuri satu request pembuatan ticket, dari submit form sampai baris ticket_users terisi.
sequenceDiagram
actor U as User
participant CT as CreateTicket
participant T as Ticket
participant P as Project
participant DB as ticket_users
U->>CT: submit form
CT->>CT: mutateFormDataBeforeCreate isi created_by
CT->>T: parent::handleRecordCreation
T->>P: Project::find project_id
T->>T: hook creating bangun uuid
T-->>CT: record Ticket tersimpan
CT->>P: members() per assignee
alt semua assignee adalah member
CT->>DB: assignees()->sync(validAssignees)
else sebagian assignee bukan member
CT->>U: warning Some assignees removed
CT->>DB: assignees()->sync(validAssignees)
else tidak ada assignee valid
CT->>U: info Auto-assigned
CT->>DB: assignees()->sync(auth()->id())
end
%% fig: Siklus pembuatan satu Ticket
%% desc: Form CreateTicket divalidasi resource, hook creating membentuk uuid, lalu handleRecordCreation memeriksa keanggotaan proyek sebelum sync ke ticket_users.CreateTicket menimpa mutateFormDataBeforeCreate agar created_by selalu diisi user yang login app/Filament/Resources/Tickets/Pages/CreateTicket.php:56-613, sehingga nilai itu tidak bisa dipalsukan dari form. Setelah baris tickets tersimpan, handleRecordCreation memeriksa setiap assignees terhadap $project->members() dan memisahkannya menjadi validAssignees dan invalidAssignees app/Filament/Resources/Tickets/Pages/CreateTicket.php:63-1224. Tabel berikut merangkum cabang keputusannya.
| Kondisi | Aksi | Notifikasi |
|---|---|---|
| Semua assignee adalah member proyek | assignees()->sync($validAssignees) | — |
| Sebagian assignee bukan member | hanya validAssignees yang di-sync | warning Some assignees removed |
| Tidak ada assignee valid, user adalah member | sync([auth()->id()]) | info Auto-assigned |
Form dikirim tanpa assignees, user adalah member | sync([auth()->id()]) | — |
Cabang Auto-assigned dipakai saat tidak satu pun assignee lolos validasi, dan user yang sedang login langsung ditugaskan app/Filament/Resources/Tickets/Pages/CreateTicket.php:96-1075. Setelah itu getRedirectUrl mengembalikan /admin/project-board-page bila header referer memuat project-board-page, dan selebihnya ke halaman index resource app/Filament/Resources/Tickets/Pages/CreateTicket.php:124-1336.
Pembentukan uuid berprefiks proyek
Nilai uuid di sini bukan UUID RFC, melainkan string pendek berpola {prefix}-{6 karakter acak}. Hook creating mengambil Project::find($ticket->project_id), memakai $project->ticket_prefix sebagai prefiks, dan jatuh ke TKT bila proyek tidak ditemukan; enam karakter terakhir berasal dari Str::upper(Str::random(6)) app/Models/Ticket.php:34-497. Hook yang sama juga mengisi created_by bila masih kosong dan ada user yang login, sehingga insert langsung lewat model tetap meninggalkan jejak pembuat app/Models/Ticket.php:34-497.
Kolom uuid ditambahkan migration sebagai string('uuid')->after('id')->unique()->nullable() database/migrations/2025_03_13_154334_add_uuid_to_ticket_table.php:14-168. Prefiksnya bersumber dari kolom ticket_prefix di tabel projects yang wajib diisi database/migrations/2025_03_02_200055_create_projects_table.php:189.
⚠️ Hook hanya mengisi
uuidketika kolomnya masih kosong dan tidak memeriksa bentrokan nilai. Karena kolom itu ber-indexunique, dua ticket dari proyek sama yang memperoleh enam karakter acak identik akan ditolak database; tidak adaretryotomatis di hook.
Pada alur salin, uuid justru dibuang dari data form sebelum form diisi, bersama id, created_at, updated_at, dan created_by, sehingga ticket hasil salinan mendapat uuid baru dari hook app/Filament/Resources/Tickets/Pages/CreateTicket.php:16-5410.
Ketergantungan field pada project_id
Form ticket bertumpu pada satu field induk: project_id bersifat live, dan setiap perubahannya mengosongkan ticket_status_id, assignees, serta epic_id app/Filament/Resources/Tickets/TicketResource.php:64-9111.
graph LR
P["Select project_id (live)"] -->|afterStateUpdated| RS["reset ticket_status_id"]
P --> RA["reset assignees"]
P --> RE["reset epic_id"]
P --> S["Select ticket_status_id"]
P --> E["Select epic_id"]
P --> A["Select assignees (multiple)"]
A -->|project_id kosong| X["whereRaw 1 = 0"]
A -->|member proyek| OK["sync ke ticket_users"]
style P fill:#f59e0b,color:#000
style S fill:#f59e0b,color:#000
style E fill:#f59e0b,color:#000
style A fill:#f59e0b,color:#000
style X fill:#dc2626,color:#fff
style OK fill:#2d7d46,color:#fff
%% fig: Ketergantungan field form Ticket pada project_id
%% desc: Mengubah project_id mengosongkan status, assignee, dan epic; opsi status, epic, dan assignee baru terisi setelah project_id ada.| Field | Komponen | Aturan |
|---|---|---|
project_id | Select | required, searchable, preload, live, disabledOn('ticket_on_board'), dehydrated |
ticket_status_id | Select | required, opsi dari TicketStatus::where('project_id', ...) |
priority_id | Select | nullable, opsi dari TicketPriority::pluck |
epic_id | Select | nullable, opsi dari Epic::where('project_id', ...), hidden bila project_id kosong |
name | TextInput | required, maxLength(255) |
description | RichEditor | lampiran ke disk public, direktori attachments |
assignees | Select multiple | relasi assignees, hanya member proyek |
start_date | DatePicker | default now(), nullable |
due_date | DatePicker | nullable |
created_by | Select | disabled, hiddenOn(['create', 'ticket_on_board']) |
Opsi project_id berasal dari seluruh Project untuk super_admin, dan dari auth()->user()->projects() untuk peran lain app/Filament/Resources/Tickets/TicketResource.php:64-9111. Opsi status dan epic mengikuti proyek terpilih app/Filament/Resources/Tickets/TicketResource.php:93-13312. Sisanya, termasuk name, description, dan created_by, didefinisikan pada sisa skema form app/Filament/Resources/Tickets/TicketResource.php:135-19013.
Field created_by hanya tampil di halaman edit: Select itu memakai relasi creator, disabled, dan disembunyikan pada konteks create serta ticket_on_board app/Filament/Resources/Tickets/TicketResource.php:185-18914. Konteks ticket_on_board dipakai ProjectBoard saat membuat ticket dari papan kanban; bahasan papan ada di Papan Kanban dan Timeline.
Penugasan multi user
Relasi assignees adalah belongsToMany(User::class, 'ticket_users') app/Models/Ticket.php:72-7515. Tabel pivotnya dibuat dengan foreign key cascade dan unique(['ticket_id', 'user_id']), dan migration yang sama menambahkan kolom created_by nullable ke tickets database/migrations/2025_06_24_212547_add_ticket_users_table_and_created_by_column.php:14-2616.
Model menyediakan empat helper: assignUser memakai syncWithoutDetaching, unassignUser memakai detach, assignUsers memakai sync, dan isAssignedTo memeriksa keberadaan baris pivot app/Models/Ticket.php:102-12017.
Pada halaman edit, penyaringan assignee dilakukan di mutateFormDataBeforeSave dan sync dijalankan di afterSave app/Filament/Resources/Tickets/Pages/EditTicket.php:25-6818.
⚠️
mutateFormDataBeforeSavemenyaring$data['assignees']menjadivalidAssignees, tetapiafterSavemelakukansync($this->data['assignees'])dari state form apa adanya. Penyaringan itu tidak otomatis terbawa kesync, sehingga user non-member masih bisa masuk keticket_userslewat halamaneditapp/Filament/Resources/Tickets/Pages/EditTicket.php:62-6819.
Tenggat waktu
start_date dan due_date adalah dua DatePicker terpisah; start_date diberi default now() dan keduanya nullable app/Filament/Resources/Tickets/TicketResource.php:177-18420. Keduanya di-cast date pada model app/Models/Ticket.php:16-3221, dan kolom start_date ditambahkan lewat migration tersendiri database/migrations/2025_08_17_041650_add_start_date_to_tickets_table.php:11-1322.
Daftar ticket bisa disaring rentang tenggat lewat Filter::make('due_date') dengan dua input, due_from dan due_until, yang masing-masing menjadi whereDate('due_date', '>=', ...) dan whereDate('due_date', '<=', ...) app/Filament/Resources/Tickets/TicketResource.php:342-35723. Di halaman detail, due_date diberi warna danger bila isPast() dan success bila belum lewat app/Filament/Resources/Tickets/Pages/ViewTicket.php:218-22224.
Daftar, detail, dan edit
getEloquentQuery membatasi ticket untuk pengguna non-super_admin pada tiga jalur: sebagai assignees, sebagai created_by, atau sebagai member proyek app/Filament/Resources/Tickets/TicketResource.php:45-6225. TicketPolicy::hasAccessToTicket mencerminkan aturan yang sama agar sebuah permission tidak membuka ticket di luar proyek pengguna app/Policies/TicketPolicy.php:22-3926; rincian peran ada di Kontrol Akses dan Peran.
Kolom tabel menampilkan uuid yang copyable, status.name sebagai badge HTML berwarna dari $record->status?->color dengan fallback #6B7280, priority.name sebagai badge hasil match (High, Medium, Low), assignees.name yang di-limitList(2) dan expandableLimitedList, serta start_date dan due_date berformat date() app/Filament/Resources/Tickets/TicketResource.php:197-26927. Navigasi resource memakai badge berisi hasil hitung getEloquentQuery app/Filament/Resources/Tickets/TicketResource.php:401-40628.
| Filter | Tipe | Perilaku |
|---|---|---|
| Project | SelectFilter | opsi mengikuti peran super_admin atau auth()->user()->projects() |
| Status | SelectFilter | opsi dibaca dari tableFilters.project_id |
| Epic | SelectFilter | opsi dibaca dari tableFilters.project_id |
| Priority | SelectFilter | opsi dari TicketPriority::pluck |
| Assignee | SelectFilter multiple | relasi assignees |
| Created By | SelectFilter | relasi creator |
| Due Date | Filter | rentang due_from sampai due_until |
Seluruh filter itu didefinisikan dalam satu blok ->filters([...]) dengan defaultSort('created_at', 'desc') app/Filament/Resources/Tickets/TicketResource.php:276-35829.
Setiap baris punya tiga aksi: ViewAction, EditAction, dan Action::make('copy') yang mengalihkan ke route create dengan parameter copy_from app/Filament/Resources/Tickets/TicketResource.php:360-37430. Hapus massal lewat DeleteBulkAction hanya tersedia untuk super_admin app/Filament/Resources/Tickets/TicketResource.php:376-38031.
Di ViewTicket, tombol EditAction hanya muncul untuk super_admin, pembuat, atau assignee ticket, dan ada tombol back menuju ProjectBoard proyek terkait app/Filament/Resources/Tickets/Pages/ViewTicket.php:144-16132.
⚠️
Action::make('copy')hanya mengopercopy_from, tanpa memeriksa ulang hak akses di sisi klien; penjagaan tetap terjadi diCreateTicket::fillForm, yang memakai query ber-scope sama sepertigetEloquentQueryuntuk pengguna non-super_adminapp/Filament/Resources/Tickets/Pages/CreateTicket.php:16-5410.