Files
breadcrumb-the-shire/pages/admin/users/_form.phtml
2026-03-13 09:49:11 +01:00

976 lines
45 KiB
PHTML

<?php
/**
* @var array $values
* @var bool $passwordRequired
* @var string|null $passwordLabel
* @var string|null $passwordLegend
* @var string|null $passwordConfirmLabel
* @var string|null $formId
* @var bool $detailsOpenAll
* @var bool $showTenants
* @var array $tenants
* @var array $selectedTenantIds
* @var bool $showRoles
* @var array $roles
* @var array $selectedRoleIds
* @var bool $showDepartments
* @var array $departmentOptionsByTenant
* @var array $selectedDepartmentIds
* @var bool $isReadOnly
* @var bool $showPermissions
* @var array $permissionRows
* @var bool $showPasswordResets
* @var array $passwordResets
* @var bool $showRememberTokens
* @var array $rememberTokens
* @var bool $showCustomFieldsTab
* @var bool $canEditCustomFieldValues
* @var array $customFieldDefinitionsByTenant
* @var array $customFieldValueMap
* @var array $customFieldPostedValues
* @var bool $showDangerZone
* @var string|null $dangerZoneDeleteFormId
* @var string|null $dangerZoneWarning
* @var string|null $dangerZoneActionLabel
* @var bool $showTokenDangerAction
* @var string|null $tokenDangerFormId
* @var string|null $tokenDangerWarning
* @var string|null $tokenDangerActionLabel
*/
use MintyPHP\I18n;
use MintyPHP\Session;
$values = $values ?? [];
$isOwnAccount = (int) ($_SESSION['user']['id'] ?? 0) === (int) ($values['id'] ?? 0);
$passwordRequired = $passwordRequired ?? false;
$passwordLabel = $passwordLabel ?? t('Password');
$passwordLegend = $passwordLegend ?? t('Password');
$passwordConfirmLabel = $passwordConfirmLabel ?? t('Password (again)');
$formId = $formId ?? 'user-form';
$detailsOpenAll = $detailsOpenAll ?? false;
$showTenants = $showTenants ?? false;
$tenants = $tenants ?? [];
$selectedTenantIds = $selectedTenantIds ?? [];
$showRoles = $showRoles ?? false;
$roles = $roles ?? [];
$selectedRoleIds = $selectedRoleIds ?? [];
$showDepartments = $showDepartments ?? false;
$departmentOptionsByTenant = $departmentOptionsByTenant ?? [];
$selectedDepartmentIds = $selectedDepartmentIds ?? [];
$primaryTenantId = (int) ($values['primary_tenant_id'] ?? 0);
if (!$primaryTenantId && count($selectedTenantIds) === 1) {
$primaryTenantId = (int) $selectedTenantIds[0];
}
$isReadOnly = $isReadOnly ?? false;
$permissionRows = $permissionRows ?? [];
$showPermissions = $showPermissions ?? false;
$passwordResets = $passwordResets ?? [];
$showPasswordResets = $showPasswordResets ?? false;
$rememberTokens = $rememberTokens ?? [];
$showRememberTokens = $showRememberTokens ?? false;
$apiTokens = $apiTokens ?? [];
$showApiTokens = $showApiTokens ?? false;
$canManageApiTokens = $canManageApiTokens ?? false;
$showCustomFieldsTab = (bool) $showCustomFieldsTab;
$canEditCustomFieldValues = (bool) $canEditCustomFieldValues;
$customFieldDefinitionsByTenant = (array) $customFieldDefinitionsByTenant;
$customFieldValueMap = (array) $customFieldValueMap;
$customFieldPostedValues = (array) $customFieldPostedValues;
$customFieldScalarPosted = is_array($customFieldPostedValues['custom_field_values'] ?? null)
? $customFieldPostedValues['custom_field_values']
: [];
$customFieldMultiPosted = is_array($customFieldPostedValues['custom_field_values_multi'] ?? null)
? $customFieldPostedValues['custom_field_values_multi']
: [];
$showDangerZone = (bool) ($showDangerZone ?? false);
$dangerZoneDeleteFormId = (string) ($dangerZoneDeleteFormId ?? '');
$dangerZoneWarning = (string) ($dangerZoneWarning ?? t('This action cannot be undone.'));
$dangerZoneActionLabel = (string) ($dangerZoneActionLabel ?? t('Delete'));
$showTokenDangerAction = (bool) ($showTokenDangerAction ?? false);
$tokenDangerFormId = (string) ($tokenDangerFormId ?? '');
$tokenDangerWarning = (string) ($tokenDangerWarning ?? t('This action cannot be undone.'));
$tokenDangerActionLabel = (string) ($tokenDangerActionLabel ?? t('Clear login tokens'));
$minLength = (int) ($passwordMinLength ?? 0);
$passwordHints = is_array($passwordHints ?? null) ? $passwordHints : [];
$requiredAttr = $passwordRequired ? 'required' : '';
$readonlyAttr = $isReadOnly ? 'readonly' : '';
$disabledAttr = $isReadOnly ? 'disabled' : '';
$dataDisabledAttr = $isReadOnly ? 'data-disabled="true"' : '';
$visibilityDisabled = $isReadOnly || $isOwnAccount;
$visibilityDisabledAttr = $visibilityDisabled ? 'disabled' : '';
$visibilityHint = $isOwnAccount
? t('You cannot deactivate your own account')
: t('Inactive entries are hidden in lists and selections.');
$locales = defined('APP_LOCALES') ? APP_LOCALES : [I18n::$defaultLocale];
$primaryTenantDisabled = $isReadOnly || count($selectedTenantIds) === 1;
$primaryTenantDisabledAttr = $primaryTenantDisabled ? 'disabled' : '';
$showOrganization = $showTenants || $showDepartments || $showRoles;
$showOrganizationTab = $showOrganization || $showPermissions;
$customFieldInputsDisabled = $isReadOnly || !$canEditCustomFieldValues;
$customFieldInputsDisabledAttr = $customFieldInputsDisabled ? 'disabled' : '';
$customFieldTenantMap = [];
foreach ($tenants as $tenant) {
$tenantId = (int) ($tenant['id'] ?? 0);
if ($tenantId <= 0 || !in_array($tenantId, $selectedTenantIds, true)) {
continue;
}
$customFieldTenantMap[$tenantId] = $tenant;
}
?>
<form id="<?php e($formId); ?>" method="post" data-details-storage="admin-user-form-details-v1" data-standard-detail-form="1">
<div class="app-tabs" data-tabs data-tabs-param="tab" data-tabs-storage-key="admin-user-form">
<div class="app-tabs-nav">
<button type="button" data-tab="profile" data-tab-default><?php e(t('Master data')); ?></button>
<?php if ($showOrganizationTab): ?>
<button type="button" data-tab="organization"><?php e(t('Organization')); ?></button>
<?php endif; ?>
<?php if ($showCustomFieldsTab): ?>
<button type="button" data-tab="custom_fields"><?php e(t('Custom fields')); ?></button>
<?php endif; ?>
<button type="button" data-tab="security"><?php e(t('Security')); ?></button>
<button type="button" data-tab="visibility"><?php e(t('Visibility')); ?></button>
<?php if ($showDangerZone || $showTokenDangerAction): ?>
<button type="button" data-tab="danger"><?php e(t('Danger zone')); ?></button>
<?php endif; ?>
</div>
<div data-tab-panel="profile">
<div class="grid">
<label for="first_name">
<span><?php e(t('First name')); ?></span>
<input required type="text" name="first_name" id="first_name" value="<?php e($values['first_name'] ?? ''); ?>"
<?php e($readonlyAttr); ?> />
</label>
<label for="last_name">
<span><?php e(t('Last name')); ?></span>
<input required type="text" name="last_name" id="last_name" value="<?php e($values['last_name'] ?? ''); ?>"
<?php e($readonlyAttr); ?> />
</label>
</div>
<label for="email">
<span><?php e(t('Email')); ?></span>
<input required type="email" name="email" id="email" value="<?php e($values['email'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
</label>
<hr>
<details name="profile-data" data-details-key="profile-data" <?php e($detailsOpenAll ? 'open' : ''); ?>>
<summary><?php e(t('Profile')); ?></summary>
<hr>
<label for="job_title">
<span><?php e(t('Job title')); ?></span>
<input type="text" name="job_title" id="job_title" value="<?php e($values['job_title'] ?? ''); ?>"
<?php e($readonlyAttr); ?> />
</label>
<label for="profile_description">
<span><?php e(t('Profile description')); ?></span>
<textarea name="profile_description" id="profile_description" rows="4"
<?php e($readonlyAttr); ?>><?php e($values['profile_description'] ?? ''); ?></textarea>
</label>
</details>
<hr>
<details name="preferences" data-details-key="preferences" <?php e($detailsOpenAll ? 'open' : ''); ?>>
<summary><?php e(t('Preferences')); ?></summary>
<hr>
<div class="grid">
<?php if (allowUserTheme()): ?>
<label for="theme">
<span><?php e(t('Theme')); ?></span>
<?php
$themes = appThemes();
$selectedTheme = (string) ($values['theme'] ?? '');
if ($selectedTheme === '' || !isset($themes[$selectedTheme])) {
$selectedTheme = appDefaultTheme();
}
?>
<select name="theme" id="theme" <?php e($disabledAttr); ?>>
<?php foreach ($themes as $key => $label): ?>
<option value="<?php e($key); ?>" <?php if ($selectedTheme === $key) { ?>selected<?php } ?>>
<?php e(t($label)); ?>
</option>
<?php endforeach; ?>
</select>
</label>
<?php endif; ?>
<label for="locale">
<span><?php e(t('Language')); ?></span>
<?php $selectedLocale = $values['locale'] ?? (I18n::$locale ?? I18n::$defaultLocale); ?>
<select name="locale" id="locale" <?php e($disabledAttr); ?>>
<?php foreach ($locales as $locale): ?>
<?php
$label = strtoupper($locale);
if ($locale === 'de') {
$label = t('German');
} elseif ($locale === 'en') {
$label = t('English');
}
?>
<option value="<?php e($locale); ?>" <?php if ($selectedLocale === $locale) { ?>selected<?php } ?>>
<?php e($label); ?>
</option>
<?php endforeach; ?>
</select>
</label>
</div>
</details>
<hr>
<details name="profile-contact-data" data-details-key="profile-contact-data" <?php e($detailsOpenAll ? 'open' : ''); ?>>
<summary><?php e(t('Contact')); ?></summary>
<hr>
<div class="grid">
<label for="phone">
<span><?php e(t('Phone')); ?></span>
<input type="tel" name="phone" id="phone" autocomplete="tel" value="<?php e($values['phone'] ?? ''); ?>"
<?php e($readonlyAttr); ?> />
</label>
<label for="mobile">
<span><?php e(t('Mobile')); ?></span>
<input type="tel" name="mobile" id="mobile" autocomplete="tel" value="<?php e($values['mobile'] ?? ''); ?>"
<?php e($readonlyAttr); ?> />
</label>
<label for="short_dial">
<span><?php e(t('Short dial')); ?></span>
<input type="text" name="short_dial" id="short_dial" inputmode="numeric"
value="<?php e($values['short_dial'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
</label>
</div>
</details>
<hr>
<details name="profile-address-data" data-details-key="profile-address-data" <?php e($detailsOpenAll ? 'open' : ''); ?>>
<summary><?php e(t('Address')); ?></summary>
<hr>
<label for="address">
<span><?php e(t('Address')); ?></span>
<input type="text" name="address" id="address" value="<?php e($values['address'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
</label>
<div class="grid">
<label for="postal_code">
<span><?php e(t('Postal code')); ?></span>
<input type="text" name="postal_code" id="postal_code" value="<?php e($values['postal_code'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
</label>
<label for="city">
<span><?php e(t('City')); ?></span>
<input type="text" name="city" id="city" value="<?php e($values['city'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
</label>
</div>
<div class="grid">
<label for="region">
<span><?php e(t('Region')); ?></span>
<input type="text" name="region" id="region" value="<?php e($values['region'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
</label>
<label for="country">
<span><?php e(t('Country')); ?></span>
<input type="text" name="country" id="country" value="<?php e($values['country'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
</label>
</div>
</details>
<hr>
<details name="profile-employment-data" data-details-key="profile-employment-data" <?php e($detailsOpenAll ? 'open' : ''); ?>>
<summary><?php e(t('Employment')); ?></summary>
<hr>
<label for="hire_date">
<span><?php e(t('Hire date')); ?></span>
<input type="date" name="hire_date" id="hire_date" value="<?php e($values['hire_date'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
</label>
</details>
</div>
<div data-tab-panel="security">
<details name="security-password" data-details-key="security-password" open>
<summary><?php e(t('Access & password')); ?></summary>
<hr>
<div class="grid">
<div>
<label for="password">
<span><?php e($passwordLabel); ?></span>
<input <?php e($requiredAttr); ?> type="password" name="password" id="password"
minlength="<?php e($minLength); ?>" autocomplete="new-password" <?php e($disabledAttr); ?> />
</label>
<label for="password2">
<span><?php e($passwordConfirmLabel); ?></span>
<input <?php e($requiredAttr); ?> type="password" name="password2" id="password2"
minlength="<?php e($minLength); ?>" autocomplete="new-password" <?php e($disabledAttr); ?> />
</label>
</div>
<div class="form-hint" data-password-hints data-password-input="#password" data-confirm-input="#password2"
data-email-input="#email" data-min-length="<?php e($minLength); ?>">
<strong><?php e(t('Password requirements')); ?></strong>
<ul class="form-hint-list">
<?php foreach ($passwordHints as $hint): ?>
<li data-rule="<?php e($hint['rule']); ?>"><?php e($hint['text']); ?></li>
<?php endforeach; ?>
<li data-rule="match"><?php e(t('Passwords must match')); ?></li>
</ul>
</div>
</div>
</details>
<?php if ($showPasswordResets): ?>
<hr>
<details name="security-password-resets" data-details-key="security-password-resets">
<summary><?php e(t('Password resets')); ?></summary>
<hr>
<?php if (!$passwordResets): ?>
<?php
$emptyState = [
'message' => t('No password reset requests'),
'hint' => t('No reset has been requested for this account yet.'),
'size' => 'compact',
];
require templatePath('partials/app-empty-state.phtml');
?>
<?php else: ?>
<div class="overflow-auto white-space-nowrap">
<table>
<thead>
<tr>
<th><?php e(t('Requested at')); ?></th>
<th><?php e(t('Expires at')); ?></th>
<th><?php e(t('Used at')); ?></th>
<th><?php e(t('Attempts')); ?></th>
<th><?php e(t('Status')); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($passwordResets as $reset): ?>
<?php
$expiresAt = (string) ($reset['expires_at'] ?? '');
$usedAt = (string) ($reset['used_at'] ?? '');
$status = t('Active');
$statusVariant = 'success';
if ($usedAt !== '') {
$status = t('Used');
$statusVariant = 'neutral';
} elseif ($expiresAt !== '') {
try {
$expired = (new \DateTimeImmutable($expiresAt)) <= new \DateTimeImmutable('now');
if ($expired) {
$status = t('Expired');
$statusVariant = 'warning';
}
} catch (\Exception $e) {
// fallback: keep Active
}
}
?>
<tr>
<td><?php e(dt($reset['created'] ?? '')); ?></td>
<td><?php e($expiresAt !== '' ? dt($expiresAt) : '-'); ?></td>
<td><?php e($usedAt !== '' ? dt($usedAt) : '-'); ?></td>
<td><?php e((string) ($reset['attempts'] ?? 0)); ?></td>
<td>
<span class="badge" data-variant="<?php e($statusVariant); ?>">
<?php e($status); ?>
</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</details>
<?php endif; ?>
<?php if ($showRememberTokens): ?>
<hr>
<details name="security-login-tokens" data-details-key="security-login-tokens">
<summary><?php e(t('Login tokens')); ?></summary>
<hr>
<?php if (!$rememberTokens): ?>
<?php
$emptyState = [
'message' => t('No login tokens'),
'hint' => t('This user is currently not signed in via remember-me.'),
'size' => 'compact',
];
require templatePath('partials/app-empty-state.phtml');
?>
<?php else: ?>
<div class="overflow-auto white-space-nowrap">
<table>
<thead>
<tr>
<th><?php e(t('Created at')); ?></th>
<th><?php e(t('Expires at')); ?></th>
<th><?php e(t('Last used')); ?></th>
<th><?php e(t('Status')); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($rememberTokens as $token): ?>
<?php
$expiresAt = (string) ($token['expires_at'] ?? '');
$expiredByAdminAt = (string) ($token['expired_by_admin_at'] ?? '');
$lastUsed = (string) ($token['last_used'] ?? '');
$status = t('Active');
$statusVariant = 'success';
if ($expiresAt !== '') {
try {
$expired = (new \DateTimeImmutable($expiresAt)) <= new \DateTimeImmutable('now');
if ($expired) {
if ($expiredByAdminAt !== '') {
$status = t('Expired by admin');
$statusVariant = 'warning';
} else {
$status = t('Expired');
$statusVariant = 'warning';
}
}
} catch (\Exception $e) {
// fallback: keep Active
}
}
?>
<tr>
<td><?php e(dt($token['created'] ?? '')); ?></td>
<td><?php e($expiresAt !== '' ? dt($expiresAt) : '-'); ?></td>
<td><?php e($lastUsed !== '' ? dt($lastUsed) : '-'); ?></td>
<td>
<span class="badge" data-variant="<?php e($statusVariant); ?>">
<?php e($status); ?>
</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</details>
<?php endif; ?>
<?php if ($showApiTokens): ?>
<hr>
<?php
$apiTokenData = $_SESSION['api_token_created'] ?? null;
$apiTokenCreated = null;
if (is_array($apiTokenData) && ($apiTokenData['uuid'] ?? '') === ($values['uuid'] ?? '')) {
$apiTokenCreated = $apiTokenData['token'];
unset($_SESSION['api_token_created']);
}
?>
<details name="security-api-tokens" data-details-key="security-api-tokens"<?php if ($apiTokenCreated): ?> open<?php endif; ?>>
<summary><?php e(t('API tokens')); ?></summary>
<hr>
<?php if ($apiTokenCreated): ?>
<div class="app-flash" data-variant="info" style="margin-bottom: 1rem;">
<p><strong><?php e(t('New API token (copy now — shown only once):')); ?></strong></p>
<span class="badge" data-variant="neutral" data-copy="true"
data-copy-value="<?php e($apiTokenCreated); ?>"
style="font-family: var(--pico-font-family-monospace); font-size: 0.85rem; word-break: break-all;">
<?php e($apiTokenCreated); ?>
</span>
</div>
<?php endif; ?>
<?php if ($canManageApiTokens): ?>
<div>
<label>
<span><?php e(t('Token name')); ?></span>
</label>
<div role="group" class="white-space-nowrap">
<input type="text" name="token_name" id="api-token-name"
form="api-token-create-form" required
placeholder="<?php e(t('e.g. CI Pipeline')); ?>">
<button type="submit"
form="api-token-create-form"
><?php e(t('Create API token')); ?></button>
</div>
</div>
<?php endif; ?>
<?php if (!empty($apiTokens)): ?>
<div class="overflow-auto white-space-nowrap">
<table>
<thead>
<tr>
<th><?php e(t('Name')); ?></th>
<th><?php e(t('Prefix')); ?></th>
<th><?php e(t('Created at')); ?></th>
<th><?php e(t('Last used')); ?></th>
<th><?php e(t('Status')); ?></th>
<?php if ($canManageApiTokens): ?>
<th></th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach ($apiTokens as $apiToken): ?>
<?php
$isRevoked = !empty($apiToken['revoked_at']);
$isExpired = false;
$expiresAtApi = (string) ($apiToken['expires_at'] ?? '');
if (!$isRevoked && $expiresAtApi !== '') {
try {
$isExpired = (new \DateTimeImmutable($expiresAtApi)) <= new \DateTimeImmutable('now');
} catch (\Exception $e) {}
}
$statusLabel = t('Active');
$statusVariant = 'success';
if ($isRevoked) {
$statusLabel = t('Revoked');
$statusVariant = 'danger';
} elseif ($isExpired) {
$statusLabel = t('Expired');
$statusVariant = 'warning';
}
?>
<tr>
<td><?php e($apiToken['name'] ?? ''); ?></td>
<td><code><?php e(substr($apiToken['selector'] ?? '', 0, 8) . '...'); ?></code></td>
<td><?php e(dt($apiToken['created'] ?? '')); ?></td>
<td><?php e(!empty($apiToken['last_used_at']) ? dt($apiToken['last_used_at']) : '-'); ?></td>
<td>
<span class="badge" data-variant="<?php e($statusVariant); ?>">
<?php e($statusLabel); ?>
</span>
</td>
<?php if ($canManageApiTokens): ?>
<td>
<?php if (!$isRevoked): ?>
<button type="submit"
name="token_id"
value="<?php e((string) ($apiToken['id'] ?? '')); ?>"
class="outline"
style="--pico-color: var(--app-danger);"
formaction="<?php e(lurl("admin/users/api-token-revoke/{$values['uuid']}")); ?>"
data-detail-confirm-message="<?php e(t('Revoke this API token?')); ?>"
data-detail-action-kind="danger"
><?php e(t('Revoke')); ?></button>
<?php endif; ?>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php if (empty($apiTokens)): ?>
<?php
$emptyState = [
'message' => t('No API tokens'),
'hint' => $canManageApiTokens
? t('Create a token to enable API access for this user.')
: t('No API token has been created for this user yet.'),
'size' => 'compact',
];
require templatePath('partials/app-empty-state.phtml');
?>
<?php endif; ?>
</details>
<?php endif; ?>
</div>
<?php if ($showOrganizationTab): ?>
<div data-tab-panel="organization">
<?php if ($showOrganization): ?>
<?php if ($showTenants): ?>
<details name="assignments-tenants" data-details-key="assignments-tenants" open>
<summary><?php e(t('Tenants')); ?></summary>
<hr>
<?php if (!$tenants): ?>
<?php
$emptyState = [
'message' => t('No tenants available'),
'hint' => t('No tenant is available in your current scope.'),
'size' => 'compact',
];
require templatePath('partials/app-empty-state.phtml');
?>
<?php else: ?>
<?php multiSelectForm('user-tenants', 'tenant_ids', 'Assigned tenants', 'Select tenants', $tenants, $selectedTenantIds, $isReadOnly); ?>
<label>
<span>
<?php e(t('Primary tenant')); ?>
</span>
</label>
<?php if ($primaryTenantDisabled && $primaryTenantId > 0): ?>
<input type="hidden" name="primary_tenant_id" value="<?php e((string) $primaryTenantId); ?>">
<?php endif; ?>
<select id="user-primary-tenant" name="primary_tenant_id" <?php e($primaryTenantDisabledAttr); ?>
<?php e($disabledAttr); ?>>
<?php if (count($selectedTenantIds) !== 1): ?>
<option value="">
<?php e(t('Select primary tenant')); ?>
</option>
<?php endif; ?>
<?php foreach ($tenants as $tenant): ?>
<?php
$tenantId = (int) ($tenant['id'] ?? 0);
if (!in_array($tenantId, $selectedTenantIds ?? [], true)) {
continue;
}
$selected = $tenantId === $primaryTenantId ? 'selected' : '';
?>
<option value="<?php e((string) $tenantId); ?>" <?php e($selected); ?>>
<?php e($tenant['description'] ?? ''); ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
</details>
<hr>
<?php endif; ?>
<?php if ($showDepartments): ?>
<details name="assignments-departments" data-details-key="assignments-departments">
<summary><?php e(t('Departments')); ?></summary>
<hr>
<?php
$selectedTenantMap = [];
foreach ($tenants as $tenant) {
$tenantId = (int) ($tenant['id'] ?? 0);
if ($tenantId <= 0 || !in_array($tenantId, $selectedTenantIds, true)) {
continue;
}
$selectedTenantMap[$tenantId] = $tenant;
}
?>
<?php if (!$selectedTenantMap): ?>
<?php
$emptyState = [
'message' => t('Assign tenants first to select departments'),
'size' => 'compact',
];
require templatePath('partials/app-empty-state.phtml');
?>
<?php else: ?>
<?php foreach ($selectedTenantMap as $tenantId => $tenant): ?>
<?php $tenantDepartments = $departmentOptionsByTenant[$tenantId] ?? []; ?>
<fieldset>
<legend>
<small><?php e(t('Departments for tenant')); ?>: <?php e($tenant['description'] ?? ''); ?></small>
</legend>
<?php if ($tenantDepartments): ?>
<?php
multiSelectForm(
'user-departments-tenant-' . $tenantId,
'department_ids',
'Assigned departments',
'Select departments',
$tenantDepartments,
$selectedDepartmentIds,
$isReadOnly
);
?>
<?php else: ?>
<?php
$emptyState = [
'message' => t('No departments available for this tenant'),
'size' => 'compact',
];
require templatePath('partials/app-empty-state.phtml');
?>
<?php endif; ?>
</fieldset>
<?php endforeach; ?>
<?php endif; ?>
</details>
<?php if ($showRoles): ?><hr><?php endif; ?>
<?php endif; ?>
<?php if ($showRoles): ?>
<details name="assignments-roles" data-details-key="assignments-roles">
<summary><?php e(t('Roles')); ?></summary>
<hr>
<?php if (!$roles): ?>
<?php
$emptyState = [
'message' => t('No roles available'),
'hint' => t('No active roles are configured yet.'),
'size' => 'compact',
];
require templatePath('partials/app-empty-state.phtml');
?>
<?php else: ?>
<?php multiSelectForm('user-roles', 'role_ids', 'Assigned roles', 'Select roles', $roles, $selectedRoleIds, $isReadOnly); ?>
<?php endif; ?>
</details>
<?php endif; ?>
<?php endif; ?>
<?php if ($showPermissions): ?>
<?php if ($showOrganization): ?><hr><?php endif; ?>
<details name="assignments-permissions" data-details-key="assignments-permissions">
<summary><?php e(t('Permissions')); ?></summary>
<hr>
<?php if (!$permissionRows): ?>
<?php
$emptyState = [
'message' => t('No effective permissions'),
'hint' => t('Assign at least one role to show effective permissions.'),
'size' => 'compact',
];
require templatePath('partials/app-empty-state.phtml');
?>
<?php else: ?>
<div class="overflow-auto white-space-nowrap">
<table>
<thead>
<tr>
<th><?php e(t('Description')); ?></th>
<th><?php e(t('Permission key')); ?></th>
<th><?php e(t('Permission origin')); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($permissionRows as $permissionRow): ?>
<?php
$permissionKey = (string) ($permissionRow['key'] ?? '');
$labelKey = 'perm.' . $permissionKey;
$label = t($labelKey);
if ($label === $labelKey) {
$label = (string) ($permissionRow['description'] ?? '');
if ($label === '') {
$label = $permissionKey;
}
}
$rolesList = [];
if (!empty($permissionRow['roles']) && is_array($permissionRow['roles'])) {
$rolesList = $permissionRow['roles'];
}
?>
<tr>
<td><?php e($label); ?></td>
<td><code><?php e($permissionKey); ?></code></td>
<td>
<?php if ($rolesList): ?>
<?php foreach ($rolesList as $roleLabel): ?>
<span class="badge" data-variant="neutral"><?php e($roleLabel); ?></span>
<?php endforeach; ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</details>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($showCustomFieldsTab): ?>
<div data-tab-panel="custom_fields">
<?php if (!$customFieldTenantMap): ?>
<?php
$emptyState = [
'message' => t('Assign tenants first to edit custom fields'),
'size' => 'compact',
];
require templatePath('partials/app-empty-state.phtml');
?>
<?php else: ?>
<?php $hasRenderedCustomFields = false; ?>
<?php $hasConfiguredTenantDefinitions = false; ?>
<?php foreach ($customFieldTenantMap as $tenantId => $tenant): ?>
<?php $tenantDefinitions = $customFieldDefinitionsByTenant[$tenantId] ?? []; ?>
<?php if ($tenantDefinitions): ?>
<?php $hasConfiguredTenantDefinitions = true; ?>
<?php endif; ?>
<details name="custom-fields-tenant" data-details-key="custom-fields-tenant-<?php e((string) $tenantId); ?>">
<summary>
<?php e((string) ($tenant['description'] ?? t('Custom fields for tenant'))); ?>
</summary>
<hr>
<?php if (!$tenantDefinitions): ?>
<?php
$emptyState = [
'message' => t('No custom fields configured for this tenant'),
'size' => 'compact',
];
require templatePath('partials/app-empty-state.phtml');
?>
<?php else: ?>
<div>
<?php foreach ($tenantDefinitions as $definition): ?>
<?php
$definitionId = (int) ($definition['id'] ?? 0);
$definitionType = strtolower((string) ($definition['type'] ?? 'text'));
$definitionLabel = (string) ($definition['label'] ?? '');
$definitionOptions = is_array($definition['options'] ?? null) ? $definition['options'] : [];
if ($definitionId <= 0 || $definitionLabel === '') {
continue;
}
$hasRenderedCustomFields = true;
$currentValue = $customFieldValueMap[$definitionId] ?? [];
$scalarPostedKey = (string) $definitionId;
$scalarPosted = array_key_exists($scalarPostedKey, $customFieldScalarPosted)
? $customFieldScalarPosted[$scalarPostedKey]
: null;
$multiPostedRaw = $customFieldMultiPosted[$scalarPostedKey] ?? null;
$multiPosted = is_array($multiPostedRaw)
? array_values(array_unique(array_map('intval', $multiPostedRaw)))
: [];
$multiPosted = array_values(array_filter($multiPosted, static fn (int $id): bool => $id > 0));
$currentOptionIds = is_array($currentValue['option_ids'] ?? null)
? array_values(array_unique(array_map('intval', $currentValue['option_ids'])))
: [];
$currentOptionIds = array_values(array_filter($currentOptionIds, static fn (int $id): bool => $id > 0));
$valueText = array_key_exists('value_text', $currentValue) ? (string) ($currentValue['value_text'] ?? '') : '';
$valueBool = array_key_exists('value_bool', $currentValue) && $currentValue['value_bool'] !== null
? (string) ((int) $currentValue['value_bool'])
: '';
$valueDate = array_key_exists('value_date', $currentValue) ? (string) ($currentValue['value_date'] ?? '') : '';
$valueOptionId = array_key_exists('option_id', $currentValue) && $currentValue['option_id'] !== null
? (int) $currentValue['option_id']
: 0;
?>
<div>
<?php if (in_array($definitionType, ['text', 'textarea'], true)): ?>
<?php $fieldValue = $scalarPosted !== null ? trim((string) $scalarPosted) : $valueText; ?>
<label for="custom-field-<?php e((string) $definitionId); ?>">
<span>
<?php e($definitionLabel); ?>
</span>
<?php if ($definitionType === 'textarea'): ?>
<textarea
id="custom-field-<?php e((string) $definitionId); ?>"
name="custom_field_values[<?php e((string) $definitionId); ?>]"
rows="4"
<?php e($customFieldInputsDisabledAttr); ?>
><?php e($fieldValue); ?></textarea>
<?php else: ?>
<input
id="custom-field-<?php e((string) $definitionId); ?>"
type="text"
name="custom_field_values[<?php e((string) $definitionId); ?>]"
value="<?php e($fieldValue); ?>"
<?php e($customFieldInputsDisabledAttr); ?>
>
<?php endif; ?>
</label>
<?php elseif ($definitionType === 'date'): ?>
<?php $fieldValue = $scalarPosted !== null ? trim((string) $scalarPosted) : $valueDate; ?>
<label for="custom-field-<?php e((string) $definitionId); ?>">
<span>
<?php e($definitionLabel); ?>
</span>
<input
id="custom-field-<?php e((string) $definitionId); ?>"
type="date"
name="custom_field_values[<?php e((string) $definitionId); ?>]"
value="<?php e($fieldValue); ?>"
<?php e($customFieldInputsDisabledAttr); ?>
>
</label>
<?php elseif ($definitionType === 'boolean'): ?>
<?php $fieldValue = $scalarPosted !== null ? strtolower(trim((string) $scalarPosted)) : $valueBool; ?>
<label for="custom-field-<?php e((string) $definitionId); ?>">
<span>
<?php e($definitionLabel); ?>
</span>
<select
id="custom-field-<?php e((string) $definitionId); ?>"
name="custom_field_values[<?php e((string) $definitionId); ?>]"
<?php e($customFieldInputsDisabledAttr); ?>
>
<option value=""><?php e(t('Please select')); ?></option>
<option value="1" <?php e($fieldValue === '1' || $fieldValue === 'true' ? 'selected' : ''); ?>>
<?php e(t('Yes')); ?>
</option>
<option value="0" <?php e($fieldValue === '0' || $fieldValue === 'false' ? 'selected' : ''); ?>>
<?php e(t('No')); ?>
</option>
</select>
</label>
<?php elseif ($definitionType === 'select'): ?>
<?php
$fieldValue = $scalarPosted !== null ? (int) $scalarPosted : $valueOptionId;
?>
<label for="custom-field-<?php e((string) $definitionId); ?>">
<span>
<?php e($definitionLabel); ?>
</span>
<select
id="custom-field-<?php e((string) $definitionId); ?>"
name="custom_field_values[<?php e((string) $definitionId); ?>]"
<?php e($customFieldInputsDisabledAttr); ?>
>
<option value=""><?php e(t('Please select')); ?></option>
<?php foreach ($definitionOptions as $option): ?>
<?php
$optionId = (int) ($option['id'] ?? 0);
if ($optionId <= 0) {
continue;
}
?>
<option value="<?php e((string) $optionId); ?>" <?php e($fieldValue === $optionId ? 'selected' : ''); ?>>
<?php e((string) ($option['label'] ?? '')); ?>
</option>
<?php endforeach; ?>
</select>
</label>
<?php elseif ($definitionType === 'multiselect'): ?>
<?php
$selectedOptions = $multiPostedRaw !== null ? $multiPosted : $currentOptionIds;
multiSelectForm(
'user-custom-field-' . $definitionId,
'custom_field_values_multi[' . $definitionId . ']',
$definitionLabel,
'Select options',
$definitionOptions,
$selectedOptions,
$customFieldInputsDisabled,
['label', 'description']
);
?>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</details>
<hr>
<?php endforeach; ?>
<?php if (!$hasRenderedCustomFields && $hasConfiguredTenantDefinitions): ?>
<?php
$emptyState = [
'message' => t('No custom fields configured for selected tenants'),
'size' => 'compact',
];
require templatePath('partials/app-empty-state.phtml');
?>
<?php endif; ?>
<?php endif; ?>
</div>
<?php endif; ?>
<div data-tab-panel="visibility">
<?php
$statusFieldName = 'active';
$statusFieldId = 'user-active';
$statusFieldValue = (string) (($values['active'] ?? 1) ? '1' : '0');
$statusFieldOptions = [
'1' => t('Active'),
'0' => t('Inactive'),
];
$statusFieldHint = $visibilityHint;
$statusFieldDisabledAttr = $visibilityDisabledAttr;
require templatePath('partials/app-visibility-status-field.phtml');
?>
</div>
<?php if ($showDangerZone || $showTokenDangerAction): ?>
<div data-tab-panel="danger">
<?php if ($showTokenDangerAction && $tokenDangerFormId !== ''): ?>
<fieldset>
<legend><small><?php e(t('Login tokens')); ?></small></legend>
<p class="muted"><?php e($tokenDangerWarning); ?></p>
<button type="submit" class="danger" form="<?php e($tokenDangerFormId); ?>">
<?php e($tokenDangerActionLabel); ?>
</button>
</fieldset>
<?php endif; ?>
<?php if ($showDangerZone && $dangerZoneDeleteFormId !== ''): ?>
<?php
$dangerFormId = $dangerZoneDeleteFormId;
$dangerLegend = t('Delete');
$dangerWarning = $dangerZoneWarning;
$dangerButtonLabel = $dangerZoneActionLabel;
require templatePath('partials/app-danger-zone-delete-field.phtml');
?>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php Session::getCsrfInput(); ?>
</form>