Files

1235 lines
90 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<x-app-layout>
<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">
{{-- roles management page header --}}
<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">{{ __('Roles Management') }}</h5>
<small class="text-muted">
{{ __('Manage roles, assign permissions, and control access levels within the system.') }}
</small>
</div>
<div class="d-flex gap-2 mt-3">
<button type="button"
class="btn btn-outline-dark px-3 rounded-pill d-flex align-items-center gap-2"
data-bs-toggle="modal" data-bs-target="#rbacDocsModal">
<i class="bi bi-book"></i> {{ __('Documentation') }}
</button>
@can('manage access rights')
<button class="btn btn-dark px-3 rounded-pill" data-bs-toggle="modal"
data-bs-target="#addRoleModal">
&emsp;+ {{ __('Add Role') }}&emsp;
</button>
@endcan
</div>
</div>
<div class="d-flex gap-2 mb-4">
<button type="button" class="btn btn-sm btn-dark rounded-pill px-3 filter-trashed-role" data-value="active">
<i class="bi bi-shield-check me-1"></i> {{ __('Active Roles') }}
</button>
<button type="button" class="btn btn-sm btn-outline-dark border border-dark rounded-pill px-3 filter-trashed-role" data-value="archived">
<i class="bi bi-archive me-1"></i> {{ __('Arsip') }}
</button>
</div>
<input type="hidden" id="filter-trashed-role-val" name="trashed" class="filter-extra" value="active">
</div>
{{-- roles table --}}
<div class="p-4">
<div class="table-responsive overflow-hidden">
<table id="datatables" class="table table-hover table-bordered w-100 mb-0"
data-server-side="true" data-ajax-url="{{ route('roles') }}"
data-order='@json([[5, "desc"]])'>
<thead>
<tr>
@can('manage access rights')
<th style="width: 20px;" data-orderable="false" data-searchable="false">
<input type="checkbox" class="form-check-input check-all">
</th>
@endcan
<th>{{ __('Status') }}</th>
<th>{{ __('Role Name') }}</th>
<th>{{ __('Guard') }}</th>
<th class="text-wrap">{{ __('Assigned Permissions') }}</th>
<th data-hide="audit">{{ __('Created At') }}</th>
<th data-hide="audit">{{ __('Created By') }}</th>
<th data-hide="audit">{{ __('Last Updated At') }}</th>
<th data-hide="audit">{{ __('Last Updated By') }}</th>
@can('manage access rights')
<th class="text-end" width="10%" data-orderable="false"
data-searchable="false">{{ __('Action') }}</th>
@endcan
</tr>
{{-- filter bar --}}
<tr class="filter-row">
@can('manage access rights')
<th></th>
@endcan
<th>
<select class="form-select form-select-sm">
<option value="">{{ __('All') }}</option>
<option value="active">{{ __('Active') }}</option>
<option value="inactive">{{ __('Inactive') }}</option>
</select>
</th>
<th><input class="form-control form-control-sm"
placeholder="{{ __('Search Role Name') }}"></th>
<th>
<select class="form-select form-select-sm">
<option value="">{{ __('All') }}</option>
<option value="web">web</option>
<option value="api">api</option>
</select>
</th>
<th><input class="form-control form-control-sm"
placeholder="{{ __('Search Permissions') }}"></th>
<th><input type="date" class="form-control form-control-sm"></th>
<th><input class="form-control form-control-sm"
placeholder="{{ __('Search User') }}"></th>
<th><input type="date" class="form-control form-control-sm"></th>
<th><input class="form-control form-control-sm"
placeholder="{{ __('Search User') }}"></th>
<th></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
{{-- add role modal --}}
<div class="modal fade" id="addRoleModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-permission">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{{ __('Add Role') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST" action="{{ route('roles.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 pt-3">
<div class="row gx-4">
<div class="col-lg-3 border-end">
{{-- Role Name --}}
<label class="form-label fw-semibold text-secondary small text-uppercase">
{{ __('Role Information') }}
</label>
<div class="mb-3">
<label class="form-label small mb-1">{{ __('Role Name') }} <span class="text-danger">*</span></label>
<input type="text" name="name" class="form-control"
placeholder="ex: administrator" required minlength="3"
maxlength="50" pattern="^[a-zA-Z0-9_\-]+$"
title="Minimum 3 characters (alphanumeric, dash, underscore)">
</div>
{{-- Guard --}}
<div class="mb-4">
<label class="form-label small mb-1">{{ __('Guard Name') }} <span class="text-danger">*</span></label>
<select name="guard_name" class="form-select" required>
<option value="web" selected>web (Frontend/Admin)</option>
<option value="api">api (Mobile/External)</option>
</select>
</div>
<div class="alert alert-info py-2 small border-0 bg-light-info">
<i class="bi bi-info-circle me-1"></i>
{{ __('Permissions assigned here will grant access to specific features.') }}
</div>
</div>
<div class="col-lg-9">
<label class="form-label fw-semibold text-secondary small text-uppercase mb-2">
{{ __('Assign Permissions') }}
</label>
@include('pages.access_control.partials.permission_dual_panel', [
'panelId' => 'add',
'rolePermIds' => [],
])
</div>
</div>
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-outline-dark rounded-pill"
data-bs-dismiss="modal">
&emsp;{{ __('Cancel') }}&emsp;
</button>
<button type="submit" class="btn btn-dark rounded-pill shadow-sm"
id="btn-save-role">
&emsp;{{ __('Save Changes') }}&emsp;
</button>
</div>
</form>
</div>
</div>
</div>
{{-- edit role modal --}}
<div class="modal fade" id="editRoleModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-permission">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{{ __('Update Role') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="editRoleForm" method="POST" autocomplete="off" class="ajax-form">
@csrf
@method('PUT')
<div class="modal-body pt-3">
<div class="row gx-4">
<div class="col-lg-3 border-end">
{{-- Role ID (hidden) --}}
<input type="hidden" id="edit-role-id" name="id">
<label class="form-label fw-semibold text-secondary small text-uppercase">
{{ __('Role Information') }}
</label>
<div class="mb-3">
<label class="form-label small mb-1">{{ __('Role Name') }} <span class="text-danger">*</span></label>
<input id="edit-role-name" name="name" type="text"
class="form-control"
placeholder="{{ __('ex: administrator') }}" required
minlength="3" maxlength="50" pattern="^[a-zA-Z0-9_\-]+$">
</div>
<div class="mb-4">
<label class="form-label small mb-1">{{ __('Guard Name') }} <span class="text-danger">*</span></label>
<select id="edit-role-guard" name="guard_name" class="form-select" required>
<option value="web">web (Frontend/Admin)</option>
<option value="api">api (Mobile/External)</option>
</select>
</div>
</div>
<div class="col-lg-9">
<label class="form-label fw-semibold text-secondary small text-uppercase mb-2">
{{ __('Assign Permissions') }}
</label>
@include('pages.access_control.partials.permission_dual_panel', [
'panelId' => 'edit',
'rolePermIds' => [],
])
</div>
</div>
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-outline-dark rounded-pill"
data-bs-dismiss="modal">
&emsp;{{ __('Close') }}&emsp;
</button>
<button type="submit" class="btn btn-dark rounded-pill shadow-sm">
&emsp;{{ __('Update Role') }}&emsp;
</button>
</div>
</form>
</div>
</div>
</div>
{{-- script handler (status, delete, edit data fill) --}}
<script>
document.addEventListener("DOMContentLoaded", () => {
// ── Dual-panel permission picker ────────────────────────────────
function dpPanel(panelEl) {
const availList = panelEl.querySelector('.dp-available-list');
const assignedList = panelEl.querySelector('.dp-assigned-list');
const availCount = panelEl.querySelector('.dp-available-count');
const assignCount = panelEl.querySelector('.dp-assigned-count');
let lastClicked = null; // for shift-click range
function refreshCounts() {
const av = availList.querySelectorAll('.dp-avail-item').length;
const as = assignedList.querySelectorAll('.dp-assigned-item').length;
if (availCount) availCount.textContent = av;
if (assignCount) {
assignCount.textContent = as;
assignCount.className = `badge dp-assigned-count rounded-pill ${as > 0 ? 'text-bg-success' : 'text-bg-secondary'}`;
assignCount.style.fontSize = '0.6rem';
}
const hint = assignedList.querySelector('.dp-empty-hint');
if (hint) hint.style.display = as > 0 ? 'none' : '';
}
// Ensure a category header exists in the Assigned panel for the given cat slug/label
function ensureAssignedCatHeader(cat, label) {
let h = assignedList.querySelector(`.dp-cat-header[data-cat="${cat}"]`);
if (!h) {
h = document.createElement('div');
h.className = 'dp-group-header dp-cat-header';
h.dataset.cat = cat;
h.textContent = label;
// Insert in same order as Available panel
const availHeader = availList.querySelector(`.dp-cat-header[data-cat="${cat}"]`);
const allAvailHeaders = [...availList.querySelectorAll('.dp-cat-header')];
const catIdx = allAvailHeaders.indexOf(availHeader);
const existingAssignedHeaders = [...assignedList.querySelectorAll('.dp-cat-header')];
let inserted = false;
for (const eh of existingAssignedHeaders) {
const ehIdx = allAvailHeaders.indexOf(availList.querySelector(`.dp-cat-header[data-cat="${eh.dataset.cat}"]`));
if (catIdx < ehIdx) {
assignedList.insertBefore(h, eh);
inserted = true;
break;
}
}
if (!inserted) assignedList.appendChild(h);
}
return h;
}
// Remove assigned cat header if no items remain in that category
function pruneAssignedCatHeader(cat) {
const remaining = assignedList.querySelectorAll(`.dp-assigned-item[data-cat="${cat}"]`).length;
if (remaining === 0) {
assignedList.querySelector(`.dp-cat-header[data-cat="${cat}"]`)?.remove();
}
}
function moveToAssigned(item) {
item.classList.remove('dp-avail-item', 'selected');
item.classList.add('dp-assigned-item');
item.querySelectorAll('.dp-hidden-input').forEach(i => i.remove());
const inp = Object.assign(document.createElement('input'), {
type: 'hidden', name: 'permissions[]',
value: item.dataset.id, className: 'dp-hidden-input'
});
item.appendChild(inp);
// Insert under the correct category header in Assigned
const cat = item.dataset.cat;
const availHeader = availList.querySelector(`.dp-cat-header[data-cat="${cat}"]`);
const label = availHeader ? availHeader.textContent.trim() : cat;
const catHeader = ensureAssignedCatHeader(cat, label);
// Append after last item of this cat, or right after header
const siblings = [...assignedList.querySelectorAll(`.dp-assigned-item[data-cat="${cat}"]`)];
if (siblings.length > 0) {
siblings[siblings.length - 1].after(item);
} else {
catHeader.after(item);
}
refreshCounts();
}
function moveToAvailable(item) {
const cat = item.dataset.cat;
item.classList.remove('dp-assigned-item', 'selected');
item.classList.add('dp-avail-item');
item.querySelectorAll('.dp-hidden-input').forEach(i => i.remove());
// Re-insert after the last sibling in its category group
const header = availList.querySelector(`.dp-cat-header[data-cat="${cat}"]`);
if (header) {
let last = header, next = header.nextElementSibling;
while (next && !next.classList.contains('dp-cat-header')) { last = next; next = next.nextElementSibling; }
last.after(item);
} else {
availList.appendChild(item);
}
pruneAssignedCatHeader(cat);
refreshCounts();
}
// Multi-select: click / Ctrl+click / Shift+click
panelEl.addEventListener('click', e => {
const item = e.target.closest('.dp-avail-item, .dp-assigned-item');
if (!item) return;
e.preventDefault();
const listEl = item.closest('.dp-panel-body');
const allItems = [...listEl.querySelectorAll('.dp-avail-item, .dp-assigned-item')].filter(i => !i.classList.contains('d-none'));
if (e.shiftKey && lastClicked && listEl.contains(lastClicked)) {
// Range select
const a = allItems.indexOf(lastClicked);
const b = allItems.indexOf(item);
const [lo, hi] = a < b ? [a, b] : [b, a];
allItems.forEach((it, idx) => {
if (idx >= lo && idx <= hi) it.classList.add('selected');
});
} else if (e.ctrlKey || e.metaKey) {
// Toggle individual
item.classList.toggle('selected');
} else {
// Single select
listEl.querySelectorAll('.dp-item').forEach(i => i.classList.remove('selected'));
item.classList.add('selected');
}
lastClicked = item;
});
// Double-click to move immediately
panelEl.addEventListener('dblclick', e => {
const avail = e.target.closest('.dp-avail-item');
const assigned = e.target.closest('.dp-assigned-item');
if (avail) { avail.classList.add('selected'); moveToAssigned(avail); }
if (assigned) { assigned.classList.add('selected'); moveToAvailable(assigned); }
});
// Buttons
panelEl.querySelector('.dp-btn-add-selected')?.addEventListener('click', () => {
[...availList.querySelectorAll('.dp-avail-item.selected')].forEach(moveToAssigned);
});
panelEl.querySelector('.dp-btn-add-all')?.addEventListener('click', () => {
[...availList.querySelectorAll('.dp-avail-item:not(.d-none)')].forEach(moveToAssigned);
});
panelEl.querySelector('.dp-btn-remove-selected')?.addEventListener('click', () => {
[...assignedList.querySelectorAll('.dp-assigned-item.selected')].forEach(moveToAvailable);
});
panelEl.querySelector('.dp-btn-remove-all')?.addEventListener('click', () => {
[...assignedList.querySelectorAll('.dp-assigned-item:not(.d-none)')].forEach(moveToAvailable);
});
// Search available
panelEl.querySelector('.dp-search-available')?.addEventListener('input', e => {
const term = e.target.value.toLowerCase().trim();
availList.querySelectorAll('.dp-avail-item').forEach(it => {
it.classList.toggle('d-none', !!term && !it.dataset.name.includes(term));
});
availList.querySelectorAll('.dp-cat-header').forEach(h => {
const vis = availList.querySelectorAll(`.dp-avail-item[data-cat="${h.dataset.cat}"]:not(.d-none)`).length;
h.classList.toggle('d-none', vis === 0);
});
});
// Search assigned
panelEl.querySelector('.dp-search-assigned')?.addEventListener('input', e => {
const term = e.target.value.toLowerCase().trim();
assignedList.querySelectorAll('.dp-assigned-item').forEach(it => {
it.classList.toggle('d-none', !!term && !it.dataset.name.includes(term));
});
assignedList.querySelectorAll('.dp-cat-header').forEach(h => {
const vis = assignedList.querySelectorAll(`.dp-assigned-item[data-cat="${h.dataset.cat}"]:not(.d-none)`).length;
h.classList.toggle('d-none', vis === 0);
});
});
// Init: move pre-selected items to assigned panel
availList.querySelectorAll('.dp-avail-item[data-preselected="1"]').forEach(moveToAssigned);
refreshCounts();
return { moveToAssigned, moveToAvailable, refreshCounts };
}
// Init both panels
document.querySelectorAll('[id^="dp-"]').forEach(el => dpPanel(el));
document.addEventListener("change", e => {
const toggle = e.target.closest(".role-toggle");
if (!toggle) {
return;
}
const id = toggle.dataset.id;
const name = toggle.dataset.name;
const status = toggle.checked ? "activate" : "deactivate";
StandardSwal.fire({
title: `${status === "activate" ? "Activate" : "Deactivate"} Role?`,
text: `You are about to change the access status for global role "${name}".`,
icon: "warning",
showCancelButton: true,
confirmButtonText: "Yes, Continue",
cancelButtonText: "Cancel",
}).then(result => {
if (!result.isConfirmed) {
toggle.checked = !toggle.checked;
return;
}
fetch("{{ route('roles.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) {
window.reloadDataTable?.();
StandardSwal.fire({
title: "{{ __('Success!') }}",
text: data.message || "{{ __('The role status has been updated successfully.') }}",
icon: "success",
timer: 2000,
showConfirmButton: false,
timerProgressBar: true
});
} else {
throw new Error();
}
})
.catch(() => {
toggle.checked = !toggle.checked;
StandardSwal.fire({
title: "{{ __('Error!') }}",
text: "{{ __('An unexpected error occurred while communicating with the server.') }}",
icon: "error"
});
});
});
});
document.addEventListener("click", e => {
// Filter Active/Archived
const filterBtn = e.target.closest(".filter-trashed-role");
if (filterBtn) {
document.querySelectorAll(".filter-trashed-role").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-role-val").value = filterBtn.dataset.value;
window.reloadDataTable?.();
return;
}
const editButton = e.target.closest(".btn-edit");
if (editButton) {
const url = `{{ route('roles.update', 'ID') }}`.replace("ID", editButton.dataset.id);
document.getElementById("editRoleForm").action = url;
document.getElementById("edit-role-id").value = editButton.dataset.id;
document.getElementById("edit-role-name").value = editButton.dataset.name;
document.getElementById("edit-role-guard").value = editButton.dataset.guard;
// Reset + reload dual panel for edit modal
const modal = document.getElementById('editRoleModal');
const editPanel = modal.querySelector('#dp-edit');
if (editPanel) {
const availList = editPanel.querySelector('.dp-available-list');
const assignedList = editPanel.querySelector('.dp-assigned-list');
const selected = JSON.parse(editButton.dataset.permissions || "[]");
// 1. Move all currently assigned items back to available, remove cat headers
[...assignedList.querySelectorAll('.dp-assigned-item')].forEach(item => {
item.classList.remove('dp-assigned-item', 'selected');
item.classList.add('dp-avail-item');
item.querySelectorAll('.dp-hidden-input').forEach(i => i.remove());
const cat = item.dataset.cat;
const header = availList.querySelector(`.dp-cat-header[data-cat="${cat}"]`);
if (header) {
let last = header, next = header.nextElementSibling;
while (next && !next.classList.contains('dp-cat-header')) { last = next; next = next.nextElementSibling; }
last.after(item);
} else {
availList.appendChild(item);
}
});
// Remove all assigned cat headers after reset
assignedList.querySelectorAll('.dp-cat-header').forEach(h => h.remove());
// 2. Move this role's permissions to assigned (with cat headers)
selected.forEach(permId => {
const item = availList.querySelector(`.dp-avail-item[data-id="${permId}"]`);
if (!item) return;
item.classList.remove('dp-avail-item', 'selected');
item.classList.add('dp-assigned-item');
item.querySelectorAll('.dp-hidden-input').forEach(i => i.remove());
const inp = Object.assign(document.createElement('input'), {
type: 'hidden', name: 'permissions[]',
value: permId, className: 'dp-hidden-input'
});
item.appendChild(inp);
// Insert under category header in assigned panel
const cat = item.dataset.cat;
const availHeader = availList.querySelector(`.dp-cat-header[data-cat="${cat}"]`);
const label = availHeader ? availHeader.textContent.trim() : cat;
let catHeader = assignedList.querySelector(`.dp-cat-header[data-cat="${cat}"]`);
if (!catHeader) {
catHeader = document.createElement('div');
catHeader.className = 'dp-group-header dp-cat-header';
catHeader.dataset.cat = cat;
catHeader.textContent = label;
// Order by Available panel order
const allAvailHeaders = [...availList.querySelectorAll('.dp-cat-header')];
const catIdx = allAvailHeaders.indexOf(availHeader);
const existingHeaders = [...assignedList.querySelectorAll('.dp-cat-header')];
let inserted = false;
for (const eh of existingHeaders) {
const ehIdx = allAvailHeaders.indexOf(availList.querySelector(`.dp-cat-header[data-cat="${eh.dataset.cat}"]`));
if (catIdx < ehIdx) { assignedList.insertBefore(catHeader, eh); inserted = true; break; }
}
if (!inserted) assignedList.appendChild(catHeader);
}
const siblings = [...assignedList.querySelectorAll(`.dp-assigned-item[data-cat="${cat}"]`)];
if (siblings.length > 0) siblings[siblings.length - 1].after(item);
else catHeader.after(item);
});
// 3. Refresh counts & empty hint
const av = availList.querySelectorAll('.dp-avail-item').length;
const as = assignedList.querySelectorAll('.dp-assigned-item').length;
const ac = editPanel.querySelector('.dp-available-count');
const sc = editPanel.querySelector('.dp-assigned-count');
if (ac) ac.textContent = av;
if (sc) {
sc.textContent = as;
sc.className = `badge dp-assigned-count rounded-pill ${as > 0 ? 'text-bg-success' : 'text-bg-secondary'}`;
sc.style.fontSize = '0.6rem';
}
const hint = assignedList.querySelector('.dp-empty-hint');
if (hint) hint.style.display = as > 0 ? 'none' : '';
}
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 Role?",
text: `"${name}" will be moved to the system archives.`,
icon: "warning",
showCancelButton: true,
confirmButtonText: "Yes, Archive",
cancelButtonText: "Cancel",
customClass: {
confirmButton: 'btn-pill-danger',
cancelButton: 'btn-pill-cancel'
}
}).then(result => {
if (!result.isConfirmed) return;
const url = "{{ route('roles.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 => {
if (!data.success) {
StandardSwal.fire({
title: "{{ __('Action Denied') }}",
text: data.message,
icon: "error"
});
return;
}
window.reloadDataTable?.();
StandardSwal.fire({
icon: "success",
title: "Archived Successfully!",
text: data.message || "The role has been added to the archives.",
timer: 2000,
showConfirmButton: false,
timerProgressBar: true
});
})
.catch(err => StandardSwal.fire({
title: "Error!",
text: "An error occurred during archiving. Please try again.",
icon: "error"
}));
});
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 Role?') }}",
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('roles.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 Role?') }}",
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('roles.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 => {
if (!data.success) {
StandardSwal.fire("{{ __('Action Denied') }}", data.message, "error");
return;
}
window.reloadDataTable?.();
StandardSwal.fire({
icon: "success",
title: "{{ __('Terminated!') }}",
text: data.message,
timer: 2000,
showConfirmButton: false,
timerProgressBar: true
});
});
});
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(".role-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} {{ __('roles.') }}`,
icon: "warning",
url: "",
method: "POST",
body: { ids: selectedIds }
};
switch(action) {
case 'activate':
config.url = "{{ route('roles.bulk-toggle-status') }}";
config.body.status = 'activate';
break;
case 'deactivate':
config.url = "{{ route('roles.bulk-toggle-status') }}";
config.body.status = 'deactivate';
break;
case 'archive':
config.url = "{{ route('roles.bulk-delete') }}";
config.icon = "error";
break;
case 'restore':
config.url = "{{ route('roles.bulk-restore') }}";
config.icon = "info";
break;
case 'terminate':
config.url = "{{ route('roles.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(".role-checkbox").forEach(cb => cb.checked = e.target.checked);
updateBulkBar();
}
if (e.target.classList.contains("role-checkbox")) {
updateBulkBar();
}
});
document.getElementById("clear-selection")?.addEventListener("click", () => {
document.querySelectorAll(".role-checkbox, .check-all").forEach(cb => cb.checked = false);
updateBulkBar();
});
function updateBulkBar() {
const checked = document.querySelectorAll(".role-checkbox:checked");
const bar = document.getElementById("bulk-action-bar");
const count = document.getElementById("selected-count");
const filterEl = document.getElementById("filter-trashed-role-val");
if (!bar || !count) return;
const filterType = filterEl ? filterEl.value : 'active';
if (checked.length > 0) {
bar.classList.remove("d-none");
count.textContent = checked.length;
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', () => {
const checkAll = document.querySelector(".check-all");
if (checkAll) checkAll.checked = false;
updateBulkBar();
});
});
</script>
</div>
</div>
</div>
</div>
{{-- ====================================================================
RBAC DOCUMENTATION (MODAL)
==================================================================== --}}
<div class="modal fade" id="rbacDocsModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-fullscreen-lg-down modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content rounded-4 border-0 shadow-lg overflow-hidden">
<div class="modal-header p-0 border-0" style="background:#0f172a;">
<div class="d-flex w-100 align-items-center justify-content-between p-4 p-lg-5 text-white">
<div>
<div class="extra-small opacity-50 fw-bold mb-2" style="letter-spacing:1.5px;">REFERENCE GUIDE</div>
<h3 class="fw-black mb-2" style="letter-spacing:-1px;">{{ __('Roles & Permissions Guide') }}</h3>
<p class="mb-0 opacity-75" style="font-size:.9rem;line-height:1.7;max-width:720px;">
How role-based access control (RBAC) works in this system
how roles, permissions, and users fit together,
and how to grant or revoke access without locking yourself out.
</p>
</div>
<div class="d-flex align-items-start gap-3">
<i class="bi bi-shield-lock-fill display-3 opacity-25 d-none d-md-inline"></i>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
</div>
<div class="modal-body p-0">
<div class="p-4 p-lg-5">
{{-- MENTAL MODEL --}}
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
<i class="bi bi-diagram-3 text-primary me-1"></i>The mental model
</h6>
<div class="p-4 rounded-4 mb-5" style="background:#f8fafc;border:1px solid #e2e8f0;">
<div class="row text-center align-items-center g-3">
<div class="col-md-3">
<div class="p-3 rounded-4 bg-white shadow-sm">
<i class="bi bi-person-fill text-primary display-6"></i>
<div class="fw-bold mt-2">USER</div>
<div class="small text-muted">A person who logs in</div>
</div>
</div>
<div class="col-md-1 d-none d-md-block">
<i class="bi bi-arrow-right fs-2 text-muted"></i>
</div>
<div class="col-md-3">
<div class="p-3 rounded-4 bg-white shadow-sm">
<i class="bi bi-people-fill text-warning display-6"></i>
<div class="fw-bold mt-2">ROLE</div>
<div class="small text-muted">A named bundle (e.g. <em>Admin</em>)</div>
</div>
</div>
<div class="col-md-1 d-none d-md-block">
<i class="bi bi-arrow-right fs-2 text-muted"></i>
</div>
<div class="col-md-3">
<div class="p-3 rounded-4 bg-white shadow-sm">
<i class="bi bi-key-fill text-success display-6"></i>
<div class="fw-bold mt-2">PERMISSION</div>
<div class="small text-muted">One specific action</div>
</div>
</div>
</div>
<p class="small text-muted text-center mb-0 mt-4" style="line-height:1.7;">
A <span class="fw-bold">user</span> is given one or more <span class="fw-bold">roles</span>.
Each <span class="fw-bold">role</span> contains a set of <span class="fw-bold">permissions</span>.
A user can do an action if <span class="fw-bold">any</span> of their roles grants the matching permission.
</p>
</div>
{{-- CONCEPTS --}}
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
<i class="bi bi-bookmark-fill text-info me-1"></i>Core concepts
</h6>
<div class="row g-3 mb-5">
@php
$concepts = [
['icon'=>'bi-people-fill','color'=>'#f59e0b','title'=>'Role','desc'=>'A named container — e.g. <code>Super Admin</code>, <code>Manager</code>, <code>Staff</code>. Roles are <span class="fw-bold">human-readable groupings</span> meant to mirror real positions in your organisation.'],
['icon'=>'bi-key-fill','color'=>'#22c55e','title'=>'Permission','desc'=>'A single capability — e.g. <code>view dashboard</code>, <code>manage users</code>. Always phrased as <span class="fw-bold">verb + object</span> (view / manage / create / delete + noun).'],
['icon'=>'bi-shield-fill','color'=>'#3b82f6','title'=>'Guard','desc'=>'The authentication context. <code>web</code> = browser session (this UI). <code>api</code> = token-based mobile/API clients. A role / permission belongs to exactly one guard.'],
['icon'=>'bi-archive-fill','color'=>'#94a3b8','title'=>'Archive','desc'=>'Soft-delete. Archived roles disappear from assignment dropdowns but their history is preserved. Users still holding an archived role lose its permissions until restored.'],
];
@endphp
@foreach($concepts as $c)
<div class="col-md-6 col-lg-3">
<div class="p-3 rounded-4 h-100" style="background:#fff;border:1px solid #e2e8f0;">
<div class="rounded-3 d-inline-flex align-items-center justify-content-center mb-2"
style="background:{{ $c['color'] }}1a;color:{{ $c['color'] }};width:42px;height:42px;">
<i class="{{ $c['icon'] }} fs-5"></i>
</div>
<div class="fw-bold text-dark mb-1">{{ $c['title'] }}</div>
<div class="small text-muted" style="line-height:1.6;">{!! $c['desc'] !!}</div>
</div>
</div>
@endforeach
</div>
{{-- VIEW vs MANAGE --}}
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
<i class="bi bi-eye-fill text-primary me-1"></i><code>view</code> vs <code>manage</code> · the most important convention
</h6>
<div class="row g-3 mb-5">
<div class="col-md-6">
<div class="p-4 rounded-4 h-100" style="background:#eff6ff;border:1px solid #bfdbfe;">
<div class="d-flex align-items-center gap-2 mb-2">
<i class="bi bi-eye text-primary fs-4"></i>
<code class="text-primary fw-bold fs-6">view &lt;module&gt;</code>
</div>
<div class="small text-muted mb-2" style="line-height:1.7;">Lets the user <span class="fw-bold">open the page</span> and read data. Read-only.</div>
<div class="extra-small text-muted">
<span class="fw-bold text-dark">Examples:</span>
<code>view dashboard</code>, <code>view health and logs</code>, <code>view action history</code>
</div>
</div>
</div>
<div class="col-md-6">
<div class="p-4 rounded-4 h-100" style="background:#fef2f2;border:1px solid #fecaca;">
<div class="d-flex align-items-center gap-2 mb-2">
<i class="bi bi-pencil-square text-danger fs-4"></i>
<code class="text-danger fw-bold fs-6">manage &lt;module&gt;</code>
</div>
<div class="small text-muted mb-2" style="line-height:1.7;">Lets the user <span class="fw-bold">create, edit, delete</span> within that module. Implies <code>view</code>.</div>
<div class="extra-small text-muted">
<span class="fw-bold text-dark">Examples:</span>
<code>manage users</code>, <code>manage access rights</code>, <code>manage backup and storage</code>
</div>
</div>
</div>
<div class="col-12">
<div class="d-flex align-items-start gap-2 p-3 rounded-3" style="background:#fffbeb;border:1px solid #fde68a;">
<i class="bi bi-lightbulb-fill text-warning"></i>
<div class="small text-muted">
<span class="fw-bold text-dark">Rule of thumb:</span> grant <code>view</code> liberally, grant <code>manage</code> sparingly.
For modules a role doesn't touch at all, don't assign either keeps the audit log clean.
</div>
</div>
</div>
</div>
{{-- LIFECYCLE --}}
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
<i class="bi bi-arrow-repeat text-success me-1"></i>Day-to-day workflow
</h6>
<div class="row g-3 mb-5">
@php
$flows = [
['icon'=>'bi-plus-circle','color'=>'#22c55e','title'=>'Create a role','desc'=>'Click <span class="fw-bold">+ Add Role</span>. Give it a name (e.g. <em>Finance Manager</em>), pick a guard (usually <code>web</code>), and tick the permissions it should grant.'],
['icon'=>'bi-person-plus','color'=>'#3b82f6','title'=>'Assign to user','desc'=>'Go to <span class="fw-bold">User Directory</span> → edit a user → assign one or more roles. Changes take effect immediately on next request.'],
['icon'=>'bi-sliders','color'=>'#8b5cf6','title'=>'Tune permissions','desc'=>'Edit the role to add/remove permissions. Every user holding that role inherits the change instantly — no need to touch each user.'],
['icon'=>'bi-archive','color'=>'#f59e0b','title'=>'Archive (soft-delete)','desc'=>'Archive removes the role from assignment dropdowns but keeps history. Use this when phasing out a position. Users keep working but lose the permissions.'],
['icon'=>'bi-arrow-counterclockwise','color'=>'#06b6d4','title'=>'Restore','desc'=>'Filter to <span class="fw-bold">Arsip</span>, find the role, click restore. Users who used to hold it regain their permissions.'],
['icon'=>'bi-x-circle','color'=>'#ef4444','title'=>'Hapus Permanen','desc'=>'Only available in the archive view. <span class="fw-bold text-danger">Irreversible</span>. Removes the role record and unassigns every user. Use with caution.'],
];
@endphp
@foreach($flows as $f)
<div class="col-md-6 col-lg-4">
<div class="p-3 rounded-4 h-100" style="background:#f8fafc;border:1px solid #e2e8f0;">
<div class="d-flex align-items-center gap-2 mb-2">
<div class="rounded-3 d-flex align-items-center justify-content-center flex-shrink-0"
style="background:{{ $f['color'] }}1a;color:{{ $f['color'] }};width:36px;height:36px;">
<i class="{{ $f['icon'] }}"></i>
</div>
<div class="fw-bold text-dark">{{ $f['title'] }}</div>
</div>
<div class="small text-muted" style="line-height:1.6;">{!! $f['desc'] !!}</div>
</div>
</div>
@endforeach
</div>
{{-- SAFETY WARNING --}}
<div class="d-flex align-items-start gap-3 p-4 rounded-4 mb-5" style="background:#fef2f2;border:2px solid #fecaca;">
<i class="bi bi-exclamation-triangle-fill text-danger fs-2"></i>
<div>
<div class="fw-black text-danger mb-1">DON'T LOCK YOURSELF OUT</div>
<div class="small text-dark" style="line-height:1.7;">
Always keep <span class="fw-bold">at least one active user</span> with the <code>manage access rights</code> permission.
If you accidentally remove this permission from every active role, <span class="fw-bold text-danger">no one can log in and fix it through the UI</span> —
recovery requires direct database edit or a CLI artisan command.
</div>
<div class="small text-muted mt-2">
<span class="fw-bold">Tip:</span> create a dedicated <em>Super Admin</em> role with every permission and assign it to at least two trusted accounts. Never archive it.
</div>
</div>
</div>
{{-- COMMON PATTERNS --}}
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
<i class="bi bi-layers-fill text-info me-1"></i>Recommended role patterns
</h6>
<div class="table-responsive mb-5">
<table class="table table-hover align-middle mb-0" style="background:#f8fafc;border-radius:12px;overflow:hidden;">
<thead style="background:#e2e8f0;">
<tr>
<th class="ps-3">ROLE</th>
<th>WHO</th>
<th>TYPICAL PERMISSIONS</th>
</tr>
</thead>
<tbody>
<tr>
<td class="ps-3 fw-bold text-dark"><i class="bi bi-shield-fill-check text-danger me-1"></i>Super Admin</td>
<td class="small text-muted">Owner / lead developer</td>
<td class="small"><span class="badge bg-danger-subtle text-danger rounded-pill">All permissions</span></td>
</tr>
<tr>
<td class="ps-3 fw-bold text-dark"><i class="bi bi-shield-fill text-warning me-1"></i>Admin</td>
<td class="small text-muted">Operations lead, head of IT</td>
<td class="small text-muted">Everything except <code>manage backup and storage</code>, <code>manage maintenance mode</code></td>
</tr>
<tr>
<td class="ps-3 fw-bold text-dark"><i class="bi bi-person-workspace text-primary me-1"></i>Manager</td>
<td class="small text-muted">Department / team lead</td>
<td class="small text-muted"><code>view *</code> + <code>manage users</code> (for their team), no infra permissions</td>
</tr>
<tr>
<td class="ps-3 fw-bold text-dark"><i class="bi bi-person-fill text-success me-1"></i>Staff</td>
<td class="small text-muted">End user / operator</td>
<td class="small text-muted"><code>view dashboard</code>, module-specific viewers, nothing destructive</td>
</tr>
<tr>
<td class="ps-3 fw-bold text-dark"><i class="bi bi-eye-fill text-secondary me-1"></i>Auditor</td>
<td class="small text-muted">Compliance / external audit</td>
<td class="small text-muted"><code>view *</code> only — including <code>view action history</code></td>
</tr>
</tbody>
</table>
</div>
{{-- TROUBLESHOOTING --}}
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
<i class="bi bi-wrench-adjustable-circle text-secondary me-1"></i>Troubleshooting
</h6>
<div class="table-responsive mb-5">
<table class="table table-hover align-middle mb-0" style="background:#f8fafc;border-radius:12px;overflow:hidden;">
<thead style="background:#e2e8f0;">
<tr>
<th class="ps-3">SYMPTOM</th>
<th>LIKELY CAUSE</th>
<th>FIX</th>
</tr>
</thead>
<tbody>
<tr>
<td class="ps-3 fw-bold text-dark">User gets 403 on a menu they should see</td>
<td class="small text-muted">Missing matching <code>view &lt;module&gt;</code> permission</td>
<td class="small text-muted">Edit role → tick the relevant <code>view</code> permission</td>
</tr>
<tr>
<td class="ps-3 fw-bold text-dark">User sees menu but action button missing</td>
<td class="small text-muted">Has <code>view</code> but not <code>manage</code></td>
<td class="small text-muted">Edit role → add the <code>manage</code> counterpart</td>
</tr>
<tr>
<td class="ps-3 fw-bold text-dark">Permission change not taking effect</td>
<td class="small text-muted">Permission cache stale</td>
<td class="small text-muted">Run <code>php artisan permission:cache-reset</code> or re-login</td>
</tr>
<tr>
<td class="ps-3 fw-bold text-dark">User has role but still blocked</td>
<td class="small text-muted">User itself is <span class="fw-bold">inactive / archived</span></td>
<td class="small text-muted">User Directory → restore / activate the user</td>
</tr>
<tr>
<td class="ps-3 fw-bold text-dark">"You don't have permission" everywhere</td>
<td class="small text-muted">All roles got <code>manage access rights</code> stripped</td>
<td class="small text-muted">CLI rescue: <code>php artisan permission:grant {user} "manage access rights"</code></td>
</tr>
<tr>
<td class="ps-3 fw-bold text-dark">Two users with same role behave differently</td>
<td class="small text-muted">One has <span class="fw-bold">additional</span> roles attached</td>
<td class="small text-muted">Permissions are <span class="fw-bold">union</span> across roles — check both users' role lists</td>
</tr>
</tbody>
</table>
</div>
{{-- BEST PRACTICES --}}
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
<i class="bi bi-stars text-success me-1"></i>Best practices
</h6>
<div class="row g-2 mb-4">
@php
$tips = [
['icon'=>'bi-1-circle-fill','title'=>'Least privilege','desc'=>'Start each new role with <span class="fw-bold">nothing</span> ticked, then add only what is truly needed. Easier than starting from "everything" and trying to remember what to remove.'],
['icon'=>'bi-2-circle-fill','title'=>'One role per real-world position','desc'=>'Mirror your org chart. <em>Finance Manager</em>, <em>Warehouse Staff</em> — not <em>Role A</em>, <em>Custom 1</em>. Future-you will thank you.'],
['icon'=>'bi-3-circle-fill','title'=>'Keep Super Admin small','desc'=>'Only 12 people should have full <code>manage access rights</code>. Everyone else gets scoped roles.'],
['icon'=>'bi-4-circle-fill','title'=>'Audit periodically','desc'=>'Every quarter, open this page → scroll the role list → ask "does each role still match how the team works?" Archive stale ones.'],
['icon'=>'bi-5-circle-fill','title'=>'Don\'t reuse a guard','desc'=>'A role with <code>web</code> guard cannot be assigned to API users and vice versa. Decide before creating.'],
['icon'=>'bi-6-circle-fill','title'=>'Document non-obvious permissions','desc'=>'If a permission name doesn\'t make its scope obvious, write a short note in the description field when adding it.'],
];
@endphp
@foreach($tips as $tip)
<div class="col-md-6">
<div class="d-flex align-items-start gap-2 p-3 rounded-3" style="background:#f0fdf4;border:1px solid #bbf7d0;">
<i class="{{ $tip['icon'] }} text-success fs-4"></i>
<div>
<div class="fw-bold text-dark mb-1" style="font-size:.85rem;">{{ $tip['title'] }}</div>
<div class="small text-muted" style="line-height:1.6;">{!! $tip['desc'] !!}</div>
</div>
</div>
</div>
@endforeach
</div>
{{-- FOOTER NOTE --}}
<div class="d-flex align-items-center gap-3 p-3 rounded-4" style="background:#eff6ff;border:1px solid #bfdbfe;">
<i class="bi bi-info-circle-fill text-primary fs-3"></i>
<div>
<div class="fw-bold text-dark mb-1" style="font-size:.85rem;">Implementation note</div>
<div class="small text-muted">
This system uses <span class="fw-bold">Spatie Laravel Permission</span>. Roles and permissions are stored in the <code>roles</code>, <code>permissions</code>, <code>role_has_permissions</code>, and <code>model_has_roles</code> tables. Permission checks happen on every request via <code>@@can('...')</code> in Blade and <code>$user->can('...')</code> in controllers / middleware.
</div>
</div>
</div>
</div>
</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>