- Implemented academic advising logs functionality with create, edit, and delete capabilities. - Created UI components for displaying academic advising logs in a data table format. - Added letter requests management with similar CRUD operations and UI components. - Refactored routes to organize academic classes, announcements, finances, and services under appropriate namespaces.
8.1 KiB
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).
// 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:
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
// ❌ 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 <Select>), pakai options()
alih-alih membangun array {value, label} manual dengan array_map:
// ❌ Jangan
'types' => array_map(fn (FeedbackType $type) => [
'value' => $type->value,
'label' => $type->label(),
], FeedbackType::cases()),
// ✅ Pakai
'types' => FeedbackType::options(),
Enum baru wajib langsung pakai use HasValues; sejak awal dibuat, tidak
perlu menunggu ada kebutuhan values()/options() baru ditambahkan.
2. Migration ✅
Aturan: kolom enum() di migration wajib pakai EnumClass::values(),
konsisten dengan aturan Enum di atas. Kolom ->default() wajib pakai
EnumClass::Case->value (string), bukan objek case-nya langsung.
// ❌ Jangan
$table->enum('status', array_values(StudentStatus::cases()))
->nullable()
->default(StudentStatus::Active); // objek enum, bukan string
// ✅ Pakai
$table->enum('status', StudentStatus::values())
->nullable()
->default(StudentStatus::Active->value);
3. Bahasa UI ✅
Seluruh teks yang tampil ke pengguna (label, judul halaman, pesan toast, placeholder, pesan error) ditulis dalam Bahasa Indonesia. Istilah teknis dan identifier kode tetap dalam bahasa aslinya (bahasa Inggris), tapi teks yang dibaca pengguna — termasuk teks di halaman auth/settings bawaan starter-kit yang aslinya berbahasa Inggris — harus diterjemahkan supaya konsisten dengan sisa aplikasi.
4. Struktur modul Admin ✅
Semua fitur berada di bawah /admin — tidak ada lagi controller atau
halaman yang menggantung di top-level App\Http\Controllers /
resources/js/pages/ di luar folder admin/.
Aturan utama: namespace/folder PHP (Controller, Request, Service) dan folder
halaman Inertia (resources/js/pages/admin/...) SELALU mengikuti prefix route
di routes/admin.php — satu grup di sidebar (app-sidebar.tsx) = satu prefix
route = satu namespace. Jangan menebak dari label sidebar; selalu cek dulu
prefix route-nya sebelum menaruh file baru.
| Grup sidebar | Namespace Controller/Request/Service | Prefix route |
|---|---|---|
| Pengguna | Admin\Users |
admin/users/* |
| Master | Admin\Master |
admin/master/* |
| Kelas | Admin\AcademicClasses |
admin/academic-classes/* |
| Kelola | Admin\Manage |
admin/manage/* |
| Pengumuman | Admin\Announcements |
admin/announcements |
| Keuangan | Admin\Finances |
admin/finances/* |
| Layanan | Admin\Services |
admin/services/* |
| Pengaturan | Admin\Settings |
admin/settings/* |
| Resource mandiri (navSecondary, bukan grup di navMain) | Admin (langsung, tanpa sub-namespace) |
admin/<resource> |
"Resource mandiri" dipakai untuk fitur yang tidak masuk ke grup sidebar
manapun di navMain — misalnya Admin\FeedbackController di admin/feedback
(dari menu navSecondary, data personal milik user yang sedang login, bukan
data akademik yang dikelola admin atas user lain).
Nama route mengikuti pola admin.<grup>.<resource>.<action> (atau
admin.<resource>.<action> untuk resource mandiri, atau admin.<grup>.<action>
langsung kalau grup itu cuma punya satu resource yang namanya sama dengan
grupnya sendiri — lihat pola Pengumuman/AnnouncementController di
routes/admin.php, supaya URL-nya tidak jadi admin/announcements/announcements
yang berulang). File Wayfinder hasil generate mengikuti struktur folder yang
sama di resources/js/routes/admin/<grup>/<resource>/, dan folder halaman
Inertia-nya di resources/js/pages/admin/<grup>/<resource>/.
Kalau satu Service dipakai lintas grup (mis. CourseClassService — miliknya
grup Kelola — dipakai juga oleh controller-controller di grup Kelas untuk
mengisi dropdown), itu boleh, tetap use dari namespace aslinya
(App\Services\Admin\Manage\CourseClassService) — jangan dipindah/duplikasi
hanya karena dipakai controller di grup lain.
Halaman CRUD frontend memakai pola: PageHeader (judul + tombol Tambah) →
FormDialog (Create/Edit) → DataTable + columns.tsx → DeleteConfirmDialog.
5. Service ✅
Method listing berpaginasi wajib bernama paginated(), bukan
getPaginated() atau nama lain. Signature-nya seragam di seluruh Service
(kecuali ada parameter domain tambahan di depan, seperti User $user di
FeedbackService):
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator
Jangan tambahkan parameter array $filters = [] kalau tidak benar-benar
dipakai di dalam method — PaginatedRequest::validatedWithDefaults() cuma
menghasilkan perPage/search/sort/direction, jadi parameter $filters
di banyak Service sebelumnya selalu kosong dan tidak pernah terisi oleh
controller manapun.
Controller yang memanggilnya selalu pakai PaginatedRequest + spread:
'items' => $this->service->paginated(...$request->validatedWithDefaults()),
Method yang mengembalikan daftar penuh tanpa paginasi dibedakan sesuai kegunaannya:
getAllForSelect()— dipakai kalau hasilnya untuk mengisi dropdown<Select>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) atauAttendanceService::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. Wayfinder ✅
Selalu jalankan generate dengan flag form variant, supaya <Form {...Controller.method.form()}> tidak error saat runtime:
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.