User profile layout (#2402)
This commit is contained in:
@@ -37,6 +37,22 @@ td {
|
||||
}
|
||||
|
||||
/* Profile */
|
||||
.avatar-xs {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.avatar-sm {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
.avatar-md {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.avatar-lg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.box-profile {
|
||||
img.img-circle {
|
||||
width: 100px;
|
||||
|
||||
@@ -7,7 +7,7 @@ admin_lte:
|
||||
# default image for missing user avatar images
|
||||
default_avatar: build/images/default_avatar.png
|
||||
# the color skin
|
||||
skin: skin-green
|
||||
skin: skin-blue
|
||||
# if activated, the boxed_layout will be deactivated
|
||||
fixed_layout: true
|
||||
# boxed layout (true) or full-screen (false)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -8,7 +8,7 @@
|
||||
"build/app.b2c3295c.js"
|
||||
],
|
||||
"css": [
|
||||
"build/app.cafebaaa.css"
|
||||
"build/app.fe8aa88b.css"
|
||||
]
|
||||
},
|
||||
"invoice": {
|
||||
@@ -54,7 +54,7 @@
|
||||
"build/0.79dbdbb9.js": "sha384-U2Ao0ORAZ8PCeDmyRsqQFET3hc7pfUBimq0PrqFdG4/s0Bdi+qBj4TJK3o70bCd5",
|
||||
"build/1.32489d92.js": "sha384-wVkjh5FzjFhMV4S4uNP23E/OLBOf+Zi7t3lpm9eWzoMr/tm2pydT+q0Op1XHuoUP",
|
||||
"build/app.b2c3295c.js": "sha384-lqEgWnKWvGpWM7J8WsMYvPZv8+EYdwCG5T9pHtwdLerTh9wlmYyXGfFCMbc/iVS6",
|
||||
"build/app.cafebaaa.css": "sha384-gTa4tNAiWKmWXUNGKUz45SW9E2Etr68bq3XC+Y+a+VC5EOfB3s9pbNGlwp6yEinW",
|
||||
"build/app.fe8aa88b.css": "sha384-xmgEZuf0VPsvPdzU/yqK8qL6W+d+d6LJn0e0oD66LYnCxs5I44d1mp1bAtRN905Q",
|
||||
"build/invoice.74279541.js": "sha384-2BXic5Sgorf2tXai6zSAN4wLY2dbg06L03/xMKW6itMcszvtnRArKzfBh6DNcF3f",
|
||||
"build/invoice.13d8ef4e.css": "sha384-B6RN/wZJToSBCZk2JeLokIqWEhbh+Eb9arYbt9dM+YoC2Z6PnCeTwTqSGyexWWJh",
|
||||
"build/invoice-pdf.0efd7a97.js": "sha384-bSdIeRCtEJiYYuc2reb0e5CpJ1Kbd1lQNEkElMTiq1SX0IINzdwJJYf6WnCcHrNC",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"build/0.79dbdbb9.js": "build/0.79dbdbb9.js",
|
||||
"build/1.32489d92.js": "build/1.32489d92.js",
|
||||
"build/2.7ab75d0a.js": "build/2.7ab75d0a.js",
|
||||
"build/app.css": "build/app.cafebaaa.css",
|
||||
"build/app.css": "build/app.fe8aa88b.css",
|
||||
"build/app.js": "build/app.b2c3295c.js",
|
||||
"build/calendar.css": "build/calendar.1408f57e.css",
|
||||
"build/calendar.js": "build/calendar.070aab88.js",
|
||||
|
||||
@@ -61,6 +61,7 @@ final class ReportingController extends AbstractController
|
||||
|
||||
private function canSelectUser(): bool
|
||||
{
|
||||
// also found in App\EventSubscriber\Actions\UserSubscriber
|
||||
if (!$this->isGranted('view_other_timesheet')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ class DoctrineCompilerPass implements CompilerPassInterface
|
||||
*/
|
||||
private $allowedEngines = [
|
||||
'mysql' => 'mysql',
|
||||
'mysqli' => 'mysql',
|
||||
'sqlite' => 'sqlite',
|
||||
];
|
||||
|
||||
|
||||
@@ -394,7 +394,7 @@ class Configuration implements ConfigurationInterface
|
||||
->setDeprecated('The node "%node%" at path "%path%" is deprecated, please use "kimai.timesheet.active_entries.soft_limit" instead.')
|
||||
->end()
|
||||
->scalarNode('box_color')
|
||||
->defaultValue('green')
|
||||
->defaultValue('blue')
|
||||
->setDeprecated('The node "%node%" at path "%path%" was removed, please delete it from your config.')
|
||||
->end()
|
||||
->scalarNode('select_type')
|
||||
|
||||
41
src/Event/PageActionsEvent.php
Normal file
41
src/Event/PageActionsEvent.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
|
||||
class PageActionsEvent extends ThemeEvent
|
||||
{
|
||||
private $action;
|
||||
|
||||
public function __construct(User $user, array $payload, string $action)
|
||||
{
|
||||
if (!\array_key_exists('actions', $payload)) {
|
||||
$payload['actions'] = [];
|
||||
}
|
||||
parent::__construct($user, $payload);
|
||||
$this->action = $action;
|
||||
}
|
||||
|
||||
public function getActionName(): string
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
public function getActions(): array
|
||||
{
|
||||
return $this->payload['actions'];
|
||||
}
|
||||
|
||||
public function setActions(array $actions): void
|
||||
{
|
||||
$this->payload['actions'] = $actions;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace App\Event;
|
||||
use App\Entity\User;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class ThemeEvent extends Event
|
||||
class ThemeEvent extends Event
|
||||
{
|
||||
public const JAVASCRIPT = 'app.theme.javascript';
|
||||
public const STYLESHEET = 'app.theme.css';
|
||||
@@ -25,15 +25,15 @@ final class ThemeEvent extends Event
|
||||
/**
|
||||
* @var User|null
|
||||
*/
|
||||
protected $user;
|
||||
private $user;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $content = '';
|
||||
private $content = '';
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
protected $payload = null;
|
||||
protected $payload;
|
||||
|
||||
public function __construct(?User $user = null, $payload = null)
|
||||
{
|
||||
@@ -41,13 +41,8 @@ final class ThemeEvent extends Event
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 1.0
|
||||
*/
|
||||
public function getUser(): ?User
|
||||
{
|
||||
@trigger_error('Using ThemeEvent::getUser() is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
|
||||
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
|
||||
36
src/EventSubscriber/Actions/AbstractActionsSubscriber.php
Normal file
36
src/EventSubscriber/Actions/AbstractActionsSubscriber.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\EventSubscriber\Actions;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
abstract class AbstractActionsSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private $auth;
|
||||
private $urlGenerator;
|
||||
|
||||
public function __construct(AuthorizationCheckerInterface $security, UrlGeneratorInterface $urlGenerator)
|
||||
{
|
||||
$this->auth = $security;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
}
|
||||
|
||||
protected function isGranted($attributes, $subject = null): bool
|
||||
{
|
||||
return $this->auth->isGranted($attributes, $subject);
|
||||
}
|
||||
|
||||
protected function path(string $route, array $parameters = []): string
|
||||
{
|
||||
return $this->urlGenerator->generate($route, $parameters);
|
||||
}
|
||||
}
|
||||
95
src/EventSubscriber/Actions/UserSubscriber.php
Normal file
95
src/EventSubscriber/Actions/UserSubscriber.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\EventSubscriber\Actions;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\PageActionsEvent;
|
||||
|
||||
class UserSubscriber extends AbstractActionsSubscriber
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
'actions.user' => ['onActions', 1000],
|
||||
];
|
||||
}
|
||||
|
||||
public function onActions(PageActionsEvent $event)
|
||||
{
|
||||
$payload = $event->getPayload();
|
||||
|
||||
if (!isset($payload['user'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $payload['user'];
|
||||
|
||||
if ($user->getId() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actions = $event->getActions();
|
||||
|
||||
if ($this->isGranted('view', $user)) {
|
||||
$actions['profile-stats'] = ['icon' => 'avatar', 'url' => $this->path('user_profile', ['username' => $user->getUsername()]), 'translation_domain' => 'actions'];
|
||||
}
|
||||
|
||||
if (\count($actions) > 0) {
|
||||
$actions['divider'] = null;
|
||||
}
|
||||
|
||||
$subActions = [];
|
||||
if ($this->isGranted('edit', $user)) {
|
||||
$subActions['edit'] = ['url' => $this->path('user_profile_edit', ['username' => $user->getUsername()]), 'title' => 'edit', 'translation_domain' => 'actions'];
|
||||
}
|
||||
if ($this->isGranted('preferences', $user)) {
|
||||
$subActions['settings'] = ['url' => $this->path('user_profile_preferences', ['username' => $user->getUsername()]), 'title' => 'settings', 'translation_domain' => 'actions'];
|
||||
}
|
||||
if ($this->isGranted('password', $user)) {
|
||||
$subActions['password'] = ['url' => $this->path('user_profile_password', ['username' => $user->getUsername()]), 'title' => 'profile.password'];
|
||||
}
|
||||
if ($this->isGranted('api-token', $user)) {
|
||||
$subActions['api-token'] = ['url' => $this->path('user_profile_api_token', ['username' => $user->getUsername()]), 'title' => 'profile.api-token'];
|
||||
}
|
||||
if ($this->isGranted('teams', $user)) {
|
||||
$subActions['teams'] = ['url' => $this->path('user_profile_teams', ['username' => $user->getUsername()]), 'title' => 'profile.teams'];
|
||||
}
|
||||
if ($this->isGranted('roles', $user)) {
|
||||
$subActions['roles'] = ['url' => $this->path('user_profile_roles', ['username' => $user->getUsername()]), 'title' => 'profile.roles'];
|
||||
}
|
||||
|
||||
if (\count($subActions) > 0) {
|
||||
$actions['edit'] = ['children' => $subActions, 'title' => 'edit'];
|
||||
$actions['divider2'] = null;
|
||||
}
|
||||
|
||||
$viewOther = $this->isGranted('view_other_timesheet');
|
||||
if ($this->isGranted('view_reporting')) {
|
||||
if ($viewOther || ($event->getUser()->getId() === $user->getId())) {
|
||||
$actions['menu.reporting'] = ['url' => $this->path('report_user_month', ['user' => $user->getId()]), 'icon' => 'reporting'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($viewOther && $user->isEnabled()) {
|
||||
$actions['timesheet'] = $this->path('admin_timesheet', ['users[]' => $user->getId()]);
|
||||
}
|
||||
|
||||
$view = $payload['view'] ?? null;
|
||||
|
||||
if ($view === 'index' && $this->isGranted('delete', $user)) {
|
||||
$actions['trash'] = ['url' => $this->path('admin_user_delete', ['id' => $user->getId()]), 'class' => 'modal-ajax-form'];
|
||||
}
|
||||
|
||||
$payload['actions'] = array_merge($payload['actions'], $actions);
|
||||
|
||||
$event->setPayload($payload);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ final class IconExtension extends AbstractExtension
|
||||
'audit' => 'fas fa-history',
|
||||
'avatar' => 'fas fa-user',
|
||||
'back' => 'fas fa-long-arrow-alt-left',
|
||||
'barcode' => 'fas fa-barcode',
|
||||
'calendar' => 'far fa-calendar-alt',
|
||||
'clock' => 'far fa-clock',
|
||||
'comment' => 'far fa-comment',
|
||||
@@ -51,6 +52,7 @@ final class IconExtension extends AbstractExtension
|
||||
'left' => 'fas fa-chevron-left',
|
||||
'list' => 'fas fa-list',
|
||||
'locked' => 'fas fa-lock',
|
||||
'login' => 'fas fa-sign-in-alt',
|
||||
'logout' => 'fas fa-sign-out-alt',
|
||||
'mail' => 'fas fa-envelope-open',
|
||||
'mail-sent' => 'fas fa-paper-plane',
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Twig\Runtime;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\PageActionsEvent;
|
||||
use App\Event\ThemeEvent;
|
||||
use App\Event\ThemeJavascriptTranslationsEvent;
|
||||
use Symfony\Bridge\Twig\AppVariable;
|
||||
@@ -51,6 +52,23 @@ final class ThemeExtension implements RuntimeExtensionInterface
|
||||
return $themeEvent;
|
||||
}
|
||||
|
||||
public function actions(User $user, string $action, array $payload): ThemeEvent
|
||||
{
|
||||
if (!\array_key_exists('actions', $payload)) {
|
||||
$payload['actions'] = [];
|
||||
}
|
||||
|
||||
$themeEvent = new PageActionsEvent($user, $payload, $action);
|
||||
|
||||
$eventName = 'actions.' . $action;
|
||||
|
||||
if ($this->eventDispatcher->hasListeners($eventName)) {
|
||||
$this->eventDispatcher->dispatch($themeEvent, $eventName);
|
||||
}
|
||||
|
||||
return $themeEvent;
|
||||
}
|
||||
|
||||
public function getJavascriptTranslations(): array
|
||||
{
|
||||
$event = new ThemeJavascriptTranslationsEvent();
|
||||
|
||||
@@ -28,6 +28,7 @@ class RuntimeExtensions extends AbstractExtension
|
||||
{
|
||||
return [
|
||||
new TwigFunction('trigger', [ThemeExtension::class, 'trigger'], ['needs_environment' => true]),
|
||||
new TwigFunction('actions', [ThemeExtension::class, 'actions']),
|
||||
new TwigFunction('javascript_translations', [ThemeExtension::class, 'getJavascriptTranslations']),
|
||||
new TwigFunction('timesheet_exporter', [ExporterExtension::class, 'getTimesheetExporter']),
|
||||
new TwigFunction('active_timesheets', [TimesheetExtension::class, 'activeEntries']),
|
||||
|
||||
@@ -3,28 +3,26 @@
|
||||
{% endmacro %}
|
||||
|
||||
{%- macro page_actions(tools) -%}
|
||||
{% import _self as macro %}
|
||||
<div class="breadcrumb">
|
||||
<div class="box-tools">
|
||||
{{ macro.button_group(tools) }}
|
||||
{{ _self.button_group(tools) }}
|
||||
</div>
|
||||
</div>
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro entity_actions(tools) -%}
|
||||
{% import _self as macro %}
|
||||
<div class="breadcrumb">
|
||||
<div class="box-tools">
|
||||
<div class="btn-group">
|
||||
{% set actions = {} %}
|
||||
{%- for icon, values in tools %}
|
||||
{% if icon == 'back' %}
|
||||
{{ macro.action_button(icon, values) }}
|
||||
{{ _self.action_button(icon, values) }}
|
||||
{% else %}
|
||||
{% set actions = actions|merge({(icon): values}) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ macro.table_actions(actions, '') }}
|
||||
{{ _self.table_actions(actions, '') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,21 +33,18 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro label_boolean(visible) %}
|
||||
{% import _self as macro %}
|
||||
{% if visible %}
|
||||
{{ macro.label('yes'|trans, 'success') }}
|
||||
{{ _self.label('yes'|trans, 'success') }}
|
||||
{% else %}
|
||||
{{ macro.label('no'|trans, 'default') }}
|
||||
{{ _self.label('no'|trans, 'default') }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro label_visible(visible) %}
|
||||
{% import _self as macro %}
|
||||
{{ macro.label_boolean(visible) }}
|
||||
{{ _self.label_boolean(visible) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro label_role(role) %}
|
||||
{% import _self as macro %}
|
||||
{% set color = 'primary' %}
|
||||
{% if role == 'ROLE_SUPER_ADMIN' %}
|
||||
{% set color = 'danger' %}
|
||||
@@ -60,7 +55,7 @@
|
||||
{% elseif role == 'ROLE_USER' %}
|
||||
{% set color = 'gray' %}
|
||||
{% endif %}
|
||||
{{ macro.label(role|trans, color) }}
|
||||
{{ _self.label(role|trans, color) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro username(user) %}
|
||||
@@ -68,13 +63,11 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro label_user(user) %}
|
||||
{% import _self as macro %}
|
||||
{{ macro.label(user.displayName, 'primary') }}
|
||||
{{ _self.label(user.displayName, 'primary') }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro label_team(team, class) %}
|
||||
{% import _self as macro %}
|
||||
{{ macro.label(team.name, class|default('primary')) }}
|
||||
{{ _self.label(team.name, class|default('primary')) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro user_avatar(user, tooltip, class) %}
|
||||
@@ -87,7 +80,6 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro label_activity(activity, url) %}
|
||||
{% import _self as macro %}
|
||||
{% set isVisible = activity.visible %}
|
||||
{% set color = activity.color %}
|
||||
{% if color is empty and activity.project is not null %}
|
||||
@@ -99,22 +91,19 @@
|
||||
{% set isVisible = activity.project.customer.visible %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ macro.label_color_dot('activity', isVisible, activity.name, url, color) }}
|
||||
{{ _self.label_color_dot('activity', isVisible, activity.name, url, color) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro label_project(project, url) %}
|
||||
{% import _self as macro %}
|
||||
{% set isVisible = false %}
|
||||
{% if project.visible and project.customer.visible %}
|
||||
{% set isVisible = true %}
|
||||
{% endif %}
|
||||
{{ macro.label_color_dot('project', isVisible, project.name, url, (project.color ?? project.customer.color)) }}
|
||||
{{ _self.label_color_dot('project', isVisible, project.name, url, (project.color ?? project.customer.color)) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro label_customer(customer, url) %}
|
||||
{% import _self as macro %}
|
||||
{% set isVisible = customer.visible %}
|
||||
{{ macro.label_color_dot('customer', isVisible, customer.name, url, customer.color) }}
|
||||
{{ _self.label_color_dot('customer', customer.visible, customer.name, url, customer.color) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro label_color_dot(type, isVisible, name, url, color) %}
|
||||
@@ -137,11 +126,10 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro badge_team_access(teams) %}
|
||||
{% import _self as macro %}
|
||||
{% if teams|length > 0 %}
|
||||
{{ macro.badge_counter(teams|length) }}
|
||||
{{ _self.badge_counter(teams|length) }}
|
||||
{% else %}
|
||||
{{ macro.icon('unlocked') }}
|
||||
{{ _self.icon('unlocked') }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -218,7 +206,6 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro table_actions(actions, class) %}
|
||||
{%- import _self as macro -%}
|
||||
{% if actions|length >= 1 %}
|
||||
{% if class is null %}
|
||||
{% set class = 'btn-sm' %}
|
||||
@@ -249,9 +236,17 @@
|
||||
{% set trash = values %}
|
||||
{% else %}
|
||||
{% set divider = false %}
|
||||
<li>
|
||||
{{ macro.action_button(icon, values, false) }}
|
||||
</li>
|
||||
{% if values.children is defined %}
|
||||
{%- for childIcon, childValues in values.children %}
|
||||
<li>
|
||||
{{ _self.action_button(childIcon, childValues, false) }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li>
|
||||
{{ _self.action_button(icon, values, false) }}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor -%}
|
||||
@@ -260,7 +255,7 @@
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li class="delete">
|
||||
{{ macro.action_button('trash', trash, false) }}
|
||||
{{ _self.action_button('trash', trash, false) }}
|
||||
</li>
|
||||
{% endif -%}
|
||||
{% endapply %}
|
||||
@@ -269,8 +264,37 @@
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro list_group_actions(actions, highlightUrl) %}
|
||||
{% if actions|length > 0 %}
|
||||
<div class="list-group">
|
||||
{% for icon, values in actions %}
|
||||
{% if 'divider' in icon and values is null %}
|
||||
</div><div class="list-group">
|
||||
{# <a class="list-group-item disabled list-group-divider"></a> #}
|
||||
{% else %}
|
||||
{% if values is not iterable %}
|
||||
{% set values = {'url': values} %}
|
||||
{% endif %}
|
||||
{% if values['children'] is defined %}
|
||||
{{ _self.list_group_actions(values['children'], highlightUrl) }}
|
||||
{% else %}
|
||||
{% if values['title'] is not defined %}
|
||||
{% set values = values|merge({'title': icon|trans({}, 'actions')}) %}
|
||||
{% endif %}
|
||||
{% set class = 'list-group-item' %}
|
||||
{% if highlightUrl == values.url %}
|
||||
{% set class = class ~ ' active' %}
|
||||
{% endif %}
|
||||
{% set values = values|merge({'class': class}) %}
|
||||
{{ _self.action_button(icon, values, false) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro action_button(icon, values, type) %}
|
||||
{%- import _self as macro -%}
|
||||
{%- apply spaceless -%}
|
||||
{% set id = null %}
|
||||
{% set onclick = null %}
|
||||
@@ -281,6 +305,7 @@
|
||||
{% set title = null %}
|
||||
{% set disabled = false %}
|
||||
{% set attr = {} %}
|
||||
{% set translation_domain = 'messages' %}
|
||||
{% if type is same as (false) %}
|
||||
{% set class = "" %}
|
||||
{% elseif type is null %}
|
||||
@@ -289,7 +314,7 @@
|
||||
{% set class = "btn btn-" ~ type ~ " btn-" ~ icon ~ " " %}
|
||||
{% endif %}
|
||||
|
||||
{% if not values is iterable %}
|
||||
{% if values is not iterable %}
|
||||
{% set url = values %}
|
||||
{% if 'onclick:' in url %}
|
||||
{% set onclick = url|replace({'onclick:': ''}) %}
|
||||
@@ -314,6 +339,7 @@
|
||||
{% set title = values.title ?? null %}
|
||||
{% set class = class ~ ( values.class | default("")) %}
|
||||
{% set attr = values.attr ?? {} %}
|
||||
{% set translation_domain = values.translation_domain ?? translation_domain %}
|
||||
{% endif %}
|
||||
|
||||
{% if disabled is same as (true) %}
|
||||
@@ -332,30 +358,33 @@
|
||||
{{ ' ' ~ name }}={% if '"' in value %}'{{ value|raw }}'{% else %}"{{ value|raw }}"{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>{% if title is not null %}{{ title }}{% else %}{{ macro.icon(icon) }}{% endif %}</a>
|
||||
>{% if title is not null %}{{ title|trans({}, translation_domain) }}{% else %}{{ _self.icon(icon) }}{% endif %}</a>
|
||||
{% endapply %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro button_group(actions, type) %}
|
||||
{%- import _self as macro -%}
|
||||
<div class="btn-group">
|
||||
{%- apply spaceless -%}
|
||||
{%- for icon,values in actions %}
|
||||
{% if values.children is defined %}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
{{ macro.icon(icon) }}
|
||||
<span class="caret"></span>
|
||||
<span class="sr-only">{{ 'label.toggle_dropdown'|trans }}</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
{% for childIcon,childValues in values.children %}
|
||||
<li>{{ macro.action_button(childIcon, childValues, false) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% if 'divider' in icon and values is null %}
|
||||
{# what to do here ? #}
|
||||
{% else %}
|
||||
{{ macro.action_button(icon, values, type) }}
|
||||
{% if values.children is defined %}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
{{ _self.icon(icon) }}
|
||||
<span class="caret"></span>
|
||||
<span class="sr-only">{{ 'label.toggle_dropdown'|trans }}</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
{% for childIcon,childValues in values.children %}
|
||||
<li>{{ _self.action_button(childIcon, childValues, false) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ _self.action_button(icon, values, type) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor -%}
|
||||
{% endapply %}
|
||||
@@ -416,7 +445,8 @@
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro team_list(teams, showTitle) %}
|
||||
{% macro team_list(teams, showTitle, collapseAt) %}
|
||||
{% set collapseAt = collapseAt ?? 5 %}
|
||||
{% if showTitle is null %}
|
||||
{% set showTitle = true %}
|
||||
{% endif %}
|
||||
@@ -447,12 +477,12 @@
|
||||
{{ macro.user_avatar(user) }}
|
||||
{% set counter = counter + 1 %}
|
||||
{% endif %}
|
||||
{% if userTeamCount > 5 and counter == 4 and loop.index != userTeamCount %}
|
||||
<a href="#" onclick="$('#{{ teamHiddenId }}').toggleClass('hidden');$(this).hide();return false;" class="badge">{{ 'label.plus_more'|trans({'%count%': (userTeamCount - 5)}) }}</a>
|
||||
{% if userTeamCount > collapseAt and counter == (collapseAt - 1) and loop.index != userTeamCount %}
|
||||
<a href="#" onclick="$('#{{ teamHiddenId }}').toggleClass('hidden');$(this).hide();return false;" class="badge">{{ 'label.plus_more'|trans({'%count%': (userTeamCount - collapseAt)}) }}</a>
|
||||
<span class="hidden" id="{{ teamHiddenId }}">
|
||||
{% set counter = counter + 1 %}
|
||||
{% endif %}
|
||||
{% if userTeamCount > 5 and counter != 4 and loop.index == userTeamCount %}
|
||||
{% if userTeamCount > collapseAt and counter != (collapseAt - 1) and loop.index == userTeamCount %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -28,80 +28,17 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro user_permissions(view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% set actions = {} %}
|
||||
|
||||
{% if view != 'index' and is_granted('role_permissions') %}
|
||||
{% set actions = actions|merge({'permissions': path('admin_user_permissions')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if view != 'role' and is_granted('role_permissions') %}
|
||||
{% set actions = actions|merge({'create': {'url': path('admin_user_roles'), 'class': 'modal-ajax-form'}}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set actions = actions|merge({'help': {'url': 'permissions.html'|docu_link, 'target': '_blank'}}) %}
|
||||
|
||||
{% set event = trigger('actions.user_permissions', {'actions': actions, 'view': view}) %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% deprecated 'The "user_permissions" macro from user/action.html.twig is deprecated since 1.14 and will be removed with 2.0. Use "permission/actions.html.twig" instead.' %}
|
||||
{% import "permission/actions.html.twig" as realMacro %}
|
||||
{{ realMacro.user_permissions(view) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro user(user, view, options) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% set actions = {} %}
|
||||
|
||||
{% if user.id is not empty %}
|
||||
{% set view_other = is_granted('view_other_timesheet') %}
|
||||
{% if is_granted('view', user) %}
|
||||
{% set actions = actions|merge({'profile-stats': {'url': path('user_profile', {'username' : user.username})}}) %}
|
||||
{% endif %}
|
||||
{% if actions|length > 0 %}
|
||||
{% set actions = actions|merge({'divider': null}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set subActions = {} %}
|
||||
{% if is_granted('edit', user) %}
|
||||
{% set subActions = subActions|merge({'edit': path('user_profile_edit', {'username' : user.username})}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('preferences', user) %}
|
||||
{% set subActions = subActions|merge({'settings': {'url': path('user_profile_preferences', {'username' : user.username})}}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('password', user) %}
|
||||
{% set subActions = subActions|merge({'password': {'url': path('user_profile_password', {'username' : user.username}), 'title': ('profile.password'|trans)}}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('api-token', user) %}
|
||||
{% set subActions = subActions|merge({'api-token': {'url': path('user_profile_api_token', {'username' : user.username}), 'title': ('profile.api-token'|trans)}}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('teams', user) %}
|
||||
{% set subActions = subActions|merge({'teams': {'url': path('user_profile_teams', {'username' : user.username}), 'title': ('profile.teams'|trans)}}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('roles', user) %}
|
||||
{% set subActions = subActions|merge({'roles': {'url': path('user_profile_roles', {'username' : user.username}), 'title': ('profile.roles'|trans)}}) %}
|
||||
{% endif %}
|
||||
{% if subActions|length > 0 %}
|
||||
{% set actions = actions|merge(subActions) %}
|
||||
{% set actions = actions|merge({'divider2': null}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('view_reporting') %}
|
||||
{% if view_other or app.user.id == user.id %}
|
||||
{% set actions = actions|merge({'report': path('report_user_month', {'user': user.id})}) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if view_other and user.enabled %}
|
||||
{% set actions = actions|merge({'timesheet': path('admin_timesheet', {'users': [user.id]})}) %}
|
||||
{% endif %}
|
||||
{% if view == 'index' and is_granted('delete', user) %}
|
||||
{% set actions = actions|merge({'trash': {'url': path('admin_user_delete', {'id': user.id}), 'class': 'modal-ajax-form'}}) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if options.back is defined %}
|
||||
{% set actions = actions|merge({'back': options.back}) %}
|
||||
{% endif %}
|
||||
{% set event = trigger('actions.user', {'actions': actions, 'view': view, 'user': user}) %}
|
||||
{% set event = actions(app.user, 'user', {'view': view, 'user': user}) %}
|
||||
{% if view == 'index' %}
|
||||
{{ widgets.table_actions(event.payload.actions) }}
|
||||
{{ widgets.table_actions(event.actions) }}
|
||||
{% else %}
|
||||
{{ widgets.entity_actions(event.payload.actions) }}
|
||||
{{ widgets.page_actions(event.actions) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends 'user/layout.html.twig' %}
|
||||
|
||||
{% block main %}
|
||||
{% block profile_content %}
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends 'user/layout.html.twig' %}
|
||||
|
||||
{% block main %}
|
||||
{% block profile_content %}
|
||||
|
||||
{{ form_start(form) }}
|
||||
{% for section in sections %}
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
{% import "user/actions.html.twig" as actions %}
|
||||
|
||||
{% set columns = {
|
||||
'avatar': {'class': 'alwaysVisible w-min', 'title': null, 'orderBy': false},
|
||||
'alias': {'class': 'alwaysVisible'},
|
||||
'title': {'class': 'hidden-xs hidden-sm'},
|
||||
'title': {'class': 'hidden-xs hidden-sm hidden'},
|
||||
'email': {'class': 'hidden-xs hidden-sm hidden', 'orderBy': false},
|
||||
'lastLogin': {'class': 'hidden-xs hidden-sm hidden', 'orderBy': false},
|
||||
'roles': {'class': 'hidden-sm', 'orderBy': false},
|
||||
@@ -27,9 +28,10 @@
|
||||
{% block page_title %}{{ 'admin_user.title'|trans }}{% endblock %}
|
||||
{% block page_search %}{{ toolbar.dropDownSearch(toolbarForm) }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.users('index') }}{% endblock %}
|
||||
{% block page_icon %}{{ 'user'|icon }}{% endblock %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ tables.data_table_column_modal(tableName, columns) }}
|
||||
{{ tables.data_table_column_modal(tableName, columns, 'kimai.userUpdate') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
@@ -41,7 +43,8 @@
|
||||
|
||||
{% for entry in entries %}
|
||||
<tr{% if is_granted('view', entry) %} class="open-edit alternative-link" data-href="{{ path('user_profile', {'username': entry.username}) }}"{% endif %}>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'alias') }}">{{ widgets.user_avatar(entry) }} {{ widgets.username(entry) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'avatar') }}">{{ widgets.user_avatar(entry) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'alias') }}">{{ widgets.username(entry) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'title') }}">{{ entry.title }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'email') }}">{{ entry.email }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'lastLogin') }}">{% if entry.lastLogin is not null %}{{ entry.lastLogin|date_full }}{% endif %}</td>
|
||||
|
||||
@@ -1,111 +1,31 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "user/actions.html.twig" as actions %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% block page_title %}{% if not user.alias is empty %}{{ user.alias }}{% else %}{{ user.username }}{% endif %}{% endblock %}
|
||||
{% block page_subtitle %}{% if not user.alias is empty %}{{ user.username }}{% endif %}{% endblock %}
|
||||
{% block page_title %}{{ 'admin_user.title'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.user(user, tab) }}{% endblock %}
|
||||
{% block page_icon %}{{ 'user'|icon }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="col-xs-12 col-sm-9 col-md-9 col-lg-10">
|
||||
{% block profile_intro %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
{{ widgets.user_avatar(user, false, 'avatar-md') }}
|
||||
{{ user.displayName }}
|
||||
{% if user.title is not empty %} – {{ user.title }} {% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block profile_content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_body_class %}box-profile{% endblock %}
|
||||
{% block box_body %}
|
||||
<div class="text-center">
|
||||
{{ widgets.user_avatar(user) }}
|
||||
<h3 class="profile-username">{{ widgets.username(user) }}</h3>
|
||||
<p class="text-muted">{{ user.title }}</p>
|
||||
</div>
|
||||
{% set seeOwnRate = is_granted('view_rate_own_timesheet') %}
|
||||
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>{{ 'stats.durationMonth'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ stats.durationThisMonth|duration }}</td>
|
||||
</tr>
|
||||
{% if seeOwnRate %}
|
||||
<tr>
|
||||
<th>{{ 'stats.amountMonth'|trans }}</th>
|
||||
{# TODO which currency shall we use here? #}
|
||||
<td class="text-nowrap pull-right">{{ stats.amountThisMonth|money }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>{{ 'stats.durationTotal'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ stats.durationTotal|duration }}</td>
|
||||
</tr>
|
||||
{% if seeOwnRate %}
|
||||
<tr>
|
||||
<th>{{ 'stats.amountTotal'|trans }}</th>
|
||||
{# TODO which currency shall we use here? #}
|
||||
<td class="text-nowrap pull-right">{{ stats.amountTotal|money }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% block box_title %}{{ 'profile.about_me'|trans }}{% endblock %}
|
||||
{% block box_body %}
|
||||
{# colors = purple, blue, aqua, red, green #}
|
||||
<table class="table no-border">
|
||||
<tr>
|
||||
<th>{{ 'label.id'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ user.id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'label.username'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ user.username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'profile.first_entry'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ stats.firstEntry|date_short }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'profile.registration_date'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ user.registeredAt|date_short }}</td>
|
||||
</tr>
|
||||
{% if is_granted('hourly-rate', user) %}
|
||||
<tr>
|
||||
<th>{{ 'label.hourlyRate'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ user.preferenceValue('hourly_rate') }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{% if user.teams is not empty and (is_granted('teams', user) or (app.user == user and is_granted('view_team_member'))) %}
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_title %}{{ 'label.my_teams'|trans }}{% endblock %}
|
||||
{% block box_tools %}
|
||||
{% if is_granted('roles', user) %}
|
||||
<a class="btn-box-tool" href="{{ path('user_profile_teams', {'username': user.username}) }}"><i class="{{ 'edit'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block box_body %}
|
||||
{{ widgets.team_list(user.teams) }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
{% endif %}
|
||||
|
||||
<div class="col-xs-12 col-sm-3 col-md-3 col-lg-2">
|
||||
<div class="hidden-xs">
|
||||
{% set event = actions(app.user, 'user', {'view': tab, 'user': user}) %}
|
||||
{{ widgets.list_group_actions(event.actions, app.request.requestUri) }}
|
||||
</div>
|
||||
{% block profile_navbar %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.initialized', function() {
|
||||
KimaiReloadPageWidget.create('kimai.teamUpdate', true);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends 'user/layout.html.twig' %}
|
||||
|
||||
{% block main %}
|
||||
{% block profile_content %}
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
@@ -11,8 +11,16 @@
|
||||
{{ encore_entry_script_tags('chart') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block profile_navbar %}
|
||||
{{ _self.about_me(user, stats) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block profile_intro %}{% endblock %}
|
||||
|
||||
{% block profile_content %}
|
||||
|
||||
{{ _self.profile_box_horizontal(user, stats) }}
|
||||
|
||||
{% if years is empty %}
|
||||
{{ widgets.nothing_found() }}
|
||||
{% endif %}
|
||||
@@ -49,14 +57,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
{%- if is_granted('view_reporting') and (app.user.id == user.id or is_granted('view_other_timesheet')) -%}
|
||||
{%- if user.enabled and is_granted('view_reporting') and (app.user.id == user.id or is_granted('view_other_timesheet')) -%}
|
||||
,
|
||||
onClick: function(event, elements) {
|
||||
var element = elements[0];
|
||||
var month = this.data.datasets[0].monthData[element._index];
|
||||
var formattedMonth = moment(month).format('{{ stat_date_format }}');
|
||||
var reportUrl = '{{ path('report_user_month', {'user': user.id, 'date': 'XXXXX'})|raw }}'.replace('XXXXX', formattedMonth);
|
||||
document.location = reportUrl;
|
||||
document.location = '{{ path('report_user_month', {'user': user.id, 'date': 'XXXXX'})|raw }}'.replace('XXXXX', formattedMonth);
|
||||
}
|
||||
{% endif %}
|
||||
};
|
||||
@@ -116,4 +123,147 @@
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% if user.teams is not empty and (is_granted('teams', user) or (app.user == user and is_granted('view_team_member'))) %}
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_title %}{{ 'label.my_teams'|trans }}{% endblock %}
|
||||
{% block box_body %}
|
||||
{{ widgets.team_list(user.teams) }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.initialized', function() {
|
||||
KimaiReloadPageWidget.create('kimai.teamUpdate', true);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% macro about_me(user, stats) %}
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_title %}
|
||||
<a href="mailto:{{ user.email }}" title="{{ 'label.email'|trans }}">{{ widgets.icon('mail') }}</a>
|
||||
<span data-toggle="tooltip" title="{{ 'label.id'|trans }}: {{ user.id }}">{{ 'profile.about_me'|trans }}</span>
|
||||
{% endblock %}
|
||||
{% block box_body %}
|
||||
<p>
|
||||
<strong>{{ 'profile.first_entry'|trans }}</strong><br>
|
||||
{# FIXME use a configuration for it #}
|
||||
{{ stats.firstEntry|date_short }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ 'profile.registration_date'|trans }}</strong><br>
|
||||
{{ user.registeredAt|date_short }}
|
||||
</p>
|
||||
{% if is_granted('hourly-rate', user) and user.preferenceValue('hourly_rate') is not null %}
|
||||
<p>
|
||||
<strong>{{ 'label.hourlyRate'|trans }}</strong><br>
|
||||
{{ user.preferenceValue('hourly_rate') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro profile_box_vertical(user, stats) %}
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_body_class %}box-profile box-user-profile{% endblock %}
|
||||
{% block box_body %}
|
||||
<div class="text-center">
|
||||
{{ widgets.user_avatar(user) }}
|
||||
<h3 class="profile-username">{{ widgets.username(user) }}</h3>
|
||||
<p class="text-muted">{{ user.title }}</p>
|
||||
</div>
|
||||
{% set seeOwnRate = is_granted('view_rate_own_timesheet') %}
|
||||
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>{{ 'stats.durationMonth'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ stats.durationThisMonth|duration }}</td>
|
||||
</tr>
|
||||
{% if seeOwnRate %}
|
||||
<tr>
|
||||
<th>{{ 'stats.amountMonth'|trans }}</th>
|
||||
{# TODO which currency shall we use here? #}
|
||||
<td class="text-nowrap pull-right">{{ stats.amountThisMonth|money }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>{{ 'stats.durationTotal'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ stats.durationTotal|duration }}</td>
|
||||
</tr>
|
||||
{% if seeOwnRate %}
|
||||
<tr>
|
||||
<th>{{ 'stats.amountTotal'|trans }}</th>
|
||||
{# TODO which currency shall we use here? #}
|
||||
<td class="text-nowrap pull-right">{{ stats.amountTotal|money }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro profile_box_horizontal(user, stats) %}
|
||||
{% import "@AdminLTE/Macros/default.html.twig" as macro %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% set color = kimai_context.chart.background_color %}
|
||||
{% set event = trigger('user.profile_background', {'color': color, 'font': color|font_contrast, 'user': user, 'image': null}) %}
|
||||
{% set config = event.payload %}
|
||||
|
||||
<div class="box box-widget widget-user box-user-profile">
|
||||
<div class="widget-user-header" style="{% if config.image is not null %}background: url('{{ config.image }}') center center;{% else %}background-color: {{ config.color }}{% endif %}">
|
||||
<h3 class="widget-user-username" style="color: {{ config.font }}">
|
||||
{{ widgets.username(user) }}
|
||||
</h3>
|
||||
<h5 class="widget-user-desc" style="color: {{ config.font }}">{{ user.title }}</h5>
|
||||
</div>
|
||||
<div class="widget-user-image">
|
||||
{{ widgets.user_avatar(user) }}
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
{% if stats is not null %}
|
||||
{% set seeOwnRate = is_granted('view_rate_own_timesheet') %}
|
||||
{% set columnLength = seeOwnRate ? 3 : 6 %}
|
||||
<div class="row">
|
||||
<div class="col-sm-{{ columnLength }} border-right">
|
||||
<div class="description-block">
|
||||
<h5 class="description-header">{{ stats.durationThisMonth|duration }}</h5>
|
||||
<span class="description-text">{{ 'stats.durationMonth'|trans }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% if seeOwnRate %}
|
||||
<div class="col-sm-{{ columnLength }} border-right">
|
||||
<div class="description-block">
|
||||
<h5 class="description-header">{{ stats.amountThisMonth|money }}</h5>
|
||||
<span class="description-text">{{ 'stats.amountMonth'|trans }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-sm-{{ columnLength }} border-right">
|
||||
<div class="description-block">
|
||||
<h5 class="description-header">{{ stats.durationTotal|duration }}</h5>
|
||||
<span class="description-text">{{ 'stats.durationTotal'|trans }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% if seeOwnRate %}
|
||||
<div class="col-sm-{{ columnLength }} border-right">
|
||||
<div class="description-block">
|
||||
<h5 class="description-header">{{ stats.amountTotal|money }}</h5>
|
||||
<span class="description-text">{{ 'stats.amountTotal'|trans }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends 'user/layout.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% block main %}
|
||||
{% block profile_content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
||||
@@ -74,7 +74,7 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
|
||||
protected function assertHasProfileBox(HttpKernelBrowser $client, string $username)
|
||||
{
|
||||
$profileBox = $client->getCrawler()->filter('div.box-body.box-profile');
|
||||
$profileBox = $client->getCrawler()->filter('div.box-user-profile');
|
||||
$this->assertEquals(1, $profileBox->count());
|
||||
$profileAvatar = $profileBox->filter('img.img-circle');
|
||||
$this->assertEquals(1, $profileAvatar->count());
|
||||
@@ -87,8 +87,7 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
{
|
||||
$content = $client->getResponse()->getContent();
|
||||
|
||||
$this->assertStringContainsString('<h3 class="box-title">About me</h3>', $content);
|
||||
$this->assertStringContainsString('<td class="text-nowrap pull-right">' . $username . '</td>', $content);
|
||||
$this->assertStringContainsString('About me', $content);
|
||||
}
|
||||
|
||||
public function getTabTestData()
|
||||
|
||||
@@ -153,7 +153,7 @@ class AppExtensionTest extends TestCase
|
||||
],
|
||||
'kimai.theme' => [
|
||||
'active_warning' => 3,
|
||||
'box_color' => 'green',
|
||||
'box_color' => 'blue',
|
||||
'select_type' => 'selectpicker',
|
||||
'show_about' => true,
|
||||
'chart' => [
|
||||
|
||||
@@ -340,7 +340,7 @@ class ConfigurationTest extends TestCase
|
||||
],
|
||||
'theme' => [
|
||||
'active_warning' => 3,
|
||||
'box_color' => 'green',
|
||||
'box_color' => 'blue',
|
||||
'select_type' => 'selectpicker',
|
||||
'auto_reload_datatable' => false,
|
||||
'show_about' => true,
|
||||
|
||||
44
tests/Event/PageActionsEventTest.php
Normal file
44
tests/Event/PageActionsEventTest.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\PageActionsEvent;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\PageActionsEvent
|
||||
*/
|
||||
class PageActionsEventTest extends TestCase
|
||||
{
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
|
||||
$sut = new PageActionsEvent($user, [], 'foo');
|
||||
$this->assertSame($user, $sut->getUser());
|
||||
$this->assertEquals([], $sut->getActions());
|
||||
$this->assertEquals(['actions' => []], $sut->getPayload());
|
||||
|
||||
$sut = new PageActionsEvent($user, ['hello' => 'world'], 'foo');
|
||||
$this->assertSame($user, $sut->getUser());
|
||||
$this->assertEquals([], $sut->getActions());
|
||||
$this->assertEquals(['hello' => 'world', 'actions' => []], $sut->getPayload());
|
||||
}
|
||||
|
||||
public function testSetActions()
|
||||
{
|
||||
$sut = new PageActionsEvent(new User(), ['hello' => 'world'], 'foo');
|
||||
$sut->setActions(['foo' => ['url' => 'bar']]);
|
||||
$this->assertEquals(['foo' => ['url' => 'bar']], $sut->getActions());
|
||||
$this->assertEquals(['hello' => 'world', 'actions' => ['foo' => ['url' => 'bar']]], $sut->getPayload());
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,14 @@ use PHPUnit\Framework\TestCase;
|
||||
*/
|
||||
class ThemeEventTest extends TestCase
|
||||
{
|
||||
public function testEmpty()
|
||||
{
|
||||
$sut = new ThemeEvent();
|
||||
$this->assertNull($sut->getUser());
|
||||
$this->assertNull($sut->getPayload());
|
||||
$this->assertEquals('', $sut->getContent());
|
||||
}
|
||||
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$user = new User();
|
||||
@@ -25,21 +33,7 @@ class ThemeEventTest extends TestCase
|
||||
|
||||
$sut = new ThemeEvent($user);
|
||||
|
||||
$this->assertNull($sut->getPayload());
|
||||
$this->assertEquals('', $sut->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group legacy
|
||||
*/
|
||||
public function testDeprecation()
|
||||
{
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
|
||||
$sut = new ThemeEvent($user);
|
||||
|
||||
$this->assertEquals($user, $sut->getUser());
|
||||
$this->assertSame($user, $sut->getUser());
|
||||
}
|
||||
|
||||
public function testGetterAndSetter()
|
||||
|
||||
@@ -37,7 +37,7 @@ class RuntimeExtensionsTest extends TestCase
|
||||
|
||||
public function testGetFunctions()
|
||||
{
|
||||
$expected = ['trigger', 'javascript_translations', 'timesheet_exporter', 'active_timesheets', 'encore_entry_css_source', 'render_widget'];
|
||||
$expected = ['trigger', 'actions', 'javascript_translations', 'timesheet_exporter', 'active_timesheets', 'encore_entry_css_source', 'render_widget'];
|
||||
$i = 0;
|
||||
|
||||
$sut = new RuntimeExtensions();
|
||||
|
||||
Reference in New Issue
Block a user