5
Manajemen Proyek dan Anggota
ProjectResource menyatukan pengelolaan Project: satu form, satu tabel daftar, empat halaman, dan lima relation manager. Halaman ini membahas siklus hidup Project, pembatasan query daftar project, lalu tiga relation manager yang mengurus anggota, catatan, dan status ticket. Alur ticket itu sendiri dibahas di Manajemen Ticket dan Status, Prioritas, dan Epic.
Siklus hidup Project
Empat route terdaftar di getPages: index, create, view, dan edit. app/Filament/Resources/Projects/ProjectResource.php:206-2151 Diagram berikut merangkum keadaan yang bisa dicapai satu Project setelah dibuat sampai dihapus.
stateDiagram-v2
[*] --> Created: "CreateProject"
Created --> Seeded: "afterCreate: 5 default statuses"
Seeded --> MemberBound: "syncWithoutDetaching"
MemberBound --> Pinned: "pin()"
Pinned --> MemberBound: "unpin()"
MemberBound --> Progress: "progress_percentage"
Progress --> Completed: "progress_percentage = 100"
Completed --> Progress: "ticket reopened"
MemberBound --> Shared: "generateExternalAccess()"
Shared --> MemberBound: "token regenerated"
Completed --> [*]: "DeleteBulkAction"
style Created fill:#2d7d46,color:#fff
style Seeded fill:#f59e0b,color:#000
style MemberBound fill:#336791,color:#fff
style Pinned fill:#8b5cf6,color:#fff
style Progress fill:#8b5cf6,color:#fff
style Completed fill:#2d7d46,color:#fff
style Shared fill:#f59e0b,color:#000
%% fig: Siklus hidup Project
%% desc: Project dibuat, diberi status default, diikat ke anggota, lalu bergerak antara pin, progres, dan akses eksternal.Pembuatan project ditangani CreateProject::afterCreate. Ketika create_default_statuses bernilai true, method itu membuat lima status — Backlog, To Do, In Progress, Review, dan Done — dengan Done ditandai is_completed => true. Setelah itu pembuat project dimasukkan sebagai anggota lewat syncWithoutDetaching, sehingga getEloquentQuery tetap menemukan project itu untuknya. app/Filament/Resources/Projects/Pages/CreateProject.php:12-342
Toggle create_default_statuses hanya tampil di halaman create, memakai dehydrated(false), dan disembunyikan oleh visible(fn($livewire) => $livewire instanceof CreateProject). app/Filament/Resources/Projects/ProjectResource.php:79-843
Pin bukan kolom boolean. getIsPinnedAttribute membaca pinned_date, sedangkan pin() dan unpin() hanya mengisi atau mengosongkan kolom itu. app/Models/Project.php:32-454 Toggle di tabel memanggil kedua method tersebut, bukan menulis kolom langsung. app/Filament/Resources/Projects/ProjectResource.php:158-1685
getProgressPercentageAttribute menghitung rasio ticket yang statusnya is_completed terhadap seluruh ticket project, lalu membulatkannya satu desimal. app/Models/Project.php:95-1106 Badge progress memilih warna dari ambang 100, 75, 50, dan 25 menjadi success, info, warning, gray, dan danger. app/Filament/Resources/Projects/ProjectResource.php:122-1357
getRemainingDaysAttribute mengembalikan null bila end_date kosong dan 0 bila end_date sudah lewat. app/Models/Project.php:79-938
Halaman daftar dan pembatasan query
ListProjects hanya menambahkan CreateAction di header; tidak ada aksi lain di halaman itu. app/Filament/Resources/Projects/Pages/ListProjects.php:10-199
getEloquentQuery membedakan super_admin dari pengguna lain. Untuk yang bukan super_admin, query disaring dengan whereHas('members', ...) pada user_id milik pengguna yang sedang login, jadi project hanya muncul bila pengguna terdaftar di pivot. app/Filament/Resources/Projects/ProjectResource.php:217-23310
| Kolom | Komponen | Perilaku | Rujukan |
|---|---|---|---|
color | ColorColumn | lebar 40px, default #6B7280 | app/Filament/Resources/Projects/ProjectResource.php:113-18311 |
name, ticket_prefix | TextColumn | searchable | app/Filament/Resources/Projects/ProjectResource.php:113-18311 |
progress_percentage | TextColumn | badge persen dari accessor, sortable | app/Filament/Resources/Projects/ProjectResource.php:113-18311 |
start_date, end_date | TextColumn | format d/m/Y, sortable | app/Filament/Resources/Projects/ProjectResource.php:113-18311 |
remaining_days | TextColumn | null tanpa end_date, badge danger bila <= 0 | app/Filament/Resources/Projects/ProjectResource.php:113-18311 |
is_pinned | ToggleColumn | memanggil pin() atau unpin() | app/Filament/Resources/Projects/ProjectResource.php:113-18311 |
members_count, tickets_count | TextColumn | memakai counts() | app/Filament/Resources/Projects/ProjectResource.php:113-18311 |
created_at, updated_at | TextColumn | tersembunyi secara default | app/Filament/Resources/Projects/ProjectResource.php:113-18311 |
Relation manager di halaman View
getRelations mendaftarkan lima relation manager dengan urutan tetap: TicketStatusesRelationManager, MembersRelationManager, EpicsRelationManager, TicketsRelationManager, dan NotesRelationManager. app/Filament/Resources/Projects/ProjectResource.php:195-20412
| Relation manager | $relationship | Badge | Fokus | Rujukan |
|---|---|---|---|---|
MembersRelationManager | members | members_count | attach dan detach anggota | app/Filament/Resources/Projects/RelationManagers/MembersRelationManager.php:23-2813 |
TicketStatusesRelationManager | ticketStatuses | ticket_statuses_count | status ticket milik project | app/Filament/Resources/Projects/RelationManagers/TicketStatusesRelationManager.php:27-3214 |
NotesRelationManager | notes | tidak ada | catatan rapat dan ringkasan | app/Filament/Resources/Projects/RelationManagers/NotesRelationManager.php:26-3415 |
Halaman ViewProject menambahkan tiga aksi header: EditAction, aksi board yang membuka ProjectBoard::getUrl(['project_id' => ...]), dan aksi external_access yang hanya terlihat bagi super_admin. app/Filament/Resources/Projects/Pages/ViewProject.php:21-5716 Aksi external_access memanggil generateExternalAccess() bila project belum punya ExternalAccess, lalu menyusun URL /external/{access_token}; mekanismenya dibahas di Portal Eksternal Klien.
Infolist ViewProject memuat Section Project Statistics yang menghitung members(), tickets(), epics(), dan ticketStatuses() langsung dari relasi. app/Filament/Resources/Projects/Pages/ViewProject.php:117-14217
Anggota project
members() adalah belongsToMany ke User melalui pivot project_members dengan withTimestamps(). app/Models/Project.php:57-6118 Tabel pivot menyimpan project_id dan user_id, menghapus baris secara cascade, dan memasang unique(['project_id', 'user_id']) sebagai penjaga duplikat. database/migrations/2025_03_02_200109_create_project_members_table.php:14-2019
Penambahan anggota memakai AttachAction berlabel Add Member dengan preloadRecordSelect() dan recordSelectSearchColumns(['name', 'email']). Select itu juga menyediakan form buat user baru lewat UserResource::form, sehingga user baru bisa dibuat dan langsung di-attach tanpa meninggalkan halaman. Hook after kemudian mengirim ProjectMemberAttached dengan tiga argumen: project, user, dan pengguna yang melakukan attach. app/Filament/Resources/Projects/RelationManagers/MembersRelationManager.php:53-8020
Penghapusan per baris memakai DetachAction berlabel Remove yang mengirim ProjectMemberDetached dengan project, user, dan pengguna yang menghapus. app/Filament/Resources/Projects/RelationManagers/MembersRelationManager.php:82-9321 Toolbar hanya berisi DetachBulkAction berlabel Remove Selected tanpa hook after, sehingga penghapusan massal tidak memicu event tersebut. app/Filament/Resources/Projects/RelationManagers/MembersRelationManager.php:95-10022
⚠️
DetachBulkActiontidak memanggil listener apa pun, jadi user yang dilepas lewatRemove Selectedtidak menerima notifikasi removal. GunakanRemoveper baris bila jejak notifikasi dibutuhkan.
Kedua event adalah kelas biasa dengan properti publik dan trait Dispatchable, InteractsWithSockets, SerializesModels. app/Events/ProjectMemberAttached.php:11-2023 app/Events/ProjectMemberDetached.php:11-2024 Pemetaan event ke listener ditulis eksplisit di EventServiceProvider::$listen, sementara shouldDiscoverEvents() mengembalikan false sehingga tidak ada penemuan otomatis. app/Providers/EventServiceProvider.php:18-2525
graph LR
UI["AttachAction / DetachAction"] --> RM["MembersRelationManager"]
RM --> PM[("project_members")]
RM --> EV["ProjectMemberAttached / ProjectMemberDetached"]
EV --> LS["SendProjectAssignmentNotification / SendProjectRemovalNotification"]
LS --> NS["NotificationService"]
NS --> DB[("notifications")]
NS --> MAIL["ProjectAssignmentNotification mail"]
style UI fill:#f59e0b,color:#000
style RM fill:#f59e0b,color:#000
style PM fill:#336791,color:#fff
style EV fill:#8b5cf6,color:#fff
style LS fill:#8b5cf6,color:#fff
style NS fill:#f59e0b,color:#000
style DB fill:#336791,color:#fff
style MAIL fill:#2d7d46,color:#fff
%% fig: Jalur event keanggotaan project
%% desc: Aksi di relation manager menulis pivot project_members lalu memicu event yang diteruskan listener ke NotificationService.SendProjectAssignmentNotification mengimplementasikan ShouldQueue dan membungkus pemanggilan service dengan try/catch yang menulis Log::error berisi project_id dan user_id. app/Listeners/SendProjectAssignmentNotification.php:12-3426 SendProjectRemovalNotification juga queued, tetapi langsung memanggil service tanpa try/catch. app/Listeners/SendProjectRemovalNotification.php:10-2527
notifyProjectAssignment membuat baris Notification bertipe project_assigned dengan judul Added to Project, lalu mengirim email ProjectAssignmentNotification ke $assignedUser->email. Kedua langkah berdiri di blok try masing-masing, jadi kegagalan email tidak membatalkan notifikasi in-app. app/Services/NotificationService.php:107-14128 notifyProjectRemoval hanya membuat baris Notification bertipe project_removed dengan judul Removed from Project dan pesan yang menyebut nama penghapus. app/Services/NotificationService.php:143-15729
Catatan project
Project::notes() adalah hasMany ke ProjectNote. app/Models/Project.php:74-7730 ProjectNote menyimpan project_id, created_by, title, content, dan note_date, meng-cast note_date menjadi date, serta menyediakan relasi creator() yang menunjuk User lewat kolom created_by, accessor formatted_note_date, dan scope forProject. app/Models/ProjectNote.php:10-4431
Form catatan mewajibkan title dengan batas 255 karakter dan content lewat RichEditor. note_date default now() dan wajib diisi, sedangkan created_by diisi lewat Hidden::make('created_by')->default(auth()->id()). Lampiran editor disimpan di disk public pada direktori attachments. app/Filament/Resources/Projects/RelationManagers/NotesRelationManager.php:36-7732
Tabel catatan menampilkan title, note_date berformat M d, Y, creator.name sebagai Created by, dan created_at yang tersembunyi secara default. Filter recent membatasi created_at ke 30 hari terakhir, defaultSort memakai note_date menurun, dan empty state memakai heading No project notes yet. app/Filament/Resources/Projects/RelationManagers/NotesRelationManager.php:79-13133
Status ticket milik project
TicketStatusesRelationManager mengelola status per project: name, color dengan default #3490dc, sort_order default 0, dan toggle is_completed. Setiap kali toggle dinyalakan, komponen mencari status lain di project yang sama dengan is_completed = true; bila ada, nilainya dibatalkan dan Notification peringatan berjudul Cannot mark as completed dikirim. app/Filament/Resources/Projects/RelationManagers/TicketStatusesRelationManager.php:34-7434
Tabel status memakai defaultSort('sort_order') dan reorderable('sort_order'), jadi urutan kolom di papan ditentukan kolom itu. app/Filament/Resources/Projects/RelationManagers/TicketStatusesRelationManager.php:76-9335 CreateAction mengambil max('sort_order') dari relasi lalu mengisi sort_order dengan nilai maksimum ditambah satu, dan mengulang validasi is_completed sebelum data disimpan. app/Filament/Resources/Projects/RelationManagers/TicketStatusesRelationManager.php:97-12136 EditAction menjalankan validasi yang sama dengan mengecualikan record yang sedang diedit melalui where('id', '!=', $record->id). app/Filament/Resources/Projects/RelationManagers/TicketStatusesRelationManager.php:123-14537
Aturan satu status selesai per project itu yang membuat progress_percentage punya satu titik acuan. Konsekuensi aturan tersebut pada ticket dijelaskan di Status, Prioritas, dan Epic, sedangkan pemakaian sort_order di papan dijelaskan di Papan Kanban dan Timeline.