# Panduan Gaya Kode Dokumen ini mencatat konvensi penulisan kode di proyek ini, supaya kontributor (termasuk AI assistant) mengikuti pola yang sama dan tidak menulis ulang solusi yang sudah ada dengan cara berbeda-beda di tiap file. Setiap bagian punya status: - ✅ **Selesai** — sudah diterapkan di seluruh kode yang relevan. - 🚧 **Sebagian** — sudah ada polanya, tapi belum diterapkan di semua tempat. - 📝 **Rencana** — baru berupa kesepakatan, belum diterapkan. --- ## 1. Enum ✅ **Aturan:** setiap backed enum (`enum X: string`) wajib pakai trait `App\Enums\Concerns\HasValues`. Jangan pernah menulis `array_values(X::cases())` atau `array_column(X::cases(), 'value')` secara manual — pakai `X::values()` (atau `X::options()` kalau butuh label untuk dropdown/select). ```php // app/Enums/Concerns/HasValues.php trait HasValues { public static function values(): array // ['male', 'female'] public static function options(): array // [{value: 'male', label: 'Laki-laki'}, ...] } ``` Struktur enum standar: ```php enum Gender: string { use HasValues; case Male = 'male'; case Female = 'female'; public function label(): string { return match ($this) { self::Male => 'Laki-laki', self::Female => 'Perempuan', }; } } ``` **Sebelum → Sesudah** ```php // ❌ Jangan 'gender' => ['required', 'string', Rule::in(array_values(Gender::cases()))], 'type' => ['required', 'string', Rule::in(array_column(FeedbackType::cases(), 'value'))], // ✅ Pakai 'gender' => ['required', 'string', Rule::in(Gender::values())], 'type' => ['required', 'string', Rule::in(FeedbackType::values())], ``` Untuk data yang dikirim ke frontend (mis. isi `` di form fitur *lain* (mis. `DepartmentService::getAllForSelect()` dipakai di form Course/Lecturer/Student, bukan di halaman Department sendiri). - **Nama sesuai domain** — kalau daftar itu justru jadi listing utama halaman itu sendiri (bukan sumber dropdown fitur lain) dan memang tidak butuh paginasi, pakai nama yang menjelaskan isinya, mis. `ScheduleService::all()` (listing halaman jadwal) atau `AttendanceService::sessions()` (listing sesi presensi). Jangan pernah menulis `getAll()` polos — nama itu ambigu antara dua kasus di atas. Urutan method standar dalam satu Service: `getAllForSelect()` (kalau ada) → `paginated()` → `create()` → `update()` → `delete()` → helper `private` di paling bawah. --- ## 6. Filter dropdown di halaman listing ✅ Tombol filter (ikon corong di samping search box) memakai dialog, bukan popover — komponennya `resources/js/components/filter-dialog.tsx` (`` dilewatkan lewat prop `toolbar` milik ``, otomatis nongol di sebelah kanan search box). Sudah diterapkan di semua halaman listing yang punya field layak difilter: | Halaman | Field filter | | --------------------------- | -------------------------------- | | Administrator/Dosen/Mahasiswa | Jenis Kelamin (+ Jurusan, Status untuk Dosen/Mahasiswa) | | Periode Akademik | Semester, Status Aktif | | Registrasi KRS | Status, Periode Akademik | | Materi, Tugas | Kelas | | Mata Kuliah | Jurusan | | Kelas Mata Kuliah | Periode Akademik, Metode | | Pengumuman | Jurusan | | Tagihan | Periode Akademik | | Surat Permohonan | Status | | Bimbingan Akademik | Dosen | | Kritik dan Saran | Jenis, Status | Halaman tanpa field yang layak difilter (mis. Jurusan/Master — tabelnya kecil, tidak butuh filter) sengaja tidak diberi `FilterDialog`. **Perilaku UI (jangan diubah tanpa alasan kuat):** - **Langsung diterapkan** — begitu satu field di dalam dialog dipilih (`onValueChange`), filter langsung jalan (navigasi Inertia), tidak ada tombol "Terapkan" terpisah yang harus diklik dulu. - **Field filter disusun 2 kolom per baris** (`grid grid-cols-2 gap-4`) kalau field-nya lebih dari satu; kalau cuma 1 field, 1 kolom saja (`grid gap-4`) — jangan sisakan slot kosong di grid. - **Tombol "Hapus Filter" ada DI LUAR dialog**, sejajar di samping tombol ikon filter (bukan di footer dialog), berupa tombol teks (`variant="ghost"` + label "Hapus Filter"), dan cuma muncul kalau ada filter yang aktif. **Backend:** 1. Field filter (mis. `gender`, `department_id`, `status`) didaftarkan di `App\Http\Requests\PaginatedRequest::rules()` sebagai `nullable` — request ini dipakai bersama oleh semua halaman listing, jadi field filter yang sifatnya umum (dipakai lebih dari satu fitur) taruh di sini alih-alih bikin FormRequest baru per halaman. 2. `Service::paginated()` menerima parameter filter tambahan sebagai parameter bernama opsional di akhir signature (setelah `$perPage/$search/$sort/$direction`), mis. `paginated(..., ?string $gender = null, ?int $departmentId = null)`, lalu diterapkan dengan `->when($gender, fn ($q) => ...)`. 3. Controller memanggilnya secara **eksplisit per parameter** — jangan nge-spread seluruh `$request->validated()` mentah-mentah ke `paginated()`, karena tidak semua Service menerima semua field filter (bisa error "Unknown named parameter"): ```php 'students' => $this->service->paginated( ...$request->validatedWithDefaults(), gender: $request->validated('gender'), departmentId: $request->validated('department_id'), status: $request->validated('status'), ), 'filters' => $request->only(['gender', 'department_id', 'status']), ``` **Frontend:** 1. `Props.filters` menampung nilai filter yang sedang aktif (dari query string, dikirim controller lewat `$request->only([...])`). 2. `useServerTable({..., filters})` — prop `filters` diteruskan apa adanya dari `Props.filters` (bukan `useState` terpisah, karena Inertia sudah selalu mengirim prop terbaru setiap navigasi). 3. Definisikan `filterFields: FilterField[]` (key, label, options) sesuai data yang tersedia di halaman itu (mis. `departments` dari prop untuk Select jurusan), lalu render `` sebagai `toolbar` di ``. --- ## 7. Validasi wajib pakai FormRequest ✅ Sekecil apapun validasinya (bahkan cuma 1 field), **jangan** pakai `$request->validate([...])` inline di controller — selalu buat class `FormRequest` sendiri di `app/Http/Requests//`, meski isinya cuma satu rule. Ini menjaga controller tetap ramping dan validasi tetap mudah ditemukan/dites secara konsisten di satu tempat. ```php // ❌ Jangan public function updateStatus(Request $request, User $user): RedirectResponse { $data = $request->validate([ 'status' => ['required', 'string', Rule::in(StudentStatus::values())], ]); // ... } // ✅ Pakai public function updateStatus(StudentStatusRequest $request, User $user): RedirectResponse { $this->service->updateStatus($user, $request->validated('status')); // ... } ``` --- ## 8. Wayfinder ✅ Selalu jalankan generate dengan flag form variant, supaya `
` tidak error saat runtime: ```bash php artisan wayfinder:generate --with-form ``` `vite.config.ts` sudah diset `wayfinder({ formVariants: true })` sehingga dev-server otomatis benar. Flag `--with-form` hanya wajib diingat kalau generate manual lewat CLI.