711 lines
45 KiB
PHP
711 lines
45 KiB
PHP
<x-app-layout>
|
|
@php
|
|
$canManageUsers = auth()->user()->can('manage user directory');
|
|
$userOrderIndex = $canManageUsers ? 5 : 3;
|
|
@endphp
|
|
<div class="container-fluid" id="main-content">
|
|
<div class="row gx-3 gx-lg-4">
|
|
<div class="col-12">
|
|
<div class="card adminuiux-card">
|
|
<div class="card-body p-0">
|
|
|
|
{{-- header halaman user management --}}
|
|
<div class="p-4 pb-0">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<div>
|
|
<h5 class="mb-0 fw-bold">{{ __('User Management') }}</h5>
|
|
<small class="text-muted">
|
|
{{ __('Manage users, assign roles, and control access within the system.') }}
|
|
</small>
|
|
{{-- Extra Filters (Picked up by app.blade.php DataTable logic) --}}
|
|
<input type="hidden" id="filter-trashed-val" name="trashed" class="filter-extra"
|
|
value="active">
|
|
</div>
|
|
@can('manage user directory')
|
|
<button class="btn btn-dark mt-3 px-3 rounded-pill" data-bs-toggle="modal"
|
|
data-bs-target="#addUserModal">
|
|
 + {{ __('Add User') }} 
