feat: add user status toggle functionality with database support and UI controls

This commit is contained in:
Yoga Pangestu 2026-04-29 22:29:51 +07:00
parent f58f909a4a
commit 7ce9409202
10 changed files with 81 additions and 6 deletions

View File

@ -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');
}
}

View File

@ -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;

View File

@ -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),
];
}

View File

@ -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();

View File

@ -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',

View File

@ -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,
};
}

View File

@ -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')
? [

View File

@ -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<User>[] => [
export const getColumns = ({ onResetPassword, onDelete, onToggleStatus }: ColumnProps): ColumnDef<User>[] => [
{
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 (
<Switch
checked={user.is_active}
onCheckedChange={() => onToggleStatus(user)}
disabled={!hasPermission('ToggleStatus:User')}
/>
);
}
},
{
id: "actions",
header: "Aksi",

View File

@ -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;
};

View File

@ -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');
});
});