575 lines
20 KiB
PHP
575 lines
20 KiB
PHP
<?php
|
|
|
|
/**
|
|
* ============================================================
|
|
*
|
|
* @project biiproject
|
|
*
|
|
* @author Andika Debi Putra
|
|
*
|
|
* @email andikadebiputra@gmail.com
|
|
*
|
|
* @website https://biiproject.com
|
|
*
|
|
* @copyright Copyright (c) 2026 Andika Debi Putra
|
|
* @license Proprietary - All Rights Reserved
|
|
*
|
|
* @version 1.0.0
|
|
*
|
|
* @created 2026-05-01
|
|
* ============================================================
|
|
*
|
|
* Unauthorized copying, modification, distribution, or use
|
|
* of this file is strictly prohibited without prior written
|
|
* permission from the author.
|
|
* ============================================================
|
|
*/
|
|
|
|
namespace App\Http\Controllers\AccessControl;
|
|
|
|
use App\Models\Role;
|
|
use App\Models\User;
|
|
use App\Support\DataTable;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Routing\Controller;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Validation\Rule;
|
|
|
|
class UserManagementController extends Controller
|
|
{
|
|
public function __construct()
|
|
{
|
|
// Middleware handled in web.php
|
|
}
|
|
|
|
/**
|
|
* INDEX — Display user list & roles (for dropdowns)
|
|
*/
|
|
public function index(Request $request)
|
|
{
|
|
if (DataTable::isDataTableRequest($request)) {
|
|
return $this->dataTable($request);
|
|
}
|
|
|
|
$roles = Role::where('is_active', 1)
|
|
->whereNull('deleted_at')
|
|
->orderBy('name')
|
|
->get();
|
|
|
|
return view('pages.access_control.users', compact('roles'));
|
|
}
|
|
|
|
protected function dataTable(Request $request)
|
|
{
|
|
try {
|
|
$authUser = $request->user();
|
|
$canManage = $authUser->can('manage user directory');
|
|
|
|
$query = User::query()
|
|
->withTrashed()
|
|
->with(['roles:id,name', 'creator:id,email', 'updater:id,email']);
|
|
|
|
if (! $authUser->hasRole('Developer')) {
|
|
$query->whereDoesntHave('roles', function ($roleQuery) {
|
|
$roleQuery->where('name', 'Developer');
|
|
});
|
|
}
|
|
|
|
// Fast count total
|
|
$recordsTotal = User::withTrashed();
|
|
if (! $authUser->hasRole('Developer')) {
|
|
$recordsTotal->whereDoesntHave('roles', function ($roleQuery) {
|
|
$roleQuery->where('name', 'Developer');
|
|
});
|
|
}
|
|
$recordsTotal = $recordsTotal->count();
|
|
$globalSearch = DataTable::globalSearch($request);
|
|
|
|
if ($canManage) {
|
|
$status = DataTable::columnSearch($request, 1);
|
|
$name = DataTable::columnSearch($request, 2);
|
|
$email = DataTable::columnSearch($request, 3);
|
|
$role = DataTable::columnSearch($request, 4);
|
|
$createdAt = DataTable::columnSearch($request, 5);
|
|
$createdBy = DataTable::columnSearch($request, 6);
|
|
$updatedAt = DataTable::columnSearch($request, 7);
|
|
$updatedBy = DataTable::columnSearch($request, 8);
|
|
} else {
|
|
$status = null;
|
|
$name = DataTable::columnSearch($request, 0);
|
|
$email = DataTable::columnSearch($request, 1);
|
|
$role = DataTable::columnSearch($request, 2);
|
|
$createdAt = DataTable::columnSearch($request, 3);
|
|
$createdBy = DataTable::columnSearch($request, 4);
|
|
$updatedAt = DataTable::columnSearch($request, 5);
|
|
$updatedBy = DataTable::columnSearch($request, 6);
|
|
}
|
|
|
|
$trashed = $request->input('trashed');
|
|
if ($trashed === 'archived') {
|
|
$query->onlyTrashed();
|
|
} elseif ($trashed === 'active') {
|
|
$query->withoutTrashed();
|
|
}
|
|
|
|
if ($status) {
|
|
$query->where('is_active', $status === 'active');
|
|
}
|
|
|
|
if ($name) {
|
|
$query->where('name', 'like', "%{$name}%");
|
|
}
|
|
|
|
if ($email) {
|
|
$query->where('email', 'like', "%{$email}%");
|
|
}
|
|
|
|
if ($role) {
|
|
$query->whereHas('roles', function ($roleQuery) use ($role) {
|
|
$roleQuery->where('name', 'like', "%{$role}%");
|
|
});
|
|
}
|
|
|
|
if ($createdAt) {
|
|
$query->whereDate('created_at', $createdAt);
|
|
}
|
|
|
|
if ($createdBy) {
|
|
$query->whereHas('creator', function ($creatorQuery) use ($createdBy) {
|
|
$creatorQuery->where('email', 'like', "%{$createdBy}%");
|
|
});
|
|
}
|
|
|
|
if ($updatedAt) {
|
|
$query->whereDate('updated_at', $updatedAt);
|
|
}
|
|
|
|
if ($updatedBy) {
|
|
$query->whereHas('updater', function ($updaterQuery) use ($updatedBy) {
|
|
$updaterQuery->where('email', 'like', "%{$updatedBy}%");
|
|
});
|
|
}
|
|
|
|
if ($globalSearch) {
|
|
$query->where(function ($searchQuery) use ($globalSearch) {
|
|
$searchQuery
|
|
->where('name', 'like', "%{$globalSearch}%")
|
|
->orWhere('email', 'like', "%{$globalSearch}%")
|
|
->orWhereHas('roles', function ($roleQuery) use ($globalSearch) {
|
|
$roleQuery->where('name', 'like', "%{$globalSearch}%");
|
|
})
|
|
->orWhereHas('creator', function ($creatorQuery) use ($globalSearch) {
|
|
$creatorQuery->where('email', 'like', "%{$globalSearch}%");
|
|
})
|
|
->orWhereHas('updater', function ($updaterQuery) use ($globalSearch) {
|
|
$updaterQuery->where('email', 'like', "%{$globalSearch}%");
|
|
});
|
|
});
|
|
}
|
|
|
|
// Perform filtered count WITHOUT eager loading or ordering
|
|
$countQuery = clone $query;
|
|
$countQuery->setEagerLoads([]);
|
|
$countQuery->orders = null;
|
|
$recordsFiltered = $countQuery->count();
|
|
[$orderIndex, $orderDirection] = DataTable::order($request, $canManage ? 5 : 3, 'desc');
|
|
|
|
$sortColumn = match (true) {
|
|
$canManage && $orderIndex === 1 => 'is_active',
|
|
($canManage && $orderIndex === 2) || (! $canManage && $orderIndex === 0) => 'name',
|
|
($canManage && $orderIndex === 3) || (! $canManage && $orderIndex === 1) => 'email',
|
|
($canManage && $orderIndex === 5) || (! $canManage && $orderIndex === 3) => 'created_at',
|
|
($canManage && $orderIndex === 7) || (! $canManage && $orderIndex === 5) => 'updated_at',
|
|
default => 'created_at',
|
|
};
|
|
|
|
$users = $query
|
|
->orderBy($sortColumn, $orderDirection)
|
|
->skip(DataTable::start($request))
|
|
->take(DataTable::length($request))
|
|
->get();
|
|
|
|
$rows = $users->map(function (User $user) use ($authUser, $canManage) {
|
|
$roles = $user->roles->pluck('name')->values();
|
|
$roleBadges = $roles->isNotEmpty()
|
|
? $roles->map(fn ($role) => '<span class="badge badge-slate-pill">'.e($role).'</span>')->implode(' ')
|
|
: '-';
|
|
|
|
$row = [];
|
|
|
|
if ($canManage) {
|
|
$row[] = sprintf(
|
|
'<input type="checkbox" class="form-check-input user-checkbox" value="%d">',
|
|
$user->id
|
|
);
|
|
$row[] = sprintf(
|
|
'<div class="form-check form-switch d-flex align-items-center gap-2"><input class="form-check-input user-toggle" type="checkbox" %s data-id="%d" data-name="%s"><span class="status-label %s">%s</span></div>',
|
|
$user->is_active ? 'checked' : '',
|
|
$user->id,
|
|
e($user->name),
|
|
$user->is_active ? 'text-success' : 'text-danger',
|
|
$user->is_active ? 'Active' : 'Inactive'
|
|
);
|
|
}
|
|
|
|
$row[] = e($user->name);
|
|
$row[] = e($user->email);
|
|
$row[] = $roleBadges;
|
|
$row[] = e(format_datetime($user->created_at));
|
|
$row[] = e($user->creator->email ?? 'System');
|
|
$row[] = e(format_datetime($user->updated_at));
|
|
$row[] = e($user->updater->email ?? '-');
|
|
|
|
if ($canManage) {
|
|
$impersonateButton = '';
|
|
if ($authUser->can('impersonate users')) {
|
|
if ($authUser->id !== $user->id && ! $user->hasRole('Developer')) {
|
|
$impersonateButton = '<form method="POST" action="'.route('impersonate.start', $user->id).'" class="d-inline">'
|
|
.csrf_field()
|
|
.'<button type="submit" class="btn btn-warning btn-sm rounded-pill px-3">'.e(__('Impersonate')).'</button>'
|
|
.'</form>';
|
|
} else {
|
|
$impersonateButton = '<button type="button" class="btn btn-light btn-sm rounded-pill px-3 text-muted disabled" style="opacity: 0.6; cursor: not-allowed;">'
|
|
.e(__('Impersonate'))
|
|
.'</button>';
|
|
}
|
|
}
|
|
|
|
if ($user->trashed()) {
|
|
$row[] = '<div class="d-inline-flex align-items-center gap-2">'
|
|
.'<button class="btn btn-link text-success btn-restore" data-id="'.$user->id.'" data-name="'.e($user->name).'"><i class="bi bi-arrow-counterclockwise"></i> '.e(__('Restore')).'</button>'
|
|
.'<button class="btn btn-link text-danger btn-force-delete" data-id="'.$user->id.'" data-name="'.e($user->name).'"><i class="bi bi-x-circle"></i> '.e(__('Terminated')).'</button>'
|
|
.'</div>';
|
|
} else {
|
|
$row[] = '<div class="d-inline-flex align-items-center gap-2">'
|
|
.$impersonateButton
|
|
.'<button class="btn btn-link btn-edit" data-id="'.$user->id.'" data-name="'.e($user->name).'" data-email="'.e($user->email).'" data-roles="'.e(json_encode($user->roles->pluck('id')->values()->all())).'" data-created="'.e(format_datetime($user->created_at)).'" data-created-by="'.e($user->creator->email ?? 'System').'" data-updated-at="'.e(format_datetime($user->updated_at)).'" data-updated-by="'.e($user->updater->email ?? '-').'" data-bs-toggle="modal" data-bs-target="#editUserModal"><i class="bi bi-pencil"></i></button>'
|
|
.'<button class="btn btn-link text-danger btn-delete" data-id="'.$user->id.'" data-name="'.e($user->name).'"><i class="bi bi-trash"></i></button>'
|
|
.'</div>';
|
|
}
|
|
}
|
|
|
|
return $row;
|
|
})->all();
|
|
|
|
return DataTable::response($request, $recordsTotal, $recordsFiltered, $rows);
|
|
} catch (\Exception $e) {
|
|
Log::error('DataTable Error [UserManagement]: '.$e->getMessage());
|
|
|
|
return DataTable::response($request, 0, 0, []);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* STORE — Tambah user + assign role
|
|
*/
|
|
public function store(Request $request)
|
|
{
|
|
$request->validate([
|
|
'name' => [
|
|
'required',
|
|
'string',
|
|
'min:3',
|
|
'max:100',
|
|
'regex:/^[a-zA-Z\s]+$/',
|
|
],
|
|
'email' => [
|
|
'required',
|
|
'email',
|
|
'max:150',
|
|
Rule::unique('users', 'email'),
|
|
],
|
|
'password' => [
|
|
'required',
|
|
'string',
|
|
'min:12',
|
|
'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{12,}$/',
|
|
],
|
|
'roles' => [
|
|
'required',
|
|
'array',
|
|
'min:1',
|
|
],
|
|
'roles.*' => [
|
|
'required',
|
|
Rule::exists('roles', 'id'),
|
|
],
|
|
], [
|
|
'name.regex' => __('Name may only contain letters and spaces.'),
|
|
'email.unique' => __('This email is already registered.'),
|
|
'password.regex' => __('Password must contain uppercase, lowercase, number, and symbol.'),
|
|
'roles.required' => __('At least one role must be selected.'),
|
|
]);
|
|
|
|
try {
|
|
// create user
|
|
$user = User::create([
|
|
'name' => $request->name,
|
|
'email' => $request->email,
|
|
'password' => bcrypt($request->password),
|
|
'is_active' => 1,
|
|
'created_by' => Auth::id(),
|
|
]);
|
|
|
|
// 🔥 FIX: Convert role IDs → role names
|
|
$roleNames = Role::whereIn('id', $request->roles)->pluck('name')->toArray();
|
|
|
|
// assign roles by name
|
|
$user->syncRoles($roleNames);
|
|
|
|
if ($request->expectsJson()) {
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => __('User created successfully.'),
|
|
]);
|
|
}
|
|
|
|
return back()->with('success', __('User created successfully.'));
|
|
} catch (\Exception $e) {
|
|
if ($request->expectsJson()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => __('Failed to create user: ').$e->getMessage(),
|
|
], 500);
|
|
}
|
|
|
|
return back()->with('error', __('Failed to create user: ').$e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* UPDATE — Edit user data + (optional) password + roles
|
|
*/
|
|
public function update(Request $request, $id)
|
|
{
|
|
$request->validate([
|
|
'name' => [
|
|
'required',
|
|
'string',
|
|
'min:3',
|
|
'max:100',
|
|
'regex:/^[a-zA-Z\s]+$/',
|
|
],
|
|
'email' => [
|
|
'required',
|
|
'email',
|
|
'max:150',
|
|
Rule::unique('users', 'email')->ignore($id),
|
|
],
|
|
'password' => [
|
|
'nullable',
|
|
'string',
|
|
'min:12',
|
|
'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{12,}$/',
|
|
],
|
|
'roles' => [
|
|
'required',
|
|
'array',
|
|
'min:1',
|
|
],
|
|
'roles.*' => [
|
|
Rule::exists('roles', 'id'),
|
|
],
|
|
], [
|
|
'name.regex' => __('Name may only contain letters and spaces.'),
|
|
'password.regex' => __('Password must contain uppercase, lowercase, number, and symbol.'),
|
|
]);
|
|
|
|
try {
|
|
$user = User::findOrFail($id);
|
|
|
|
$data = [
|
|
'name' => $request->name,
|
|
'email' => $request->email,
|
|
'updated_by' => Auth::id(),
|
|
];
|
|
|
|
if ($request->filled('password')) {
|
|
$data['password'] = bcrypt($request->password);
|
|
}
|
|
|
|
$user->update($data);
|
|
|
|
// 🔥 FIX: Convert ID role → Role Name
|
|
$roleNames = Role::whereIn('id', $request->roles)->pluck('name')->toArray();
|
|
$user->syncRoles($roleNames);
|
|
|
|
if ($request->expectsJson()) {
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => __('User updated successfully.'),
|
|
]);
|
|
}
|
|
|
|
return back()->with('success', __('User updated successfully.'));
|
|
|
|
} catch (\Exception $e) {
|
|
if ($request->expectsJson()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => __('Failed to update user: ').$e->getMessage(),
|
|
], 500);
|
|
}
|
|
|
|
return back()->with('error', __('Failed to update user: ').$e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* TOGGLE — Aktivasi/Non-aktifkan user
|
|
*/
|
|
public function toggleStatus(Request $request)
|
|
{
|
|
$request->validate([
|
|
'id' => 'required|exists:users,id',
|
|
'status' => 'required|in:activate,deactivate',
|
|
]);
|
|
|
|
$user = User::findOrFail($request->id);
|
|
|
|
$user->update([
|
|
'is_active' => $request->status === 'activate' ? 1 : 0,
|
|
'updated_by' => Auth::id(),
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => __('User status updated successfully.'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* DESTROY — Archive (Soft Delete)
|
|
*/
|
|
public function destroy($id)
|
|
{
|
|
$user = User::findOrFail($id);
|
|
|
|
$user->delete();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => __('User has been archived.'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* RESTORE — Restore user (Soft Delete)
|
|
*/
|
|
public function restore($id)
|
|
{
|
|
$user = User::withTrashed()->findOrFail($id);
|
|
$user->restore();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => __('User has been restored.'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* FORCE DELETE — Permanent removal
|
|
*/
|
|
public function forceDelete($id)
|
|
{
|
|
$user = User::withTrashed()->findOrFail($id);
|
|
|
|
// Prevent accidental deletion of self
|
|
if ($user->id === Auth::id()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => __('You cannot permanently delete yourself.'),
|
|
], 403);
|
|
}
|
|
|
|
$user->forceDelete();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => __('User has been permanently removed.'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* BULK TOGGLE STATUS
|
|
*/
|
|
public function bulkToggleStatus(Request $request)
|
|
{
|
|
$request->validate([
|
|
'ids' => 'required|array',
|
|
'ids.*' => 'exists:users,id',
|
|
'status' => 'required|in:activate,deactivate',
|
|
]);
|
|
|
|
$status = $request->status === 'activate' ? 1 : 0;
|
|
|
|
User::whereIn('id', $request->ids)->update([
|
|
'is_active' => $status,
|
|
'updated_by' => Auth::id(),
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => __('Selected users have been updated.'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* BULK DELETE (Archive)
|
|
*/
|
|
public function bulkDelete(Request $request)
|
|
{
|
|
$request->validate([
|
|
'ids' => 'required|array',
|
|
'ids.*' => 'exists:users,id',
|
|
]);
|
|
|
|
User::whereIn('id', $request->ids)->delete();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => __('Selected users have been archived.'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* BULK RESTORE
|
|
*/
|
|
public function bulkRestore(Request $request)
|
|
{
|
|
$request->validate([
|
|
'ids' => 'required|array',
|
|
'ids.*' => 'exists:users,id',
|
|
]);
|
|
|
|
User::withTrashed()->whereIn('id', $request->ids)->restore();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => __('Selected users have been restored.'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* BULK FORCE DELETE
|
|
*/
|
|
public function bulkForceDelete(Request $request)
|
|
{
|
|
$request->validate([
|
|
'ids' => 'required|array',
|
|
'ids.*' => 'exists:users,id',
|
|
]);
|
|
|
|
// Prevent self deletion
|
|
$ids = array_filter($request->ids, fn ($id) => $id != Auth::id());
|
|
|
|
User::withTrashed()->whereIn('id', $ids)->forceDelete();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => __('Selected users have been permanently removed.'),
|
|
]);
|
|
}
|
|
}
|