refactor: streamline user permission management

Move per-user permissions into row actions, add contextual module permission guides, show selected-user details, and relocate online sessions to the profile menu.
This commit is contained in:
DevSoftTech Server 2026-08-09 23:45:35 +05:30
parent c263b9e4ac
commit 8f25904a13
3 changed files with 159 additions and 50 deletions

View File

@ -11,14 +11,6 @@
<i class="bi bi-person-badge"></i>
Role Master
</button>
<button class="btn btn-outline-primary" type="button" onclick="load_form_div('<?= base_url('setting/admin/user-management/permissions') ?>','maindiv','Additional Permissions');">
<i class="bi bi-shield-lock"></i>
Additional Permissions
</button>
<button class="btn btn-outline-success" type="button" onclick="load_form_div('<?= base_url('setting/admin/user-management/sessions') ?>','maindiv','Active Sessions');">
<i class="bi bi-person-check"></i>
Who is Online
</button>
</div>
</div>
<div class="card-body">
@ -106,10 +98,19 @@
</td>
<td>
<div class="d-flex gap-1">
<button type="button" class="btn btn-sm btn-outline-primary" onclick="load_form_div('<?= base_url('setting/admin/user-management/edit/' . (int) ($user->id ?? 0)) ?>','maindiv','Edit User');">Edit</button>
<button type="button" class="btn btn-sm btn-outline-warning" onclick="load_form_div('<?= base_url('setting/admin/user-management/reset-password/' . (int) ($user->id ?? 0)) ?>','maindiv','Reset Password');">Reset Password</button>
<button type="button" class="btn btn-sm btn-outline-primary" title="Edit user" aria-label="Edit user" onclick="load_form_div('<?= base_url('setting/admin/user-management/edit/' . (int) ($user->id ?? 0)) ?>','maindiv','Edit User');">
<i class="bi bi-pencil-square" aria-hidden="true"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-warning" title="Reset password" aria-label="Reset password" onclick="load_form_div('<?= base_url('setting/admin/user-management/reset-password/' . (int) ($user->id ?? 0)) ?>','maindiv','Reset Password');">
<i class="bi bi-key" aria-hidden="true"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-info" title="Additional permissions" aria-label="Additional permissions" onclick="load_form_div('<?= base_url('setting/admin/user-management/permissions?user_id=' . (int) ($user->id ?? 0)) ?>','maindiv','Additional Permissions');">
<i class="bi bi-shield-lock" aria-hidden="true"></i>
</button>
<?php if ((int) ($user->id ?? 0) !== (int) (auth()->user()->id ?? 0)) : ?>
<button type="button" class="btn btn-sm btn-outline-danger btn-delete-user" data-user-id="<?= (int) ($user->id ?? 0) ?>" data-username="<?= esc((string) ($user->username ?? ''), 'attr') ?>">Delete</button>
<button type="button" class="btn btn-sm btn-outline-danger btn-delete-user" title="Delete user" aria-label="Delete user" data-user-id="<?= (int) ($user->id ?? 0) ?>" data-username="<?= esc((string) ($user->username ?? ''), 'attr') ?>">
<i class="bi bi-trash" aria-hidden="true"></i>
</button>
<?php endif ?>
</div>
</td>

View File

