baseline
This commit is contained in:
55
pages/admin/departments/_form.phtml
Normal file
55
pages/admin/departments/_form.phtml
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array $values
|
||||
* @var string|null $formId
|
||||
* @var bool $detailsOpenAll
|
||||
* @var bool $isReadOnly
|
||||
* @var array $tenants
|
||||
* @var array $selectedTenantIds
|
||||
*/
|
||||
|
||||
use MintyPHP\Session;
|
||||
|
||||
$values = $values ?? [];
|
||||
$formId = $formId ?? 'department-form';
|
||||
$detailsOpenAll = $detailsOpenAll ?? false;
|
||||
$isReadOnly = $isReadOnly ?? false;
|
||||
$tenants = $tenants ?? [];
|
||||
$selectedTenantIds = $selectedTenantIds ?? [];
|
||||
$detailsOpenAttr = $detailsOpenAll ? 'open' : '';
|
||||
$readonlyAttr = $isReadOnly ? 'readonly' : '';
|
||||
$disabledAttr = $isReadOnly ? 'disabled' : '';
|
||||
$dataDisabledAttr = $isReadOnly ? 'data-disabled="true"' : '';
|
||||
|
||||
?>
|
||||
<form id="<?php e($formId); ?>" method="post">
|
||||
<details name="basic-data" open>
|
||||
<summary>Stammdaten</summary>
|
||||
<hr>
|
||||
<label for="description">
|
||||
<span><?php e(t('Description')); ?></span>
|
||||
<input required type="text" name="description" id="description" value="<?php e($values['description'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
</details>
|
||||
<?php if ($tenants): ?>
|
||||
<label>
|
||||
<span><?php e(t('Assigned tenants')); ?></span>
|
||||
</label>
|
||||
<select id="department-tenants" name="tenant_ids" multiple data-multi-select="true" <?php e($dataDisabledAttr); ?> <?php e($disabledAttr); ?>
|
||||
data-placeholder="<?php e(t('Select tenants')); ?>" data-search="true"
|
||||
data-search-placeholder="<?php e(t('Search...')); ?>" data-select-all="true"
|
||||
data-select-all-label="<?php e(t('Select all')); ?>">
|
||||
<?php foreach ($tenants as $tenant): ?>
|
||||
<?php
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
$selected = in_array($tenantId, $selectedTenantIds ?? [], true) ? 'selected' : '';
|
||||
?>
|
||||
<option value="<?php e((string) $tenantId); ?>" <?php e($selected); ?>>
|
||||
<?php e($tenant['description'] ?? ''); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php endif; ?>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
75
pages/admin/departments/create().php
Normal file
75
pages/admin/departments/create().php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\DepartmentService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Service\TenantService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::DEPARTMENTS_CREATE);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$allowedTenantIds = TenantScopeService::getUserTenantIds($currentUserId);
|
||||
if (TenantScopeService::isStrict() && !$allowedTenantIds) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$form = [
|
||||
'description' => '',
|
||||
];
|
||||
$tenants = TenantService::list();
|
||||
if ($allowedTenantIds) {
|
||||
$tenants = array_values(array_filter($tenants, static function (array $tenant) use ($allowedTenantIds): bool {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
return $tenantId > 0 && in_array($tenantId, $allowedTenantIds, true);
|
||||
}));
|
||||
} elseif (TenantScopeService::isStrict()) {
|
||||
$tenants = [];
|
||||
}
|
||||
$selectedTenantIds = [];
|
||||
|
||||
if (isset($_POST['description'])) {
|
||||
$result = DepartmentService::createFromAdmin($_POST, $currentUserId);
|
||||
$form = $result['form'] ?? $form;
|
||||
$errors = $result['errors'] ?? [];
|
||||
$tenantIds = $_POST['tenant_ids'] ?? [];
|
||||
if (!is_array($tenantIds)) {
|
||||
$tenantIds = [$tenantIds];
|
||||
}
|
||||
$selectedTenantIds = array_values(array_unique(array_map('intval', $tenantIds)));
|
||||
$selectedTenantIds = TenantScopeService::filterTenantIdsForUser($selectedTenantIds, $currentUserId);
|
||||
|
||||
if (TenantScopeService::isStrict() && !$selectedTenantIds) {
|
||||
$errors[] = t('Please select at least one tenant');
|
||||
}
|
||||
|
||||
if (($result['ok'] ?? false) && !$errors) {
|
||||
$departmentId = (int) ($result['id'] ?? 0);
|
||||
if ($departmentId) {
|
||||
DepartmentService::syncTenants($departmentId, $selectedTenantIds);
|
||||
}
|
||||
$action = (string) ($_POST['action'] ?? 'create');
|
||||
if ($action === 'create_close') {
|
||||
Flash::success('Department created', 'admin/departments', 'department_created');
|
||||
Router::redirect('admin/departments');
|
||||
} else {
|
||||
$uuid = (string) ($result['uuid'] ?? '');
|
||||
if ($uuid !== '') {
|
||||
$target = "admin/departments/edit/{$uuid}";
|
||||
Flash::success('Department created', $target, 'department_created');
|
||||
Router::redirect($target);
|
||||
} else {
|
||||
Flash::success('Department created', 'admin/departments', 'department_created');
|
||||
Router::redirect('admin/departments');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Buffer::set('title', t('Create department'));
|
||||
44
pages/admin/departments/create(default).phtml
Normal file
44
pages/admin/departments/create(default).phtml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array<int, string> $errors
|
||||
* @var array $form
|
||||
*/
|
||||
|
||||
?>
|
||||
<div class="app-details-container">
|
||||
<section>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
|
||||
href="/admin/departments"><?php e(t('Departments')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('Create department')); ?>
|
||||
</div>
|
||||
<div class="app-details-titlebar">
|
||||
<h1>
|
||||
<a href="admin/departments" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>
|
||||
<?php e(t('Create department')); ?>
|
||||
</h1>
|
||||
<div class="app-details-titlebar-actions">
|
||||
<button type="submit" form="department-form" name="action" value="create" class="secondary outline">
|
||||
<?php e(t('Create')); ?>
|
||||
</button>
|
||||
<button type="submit" form="department-form" name="action" value="create_close" class="primary">
|
||||
<?php e(t('Create & close')); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="notice" data-variant="error">
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?php e($error); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$values = $form ?? [];
|
||||
$detailsOpenAll = true;
|
||||
require __DIR__ . '/_form.phtml';
|
||||
?>
|
||||
</section>
|
||||
</div>
|
||||
52
pages/admin/departments/data().php
Normal file
52
pages/admin/departments/data().php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\DepartmentService;
|
||||
use MintyPHP\Service\SettingService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::DEPARTMENTS_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$tenantIds = TenantScopeService::getUserTenantIds($currentUserId);
|
||||
|
||||
$limit = (int) ($_GET['limit'] ?? 10);
|
||||
$offset = (int) ($_GET['offset'] ?? 0);
|
||||
$search = trim((string) ($_GET['search'] ?? ''));
|
||||
$order = (string) ($_GET['order'] ?? 'description');
|
||||
$dir = (string) ($_GET['dir'] ?? 'asc');
|
||||
$tenant = trim((string) ($_GET['tenant'] ?? ''));
|
||||
|
||||
$result = DepartmentService::listPaged([
|
||||
'limit' => $limit,
|
||||
'offset' => $offset,
|
||||
'search' => $search,
|
||||
'order' => $order,
|
||||
'dir' => $dir,
|
||||
'tenant' => $tenant,
|
||||
'tenantIds' => $tenantIds,
|
||||
'tenantUserId' => $currentUserId,
|
||||
]);
|
||||
|
||||
$defaultDepartmentId = SettingService::getDefaultDepartmentId();
|
||||
$rows = [];
|
||||
foreach ($result['rows'] as $row) {
|
||||
$departmentId = (int) ($row['id'] ?? 0);
|
||||
$rows[] = [
|
||||
'id' => $row['id'] ?? null,
|
||||
'uuid' => $row['uuid'] ?? '',
|
||||
'is_default' => $departmentId > 0 && $defaultDepartmentId === $departmentId,
|
||||
'description' => $row['description'] ?? '',
|
||||
'tenants' => $row['tenant_labels'] ?? [],
|
||||
'created' => dt($row['created'] ?? ''),
|
||||
'modified' => dt($row['modified'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
Router::json([
|
||||
'data' => $rows,
|
||||
'total' => $result['total'] ?? 0,
|
||||
]);
|
||||
37
pages/admin/departments/delete($id).php
Normal file
37
pages/admin/departments/delete($id).php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\AuthService;
|
||||
use MintyPHP\Service\DepartmentService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::DEPARTMENTS_DELETE);
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
if ($uuid === '') {
|
||||
Router::redirect('admin/departments');
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$department = $uuid !== '' ? DepartmentService::findByUuid($uuid) : null;
|
||||
if ($department && !TenantScopeService::canAccess('departments', (int) ($department['id'] ?? 0), $currentUserId)) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = DepartmentService::deleteByUuid($uuid);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
Flash::error('Department not found', 'admin/departments', 'department_not_found');
|
||||
Router::redirect('admin/departments');
|
||||
}
|
||||
|
||||
if ($currentUserId > 0) {
|
||||
AuthService::loadTenantDataIntoSession($currentUserId);
|
||||
}
|
||||
|
||||
Flash::success('Department deleted', 'admin/departments', 'department_deleted');
|
||||
Router::redirect('admin/departments');
|
||||
111
pages/admin/departments/edit($id).php
Normal file
111
pages/admin/departments/edit($id).php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\DepartmentService;
|
||||
use MintyPHP\Repository\TenantDepartmentRepository;
|
||||
use MintyPHP\Service\TenantService;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\SettingService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::DEPARTMENTS_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
PermissionService::getUserPermissions($currentUserId);
|
||||
}
|
||||
$allowedTenantIds = TenantScopeService::getUserTenantIds($currentUserId);
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$department = $uuid !== '' ? DepartmentService::findByUuid($uuid) : null;
|
||||
if (!$department) {
|
||||
Flash::error('Department not found', 'admin/departments', 'department_not_found');
|
||||
Router::redirect('admin/departments');
|
||||
}
|
||||
|
||||
$departmentId = (int) ($department['id'] ?? 0);
|
||||
if (!TenantScopeService::canAccess('departments', $departmentId, $currentUserId)) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
$creatorId = (int) ($department['created_by'] ?? 0);
|
||||
if ($creatorId > 0) {
|
||||
$creator = UserService::findById($creatorId);
|
||||
if ($creator) {
|
||||
$creatorName = trim(($creator['first_name'] ?? '') . ' ' . ($creator['last_name'] ?? ''));
|
||||
$department['created_by_label'] = $creatorName !== '' ? $creatorName : ($creator['email'] ?? '');
|
||||
$department['created_by_uuid'] = $creator['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$modifierId = (int) ($department['modified_by'] ?? 0);
|
||||
if ($modifierId > 0) {
|
||||
$modifier = UserService::findById($modifierId);
|
||||
if ($modifier) {
|
||||
$modifierName = trim(($modifier['first_name'] ?? '') . ' ' . ($modifier['last_name'] ?? ''));
|
||||
$department['modified_by_label'] = $modifierName !== '' ? $modifierName : ($modifier['email'] ?? '');
|
||||
$department['modified_by_uuid'] = $modifier['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$form = $department;
|
||||
$tenants = TenantService::list();
|
||||
$selectedTenantIds = TenantDepartmentRepository::listTenantIdsByDepartmentId($departmentId);
|
||||
if ($allowedTenantIds) {
|
||||
$tenants = array_values(array_filter($tenants, static function (array $tenant) use ($allowedTenantIds): bool {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
return $tenantId > 0 && in_array($tenantId, $allowedTenantIds, true);
|
||||
}));
|
||||
$selectedTenantIds = array_values(array_intersect($selectedTenantIds, $allowedTenantIds));
|
||||
} elseif (TenantScopeService::isStrict()) {
|
||||
$tenants = [];
|
||||
$selectedTenantIds = [];
|
||||
}
|
||||
|
||||
if (isset($_POST['description'])) {
|
||||
if (!PermissionService::userHas($currentUserId, PermissionService::DEPARTMENTS_UPDATE)) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
$result = DepartmentService::updateFromAdmin($departmentId, $_POST, $currentUserId);
|
||||
$form = $result['form'] ?? $form;
|
||||
$errors = $result['errors'] ?? [];
|
||||
$tenantIds = $_POST['tenant_ids'] ?? [];
|
||||
if (!is_array($tenantIds)) {
|
||||
$tenantIds = [$tenantIds];
|
||||
}
|
||||
$selectedTenantIds = array_values(array_unique(array_map('intval', $tenantIds)));
|
||||
$selectedTenantIds = TenantScopeService::filterTenantIdsForUser($selectedTenantIds, $currentUserId);
|
||||
|
||||
if (TenantScopeService::isStrict() && !$selectedTenantIds) {
|
||||
$errors[] = t('Please select at least one tenant');
|
||||
}
|
||||
|
||||
if (($result['ok'] ?? false) && !$errors) {
|
||||
$existingTenantIds = TenantDepartmentRepository::listTenantIdsByDepartmentId($departmentId);
|
||||
$finalTenantIds = TenantScopeService::mergeTenantIdsPreservingOutOfScope(
|
||||
$selectedTenantIds,
|
||||
$existingTenantIds,
|
||||
$allowedTenantIds
|
||||
);
|
||||
$cleaned = DepartmentService::syncTenants($departmentId, $finalTenantIds);
|
||||
$action = (string) ($_POST['action'] ?? 'save');
|
||||
if ($cleaned > 0) {
|
||||
Flash::info(t('Department assignments cleaned: %d', $cleaned), "admin/departments/edit/{$uuid}", 'department_assignments_cleaned');
|
||||
}
|
||||
if ($action === 'save_close') {
|
||||
Flash::success('Department updated', 'admin/departments', 'department_updated');
|
||||
Router::redirect('admin/departments');
|
||||
} else {
|
||||
Flash::success('Department updated', "admin/departments/edit/{$uuid}", 'department_updated');
|
||||
Router::redirect("admin/departments/edit/{$uuid}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Buffer::set('title', t('Edit department'));
|
||||
167
pages/admin/departments/edit(default).phtml
Normal file
167
pages/admin/departments/edit(default).phtml
Normal file
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array<int, string> $errors
|
||||
* @var array $form
|
||||
* @var array $department
|
||||
*/
|
||||
|
||||
use MintyPHP\Session;
|
||||
|
||||
$values = $form ?? $department ?? [];
|
||||
$isReadOnly = !can('departments.update');
|
||||
|
||||
?>
|
||||
|
||||
<div class="app-details-container">
|
||||
<section>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
|
||||
href="/admin/departments"><?php e(t('Departments')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('Edit department')); ?>
|
||||
</div>
|
||||
<div class="app-details-titlebar">
|
||||
<h1>
|
||||
<a href="admin/departments" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>
|
||||
<?php e(t('Edit department')); ?>
|
||||
</h1>
|
||||
<div class="app-details-titlebar-actions">
|
||||
<?php if (can('departments.delete')): ?>
|
||||
<details class="dropdown">
|
||||
<summary role="button" class="outline secondary"><i class="bi bi-three-dots"></i></summary>
|
||||
<ul dir="rtl">
|
||||
<li>
|
||||
<form method="post" action="admin/departments/delete/<?php e($values['uuid'] ?? ''); ?>"
|
||||
onsubmit="return confirm('<?php e(t('Delete this department?')); ?>');">
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
<button type="submit" class="transparent">
|
||||
<?php e(t('Delete')); ?>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (can('departments.update')): ?>
|
||||
<button type="submit" form="department-form" name="action" value="save" class="secondary outline">
|
||||
<?php e(t('Save')); ?>
|
||||
</button>
|
||||
<button type="submit" form="department-form" name="action" value="save_close" class="primary">
|
||||
<?php e(t('Save & close')); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="app-details-errors">
|
||||
<div class="notice" data-variant="error">
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?php e($error); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$detailsOpenAll = false;
|
||||
$isReadOnly = $isReadOnly ?? false;
|
||||
require __DIR__ . '/_form.phtml';
|
||||
?>
|
||||
</section>
|
||||
<aside id="app-details-aside-section">
|
||||
<div class="app-details-aside-section">
|
||||
<hgroup>
|
||||
<h2><?php e($values['description'] ?? ''); ?></h2>
|
||||
<p><?php e(t('Department')); ?></p>
|
||||
</hgroup>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small><?php e(t('ID')); ?></small>
|
||||
<p>
|
||||
<span class="badge" data-variant="neutral" data-copy="true"
|
||||
data-copy-value="<?php e($values['id'] ?? ''); ?>">
|
||||
<?php e($values['id'] ?? '-'); ?>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$createdByLabel = $values['created_by_label'] ?? '';
|
||||
$createdById = $values['created_by'] ?? null;
|
||||
$createdByUuid = $values['created_by_uuid'] ?? null;
|
||||
$modifiedByLabel = $values['modified_by_label'] ?? '';
|
||||
$modifiedById = $values['modified_by'] ?? null;
|
||||
$modifiedByUuid = $values['modified_by_uuid'] ?? null;
|
||||
?>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small><?php e(t('Created')); ?></small>
|
||||
<p><?php e(dt($values['created'] ?? '') ?: '-'); ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
<?php e(t('Created by')); ?>
|
||||
</small>
|
||||
<p>
|
||||
<?php if ($createdByLabel !== ''): ?>
|
||||
<?php if ($createdByUuid): ?>
|
||||
<a href="admin/users/edit/<?php e($createdByUuid); ?>"><?php e($createdByLabel); ?></a>
|
||||
<?php else: ?>
|
||||
<?php e($createdByLabel); ?>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($createdById): ?>
|
||||
<?php e('#' . $createdById); ?>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small><?php e(t('Modified')); ?></small>
|
||||
<p><?php e(dt($values['modified'] ?? '') ?: '-'); ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
<?php e(t('Modified by')); ?>
|
||||
</small>
|
||||
<p>
|
||||
<?php if ($modifiedByLabel !== ''): ?>
|
||||
<?php if ($modifiedByUuid): ?>
|
||||
<a href="admin/users/edit/<?php e($modifiedByUuid); ?>"><?php e($modifiedByLabel); ?></a>
|
||||
<?php else: ?>
|
||||
<?php e($modifiedByLabel); ?>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($modifiedById): ?>
|
||||
<?php e('#' . $modifiedById); ?>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small>
|
||||
<?php e(t('UUID')); ?>
|
||||
</small>
|
||||
<p>
|
||||
<span class="badge" data-variant="neutral" data-copy="true"
|
||||
data-copy-value="<?php e($values['uuid'] ?? ''); ?>">
|
||||
<?php
|
||||
$uuidValue = (string) ($values['uuid'] ?? '');
|
||||
$uuidDisplay = $uuidValue !== '' ? substr($uuidValue, 0, 10) : '-';
|
||||
e($uuidDisplay);
|
||||
?>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
39
pages/admin/departments/index().php
Normal file
39
pages/admin/departments/index().php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Service\TenantService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::DEPARTMENTS_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
PermissionService::getUserPermissions($currentUserId);
|
||||
}
|
||||
|
||||
$tenants = TenantService::list();
|
||||
$allowedTenantIds = TenantScopeService::getUserTenantIds($currentUserId);
|
||||
if ($allowedTenantIds) {
|
||||
$tenants = array_values(array_filter($tenants, static function (array $tenant) use ($allowedTenantIds): bool {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
return $tenantId > 0 && in_array($tenantId, $allowedTenantIds, true);
|
||||
}));
|
||||
} elseif (TenantScopeService::isStrict()) {
|
||||
$tenants = [];
|
||||
}
|
||||
usort($tenants, static function ($a, $b) {
|
||||
return strcmp((string) ($a['description'] ?? ''), (string) ($b['description'] ?? ''));
|
||||
});
|
||||
|
||||
Buffer::set('title', t('Departments'));
|
||||
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
$csrfKey = Session::$csrfSessionKey;
|
||||
$csrfToken = $_SESSION[$csrfKey] ?? '';
|
||||
Buffer::set(
|
||||
'grid_csrf',
|
||||
json_encode(['key' => $csrfKey, 'token' => $csrfToken], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
);
|
||||
175
pages/admin/departments/index(default).phtml
Normal file
175
pages/admin/departments/index(default).phtml
Normal file
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
use MintyPHP\Router;
|
||||
|
||||
$canCreateDepartments = can('departments.create');
|
||||
$canDeleteDepartments = can('departments.delete');
|
||||
$activeTenant = trim((string) ($_GET['tenant'] ?? ''));
|
||||
$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('Departments')); ?>
|
||||
</div>
|
||||
<div class="app-list-titlebar">
|
||||
<h1><?php e(t('Departments')); ?></h1>
|
||||
<div class="app-list-titlebar-actions">
|
||||
<button
|
||||
class="outline secondary"
|
||||
type="button"
|
||||
data-toolbar-toggle
|
||||
data-toolbar-target="#departments-toolbar"
|
||||
data-toolbar-text-show="<?php e(t('Show filters')); ?>"
|
||||
data-toolbar-text-hide="<?php e(t('Hide filters')); ?>"
|
||||
>
|
||||
<i class="bi bi-funnel-fill"></i> <span data-toolbar-label><?php e(t('Hide filters')); ?></span>
|
||||
</button>
|
||||
<?php if ($canCreateDepartments): ?>
|
||||
<a role="button" class="primary" href="admin/departments/create">
|
||||
<i class="bi bi-plus"></i> <?php e(t('Create department')); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($showTenantTabs): ?>
|
||||
<div class="app-list-tabs" id="departments-tabs">
|
||||
<a href="admin/departments" 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/departments?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="departments-toolbar">
|
||||
<input type="hidden" id="department-tenant-filter" value="<?php e($activeTenant); ?>">
|
||||
<input type="search" id="department-search" placeholder="<?php e(t('Search...')); ?>">
|
||||
</div>
|
||||
<div class="app-list-table">
|
||||
<div id="departments-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 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 { buildUrl, postAction, badgeHtml } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
|
||||
|
||||
initToolbarToggles();
|
||||
|
||||
const appBase = "<?php e(localeBase()); ?>";
|
||||
const csrf = <?php MintyPHP\Buffer::get('grid_csrf'); ?>;
|
||||
const deleteConfirm = "<?php e(t('Delete this department?')); ?>";
|
||||
const buildActionUrl = (action, id) => buildUrl(appBase, `admin/departments/${action}/${id}`);
|
||||
|
||||
const initDepartmentsGrid = () => {
|
||||
const formatBadge = (value) => badgeHtml(gridjs, value);
|
||||
const buildBadgeList = (items) => {
|
||||
if (!Array.isArray(items) || items.length === 0) return '';
|
||||
const badges = items.map((label) => `<span class="badge" data-variant="neutral">${label}</span>`).join(' ');
|
||||
return `<div class="badge-list">${badges}</div>`;
|
||||
};
|
||||
const gridConfig = createServerGrid({
|
||||
gridjs: window.gridjs,
|
||||
container: '#departments-grid',
|
||||
dataUrl: 'admin/departments/data',
|
||||
appBase,
|
||||
columns: [
|
||||
{ name: "<?php e(t('Description')); ?>", sort: true },
|
||||
{
|
||||
name: "<?php e(t('Created')); ?>",
|
||||
sort: true,
|
||||
formatter: (cell) => formatBadge(cell)
|
||||
},
|
||||
{
|
||||
name: "<?php e(t('Modified')); ?>",
|
||||
sort: true,
|
||||
formatter: (cell) => formatBadge(cell)
|
||||
},
|
||||
{
|
||||
name: "UUID",
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
name: "ID",
|
||||
hidden: true
|
||||
}
|
||||
],
|
||||
sortColumns: ['description', 'created', 'modified', null, null],
|
||||
paginationLimit: 10,
|
||||
language: <?php MintyPHP\Buffer::get('grid_lang'); ?>,
|
||||
mapData: (data) => data.data.map((row) => [
|
||||
row.description,
|
||||
row.created,
|
||||
row.modified,
|
||||
row.uuid,
|
||||
row.id
|
||||
]),
|
||||
actions: {
|
||||
enabled: true,
|
||||
label: "<?php e(t('Actions')); ?>",
|
||||
getRowId: (row) => row?.cells?.[3]?.data,
|
||||
items: [
|
||||
{
|
||||
key: 'edit',
|
||||
type: 'link',
|
||||
label: "<i class=\"bi bi-pencil-fill\"></i>",
|
||||
href: ({ id }) => buildActionUrl('edit', id)
|
||||
},
|
||||
<?php if ($canDeleteDepartments): ?>
|
||||
{
|
||||
key: 'delete',
|
||||
label: "<i class=\"bi bi-trash3-fill\"></i>",
|
||||
className: 'danger outline',
|
||||
confirm: deleteConfirm
|
||||
}
|
||||
<?php endif; ?>
|
||||
],
|
||||
onAction: async ({ action, id, grid }) => {
|
||||
if (action !== 'delete') return;
|
||||
const url = buildActionUrl('delete', id);
|
||||
const response = await postAction(url, csrf);
|
||||
if (response.ok) {
|
||||
grid.forceRender();
|
||||
} else {
|
||||
window.location.href = new URL('admin/departments', appBase).toString();
|
||||
}
|
||||
}
|
||||
},
|
||||
search: {
|
||||
input: '#department-search',
|
||||
param: 'search',
|
||||
debounce: 250
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
input: '#department-tenant-filter',
|
||||
param: 'tenant'
|
||||
}
|
||||
],
|
||||
urlSync: true,
|
||||
rowDataset: (row) => ({
|
||||
uuid: row?.cells?.[4]?.data
|
||||
}),
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => {
|
||||
const uuid = rowData?.cells?.[3]?.data;
|
||||
return uuid ? new URL(`admin/departments/edit/${uuid}`, appBase).toString() : '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!gridConfig || !gridConfig.grid) {
|
||||
console.warn('Departments grid init failed');
|
||||
return null;
|
||||
}
|
||||
|
||||
return gridConfig;
|
||||
};
|
||||
|
||||
initDepartmentsGrid();
|
||||
</script>
|
||||
16
pages/admin/index($slug).php
Normal file
16
pages/admin/index($slug).php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Buffer;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$slug = trim((string) ($slug ?? ''));
|
||||
if ($slug === '') {
|
||||
require __DIR__ . '/index().php';
|
||||
return;
|
||||
}
|
||||
|
||||
http_response_code(404);
|
||||
Buffer::set('title', t('Page not found'));
|
||||
$notFound = true;
|
||||
10
pages/admin/index().php
Normal file
10
pages/admin/index().php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$user = $_SESSION['user'] ?? null;
|
||||
$first_name = $user['first_name'] ?? '';
|
||||
|
||||
Buffer::set('title', t('Admin'));
|
||||
25
pages/admin/index(default).phtml
Normal file
25
pages/admin/index(default).phtml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array|null $user
|
||||
* @var string $first_name
|
||||
*/
|
||||
|
||||
if (!empty($notFound)) {
|
||||
require __DIR__ . '/../error/not_found(error).phtml';
|
||||
return;
|
||||
}
|
||||
|
||||
?>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/logout">Login</a><i class="bi bi-chevron-right"></i><?php e(t('Home')); ?>
|
||||
</div>
|
||||
<div class="app-dashboard-titlebar">
|
||||
<h1><?php e(t('Welcome back')); ?> <?php e($first_name )?></h1>
|
||||
<div class="app-dashboard-titlebar-actions">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-dashboard">
|
||||
|
||||
</div>
|
||||
57
pages/admin/mail-log/data().php
Normal file
57
pages/admin/mail-log/data().php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\MailLogService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::MAIL_LOG_VIEW);
|
||||
|
||||
$limit = (int) ($_GET['limit'] ?? 10);
|
||||
$offset = (int) ($_GET['offset'] ?? 0);
|
||||
$search = trim((string) ($_GET['search'] ?? ''));
|
||||
$order = (string) ($_GET['order'] ?? 'created_at');
|
||||
$dir = (string) ($_GET['dir'] ?? 'desc');
|
||||
$status = trim((string) ($_GET['status'] ?? ''));
|
||||
$createdFrom = trim((string) ($_GET['created_from'] ?? ''));
|
||||
$createdTo = trim((string) ($_GET['created_to'] ?? ''));
|
||||
|
||||
$result = MailLogService::listPaged([
|
||||
'limit' => $limit,
|
||||
'offset' => $offset,
|
||||
'search' => $search,
|
||||
'order' => $order,
|
||||
'dir' => $dir,
|
||||
'status' => $status,
|
||||
'created_from' => $createdFrom,
|
||||
'created_to' => $createdTo,
|
||||
]);
|
||||
|
||||
$rows = [];
|
||||
foreach ($result['rows'] as $row) {
|
||||
$status = (string) ($row['status'] ?? '');
|
||||
$statusBadge = 'neutral';
|
||||
if ($status === 'sent') {
|
||||
$statusBadge = 'success';
|
||||
} elseif ($status === 'failed') {
|
||||
$statusBadge = 'danger';
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'id' => $row['id'] ?? null,
|
||||
'to_email' => $row['to_email'] ?? '',
|
||||
'subject' => $row['subject'] ?? '',
|
||||
'template' => $row['template'] ?? '',
|
||||
'status' => $status,
|
||||
'status_badge' => $statusBadge,
|
||||
'status_label' => t(ucfirst($status)),
|
||||
'created_at' => dt($row['created_at'] ?? ''),
|
||||
'sent_at' => dt($row['sent_at'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
Router::json([
|
||||
'data' => $rows,
|
||||
'total' => $result['total'] ?? 0,
|
||||
]);
|
||||
23
pages/admin/mail-log/index().php
Normal file
23
pages/admin/mail-log/index().php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::MAIL_LOG_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
PermissionService::getUserPermissions($currentUserId);
|
||||
}
|
||||
|
||||
Buffer::set('title', t('Mail logs'));
|
||||
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
$csrfKey = Session::$csrfSessionKey;
|
||||
$csrfToken = $_SESSION[$csrfKey] ?? '';
|
||||
Buffer::set(
|
||||
'grid_csrf',
|
||||
json_encode(['key' => $csrfKey, 'token' => $csrfToken], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
);
|
||||
174
pages/admin/mail-log/index(default).phtml
Normal file
174
pages/admin/mail-log/index(default).phtml
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
use MintyPHP\Router;
|
||||
?>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><?php e(t('Mail logs')); ?>
|
||||
</div>
|
||||
<div class="app-list-titlebar">
|
||||
<h1><?php e(t('Mail logs')); ?></h1>
|
||||
<div class="app-list-titlebar-actions">
|
||||
<button
|
||||
class="outline secondary"
|
||||
type="button"
|
||||
data-toolbar-toggle
|
||||
data-toolbar-target="#mail-log-toolbar"
|
||||
data-toolbar-text-show="<?php e(t('Show filters')); ?>"
|
||||
data-toolbar-text-hide="<?php e(t('Hide filters')); ?>"
|
||||
>
|
||||
<i class="bi bi-funnel-fill"></i> <span data-toolbar-label><?php e(t('Hide filters')); ?></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-list-toolbar" id="mail-log-toolbar">
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Search')); ?></span>
|
||||
<input type="search" id="mail-log-search" placeholder="<?php e(t('Search...')); ?>">
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Status')); ?></span>
|
||||
<select id="mail-log-status-filter">
|
||||
<option value=""><?php e(t('All statuses')); ?></option>
|
||||
<option value="sent"><?php e(t('Sent')); ?></option>
|
||||
<option value="queued"><?php e(t('Queued')); ?></option>
|
||||
<option value="failed"><?php e(t('Failed')); ?></option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Created from')); ?></span>
|
||||
<input type="date" id="mail-log-created-from">
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Created to')); ?></span>
|
||||
<input type="date" id="mail-log-created-to">
|
||||
</label>
|
||||
</div>
|
||||
<div class="app-list-table">
|
||||
<div id="mail-log-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 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 { buildUrl, badgeHtml } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
|
||||
|
||||
initToolbarToggles();
|
||||
|
||||
const appBase = "<?php e(localeBase()); ?>";
|
||||
|
||||
const initMailLogGrid = () => {
|
||||
const formatBadge = (value) => badgeHtml(gridjs, value);
|
||||
const gridConfig = createServerGrid({
|
||||
gridjs: window.gridjs,
|
||||
container: '#mail-log-grid',
|
||||
dataUrl: 'admin/mail-log/data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
name: "<?php e(t('Created')); ?>",
|
||||
sort: true,
|
||||
formatter: (cell) => formatBadge(cell)
|
||||
},
|
||||
{
|
||||
name: "<?php e(t('Recipient')); ?>",
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
if (!cell || typeof cell !== 'object') {
|
||||
return gridjs.html(String(cell ?? ''));
|
||||
}
|
||||
return gridjs.html(String(cell ?? ''));
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "<?php e(t('Subject')); ?>",
|
||||
sort: true
|
||||
},
|
||||
{
|
||||
name: "<?php e(t('Template')); ?>",
|
||||
sort: false
|
||||
},
|
||||
{
|
||||
name: "<?php e(t('Status')); ?>",
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
if (!cell || typeof cell !== 'object') {
|
||||
return gridjs.html('');
|
||||
}
|
||||
const variant = cell.variant || 'neutral';
|
||||
const label = cell.label || '';
|
||||
return gridjs.html(`<span class="badge" data-variant="${variant}">${label}</span>`);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "ID",
|
||||
hidden: true
|
||||
}
|
||||
],
|
||||
sortColumns: ['created_at', 'to_email', 'subject', null, 'status', null],
|
||||
paginationLimit: 10,
|
||||
language: <?php MintyPHP\Buffer::get('grid_lang'); ?>,
|
||||
mapData: (data) => data.data.map((row) => [
|
||||
row.created_at,
|
||||
row.to_email,
|
||||
row.subject,
|
||||
row.template || '-',
|
||||
{
|
||||
variant: row.status_badge,
|
||||
label: row.status_label
|
||||
},
|
||||
row.id
|
||||
]),
|
||||
actions: {
|
||||
enabled: true,
|
||||
label: "<?php e(t('Actions')); ?>",
|
||||
getRowId: (row) => row?.cells?.[5]?.data,
|
||||
items: [
|
||||
{
|
||||
key: 'view',
|
||||
type: 'link',
|
||||
label: "<i class=\"bi bi-eye-fill\"></i>",
|
||||
href: ({ id }) => buildUrl(appBase, `admin/mail-log/view/${id}`)
|
||||
}
|
||||
]
|
||||
},
|
||||
search: {
|
||||
input: '#mail-log-search',
|
||||
param: 'search',
|
||||
debounce: 250
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
input: '#mail-log-status-filter',
|
||||
param: 'status'
|
||||
},
|
||||
{
|
||||
input: '#mail-log-created-from',
|
||||
param: 'created_from',
|
||||
normalize: (value) => (value ? value : '')
|
||||
},
|
||||
{
|
||||
input: '#mail-log-created-to',
|
||||
param: 'created_to',
|
||||
normalize: (value) => (value ? value : '')
|
||||
}
|
||||
],
|
||||
urlSync: true,
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => {
|
||||
const id = rowData?.cells?.[5]?.data;
|
||||
return id ? new URL(`admin/mail-log/view/${id}`, appBase).toString() : '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!gridConfig || !gridConfig.grid) {
|
||||
console.warn('Mail log grid init failed');
|
||||
return null;
|
||||
}
|
||||
|
||||
return gridConfig;
|
||||
};
|
||||
|
||||
initMailLogGrid();
|
||||
</script>
|
||||
25
pages/admin/mail-log/view($id).php
Normal file
25
pages/admin/mail-log/view($id).php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\MailLogService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::MAIL_LOG_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
PermissionService::getUserPermissions($currentUserId);
|
||||
}
|
||||
|
||||
$mailLogId = (int) ($id ?? 0);
|
||||
$mailLog = $mailLogId > 0 ? MailLogService::find($mailLogId) : null;
|
||||
if (!$mailLog) {
|
||||
Flash::error('Mail log not found', 'admin/mail-log', 'mail_log_not_found');
|
||||
Router::redirect('admin/mail-log');
|
||||
}
|
||||
|
||||
Buffer::set('title', t('View mail log'));
|
||||
137
pages/admin/mail-log/view(default).phtml
Normal file
137
pages/admin/mail-log/view(default).phtml
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array $mailLog
|
||||
*/
|
||||
|
||||
$mailLog = $mailLog ?? [];
|
||||
$status = (string) ($mailLog['status'] ?? '');
|
||||
$statusBadge = 'neutral';
|
||||
if ($status === 'sent') {
|
||||
$statusBadge = 'success';
|
||||
} elseif ($status === 'failed') {
|
||||
$statusBadge = 'danger';
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<div class="app-details-container">
|
||||
<section>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
|
||||
href="/admin/mail-log"><?php e(t('Mail logs')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('View')); ?>
|
||||
</div>
|
||||
<div class="app-details-titlebar">
|
||||
<h1>
|
||||
<a href="admin/mail-log" title="<?php e(t('Back')); ?>"><i class="bi bi-arrow-left"></i></a>
|
||||
<?php e(t('View mail log')); ?>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="app-details-content">
|
||||
<details open>
|
||||
<summary><?php e(t('Email details')); ?></summary>
|
||||
<hr>
|
||||
<div class="app-details-section">
|
||||
<div class="grid">
|
||||
<div class="app-form-group">
|
||||
<label><?php e(t('Recipient')); ?></label>
|
||||
<input type="text" value="<?php e($mailLog['to_email'] ?? ''); ?>" readonly>
|
||||
</div>
|
||||
<div class="app-form-group">
|
||||
<label><?php e(t('Subject')); ?></label>
|
||||
<input type="text" value="<?php e($mailLog['subject'] ?? ''); ?>" readonly>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="app-form-group">
|
||||
<label><?php e(t('Template')); ?></label>
|
||||
<input type="text" value="<?php e($mailLog['template'] ?? '-'); ?>" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<?php if ($status === 'sent'): ?>
|
||||
<hr>
|
||||
<details open>
|
||||
<summary><?php e(t('Delivery information')); ?></summary>
|
||||
<hr>
|
||||
<div class="app-details-section">
|
||||
<div class="app-form-group">
|
||||
<label><?php e(t('Sent at')); ?></label>
|
||||
<input type="text" value="<?php e(dt($mailLog['sent_at'] ?? '')); ?>" readonly>
|
||||
</div>
|
||||
<?php if (!empty($mailLog['provider_message_id'])): ?>
|
||||
<div class="app-form-group">
|
||||
<label><?php e(t('Provider message ID')); ?></label>
|
||||
<input type="text" value="<?php e($mailLog['provider_message_id'] ?? ''); ?>" readonly>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($status === 'failed' && !empty($mailLog['error_message'])): ?>
|
||||
<hr>
|
||||
<details open>
|
||||
<summary><?php e(t('Error information')); ?></summary>
|
||||
<hr>
|
||||
<div class="app-details-section">
|
||||
<div class="app-form-group">
|
||||
<label><?php e(t('Error message')); ?></label>
|
||||
<textarea readonly rows="6"><?php e($mailLog['error_message'] ?? ''); ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
<aside id="app-details-aside-section">
|
||||
<div class="app-details-aside-section">
|
||||
<hgroup>
|
||||
<strong><?php e($mailLog['to_email'] ?? ''); ?></strong>
|
||||
<p><small><?php e(t('Mail log')); ?></small></p>
|
||||
</hgroup>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small><?php e(t('ID')); ?></small>
|
||||
<p>
|
||||
<span class="badge" data-variant="neutral">
|
||||
<?php e($mailLog['id'] ?? '-'); ?>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
<?php e(t('Status')); ?>
|
||||
</small>
|
||||
<div>
|
||||
<span class="badge" data-variant="<?php e($statusBadge); ?>">
|
||||
<?php e(t(ucfirst($status))); ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<small><?php e(t('Created')); ?></small>
|
||||
<p><?php e(dt($mailLog['created_at'] ?? '') ?: '-'); ?></p>
|
||||
</div>
|
||||
<?php if ($status === 'sent' && !empty($mailLog['sent_at'])): ?>
|
||||
<div>
|
||||
<small><?php e(t('Sent')); ?></small>
|
||||
<p><?php e(dt($mailLog['sent_at'] ?? '') ?: '-'); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($mailLog['provider_message_id'])): ?>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small><?php e(t('Provider message ID')); ?></small>
|
||||
<span class="badge" data-copy="true" data-copy-value="<?php e($mailLog['provider_message_id']); ?>"
|
||||
data-variant="neutral"><?php e(substr($mailLog['provider_message_id'], 0, 20)); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
38
pages/admin/permissions/_form.phtml
Normal file
38
pages/admin/permissions/_form.phtml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array $values
|
||||
* @var string|null $formId
|
||||
* @var bool $detailsOpenAll
|
||||
* @var bool $isReadOnly
|
||||
*/
|
||||
|
||||
use MintyPHP\Session;
|
||||
|
||||
$values = $values ?? [];
|
||||
$formId = $formId ?? 'permission-form';
|
||||
$detailsOpenAll = $detailsOpenAll ?? false;
|
||||
$isReadOnly = $isReadOnly ?? false;
|
||||
$detailsOpenAttr = $detailsOpenAll ? 'open' : '';
|
||||
$readonlyAttr = $isReadOnly ? 'readonly' : '';
|
||||
|
||||
?>
|
||||
<form id="<?php e($formId); ?>" method="post">
|
||||
<details name="basic-data" <?php e($detailsOpenAttr); ?>>
|
||||
<summary>Stammdaten</summary>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<label for="permission-description">
|
||||
<span><?php e(t('Description')); ?></span>
|
||||
<input type="text" name="description" id="permission-description"
|
||||
value="<?php e($values['description'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
<label for="permission-key">
|
||||
<span><?php e(t('Permission key')); ?></span>
|
||||
<input required type="text" name="key" id="permission-key" value="<?php e($values['key'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
<hr>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
42
pages/admin/permissions/create().php
Normal file
42
pages/admin/permissions/create().php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::PERMISSIONS_CREATE);
|
||||
|
||||
$errors = [];
|
||||
$form = [
|
||||
'key' => '',
|
||||
'description' => '',
|
||||
];
|
||||
|
||||
if (isset($_POST['key'])) {
|
||||
$result = PermissionService::createFromAdmin($_POST);
|
||||
$form = $result['form'] ?? $form;
|
||||
$errors = $result['errors'] ?? [];
|
||||
|
||||
if ($result['ok'] ?? false) {
|
||||
$action = (string) ($_POST['action'] ?? 'create');
|
||||
if ($action === 'create_close') {
|
||||
Flash::success('Permission created', 'admin/permissions', 'permission_created');
|
||||
Router::redirect('admin/permissions');
|
||||
} else {
|
||||
$id = (int) ($result['id'] ?? 0);
|
||||
if ($id > 0) {
|
||||
$target = "admin/permissions/edit/{$id}";
|
||||
Flash::success('Permission created', $target, 'permission_created');
|
||||
Router::redirect($target);
|
||||
} else {
|
||||
Flash::success('Permission created', 'admin/permissions', 'permission_created');
|
||||
Router::redirect('admin/permissions');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Buffer::set('title', t('Create permission'));
|
||||
44
pages/admin/permissions/create(default).phtml
Normal file
44
pages/admin/permissions/create(default).phtml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array<int, string> $errors
|
||||
* @var array $form
|
||||
*/
|
||||
|
||||
?>
|
||||
<div class="app-details-container">
|
||||
<section>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
|
||||
href="/admin/permissions"><?php e(t('Permissions')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('Create permission')); ?>
|
||||
</div>
|
||||
<div class="app-details-titlebar">
|
||||
<h1>
|
||||
<a href="admin/permissions" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>
|
||||
<?php e(t('Create permission')); ?>
|
||||
</h1>
|
||||
<div class="app-details-titlebar-actions">
|
||||
<button type="submit" form="permission-form" name="action" value="create" class="secondary outline">
|
||||
<?php e(t('Create')); ?>
|
||||
</button>
|
||||
<button type="submit" form="permission-form" name="action" value="create_close" class="primary">
|
||||
<?php e(t('Create & close')); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="notice" data-variant="error">
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?php e($error); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$values = $form ?? [];
|
||||
$detailsOpenAll = true;
|
||||
require __DIR__ . '/_form.phtml';
|
||||
?>
|
||||
</section>
|
||||
</div>
|
||||
41
pages/admin/permissions/data().php
Normal file
41
pages/admin/permissions/data().php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::PERMISSIONS_VIEW);
|
||||
if (!isset($_SESSION['user'])) {
|
||||
http_response_code(401);
|
||||
Router::json(['data' => [], 'total' => 0]);
|
||||
}
|
||||
|
||||
$limit = (int) ($_GET['limit'] ?? 10);
|
||||
$offset = (int) ($_GET['offset'] ?? 0);
|
||||
$search = trim((string) ($_GET['search'] ?? ''));
|
||||
$order = (string) ($_GET['order'] ?? 'key');
|
||||
$dir = (string) ($_GET['dir'] ?? 'asc');
|
||||
|
||||
$result = PermissionService::listPaged([
|
||||
'limit' => $limit,
|
||||
'offset' => $offset,
|
||||
'search' => $search,
|
||||
'order' => $order,
|
||||
'dir' => $dir,
|
||||
]);
|
||||
|
||||
$rows = [];
|
||||
foreach ($result['data'] ?? [] as $row) {
|
||||
$rows[] = [
|
||||
'id' => $row['id'] ?? null,
|
||||
'key' => $row['key'] ?? '',
|
||||
'description' => $row['description'] ?? '',
|
||||
'created' => dt($row['created'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
Router::json([
|
||||
'data' => $rows,
|
||||
'total' => $result['total'] ?? 0,
|
||||
]);
|
||||
23
pages/admin/permissions/delete($id).php
Normal file
23
pages/admin/permissions/delete($id).php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::PERMISSIONS_DELETE);
|
||||
|
||||
$id = (int) ($id ?? 0);
|
||||
if ($id <= 0) {
|
||||
Router::redirect('admin/permissions');
|
||||
}
|
||||
|
||||
$result = PermissionService::deleteById($id);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
Flash::error('Permission not found', 'admin/permissions', 'permission_not_found');
|
||||
Router::redirect('admin/permissions');
|
||||
}
|
||||
|
||||
Flash::success('Permission deleted', 'admin/permissions', 'permission_deleted');
|
||||
Router::redirect('admin/permissions');
|
||||
64
pages/admin/permissions/edit($id).php
Normal file
64
pages/admin/permissions/edit($id).php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Repository\RolePermissionRepository;
|
||||
use MintyPHP\Repository\RoleRepository;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::PERMISSIONS_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
PermissionService::getUserPermissions($currentUserId);
|
||||
}
|
||||
|
||||
$id = (int) ($id ?? 0);
|
||||
$permission = $id > 0 ? PermissionService::find($id) : null;
|
||||
if (!$permission) {
|
||||
Flash::error('Permission not found', 'admin/permissions', 'permission_not_found');
|
||||
Router::redirect('admin/permissions');
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$form = $permission;
|
||||
$roles = RoleRepository::list();
|
||||
$selectedRoleIds = RolePermissionRepository::listRoleIdsByPermissionId($id);
|
||||
|
||||
if (isset($_POST['key'])) {
|
||||
if (!PermissionService::userHas($currentUserId, PermissionService::PERMISSIONS_UPDATE)) {
|
||||
Flash::error('Permission denied', "admin/permissions/edit/{$id}", 'permission_denied');
|
||||
Router::redirect("admin/permissions/edit/{$id}");
|
||||
return;
|
||||
}
|
||||
$result = PermissionService::updateFromAdmin($id, $_POST);
|
||||
$form = $result['form'] ?? $form;
|
||||
$errors = $result['errors'] ?? [];
|
||||
$selectedRoleIds = $_POST['role_ids'] ?? $selectedRoleIds;
|
||||
if (!is_array($selectedRoleIds)) {
|
||||
$selectedRoleIds = [$selectedRoleIds];
|
||||
}
|
||||
$selectedRoleIds = array_values(array_unique(array_map('intval', $selectedRoleIds)));
|
||||
|
||||
if ($result['ok'] ?? false) {
|
||||
$roleIds = $_POST['role_ids'] ?? [];
|
||||
if (!is_array($roleIds)) {
|
||||
$roleIds = [$roleIds];
|
||||
}
|
||||
$roleIds = array_values(array_unique(array_map('intval', $roleIds)));
|
||||
RolePermissionRepository::replaceForPermission($id, $roleIds);
|
||||
$action = (string) ($_POST['action'] ?? 'save');
|
||||
if ($action === 'save_close') {
|
||||
Flash::success('Permission updated', 'admin/permissions', 'permission_updated');
|
||||
Router::redirect('admin/permissions');
|
||||
} else {
|
||||
Flash::success('Permission updated', "admin/permissions/edit/{$id}", 'permission_updated');
|
||||
Router::redirect("admin/permissions/edit/{$id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Buffer::set('title', t('Edit permission'));
|
||||
79
pages/admin/permissions/edit(default).phtml
Normal file
79
pages/admin/permissions/edit(default).phtml
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array<int, string> $errors
|
||||
* @var array $form
|
||||
* @var array $permission
|
||||
*/
|
||||
|
||||
?>
|
||||
<div class="app-details-container">
|
||||
<section>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
|
||||
href="/admin/permissions"><?php e(t('Permissions')); ?></a><i
|
||||
class="bi bi-chevron-right"></i><?php e(t('Edit permission')); ?>
|
||||
</div>
|
||||
<div class="app-details-titlebar">
|
||||
<h1>
|
||||
<a href="admin/permissions" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>
|
||||
<?php e(t('Edit permission')); ?>
|
||||
</h1>
|
||||
<div class="app-details-titlebar-actions">
|
||||
<?php if (can('permissions.delete')): ?>
|
||||
<details class="dropdown">
|
||||
<summary role="button" class="outline secondary"><i class="bi bi-three-dots"></i></summary>
|
||||
<ul dir="rtl">
|
||||
<li>
|
||||
<form method="post" action="admin/permissions/delete/<?php e((string) ($permission['id'] ?? '')); ?>"
|
||||
onsubmit="return confirm('<?php e(t('Delete this permission?')); ?>');">
|
||||
<?php MintyPHP\Session::getCsrfInput(); ?>
|
||||
<button type="submit" class="transparent">
|
||||
<?php e(t('Delete')); ?>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (can('permissions.update')): ?>
|
||||
<button type="submit" form="permission-form" name="action" value="save" class="secondary outline">
|
||||
<?php e(t('Save')); ?>
|
||||
</button>
|
||||
<button type="submit" form="permission-form" name="action" value="save_close" class="primary">
|
||||
<?php e(t('Save & close')); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="notice" data-variant="error">
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?php e($error); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$values = $form ?? $permission ?? [];
|
||||
$isReadOnly = !can('permissions.update');
|
||||
$detailsOpenAll = true;
|
||||
$isReadOnly = $isReadOnly ?? false;
|
||||
require __DIR__ . '/_form.phtml';
|
||||
?>
|
||||
</section>
|
||||
<aside id="app-details-aside-section">
|
||||
<div class="app-details-aside-section">
|
||||
<hgroup>
|
||||
<h2><?php e($values['key'] ?? ''); ?></h2>
|
||||
<p><?php e(t('Permission')); ?></p>
|
||||
</hgroup>
|
||||
<hr>
|
||||
<?php if (!empty($roles)): ?>
|
||||
<?php multiSelectForm('permission-roles', 'role_ids', 'Assigned roles', 'Select roles', $roles, $selectedRoleIds ?? [], $isReadOnly, 'description', 'permission-form'); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
23
pages/admin/permissions/index().php
Normal file
23
pages/admin/permissions/index().php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::PERMISSIONS_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
PermissionService::getUserPermissions($currentUserId);
|
||||
}
|
||||
|
||||
Buffer::set('title', t('Permissions'));
|
||||
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
$csrfKey = Session::$csrfSessionKey;
|
||||
$csrfToken = $_SESSION[$csrfKey] ?? '';
|
||||
Buffer::set(
|
||||
'grid_csrf',
|
||||
json_encode(['key' => $csrfKey, 'token' => $csrfToken], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
);
|
||||
121
pages/admin/permissions/index(default).phtml
Normal file
121
pages/admin/permissions/index(default).phtml
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
use MintyPHP\Router;
|
||||
|
||||
$canCreatePermissions = can('permissions.create');
|
||||
$canUpdatePermissions = can('permissions.update');
|
||||
$canDeletePermissions = can('permissions.delete');
|
||||
?>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><?php e(t('Permissions')); ?>
|
||||
</div>
|
||||
<div class="app-list-titlebar">
|
||||
<h1><?php e(t('Permissions')); ?></h1>
|
||||
<div class="app-list-titlebar-actions">
|
||||
<button
|
||||
class="outline secondary"
|
||||
type="button"
|
||||
data-toolbar-toggle
|
||||
data-toolbar-target="#permissions-toolbar"
|
||||
data-toolbar-text-show="<?php e(t('Show filters')); ?>"
|
||||
data-toolbar-text-hide="<?php e(t('Hide filters')); ?>"
|
||||
>
|
||||
<i class="bi bi-funnel-fill"></i> <span data-toolbar-label><?php e(t('Hide filters')); ?></span>
|
||||
</button>
|
||||
<?php if ($canCreatePermissions): ?>
|
||||
<a role="button" class="primary" href="admin/permissions/create">
|
||||
<i class="bi bi-plus"></i> <?php e(t('Create permission')); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-list-toolbar" id="permissions-toolbar">
|
||||
<input type="search" id="permission-search" placeholder="<?php e(t('Search...')); ?>">
|
||||
</div>
|
||||
<div class="app-list-table">
|
||||
<div id="permissions-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 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 { buildUrl, postAction, badgeHtml } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
|
||||
|
||||
initToolbarToggles();
|
||||
|
||||
const appBase = "<?php e(localeBase()); ?>";
|
||||
const csrf = <?php MintyPHP\Buffer::get('grid_csrf'); ?>;
|
||||
const deleteConfirm = "<?php e(t('Delete this permission?')); ?>";
|
||||
const buildActionUrl = (action, id) => buildUrl(appBase, `admin/permissions/${action}/${id}`);
|
||||
|
||||
const initPermissionsGrid = () => {
|
||||
const gridConfig = createServerGrid({
|
||||
gridjs: window.gridjs,
|
||||
container: '#permissions-grid',
|
||||
dataUrl: 'admin/permissions/data',
|
||||
appBase,
|
||||
columns: [
|
||||
{ name: "<?php e(t('Description')); ?>", sort: true },
|
||||
{ name: "<?php e(t('Permission key')); ?>", sort: true },
|
||||
{ name: "<?php e(t('Created')); ?>", sort: true, formatter: (cell) => badgeHtml(gridjs, cell) },
|
||||
{ name: 'ID', hidden: true }
|
||||
],
|
||||
sortColumns: ['description', 'key', 'created', null],
|
||||
paginationLimit: 10,
|
||||
language: <?php MintyPHP\Buffer::get('grid_lang'); ?>,
|
||||
mapData: (data) => data.data.map((row) => [
|
||||
row.description,
|
||||
row.key,
|
||||
row.created,
|
||||
row.id
|
||||
]),
|
||||
actions: {
|
||||
enabled: true,
|
||||
label: "<?php e(t('Actions')); ?>",
|
||||
getRowId: (row) => row?.cells?.[3]?.data,
|
||||
items: [
|
||||
{
|
||||
key: 'edit',
|
||||
type: 'link',
|
||||
label: "<i class=\"bi bi-pencil-fill\"></i>",
|
||||
href: ({ id }) => buildActionUrl('edit', id)
|
||||
},
|
||||
<?php if ($canDeletePermissions): ?>
|
||||
{
|
||||
key: 'delete',
|
||||
label: "<i class=\"bi bi-trash3-fill\"></i>",
|
||||
className: 'danger outline',
|
||||
confirm: deleteConfirm
|
||||
}
|
||||
<?php endif; ?>
|
||||
],
|
||||
onAction: async ({ action, id, grid }) => {
|
||||
if (action !== 'delete') return;
|
||||
const url = buildActionUrl('delete', id);
|
||||
const response = await postAction(url, csrf);
|
||||
if (response.ok) {
|
||||
grid.forceRender();
|
||||
} else {
|
||||
window.location.href = new URL('admin/permissions', appBase).toString();
|
||||
}
|
||||
}
|
||||
},
|
||||
rowDblClick: {
|
||||
getUrl: (row) => {
|
||||
const id = row?.cells?.[3]?.data;
|
||||
return id ? buildActionUrl('edit', id) : '';
|
||||
}
|
||||
},
|
||||
search: {
|
||||
input: '#permission-search',
|
||||
param: 'search',
|
||||
debounce: 250
|
||||
}
|
||||
});
|
||||
|
||||
return gridConfig;
|
||||
};
|
||||
|
||||
initPermissionsGrid();
|
||||
</script>
|
||||
39
pages/admin/roles/_form.phtml
Normal file
39
pages/admin/roles/_form.phtml
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array $values
|
||||
* @var string|null $formId
|
||||
* @var bool $detailsOpenAll
|
||||
* @var bool $isReadOnly
|
||||
*/
|
||||
|
||||
use MintyPHP\Session;
|
||||
|
||||
$values = $values ?? [];
|
||||
$formId = $formId ?? 'role-form';
|
||||
$detailsOpenAll = $detailsOpenAll ?? false;
|
||||
$isReadOnly = $isReadOnly ?? false;
|
||||
$detailsOpenAttr = $detailsOpenAll ? 'open' : '';
|
||||
$readonlyAttr = $isReadOnly ? 'readonly' : '';
|
||||
|
||||
?>
|
||||
<form id="<?php e($formId); ?>" method="post">
|
||||
<details name="basic-data" open>
|
||||
<summary>Stammdaten</summary>
|
||||
<hr>
|
||||
<label for="description">
|
||||
<span><?php e(t('Description')); ?></span>
|
||||
<input required type="text" name="description" id="description" value="<?php e($values['description'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
</details>
|
||||
<hr>
|
||||
<?php if (!empty($permissions)): ?>
|
||||
<details name="permissions" <?php e($detailsOpenAttr); ?>>
|
||||
<summary><?php e(t('Permissions')); ?></summary>
|
||||
<hr>
|
||||
<?php multiSelectForm('role-permissions', 'permission_ids', 'Assigned permissions', 'Select permissions', $permissions, $selectedPermissionIds ?? [], $isReadOnly, ['description', 'key']); ?>
|
||||
</details>
|
||||
<hr>
|
||||
<?php endif; ?>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
61
pages/admin/roles/create().php
Normal file
61
pages/admin/roles/create().php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Repository\PermissionRepository;
|
||||
use MintyPHP\Repository\RolePermissionRepository;
|
||||
use MintyPHP\Service\RoleService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::ROLES_CREATE);
|
||||
|
||||
$errors = [];
|
||||
$form = [
|
||||
'description' => '',
|
||||
];
|
||||
$permissions = PermissionRepository::list();
|
||||
$selectedPermissionIds = [];
|
||||
|
||||
if (isset($_POST['description'])) {
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$result = RoleService::createFromAdmin($_POST, $currentUserId);
|
||||
$form = $result['form'] ?? $form;
|
||||
$errors = $result['errors'] ?? [];
|
||||
$selectedPermissionIds = $_POST['permission_ids'] ?? $selectedPermissionIds;
|
||||
if (!is_array($selectedPermissionIds)) {
|
||||
$selectedPermissionIds = [$selectedPermissionIds];
|
||||
}
|
||||
$selectedPermissionIds = array_values(array_unique(array_map('intval', $selectedPermissionIds)));
|
||||
|
||||
if ($result['ok'] ?? false) {
|
||||
$permissionIds = $_POST['permission_ids'] ?? [];
|
||||
if (!is_array($permissionIds)) {
|
||||
$permissionIds = [$permissionIds];
|
||||
}
|
||||
$permissionIds = array_values(array_unique(array_map('intval', $permissionIds)));
|
||||
$createdId = (int) ($result['id'] ?? 0);
|
||||
if ($createdId > 0) {
|
||||
RolePermissionRepository::replaceForRole($createdId, $permissionIds);
|
||||
}
|
||||
$action = (string) ($_POST['action'] ?? 'create');
|
||||
if ($action === 'create_close') {
|
||||
Flash::success('Role created', 'admin/roles', 'role_created');
|
||||
Router::redirect('admin/roles');
|
||||
} else {
|
||||
$uuid = (string) ($result['uuid'] ?? '');
|
||||
if ($uuid !== '') {
|
||||
$target = "admin/roles/edit/{$uuid}";
|
||||
Flash::success('Role created', $target, 'role_created');
|
||||
Router::redirect($target);
|
||||
} else {
|
||||
Flash::success('Role created', 'admin/roles', 'role_created');
|
||||
Router::redirect('admin/roles');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Buffer::set('title', t('Create role'));
|
||||
44
pages/admin/roles/create(default).phtml
Normal file
44
pages/admin/roles/create(default).phtml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array<int, string> $errors
|
||||
* @var array $form
|
||||
*/
|
||||
|
||||
?>
|
||||
<div class="app-details-container">
|
||||
<section>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
|
||||
href="/admin/roles"><?php e(t('Roles')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('Create role')); ?>
|
||||
</div>
|
||||
<div class="app-details-titlebar">
|
||||
<h1>
|
||||
<a href="admin/roles" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>
|
||||
<?php e(t('Create role')); ?>
|
||||
</h1>
|
||||
<div class="app-details-titlebar-actions">
|
||||
<button type="submit" form="role-form" name="action" value="create" class="secondary outline">
|
||||
<?php e(t('Create')); ?>
|
||||
</button>
|
||||
<button type="submit" form="role-form" name="action" value="create_close" class="primary">
|
||||
<?php e(t('Create & close')); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="notice" data-variant="error">
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?php e($error); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$values = $form ?? [];
|
||||
$detailsOpenAll = true;
|
||||
require __DIR__ . '/_form.phtml';
|
||||
?>
|
||||
</section>
|
||||
</div>
|
||||
43
pages/admin/roles/data().php
Normal file
43
pages/admin/roles/data().php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\RoleService;
|
||||
use MintyPHP\Service\SettingService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::ROLES_VIEW);
|
||||
|
||||
$limit = (int) ($_GET['limit'] ?? 10);
|
||||
$offset = (int) ($_GET['offset'] ?? 0);
|
||||
$search = trim((string) ($_GET['search'] ?? ''));
|
||||
$order = (string) ($_GET['order'] ?? 'description');
|
||||
$dir = (string) ($_GET['dir'] ?? 'asc');
|
||||
|
||||
$result = RoleService::listPaged([
|
||||
'limit' => $limit,
|
||||
'offset' => $offset,
|
||||
'search' => $search,
|
||||
'order' => $order,
|
||||
'dir' => $dir,
|
||||
]);
|
||||
|
||||
$defaultRoleId = SettingService::getDefaultRoleId();
|
||||
$rows = [];
|
||||
foreach ($result['rows'] as $row) {
|
||||
$roleId = (int) ($row['id'] ?? 0);
|
||||
$rows[] = [
|
||||
'id' => $row['id'] ?? null,
|
||||
'uuid' => $row['uuid'] ?? '',
|
||||
'is_default' => $roleId > 0 && $defaultRoleId === $roleId,
|
||||
'description' => $row['description'] ?? '',
|
||||
'created' => dt($row['created'] ?? ''),
|
||||
'modified' => dt($row['modified'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
Router::json([
|
||||
'data' => $rows,
|
||||
'total' => $result['total'] ?? 0,
|
||||
]);
|
||||
29
pages/admin/roles/delete($id).php
Normal file
29
pages/admin/roles/delete($id).php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\RoleService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::ROLES_DELETE);
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
if ($uuid === '') {
|
||||
Router::redirect('admin/roles');
|
||||
}
|
||||
|
||||
$result = RoleService::deleteByUuid($uuid);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
$error = $result['error'] ?? 'unknown';
|
||||
if ($error === 'admin_role_protected') {
|
||||
Flash::error('The Admin role cannot be deleted', 'admin/roles', 'admin_role_protected');
|
||||
} else {
|
||||
Flash::error('Role not found', 'admin/roles', 'role_not_found');
|
||||
}
|
||||
Router::redirect('admin/roles');
|
||||
}
|
||||
|
||||
Flash::success('Role deleted', 'admin/roles', 'role_deleted');
|
||||
Router::redirect('admin/roles');
|
||||
87
pages/admin/roles/edit($id).php
Normal file
87
pages/admin/roles/edit($id).php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Repository\PermissionRepository;
|
||||
use MintyPHP\Repository\RolePermissionRepository;
|
||||
use MintyPHP\Service\RoleService;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\SettingService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::ROLES_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
PermissionService::getUserPermissions($currentUserId);
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$role = $uuid !== '' ? RoleService::findByUuid($uuid) : null;
|
||||
if (!$role) {
|
||||
Flash::error('Role not found', 'admin/roles', 'role_not_found');
|
||||
Router::redirect('admin/roles');
|
||||
}
|
||||
|
||||
$roleId = (int) ($role['id'] ?? 0);
|
||||
$creatorId = (int) ($role['created_by'] ?? 0);
|
||||
if ($creatorId > 0) {
|
||||
$creator = UserService::findById($creatorId);
|
||||
if ($creator) {
|
||||
$creatorName = trim(($creator['first_name'] ?? '') . ' ' . ($creator['last_name'] ?? ''));
|
||||
$role['created_by_label'] = $creatorName !== '' ? $creatorName : ($creator['email'] ?? '');
|
||||
$role['created_by_uuid'] = $creator['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$modifierId = (int) ($role['modified_by'] ?? 0);
|
||||
if ($modifierId > 0) {
|
||||
$modifier = UserService::findById($modifierId);
|
||||
if ($modifier) {
|
||||
$modifierName = trim(($modifier['first_name'] ?? '') . ' ' . ($modifier['last_name'] ?? ''));
|
||||
$role['modified_by_label'] = $modifierName !== '' ? $modifierName : ($modifier['email'] ?? '');
|
||||
$role['modified_by_uuid'] = $modifier['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$form = $role;
|
||||
$permissions = PermissionRepository::list();
|
||||
$selectedPermissionIds = RolePermissionRepository::listPermissionIdsByRoleId($roleId);
|
||||
|
||||
if (isset($_POST['description'])) {
|
||||
if (!PermissionService::userHas($currentUserId, PermissionService::ROLES_UPDATE)) {
|
||||
Flash::error('Permission denied', "admin/roles/edit/{$uuid}", 'permission_denied');
|
||||
Router::redirect("admin/roles/edit/{$uuid}");
|
||||
return;
|
||||
}
|
||||
$result = RoleService::updateFromAdmin($roleId, $_POST, $currentUserId);
|
||||
$form = $result['form'] ?? $form;
|
||||
$errors = $result['errors'] ?? [];
|
||||
$selectedPermissionIds = $_POST['permission_ids'] ?? $selectedPermissionIds;
|
||||
if (!is_array($selectedPermissionIds)) {
|
||||
$selectedPermissionIds = [$selectedPermissionIds];
|
||||
}
|
||||
$selectedPermissionIds = array_values(array_unique(array_map('intval', $selectedPermissionIds)));
|
||||
|
||||
if ($result['ok'] ?? false) {
|
||||
$permissionIds = $_POST['permission_ids'] ?? [];
|
||||
if (!is_array($permissionIds)) {
|
||||
$permissionIds = [$permissionIds];
|
||||
}
|
||||
$permissionIds = array_values(array_unique(array_map('intval', $permissionIds)));
|
||||
RolePermissionRepository::replaceForRole($roleId, $permissionIds);
|
||||
$action = (string) ($_POST['action'] ?? 'save');
|
||||
if ($action === 'save_close') {
|
||||
Flash::success('Role updated', 'admin/roles', 'role_updated');
|
||||
Router::redirect('admin/roles');
|
||||
} else {
|
||||
Flash::success('Role updated', "admin/roles/edit/{$uuid}", 'role_updated');
|
||||
Router::redirect("admin/roles/edit/{$uuid}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Buffer::set('title', t('Edit role'));
|
||||
167
pages/admin/roles/edit(default).phtml
Normal file
167
pages/admin/roles/edit(default).phtml
Normal file
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array<int, string> $errors
|
||||
* @var array $form
|
||||
* @var array $role
|
||||
*/
|
||||
|
||||
use MintyPHP\Session;
|
||||
|
||||
$values = $form ?? $role ?? [];
|
||||
$isReadOnly = !can('roles.update');
|
||||
|
||||
?>
|
||||
|
||||
<div class="app-details-container">
|
||||
<section>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
|
||||
href="/admin/roles"><?php e(t('Roles')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('Edit role')); ?>
|
||||
</div>
|
||||
<div class="app-details-titlebar">
|
||||
<h1>
|
||||
<a href="admin/roles" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>
|
||||
<?php e(t('Edit role')); ?>
|
||||
</h1>
|
||||
<div class="app-details-titlebar-actions">
|
||||
<?php if (can('roles.delete')): ?>
|
||||
<details class="dropdown">
|
||||
<summary role="button" class="outline secondary"><i class="bi bi-three-dots"></i></summary>
|
||||
<ul dir="rtl">
|
||||
<li>
|
||||
<form method="post" action="admin/roles/delete/<?php e($values['uuid'] ?? ''); ?>"
|
||||
onsubmit="return confirm('<?php e(t('Delete this role?')); ?>');">
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
<button type="submit" class="transparent">
|
||||
<?php e(t('Delete')); ?>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (can('roles.update')): ?>
|
||||
<button type="submit" form="role-form" name="action" value="save" class="secondary outline">
|
||||
<?php e(t('Save')); ?>
|
||||
</button>
|
||||
<button type="submit" form="role-form" name="action" value="save_close" class="primary">
|
||||
<?php e(t('Save & close')); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="app-details-errors">
|
||||
<div class="notice" data-variant="error">
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?php e($error); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$detailsOpenAll = true;
|
||||
$isReadOnly = $isReadOnly ?? false;
|
||||
require __DIR__ . '/_form.phtml';
|
||||
?>
|
||||
</section>
|
||||
<aside id="app-details-aside-section">
|
||||
<div class="app-details-aside-section">
|
||||
<hgroup>
|
||||
<h2><?php e($values['description'] ?? ''); ?></h2>
|
||||
<p><?php e(t('Role')); ?></p>
|
||||
</hgroup>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small><?php e(t('ID')); ?></small>
|
||||
<p>
|
||||
<span class="badge" data-variant="neutral" data-copy="true"
|
||||
data-copy-value="<?php e($values['id'] ?? ''); ?>">
|
||||
<?php e($values['id'] ?? '-'); ?>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$createdByLabel = $values['created_by_label'] ?? '';
|
||||
$createdById = $values['created_by'] ?? null;
|
||||
$createdByUuid = $values['created_by_uuid'] ?? null;
|
||||
$modifiedByLabel = $values['modified_by_label'] ?? '';
|
||||
$modifiedById = $values['modified_by'] ?? null;
|
||||
$modifiedByUuid = $values['modified_by_uuid'] ?? null;
|
||||
?>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small><?php e(t('Created')); ?></small>
|
||||
<p><?php e(dt($values['created'] ?? '') ?: '-'); ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
<?php e(t('Created by')); ?>
|
||||
</small>
|
||||
<p>
|
||||
<?php if ($createdByLabel !== ''): ?>
|
||||
<?php if ($createdByUuid): ?>
|
||||
<a href="admin/users/edit/<?php e($createdByUuid); ?>"><?php e($createdByLabel); ?></a>
|
||||
<?php else: ?>
|
||||
<?php e($createdByLabel); ?>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($createdById): ?>
|
||||
<?php e('#' . $createdById); ?>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small><?php e(t('Modified')); ?></small>
|
||||
<p><?php e(dt($values['modified'] ?? '') ?: '-'); ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
<?php e(t('Modified by')); ?>
|
||||
</small>
|
||||
<p>
|
||||
<?php if ($modifiedByLabel !== ''): ?>
|
||||
<?php if ($modifiedByUuid): ?>
|
||||
<a href="admin/users/edit/<?php e($modifiedByUuid); ?>"><?php e($modifiedByLabel); ?></a>
|
||||
<?php else: ?>
|
||||
<?php e($modifiedByLabel); ?>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($modifiedById): ?>
|
||||
<?php e('#' . $modifiedById); ?>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small>
|
||||
<?php e(t('UUID')); ?>
|
||||
</small>
|
||||
<p>
|
||||
<span class="badge" data-variant="neutral" data-copy="true"
|
||||
data-copy-value="<?php e($values['uuid'] ?? ''); ?>">
|
||||
<?php
|
||||
$uuidValue = (string) ($values['uuid'] ?? '');
|
||||
$uuidDisplay = $uuidValue !== '' ? substr($uuidValue, 0, 10) : '-';
|
||||
e($uuidDisplay);
|
||||
?>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
23
pages/admin/roles/index().php
Normal file
23
pages/admin/roles/index().php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::ROLES_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
PermissionService::getUserPermissions($currentUserId);
|
||||
}
|
||||
|
||||
Buffer::set('title', t('Roles'));
|
||||
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
$csrfKey = Session::$csrfSessionKey;
|
||||
$csrfToken = $_SESSION[$csrfKey] ?? '';
|
||||
Buffer::set(
|
||||
'grid_csrf',
|
||||
json_encode(['key' => $csrfKey, 'token' => $csrfToken], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
);
|
||||
159
pages/admin/roles/index(default).phtml
Normal file
159
pages/admin/roles/index(default).phtml
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
use MintyPHP\Router;
|
||||
|
||||
$canCreateRoles = can('roles.create');
|
||||
$canDeleteRoles = can('roles.delete');
|
||||
?>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><?php e(t('Roles')); ?>
|
||||
</div>
|
||||
<div class="app-list-titlebar">
|
||||
<h1><?php e(t('Roles')); ?></h1>
|
||||
<div class="app-list-titlebar-actions">
|
||||
<button
|
||||
class="outline secondary"
|
||||
type="button"
|
||||
data-toolbar-toggle
|
||||
data-toolbar-target="#roles-toolbar"
|
||||
data-toolbar-text-show="<?php e(t('Show filters')); ?>"
|
||||
data-toolbar-text-hide="<?php e(t('Hide filters')); ?>"
|
||||
>
|
||||
<i class="bi bi-funnel-fill"></i> <span data-toolbar-label><?php e(t('Hide filters')); ?></span>
|
||||
</button>
|
||||
<?php if ($canCreateRoles): ?>
|
||||
<a role="button" class="primary" href="admin/roles/create">
|
||||
<i class="bi bi-plus"></i> <?php e(t('Create role')); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-list-toolbar" id="roles-toolbar">
|
||||
<input type="search" id="role-search" placeholder="<?php e(t('Search...')); ?>">
|
||||
</div>
|
||||
<div class="app-list-table">
|
||||
<div id="roles-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 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 { buildUrl, postAction, badgeHtml } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
|
||||
|
||||
initToolbarToggles();
|
||||
|
||||
const appBase = "<?php e(localeBase()); ?>";
|
||||
const csrf = <?php MintyPHP\Buffer::get('grid_csrf'); ?>;
|
||||
const deleteConfirm = "<?php e(t('Delete this role?')); ?>";
|
||||
const buildActionUrl = (action, id) => buildUrl(appBase, `admin/roles/${action}/${id}`);
|
||||
|
||||
const initRolesGrid = () => {
|
||||
const formatBadge = (value) => badgeHtml(gridjs, value);
|
||||
const gridConfig = createServerGrid({
|
||||
gridjs: window.gridjs,
|
||||
container: '#roles-grid',
|
||||
dataUrl: 'admin/roles/data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
name: "<?php e(t('Description')); ?>",
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
if (!cell || typeof cell !== 'object') {
|
||||
return gridjs.html(String(cell ?? ''));
|
||||
}
|
||||
const label = cell.label ?? '';
|
||||
return gridjs.html(`${label}`);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "<?php e(t('Created')); ?>",
|
||||
sort: true,
|
||||
formatter: (cell) => formatBadge(cell)
|
||||
},
|
||||
{
|
||||
name: "<?php e(t('Modified')); ?>",
|
||||
sort: true,
|
||||
formatter: (cell) => formatBadge(cell)
|
||||
},
|
||||
{
|
||||
name: "UUID",
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
name: "ID",
|
||||
hidden: true
|
||||
}
|
||||
],
|
||||
sortColumns: ['description', 'created', 'modified', null, null],
|
||||
paginationLimit: 10,
|
||||
language: <?php MintyPHP\Buffer::get('grid_lang'); ?>,
|
||||
mapData: (data) => data.data.map((row) => [
|
||||
{
|
||||
label: row.description,
|
||||
is_default: row.is_default ? 1 : 0
|
||||
},
|
||||
row.created,
|
||||
row.modified,
|
||||
row.uuid,
|
||||
row.id
|
||||
]),
|
||||
actions: {
|
||||
enabled: true,
|
||||
label: "<?php e(t('Actions')); ?>",
|
||||
getRowId: (row) => row?.cells?.[3]?.data,
|
||||
items: [
|
||||
{
|
||||
key: 'edit',
|
||||
type: 'link',
|
||||
label: "<i class=\"bi bi-pencil-fill\"></i>",
|
||||
href: ({ id }) => buildActionUrl('edit', id)
|
||||
},
|
||||
<?php if ($canDeleteRoles): ?>
|
||||
{
|
||||
key: 'delete',
|
||||
label: "<i class=\"bi bi-trash3-fill\"></i>",
|
||||
className: 'danger outline',
|
||||
confirm: deleteConfirm
|
||||
}
|
||||
<?php endif; ?>
|
||||
],
|
||||
onAction: async ({ action, id, grid }) => {
|
||||
if (action !== 'delete') return;
|
||||
const url = buildActionUrl('delete', id);
|
||||
const response = await postAction(url, csrf);
|
||||
if (response.ok) {
|
||||
grid.forceRender();
|
||||
} else {
|
||||
window.location.href = new URL('admin/roles', appBase).toString();
|
||||
}
|
||||
}
|
||||
},
|
||||
search: {
|
||||
input: '#role-search',
|
||||
param: 'search',
|
||||
debounce: 250
|
||||
},
|
||||
urlSync: true,
|
||||
rowDataset: (row) => ({
|
||||
uuid: row?.cells?.[3]?.data
|
||||
}),
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => {
|
||||
const uuid = rowData?.cells?.[3]?.data;
|
||||
return uuid ? new URL(`admin/roles/edit/${uuid}`, appBase).toString() : '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!gridConfig || !gridConfig.grid) {
|
||||
console.warn('Roles grid init failed');
|
||||
return null;
|
||||
}
|
||||
|
||||
return gridConfig;
|
||||
};
|
||||
|
||||
initRolesGrid();
|
||||
</script>
|
||||
92
pages/admin/search/data().php
Normal file
92
pages/admin/search/data().php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Support\SearchConfig;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\DB;
|
||||
use MintyPHP\I18n;
|
||||
use MintyPHP\Repository\UserTenantRepository;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$userId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($userId > 0) {
|
||||
PermissionService::getUserPermissions($userId);
|
||||
}
|
||||
|
||||
$query = trim((string) ($_GET['q'] ?? ''));
|
||||
if ($query === '') {
|
||||
Router::json([]);
|
||||
}
|
||||
|
||||
$previewLimit = 5;
|
||||
$locale = I18n::$locale ?? (I18n::$defaultLocale ?? 'de');
|
||||
$tenantIds = $userId > 0 ? UserTenantRepository::listTenantIdsByUserId($userId) : [];
|
||||
$tenantScoped = !empty($tenantIds);
|
||||
|
||||
$tenantScopeFilters = SearchConfig::tenantScopeFilters();
|
||||
$resources = SearchConfig::resources($query, $locale);
|
||||
$results = [];
|
||||
|
||||
foreach ($resources as $resource) {
|
||||
$key = (string) ($resource['key'] ?? '');
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tenantFilter = $tenantScopeFilters[$key] ?? '';
|
||||
$isTenantScoped = $tenantFilter !== '';
|
||||
if ($isTenantScoped && !$tenantScoped) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$perm = (string) ($resource['permission'] ?? '');
|
||||
if ($perm !== '' && !PermissionService::userHas($userId, $perm)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$countSql = str_replace('{{tenantFilter}}', $isTenantScoped ? $tenantFilter : '', $resource['countSql'] ?? '');
|
||||
$countParams = $resource['countParams'] ?? [];
|
||||
if ($isTenantScoped && strpos($countSql, '???') !== false) {
|
||||
$countParams[] = $tenantIds;
|
||||
}
|
||||
$count = $countSql !== '' ? (int) DB::selectValue($countSql, ...$countParams) : 0;
|
||||
|
||||
$items = [];
|
||||
$previewSql = $resource['previewSql'] ?? null;
|
||||
if ($previewSql) {
|
||||
$previewSql = str_replace('{{tenantFilter}}', $isTenantScoped ? $tenantFilter : '', $previewSql);
|
||||
$previewParams = $resource['previewParams'] ?? [];
|
||||
if ($isTenantScoped && strpos($previewSql, '???') !== false) {
|
||||
$previewParams[] = $tenantIds;
|
||||
}
|
||||
$previewParams[] = $previewLimit;
|
||||
$rows = DB::select($previewSql, ...$previewParams);
|
||||
foreach ($rows as $row) {
|
||||
$data = $row;
|
||||
if (is_array($row) && count($row) === 1) {
|
||||
$data = reset($row);
|
||||
}
|
||||
$data = is_array($data) ? $data : [];
|
||||
$mapped = SearchConfig::mapPreviewItem($key, $data, $query);
|
||||
if ($mapped !== null) {
|
||||
$items[] = $mapped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result = [
|
||||
'key' => $key,
|
||||
'label' => $resource['label'] ?? '',
|
||||
'count' => $count,
|
||||
'url' => SearchConfig::listUrl($key, $query),
|
||||
'items' => $items,
|
||||
];
|
||||
if ($key === 'pages' && !empty($items[0]['url'])) {
|
||||
$result['url'] = $items[0]['url'];
|
||||
}
|
||||
$results[] = $result;
|
||||
}
|
||||
|
||||
Router::json($results);
|
||||
26
pages/admin/settings/favicon().php
Normal file
26
pages/admin/settings/favicon().php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\BrandingFaviconService;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::SETTINGS_UPDATE);
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/settings');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = BrandingFaviconService::saveUpload($_FILES['favicon'] ?? []);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
$error = $result['error'] ?? t('Upload failed');
|
||||
Flash::error($error, 'admin/settings', 'favicon_upload_failed');
|
||||
Router::redirect('admin/settings');
|
||||
return;
|
||||
}
|
||||
|
||||
Flash::success('Favicon updated', 'admin/settings', 'favicon_updated');
|
||||
Router::redirect('admin/settings');
|
||||
19
pages/admin/settings/favicon-delete().php
Normal file
19
pages/admin/settings/favicon-delete().php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\BrandingFaviconService;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::SETTINGS_UPDATE);
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/settings');
|
||||
return;
|
||||
}
|
||||
|
||||
BrandingFaviconService::delete();
|
||||
Flash::success('Favicon removed', 'admin/settings', 'favicon_removed');
|
||||
Router::redirect('admin/settings');
|
||||
130
pages/admin/settings/index().php
Normal file
130
pages/admin/settings/index().php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\SettingService;
|
||||
use MintyPHP\Service\TenantService;
|
||||
use MintyPHP\Service\RoleService;
|
||||
use MintyPHP\Service\DepartmentService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::SETTINGS_VIEW);
|
||||
|
||||
$tenants = TenantService::list();
|
||||
$roles = RoleService::list();
|
||||
$departments = DepartmentService::list();
|
||||
|
||||
$sortByDescription = static function ($a, $b) {
|
||||
return strcmp((string) ($a['description'] ?? ''), (string) ($b['description'] ?? ''));
|
||||
};
|
||||
usort($tenants, $sortByDescription);
|
||||
usort($roles, $sortByDescription);
|
||||
usort($departments, $sortByDescription);
|
||||
|
||||
$values = [
|
||||
'default_tenant_id' => SettingService::getDefaultTenantId(),
|
||||
'default_role_id' => SettingService::getDefaultRoleId(),
|
||||
'default_department_id' => SettingService::getDefaultDepartmentId(),
|
||||
'app_title' => SettingService::getAppTitle(),
|
||||
'app_locale' => SettingService::getAppLocale(),
|
||||
'app_theme' => SettingService::getAppTheme(),
|
||||
'app_theme_user' => SettingService::isUserThemeAllowed(),
|
||||
'app_primary_color' => SettingService::getAppPrimaryColor(),
|
||||
'smtp_host' => SettingService::getSmtpHost(),
|
||||
'smtp_port' => SettingService::getSmtpPort(),
|
||||
'smtp_user' => SettingService::getSmtpUser(),
|
||||
'smtp_secure' => SettingService::getSmtpSecure(),
|
||||
'smtp_from' => SettingService::getSmtpFrom(),
|
||||
'smtp_from_name' => SettingService::getSmtpFromName(),
|
||||
];
|
||||
$settings = [
|
||||
'default_tenant' => SettingService::get(SettingService::DEFAULT_TENANT_KEY),
|
||||
'default_role' => SettingService::get(SettingService::DEFAULT_ROLE_KEY),
|
||||
'default_department' => SettingService::get(SettingService::DEFAULT_DEPARTMENT_KEY),
|
||||
'app_title' => SettingService::get(SettingService::APP_TITLE_KEY),
|
||||
'app_locale' => SettingService::get(SettingService::APP_LOCALE_KEY),
|
||||
'app_theme' => SettingService::get(SettingService::APP_THEME_KEY),
|
||||
'app_theme_user' => SettingService::get(SettingService::APP_THEME_USER_KEY),
|
||||
'app_primary_color' => SettingService::get(SettingService::APP_PRIMARY_COLOR_KEY),
|
||||
'smtp_host' => SettingService::get(SettingService::SMTP_HOST_KEY),
|
||||
'smtp_port' => SettingService::get(SettingService::SMTP_PORT_KEY),
|
||||
'smtp_user' => SettingService::get(SettingService::SMTP_USER_KEY),
|
||||
'smtp_password' => SettingService::get(SettingService::SMTP_PASSWORD_KEY),
|
||||
'smtp_secure' => SettingService::get(SettingService::SMTP_SECURE_KEY),
|
||||
'smtp_from' => SettingService::get(SettingService::SMTP_FROM_KEY),
|
||||
'smtp_from_name' => SettingService::get(SettingService::SMTP_FROM_NAME_KEY),
|
||||
];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!isset($_POST['settings_submit'])) {
|
||||
Router::redirect('admin/settings');
|
||||
}
|
||||
Guard::requirePermission(PermissionService::SETTINGS_UPDATE);
|
||||
$defaultTenantId = (int) ($_POST['default_tenant_id'] ?? 0);
|
||||
$defaultRoleId = (int) ($_POST['default_role_id'] ?? 0);
|
||||
$defaultDepartmentId = (int) ($_POST['default_department_id'] ?? 0);
|
||||
$appTitle = trim((string) ($_POST['app_title'] ?? ''));
|
||||
$appLocale = trim((string) ($_POST['app_locale'] ?? ''));
|
||||
$appTheme = trim((string) ($_POST['app_theme'] ?? ''));
|
||||
$appThemeUser = isset($_POST['app_theme_user']);
|
||||
$rawPrimaryColor = $_POST['app_primary_color'] ?? '';
|
||||
if (is_array($rawPrimaryColor)) {
|
||||
$rawPrimaryColor = '';
|
||||
}
|
||||
$appPrimaryColor = trim((string) $rawPrimaryColor);
|
||||
if (in_array(strtolower($appPrimaryColor), ['null', 'undefined'], true)) {
|
||||
$appPrimaryColor = '';
|
||||
}
|
||||
$smtpHost = trim((string) ($_POST['smtp_host'] ?? ''));
|
||||
$smtpPort = (int) ($_POST['smtp_port'] ?? 0);
|
||||
$smtpUser = trim((string) ($_POST['smtp_user'] ?? ''));
|
||||
$smtpPassword = (string) ($_POST['smtp_password'] ?? '');
|
||||
$smtpSecure = trim((string) ($_POST['smtp_secure'] ?? ''));
|
||||
$smtpFrom = trim((string) ($_POST['smtp_from'] ?? ''));
|
||||
$smtpFromName = trim((string) ($_POST['smtp_from_name'] ?? ''));
|
||||
|
||||
SettingService::setDefaultTenantId($defaultTenantId > 0 ? $defaultTenantId : null);
|
||||
SettingService::setDefaultRoleId($defaultRoleId > 0 ? $defaultRoleId : null);
|
||||
SettingService::setDefaultDepartmentId($defaultDepartmentId > 0 ? $defaultDepartmentId : null);
|
||||
SettingService::setAppTitle($appTitle);
|
||||
SettingService::setAppLocale($appLocale);
|
||||
SettingService::setAppTheme($appTheme);
|
||||
SettingService::setUserThemeAllowed($appThemeUser);
|
||||
$primaryOk = SettingService::setAppPrimaryColor($appPrimaryColor);
|
||||
if (!$primaryOk) {
|
||||
$appPrimaryColor = '';
|
||||
Flash::error('Primary color is invalid', 'admin/settings', 'app_primary_invalid');
|
||||
}
|
||||
SettingService::setSmtpHost($smtpHost);
|
||||
SettingService::setSmtpPort($smtpPort > 0 ? $smtpPort : null);
|
||||
SettingService::setSmtpUser($smtpUser);
|
||||
if (trim($smtpPassword) !== '') {
|
||||
SettingService::setSmtpPassword($smtpPassword);
|
||||
}
|
||||
SettingService::setSmtpSecure($smtpSecure);
|
||||
SettingService::setSmtpFrom($smtpFrom);
|
||||
SettingService::setSmtpFromName($smtpFromName);
|
||||
|
||||
$cacheFile = __DIR__ . '/../../../config/settings.php';
|
||||
$cacheData = [];
|
||||
if (is_file($cacheFile)) {
|
||||
$existing = include $cacheFile;
|
||||
if (is_array($existing)) {
|
||||
$cacheData = $existing;
|
||||
}
|
||||
}
|
||||
$cacheData['app_title'] = $appTitle !== '' ? $appTitle : null;
|
||||
$cacheData['app_locale'] = $appLocale !== '' ? $appLocale : null;
|
||||
$cacheData['app_theme'] = $appTheme !== '' ? $appTheme : null;
|
||||
$cacheData['app_theme_user'] = $appThemeUser ? '1' : '0';
|
||||
$cacheData['app_primary_color'] = $appPrimaryColor !== '' ? $appPrimaryColor : null;
|
||||
file_put_contents($cacheFile, "<?php\nreturn " . var_export($cacheData, true) . ";\n");
|
||||
|
||||
Flash::success('Settings updated', 'admin/settings', 'settings_updated');
|
||||
Router::redirect('admin/settings');
|
||||
}
|
||||
|
||||
Buffer::set('title', t('Settings'));
|
||||
353
pages/admin/settings/index(default).phtml
Normal file
353
pages/admin/settings/index(default).phtml
Normal file
@@ -0,0 +1,353 @@
|
||||
<?php
|
||||
/**
|
||||
* @var array $tenants
|
||||
* @var array $roles
|
||||
* @var array $departments
|
||||
* @var array $values
|
||||
* @var array $settings
|
||||
*/
|
||||
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\BrandingLogoService;
|
||||
use MintyPHP\Service\BrandingFaviconService;
|
||||
|
||||
$values = $values ?? [];
|
||||
$settings = $settings ?? [];
|
||||
$defaultTenantId = (int) ($values['default_tenant_id'] ?? 0);
|
||||
$defaultRoleId = (int) ($values['default_role_id'] ?? 0);
|
||||
$defaultDepartmentId = (int) ($values['default_department_id'] ?? 0);
|
||||
$appTitle = (string) ($values['app_title'] ?? '');
|
||||
$appLocale = (string) ($values['app_locale'] ?? '');
|
||||
$appTheme = (string) ($values['app_theme'] ?? '');
|
||||
$appThemeUser = !empty($values['app_theme_user']);
|
||||
$appPrimaryColor = (string) ($values['app_primary_color'] ?? '');
|
||||
$smtpHost = (string) ($values['smtp_host'] ?? '');
|
||||
$smtpPort = (string) ($values['smtp_port'] ?? '');
|
||||
$smtpUser = (string) ($values['smtp_user'] ?? '');
|
||||
$smtpSecure = (string) ($values['smtp_secure'] ?? '');
|
||||
$smtpFrom = (string) ($values['smtp_from'] ?? '');
|
||||
$smtpFromName = (string) ($values['smtp_from_name'] ?? '');
|
||||
$defaultTenantDesc = $settings['default_tenant']['description'] ?? 'setting.default_tenant';
|
||||
$defaultRoleDesc = $settings['default_role']['description'] ?? 'setting.default_role';
|
||||
$defaultDepartmentDesc = $settings['default_department']['description'] ?? 'setting.default_department';
|
||||
$appTitleDesc = $settings['app_title']['description'] ?? 'setting.app_title';
|
||||
$appLocaleDesc = $settings['app_locale']['description'] ?? 'setting.app_locale';
|
||||
$appThemeDesc = $settings['app_theme']['description'] ?? 'setting.app_theme';
|
||||
$appThemeUserDesc = $settings['app_theme_user']['description'] ?? 'setting.app_theme_user';
|
||||
$appPrimaryColorDesc = $settings['app_primary_color']['description'] ?? 'setting.app_primary_color';
|
||||
$smtpHostDesc = $settings['smtp_host']['description'] ?? 'setting.smtp_host';
|
||||
$smtpPortDesc = $settings['smtp_port']['description'] ?? 'setting.smtp_port';
|
||||
$smtpUserDesc = $settings['smtp_user']['description'] ?? 'setting.smtp_user';
|
||||
$smtpPasswordDesc = $settings['smtp_password']['description'] ?? 'setting.smtp_password';
|
||||
$smtpSecureDesc = $settings['smtp_secure']['description'] ?? 'setting.smtp_secure';
|
||||
$smtpFromDesc = $settings['smtp_from']['description'] ?? 'setting.smtp_from';
|
||||
$smtpFromNameDesc = $settings['smtp_from_name']['description'] ?? 'setting.smtp_from_name';
|
||||
$locales = defined('APP_LOCALES') ? APP_LOCALES : [];
|
||||
$hasLogo = BrandingLogoService::hasLogo();
|
||||
$hasFavicon = BrandingFaviconService::hasFavicon();
|
||||
$canUpdateSettings = can('settings.update');
|
||||
$isReadOnly = !$canUpdateSettings;
|
||||
$readonlyAttr = $isReadOnly ? 'readonly' : '';
|
||||
$disabledAttr = $isReadOnly ? 'disabled' : '';
|
||||
?>
|
||||
|
||||
<div class="app-details-container">
|
||||
<section>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><?php e(t('Settings')); ?>
|
||||
</div>
|
||||
<div class="app-details-titlebar">
|
||||
<h1>
|
||||
<?php e(t('Settings')); ?>
|
||||
</h1>
|
||||
<div class="app-details-titlebar-actions">
|
||||
<?php if ($canUpdateSettings): ?>
|
||||
<button type="submit" form="settings-form" class="primary">
|
||||
<?php e(t('Save')); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="settings-form" method="post">
|
||||
<input type="hidden" name="settings_submit" value="1">
|
||||
<details name="general" open>
|
||||
<summary><?php e(t('General')); ?></summary>
|
||||
<hr>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('App title')); ?></span>
|
||||
<input type="text" name="app_title" value="<?php e($appTitle); ?>" placeholder="<?php e(APP_NAME); ?>" <?php e($readonlyAttr); ?>>
|
||||
<small><?php e(t($appTitleDesc)); ?></small>
|
||||
</label>
|
||||
</details>
|
||||
<hr>
|
||||
<details name="appearance">
|
||||
<summary><?php e(t('Appearance')); ?></summary>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<fieldset>
|
||||
<legend><small><?php e(t('Default theme')); ?></small></legend>
|
||||
<select name="app_theme" <?php e($disabledAttr); ?>>
|
||||
<option value=""><?php e(t('None')); ?></option>
|
||||
<option value="light" <?php e($appTheme === 'light' ? 'selected' : ''); ?>>
|
||||
<?php e(t('Light')); ?>
|
||||
</option>
|
||||
<option value="dark" <?php e($appTheme === 'dark' ? 'selected' : ''); ?>>
|
||||
<?php e(t('Dark')); ?>
|
||||
</option>
|
||||
</select>
|
||||
<small><?php e(t($appThemeDesc)); ?></small>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend><small><?php e(t('Primary color')); ?></small></legend>
|
||||
<label class="app-field">
|
||||
<input type="color" name="app_primary_color"
|
||||
value="<?php e($appPrimaryColor !== '' ? $appPrimaryColor : '#2fa4a4'); ?>" <?php e($disabledAttr); ?>>
|
||||
<div>
|
||||
<div>
|
||||
<?php e(t('Choose color')); ?>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<small class="muted"><?php e(t($appPrimaryColorDesc)); ?></small>
|
||||
</fieldset>
|
||||
</div>
|
||||
<blockquote data-variant="info">
|
||||
<?php e(t('Tenants can override this color in their appearance settings.')); ?>
|
||||
</blockquote>
|
||||
<fieldset>
|
||||
<legend>
|
||||
<small>
|
||||
<?php e(t($appThemeUserDesc)); ?>
|
||||
</small>
|
||||
</legend>
|
||||
<label class="app-field">
|
||||
<input type="checkbox" name="app_theme_user" value="1" <?php e($appThemeUser ? 'checked' : ''); ?> <?php e($disabledAttr); ?>>
|
||||
<span><?php e(t('Allow user theme')); ?></span>
|
||||
</label>
|
||||
</fieldset>
|
||||
</details>
|
||||
<hr>
|
||||
<details name="smtp">
|
||||
<summary><?php e(t('Email settings')); ?></summary>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<label class="app-field">
|
||||
<span><?php e(t('SMTP host')); ?></span>
|
||||
<input type="text" name="smtp_host" value="<?php e($smtpHost); ?>" <?php e($readonlyAttr); ?>>
|
||||
<small><?php e(t($smtpHostDesc)); ?></small>
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('SMTP port')); ?></span>
|
||||
<input type="number" name="smtp_port" value="<?php e($smtpPort); ?>" min="1" max="65535" <?php e($readonlyAttr); ?>>
|
||||
<small><?php e(t($smtpPortDesc)); ?></small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<label class="app-field">
|
||||
<span><?php e(t('SMTP user')); ?></span>
|
||||
<input type="text" name="smtp_user" value="<?php e($smtpUser); ?>" autocomplete="username" <?php e($readonlyAttr); ?>>
|
||||
<small><?php e(t($smtpUserDesc)); ?></small>
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('SMTP password')); ?></span>
|
||||
<input type="password" name="smtp_password" value="" autocomplete="new-password" <?php e($readonlyAttr); ?>>
|
||||
<small><?php e(t($smtpPasswordDesc)); ?></small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<label class="app-field">
|
||||
<span><?php e(t('SMTP security')); ?></span>
|
||||
<select name="smtp_secure" <?php e($disabledAttr); ?>>
|
||||
<option value=""><?php e(t('None')); ?></option>
|
||||
<option value="tls" <?php e($smtpSecure === 'tls' ? 'selected' : ''); ?>>TLS</option>
|
||||
<option value="ssl" <?php e($smtpSecure === 'ssl' ? 'selected' : ''); ?>>SSL</option>
|
||||
</select>
|
||||
<small><?php e(t($smtpSecureDesc)); ?></small>
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('SMTP from address')); ?></span>
|
||||
<input type="email" name="smtp_from" value="<?php e($smtpFrom); ?>" <?php e($readonlyAttr); ?>>
|
||||
<small><?php e(t($smtpFromDesc)); ?></small>
|
||||
</label>
|
||||
</div>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('SMTP from name')); ?></span>
|
||||
<input type="text" name="smtp_from_name" value="<?php e($smtpFromName); ?>" <?php e($readonlyAttr); ?>>
|
||||
<small><?php e(t($smtpFromNameDesc)); ?></small>
|
||||
</label>
|
||||
</details>
|
||||
<hr>
|
||||
<details name="internationalization">
|
||||
<summary><?php e(t('Internationalization')); ?></summary>
|
||||
<hr>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Default language')); ?></span>
|
||||
<select name="app_locale" <?php e($disabledAttr); ?>>
|
||||
<option value=""><?php e(t('None')); ?></option>
|
||||
<?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 e($appLocale === $locale ? 'selected' : ''); ?>>
|
||||
<?php e($label); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<small><?php e(t($appLocaleDesc)); ?></small>
|
||||
</label>
|
||||
</details>
|
||||
<hr>
|
||||
<details name="user-creation">
|
||||
<summary><?php e(t('User creation rules')); ?></summary>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<label class="app-field">
|
||||
<span>
|
||||
<?php e(t('Default tenant')); ?>
|
||||
</span>
|
||||
<select name="default_tenant_id" <?php e($disabledAttr); ?>>
|
||||
<option value="">
|
||||
<?php e(t('None')); ?>
|
||||
</option>
|
||||
<?php foreach ($tenants ?? [] as $tenant): ?>
|
||||
<?php $tenantId = (int) ($tenant['id'] ?? 0); ?>
|
||||
<?php if ($tenantId > 0): ?>
|
||||
<option value="<?php e((string) $tenantId); ?>" <?php e($tenantId === $defaultTenantId ? 'selected' : ''); ?>>
|
||||
<?php e($tenant['description'] ?? ''); ?>
|
||||
</option>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<small>
|
||||
<?php e(t($defaultTenantDesc)); ?>
|
||||
</small>
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span>
|
||||
<?php e(t('Default department')); ?>
|
||||
</span>
|
||||
<select name="default_department_id" <?php e($disabledAttr); ?>>
|
||||
<option value="">
|
||||
<?php e(t('None')); ?>
|
||||
</option>
|
||||
<?php foreach ($departments ?? [] as $department): ?>
|
||||
<?php $departmentId = (int) ($department['id'] ?? 0); ?>
|
||||
<?php if ($departmentId > 0): ?>
|
||||
<option value="<?php e((string) $departmentId); ?>" <?php e($departmentId === $defaultDepartmentId ? 'selected' : ''); ?>>
|
||||
<?php e($department['description'] ?? ''); ?>
|
||||
</option>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<small>
|
||||
<?php e(t($defaultDepartmentDesc)); ?>
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Default role')); ?></span>
|
||||
<select name="default_role_id" <?php e($disabledAttr); ?>>
|
||||
<option value=""><?php e(t('None')); ?></option>
|
||||
<?php foreach ($roles ?? [] as $role): ?>
|
||||
<?php $roleId = (int) ($role['id'] ?? 0); ?>
|
||||
<?php if ($roleId > 0): ?>
|
||||
<option value="<?php e((string) $roleId); ?>" <?php e($roleId === $defaultRoleId ? 'selected' : ''); ?>>
|
||||
<?php e($role['description'] ?? ''); ?>
|
||||
</option>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<small><?php e(t($defaultRoleDesc)); ?></small>
|
||||
</label>
|
||||
|
||||
</details>
|
||||
<hr>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
</section>
|
||||
<aside id="app-details-aside-section">
|
||||
<div class="app-details-aside-section">
|
||||
<div class="entity-avatar-block avatar-square avatar-ratio-16-9 avatar-borderless">
|
||||
<a data-fslightbox="app-logo" href="<?php e(appLogoUrl(256)); ?>">
|
||||
<img class="entity-avatar-image" src="<?php e(appLogoUrl(256)); ?>" alt="<?php e(t('App logo')); ?>">
|
||||
</a>
|
||||
</div>
|
||||
<hgroup>
|
||||
<h2><?php e(appTitle()); ?></h2>
|
||||
<p><?php e(t('Settings')); ?></p>
|
||||
</hgroup>
|
||||
<hr>
|
||||
<?php if ($canUpdateSettings): ?>
|
||||
<details name="app-logo">
|
||||
<summary>
|
||||
<?php e(t('Upload logo')); ?>
|
||||
</summary>
|
||||
<hr>
|
||||
<small>
|
||||
<?php e(t('Allowed file types: SVG, PNG, JPG, WEBP')); ?>
|
||||
</small>
|
||||
<hr>
|
||||
<form class="user-avatar-form" method="post" action="admin/settings/logo" enctype="multipart/form-data">
|
||||
<label class="user-avatar-upload">
|
||||
<input type="file" name="logo" accept="image/svg+xml,image/png,image/jpeg,image/webp">
|
||||
</label>
|
||||
<div class="grid">
|
||||
<button type="submit" class="primary">
|
||||
<?php e(t('Save')); ?>
|
||||
</button>
|
||||
<?php if ($hasLogo): ?>
|
||||
<button type="submit" class="danger" formaction="admin/settings/logo-delete" formmethod="post">
|
||||
<?php e(t('Remove logo')); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
<hr>
|
||||
<?php if ($canUpdateSettings): ?>
|
||||
<details name="app-favicon">
|
||||
<summary>
|
||||
<?php e(t('Upload favicon')); ?>
|
||||
</summary>
|
||||
<hr>
|
||||
<?php if ($hasFavicon): ?>
|
||||
<div class="entity-avatar-block avatar-square avatar-borderless" style="--avatar-size: 48px;">
|
||||
<img class="entity-avatar-image" src="<?php e(asset('favicon/favicon-32x32.png')); ?>"
|
||||
alt="<?php e(t('Favicon')); ?>">
|
||||
</div>
|
||||
<small><?php e(t('Favicon preview')); ?></small>
|
||||
<hr>
|
||||
<?php endif; ?>
|
||||
<small><?php e(t('Allowed file types: PNG')); ?></small>
|
||||
<small><?php e(t('Square images are recommended (icons are center-cropped).')); ?></small>
|
||||
<hr>
|
||||
<form class="user-avatar-form" method="post" action="admin/settings/favicon" enctype="multipart/form-data">
|
||||
<label class="user-avatar-upload">
|
||||
<input type="file" name="favicon" accept="image/png">
|
||||
</label>
|
||||
<div class="grid">
|
||||
<button type="submit" class="primary">
|
||||
<?php e(t('Save')); ?>
|
||||
</button>
|
||||
<?php if ($hasFavicon): ?>
|
||||
<button type="submit" class="danger" formaction="admin/settings/favicon-delete" formmethod="post">
|
||||
<?php e(t('Remove favicon')); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
26
pages/admin/settings/logo().php
Normal file
26
pages/admin/settings/logo().php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\BrandingLogoService;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::SETTINGS_UPDATE);
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/settings');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = BrandingLogoService::saveUpload($_FILES['logo'] ?? []);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
$error = $result['error'] ?? t('Upload failed');
|
||||
Flash::error($error, 'admin/settings', 'logo_upload_failed');
|
||||
Router::redirect('admin/settings');
|
||||
return;
|
||||
}
|
||||
|
||||
Flash::success('Logo updated', 'admin/settings', 'logo_updated');
|
||||
Router::redirect('admin/settings');
|
||||
19
pages/admin/settings/logo-delete().php
Normal file
19
pages/admin/settings/logo-delete().php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\BrandingLogoService;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::SETTINGS_UPDATE);
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/settings');
|
||||
return;
|
||||
}
|
||||
|
||||
BrandingLogoService::delete();
|
||||
Flash::success('Logo removed', 'admin/settings', 'logo_removed');
|
||||
Router::redirect('admin/settings');
|
||||
166
pages/admin/stats/index().php
Normal file
166
pages/admin/stats/index().php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\TenantService;
|
||||
use MintyPHP\Service\DepartmentService;
|
||||
use MintyPHP\Service\RoleService;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Repository\PermissionRepository;
|
||||
use MintyPHP\Repository\MailLogRepository;
|
||||
use MintyPHP\DB;
|
||||
|
||||
Guard::requirePermissionOrForbidden(PermissionService::STATS_VIEW);
|
||||
|
||||
// Basic counts
|
||||
$tenants = TenantService::list();
|
||||
$tenantCount = count($tenants);
|
||||
|
||||
$departments = DepartmentService::list();
|
||||
$departmentCount = count($departments);
|
||||
|
||||
$roles = RoleService::list();
|
||||
$roleCount = count($roles);
|
||||
|
||||
$users = UserService::list();
|
||||
$userCount = count($users);
|
||||
|
||||
$permissions = PermissionRepository::list();
|
||||
$permissionCount = count($permissions);
|
||||
|
||||
// User stats (active/inactive)
|
||||
$activeUserCount = 0;
|
||||
$inactiveUserCount = 0;
|
||||
foreach ($users as $user) {
|
||||
$isActive = (int) ($user['active'] ?? 1);
|
||||
if ($isActive === 1) {
|
||||
$activeUserCount++;
|
||||
} else {
|
||||
$inactiveUserCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Organization breakdown (per tenant)
|
||||
$tenantBreakdown = [];
|
||||
|
||||
// Load user-tenant assignments
|
||||
$userTenantAssignments = [];
|
||||
$userTenantRows = DB::select('select user_id, tenant_id from user_tenants');
|
||||
if (is_array($userTenantRows)) {
|
||||
foreach ($userTenantRows as $row) {
|
||||
$data = $row['user_tenants'] ?? $row;
|
||||
$userId = (int) ($data['user_id'] ?? 0);
|
||||
$tenantId = (int) ($data['tenant_id'] ?? 0);
|
||||
if ($userId > 0 && $tenantId > 0) {
|
||||
if (!isset($userTenantAssignments[$userId])) {
|
||||
$userTenantAssignments[$userId] = [];
|
||||
}
|
||||
$userTenantAssignments[$userId][] = $tenantId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load tenant-department assignments
|
||||
$tenantDepartmentAssignments = [];
|
||||
$tenantDepartmentRows = DB::select('select tenant_id, department_id from tenant_departments');
|
||||
if (is_array($tenantDepartmentRows)) {
|
||||
foreach ($tenantDepartmentRows as $row) {
|
||||
$data = $row['tenant_departments'] ?? $row;
|
||||
$tenantId = (int) ($data['tenant_id'] ?? 0);
|
||||
$departmentId = (int) ($data['department_id'] ?? 0);
|
||||
if ($tenantId > 0 && $departmentId > 0) {
|
||||
if (!isset($tenantDepartmentAssignments[$tenantId])) {
|
||||
$tenantDepartmentAssignments[$tenantId] = [];
|
||||
}
|
||||
$tenantDepartmentAssignments[$tenantId][] = $departmentId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($tenants as $tenant) {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
if ($tenantId === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Count departments for this tenant
|
||||
$deptCount = 0;
|
||||
$tenantDepartmentIds = $tenantDepartmentAssignments[$tenantId] ?? [];
|
||||
foreach ($departments as $dept) {
|
||||
$deptId = (int) ($dept['id'] ?? 0);
|
||||
if ($deptId > 0 && in_array($deptId, $tenantDepartmentIds, true)) {
|
||||
$deptCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Count users for this tenant
|
||||
$totalUsers = 0;
|
||||
$activeUsers = 0;
|
||||
$inactiveUsers = 0;
|
||||
foreach ($users as $user) {
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if user is assigned to this tenant
|
||||
$userTenantIds = $userTenantAssignments[$userId] ?? [];
|
||||
if (in_array($tenantId, $userTenantIds, true)) {
|
||||
$totalUsers++;
|
||||
$isActive = (int) ($user['active'] ?? 1);
|
||||
if ($isActive === 1) {
|
||||
$activeUsers++;
|
||||
} else {
|
||||
$inactiveUsers++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$tenantBreakdown[] = [
|
||||
'id' => $tenantId,
|
||||
'uuid' => $tenant['uuid'] ?? '',
|
||||
'description' => $tenant['description'] ?? '',
|
||||
'departments' => $deptCount,
|
||||
'total_users' => $totalUsers,
|
||||
'active_users' => $activeUsers,
|
||||
'inactive_users' => $inactiveUsers,
|
||||
];
|
||||
}
|
||||
|
||||
// Sort by description
|
||||
usort($tenantBreakdown, function ($a, $b) {
|
||||
return strcmp((string) ($a['description'] ?? ''), (string) ($b['description'] ?? ''));
|
||||
});
|
||||
|
||||
// Mail log stats
|
||||
$mailLogTotalCount = DB::selectValue('select count(*) from mail_log');
|
||||
$mailLogTotalCount = $mailLogTotalCount ? (int) $mailLogTotalCount : 0;
|
||||
|
||||
$mailLogSentCount = DB::selectValue("select count(*) from mail_log where status = 'sent'");
|
||||
$mailLogSentCount = $mailLogSentCount ? (int) $mailLogSentCount : 0;
|
||||
|
||||
$mailLogFailedCount = DB::selectValue("select count(*) from mail_log where status = 'failed'");
|
||||
$mailLogFailedCount = $mailLogFailedCount ? (int) $mailLogFailedCount : 0;
|
||||
|
||||
$mailLogQueuedCount = DB::selectValue("select count(*) from mail_log where status = 'queued'");
|
||||
$mailLogQueuedCount = $mailLogQueuedCount ? (int) $mailLogQueuedCount : 0;
|
||||
|
||||
// Mail log stats (last 24h)
|
||||
$mailLogTotal24hCount = DB::selectValue("select count(*) from mail_log where created_at >= NOW() - INTERVAL 24 HOUR");
|
||||
$mailLogTotal24hCount = $mailLogTotal24hCount ? (int) $mailLogTotal24hCount : 0;
|
||||
|
||||
$mailLogSent24hCount = DB::selectValue("select count(*) from mail_log where status = 'sent' and created_at >= NOW() - INTERVAL 24 HOUR");
|
||||
$mailLogSent24hCount = $mailLogSent24hCount ? (int) $mailLogSent24hCount : 0;
|
||||
|
||||
$mailLogFailed24hCount = DB::selectValue("select count(*) from mail_log where status = 'failed' and created_at >= NOW() - INTERVAL 24 HOUR");
|
||||
$mailLogFailed24hCount = $mailLogFailed24hCount ? (int) $mailLogFailed24hCount : 0;
|
||||
|
||||
$mailLogQueued24hCount = DB::selectValue("select count(*) from mail_log where status = 'queued' and created_at >= NOW() - INTERVAL 24 HOUR");
|
||||
$mailLogQueued24hCount = $mailLogQueued24hCount ? (int) $mailLogQueued24hCount : 0;
|
||||
|
||||
// Calculate date range for last 24 hours
|
||||
$date24hAgo = gmdate('Y-m-d', strtotime('-24 hours'));
|
||||
$dateToday = gmdate('Y-m-d');
|
||||
|
||||
Buffer::set('title', t('Statistics'));
|
||||
283
pages/admin/stats/index(default).phtml
Normal file
283
pages/admin/stats/index(default).phtml
Normal file
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var int $tenantCount
|
||||
* @var int $departmentCount
|
||||
* @var int $roleCount
|
||||
* @var int $userCount
|
||||
* @var int $permissionCount
|
||||
* @var int $activeUserCount
|
||||
* @var int $inactiveUserCount
|
||||
* @var array $tenantBreakdown
|
||||
* @var int $mailLogTotalCount
|
||||
* @var int $mailLogSentCount
|
||||
* @var int $mailLogFailedCount
|
||||
* @var int $mailLogQueuedCount
|
||||
* @var int $mailLogTotal24hCount
|
||||
* @var int $mailLogSent24hCount
|
||||
* @var int $mailLogFailed24hCount
|
||||
* @var int $mailLogQueued24hCount
|
||||
*/
|
||||
|
||||
?>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin"><?php e(t('Home')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('Statistics')); ?>
|
||||
</div>
|
||||
<div class="app-dashboard-titlebar">
|
||||
<h1><?php e(t('Statistics')); ?></h1>
|
||||
<div class="app-dashboard-titlebar-actions">
|
||||
<!-- Optional actions -->
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="app-dashboard">
|
||||
<div class="app-tabs" data-tabs id="stats-tabs">
|
||||
<!-- Tab Navigation -->
|
||||
<div class="app-tabs-nav">
|
||||
<button data-tab="organization" data-tab-default class="transparent tab-button">
|
||||
<?php e(t('Organization')); ?>
|
||||
</button>
|
||||
<button data-tab="users-roles" class="transparent tab-button">
|
||||
<?php e(t('Users & roles')); ?>
|
||||
</button>
|
||||
<?php if (can('mail_log.view')): ?>
|
||||
<button data-tab="system" class="transparent tab-button">
|
||||
<?php e(t('System')); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Tab Panel: Organization -->
|
||||
<div data-tab-panel="organization">
|
||||
<div class="app-tiles">
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => 'admin/tenants',
|
||||
'label' => t('Tenants'),
|
||||
'count' => (string) $tenantCount,
|
||||
'icon' => 'bi bi-building',
|
||||
'iconBg' => '#e9f0ff',
|
||||
'iconColor' => '#264db3',
|
||||
]);
|
||||
?>
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => 'admin/departments',
|
||||
'label' => t('Departments'),
|
||||
'count' => (string) $departmentCount,
|
||||
'icon' => 'bi bi-diagram-3-fill',
|
||||
'iconBg' => '#fff2d9',
|
||||
'iconColor' => '#9a5a00',
|
||||
]);
|
||||
?>
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => 'admin/users',
|
||||
'label' => t('Users'),
|
||||
'count' => (string) $userCount,
|
||||
'icon' => 'bi bi-people-fill',
|
||||
'iconBg' => '#d9f2e6',
|
||||
'iconColor' => '#1f6a3a',
|
||||
]);
|
||||
?>
|
||||
</div>
|
||||
<?php if (!empty($tenantBreakdown)): ?>
|
||||
<div class="app-stats-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<?php e(t('Tenant')); ?>
|
||||
</th>
|
||||
<th>
|
||||
<?php e(t('Departments')); ?>
|
||||
</th>
|
||||
<th>
|
||||
<?php e(t('Assigned users (Active/Inactive)')); ?>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($tenantBreakdown as $item): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?php if (can('tenants.view') && !empty($item['uuid'])): ?>
|
||||
<a href="admin/tenants/edit/<?php e($item['uuid']); ?>">
|
||||
<?php e($item['description']); ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<?php e($item['description']); ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php e($item['departments']); ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php e($item['total_users']); ?> <small>(
|
||||
<?php e($item['active_users']); ?> /
|
||||
<?php e($item['inactive_users']); ?>)
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Tab Panel: Users & Roles -->
|
||||
<div data-tab-panel="users-roles">
|
||||
<div class="grid">
|
||||
<div>
|
||||
<div class="app-tiles">
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => 'admin/users',
|
||||
'label' => t('Active users'),
|
||||
'count' => (string) $activeUserCount,
|
||||
'icon' => 'bi bi-person-check-fill',
|
||||
'iconBg' => '#d9f2e6',
|
||||
'iconColor' => '#1f6a3a',
|
||||
]);
|
||||
?>
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => 'admin/users',
|
||||
'label' => t('Inactive users'),
|
||||
'count' => (string) $inactiveUserCount,
|
||||
'icon' => 'bi bi-person-x-fill',
|
||||
'iconBg' => '#ffe9e9',
|
||||
'iconColor' => '#a32e2e',
|
||||
]);
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="app-tiles">
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => 'admin/roles',
|
||||
'label' => t('Roles'),
|
||||
'count' => (string) $roleCount,
|
||||
'icon' => 'bi bi-shield-lock-fill',
|
||||
'iconBg' => '#f3e9ff',
|
||||
'iconColor' => '#5b2c83',
|
||||
]);
|
||||
?>
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => 'admin/permissions',
|
||||
'label' => t('Permissions'),
|
||||
'count' => (string) $permissionCount,
|
||||
'icon' => 'bi bi-key-fill',
|
||||
'iconBg' => '#fff2d9',
|
||||
'iconColor' => '#9a5a00',
|
||||
]);
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Panel: System -->
|
||||
<?php if (can('mail_log.view')): ?>
|
||||
<div data-tab-panel="system">
|
||||
<div class="grid">
|
||||
<div>
|
||||
<h4><?php e(t('Mail logs')); ?></h4>
|
||||
<div class="app-tiles">
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => 'admin/mail-log',
|
||||
'label' => t('Total emails'),
|
||||
'count' => (string) $mailLogTotalCount,
|
||||
'icon' => 'bi bi-envelope-fill',
|
||||
'iconBg' => '#e9f0ff',
|
||||
'iconColor' => '#264db3',
|
||||
]);
|
||||
?>
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => 'admin/mail-log?status=sent',
|
||||
'label' => t('Sent emails'),
|
||||
'count' => (string) $mailLogSentCount,
|
||||
'icon' => 'bi bi-envelope-check-fill',
|
||||
'iconBg' => '#d9f2e6',
|
||||
'iconColor' => '#1f6a3a',
|
||||
]);
|
||||
?>
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => 'admin/mail-log?status=failed',
|
||||
'label' => t('Failed emails'),
|
||||
'count' => (string) $mailLogFailedCount,
|
||||
'icon' => 'bi bi-envelope-x-fill',
|
||||
'iconBg' => '#ffe9e9',
|
||||
'iconColor' => '#a32e2e',
|
||||
]);
|
||||
?>
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => 'admin/mail-log',
|
||||
'label' => t('Queued emails'),
|
||||
'count' => (string) $mailLogQueuedCount,
|
||||
'icon' => 'bi bi-envelope-paper-fill',
|
||||
'iconBg' => '#fff2d9',
|
||||
'iconColor' => '#9a5a00',
|
||||
]);
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4>
|
||||
<?php e(t('Last 24 hours')); ?>
|
||||
</h4>
|
||||
<div class="app-tiles">
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => 'admin/mail-log?created_from=' . urlencode($date24hAgo ?? '') . '&created_to=' . urlencode($dateToday ?? ''),
|
||||
'label' => t('Total (24h)'),
|
||||
'count' => (string) $mailLogTotal24hCount,
|
||||
'icon' => 'bi bi-envelope-fill',
|
||||
'iconBg' => '#e9f0ff',
|
||||
'iconColor' => '#264db3',
|
||||
]);
|
||||
?>
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => 'admin/mail-log?status=sent&created_from=' . urlencode($date24hAgo ?? '') . '&created_to=' . urlencode($dateToday ?? ''),
|
||||
'label' => t('Sent (24h)'),
|
||||
'count' => (string) $mailLogSent24hCount,
|
||||
'icon' => 'bi bi-envelope-check-fill',
|
||||
'iconBg' => '#d9f2e6',
|
||||
'iconColor' => '#1f6a3a',
|
||||
]);
|
||||
?>
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => 'admin/mail-log?status=failed&created_from=' . urlencode($date24hAgo ?? '') . '&created_to=' . urlencode($dateToday ?? ''),
|
||||
'label' => t('Failed (24h)'),
|
||||
'count' => (string) $mailLogFailed24hCount,
|
||||
'icon' => 'bi bi-envelope-x-fill',
|
||||
'iconBg' => '#ffe9e9',
|
||||
'iconColor' => '#a32e2e',
|
||||
]);
|
||||
?>
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => 'admin/mail-log?status=queued&created_from=' . urlencode($date24hAgo ?? '') . '&created_to=' . urlencode($dateToday ?? ''),
|
||||
'label' => t('Queued (24h)'),
|
||||
'count' => (string) $mailLogQueued24hCount,
|
||||
'icon' => 'bi bi-envelope-paper-fill',
|
||||
'iconBg' => '#fff2d9',
|
||||
'iconColor' => '#9a5a00',
|
||||
]);
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
187
pages/admin/tenants/_form.phtml
Normal file
187
pages/admin/tenants/_form.phtml
Normal file
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array $values
|
||||
* @var string|null $formId
|
||||
* @var bool $detailsOpenAll
|
||||
* @var bool $isReadOnly
|
||||
*/
|
||||
|
||||
use MintyPHP\Session;
|
||||
|
||||
$values = $values ?? [];
|
||||
$formId = $formId ?? 'tenant-form';
|
||||
$detailsOpenAll = $detailsOpenAll ?? false;
|
||||
$isReadOnly = $isReadOnly ?? false;
|
||||
$readonlyAttr = $isReadOnly ? 'readonly' : '';
|
||||
$disabledAttr = $isReadOnly ? 'disabled' : '';
|
||||
$primaryColor = (string) ($values['primary_color'] ?? '');
|
||||
$defaultPrimaryColor = appSetting('app_primary_color') ?? '#2fa4a4';
|
||||
$useDefaultPrimaryColor = $primaryColor === '';
|
||||
|
||||
?>
|
||||
<form id="<?php e($formId); ?>" method="post">
|
||||
<div class="app-tabs" data-tabs data-tabs-param="tab" data-tabs-storage-key="admin-tenant-form">
|
||||
<div class="app-tabs-nav">
|
||||
<button type="button" data-tab="basic" data-tab-default><?php e(t('Master data')); ?></button>
|
||||
<button type="button" data-tab="accounting"><?php e(t('Accounting info')); ?></button>
|
||||
<button type="button" data-tab="communication"><?php e(t('Communication')); ?></button>
|
||||
<button type="button" data-tab="support"><?php e(t('Support')); ?></button>
|
||||
<button type="button" data-tab="legal"><?php e(t('Legal')); ?></button>
|
||||
<button type="button" data-tab="appearance"><?php e(t('Appearance')); ?></button>
|
||||
</div>
|
||||
|
||||
<div data-tab-panel="basic">
|
||||
<label for="description">
|
||||
<span><?php e(t('Description')); ?></span>
|
||||
<input required type="text" name="description" id="description" value="<?php e($values['description'] ?? ''); ?>"
|
||||
<?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
<label for="status">
|
||||
<span><?php e(t('Status')); ?></span>
|
||||
<?php $statusValue = $values['status'] ?? 'active'; ?>
|
||||
<select name="status" id="status" <?php e($disabledAttr); ?>>
|
||||
<option value="active" <?php if ($statusValue === 'active') { ?>selected<?php } ?>>
|
||||
<?php e(t('Active')); ?>
|
||||
</option>
|
||||
<option value="inactive" <?php if ($statusValue === 'inactive') { ?>selected<?php } ?>>
|
||||
<?php e(t('Inactive')); ?>
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<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="country">
|
||||
<span><?php e(t('Country')); ?></span>
|
||||
<input type="text" name="country" id="country" value="<?php e($values['country'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-tab-panel="accounting">
|
||||
<div class="grid">
|
||||
<label for="vat_id">
|
||||
<span><?php e(t('VAT ID')); ?></span>
|
||||
<input type="text" name="vat_id" id="vat_id" value="<?php e($values['vat_id'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
<label for="tax_number">
|
||||
<span><?php e(t('Tax number')); ?></span>
|
||||
<input type="text" name="tax_number" id="tax_number" value="<?php e($values['tax_number'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
</div>
|
||||
<label for="billing_email">
|
||||
<span><?php e(t('Billing email')); ?></span>
|
||||
<input type="email" name="billing_email" id="billing_email" value="<?php e($values['billing_email'] ?? ''); ?>"
|
||||
<?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div data-tab-panel="communication">
|
||||
<div class="grid">
|
||||
<label for="phone">
|
||||
<span><?php e(t('Phone')); ?></span>
|
||||
<input type="tel" name="phone" id="phone" value="<?php e($values['phone'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
<label for="fax">
|
||||
<span><?php e(t('Fax')); ?></span>
|
||||
<input type="tel" name="fax" id="fax" value="<?php e($values['fax'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<label for="email">
|
||||
<span><?php e(t('Email')); ?></span>
|
||||
<input type="email" name="email" id="email" value="<?php e($values['email'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
<label for="website">
|
||||
<span><?php e(t('Website')); ?></span>
|
||||
<input type="url" name="website" id="website" value="<?php e($values['website'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-tab-panel="support">
|
||||
<div class="grid">
|
||||
<label for="support_email">
|
||||
<span><?php e(t('Support email')); ?></span>
|
||||
<input type="email" name="support_email" id="support_email" value="<?php e($values['support_email'] ?? ''); ?>"
|
||||
<?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
<label for="support_phone">
|
||||
<span><?php e(t('Support phone')); ?></span>
|
||||
<input type="tel" name="support_phone" id="support_phone" value="<?php e($values['support_phone'] ?? ''); ?>"
|
||||
<?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-tab-panel="legal">
|
||||
<div class="grid">
|
||||
<label for="privacy_url">
|
||||
<span><?php e(t('Privacy URL')); ?></span>
|
||||
<input type="url" name="privacy_url" id="privacy_url" value="<?php e($values['privacy_url'] ?? ''); ?>"
|
||||
<?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
<label for="imprint_url">
|
||||
<span><?php e(t('Imprint URL')); ?></span>
|
||||
<input type="url" name="imprint_url" id="imprint_url" value="<?php e($values['imprint_url'] ?? ''); ?>"
|
||||
<?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-tab-panel="appearance">
|
||||
<blockquote data-variant="info">
|
||||
<?php e(t('Using the default color applies the global system appearance.')); ?>
|
||||
</blockquote>
|
||||
<div class="grid">
|
||||
<fieldset>
|
||||
<legend><small><?php e(t('Primary color')); ?></small></legend>
|
||||
<label for="primary_color">
|
||||
<input type="color" name="primary_color" id="primary_color"
|
||||
value="<?php e($primaryColor !== '' ? $primaryColor : $defaultPrimaryColor); ?>"
|
||||
<?php e($readonlyAttr); ?> />
|
||||
<div>
|
||||
<?php e(t('Choose color')); ?>
|
||||
</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>
|
||||
<small>
|
||||
<?php e(t('Use default color')); ?>
|
||||
</small>
|
||||
</legend>
|
||||
<label class="app-field">
|
||||
<input type="hidden" name="primary_color_use_default" value="0">
|
||||
<input type="checkbox" name="primary_color_use_default" value="1"
|
||||
data-color-default-toggle
|
||||
data-color-target="#primary_color"
|
||||
data-color-default="<?php e($defaultPrimaryColor); ?>"
|
||||
<?php e($useDefaultPrimaryColor ? 'checked' : ''); ?>
|
||||
<?php e($readonlyAttr); ?>>
|
||||
<span><?php e(t('Use default color')); ?></span>
|
||||
</label>
|
||||
<small class="muted"><?php e(t('Be careful - this resets the tenants color')); ?></small>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
33
pages/admin/tenants/avatar($id).php
Normal file
33
pages/admin/tenants/avatar($id).php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\TenantAvatarService;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::TENANTS_UPDATE);
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/tenants');
|
||||
return;
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
if (!TenantAvatarService::isValidUuid($uuid)) {
|
||||
Flash::error('Tenant not found', 'admin/tenants', 'tenant_not_found');
|
||||
Router::redirect('admin/tenants');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = TenantAvatarService::saveUpload($uuid, $_FILES['avatar'] ?? []);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
$error = $result['error'] ?? t('Upload failed');
|
||||
Flash::error($error, "admin/tenants/edit/{$uuid}", 'tenant_avatar_upload_failed');
|
||||
Router::redirect("admin/tenants/edit/{$uuid}");
|
||||
return;
|
||||
}
|
||||
|
||||
Flash::success('Avatar updated', "admin/tenants/edit/{$uuid}", 'tenant_avatar_updated');
|
||||
Router::redirect("admin/tenants/edit/{$uuid}");
|
||||
26
pages/admin/tenants/avatar-delete($id).php
Normal file
26
pages/admin/tenants/avatar-delete($id).php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\TenantAvatarService;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::TENANTS_UPDATE);
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/tenants');
|
||||
return;
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
if (!TenantAvatarService::isValidUuid($uuid)) {
|
||||
Flash::error('Tenant not found', 'admin/tenants', 'tenant_not_found');
|
||||
Router::redirect('admin/tenants');
|
||||
return;
|
||||
}
|
||||
|
||||
TenantAvatarService::delete($uuid);
|
||||
Flash::success('Avatar removed', "admin/tenants/edit/{$uuid}", 'tenant_avatar_removed');
|
||||
Router::redirect("admin/tenants/edit/{$uuid}");
|
||||
28
pages/admin/tenants/avatar-file(none).phtml
Normal file
28
pages/admin/tenants/avatar-file(none).phtml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Service\TenantAvatarService;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$uuid = trim((string) ($_GET['uuid'] ?? ''));
|
||||
$size = isset($_GET['size']) ? (int) $_GET['size'] : null;
|
||||
if (!TenantAvatarService::isValidUuid($uuid)) {
|
||||
http_response_code(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$path = TenantAvatarService::findAvatarPath($uuid, $size);
|
||||
if (!$path || !is_file($path)) {
|
||||
http_response_code(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$mime = TenantAvatarService::detectMime($path);
|
||||
header('Content-Type: ' . $mime);
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header("Content-Security-Policy: sandbox");
|
||||
header('Cache-Control: private, max-age=300');
|
||||
header('Content-Length: ' . filesize($path));
|
||||
readfile($path);
|
||||
return;
|
||||
62
pages/admin/tenants/create().php
Normal file
62
pages/admin/tenants/create().php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\TenantService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::TENANTS_CREATE);
|
||||
|
||||
$errors = [];
|
||||
$form = [
|
||||
'description' => '',
|
||||
'address' => '',
|
||||
'postal_code' => '',
|
||||
'city' => '',
|
||||
'country' => '',
|
||||
'region' => '',
|
||||
'vat_id' => '',
|
||||
'tax_number' => '',
|
||||
'phone' => '',
|
||||
'fax' => '',
|
||||
'email' => '',
|
||||
'support_email' => '',
|
||||
'support_phone' => '',
|
||||
'billing_email' => '',
|
||||
'website' => '',
|
||||
'privacy_url' => '',
|
||||
'imprint_url' => '',
|
||||
'primary_color' => '',
|
||||
'primary_color_use_default' => 0,
|
||||
'status' => 'active',
|
||||
];
|
||||
|
||||
if (isset($_POST['description'])) {
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$result = TenantService::createFromAdmin($_POST, $currentUserId);
|
||||
$form = $result['form'] ?? $form;
|
||||
$errors = $result['errors'] ?? [];
|
||||
|
||||
if ($result['ok'] ?? false) {
|
||||
$action = (string) ($_POST['action'] ?? 'create');
|
||||
if ($action === 'create_close') {
|
||||
Flash::success('Tenant created', 'admin/tenants', 'tenant_created');
|
||||
Router::redirect('admin/tenants');
|
||||
} else {
|
||||
$uuid = (string) ($result['uuid'] ?? '');
|
||||
if ($uuid !== '') {
|
||||
$target = "admin/tenants/edit/{$uuid}";
|
||||
Flash::success('Tenant created', $target, 'tenant_created');
|
||||
Router::redirect($target);
|
||||
} else {
|
||||
Flash::success('Tenant created', 'admin/tenants', 'tenant_created');
|
||||
Router::redirect('admin/tenants');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Buffer::set('title', t('Create tenant'));
|
||||
44
pages/admin/tenants/create(default).phtml
Normal file
44
pages/admin/tenants/create(default).phtml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array<int, string> $errors
|
||||
* @var array $form
|
||||
*/
|
||||
|
||||
?>
|
||||
<div class="app-details-container">
|
||||
<section>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
|
||||
href="/admin/tenants"><?php e(t('Tenants')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('Create tenant')); ?>
|
||||
</div>
|
||||
<div class="app-details-titlebar">
|
||||
<h1>
|
||||
<a href="admin/tenants" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>
|
||||
<?php e(t('Create tenant')); ?>
|
||||
</h1>
|
||||
<div class="app-details-titlebar-actions">
|
||||
<button type="submit" form="tenant-form" name="action" value="create" class="secondary outline">
|
||||
<?php e(t('Create')); ?>
|
||||
</button>
|
||||
<button type="submit" form="tenant-form" name="action" value="create_close" class="primary">
|
||||
<?php e(t('Create & close')); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="notice" data-variant="error">
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?php e($error); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$values = $form ?? [];
|
||||
$detailsOpenAll = true;
|
||||
require __DIR__ . '/_form.phtml';
|
||||
?>
|
||||
</section>
|
||||
</div>
|
||||
43
pages/admin/tenants/data().php
Normal file
43
pages/admin/tenants/data().php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\TenantService;
|
||||
use MintyPHP\Service\SettingService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::TENANTS_VIEW);
|
||||
|
||||
$limit = (int) ($_GET['limit'] ?? 10);
|
||||
$offset = (int) ($_GET['offset'] ?? 0);
|
||||
$search = trim((string) ($_GET['search'] ?? ''));
|
||||
$order = (string) ($_GET['order'] ?? 'description');
|
||||
$dir = (string) ($_GET['dir'] ?? 'asc');
|
||||
|
||||
$result = TenantService::listPaged([
|
||||
'limit' => $limit,
|
||||
'offset' => $offset,
|
||||
'search' => $search,
|
||||
'order' => $order,
|
||||
'dir' => $dir,
|
||||
]);
|
||||
|
||||
$defaultTenantId = SettingService::getDefaultTenantId();
|
||||
$rows = [];
|
||||
foreach ($result['rows'] as $row) {
|
||||
$tenantId = (int) ($row['id'] ?? 0);
|
||||
$rows[] = [
|
||||
'id' => $row['id'] ?? null,
|
||||
'uuid' => $row['uuid'] ?? '',
|
||||
'is_default' => $tenantId > 0 && $defaultTenantId === $tenantId,
|
||||
'description' => $row['description'] ?? '',
|
||||
'created' => dt($row['created'] ?? ''),
|
||||
'modified' => dt($row['modified'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
Router::json([
|
||||
'data' => $rows,
|
||||
'total' => $result['total'] ?? 0,
|
||||
]);
|
||||
31
pages/admin/tenants/delete($id).php
Normal file
31
pages/admin/tenants/delete($id).php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\TenantService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Service\AuthService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::TENANTS_DELETE);
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
if ($uuid === '') {
|
||||
Router::redirect('admin/tenants');
|
||||
}
|
||||
|
||||
$result = TenantService::deleteByUuid($uuid);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
Flash::error('Tenant not found', 'admin/tenants', 'tenant_not_found');
|
||||
Router::redirect('admin/tenants');
|
||||
}
|
||||
|
||||
// Refresh session tenant data (removes deleted tenant, switches if it was current)
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
AuthService::loadTenantDataIntoSession($currentUserId);
|
||||
}
|
||||
|
||||
Flash::success('Tenant deleted', 'admin/tenants', 'tenant_deleted');
|
||||
Router::redirect('admin/tenants');
|
||||
82
pages/admin/tenants/edit($id).php
Normal file
82
pages/admin/tenants/edit($id).php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\TenantService;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\SettingService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::TENANTS_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
PermissionService::getUserPermissions($currentUserId);
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$tenant = $uuid !== '' ? TenantService::findByUuid($uuid) : null;
|
||||
if (!$tenant) {
|
||||
Flash::error('Tenant not found', 'admin/tenants', 'tenant_not_found');
|
||||
Router::redirect('admin/tenants');
|
||||
}
|
||||
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
$creatorId = (int) ($tenant['created_by'] ?? 0);
|
||||
if ($creatorId > 0) {
|
||||
$creator = UserService::findById($creatorId);
|
||||
if ($creator) {
|
||||
$creatorName = trim(($creator['first_name'] ?? '') . ' ' . ($creator['last_name'] ?? ''));
|
||||
$tenant['created_by_label'] = $creatorName !== '' ? $creatorName : ($creator['email'] ?? '');
|
||||
$tenant['created_by_uuid'] = $creator['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$modifierId = (int) ($tenant['modified_by'] ?? 0);
|
||||
if ($modifierId > 0) {
|
||||
$modifier = UserService::findById($modifierId);
|
||||
if ($modifier) {
|
||||
$modifierName = trim(($modifier['first_name'] ?? '') . ' ' . ($modifier['last_name'] ?? ''));
|
||||
$tenant['modified_by_label'] = $modifierName !== '' ? $modifierName : ($modifier['email'] ?? '');
|
||||
$tenant['modified_by_uuid'] = $modifier['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$statusChangedById = (int) ($tenant['status_changed_by'] ?? 0);
|
||||
if ($statusChangedById > 0) {
|
||||
$statusChanger = UserService::findById($statusChangedById);
|
||||
if ($statusChanger) {
|
||||
$statusChangerName = trim(($statusChanger['first_name'] ?? '') . ' ' . ($statusChanger['last_name'] ?? ''));
|
||||
$tenant['status_changed_by_label'] = $statusChangerName !== '' ? $statusChangerName : ($statusChanger['email'] ?? '');
|
||||
$tenant['status_changed_by_uuid'] = $statusChanger['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$form = $tenant;
|
||||
|
||||
if (isset($_POST['description'])) {
|
||||
if (!PermissionService::userHas($currentUserId, PermissionService::TENANTS_UPDATE)) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
$result = TenantService::updateFromAdmin($tenantId, $_POST, $currentUserId);
|
||||
$form = $result['form'] ?? $form;
|
||||
$errors = $result['errors'] ?? [];
|
||||
|
||||
if ($result['ok'] ?? false) {
|
||||
if ($currentUserId > 0) {
|
||||
\MintyPHP\Service\AuthService::loadTenantDataIntoSession($currentUserId);
|
||||
}
|
||||
$action = (string) ($_POST['action'] ?? 'save');
|
||||
if ($action === 'save_close') {
|
||||
Flash::success('Tenant updated', 'admin/tenants', 'tenant_updated');
|
||||
Router::redirect('admin/tenants');
|
||||
} else {
|
||||
Flash::success('Tenant updated', "admin/tenants/edit/{$uuid}", 'tenant_updated');
|
||||
Router::redirect("admin/tenants/edit/{$uuid}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Buffer::set('title', t('Edit tenant'));
|
||||
371
pages/admin/tenants/edit(default).phtml
Normal file
371
pages/admin/tenants/edit(default).phtml
Normal file
@@ -0,0 +1,371 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array<int, string> $errors
|
||||
* @var array $form
|
||||
* @var array $tenant
|
||||
*/
|
||||
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\TenantAvatarService;
|
||||
use MintyPHP\Service\TenantFaviconService;
|
||||
|
||||
$values = $form ?? $tenant ?? [];
|
||||
$isReadOnly = !can('tenants.update');
|
||||
$avatarUuid = (string) ($values['uuid'] ?? '');
|
||||
$hasAvatar = $avatarUuid !== '' && TenantAvatarService::hasAvatar($avatarUuid);
|
||||
$hasFavicon = $avatarUuid !== '' && TenantFaviconService::hasFavicon($avatarUuid);
|
||||
|
||||
?>
|
||||
|
||||
<div class="app-details-container">
|
||||
<section>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
|
||||
href="/admin/tenants"><?php e(t('Tenants')); ?></a><i
|
||||
class="bi bi-chevron-right"></i><?php e(t('Edit tenant')); ?>
|
||||
</div>
|
||||
<div class="app-details-titlebar">
|
||||
<h1>
|
||||
<a href="admin/tenants" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>
|
||||
<?php e(t('Edit tenant')); ?>
|
||||
</h1>
|
||||
<div class="app-details-titlebar-actions">
|
||||
<?php if (can('tenants.delete')): ?>
|
||||
<details class="dropdown">
|
||||
<summary role="button" class="outline secondary"><i class="bi bi-three-dots"></i></summary>
|
||||
<ul dir="rtl">
|
||||
<li>
|
||||
<form method="post" action="admin/tenants/delete/<?php e($values['uuid'] ?? ''); ?>"
|
||||
onsubmit="return confirm('<?php e(t('Delete this tenant?')); ?>');">
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
<button type="submit" class="transparent">
|
||||
<?php e(t('Delete')); ?>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (can('tenants.update')): ?>
|
||||
<button type="submit" form="tenant-form" name="action" value="save" class="secondary outline">
|
||||
<?php e(t('Save')); ?>
|
||||
</button>
|
||||
<button type="submit" form="tenant-form" name="action" value="save_close" class="primary">
|
||||
<?php e(t('Save & close')); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="app-details-errors">
|
||||
<div class="notice" data-variant="error">
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?php e($error); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$detailsOpenAll = false;
|
||||
$isReadOnly = $isReadOnly ?? false;
|
||||
require __DIR__ . '/_form.phtml';
|
||||
?>
|
||||
</section>
|
||||
<aside id="app-details-aside-section">
|
||||
<div class="app-details-aside-section">
|
||||
<div class="entity-avatar-block avatar-size-auto avatar-borderless">
|
||||
<?php if ($hasAvatar): ?>
|
||||
<a data-fslightbox="tenant-avatar" href="admin/tenants/avatar-file?uuid=<?php e($avatarUuid); ?>&size=256">
|
||||
<img class="entity-avatar-image" src="admin/tenants/avatar-file?uuid=<?php e($avatarUuid); ?>&size=128"
|
||||
alt="<?php e(t('Tenant image')); ?>">
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<hgroup>
|
||||
<h2><?php e($values['description'] ?? ''); ?></h2>
|
||||
<p><?php e(t('Tenant')); ?></p>
|
||||
</hgroup>
|
||||
<hr>
|
||||
<?php if (can('tenants.update')): ?>
|
||||
<details name="tenant-avatar">
|
||||
<summary>
|
||||
<?php e(t('Upload image')); ?>
|
||||
</summary>
|
||||
<hr>
|
||||
<form class="user-avatar-form" method="post" action="admin/tenants/avatar/<?php e($avatarUuid); ?>"
|
||||
enctype="multipart/form-data">
|
||||
<label class="user-avatar-upload">
|
||||
<input type="file" name="avatar" accept="image/*">
|
||||
</label>
|
||||
<div class="grid">
|
||||
<button type="submit" class="primary">
|
||||
<?php e(t('Save')); ?>
|
||||
</button>
|
||||
<?php if ($hasAvatar): ?>
|
||||
<button data-tooltip-pos="top" data-tooltip="<?php e(t('Remove image')); ?>" type="submit" class="danger" formaction="admin/tenants/avatar-delete/<?php e($avatarUuid); ?>"
|
||||
formmethod="post">
|
||||
<i class="bi bi-trash3-fill"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
<hr>
|
||||
<?php if (can('tenants.update')): ?>
|
||||
<details name="tenant-favicon">
|
||||
<summary>
|
||||
<?php e(t('Upload favicon')); ?>
|
||||
</summary>
|
||||
<hr>
|
||||
<?php if ($hasFavicon): ?>
|
||||
<div class="entity-avatar-block avatar-square avatar-borderless" style="--avatar-size: 48px;">
|
||||
<img class="entity-avatar-image"
|
||||
src="<?php e(asset('favicon/tenants/' . $avatarUuid . '/favicon-32x32.png')); ?>"
|
||||
alt="<?php e(t('Favicon')); ?>">
|
||||
</div>
|
||||
<small><?php e(t('Favicon preview')); ?></small>
|
||||
<hr>
|
||||
<?php endif; ?>
|
||||
<small><?php e(t('Allowed file types: PNG')); ?></small>
|
||||
<small><?php e(t('Square images are recommended (icons are center-cropped).')); ?></small>
|
||||
<hr>
|
||||
<form class="user-avatar-form" method="post" action="admin/tenants/favicon/<?php e($avatarUuid); ?>"
|
||||
enctype="multipart/form-data">
|
||||
<label class="user-avatar-upload">
|
||||
<input type="file" name="favicon" accept="image/png">
|
||||
</label>
|
||||
<div class="grid">
|
||||
<button type="submit" class="primary">
|
||||
<?php e(t('Save')); ?>
|
||||
</button>
|
||||
<?php if ($hasFavicon): ?>
|
||||
<button type="submit" class="danger" formaction="admin/tenants/favicon-delete/<?php e($avatarUuid); ?>" formmethod="post">
|
||||
<?php e(t('Remove favicon')); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
<hr>
|
||||
<?php
|
||||
$phone = trim((string) ($values['phone'] ?? ''));
|
||||
$fax = trim((string) ($values['fax'] ?? ''));
|
||||
$email = trim((string) ($values['email'] ?? ''));
|
||||
$supportEmail = trim((string) ($values['support_email'] ?? ''));
|
||||
$supportPhone = trim((string) ($values['support_phone'] ?? ''));
|
||||
$billingEmail = trim((string) ($values['billing_email'] ?? ''));
|
||||
$website = trim((string) ($values['website'] ?? ''));
|
||||
$privacyUrl = trim((string) ($values['privacy_url'] ?? ''));
|
||||
$imprintUrl = trim((string) ($values['imprint_url'] ?? ''));
|
||||
$statusValue = (string) ($values['status'] ?? 'active');
|
||||
?>
|
||||
<details name="tenant-contact">
|
||||
<summary><?php e(t('Contact')); ?></summary>
|
||||
<hr>
|
||||
<?php if ($supportEmail !== '' || $supportPhone !== '' || $billingEmail !== '' || $email !== '' || $phone !== '' || $fax !== '' || $website !== ''): ?>
|
||||
<?php if ($supportEmail !== ''): ?>
|
||||
<div>
|
||||
<small><?php e(t('Support email')); ?></small>
|
||||
<p><a href="mailto:<?php e($supportEmail); ?>"><?php e($supportEmail); ?></a></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($supportPhone !== ''): ?>
|
||||
<div>
|
||||
<small><?php e(t('Support phone')); ?></small>
|
||||
<p><a href="tel:<?php e($supportPhone); ?>"><?php e($supportPhone); ?></a></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($billingEmail !== ''): ?>
|
||||
<div>
|
||||
<small><?php e(t('Billing email')); ?></small>
|
||||
<p><a href="mailto:<?php e($billingEmail); ?>"><?php e($billingEmail); ?></a></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($email !== ''): ?>
|
||||
<div>
|
||||
<small><?php e(t('Email')); ?></small>
|
||||
<p><a href="mailto:<?php e($email); ?>"><?php e($email); ?></a></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($phone !== ''): ?>
|
||||
<div>
|
||||
<small><?php e(t('Phone')); ?></small>
|
||||
<p><a href="tel:<?php e($phone); ?>"><?php e($phone); ?></a></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($fax !== ''): ?>
|
||||
<div>
|
||||
<small><?php e(t('Fax')); ?></small>
|
||||
<div><?php e($fax); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($website !== ''): ?>
|
||||
<div>
|
||||
<small><?php e(t('Website')); ?></small>
|
||||
<div>
|
||||
<a href="<?php e($website); ?>" target="_blank" rel="noopener noreferrer">
|
||||
<?php e($website); ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<p>-</p>
|
||||
<?php endif; ?>
|
||||
</details>
|
||||
<hr>
|
||||
<details name="tenant-legal">
|
||||
<summary><?php e(t('Legal')); ?></summary>
|
||||
<hr>
|
||||
<?php if ($privacyUrl !== '' || $imprintUrl !== ''): ?>
|
||||
<?php if ($privacyUrl !== ''): ?>
|
||||
<div>
|
||||
<small><?php e(t('Privacy URL')); ?></small>
|
||||
<p>
|
||||
<a href="<?php e($privacyUrl); ?>" target="_blank" rel="noopener noreferrer">
|
||||
<?php e($privacyUrl); ?>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($imprintUrl !== ''): ?>
|
||||
<div>
|
||||
<small><?php e(t('Imprint URL')); ?></small>
|
||||
<p>
|
||||
<a href="<?php e($imprintUrl); ?>" target="_blank" rel="noopener noreferrer">
|
||||
<?php e($imprintUrl); ?>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<p>-</p>
|
||||
<?php endif; ?>
|
||||
</details>
|
||||
<hr>
|
||||
<?php
|
||||
$createdByLabel = $values['created_by_label'] ?? '';
|
||||
$createdById = $values['created_by'] ?? null;
|
||||
$createdByUuid = $values['created_by_uuid'] ?? null;
|
||||
$modifiedByLabel = $values['modified_by_label'] ?? '';
|
||||
$modifiedById = $values['modified_by'] ?? null;
|
||||
$modifiedByUuid = $values['modified_by_uuid'] ?? null;
|
||||
$statusChangedByLabel = $values['status_changed_by_label'] ?? '';
|
||||
$statusChangedById = $values['status_changed_by'] ?? null;
|
||||
$statusChangedByUuid = $values['status_changed_by_uuid'] ?? null;
|
||||
?>
|
||||
<details name="tenant-status">
|
||||
<summary><?php e(t('Status')); ?></summary>
|
||||
<hr>
|
||||
<div>
|
||||
<small><?php e(t('Status')); ?></small>
|
||||
<p><?php e($statusValue !== '' ? t(ucfirst($statusValue)) : '-'); ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<small><?php e(t('Status changed')); ?></small>
|
||||
<p><?php e(dt($values['status_changed_at'] ?? '') ?: '-'); ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<small><?php e(t('Status changed by')); ?></small>
|
||||
<p>
|
||||
<?php if ($statusChangedByLabel !== ''): ?>
|
||||
<?php if ($statusChangedByUuid): ?>
|
||||
<a href="admin/users/edit/<?php e($statusChangedByUuid); ?>"><?php e($statusChangedByLabel); ?></a>
|
||||
<?php else: ?>
|
||||
<?php e($statusChangedByLabel); ?>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($statusChangedById): ?>
|
||||
<?php e('#' . $statusChangedById); ?>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
<hr>
|
||||
<details name="tenant-audit">
|
||||
<summary><?php e(t('Audit')); ?></summary>
|
||||
<hr>
|
||||
<div>
|
||||
<small><?php e(t('Created')); ?></small>
|
||||
<p><?php e(dt($values['created'] ?? '') ?: '-'); ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<small><?php e(t('Created by')); ?></small>
|
||||
<p>
|
||||
<?php if ($createdByLabel !== ''): ?>
|
||||
<?php if ($createdByUuid): ?>
|
||||
<a href="admin/users/edit/<?php e($createdByUuid); ?>"><?php e($createdByLabel); ?></a>
|
||||
<?php else: ?>
|
||||
<?php e($createdByLabel); ?>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($createdById): ?>
|
||||
<?php e('#' . $createdById); ?>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<small><?php e(t('Modified')); ?></small>
|
||||
<p><?php e(dt($values['modified'] ?? '') ?: '-'); ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<small><?php e(t('Modified by')); ?></small>
|
||||
<p>
|
||||
<?php if ($modifiedByLabel !== ''): ?>
|
||||
<?php if ($modifiedByUuid): ?>
|
||||
<a href="admin/users/edit/<?php e($modifiedByUuid); ?>"><?php e($modifiedByLabel); ?></a>
|
||||
<?php else: ?>
|
||||
<?php e($modifiedByLabel); ?>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($modifiedById): ?>
|
||||
<?php e('#' . $modifiedById); ?>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small>
|
||||
<?php e(t('UUID')); ?>
|
||||
</small>
|
||||
<p>
|
||||
<span class="badge" data-variant="neutral" data-copy="true"
|
||||
data-copy-value="<?php e($values['uuid'] ?? ''); ?>">
|
||||
<?php
|
||||
$uuidValue = (string) ($values['uuid'] ?? '');
|
||||
$uuidDisplay = $uuidValue !== '' ? substr($uuidValue, 0, 10) : '-';
|
||||
e($uuidDisplay);
|
||||
?>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
<?php e(t('ID')); ?>
|
||||
</small>
|
||||
<p>
|
||||
<span class="badge" data-variant="neutral" data-copy="true"
|
||||
data-copy-value="<?php e($values['id'] ?? ''); ?>">
|
||||
<?php e($values['id'] ?? '-'); ?>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
33
pages/admin/tenants/favicon($id).php
Normal file
33
pages/admin/tenants/favicon($id).php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\TenantFaviconService;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::TENANTS_UPDATE);
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/tenants');
|
||||
return;
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
if (!TenantFaviconService::isValidUuid($uuid)) {
|
||||
Flash::error('Tenant not found', 'admin/tenants', 'tenant_not_found');
|
||||
Router::redirect('admin/tenants');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = TenantFaviconService::saveUpload($uuid, $_FILES['favicon'] ?? []);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
$error = $result['error'] ?? t('Upload failed');
|
||||
Flash::error($error, "admin/tenants/edit/{$uuid}", 'tenant_favicon_upload_failed');
|
||||
Router::redirect("admin/tenants/edit/{$uuid}");
|
||||
return;
|
||||
}
|
||||
|
||||
Flash::success('Favicon updated', "admin/tenants/edit/{$uuid}", 'tenant_favicon_updated');
|
||||
Router::redirect("admin/tenants/edit/{$uuid}");
|
||||
26
pages/admin/tenants/favicon-delete($id).php
Normal file
26
pages/admin/tenants/favicon-delete($id).php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\TenantFaviconService;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::TENANTS_UPDATE);
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/tenants');
|
||||
return;
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
if (!TenantFaviconService::isValidUuid($uuid)) {
|
||||
Flash::error('Tenant not found', 'admin/tenants', 'tenant_not_found');
|
||||
Router::redirect('admin/tenants');
|
||||
return;
|
||||
}
|
||||
|
||||
TenantFaviconService::delete($uuid);
|
||||
Flash::success('Favicon removed', "admin/tenants/edit/{$uuid}", 'tenant_favicon_removed');
|
||||
Router::redirect("admin/tenants/edit/{$uuid}");
|
||||
23
pages/admin/tenants/index().php
Normal file
23
pages/admin/tenants/index().php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::TENANTS_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
PermissionService::getUserPermissions($currentUserId);
|
||||
}
|
||||
|
||||
Buffer::set('title', t('Tenants'));
|
||||
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
$csrfKey = Session::$csrfSessionKey;
|
||||
$csrfToken = $_SESSION[$csrfKey] ?? '';
|
||||
Buffer::set(
|
||||
'grid_csrf',
|
||||
json_encode(['key' => $csrfKey, 'token' => $csrfToken], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
);
|
||||
159
pages/admin/tenants/index(default).phtml
Normal file
159
pages/admin/tenants/index(default).phtml
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
use MintyPHP\Router;
|
||||
|
||||
$canCreateTenants = can('tenants.create');
|
||||
$canDeleteTenants = can('tenants.delete');
|
||||
?>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><?php e(t('Tenants')); ?>
|
||||
</div>
|
||||
<div class="app-list-titlebar">
|
||||
<h1><?php e(t('Tenants')); ?></h1>
|
||||
<div class="app-list-titlebar-actions">
|
||||
<button
|
||||
class="outline secondary"
|
||||
type="button"
|
||||
data-toolbar-toggle
|
||||
data-toolbar-target="#tenants-toolbar"
|
||||
data-toolbar-text-show="<?php e(t('Show filters')); ?>"
|
||||
data-toolbar-text-hide="<?php e(t('Hide filters')); ?>"
|
||||
>
|
||||
<i class="bi bi-funnel-fill"></i> <span data-toolbar-label><?php e(t('Hide filters')); ?></span>
|
||||
</button>
|
||||
<?php if ($canCreateTenants): ?>
|
||||
<a role="button" class="primary" href="admin/tenants/create">
|
||||
<i class="bi bi-plus"></i> <?php e(t('Create tenant')); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-list-toolbar" id="tenants-toolbar">
|
||||
<input type="search" id="tenant-search" placeholder="<?php e(t('Search...')); ?>">
|
||||
</div>
|
||||
<div class="app-list-table">
|
||||
<div id="tenants-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 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 { buildUrl, postAction, badgeHtml } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
|
||||
|
||||
initToolbarToggles();
|
||||
|
||||
const appBase = "<?php e(localeBase()); ?>";
|
||||
const csrf = <?php MintyPHP\Buffer::get('grid_csrf'); ?>;
|
||||
const deleteConfirm = "<?php e(t('Delete this tenant?')); ?>";
|
||||
const buildActionUrl = (action, id) => buildUrl(appBase, `admin/tenants/${action}/${id}`);
|
||||
|
||||
const initTenantsGrid = () => {
|
||||
const formatBadge = (value) => badgeHtml(gridjs, value);
|
||||
const gridConfig = createServerGrid({
|
||||
gridjs: window.gridjs,
|
||||
container: '#tenants-grid',
|
||||
dataUrl: 'admin/tenants/data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
name: "<?php e(t('Description')); ?>",
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
if (!cell || typeof cell !== 'object') {
|
||||
return gridjs.html(String(cell ?? ''));
|
||||
}
|
||||
const label = cell.label ?? '';
|
||||
return gridjs.html(`${label}`);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "<?php e(t('Created')); ?>",
|
||||
sort: true,
|
||||
formatter: (cell) => formatBadge(cell)
|
||||
},
|
||||
{
|
||||
name: "<?php e(t('Modified')); ?>",
|
||||
sort: true,
|
||||
formatter: (cell) => formatBadge(cell)
|
||||
},
|
||||
{
|
||||
name: "UUID",
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
name: "ID",
|
||||
hidden: true
|
||||
}
|
||||
],
|
||||
sortColumns: ['description', 'created', 'modified', null, null],
|
||||
paginationLimit: 10,
|
||||
language: <?php MintyPHP\Buffer::get('grid_lang'); ?>,
|
||||
mapData: (data) => data.data.map((row) => [
|
||||
{
|
||||
label: row.description,
|
||||
is_default: row.is_default ? 1 : 0
|
||||
},
|
||||
row.created,
|
||||
row.modified,
|
||||
row.uuid,
|
||||
row.id
|
||||
]),
|
||||
actions: {
|
||||
enabled: true,
|
||||
label: "<?php e(t('Actions')); ?>",
|
||||
getRowId: (row) => row?.cells?.[3]?.data,
|
||||
items: [
|
||||
{
|
||||
key: 'edit',
|
||||
type: 'link',
|
||||
label: "<i class=\"bi bi-pencil-fill\"></i>",
|
||||
href: ({ id }) => buildActionUrl('edit', id)
|
||||
},
|
||||
<?php if ($canDeleteTenants): ?>
|
||||
{
|
||||
key: 'delete',
|
||||
label: "<i class=\"bi bi-trash3-fill\"></i>",
|
||||
className: 'danger outline',
|
||||
confirm: deleteConfirm
|
||||
}
|
||||
<?php endif; ?>
|
||||
],
|
||||
onAction: async ({ action, id, grid }) => {
|
||||
if (action !== 'delete') return;
|
||||
const url = buildActionUrl('delete', id);
|
||||
const response = await postAction(url, csrf);
|
||||
if (response.ok) {
|
||||
grid.forceRender();
|
||||
} else {
|
||||
window.location.href = new URL('admin/tenants', appBase).toString();
|
||||
}
|
||||
}
|
||||
},
|
||||
search: {
|
||||
input: '#tenant-search',
|
||||
param: 'search',
|
||||
debounce: 250
|
||||
},
|
||||
urlSync: true,
|
||||
rowDataset: (row) => ({
|
||||
uuid: row?.cells?.[3]?.data
|
||||
}),
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => {
|
||||
const uuid = rowData?.cells?.[3]?.data;
|
||||
return uuid ? new URL(`admin/tenants/edit/${uuid}`, appBase).toString() : '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!gridConfig || !gridConfig.grid) {
|
||||
console.warn('Tenants grid init failed');
|
||||
return null;
|
||||
}
|
||||
|
||||
return gridConfig;
|
||||
};
|
||||
|
||||
initTenantsGrid();
|
||||
</script>
|
||||
359
pages/admin/users/_form.phtml
Normal file
359
pages/admin/users/_form.phtml
Normal file
@@ -0,0 +1,359 @@
|
||||
<?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 $departments
|
||||
* @var array $selectedDepartmentIds
|
||||
* @var bool $isReadOnly
|
||||
* @var bool $showPermissions
|
||||
* @var array $permissionRows
|
||||
*/
|
||||
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\I18n;
|
||||
|
||||
$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;
|
||||
$departments = $departments ?? [];
|
||||
$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;
|
||||
$minLength = UserService::passwordMinLength();
|
||||
$requiredAttr = $passwordRequired ? 'required' : '';
|
||||
$readonlyAttr = $isReadOnly ? 'readonly' : '';
|
||||
$disabledAttr = $isReadOnly ? 'disabled' : '';
|
||||
$dataDisabledAttr = $isReadOnly ? 'data-disabled="true"' : '';
|
||||
$locales = defined('APP_LOCALES') ? APP_LOCALES : [I18n::$defaultLocale];
|
||||
$primaryTenantDisabled = $isReadOnly || count($selectedTenantIds) === 1;
|
||||
$primaryTenantDisabledAttr = $primaryTenantDisabled ? 'disabled' : '';
|
||||
$showOrganization = $showTenants || $showDepartments || $showRoles;
|
||||
|
||||
?>
|
||||
<form id="<?php e($formId); ?>" method="post">
|
||||
<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('Profile')); ?></button>
|
||||
<button type="button" data-tab="access"><?php e(t('Access & password')); ?></button>
|
||||
<?php if ($showOrganization): ?>
|
||||
<button type="button" data-tab="organization"><?php e(t('Organization')); ?></button>
|
||||
<?php endif; ?>
|
||||
<button type="button" data-tab="masterdata"><?php e(t('Master data')); ?></button>
|
||||
<?php if ($showPermissions): ?>
|
||||
<button type="button" data-tab="permissions"><?php e(t('Permissions')); ?></button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div data-tab-panel="profile">
|
||||
<details name="identity-data" open>
|
||||
<summary><?php e(t('Identity')); ?></summary>
|
||||
<hr>
|
||||
<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>
|
||||
</details>
|
||||
<hr>
|
||||
<details name="profile-data" 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" open>
|
||||
<summary><?php e(t('Preferences')); ?></summary>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<?php if (allowUserTheme()): ?>
|
||||
<label for="theme">
|
||||
<span><?php e(t('Theme')); ?></span>
|
||||
<?php $selectedTheme = $values['theme'] ?? 'light'; ?>
|
||||
<select name="theme" id="theme" <?php e($disabledAttr); ?>>
|
||||
<option value="light" <?php if ($selectedTheme === 'light') { ?>selected<?php } ?>>
|
||||
<?php e(t('Light')); ?>
|
||||
</option>
|
||||
<option value="dark" <?php if ($selectedTheme === 'dark') { ?>selected<?php } ?>>
|
||||
<?php e(t('Dark')); ?>
|
||||
</option>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div data-tab-panel="access">
|
||||
<details name="access-data" 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 (UserService::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>
|
||||
<?php if (!$isOwnAccount): ?>
|
||||
<label for="active">
|
||||
<input type="checkbox" name="active" id="active" value="1" <?php e(($values['active'] ?? 0) ? 'checked' : ''); ?>
|
||||
<?php e($disabledAttr); ?> />
|
||||
<span><?php e(t('Active')); ?></span>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<?php if ($showOrganization): ?>
|
||||
<div data-tab-panel="organization">
|
||||
<?php if ($showTenants): ?>
|
||||
<details name="assignments-tenants" open>
|
||||
<summary><?php e(t('Tenants')); ?></summary>
|
||||
<hr>
|
||||
<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 multiSelectForm('user-tenants', 'tenant_ids', 'Assigned tenants', 'Select tenants', $tenants, $selectedTenantIds, $isReadOnly); ?>
|
||||
</details>
|
||||
<hr>
|
||||
<?php endif; ?>
|
||||
<?php if ($showDepartments || $showRoles): ?>
|
||||
<details name="assignments-structure" open>
|
||||
<summary><?php e(t('Departments & roles')); ?></summary>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<?php if ($showDepartments): ?>
|
||||
<div>
|
||||
<?php multiSelectForm('user-departments', 'department_ids', 'Assigned departments', 'Select departments', $departments, $selectedDepartmentIds, $isReadOnly); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($showRoles): ?>
|
||||
<div>
|
||||
<?php multiSelectForm('user-roles', 'role_ids', 'Assigned roles', 'Select roles', $roles, $selectedRoleIds, $isReadOnly); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div data-tab-panel="masterdata">
|
||||
<details name="contact-data" 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="address-data" 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="employment-data" 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>
|
||||
<?php if ($showPermissions): ?>
|
||||
<div data-tab-panel="permissions">
|
||||
<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; ?>
|
||||
</div>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
41
pages/admin/users/activate($id).php
Normal file
41
pages/admin/users/activate($id).php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId > 0 && $currentUserId !== $userId && !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(403);
|
||||
Router::json(['error' => 'permission_denied']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
$result = UserService::setActiveByUuid($uuid, true, $currentUserId);
|
||||
if ($result['ok'] ?? false) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(204);
|
||||
return;
|
||||
}
|
||||
} elseif (Request::wantsJson()) {
|
||||
http_response_code((int) ($result['status'] ?? 404));
|
||||
Router::json(['error' => 'not_found']);
|
||||
return;
|
||||
}
|
||||
|
||||
Router::redirect('admin/users');
|
||||
41
pages/admin/users/avatar($id).php
Normal file
41
pages/admin/users/avatar($id).php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\UserAvatarService;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
if (!UserAvatarService::isValidUuid($uuid)) {
|
||||
Flash::error('User not found', 'admin/users', 'user_not_found');
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId > 0 && $currentUserId !== $userId && !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = UserAvatarService::saveUpload($uuid, $_FILES['avatar'] ?? []);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
$error = $result['error'] ?? t('Upload failed');
|
||||
Flash::error($error, "admin/users/edit/{$uuid}", 'avatar_upload_failed');
|
||||
Router::redirect("admin/users/edit/{$uuid}");
|
||||
return;
|
||||
}
|
||||
|
||||
Flash::success('Avatar updated', "admin/users/edit/{$uuid}", 'avatar_updated');
|
||||
Router::redirect("admin/users/edit/{$uuid}");
|
||||
34
pages/admin/users/avatar-delete($id).php
Normal file
34
pages/admin/users/avatar-delete($id).php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\UserAvatarService;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
if (!UserAvatarService::isValidUuid($uuid)) {
|
||||
Flash::error('User not found', 'admin/users', 'user_not_found');
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId > 0 && $currentUserId !== $userId && !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
UserAvatarService::delete($uuid);
|
||||
Flash::success('Avatar removed', "admin/users/edit/{$uuid}", 'avatar_removed');
|
||||
Router::redirect("admin/users/edit/{$uuid}");
|
||||
37
pages/admin/users/avatar-file().php
Normal file
37
pages/admin/users/avatar-file().php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Service\UserAvatarService;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$uuid = trim((string) ($_GET['uuid'] ?? ''));
|
||||
$size = isset($_GET['size']) ? (int) $_GET['size'] : null;
|
||||
if (!UserAvatarService::isValidUuid($uuid)) {
|
||||
http_response_code(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId > 0 && $currentUserId !== $userId && !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
http_response_code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
$path = UserAvatarService::findAvatarPath($uuid, $size);
|
||||
if (!$path || !is_file($path)) {
|
||||
http_response_code(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$mime = UserAvatarService::detectMime($path);
|
||||
header('Content-Type: ' . $mime);
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Content-Security-Policy: sandbox');
|
||||
header('Cache-Control: private, max-age=300');
|
||||
header('Content-Length: ' . filesize($path));
|
||||
readfile($path);
|
||||
4
pages/admin/users/avatar-file(none).phtml
Normal file
4
pages/admin/users/avatar-file(none).phtml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
|
||||
http_response_code(404);
|
||||
return;
|
||||
135
pages/admin/users/bulk($action).php
Normal file
135
pages/admin/users/bulk($action).php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\MailService;
|
||||
use MintyPHP\I18n;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$action = strtolower(trim((string) ($action ?? '')));
|
||||
$handlers = [
|
||||
'activate' => 'handleActivate',
|
||||
'deactivate' => 'handleDeactivate',
|
||||
'delete' => 'handleDelete',
|
||||
'send-access' => 'handleSendAccess',
|
||||
];
|
||||
if (!isset($handlers[$action])) {
|
||||
Router::json(['ok' => false, 'error' => 'invalid_action']);
|
||||
}
|
||||
if ($action === 'delete') {
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_DELETE);
|
||||
}
|
||||
if ($action === 'send-access') {
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_UPDATE);
|
||||
}
|
||||
|
||||
$raw = $_POST['uuids'] ?? '';
|
||||
$uuids = [];
|
||||
if (is_array($raw)) {
|
||||
$uuids = $raw;
|
||||
} elseif (is_string($raw)) {
|
||||
$uuids = array_filter(array_map('trim', explode(',', $raw)));
|
||||
}
|
||||
$uuids = array_values(array_unique(array_filter($uuids)));
|
||||
if (!$uuids) {
|
||||
Router::json(['ok' => false, 'error' => 'no_selection']);
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
call_user_func($handlers[$action], $uuids, $currentUserId);
|
||||
|
||||
function handleActivate(array $uuids, int $currentUserId): void
|
||||
{
|
||||
$result = UserService::setActiveByUuids($uuids, true, $currentUserId);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
Router::json(['ok' => false, 'error' => 'update_failed']);
|
||||
}
|
||||
Router::json(['ok' => true, 'count' => (int) ($result['count'] ?? 0)]);
|
||||
}
|
||||
|
||||
function handleDeactivate(array $uuids, int $currentUserId): void
|
||||
{
|
||||
if ($currentUserId > 0) {
|
||||
$currentUser = UserService::findById($currentUserId);
|
||||
$currentUuid = $currentUser['uuid'] ?? '';
|
||||
if ($currentUuid !== '') {
|
||||
$uuids = array_values(array_filter($uuids, static fn ($uuid) => $uuid !== $currentUuid));
|
||||
}
|
||||
if (!$uuids) {
|
||||
Router::json(['ok' => false, 'error' => 'self_deactivate']);
|
||||
}
|
||||
}
|
||||
|
||||
$result = UserService::setActiveByUuids($uuids, false, $currentUserId);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
Router::json(['ok' => false, 'error' => 'update_failed']);
|
||||
}
|
||||
Router::json(['ok' => true, 'count' => (int) ($result['count'] ?? 0)]);
|
||||
}
|
||||
|
||||
function handleDelete(array $uuids, int $currentUserId): void
|
||||
{
|
||||
$result = UserService::deleteByUuids($uuids, $currentUserId);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
Router::json(['ok' => false, 'error' => $result['error'] ?? 'delete_failed']);
|
||||
}
|
||||
Router::json(['ok' => true, 'count' => (int) ($result['count'] ?? 0)]);
|
||||
}
|
||||
|
||||
function handleSendAccess(array $uuids, int $currentUserId): void
|
||||
{
|
||||
$successCount = 0;
|
||||
$failedCount = 0;
|
||||
|
||||
foreach ($uuids as $uuid) {
|
||||
$user = UserService::findByUuid($uuid);
|
||||
if (!$user || empty($user['email'])) {
|
||||
$failedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$locale = $user['locale'] ?? (I18n::$locale ?? I18n::$defaultLocale);
|
||||
$name = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
|
||||
$isGerman = strpos((string) $locale, 'de') === 0;
|
||||
$greeting = $isGerman ? 'Hallo' : 'Hello';
|
||||
if ($name !== '') {
|
||||
$greeting .= ' ' . $name;
|
||||
}
|
||||
$greeting .= ',';
|
||||
|
||||
$loginUrl = appUrl(Request::withLocale('login', $locale));
|
||||
$resetUrl = appUrl(Request::withLocale('password/forgot', $locale));
|
||||
$previousLocale = I18n::$locale ?? null;
|
||||
I18n::$locale = $locale;
|
||||
$subject = t('Your access details');
|
||||
I18n::$locale = $previousLocale;
|
||||
|
||||
$vars = [
|
||||
'app_name' => appTitle(),
|
||||
'app_logo_url' => appLogoUrlAbsolute(128),
|
||||
'imprint_url' => appUrl(Request::withLocale('imprint', $locale)),
|
||||
'privacy_url' => appUrl(Request::withLocale('privacy', $locale)),
|
||||
'greeting' => $greeting,
|
||||
'username' => (string) ($user['email'] ?? ''),
|
||||
'login_url' => $loginUrl,
|
||||
'reset_url' => $resetUrl,
|
||||
];
|
||||
|
||||
$result = MailService::sendTemplate('access_info', $vars, (string) $user['email'], $subject, $locale);
|
||||
if ($result['ok'] ?? false) {
|
||||
$successCount++;
|
||||
} else {
|
||||
$failedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
Router::json([
|
||||
'ok' => $successCount > 0,
|
||||
'count' => $successCount,
|
||||
'failed' => $failedCount,
|
||||
]);
|
||||
}
|
||||
93
pages/admin/users/create().php
Normal file
93
pages/admin/users/create().php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\I18n;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
use MintyPHP\Service\TenantService;
|
||||
use MintyPHP\Service\RoleService;
|
||||
use MintyPHP\Service\DepartmentService;
|
||||
use MintyPHP\Service\SettingService;
|
||||
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_CREATE);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$allowedTenantIds = TenantScopeService::getUserTenantIds($currentUserId);
|
||||
|
||||
$tenants = TenantService::list();
|
||||
if ($allowedTenantIds) {
|
||||
$tenants = array_values(array_filter($tenants, static function (array $tenant) use ($allowedTenantIds): bool {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
return $tenantId > 0 && in_array($tenantId, $allowedTenantIds, true);
|
||||
}));
|
||||
} elseif (TenantScopeService::isStrict()) {
|
||||
$tenants = [];
|
||||
}
|
||||
$roles = RoleService::list();
|
||||
$departments = $allowedTenantIds ? DepartmentService::listByTenantIds($allowedTenantIds) : [];
|
||||
if (!$allowedTenantIds && !TenantScopeService::isStrict()) {
|
||||
$departments = DepartmentService::list();
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$form = [
|
||||
'first_name' => '',
|
||||
'last_name' => '',
|
||||
'email' => '',
|
||||
'profile_description' => '',
|
||||
'job_title' => '',
|
||||
'phone' => '',
|
||||
'mobile' => '',
|
||||
'short_dial' => '',
|
||||
'address' => '',
|
||||
'postal_code' => '',
|
||||
'city' => '',
|
||||
'country' => '',
|
||||
'region' => '',
|
||||
'hire_date' => '',
|
||||
'totp_secret' => '',
|
||||
'locale' => I18n::$locale ?? I18n::$defaultLocale,
|
||||
'active' => 1,
|
||||
];
|
||||
|
||||
if (isset($_POST['email'])) {
|
||||
$input = $_POST;
|
||||
$tenantIds = UserService::normalizeIdInput($input['tenant_ids'] ?? []);
|
||||
if ($allowedTenantIds) {
|
||||
$tenantIds = array_values(array_intersect($tenantIds, $allowedTenantIds));
|
||||
} elseif (TenantScopeService::isStrict()) {
|
||||
$tenantIds = [];
|
||||
}
|
||||
$defaultTenantId = SettingService::getDefaultTenantId();
|
||||
if (TenantScopeService::isStrict() && !$tenantIds && !$defaultTenantId) {
|
||||
$errors[] = t('Please select at least one tenant');
|
||||
}
|
||||
$input['tenant_ids'] = $tenantIds;
|
||||
$result = UserService::createFromAdmin($input, $currentUserId);
|
||||
$form = $result['form'] ?? $form;
|
||||
$errors = array_merge($errors, $result['errors'] ?? []);
|
||||
|
||||
if (($result['ok'] ?? false) && !$errors) {
|
||||
$action = (string) ($_POST['action'] ?? 'create');
|
||||
if ($action === 'create_close') {
|
||||
Flash::success('User created', 'admin/users', 'user_created');
|
||||
Router::redirect('admin/users');
|
||||
} else {
|
||||
$uuid = (string) ($result['uuid'] ?? '');
|
||||
if ($uuid !== '') {
|
||||
$target = "admin/users/edit/{$uuid}";
|
||||
Flash::success('User created', $target, 'user_created');
|
||||
Router::redirect($target);
|
||||
} else {
|
||||
Flash::success('User created', 'admin/users', 'user_created');
|
||||
Router::redirect('admin/users');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Buffer::set('title', t('Create user'));
|
||||
51
pages/admin/users/create(default).phtml
Normal file
51
pages/admin/users/create(default).phtml
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array<int, string> $errors
|
||||
* @var array $form
|
||||
*/
|
||||
|
||||
|
||||
|
||||
?>
|
||||
<div class="app-details-container">
|
||||
|
||||
<section>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
|
||||
href="/admin/users"><?php e(t('Users')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('Create user')); ?>
|
||||
</div>
|
||||
<div class="app-details-titlebar">
|
||||
<h1>
|
||||
<a href="admin/users" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>
|
||||
<?php e(t('Create user')); ?>
|
||||
</h1>
|
||||
<div class="app-details-titlebar-actions">
|
||||
<button type="submit" form="user-form" name="action" value="create" class="secondary outline">
|
||||
<?php e(t('Create')); ?>
|
||||
</button>
|
||||
<button type="submit" form="user-form" name="action" value="create_close" class="primary">
|
||||
<?php e(t('Create & close')); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="notice" data-variant="error">
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?php e($error); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$values = $form ?? [];
|
||||
$passwordRequired = true;
|
||||
$detailsOpenAll = true;
|
||||
$passwordLabel = t('Password');
|
||||
$passwordLegend = t('Password');
|
||||
$passwordConfirmLabel = t('Password (again)');
|
||||
require __DIR__ . '/_form.phtml';
|
||||
?>
|
||||
</section>
|
||||
</div>
|
||||
106
pages/admin/users/data().php
Normal file
106
pages/admin/users/data().php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\UserAvatarService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
if (!isset($_SESSION['user'])) {
|
||||
http_response_code(401);
|
||||
Router::json(['data' => [], 'total' => 0]);
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
|
||||
$limit = (int) ($_GET['limit'] ?? 10);
|
||||
$offset = (int) ($_GET['offset'] ?? 0);
|
||||
$search = trim((string) ($_GET['search'] ?? ''));
|
||||
$order = (string) ($_GET['order'] ?? 'last_name');
|
||||
$dir = (string) ($_GET['dir'] ?? 'asc');
|
||||
$active = $_GET['active'] ?? 'all';
|
||||
$createdFrom = trim((string) ($_GET['created_from'] ?? ''));
|
||||
$createdTo = trim((string) ($_GET['created_to'] ?? ''));
|
||||
$tenant = trim((string) ($_GET['tenant'] ?? ''));
|
||||
$roles = $_GET['roles'] ?? '';
|
||||
$departments = $_GET['departments'] ?? '';
|
||||
$emailVerified = $_GET['email_verified'] ?? 'all';
|
||||
|
||||
$result = UserService::listPaged([
|
||||
'limit' => $limit,
|
||||
'offset' => $offset,
|
||||
'search' => $search,
|
||||
'order' => $order,
|
||||
'dir' => $dir,
|
||||
'active' => $active,
|
||||
'created_from' => $createdFrom,
|
||||
'created_to' => $createdTo,
|
||||
'tenant' => $tenant,
|
||||
'roles' => $roles,
|
||||
'departments' => $departments,
|
||||
'email_verified' => $emailVerified,
|
||||
'tenantUserId' => $currentUserId,
|
||||
]);
|
||||
|
||||
$rows = [];
|
||||
foreach ($result['rows'] as $row) {
|
||||
$tenantLabels = $row['tenant_labels'] ?? [];
|
||||
if (is_string($tenantLabels)) {
|
||||
$tenantList = $tenantLabels !== ''
|
||||
? array_values(array_filter(array_map('trim', explode('||', $tenantLabels))))
|
||||
: [];
|
||||
} elseif (is_array($tenantLabels)) {
|
||||
$tenantList = array_values(array_filter(array_map('trim', $tenantLabels)));
|
||||
} else {
|
||||
$tenantList = [];
|
||||
}
|
||||
$roleLabels = $row['role_labels'] ?? [];
|
||||
if (is_string($roleLabels)) {
|
||||
$roleList = $roleLabels !== ''
|
||||
? array_values(array_filter(array_map('trim', explode('||', $roleLabels))))
|
||||
: [];
|
||||
} elseif (is_array($roleLabels)) {
|
||||
$roleList = array_values(array_filter(array_map('trim', $roleLabels)));
|
||||
} else {
|
||||
$roleList = [];
|
||||
}
|
||||
$departmentLabels = $row['department_labels'] ?? [];
|
||||
if (is_string($departmentLabels)) {
|
||||
$departmentList = $departmentLabels !== ''
|
||||
? array_values(array_filter(array_map('trim', explode('||', $departmentLabels))))
|
||||
: [];
|
||||
} elseif (is_array($departmentLabels)) {
|
||||
$departmentList = array_values(array_filter(array_map('trim', $departmentLabels)));
|
||||
} else {
|
||||
$departmentList = [];
|
||||
}
|
||||
$primaryTenantLabel = (string) ($row['primary_tenant_label'] ?? '');
|
||||
if ($primaryTenantLabel === '' && count($tenantList) === 1) {
|
||||
$primaryTenantLabel = (string) $tenantList[0];
|
||||
}
|
||||
$uuid = (string) ($row['uuid'] ?? '');
|
||||
$rows[] = [
|
||||
'id' => $row['id'] ?? null,
|
||||
'uuid' => $uuid,
|
||||
'first_name' => $row['first_name'] ?? '',
|
||||
'last_name' => $row['last_name'] ?? '',
|
||||
'email' => $row['email'] ?? '',
|
||||
'phone' => $row['phone'] ?? '',
|
||||
'mobile' => $row['mobile'] ?? '',
|
||||
'short_dial' => $row['short_dial'] ?? '',
|
||||
'tenants' => [
|
||||
'items' => $tenantList,
|
||||
'primary' => $primaryTenantLabel,
|
||||
],
|
||||
'departments' => $departmentList,
|
||||
'roles' => $roleList,
|
||||
'created' => dt($row['created'] ?? ''),
|
||||
'modified' => dt($row['modified'] ?? ''),
|
||||
'active' => (int) ($row['active'] ?? 0),
|
||||
'has_avatar' => $uuid !== '' && UserAvatarService::hasAvatar($uuid),
|
||||
];
|
||||
}
|
||||
|
||||
Router::json([
|
||||
'data' => $rows,
|
||||
'total' => $result['total'] ?? 0,
|
||||
]);
|
||||
53
pages/admin/users/deactivate($id).php
Normal file
53
pages/admin/users/deactivate($id).php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId > 0 && $currentUserId !== $userId && !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(403);
|
||||
Router::json(['error' => 'permission_denied']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
$result = UserService::setActiveByUuid($uuid, false, $currentUserId);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
if (($result['error'] ?? '') === 'self_deactivate') {
|
||||
Flash::error('You cannot deactivate your own account', 'admin/users', 'user_self_deactivate');
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(400);
|
||||
Router::json(['error' => 'self_deactivate']);
|
||||
return;
|
||||
}
|
||||
} elseif (Request::wantsJson()) {
|
||||
http_response_code((int) ($result['status'] ?? 404));
|
||||
Router::json(['error' => 'not_found']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(204);
|
||||
return;
|
||||
}
|
||||
|
||||
Router::redirect('admin/users');
|
||||
61
pages/admin/users/delete($id).php
Normal file
61
pages/admin/users/delete($id).php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_DELETE);
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId > 0 && $currentUserId !== $userId && !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(403);
|
||||
Router::json(['error' => 'permission_denied']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
$result = UserService::deleteByUuid($uuid, $currentUserId);
|
||||
|
||||
if (!($result['ok'] ?? false)) {
|
||||
if (($result['error'] ?? '') === 'self_delete') {
|
||||
Flash::error('You cannot delete your own account', 'admin/users', 'user_self_delete');
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(400);
|
||||
Router::json(['error' => 'self_delete']);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
Flash::error('User not found', 'admin/users', 'user_not_found');
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(404);
|
||||
Router::json(['error' => 'not_found']);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(204);
|
||||
Router::json([]);
|
||||
return;
|
||||
}
|
||||
|
||||
Flash::success('User deleted', 'admin/users', 'user_deleted');
|
||||
Router::redirect('admin/users');
|
||||
160
pages/admin/users/edit($id).php
Normal file
160
pages/admin/users/edit($id).php
Normal file
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\I18n;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Repository\UserRoleRepository;
|
||||
use MintyPHP\Repository\UserTenantRepository;
|
||||
use MintyPHP\Repository\UserDepartmentRepository;
|
||||
use MintyPHP\Repository\RolePermissionRepository;
|
||||
use MintyPHP\Service\AuthService;
|
||||
use MintyPHP\Service\DepartmentService;
|
||||
use MintyPHP\Service\RoleService;
|
||||
use MintyPHP\Service\TenantService;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
PermissionService::getUserPermissions($currentUserId, true);
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
if (!$user) {
|
||||
Flash::error('User not found', 'admin/users', 'user_not_found');
|
||||
Router::redirect('admin/users');
|
||||
}
|
||||
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
$canViewUsers = PermissionService::userHas($currentUserId, PermissionService::USERS_VIEW);
|
||||
$canUpdateUser = PermissionService::userHas($currentUserId, PermissionService::USERS_UPDATE);
|
||||
$canUpdateSelf = PermissionService::userHas($currentUserId, PermissionService::USERS_SELF_UPDATE);
|
||||
$canUpdateAssignments = PermissionService::userHas($currentUserId, PermissionService::USERS_UPDATE_ASSIGNMENTS);
|
||||
$isOwnAccount = $currentUserId === $userId;
|
||||
$canEditUser = $isOwnAccount ? ($canUpdateUser || $canUpdateSelf) : $canUpdateUser;
|
||||
$canEditAssignments = $canEditUser && $canUpdateAssignments;
|
||||
if (!$isOwnAccount && !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
if (!$canViewUsers && !$canEditUser) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
$creatorId = (int) ($user['created_by'] ?? 0);
|
||||
if ($creatorId > 0) {
|
||||
$creator = UserService::findById($creatorId);
|
||||
if ($creator) {
|
||||
$creatorName = trim(($creator['first_name'] ?? '') . ' ' . ($creator['last_name'] ?? ''));
|
||||
$user['created_by_label'] = $creatorName !== '' ? $creatorName : ($creator['email'] ?? '');
|
||||
$user['created_by_uuid'] = $creator['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$modifierId = (int) ($user['modified_by'] ?? 0);
|
||||
if ($modifierId > 0) {
|
||||
$modifier = UserService::findById($modifierId);
|
||||
if ($modifier) {
|
||||
$modifierName = trim(($modifier['first_name'] ?? '') . ' ' . ($modifier['last_name'] ?? ''));
|
||||
$user['modified_by_label'] = $modifierName !== '' ? $modifierName : ($modifier['email'] ?? '');
|
||||
$user['modified_by_uuid'] = $modifier['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$activeChangedById = (int) ($user['active_changed_by'] ?? 0);
|
||||
if ($activeChangedById > 0) {
|
||||
$activeChanger = UserService::findById($activeChangedById);
|
||||
if ($activeChanger) {
|
||||
$activeChangerName = trim(($activeChanger['first_name'] ?? '') . ' ' . ($activeChanger['last_name'] ?? ''));
|
||||
$user['active_changed_by_label'] = $activeChangerName !== '' ? $activeChangerName : ($activeChanger['email'] ?? '');
|
||||
$user['active_changed_by_uuid'] = $activeChanger['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$errors = [];
|
||||
$form = $user;
|
||||
// Users with tenants.update permission can see ALL tenants (to assign new tenants to users)
|
||||
$canManageTenants = PermissionService::userHas($currentUserId, PermissionService::TENANTS_UPDATE);
|
||||
$allowedTenantIds = $canManageTenants ? null : TenantScopeService::getUserTenantIds($currentUserId);
|
||||
|
||||
$tenants = TenantService::list();
|
||||
if ($allowedTenantIds) {
|
||||
$tenants = array_values(array_filter($tenants, static function (array $tenant) use ($allowedTenantIds): bool {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
return $tenantId > 0 && in_array($tenantId, $allowedTenantIds, true);
|
||||
}));
|
||||
} elseif (!$canManageTenants && TenantScopeService::isStrict()) {
|
||||
$tenants = [];
|
||||
}
|
||||
$selectedTenantIds = UserTenantRepository::listTenantIdsByUserId($userId);
|
||||
if ($allowedTenantIds) {
|
||||
$selectedTenantIds = array_values(array_intersect($selectedTenantIds, $allowedTenantIds));
|
||||
}
|
||||
$primaryTenantId = (int) ($user['primary_tenant_id'] ?? 0);
|
||||
if (!$primaryTenantId && count($selectedTenantIds) === 1) {
|
||||
$primaryTenantId = (int) $selectedTenantIds[0];
|
||||
$user['primary_tenant_id'] = $primaryTenantId;
|
||||
}
|
||||
$roles = RoleService::list();
|
||||
$selectedRoleIds = UserRoleRepository::listRoleIdsByUserId($userId);
|
||||
$permissionRows = RolePermissionRepository::listPermissionsWithRolesByRoleIds($selectedRoleIds);
|
||||
$departments = DepartmentService::listForUserAssignments($selectedTenantIds, []);
|
||||
$selectedDepartmentIds = UserDepartmentRepository::listDepartmentIdsByUserId($userId);
|
||||
if ($selectedDepartmentIds) {
|
||||
$departments = DepartmentService::listForUserAssignments($selectedTenantIds, $selectedDepartmentIds);
|
||||
}
|
||||
// Keep initial $isOwnAccount/$canEditUser values for view permissions.
|
||||
|
||||
if (isset($_POST['email'])) {
|
||||
if (!$canEditUser) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
$result = UserService::updateFromAdmin($userId, $_POST, $currentUserId);
|
||||
$form = $result['form'] ?? $form;
|
||||
$errors = $result['errors'] ?? [];
|
||||
$tenantIds = UserService::normalizeIdInput($_POST['tenant_ids'] ?? []);
|
||||
if ($allowedTenantIds) {
|
||||
$tenantIds = array_values(array_intersect($tenantIds, $allowedTenantIds));
|
||||
} elseif (!$canManageTenants && TenantScopeService::isStrict()) {
|
||||
$tenantIds = [];
|
||||
}
|
||||
$selectedTenantIds = $tenantIds;
|
||||
$primaryTenantId = (int) ($_POST['primary_tenant_id'] ?? 0);
|
||||
$selectedRoleIds = UserService::normalizeIdInput($_POST['role_ids'] ?? []);
|
||||
$selectedDepartmentIds = UserService::normalizeIdInput($_POST['department_ids'] ?? []);
|
||||
$departments = DepartmentService::listForUserAssignments($selectedTenantIds, $selectedDepartmentIds);
|
||||
|
||||
if ($result['ok'] ?? false) {
|
||||
if ($canEditAssignments) {
|
||||
UserService::syncTenants($userId, $tenantIds);
|
||||
UserService::syncRoles($userId, $selectedRoleIds);
|
||||
UserService::syncDepartments($userId, $selectedDepartmentIds);
|
||||
|
||||
// Update session if editing own account (tenant assignments changed)
|
||||
if ($currentUserId === $userId) {
|
||||
AuthService::loadTenantDataIntoSession($userId);
|
||||
}
|
||||
}
|
||||
if ($currentUserId === $userId && isset($form['theme'])) {
|
||||
$_SESSION['user']['theme'] = $form['theme'] ?: 'light';
|
||||
}
|
||||
if ($currentUserId === $userId && isset($form['locale'])) {
|
||||
$_SESSION['user']['locale'] = $form['locale'];
|
||||
I18n::$locale = $form['locale'];
|
||||
}
|
||||
$action = (string) ($_POST['action'] ?? 'save');
|
||||
if ($action === 'save_close') {
|
||||
Flash::success('User updated', 'admin/users', 'user_updated');
|
||||
Router::redirect('admin/users');
|
||||
} else {
|
||||
Flash::success('User updated', "admin/users/edit/{$uuid}", 'user_updated');
|
||||
Router::redirect("admin/users/edit/{$uuid}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Buffer::set('title', $isOwnAccount ? t('My account') : t('Edit user'));
|
||||
312
pages/admin/users/edit(default).phtml
Normal file
312
pages/admin/users/edit(default).phtml
Normal file
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array<int, string> $errors
|
||||
* @var array $form
|
||||
* @var array $user
|
||||
*/
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\UserAvatarService;
|
||||
|
||||
$values = $form ?? $user ?? [];
|
||||
$avatarUuid = (string) ($values['uuid'] ?? '');
|
||||
$hasAvatar = $avatarUuid !== '' && UserAvatarService::hasAvatar($avatarUuid);
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$isOwnAccount = $currentUserId > 0 && $currentUserId === (int) ($values['id'] ?? 0);
|
||||
$titleText = $isOwnAccount ? t('My account') : t('Edit user');
|
||||
$canViewUsers = can('users.view');
|
||||
$hideNavigation = $isOwnAccount && !$canViewUsers;
|
||||
|
||||
?>
|
||||
|
||||
<div class="app-details-container">
|
||||
<section>
|
||||
<?php if (!$hideNavigation): ?>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
|
||||
href="/admin/users"><?php e(t('Users')); ?></a><i class="bi bi-chevron-right"></i><?php e($titleText); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="app-details-titlebar">
|
||||
<h1>
|
||||
<?php if (!$hideNavigation): ?>
|
||||
<a href="admin/users" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>
|
||||
<?php endif; ?>
|
||||
<?php e($titleText); ?>
|
||||
</h1>
|
||||
<div class="app-details-titlebar-actions">
|
||||
<?php if (can('users.delete')): ?>
|
||||
<details class="dropdown">
|
||||
<summary role="button" class="outline secondary"><i class="bi bi-three-dots"></i></summary>
|
||||
<ul dir="rtl">
|
||||
<li>
|
||||
<?php if (!empty($canEditUser)): ?>
|
||||
<form method="post" action="admin/users/send-access/<?php e($values['uuid'] ?? ''); ?>"
|
||||
onsubmit="return confirm('<?php e(t('Send access email to this user?')); ?>');">
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
<button type="submit" class="transparent">
|
||||
<?php e(t('Send access')); ?>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<li>
|
||||
<?php if (!empty($canEditUser)): ?>
|
||||
<form method="post" action="admin/users/forget-tokens/<?php e($values['uuid'] ?? ''); ?>"
|
||||
onsubmit="return confirm('<?php e(t('Clear all login tokens for this user?')); ?>');">
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
<button type="submit" class="transparent">
|
||||
<?php e(t('Clear login tokens')); ?>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<li>
|
||||
<form method="post" action="admin/users/delete/<?php e($values['uuid'] ?? ''); ?>"
|
||||
onsubmit="return confirm('<?php e(t('Delete this user?')); ?>');">
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
<button type="submit" class="transparent">
|
||||
<?php e(t('Delete')); ?>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($canEditUser)): ?>
|
||||
<button type="submit" form="user-form" name="action" value="save" class="secondary outline">
|
||||
<?php e(t('Save')); ?>
|
||||
</button>
|
||||
<?php if (!$hideNavigation): ?>
|
||||
<button type="submit" form="user-form" name="action" value="save_close" class="primary">
|
||||
<?php e(t('Save & close')); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="app-details-errors">
|
||||
<div class="notice" data-variant="error">
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?php e($error); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$passwordRequired = false;
|
||||
$passwordLabel = t('New password (optional)');
|
||||
$passwordLegend = t('Password');
|
||||
$passwordConfirmLabel = t('Password (again)');
|
||||
$showTenants = !empty($canEditAssignments);
|
||||
$tenants = $tenants ?? [];
|
||||
$selectedTenantIds = $selectedTenantIds ?? [];
|
||||
$showRoles = !empty($canEditAssignments);
|
||||
$roles = $roles ?? [];
|
||||
$selectedRoleIds = $selectedRoleIds ?? [];
|
||||
$showDepartments = !empty($canEditAssignments);
|
||||
$departments = $departments ?? [];
|
||||
$selectedDepartmentIds = $selectedDepartmentIds ?? [];
|
||||
$isReadOnly = !(!empty($canEditUser));
|
||||
$canEditAssignments = $canEditAssignments ?? false;
|
||||
$permissionRows = $permissionRows ?? [];
|
||||
$showPermissions = !empty($permissionRows) && can('permissions.view');
|
||||
require __DIR__ . '/_form.phtml';
|
||||
?>
|
||||
</section>
|
||||
<aside id="app-details-aside-section">
|
||||
<div class="app-details-aside-section">
|
||||
<div class="user-avatar-block avatar-round">
|
||||
<?php if ($hasAvatar): ?>
|
||||
<a data-fslightbox="user-avatar" href="admin/users/avatar-file?uuid=<?php e($avatarUuid); ?>&size=256">
|
||||
<img class="user-avatar-image" src="admin/users/avatar-file?uuid=<?php e($avatarUuid); ?>&size=128"
|
||||
alt="<?php e(t('Profile image')); ?>">
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<?php
|
||||
$initials = strtoupper(substr((string) ($values['first_name'] ?? ''), 0, 1) . substr((string) ($values['last_name'] ?? ''), 0, 1));
|
||||
?>
|
||||
<div class="user-avatar-placeholder"><?php e($initials ?: '?'); ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<hgroup>
|
||||
<h2><?php e($values['first_name'] ?? ''); ?> <?php e($values['last_name'] ?? ''); ?></h2>
|
||||
<p><?php e($values['email'] ?? ''); ?></p>
|
||||
</hgroup>
|
||||
<hr>
|
||||
<?php if (!empty($canEditUser)): ?>
|
||||
<details name="user-avatar">
|
||||
<summary>
|
||||
<?php e(t('Upload image')); ?>
|
||||
</summary>
|
||||
<hr>
|
||||
<form class="user-avatar-form" method="post" action="admin/users/avatar/<?php e($avatarUuid); ?>"
|
||||
enctype="multipart/form-data">
|
||||
<label class="user-avatar-upload">
|
||||
<input type="file" name="avatar" accept="image/*">
|
||||
</label>
|
||||
<div class="grid">
|
||||
<button type="submit" class="primary">
|
||||
<?php e(t('Save')); ?>
|
||||
</button>
|
||||
<?php if ($hasAvatar): ?>
|
||||
<button data-tooltip-pos="top" data-tooltip="<?php e(t('Remove image')); ?>" type="submit" class="danger" formaction="admin/users/avatar-delete/<?php e($avatarUuid); ?>"
|
||||
formmethod="post">
|
||||
<i class="bi bi-trash3-fill"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
<hr>
|
||||
<details name="user-meta" open>
|
||||
<summary><?php e(t('Status & meta')); ?></summary>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small><?php e(t('ID')); ?></small>
|
||||
<p>
|
||||
<span class="badge" data-variant="neutral" data-copy="true"
|
||||
data-copy-value="<?php e($values['id'] ?? ''); ?>">
|
||||
<?php e($values['id'] ?? '-'); ?>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<small><?php e(t('Status')); ?></small>
|
||||
<p>
|
||||
<?php if (!empty($values['active'])): ?>
|
||||
<span class="badge" data-variant="success"><?php e(t('Active')); ?></span>
|
||||
<?php else: ?>
|
||||
<span class="badge" data-variant="danger"><?php e(t('Inactive')); ?></span>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<small><?php e(t('Email')); ?></small>
|
||||
<p>
|
||||
<?php if (!empty($values['email_verified_at'])): ?>
|
||||
<span class="badge" data-variant="success"><?php e(t('Verified')); ?> <?php e(dt($values['email_verified_at'])); ?></span>
|
||||
<?php else: ?>
|
||||
<span class="badge" data-variant="warning"><?php e(t('Not verified')); ?></span>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
<?php e(t('UUID')); ?>
|
||||
</small>
|
||||
<p>
|
||||
<span class="badge" data-variant="neutral" data-copy="true"
|
||||
data-copy-value="<?php e($values['uuid'] ?? ''); ?>">
|
||||
<?php
|
||||
$uuidValue = (string) ($values['uuid'] ?? '');
|
||||
$uuidDisplay = $uuidValue !== '' ? substr($uuidValue, 0, 10) : '-';
|
||||
e($uuidDisplay);
|
||||
?>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
<hr>
|
||||
<?php
|
||||
$createdByLabel = $values['created_by_label'] ?? '';
|
||||
$createdById = $values['created_by'] ?? null;
|
||||
$createdByUuid = $values['created_by_uuid'] ?? null;
|
||||
$modifiedByLabel = $values['modified_by_label'] ?? '';
|
||||
$modifiedById = $values['modified_by'] ?? null;
|
||||
$modifiedByUuid = $values['modified_by_uuid'] ?? null;
|
||||
$activeChangedByLabel = $values['active_changed_by_label'] ?? '';
|
||||
$activeChangedById = $values['active_changed_by'] ?? null;
|
||||
$activeChangedByUuid = $values['active_changed_by_uuid'] ?? null;
|
||||
?>
|
||||
<details name="user-audit">
|
||||
<summary><?php e(t('Audit')); ?></summary>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small><?php e(t('Created')); ?></small>
|
||||
<p><?php e(dt($values['created'] ?? '') ?: '-'); ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
<?php e(t('Created by')); ?>
|
||||
</small>
|
||||
<p>
|
||||
<?php if ($createdByLabel !== ''): ?>
|
||||
<?php if ($createdByUuid): ?>
|
||||
<a href="admin/users/edit/<?php e($createdByUuid); ?>"><?php e($createdByLabel); ?></a>
|
||||
<?php else: ?>
|
||||
<?php e($createdByLabel); ?>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($createdById): ?>
|
||||
<?php e('#' . $createdById); ?>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small><?php e(t('Modified')); ?></small>
|
||||
<p><?php e(dt($values['modified'] ?? '') ?: '-'); ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
<?php e(t('Modified by')); ?>
|
||||
</small>
|
||||
<p>
|
||||
<?php if ($modifiedByLabel !== ''): ?>
|
||||
<?php if ($modifiedByUuid): ?>
|
||||
<a href="admin/users/edit/<?php e($modifiedByUuid); ?>"><?php e($modifiedByLabel); ?></a>
|
||||
<?php else: ?>
|
||||
<?php e($modifiedByLabel); ?>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($modifiedById): ?>
|
||||
<?php e('#' . $modifiedById); ?>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small><?php e(t('Status changed')); ?></small>
|
||||
<p><?php e(dt($values['active_changed_at'] ?? '') ?: '-'); ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
<?php e(t('Status changed by')); ?>
|
||||
</small>
|
||||
<p>
|
||||
<?php if ($activeChangedByLabel !== ''): ?>
|
||||
<?php if ($activeChangedByUuid): ?>
|
||||
<a href="admin/users/edit/<?php e($activeChangedByUuid); ?>"><?php e($activeChangedByLabel); ?></a>
|
||||
<?php else: ?>
|
||||
<?php e($activeChangedByLabel); ?>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($activeChangedById): ?>
|
||||
<?php e('#' . $activeChangedById); ?>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
49
pages/admin/users/export().php
Normal file
49
pages/admin/users/export().php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
|
||||
$search = trim((string) ($_GET['search'] ?? ''));
|
||||
$order = (string) ($_GET['order'] ?? 'id');
|
||||
$dir = strtolower((string) ($_GET['dir'] ?? 'desc')) === 'asc' ? 'asc' : 'desc';
|
||||
$active = $_GET['active'] ?? 'all';
|
||||
$createdFrom = trim((string) ($_GET['created_from'] ?? ''));
|
||||
$createdTo = trim((string) ($_GET['created_to'] ?? ''));
|
||||
$tenant = trim((string) ($_GET['tenant'] ?? ''));
|
||||
$roles = $_GET['roles'] ?? '';
|
||||
$departments = $_GET['departments'] ?? '';
|
||||
|
||||
$allowedOrder = ['id', 'uuid', 'first_name', 'last_name', 'email', 'locale', 'theme', 'created', 'modified', 'active'];
|
||||
if (!in_array($order, $allowedOrder, true)) {
|
||||
$order = 'id';
|
||||
}
|
||||
|
||||
$limit = (int) ($_GET['limit'] ?? 5000);
|
||||
if ($limit < 1) {
|
||||
$limit = 5000;
|
||||
}
|
||||
if ($limit > 5000) {
|
||||
$limit = 5000;
|
||||
}
|
||||
|
||||
$result = UserService::listPaged([
|
||||
'limit' => $limit,
|
||||
'offset' => 0,
|
||||
'search' => $search,
|
||||
'order' => $order,
|
||||
'dir' => $dir,
|
||||
'active' => $active,
|
||||
'created_from' => $createdFrom,
|
||||
'created_to' => $createdTo,
|
||||
'tenant' => $tenant,
|
||||
'roles' => $roles,
|
||||
'departments' => $departments,
|
||||
'tenantUserId' => $currentUserId,
|
||||
]);
|
||||
|
||||
$exportRows = $result['rows'] ?? [];
|
||||
$exportFilename = 'users-' . date('Ymd-His') . '.csv';
|
||||
75
pages/admin/users/export(none).phtml
Normal file
75
pages/admin/users/export(none).phtml
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
$filename = $exportFilename ?? ('users-' . date('Ymd-His') . '.csv');
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
|
||||
$escapeValue = static function ($value, bool $allowSignedNumeric = false) {
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
$string = (string) $value;
|
||||
if ($string !== '' && in_array($string[0], ['=', '@'], true)) {
|
||||
return "'" . $string;
|
||||
}
|
||||
if ($string !== '' && in_array($string[0], ['+', '-'], true)) {
|
||||
if ($allowSignedNumeric && preg_match('/^[+-]?[0-9\\s().-]+$/', $string)) {
|
||||
return $string;
|
||||
}
|
||||
return "'" . $string;
|
||||
}
|
||||
return $string;
|
||||
};
|
||||
|
||||
$formatTimestamp = static function ($value) {
|
||||
if ($value === null || $value === '') {
|
||||
return '';
|
||||
}
|
||||
return dt($value, 'Y-m-d H:i:s');
|
||||
};
|
||||
$joinLabels = static function ($value) {
|
||||
if (is_array($value)) {
|
||||
return implode(' | ', array_map('strval', $value));
|
||||
}
|
||||
if (is_string($value)) {
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
return str_replace('||', ' | ', $value);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
$handle = fopen('php://output', 'w');
|
||||
if ($handle === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
fputcsv($handle, ['id', 'uuid', 'first_name', 'last_name', 'email', 'phone', 'mobile', 'short_dial', 'locale', 'theme', 'active', 'tenants', 'departments', 'roles', 'created_by', 'modified_by', 'created', 'modified'], ',', '"', '\\');
|
||||
|
||||
foreach (($exportRows ?? []) as $row) {
|
||||
fputcsv($handle, [
|
||||
$escapeValue($row['id'] ?? ''),
|
||||
$escapeValue($row['uuid'] ?? ''),
|
||||
$escapeValue($row['first_name'] ?? ''),
|
||||
$escapeValue($row['last_name'] ?? ''),
|
||||
$escapeValue($row['email'] ?? ''),
|
||||
$escapeValue($row['phone'] ?? '', true),
|
||||
$escapeValue($row['mobile'] ?? '', true),
|
||||
$escapeValue($row['short_dial'] ?? '', true),
|
||||
$escapeValue($row['locale'] ?? ''),
|
||||
$escapeValue($row['theme'] ?? ''),
|
||||
(int) ($row['active'] ?? 0),
|
||||
$escapeValue($joinLabels($row['tenant_labels'] ?? [])),
|
||||
$escapeValue($joinLabels($row['department_labels'] ?? [])),
|
||||
$escapeValue($joinLabels($row['role_labels'] ?? [])),
|
||||
$escapeValue($row['created_by'] ?? ''),
|
||||
$escapeValue($row['modified_by'] ?? ''),
|
||||
$formatTimestamp($row['created'] ?? ''),
|
||||
$formatTimestamp($row['modified'] ?? ''),
|
||||
], ',', '"', '\\');
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
39
pages/admin/users/forget-tokens($id).php
Normal file
39
pages/admin/users/forget-tokens($id).php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Service\RememberMeService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
use MintyPHP\Service\UserService;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$canUpdateUser = PermissionService::userHas($currentUserId, PermissionService::USERS_UPDATE);
|
||||
$canUpdateSelf = PermissionService::userHas($currentUserId, PermissionService::USERS_SELF_UPDATE);
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
if (!$user) {
|
||||
Flash::error(t('User not found'), 'admin/users', 'user_not_found');
|
||||
Router::redirect('admin/users');
|
||||
}
|
||||
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
$isOwnAccount = $currentUserId > 0 && $currentUserId === $userId;
|
||||
$canEditUser = $isOwnAccount ? ($canUpdateUser || $canUpdateSelf) : $canUpdateUser;
|
||||
|
||||
if (!$canEditUser) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
if (!$isOwnAccount && !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
RememberMeService::forgetAllForUser($userId);
|
||||
Flash::success(t('Login tokens cleared'), "admin/users/edit/{$uuid}", 'tokens_cleared');
|
||||
Router::redirect("admin/users/edit/{$uuid}");
|
||||
48
pages/admin/users/index().php
Normal file
48
pages/admin/users/index().php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\DepartmentService;
|
||||
use MintyPHP\Service\RoleService;
|
||||
use MintyPHP\Service\TenantService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
PermissionService::getUserPermissions($currentUserId);
|
||||
}
|
||||
$allowedTenantIds = TenantScopeService::getUserTenantIds($currentUserId);
|
||||
|
||||
Buffer::set('title', t('Users'));
|
||||
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
$csrfKey = Session::$csrfSessionKey;
|
||||
$csrfToken = $_SESSION[$csrfKey] ?? '';
|
||||
Buffer::set(
|
||||
'grid_csrf',
|
||||
json_encode(['key' => $csrfKey, 'token' => $csrfToken], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
);
|
||||
|
||||
$tenants = TenantService::list();
|
||||
if ($allowedTenantIds) {
|
||||
$tenants = array_values(array_filter($tenants, static function (array $tenant) use ($allowedTenantIds): bool {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
return $tenantId > 0 && in_array($tenantId, $allowedTenantIds, true);
|
||||
}));
|
||||
} elseif (TenantScopeService::isStrict()) {
|
||||
$tenants = [];
|
||||
}
|
||||
sortByDescription($tenants);
|
||||
|
||||
$roles = RoleService::list();
|
||||
sortByDescription($roles);
|
||||
|
||||
$departments = $allowedTenantIds ? DepartmentService::listByTenantIds($allowedTenantIds) : [];
|
||||
if (!$allowedTenantIds && !TenantScopeService::isStrict()) {
|
||||
$departments = DepartmentService::list();
|
||||
}
|
||||
sortByDescription($departments);
|
||||
389
pages/admin/users/index(default).phtml
Normal file
389
pages/admin/users/index(default).phtml
Normal file
@@ -0,0 +1,389 @@
|
||||
<?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>
|
||||
73
pages/admin/users/send-access($id).php
Normal file
73
pages/admin/users/send-access($id).php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\MailService;
|
||||
use MintyPHP\I18n;
|
||||
use MintyPHP\Http\Request;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$canUpdateUser = PermissionService::userHas($currentUserId, PermissionService::USERS_UPDATE);
|
||||
$canUpdateSelf = PermissionService::userHas($currentUserId, PermissionService::USERS_SELF_UPDATE);
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
if (!$user) {
|
||||
Flash::error(t('User not found'), 'admin/users', 'user_not_found');
|
||||
Router::redirect('admin/users');
|
||||
}
|
||||
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
$isOwnAccount = $currentUserId > 0 && $currentUserId === $userId;
|
||||
$canEditUser = $isOwnAccount ? ($canUpdateUser || $canUpdateSelf) : $canUpdateUser;
|
||||
|
||||
if (!$canEditUser) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
if (!$isOwnAccount && !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$locale = $user['locale'] ?? (I18n::$locale ?? I18n::$defaultLocale);
|
||||
$name = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
|
||||
$isGerman = strpos((string) $locale, 'de') === 0;
|
||||
$greeting = $isGerman ? 'Hallo' : 'Hello';
|
||||
if ($name !== '') {
|
||||
$greeting .= ' ' . $name;
|
||||
}
|
||||
$greeting .= ',';
|
||||
|
||||
$loginUrl = appUrl(Request::withLocale('login', $locale));
|
||||
$resetUrl = appUrl(Request::withLocale('password/forgot', $locale));
|
||||
$previousLocale = I18n::$locale ?? null;
|
||||
I18n::$locale = $locale;
|
||||
$subject = t('Your access details');
|
||||
I18n::$locale = $previousLocale;
|
||||
|
||||
$vars = [
|
||||
'app_name' => appTitle(),
|
||||
'app_logo_url' => appLogoUrlAbsolute(128),
|
||||
'imprint_url' => appUrl(Request::withLocale('imprint', $locale)),
|
||||
'privacy_url' => appUrl(Request::withLocale('privacy', $locale)),
|
||||
'greeting' => $greeting,
|
||||
'username' => (string) ($user['email'] ?? ''),
|
||||
'login_url' => $loginUrl,
|
||||
'reset_url' => $resetUrl,
|
||||
];
|
||||
|
||||
$result = MailService::sendTemplate('access_info', $vars, (string) $user['email'], $subject, $locale);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
Flash::error(t('Access email failed'), "admin/users/edit/{$uuid}", 'access_email_failed');
|
||||
Router::redirect("admin/users/edit/{$uuid}");
|
||||
}
|
||||
|
||||
Flash::success(t('Access email sent'), "admin/users/edit/{$uuid}", 'access_email_sent');
|
||||
Router::redirect("admin/users/edit/{$uuid}");
|
||||
83
pages/admin/users/switch-tenant().php
Normal file
83
pages/admin/users/switch-tenant().php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Session::checkCsrfToken()) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(400);
|
||||
Router::json(['ok' => false, 'error' => 'csrf']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($userId <= 0) {
|
||||
http_response_code(401);
|
||||
if (Request::wantsJson()) {
|
||||
Router::json(['ok' => false, 'error' => 'unauthorized']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('login');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get tenant_id from POST
|
||||
$tenantId = (int) ($_POST['tenant_id'] ?? 0);
|
||||
|
||||
if ($tenantId <= 0) {
|
||||
http_response_code(400);
|
||||
if (Request::wantsJson()) {
|
||||
Router::json(['ok' => false, 'error' => 'invalid_tenant_id']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = UserService::setCurrentTenant($userId, $tenantId);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
$error = $result['error'] ?? 'update_failed';
|
||||
http_response_code(400);
|
||||
if (Request::wantsJson()) {
|
||||
Router::json(['ok' => false, 'error' => $error]);
|
||||
return;
|
||||
}
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update session with new current tenant
|
||||
if (isset($_SESSION['user'])) {
|
||||
$_SESSION['user']['current_tenant_id'] = $tenantId;
|
||||
}
|
||||
|
||||
// Update session with full tenant data
|
||||
$_SESSION['current_tenant'] = $result['tenant'] ?? null;
|
||||
|
||||
// Reload available tenants and departments
|
||||
$_SESSION['available_tenants'] = UserService::getAvailableTenants($userId);
|
||||
$_SESSION['available_departments_by_tenant'] = UserService::getAvailableDepartmentsByTenant($userId);
|
||||
|
||||
if (Request::wantsJson()) {
|
||||
Router::json([
|
||||
'ok' => true,
|
||||
'tenant_id' => $tenantId,
|
||||
'tenant' => $result['tenant'] ?? null,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
Router::redirect('admin');
|
||||
66
pages/admin/users/theme().php
Normal file
66
pages/admin/users/theme().php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Session::checkCsrfToken()) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(400);
|
||||
Router::json(['ok' => false, 'error' => 'csrf']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($userId <= 0) {
|
||||
http_response_code(401);
|
||||
if (Request::wantsJson()) {
|
||||
Router::json(['ok' => false, 'error' => 'unauthorized']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('login');
|
||||
return;
|
||||
}
|
||||
|
||||
$theme = trim((string) ($_POST['theme'] ?? ''));
|
||||
if (!in_array($theme, ['light', 'dark'], true)) {
|
||||
http_response_code(400);
|
||||
if (Request::wantsJson()) {
|
||||
Router::json(['ok' => false, 'error' => 'invalid_theme']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
$updated = UserService::setTheme($userId, $theme);
|
||||
if (!$updated) {
|
||||
http_response_code(500);
|
||||
if (Request::wantsJson()) {
|
||||
Router::json(['ok' => false, 'error' => 'update_failed']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
$_SESSION['user']['theme'] = $theme;
|
||||
|
||||
if (Request::wantsJson()) {
|
||||
Router::json(['ok' => true, 'theme' => $theme]);
|
||||
return;
|
||||
}
|
||||
|
||||
Router::redirect('admin');
|
||||
Reference in New Issue
Block a user