|
|
</button>
|
|
@endcan
|
|
</div>
|
|
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-4">
|
|
<div class="d-flex gap-2">
|
|
<button type="button"
|
|
class="btn btn-sm btn-dark rounded-pill px-3 filter-trashed"
|
|
data-value="active">
|
|
<i class="bi bi-people me-1"></i> {{ __('Active Users') }}
|
|
</button>
|
|
<button type="button"
|
|
class="btn btn-sm btn-outline-dark border border-dark rounded-pill px-3 filter-trashed"
|
|
data-value="archived">
|
|
<i class="bi bi-archive me-1"></i> {{ __('Archived') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- tabel users --}}
|
|
<div class="p-4">
|
|
<div class="table-responsive overflow-hidden">
|
|
<table id="datatables" class="table table-hover table-bordered w-100 nowrap mb-0"
|
|
data-server-side="true" data-ajax-url="{{ route('users') }}"
|
|
data-order='@json([[$userOrderIndex, "desc"]])'>
|
|
<thead>
|
|
<tr>
|
|
@can('manage user directory')
|
|
<th style="width: 20px;" data-orderable="false" data-searchable="false">
|
|
<input type="checkbox" class="form-check-input check-all">
|
|
</th>
|
|
<th class="text-wrap">{{ __('Status') }}</th>
|
|
@endcan
|
|
<th class="text-wrap">{{ __('User Name') }}</th>
|
|
<th class="text-wrap">{{ __('Email') }}</th>
|
|
<th class="text-wrap">{{ __('Roles') }}</th>
|
|
<th class="text-wrap" data-hide="audit">{{ __('Created At') }}</th>
|
|
<th class="text-wrap" data-hide="audit">{{ __('Created By') }}</th>
|
|
<th class="text-wrap" data-hide="audit">{{ __('Last Updated At') }}</th>
|
|
<th class="text-wrap" data-hide="audit">{{ __('Last Updated By') }}</th>
|
|
@can('manage user directory')
|
|
<th class="text-wrap text-end" data-orderable="false"
|
|
data-searchable="false">{{ __('Action') }}</th>
|
|
@endcan
|
|
</tr>
|
|
|
|
{{-- filter --}}
|
|
<tr class="filter-row">
|
|
@can('manage user directory')
|
|
<th></th>
|
|
<th>
|
|
<select class="form-select form-select-sm">
|
|
<option value="">{{ __('All') }}</option>
|
|
<option value="active">{{ __('Active') }}</option>
|
|
<option value="inactive">{{ __('Inactive') }}</option>
|
|
</select>
|
|
</th>
|
|
@endcan
|
|
<th><input class="form-control form-control-sm"
|
|
placeholder="{{ __('Search Name') }}"></th>
|
|
<th><input class="form-control form-control-sm"
|
|
placeholder="{{ __('Search Email') }}"></th>
|
|
<th><input class="form-control form-control-sm"
|
|
placeholder="{{ __('Search Role') }}"></th>
|
|
<th><input type="date" class="form-control form-control-sm"></th>
|
|
<th><input class="form-control form-control-sm"
|
|
placeholder="{{ __('Created By') }}"></th>
|
|
<th><input type="date" class="form-control form-control-sm"></th>
|
|
<th><input class="form-control form-control-sm"
|
|
placeholder="{{ __('Updated By') }}"></th>
|
|
@can('manage user directory')
|
|
<th></th>
|
|
@endcan
|
|
</tr>
|
|
</thead>
|
|
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- MODAL ADD USER --}}
|
|
<div class="modal fade" id="addUserModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content rounded-3">
|
|
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">{{ __('Add User') }}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" action="{{ route('users.store') }}" autocomplete="off"
|
|
class="ajax-form" data-reset="true">
|
|
|
|
@csrf
|
|
|
|
{{-- anti autofill trap --}}
|
|
<input type="text" name="fakeuser" style="display:none">
|
|
<input type="password" name="fakepass" style="display:none">
|
|
|
|
<div class="modal-body">
|
|
|
|
{{-- User Name --}}
|
|
<label class="form-label fw-semibold">
|
|
{{ __('User Name') }} <span class="text-danger">*</span>
|
|
</label>
|
|
<input type="text" name="name" class="form-control mb-3"
|
|
placeholder="ex: John Wick" required minlength="3" maxlength="100"
|
|
pattern="^[a-zA-Z\s]+$"
|
|
title="Name must be at least 3 characters and contain letters and spaces only">
|
|
|
|
{{-- Email --}}
|
|
<label class="form-label fw-semibold">
|
|
{{ __('Email') }} <span class="text-danger">*</span>
|
|
</label>
|
|
<input type="email" name="email" class="form-control mb-3"
|
|
placeholder="john@email.com" required maxlength="150"
|
|
title="Enter a valid and unique email address">
|
|
|
|
{{-- Password --}}
|
|
<label class="form-label fw-semibold">
|
|
{{ __('Password') }} <span class="text-danger">*</span>
|
|
</label>
|
|
<div class="input-group mb-3">
|
|
<input type="password" name="password" class="form-control border-end-0"
|
|
placeholder="Minimum 12 characters" required minlength="12"
|
|
autocomplete="new-password"
|
|
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{12,}$"
|
|
title="Minimum 12 characters with uppercase, lowercase, number, and symbol">
|
|
<button
|
|
class="btn btn-outline-secondary bg-white border-start-0 password-toggle"
|
|
type="button" style="border-color: #dee2e6;">
|
|
<i class="bi bi-eye text-secondary"></i>
|
|
</button>
|
|
</div>
|
|
|
|
{{-- Assign Role --}}
|
|
<label class="form-label fw-semibold">
|
|
{{ __('Assign Role') }} <span class="text-danger">*</span>
|
|
</label>
|
|
<select id="roleSelect" name="roles[]" class="form-select" multiple required
|
|
title="Select at least one role">
|
|
@foreach ($roles as $role)
|
|
@if($role->name !== 'Developer' || auth()->user()->hasRole('Developer'))
|
|
<option value="{{ $role->id }}">{{ $role->name }}</option>
|
|
@endif
|
|
@endforeach
|
|
</select>
|
|
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-dark rounded-pill"
|
|
data-bs-dismiss="modal">
|
|
 Close 
|
|
</button>
|
|
<button type="submit" class="btn btn-dark rounded-pill">
|
|
 Save User 