@ -24,32 +24,49 @@
<?php endforeach ?>
</div>
<?php endif ?>
<?php
$selectedEmail = '';
$selectedPersonName = '';
$selectedPhoneNo = '';
if (! empty($selectedUser)) {
$selectedIdentity = $selectedUser->getEmailIdentity();
$selectedEmail = trim((string) ($selectedIdentity->secret ?? ''));
$selectedExtra = $selectedIdentity->extra ?? [];
if (is_string($selectedExtra) && trim($selectedExtra) !== '') {
$decodedExtra = json_decode($selectedExtra, true);
$selectedExtra = is_array($decodedExtra) ? $decodedExtra : [];
}
if (is_array($selectedExtra)) {
$selectedPersonName = trim((string) ($selectedExtra['full_name'] ?? ''));
$selectedPhoneNo = trim((string) ($selectedExtra['phone_no'] ?? ''));
}
}
?>
<form class="needs-validation" novalidate action="<?= base_url('setting/admin/user-management/permissions') ?>" method="post">
<?= csrf_field() ?>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label" for="user_id">User</label>
<select class="form-select" id="user_id" name="user_id" required>
<option value="">Select user</option>
<?php if (! empty($users)) : ?>
<?php foreach ($users as $user) : ?>
<?php
$emailIdentity = $user->getEmailIdentity();
$email = $emailIdentity ? $emailIdentity->secret : '';
$label = trim(($user->username ?? '') . ($email ? ' (' . $email . ')' : ''));
?>
<option value="<?= esc($user->id) ?>" <?= ! empty($selectedUser) && (int) $selectedUser->id === (int) $user->id ? 'selected' : '' ?>>
<?= esc($label) ?>
</option>
<?php endforeach ?>
<?php endif ?>
</select>
<div class="invalid-feedback">User is required.</div>
<input type="hidden" name="user_id" value="<?= ! empty($selectedUser) ? (int) $selectedUser->id : '' ?>">
<?php if (! empty($selectedUser)) : ?>
<div class="border rounded p-3 mb-3 bg-light">
<div class="d-flex align-items-center gap-2 mb-2">
<i class="bi bi-person-badge fs-4 text-primary"></i>
<div>
<div class="fw-bold"><?= esc($selectedPersonName !== '' ? $selectedPersonName : (string) ($selectedUser->username ?? 'User')) ?></div>
<div class="text-muted small">Selected user for additional permissions</div>
</div>
</div>
<div class="row g-2 small">
<div class="col-md-4"><strong>Login ID:</strong> <?= esc((string) ($selectedUser->username ?? '-')) ?></div>
<div class="col-md-4"><strong>User ID:</strong> <?= (int) $selectedUser->id ?></div>
<div class="col-md-4"><strong>Status:</strong> <?= ! empty($selectedUser->active) ? 'Active' : 'Inactive' ?></div>
<div class="col-md-4"><strong>Email:</strong> <?= esc($selectedEmail !== '' ? $selectedEmail : '-') ?></div>
<div class="col-md-4"><strong>Phone:</strong> <?= esc($selectedPhoneNo !== '' ? $selectedPhoneNo : '-') ?></div>
<div class="col-md-4"><strong>Role:</strong> <?= esc(! empty($selectedRoleTitles) ? implode(', ', $selectedRoleTitles) : 'No role') ?></div>
</div>
</div>
<div class="col-md-6 d-flex align-items-end">
<button class="btn btn-outline-secondary" type="button" id="loadPermissions">Load Permissions</button>
</div>
</div>
<?php else : ?>
<div class="alert alert-warning">No user was selected. Return to the user list and choose the permissions action for a user.</div>
<?php endif ?>
<div class="mt-4">
<?php if (! empty($selectedUser)) : ?>
@ -61,9 +78,6 @@
<?php endif ?>
<label class="form-label">Direct grants</label>
<div class="text-muted small mb-2">Checked boxes are additional permissions saved specifically for this user. Unchecking one does not remove access inherited from the role.</div>
<div class="alert alert-info py-2 mb-2">
Pharmacy module access is controlled by <strong>pharmacy.access</strong>. Editing past Walk-in/Registered pharmacy invoices requires <strong>pharmacy.invoice.edit-old</strong> (or <strong>pharmacy.invoice.admin</strong>). Discount caps can be granted with <strong>pharmacy.invoice.discount.10</strong>, <strong>pharmacy.invoice.discount.20</strong>, <strong>pharmacy.invoice.discount.30</strong>, or full override using <strong>pharmacy.invoice.admin</strong>. Base user discount can be set in hospital setting key <strong>PHARMACY_NORMAL_MAX_DISCOUNT_PERCENT</strong>.
</div>
<div class="border rounded p-3 bg-light">
<?php
$selectedPermissions = [];
@ -85,8 +99,19 @@
'billing.opd' => 'OPD',
'billing.charges' => 'Charges',
'billing.ipd' => 'IPD Billing',
'billing' => 'Billing',
'billing.access' => 'Billing Access',
'billing.patient' => 'Patient Billing',
'billing.payment_request' => 'Payment Requests',
'billing.refund' => 'Refunds',
'billing.items' => 'Billing Items',
'billing.packages' => 'Billing Packages',
'abdm' => 'ABDM',
'diagnosis' => 'Diagnosis',
'doctor_work' => 'Doctor Work',
'hospital_stock' => 'Hospital Stock',
'media' => 'Media',
'reports' => 'Reports',
'settings' => 'Settings',
'other' => 'Other',
];
foreach ($permissions as $permissionKey => $permissionLabel) {
@ -134,13 +159,45 @@
$groupOrder[] = $detectedGroup;
}
}
$groupHelp = [
'admin' => 'Controls access to the administrative area and core application settings. Grant only to trusted system administrators.',
'users' => 'Controls user-account lifecycle actions such as creating, editing, deleting, and managing privileged users.',
'template' => 'Controls access to clinical, diagnostic, discharge, document, and print-template configuration areas.',
'pharmacy' => 'Controls pharmacy access, invoice administration, old-invoice editing, purchase status changes, and user discount limits. The base discount is configured by hospital setting PHARMACY_NORMAL_MAX_DISCOUNT_PERCENT.',
'finance' => 'Controls Accounts and Finance workflows including cash submission, verification, bank reconciliation, compliance, vendors, purchase orders, GRNs, invoices, and payouts.',
'opd' => 'Controls access to the OPD doctor panel used for clinical work on outpatient visits.',
'billing.access' => 'Provides the main entry permission for billing workflows. More specific billing permissions below restrict individual operations.',
'billing.patient' => 'Controls exceptional patient billing actions, including editing a patient name after the normal time restriction.',
'billing.opd' => 'Controls OPD registration editing and payment confirmation actions.',
'billing.charges' => 'Controls charges invoice viewing, editing, date changes, payment, cancellation, and correction actions.',
'billing.ipd' => 'Controls IPD billing access, admissions, invoices, cash reports, bill printing, discharge edits, status management, and exports.',
'billing.payment_request' => 'Controls viewing and processing of organization payment requests.',
'billing.refund' => 'Controls viewing and processing of billing refund requests.',
'billing.items' => 'Controls access to billing item masters and permission to maintain them.',
'billing.packages' => 'Controls access to billing package masters and permission to maintain them.',
'abdm' => 'Controls ABDM access, ABHA operations, task board, gateway actions, and Bridge communication logs.',
'diagnosis' => 'Controls entry to the diagnosis module and access to diagnosis reports.',
'doctor_work' => 'Controls doctor workflows including appointments, Rx groups, medicines, advice, clinical templates, and immunization records.',
'hospital_stock' => 'Controls hospital stock masters, indents, approvals, issues, purchases, receiving, reports, and alerts.',
'media' => 'Controls optional image preparation actions such as rotating or cropping before upload.',
'reports' => 'Controls access to operational, collection, insurance, NABH audit, billing, and document issue reports.',
'settings' => 'Controls access to operational settings such as bed management and charges configuration.',
'beta' => 'Provides access to features designated for beta testing before general availability.',
'other' => 'Contains permissions that do not yet belong to a dedicated module heading.',
];
?>
<?php if (! empty($permissions)) : ?>
<div class="row g-3">
<?php foreach ($groupOrder as $groupKey) : ?>
<?php if (empty($groupedPermissions[$groupKey])) { continue; } ?>
<div class="col-12 col-md-6 col-lg-4">
<div class="fw-bold mb-2"><?= esc($groupTitles[$groupKey] ?? $groupKey) ?></div>
<div class="d-flex align-items-center gap-1 mb-2">
<div class="fw-bold"><?= esc($groupTitles[$groupKey] ?? $groupKey) ?></div>
<button type="button" class="btn btn-sm p-0 border-0 text-primary permission-help" data-bs-toggle="modal" data-bs-target="#permissionHelpModal" data-group-key="<?= esc($groupKey, 'attr') ?>" title="About <?= esc($groupTitles[$groupKey] ?? $groupKey, 'attr') ?> permissions" aria-label="About <?= esc($groupTitles[$groupKey] ?? $groupKey, 'attr') ?> permissions">
<i class="bi bi-info-circle" aria-hidden="true"></i>
</button>
</div>
<?php foreach ($groupedPermissions[$groupKey] as $permissionKey => $permissionLabel) : ?>
<div class="form-check mb-1">
<input class="form-check-input" type="checkbox" id="perm_<?= esc($permissionKey) ?>" name="permissions[]" value="<?= esc($permissionKey) ?>" <?= in_array($permissionKey, $selectedPermissions, true) ? 'checked' : '' ?>>
@ -169,23 +226,61 @@
</div>
</div>
<div class="modal fade" id="permissionHelpModal" tabindex="-1" aria-labelledby="permissionHelpModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="permissionHelpModalLabel">Permission Guide</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p id="permissionHelpSummary" class="mb-3"></p>
<h6>Permissions in this section</h6>
<div id="permissionHelpList" class="list-group list-group-flush"></div>
</div>
</div>
</div>
</div>
<script>
(function() {
var loadButton = document.getElementById('loadPermissions');
var userSelect = document.getElementById('user_id');
var form = document.querySelector('form[action="<?= base_url('setting/admin/user-management/permissions') ?>"]');
var moduleGuides = <?= json_encode(array_reduce(array_keys($groupedPermissions), static function (array $guides, string $groupKey) use ($groupedPermissions, $groupTitles, $groupHelp): array {
$permissionRows = [];
foreach ($groupedPermissions[$groupKey] as $permissionKey => $permissionLabel) {
$permissionRows[] = ['key' => $permissionKey, 'label' => $permissionLabel];
}
$guides[$groupKey] = [
'title' => ($groupTitles[$groupKey] ?? $groupKey) . ' Permission Guide',
'summary' => $groupHelp[$groupKey] ?? 'Review these permissions before granting additional access for this module.',
'permissions' => $permissionRows,
];
return $guides;
}, []), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
if (!loadButton || !userSelect) {
return;
}
$(document).off('click.userPermissionsLoad', '#loadPermissions').on('click.userPermissionsLoad', '#loadPermissions', function() {
var userId = userSelect.value;
if (!userId) {
$(document).off('show.bs.modal.permissionHelp', '#permissionHelpModal').on('show.bs.modal.permissionHelp', '#permissionHelpModal', function(event) {
var button = event.relatedTarget;
var groupKey = button ? button.getAttribute('data-group-key') : '';
var guide = moduleGuides[groupKey];
if (!guide) {
return;
}
var url = '<?= base_url('setting/admin/user-management/permissions') ?>' + '?user_id=' + encodeURIComponent(userId);
load_form_div(url, 'maindiv', 'Give Permission');
$('#permissionHelpModalLabel').text(guide.title);
$('#permissionHelpSummary').text(guide.summary);
var list = document.getElementById('permissionHelpList');
list.replaceChildren();
guide.permissions.forEach(function(permission) {
var item = document.createElement('div');
item.className = 'list-group-item px-0';
var key = document.createElement('code');
key.className = 'd-block mb-1';
key.textContent = permission.key;
var label = document.createElement('div');
label.textContent = permission.label;
item.append(key, label);
list.appendChild(item);
});
});
if (!form || !window.jQuery) {

View File

@ -7,6 +7,7 @@
$hdrCanIpdBilling = false;
$hdrCanDoctorWork = false;
$hdrCanPharmacy = false;
$hdrCanManageSessions = false;
if ($authUser && method_exists($authUser, 'can')) {
$hdrCanBilling = $authUser->can('billing.access')
|| $authUser->can('billing.opd.edit')
@ -32,6 +33,9 @@
|| $authUser->can('doctor_work.*');
$hdrCanPharmacy = $authUser->can('pharmacy.access')
|| $authUser->can('billing.access');
$hdrCanManageSessions = $authUser->can('users.edit')
|| $authUser->can('users.manage-admins')
|| $authUser->can('users.*');
}
if ($authUser && method_exists($authUser, 'inGroup')) {
$inAdminGroup = $authUser->inGroup('superadmin', 'admin', 'developer');
@ -39,6 +43,7 @@
if (! $hdrCanIpdBilling) { $hdrCanIpdBilling = $inAdminGroup; }
if (! $hdrCanDoctorWork) { $hdrCanDoctorWork = $inAdminGroup; }
if (! $hdrCanPharmacy) { $hdrCanPharmacy = $inAdminGroup; }
if (! $hdrCanManageSessions) { $hdrCanManageSessions = $inAdminGroup; }
}
$loginId = trim((string) ($authUser->username ?? ''));
@ -166,6 +171,14 @@
<span>My Profile</span>
</a>
</li>
<?php if ($hdrCanManageSessions) { ?>
<li>
<a class="dropdown-item d-flex align-items-center" href="javascript:load_form('<?= base_url('setting/admin/user-management/sessions') ?>','Who is Online');">
<i class="bi bi-person-check"></i>
<span>Who is Online</span>
</a>
</li>
<?php } ?>
<li>
<hr class="dropdown-divider">
</li>