From d59e19ed3e68c5eb3c67c6e1b8b5c11fcf5afc71 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Wed, 26 Aug 2026 01:13:18 +0700 Subject: [PATCH] feat: implement student status update functionality and validation --- .../Admin/Users/StudentController.php | 8 +++ .../Admin/Users/StudentStatusRequest.php | 22 +++++++ app/Services/Admin/Users/StudentService.php | 6 +- docs/code-style.md | 30 +++++++++- .../js/components/student-status-badge.tsx | 60 ++++++++++++++----- .../js/pages/admin/users/students/columns.tsx | 20 ++++++- .../js/pages/admin/users/students/edit.tsx | 19 +----- .../js/pages/admin/users/students/index.tsx | 23 ++++++- routes/admin.php | 1 + 9 files changed, 152 insertions(+), 37 deletions(-) create mode 100644 app/Http/Requests/Admin/Users/StudentStatusRequest.php diff --git a/app/Http/Controllers/Admin/Users/StudentController.php b/app/Http/Controllers/Admin/Users/StudentController.php index 3de1306..c568377 100644 --- a/app/Http/Controllers/Admin/Users/StudentController.php +++ b/app/Http/Controllers/Admin/Users/StudentController.php @@ -5,6 +5,7 @@ use App\Enums\StudentStatus; use App\Http\Controllers\Controller; use App\Http\Requests\Admin\Users\StudentRequest; +use App\Http\Requests\Admin\Users\StudentStatusRequest; use App\Http\Requests\PaginatedRequest; use App\Models\User; use App\Services\Admin\Master\DepartmentService; @@ -87,4 +88,11 @@ public function resetPassword(User $user): RedirectResponse return Inertia::flash('toast', ['type' => 'success', 'message' => 'Kata sandi berhasil direset.'])->back(); } + + public function updateStatus(StudentStatusRequest $request, User $user): RedirectResponse + { + $this->service->updateStatus($user, $request->validated('status')); + + return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status mahasiswa berhasil diperbarui.'])->back(); + } } diff --git a/app/Http/Requests/Admin/Users/StudentStatusRequest.php b/app/Http/Requests/Admin/Users/StudentStatusRequest.php new file mode 100644 index 0000000..bc2bf54 --- /dev/null +++ b/app/Http/Requests/Admin/Users/StudentStatusRequest.php @@ -0,0 +1,22 @@ + ['required', 'string', Rule::in(StudentStatus::values())], + ]; + } +} diff --git a/app/Services/Admin/Users/StudentService.php b/app/Services/Admin/Users/StudentService.php index 3169d95..a384083 100644 --- a/app/Services/Admin/Users/StudentService.php +++ b/app/Services/Admin/Users/StudentService.php @@ -87,7 +87,6 @@ public function update(User $user, array $data): User 'department_id' => $data['department_id'], 'enrollment_year' => $data['enrollment_year'], 'academic_advisor_id' => $data['academic_advisor_id'], - 'status' => $data['status'] ?? null, ]); }); @@ -109,4 +108,9 @@ public function resetPassword(User $user): void 'password' => Hash::make(config('app.default_password')), ]); } + + public function updateStatus(User $user, string $status): void + { + $user->student()->update(['status' => $status]); + } } diff --git a/docs/code-style.md b/docs/code-style.md index c3ad710..20c5775 100644 --- a/docs/code-style.md +++ b/docs/code-style.md @@ -286,7 +286,35 @@ ## 6. Filter dropdown di halaman listing ✅ --- -## 7. Wayfinder ✅ +## 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: diff --git a/resources/js/components/student-status-badge.tsx b/resources/js/components/student-status-badge.tsx index 8ec37bf..8694883 100644 --- a/resources/js/components/student-status-badge.tsx +++ b/resources/js/components/student-status-badge.tsx @@ -1,14 +1,18 @@ import { Badge } from '@/components/ui/badge'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; + +type StatusOption = { value: string; label: string }; type StudentStatusBadgeProps = { status: string | null | undefined; -}; - -const StudentStatusLabels: Record = { - active: 'Aktif', - on_leave: 'Cuti', - graduated: 'Lulus', - dropped_out: 'Drop Out', + statuses: StatusOption[]; + onChange: (status: string) => void; }; const StudentStatusVariants: Record< @@ -21,14 +25,42 @@ const StudentStatusVariants: Record< dropped_out: 'destructive', }; -export function StudentStatusBadge({ status }: StudentStatusBadgeProps) { - if (!status) { - return -; - } +export function StudentStatusBadge({ + status, + statuses, + onChange, +}: StudentStatusBadgeProps) { + const label = + statuses.find((option) => option.value === status)?.label ?? + status ?? + '-'; + const variant = (status && StudentStatusVariants[status]) || 'outline'; return ( - - {StudentStatusLabels[status] ?? status} - + + + + + + + {statuses.map((option) => ( + + {option.label} + + ))} + + + ); } diff --git a/resources/js/pages/admin/users/students/columns.tsx b/resources/js/pages/admin/users/students/columns.tsx index 48ac37a..d028672 100644 --- a/resources/js/pages/admin/users/students/columns.tsx +++ b/resources/js/pages/admin/users/students/columns.tsx @@ -17,16 +17,26 @@ export type Student = { } | null; }; +type StatusOption = { value: string; label: string }; + type CreateColumnsParams = { handleEdit: (student: Student) => void; handleDeleteClick: (student: Student) => void; handleResetPassword: (student: Student) => void; + handleStatusChange: (student: Student, status: string) => void; + statuses: StatusOption[]; }; export function createStudentColumns( params: CreateColumnsParams, ): ColumnDef[] { - const { handleEdit, handleDeleteClick, handleResetPassword } = params; + const { + handleEdit, + handleDeleteClick, + handleResetPassword, + handleStatusChange, + statuses, + } = params; return [ { @@ -67,7 +77,13 @@ export function createStudentColumns( accessorKey: 'student.status', header: () => Status, cell: ({ row }) => ( - + + handleStatusChange(row.original, status) + } + /> ), }, { diff --git a/resources/js/pages/admin/users/students/edit.tsx b/resources/js/pages/admin/users/students/edit.tsx index ea8e8a3..b61b39a 100644 --- a/resources/js/pages/admin/users/students/edit.tsx +++ b/resources/js/pages/admin/users/students/edit.tsx @@ -23,7 +23,7 @@ type User = { username: string; email: string; profile: { full_name: string; phone_number: string; address: string; gender: string; birth_date: string; birth_place: string } | null; - student: { student_number: string; department_id: number; enrollment_year: number; academic_advisor_id: number; status: string | null } | null; + student: { student_number: string; department_id: number; enrollment_year: number; academic_advisor_id: number; } | null; }; type Props = { @@ -198,23 +198,6 @@ export default function StudentEdit({ user, departments, lecturers }: Props) { -
- - - -
diff --git a/resources/js/pages/admin/users/students/index.tsx b/resources/js/pages/admin/users/students/index.tsx index 142363c..f91ba64 100644 --- a/resources/js/pages/admin/users/students/index.tsx +++ b/resources/js/pages/admin/users/students/index.tsx @@ -1,5 +1,5 @@ import { Head, router } from '@inertiajs/react'; -import { Plus, RotateCcw } from 'lucide-react'; +import { Info, Plus, RotateCcw } from 'lucide-react'; import { useState } from 'react'; import { ConfirmDialog } from '@/components/confirm-dialog'; import type { PaginationState } from '@/components/data-table'; @@ -8,6 +8,7 @@ import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; import type { FilterField } from '@/components/filter-dialog'; import { FilterDialog } from '@/components/filter-dialog'; import { PageHeader } from '@/components/page-header'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; import { useServerTable } from '@/hooks/use-server-table'; import { @@ -15,6 +16,7 @@ import { destroy, edit, reset_password, + update_status, index as studentsIndex, } from '@/routes/admin/users/students'; import { createStudentColumns } from './columns'; @@ -116,12 +118,22 @@ export default function StudentIndex({ ); } + function handleStatusChange(student: Student, status: string) { + router.patch( + update_status.url(student.id), + { status }, + { preserveScroll: true }, + ); + } + const columns = createStudentColumns({ handleEdit: (student) => { router.get(edit.url(student.id)); }, handleDeleteClick: (student) => setDeleting(student), handleResetPassword: (student) => setResetting(student), + handleStatusChange, + statuses, }); return ( @@ -141,6 +153,15 @@ export default function StudentIndex({ } /> + + + Ubah status mahasiswa + + Klik badge Status pada tabel untuk mengubah status + mahasiswa secara langsung. + + + except(['show'])->parameters(['students' => 'user']); Route::patch('students/{user}/reset-password', [StudentController::class, 'resetPassword'])->name('students.reset_password'); + Route::patch('students/{user}/status', [StudentController::class, 'updateStatus'])->name('students.update_status'); Route::resource('administrators', AdministratorController::class)->except(['show'])->parameters(['administrators' => 'user']); Route::patch('administrators/{user}/reset-password', [AdministratorController::class, 'resetPassword'])->name('administrators.reset_password');