2026-02-04 23:31:53 +01:00
|
|
|
<?php
|
|
|
|
|
use MintyPHP\Router;
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
use MintyPHP\Session;
|
2026-03-11 23:32:11 +01:00
|
|
|
use MintyPHP\Http\Request;
|
2026-02-04 23:31:53 +01:00
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$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);
|
2026-02-04 23:31:53 +01:00
|
|
|
$currentUserUuid = (string) ($_SESSION['user']['uuid'] ?? '');
|
|
|
|
|
$showTenantTabs = isset($tenants) && is_array($tenants) && count($tenants) > 1;
|
|
|
|
|
?>
|
2026-02-11 19:28:12 +01:00
|
|
|
<?php
|
2026-03-04 15:56:58 +01:00
|
|
|
$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; ?>
|
refactor(admin/users): migrate CSV export to core primitive
Replace the inline export implementation with the new generic primitive
(core/Service/Export + helpers/export + app-list-export.js). No more
hand-rolled fputcsv loop, header setup, or escape closure duplicated
here — one implementation, tested once, reused everywhere.
- export().php: uses exportRequireGetRequest, exportCapLimit,
exportResolveFlavor, ExportColumn[], exportSendCsv. Phone/mobile/
short_dial keep allowSignedNumeric=true so +49 numbers render
untouched.
- export(none).phtml: deleted — headers + fputcsv now live in the
core helper.
- index(default).phtml: inline <details class="dropdown"> "Export CSV"
replaced by the reusable app-list-export-dropdown partial, exposing
CSV and Excel flavors. exportUrl added to pageConfig.
- app-users-list.js: hand-rolled export-button binding removed in
favor of initListExport({ gridConfig, exportUrl }).
- admin-users-index.js: forwards config.exportUrl into
initUsersListPage.
Net effect: ~100 lines of duplicated export plumbing removed from the
users page; users gain the Excel flavor for free.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:57:31 +02:00
|
|
|
<?php require templatePath('partials/app-list-export-dropdown.phtml'); ?>
|
2026-02-04 23:31:53 +01:00
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
<?php if ($canCreateUser): ?>
|
2026-03-12 16:47:53 +01:00
|
|
|
<a role="button" class="app-action-success" href="<?php e(requestPathWithReturnTarget('admin/users/create', Request::pathWithQuery())); ?>">
|
2026-03-04 15:56:58 +01:00
|
|
|
<i class="bi bi-plus"></i> <?php e(t('Create user')); ?>
|
|
|
|
|
</a>
|
2026-02-04 23:31:53 +01:00
|
|
|
<?php endif; ?>
|
2026-03-04 15:56:58 +01:00
|
|
|
<?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;
|
|
|
|
|
}
|
2026-02-11 19:28:12 +01:00
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$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');
|
|
|
|
|
?>
|
2026-02-04 23:31:53 +01:00
|
|
|
<div class="app-list-table">
|
|
|
|
|
<div id="users-grid"></div>
|
|
|
|
|
</div>
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
<?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; ?>
|
2026-02-04 23:31:53 +01:00
|
|
|
|
|
|
|
|
|
2026-03-05 08:26:51 +01:00
|
|
|
<?php
|
|
|
|
|
$gridLang = json_decode(appBufferValue('grid_lang'), true);
|
|
|
|
|
if (!is_array($gridLang)) {
|
|
|
|
|
$gridLang = [];
|
|
|
|
|
}
|
|
|
|
|
$gridCsrf = json_decode(appBufferValue('grid_csrf'), true);
|
|
|
|
|
if (!is_array($gridCsrf)) {
|
|
|
|
|
$gridCsrf = [];
|
|
|
|
|
}
|
|
|
|
|
$pageConfig = [
|
|
|
|
|
'gridSearch' => $searchConfig,
|
|
|
|
|
'filterSchema' => $clientFilterSchema,
|
|
|
|
|
'filterChipMeta' => $filterChipMeta,
|
feat(core): endpointUrl() helper — query-safe URLs for module routes
Introduce a helper that returns the registered route TARGET for a given
source path, so URLs built for endpoints that carry query parameters do
not rely on MintyPHP's applyRoutes() rewrite layer — that layer compares
the full request URI (including `?query`) against source paths and
silently misses modules where path ≠ target.
- core/Support/helpers/app.php: endpointUrl($path) consults
ModuleRegistry::getRoutes(), returns lurl(target) when the path is a
registered module source path, otherwise falls through to lurl($path).
Safe fallback when the container or registry is unavailable.
- modules/audit/pages/audit/system-audit/index(default).phtml: replace
the ad-hoc target-path workaround with endpointUrl('admin/system-audit/
export'). Callers can now write the natural source path without
knowing about the rewrite trap.
- Apply the same helper to modules/helpdesk/.../domains/index and
pages/admin/users/index for consistency — a no-op where path already
equals target, but establishes the convention: every export/data
endpoint URL in page configs goes through endpointUrl().
- tests/Support/Helpers/EndpointUrlTest: 5 cases covering source→target
resolution, idempotence when path == target, fall-through for
unregistered paths, leading-slash normalization, and graceful
degradation when the registry is missing. Uses
AppContainerIsolationTrait per the contract test in
tests/Architecture/AppContainerIsolationContractTest.
Gates: PHPUnit 1894 OK, PHPStan 0 errors, module:sync ok.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:39:36 +02:00
|
|
|
'exportUrl' => endpointUrl('admin/users/export'),
|
2026-03-05 08:26:51 +01:00
|
|
|
'currentUserUuid' => (string) $currentUserUuid,
|
|
|
|
|
'canUpdateUsers' => (bool) $canUpdateUsers,
|
|
|
|
|
'canUpdateSelf' => (bool) $canUpdateSelf,
|
|
|
|
|
'csrf' => $gridCsrf,
|
|
|
|
|
'gridLang' => $gridLang,
|
|
|
|
|
'labels' => [
|
refactor(admin-lists): slim tenant + user grids, Stripe-style pagination
Tenant list:
- Drop the avatar/logo column — the list now shows only Tenant name
and user count. Also drop the logo-hasLogo server lookup and the
initials/placeholder markup.
User list (13 → 4 columns, Stripe pattern):
- User (compound: avatar + Name + email subtitle, click opens drawer)
- Tenants (badges, primary tooltip)
- Last login (relative badge)
- State (active/inactive badge)
- UUID hidden at the last column for cells[uuidIndex].data
- Everything else (departments, roles, phone, mobile, short_dial,
created, modified) lives in the detail drawer.
- Document the off-by-one gotcha: with row selection enabled gridjs
prepends a checkbox cell, so runtime cells[] are shifted by +1;
uuidIndex is 5 (column position 4 + selection offset).
- New .grid-user-profile css mirrors .grid-tenant-profile (avatar +
stacked name/email) with ellipsis and primary-color hover affordance.
Pagination (all Grid.js lists):
- Non-current page buttons now use the neutral-chip secondary-outline
tokens (--app-button-neutral-*) plus the raised box-shadow — matches
the rest of the button system in both light and dark themes.
- Current page stays distinctly primary-filled with the filled-shadow;
focus-visible combines the chip shadow with the primary focus ring.
- Disabled pager buttons remain neutral but lose the lift shadow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:52:44 +02:00
|
|
|
'user' => t('User'),
|
2026-03-05 08:26:51 +01:00
|
|
|
'state' => t('State'),
|
|
|
|
|
'tenants' => t('Tenants'),
|
|
|
|
|
'lastLogin' => t('Last login'),
|
|
|
|
|
'selectAll' => t('Select all'),
|
|
|
|
|
'active' => t('Active'),
|
|
|
|
|
'inactive' => t('Inactive'),
|
|
|
|
|
'never' => t('Never'),
|
|
|
|
|
'bulkActivateConfirm' => t('Activate users?'),
|
|
|
|
|
'bulkDeactivateConfirm' => t('Deactivate users?'),
|
|
|
|
|
'bulkDeleteConfirm' => t('Delete users?'),
|
|
|
|
|
'bulkSendAccessConfirm' => t('Send access emails to selected users?'),
|
|
|
|
|
'bulkAccessPdfConfirm' => t('Generate access PDFs for selected users?'),
|
|
|
|
|
'primaryTenant' => t('Primary tenant'),
|
|
|
|
|
'bulkMessages' => [
|
|
|
|
|
'activate' => [
|
|
|
|
|
'success' => t('%d users activated'),
|
|
|
|
|
'error' => t('Failed to activate users'),
|
|
|
|
|
],
|
|
|
|
|
'deactivate' => [
|
|
|
|
|
'success' => t('%d users deactivated'),
|
|
|
|
|
'error' => t('Failed to deactivate users'),
|
|
|
|
|
],
|
|
|
|
|
'delete' => [
|
|
|
|
|
'success' => t('%d users deleted'),
|
|
|
|
|
'error' => t('Failed to delete users'),
|
|
|
|
|
],
|
|
|
|
|
'send-access' => [
|
|
|
|
|
'success' => t('Access emails sent to %d users'),
|
|
|
|
|
'partial' => t('Access emails sent to %d users, %f failed'),
|
|
|
|
|
'error' => t('Failed to send access emails'),
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
?>
|
|
|
|
|
<script src="<?php e(assetVersion('vendor/gridjs/gridjs.umd.js')); ?>"></script>
|
|
|
|
|
<script src="<?php e(assetVersion('vendor/gridjs/plugins/selection/selection.umd.js')); ?>"></script>
|
|
|
|
|
<script type="application/json" id="page-config-admin-users-index"><?php gridJsonForJs($pageConfig); ?></script>
|
|
|
|
|
<script type="module" src="<?php e(assetVersion('js/pages/admin-users-index.js')); ?>"></script>
|