|
|
</button>
|
|
</div>
|
|
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- MODAL EDIT USER --}}
|
|
<div class="modal fade" id="editUserModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content rounded-3">
|
|
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">{{ __('Edit User') }}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form id="editUserForm" method="POST" autocomplete="off" class="ajax-form">
|
|
|
|
@csrf
|
|
@method('PUT')
|
|
|
|
{{-- anti autofill trap --}}
|
|
<input type="text" name="fakeuser" style="display:none">
|
|
<input type="password" name="fakepass" style="display:none">
|
|
|
|
<div class="modal-body">
|
|
|
|
<input type="hidden" id="edit-user-id" name="id">
|
|
|
|
{{-- User Name --}}
|
|
<label class="form-label fw-semibold">
|
|
{{ __('User Name') }} <span class="text-danger">*</span>
|
|
</label>
|
|
<input id="edit-user-name" name="name" type="text" class="form-control mb-3"
|
|
placeholder="ex: John Wick" required minlength="3" maxlength="100"
|
|
pattern="^[a-zA-Z\s]+$"
|
|
title="Name must be at least 3 characters and contain letters and spaces only">
|
|
|
|
{{-- Email --}}
|
|
<label class="form-label fw-semibold">
|
|
{{ __('Email') }} <span class="text-danger">*</span>
|
|
</label>
|
|
<input id="edit-user-email" name="email" type="email"
|
|
class="form-control mb-3" placeholder="john@email.com" required
|
|
maxlength="150" title="Enter a valid and unique email address">
|
|
|
|
{{-- Password (Optional) --}}
|
|
<label class="form-label fw-semibold">
|
|
{{ __('Password (Optional)') }}
|
|
</label>
|
|
<div class="input-group mb-3">
|
|
<input name="password" type="password" class="form-control border-end-0"
|
|
placeholder="leave empty to keep current" minlength="12"
|
|
autocomplete="new-password"
|
|
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{12,}$"
|
|
title="Minimum 12 characters with uppercase, lowercase, number, and symbol">
|
|
<button
|
|
class="btn btn-outline-secondary bg-white border-start-0 password-toggle"
|
|
type="button" style="border-color: #dee2e6;">
|
|
<i class="bi bi-eye text-secondary"></i>
|
|
</button>
|
|
</div>
|
|
|
|
{{-- Roles --}}
|
|
<label class="form-label fw-semibold">
|
|
{{ __('Roles') }} <span class="text-danger">*</span>
|
|
</label>
|
|
<select id="roleSelect2" name="roles[]" class="form-select" multiple
|
|
required title="Select at least one role">
|
|
@foreach ($roles as $role)
|
|
@if($role->name !== 'Developer' || auth()->user()->hasRole('Developer'))
|
|
<option value="{{ $role->id }}">{{ $role->name }}</option>
|
|
@endif
|
|
@endforeach
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-dark rounded-pill"
|
|
data-bs-dismiss="modal">
|
|
 Close 
|
|
</button>
|
|
<button type="submit" class="btn btn-dark rounded-pill">
|
|
 Update User 
