From 9c03876a7191f0157e8467b1cb7192f04c51b857 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sun, 30 Aug 2026 11:08:08 +0700 Subject: [PATCH] feat: implement user status management with active/inactive toggle --- app/Actions/Fortify/AuthenticateUser.php | 32 ++++++++++++++++++ .../Admin/Users/AdministratorController.php | 8 +++++ .../Admin/Users/LecturerController.php | 8 +++++ .../Admin/Users/StudentController.php | 8 +++++ .../Admin/Users/UpdateUserStatusRequest.php | 20 +++++++++++ app/Providers/FortifyServiceProvider.php | 2 ++ .../Admin/Users/AdministratorService.php | 5 +++ app/Services/Admin/Users/LecturerService.php | 5 +++ app/Services/Admin/Users/StudentService.php | 5 +++ resources/js/components/ui/switch.tsx | 33 +++++++++++++++++++ .../js/components/user-active-switch.tsx | 17 ++++++++++ .../admin/users/administrators/columns.tsx | 22 ++++++++++++- .../admin/users/administrators/index.tsx | 10 ++++++ .../pages/admin/users/lecturers/columns.tsx | 22 ++++++++++++- .../js/pages/admin/users/lecturers/index.tsx | 10 ++++++ .../js/pages/admin/users/students/columns.tsx | 16 +++++++++ .../js/pages/admin/users/students/index.tsx | 10 ++++++ routes/admin.php | 3 ++ 18 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 app/Actions/Fortify/AuthenticateUser.php create mode 100644 app/Http/Requests/Admin/Users/UpdateUserStatusRequest.php create mode 100644 resources/js/components/ui/switch.tsx create mode 100644 resources/js/components/user-active-switch.tsx diff --git a/app/Actions/Fortify/AuthenticateUser.php b/app/Actions/Fortify/AuthenticateUser.php new file mode 100644 index 0000000..a0f9836 --- /dev/null +++ b/app/Actions/Fortify/AuthenticateUser.php @@ -0,0 +1,32 @@ +input(Fortify::username()))->first(); + + if (! $user || ! Hash::check($request->password, $user->password)) { + return null; + } + + if (! $user->is_active) { + throw ValidationException::withMessages([ + Fortify::username() => 'Akun Anda tidak aktif. Silakan hubungi administrator.', + ]); + } + + return $user; + } +} diff --git a/app/Http/Controllers/Admin/Users/AdministratorController.php b/app/Http/Controllers/Admin/Users/AdministratorController.php index 7c86c75..19e614a 100644 --- a/app/Http/Controllers/Admin/Users/AdministratorController.php +++ b/app/Http/Controllers/Admin/Users/AdministratorController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Admin\Users\AdministratorRequest; +use App\Http\Requests\Admin\Users\UpdateUserStatusRequest; use App\Http\Requests\PaginatedRequest; use App\Models\User; use App\Services\Admin\Users\AdministratorService; @@ -77,4 +78,11 @@ public function resetPassword(User $user): RedirectResponse return Inertia::flash('toast', ['type' => 'success', 'message' => 'Kata sandi berhasil direset.'])->back(); } + + public function updateUserStatus(UpdateUserStatusRequest $request, User $user): RedirectResponse + { + $this->service->updateUserStatus($user, $request->boolean('is_active')); + + return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status akun berhasil diperbarui.'])->back(); + } } diff --git a/app/Http/Controllers/Admin/Users/LecturerController.php b/app/Http/Controllers/Admin/Users/LecturerController.php index 1d000f9..f7f19ac 100644 --- a/app/Http/Controllers/Admin/Users/LecturerController.php +++ b/app/Http/Controllers/Admin/Users/LecturerController.php @@ -5,6 +5,7 @@ use App\Exports\LecturersExport; use App\Http\Controllers\Controller; use App\Http\Requests\Admin\Users\LecturerRequest; +use App\Http\Requests\Admin\Users\UpdateUserStatusRequest; use App\Http\Requests\PaginatedRequest; use App\Models\User; use App\Services\Admin\Master\DepartmentService; @@ -84,6 +85,13 @@ public function resetPassword(User $user): RedirectResponse return Inertia::flash('toast', ['type' => 'success', 'message' => 'Kata sandi berhasil direset.'])->back(); } + public function updateUserStatus(UpdateUserStatusRequest $request, User $user): RedirectResponse + { + $this->service->updateUserStatus($user, $request->boolean('is_active')); + + return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status akun berhasil diperbarui.'])->back(); + } + public function export(PaginatedRequest $request): BinaryFileResponse { return Excel::download(new LecturersExport( diff --git a/app/Http/Controllers/Admin/Users/StudentController.php b/app/Http/Controllers/Admin/Users/StudentController.php index e6ccf61..9f716c4 100644 --- a/app/Http/Controllers/Admin/Users/StudentController.php +++ b/app/Http/Controllers/Admin/Users/StudentController.php @@ -7,6 +7,7 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Admin\Users\Student\StudentRequest; use App\Http\Requests\Admin\Users\Student\StudentStatusRequest; +use App\Http\Requests\Admin\Users\UpdateUserStatusRequest; use App\Http\Requests\PaginatedRequest; use App\Models\User; use App\Services\Admin\Master\DepartmentService; @@ -99,6 +100,13 @@ public function updateStatus(StudentStatusRequest $request, User $user): Redirec return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status mahasiswa berhasil diperbarui.'])->back(); } + public function updateUserStatus(UpdateUserStatusRequest $request, User $user): RedirectResponse + { + $this->service->updateUserStatus($user, $request->boolean('is_active')); + + return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status akun berhasil diperbarui.'])->back(); + } + public function export(PaginatedRequest $request): BinaryFileResponse { return Excel::download(new StudentsExport( diff --git a/app/Http/Requests/Admin/Users/UpdateUserStatusRequest.php b/app/Http/Requests/Admin/Users/UpdateUserStatusRequest.php new file mode 100644 index 0000000..4cb9cbf --- /dev/null +++ b/app/Http/Requests/Admin/Users/UpdateUserStatusRequest.php @@ -0,0 +1,20 @@ + ['required', 'boolean'], + ]; + } +} diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index 2bf0787..d4af79b 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use App\Actions\Fortify\AuthenticateUser; use App\Actions\Fortify\ResetUserPassword; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Http\Request; @@ -30,6 +31,7 @@ public function boot(): void private function configureActions(): void { Fortify::resetUserPasswordsUsing(ResetUserPassword::class); + Fortify::authenticateUsing(new AuthenticateUser); } private function configureViews(): void diff --git a/app/Services/Admin/Users/AdministratorService.php b/app/Services/Admin/Users/AdministratorService.php index f8f31c6..aa4b525 100644 --- a/app/Services/Admin/Users/AdministratorService.php +++ b/app/Services/Admin/Users/AdministratorService.php @@ -84,4 +84,9 @@ public function resetPassword(User $user): void 'password' => Hash::make(config('app.default_password')), ]); } + + public function updateUserStatus(User $user, bool $isActive): void + { + $user->update(['is_active' => $isActive]); + } } diff --git a/app/Services/Admin/Users/LecturerService.php b/app/Services/Admin/Users/LecturerService.php index 7bc4f57..0ae4d20 100644 --- a/app/Services/Admin/Users/LecturerService.php +++ b/app/Services/Admin/Users/LecturerService.php @@ -115,4 +115,9 @@ public function resetPassword(User $user): void 'password' => Hash::make(config('app.default_password')), ]); } + + public function updateUserStatus(User $user, bool $isActive): void + { + $user->update(['is_active' => $isActive]); + } } diff --git a/app/Services/Admin/Users/StudentService.php b/app/Services/Admin/Users/StudentService.php index 45cd2f0..3c3f543 100644 --- a/app/Services/Admin/Users/StudentService.php +++ b/app/Services/Admin/Users/StudentService.php @@ -126,4 +126,9 @@ public function updateStatus(User $user, string $status): void { $user->student()->update(['status' => $status]); } + + public function updateUserStatus(User $user, bool $isActive): void + { + $user->update(['is_active' => $isActive]); + } } diff --git a/resources/js/components/ui/switch.tsx b/resources/js/components/ui/switch.tsx new file mode 100644 index 0000000..dd2a617 --- /dev/null +++ b/resources/js/components/ui/switch.tsx @@ -0,0 +1,33 @@ +import * as React from "react" +import { Switch as SwitchPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Switch({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + + + ) +} + +export { Switch } diff --git a/resources/js/components/user-active-switch.tsx b/resources/js/components/user-active-switch.tsx new file mode 100644 index 0000000..696c8e7 --- /dev/null +++ b/resources/js/components/user-active-switch.tsx @@ -0,0 +1,17 @@ +import { Switch } from '@/components/ui/switch'; + +type UserActiveSwitchProps = { + isActive: boolean; + onChange: (isActive: boolean) => void; +}; + +export function UserActiveSwitch({ isActive, onChange }: UserActiveSwitchProps) { + return ( +
+ + + {isActive ? 'Aktif' : 'Nonaktif'} + +
+ ); +} diff --git a/resources/js/pages/admin/users/administrators/columns.tsx b/resources/js/pages/admin/users/administrators/columns.tsx index 0ea11b2..173a967 100644 --- a/resources/js/pages/admin/users/administrators/columns.tsx +++ b/resources/js/pages/admin/users/administrators/columns.tsx @@ -2,11 +2,13 @@ import type { ColumnDef } from '@tanstack/react-table'; import { Key, Pencil, Trash2 } from 'lucide-react'; import { GenderBadge } from '@/components/gender-badge'; import { RowActions } from '@/components/row-actions'; +import { UserActiveSwitch } from '@/components/user-active-switch'; export type Administrator = { id: number; username: string; email: string; + is_active: boolean; profile: { full_name: string; phone_number: string; gender: string } | null; roles: { id: number; name: string }[]; }; @@ -15,12 +17,18 @@ type CreateColumnsParams = { handleEdit: (admin: Administrator) => void; handleDeleteClick: (admin: Administrator) => void; handleResetPassword: (admin: Administrator) => void; + handleUserStatusChange: (admin: Administrator, isActive: boolean) => void; }; export function createAdministratorColumns( params: CreateColumnsParams, ): ColumnDef[] { - const { handleEdit, handleDeleteClick, handleResetPassword } = params; + const { + handleEdit, + handleDeleteClick, + handleResetPassword, + handleUserStatusChange, + } = params; return [ { @@ -57,6 +65,18 @@ export function createAdministratorColumns( ), }, + { + accessorKey: 'is_active', + header: () => Status Akun, + cell: ({ row }) => ( + + handleUserStatusChange(row.original, isActive) + } + /> + ), + }, { id: 'actions', header: () => Aksi, diff --git a/resources/js/pages/admin/users/administrators/index.tsx b/resources/js/pages/admin/users/administrators/index.tsx index 4c344e5..26e8855 100644 --- a/resources/js/pages/admin/users/administrators/index.tsx +++ b/resources/js/pages/admin/users/administrators/index.tsx @@ -16,6 +16,7 @@ import { destroy, edit, reset_password, + update_user_status, } from '@/routes/admin/users/administrators'; import type { Administrator } from './columns'; import { createAdministratorColumns } from './columns'; @@ -91,12 +92,21 @@ export default function AdministratorIndex({ administrators, filters }: Props) { ); } + function handleUserStatusChange(admin: Administrator, isActive: boolean) { + router.patch( + update_user_status.url(admin.id), + { is_active: isActive }, + { preserveScroll: true }, + ); + } + const columns = createAdministratorColumns({ handleEdit: (admin) => { router.get(edit.url(admin.id)); }, handleDeleteClick: (admin) => setDeleting(admin), handleResetPassword: (admin) => setResetting(admin), + handleUserStatusChange, }); return ( diff --git a/resources/js/pages/admin/users/lecturers/columns.tsx b/resources/js/pages/admin/users/lecturers/columns.tsx index 6edc468..2dd0623 100644 --- a/resources/js/pages/admin/users/lecturers/columns.tsx +++ b/resources/js/pages/admin/users/lecturers/columns.tsx @@ -2,11 +2,13 @@ import type { ColumnDef } from '@tanstack/react-table'; import { Key, Pencil, Trash2 } from 'lucide-react'; import { GenderBadge } from '@/components/gender-badge'; import { RowActions } from '@/components/row-actions'; +import { UserActiveSwitch } from '@/components/user-active-switch'; export type Lecturer = { id: number; username: string; email: string; + is_active: boolean; profile: { full_name: string; phone_number: string; gender: string } | null; lecturer: { lecturer_number: string; @@ -18,12 +20,18 @@ type CreateColumnsParams = { handleEdit: (lecturer: Lecturer) => void; handleDeleteClick: (lecturer: Lecturer) => void; handleResetPassword: (lecturer: Lecturer) => void; + handleUserStatusChange: (lecturer: Lecturer, isActive: boolean) => void; }; export function createLecturerColumns( params: CreateColumnsParams, ): ColumnDef[] { - const { handleEdit, handleDeleteClick, handleResetPassword } = params; + const { + handleEdit, + handleDeleteClick, + handleResetPassword, + handleUserStatusChange, + } = params; return [ { @@ -67,6 +75,18 @@ export function createLecturerColumns( ), }, + { + accessorKey: 'is_active', + header: () => Status Akun, + cell: ({ row }) => ( + + handleUserStatusChange(row.original, isActive) + } + /> + ), + }, { id: 'actions', header: () => Aksi, diff --git a/resources/js/pages/admin/users/lecturers/index.tsx b/resources/js/pages/admin/users/lecturers/index.tsx index c20455d..ca7ba33 100644 --- a/resources/js/pages/admin/users/lecturers/index.tsx +++ b/resources/js/pages/admin/users/lecturers/index.tsx @@ -16,6 +16,7 @@ import { edit, destroy, reset_password, + update_user_status, exportMethod as exportLecturers, } from '@/routes/admin/users/lecturers'; import type { Lecturer } from './columns'; @@ -108,12 +109,21 @@ export default function LecturerIndex({ ); } + function handleUserStatusChange(lecturer: Lecturer, isActive: boolean) { + router.patch( + update_user_status.url(lecturer.id), + { is_active: isActive }, + { preserveScroll: true }, + ); + } + const columns = createLecturerColumns({ handleEdit: (lecturer) => { router.get(edit.url(lecturer.id)); }, handleDeleteClick: (lecturer) => setDeleting(lecturer), handleResetPassword: (lecturer) => setResetting(lecturer), + handleUserStatusChange, }); return ( diff --git a/resources/js/pages/admin/users/students/columns.tsx b/resources/js/pages/admin/users/students/columns.tsx index d028672..4c2aab0 100644 --- a/resources/js/pages/admin/users/students/columns.tsx +++ b/resources/js/pages/admin/users/students/columns.tsx @@ -3,11 +3,13 @@ import { Key, Pencil, Trash2 } from 'lucide-react'; import { GenderBadge } from '@/components/gender-badge'; import { RowActions } from '@/components/row-actions'; import { StudentStatusBadge } from '@/components/student-status-badge'; +import { UserActiveSwitch } from '@/components/user-active-switch'; export type Student = { id: number; username: string; email: string; + is_active: boolean; profile: { full_name: string; phone_number: string; gender: string } | null; student: { student_number: string; @@ -24,6 +26,7 @@ type CreateColumnsParams = { handleDeleteClick: (student: Student) => void; handleResetPassword: (student: Student) => void; handleStatusChange: (student: Student, status: string) => void; + handleUserStatusChange: (student: Student, isActive: boolean) => void; statuses: StatusOption[]; }; @@ -35,6 +38,7 @@ export function createStudentColumns( handleDeleteClick, handleResetPassword, handleStatusChange, + handleUserStatusChange, statuses, } = params; @@ -98,6 +102,18 @@ export function createStudentColumns( ), }, + { + accessorKey: 'is_active', + header: () => Status Akun, + cell: ({ row }) => ( + + handleUserStatusChange(row.original, isActive) + } + /> + ), + }, { id: 'actions', header: () => Aksi, diff --git a/resources/js/pages/admin/users/students/index.tsx b/resources/js/pages/admin/users/students/index.tsx index ff799db..ccedebb 100644 --- a/resources/js/pages/admin/users/students/index.tsx +++ b/resources/js/pages/admin/users/students/index.tsx @@ -17,6 +17,7 @@ import { edit, reset_password, update_status, + update_user_status, exportMethod as exportStudents, index as studentsIndex, } from '@/routes/admin/users/students'; @@ -127,6 +128,14 @@ export default function StudentIndex({ ); } + function handleUserStatusChange(student: Student, isActive: boolean) { + router.patch( + update_user_status.url(student.id), + { is_active: isActive }, + { preserveScroll: true }, + ); + } + const columns = createStudentColumns({ handleEdit: (student) => { router.get(edit.url(student.id)); @@ -134,6 +143,7 @@ export default function StudentIndex({ handleDeleteClick: (student) => setDeleting(student), handleResetPassword: (student) => setResetting(student), handleStatusChange, + handleUserStatusChange, statuses, }); diff --git a/routes/admin.php b/routes/admin.php index d739cf7..b529517 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -119,14 +119,17 @@ Route::prefix('admin/users')->name('admin.users.')->group(function () { Route::resource('lecturers', LecturerController::class)->except(['show'])->parameters(['lecturers' => 'user']); Route::patch('lecturers/{user}/reset-password', [LecturerController::class, 'resetPassword'])->name('lecturers.reset_password'); + Route::patch('lecturers/{user}/user-status', [LecturerController::class, 'updateUserStatus'])->name('lecturers.update_user_status'); Route::get('lecturers/export', [LecturerController::class, 'export'])->name('lecturers.export'); 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::patch('students/{user}/user-status', [StudentController::class, 'updateUserStatus'])->name('students.update_user_status'); Route::get('students/export', [StudentController::class, 'export'])->name('students.export'); 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}/user-status', [AdministratorController::class, 'updateUserStatus'])->name('administrators.update_user_status'); }); });