feat: refactor controllers and update department service usage for consistency #38

Merged
pangestu merged 1 commits from feat/refactor-controllers into dev 2026-08-25 20:52:13 +08:00
14 changed files with 177 additions and 57 deletions

View File

@ -15,7 +15,9 @@
class FeedbackController extends Controller
{
public function __construct(private readonly FeedbackService $service) {}
public function __construct(
private readonly FeedbackService $service,
) {}
public function index(PaginatedRequest $request): Response
{

View File

@ -7,7 +7,7 @@
use App\Http\Requests\PaginatedRequest;
use App\Models\Course;
use App\Services\Admin\Manage\CourseService;
use App\Services\Admin\Users\DepartmentService;
use App\Services\Admin\Master\DepartmentService;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;

View File

@ -49,8 +49,6 @@ public function destroy(Material $material): RedirectResponse
{
$this->service->delete($material);
Inertia::flash('toast', ['type' => 'success', 'message' => 'Materi berhasil dihapus.']);
return back();
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Materi berhasil dihapus.'])->back();
}
}

View File

@ -48,8 +48,6 @@ public function destroy(Schedule $schedule): RedirectResponse
{
$this->service->delete($schedule);
Inertia::flash('toast', ['type' => 'success', 'message' => 'Jadwal berhasil dihapus.']);
return back();
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Jadwal berhasil dihapus.'])->back();
}
}

View File

@ -52,8 +52,6 @@ public function destroy(TuitionInvoice $tuitionInvoice): RedirectResponse
{
$this->service->delete($tuitionInvoice);
Inertia::flash('toast', ['type' => 'success', 'message' => 'Tagihan berhasil dihapus.']);
return back();
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Tagihan berhasil dihapus.'])->back();
}
}

View File

@ -14,7 +14,7 @@
class AcademicTermController extends Controller
{
public function __construct(
private readonly AcademicTermService $service
private readonly AcademicTermService $service,
) {}
public function index(PaginatedRequest $request): Response
@ -33,18 +33,18 @@ public function store(AcademicTermRequest $request): RedirectResponse
return to_route('admin.master.academic-terms.index');
}
public function update(AcademicTermRequest $request, AcademicTerm $AcademicTerm): RedirectResponse
public function update(AcademicTermRequest $request, AcademicTerm $academicTerm): RedirectResponse
{
$this->service->update($AcademicTerm, $request->validated());
$this->service->update($academicTerm, $request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Periode akademik berhasil diperbarui.']);
return to_route('admin.master.academic-terms.index');
}
public function destroy(AcademicTerm $AcademicTerm): RedirectResponse
public function destroy(AcademicTerm $academicTerm): RedirectResponse
{
$this->service->delete($AcademicTerm);
$this->service->delete($academicTerm);
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Periode akademik berhasil dihapus.'])->back();
}

View File

@ -14,7 +14,7 @@
class DepartmentController extends Controller
{
public function __construct(
private readonly DepartmentService $service
private readonly DepartmentService $service,
) {}
public function index(PaginatedRequest $request): Response

View File

@ -15,9 +15,6 @@
class ProfileController extends Controller
{
/**
* Show the user's profile settings page.
*/
public function edit(Request $request): Response
{
$user = $request->user();
@ -37,9 +34,6 @@ public function edit(Request $request): Response
]);
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$user = $request->user();
@ -61,9 +55,6 @@ public function update(ProfileUpdateRequest $request): RedirectResponse
return to_route('admin.settings.profile.edit');
}
/**
* Delete the user's profile.
*/
public function destroy(ProfileDeleteRequest $request): RedirectResponse
{
$user = $request->user();

View File

@ -11,9 +11,6 @@
class SecurityController extends Controller
{
/**
* Show the user's security settings page.
*/
public function edit(): Response
{
return Inertia::render('admin/settings/security', [
@ -21,17 +18,12 @@ public function edit(): Response
]);
}
/**
* Update the user's password.
*/
public function update(PasswordUpdateRequest $request): RedirectResponse
{
$request->user()->update([
'password' => $request->password,
]);
Inertia::flash('toast', ['type' => 'success', 'message' => 'Kata sandi berhasil diperbarui.']);
return back();
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Kata sandi berhasil diperbarui.'])->back();
}
}

View File

@ -15,7 +15,7 @@
class AdministratorController extends Controller
{
public function __construct(
protected AdministratorService $service,
private readonly AdministratorService $service,
) {}
public function index(Request $request): Response

View File

@ -5,7 +5,7 @@
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Users\LecturerRequest;
use App\Models\User;
use App\Services\Admin\Users\DepartmentService;
use App\Services\Admin\Master\DepartmentService;
use App\Services\Admin\Users\LecturerService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@ -15,8 +15,8 @@
class LecturerController extends Controller
{
public function __construct(
protected LecturerService $service,
protected DepartmentService $departmentService,
private readonly LecturerService $service,
private readonly DepartmentService $departmentService,
) {}
public function index(Request $request): Response

View File

@ -5,7 +5,7 @@
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Users\StudentRequest;
use App\Models\User;
use App\Services\Admin\Users\DepartmentService;
use App\Services\Admin\Master\DepartmentService;
use App\Services\Admin\Users\LecturerService;
use App\Services\Admin\Users\StudentService;
use Illuminate\Http\RedirectResponse;
@ -16,9 +16,9 @@
class StudentController extends Controller
{
public function __construct(
protected StudentService $service,
protected DepartmentService $departmentService,
protected LecturerService $lecturerService,
private readonly StudentService $service,
private readonly DepartmentService $departmentService,
private readonly LecturerService $lecturerService,
) {}
public function index(Request $request): Response

View File

@ -1,14 +0,0 @@
<?php
namespace App\Services\Admin\Users;
use App\Models\Department;
use Illuminate\Support\Collection;
class DepartmentService
{
public function getAll(): Collection
{
return Department::select(['id', 'name'])->get();
}
}

155
docs/code-style.md Normal file
View File

@ -0,0 +1,155 @@
# 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 `<Select>`), pakai `options()`
alih-alih membangun array `{value, label}` manual dengan `array_map`:
```php
// ❌ 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.
```php
// ❌ 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/`. Setiap fitur admin baru
(CRUD) mengikuti struktur namespace yang sama, di salah satu dari lima grup:
| Grup | Namespace Controller/Request | Prefix route |
| ------------------ | --------------------------------------- | ------------------ |
| Master data | `Admin\Master` | `admin/master/*` |
| Data operasional | `Admin\Manage` | `admin/manage/*` |
| Manajemen pengguna | `Admin\Users` | `admin/users/*` |
| Pengaturan akun | `Admin\Settings` | `admin/settings/*` |
| Resource mandiri | `Admin` (langsung, tanpa sub-namespace) | `admin/<resource>` |
"Resource mandiri" dipakai untuk fitur yang tidak cocok masuk ke 4 grup di
atas — misalnya `Admin\FeedbackController` di `admin/feedback` (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), dan file Wayfinder
hasil generate mengikuti struktur folder yang sama di
`resources/js/routes/admin/<grup>/<resource>/`.
Halaman CRUD frontend memakai pola: `PageHeader` (judul + tombol Tambah) →
`FormDialog` (Create/Edit) → `DataTable` + `columns.tsx``DeleteConfirmDialog`.
---
## 5. Wayfinder ✅
Selalu jalankan generate dengan flag form variant, supaya `<Form
{...Controller.method.form()}>` 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.