976 lines
45 KiB
PHTML
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>
|