feat: add resources and view components
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
<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">
|
||||
|
||||
{{-- permission 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">{{ __('Permission Management') }}</h5>
|
||||
<small class="text-muted">
|
||||
{{ __('Manage permissions, define access rules, and control user capabilities within the system.') }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<a href="{{ route('roles') }}#rbac-docs"
|
||||
class="btn btn-outline-dark px-3 rounded-pill d-flex align-items-center gap-2">
|
||||
<i class="bi bi-book"></i> {{ __('Documentation') }}
|
||||
</a>
|
||||
@can('manage access rights')
|
||||
<button class="btn btn-primary px-3" data-bs-toggle="modal"
|
||||
data-bs-target="#addPermissionModal">
|
||||
 + {{ __('Add Permission') }} 
|
||||
</button>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Quick reference banner --}}
|
||||
<div class="d-flex align-items-start gap-3 p-3 rounded-4 mb-3" style="background:#eff6ff;border:1px solid #bfdbfe;">
|
||||
<i class="bi bi-info-circle-fill text-primary fs-4"></i>
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-bold text-dark mb-1" style="font-size:.88rem;">{{ __('Permissions are the atomic building blocks') }}</div>
|
||||
<div class="small text-muted" style="line-height:1.6;">
|
||||
{{ __('Each permission represents ONE specific action (e.g.') }} <code>view dashboard</code>,
|
||||
<code>manage users</code>). {{ __('You assign permissions to') }} <span class="fw-bold">{{ __('roles') }}</span>,
|
||||
{{ __('then assign roles to') }} <span class="fw-bold">{{ __('users') }}</span>.
|
||||
<a href="{{ route('roles') }}#rbac-docs" class="text-primary fw-bold text-decoration-none">{{ __('Read the full RBAC guide →') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- permissions table --}}
|
||||
<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('permissions') }}"
|
||||
data-order='@json([[4, "desc"]])'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-wrap">{{ __('Status') }}</th>
|
||||
<th class="text-wrap">{{ __('Permission Name') }}</th>
|
||||
<th class="text-wrap">{{ __('Module / Guard') }}</th>
|
||||
<th class="text-wrap">{{ __('Assigned To (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 access rights')
|
||||
<th class="text-end text-wrap" data-orderable="false"
|
||||
data-searchable="false">{{ __('Action') }}</th>
|
||||
@endcan
|
||||
</tr>
|
||||
|
||||
{{-- filter bar --}}
|
||||
<tr class="filter-row">
|
||||
<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 Permission 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 Assigned Roles') }}"></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 permission modal --}}
|
||||
<div class="modal fade" id="addPermissionModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content rounded-3">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ __('Add Permission') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="POST" action="{{ route('permissions.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">
|
||||
|
||||
{{-- Permission Name --}}
|
||||
<label class="form-label fw-semibold">
|
||||
{{ __('Permission Name') }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="text" name="name" class="form-control mb-3"
|
||||
placeholder="ex: view_report / edit_data" required minlength="3"
|
||||
maxlength="100" pattern="^[a-zA-Z0-9_\-\.\/]+$"
|
||||
title="Minimum 3 characters. Allowed: letters, numbers, dash, underscore, dot, and slash">
|
||||
|
||||
{{-- Module / Guard --}}
|
||||
<label class="form-label fw-semibold">
|
||||
{{ __('Module / Guard') }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select name="guard_name" class="form-select mb-3" required
|
||||
title="Select guard for this permission">
|
||||
<option value="web" selected>web</option>
|
||||
<option value="api">api</option>
|
||||
</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 
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- edit permission modal --}}
|
||||
<div class="modal fade" id="editPermissionModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content rounded-3">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ __('Edit Permission') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form id="editPermissionForm" 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">
|
||||
|
||||
{{-- hidden id --}}
|
||||
<input type="hidden" id="edit-permission-id" name="id">
|
||||
|
||||
{{-- Permission Name --}}
|
||||
<label class="form-label fw-semibold">
|
||||
{{ __('Permission Name') }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input id="edit-permission-name" name="name" type="text"
|
||||
class="form-control mb-3" placeholder="ex: view_report / edit_data" required
|
||||
minlength="3" maxlength="100" pattern="^[a-zA-Z0-9_\-\.\/]+$"
|
||||
title="Minimum 3 characters. Allowed: letters, numbers, dash, underscore, dot, and slash">
|
||||
|
||||
{{-- Module / Guard --}}
|
||||
<label class="form-label fw-semibold">
|
||||
{{ __('Module / Guard') }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select id="edit-permission-guard" name="guard_name" class="form-select mb-3"
|
||||
required title="Select guard for this permission">
|
||||
<option value="web">web</option>
|
||||
<option value="api">api</option>
|
||||
</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-primary rounded-pill">
|
||||
 Update 
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- script handler (status, delete, edit data fill) --}}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
// =========================
|
||||
// TOGGLE STATUS (SERVER-DRIVEN)
|
||||
// =========================
|
||||
document.addEventListener("change", e => {
|
||||
const toggle = e.target.closest(".permission-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"} Permission?`,
|
||||
text: `You are about to change the system access rights for "${name}".`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, Continue",
|
||||
cancelButtonText: "Cancel",
|
||||
}).then(result => {
|
||||
if (!result.isConfirmed) {
|
||||
toggle.checked = !toggle.checked;
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("{{ route('permissions.toggle-status') }}", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": document.querySelector(
|
||||
'meta[name="csrf-token"]').content,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
status
|
||||
}),
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Live Reload immediately
|
||||
window.reloadDataTable?.();
|
||||
|
||||
StandardSwal.fire({
|
||||
title: "{{ __('Success!') }}",
|
||||
text: data.message || "{{ __('The permission status has been updated successfully.') }}",
|
||||
icon: "success",
|
||||
timer: 2000,
|
||||
showConfirmButton: false,
|
||||
timerProgressBar: true
|
||||
});
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
StandardSwal.fire({
|
||||
title: "{{ __('Error!') }}",
|
||||
text: "{{ __('A server error occurred while updating the permission.') }}",
|
||||
icon: "error"
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// =========================
|
||||
// FILL EDIT MODAL
|
||||
// =========================
|
||||
const form = document.getElementById("editPermissionForm");
|
||||
|
||||
document.addEventListener("click", e => {
|
||||
const editButton = e.target.closest(".btn-edit");
|
||||
|
||||
if (editButton) {
|
||||
const updateRoute =
|
||||
`{{ route('permissions.update', 'PERMISSION_ID') }}`
|
||||
.replace("PERMISSION_ID", editButton.dataset.id);
|
||||
|
||||
form.action = updateRoute;
|
||||
|
||||
document.getElementById("edit-permission-id").value = editButton
|
||||
.dataset.id ?? "";
|
||||
document.getElementById("edit-permission-name").value = editButton
|
||||
.dataset.name ?? "";
|
||||
document.getElementById("edit-permission-guard").value = editButton
|
||||
.dataset.guard ?? "";
|
||||
return;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// DELETE / ARCHIVE
|
||||
// =========================
|
||||
const deleteButton = e.target.closest(".btn-delete");
|
||||
|
||||
if (!deleteButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = deleteButton.dataset.id;
|
||||
const name = deleteButton.dataset.name;
|
||||
|
||||
StandardSwal.fire({
|
||||
title: "Archive Permission?",
|
||||
text: `"${name}" will be deactivated and moved to global 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('permissions.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 => {
|
||||
// Live Reload immediately
|
||||
window.reloadDataTable?.();
|
||||
|
||||
StandardSwal.fire({
|
||||
icon: "success",
|
||||
title: "Archived Successfully!",
|
||||
text: data.message || "The permission has been archived.",
|
||||
timer: 2000,
|
||||
showConfirmButton: false,
|
||||
timerProgressBar: true
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
StandardSwal.fire({
|
||||
title: "Error!",
|
||||
text: "An error occurred during the archive process.",
|
||||
icon: "error"
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
Reference in New Issue
Block a user