|
|
</button>
|
|
</div>
|
|
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- SCRIPT HANDLER --}}
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
|
|
// init choices create
|
|
const roleSelect = document.querySelector('#roleSelect');
|
|
if (roleSelect && !roleSelect.dataset.choices) {
|
|
new Choices(roleSelect, {
|
|
removeItemButton: true,
|
|
searchEnabled: true,
|
|
placeholderValue: 'Select roles'
|
|
});
|
|
roleSelect.dataset.choices = "initialized";
|
|
}
|
|
|
|
// init choices edit
|
|
const roleSelect2 = document.querySelector('#roleSelect2');
|
|
let editChoices = null;
|
|
if (roleSelect2 && !roleSelect2.dataset.choices) {
|
|
editChoices = new Choices(roleSelect2, {
|
|
removeItemButton: true,
|
|
searchEnabled: true,
|
|
placeholderValue: 'Select roles'
|
|
});
|
|
roleSelect2.dataset.choices = "initialized";
|
|
}
|
|
|
|
document.addEventListener("change", e => {
|
|
const toggle = e.target.closest(".user-toggle");
|
|
|
|
if (!toggle) {
|
|
return;
|
|
}
|
|
|
|
const id = toggle.dataset.id;
|
|
const name = toggle.dataset.name;
|
|
const status = toggle.checked ? "activate" : "deactivate";
|
|
const label = toggle.closest(".form-switch").querySelector(".status-label");
|
|
|
|
StandardSwal.fire({
|
|
title: `${status === "activate" ? "Activate" : "Deactivate"} User?`,
|
|
text: `You are about to change the access status for "${name}".`,
|
|
icon: "warning",
|
|
showCancelButton: true,
|
|
confirmButtonText: "Yes, Continue",
|
|
cancelButtonText: "Cancel",
|
|
}).then(result => {
|
|
if (!result.isConfirmed) {
|
|
toggle.checked = !toggle.checked;
|
|
return;
|
|
}
|
|
|
|
label.textContent = toggle.checked ? "Active" : "Inactive";
|
|
label.classList.toggle("text-success", toggle.checked);
|
|
label.classList.toggle("text-danger", !toggle.checked);
|
|
|
|
fetch("{{ route('users.toggle-status') }}", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-CSRF-TOKEN": "{{ csrf_token() }}"
|
|
},
|
|
body: JSON.stringify({
|
|
id,
|
|
status
|
|
}),
|
|
}).then(res => res.json())
|
|
.then(data => {
|
|
if (!data.success) {
|
|
throw new Error("Failed to update status.");
|
|
}
|
|
|
|
// Live Reload immediately
|
|
window.reloadDataTable?.();
|
|
|
|
StandardSwal.fire({
|
|
title: "{{ __('Success!') }}",
|
|
text: data.message || "{{ __('User status has been updated successfully.') }}",
|
|
icon: "success",
|
|
timer: 2000,
|
|
showConfirmButton: false,
|
|
timerProgressBar: true
|
|
});
|
|
})
|
|
.catch(() => {
|
|
toggle.checked = !toggle.checked;
|
|
StandardSwal.fire({
|
|
title: "{{ __('Error!') }}",
|
|
text: "{{ __('An unexpected error occurred while updating status.') }}",
|
|
icon: "error"
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
document.addEventListener("click", e => {
|
|
// Filter Active/Archived
|
|
const filterBtn = e.target.closest(".filter-trashed");
|
|
if (filterBtn) {
|
|
document.querySelectorAll(".filter-trashed").forEach(b => {
|
|
b.classList.remove("btn-dark");
|
|
b.classList.add("btn-outline-dark", "border", "border-dark");
|
|
});
|
|
filterBtn.classList.add("btn-dark");
|
|
filterBtn.classList.remove("btn-outline-dark", "border", "border-dark");
|
|
|
|
document.getElementById("filter-trashed-val").value = filterBtn.dataset.value;
|
|
window.reloadDataTable?.();
|
|
return;
|
|
}
|
|
|
|
const editButton = e.target.closest(".btn-edit");
|
|
|
|
if (editButton) {
|
|
const url = `{{ route('users.update', 'ID') }}`.replace("ID",
|
|
editButton.dataset.id);
|
|
document.getElementById("editUserForm").action = url;
|
|
|
|
document.getElementById("edit-user-id").value = editButton.dataset.id;
|
|
document.getElementById("edit-user-name").value = editButton.dataset
|
|
.name;
|
|
document.getElementById("edit-user-email").value = editButton.dataset
|
|
.email;
|
|
|
|
const selected = JSON.parse(editButton.dataset.roles || "[]");
|
|
if (editChoices) {
|
|
editChoices.removeActiveItems();
|
|
selected.forEach(role => editChoices.setChoiceByValue(role
|
|
.toString()));
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
const deleteButton = e.target.closest(".btn-delete");
|
|
|
|
if (deleteButton) {
|
|
e.preventDefault();
|
|
const id = deleteButton.dataset.id;
|
|
const name = deleteButton.dataset.name;
|
|
|
|
StandardSwal.fire({
|
|
title: "Archive User?",
|
|
text: `"${name}" will be deactivated and moved to the system archives.`,
|
|
icon: "warning",
|
|
showCancelButton: true,
|
|
customClass: {
|
|
confirmButton: 'btn-pill-danger',
|
|
cancelButton: 'btn-pill-cancel'
|
|
},
|
|
confirmButtonText: "Yes, Archive",
|
|
cancelButtonText: "Cancel",
|
|
}).then(result => {
|
|
if (!result.isConfirmed) return;
|
|
|
|
const url = `{{ route('users.destroy', 'ID') }}`.replace("ID", id);
|
|
|
|
fetch(url, {
|
|
method: "DELETE",
|
|
headers: {
|
|
"X-CSRF-TOKEN": document.querySelector(
|
|
'meta[name="csrf-token"]').content,
|
|
"Accept": "application/json",
|
|
}
|
|
}).then(res => res.json())
|
|
.then(data => {
|
|
window.reloadDataTable?.();
|
|
StandardSwal.fire({
|
|
icon: "success",
|
|
title: "Archived Successfully!",
|
|
text: data.message || "The user has been moved to the archived list.",
|
|
timer: 2000,
|
|
showConfirmButton: false,
|
|
timerProgressBar: true
|
|
});
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
// RESTORE HANDLER
|
|
const restoreBtn = e.target.closest(".btn-restore");
|
|
if (restoreBtn) {
|
|
e.preventDefault();
|
|
const id = restoreBtn.dataset.id;
|
|
const name = restoreBtn.dataset.name;
|
|
|
|
StandardSwal.fire({
|
|
title: "{{ __('Restore User?') }}",
|
|
text: `{{ __('Do you want to restore access for') }} "${name}"?`,
|
|
icon: "info",
|
|
showCancelButton: true,
|
|
confirmButtonText: "{{ __('Yes, Restore') }}",
|
|
cancelButtonText: "{{ __('Cancel') }}",
|
|
}).then(result => {
|
|
if (!result.isConfirmed) return;
|
|
|
|
const url = `{{ route('users.restore', 'ID') }}`.replace("ID", id);
|
|
fetch(url, {
|
|
method: "POST",
|
|
headers: {
|
|
"X-CSRF-TOKEN": "{{ csrf_token() }}",
|
|
"Accept": "application/json",
|
|
}
|
|
}).then(res => res.json())
|
|
.then(data => {
|
|
window.reloadDataTable?.();
|
|
StandardSwal.fire({
|
|
icon: "success",
|
|
title: "{{ __('Restored!') }}",
|
|
text: data.message,
|
|
timer: 2000,
|
|
showConfirmButton: false,
|
|
timerProgressBar: true
|
|
});
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
// FORCE DELETE HANDLER
|
|
const forceBtn = e.target.closest(".btn-force-delete");
|
|
if (forceBtn) {
|
|
e.preventDefault();
|
|
const id = forceBtn.dataset.id;
|
|
const name = forceBtn.dataset.name;
|
|
|
|
StandardSwal.fire({
|
|
title: "{{ __('Terminate Account?') }}",
|
|
text: `{{ __('This will PERMANENTLY delete') }} "${name}". {{ __('This action cannot be undone.') }}`,
|
|
icon: "error",
|
|
showCancelButton: true,
|
|
confirmButtonText: "{{ __('Yes, Terminate') }}",
|
|
cancelButtonText: "{{ __('Cancel') }}",
|
|
confirmButtonColor: "#dc3545",
|
|
}).then(result => {
|
|
if (!result.isConfirmed) return;
|
|
|
|
const url = `{{ route('users.force-delete', 'ID') }}`.replace("ID", id);
|
|
fetch(url, {
|
|
method: "DELETE",
|
|
headers: {
|
|
"X-CSRF-TOKEN": "{{ csrf_token() }}",
|
|
"Accept": "application/json",
|
|
}
|
|
}).then(res => res.json())
|
|
.then(data => {
|
|
window.reloadDataTable?.();
|
|
StandardSwal.fire({
|
|
icon: "success",
|
|
title: "{{ __('Terminated!') }}",
|
|
text: data.message,
|
|
timer: 2000,
|
|
showConfirmButton: false,
|
|
timerProgressBar: true
|
|
});
|
|
}).catch(err => {
|
|
StandardSwal.fire("Error", "Failed to terminate user.", "error");
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
// BULK ACTION LOGIC
|
|
const bulkBtn = e.target.closest(".bulk-btn");
|
|
if (bulkBtn) {
|
|
e.preventDefault();
|
|
const action = bulkBtn.dataset.action;
|
|
const selectedIds = Array.from(document.querySelectorAll(".user-checkbox:checked")).map(cb => cb.value);
|
|
|
|
if (selectedIds.length === 0) return;
|
|
|
|
let config = {
|
|
title: "Are you sure?",
|
|
text: `You are about to perform ${action} on ${selectedIds.length} users.`,
|
|
icon: "warning",
|
|
url: "",
|
|
method: "POST",
|
|
body: { ids: selectedIds }
|
|
};
|
|
|
|
switch(action) {
|
|
case 'activate':
|
|
config.url = "{{ route('users.bulk-toggle-status') }}";
|
|
config.body.status = 'activate';
|
|
break;
|
|
case 'deactivate':
|
|
config.url = "{{ route('users.bulk-toggle-status') }}";
|
|
config.body.status = 'deactivate';
|
|
break;
|
|
case 'archive':
|
|
config.url = "{{ route('users.bulk-delete') }}";
|
|
config.icon = "error";
|
|
break;
|
|
case 'restore':
|
|
config.url = "{{ route('users.bulk-restore') }}";
|
|
config.icon = "info";
|
|
break;
|
|
case 'terminate':
|
|
config.url = "{{ route('users.bulk-force-delete') }}";
|
|
config.icon = "error";
|
|
config.text += " THIS ACTION IS PERMANENT!";
|
|
break;
|
|
}
|
|
|
|
StandardSwal.fire({
|
|
title: config.title,
|
|
text: config.text,
|
|
icon: config.icon,
|
|
showCancelButton: true,
|
|
confirmButtonText: "Yes, Proceed",
|
|
}).then(result => {
|
|
if (!result.isConfirmed) return;
|
|
|
|
fetch(config.url, {
|
|
method: config.method,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-CSRF-TOKEN": "{{ csrf_token() }}",
|
|
"Accept": "application/json"
|
|
},
|
|
body: JSON.stringify(config.body)
|
|
}).then(res => res.json())
|
|
.then(data => {
|
|
window.reloadDataTable?.();
|
|
document.querySelector(".check-all").checked = false;
|
|
StandardSwal.fire("Success", data.message, "success");
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
// Check All Handler
|
|
document.addEventListener("change", e => {
|
|
if (e.target.classList.contains("check-all")) {
|
|
document.querySelectorAll(".user-checkbox").forEach(cb => cb.checked = e.target.checked);
|
|
updateBulkBar();
|
|
}
|
|
|
|
if (e.target.classList.contains("user-checkbox")) {
|
|
updateBulkBar();
|
|
}
|
|
});
|
|
|
|
document.getElementById("clear-selection")?.addEventListener("click", () => {
|
|
document.querySelectorAll(".user-checkbox, .check-all").forEach(cb => cb.checked = false);
|
|
updateBulkBar();
|
|
});
|
|
|
|
function updateBulkBar() {
|
|
const checked = document.querySelectorAll(".user-checkbox:checked");
|
|
const bar = document.getElementById("bulk-action-bar");
|
|
const count = document.getElementById("selected-count");
|
|
const filterEl = document.getElementById("filter-trashed-val");
|
|
|
|
if (!bar || !count) return;
|
|
|
|
const filterType = filterEl ? filterEl.value : 'active';
|
|
|
|
if (checked.length > 0) {
|
|
bar.classList.remove("d-none");
|
|
count.textContent = checked.length;
|
|
|
|
// Toggle visibility of specific bulk actions based on active/archived view
|
|
if (filterType === 'archived') {
|
|
document.querySelectorAll(".show-if-active").forEach(el => el.classList.add("d-none"));
|
|
document.querySelectorAll(".show-if-archived").forEach(el => el.classList.remove("d-none"));
|
|
} else {
|
|
document.querySelectorAll(".show-if-active").forEach(el => el.classList.remove("d-none"));
|
|
document.querySelectorAll(".show-if-archived").forEach(el => el.classList.add("d-none"));
|
|
}
|
|
} else {
|
|
bar.classList.add("d-none");
|
|
}
|
|
}
|
|
|
|
// Reset selection on dataTable reload
|
|
window.addEventListener('dataTableReloaded', () => {
|
|
document.querySelector(".check-all").checked = false;
|
|
updateBulkBar();
|
|
});
|
|
|
|
});
|
|
</script>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Floating Bulk Action Bar --}}
|
|
<div id="bulk-action-bar" class="d-none animate__animated animate__fadeIn position-fixed bottom-0 mb-4 start-50 translate-middle-x" style="z-index: 2500;">
|
|
<div class="bg-dark text-white border-0 rounded-pill px-3 py-2 d-flex align-items-center gap-3 shadow-lg" style="backdrop-filter: blur(15px); background-color: rgba(20, 20, 20, 0.98) !important; border: 1px solid rgba(255,255,255,0.1) !important;">
|
|
<div class="d-flex align-items-center gap-2 ps-2">
|
|
<div class="bg-primary rounded-circle d-flex align-items-center justify-content-center" style="width: 28px; height: 28px;">
|
|
<span class="small fw-bold text-white" id="selected-count">0</span>
|
|
</div>
|
|
<span class="small fw-bold d-none d-sm-inline">{{ __('Terpilih') }}</span>
|
|
</div>
|
|
|
|
<div class="vr bg-white opacity-25" style="height: 20px;"></div>
|
|
|
|
<div class="d-flex gap-1">
|
|
{{-- Quick Actions --}}
|
|
<button title="{{ __('Aktifkan') }}" class="btn btn-link text-success p-2 bulk-btn" data-action="activate">
|
|
<i class="bi bi-check-circle fs-5"></i>
|
|
</button>
|
|
<button title="{{ __('Nonaktifkan') }}" class="btn btn-link text-white p-2 bulk-btn" data-action="deactivate">
|
|
<i class="bi bi-dash-circle fs-5"></i>
|
|
</button>
|
|
|
|
<div class="vr bg-white opacity-25 mx-1" style="height: 20px; margin-top: 10px;"></div>
|
|
|
|
{{-- Contextual Actions --}}
|
|
<button title="{{ __('Arsipkan') }}" class="btn btn-link p-2 bulk-btn show-if-active" data-action="archive">
|
|
<i class="bi bi-trash fs-5 text-danger" style="color: #dc3545 !important;"></i>
|
|
</button>
|
|
<button title="{{ __('Pulihkan') }}" class="btn btn-link text-info p-2 bulk-btn show-if-archived d-none" data-action="restore">
|
|
<i class="bi bi-arrow-counterclockwise fs-5"></i>
|
|
</button>
|
|
<button title="{{ __('Hapus Permanen') }}" class="btn btn-link text-danger p-2 bulk-btn show-if-archived d-none" data-action="terminate">
|
|
<i class="bi bi-x-circle fs-5"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="vr bg-white opacity-25" style="height: 20px;"></div>
|
|
|
|
<button type="button" class="btn-close btn-close-white btn-sm me-2" id="clear-selection" style="font-size: 0.6rem;"></button>
|
|
</div>
|
|
</div>
|
|
</x-app-layout> |