Merge pull request 'feat: implement student status update functionality and validation' (#48) from feat/implement-student-status into dev
Reviewed-on: #48
This commit is contained in:
commit
c321c99ac1
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
22
app/Http/Requests/Admin/Users/StudentStatusRequest.php
Normal file
22
app/Http/Requests/Admin/Users/StudentStatusRequest.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Users;
|
||||
|
||||
use App\Enums\StudentStatus;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StudentStatusRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'status' => ['required', 'string', Rule::in(StudentStatus::values())],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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/<namespace-controller>/`, 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 `<Form
|
||||
{...Controller.method.form()}>` tidak error saat runtime:
|
||||
|
||||
@ -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<string, string> = {
|
||||
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 <span className="text-muted-foreground">-</span>;
|
||||
}
|
||||
export function StudentStatusBadge({
|
||||
status,
|
||||
statuses,
|
||||
onChange,
|
||||
}: StudentStatusBadgeProps) {
|
||||
const label =
|
||||
statuses.find((option) => option.value === status)?.label ??
|
||||
status ??
|
||||
'-';
|
||||
const variant = (status && StudentStatusVariants[status]) || 'outline';
|
||||
|
||||
return (
|
||||
<Badge variant={StudentStatusVariants[status] ?? 'outline'}>
|
||||
{StudentStatusLabels[status] ?? status}
|
||||
</Badge>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer rounded-full border-0 bg-transparent p-0 outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<Badge variant={variant}>{label}</Badge>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuRadioGroup
|
||||
value={status ?? ''}
|
||||
onValueChange={onChange}
|
||||
>
|
||||
{statuses.map((option) => (
|
||||
<DropdownMenuRadioItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
{option.label}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<Student>[] {
|
||||
const { handleEdit, handleDeleteClick, handleResetPassword } = params;
|
||||
const {
|
||||
handleEdit,
|
||||
handleDeleteClick,
|
||||
handleResetPassword,
|
||||
handleStatusChange,
|
||||
statuses,
|
||||
} = params;
|
||||
|
||||
return [
|
||||
{
|
||||
@ -67,7 +77,13 @@ export function createStudentColumns(
|
||||
accessorKey: 'student.status',
|
||||
header: () => <span>Status</span>,
|
||||
cell: ({ row }) => (
|
||||
<StudentStatusBadge status={row.original.student?.status} />
|
||||
<StudentStatusBadge
|
||||
status={row.original.student?.status}
|
||||
statuses={statuses}
|
||||
onChange={(status) =>
|
||||
handleStatusChange(row.original, status)
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@ -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) {
|
||||
</Select>
|
||||
<InputError message={errors.academic_advisor_id} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Status <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select name="status" defaultValue={user.student?.status ?? undefined}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Pilih status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="active">Aktif</SelectItem>
|
||||
<SelectItem value="on_leave">Cuti</SelectItem>
|
||||
<SelectItem value="graduated">Lulus</SelectItem>
|
||||
<SelectItem value="dropped_out">Drop Out</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={errors.status} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@ -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({
|
||||
}
|
||||
/>
|
||||
|
||||
<Alert>
|
||||
<Info />
|
||||
<AlertTitle>Ubah status mahasiswa</AlertTitle>
|
||||
<AlertDescription>
|
||||
Klik badge Status pada tabel untuk mengubah status
|
||||
mahasiswa secara langsung.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={students.data}
|
||||
|
||||
@ -122,6 +122,7 @@
|
||||
|
||||
Route::resource('students', StudentController::class)->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');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user