Lompat ke konten
dokumentasi.id
Commit a2522f5
v1
Star645

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-215 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.
Fig. 1: Siklus hidup ProjectProject 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-34

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-84

Pin bukan kolom boolean. getIsPinnedAttribute membaca pinned_date, sedangkan pin() dan unpin() hanya mengisi atau mengosongkan kolom itu. app/Models/Project.php:32-45 Toggle di tabel memanggil kedua method tersebut, bukan menulis kolom langsung. app/Filament/Resources/Projects/ProjectResource.php:158-168

getProgressPercentageAttribute menghitung rasio ticket yang statusnya is_completed terhadap seluruh ticket project, lalu membulatkannya satu desimal. app/Models/Project.php:95-110 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-135

getRemainingDaysAttribute mengembalikan null bila end_date kosong dan 0 bila end_date sudah lewat. app/Models/Project.php:79-93

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-19

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-233

KolomKomponenPerilakuRujukan
colorColorColumnlebar 40px, default #6B7280app/Filament/Resources/Projects/ProjectResource.php:113-183
name, ticket_prefixTextColumnsearchableapp/Filament/Resources/Projects/ProjectResource.php:113-183
progress_percentageTextColumnbadge persen dari accessor, sortableapp/Filament/Resources/Projects/ProjectResource.php:113-183
start_date, end_dateTextColumnformat d/m/Y, sortableapp/Filament/Resources/Projects/ProjectResource.php:113-183
remaining_daysTextColumnnull tanpa end_date, badge danger bila <= 0app/Filament/Resources/Projects/ProjectResource.php:113-183
is_pinnedToggleColumnmemanggil pin() atau unpin()app/Filament/Resources/Projects/ProjectResource.php:113-183
members_count, tickets_countTextColumnmemakai counts()app/Filament/Resources/Projects/ProjectResource.php:113-183
created_at, updated_atTextColumntersembunyi secara defaultapp/Filament/Resources/Projects/ProjectResource.php:113-183

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-204

Relation manager$relationshipBadgeFokusRujukan
MembersRelationManagermembersmembers_countattach dan detach anggotaapp/Filament/Resources/Projects/RelationManagers/MembersRelationManager.php:23-28
TicketStatusesRelationManagerticketStatusesticket_statuses_countstatus ticket milik projectapp/Filament/Resources/Projects/RelationManagers/TicketStatusesRelationManager.php:27-32
NotesRelationManagernotestidak adacatatan rapat dan ringkasanapp/Filament/Resources/Projects/RelationManagers/NotesRelationManager.php:26-34

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-57 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-142

Anggota project

members() adalah belongsToMany ke User melalui pivot project_members dengan withTimestamps(). app/Models/Project.php:57-61 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-20

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-80

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-93 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-100

⚠️ DetachBulkAction tidak memanggil listener apa pun, jadi user yang dilepas lewat Remove Selected tidak menerima notifikasi removal. Gunakan Remove per baris bila jejak notifikasi dibutuhkan.

Kedua event adalah kelas biasa dengan properti publik dan trait Dispatchable, InteractsWithSockets, SerializesModels. app/Events/ProjectMemberAttached.php:11-20 app/Events/ProjectMemberDetached.php:11-20 Pemetaan event ke listener ditulis eksplisit di EventServiceProvider::$listen, sementara shouldDiscoverEvents() mengembalikan false sehingga tidak ada penemuan otomatis. app/Providers/EventServiceProvider.php:18-25

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.
Fig. 2: Jalur event keanggotaan projectAksi 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-34 SendProjectRemovalNotification juga queued, tetapi langsung memanggil service tanpa try/catch. app/Listeners/SendProjectRemovalNotification.php:10-25

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-141 notifyProjectRemoval hanya membuat baris Notification bertipe project_removed dengan judul Removed from Project dan pesan yang menyebut nama penghapus. app/Services/NotificationService.php:143-157

Catatan project

Project::notes() adalah hasMany ke ProjectNote. app/Models/Project.php:74-77 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-44

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-77

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-131

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-74

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-93 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-121 EditAction menjalankan validasi yang sama dengan mengecualikan record yang sedang diedit melalui where('id', '!=', $record->id). app/Filament/Resources/Projects/RelationManagers/TicketStatusesRelationManager.php:123-145

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.