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'); 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\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
@ -31,9 +32,20 @@ protected function casts(): array
{ {
return [ return [
'password' => 'hashed', '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 protected function name(): Attribute
{ {
return Attribute::make( return Attribute::make(
@ -79,7 +91,7 @@ public function salaryHistories(): HasMany
public function getActivitylogOptions(): LogOptions public function getActivitylogOptions(): LogOptions
{ {
return LogOptions::defaults() return LogOptions::defaults()
->logOnly(['username']) ->logOnly(['username', 'is_active'])
->logOnlyDirty() ->logOnlyDirty()
->useLogName('Pegawai'); ->useLogName('Pegawai');
} }
@ -96,6 +108,7 @@ public function tapActivity(Activity $activity, string $eventName)
if (isset($activity->properties['attributes'])) { if (isset($activity->properties['attributes'])) {
$attributeMap = [ $attributeMap = [
'username' => 'Username', 'username' => 'Username',
'is_active' => 'Status',
]; ];
$properties = $activity->properties->toArray(); $properties = $activity->properties->toArray();
@ -104,7 +117,13 @@ public function tapActivity(Activity $activity, string $eventName)
$newAttrs = []; $newAttrs = [];
foreach ($attrs as $key => $value) { foreach ($attrs as $key => $value) {
$label = $attributeMap[$key] ?? $key; $label = $attributeMap[$key] ?? $key;
$newAttrs[$label] = $value; $displayValue = $value;
if ($key === 'is_active') {
$displayValue = $value ? 'Aktif' : 'Tidak Aktif';
}
$newAttrs[$label] = $displayValue;
} }
return $newAttrs; return $newAttrs;

View File

@ -27,6 +27,7 @@ public function definition(): array
'username' => fake()->userName(), 'username' => fake()->userName(),
'email' => fake()->unique()->safeEmail(), 'email' => fake()->unique()->safeEmail(),
'password' => static::$password ??= Hash::make('password'), '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('email', 100)->unique();
$table->string('password', 60); $table->string('password', 60);
$table->rememberToken(); $table->rememberToken();
$table->boolean('is_active')->default(true);
$table->timestamp('created_at')->useCurrent(); $table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
$table->softDeletes(); $table->softDeletes();

View File

@ -26,6 +26,7 @@ public function run(): void
Permission::firstOrCreate(['name' => 'Delete:User', 'guard_name' => 'web']); Permission::firstOrCreate(['name' => 'Delete:User', 'guard_name' => 'web']);
Permission::firstOrCreate(['name' => 'DeleteAny:User', 'guard_name' => 'web']); Permission::firstOrCreate(['name' => 'DeleteAny:User', 'guard_name' => 'web']);
Permission::firstOrCreate(['name' => 'ResetPassword:User', 'guard_name' => 'web']); Permission::firstOrCreate(['name' => 'ResetPassword:User', 'guard_name' => 'web']);
Permission::firstOrCreate(['name' => 'ToggleStatus:User', 'guard_name' => 'web']);
// Permissions Category // Permissions Category
Permission::firstOrCreate(['name' => 'View:Category', 'guard_name' => 'web']); Permission::firstOrCreate(['name' => 'View:Category', 'guard_name' => 'web']);
@ -125,6 +126,7 @@ public function run(): void
'Delete:User', 'Delete:User',
'DeleteAny:User', 'DeleteAny:User',
'ResetPassword:User', 'ResetPassword:User',
'ToggleStatus:User',
'Create:Category', 'Create:Category',
'Edit: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 { router } from '@inertiajs/react';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import userRoutes from '@/routes/user';
import type { User } from '@/types';
export function useUserIndex() { export function useUserIndex() {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); 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 { return {
isDeleteDialogOpen, isDeleteDialogOpen,
isBulkDeleteDialogOpen, isBulkDeleteDialogOpen,
@ -80,5 +90,6 @@ export function useUserIndex() {
onDelete, onDelete,
confirmDelete, confirmDelete,
confirmBulkDelete, confirmBulkDelete,
onToggleStatus,
}; };
} }

View File

@ -42,9 +42,10 @@ export default function UserIndex({ users, defaultPassword }: { users: User[], d
onDelete, onDelete,
confirmDelete, confirmDelete,
confirmBulkDelete, confirmBulkDelete,
onToggleStatus,
} = useUserIndex(); } = 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') (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')} showSelection={hasPermission('DeleteAny:User')}
rowSelection={rowSelection} rowSelection={rowSelection}
onRowSelectionChange={setRowSelection} onRowSelectionChange={setRowSelection}
filters={[
{
columnId: 'is_active',
title: 'Status',
options: [
{ label: 'Aktif', value: 'true' },
{ label: 'Tidak Aktif', value: 'false' },
]
}
]}
bulkActions={ bulkActions={
hasPermission('DeleteAny:User') hasPermission('DeleteAny:User')
? [ ? [

View File

@ -1,5 +1,6 @@
import { DataTableColumnHeader } from '@/components/data-table-column-header'; import { DataTableColumnHeader } from '@/components/data-table-column-header';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { UserInfo } from '@/components/user-info'; import { UserInfo } from '@/components/user-info';
import { usePermission } from '@/hooks/use-permission'; import { usePermission } from '@/hooks/use-permission';
@ -12,9 +13,10 @@ import { KeyRound, Pencil, Trash2 } from 'lucide-react';
interface ColumnProps { interface ColumnProps {
onResetPassword: (user: User) => void; onResetPassword: (user: User) => void;
onDelete: (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", accessorKey: "name",
header: ({ column }) => ( header: ({ column }) => (
@ -46,6 +48,23 @@ export const getColumns = ({ onResetPassword, onDelete }: ColumnProps): ColumnDe
), ),
meta: { title: "Nomor Telepon" }, 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", id: "actions",
header: "Aksi", header: "Aksi",

View File

@ -21,6 +21,7 @@ export type User = {
two_factor_enabled?: boolean; two_factor_enabled?: boolean;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
is_active: boolean;
profile?: UserProfile; profile?: UserProfile;
[key: string]: unknown; [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::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/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::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');
}); });
}); });