OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
88
app/Domain/Users/Controllers/DelUser.php
Normal file
88
app/Domain/Users/Controllers/DelUser.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Users\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Domain\Users\Permissions\UsersPermissions;
|
||||
use Leantime\Domain\Users\Services\Users;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class DelUser extends Controller
|
||||
{
|
||||
private Users $userService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(Users $userService): void
|
||||
{
|
||||
$this->userService = $userService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the delete user confirmation page.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(UsersPermissions::DELETE, global: true)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
if (! isset($params['id'])) {
|
||||
return $this->tpl->display('errors.error403', responseCode: 403);
|
||||
}
|
||||
|
||||
$id = (int) $params['id'];
|
||||
$user = $this->userService->getUser($id);
|
||||
|
||||
$this->generateFormTokens();
|
||||
|
||||
$this->tpl->assign('user', $user);
|
||||
|
||||
return $this->tpl->display('users.delUser');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles user deletion.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(UsersPermissions::DELETE, global: true)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
if (! isset($params['id'])) {
|
||||
return $this->tpl->display('errors.error403', responseCode: 403);
|
||||
}
|
||||
|
||||
$id = (int) $params['id'];
|
||||
$user = $this->userService->getUser($id);
|
||||
|
||||
if (isset($_POST['del'])) {
|
||||
if (isset($_POST[session('formTokenName')]) && $_POST[session('formTokenName')] == session('formTokenValue')) {
|
||||
$this->userService->deleteUser($id);
|
||||
$this->tpl->setNotification($this->language->__('notifications.user_deleted'), 'success', 'user_deleted');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/users/showAll');
|
||||
}
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.form_token_incorrect'), 'error');
|
||||
}
|
||||
|
||||
$this->generateFormTokens();
|
||||
|
||||
$this->tpl->assign('user', $user);
|
||||
|
||||
return $this->tpl->display('users.delUser');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates CSRF form tokens for the sensitive delete form.
|
||||
*/
|
||||
private function generateFormTokens(): void
|
||||
{
|
||||
$permitted_chars = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
session(['formTokenName' => substr(str_shuffle($permitted_chars), 0, 32)]);
|
||||
session(['formTokenValue' => substr(str_shuffle($permitted_chars), 0, 32)]);
|
||||
}
|
||||
}
|
||||
129
app/Domain/Users/Controllers/EditOwn.php
Normal file
129
app/Domain/Users/Controllers/EditOwn.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Users\Controllers;
|
||||
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller as FrontcontrollerCore;
|
||||
use Leantime\Domain\Users\Services\Users as UserService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EditOwn extends Controller
|
||||
{
|
||||
private UserService $userService;
|
||||
|
||||
private int $userId;
|
||||
|
||||
/**
|
||||
* init - initialize private variables
|
||||
*/
|
||||
public function init(UserService $userService): void
|
||||
{
|
||||
$this->userService = $userService;
|
||||
|
||||
$this->userId = session('userdata.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function get(): Response
|
||||
{
|
||||
$permitted_chars = '123456789abcdefghijklmnopqrstuvwxyz';
|
||||
session(['formTokenName' => substr(str_shuffle($permitted_chars), 0, 32)]);
|
||||
session(['formTokenValue' => substr(str_shuffle($permitted_chars), 0, 32)]);
|
||||
|
||||
$profileSettings = $this->userService->getOwnProfileSettings($this->userId);
|
||||
array_map([$this->tpl, 'assign'], array_keys($profileSettings), array_values($profileSettings));
|
||||
|
||||
$notificationPreferences = $this->userService->getNotificationPreferences($this->userId);
|
||||
array_map([$this->tpl, 'assign'], array_keys($notificationPreferences), array_values($notificationPreferences));
|
||||
|
||||
return $this->tpl->display('users.editOwn');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function post(): Response
|
||||
{
|
||||
// Save Profile Info
|
||||
$tab = '';
|
||||
|
||||
if (isset($_POST[session('formTokenName')]) && session()->exists('formTokenName') && $_POST[session('formTokenName')] === session('formTokenValue')) {
|
||||
// profile Info
|
||||
if (isset($_POST['profileInfo'])) {
|
||||
$tab = '#myProfile';
|
||||
|
||||
$result = $this->userService->saveOwnProfile($this->userId, $_POST);
|
||||
|
||||
if ($result === 'success') {
|
||||
$this->tpl->setNotification($this->language->__('notifications.profile_edited'), 'success', 'profile_edited');
|
||||
} elseif ($result === 'user_exists') {
|
||||
$this->tpl->setNotification($this->language->__('notification.user_exists'), 'error');
|
||||
} elseif ($result === 'no_valid_email') {
|
||||
$this->tpl->setNotification($this->language->__('notification.no_valid_email'), 'error');
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.enter_email'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Save Password
|
||||
if (isset($_POST['savepw'])) {
|
||||
$tab = '#security';
|
||||
|
||||
$result = $this->userService->changeOwnPassword(
|
||||
$this->userId,
|
||||
$_POST['currentPassword'],
|
||||
$_POST['newPassword'],
|
||||
$_POST['confirmPassword']
|
||||
);
|
||||
|
||||
if ($result === 'success') {
|
||||
$this->tpl->setNotification($this->language->__('notifications.password_changed'), 'success', 'password_edited');
|
||||
} elseif ($result === 'password_not_strong_enough') {
|
||||
$this->tpl->setNotification($this->language->__('notification.password_not_strong_enough'), 'error');
|
||||
} elseif ($result === 'passwords_dont_match') {
|
||||
$this->tpl->setNotification($this->language->__('notification.passwords_dont_match'), 'error');
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.previous_password_incorrect'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['saveTheme'])) {
|
||||
$tab = '#theme';
|
||||
|
||||
$this->userService->saveOwnAppearanceSettings($this->userId, $_POST);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notifications.changed_profile_settings_successfully'), 'success', 'themsettings_updated');
|
||||
}
|
||||
|
||||
// Save Look & Feel
|
||||
if (isset($_POST['saveSettings'])) {
|
||||
$tab = '#settings';
|
||||
|
||||
$this->userService->saveOwnLocaleSettings($this->userId, $_POST);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notifications.changed_profile_settings_successfully'), 'success', 'profilesettings_updated');
|
||||
}
|
||||
|
||||
// Save Profile Image
|
||||
if (isset($_POST['profileImage'])) {
|
||||
$tab = '#myProfile';
|
||||
}
|
||||
|
||||
// Save Notifications
|
||||
if (isset($_POST['savenotifications'])) {
|
||||
$tab = '#notifications';
|
||||
|
||||
$this->userService->saveOwnNotificationPreferences($this->userId, $_POST);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notifications.changed_profile_settings_successfully'), 'success', 'profilesettings_updated');
|
||||
}
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.form_token_incorrect'), 'error');
|
||||
}
|
||||
|
||||
// Redirect
|
||||
return FrontcontrollerCore::redirect(BASE_URL.'/users/editOwn'.$tab);
|
||||
}
|
||||
}
|
||||
250
app/Domain/Users/Controllers/EditUser.php
Normal file
250
app/Domain/Users/Controllers/EditUser.php
Normal file
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Users\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Domain\Auth\Models\Roles;
|
||||
use Leantime\Domain\Clients\Services\Clients as ClientService;
|
||||
use Leantime\Domain\Projects\Services\Projects as ProjectService;
|
||||
use Leantime\Domain\Users\Permissions\UsersPermissions;
|
||||
use Leantime\Domain\Users\Repositories\Users as UserRepository;
|
||||
use Leantime\Domain\Users\Services\Users;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EditUser extends Controller
|
||||
{
|
||||
private ProjectService $projectService;
|
||||
|
||||
private ClientService $clientService;
|
||||
|
||||
private Users $userService;
|
||||
|
||||
private UserRepository $userRepo;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(
|
||||
ProjectService $projectService,
|
||||
ClientService $clientService,
|
||||
Users $userService,
|
||||
UserRepository $userRepo
|
||||
): void {
|
||||
$this->projectService = $projectService;
|
||||
$this->clientService = $clientService;
|
||||
$this->userService = $userService;
|
||||
$this->userRepo = $userRepo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the edit user form.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(UsersPermissions::EDIT, global: true)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
if (! isset($params['id'])) {
|
||||
return $this->tpl->display('errors.error403', responseCode: 403);
|
||||
}
|
||||
|
||||
$id = (int) $params['id'];
|
||||
// Admin edit form needs the full row (e.g. pwReset for the invite
|
||||
// link), so read from the repository — the service getUser strips
|
||||
// secrets for API safety (#3556).
|
||||
$row = $this->userRepo->getUser($id);
|
||||
|
||||
if ($row === false) {
|
||||
return $this->tpl->display('errors.error404', responseCode: 404);
|
||||
}
|
||||
|
||||
if (array_key_exists('resendInvite', $_GET)) {
|
||||
return $this->handleResendInvite($id, $row);
|
||||
}
|
||||
|
||||
$values = $this->buildValuesFromUser($row);
|
||||
$projectrelation = $this->userService->getUserProjectIds($id);
|
||||
|
||||
$this->generateFormTokens();
|
||||
|
||||
$this->tpl->assign('values', $values);
|
||||
$this->tpl->assign('relations', $projectrelation);
|
||||
$this->tpl->assign('id', $id);
|
||||
$this->assignTemplateVars();
|
||||
|
||||
return $this->tpl->display('users.editUser');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles user profile updates.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(UsersPermissions::EDIT, global: true)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
if (! isset($params['id'])) {
|
||||
return $this->tpl->display('errors.error403', responseCode: 403);
|
||||
}
|
||||
|
||||
$id = (int) $params['id'];
|
||||
// Admin edit form needs the full row (e.g. pwReset for the invite
|
||||
// link), so read from the repository — the service getUser strips
|
||||
// secrets for API safety (#3556).
|
||||
$row = $this->userRepo->getUser($id);
|
||||
|
||||
if ($row === false) {
|
||||
return $this->tpl->display('errors.error404', responseCode: 404);
|
||||
}
|
||||
|
||||
$values = $this->buildValuesFromUser($row);
|
||||
$edit = false;
|
||||
|
||||
if (isset($_POST['save'])) {
|
||||
if (! isset($_POST[session('formTokenName')]) || $_POST[session('formTokenName')] != session('formTokenValue')) {
|
||||
$this->tpl->setNotification($this->language->__('notification.form_token_incorrect'), 'error');
|
||||
} else {
|
||||
$values = $this->buildValuesFromPost($row);
|
||||
$edit = $this->handleValidation($values, $row, $id);
|
||||
}
|
||||
}
|
||||
|
||||
if ($edit) {
|
||||
$this->userService->updateUser($values, $id, $_POST['projects'] ?? null);
|
||||
$this->tpl->setNotification($this->language->__('notifications.user_edited'), 'success');
|
||||
}
|
||||
|
||||
$projectrelation = $this->userService->getUserProjectIds($id);
|
||||
|
||||
$this->generateFormTokens();
|
||||
|
||||
$this->tpl->assign('values', $values);
|
||||
$this->tpl->assign('relations', $projectrelation);
|
||||
$this->tpl->assign('id', $id);
|
||||
$this->assignTemplateVars();
|
||||
|
||||
return $this->tpl->display('users.editUser');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the resend invite action.
|
||||
*/
|
||||
private function handleResendInvite(int $id, array $row): Response
|
||||
{
|
||||
$result = $this->userService->resendUserInvite($id, $row);
|
||||
|
||||
if ($result === 'too_soon') {
|
||||
$this->tpl->setNotification($this->language->__('notification.invite_too_soon'), 'error');
|
||||
} elseif ($result === 'too_many_invites') {
|
||||
$this->tpl->setNotification($this->language->__('notification.too_many_invites'), 'error');
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.invitation_sent'), 'success', 'userinvitation_sent');
|
||||
}
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/users/editUser/'.$id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a user update and sets the matching notification on failure.
|
||||
*
|
||||
* @return bool True if the update should proceed.
|
||||
*/
|
||||
private function handleValidation(array $values, array $row, int $id): bool
|
||||
{
|
||||
$result = $this->userService->validateUserUpdate($values, $row, $id, $_POST);
|
||||
|
||||
if ($result === 'valid') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$messages = [
|
||||
'passwords_dont_match' => 'notification.passwords_dont_match',
|
||||
'enter_email' => 'notification.enter_email',
|
||||
'no_valid_email' => 'notification.no_valid_email',
|
||||
'user_exists' => 'notification.user_exists',
|
||||
];
|
||||
|
||||
$this->tpl->setNotification($this->language->__($messages[$result]), 'error');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a values array from the user database row.
|
||||
*/
|
||||
private function buildValuesFromUser(array $row): array
|
||||
{
|
||||
return [
|
||||
'id' => $row['id'],
|
||||
'firstname' => $row['firstname'],
|
||||
'lastname' => $row['lastname'],
|
||||
'user' => $row['username'],
|
||||
'phone' => $row['phone'],
|
||||
'status' => $row['status'],
|
||||
'role' => $row['role'],
|
||||
'hours' => $row['hours'],
|
||||
'wage' => $row['wage'],
|
||||
'clientId' => $row['clientId'],
|
||||
'source' => $row['source'],
|
||||
'pwReset' => $row['pwReset'],
|
||||
'jobTitle' => $row['jobTitle'],
|
||||
'jobLevel' => $row['jobLevel'],
|
||||
'department' => $row['department'],
|
||||
'weekly_hours' => $row['weekly_hours'] ?? null,
|
||||
'employment_type' => $row['employment_type'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a values array from POST data, falling back to the original user row.
|
||||
*/
|
||||
private function buildValuesFromPost(array $row): array
|
||||
{
|
||||
return [
|
||||
'id' => $row['id'],
|
||||
'firstname' => $_POST['firstname'] ?? $row['firstname'],
|
||||
'lastname' => $_POST['lastname'] ?? $row['lastname'],
|
||||
'user' => $_POST['user'] ?? $row['username'],
|
||||
'phone' => $_POST['phone'] ?? $row['phone'],
|
||||
'status' => $_POST['status'] ?? $row['status'],
|
||||
'role' => $_POST['role'] ?? $row['role'],
|
||||
'hours' => $_POST['hours'] ?? $row['hours'],
|
||||
'wage' => $_POST['wage'] ?? $row['wage'],
|
||||
'clientId' => $_POST['client'] ?? $row['clientId'],
|
||||
'source' => $row['source'],
|
||||
'pwReset' => $row['pwReset'],
|
||||
'jobTitle' => $_POST['jobTitle'] ?? $row['jobTitle'],
|
||||
'jobLevel' => $_POST['jobLevel'] ?? $row['jobLevel'],
|
||||
'department' => $_POST['department'] ?? $row['department'],
|
||||
// Capacity fields — only pass when actually posted so the
|
||||
// repo's array_key_exists guard preserves existing values on
|
||||
// a form submit that omits them (non-admin path today, but
|
||||
// also future partial-update callers).
|
||||
...(array_key_exists('weekly_hours', $_POST) ? ['weekly_hours' => $_POST['weekly_hours']] : []),
|
||||
...(array_key_exists('employment_type', $_POST) ? ['employment_type' => $_POST['employment_type']] : []),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates CSRF form tokens.
|
||||
*/
|
||||
private function generateFormTokens(): void
|
||||
{
|
||||
$permitted_chars = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
session(['formTokenName' => substr(str_shuffle($permitted_chars), 0, 32)]);
|
||||
session(['formTokenValue' => substr(str_shuffle($permitted_chars), 0, 32)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns common template variables.
|
||||
*/
|
||||
private function assignTemplateVars(): void
|
||||
{
|
||||
$this->tpl->assign('allProjects', $this->projectService->getAll(true));
|
||||
$this->tpl->assign('roles', Roles::getRoles());
|
||||
$this->tpl->assign('clients', $this->clientService->getAll());
|
||||
$this->tpl->assign('status', $this->userService->getUserStatuses());
|
||||
}
|
||||
}
|
||||
79
app/Domain/Users/Controllers/Import.php
Normal file
79
app/Domain/Users/Controllers/Import.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/* Not production ready yet. Prepping for future version */
|
||||
|
||||
namespace Leantime\Domain\Users\Controllers;
|
||||
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Domain\Auth\Models\Roles;
|
||||
use Leantime\Domain\Ldap\Services\Ldap as LdapService;
|
||||
use Leantime\Domain\Users\Permissions\UsersPermissions;
|
||||
use Leantime\Domain\Users\Services\Users as UserService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class Import extends Controller
|
||||
{
|
||||
private UserService $userService;
|
||||
|
||||
private LdapService $ldapService;
|
||||
|
||||
public function init(UserService $userService, LdapService $ldapService): void
|
||||
{
|
||||
$this->userService = $userService;
|
||||
$this->ldapService = $ldapService;
|
||||
|
||||
if (! session()->exists('tmp')) {
|
||||
session(['tmp' => []]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
#[RequiresPermission(UsersPermissions::IMPORT, global: true)]
|
||||
public function get(): Response
|
||||
{
|
||||
$this->tpl->assign('allUsers', $this->userService->getAll());
|
||||
$this->tpl->assign('admin', true);
|
||||
$this->tpl->assign('roles', Roles::getRoles());
|
||||
|
||||
if (session()->exists('tmp.ldapUsers') && count(session('tmp.ldapUsers')) > 0) {
|
||||
$this->tpl->assign('allLdapUsers', session('tmp.ldapUsers'));
|
||||
$this->tpl->assign('confirmUsers', true);
|
||||
}
|
||||
|
||||
return $this->tpl->displayPartial('users.importLdapDialog');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
#[RequiresPermission(UsersPermissions::IMPORT, global: true)]
|
||||
public function post($params): Response
|
||||
{
|
||||
// Password Submit to connect to ldap and retrieve users. Sets tmp session var
|
||||
if (isset($params['pwSubmit'])) {
|
||||
$bindUsername = $this->ldapService->extractLdapFromUsername(session('userdata.mail'));
|
||||
$members = $this->userService->fetchLdapMembers($bindUsername, $params['password']);
|
||||
|
||||
if ($members !== false) {
|
||||
session(['tmp.ldapUsers' => $members]);
|
||||
$this->tpl->assign('allLdapUsers', session('tmp.ldapUsers'));
|
||||
$this->tpl->assign('confirmUsers', true);
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notifications.username_or_password_incorrect'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Import/Update User Post
|
||||
if (isset($params['importSubmit'])) {
|
||||
if (is_array($params['users'])) {
|
||||
$this->userService->importSelectedLdapUsers(session('tmp.ldapUsers'), $params['users']);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->tpl->displayPartial('users.importLdapDialog');
|
||||
}
|
||||
}
|
||||
146
app/Domain/Users/Controllers/NewUser.php
Normal file
146
app/Domain/Users/Controllers/NewUser.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Users\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Domain\Auth\Models\Roles;
|
||||
use Leantime\Domain\Auth\Services\Auth;
|
||||
use Leantime\Domain\Clients\Services\Clients as ClientService;
|
||||
use Leantime\Domain\Projects\Services\Projects as ProjectService;
|
||||
use Leantime\Domain\Users\Permissions\UsersPermissions;
|
||||
use Leantime\Domain\Users\Services\Users as UserService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class NewUser extends Controller
|
||||
{
|
||||
private UserService $userService;
|
||||
|
||||
private ProjectService $projectService;
|
||||
|
||||
private ClientService $clientService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(
|
||||
UserService $userService,
|
||||
ProjectService $projectService,
|
||||
ClientService $clientService
|
||||
): void {
|
||||
$this->userService = $userService;
|
||||
$this->projectService = $projectService;
|
||||
$this->clientService = $clientService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the new user invitation form.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(UsersPermissions::CREATE, global: true)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
$projectrelation = [];
|
||||
if (isset($params['preSelectProjectId'])) {
|
||||
$preSelected = explode(',', $params['preSelectProjectId']);
|
||||
foreach ($preSelected as $item) {
|
||||
$projectrelation[] = (int) $item;
|
||||
}
|
||||
}
|
||||
|
||||
$this->tpl->assign('values', $this->getDefaultValues());
|
||||
$this->tpl->assign('preSelectedClient', isset($params['preSelectedClient']) ? (int) $params['preSelectedClient'] : '');
|
||||
$this->tpl->assign('relations', $projectrelation);
|
||||
$this->assignTemplateVars();
|
||||
|
||||
return $this->tpl->displayPartial('users.newUser');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles new user creation / invitation.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(UsersPermissions::CREATE, global: true)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
$values = $this->getDefaultValues();
|
||||
$projectrelation = [];
|
||||
|
||||
if (isset($_POST['save'])) {
|
||||
$isManager = Auth::userHasRole(Roles::$manager);
|
||||
|
||||
$values = [
|
||||
'firstname' => $_POST['firstname'],
|
||||
'lastname' => $_POST['lastname'],
|
||||
'user' => $_POST['user'],
|
||||
'phone' => $_POST['phone'],
|
||||
'role' => $_POST['role'],
|
||||
'password' => '',
|
||||
'pwReset' => '',
|
||||
'status' => '',
|
||||
'jobTitle' => $_POST['jobTitle'],
|
||||
'jobLevel' => $_POST['jobLevel'],
|
||||
'department' => $_POST['department'],
|
||||
'clientId' => $isManager ? session('userdata.clientId') : $_POST['client'],
|
||||
];
|
||||
|
||||
if (isset($_POST['projects']) && is_array($_POST['projects'])) {
|
||||
foreach ($_POST['projects'] as $project) {
|
||||
$projectrelation[] = $project;
|
||||
}
|
||||
}
|
||||
|
||||
$result = $this->userService->inviteNewUser($_POST, session('userdata.clientId'), $isManager);
|
||||
|
||||
if ($result === 'enter_email') {
|
||||
$this->tpl->setNotification($this->language->__('notification.enter_email'), 'error');
|
||||
} elseif ($result === 'no_valid_email') {
|
||||
$this->tpl->setNotification($this->language->__('notification.no_valid_email'), 'error');
|
||||
} elseif ($result === 'user_exists') {
|
||||
$this->tpl->setNotification($this->language->__('notification.user_exists'), 'error');
|
||||
} elseif ($result === 'invite_failed') {
|
||||
$this->tpl->setNotification($this->language->__('notification.invite_failed'), 'error');
|
||||
} else {
|
||||
$this->tpl->setNotification('notification.user_invited_successfully', 'success', 'user_invited');
|
||||
}
|
||||
}
|
||||
|
||||
$this->tpl->assign('values', $values);
|
||||
$this->tpl->assign('preSelectedClient', '');
|
||||
$this->tpl->assign('relations', $projectrelation);
|
||||
$this->assignTemplateVars();
|
||||
|
||||
return $this->tpl->displayPartial('users.newUser');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns default empty values for the new user form.
|
||||
*/
|
||||
private function getDefaultValues(): array
|
||||
{
|
||||
return [
|
||||
'firstname' => '',
|
||||
'lastname' => '',
|
||||
'user' => '',
|
||||
'phone' => '',
|
||||
'role' => '',
|
||||
'password' => '',
|
||||
'clientId' => '',
|
||||
'jobTitle' => '',
|
||||
'jobLevel' => '',
|
||||
'department' => '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns common template variables.
|
||||
*/
|
||||
private function assignTemplateVars(): void
|
||||
{
|
||||
$this->tpl->assign('clients', $this->clientService->getAll());
|
||||
$this->tpl->assign('allProjects', $this->projectService->getAll());
|
||||
$this->tpl->assign('roles', Roles::getRoles());
|
||||
}
|
||||
}
|
||||
51
app/Domain/Users/Controllers/PatchUserSettings.php
Normal file
51
app/Domain/Users/Controllers/PatchUserSettings.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Users\Controllers;
|
||||
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Domain\Auth\Services\Auth;
|
||||
use Leantime\Domain\Users\Services\Users as UserService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class PatchUserSettings extends Controller
|
||||
{
|
||||
private Auth $authService;
|
||||
|
||||
private UserService $userService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(
|
||||
Auth $authService,
|
||||
UserService $userService
|
||||
): void {
|
||||
$this->authService = $authService;
|
||||
$this->userService = $userService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles PATCH requests for user UI settings (e.g. dismissing modals).
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
public function patch(array $params): Response
|
||||
{
|
||||
if (! $this->authService->isLoggedIn()) {
|
||||
return $this->tpl->displayJson(['status' => 'error', 'message' => 'Not authorized'], 401);
|
||||
}
|
||||
|
||||
// Handle modal dismissal updates
|
||||
if (
|
||||
isset($params['patchModalSettings'], $params['settings'])
|
||||
&& $params['patchModalSettings'] == 1
|
||||
) {
|
||||
$permanent = isset($params['permanent']) && $params['permanent'] == 1;
|
||||
$this->userService->saveModalDismissal($params['settings'], $permanent);
|
||||
|
||||
return $this->tpl->displayJson(['status' => 'success']);
|
||||
}
|
||||
|
||||
return $this->tpl->displayJson(['status' => 'error', 'message' => 'Invalid request'], 400);
|
||||
}
|
||||
}
|
||||
60
app/Domain/Users/Controllers/ProfileImage.php
Normal file
60
app/Domain/Users/Controllers/ProfileImage.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Users\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Leantime\Core\Http\Responses\ImageResponse;
|
||||
use Leantime\Domain\Users\Services\Users as UserService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Serves and updates user profile images.
|
||||
*
|
||||
* A native Laravel controller (constructor DI, route-bound actions returning a
|
||||
* Response/Responsable) — no Frontcontroller/legacy base-controller machinery.
|
||||
* Relocated from the retired Api\Controllers\Users. Bound in Users/routes.php at the
|
||||
* canonical /users/profileImage/{id} plus the backward-compatible /api/users alias that
|
||||
* core templates and the plugin submodule still reference.
|
||||
*/
|
||||
class ProfileImage
|
||||
{
|
||||
public function __construct(private UserService $userService) {}
|
||||
|
||||
/**
|
||||
* GET — returns the profile image as a binary/SVG response.
|
||||
*
|
||||
* The id comes from the canonical path segment ({id}) or the legacy ?profileImage=
|
||||
* query param. "me" resolves to the session user; "false"/empty yields the placeholder.
|
||||
*/
|
||||
public function show(Request $request, ?string $id = null): ImageResponse
|
||||
{
|
||||
$id = $request->query('profileImage', $id) ?? 'false';
|
||||
|
||||
if ($id === 'me') {
|
||||
$id = session('userdata.id');
|
||||
}
|
||||
|
||||
return new ImageResponse($this->userService->getProfilePicture($id));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST — uploads a new profile photo for the current user.
|
||||
*
|
||||
* The target is always the session user; a client-supplied id is never trusted.
|
||||
*/
|
||||
public function upload(): Response
|
||||
{
|
||||
if (! isset($_FILES['file'])) {
|
||||
return response()->json(['error' => 'File not included'], 400);
|
||||
}
|
||||
|
||||
$_FILES['file']['name'] = 'userPicture.png';
|
||||
|
||||
$this->userService->setProfilePicture($_FILES, session('userdata.id'));
|
||||
|
||||
session(['msg' => 'PICTURE_CHANGED']);
|
||||
session(['msgT' => 'success']);
|
||||
|
||||
return response()->json(['status' => 'ok']);
|
||||
}
|
||||
}
|
||||
38
app/Domain/Users/Controllers/ShowAll.php
Normal file
38
app/Domain/Users/Controllers/ShowAll.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Users\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Domain\Auth\Models\Roles;
|
||||
use Leantime\Domain\Users\Permissions\UsersPermissions;
|
||||
use Leantime\Domain\Users\Services\Users as UserService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ShowAll extends Controller
|
||||
{
|
||||
private UserService $userService;
|
||||
|
||||
public function init(UserService $userService): void
|
||||
{
|
||||
$this->userService = $userService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
#[RequiresPermission(UsersPermissions::VIEW, global: true)]
|
||||
public function get(): Response
|
||||
{
|
||||
$this->tpl->assign('allUsers', $this->userService->getAllVisibleToUser());
|
||||
$this->tpl->assign('admin', true);
|
||||
$this->tpl->assign('roles', Roles::getRoles());
|
||||
|
||||
return $this->tpl->display('users.showAll');
|
||||
}
|
||||
|
||||
public function post($params): Response
|
||||
{
|
||||
return $this->tpl->displayJson(['status' => 'Not Implemented']);
|
||||
}
|
||||
}
|
||||
81
app/Domain/Users/Enums/EmploymentType.php
Normal file
81
app/Domain/Users/Enums/EmploymentType.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Leantime\Domain\Users\Enums;
|
||||
|
||||
/**
|
||||
* Employment type for a user. Drives how weekly_hours is interpreted
|
||||
* (target vs ceiling vs bill-to cap vs best-effort) and how downstream
|
||||
* capacity math (over-allocation warnings, portfolio rollups) treats
|
||||
* this person.
|
||||
*
|
||||
* Stored on zp_user.employment_type (nullable — admin sets it explicitly,
|
||||
* no assumed default; treat NULL as "not configured").
|
||||
*
|
||||
* Value semantics:
|
||||
* FTE — weekly_hours is the *target*. Over = amber "over target"
|
||||
* (burnout signal). Common case.
|
||||
* PT — weekly_hours is a *ceiling* the user set. Over = red
|
||||
* "over ceiling" (planning violates commitment).
|
||||
* Contractor — weekly_hours is a *billable cap*. Over = red "over
|
||||
* billable cap" (cost overrun risk).
|
||||
* Volunteer — weekly_hours is *best-effort*. Excluded from over-
|
||||
* allocation math entirely; contributes to project totals
|
||||
* as separate "volunteer support" throughput.
|
||||
*/
|
||||
enum EmploymentType: string
|
||||
{
|
||||
case FTE = 'fte';
|
||||
case PartTime = 'pt';
|
||||
case Contractor = 'contractor';
|
||||
case Volunteer = 'volunteer';
|
||||
|
||||
/**
|
||||
* Human-readable label — used in form selects and badges. Not
|
||||
* translated at the enum level; templates run these through __()
|
||||
* with the corresponding language key when i18n is needed.
|
||||
*/
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::FTE => 'Full-time',
|
||||
self::PartTime => 'Part-time',
|
||||
self::Contractor => 'Contractor',
|
||||
self::Volunteer => 'Volunteer',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* i18n key for the label — used by admin templates and any
|
||||
* downstream UI that wants localised strings.
|
||||
*/
|
||||
public function langKey(): string
|
||||
{
|
||||
return 'users.employment_type.'.$this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether over-allocation warnings apply to this person. False for
|
||||
* Volunteer only — a volunteer at 3× their nominal hours is not a
|
||||
* problem, it's a gift. Everyone else gets warned.
|
||||
*/
|
||||
public function countsAgainstCapacity(): bool
|
||||
{
|
||||
return $this !== self::Volunteer;
|
||||
}
|
||||
|
||||
/**
|
||||
* The tone of the over-cap warning when it fires. FTE = amber
|
||||
* (burnout signal, not a violation). PT/Contractor = red
|
||||
* (violates an explicit ceiling or a billable cap).
|
||||
*/
|
||||
public function overCapTone(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::FTE => 'warn',
|
||||
self::PartTime, self::Contractor => 'danger',
|
||||
self::Volunteer => 'none',
|
||||
};
|
||||
}
|
||||
}
|
||||
208
app/Domain/Users/Js/usersController.js
Normal file
208
app/Domain/Users/Js/usersController.js
Normal file
@@ -0,0 +1,208 @@
|
||||
leantime.usersController = (function () {
|
||||
|
||||
var readURL = function (input) {
|
||||
|
||||
clearCroppie();
|
||||
|
||||
if (input.files && input.files[0]) {
|
||||
var reader = new FileReader();
|
||||
|
||||
var profileImg = jQuery('#profileImg');
|
||||
reader.onload = function (e) {
|
||||
//profileImg.attr('src', e.currentTarget.result);
|
||||
|
||||
_uploadResult = profileImg
|
||||
.croppie(
|
||||
{
|
||||
enableExif: true,
|
||||
viewport: {
|
||||
width: 175,
|
||||
height: 175,
|
||||
type: 'circle'
|
||||
},
|
||||
boundary: {
|
||||
width: 200,
|
||||
height: 200
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
_uploadResult.croppie(
|
||||
'bind',
|
||||
{
|
||||
url: e.currentTarget.result
|
||||
}
|
||||
);
|
||||
|
||||
jQuery("#previousImage").hide();
|
||||
};
|
||||
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
var clearCroppie = function () {
|
||||
jQuery('#profileImg').croppie('destroy');
|
||||
jQuery("#previousImage").show();
|
||||
};
|
||||
|
||||
var saveCroppie = function () {
|
||||
|
||||
jQuery('#save-picture').addClass('running');
|
||||
|
||||
jQuery('#profileImg').attr('src', leantime.appUrl + '/images/loaders/loader28.gif');
|
||||
_uploadResult.croppie(
|
||||
'result',
|
||||
{
|
||||
type: "blob",
|
||||
circle: true
|
||||
}
|
||||
).then(
|
||||
function (result) {
|
||||
leantime.usersService.saveUserPhoto(result);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var initUserTable = function () {
|
||||
|
||||
jQuery(document).ready(function () {
|
||||
|
||||
var size = 100;
|
||||
|
||||
var allUsersTable = jQuery("#allUsersTable").DataTable({
|
||||
"language": {
|
||||
"decimal": leantime.i18n.__("datatables.decimal"),
|
||||
"emptyTable": leantime.i18n.__("datatables.emptyTable"),
|
||||
"info": leantime.i18n.__("datatables.info"),
|
||||
"infoEmpty": leantime.i18n.__("datatables.infoEmpty"),
|
||||
"infoFiltered": leantime.i18n.__("datatables.infoFiltered"),
|
||||
"infoPostFix": leantime.i18n.__("datatables.infoPostFix"),
|
||||
"thousands": leantime.i18n.__("datatables.thousands"),
|
||||
"lengthMenu": leantime.i18n.__("datatables.lengthMenu"),
|
||||
"loadingRecords": leantime.i18n.__("datatables.loadingRecords"),
|
||||
"processing": leantime.i18n.__("datatables.processing"),
|
||||
"search": leantime.i18n.__("datatables.search"),
|
||||
"zeroRecords": leantime.i18n.__("datatables.zeroRecords"),
|
||||
"paginate": {
|
||||
"first": leantime.i18n.__("datatables.first"),
|
||||
"last": leantime.i18n.__("datatables.last"),
|
||||
"next": leantime.i18n.__("datatables.next"),
|
||||
"previous": leantime.i18n.__("datatables.previous"),
|
||||
},
|
||||
"aria": {
|
||||
"sortAscending": leantime.i18n.__("datatables.sortAscending"),
|
||||
"sortDescending":leantime.i18n.__("datatables.sortDescending"),
|
||||
}
|
||||
|
||||
},
|
||||
"dom": '<"top">rt<"bottom"ilp><"clear">',
|
||||
"searching": false,
|
||||
"displayLength":100
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
var _initModals = function () {
|
||||
|
||||
var userImportModalConfig = {
|
||||
sizes: {
|
||||
minW: 400,
|
||||
minH: 350
|
||||
},
|
||||
resizable: true,
|
||||
autoSizable: true,
|
||||
callbacks: {
|
||||
afterShowCont: function () {
|
||||
jQuery(".showDialogOnLoad").show();
|
||||
jQuery(".userImportModal").nyroModal(userImportModalConfig);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
jQuery(".userImportModal").nyroModal(userImportModalConfig);
|
||||
}
|
||||
|
||||
var initUserEditModal = function () {
|
||||
|
||||
var userEditModal = {
|
||||
sizes: {
|
||||
minW: 900,
|
||||
minH: 250
|
||||
},
|
||||
resizable: true,
|
||||
autoSizable: true,
|
||||
callbacks: {
|
||||
afterShowCont: function () {
|
||||
jQuery(".showDialogOnLoad").show();
|
||||
jQuery(".userEditModal").nyroModal(userEditModal);
|
||||
},
|
||||
beforeClose: function () {
|
||||
|
||||
location.reload();
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
jQuery(".userEditModal").nyroModal(userEditModal);
|
||||
}
|
||||
|
||||
var checkPWStrength = function (pwField) {
|
||||
|
||||
let timeout;
|
||||
|
||||
// traversing the DOM and getting the input and span using their IDs
|
||||
|
||||
let password = document.getElementById(pwField)
|
||||
let strengthBadge = document.getElementById('pwStrength')
|
||||
|
||||
// The strong and weak password Regex pattern checker
|
||||
|
||||
let strongPassword = new RegExp('(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{8,})')
|
||||
let mediumPassword = new RegExp('((?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{6,}))|((?=.*[a-z])(?=.*[A-Z])(?=.*[^A-Za-z0-9])(?=.{8,}))')
|
||||
|
||||
function StrengthChecker(PasswordParameter)
|
||||
{
|
||||
if (strongPassword.test(PasswordParameter)) {
|
||||
strengthBadge.style.backgroundColor = "#107530";
|
||||
strengthBadge.textContent = leantime.i18n.__('label.strong');
|
||||
} else if (mediumPassword.test(PasswordParameter)) {
|
||||
strengthBadge.style.backgroundColor = '#C5850D';
|
||||
strengthBadge.textContent = leantime.i18n.__('label.medium');
|
||||
} else {
|
||||
strengthBadge.style.backgroundColor = '#CC6B6B';
|
||||
strengthBadge.textContent = leantime.i18n.__('label.weak');
|
||||
}
|
||||
}
|
||||
|
||||
password.addEventListener("input", () => {
|
||||
|
||||
//The badge is hidden by default, so we show it
|
||||
|
||||
strengthBadge.style.display = 'block';
|
||||
clearTimeout(timeout);
|
||||
|
||||
timeout = setTimeout(() => StrengthChecker(password.value), 500);
|
||||
|
||||
if (password.value.length !== 0) {
|
||||
strengthBadge.style.display != 'block'
|
||||
} else {
|
||||
strengthBadge.style.display = 'none'
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Make public what you want to have public, everything else is private
|
||||
return {
|
||||
readURL: readURL,
|
||||
clearCroppie: clearCroppie,
|
||||
saveCroppie: saveCroppie,
|
||||
initUserTable:initUserTable,
|
||||
_initModals:_initModals,
|
||||
checkPWStrength:checkPWStrength,
|
||||
initUserEditModal:initUserEditModal
|
||||
};
|
||||
})();
|
||||
40
app/Domain/Users/Js/usersRepository.js
Normal file
40
app/Domain/Users/Js/usersRepository.js
Normal file
@@ -0,0 +1,40 @@
|
||||
leantime.usersRepository = (function () {
|
||||
|
||||
//Functions
|
||||
|
||||
var saveUserPhoto = function (photo) {
|
||||
var formData = new FormData();
|
||||
formData.append('file', photo);
|
||||
jQuery.ajax(
|
||||
{
|
||||
type: 'POST',
|
||||
url: leantime.appUrl + '/users/profileImage',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function (resp) {
|
||||
|
||||
jQuery('#save-picture').removeClass('running');
|
||||
|
||||
location.reload();
|
||||
},
|
||||
error: function (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var updateUserViewSettings = function (module, value) {
|
||||
|
||||
leantime.rpc('Users.Users.updateUserSettings', { category: 'views', setting: module, value: value })
|
||||
.catch(function (e) { console.error('Could not save view setting', e); });
|
||||
|
||||
};
|
||||
|
||||
// Make public what you want to have public, everything else is private
|
||||
return {
|
||||
saveUserPhoto: saveUserPhoto,
|
||||
updateUserViewSettings:updateUserViewSettings
|
||||
};
|
||||
})();
|
||||
13
app/Domain/Users/Js/usersService.js
Normal file
13
app/Domain/Users/Js/usersService.js
Normal file
@@ -0,0 +1,13 @@
|
||||
leantime.usersService = (function () {
|
||||
|
||||
//Functions
|
||||
|
||||
var saveUserPhoto = function (photo) {
|
||||
leantime.usersRepository.saveUserPhoto(photo);
|
||||
};
|
||||
|
||||
// Make public what you want to have public, everything else is private
|
||||
return {
|
||||
saveUserPhoto: saveUserPhoto
|
||||
};
|
||||
})();
|
||||
57
app/Domain/Users/Permissions/UsersPermissions.php
Normal file
57
app/Domain/Users/Permissions/UsersPermissions.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Users\Permissions;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\Permission;
|
||||
use Leantime\Core\Auth\Permissions\ProvidesPermissions;
|
||||
|
||||
/**
|
||||
* The Users (account management) permission vocabulary — the verbs only.
|
||||
*
|
||||
* Declares *what* can be done with user accounts; it says nothing about *which roles* may do
|
||||
* it. Role assignment is centrally owned (defaults in
|
||||
* {@see \Leantime\Core\Auth\Permissions\DefaultRolePermissions}, runtime in `zp_role_permissions`).
|
||||
*
|
||||
* Every user capability is COMPANY-WIDE, not project-scoped: managing accounts is authority a
|
||||
* user holds across the whole installation, evaluated against their GLOBAL role (see
|
||||
* {@see \Leantime\Core\Auth\RoleResolver}). So every Permission below is constructed with
|
||||
* `projectScoped = false`, and call sites gate with `#[RequiresPermission(..., global: true)]`.
|
||||
*
|
||||
* These verbs cover MANAGING OTHER accounts only. Editing one's OWN profile/settings/avatar is
|
||||
* self-service that every authenticated user may do and is deliberately NOT gated by any
|
||||
* `users.*` permission (see {@see \Leantime\Domain\Users\Controllers\EditOwn} and the
|
||||
* self-service methods on the Users service).
|
||||
*/
|
||||
final class UsersPermissions implements ProvidesPermissions
|
||||
{
|
||||
/** View the company user roster / read another account. */
|
||||
public const VIEW = 'users.view';
|
||||
|
||||
/** Invite / create new accounts. */
|
||||
public const CREATE = 'users.create';
|
||||
|
||||
/** Edit another account (role, client, status, profile fields). */
|
||||
public const EDIT = 'users.edit';
|
||||
|
||||
/** Delete accounts. */
|
||||
public const DELETE = 'users.delete';
|
||||
|
||||
/** Bulk-import accounts from a directory (LDAP). */
|
||||
public const IMPORT = 'users.import';
|
||||
|
||||
public function domain(): string
|
||||
{
|
||||
return 'users';
|
||||
}
|
||||
|
||||
public function permissions(): array
|
||||
{
|
||||
return [
|
||||
new Permission(self::VIEW, 'View users', false),
|
||||
new Permission(self::CREATE, 'Invite/create users', false),
|
||||
new Permission(self::EDIT, 'Edit users', false),
|
||||
new Permission(self::DELETE, 'Delete users', false),
|
||||
new Permission(self::IMPORT, 'Import users (LDAP)', false),
|
||||
];
|
||||
}
|
||||
}
|
||||
619
app/Domain/Users/Repositories/Users.php
Normal file
619
app/Domain/Users/Repositories/Users.php
Normal file
@@ -0,0 +1,619 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Users\Repositories;
|
||||
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Leantime\Core\Configuration\Environment;
|
||||
use Leantime\Core\Db\DatabaseHelper;
|
||||
use Leantime\Core\Db\Db as DbCore;
|
||||
use Leantime\Domain\Files\Repositories\Files;
|
||||
use Leantime\Domain\Users\Enums\EmploymentType;
|
||||
|
||||
class Users
|
||||
{
|
||||
private ConnectionInterface $connection;
|
||||
|
||||
public string $user;
|
||||
|
||||
public string $lastname;
|
||||
|
||||
public string $firstname;
|
||||
|
||||
public int $role;
|
||||
|
||||
public string $jobTitle;
|
||||
|
||||
public string $jobLevel;
|
||||
|
||||
public string $department;
|
||||
|
||||
public int $id;
|
||||
|
||||
public array $adminRoles = [40, 50];
|
||||
|
||||
public array $status = ['active' => 'label.active', 'inactive' => 'label.inactive', 'invited' => 'label.invited'];
|
||||
|
||||
/**
|
||||
* Request-scoped memo for getUser(), keyed by user id. Cleared on writes.
|
||||
*
|
||||
* @var array<int|string, array|bool>
|
||||
*/
|
||||
private array $userMemo = [];
|
||||
|
||||
/**
|
||||
* __construct - neu db connection
|
||||
*/
|
||||
public function __construct(
|
||||
protected Environment $config,
|
||||
protected DbCore $db,
|
||||
protected DatabaseHelper $dbHelper,
|
||||
protected Files $files
|
||||
) {
|
||||
$this->connection = $db->getConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* getUser - get on user from db
|
||||
*/
|
||||
public function getUser($id): array|bool
|
||||
{
|
||||
// Request-scoped memo: getUser is hit repeatedly per request for
|
||||
// author/role/avatar lookups. Cleared by editUser/patchUser/deleteUser.
|
||||
if (array_key_exists($id, $this->userMemo)) {
|
||||
return $this->userMemo[$id];
|
||||
}
|
||||
|
||||
$result = $this->connection->table('zp_user')
|
||||
->where('id', $id)
|
||||
->limit(1)
|
||||
->first();
|
||||
|
||||
return $this->userMemo[$id] = $result ? (array) $result : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* getUser - get on user from db
|
||||
*/
|
||||
public function getUserBySha($hash): array|false
|
||||
{
|
||||
$result = $this->connection->table('zp_user')
|
||||
->whereRaw('SHA1(CONCAT(id, ?)) = ?', [$this->config->sessionPassword, $hash])
|
||||
->first();
|
||||
|
||||
return $result ? (array) $result : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* getLastLogin - get the date of the last login of any user
|
||||
*
|
||||
* @return string|null returns datetime string with last login or null if nothing could be found
|
||||
*/
|
||||
public function getLastLogin(): ?string
|
||||
{
|
||||
$result = $this->connection->table('zp_user')
|
||||
->select('lastlogin')
|
||||
->orderByDesc('lastlogin')
|
||||
->limit(1)
|
||||
->first();
|
||||
|
||||
return $result->lastlogin ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* getUserByEmail - get on user from db
|
||||
*/
|
||||
public function getUserByEmail(string $email, string $status = 'a'): array|false
|
||||
{
|
||||
$query = $this->connection->table('zp_user')
|
||||
->where('username', $email);
|
||||
|
||||
if ($status === 'a') {
|
||||
$query->whereRaw('LOWER(status) = ?', ['a']);
|
||||
}
|
||||
|
||||
if ($status === 'i') {
|
||||
$query->whereRaw('LOWER(status) = ?', ['i']);
|
||||
}
|
||||
|
||||
$result = $query->limit(1)->first();
|
||||
|
||||
return $result ? (array) $result : false;
|
||||
}
|
||||
|
||||
public function getNumberOfUsers($activeOnly = false, $includeApi = true): int
|
||||
{
|
||||
$query = $this->connection->table('zp_user')
|
||||
->selectRaw('COUNT(id) AS '.$this->dbHelper->wrapColumn('userCount'));
|
||||
|
||||
if ($activeOnly) {
|
||||
$query->where('status', 'a');
|
||||
}
|
||||
|
||||
if ($includeApi) {
|
||||
$query->where(function ($q) {
|
||||
$q->where('source', '!=', 'api')
|
||||
->orWhereNull('source');
|
||||
});
|
||||
}
|
||||
|
||||
$result = $query->first();
|
||||
|
||||
return $result->userCount ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* getEmployees - get all employees
|
||||
*/
|
||||
public function getEmployees(): array
|
||||
{
|
||||
$results = $this->connection->table('zp_user')
|
||||
->select([
|
||||
'zp_user.id',
|
||||
'zp_user.lastname',
|
||||
'zp_user.jobTitle',
|
||||
'zp_user.jobLevel',
|
||||
'zp_user.department',
|
||||
'zp_user.modified',
|
||||
])
|
||||
->selectRaw('COALESCE(zp_user.firstname, zp_user.username) AS firstname')
|
||||
->orderBy('lastname')
|
||||
->get();
|
||||
|
||||
return array_map(fn ($item) => (array) $item, $results->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* getAll - get all user
|
||||
*
|
||||
* @param bool $activeOnly
|
||||
*/
|
||||
public function getAll($activeOnly = false): array
|
||||
{
|
||||
$query = $this->connection->table('zp_user')
|
||||
->select([
|
||||
'zp_user.id',
|
||||
'lastname',
|
||||
'role',
|
||||
'profileId',
|
||||
'status',
|
||||
'username',
|
||||
'twoFAEnabled',
|
||||
'clientId',
|
||||
'zp_clients.name as clientName',
|
||||
'jobTitle',
|
||||
'jobLevel',
|
||||
'department',
|
||||
'zp_user.modified',
|
||||
])
|
||||
->selectRaw("CASE WHEN firstname <> '' THEN firstname ELSE username END AS firstname")
|
||||
->leftJoin('zp_clients', 'zp_clients.id', '=', 'zp_user.clientId')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('source')
|
||||
->orWhere('source', '!=', 'api');
|
||||
});
|
||||
|
||||
if ($activeOnly) {
|
||||
$query->where('status', 'like', 'a');
|
||||
}
|
||||
|
||||
$results = $query->orderBy('lastname')->get();
|
||||
|
||||
return array_map(fn ($item) => (array) $item, $results->toArray());
|
||||
}
|
||||
|
||||
public function getAllBySource($source): false|array
|
||||
{
|
||||
$query = $this->connection->table('zp_user')
|
||||
->select([
|
||||
'zp_user.id',
|
||||
'lastname',
|
||||
'firstname',
|
||||
'role',
|
||||
'profileId',
|
||||
'status',
|
||||
'username',
|
||||
'lastlogin',
|
||||
'createdOn',
|
||||
'jobTitle',
|
||||
'jobLevel',
|
||||
'department',
|
||||
'modified',
|
||||
]);
|
||||
|
||||
if ($source === null) {
|
||||
$query->whereNull('source');
|
||||
} else {
|
||||
$query->where('source', $source);
|
||||
}
|
||||
|
||||
$results = $query->orderBy('lastname')->get();
|
||||
|
||||
return array_map(fn ($item) => (array) $item, $results->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* getAll - get all user
|
||||
*/
|
||||
public function getAllClientUsers($clientId): array
|
||||
{
|
||||
$results = $this->connection->table('zp_user')
|
||||
->select([
|
||||
'zp_user.id',
|
||||
'lastname',
|
||||
'firstname',
|
||||
'role',
|
||||
'profileId',
|
||||
'status',
|
||||
'username',
|
||||
'twoFAEnabled',
|
||||
'zp_clients.name as clientName',
|
||||
'jobTitle',
|
||||
'jobLevel',
|
||||
'department',
|
||||
'modified',
|
||||
])
|
||||
->leftJoin('zp_clients', 'zp_clients.id', '=', 'zp_user.clientId')
|
||||
->where('clientId', $clientId)
|
||||
->orderBy('lastname')
|
||||
->get();
|
||||
|
||||
return array_map(fn ($item) => (array) $item, $results->toArray());
|
||||
}
|
||||
|
||||
public function isAdmin($userId): bool
|
||||
{
|
||||
$result = $this->connection->table('zp_user')
|
||||
->select('role')
|
||||
->where('id', $userId)
|
||||
->limit(1)
|
||||
->first();
|
||||
|
||||
if ($result && in_array($result->role, $this->adminRoles)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* editUSer - edit user
|
||||
*/
|
||||
public function editUser(array $values, $id): bool
|
||||
{
|
||||
unset($this->userMemo[$id]);
|
||||
|
||||
$updateData = [
|
||||
'firstname' => $values['firstname'],
|
||||
'lastname' => $values['lastname'],
|
||||
'username' => $values['user'],
|
||||
'phone' => $values['phone'] ?? '',
|
||||
'status' => $values['status'],
|
||||
'role' => $values['role'],
|
||||
'hours' => $values['hours'] ?? 0,
|
||||
'wage' => $values['wage'] ?? 0,
|
||||
'clientId' => $values['clientId'],
|
||||
'jobTitle' => $values['jobTitle'] ?? '',
|
||||
'jobLevel' => $values['jobLevel'] ?? '',
|
||||
'department' => $values['department'] ?? '',
|
||||
'modified' => now(),
|
||||
];
|
||||
|
||||
// Capacity attributes (v3.5.23) — only overwrite when explicitly
|
||||
// present so an admin form that omits the fields (or a legacy
|
||||
// caller) doesn't null out a value already set. Validation runs
|
||||
// at this boundary so every caller (form controller, service,
|
||||
// JSON-RPC) gets the same guarantees: unrecognised employment
|
||||
// types normalise to NULL; weekly_hours outside a sane range
|
||||
// normalises to NULL.
|
||||
if (array_key_exists('weekly_hours', $values)) {
|
||||
$updateData['weekly_hours'] = $this->normalizeWeeklyHours($values['weekly_hours']);
|
||||
}
|
||||
if (array_key_exists('employment_type', $values)) {
|
||||
$updateData['employment_type'] = $this->normalizeEmploymentType($values['employment_type']);
|
||||
}
|
||||
|
||||
if (isset($values['password']) && $values['password'] != '' && ! $this->isHashedPassword($values['password'])) {
|
||||
$updateData['password'] = password_hash($values['password'], PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
return $this->connection->table('zp_user')
|
||||
->where('id', $id)
|
||||
->update($updateData) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* usernameExist - Check if a username is already in db
|
||||
*/
|
||||
public function usernameExist($username, string $userId = ''): bool
|
||||
{
|
||||
$query = $this->connection->table('zp_user')
|
||||
->selectRaw('COUNT(username) AS '.$this->dbHelper->wrapColumn('numUser'))
|
||||
->where('username', $username);
|
||||
|
||||
if ($userId != '') {
|
||||
$query->where('id', '!=', $userId);
|
||||
}
|
||||
|
||||
$result = $query->limit(1)->first();
|
||||
|
||||
return (int) $result->numUser === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* removeFromClient - Remove user from client by setting clientId to null
|
||||
*
|
||||
* @param int $userId User ID to remove from client
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function removeFromClient(int $userId): bool
|
||||
{
|
||||
return $this->connection->table('zp_user')
|
||||
->where('id', $userId)
|
||||
->update([
|
||||
'clientId' => null,
|
||||
'modified' => now(),
|
||||
]) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* editOwn - Edit own Userdates
|
||||
*/
|
||||
public function editOwn($values, $id): void
|
||||
{
|
||||
$updateData = [
|
||||
'lastname' => $values['lastname'],
|
||||
'firstname' => $values['firstname'],
|
||||
'username' => $values['user'],
|
||||
'phone' => $values['phone'],
|
||||
'notifications' => $values['notifications'],
|
||||
'modified' => now(),
|
||||
];
|
||||
|
||||
if (isset($values['password']) && $values['password'] != '' && ! $this->isHashedPassword($values['password'])) {
|
||||
$updateData['password'] = password_hash($values['password'], PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
$this->connection->table('zp_user')
|
||||
->where('id', $id)
|
||||
->update($updateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* isHashedPassword - Detect a value that is already a password hash so callers that pass a
|
||||
* full user row (e.g. the OIDC login sync) don't re-hash the stored hash and lock the user out.
|
||||
*/
|
||||
private function isHashedPassword(string $password): bool
|
||||
{
|
||||
return ! empty(password_get_info($password)['algo']);
|
||||
}
|
||||
|
||||
/**
|
||||
* addUser - add User to db
|
||||
*/
|
||||
public function addUser(array $values): false|string
|
||||
{
|
||||
$userId = $this->connection->table('zp_user')->insertGetId([
|
||||
'firstname' => $values['firstname'] ?? '',
|
||||
'lastname' => $values['lastname'] ?? '',
|
||||
'phone' => $values['phone'] ?? '',
|
||||
'username' => $values['user'],
|
||||
'role' => $values['role'],
|
||||
'notifications' => 1,
|
||||
'clientId' => $values['clientId'] ?? '',
|
||||
'password' => password_hash($values['password'], PASSWORD_DEFAULT),
|
||||
'source' => $values['source'] ?? '',
|
||||
'pwReset' => $values['pwReset'] ?? '',
|
||||
'status' => $values['status'] ?? '',
|
||||
'createdOn' => now(),
|
||||
'jobTitle' => $values['jobTitle'] ?? '',
|
||||
'jobLevel' => $values['jobLevel'] ?? '',
|
||||
'department' => $values['department'] ?? '',
|
||||
'weekly_hours' => $this->normalizeWeeklyHours($values['weekly_hours'] ?? null),
|
||||
'employment_type' => $this->normalizeEmploymentType($values['employment_type'] ?? null),
|
||||
'modified' => now(),
|
||||
]);
|
||||
|
||||
return (string) $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* deleteUser - delete user from db
|
||||
*/
|
||||
public function deleteUser($id): void
|
||||
{
|
||||
unset($this->userMemo[$id]);
|
||||
|
||||
$this->connection->table('zp_user')
|
||||
->where('zp_user.id', $id)
|
||||
->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* setPicture - set the profile picture for an individual
|
||||
*
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function setPicture($fileId, $id): bool
|
||||
{
|
||||
return $this->connection->table('zp_user')
|
||||
->where('id', $id)
|
||||
->update([
|
||||
'profileId' => $fileId,
|
||||
'modified' => dtHelper()->dbNow()->formatDateTimeForDb(),
|
||||
]) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function getProfilePicture($id): array|false
|
||||
{
|
||||
if ($id === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = $this->connection->table('zp_user')
|
||||
->select(['profileId', 'firstname', 'lastname'])
|
||||
->where('id', $id)
|
||||
->limit(1)
|
||||
->first();
|
||||
|
||||
return $result ? (array) $result : false;
|
||||
}
|
||||
|
||||
public function patchUser($id, $params): bool
|
||||
{
|
||||
unset($this->userMemo[$id]);
|
||||
|
||||
$updates = [];
|
||||
foreach ($params as $key => $value) {
|
||||
$cleanKey = DbCore::sanitizeToColumnString($key);
|
||||
if ($cleanKey === 'password') {
|
||||
$updates[$cleanKey] = password_hash($value, PASSWORD_DEFAULT);
|
||||
} else {
|
||||
$updates[$cleanKey] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$updates['modified'] = now();
|
||||
|
||||
return $this->connection->table('zp_user')
|
||||
->where('id', $id)
|
||||
->update($updates) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* getUserIdByName - Get Author/User Id by first- and lastname
|
||||
*
|
||||
* @param string $lastname Lastname
|
||||
* @return int|bool Identifier of user or false, if not found
|
||||
*/
|
||||
public function getUserIdByName(string $firstname, string $lastname): int|bool
|
||||
{
|
||||
$result = $this->connection->table('zp_user')
|
||||
->select('profileId')
|
||||
->where('firstname', $firstname)
|
||||
->where('lastname', $lastname)
|
||||
->first();
|
||||
|
||||
return $result->profileId ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user settings - retrieves and deserializes user settings
|
||||
*
|
||||
* @param int $userId The user ID to get settings for
|
||||
* @param string|null $settingPath Optional dot notation path to retrieve specific setting (e.g. 'onboarding.firstLoginCompleted')
|
||||
* @return mixed The requested settings or specific setting value, empty array if no settings exist
|
||||
*/
|
||||
public function getUserSettings(int $userId, ?string $settingPath = null): mixed
|
||||
{
|
||||
$result = $this->connection->table('zp_user')
|
||||
->select('settings')
|
||||
->where('id', $userId)
|
||||
->limit(1)
|
||||
->first();
|
||||
|
||||
// If no settings exist yet, return empty array
|
||||
if (! $result || empty($result->settings)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Try to unserialize the settings
|
||||
try {
|
||||
$settings = safe_unserialize($result->settings, []);
|
||||
|
||||
// Ensure settings is an array (unserialize may return stdClass)
|
||||
if (is_object($settings)) {
|
||||
$settings = json_decode(json_encode($settings), true);
|
||||
}
|
||||
|
||||
if (! is_array($settings)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// If we have a specific path to retrieve
|
||||
if ($settingPath !== null) {
|
||||
return $this->getNestedSetting($settings, $settingPath);
|
||||
}
|
||||
|
||||
return $settings;
|
||||
} catch (\Exception $e) {
|
||||
// If there's an error unserializing, return empty array
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get a nested setting using dot notation
|
||||
*
|
||||
* @param array $settings The settings array
|
||||
* @param string $path Dot notation path (e.g. 'onboarding.firstLoginCompleted')
|
||||
* @return mixed The setting value or null if not found
|
||||
*/
|
||||
private function getNestedSetting(array $settings, string $path)
|
||||
{
|
||||
$keys = explode('.', $path);
|
||||
$current = $settings;
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if (! is_array($current) || ! isset($current[$key])) {
|
||||
return null;
|
||||
}
|
||||
$current = $current[$key];
|
||||
}
|
||||
|
||||
return $current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise a POSTed weekly_hours value into the persisted shape.
|
||||
* Accepts numeric input; rejects anything outside 0..168 (168 =
|
||||
* hours in a week, the largest value that has physical meaning).
|
||||
* Anything that fails validation becomes NULL — treated by the
|
||||
* capacity math as "not configured" rather than saving garbage.
|
||||
*
|
||||
* Fractional input is ROUNDED, not truncated. The column is an int
|
||||
* and the edit-user field is a step-1 number input, so fractions
|
||||
* only reach here from the API — where a plain (int) cast silently
|
||||
* turned 37.5 into 37. Rounding keeps the value closest to intent;
|
||||
* rejecting it outright would be worse, since NULL removes the
|
||||
* person from capacity math entirely.
|
||||
*
|
||||
* NOTE: the int column cannot represent genuinely fractional
|
||||
* contracts (37.5 h/wk is common part-time). Storing minutes, or
|
||||
* widening the column, is the real fix if that case matters.
|
||||
*/
|
||||
private function normalizeWeeklyHours(mixed $value): ?int
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
if (! is_numeric($value)) {
|
||||
return null;
|
||||
}
|
||||
$hours = (int) round((float) $value);
|
||||
if ($hours < 0 || $hours > 168) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $hours;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise a POSTed employment_type into one of the four enum
|
||||
* cases or NULL. Any string that isn't a recognised case is
|
||||
* silently normalised to NULL so a crafted POST can't persist
|
||||
* garbage that later EmploymentType::from() would throw on.
|
||||
*/
|
||||
private function normalizeEmploymentType(mixed $value): ?string
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
$case = EmploymentType::tryFrom((string) $value);
|
||||
|
||||
return $case?->value;
|
||||
}
|
||||
}
|
||||
1561
app/Domain/Users/Services/Users.php
Normal file
1561
app/Domain/Users/Services/Users.php
Normal file
File diff suppressed because it is too large
Load Diff
16
app/Domain/Users/Templates/components/profile-box.blade.php
Normal file
16
app/Domain/Users/Templates/components/profile-box.blade.php
Normal file
@@ -0,0 +1,16 @@
|
||||
@props([
|
||||
'user' => null
|
||||
])
|
||||
|
||||
<div class="profileBox">
|
||||
<div class="commentImage">
|
||||
@if (isset($user['userId']) || isset($user->userId))
|
||||
<x-users::profile-image :user="$user" />
|
||||
@else
|
||||
<i class="fa fa-user"></i>
|
||||
@endif
|
||||
</div>
|
||||
<div class="userName">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
@props([
|
||||
'user' => null
|
||||
])
|
||||
|
||||
<img {{ $attributes->merge([
|
||||
'src' => BASE_URL . '/api/users?profileImage=' . $user['id'] .'&v='.format($user['modified'])->timestamp(),
|
||||
]) }} />
|
||||
32
app/Domain/Users/Templates/delUser.blade.php
Normal file
32
app/Domain/Users/Templates/delUser.blade.php
Normal file
@@ -0,0 +1,32 @@
|
||||
@extends($layout)
|
||||
@section('content')
|
||||
|
||||
<div class="pageheader">
|
||||
<div class="pageicon"><span class="fa {{ $tpl->getModulePicture() }}"></span></div>
|
||||
<div class="pagetitle">
|
||||
<h5>{!! __('label.administration') !!}</h5>
|
||||
<h1><h1>{!! __('headlines.delete_user') !!}</h1></h1>
|
||||
</div>
|
||||
</div><!--pageheader-->
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<h4 class="widget widgettitle">{!! __('subtitles.delete') !!}</h4>
|
||||
<div class="widgetcontent">
|
||||
|
||||
<form method="post">
|
||||
<input type="hidden" name="{{ session('formTokenName') }}" value="{{ session('formTokenValue') }}" />
|
||||
<p>{!! __('text.confirm_user_deletion') !!}</p><br />
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.yes_delete')" name="del" />
|
||||
<x-global::forms.button tag="a" contentRole="tertiary" link="{{ BASE_URL }}/users/showAll">{!! __('buttons.back') !!}</x-global::forms.button>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
508
app/Domain/Users/Templates/editOwn.blade.php
Normal file
508
app/Domain/Users/Templates/editOwn.blade.php
Normal file
@@ -0,0 +1,508 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
<x-global::pageheader :icon="'fa fa-user'">
|
||||
<h5>{{ __('label.overview') }}</h5>
|
||||
<h1>{!! __('headlines.accountSettings') !!}</h1>
|
||||
|
||||
</x-global::pageheader>
|
||||
|
||||
<div class="maincontent">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="maincontentinner">
|
||||
<div class="tabbedwidget tab-primary accountTabs">
|
||||
|
||||
<ul>
|
||||
<li><a href="#myProfile">{!! __('tabs.myProfile') !!}</a></li>
|
||||
<li><a href="#security">{!! __('tabs.security') !!}</a></li>
|
||||
<li><a href="#settings">{!! __('tabs.settings') !!}</a></li>
|
||||
<li><a href="#notifications">{!! __('tabs.notifications') !!}</a></li>
|
||||
<li><a href="#theme">{!! __('tabs.theme') !!}</a></li>
|
||||
@dispatchEvent('tabs')
|
||||
</ul>
|
||||
|
||||
<div id="myProfile">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<form action="" method="post">
|
||||
<h4 class="widgettitle title-light"><?php echo $tpl->__('label.profile_information'); ?></h4>
|
||||
<input type="hidden" name="{{ session("formTokenName") }}" value="{{ session("formTokenValue") }}" />
|
||||
<div class="row-fluid">
|
||||
<div class="form-group">
|
||||
<label for="firstname" >{{ __('label.firstname') }}</label>
|
||||
<span>
|
||||
<input type="text" class="input" name="firstname" id="firstname" {{ session("userdata.isExternalAuth") ? "disabled='disabled'" : '' }}
|
||||
value="{{ $values['firstname'] }}"/><br/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="lastname" >{{ __('label.lastname') }}</label>
|
||||
<span>
|
||||
<input type="text" name="lastname" class="input" id="lastname" {{ session("userdata.isExternalAuth") ? "disabled='disabled'" : '' }}
|
||||
value="{{ $values['lastname'] }}"/><br/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="user" >{{ __('label.email') }}</label>
|
||||
<span>
|
||||
<input type="text" name="user" class="input" id="user" {{ session("userdata.isExternalAuth") ? "disabled='disabled'" : '' }}
|
||||
value="{{ $values['user'] }}"/><br/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="phone" >{{ __('label.phone') }}</label>
|
||||
<span>
|
||||
<input type="text" name="phone" class="input" id="phone" {{ session("userdata.isExternalAuth") ? "disabled='disabled'" : '' }}
|
||||
value="{{ $values['phone'] }}"/><br/>
|
||||
</span>
|
||||
</div>
|
||||
<p class='stdformbutton'>
|
||||
<input type="hidden" name="profileInfo" value="1" />
|
||||
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="save" id="save" />
|
||||
</p>
|
||||
<br />
|
||||
<h4 class="widgettitle title-light">{{ __('label.employee_information') }}</h4>
|
||||
<em>{{ __('text.only_admins_can_change_user_info') }}</em><br /><br />
|
||||
<div class="form-group">
|
||||
<label for="phone" >{{ __('label.jobTitle') }}</label>
|
||||
<span>
|
||||
<input type="text" name="jobTitle" readonly class="input" id="jobTitle"}}
|
||||
value="{{ $values['jobTitle'] }}"/><br/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="phone" >{{ __('label.jobLevel') }}</label>
|
||||
<span>
|
||||
<input type="text" name="jobLevel" readonly class="input" id="jobLevel"}}
|
||||
value="{{ $values['jobLevel'] }}"/><br/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="phone" >{{ __('label.department') }}</label>
|
||||
<span>
|
||||
<input type="text" name="department" readonly class="input" id="department"}}
|
||||
value="{{ $values['department'] }}"/><br/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="center">
|
||||
<img src='{{ BASE_URL }}/api/users?profileImage={{ $user['id'] }}&v={{ format($user['modified'])->timestamp() }}' class='profileImg tw-rounded-full' alt='Profile Picture' id="previousImage"/>
|
||||
<div id="profileImg">
|
||||
</div>
|
||||
|
||||
<div class="par">
|
||||
|
||||
<label>{{ __('label.upload') }}</label>
|
||||
|
||||
<div class='fileupload fileupload-new' data-provides='fileupload'>
|
||||
<input type="hidden"/>
|
||||
<div class="input-append">
|
||||
<div class="uneditable-input span3">
|
||||
<i class="fa-file fileupload-exists"></i>
|
||||
<span class="fileupload-preview"></span>
|
||||
</div>
|
||||
<span class="btn btn-file">
|
||||
<span class="fileupload-new">{{ __('buttons.select_file') }}</span>
|
||||
<span class='fileupload-exists'>{{ __('buttons.change') }}</span>
|
||||
<input type='file' name='file' onchange="leantime.usersController.readURL(this)" accept=".jpg,.png,.gif,.webp"/>
|
||||
</span>
|
||||
|
||||
<a href='#' class='btn fileupload-exists' data-dismiss='fileupload' onclick="leantime.usersController.clearCroppie()">{{ __('buttons.remove') }}</a>
|
||||
</div>
|
||||
<p class='stdformbutton'>
|
||||
<span id="save-picture" class="btn btn-primary fileupload-exists ld-ext-right">
|
||||
<span onclick="leantime.usersController.saveCroppie()">{{ __('buttons.save') }}</span>
|
||||
<span class="ld ld-ring ld-spin"></span>
|
||||
</span>
|
||||
<input type="hidden" name="profileImage" value="1" />
|
||||
<input id="picSubmit" type="submit" name="savePic" class="hidden"
|
||||
value="{{ __('buttons.upload') }}"/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="security">
|
||||
<h4 class="widgettitle title-light">
|
||||
{!! __('headlines.change_password') !!}
|
||||
</h4>
|
||||
@if (session("userdata.isExternalAuth") )
|
||||
<strong> {{ __("text.account_managed_external_auth") }}</strong><br /><br />
|
||||
@endif
|
||||
<form method="post">
|
||||
<input type="hidden" name="{{ session("formTokenName") }}" value="{{ session("formTokenValue") }}" />
|
||||
<div class="row-fluid">
|
||||
<div class="form-group">
|
||||
<label for="currentPassword" >{{ __('label.old_password') }}</label>
|
||||
<span>
|
||||
<input type='password' value="" name="currentPassword" class="input" {{ session("userdata.isExternalAuth") ? "disabled='disabled'" : '' }}
|
||||
id="currentPassword"/><br/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="newPassword" >{{ __('label.new_password') }}</label>
|
||||
<span>
|
||||
<input type='password' value="" name="newPassword" class="input" {{ session("userdata.isExternalAuth") ? "disabled='disabled'" : '' }}
|
||||
id="newPassword"/>
|
||||
<span id="pwStrength"></span>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirmPassword" >{{ __('label.password_repeat') }}</label>
|
||||
<span>
|
||||
<input type="password" value="" name="confirmPassword" class="input" {{ session("userdata.isExternalAuth") ? "disabled='disabled'" : '' }}
|
||||
id="confirmPassword"/><br/>
|
||||
@if (!session("userdata.isExternalAuth") )
|
||||
<small>{{ __('label.passwordRequirements') }}</small>
|
||||
@endif
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@if (!session("userdata.isExternalAuth") )
|
||||
<input type="hidden" name="savepw" value="1" />
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="save" id="savePw" />
|
||||
@endif
|
||||
</form>
|
||||
<br /><br />
|
||||
<h4 class="widgettitle title-light">
|
||||
<i class="fa-solid fa-shield-halved"></i> {{ __('headlines.twoFA') }}
|
||||
</h4>
|
||||
@if ($values['twoFAEnabled'] )
|
||||
<p>{!! __('text.twoFA_enabled') !!}</p>
|
||||
@else
|
||||
<p>{!! __('text.twoFA_disabled') !!}</p>
|
||||
@endif
|
||||
<p><a href="{{ BASE_URL }}/twoFA/edit">{!! __('text.twoFA_manage') !!}</a></p>
|
||||
</div>
|
||||
|
||||
<div id="settings">
|
||||
<form action="" method="post">
|
||||
<input type="hidden" name="{{ session("formTokenName") }}" value="{{ session("formTokenValue") }}" />
|
||||
<div class="row-fluid">
|
||||
<div class="form-group">
|
||||
<label for="language" >{{ __('label.language') }}</label>
|
||||
<span class='field'>
|
||||
<select name="language" id="language" style="width: 220px">
|
||||
@foreach ($languageList as $languagKey => $languageValue )
|
||||
<option value="{{ $languagKey }}"
|
||||
@if ($userLang == $languagKey )
|
||||
selected='selected'
|
||||
@endif >{{ $languageValue }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="date_format" >{{ __('label.date_format') }}</label>
|
||||
<span>
|
||||
<select name="date_format" id="date_format" style="width: 220px">
|
||||
@php
|
||||
$dateFormats = $dateTimeValues['dates'];
|
||||
$dateTimeNow = date_create();
|
||||
@endphp
|
||||
|
||||
@foreach ($dateFormats as $format)
|
||||
|
||||
<option value="{{ $format }}"
|
||||
@if ($dateFormat == $format)
|
||||
selected='selected'
|
||||
@endif >{{ date_format($dateTimeNow, $format) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="time_format" >{{ __('label.time_format') }}</label>
|
||||
<span>
|
||||
<select name="time_format" id="time_format" style="width: 220px">
|
||||
@php
|
||||
$timeFormats = $dateTimeValues['times'];
|
||||
$dateTimeNow = date_create();
|
||||
@endphp
|
||||
|
||||
@foreach ($timeFormats as $format)
|
||||
|
||||
<option value="{{ $format }}"
|
||||
@if ($timeFormat == $format)
|
||||
selected='selected'
|
||||
@endif>{{ date_format($dateTimeNow, $format) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="timezone" >{{ __('label.timezone') }}</label>
|
||||
<span>
|
||||
<select name="timezone" id="timezone" style="width: 220px">
|
||||
|
||||
@foreach ($timezoneOptions as $tz)
|
||||
<option value="{{ $tz }}"
|
||||
@if ($timezone === $tz )
|
||||
selected='selected'
|
||||
@endif
|
||||
>{{ $tz }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="saveSettings" value="1" />
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="save" id="saveSettings" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="theme">
|
||||
<form action="" method="post">
|
||||
<input type="hidden" name="{{ session("formTokenName") }}" value="{{ session("formTokenValue") }}" />
|
||||
<div class="row-fluid">
|
||||
<div class="form-group">
|
||||
<label for="themeSelect">Optimal Stimulation</label>
|
||||
<span class='field tw-flex tw-w-80'>
|
||||
|
||||
<?php
|
||||
foreach ($availableThemes as $key => $theme) { ?>
|
||||
<x-global::selectable selected="{{ ($userTheme == $key ? 'true' : 'false') }}" :id="''" :name="'theme'" :value="$key" :label="''" class="tw-w-1/2" onclick="leantime.snippets.toggleBg('{{ $key }}')">
|
||||
<img src="{{ BASE_URL }}/dist/images/background-{{$key}}.png" style="margin:0; border-radius:10px;" />
|
||||
<br /><?= $tpl->__($theme['name']) ?>
|
||||
</x-global::selectable>
|
||||
|
||||
<?php } ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
||||
<hr />
|
||||
<label for="colormode" >{{ __('label.colormode') }}</label>
|
||||
|
||||
<x-global::selectable :selected="($userColorMode == 'light') ? 'true' : ''" :id="'light'" :name="'colormode'" :value="'light'" :label="'Light'" onclick="leantime.snippets.toggleTheme('light')">
|
||||
<label for="colormode-light" class="tw-w-[100px]">
|
||||
<i class="fa-solid fa-sun tw-font-xxl"></i>
|
||||
</label>
|
||||
</x-global::selectable>
|
||||
|
||||
<x-global::selectable :selected="($userColorMode == 'dark') ? 'true' : ''" :id="'dark'" :name="'colormode'" :value="'dark'" :label="'Dark'" onclick="leantime.snippets.toggleTheme('dark')">
|
||||
<label for="colormode-light" class="tw-w-[100px]">
|
||||
<i class="fa-solid fa-moon tw-font-xxl"></i>
|
||||
</label>
|
||||
</x-global::selectable>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<hr />
|
||||
<label>Font</label>
|
||||
@foreach($availableFonts as $key => $font)
|
||||
|
||||
<x-global::selectable :selected="($themeFont == $font) ? 'true' : ''" :id="$key" :name="'themeFont'" :value="$font" :label="$font" onclick="leantime.snippets.toggleFont('{{ $font }}')">
|
||||
<label for="selectable-{{ $key }}" class="font tw-w-[200px]"
|
||||
style="font-family:'{{ $font }}'; font-size:16px;">
|
||||
The quick brown fox jumps over the lazy dog
|
||||
</label>
|
||||
</x-global::selectable>
|
||||
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<hr />
|
||||
<label>Color Scheme</label>
|
||||
@foreach($availableColorSchemes as $key => $scheme )
|
||||
<x-global::selectable class="circle" :selected="($userColorScheme == $key) ? 'true' : ''" :id="$key" :name="'colorscheme'" :value="$key" :label="__($scheme['name'])" onclick="leantime.snippets.toggleColors('{{ $scheme['primaryColor'] }}','{{ $scheme['secondaryColor'] }}');">
|
||||
<label for="color-{{ $key }}" class="colorCircle"
|
||||
style="background:linear-gradient(135deg, {{ $scheme["primaryColor"] }} 20%, {{ $scheme["secondaryColor"] }} 100%);">
|
||||
</label>
|
||||
</x-global::selectable>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br /><br />
|
||||
<input type="hidden" name="saveTheme" value="1" />
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="save" id="saveTheme" />
|
||||
</form>
|
||||
|
||||
@dispatchEvent('themecontent')
|
||||
</div>
|
||||
|
||||
<div id="notifications">
|
||||
<form action="" method="post">
|
||||
<input type="hidden" name="{{ session("formTokenName") }}" value="{{ session("formTokenValue") }}" />
|
||||
<div class="row-fluid">
|
||||
<div class="form-group">
|
||||
<label for="notifications" style="display: inline-flex; align-items: center; gap: 8px; cursor: pointer;">
|
||||
<input type="checkbox" value="on" name="notifications" class="input"
|
||||
id="notifications"
|
||||
@if ($values['notifications'] == "1" )
|
||||
checked='checked'
|
||||
@endif/>
|
||||
{{ __('label.receive_notifications') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="messagesfrequency" >{{ __('label.messages_frequency') }}</label>
|
||||
<span>
|
||||
<select name="messagesfrequency" class="input" id="messagesfrequency" style="width: 220px">
|
||||
<option value="">--{{ __('label.choose_option') }}--</option>
|
||||
<option value="60"
|
||||
@if ($values['messagesfrequency'] == "60" )
|
||||
selected="selected"
|
||||
@endif>{{ __('label.1min') }}</option>
|
||||
<option value="300" @if ($values['messagesfrequency'] == "300" )
|
||||
selected="selected"
|
||||
@endif>{{ __('label.5min') }}</option>
|
||||
<option value="900" @if ($values['messagesfrequency'] == "900" )
|
||||
selected="selected"
|
||||
@endif>{{ __('label.15min') }}</option>
|
||||
<option value="1800" @if ($values['messagesfrequency'] == "1800" )
|
||||
selected="selected"
|
||||
@endif>{{ __('label.30min') }}</option>
|
||||
<option value="3600" @if ($values['messagesfrequency'] == "3600" )
|
||||
selected="selected"
|
||||
@endif>{{ __('label.1h') }}</option>
|
||||
<option value="10800" @if ($values['messagesfrequency'] == "10800" )
|
||||
selected="selected"
|
||||
@endif>{{ __('label.3h') }}</option>
|
||||
<option value="36000" @if ($values['messagesfrequency'] == "36000" )
|
||||
selected="selected"
|
||||
@endif>{{ __('label.6h') }}</option>
|
||||
<option value="43200" @if ($values['messagesfrequency'] == "43200" )
|
||||
selected="selected"
|
||||
@endif>{{ __('label.12h') }}</option>
|
||||
<option value="86400" @if ($values['messagesfrequency'] == "86400" )
|
||||
selected="selected"
|
||||
@endif>{{ __('label.24h') }}</option>
|
||||
<option value="172800" @if ($values['messagesfrequency'] == "172800" )
|
||||
selected="selected"
|
||||
@endif>{{ __('label.48h') }}</option>
|
||||
<option value="604800" @if ($values['messagesfrequency'] == "604800" )
|
||||
selected="selected"
|
||||
@endif>{{ __('label.1w') }}</option>
|
||||
</select> <br/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<h4 class="widgettitle title-light">{{ __('label.notification_event_types') }}</h4>
|
||||
<p><small>{{ __('label.notification_event_types_description') }}</small></p>
|
||||
<div class="tw-mb-4">
|
||||
@php
|
||||
$categoryLabels = [
|
||||
'tasks' => 'label.notification_category_tasks',
|
||||
'comments' => 'label.notification_category_comments',
|
||||
'goals' => 'label.notification_category_goals',
|
||||
'ideas' => 'label.notification_category_ideas',
|
||||
'projects' => 'label.notification_category_projects',
|
||||
'boards' => 'label.notification_category_boards',
|
||||
];
|
||||
@endphp
|
||||
@foreach ($notificationCategories as $categoryKey => $config)
|
||||
<label class="tw-flex tw-items-start tw-gap-2 tw-cursor-pointer tw-m-0 tw-py-1.5">
|
||||
<input type="checkbox"
|
||||
name="enabledEventTypes[]"
|
||||
value="{{ $categoryKey }}"
|
||||
class="input tw-mt-0.5"
|
||||
@if (in_array($categoryKey, $enabledEventTypes))
|
||||
checked="checked"
|
||||
@endif
|
||||
/>
|
||||
<span>
|
||||
<strong>{{ __($categoryLabels[$categoryKey] ?? $categoryKey) }}</strong><br />
|
||||
<small class="tw-text-gray-500">{{ __($config['description'] ?? '') }}</small>
|
||||
</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<h4 class="widgettitle title-light">{{ __('label.project_notifications') }}</h4>
|
||||
<p><small>{{ __('label.project_notifications_description') }}</small></p>
|
||||
<div class="tw-max-w-lg tw-max-h-[350px] tw-overflow-y-auto tw-mb-5">
|
||||
@if (count($userProjects) > 0)
|
||||
@foreach ($userProjects as $project)
|
||||
@php
|
||||
$currentLevel = $projectNotificationLevels[$project['id']] ?? $companyDefaultRelevance;
|
||||
@endphp
|
||||
<div class="tw-flex tw-items-center tw-justify-between tw-py-1.5 tw-border-b tw-border-gray-100">
|
||||
<span class="tw-truncate tw-mr-3">
|
||||
{{ $project['name'] }}
|
||||
@if (!empty($project['clientName']))
|
||||
<span class="tw-text-gray-400 tw-text-xs">({{ $project['clientName'] }})</span>
|
||||
@endif
|
||||
</span>
|
||||
<select name="projectNotificationLevel[{{ $project['id'] }}]"
|
||||
class="tw-text-sm tw-border tw-border-gray-300 tw-rounded tw-px-2 tw-py-1 tw-min-w-[140px]">
|
||||
@foreach ($relevanceLevels as $level => $labelKey)
|
||||
<option value="{{ $level }}"
|
||||
@if ($currentLevel === $level) selected @endif
|
||||
>{{ __($labelKey) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@endforeach
|
||||
@else
|
||||
<p class="tw-text-gray-400 tw-p-2">{{ __('label.no_projects') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="savenotifications" value="1" />
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="save" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@dispatchEvent('tabsContent')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
jQuery(document).ready(function(){
|
||||
|
||||
leantime.usersController.checkPWStrength('newPassword');
|
||||
|
||||
jQuery('.accountTabs').tabs();
|
||||
|
||||
jQuery("#messagesfrequency").chosen();
|
||||
jQuery("#language").chosen();
|
||||
jQuery("#themeSelect").chosen();
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
243
app/Domain/Users/Templates/editUser.blade.php
Normal file
243
app/Domain/Users/Templates/editUser.blade.php
Normal file
@@ -0,0 +1,243 @@
|
||||
@extends($layout)
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$projects = $relations;
|
||||
@endphp
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<div class="pageheader">
|
||||
<div class="pageicon"><span class="fa {{ $tpl->getModulePicture() }}"></span></div>
|
||||
<div class="pagetitle">
|
||||
<h5>{!! __('label.administration') !!}</h5>
|
||||
<h1>{!! __('headlines.edit_user') !!}</h1>
|
||||
</div>
|
||||
</div><!--pageheader-->
|
||||
|
||||
<form action="" method="post" class="stdform userEditModal">
|
||||
<input type="hidden" name="{{ session('formTokenName') }}" value="{{ session('formTokenValue') }}" />
|
||||
<div class="maincontent">
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="maincontentinner">
|
||||
<h4 class="widgettitle title-light">{!! __('label.profile_information') !!}</h4>
|
||||
|
||||
<label for="firstname">{!! __('label.firstname') !!}</label> <x-global::forms.text-input
|
||||
name="firstname" id="firstname"
|
||||
value="{{ $values['firstname'] }}" /><br />
|
||||
|
||||
<label for="lastname">{!! __('label.lastname') !!}</label> <x-global::forms.text-input
|
||||
name="lastname" id="lastname"
|
||||
value="{{ $values['lastname'] }}" /><br />
|
||||
|
||||
|
||||
|
||||
<label for="role">{!! __('label.role') !!}</label>
|
||||
<select name="role" id="role">
|
||||
|
||||
@foreach ($roles as $key => $role)
|
||||
{{-- Privilege ceiling (parity with newUser): a manager may never
|
||||
assign the admin(40)/owner(50) roles. Editing users is admin+
|
||||
today, so this is defense-in-depth against any future widening
|
||||
of who can reach this screen — a lower role must never be able
|
||||
to escalate an account above its own authority. --}}
|
||||
@if ($login::userHasRole(\Leantime\Domain\Auth\Models\Roles::$manager) && $key > 30)
|
||||
@continue
|
||||
@endif
|
||||
<option value="{{ $key }}"
|
||||
@if ($key == $values['role']) selected="selected" @endif>
|
||||
{!! __('label.roles.' . $role) !!}
|
||||
</option>
|
||||
@endforeach
|
||||
|
||||
</select> <br />
|
||||
|
||||
<label for="status">{!! __('label.status') !!}</label>
|
||||
<select name="status" id="status" class="pull-left">
|
||||
|
||||
<option value="a"
|
||||
@if (strtolower($values['status']) == 'a') selected="selected" @endif>
|
||||
{!! __('label.active') !!}
|
||||
</option>
|
||||
|
||||
<option value="i"
|
||||
@if (strtolower($values['status']) == 'i') selected="selected" @endif>
|
||||
{!! __('label.invited') !!}
|
||||
</option>
|
||||
|
||||
<option value="0"
|
||||
@if (strtolower($values['status']) === '' || $values['status'] === 0 || $values['status'] === '0') selected="selected" @endif>
|
||||
{!! __('label.deactivated') !!}
|
||||
</option>
|
||||
|
||||
|
||||
</select>
|
||||
@if ($values['status'] == 'i')
|
||||
<div class="pull-left dropdownWrapper" style="padding-left:5px; line-height: 29px;">
|
||||
<a class="dropdown-toggle btn btn-default" data-toggle="dropdown" href="{{ BASE_URL }}/auth/userInvite/{{ $values['pwReset'] }}"><i class="fa fa-link"></i> {!! __('label.copyinviteLink') !!}</a>
|
||||
<div class="dropdown-menu padding-md noClickProp">
|
||||
<x-global::forms.text-input id="inviteURL" value="{{ BASE_URL }}/auth/userInvite/{{ $values['pwReset'] }}" />
|
||||
<x-global::forms.button contentRole="primary" onclick="leantime.snippets.copyUrl('inviteURL');">{!! __('links.copy_url') !!}</x-global::forms.button>
|
||||
</div>
|
||||
<x-global::forms.button tag="a" link="{{ BASE_URL }}/users/editUser/{{ $values['id'] }}?resendInvite=1" contentRole="default" style="margin-left:5px;"><i class="fa fa-envelope"></i> {!! __('buttons.resend_invite') !!}</x-global::forms.button>
|
||||
</div>
|
||||
@endif
|
||||
<div class="clearfix"></div>
|
||||
|
||||
|
||||
|
||||
|
||||
<label for="client">{!! __('label.client') !!}</label>
|
||||
<select name='client' id="client">
|
||||
@if ($login::userIsAtLeast('manager'))
|
||||
<option value="0" selected="selected">{!! __('label.no_clients') !!}</option>
|
||||
@endif
|
||||
@foreach ($clients as $clientItem)
|
||||
<option value="{{ $clientItem['id'] }}" @if ($clientItem['id'] == $values['clientId']) selected="selected" @endif>{{ $clientItem['name'] }}</option>
|
||||
@endforeach
|
||||
</select><br/>
|
||||
<br/>
|
||||
|
||||
<h4 class="widgettitle title-light">{!! __('label.contact_information') !!}</h4>
|
||||
|
||||
<label for="user">{!! __('label.email') !!}</label> <x-global::forms.text-input
|
||||
name="user" id="user" value="{{ $values['user'] }}" /><br />
|
||||
|
||||
<label for="phone">{!! __('label.phone') !!}</label> <x-global::forms.text-input
|
||||
name="phone" id="phone"
|
||||
value="{{ $values['phone'] }}" /><br /><br />
|
||||
|
||||
|
||||
<h4 class="widgettitle title-light">{!! __('label.employee_information') !!}</h4>
|
||||
<label for="jobTitle">{!! __('label.jobTitle') !!}</label> <x-global::forms.text-input
|
||||
name="jobTitle" id="jobTitle" value="{{ $values['jobTitle'] }}" /><br />
|
||||
|
||||
<label for="jobLevel">{!! __('label.jobLevel') !!}</label> <x-global::forms.text-input
|
||||
name="jobLevel" id="jobLevel" value="{{ $values['jobLevel'] }}" /><br />
|
||||
|
||||
<label for="department">{!! __('label.department') !!}</label> <x-global::forms.text-input
|
||||
name="department" id="department" value="{{ $values['department'] }}" /><br />
|
||||
|
||||
|
||||
@if ($login::userIsAtLeast(\Leantime\Domain\Auth\Models\Roles::$admin))
|
||||
{{-- Capacity attributes — admin-only. Weekly hours is what
|
||||
downstream resource-planning surfaces (Resource
|
||||
Allocation, cross-program warnings, portfolio
|
||||
rollups) use to answer "how much of this person
|
||||
is available to plan against?". Employment type
|
||||
sets the semantic (target vs ceiling vs cap vs
|
||||
best-effort). Both nullable — until an admin
|
||||
sets them, capacity math treats this user as
|
||||
unconfigured and skips warnings. --}}
|
||||
<h4 class="widgettitle title-light">{!! __('label.capacity') !!}</h4>
|
||||
|
||||
<label for="weekly_hours">{!! __('label.weekly_hours') !!}</label>
|
||||
<x-global::forms.text-input
|
||||
name="weekly_hours"
|
||||
id="weekly_hours"
|
||||
type="number"
|
||||
min="0"
|
||||
max="168"
|
||||
:value="$values['weekly_hours'] ?? ''"
|
||||
placeholder="{{ __('label.weekly_hours_placeholder') }}" /><br />
|
||||
<span class="hint">{!! __('label.weekly_hours_hint') !!}</span><br /><br />
|
||||
|
||||
<label for="employment_type">{!! __('label.employment_type') !!}</label>
|
||||
<select name="employment_type" id="employment_type">
|
||||
<option value="">{!! __('label.employment_type.unset') !!}</option>
|
||||
@foreach (\Leantime\Domain\Users\Enums\EmploymentType::cases() as $type)
|
||||
<option value="{{ $type->value }}"
|
||||
@if (($values['employment_type'] ?? '') === $type->value) selected="selected" @endif>
|
||||
{!! __($type->langKey()) !!}
|
||||
</option>
|
||||
@endforeach
|
||||
</select><br />
|
||||
<span class="hint">{!! __('label.employment_type_hint') !!}</span><br /><br />
|
||||
@endif
|
||||
|
||||
|
||||
<p class="stdformbutton">
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="save" id="save" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="maincontentinner">
|
||||
<h4 class="widgettitle title-light">{!! __('label.project_assignment') !!}</h4>
|
||||
|
||||
<div class="scrollableItemList">
|
||||
@php
|
||||
$currentClient = '';
|
||||
$i = 0;
|
||||
@endphp
|
||||
@foreach ($allProjects as $row)
|
||||
@php
|
||||
if ($row['clientName'] == null) {
|
||||
$row['clientName'] = 'Not assigned to client';
|
||||
}
|
||||
@endphp
|
||||
@if ($currentClient != $row['clientName'])
|
||||
@if ($i > 0)
|
||||
</div>
|
||||
@endif
|
||||
<h3 id='accordion_link_{{ $i }}'>
|
||||
<a href='#' onclick='accordionToggle({{ $i }});' id='accordion_toggle_{{ $i }}'><i class='fa fa-angle-down'></i> {{ $row['clientName'] }}</a>
|
||||
</h3>
|
||||
<div id='accordion_{{ $i }}' class='simpleAccordionContainer'>
|
||||
@php $currentClient = $row['clientName']; @endphp
|
||||
@endif
|
||||
<div class="item" style="padding:10px 0px;">
|
||||
<input type="checkbox" name="projects[]" id='project_{{ $row['id'] }}' value="{{ $row['id'] }}"
|
||||
@if (is_array($projects) === true && in_array($row['id'], $projects) === true)
|
||||
checked='checked'
|
||||
@endif
|
||||
/>
|
||||
<span class="projectAvatar" style="width:30px; float:left; margin-right:10px;">
|
||||
<img src='{{ BASE_URL }}/api/projects?projectAvatar={{ $row['id'] }}&v={{ format($row['modified'])->timestamp() }}' />
|
||||
</span>
|
||||
|
||||
<label for="project_{{ $row['id'] }}" style="margin-top:-11px">
|
||||
<small>{{ $row['type'] }}</small><br />
|
||||
{{ $row['name'] }}</label>
|
||||
<div class="clearall"></div>
|
||||
</div>
|
||||
@php $i++; @endphp
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script>
|
||||
|
||||
jQuery(".noClickProp.dropdown-menu").on("click", function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
function accordionToggle(id) {
|
||||
|
||||
let currentLink = jQuery("#accordion_toggle_"+id).find("i.fa");
|
||||
|
||||
if(currentLink.hasClass("fa-angle-right")){
|
||||
currentLink.removeClass("fa-angle-right");
|
||||
currentLink.addClass("fa-angle-down");
|
||||
jQuery('#accordion_'+id).slideDown("fast");
|
||||
}else{
|
||||
currentLink.removeClass("fa-angle-down");
|
||||
currentLink.addClass("fa-angle-right");
|
||||
jQuery('#accordion_'+id).slideUp("fast");
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
|
||||
@endsection
|
||||
35
app/Domain/Users/Templates/importLdapDialog.blade.php
Normal file
35
app/Domain/Users/Templates/importLdapDialog.blade.php
Normal file
@@ -0,0 +1,35 @@
|
||||
@extends($layout)
|
||||
@section('content')
|
||||
|
||||
<div class="showDialogOnLoad" style="display:none;">
|
||||
|
||||
<h4 class="widgettitle title-light"><i class="fa fa-arrow-circle-o-right"></i>
|
||||
{!! __('headlines.import_ldap_users') !!}
|
||||
</h4>
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
@if ($confirmUsers)
|
||||
<form class="importModal userImportModal" method="post" action="{{ BASE_URL }}/users/import">
|
||||
@foreach ($allLdapUsers as $user)
|
||||
<input type="checkbox" value="{{ $user['user'] }}" id="{{ $user['user'] }}" name="users[]" checked="checked"/>
|
||||
<label for="{{ $user['user'] }}" style="display:inline;">{{ $user['user'] }} - {{ $user['firstname'] }}, {{ $user['lastname'] }}<br />
|
||||
@endforeach
|
||||
<br />
|
||||
<input type="hidden" name="importSubmit" value="1"/>
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.import')" />
|
||||
</form>
|
||||
|
||||
@else
|
||||
<form class="importModal userImportModal" method="post" action="{{ BASE_URL }}/users/import">
|
||||
<label>{!! __('label.please_enter_password') !!} </label>
|
||||
<x-global::forms.text-input type="password" name="password" />
|
||||
<input type="hidden" name="pwSubmit" value="1"/>
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.find_users')" />
|
||||
</form>
|
||||
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
219
app/Domain/Users/Templates/newUser.blade.php
Normal file
219
app/Domain/Users/Templates/newUser.blade.php
Normal file
@@ -0,0 +1,219 @@
|
||||
@extends($layout)
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$projects = $relations;
|
||||
@endphp
|
||||
|
||||
<h4 class="widgettitle title-light"><i class="fa fa-people-group"></i> {!! __('headlines.new_user') !!}</h4>
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<form action="{{ BASE_URL }}/users/newUser" method="post" class="stdform userEditModal formModal">
|
||||
<div class="row" style="width:800px;">
|
||||
<div class="col-md-7">
|
||||
|
||||
<h4 class="widgettitle title-light">{!! __('label.profile_information') !!}</h4>
|
||||
|
||||
<label for="firstname">{!! __('label.firstname') !!}</label> <x-global::forms.text-input
|
||||
name="firstname" id="firstname"
|
||||
value="{{ $values['firstname'] }}" /><br />
|
||||
|
||||
<label for="lastname">{!! __('label.lastname') !!}</label> <x-global::forms.text-input
|
||||
name="lastname" id="lastname"
|
||||
value="{{ $values['lastname'] }}" /><br />
|
||||
|
||||
<label for="role">{!! __('label.role') !!}</label>
|
||||
<select name="role" id="role">
|
||||
|
||||
@foreach ($roles as $key => $role)
|
||||
@if ($login::userHasRole(\Leantime\Domain\Auth\Models\Roles::$manager) && $key > 30)
|
||||
@continue
|
||||
@endif
|
||||
<option value="{{ $key }}"
|
||||
@if ($key == $values['role']) selected="selected" @endif>
|
||||
{!! __('label.roles.' . $role) !!}
|
||||
</option>
|
||||
@endforeach
|
||||
|
||||
</select> <br />
|
||||
|
||||
<label for="client">{!! __('label.client') !!}</label>
|
||||
<select name='client' id="client">
|
||||
@if ($login::userIsAtLeast('admin'))
|
||||
<option value="0" selected="selected">{!! __('label.no_clients') !!}</option>
|
||||
@endif
|
||||
@foreach ($clients as $clientItem)
|
||||
@if ($login::userHasRole(\Leantime\Domain\Auth\Models\Roles::$manager) && $clientItem['id'] !== session('userdata.clientId'))
|
||||
@continue
|
||||
@endif
|
||||
<option value="{{ $clientItem['id'] }}"
|
||||
@if ($clientItem['id'] == $values['clientId'] || $preSelectedClient == $clientItem['id']) selected="selected" @endif>{{ $clientItem['name'] }}</option>
|
||||
@endforeach
|
||||
</select><br/>
|
||||
<br/>
|
||||
|
||||
|
||||
<h4 class="widgettitle title-light">{!! __('label.contact_information') !!}</h4>
|
||||
|
||||
|
||||
<label for="user">{!! __('label.email') !!}</label> <x-global::forms.text-input
|
||||
name="user" id="user" value="{{ $values['user'] }}" /><br />
|
||||
|
||||
<label for="phone">{!! __('label.phone') !!}</label> <x-global::forms.text-input
|
||||
name="phone" id="phone"
|
||||
value="{{ $values['phone'] }}" /><br />
|
||||
<br/>
|
||||
|
||||
<h4 class="widgettitle title-light">{!! __('label.employee_information') !!}</h4>
|
||||
<label for="jobTitle">{!! __('label.jobTitle') !!}</label> <x-global::forms.text-input
|
||||
name="jobTitle" id="jobTitle" value="{{ $values['jobTitle'] }}" /><br />
|
||||
|
||||
<label for="jobLevel">{!! __('label.jobLevel') !!}</label> <x-global::forms.text-input
|
||||
name="jobLevel" id="jobLevel" value="{{ $values['jobLevel'] }}" /><br />
|
||||
|
||||
<label for="department">{!! __('label.department') !!}</label> <x-global::forms.text-input
|
||||
name="department" id="department" value="{{ $values['department'] }}" /><br />
|
||||
|
||||
|
||||
<p class="stdformbutton">
|
||||
<input type="hidden" name="save" value="1" />
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.invite_user')" name="save" id="save" />
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
|
||||
<h4 class="widgettitle title-light">{!! __('label.project_assignment') !!}</h4>
|
||||
|
||||
<div class="scrollableItemList">
|
||||
@php
|
||||
$currentClient = '';
|
||||
$i = 0;
|
||||
@endphp
|
||||
@foreach ($allProjects as $row)
|
||||
@if ($login::userHasRole(\Leantime\Domain\Auth\Models\Roles::$manager) && $row['clientId'] !== session('userdata.clientId'))
|
||||
@continue
|
||||
@endif
|
||||
|
||||
@php
|
||||
if ($row['clientName'] == '') {
|
||||
$row['clientName'] = 'Not assigned to client';
|
||||
}
|
||||
@endphp
|
||||
@if ($currentClient != $row['clientName'])
|
||||
@if ($i > 0)
|
||||
</div>
|
||||
@endif
|
||||
<h3 id='accordion_link_{{ $i }}'>
|
||||
<a href='#' onclick='accordionToggle({{ $i }});' id='accordion_toggle_{{ $i }}'><i class='fa fa-angle-down'></i> {{ $row['clientName'] }}</a>
|
||||
</h3>
|
||||
<div id='accordion_{{ $i }}' class='simpleAccordionContainer'>
|
||||
@php $currentClient = $row['clientName']; @endphp
|
||||
@endif
|
||||
<div class="item" style="padding:10px 0px;">
|
||||
<input type="checkbox" name="projects[]" id='project_{{ $row['id'] }}' value="{{ $row['id'] }}"
|
||||
@if (is_array($projects) === true && in_array($row['id'], $projects) === true)
|
||||
checked='checked'
|
||||
@endif
|
||||
/>
|
||||
<span class="projectAvatar" style="width:30px; float:left; margin-right:10px;">
|
||||
<img src='{{ BASE_URL }}/api/projects?projectAvatar={{ $row['id'] }}&v={{ format($row['modified'])->timestamp() }}' />
|
||||
</span>
|
||||
|
||||
<label for="project_{{ $row['id'] }}" style="margin-top:-11px">
|
||||
<small>{{ $row['type'] }}</small><br />
|
||||
{{ $row['name'] }}</label>
|
||||
<div class="clearall"></div>
|
||||
</div>
|
||||
@php $i++; @endphp
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
jQuery(document).ready(function(){
|
||||
// Dual Box Select
|
||||
var db = jQuery('#dualselect').find('.ds_arrow button'); //get arrows of dual select
|
||||
var sel1 = jQuery('#dualselect select#selectOrigin'); //get first select element
|
||||
var sel2 = jQuery('#dualselect select#selectDest'); //get second select element
|
||||
var projects = jQuery('#projects');
|
||||
//sel2.empty(); //empty it first from dom.
|
||||
|
||||
db.click(function(){
|
||||
|
||||
var t = (jQuery(this).hasClass('ds_prev'))? 0 : 1; // 0 if arrow prev otherwise arrow next
|
||||
|
||||
if(t) {
|
||||
|
||||
sel1.find('option').each(function(){
|
||||
|
||||
if(jQuery(this).is(':selected')) {
|
||||
|
||||
jQuery(this).attr('selected',false);
|
||||
|
||||
sel2.append(jQuery(this).clone());
|
||||
|
||||
jQuery('#projects').append(jQuery(this));
|
||||
|
||||
jQuery('#projects option').attr("selected", "selected");
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
} else {
|
||||
sel2.find('option').each(function(){
|
||||
|
||||
if(jQuery(this).is(':selected')) {
|
||||
|
||||
jQuery(this).attr('selected',false);
|
||||
index = jQuery(this).index();
|
||||
|
||||
sel1.append(jQuery(this));
|
||||
|
||||
jQuery('#projects option:eq('+index+')').remove();
|
||||
|
||||
jQuery('#projects option').attr("selected", "selected");
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function accordionToggle(id) {
|
||||
|
||||
let currentLink = jQuery("#accordion_toggle_"+id).find("i.fa");
|
||||
|
||||
if(currentLink.hasClass("fa-angle-right")){
|
||||
currentLink.removeClass("fa-angle-right");
|
||||
currentLink.addClass("fa-angle-down");
|
||||
jQuery('#accordion_'+id).slideDown("fast");
|
||||
}else{
|
||||
currentLink.removeClass("fa-angle-down");
|
||||
currentLink.addClass("fa-angle-right");
|
||||
jQuery('#accordion_'+id).slideUp("fast");
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
|
||||
@endsection
|
||||
94
app/Domain/Users/Templates/showAll.blade.php
Normal file
94
app/Domain/Users/Templates/showAll.blade.php
Normal file
@@ -0,0 +1,94 @@
|
||||
@extends($layout)
|
||||
@section('content')
|
||||
|
||||
<div class="pageheader">
|
||||
|
||||
<div class="pageicon"><span class="fa {{ $tpl->getModulePicture() }}"></span></div>
|
||||
<div class="pagetitle">
|
||||
<h5>{!! __('label.administration') !!}</h5>
|
||||
<h1>{!! __('headlines.users') !!}</h1>
|
||||
</div>
|
||||
</div><!--pageheader-->
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@can('users.create')
|
||||
<x-global::forms.button tag="a" link="{{ BASE_URL }}/users/newUser" contentRole="primary" class="userEditModal"><i class='fa fa-plus'></i> {!! __('buttons.add_user') !!} </x-global::forms.button>
|
||||
@endcan
|
||||
</div>
|
||||
<div class="col-md-6 align-right">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-bordered" id="allUsersTable">
|
||||
<colgroup>
|
||||
<col class="con1">
|
||||
<col class="con0">
|
||||
<col class="con1">
|
||||
<col class="con0">
|
||||
<col class="con1">
|
||||
<col class="con0">
|
||||
<col class="con1">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class='head1'>{!! __('label.name') !!}</th>
|
||||
<th class='head0'>{!! __('label.email') !!}</th>
|
||||
<th class='head1'>{!! __('label.client') !!}</th>
|
||||
<th class='head1'>{!! __('label.role') !!}</th>
|
||||
<th class='head1'>{!! __('label.status') !!}</th>
|
||||
<th class='head1'>{!! __('headlines.twoFA') !!}</th>
|
||||
<th class='head0 no-sort'></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($allUsers as $row)
|
||||
<tr>
|
||||
<td style="padding:6px 10px;">
|
||||
<a href="{{ BASE_URL }}/users/editUser/{{ $row['id'] }}">{!! sprintf(__('text.full_name'), e($row['firstname']), e($row['lastname'])) !!}</a>
|
||||
</td>
|
||||
<td><a href="{{ BASE_URL }}/users/editUser/{{ $row['id'] }}">{{ $row['username'] }}</a></td>
|
||||
<td>{{ $row['clientName'] }}</td>
|
||||
<td>{!! __('label.roles.' . $roles[$row['role']]) !!}</td>
|
||||
<td>@if (strtolower($row['status']) == 'a')
|
||||
{!! __('label.active') !!}
|
||||
@elseif (strtolower($row['status']) == 'i')
|
||||
{!! __('label.invited') !!}
|
||||
@else
|
||||
{!! __('label.deactivated') !!}
|
||||
@endif</td>
|
||||
<td>@if ($row['twoFAEnabled'])
|
||||
{!! __('label.yes') !!}
|
||||
@else
|
||||
{!! __('label.no') !!}
|
||||
@endif</td>
|
||||
<td>@can('users.delete')<a href="{{ BASE_URL }}/users/delUser/{{ $row['id'] }}" class="delete"><i class="fa fa-trash"></i> {!! __('links.delete') !!}</a>@endcan</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
jQuery(document).ready(function() {
|
||||
leantime.usersController.initUserTable();
|
||||
leantime.usersController._initModals();
|
||||
leantime.usersController.initUserEditModal();
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
|
||||
@endsection
|
||||
26
app/Domain/Users/routes.php
Normal file
26
app/Domain/Users/routes.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Leantime\Domain\Users\Controllers\ProfileImage;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Users Domain Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Profile images were relocated here from the retired Api\Controllers\Users.
|
||||
| The canonical route is /users/profileImage/{id}. The /api/users alias is kept
|
||||
| because ~70 <img src> references across core templates AND the plugin submodule
|
||||
| (Copilot, StrategyPro, PgmPro, Whiteboardscanvas, Llamadorian) hardcode
|
||||
| /api/users?profileImage= and cannot be rewritten from this repo. Both paths are
|
||||
| served directly (no redirect) to avoid an avatar redirect-storm on list views.
|
||||
|
|
||||
*/
|
||||
|
||||
// Canonical
|
||||
Route::get('/users/profileImage/{id?}', [ProfileImage::class, 'show'])->name('users.profileImage');
|
||||
Route::post('/users/profileImage', [ProfileImage::class, 'upload']);
|
||||
|
||||
// Backward-compat alias for the retired /api/users endpoint
|
||||
Route::get('/api/users', [ProfileImage::class, 'show'])->name('users.profileImage.legacy');
|
||||
Route::post('/api/users', [ProfileImage::class, 'upload']);
|
||||
Reference in New Issue
Block a user