390 lines
15 KiB
PHTML
390 lines
15 KiB
PHTML
<?php
|
|
use MintyPHP\Router;
|
|
|
|
$activeTenant = trim((string) ($_GET['tenant'] ?? ''));
|
|
$activeRoles = array_filter(array_map('trim', explode(',', (string) ($_GET['roles'] ?? ''))));
|
|
$activeDepartments = array_filter(array_map('trim', explode(',', (string) ($_GET['departments'] ?? ''))));
|
|
$canDeleteUsers = can('users.delete');
|
|
$canUpdateUsers = can('users.update');
|
|
$canUpdateSelf = can('users.self_update');
|
|
$currentUserUuid = (string) ($_SESSION['user']['uuid'] ?? '');
|
|
$showTenantTabs = isset($tenants) && is_array($tenants) && count($tenants) > 1;
|
|
?>
|
|
<div class="app-breadcrumb">
|
|
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><?php e(t('Users')); ?>
|
|
</div>
|
|
<div class="app-list-titlebar">
|
|
<h1><?php e(t('Users')); ?></h1>
|
|
<div class="app-list-titlebar-actions">
|
|
<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 ($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-toolbar-toggle data-toolbar-target="#users-toolbar, #users-tabs"
|
|
data-toolbar-text-show="<?php e(t('Show filters')); ?>"
|
|
data-toolbar-text-hide="<?php e(t('Hide filters')); ?>">
|
|
<span data-toolbar-label>
|
|
<?php e(t('Hide filters')); ?>
|
|
</span>
|
|
</button>
|
|
</li>
|
|
<li dir="ltr">
|
|
<button class="transparent" type="button" data-users-reset>
|
|
<?php e(t('Reset filters')); ?>
|
|
</button>
|
|
</li>
|
|
<li dir="ltr">
|
|
<button class="transparent" type="button" data-users-export>
|
|
<?php e(t('Export CSV')); ?>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</details>
|
|
|
|
<?php if (can('users.create')): ?>
|
|
<a role="button" class="primary" href="admin/users/create">
|
|
<i class="bi bi-plus"></i> <?php e(t('Create user')); ?>
|
|
</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php if ($showTenantTabs): ?>
|
|
<div class="app-list-tabs" id="users-tabs">
|
|
<a href="admin/users" class="<?php e($activeTenant === '' ? 'tab-link is-active' : 'tab-link'); ?>">
|
|
<?php e(t('All')); ?>
|
|
</a>
|
|
<?php foreach ($tenants ?? [] as $tenant): ?>
|
|
<?php if (!empty($tenant['uuid'])): ?>
|
|
<?php $isActive = $activeTenant === $tenant['uuid']; ?>
|
|
<a href="admin/users?tenant=<?php e($tenant['uuid']); ?>" class="<?php e($isActive ? 'tab-link is-active' : 'tab-link'); ?>">
|
|
<?php e($tenant['description'] ?? ''); ?>
|
|
</a>
|
|
<?php endif; ?>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="app-list-toolbar" id="users-toolbar">
|
|
<input type="hidden" id="user-tenant-filter" value="">
|
|
<label class="app-field">
|
|
<span><?php e(t('Search')); ?></span>
|
|
<input type="search" id="user-search" placeholder="<?php e(t('Search...')); ?>">
|
|
</label>
|
|
<label class="app-field">
|
|
<span><?php e(t('Status')); ?></span>
|
|
<select id="user-status-filter">
|
|
<option value="all"><?php e(t('All')); ?></option>
|
|
<option value="active"><?php e(t('Active')); ?></option>
|
|
<option value="inactive"><?php e(t('Inactive')); ?></option>
|
|
</select>
|
|
</label>
|
|
<label class="app-field">
|
|
<span><?php e(t('Email verified')); ?></span>
|
|
<select id="user-email-verified-filter">
|
|
<option value="all"><?php e(t('All')); ?></option>
|
|
<option value="verified"><?php e(t('Verified')); ?></option>
|
|
<option value="unverified"><?php e(t('Unverified')); ?></option>
|
|
</select>
|
|
</label>
|
|
<?php multiSelectFilter('user-department-filter', 'Departments', 'Select departments', $departments ?? [], $activeDepartments); ?>
|
|
<?php multiSelectFilter('user-role-filter', 'Roles', 'Select roles', $roles ?? [], $activeRoles); ?>
|
|
<label class="app-field">
|
|
<span><?php e(t('Created from')); ?></span>
|
|
<input type="date" id="user-created-from">
|
|
</label>
|
|
<label class="app-field">
|
|
<span><?php e(t('Created to')); ?></span>
|
|
<input type="date" id="user-created-to">
|
|
</label>
|
|
</div>
|
|
<div class="app-list-table">
|
|
<div id="users-grid"></div>
|
|
</div>
|
|
|
|
|
|
<?php $gridFactoryVersion = @filemtime('web/js/core/app-grid-factory.js') ?: time(); ?>
|
|
<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 { initToolbarToggles } from "<?php e(asset('js/components/app-toggle-toolbar.js')); ?>";
|
|
import { createServerGrid } from "<?php e(asset('js/core/app-grid-factory.js?v=' . $gridFactoryVersion)); ?>";
|
|
import { initUsersListPage } from "<?php e(asset('js/pages/app-users-list.js')); ?>";
|
|
import { bindBulkVisibility } from "<?php e(asset('js/components/app-bulk-selection.js')); ?>";
|
|
import { buildUrl, badgeHtml } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
|
|
import { showAsyncFlash, withLoading } from "<?php e(asset('js/components/app-async-flash.js')); ?>";
|
|
|
|
initToolbarToggles();
|
|
|
|
const appBase = "<?php e(localeBase()); ?>";
|
|
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')); ?>",
|
|
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?')); ?>",
|
|
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 normalizeCommaSeparated = (value) => {
|
|
if (Array.isArray(value)) return value;
|
|
if (typeof value === 'string') {
|
|
return value.split(',').map((item) => item.trim()).filter(Boolean);
|
|
}
|
|
return [];
|
|
};
|
|
|
|
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 escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
}[char]));
|
|
const buildBadgeList = (items) => {
|
|
let list = items;
|
|
let primary = '';
|
|
if (items && typeof items === 'object' && !Array.isArray(items)) {
|
|
list = Array.isArray(items.items) ? items.items : [];
|
|
primary = String(items.primary ?? '');
|
|
}
|
|
if (!Array.isArray(list) || list.length === 0) return '';
|
|
const badges = list.map((label) => {
|
|
const isPrimary = primary !== '' && label === primary;
|
|
const variant = isPrimary ? 'info' : 'neutral';
|
|
const tooltip = isPrimary ? ` data-tooltip="${escapeHtml(labels.primaryTenant)}" data-tooltip-pos="top"` : '';
|
|
return `<span class="badge" data-variant="${variant}"${tooltip}>${escapeHtml(label)}</span>`;
|
|
}).join(' ');
|
|
return `<div class="badge-list">${badges}</div>`;
|
|
};
|
|
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)) },
|
|
{ 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: 'ID', hidden: true }
|
|
];
|
|
|
|
const gridConfig = createServerGrid({
|
|
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', 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.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: {
|
|
input: '#user-search',
|
|
param: 'search',
|
|
debounce: 250
|
|
},
|
|
filters: [
|
|
{
|
|
input: '#user-status-filter',
|
|
param: 'active',
|
|
default: 'all',
|
|
normalize: (value) => (value && value !== 'all' ? value : '')
|
|
},
|
|
{
|
|
input: '#user-email-verified-filter',
|
|
param: 'email_verified',
|
|
default: 'all',
|
|
normalize: (value) => (value && value !== 'all' ? value : '')
|
|
},
|
|
{
|
|
input: '#user-department-filter',
|
|
param: 'departments',
|
|
default: [],
|
|
normalize: normalizeCommaSeparated
|
|
},
|
|
{
|
|
input: '#user-role-filter',
|
|
param: 'roles',
|
|
default: [],
|
|
normalize: normalizeCommaSeparated
|
|
},
|
|
{
|
|
input: '#user-tenant-filter',
|
|
param: 'tenant',
|
|
default: '',
|
|
normalize: (value) => value || ''
|
|
},
|
|
{
|
|
input: '#user-created-from',
|
|
param: 'created_from',
|
|
normalize: (value) => (value ? value : '')
|
|
},
|
|
{
|
|
input: '#user-created-to',
|
|
param: 'created_to',
|
|
normalize: (value) => (value ? value : '')
|
|
}
|
|
],
|
|
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() : '';
|
|
}
|
|
}
|
|
});
|
|
|
|
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,
|
|
showFlash: showAsyncFlash,
|
|
withLoading
|
|
});
|
|
bindBulkVisibility({
|
|
containerSelector: '#users-grid',
|
|
buttonSelector: '[data-users-bulk]'
|
|
});
|
|
</script>
|