From 7ce9409202824f631b15d13024b5fd86d7f1daee Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Wed, 29 Apr 2026 22:29:51 +0700 Subject: [PATCH] feat: add user status toggle functionality with database support and UI controls --- .../Admin/Master/UserController.php | 9 ++++++++ app/Models/User.php | 23 +++++++++++++++++-- database/factories/UserFactory.php | 1 + .../0001_01_01_000000_create_users_table.php | 1 + database/seeders/RolePermissionSeeder.php | 2 ++ .../admin/master/user/hooks/use-user-index.ts | 15 ++++++++++-- .../js/pages/admin/master/user/index.tsx | 13 ++++++++++- .../admin/master/user/partials/columns.tsx | 21 ++++++++++++++++- resources/js/types/auth.ts | 1 + routes/master.php | 1 + 10 files changed, 81 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/Admin/Master/UserController.php b/app/Http/Controllers/Admin/Master/UserController.php index 7562e0a..037c857 100644 --- a/app/Http/Controllers/Admin/Master/UserController.php +++ b/app/Http/Controllers/Admin/Master/UserController.php @@ -134,4 +134,13 @@ public function bulkDestroy(Request $request): RedirectResponse return redirect()->back()->with('success', 'Data terpilih berhasil dihapus'); } + + public function toggleStatus(User $user): RedirectResponse + { + $user->update([ + 'is_active' => ! $user->is_active, + ]); + + return redirect()->back()->with('success', 'Status berhasil diperbarui'); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 1c4debc..3a9fe49 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Attributes\Appends; use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Hidden; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -31,9 +32,20 @@ protected function casts(): array { return [ 'password' => 'hashed', + 'is_active' => 'boolean', ]; } + public function scopeActive(Builder $query): void + { + $query->where('is_active', true); + } + + public function scopeInactive(Builder $query): void + { + $query->where('is_active', false); + } + protected function name(): Attribute { return Attribute::make( @@ -79,7 +91,7 @@ public function salaryHistories(): HasMany public function getActivitylogOptions(): LogOptions { return LogOptions::defaults() - ->logOnly(['username']) + ->logOnly(['username', 'is_active']) ->logOnlyDirty() ->useLogName('Pegawai'); } @@ -96,6 +108,7 @@ public function tapActivity(Activity $activity, string $eventName) if (isset($activity->properties['attributes'])) { $attributeMap = [ 'username' => 'Username', + 'is_active' => 'Status', ]; $properties = $activity->properties->toArray(); @@ -104,7 +117,13 @@ public function tapActivity(Activity $activity, string $eventName) $newAttrs = []; foreach ($attrs as $key => $value) { $label = $attributeMap[$key] ?? $key; - $newAttrs[$label] = $value; + $displayValue = $value; + + if ($key === 'is_active') { + $displayValue = $value ? 'Aktif' : 'Tidak Aktif'; + } + + $newAttrs[$label] = $displayValue; } return $newAttrs; diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index c3bbf92..7ca4261 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -27,6 +27,7 @@ public function definition(): array 'username' => fake()->userName(), 'email' => fake()->unique()->safeEmail(), 'password' => static::$password ??= Hash::make('password'), + 'is_active' => fake()->boolean(80), ]; } diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 45bc592..61982d0 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -17,6 +17,7 @@ public function up(): void $table->string('email', 100)->unique(); $table->string('password', 60); $table->rememberToken(); + $table->boolean('is_active')->default(true); $table->timestamp('created_at')->useCurrent(); $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); $table->softDeletes(); diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php index 5985617..4db6842 100644 --- a/database/seeders/RolePermissionSeeder.php +++ b/database/seeders/RolePermissionSeeder.php @@ -26,6 +26,7 @@ public function run(): void Permission::firstOrCreate(['name' => 'Delete:User', 'guard_name' => 'web']); Permission::firstOrCreate(['name' => 'DeleteAny:User', 'guard_name' => 'web']); Permission::firstOrCreate(['name' => 'ResetPassword:User', 'guard_name' => 'web']); + Permission::firstOrCreate(['name' => 'ToggleStatus:User', 'guard_name' => 'web']); // Permissions Category Permission::firstOrCreate(['name' => 'View:Category', 'guard_name' => 'web']); @@ -125,6 +126,7 @@ public function run(): void 'Delete:User', 'DeleteAny:User', 'ResetPassword:User', + 'ToggleStatus:User', 'Create:Category', 'Edit:Category', diff --git a/resources/js/pages/admin/master/user/hooks/use-user-index.ts b/resources/js/pages/admin/master/user/hooks/use-user-index.ts index 510309d..46abec8 100644 --- a/resources/js/pages/admin/master/user/hooks/use-user-index.ts +++ b/resources/js/pages/admin/master/user/hooks/use-user-index.ts @@ -1,8 +1,8 @@ +import userRoutes from '@/routes/user'; +import type { User } from '@/types'; import { router } from '@inertiajs/react'; import { useState } from 'react'; import { toast } from 'sonner'; -import userRoutes from '@/routes/user'; -import type { User } from '@/types'; export function useUserIndex() { const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); @@ -62,6 +62,16 @@ export function useUserIndex() { }); }; + const onToggleStatus = (user: User) => { + router.patch(userRoutes.toggleStatus(user.id).url, {}, { + preserveState: true, + preserveScroll: true, + onSuccess: (response: any) => { + toast.success(response.props.flash.success); + }, + }); + }; + return { isDeleteDialogOpen, isBulkDeleteDialogOpen, @@ -80,5 +90,6 @@ export function useUserIndex() { onDelete, confirmDelete, confirmBulkDelete, + onToggleStatus, }; } diff --git a/resources/js/pages/admin/master/user/index.tsx b/resources/js/pages/admin/master/user/index.tsx index 6b323b1..0b74d44 100644 --- a/resources/js/pages/admin/master/user/index.tsx +++ b/resources/js/pages/admin/master/user/index.tsx @@ -42,9 +42,10 @@ export default function UserIndex({ users, defaultPassword }: { users: User[], d onDelete, confirmDelete, confirmBulkDelete, + onToggleStatus, } = useUserIndex(); - const columns = getColumns({ onResetPassword, onDelete }).filter( + const columns = getColumns({ onResetPassword, onDelete, onToggleStatus }).filter( (col) => col.id !== 'actions' || hasPermission('Edit:User') || hasPermission('Delete:User') || hasPermission('ResetPassword:User') ); @@ -74,6 +75,16 @@ export default function UserIndex({ users, defaultPassword }: { users: User[], d showSelection={hasPermission('DeleteAny:User')} rowSelection={rowSelection} onRowSelectionChange={setRowSelection} + filters={[ + { + columnId: 'is_active', + title: 'Status', + options: [ + { label: 'Aktif', value: 'true' }, + { label: 'Tidak Aktif', value: 'false' }, + ] + } + ]} bulkActions={ hasPermission('DeleteAny:User') ? [ diff --git a/resources/js/pages/admin/master/user/partials/columns.tsx b/resources/js/pages/admin/master/user/partials/columns.tsx index 77ad482..357103a 100644 --- a/resources/js/pages/admin/master/user/partials/columns.tsx +++ b/resources/js/pages/admin/master/user/partials/columns.tsx @@ -1,5 +1,6 @@ import { DataTableColumnHeader } from '@/components/data-table-column-header'; import { Button } from '@/components/ui/button'; +import { Switch } from '@/components/ui/switch'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { UserInfo } from '@/components/user-info'; import { usePermission } from '@/hooks/use-permission'; @@ -12,9 +13,10 @@ import { KeyRound, Pencil, Trash2 } from 'lucide-react'; interface ColumnProps { onResetPassword: (user: User) => void; onDelete: (user: User) => void; + onToggleStatus: (user: User) => void; } -export const getColumns = ({ onResetPassword, onDelete }: ColumnProps): ColumnDef[] => [ +export const getColumns = ({ onResetPassword, onDelete, onToggleStatus }: ColumnProps): ColumnDef[] => [ { accessorKey: "name", header: ({ column }) => ( @@ -46,6 +48,23 @@ export const getColumns = ({ onResetPassword, onDelete }: ColumnProps): ColumnDe ), meta: { title: "Nomor Telepon" }, }, + { + accessorKey: "is_active", + header: "Status", + meta: { title: "Status" }, + cell: ({ row }) => { + const user = row.original; + const { hasPermission } = usePermission(); + + return ( + onToggleStatus(user)} + disabled={!hasPermission('ToggleStatus:User')} + /> + ); + } + }, { id: "actions", header: "Aksi", diff --git a/resources/js/types/auth.ts b/resources/js/types/auth.ts index b2e24cc..0b58520 100644 --- a/resources/js/types/auth.ts +++ b/resources/js/types/auth.ts @@ -21,6 +21,7 @@ export type User = { two_factor_enabled?: boolean; created_at: string; updated_at: string; + is_active: boolean; profile?: UserProfile; [key: string]: unknown; }; diff --git a/routes/master.php b/routes/master.php index 0731626..ef6162f 100644 --- a/routes/master.php +++ b/routes/master.php @@ -31,5 +31,6 @@ Route::patch('user/reset-password/{user}', [UserController::class, 'resetPassword'])->name('user.resetPassword')->middleware('can:ResetPassword:User'); Route::delete('user/destroy/{user}', [UserController::class, 'destroy'])->name('user.destroy')->middleware('can:Delete:User'); Route::delete('user/bulk-destroy', [UserController::class, 'bulkDestroy'])->name('user.bulkDestroy')->middleware('can:DeleteAny:User'); + Route::patch('user/toggle-status/{user}', [UserController::class, 'toggleStatus'])->name('user.toggleStatus')->middleware('can:ToggleStatus:User'); }); });