feat: implement student status update functionality and validation
Some checks failed
tests / ci (pull_request) Has been cancelled
Some checks failed
tests / ci (pull_request) Has been cancelled
This commit is contained in:
parent
3418b3c44e
commit
d59e19ed3e
@ -5,6 +5,7 @@
|
|||||||
use App\Enums\StudentStatus;
|
use App\Enums\StudentStatus;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\Users\StudentRequest;
|
use App\Http\Requests\Admin\Users\StudentRequest;
|
||||||
|
use App\Http\Requests\Admin\Users\StudentStatusRequest;
|
||||||
use App\Http\Requests\PaginatedRequest;
|
use App\Http\Requests\PaginatedRequest;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Admin\Master\DepartmentService;
|
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();
|
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'],
|
'department_id' => $data['department_id'],
|
||||||
'enrollment_year' => $data['enrollment_year'],
|
'enrollment_year' => $data['enrollment_year'],
|
||||||
'academic_advisor_id' => $data['academic_advisor_id'],
|
'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')),
|
'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
|
Selalu jalankan generate dengan flag form variant, supaya `<Form
|
||||||
{...Controller.method.form()}>` tidak error saat runtime:
|
{...Controller.method.form()}>` tidak error saat runtime:
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
import { Badge } from '@/components/ui/badge';
|
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 = {
|
type StudentStatusBadgeProps = {
|
||||||
status: string | null | undefined;
|
status: string | null | undefined;
|
||||||
};
|
statuses: StatusOption[];
|
||||||
|
onChange: (status: string) => void;
|
||||||
const StudentStatusLabels: Record<string, string> = {
|
|
||||||
active: 'Aktif',
|
|
||||||
on_leave: 'Cuti',
|
|
||||||
graduated: 'Lulus',
|
|
||||||
dropped_out: 'Drop Out',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const StudentStatusVariants: Record<
|
const StudentStatusVariants: Record<
|
||||||
@ -21,14 +25,42 @@ const StudentStatusVariants: Record<
|
|||||||
dropped_out: 'destructive',
|
dropped_out: 'destructive',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function StudentStatusBadge({ status }: StudentStatusBadgeProps) {
|
export function StudentStatusBadge({
|
||||||
if (!status) {
|
status,
|
||||||
return <span className="text-muted-foreground">-</span>;
|
statuses,
|
||||||
}
|
onChange,
|
||||||
|
}: StudentStatusBadgeProps) {
|
||||||
|
const label =
|
||||||
|
statuses.find((option) => option.value === status)?.label ??
|
||||||
|
status ??
|
||||||
|
'-';
|
||||||
|
const variant = (status && StudentStatusVariants[status]) || 'outline';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge variant={StudentStatusVariants[status] ?? 'outline'}>
|
<DropdownMenu>
|
||||||
{StudentStatusLabels[status] ?? status}
|
<DropdownMenuTrigger asChild>
|
||||||
</Badge>
|
<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;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type StatusOption = { value: string; label: string };
|
||||||
|
|
||||||
type CreateColumnsParams = {
|
type CreateColumnsParams = {
|
||||||
handleEdit: (student: Student) => void;
|
handleEdit: (student: Student) => void;
|
||||||
handleDeleteClick: (student: Student) => void;
|
handleDeleteClick: (student: Student) => void;
|
||||||
handleResetPassword: (student: Student) => void;
|
handleResetPassword: (student: Student) => void;
|
||||||
|
handleStatusChange: (student: Student, status: string) => void;
|
||||||
|
statuses: StatusOption[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createStudentColumns(
|
export function createStudentColumns(
|
||||||
params: CreateColumnsParams,
|
params: CreateColumnsParams,
|
||||||
): ColumnDef<Student>[] {
|
): ColumnDef<Student>[] {
|
||||||
const { handleEdit, handleDeleteClick, handleResetPassword } = params;
|
const {
|
||||||
|
handleEdit,
|
||||||
|
handleDeleteClick,
|
||||||
|
handleResetPassword,
|
||||||
|
handleStatusChange,
|
||||||
|
statuses,
|
||||||
|
} = params;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -67,7 +77,13 @@ export function createStudentColumns(
|
|||||||
accessorKey: 'student.status',
|
accessorKey: 'student.status',
|
||||||
header: () => <span>Status</span>,
|
header: () => <span>Status</span>,
|
||||||
cell: ({ row }) => (
|
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;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
profile: { full_name: string; phone_number: string; address: string; gender: string; birth_date: string; birth_place: string } | null;
|
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 = {
|
type Props = {
|
||||||
@ -198,23 +198,6 @@ export default function StudentEdit({ user, departments, lecturers }: Props) {
|
|||||||
</Select>
|
</Select>
|
||||||
<InputError message={errors.academic_advisor_id} />
|
<InputError message={errors.academic_advisor_id} />
|
||||||
</div>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Head, router } from '@inertiajs/react';
|
import { Head, router } from '@inertiajs/react';
|
||||||
import { Plus, RotateCcw } from 'lucide-react';
|
import { Info, Plus, RotateCcw } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||||
import type { PaginationState } from '@/components/data-table';
|
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 type { FilterField } from '@/components/filter-dialog';
|
||||||
import { FilterDialog } from '@/components/filter-dialog';
|
import { FilterDialog } from '@/components/filter-dialog';
|
||||||
import { PageHeader } from '@/components/page-header';
|
import { PageHeader } from '@/components/page-header';
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useServerTable } from '@/hooks/use-server-table';
|
import { useServerTable } from '@/hooks/use-server-table';
|
||||||
import {
|
import {
|
||||||
@ -15,6 +16,7 @@ import {
|
|||||||
destroy,
|
destroy,
|
||||||
edit,
|
edit,
|
||||||
reset_password,
|
reset_password,
|
||||||
|
update_status,
|
||||||
index as studentsIndex,
|
index as studentsIndex,
|
||||||
} from '@/routes/admin/users/students';
|
} from '@/routes/admin/users/students';
|
||||||
import { createStudentColumns } from './columns';
|
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({
|
const columns = createStudentColumns({
|
||||||
handleEdit: (student) => {
|
handleEdit: (student) => {
|
||||||
router.get(edit.url(student.id));
|
router.get(edit.url(student.id));
|
||||||
},
|
},
|
||||||
handleDeleteClick: (student) => setDeleting(student),
|
handleDeleteClick: (student) => setDeleting(student),
|
||||||
handleResetPassword: (student) => setResetting(student),
|
handleResetPassword: (student) => setResetting(student),
|
||||||
|
handleStatusChange,
|
||||||
|
statuses,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
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
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={students.data}
|
data={students.data}
|
||||||
|
|||||||
@ -122,6 +122,7 @@
|
|||||||
|
|
||||||
Route::resource('students', StudentController::class)->except(['show'])->parameters(['students' => 'user']);
|
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}/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::resource('administrators', AdministratorController::class)->except(['show'])->parameters(['administrators' => 'user']);
|
||||||
Route::patch('administrators/{user}/reset-password', [AdministratorController::class, 'resetPassword'])->name('administrators.reset_password');
|
Route::patch('administrators/{user}/reset-password', [AdministratorController::class, 'resetPassword'])->name('administrators.reset_password');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user