Files
breadcrumb-the-shire/pages/admin/users/index(default).phtml
2026-03-04 15:56:58 +01:00

341 lines
12 KiB
PHTML

<?php
use MintyPHP\Router;
use MintyPHP\Session;
$toolbarFilterSchema = is_array($toolbarFilterSchema ?? null) ? $toolbarFilterSchema : [];
$searchToolbarFilterSchema = is_array($searchToolbarFilterSchema ?? null) ? $searchToolbarFilterSchema : [];
$drawerToolbarFilterSchema = is_array($drawerToolbarFilterSchema ?? null) ? $drawerToolbarFilterSchema : [];
$toolbarFilterState = is_array($toolbarFilterState ?? null) ? $toolbarFilterState : [];
$toolbarOptionSets = is_array($toolbarOptionSets ?? null) ? $toolbarOptionSets : [];
$clientFilterSchema = is_array($clientFilterSchema ?? null) ? $clientFilterSchema : [];
$filterChipMeta = is_array($filterChipMeta ?? null) ? $filterChipMeta : [];
$searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
$activeTenant = (string) ($toolbarFilterState['tenant'] ?? '');
$pageAuth = is_array($viewAuth['page'] ?? null) ? $viewAuth['page'] : [];
$canDeleteUsers = (bool) ($pageAuth['can_delete_users'] ?? false);
$canUpdateUsers = (bool) ($pageAuth['can_update_users'] ?? false);
$canAccessPdf = (bool) ($pageAuth['can_access_pdf'] ?? false);
$canUpdateSelf = (bool) ($pageAuth['can_update_self'] ?? false);
$canCreateUser = (bool) ($pageAuth['can_create_user'] ?? false);
$currentUserUuid = (string) ($_SESSION['user']['uuid'] ?? '');
$showTenantTabs = isset($tenants) && is_array($tenants) && count($tenants) > 1;
?>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Users')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<?php
$listTitle = t('Users');
ob_start();
?>
<button type="button" class="secondary outline" data-users-bulk="activate" hidden>
<i class="bi bi-check-circle"></i> <?php e(t('Activate')); ?>
</button>
<button type="button" class="secondary outline" data-users-bulk="deactivate" hidden>
<i class="bi bi-ban"></i> <?php e(t('Deactivate')); ?>
</button>
<?php if ($canUpdateUsers): ?>
<button type="button" class="secondary outline" data-users-bulk="send-access" hidden>
<i class="bi bi-envelope"></i> <?php e(t('Send access')); ?>
</button>
<?php endif; ?>
<?php if ($canAccessPdf): ?>
<button type="button" class="secondary outline" data-users-bulk="access-pdf" hidden>
<i class="bi bi-file-earmark-pdf"></i> <?php e(t('Download access PDFs')); ?>
</button>
<?php endif; ?>
<?php if ($canDeleteUsers): ?>
<button type="button" class="danger outline" data-users-bulk="delete" hidden>
<i class="bi bi-trash3-fill"></i> <?php e(t('Delete')); ?>
</button>
<?php endif; ?>
<details class="dropdown" data-tooltip="<?php e(t("Actions")); ?>" data-tooltip-pos="top">
<summary role="button" class="outline secondary"><i class="bi bi-three-dots"></i></summary>
<ul dir="rtl">
<li dir="ltr">
<button class="transparent" type="button" data-users-export>
<?php e(t('Export CSV')); ?>
</button>
</li>
</ul>
</details>
<?php if ($canCreateUser): ?>
<a role="button" class="primary" href="admin/users/create">
<i class="bi bi-plus"></i> <?php e(t('Create user')); ?>
</a>
<?php endif; ?>
<?php
$listTitleActionsHtml = ob_get_clean();
require templatePath('partials/app-list-titlebar.phtml');
?>
<?php if ($showTenantTabs): ?>
<?php
$listTabsId = 'users-tabs';
$listTabsItems = [
[
'href' => 'admin/users',
'label' => t('All'),
'active' => $activeTenant === '',
],
];
foreach ($tenants ?? [] as $tenant) {
$tenantUuid = (string) ($tenant['uuid'] ?? '');
if ($tenantUuid === '') {
continue;
}
$listTabsItems[] = [
'href' => 'admin/users?tenant=' . rawurlencode($tenantUuid),
'label' => (string) ($tenant['description'] ?? ''),
'active' => $activeTenant === $tenantUuid,
];
}
require templatePath('partials/app-list-tabs.phtml');
?>
<?php endif; ?>
<?php
$filterUiNamespace = 'users';
require templatePath('partials/app-list-filters.phtml');
?>
<div class="app-list-table">
<div id="users-grid"></div>
</div>
<?php if ($canAccessPdf): ?>
<form id="users-access-pdf-bulk-form" method="post" action="admin/users/access-pdf-bulk" hidden>
<?php Session::getCsrfInput(); ?>
<input type="hidden" name="uuids" value="">
</form>
<?php endif; ?>
<script src="<?php e(asset('vendor/gridjs/gridjs.umd.js')); ?>"></script>
<script src="<?php e(asset('vendor/gridjs/plugins/selection/selection.umd.js')); ?>"></script>
<script type="module">
import { initStandardListPage } from "<?php e(assetVersion('js/core/app-grid-factory.js')); ?>";
import { initUsersListPage } from "<?php e(assetVersion('js/pages/app-users-list.js')); ?>";
import { bindBulkVisibility } from "<?php e(assetVersion('js/components/app-bulk-selection.js')); ?>";
import { buildUrl, badgeHtml, getAppBase, escapeHtml, buildBadgeList } from "<?php e(assetVersion('js/pages/app-list-utils.js')); ?>";
import { showAsyncFlash, withLoading } from "<?php e(assetVersion('js/components/app-async-flash.js')); ?>";
const appBase = getAppBase();
const gridSearch = <?php gridJsonForJs($searchConfig); ?>;
const filterSchema = <?php gridJsonForJs($clientFilterSchema); ?>;
const usersFilterChipMeta = <?php gridJsonForJs($filterChipMeta); ?>;
const currentUserUuid = "<?php e($currentUserUuid); ?>";
const canUpdateUsers = <?php e($canUpdateUsers ? 'true' : 'false'); ?>;
const canUpdateSelf = <?php e($canUpdateSelf ? 'true' : 'false'); ?>;
const csrf = <?php MintyPHP\Buffer::get('grid_csrf'); ?>;
const labels = {
active: "<?php e(t('Active')); ?>",
inactive: "<?php e(t('Inactive')); ?>",
never: "<?php e(t('Never')); ?>",
bulkActivateConfirm: "<?php e(t('Activate users?')); ?>",
bulkDeactivateConfirm: "<?php e(t('Deactivate users?')); ?>",
bulkDeleteConfirm: "<?php e(t('Delete users?')); ?>",
bulkSendAccessConfirm: "<?php e(t('Send access emails to selected users?')); ?>",
bulkAccessPdfConfirm: "<?php e(t('Generate access PDFs for selected users?')); ?>",
primaryTenant: "<?php e(t('Primary tenant')); ?>",
bulkMessages: {
'activate': {
success: "<?php e(t('%d users activated')); ?>",
error: "<?php e(t('Failed to activate users')); ?>"
},
'deactivate': {
success: "<?php e(t('%d users deactivated')); ?>",
error: "<?php e(t('Failed to deactivate users')); ?>"
},
'delete': {
success: "<?php e(t('%d users deleted')); ?>",
error: "<?php e(t('Failed to delete users')); ?>"
},
'send-access': {
success: "<?php e(t('Access emails sent to %d users')); ?>",
partial: "<?php e(t('Access emails sent to %d users, %f failed')); ?>",
error: "<?php e(t('Failed to send access emails')); ?>"
}
}
};
const buildBulkUrl = (action) => buildUrl(appBase, `admin/users/bulk/${action}`);
const initUsersGrid = () => {
const rowSelection = window.gridjs?.plugins?.selection?.RowSelection;
if (!rowSelection) {
throw new Error('Grid.js RowSelection plugin is not available.');
}
const activeIndex = 6;
const uuidIndex = 2;
const canEditRow = (row) => {
const uuid = row?.cells?.[uuidIndex]?.data;
if (!uuid) return false;
if (uuid === currentUserUuid) {
return canUpdateUsers || canUpdateSelf;
}
return canUpdateUsers;
};
const initialsForRow = (row) => {
const first = row?.cells?.[3]?.data ?? '';
const last = row?.cells?.[4]?.data ?? '';
const parts = [first, last]
.map((value) => String(value || '').trim())
.filter(Boolean);
const chars = parts.map((value) => value[0] || '').join('').toUpperCase();
return chars || '?';
};
const columns = [
{
name: "<?php e(t('Avatar')); ?>",
sort: false,
formatter: (cell, row) => {
if (!cell) {
const initials = escapeHtml(initialsForRow(row));
return gridjs.html(`<span class="grid-avatar grid-avatar-placeholder">${initials}</span>`);
}
const src = new URL(`admin/users/avatar-file?uuid=${cell}&size=64`, appBase).toString();
const full = new URL(`admin/users/avatar-file?uuid=${cell}&size=256`, appBase).toString();
return gridjs.html(`<a data-fslightbox="user-avatars" href="${full}"><img class="grid-avatar" src="${src}" alt="" loading="lazy"></a>`);
}
},
{ name: 'UUID', hidden: true },
{ name: "<?php e(t('First name')); ?>", sort: true },
{ name: "<?php e(t('Last name')); ?>", sort: true },
{ name: "<?php e(t('Email')); ?>", sort: true },
{
name: "<?php e(t('State')); ?>",
sort: true,
formatter: (cell) => cell
? gridjs.html(`<span class="badge" data-variant="success">${labels.active}</span>`)
: gridjs.html(`<span class="badge" data-variant="danger">${labels.inactive}</span>`)
},
{
name: "<?php e(t('Tenants')); ?>",
sort: false,
formatter: (cell) => gridjs.html(buildBadgeList(cell, { primaryTooltip: labels.primaryTenant }))
},
{ name: "<?php e(t('Departments')); ?>", sort: false, formatter: (cell) => gridjs.html(buildBadgeList(cell)) },
{ name: "<?php e(t('Roles')); ?>", sort: false, formatter: (cell) => gridjs.html(buildBadgeList(cell)) },
{ name: "<?php e(t('Phone')); ?>", sort: false },
{ name: "<?php e(t('Mobile')); ?>", sort: false },
{ name: "<?php e(t('Short dial')); ?>", sort: false },
{ name: "<?php e(t('Created')); ?>", sort: true, formatter: (cell) => badgeHtml(gridjs, cell) },
{ name: "<?php e(t('Modified')); ?>", sort: true, formatter: (cell) => badgeHtml(gridjs, cell) },
{
name: "<?php e(t('Last login')); ?>",
sort: true,
formatter: (cell) => badgeHtml(gridjs, cell || labels.never)
},
{ name: 'ID', hidden: true }
];
const gridOptions = {
gridjs: window.gridjs,
container: '#users-grid',
dataUrl: 'admin/users/data',
appBase,
columns,
sortColumns: [null, null, 'first_name', 'last_name', 'email', 'active', null, null, null, null, null, null, 'created', 'modified', 'last_login_at', null],
paginationLimit: 10,
language: <?php MintyPHP\Buffer::get('grid_lang'); ?>,
mapData: (data) => data.data.map((row) => [
row.has_avatar ? row.uuid : '',
row.uuid,
row.first_name,
row.last_name,
row.email,
row.active,
row.tenants,
row.departments,
row.roles,
row.phone,
row.mobile,
row.short_dial,
row.created,
row.modified,
row.last_login,
row.id
]),
selection: {
enabled: true,
id: 'selectRow',
component: rowSelection,
selectAllLabel: "<?php e(t('Select all')); ?>",
props: {
id: (row) => row.cell(uuidIndex).data
},
getSelectedIds: ({ container }) => {
if (!container) return [];
return Array.from(container.querySelectorAll('.gridjs-tr-selected'))
.map((row) => row.getAttribute('data-uuid'))
.filter(Boolean);
}
},
search: gridSearch,
filterSchema,
urlSync: true,
rowDataset: (row) => ({
uuid: row?.cells?.[uuidIndex]?.data
}),
rowDblClick: {
getUrl: (rowData) => {
if (!canEditRow(rowData)) {
return '';
}
const uuid = rowData?.cells?.[uuidIndex]?.data;
return uuid ? new URL(`admin/users/edit/${uuid}`, appBase).toString() : '';
}
}
};
const { gridConfig } = initStandardListPage({
grid: gridOptions,
filters: {
mode: 'drawer',
chipMeta: usersFilterChipMeta,
watchInputs: ['#user-search'],
preserveFilterParamsOnReset: ['tenant'],
drawer: {
countExcludeParams: ['tenant']
},
clearMetaOptions: {
preserveFilterParams: ['tenant']
}
}
});
if (!gridConfig || !gridConfig.grid) {
console.warn('Users grid init failed');
return null;
}
return { ...gridConfig, indices: { activeIndex, uuidIndex } };
};
const gridConfig = initUsersGrid();
initUsersListPage({
gridConfig,
appBase,
csrf,
labels,
buildBulkUrl,
bulkDownloadForms: {
'access-pdf': '#users-access-pdf-bulk-form'
},
resetOptions: {
resetPage: true,
urlHistoryMode: 'push',
preserveFilterParams: ['tenant']
},
showFlash: showAsyncFlash,
withLoading
});
bindBulkVisibility({
containerSelector: '#users-grid',
buttonSelector: '[data-users-bulk]'
});
</script>